如何在JavaScript Worklet中使用Rust Code(无WASM-PACK)
#javascript #rust #webassembly

I recently posted about how I have wanted to build a synthesizer and that I finally found the technologies I needed to make it happen.

我项目的下一步是构建原型。此过程的第一步是产生正弦波,并通过扬声器播放。一旦完成了第一步,我就可以在这个基础上开始建立更复杂的功能。

设置

Web Audio API documentation仔细看了一眼之后,我认为合成器向扬声器输出声音的最佳方法是通过使用AudioWorkletNodeAudioWorkletProcessor结合使用。两个节点的这种组合将使我能够在背景线程中生成音频,然后可以通过扬声器播放。

如果我想采用其他方法,则需要扩展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 }
    }
}

在这一点上,我有一个基本的振荡器。将音高(以赫兹为单位)传递,并在01之间增益到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(),因此该解决方案也不起作用。

然后,我尝试通过AudioWorkletNodeMessagePort实例将导入的JavaScript软件包从主JavaScript文件传递到AudioWorkletProcessor。这是行不通的,因为使用MessagePort依赖于传递的对象可复制,而JavaScript函数则无法复制。我尝试了这些解决方案变体的变体,但我所做的任何事情都无法正常工作。

最终,我偶然发现了这个github问题:

Support using wasm-pack in Worklets (particularly AudioWorklet) #689

Smona avatar
Smona 发布在

功能描述

WebAssembly的关键用例之一是音频处理。 WASM-PACK当前不支持AudioWorklet(据称是Web上的自定义音频处理的未来),其当前的--target选项。

webno-modules目标越来越接近,但是在实例化过程中出现了错误,因为AudioWorklet上下文缺少JS包装器所期望的几个浏览器API。这个问题可能会扩展到其他工人/工作点环境,但是我只尝试使用AudioWorklet。

基本示例

my_processor.worklet.js

  class   myProcessor  扩展class =“ pl-v”> audioworkletprocessor   {
			 constructor     { import   “ ../ pkg/pkg/audio”    然后   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重新结合到我的合成器项目中的尝试。

此项目简介: