WebAssembly / Rust / AudioWorklet でシンセ作ってみる

この記事はLivesense AdvendCalendar 21 日目の記事です。

表題の通り、AudioWorklet で動作するような WebAssembly によるシンセを Rust で作ってみようというやつです。
出来たものはこちら。
https://reprimande.github.io/wasm-audioworklet-synth/

UI は g200kg 先生 の [webaudio-controls](https://github.com/g200kg/webaudio-controls) を使わせていただきましたがメチャメチャ便利です。

## Rust で WebAssembly を作る

現在の Rust ではコンパイル時のターゲットに `wasm32-unknown-unknown` を指定すると Emscripten なしで WebAssembly にコンパイル出来る機能が単体で備わってるので Rust で頑張っていきます。

さらに最近では `wasm-bindgen` / `js-sys` / `web-sys` / `wasm-pack` といった JavaScript との連携周りをより簡潔に行えるようにするライブラリも増えてきているようで Rust で WebAssembly を作るのはより簡単になってきています。

ただ、今回は WebAssembly の開発自体が初めてなので出来るだけ言語 / ブラウザ標準の機能での動作や WebAssembly を扱う時に言語に依存しない部分の仕組みも分かりたい、といった理由でこれらを使わないで単体での機能 + `wasm-gc` でのバイナリの容量の削減のみで進めます。
 
そうすると、今回の Rust で WebAssembly を作る手順としては以下のようにな感じです。各々詳細に解説しませんが良い記事やドキュメントなど他にあるので気になる人は調べて下さい ><

`Cargo.toml`

```toml
...
[lib]
crate-type = ["cdylib"]
...
```

`src/lib.rs`

```rust
...
#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut f32 {
...
}
...
```

ビルド

```sh
$ cargo build --target wasm32-unknown-unknown --release
$ wasm-gc target/wasm32-unknown-unknown/release/wasm_audioworklet_synth.wasm -o wasm/wasm_audioworklet_synth.wasm
```

## WebAssembly を AudioWorklet で使う

折角なので deprecated な `ScriptProcessorNode` ではなく AudioWorklet を使ってやっていきます。
AudioWorklet はオーディオ処理をそれ専用のスレッドで動作させるための機能です。WebWorker / ServiceWorker のオーディオ版と思うとイメージしやすいかと思います。

AudioWorklet は `process` という関数をオーバーライドすることにより、決まったサンプル分の信号処理をそこに記述出来、それが止めどなく実行されるというものなのですが、今回はそこで実行される全ての音声信号処理 WebAssembly / Rust 側で行うこととします。

WebAssembly を AudioWorklet で使う場合どうすれば良いのかと考えましたが、[Enter Audio Worklet の MessagePort の項](https://developers.google.com/web/updates/2017/12/audio-worklet#bi-directional_communication_with_messageport) で WASM モジュールを転送するのにも使えるよねって書いてあったのでそれでやってみます。

`app.js`

```javascript
const ctx = new AudioContext()
ctx.audioWorklet.addModule('js/processor.js').then(() => {
  const n = new AudioWorkletNode(ctx, 'my-processor')
  n.connect(ctx.destination)
  fetch('wasm/wasm_audioworklet_synth.wasm')
    .then(r => r.arrayBuffer())
    .then(r => n.port.postMessage({ type: 'loadWasm', data: r }))
}
```

`processor.js`

```javascript
class MyProcessor extends AudioWorkletProcessor {
  constructor() {
    super()
    this.port.onmessage = e => {
      if (e.data.type === 'loadWasm') {
        WebAssembly.instantiate(e.data.data).then(w => {
          this._wasm = w.instance
          this._size = 128
          this._outPtr = this._wasm.exports.alloc(this._size)
          this._outBuf = new Float32Array(
            this._wasm.exports.memory.buffer,
            this._outPtr,
            this._size
          )
      }
    }
  }

  process(inputs, outputs, parameters) {
    if (!this._wasm) {
      return true
    }

    let output = outputs[0]
    for (let channel = 0; channel < output.length; ++channel) {
      let outputChannel = output[channel]
      this._wasm.exports.process(this._outPtr, this._size)
      outputChannel.set(this._outBuf)
    }

    return true
  }
}

registerProcessor('my-processor', MyProcessor)
```

実際のコードからは簡略化してますが、以下のような手順で WebAssembly を AudioWorklet の Processor 内で使うようにしています。

1. AudioWorkletProcessor モジュールを追加
2. 追加された Processor ノードを生成
3. WASM バイナリを `fetch`
4. WASM バイナリのバイト列を `postMessage` で Processor ノードに送信
5. Processor ノードで WebAssembly のインスタンス生成

WebAssembly のインスタンス生成時に `alloc` したりポインタ設定したりしているのは、現状 JavaScript と WebAssembly 間で配列のやりとりは関数経由では直接行えず、共有のメモリ内に領域を確保し、そのポインタを受け渡すという実装になっています。
今回は関数とのやりとりに毎回決まったサイズの配列を受け渡しのためだけ (受けとった配列はすぐに別領域にコピーしている) なのでインスタンス生成時にその領域を確保して使い回します。


## Rust によるシンセの実装

あまり複雑なことはしていなく、シンプルに 1 VCO (ノコギリ波のみ) / 1 VCF (ローパスフィルタ) / 1 VCA / 1 ENV なシンセサイザーです。

シンセサイザーは時間的な状態を持つことになるので、グローバルな領域に変数を保持する必要があり、今回はそれを `lazy_static` を使ってやっています。

```rust
lazy_static! {
    static ref SYNTH: Mutex<Synth> = Mutex::new(Synth::new());
}

#[no_mangle]
pub extern "C" fn process(out_ptr: *mut f32, size: usize) {
    let mut synth = SYNTH.lock().unwrap();
    synth.process(out_ptr, size);
}
```

また、JavaScript 側と受け渡すバッファの領域を作成しそのポインタを返す関数 `alloc` と、関数が呼ばれた時に配列にアクセスするところは以下です。
この辺りは色々参考にしていてもみな似たような実装行っているようでした。`std::mem::forget` はなるほどと思いました。

```rust
#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut f32 {
    let mut buf = Vec::<f32>::with_capacity(size);
    let ptr = buf.as_mut_ptr();
    std::mem::forget(buf);
    ptr as *mut f32
}
...
    pub fn process(&mut self, out_ptr: *mut f32, size: usize) {
        let out_buf: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(out_ptr, size) };
        ...
    }
```


他はシンセを実装すると大体こんな感じになるよねといった普通かつ対した実装にはなっていないので詳細は省略…
気になる方はこのあたりを見てみて下さい。現状妙なハードコードとかあってだいぶ雑でございますm(_ _)m

- [src/lib.rs](https://github.com/reprimande/wasm-audioworklet-synth/blob/master/src/lib.rs)
- [src/synth.rs](https://github.com/reprimande/wasm-audioworklet-synth/blob/master/src/synth.rs) 


## まとめ

- Rust でシンセは作れる
- ちょっとめんどくさいけど難しくはない
- `wasm-bindgen` 使えるなら使った方が開発は楽しいと思う
- Rust 力の足りなさ
- 誰か [TB-303](https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%B3%E3%83%89%E3%83%BBTB-303) ください