HaskellのFFIでVSTを作る [Haskell Advent Calendar 2012 14日目]

久等了!

記事を投稿しようとしたのだが、膝にunsafeInterleaveIOを受けてしまってな…

今回は、HaskellによってVSTを作る方法を紹介したいと思います。

VSTとは

VST(Virtual Studio Technology)は、Steinbergによって定められた、ホストアプリケーション(DAW)とソフトウェア音源/エフェクタで信号のやりとりをするプラグインの規格の一つです。プラグインの多くはVSTであり、またほとんどのDAWがサポートしています。

つらいC++くるしく使おう

下準備として、SteinbergのサイトからVST SDK(2.4)をダウンロードします(会員登録が必須)。

そして、おもむろにpublic.sdk/samples/vst2.x/vstxsynthを適当な場所にコピーし、インクルードパスにSDKを追加します。

そして、vstxsynthproc.cppは一旦削除し、それに相当する部分をこれからHaskellで書きます。
音声を鳴らすだけなら、processReplacingを実装するだけでできます。

C++からHaskellを呼ぶ

C++からHaskellを呼ぶには二つの選択肢があります。

  • Haskellのプログラムと、依存している中間ファイル(.o)をすべてリンクする

http://www.haskell.org/haskellwiki/FFI_complete_examples#When_main_is_in_C.2B.2Bで紹介されている方法。ただし、今のGHCでは中間ファイルを列挙する方法がなさそうに見える(情報求む)ので無理なようだ…

  • DLLを作る。

簡単で確実な方法。今回はこれを使うことになった。

C++からprocessReplacingのコールバックを設定する関数を呼び、さらにその関数をHaskellで作ってしまおう、という戦略です。

関数をエクスポートしたい場合はforeign export、Haskellの関数へのポインタを作りたい場合はforeign import ccall "wrapper"を使います。

vstxsynthproc.cpp

#include <stdlib.h>
#include "vstxsynth.h"
#include "HsFFI.h"
#include "VSTMain_stub.h"

void (*vstProcessCallback)(float* outL, float* outR, VstInt32 sampleFrames);

void VstXSynth::setSampleRate (float sampleRate)
{
	AudioEffectX::setSampleRate (sampleRate);
}

void VstXSynth::setBlockSize (VstInt32 blockSize)
{
	AudioEffectX::setBlockSize (blockSize);
	// you may need to have to do something here...
}

void VstXSynth::initProcess ()
{
	int argc = 0;
	char **argv;
	argv = (char **)malloc(sizeof(char*));
	argv[0] = "VSTTest";
	hs_init(&argc, &argv);
	hsVSTTest(&vstProcessCallback);
}

//-----------------------------------------------------------------------------------------
void VstXSynth::processReplacing (float** inputs, float** outputs, VstInt32 sampleFrames)
{
	(*vstProcessCallback)(outputs[0], outputs[1], sampleFrames);
}

VSTTest.hs

{-# LANGUAGE ForeignFunctionInterface #-}
module VSTTest where
import Foreign.Storable
import Foreign.Ptr
import Foreign.C.Types
import Data.IORef
import Control.Monad

type ProcessReplacing = Ptr CFloat -> Ptr CFloat -> Int -> IO ()

foreign import ccall "wrapper"
    mkProcessReplacing :: ProcessReplacing -> IO (FunPtr ProcessReplacing)

processReplacing :: IORef CFloat -> IORef CFloat -> ProcessReplacing
processReplacing phaseL phaseR bufL bufR frames = forM_ [0..frames-1] $ \i -> do
    pL <- readIORef phaseL
    pR <- readIORef phaseR
    pokeElemOff bufL i (sin pL)
    pokeElemOff bufR i (sin pR)
    writeIORef phaseL (pL + 2*pi*(1/44100)*440.0)
    writeIORef phaseR (pR + 2*pi*(1/44100)*622.3)
    modifyIORef phaseL $ \p -> if p > 2 * pi then p - 2 * pi else p
    modifyIORef phaseR $ \p -> if p > 2 * pi then p - 2 * pi else p

hsVSTTest :: Ptr (FunPtr ProcessReplacing) -> IO ()
hsVSTTest p = do
    phaseL <- newIORef 0.0
    phaseR <- newIORef 0.0
    fp <- mkProcessReplacing $ processReplacing phaseL phaseR
    poke p fp

foreign export ccall hsVSTTest :: Ptr (FunPtr ProcessReplacing) -> IO ()

$ ghc -O -shared -o VSTTest.dll VSTTest.hs
とすると、VSTTest.dll、VSTTest.dll.aVSTTest_stub.hが生成されるので、すかさずプロジェクトの構成プロパティ→リンカ→入力→追加の依存ファイルにVSTTest.dll.aを追加します。モジュール定義ファイルをvstsdk2.4\public.sdk\samples\vst2.x\win\vstplug.defに設定するのも忘れずに。

そして、リンクするとvstxsynth.dllが生成されるので、VSTTest.dllと同じディレクトリに置きます。適当なホストアプリケーションから開いて、左右から正弦波が出てくれば成功です。

(実はArrowによって信号処理するライブラリを既に開発しているのだが、ものすごい勢いでメモリを消費する謎の挙動を示すので保留となった。せっかくそのための曲も作ったのに…)

まとめ

Haskellは(頑張れば)なんでもできる。