Edited at

Unity VR でシンセサイザー(vst)を触ろう!

More than 1 year has passed since last update.

はじめまして katoutです。

また、こういう技術記事を書くのは初めてなので駄文なのはご了承ください。

この記事では表題の通り、Unityという巷で流行ってると聞くゲームエンジン上で作曲ソフト上で動作するシンセサイザーやエフェクターの標準規格であるvstを動作させた備忘録として紹介します。

IMAGE ALT TEXT HERE

https://www.youtube.com/watch?v=0ICJQXpXaSU

また、今回実現出来なかった問題点を先に挙げさせて頂きます。


  • winのみでmacでの動作

  • build版の動作

  • vstの別途立ち上がるウインドウGUIの非表示

  • vst2.4のみの動作でvst3系は非対応


vstについて

プログラマには馴染みが薄いかと思いますが要は

input : midi信号 音声波形データ

output : かっこいい波形データや技巧なmidi信号

と言うもので作曲者は以下のようなイカしたGUIを操作して我々の耳を幸せにしてくれるカッコいい音を作り出してくれてます。


プログラムのお話

vstはSteinberg's さんによる業界標準規格となっておりdllフォーマットでc++で呼び出せGUIはwin32のウインドウシステムで動作するようです(GUIシステムはvstでクロスプラットフォーム向けに抽象化されており詳細は未検証)。そのため今回行うアプローチとしては以下のようになります。

無題.png


Native Plugin

vstも結局dllなのでUnityから直接呼ぶアプローチも考えましたが、win32の細かなウインドウ操作や今後考えられる大量の波形操作の処理を考え自前dllを噛ませます。NativePluginの作成は他の方の素晴らしい情報が沢山あるので詳細は割愛させて頂きます。


  1. Visual Stadioでdll作成(vstは大抵32bitなのでNativePluginも32bitで作りUnityも32bit版を使う)

  2. 同じソリューション内にテスト用のプロジェクト作って上記のdll読み込んでテストして効率化

  3. Unityに配置してC#から呼ぶ。visual studioのプロセスアタッチでNativePluginのデバッグ


参考資料


意外に一番ためになった。DLLの作り方はもちろんだが、Unityの特性上UnityEditorを立ち上げている間DLLの差し替えを行うことができずtry&error効率が悪いためdll用のテストプロジェクトを作ることで効率が上がりました。



unityとのやりとり、配列の取扱など参考になりました。



Unityで読み込んだNative Pluginのデバッグが出来るのは助かります...一回Unity側で実行しなければdllは読み込まれないようなので即時実行系の処理(vsthostの初期化など)は一旦C#側でブレークポイントを貼って止めてから上記の記事のように実行中のプロセスにアタッチをすると上手く行きました。(2つのVisual Studioを駆使して開発ってなんか出来るプログラマっぽくてカッコイイですよね!(こなみかん



vst制御

vstの初期化, 命令, 破棄は上記のNative Plugin同様数は少ないにしても資料があるのでこちらも割愛させていただいきます。


  • VstHepler.h 今回用意したDLLのインターフェイス

    // vstHost

VSTHELPER_API VstHost* CreateVstHost( int samplingRate, int blockSize );
VSTHELPER_API void DeleteVstHost( VstHost* host );

// vstPlugin
VSTHELPER_API VstPlugin* LoadVstPlugin( VstHost* host, const char* path );
VSTHELPER_API bool DeleteVstPlugin( VstHost* host, VstPlugin* plugin );
VSTHELPER_API void OnAudioFilterRead( VstPlugin* plugin, float* data, size_t device_channel, size_t sample );
VSTHELPER_API void AddNoteOn( VstPlugin* plugin, size_t note_number );
VSTHELPER_API void AddNoteOff( VstPlugin* plugin, size_t note_number );

// vstEditor
VSTHELPER_API VstEditor* GetEditor( VstPlugin* plugin );
VSTHELPER_API void GetTexture( VstEditor* plugin, unsigned char* arr, int w, int h, int ch );
VSTHELPER_API int GetWidth( VstEditor* plugin );
VSTHELPER_API int GetHeight( VstEditor* plugin );
VSTHELPER_API void SendMouseDown( VstEditor* plugin, int x, int y );
VSTHELPER_API void SendMouseUp( VstEditor* plugin, int x, int y );
VSTHELPER_API void SendMouseMove( VstEditor* plugin, int x, int y );
VSTHELPER_API void SendDoubleClick( VstEditor* plugin, int x, int y );


