node.js/SDL2でオーディオ再生

2018-10-10

だいぶ前からTypeScriptでファミコンエミュレータを書いていてブラウザではそこそこ動くんだけど、ブラウザだけじゃなくコマンドラインからスタンドアロンとしても動かせるといいなあと思っていた。 しかしnode.jsからSDL(2)を使用する方法がわからなかったので長いこと放置していた。 でもようやく動かす方法がわかって、またオーディオも鳴らせるようになったのでメモっておく。

リポジトリはこちら

node-sdl2のインストール

npm sdl2で検索して最初に出てくるnode-sdl2がインストールで躓くので、ここで相当長いこと放置していた。 重い腰を上げて調べてわかったのは、

どちらもポピュラーなモジュールだと思うんだが未だにそんな状態とかありえるのか…。 ともかく要件を合わせればインストールはできた。

オーディオ機能の追加

node-sdl2はSDL2の全機能をNodeJSから使えるようになっているわけではなくてオーディオのAPIは触れるようになっていなかった。 なのでフォークして追加することにした (プルリクあまり期待できそうになかったので…)。

オーディオを鳴らすには一定間隔で呼び出されるコールバックに渡ってくるバッファに値を書き込んでやることで音が鳴るようになっている。 それをNodeJS上で実現するのはどうするのかというので苦労した。

コールバックの引数であるポインタはBufferとして受け渡しされる。 ただlength0として渡ってくるのでそのまま書き込んでも無視されてしまい内容が更新されず、雑音が鳴ってしまっていた。

そこでBuffer#reinterpretを使ってサイズを正しく指定してやって、 Buffer.bufferArrayBufferを取り出し、 TypedArrayを作成して書き込むことでバッファが指すメモリ上に任意の型で書き込むことができるようになる (該当するソース)。

TypeScriptでグローバルの型宣言

ブラウザではWebAudioを使って音を鳴らしている。 SDL2上ではそういうものはないが、同じ仕組みで鳴らせるとアプリ側のオーディオ関連のソースがそのまま使えて便利。 なので同じインタフェースででっちあげることにする。

TypeScriptでトランスパイルする際にWebAudioで使用されるクラス(AudioContextなど)がNodeJS用にビルドする場合には未定義なので自分で定義してやる必要がある。 ただブラウザ版では予め定義されているから、NodeJS版のビルド時にだけ参照させたい。 なので使用するソース内からのimport/// <reference path="..." />の記述での参照はできない。 そのように予めグローバルな定義をしたい場合にはTypeScriptの設定ファイルtsconfig.jsonにオプションを設定する必要がある:

{
"compilerOptions": {
...
"typeRoots": [
"./src/@types" // <- 自前の定義ファイルのパス
],
"types": [
"audio_context" // <- 自前の定義ファイル
],
...
}

定義ファイルはよしなに、使う機能だけ書いてやる(src/@types/audio_context/index.d.ts):

interface AudioContext {
readonly currentTime: number
readonly destination: AudioDestinationNode

createGain(): GainNode
createOscillator(): OscillatorNode
createDelay(): DelayNode
}

...

ノードベースの波形生成

WebAudioはノードベースになっていて、発振器で生成した波形をゲインノードで増幅させて音量を調整し、ディレイノードで位相をずらす、などということがノードのつなぎ方次第で自由に組み合わせることができるようになっている。

自前で実装する場合に、最初はバッファを受け渡して書き込んでもらおうかと考えたが、分岐したり合成させたりできる必要があってその場合に効率のいい方法が思いつかなかったので、 毎サンプル各ノードに対して関数を呼び出してやることにした。

サンプリングレートが48kHzだったら毎秒48k回×延べノード数呼び出すのでどうかと思うけど、一応動いているのでよしとしよう。