diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f560eca28a1..9d1d28cd8c0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -232,7 +232,7 @@ jobs:
ln -snf `pwd`/target/debug/wasm-bindgen $(dirname `which cargo`)/wasm-bindgen
- run: mv _package.json package.json && npm install && rm package.json
- run: |
- for dir in `ls examples | grep -v README | grep -v raytrace | grep -v deno`; do
+ for dir in `ls examples | grep -v README | grep -v raytrace | grep -v deno | grep -v wasm-audio-worklet`; do
(cd examples/$dir &&
(npm run build -- --output-path ../../exbuild/$dir ||
(./build.sh && mkdir -p ../../exbuild/$dir && cp -r ./* ../../exbuild/$dir))
@@ -245,7 +245,7 @@ jobs:
name: examples1
path: exbuild
- build_raytrace:
+ build_nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -253,9 +253,11 @@ jobs:
- run: rustup target add wasm32-unknown-unknown
- run: rustup component add rust-src
- run: |
- (cd examples/raytrace-parallel && ./build.sh)
- mkdir exbuild
- cp examples/raytrace-parallel/*.{js,html,wasm} exbuild
+ for dir in raytrace-parallel wasm-audio-worklet; do
+ (cd examples/$dir &&
+ ./build.sh && mkdir -p ../../exbuild/$dir && cp -r ./* ../../exbuild/$dir
+ ) || exit 1;
+ done
- uses: actions/upload-artifact@v2
with:
name: examples2
@@ -264,7 +266,7 @@ jobs:
test_examples:
needs:
- build_examples
- - build_raytrace
+ - build_nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -275,7 +277,7 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: examples2
- path: exbuild/raytrace-parallel
+ path: exbuild
- run: rustup update --no-self-update stable && rustup default stable
- run: cargo test -p example-tests
env:
@@ -376,7 +378,7 @@ jobs:
- dist_macos
- dist_windows
- build_examples
- - build_raytrace
+ - build_nightly
- build_benchmarks
runs-on: ubuntu-latest
steps:
diff --git a/Cargo.toml b/Cargo.toml
index 72d96aeeab4..5101fa4b409 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -78,6 +78,7 @@ members = [
"examples/raytrace-parallel",
"examples/request-animation-frame",
"examples/todomvc",
+ "examples/wasm-audio-worklet",
"examples/wasm-in-wasm",
"examples/wasm-in-wasm-imports",
"examples/wasm-in-web-worker",
diff --git a/examples/wasm-audio-worklet/Cargo.toml b/examples/wasm-audio-worklet/Cargo.toml
new file mode 100644
index 00000000000..cc187f6ec36
--- /dev/null
+++ b/examples/wasm-audio-worklet/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "wasm-audio-worklet"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+console_log = "0.2.0"
+js-sys = "0.3.59"
+wasm-bindgen = "0.2.82"
+wasm-bindgen-futures = "0.4.32"
+
+[dependencies.web-sys]
+version = "0.3.59"
+features = [
+ "AudioContext",
+ "AudioDestinationNode",
+ "AudioWorklet",
+ "AudioWorkletNode",
+ "AudioWorkletNodeOptions",
+ "Blob",
+ "BlobPropertyBag",
+ "Document",
+ "HtmlInputElement",
+ "HtmlLabelElement",
+ "Url",
+ "Window",
+]
diff --git a/examples/wasm-audio-worklet/README.md b/examples/wasm-audio-worklet/README.md
new file mode 100644
index 00000000000..157e0c41ab4
--- /dev/null
+++ b/examples/wasm-audio-worklet/README.md
@@ -0,0 +1,17 @@
+# wasm-audio-worklet
+
+[View documentation for this example online][dox] or [View compiled example
+online][compiled]
+
+[dox]: https://rustwasm.github.io/docs/wasm-bindgen/examples/wasm-audio-worklet.html
+[compiled]: https://wasm-bindgen.netlify.app/exbuild/wasm-audio-worklet/
+
+You can build the example locally with:
+
+```
+$ ./run.sh
+```
+
+(or running the commands on Windows manually)
+
+and then visiting http://localhost:8080 in a browser should run the example!
diff --git a/examples/wasm-audio-worklet/build.sh b/examples/wasm-audio-worklet/build.sh
new file mode 100755
index 00000000000..90b8fc9cb14
--- /dev/null
+++ b/examples/wasm-audio-worklet/build.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -ex
+
+# A couple of steps are necessary to get this build working which makes it slightly
+# nonstandard compared to most other builds.
+#
+# * First, the Rust standard library needs to be recompiled with atomics
+# enabled. to do that we use Cargo's unstable `-Zbuild-std` feature.
+#
+# * Next we need to compile everything with the `atomics` and `bulk-memory`
+# features enabled, ensuring that LLVM will generate atomic instructions,
+# shared memory, passive segments, etc.
+
+RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
+ cargo build --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort
+
+cargo run -p wasm-bindgen-cli -- \
+ ../../target/wasm32-unknown-unknown/release/wasm_audio_worklet.wasm \
+ --out-dir . \
+ --target web
diff --git a/examples/wasm-audio-worklet/index.html b/examples/wasm-audio-worklet/index.html
new file mode 100644
index 00000000000..b2d8681ab56
--- /dev/null
+++ b/examples/wasm-audio-worklet/index.html
@@ -0,0 +1,16 @@
+
+
+
+ WASM audio worklet
+
+
+
+
+
diff --git a/examples/wasm-audio-worklet/run.sh b/examples/wasm-audio-worklet/run.sh
new file mode 100755
index 00000000000..e32cfb456e9
--- /dev/null
+++ b/examples/wasm-audio-worklet/run.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -ex
+
+./build.sh
+
+python3 server.py
diff --git a/examples/wasm-audio-worklet/server.py b/examples/wasm-audio-worklet/server.py
new file mode 100644
index 00000000000..8886642ae30
--- /dev/null
+++ b/examples/wasm-audio-worklet/server.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+from http.server import HTTPServer, SimpleHTTPRequestHandler, test
+import sys
+
+class RequestHandler(SimpleHTTPRequestHandler):
+ def end_headers(self):
+ self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
+ self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
+ SimpleHTTPRequestHandler.end_headers(self)
+
+if __name__ == '__main__':
+ test(RequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000)
diff --git a/examples/wasm-audio-worklet/src/dependent_module.rs b/examples/wasm-audio-worklet/src/dependent_module.rs
new file mode 100644
index 00000000000..d4180e3985e
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/dependent_module.rs
@@ -0,0 +1,45 @@
+use js_sys::{Array, JsString};
+use wasm_bindgen::prelude::*;
+use web_sys::{Blob, BlobPropertyBag, Url};
+
+// This is a not-so-clean approach to get the current bindgen ES module URL
+// in Rust. This will fail at run time on bindgen targets not using ES modules.
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen]
+ type ImportMeta;
+
+ #[wasm_bindgen(method, getter)]
+ fn url(this: &ImportMeta) -> JsString;
+
+ #[wasm_bindgen(js_namespace = import, js_name = meta)]
+ static IMPORT_META: ImportMeta;
+}
+
+pub fn on_the_fly(code: &str) -> Result {
+ // Generate the import of the bindgen ES module, assuming `--target web`:
+ let header = format!(
+ "import init, * as bindgen from '{}';\n\n",
+ IMPORT_META.url(),
+ );
+
+ Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options(
+ &Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)),
+ &BlobPropertyBag::new().type_("text/javascript"),
+ )?)
+}
+
+// dependent_module! takes a local file name to a JS module as input and
+// returns a URL to a slightly modified module in run time. This modified module
+// has an additional import statement in the header that imports the current
+// bindgen JS module under the `bindgen` alias, and the separate init function.
+// How this URL is produced does not matter for the macro user. on_the_fly
+// creates a blob URL in run time. A better, more sophisticated solution
+// would add wasm_bindgen support to put such a module in pkg/ during build time
+// and return a URL to this file instead (described in #3019).
+#[macro_export]
+macro_rules! dependent_module {
+ ($file_name:expr) => {
+ crate::dependent_module::on_the_fly(include_str!($file_name))
+ };
+}
diff --git a/examples/wasm-audio-worklet/src/gui.rs b/examples/wasm-audio-worklet/src/gui.rs
new file mode 100644
index 00000000000..fa96103b440
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/gui.rs
@@ -0,0 +1,39 @@
+use crate::oscillator::Params;
+use wasm_bindgen::{closure::Closure, JsCast, JsValue};
+use web_sys::{AudioContext, HtmlInputElement, HtmlLabelElement};
+
+pub fn create_gui(params: &'static Params, ctx: AudioContext) {
+ let window = web_sys::window().unwrap();
+ let document = window.document().unwrap();
+ let body = document.body().unwrap();
+
+ let volume = add_slider(&document, &body, "Volume:").unwrap();
+ let frequency = add_slider(&document, &body, "Frequency:").unwrap();
+ volume.set_value("0");
+ frequency.set_min("20");
+ frequency.set_value("60");
+
+ let listener = Closure::::new(move |_: web_sys::Event| {
+ params.set_frequency(frequency.value().parse().unwrap());
+ params.set_volume(volume.value().parse().unwrap());
+ ctx.resume().unwrap();
+ })
+ .into_js_value();
+
+ body.add_event_listener_with_callback("input", listener.as_ref().unchecked_ref())
+ .unwrap();
+}
+
+fn add_slider(
+ document: &web_sys::Document,
+ body: &web_sys::HtmlElement,
+ name: &str,
+) -> Result {
+ let input: HtmlInputElement = document.create_element("input")?.unchecked_into();
+ let label: HtmlLabelElement = document.create_element("label")?.unchecked_into();
+ input.set_type("range");
+ label.set_text_content(Some(name));
+ label.append_child(&input)?;
+ body.append_child(&label)?;
+ Ok(input)
+}
diff --git a/examples/wasm-audio-worklet/src/lib.rs b/examples/wasm-audio-worklet/src/lib.rs
new file mode 100644
index 00000000000..d5a1ded9bde
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/lib.rs
@@ -0,0 +1,20 @@
+mod dependent_module;
+mod gui;
+mod oscillator;
+mod wasm_audio;
+
+use gui::create_gui;
+use oscillator::{Oscillator, Params};
+use wasm_audio::wasm_audio;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub async fn web_main() {
+ // On the application level, audio worklet internals are abstracted by wasm_audio:
+ let params: &'static Params = Box::leak(Box::new(Params::default()));
+ let mut osc = Oscillator::new(¶ms);
+ let ctx = wasm_audio(Box::new(move |buf| osc.process(buf)))
+ .await
+ .unwrap();
+ create_gui(params, ctx);
+}
diff --git a/examples/wasm-audio-worklet/src/oscillator.rs b/examples/wasm-audio-worklet/src/oscillator.rs
new file mode 100644
index 00000000000..a6247703aa7
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/oscillator.rs
@@ -0,0 +1,54 @@
+// WASM audio processors can be implemented in Rust without knowing
+// about audio worklets.
+
+use std::sync::atomic::{AtomicU8, Ordering};
+
+// Let's implement a simple sine oscillator with variable frequency and volume.
+pub struct Oscillator {
+ params: &'static Params,
+ accumulator: u32,
+}
+
+impl Oscillator {
+ pub fn new(params: &'static Params) -> Self {
+ Self {
+ params,
+ accumulator: 0,
+ }
+ }
+}
+
+impl Oscillator {
+ pub fn process(&mut self, output: &mut [f32]) -> bool {
+ // This method is called in the audio process thread.
+ // All imports are set, so host functionality available in worklets
+ // (for example, logging) can be used:
+ // `web_sys::console::log_1(&JsValue::from(output.len()));`
+ // Note that currently TextEncoder and TextDecoder are stubs, so passing
+ // strings may not work in this thread.
+ for a in output {
+ let frequency = self.params.frequency.load(Ordering::Relaxed);
+ let volume = self.params.volume.load(Ordering::Relaxed);
+ self.accumulator += u32::from(frequency);
+ *a = (self.accumulator as f32 / 512.).sin() * (volume as f32 / 100.);
+ }
+ true
+ }
+}
+
+#[derive(Default)]
+pub struct Params {
+ // Use atomics for parameters so they can be set in the main thread and
+ // fetched by the audio process thread without further synchronization.
+ frequency: AtomicU8,
+ volume: AtomicU8,
+}
+
+impl Params {
+ pub fn set_frequency(&self, frequency: u8) {
+ self.frequency.store(frequency, Ordering::Relaxed);
+ }
+ pub fn set_volume(&self, volume: u8) {
+ self.volume.store(volume, Ordering::Relaxed);
+ }
+}
diff --git a/examples/wasm-audio-worklet/src/polyfill.js b/examples/wasm-audio-worklet/src/polyfill.js
new file mode 100644
index 00000000000..146a0e2c578
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/polyfill.js
@@ -0,0 +1,26 @@
+if (!globalThis.TextDecoder) {
+ globalThis.TextDecoder = class TextDecoder {
+ decode(arg) {
+ if (typeof arg !== 'undefined') {
+ throw Error('TextDecoder stub called');
+ } else {
+ return '';
+ }
+ }
+ };
+}
+
+if (!globalThis.TextEncoder) {
+ globalThis.TextEncoder = class TextEncoder {
+ encode(arg) {
+ if (typeof arg !== 'undefined') {
+ throw Error('TextEncoder stub called');
+ } else {
+ return new Uint8Array(0);
+ }
+ }
+ };
+}
+
+export function nop() {
+}
diff --git a/examples/wasm-audio-worklet/src/wasm_audio.rs b/examples/wasm-audio-worklet/src/wasm_audio.rs
new file mode 100644
index 00000000000..fa149dd0471
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/wasm_audio.rs
@@ -0,0 +1,68 @@
+use crate::dependent_module;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsValue;
+use wasm_bindgen_futures::JsFuture;
+use web_sys::{AudioContext, AudioWorkletNode, AudioWorkletNodeOptions};
+
+#[wasm_bindgen]
+pub struct WasmAudioProcessor(Box bool>);
+
+#[wasm_bindgen]
+impl WasmAudioProcessor {
+ pub fn process(&mut self, buf: &mut [f32]) -> bool {
+ self.0(buf)
+ }
+ pub fn pack(self) -> usize {
+ Box::into_raw(Box::new(self)) as usize
+ }
+ pub unsafe fn unpack(val: usize) -> Self {
+ *Box::from_raw(val as *mut _)
+ }
+}
+
+// Use wasm_audio if you have a single wasm audio processor in your application
+// whose samples should be played directly. Ideally, call wasm_audio based on
+// user interaction. Otherwise, resume the context on user interaction, so
+// playback starts reliably on all browsers.
+pub async fn wasm_audio(
+ process: Box bool>,
+) -> Result {
+ let ctx = AudioContext::new()?;
+ prepare_wasm_audio(&ctx).await?;
+ let node = wasm_audio_node(&ctx, process)?;
+ node.connect_with_audio_node(&ctx.destination())?;
+ Ok(ctx)
+}
+
+// wasm_audio_node creates an AudioWorkletNode running a wasm audio processor.
+// Remember to call prepare_wasm_audio once on your context before calling
+// this function.
+pub fn wasm_audio_node(
+ ctx: &AudioContext,
+ process: Box bool>,
+) -> Result {
+ AudioWorkletNode::new_with_options(
+ &ctx,
+ "WasmProcessor",
+ &AudioWorkletNodeOptions::new().processor_options(Some(&js_sys::Array::of3(
+ &wasm_bindgen::module(),
+ &wasm_bindgen::memory(),
+ &WasmAudioProcessor(process).pack().into(),
+ ))),
+ )
+}
+
+pub async fn prepare_wasm_audio(ctx: &AudioContext) -> Result<(), JsValue> {
+ nop();
+ let mod_url = dependent_module!("worklet.js")?;
+ JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?).await?;
+ Ok(())
+}
+
+// TextEncoder and TextDecoder are not available in Audio Worklets, but there
+// is a dirty workaround: Import polyfill.js to install stub implementations
+// of these classes in globalThis.
+#[wasm_bindgen(module = "/src/polyfill.js")]
+extern "C" {
+ fn nop();
+}
diff --git a/examples/wasm-audio-worklet/src/worklet.js b/examples/wasm-audio-worklet/src/worklet.js
new file mode 100644
index 00000000000..4223d72ca30
--- /dev/null
+++ b/examples/wasm-audio-worklet/src/worklet.js
@@ -0,0 +1,11 @@
+registerProcessor("WasmProcessor", class WasmProcessor extends AudioWorkletProcessor {
+ constructor(options) {
+ super();
+ let [module, memory, handle] = options.processorOptions;
+ bindgen.initSync(module, memory);
+ this.processor = bindgen.WasmAudioProcessor.unpack(handle);
+ }
+ process(inputs, outputs) {
+ return this.processor.process(outputs[0][0]);
+ }
+});
diff --git a/guide/src/examples/raytrace.md b/guide/src/examples/raytrace.md
index 2b17a00a73e..522a2f70b96 100644
--- a/guide/src/examples/raytrace.md
+++ b/guide/src/examples/raytrace.md
@@ -16,12 +16,14 @@ and wasm with Rust on the web.
One of the major gotchas with threaded WebAssembly is that Rust does not ship a
precompiled target (e.g. standard library) which has threading support enabled.
This means that you'll need to recompile the standard library with the
-appropriate rustc flags, namely `-C target-feature=+atomics,+bulk-memory`.
+appropriate rustc flags, namely
+`-C target-feature=+atomics,+bulk-memory,+mutable-globals`.
+Note that this requires a nightly Rust toolchain.
To do this you can use the `RUSTFLAGS` environment variable that Cargo reads:
```sh
-export RUSTFLAGS='-C target-feature=+atomics,+bulk-memory'
+export RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals'
```
To recompile the standard library it's recommended to use Cargo's
@@ -40,7 +42,7 @@ build-std = ['std', 'panic_abort']
[build]
target = "wasm32-unknown-unknown"
-rustflags = '-Ctarget-feature=+atomics,+bulk-memory'
+rustflags = '-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals'
```
After this `cargo build` should produce a WebAssembly file with threading
diff --git a/guide/src/examples/wasm-audio-worklet.md b/guide/src/examples/wasm-audio-worklet.md
new file mode 100644
index 00000000000..b7c9429f05b
--- /dev/null
+++ b/guide/src/examples/wasm-audio-worklet.md
@@ -0,0 +1,45 @@
+# WASM audio worklet
+
+[View full source code][code] or [view the compiled example online][online]
+
+[online]: https://wasm-bindgen.netlify.app/exbuild/wasm-audio-worklet/
+[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/wasm-audio-worklet
+
+This is an example of using threads inside specific worklets with WebAssembly,
+Rust, and `wasm-bindgen`, culminating in an oscillator demo. This demo should
+complement the [parallel-raytrace][parallel-raytrace] example by
+demonstrating an alternative approach using ES modules with on-the-fly module
+creation.
+
+[parallel-raytrace]: https://rustwasm.github.io/docs/wasm-bindgen/examples/raytrace.html
+
+### Building the demo
+
+One of the major gotchas with threaded WebAssembly is that Rust does not ship a
+precompiled target (e.g. standard library) which has threading support enabled.
+This means that you'll need to recompile the standard library with the
+appropriate rustc flags, namely
+`-C target-feature=+atomics,+bulk-memory,+mutable-globals`.
+Note that this requires a nightly Rust toolchain. See the [more detailed
+instructions][build] of the parallel-raytrace example.
+
+[build]: https://rustwasm.github.io/docs/wasm-bindgen/examples/raytrace.html#building-the-demo
+
+### Caveats
+
+This example shares most of its [caveats][caveats] with the parallel-raytrace
+example. However, it tries to encapsulate worklet creation in a Rust module, so
+the application developer does not need to maintain custom JS code.
+
+[caveats]: https://rustwasm.github.io/docs/wasm-bindgen/examples/raytrace.html#caveats
+
+### Browser Requirements
+
+This demo should work in the latest Chrome and Safari versions at this time.
+Firefox [does not support][firefox-worklet-import] imports in worklet modules,
+which are difficult to avoid in this example, as `importScripts` is unavailable
+in worklets. Note that this example requires HTTP headers to be set like in
+[parallel-raytrace][headers].
+
+[firefox-worklet-import]: https://bugzilla.mozilla.org/show_bug.cgi?id=1572644
+[headers]: https://rustwasm.github.io/docs/wasm-bindgen/examples/raytrace.html#browser-requirements