Skip to content

Commit 33a8682

Browse files
committed
Add example with WASM audio worklet
1 parent 6daa3d1 commit 33a8682

File tree

15 files changed

+355
-0
lines changed

15 files changed

+355
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ members = [
7777
"examples/raytrace-parallel",
7878
"examples/request-animation-frame",
7979
"examples/todomvc",
80+
"examples/wasm-audio-worklet",
8081
"examples/wasm-in-wasm",
8182
"examples/wasm-in-wasm-imports",
8283
"examples/wasm-in-web-worker",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "wasm-audio-worklet"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
console_log = "0.2.0"
8+
js-sys = "0.3.59"
9+
wasm-bindgen = "0.2.82"
10+
wasm-bindgen-futures = "0.4.32"
11+
web-sys = {version = "0.3.59", features = ["AudioContext", "AudioDestinationNode", "AudioWorklet", "AudioWorkletNode", "AudioWorkletNodeOptions", "Blob", "BlobPropertyBag", "Document", "HtmlInputElement", "HtmlLabelElement", "Url", "Window"]}
12+
13+
[lib]
14+
crate-type = ["cdylib", "rlib"]

examples/wasm-audio-worklet/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# wasm-audio-worklet
2+
3+
[View documentation for this example online][dox] or [View compiled example
4+
online][compiled]
5+
6+
[dox]: https://rustwasm.github.io/docs/wasm-bindgen/examples/wasm-audio-worklet.html
7+
[compiled]: https://rustwasm.github.io/wasm-bindgen/exbuild/wasm-audio-worklet/
8+
9+
You can build the example locally with:
10+
11+
```
12+
$ ./build.sh
13+
```
14+
15+
(or running the commands on Windows manually)
16+
17+
and then visiting http://localhost:8080 in a browser should run the example!
18+
19+
## dependent_module
20+
21+
This example shows how libraries can create worklets with minimal extra effort
22+
for application developers. No extra scripts or specific JS class names
23+
are needed. Just the build commands need to be changed to enable threads,
24+
and the server needs to send extra headers to enable site isolation.
25+
26+
dependent_module relies on ES6 `import`, which is
27+
[currently unavailable](https://bugzilla.mozilla.org/show_bug.cgi?id=1572644)
28+
for worklets in Firefox. The example works in Chrome.

examples/wasm-audio-worklet/build.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/sh
2+
3+
set -ex
4+
5+
# A couple of steps are necessary to get this build working which makes it slightly
6+
# nonstandard compared to most other builds.
7+
#
8+
# * First, the Rust standard library needs to be recompiled with atomics
9+
# enabled. to do that we use Cargo's unstable `-Zbuild-std` feature.
10+
#
11+
# * Next we need to compile everything with the `atomics` and `bulk-memory`
12+
# features enabled, ensuring that LLVM will generate atomic instructions,
13+
# shared memory, passive segments, etc.
14+
15+
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
16+
cargo build --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort
17+
18+
cargo run -p wasm-bindgen-cli -- \
19+
../../target/wasm32-unknown-unknown/release/wasm_audio_worklet.wasm \
20+
--out-dir . \
21+
--target web
22+
23+
python3 server.py
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>WASM audio worklet</title>
5+
</head>
6+
<body>
7+
<script type="module">
8+
import init, {web_main} from "./wasm_audio_worklet.js";
9+
async function run() {
10+
await init();
11+
web_main();
12+
}
13+
run();
14+
</script>
15+
</body>
16+
</html>

examples/wasm-audio-worklet/server.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python3
2+
from http.server import HTTPServer, SimpleHTTPRequestHandler, test
3+
import sys
4+
5+
class RequestHandler(SimpleHTTPRequestHandler):
6+
def end_headers(self):
7+
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
8+
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
9+
SimpleHTTPRequestHandler.end_headers(self)
10+
11+
if __name__ == '__main__':
12+
test(RequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use js_sys::{Array, JsString};
2+
use wasm_bindgen::prelude::*;
3+
use web_sys::{Blob, BlobPropertyBag, Url};
4+
5+
// This is a not-so-clean approach to get the current bindgen JS module URL
6+
// in Rust.
7+
#[wasm_bindgen]
8+
extern "C" {
9+
#[wasm_bindgen]
10+
type ImportMeta;
11+
12+
#[wasm_bindgen(method, getter)]
13+
fn url(this: &ImportMeta) -> JsString;
14+
15+
#[wasm_bindgen(js_namespace = import, js_name = meta)]
16+
static IMPORT_META: ImportMeta;
17+
}
18+
19+
pub fn on_the_fly(code: &str) -> Result<String, JsValue> {
20+
let header = format!(
21+
"import init, * as bindgen from '{}';\n\n",
22+
IMPORT_META.url(),
23+
);
24+
Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options(
25+
&Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)),
26+
&BlobPropertyBag::new().type_("text/javascript"),
27+
)?)
28+
}
29+
30+
// dependent_module! takes a local file name to a JS module as input and
31+
// returns a URL to a slightly modified module in run time. This modified module
32+
// has an additional import statement in the header that imports the current
33+
// bindgen JS module under the `bindgen` alias.
34+
// How this URL is produced does not matter for the macro user. on_the_fly
35+
// creates a blob URL in run time. A better, more sophisticated solution
36+
// would add wasm_bindgen support to put such a module in pkg/ during build time
37+
// and return a relative URL to this file instead.
38+
#[macro_export]
39+
macro_rules! dependent_module {
40+
($file_name:expr) => {
41+
crate::dependent_module::on_the_fly(include_str!($file_name))
42+
};
43+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function get_bindgen_transferable(o) {
2+
return {
3+
type: Object.getPrototypeOf(o).constructor.name,
4+
ptr: o.ptr,
5+
};
6+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use crate::oscillator::{Oscillator, Params};
2+
use crate::wasm_audio::wasm_audio;
3+
use wasm_bindgen::prelude::*;
4+
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
5+
use web_sys::{HtmlInputElement, HtmlLabelElement};
6+
7+
#[wasm_bindgen]
8+
pub async fn web_main() {
9+
// On the application level, most audio worklet internals are abstracted
10+
// by wasm_audio:
11+
let params: &'static Params = Box::leak(Box::new(Params::default()));
12+
let osc = Oscillator::new(&params);
13+
let ctx = wasm_audio(osc.into()).await.unwrap();
14+
15+
let window = web_sys::window().unwrap();
16+
let document = window.document().unwrap();
17+
let body = document.body().unwrap();
18+
19+
let volume = add_slider(&document, &body, "Volume:").unwrap();
20+
let frequency = add_slider(&document, &body, "Frequency:").unwrap();
21+
volume.set_value("0");
22+
frequency.set_min("20");
23+
frequency.set_value("60");
24+
25+
let listener = Closure::<dyn FnMut(_)>::new(move |_: web_sys::Event| {
26+
params.set_frequency(frequency.value().parse().unwrap());
27+
params.set_volume(volume.value().parse().unwrap());
28+
ctx.resume().unwrap();
29+
})
30+
.into_js_value();
31+
32+
body.add_event_listener_with_callback("input", listener.as_ref().unchecked_ref())
33+
.unwrap();
34+
}
35+
36+
fn add_slider(
37+
document: &web_sys::Document,
38+
body: &web_sys::HtmlElement,
39+
name: &str,
40+
) -> Result<HtmlInputElement, JsValue> {
41+
let input: HtmlInputElement = document.create_element("input")?.unchecked_into();
42+
let label: HtmlLabelElement = document.create_element("label")?.unchecked_into();
43+
input.set_type("range");
44+
label.set_text_content(Some(name));
45+
label.append_child(&input)?;
46+
body.append_child(&label)?;
47+
Ok(input)
48+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mod dependent_module;
2+
mod gui;
3+
mod oscillator;
4+
mod wasm_audio;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// WASM audio processors can be implemented in Rust without knowing
2+
// about audio worklets. Audio processors just need to expose the process method
3+
// through wasm_bindgen. Currently, this interface cannot be assured by traits.
4+
5+
use std::sync::atomic::{AtomicU8, Ordering};
6+
use wasm_bindgen::prelude::*;
7+
8+
// Let's implement a simple sine oscillator with variable frequency and volume.
9+
#[wasm_bindgen]
10+
pub struct Oscillator {
11+
params: &'static Params,
12+
accumulator: u32,
13+
}
14+
15+
impl Oscillator {
16+
pub fn new(params: &'static Params) -> Self {
17+
Self {
18+
params,
19+
accumulator: 0,
20+
}
21+
}
22+
}
23+
24+
#[wasm_bindgen]
25+
impl Oscillator {
26+
pub fn process(&mut self, output: &mut [f32]) -> bool {
27+
// This method is called in the audio process thread.
28+
// All imports are set, so host functionality available in worklets
29+
// (for example, logging) can be used:
30+
// `web_sys::console::log_1(&JsValue::from(output.len()));`
31+
// Note that currently TextEncoder and TextDecoder are stubs, so passing
32+
// strings may not work in this thread.
33+
for a in output {
34+
let frequency = self.params.frequency.load(Ordering::Relaxed);
35+
let volume = self.params.volume.load(Ordering::Relaxed);
36+
self.accumulator += u32::from(frequency);
37+
*a = (self.accumulator as f32 / 512.).sin() * (volume as f32 / 100.);
38+
}
39+
true
40+
}
41+
}
42+
43+
#[derive(Default)]
44+
pub struct Params {
45+
// Use atomics for parameters so they can be set in the main thread and
46+
// fetched by the audio process thread without further synchronization.
47+
frequency: AtomicU8,
48+
volume: AtomicU8,
49+
}
50+
51+
impl Params {
52+
pub fn set_frequency(&self, frequency: u8) {
53+
self.frequency.store(frequency, Ordering::Relaxed);
54+
}
55+
pub fn set_volume(&self, volume: u8) {
56+
self.volume.store(volume, Ordering::Relaxed);
57+
}
58+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
if (!globalThis.TextDecoder) {
2+
globalThis.TextDecoder = class TextDecoder {
3+
decode() {
4+
return "";
5+
}
6+
};
7+
}
8+
9+
if (!globalThis.TextEncoder) {
10+
globalThis.TextEncoder = class TextEncoder {
11+
encode() {
12+
return new Uint8Array(0);
13+
}
14+
};
15+
}
16+
17+
export function nop() {
18+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use crate::dependent_module;
2+
use wasm_bindgen::prelude::*;
3+
use wasm_bindgen::JsValue;
4+
use wasm_bindgen_futures::JsFuture;
5+
use web_sys::{AudioContext, AudioWorkletNode, AudioWorkletNodeOptions};
6+
7+
// Use wasm_audio if you have a single wasm audio processor in your application
8+
// whose samples should be played directly.
9+
pub async fn wasm_audio(wasm_processor: JsValue) -> Result<AudioContext, JsValue> {
10+
let ctx = AudioContext::new()?;
11+
prepare_wasm_audio(&ctx).await?;
12+
let node = wasm_audio_node(&ctx, wasm_processor)?;
13+
node.connect_with_audio_node(&ctx.destination())?;
14+
Ok(ctx)
15+
}
16+
17+
// wasm_audio_node creates an AudioWorkletNode running a wasm audio processor.
18+
// Remember to call prepare_wasm_audio once on your context before calling
19+
// this function.
20+
pub fn wasm_audio_node(
21+
ctx: &AudioContext,
22+
wasm_processor: JsValue,
23+
) -> Result<AudioWorkletNode, JsValue> {
24+
AudioWorkletNode::new_with_options(
25+
&ctx,
26+
"WasmProcessor",
27+
&AudioWorkletNodeOptions::new().processor_options(Some(&js_sys::Array::of3(
28+
&wasm_bindgen::module(),
29+
&wasm_bindgen::memory(),
30+
&get_bindgen_transferable(wasm_processor),
31+
))),
32+
)
33+
}
34+
35+
pub async fn prepare_wasm_audio(ctx: &AudioContext) -> Result<(), JsValue> {
36+
nop();
37+
let mod_url = dependent_module!("worklet_async.js")?;
38+
JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?).await?;
39+
Ok(())
40+
}
41+
42+
// TextEncoder and TextDecoder are not available in Audio Worklets, but there
43+
// is a dirty workaround: Import polyfill.js to install stub implementations
44+
// of these classes in globalThis.
45+
#[wasm_bindgen(module = "/src/polyfill.js")]
46+
extern "C" {
47+
fn nop();
48+
}
49+
50+
#[wasm_bindgen(module = "/src/get_bindgen_transferable.js")]
51+
extern "C" {
52+
// When creating a JsValue from a wasm_bindgen struct, the resulting object
53+
// contains just the numeric pointer after structured clone. To call methods
54+
// on objects, the type must be known too. get_bindgen_transferable creates
55+
// a JS object that contains both the type and the numeric pointer.
56+
fn get_bindgen_transferable(obj: JsValue) -> JsValue;
57+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Doesn't work until https://github.com/rustwasm/wasm-bindgen/pull/3016 is available.
2+
// Meanwhile, worklet_async can be used instead.
3+
4+
registerProcessor("WasmProcessor", class WasmProcessor extends AudioWorkletProcessor {
5+
constructor(options) {
6+
super();
7+
let [module, memory, {type, ptr}] = options.processorOptions;
8+
bindgen.initSync(module, memory);
9+
this.processor = bindgen[type].__wrap(ptr);
10+
}
11+
process(inputs, outputs) {
12+
return this.processor.process(outputs[0][0]);
13+
}
14+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
registerProcessor("WasmProcessor", class WasmProcessor extends AudioWorkletProcessor {
2+
constructor(options) {
3+
super();
4+
let [module, memory, {type, ptr}] = options.processorOptions;
5+
init(module, memory).then(() => {
6+
this.processor = bindgen[type].__wrap(ptr);
7+
});
8+
}
9+
process(inputs, outputs) {
10+
if (!this.processor) return true;
11+
return this.processor.process(outputs[0][0]);
12+
}
13+
});

0 commit comments

Comments
 (0)