我如何使用WASM-pack为AudioWorkletProcessor构建WebAssembly模块
#javascript #rust #webassembly

如果您还没有关注本系列,我一直在RustWebAssembly中构建合成器。使用WebAssembly的优点是,我可以利用Rust的所有功能,同时避免尝试使用操作系统的复杂库来播放音频的陷阱。

我的最后解决方案不是理想的

In the last post,我到达了一个工作,但却不是将WebAssembly导入AudioWorkletProcessor的理想解决方案。即使我最初无法弄清楚如何使用wasm-pack将WebAssembly模块导入worklet,但在没有wasm-pack的情况下,这样做的过程使我能够识别使用wasm-pack的潜在解决方案。

下一步是试图实施它。

一个更好的主意

一旦我理解将WebAssembly模块导入工作点的过程,使用wasm-pack就非常简单。有两个一般步骤:

  1. 使用wasm-pack web目标编译WebAssembly模块。 web target创建的JavaScript代码无捆绑器或节点。
  2. 将javascript复制到AudioWorkletProcessor文件中,并将其调整到AudioWorkletGlobalScope的上下文中。

解释

由于web目标是在不需要捆绑器的情况下工作的,因此可以将最终的JVASCRIPT代码复制到AudioWorkletProcessor文件中,并且只需要较小的更改即可。此方法目前是最好的选择,因为AudioWorkletGlobalScope缺少导入WebAssembly模块所需的功能。

实施解决方案

第一步是用web
构建生锈代码

wasm-pack build --target web

编译了代码后,我打开了名为“ _bg.js”的JavaScript文件。该文件包含wasm-pack生成的WebAssembly绑定。我复制了此文件中的所有内容,然后粘贴到我的AudioWorkletProcessor类中。这是将JavaScript绑定到AudioWorkletGlobalScope的唯一方法。

接下来,我将.wasm文件复制到浏览器可以使用fetch()加载的位置。在我的Worklet文件的顶部,我有以下代码,我知道需要调整这些代码。

// processor.js

let wasm;

const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });

cachedTextDecoder.decode();

let cachedUint8Memory0 = null;

function getUint8Memory0() {
    if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
    }
    return cachedUint8Memory0;
}

// wasm-pack generated code ...

export { initSync }
export default init;

class WebSynthProcessor extends AudioWorkletProcessor {
    // ...
}

我用以下内容替换了此代码:

// processor.js

let wasm;

let cachedTextDecoder;

// decode call will be made later

let cachedUint8Memory0 = null;

function getUint8Memory0() {
    if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
    }
    return cachedUint8Memory0;
}

// wasm-pack generated JavaScript

// Notice, I removed the exports.

class WebSynthProcessor extends AudioWorkletProcessor {
    // ...
}

TextDecoder类的实例需要在AudioWorkletGlobalScope中可用,但是音频工作范围范围不允许使用TextDecoder类的构造函数。我使用AudioWorkletProcessorMessagePort为音频工作点一个TextDecoder实例。

设置WebAssembly模块

为了将WebAssembly模块传递到AudioWorkletProcessor,我知道我需要在主线程中调用WebAssembly.compile()并将其传递给我的音频处理器。为此,我需要像这样呼叫fetch()WebAssembly.compileStreaming()

// index.js

// We will need the worklet node later.
let node = new AudioWorkletNode(context, 'web-synth-proto');

WebAssembly.compileStreaming(fetch('/path/to/library.wasm')).then(module => {
    // Pass the module to the AudioWorkletProcessor
});

我最初是在async函数中进行的,因此我可以使用await直接获取返回值。在这里,我正在使用带有承诺的回调,因为这种模式更常见。
一旦我使用了WebAssembly模块实例,我使用这样的MessagePort实例将其传递给AudioWorkletProcessor

// index.js

WebAssembly.compileStreaming(fetch('/path/to/library.wasm')).then(module => {
    // Now that the module has been created, it can be passed to the processor.
    node.port.postMessage({type: 'init-wasm', wasmData: module});
});

在AudioWorkletProcessor中使用模块

要使用WebAssembly模块,需要从音频工作点线内实例化。此过程的第一步是为MessagePort实例设置消息侦听器。我在AudioWorkletProcessor的构造函数中做到了。

