我项目的下一步是构建原型。此过程的第一步是产生正弦波,并通过扬声器播放。一旦完成了第一步,我就可以在这个基础上开始建立更复杂的功能。
设置
在Web Audio API documentation仔细看了一眼之后,我认为合成器向扬声器输出声音的最佳方法是通过使用AudioWorkletNode
与AudioWorkletProcessor
结合使用。两个节点的这种组合将使我能够在背景线程中生成音频,然后可以通过扬声器播放。
如果我想采用其他方法,则需要扩展AudioNode
并通过使用较少文档的功能来实现音频生成。由于我找不到有关扩展AudioNode
的文档,因此我想避免将其扩展尽可能长时间。
实施振荡器
现在我准备开始编写Rust Code。我几乎所有的文档和教程都可以在线找到使用koude4。
遵循设置教程后,我将以下代码添加到我的lib.rs
文件中。
#[wasm_bindgen]
pub struct SineOsc {
sample_rate: i32,
cycler: i32,
}
#[wasm_bindgen]
impl SineOsc {
pub fn sample(&mut self, pitch: i32, gain: f32) -> f32 {
// This number is the base from which `sine` is calculated
let seed: f32 = ((2.0 * std::f32::consts::PI) / (self.sample_rate / pitch) as f32) * self.cycler as f32;
// Keeps track of the position within the wave
self.cycler += 1;
// Reset position if it gets too large
if self.cycler > (self.sample_rate / pitch) {
self.cycler = 0;
}
// Control the volume by multiplying gain by the sine.
gain * seed.sin()
}
pub fn new(sample_rate: i32) -> Self {
SineOsc { sample_rate: sample_rate, cycler: 0 }
}
}
在这一点上,我有一个基本的振荡器。将音高(以赫兹为单位)传递,并在0
和1
之间增益到sample()
函数,将产生一个可以添加到音频缓冲区中的单个数字。反复调用sample()
会产生数字的顺序,需要创建一个可听见的正弦波。
为了构建此项目,我运行了wasm-pack build
,该项目将Rust Code编译到.wasm
文件并生成可用于调用Rust Code的JavaScript绑定。我将新生成的.wasm
文件集成到我的原型JavaScript代码中。那是我开始遇到问题的时候。
障碍
为了使用AudioWorkletNode
,必须首先设置一个带有一个扩展AudioWorkletProcessor
接口的类的单独文件。 AudioWorkletProcessor
类必须在单独的文件中定义,因为AudioWorkletProcessor
接口仅存在于AudioWorkletGlobalScope
中,该界面是从JavaScript执行的主线程中的独立线程。因为JavaScript不支持多域阅读,因此必须使用Web Workers API实现任何多线程行为 - 它只能在后台运行JavaScript文件,而不是像典型的多线程应用程序那样分支。
首先,我尝试按照教程的说明来导入我新铸造的WebAssembly软件包。但是根据我一直遇到的控制台错误,我很快发现AudioWorkletGlobalContext
不支持导入模块。
接下来,我尝试将JavaScript软件包直接复制并粘贴到AudioWorkletProcessor
文件中,然后从那里加载它。但是AudioWorkletGlobalScope
不支持读取幕后.wasm
文件所需的fetch()
,因此该解决方案也不起作用。
AudioWorkletNode
的MessagePort
实例将导入的JavaScript软件包从主JavaScript文件传递到AudioWorkletProcessor
。这是行不通的,因为使用MessagePort
依赖于传递的对象可复制,而JavaScript函数则无法复制。我尝试了这些解决方案变体的变体,但我所做的任何事情都无法正常工作。
最终,我偶然发现了这个github问题:
Support using wasm-pack in Worklets (particularly AudioWorklet) #689
功能描述
WebAssembly的关键用例之一是音频处理。 WASM-PACK当前不支持AudioWorklet(据称是Web上的自定义音频处理的未来),其当前的--target
选项。
web
和no-modules
目标越来越接近,但是在实例化过程中出现了错误,因为AudioWorklet上下文缺少JS包装器所期望的几个浏览器API。这个问题可能会扩展到其他工人/工作点环境,但是我只尝试使用AudioWorklet。
基本示例
my_processor.worklet.js
class myProcessor 扩展class =“ pl-v”> audioworkletprocessor {
constructor ( ) {
在
import ( “ ../ pkg/pkg/audio” span> ) 。 然后 ( module => {
this 。 _ wasm = 模块
} ) ;
}
在pl-kos“>, 输出 , 参数 ) {
如果 ( ! this 。 _ wasm ) {
返回 true
}
在pl-s1“>输出 [ 0 ]
this 。 _ wasm 。 enforts 。 process ( this 。 _ outptr , this 。 _ size )
for ( 让 Channel = 0 ; channel < 输出 。 长度 ; ++ channel ) {
输出 [ channel ] 。 set ( this 。 _ outbuf )
}
返回 true
}
}
最后,我知道为什么我不知道如何完成这项工作。答案很简单,wasm-pack
不支持工作点编译目标。
我认为,这是WebAssembly工具链生锈的严重缺陷,因为工作点是某些用例中最大程度地从使用WebAssembly中受益的用例。工作点用于在后台执行计算密集的任务,可以通过将其卸载到以近速度运行的WebAssembly代码来进一步优化的任务。
这一发现被证明是使原型运行的主要障碍,但我还没有任何想法。
解决方案
我进行了一些快速的搜索,发现Rust实际上已经具有内置内置的WebAssembly编译目标。不幸的是,由于wasm-pack
提供的功能比仅使用内置编译目标可用的功能要有用,因此基本上没有关于如何在不使用wasm-pack
的情况下使用Rust WebAssembly的教程。
尽管如此,经过一些挖掘,我找到了一个旧的项目,该项目不使用wasm-pack
编译了Rust WebAssembly代码,我将我从该项目中学到的东西调整了我的用例。
这种方法的成本是,我将不再对我的Rust struct
具有JavaScript绑定,因为JavaScript绑定是wasm-pack
提供的功能。因此,我调整了我的采样功能,因此它不再需要struct
,这就是我最终得到的:
#[no_mangle]
pub extern "C" fn samplex(pitch: i32, gain: f32, sample_rate: i32, transport: i32) -> f32 {
// Does the same thing as above, but statelessly
let seed = ((2.0 * std::f32::consts::PI) / (sample_rate / pitch) as f32) * transport as f32;
gain * seed.sin()
}
从这里,我要做的就是跑步
cargo build --target wasm32-unknown-unknown
它将生成一个可以使用的.wasm
文件。
我在阅读有关WebAssembly.compile()
函数的Mozilla Developer Network文章时,学会了如何将WebAssembly模块传递到工作点。有了这些知识,这就是我的两个JavaScript文件最终看起来像:
// index.js
let startBtn = document.getElementById('start-sound');
async function setup() {
// Load and compile our WebAssembly module
const bin = await WebAssembly.compileStreaming(await fetch('/wasm_demo.wasm'));
let context = new AudioContext();
// Add our processor to the Worklet
await context.audioWorklet.addModule('processor.js');
let node = new AudioWorkletNode(context, 'web-synth-proto');
node.port.postMessage({type: 'init-wasm', wasmData: bin});
// Connect to the speakers
node.connect(context.destination);
// Make sure sound can be stopped to prevent it from getting annoying
let stopBtn = document.getElementById('stop-sound');
stopBtn.addEventListener('click', () => {
context.close();
});
}
// User must click to start sound
startBtn.addEventListener('click', () => {
setup();
});
// processor.js
class WebSynthProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.cycler = 0;
this.transport = 0;
// Setup `MessagePort` so we can receive WebAssembly
// module from the main thread
this.port.onmessage = event => this.onmessage(event.data);
console.log(sampleRate);
}
onmessage(data) {
// Receive WebAssembly module from main thread
if (data.type === 'init-wasm') {
// Declare this as an async function so we can use `await` keyword
const instance = async () => {
try {
// We need to instantiate the module to use it
let mod = await WebAssembly.instantiate(data.wasmData, {});
this._wasm = mod;
} catch(e) {
console.log("Caught error in instantiating wasm", e);
}
}
// Call the setup function
instance();
}
}
process(inputs, outputs, parameters) {
if (typeof this._wasm !== 'undefined' && this._wasm !== null) {
let output = outputs[0];
output.forEach(channel => {
for (let i = 0; i < channel.length; i++) {
let pitch = 880;
// Call our WebAssembly function
let sample = this._wasm.exports.samplex(pitch, 0.3, sampleRate, this.transport);
// Add Sample to audio buffer
channel[i] = sample;
this.transport += 1;
let resetPoint = Math.ceil(sampleRate / pitch);
if (this.transport > this.resetPoint) {
this.transport = 0;
}
}
});
} else {
console.log('wasm not instantiated yet');
}
// Must return `true` to continue processing audio
return true;
}
}
// register our processor with the `AudioWorkletGlobalContext`
registerProcessor('web-synth-proto', WebSynthProcessor);
改进的时间
虽然这种新解决方案奏效了,但这并不理想,因为我没有很好的方法来跟踪锈蚀代码中的状态,一旦我开始使我的合成器更加先进,我知道这将是有问题的。
但是,当我对我的代码进行重新设计而无需wasm-pack
时,这些拼图开始落入到位,我开始看到,尽管存在缺陷,但我仍然可以使用wasm-pack
。基于Web的合成器。
所需要的只是有点适应...
在我的下一篇文章中,我将描述我将
wasm-pack
重新结合到我的合成器项目中的尝试。此项目简介: