だいぶ前からTypeScriptでファミコンエミュレータを書いていてブラウザではそこそこ動くんだけど、ブラウザだけじゃなくコマンドラインからスタンドアロンとしても動かせるといいなあと思っていた。 しかしnode.jsからSDL(2)を使用する方法がわからなかったので長いこと放置していた。 でもようやく動かす方法がわかって、またオーディオも鳴らせるようになったのでメモっておく。
リポジトリはこちら
node-sdl2のインストール
npm sdl2
で検索して最初に出てくるnode-sdl2がインストールで躓くので、ここで相当長いこと放置していた。
重い腰を上げて調べてわかったのは、
node-sdl2
で使われているnode-ffi
がNodeJS9では動かず、8以下にする必要がある(node-ffi does not build with NodeJS 9.x on Linux · Issue #451 · node-ffi/node-ffi)- また
node-ffi
をビルドするためのnode-gyp
がPython3だとエラーが出てしまうので、Python2系を使う必要がある(Support for Python 3 · Issue #193 · nodejs/node-gyp)
どちらもポピュラーなモジュールだと思うんだが未だにそんな状態とかありえるのか…。 ともかく要件を合わせればインストールはできた。
オーディオ機能の追加
node-sdl2
はSDL2の全機能をNodeJSから使えるようになっているわけではなくてオーディオのAPIは触れるようになっていなかった。
なのでフォークして追加することにした
(プルリクあまり期待できそうになかったので…)。
オーディオを鳴らすには一定間隔で呼び出されるコールバックに渡ってくるバッファに値を書き込んでやることで音が鳴るようになっている。 それをNodeJS上で実現するのはどうするのかというので苦労した。
コールバックの引数であるポインタはBufferとして受け渡しされる。
ただlength
が0
として渡ってくるのでそのまま書き込んでも無視されてしまい内容が更新されず、雑音が鳴ってしまっていた。
そこでBuffer#reinterpretを使ってサイズを正しく指定してやって、
Buffer.bufferでArrayBuffer
を取り出し、
TypedArray
を作成して書き込むことでバッファが指すメモリ上に任意の型で書き込むことができるようになる
(該当するソース)。
TypeScriptでグローバルの型宣言
ブラウザではWebAudioを使って音を鳴らしている。 SDL2上ではそういうものはないが、同じ仕組みで鳴らせるとアプリ側のオーディオ関連のソースがそのまま使えて便利。 なので同じインタフェースででっちあげることにする。
TypeScriptでトランスパイルする際にWebAudioで使用されるクラス(AudioContext
など)がNodeJS用にビルドする場合には未定義なので自分で定義してやる必要がある。
ただブラウザ版では予め定義されているから、NodeJS版のビルド時にだけ参照させたい。
なので使用するソース内からのimport
や/// <reference path="..." />
の記述での参照はできない。
そのように予めグローバルな定義をしたい場合にはTypeScriptの設定ファイルtsconfig.jsonにオプションを設定する必要がある:
{ |
定義ファイルはよしなに、使う機能だけ書いてやる(src/@types/audio_context/index.d.ts):
interface AudioContext { |
- 定義ファイル内に
export
を書くとグローバルとして自動的に定義されている状態にはならないので注意(How to define a global variable in a typescript module, which could be used in other modules directly without import? · Issue #18237 · Microsoft/TypeScript)
ノードベースの波形生成
WebAudioはノードベースになっていて、発振器で生成した波形をゲインノードで増幅させて音量を調整し、ディレイノードで位相をずらす、などということがノードのつなぎ方次第で自由に組み合わせることができるようになっている。
自前で実装する場合に、最初はバッファを受け渡して書き込んでもらおうかと考えたが、分岐したり合成させたりできる必要があってその場合に効率のいい方法が思いつかなかったので、 毎サンプル各ノードに対して関数を呼び出してやることにした。
サンプリングレートが48kHzだったら毎秒48k回×延べノード数呼び出すのでどうかと思うけど、一応動いているのでよしとしよう。