初期化, midi信号の送信, 波形データの作成, GUIの制御, 破棄と一通り網羅してるのでほぼコピペで参考にさせて頂きました...資料が少ないなかここまでまとめられてるのは助かります...



上記ではwin32のラッパーライブラリを使っていたのでwin32のwindow管理部分だけ参考にしました。



VST Win32とUnityの接続

UnityからVstを制御するためには以下のことが必要となってきました。


  • midi情報の送信

  • 波形データの受取, 再生

  • GUIの表示

  • GUI操作


midi情報の送信

本稿から逸れますがVR空間での操作を行いたかったのでMIDIキーボードからの入力ではなくUnity内にキーボードインターフェイスを作り送るようにいたしました。

GIF2.gif

Oculus Utilities for Unity 5

Oculus Avatar SDK

をセットアップし、「LocalAvatar」「OVRCameraRig」を配置

また、指での当たり判定が取れなかったので以下のコードで指情報取って

if(controller == OVRInput.Controller.RTouch) {

m_fingerRoot = GameObject.Find("hands:b_r_index1").transfomr;
m_fingerTop = GameObject.Find("hands:b_r_index_ignore").transfomr;
} else if( controller == OVRInput.Controller.LTouch ) {
m_fingerRoot = GameObject.Find("hands:b_l_index1").transfomr;
m_fingerTop = GameObject.Find("hands:b_l_index_ignore").transfomr;
}

BoxCollisionを変形させ無理やり指のあたり判定つくりOnTriggerEnterとOnTriggerExitでDLLのインターフェイス呼ぶだけです()

transform.position = ( ( m_fingerRoot.position + m_fingerTop.position ) / 2 );

Vector3 scale = transform.localScale;
scale.x = ( m_fingerRoot.position - m_fingerTop.position ).sqrMagnitude / 0.1f;
transform.localScale = scale;
transform.rotation = Quaternion.Lerp(m_fingerRoot.rotation, m_fingerTop.rotation, 0.5f);

ホントはバネ感のあるリアルキーボードのような物を作りたかったのですがUnity初心者のせいで触り心地のよいspring jointがつくれずIsTriggerで代用しました...リアル感はないですが操作は楽です!


波形データの受取, 再生

UnityではGameObjectにAudioSouceコンポーネントを付けOnAudioFilterReadメソッドを記述するとサウンド再生時にコールバックしてくれるのでそこでvstから音を生成させます。割愛してますがsamplingRateとBlockSizeには注意してください

void OnAudioFilterRead(float[] data, int channels) {

IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(float)) * data.Length);
Marshal.Copy(data, 0, ptr, data.Length);
OnAudioFilterRead(m_vstPlugin, ptr, (uint)channels, (uint)(data.Length / channels));
Marshal.Copy(ptr, data, 0, data.Length);
Marshal.FreeHGlobal(ptr);
}

キャプチャ.PNG


GUIの表示

想像ですが、通常のdawではVSTのGUI用windowを「WS_CHILD」で表示しているかと考えますが、今回はUnityという3D空間なのでWindowをそのまま表示することができません。そのため、windowをキャプチャーしTexutreに一旦変換しUnity上の3D空間に表示させます。

VSTの初期化時にNativePlugin側にてTextureを生成し

void VstEditor::createBitmap( HWND hwnd ) {

RECT DesktopParams;
GetWindowRect( hwnd, &DesktopParams );
DWORD Width = DesktopParams.right - DesktopParams.left;
DWORD Height = DesktopParams.bottom - DesktopParams.top;

bmpInfo.bmiHeader.biSize = sizeof( BITMAPINFOHEADER );
bmpInfo.bmiHeader.biWidth = Width;
bmpInfo.bmiHeader.biHeight = Height;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 24;
bmpInfo.bmiHeader.biCompression = BI_RGB;

HDC DevC = GetDC( hwnd );
CaptureBitmap = CreateDIBSection( DevC, &bmpInfo, DIB_RGB_COLORS, (void**) &lpPixel, NULL, 0 );
CaptureDC = CreateCompatibleDC( DevC );
SelectObject( CaptureDC, CaptureBitmap );
ReleaseDC( hwnd, DevC );
}

VSTのidleにてBitBltを用いてTextureを更新させます。

RECT DesktopParams;

GetWindowRect( hwnd, &DesktopParams );
DWORD Width = DesktopParams.right - DesktopParams.left;
DWORD Height = DesktopParams.bottom - DesktopParams.top;

