如果您还没有关注本系列,我一直在Rust和WebAssembly中构建合成器。使用WebAssembly的优点是,我可以利用Rust的所有功能,同时避免尝试使用操作系统的复杂库来播放音频的陷阱。
我的最后解决方案不是理想的
In the last post,我到达了一个工作,但却不是将WebAssembly导入AudioWorkletProcessor
的理想解决方案。即使我最初无法弄清楚如何使用wasm-pack
将WebAssembly模块导入worklet,但在没有wasm-pack
的情况下,这样做的过程使我能够识别使用wasm-pack
的潜在解决方案。
下一步是试图实施它。
一个更好的主意
一旦我理解将WebAssembly模块导入工作点的过程,使用wasm-pack
就非常简单。有两个一般步骤:
- 使用
wasm-pack
web
目标编译WebAssembly模块。web
target创建的JavaScript代码无捆绑器或节点。 - 将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
类的构造函数。我使用AudioWorkletProcessor
的MessagePort
为音频工作点一个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
文件中使用。下一步是开始构建可用合成器所需的功能。
如果您想了解有关此项目的更多信息,请随时关注我或阅读本系列中的其他帖子。
系列简介:
此原型的源代码:
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
尽管我尚未对此方法进行彻底测试,但我认为您不会运行。