Skip to content

Commit d881d9d

Browse files
Add example with WASM audio worklet (#3017)
1 parent d759c66 commit d881d9d

File tree

17 files changed

+427
-11
lines changed

17 files changed

+427
-11
lines changed

.github/workflows/main.yml

+10-8
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ jobs:
232232
ln -snf `pwd`/target/debug/wasm-bindgen $(dirname `which cargo`)/wasm-bindgen
233233
- run: mv _package.json package.json && npm install && rm package.json
234234
- run: |
235-
for dir in `ls examples | grep -v README | grep -v raytrace | grep -v deno`; do
235+
for dir in `ls examples | grep -v README | grep -v raytrace | grep -v deno | grep -v wasm-audio-worklet`; do
236236
(cd examples/$dir &&
237237
(npm run build -- --output-path ../../exbuild/$dir ||
238238
(./build.sh && mkdir -p ../../exbuild/$dir && cp -r ./* ../../exbuild/$dir))
@@ -245,17 +245,19 @@ jobs:
245245
name: examples1
246246
path: exbuild
247247

248-
build_raytrace:
248+
build_nightly:
249249
runs-on: ubuntu-latest
250250
steps:
251251
- uses: actions/checkout@v2
252252
- run: rustup default nightly-2022-05-19
253253
- run: rustup target add wasm32-unknown-unknown
254254
- run: rustup component add rust-src
255255
- run: |
256-
(cd examples/raytrace-parallel && ./build.sh)
257-
mkdir exbuild
258-
cp examples/raytrace-parallel/*.{js,html,wasm} exbuild
256+
for dir in raytrace-parallel wasm-audio-worklet; do
257+
(cd examples/$dir &&
258+
./build.sh && mkdir -p ../../exbuild/$dir && cp -r ./* ../../exbuild/$dir
259+
) || exit 1;
260+
done
259261
- uses: actions/upload-artifact@v2
260262
with:
261263
name: examples2
@@ -264,7 +266,7 @@ jobs:
264266
test_examples:
265267
needs:
266268
- build_examples
267-
- build_raytrace
269+
- build_nightly
268270
runs-on: ubuntu-latest
269271
steps:
270272
- uses: actions/checkout@v2
@@ -275,7 +277,7 @@ jobs:
275277
- uses: actions/download-artifact@v3
276278
with:
277279
name: examples2
278-
path: exbuild/raytrace-parallel
280+
path: exbuild
279281
- run: rustup update --no-self-update stable && rustup default stable
280282
- run: cargo test -p example-tests
281283
env:
@@ -376,7 +378,7 @@ jobs:
376378
- dist_macos
377379
- dist_windows
378380
- build_examples
379-
- build_raytrace
381+
- build_nightly
380382
- build_benchmarks
381383
runs-on: ubuntu-latest
382384
steps:

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ members = [
7878
"examples/raytrace-parallel",
7979
"examples/request-animation-frame",
8080
"examples/todomvc",
81+
"examples/wasm-audio-worklet",
8182
"examples/wasm-in-wasm",
8283
"examples/wasm-in-wasm-imports",
8384
"examples/wasm-in-web-worker",
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "wasm-audio-worklet"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies]
10+
console_log = "0.2.0"
11+
js-sys = "0.3.59"
12+
wasm-bindgen = "0.2.82"
13+
wasm-bindgen-futures = "0.4.32"
14+
15+
[dependencies.web-sys]
16+
version = "0.3.59"
17+
features = [
18+
"AudioContext",
19+
"AudioDestinationNode",
20+
"AudioWorklet",
21+
"AudioWorkletNode",
22+
"AudioWorkletNodeOptions",
23+
"Blob",
24+
"BlobPropertyBag",
25+
"Document",
26+
"HtmlInputElement",
27+
"HtmlLabelElement",
28+
"Url",
29+
"Window",
30+
]

examples/wasm-audio-worklet/README.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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://wasm-bindgen.netlify.app/exbuild/wasm-audio-worklet/
8+
9+
You can build the example locally with:
10+
11+
```
12+
$ ./run.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!

examples/wasm-audio-worklet/build.sh

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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
+16
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/run.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
set -ex
4+
5+
./build.sh
6+
7+
python3 server.py

examples/wasm-audio-worklet/server.py

+12
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)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 ES module URL
6+
// in Rust. This will fail at run time on bindgen targets not using ES modules.
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+
// Generate the import of the bindgen ES module, assuming `--target web`:
21+
let header = format!(
22+
"import init, * as bindgen from '{}';\n\n",
23+
IMPORT_META.url(),
24+
);
25+
26+
Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options(
27+
&Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)),
28+
&BlobPropertyBag::new().type_("text/javascript"),
29+
)?)
30+
}
31+
32+
// dependent_module! takes a local file name to a JS module as input and
33+
// returns a URL to a slightly modified module in run time. This modified module
34+
// has an additional import statement in the header that imports the current
35+
// bindgen JS module under the `bindgen` alias, and the separate init function.
36+
// How this URL is produced does not matter for the macro user. on_the_fly
37+
// creates a blob URL in run time. A better, more sophisticated solution
38+
// would add wasm_bindgen support to put such a module in pkg/ during build time
39+
// and return a URL to this file instead (described in #3019).
40+
#[macro_export]
41+
macro_rules! dependent_module {
42+
($file_name:expr) => {
43+
crate::dependent_module::on_the_fly(include_str!($file_name))
44+
};
45+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::oscillator::Params;
2+
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
3+
use web_sys::{AudioContext, HtmlInputElement, HtmlLabelElement};
4+
5+
pub fn create_gui(params: &'static Params, ctx: AudioContext) {
6+
let window = web_sys::window().unwrap();
7+
let document = window.document().unwrap();
8+
let body = document.body().unwrap();
9+
10+
let volume = add_slider(&document, &body, "Volume:").unwrap();
11+
let frequency = add_slider(&document, &body, "Frequency:").unwrap();
12+
volume.set_value("0");
13+
frequency.set_min("20");
14+
frequency.set_value("60");
15+
16+
let listener = Closure::<dyn FnMut(_)>::new(move |_: web_sys::Event| {
17+
params.set_frequency(frequency.value().parse().unwrap());
18+
params.set_volume(volume.value().parse().unwrap());
19+
ctx.resume().unwrap();
20+
})
21+
.into_js_value();
22+
23+
body.add_event_listener_with_callback("input", listener.as_ref().unchecked_ref())
24+
.unwrap();
25+
}
26+
27+
fn add_slider(
28+
document: &web_sys::Document,
29+
body: &web_sys::HtmlElement,
30+
name: &str,
31+
) -> Result<HtmlInputElement, JsValue> {
32+
let input: HtmlInputElement = document.create_element("input")?.unchecked_into();
33+
let label: HtmlLabelElement = document.create_element("label")?.unchecked_into();
34+
input.set_type("range");
35+
label.set_text_content(Some(name));
36+
label.append_child(&input)?;
37+
body.append_child(&label)?;
38+
Ok(input)
39+
}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
mod dependent_module;
2+
mod gui;
3+
mod oscillator;
4+
mod wasm_audio;
5+
6+
use gui::create_gui;
7+
use oscillator::{Oscillator, Params};
8+
use wasm_audio::wasm_audio;
9+
use wasm_bindgen::prelude::*;
10+
11+
#[wasm_bindgen]
12+
pub async fn web_main() {
13+
// On the application level, audio worklet internals are abstracted by wasm_audio:
14+
let params: &'static Params = Box::leak(Box::new(Params::default()));
15+
let mut osc = Oscillator::new(&params);
16+
let ctx = wasm_audio(Box::new(move |buf| osc.process(buf)))
17+
.await
18+
.unwrap();
19+
create_gui(params, ctx);
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// WASM audio processors can be implemented in Rust without knowing
2+
// about audio worklets.
3+
4+
use std::sync::atomic::{AtomicU8, Ordering};
5+
6+
// Let's implement a simple sine oscillator with variable frequency and volume.
7+
pub struct Oscillator {
8+
params: &'static Params,
9+
accumulator: u32,
10+
}
11+
12+
impl Oscillator {
13+
pub fn new(params: &'static Params) -> Self {
14+
Self {
15+
params,
16+
accumulator: 0,
17+
}
18+
}
19+
}
20+
21+
impl Oscillator {
22+
pub fn process(&mut self, output: &mut [f32]) -> bool {
23+
// This method is called in the audio process thread.
24+
// All imports are set, so host functionality available in worklets
25+
// (for example, logging) can be used:
26+
// `web_sys::console::log_1(&JsValue::from(output.len()));`
27+
// Note that currently TextEncoder and TextDecoder are stubs, so passing
28+
// strings may not work in this thread.
29+
for a in output {
30+
let frequency = self.params.frequency.load(Ordering::Relaxed);
31+
let volume = self.params.volume.load(Ordering::Relaxed);
32+
self.accumulator += u32::from(frequency);
33+
*a = (self.accumulator as f32 / 512.).sin() * (volume as f32 / 100.);
34+
}
35+
true
36+
}
37+
}
38+
39+
#[derive(Default)]
40+
pub struct Params {
41+
// Use atomics for parameters so they can be set in the main thread and
42+
// fetched by the audio process thread without further synchronization.
43+
frequency: AtomicU8,
44+
volume: AtomicU8,
45+
}
46+
47+
impl Params {
48+
pub fn set_frequency(&self, frequency: u8) {
49+
self.frequency.store(frequency, Ordering::Relaxed);
50+
}
51+
pub fn set_volume(&self, volume: u8) {
52+
self.volume.store(volume, Ordering::Relaxed);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
if (!globalThis.TextDecoder) {
2+
globalThis.TextDecoder = class TextDecoder {
3+
decode(arg) {
4+
if (typeof arg !== 'undefined') {
5+
throw Error('TextDecoder stub called');
6+
} else {
7+
return '';
8+
}
9+
}
10+
};
11+
}
12+
13+
if (!globalThis.TextEncoder) {
14+
globalThis.TextEncoder = class TextEncoder {
15+
encode(arg) {
16+
if (typeof arg !== 'undefined') {
17+
throw Error('TextEncoder stub called');
18+
} else {
19+
return new Uint8Array(0);
20+
}
21+
}
22+
};
23+
}
24+
25+
export function nop() {
26+
}

0 commit comments

Comments
 (0)