// processor.js

class WebSynthProcessor extends AudioWorkletProcessor {
    constructor(options) {
        super(options);


        this.port.onmessage = event => this.onmessage(event.data);

    }

    onmessage(data) {
        // Check to make sure the message we receive is the correct type.
        if (data.type === 'init-wasm') {                                 
            // Finish instantiating the WebAssembly module
        }
    }
}

现在有一个onmessage侦听器,MessagePort,工作点可以聆听主线程发送的消息。

// processor.js

class WebSynthProcessor extends AudioWorkletProcessor {
    constructor(options) {
        super(options);

        this.port.onmessage = event => this.onmessage(event.data);

    }

    onmessage(data) {
        // Check to make sure the message we receive is the correct type.
        if (data.type === 'init-wasm') {                                 
            cachedTextDecoder = data.decoder;

            // Load returns a promise
            load(data.wasmData, getImports()).then(mod => {
                // Once the WebAssembly module has been instantiated, it needs to be finalized
                // so that it can be accessed later.
                finalizeInit(mod.instance, mod.module);
            });
        }
    }
}

一旦工作点从主线程接收WebAssembly模块,就需要调用从WASM-PACK输出复制的load()函数,以便可以正确实例化WebAssembly模块。最后,音频处理器线程需要调用finalizeInit()函数,以确保它可以继续访问WebAssembly代码。

现在所有设置都完成了,音频线程可以通过调用JavaScript包装器函数来使用SineOsc struct。
这是我设置振荡器的方式:

// processor.js

class WebSynthProcessor extends AudioWorkletProcessor {
    constructor(options) {
        super(options);

        this.port.onmessage = event => this.onmessage(event.data);

    }

    onmessage(data) {
        // Check to make sure the message we receive is the correct type.
        if (data.type === 'init-wasm') {                                 
            cachedTextDecoder = data.decoder;

            // Load returns a promise
            load(data.wasmData, getImports()).then(mod => {
                // Once the WebAssembly module has been instantiated, it needs to be finalized
                // so that it can be accessed later.
                finalizeInit(mod.instance, mod.module);

                // Create the oscillator instance.
                this.osc = SineOsc.new(sampleRate);
            });
        }
    }
}

现在,音频线程创建了一个振荡器,振荡器可以被调用以进行实际的声音生成。

// processor.js

class WebSynthProcessor extends AudioWorkletProcessor {

    // ...

    process(inputs, outputs, parameters) {
        if (typeof this.osc !== 'undefined' && this.osc !== null) {
            // Get the first output channel.
            let output = outputs[0];

            output.forEach(channel => {
                // populate the channel's buffer with samples from the WebAssembly code.
                for (let i = 0; i < channel.length; i++) {
                    // Remember the first argument for the sample method is the pitch we want to 
                    // synthesize and the second argument is the volume.
                    let sample = this.osc.sample(440, 0.5);

                    channel[i] = sample;
                }
            });
        } else {
            console.log('wasm not instantiated yet');
        }
        return true;
    }
}

接下来是什么?

在这一点上,我演示了如何调整wasm-pack的输出,以便可以在AudioWorkletProcessor文件中使用。下一步是开始构建可用合成器所需的功能。

如果您想了解有关此项目的更多信息,请随时关注我或阅读本系列中的其他帖子。

系列简介:

此原型的源代码:

GitHub logo speratus / web-synth-proto2

Web合成器原型2

Websynth Prototype

This project is a proof of concept for using Rust to generate samples for a Web Audio API backed synthesizer.

You can read an introductiont to the project on dev.to.

依赖项

要构建此项目,您将需要以下内容:

建筑

要构建该项目的生锈组件,只需运行

 wasm-pack build-target web 

有关如何将其集成到网页的详细信息,请参见this post

运行

可以使用Visual Studio代码的“ Live Server”扩展程序运行此项目。只需安装它,然后按照说明进行直播 运行服务器后,导航到“ www”目录以查看网页。

如果您没有Visual Studio代码,也应该能够在任何现代Web浏览器中的www目录中打开index.html 尽管我尚未对此方法进行彻底测试,但我认为您不会运行。