HDC DevC = GetDC( hwnd );
BitBlt( CaptureDC, 0, 0, Width, Height, DevC, 0, 0, SRCCOPY | CAPTUREBLT );
ReleaseDC( hwnd, DevC );
isCaptured = true;

最後にUnity側のVstEditor.csのUpdateタイミングでUnity側のTextureを更新させます

void Update() {

GetTexture(m_vstEditor, pixels_ptr_, texture_.width, texture_.height, 3);
texture_.SetPixels32(pixels_);
texture_.Apply();
}

NativePlugin側では以下のようにしてTextureをコピーさせてます

void VstEditor::GetTexture( unsigned char* arr, int w, int h, int ch ) {

if( isCaptured ) {
LPBYTE lpSrc;
BITMAP bm;

GetObject( CaptureBitmap, sizeof( BITMAP ), &bm );

for( int y = 0; y < bm.bmHeight; y++ ) {
lpSrc = GetBits( bm, 0, y );
for( int x = 0; x < bm.bmWidth; x++ ) {
arr[(y * w + (w - x))*4 + 0] = lpSrc[2];
arr[(y * w + (w - x))*4 + 1] = lpSrc[1];
arr[(y * w + (w - x))*4 + 2] = lpSrc[0];
arr[(y * w + (w - x))*4 + 3] = 255;

lpSrc += 3;
}
}
}
}

最近凹みさんによる「Desktop Duplication API 」を用いたキャプチャーが流行ってる?らしいですがどうも力量不足らしく「CreateSwapChainForHwnd 」を用いて「HWND」から「IDXGIOutput1」まで持っていくことができず「BitBlt」という古き良き?APIを叩きキャプチャーしてます。お陰でウインドウはUnityとは別途表示され、キャプチャのせいかマウスカーソルは点滅するという弊害が出ますが一応目的は達成できてるので良しとしてます。

キャプチャ.PNG


GUI操作

せっかく空間にGUIが出たのですから操作できないと意味がありません。しかし、これはwin32のwindowではなくUnity上のテクスチャのため操作命令も独自で呼ぶ必要があります。更に残念ながらvstにはkeyイベントを受け渡すインターフェイスはあってもマウス操作はなくOS側で仮想的にイベントを送る必要があります。

win32apiには「SendMessage」「PostMessage」という仕組みがありそれを用いてwindowにマウスイベントを流すことでタッチを擬似的に再現可能となりました。

エディタ上でのタッチ点を求める

void OnTriggerEnter(Collider other) {

// 接触点を求める
Vector3 hitPos = other.ClosestPointOnBounds(this.transform.position);
VstEditor editor = other.gameObject.GetComponent<VstEditor>();
if ( editor ) {
Vector2 hitPos -= editor.transform.position; // 原点へ移動
Quaternion inv = Quaternion.Inverse(editor.transform.rotation); // 回転角の逆を求める
hitPos = inv * hitPos;  // エディタの回転を打ち消す

hitPos *= VstEditor.EditorScale; // エディタのUnity上スケールに
hitPos.x += editor.Width / 2; // 真ん中が中心なので移動させる
hitPos.y = editor.Height / 2 - hitPos.y; // 真ん中が中心なので移動させる
editor.sendMouseDown((int)hitPos.x, (int)hitPos.y);
}
}

ボタンなどは階層的なウインドウ構造になっている可能性があるので再帰的にマウスイベントを投げる必要があります

void VstEditor::postMessageChild(HWND hParentWindow, UINT Msg, WPARAM wParam, LPARAM lParam ) {

HWND hWnd = 0;
while( (hWnd = FindWindowEx( hParentWindow, hWnd, NULL, NULL )) != 0 ) {
PostMessage( hWnd, Msg, wParam, lParam );
postMessageChild( hWnd, Msg, wParam, lParam );
}
}

void VstEditor::sendMouseDown( int x, int y ) {
postMessageChild( m_vstHwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM( x, y ) );
}

GIF.gif

ちなみにマウスイベントは


マウスダウン → WM_LBUTTONDOWN

マウス移動 → WM_MOUSEMOVE

マウスアップ → WM_LBUTTONUP

ダブルクリック → WM_LBUTTONDBLCLK


で再現することが出来ました。


おわり

以上でunity上での市販シンセサイザー動作方法となります。

今回はシンセサイザーでの利用でしたが他のdllを用いた拡張でも使える手法だと思うのでご参考になれば幸いです。

はじめにも書きましたがGUIを表示するとUnityのWindow以外に開いたシンセサイザーのウインドウが表示されてしまうのが現在の課題です。解決方法がございましたらコメントやtwitterで教えていただけると嬉しいです。