Skip to content

Add example with WASM audio worklet #3017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -245,17 +245,19 @@ jobs:
name: examples1
path: exbuild

build_raytrace:
build_nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: rustup default nightly-2022-05-19
- 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
Expand All @@ -264,7 +266,7 @@ jobs:
test_examples:
needs:
- build_examples
- build_raytrace
- build_nightly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand All @@ -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:
Expand Down Expand Up @@ -376,7 +378,7 @@ jobs:
- dist_macos
- dist_windows
- build_examples
- build_raytrace
- build_nightly
- build_benchmarks
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions examples/wasm-audio-worklet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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",
]
17 changes: 17 additions & 0 deletions examples/wasm-audio-worklet/README.md
Original file line number Diff line number Diff line change
@@ -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!
21 changes: 21 additions & 0 deletions examples/wasm-audio-worklet/build.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions examples/wasm-audio-worklet/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>WASM audio worklet</title>
</head>
<body>
<script type="module">
import init, {web_main} from "./wasm_audio_worklet.js";
async function run() {
await init();
web_main();
}
run();
</script>
</body>
</html>
7 changes: 7 additions & 0 deletions examples/wasm-audio-worklet/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

set -ex

./build.sh

python3 server.py
12 changes: 12 additions & 0 deletions examples/wasm-audio-worklet/server.py
Original file line number Diff line number Diff line change
@@ -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)
45 changes: 45 additions & 0 deletions examples/wasm-audio-worklet/src/dependent_module.rs
Original file line number Diff line number Diff line change
@@ -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<String, JsValue> {
// 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))
};
}
39 changes: 39 additions & 0 deletions examples/wasm-audio-worklet/src/gui.rs
Original file line number Diff line number Diff line change
@@ -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::<dyn FnMut(_)>::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<HtmlInputElement, JsValue> {
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)
}
20 changes: 20 additions & 0 deletions examples/wasm-audio-worklet/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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(&params);
let ctx = wasm_audio(Box::new(move |buf| osc.process(buf)))
.await
.unwrap();
create_gui(params, ctx);
}
54 changes: 54 additions & 0 deletions examples/wasm-audio-worklet/src/oscillator.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
26 changes: 26 additions & 0 deletions examples/wasm-audio-worklet/src/polyfill.js
Original file line number Diff line number Diff line change
@@ -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() {
}
Loading