Freeモナドでゲームを作ろう!第1回: Gameモナドの基本
連載目次
free-gameを使う
さて、みなさん、free-gameはインストールしましたか?まだの
2013/5/6: free-game 0.9.3をリリースし、内容も改訂しました。必ずcabal update && cabal install free-gameしてください。型方も、もうインストールした方も、本日free-gameを更新したのでcabal update && cabal install free-game
しましょう。
free-gameは、Gameモナドという独自のモナドによってGUIの抽象化を実現しています。GameはFreeモナドによって生成されたモナドであり、元は単なるFunctorです(これが何を意味するかは、連載の中で明らかにしていきます)。
今回は、実際にプログラムを組みながら、どのようにしてfree-gameを扱うかを学んでいきましょう。
Hello, world!
まずはHello, world!ですね。最初に、https://github.com/fumieval/free-game/blob/master/examples/VL-PGothic-Regular.ttf からフォントをダウンロードしましょう。
helloworld.hs:
{-# LANGUAGE ImplicitParams, OverloadedStrings #-} import Graphics.UI.FreeGame main = do font <- loadFont "VL-PGothic-Regular.ttf" let ?font = font runGame def mainLoop mainLoop :: (?font :: Font) => Game a mainLoop = do translate (V2 40 240) $ colored black $ text ?font 30 "Hello, Free World!" tick mainLoop
$ ghc helloworld.hs
$ ./helloworld (Windowsの場合は単にhelloworld.exe)
このように表示されれば成功です。FreeType Errorが表示された場合、カレントディレクトリにダウンロードしたフォントがあるかどうかもう一度確認してください。
このプログラムの肝は、Gameモナドを実行するrunGame :: GUIParam -> Game a -> IO (Maybe a)
と、Picture型の値を画面に表示する関数、画面を更新するアクション、drawPicture :: Picture -> Game ()
です。tick :: Game ()
です。
Picture型の定義を見てみましょう。
*** この型は削除されました data Picture -- ビットマップ(後述)をPictureとして使う = BitmapPicture Bitmap -- 複数のPictureをまとめて一つのPictureにする。 | Pictures [Picture] -- IOモナドに入ったPicture(内部実装のためのもの)。 | IOPicture (IO Picture) -- Pictureを回転する。 | Rotate Float Picture -- Pictureを拡大・縮小する。 | Scale Vec2 Picture -- Pictureを移動する。 | Translate Vec2 Picture -- Pictureに色を付ける。 | Colored Color Picture
実はこれはglossにインスパイアされたものです。わかりやすいでしょう?*1
free-game 0.9では、Picture2DおよびFigure2Dという型クラスが定義されています。Gameモナドはこれらのインスタンスであるため、以下のような操作が利用できます。
class Picture2D p where -- ビットマップから生成する fromBitmap :: Bitmap -> p () -- 指定した角度だけ回転させる rotate :: Float -> p a -> p a -- 拡大・縮小する scale :: V2 Float -> p a -> p a -- 平行移動する translate :: V2 Float -> p a -> p a -- 色を付ける colored :: Color -> p a -> p a class Picture2D p => Figure2D p where -- 線 line :: [V2 Float] -> p () -- 多角形 polygon :: [V2 Float] -> p () -- 多角形(枠のみ) polygonOutline :: [V2 Float] -> p () -- 円 circle :: Float -> p () -- 円(枠のみ) circleOutline :: Float -> p () -- 太さを設定 thickness :: Float -> p a -> p a
V2
は、vectというライブラリで定義されている、二次元のベクトルを表すデータ型です。が、具体的な使い方はまた次回。
ビットマップはloadBitmapFromFile :: FilePath -> IO Bitmap
で読み込むことができます。http://www.haskell.org/wikiupload/4/4a/HaskellLogoStyPreview-1.pngを表示してみましょう(カレントディレクトリに保存してください)。当然ながら、loadBitmapFromFileはGameモナドの中では実行できないので注意してください。Gameモナドの中でIOアクションを使いたいときは、embedIO :: IO a -> Game a
を適用しましょう。
{-# LANGUAGE ImplicitParams #-} import Graphics.UI.FreeGame import System.Random main = do font <- loadFont "VL-PGothic-Regular.ttf" bmp <- loadBitmapFromFile "HaskellLogoStyPreview-1.png" let ?font = font let ?bmp = bmp runGame def mainLoop mainLoop :: (?bmp :: Bitmap, ?font :: Font) => Game a mainLoop = do translate (V2 40 240) $ colored black $ text ?font 30 "Hello, Free World!" translate (V2 240 480) $ rotate 45 -- 反時計回りに45°回転 $ colored red -- 赤色 $ text ?font 70 "真っ赤な誓いいいいいいいいいい" r <- randomness (-40, 40) translate (V2 (-40) (360 + r)) -- ランダムに振動させる $ scale (V2 0.7 1) -- x方向に0.7倍 $ colored blue -- 青色 $ text ?font 100 (replicate 20 'ド') translate (V2 300 300) $ colored magenta $ circle 40 -- 円を表示 translate (V2 240 80) $ fromBitmap ?bmp -- 読み込んだビットマップを(240,80)に表示 tick mainLoop
入力を行う
これだけでは、ゲームを表現するのに不十分です――「入力」ができないからです。free-gameでは、入力を行うためにKeyboardとMouseという二つのインターフェイスを提供しています。Gameモナドはこれらのインスタンスであるため、直に以下のアクションを実行できます。
-- | The class of types that can handle inputs of the keyboard. class Keyboard t where keyChar :: Char -> t Bool keySpecial :: SpecialKey -> t Bool -- | The class of types that can handle inputs of the mouse. class Mouse t where mousePosition :: t (V2 Float) mouseWheel :: t Int mouseButtonL :: t Bool mouseButtonM :: t Bool mouseButtonR :: t Bool
引数に指定するキーについては、http://hackage.haskell.org/packages/archive/free-game/0.9.1/doc/html/Graphics-UI-FreeGame-Base.html#t:SpecialKeyを参照してください。
例: Zキーを押している間だけカウントアップを行う。Escキーを押すとプログラムが終了する
{-# LANGUAGE ImplicitParams #-} import Graphics.UI.FreeGame import System.Random import Control.Monad main = do font <- loadFont "VL-PGothic-Regular.ttf" let ?font = font runGame def (mainLoop 0) mainLoop n = do translate (V2 40 120) $ colored green $ text ?font 20 $ show n translate (V2 40 240) $ colored black $ text ?font 20 "Hello, Free World!" key <- keySpecial KeyEsc -- Escキーの状態を取得 when key quit -- Trueならば終了 key <- keyChar 'Z' -- Zキーの状態を取得 tick mainLoop $ if key then succ n else n -- Trueのときだけ1増やす
mousePosition, mouseWheel, mouseButtonL, mouseButtonR, mouseButtonMを使うことで、位置、ホイール、ボタンの状態が取得できます。
例: カウンタがマウスカーソルの右上に表示され、左クリックするたびに値が1ずつ増える
{-# LANGUAGE ImplicitParams #-} import Graphics.UI.FreeGame import System.Random import Control.Monad main = do font <- loadFont "VL-PGothic-Regular.ttf" let ?font = font runGame def $ mainLoop (0, False) mainLoop (n, btn) = do pos <- mousePosition b <- mouseButtonL translate pos $ colored green $ text ?font 20 $ show n translate (V2 40 240) $ colored black $ text ?font 20 "Hello, Free World!" tick mainLoop (if not btn && b then n + 1 else n, b)
drawPicture、askInput、getMouseStateの使い方が分かりましたね。実はfree-gameの基本はこれで全部なのです!
ゲームを開発する準備は整いました。次回は、これらを踏まえて実際にゲームを作ってみましょう!
まとめ
- 基本はdrawPicture、getButtonState、getMouseStateの3つだけ
- embedIOがあるからIOだって使えるぞ
- runSimpleですべてが動きだす
Tips
今回の例では、GHC拡張のImplicitParamsを使っています。?のついた変数は、一旦束縛すればスコープ内で呼び出した関数からどこでも参照できるようになるので、フォントや画像データなどの不変なものを扱うのに適しています。
*1:生意気にもnice data typeを標榜するだけある
Freeモナドでゲームを作ろう!第0回: 概要
連載目次
- 第0回: 概要 (この記事)
- 第1回: Gameモナドの基本
先ほど、free-gameというライブラリをアップロードしました。free-gameは、世界で一番柔軟かつ簡単にゲームなどのGUIを作れるライブラリを目指しています。
Freeモナドとは
Freeモナドは、Functorを与えると自動的にモナドを生成してくれる構造で、これを使うことで、アクションをデータの一つとして柔軟に扱えるようになります。去年の10月ごろからにわかに日本のHaskeller間で流行り始めた、今一番熱いモナドです[要出典]。Freeモナドそのものに関してはこれらの記事を読みましょう。
- Haskell for all: Why free monads matter
- そろそろFreeモナドに関して一言いっとくか
- 型クラスとモナドと Free モナド - あどけない話
- Freeモナドって何なのさっ!? - capriccioso String Creating(Object something){ return My.Expression(something); }
free-gameの軌跡
私はしばらく前から、Haskellでゲームを作るためのライブラリとしてglossを推していたのだが…
DXLibに さよならバイバイ オレはGlossと 旅に出る
— 高階 文(ふみ)さん (@fumieval) 8月 22, 2012
@myuon_myon Glossです。世界で一番柔軟かつ簡単にグラフィックや入出力ができるライブラリだと思います hackage.haskell.org/package/gloss-…
— 高階 文(ふみ) (@fumieval) Septembre 15, 2012
glossの仕様に結構不満を抱いていた。
Glossの入力がものすごいクソ。Shiftキー単体の状態を取得できないとか…
— 高階 文(ふみ)さん (@fumieval) 8月 23, 2012
Glossの、状態の更新と出力の生成を完全に分離してしまう仕様は実はあんまり好きではない
— 高階 文(ふみ)さん (@fumieval) 9月 25, 2012
そんなある日、私はFreeモナドと出会う。
"cabal install free" 哲学的なコマンドだな…
— 高階 文(ふみ)さん (@fumieval) 10月 10, 2012
FunctorをFreeモナドにシュウウゥゥゥゥーーッ!超!エキサイティン!
— 高階 文(ふみ)さん (@fumieval) 10月 11, 2012
そして私は、その記述力をゲーム開発に生かそうとライブラリの開発に取り掛かった。
Freeモナドでゲームを記述するライブラリを作っている。打倒Gloss
— 高階 文(ふみ)さん (@fumieval) 10月 13, 2012
そして…ついにプロトタイプが完成した。
Freeモナドで作りました twitter.com/fumieval/statu…
— 高階 文(ふみ) (@fumieval) Novembre 1, 2012
紆余曲折を経てOpenGLに対応し、今のfree-gameができたのである。
Haskellでゲームを作るならfree-game!と胸を張って言えるようなものにしていきたい
— 高階 文(ふみ)さん (@fumieval) 11月 6, 2012
今なら胸を張って言える。「Haskellでゲームを作るならfree-game!」と。
インストール
cabal update
cabal install free-game
これだけ。
補足
- Windows環境においてLexical Errorが発生した場合、set LANG=Cとしてからリトライするとうまくいく。
- Ubuntuに入れる際に必要になったもの: libgl-mesa-dev, libglu1-mesa-dev, zlib1g-dev, libxrandr-dev
次回から、free-gameでどのようにしてゲームを作るのかをコードを交えて解説していきたいと思います。
究極のモナド「Idealモナド」を垣間見る(続/その0)
前回の記事究極のモナド「Idealモナド」を垣間見るではFreeモナドを構成して興奮して終わってしまったが、今回はイデアルモナドの仕組みについてもう少し考えてみる。
data Ideal f a = Pure a | Ideal (f a)
Idealモナドは、純粋な値Pure aか、fというなにかに包まれているIdeal (f a)のどちらかを取る、という仕組みになっている。returnはPureに、何らかの作用を持つアクションはIdealに行く─純粋かそうでないかを分離することが、イデアルモナドの本質を表しているのかもしれない。
例示は理解の試金石*1という言葉を信じて、実際にいくつかモナドを構成してみよう。
Ideal型とIdealizeクラスについて、以下のような定義がなされているものとする。
import Control.Monad import Control.Applicative data Ideal f a = Pure a | Ideal (f a) class Idealize f where (>>~) :: f a -> (a -> Ideal f b) -> f b instance Idealize f => Monad (Ideal f) where return = Pure Pure a >>= k = k a Ideal fa >>= k = Ideal (fa >>~ k)
Identity
data Empty a instance Idealize Empty where (>>~) = undefined type Identity = Ideal Empty runIdentity (Pure a) = a
Maybe, Either
instance Idealize (Const a) where Const a >>~ _ = Const a type Maybe = Ideal (Const ()) nothing = Ideal (Const ()) maybe _ f (Pure a) = f a maybe v _ (Ideal (Const ())) = v type Either a b = Ideal (Const b) a left = Ideal . Const right = Pure either f g (Pure b) = g b either f g (Ideal (Const a)) = f a
Reader
type Reader r = Ideal ((->) r) instance Idealize ((->) r) where f >>~ k = \r -> runReader (k $ f r) r ask = Ideal id runReader (Pure a) _ = a runReader (Ideal f) r = f r
二つの計算に同じ環境を与えることがReaderの本質なので、そういった意味ではわかりやすいかもしれない。
Writer
instance Monoid w => Idealize ((,) w) where (w, a) >>~ k = let (b, w') = runWriter (k a) in (mappend w w', b) type Writer w = Ideal ((,) w) tell w = Ideal (w, ()) runWriter (Pure a) = (a, mempty) runWriter (Ideal (w, a)) = (a, w)
State
newtype StateBase s a = StateBase (s -> (a, s)) instance Idealize (StateBase s) where StateBase f >>~ k = StateBase $ \s -> let (b, s') = f s in runState (k b) s' type State s = Ideal (StateBase s) runState (Pure a) s = (a, s) runState (Ideal (StateBase f)) s = f s get = Ideal $ StateBase (\s -> (s, s)) put s = Ideal $ StateBase (\_ -> ((), s)) modify f = Ideal $ StateBase (\s -> ((), f s))
なるほど、「(>>~)で純粋な場合の挙動とそうでない場合の挙動を記述すればMonadを作ってくれる*2」ということか。普通にMonadを作ったときと複雑さが変わらないような気もする…(FunctorとかApplicativeを自分で宣言しなくてよいのは楽だが)。
次回は、Idealの汎用性についてもう少し考えてみる。
究極のモナド「Idealモナド」を垣間見る
新年おめでとうございます。
突然だが、中身への関数適用(fmap)、シングルトンの生成(return)、ネストの結合(join)ができるコンテナを一般化するとモナドになる。
昨年話題になったのでご存知の方も多いと思うが、モナドをシングルトンの生成とネストの結合に関して一般化する、Freeモナドという構造がある。
さらにFreeモナドを一般化すると…Idealモナドになるのだ。
発端
自由モナドの一般化のイデアルモナドというものがあるらしいのでふみさんには是非これにも取り組んでほしい bit.ly/Wn5Arc
— Hiromi Ishii (@mr_konn) January 3, 2013
そして、私の長い旅が始まる…
定義
An ideal monad on C is a monad (T, η, μ) together with an endofunctor T' on C and a natural transformation μ' : T' T → T such that T = Id + T', η = inl, μ = [id, inr ◦ μ']
Tは対象のイデアルモナド、ηはreturn、μはjoin、IdはIdentity、+は直和型、inlは左のコンストラクタ、inrは右のコンストラクタに、[f, g]はeither f g的な関数に対応する。で、T'とμ'が新たに定義されるようだ。とりあえず愚直に組んでみる。
import Control.Monad data Ideal f a = Pure a | Ideal (f a) -- T = Id + T' class Functor f => Mu' f where mu' :: f (Ideal f a) -> f a -- μ' : T' T → T instance Mu' f => Monad (Ideal f) where return = Pure -- η = inl Pure a >>= k = k a -- id Ideal fa >>= k = Ideal $ mu' $ fmap k fa -- inr ◦ μ'
What is the correct definition of ideal monads?によれば、
mu' . fmap return = id :: Mu' f => f a -> f a
mu' . mu' = mu' . fmap join :: Mu' f => f (Ideal f (Ideal f a)) -> f a
の二つの式を満たすときイデアルモナドになる。コンパイルトオッタァァァァァwwwwwwww
実例
これだけでは動かしようがないので、Freeモナドを構成してみよう。
newtype Liberty f a = Liberty (f (Free f a)) type Free f = Ideal (Liberty f) instance Functor f => Functor (Liberty f) where fmap f (Liberty fia) = Liberty (fmap (liftM f) fia) instance Functor f => Mu' (Liberty f) where mu' (Liberty fii) = Liberty $ fmap join fii free :: Functor f => f (Free f a) -> Free f a free f = Ideal (Liberty f)
Libertyは与えられたFunctorで包まれたFree、FreeはLibertyに対するIdealと定義する。
Freeモナド(=Ideal (Liberty f))に対するliftMとjoinを、それぞれLibertyの中身に使うことでfmapとmu'を実装している。
よし早速テスト…って、よく考えてみたらFreeモナドも単体では動かしようがないじゃないか!
実例の実例
仕方ないので、そろそろFreeモナドに関して一言いっとくかから例を引っ張り出してきた。
data CharIO a = GetCh (Char -> a) | PutCh Char a instance Functor CharIO where fmap f (GetCh g) = GetCh (f . g) fmap f (PutCh c x) = PutCh c (f x) getCh :: Free CharIO Char getCh = free $ GetCh $ \ch -> Pure ch putCh :: Char -> Free CharIO () putCh ch = free $ PutCh ch (Pure ()) runStdIO :: Free CharIO a -> IO a runStdIO (Pure a) = return a runStdIO (Ideal (Liberty (GetCh f))) = getChar >>= \ch -> runStdIO (f ch) runStdIO (Ideal (Liberty (PutCh ch cont))) = putChar ch >> runStdIO cont main = runStdIO $ do mapM_ putCh "Hello, Haskeller! Please input a character: " ch <- getCh mapM_ putCh "The ordinal of the character is: " mapM_ putCh (show (ord ch)) mapM_ putCh ".\nThank you!\n"
早速実行してみる。
Prelude> main
Hello, Haskeller! P(メモリをすごい勢いで消費しつつだんだん遅くなる)
理由は簡単だった。(>>=)はfmapとmu'を呼び出し、そのfmapとmu'はliftM、join経由で(>>=)を呼び出していたのだ!これでは組み合わせおねえさんめいて計算量が爆発四散してしまう!
2013/3/5追記: IdealをFunctorのインスタンスにし、Liberty (fmap (liftM f) fia)
の代わりにLiberty (fmap (fmap f) fia)
とすれば問題なく実行できる。
融合と本質
一つにしてしまえばこの問題は起こらないはずなので、fmapとmu'の機能を併せ持つ(>>~)を使うようにした。
class Idealize f where (>>~) :: f a -> (a -> Ideal f b) -> f b instance Idealize f => Monad (Ideal f) where return = Pure Pure a >>= k = k a Ideal fa >>= k = Ideal $ fa >>~ k newtype Liberty f a = Liberty (f (Free f a)) type Free f = Ideal (Liberty f) instance Functor f => Idealize (Liberty f) where Liberty fm >>~ k = Liberty (fmap (>>=k) fm) free :: Functor f => f (Free f a) -> Free f a free f = Ideal (Liberty f)
おっと!?
おわかりいただけただろうか…Idealizeのインスタンス宣言をもう一度よく見て欲しい。
Liberty fm >>~ k = Liberty (fmap (>>=k) fm)
普通のFreeモナドの定義を思い出してみよう。
data Free f a = Pure a | Free (f (Free f a)) instance Functor f => Monad (Free f) where return = Pure Pure a >>= k = k a Free fm >>= k = Free (fmap (>>=k) fm)
そう、まさに(>>~)の定義は、Freeモナドをモナドたらしめる最も重要な式、Free fm >>= k = Free (fmap (>>=k) fm)
と同じなのである!
無事に定義ができたので、早速動かしてみよう。
Prelude> main
Hello, Haskeller! Please input a character: m
The ordinal of the character is: 109.
Thank you!
当たり前だが、動いた!感動した!これが…モナドの力なのか…
まとめ
Freeのさらなる一般化、Ideal。抽象的すぎて実用性があるのかどうかわからないが、大変興味深い構造である。今のところ、Hackage上にIdealモナドを扱うパッケージはない(以前はあったが廃止されたようだ)ので、あとでアップロードするかもしれない。
この記事で使用したソースコードはhttps://gist.github.com/4445447にある。
Freeモナド実用の旅(5): MonadPlus for Free
Control.MonadPlus.Freeはいなくなりました
えにっきをみてください
freeの新バージョンにControl.MonadPlus.Freeなるものがあったのでさっそく使ってみた。MonadPlus版Freeのコンストラクタは、Pure、Freeに加えて新たにPlusが加わっており、任意のFunctorからMonadPlusなモナドを生成することができる。
以下は、Freeモナドによって錬成されたパーサコンビネータの例。
import Control.Monad import Control.MonadPlus.Free import Control.Applicative import Data.Char data ParseStep a = ParseStep (Maybe Char -> Maybe a) instance Functor ParseStep where fmap f (ParseStep g) = ParseStep (fmap f . g) type Parser = Free ParseStep runParser :: Parser a -> String -> Maybe a runParser (Pure a) "" = Just a runParser (Pure _) _ = Nothing runParser (Free (ParseStep f)) str = case str of [] -> f Nothing >>= flip runParser [] (x:xs) -> f (Just x) >>= flip runParser xs runParser (Plus xs) str = msum $ map (flip runParser str) xs -- ここがポイント! anyChar :: Parser Char anyChar = liftF (ParseStep id) satisfy :: (Char -> Bool) -> Parser Char satisfy f = do ch <- anyChar if f ch then pure ch else empty char :: Char -> Parser Char char ch = satisfy (==ch) string :: String -> Parser String string str = mapM char str digit :: Parser Int digit = digitToInt <$> satisfy isDigit natural :: Parser Int natural = foldl ((+) . (*10)) 0 <$> some digit parens :: Parser a -> Parser a parens p = char '(' *> p <* char ')' chainl :: Parser a -> Parser (a -> a -> a) -> Parser a chainl p op = p >>= rest where rest x = (op <*> pure x <*> p >>= rest) <|> return x spaces :: Parser a -> Parser a spaces p = many (char ' ') *> p <* many (char ' ') fact :: Parser Int fact = parens expr <|> natural term :: Parser Int term = chainl fact $ spaces $ char '*' *> return (*) <|> char '/' *> return div expr :: Parser Int expr = chainl term $ spaces $ char '+' *> return (+) <|> char '-' *> return (-) main = do let ev = runParser expr print $ ev "42" -- Just 42 print $ ev "1 + 1" -- Just 2 print $ ev "1 - (2 - 3)" -- Just 2 print $ ev "(1 - 2) - 3" -- Just (-4) print $ ev "1 - 2 - 3" -- Just (-4) print $ ev "2 * (3 + 5)" -- Just 16 print $ ev "(5 * 6 - 10) / 4" -- Just 5 print $ ev "1 + " -- Nothing
やっぱりFreeモナドはすごい。
Toxic Biohazardを買った
何だと思う?…これね、減算式FMシンセ。
ダークで重厚な音が出せるシンセを探していたところ、Image-LineのToxic Biohazardに出会った。名前も見た目も、一見するとシンセサイザーではないように思えるが、これはれっきとした生物兵器<トキシックシンセサイザー>なのだ。
まず目につくのは6つのオシレータパネル。32種類の基本波形+ユーザ定義の波形1つから選べ、それぞれエンベロープとピッチの設定ができる。蛍光表示管めいた中央のパネルでオシレータの変調を行う。
フィルターはローパス、ハイパス、バンドパスの三つから選び、エンベロープ、カットオフ、レゾナンスなどを設定するという非常にシンプルなもの。フィルタの左右にはゲートを兼ねたLFOがあり、片方はカットオフ、もう片方はピッチを揺らすのに使える。
左上でユニゾンとポルタメント、右上で全体のエンベロープとオーバードライブを設定できる。ただの鋸波をユニゾンするだけでもそこそこ使えそうな音が出る。
バイオハザードマークが描かれた二つのパネルでエフェクタのパラメータを設定する。ディレイ、コーラス、リバーブ、フランジャー、フェーザの他に、サンプリングレートとビット深度を下げるLo-Fiがあるのが印象的。
全体的には「超強力になったSynth1」という印象。シンセ初心者でも扱いやすいと思う。
プリセットの音色はSFやホラー映画で使われていそうな雰囲気を醸し出している…が、さまざまなジャンルで使えそうだ。
ちなみに、上の画像の設定だとこんな音が出る。
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を呼ぶ
- 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.a、VSTTest_stub.hが生成されるので、すかさずプロジェクトの構成プロパティ→リンカ→入力→追加の依存ファイルにVSTTest.dll.aを追加します。モジュール定義ファイルをvstsdk2.4\public.sdk\samples\vst2.x\win\vstplug.defに設定するのも忘れずに。
そして、リンクするとvstxsynth.dllが生成されるので、VSTTest.dllと同じディレクトリに置きます。適当なホストアプリケーションから開いて、左右から正弦波が出てくれば成功です。
(実はArrowによって信号処理するライブラリを既に開発しているのだが、ものすごい勢いでメモリを消費する謎の挙動を示すので保留となった。せっかくそのための曲も作ったのに…)
まとめ
Haskellは(頑張れば)なんでもできる。