木瓜丸です。
只今音楽のサービスをNext.jsで作っていて、「音声処理にwasm使ったら速そうだし使いたいなー」と思って試してみたら案外大変だったので、備忘録をまとめたいと思います。
AudioWorkletってどんな?
AudioWorkletはダイナミックにAudioWorklet用のスクリプトをAudioContextに読み込ませて使います。
const audioContext = new AudioContext();
audioContext.audioWorklet.addModule('path/to/audioworklet').then(() => {
const node = new AudioWorklet(audioContext, 'my-processor');
})
読み込ませるスクリプトの中ではAudioWorkletProcessorを実装し、これをregisterして使えるようにしています。
class MyProcessor extends AudioWorkletProcessor {
process(inputs, outputs, params) {
//なんかむずかしいしょり
return true;
}
}
registerProcessor('my-processor', MyProcessor)
ハマったポイント
ここで、AudioWorkletProcessor内でwasmを呼び出したいのですが、これがうまく行かないです。
class MyProcessor extends AudioWorkletProcessor {
constructor(opts){
super(opts);
import('path/to/wasm').then(() => {
//初期化処理
})
}
process(inputs, outputs, params) {
//なんかむずかしいしょり
return true;
}
}
registerProcessor('my-processor', MyProcessor)
Webpackを使ってこれとwasmをバンドルしてやろうと思っていたのですが、document is not defined
というエラーが帰ってきてしまいました。
ブラウザでJavaScriptを読み込む場合はグローバルオブジェクトはwindowですが、読み込まれる側のスクリプトの場合はAudioWorkletGlobalScopeになるため、window.fetchやwindow.documentと言ったオブジェクトにアクセスできません。 ので、wasmを非同期に読み込むために.wasmファイルをfetchすると言ったことがAudioWorkletProcessor内部では出来ないみたいです。
Webpackしてあげるとこのダイナミックインポートがwasmのfetchに置き換えられたりするので色々止まってしまうっぽいです。
試したこと
MessagePort + Next.js
AudioWorklet+wasmみたいなキーワードで調べると結構出てきますが、MessagePortという機能を使ってAudioWorklet外部からwasmを渡してあげます。 なんかスマートじゃねえなと思いながら見ていたのですが、大人しくこれにしたいましょう(TT)
ただ、私の場合はNext.jsを使っているので、一旦Next.jsでwasmをimportし、同期的に関数を読み込んで渡してあげようと思いました。 Next.jsで読み込んであげた関数そのものをMessagePortから投げてあげます。
import { processor } from 'path/to/processor.wasm';
export default function MyComponent() {
const audioContext = new AudioContext();
audioContext.audioWorklet.addModule('/path/to/worklet').then(() => {
const node = new AudioWorkletNode('my-processor');
node.postMessage({
type: 'loadWasm',
processor
});
});
return (
<>
hogehoge
</>
)
}
Processor側ではMessagePortから受け取った関数を使うようにセットしてあげます。
class MyProcessor extends AudioWorkletProcessor {
constructor(opt) {
super(opt);
this.port.onmessage = e => {
if(e.data.type == 'loadWasm') {
this.processor = e.data.processor;
}
}
}
process(input, output, params) {
if(this.processor){
const input_ = input[0];
for(let channel = 0; channel < input_.length; channel ++){
const processed = this.processor(input_[channel]);
for(let i = 0; i < output.length; i++){
output[i].set(processed)
}
}
}
return true;
}
}
これで一見落着!...と思ったのですが、MessagePortからは関数オブジェクトを渡すことは出来ないらしく、うまく行きませんでした。
じゃあfetchしてバイナリを食わせる
というわけで、MessagePortからwasmのバイナリを投げてみることにしました。
import { processor } from 'path/to/processor.wasm';
export default function MyComponent() {
const audioContext = new AudioContext();
audioContext.audioWorklet.addModule('/path/to/worklet').then(() => {
const node = new AudioWorkletNode('my-processor');
const bin = await fetch('/path/to/processor.wasm').then(res => res.arrayBuffer());
node.postMessage({
type: 'loadWasm',
processor: bin
});
});
return (
<>
hogehoge
</>
)
}
class MyProcessor extends AudioWorkletProcessor {
constructor(opt) {
super(opt);
this.port.onmessage = e => {
if(e.data.type == 'loadWasm'){
WebAssembly.instantiate(e.data.processor, {}).then(w => {
this.processor = w.instance
})
}
}
}
process(input, output, params) {
if(this.processor){
const input_ = input[0];
for(let channel = 0; channel < input_.length; channel ++){
const processed = this.processor(input_[channel]);
for(let i = 0; i < output.length; i++){
output[i].set(processed)
}
}
}
return true;
}
}
今度は読み込みはうまく行きました!しかし、AudioWorkletNodeをインスタンス化するとこんなエラーが出てきました。
TypeError: WebAssembly.instantiate(): Import #0 module="wbg" error: module is not an object or function
コードを確認した所、wasm-bindgenを使っていたせいで生成されたjsを見に行ってたらしく、そこで止まっていました。wasm-bindgenには頼れないらしい。
解決方法
というわけで、wasm-bindgenに頼らずに実装を行い、線形メモリをゴニョゴニョしながらRustで書いたものをjsに読ませたら行けました。
なんか、wasmもまだまだ大変だけど、何はともあれwasmへの夢を捨てきれないので食いついていこうかなと思っています。がんばろ...