Skip to content

Commit

Permalink
AudioWorklet based host for when atomics are enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
kettle11 committed Mar 1, 2025
1 parent 33b8919 commit 58ee0ed
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 1 deletion.
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ edition = "2021"
rust-version = "1.70"

[features]
default = []
asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions.
oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does.
# Only available on web when atomics are enabled. See README for what it does.
web_audio_worklet = [
"wasm-bindgen-futures",
"web-sys/Blob",
"web-sys/BlobPropertyBag",
"web-sys/Url",
"web-sys/AudioWorklet",
"web-sys/AudioWorkletNode",
"web-sys/AudioWorkletNodeOptions",
]

[dependencies]
wasm-bindgen-futures = {version = "0.4.33", optional = true}
dasp_sample = "0.11"

[dev-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions examples/web-audio-worklet-beep/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Cargo.lock
/dist
/target
40 changes: 40 additions & 0 deletions examples/web-audio-worklet-beep/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "web-audio-worklet-beep"
description = "cpal beep example for WebAssembly on an AudioWorklet"
version = "0.1.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[profile.release]
# This makes the compiled code faster and smaller, but it makes compiling slower,
# so it's only enabled in release mode.
lto = true

[features]
# If you uncomment this line, it will enable `wee_alloc`:
#default = ["wee_alloc"]

[dependencies]
cpal = { path = "../..", features = ["wasm-bindgen", "web_audio_worklet"] }
# `gloo` is a utility crate which improves ergonomics over direct `web-sys` usage.
gloo = "0.11.0"
# The `wasm-bindgen` crate provides the bare minimum functionality needed
# to interact with JavaScript.
wasm-bindgen = "0.2.45"

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. However, it is slower than the default
# allocator, so it's not enabled by default.
wee_alloc = { version = "0.4.2", optional = true }

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`.
console_error_panic_hook = "0.1.5"

# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
version = "0.3.22"
features = ["console", "MouseEvent"]
39 changes: 39 additions & 0 deletions examples/web-audio-worklet-beep/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## How to install

This example requires a nightly version of Rust to enable WebAssembly atomics and to recompile the standard library with atomics enabled.

Note the flags set to configure that in .cargo/config.toml.

This allows Rust to used shared memory and have the audio thread directly read / write to shared memory like a native platform.

To use shared memory the browser requires a specific 'CORS' configuration on the server-side.

Note the flags set to configure that in Trunk.toml.

[trunk](https://trunkrs.dev/) is used to build and serve the example.

```sh
cargo install --locked trunk
# -- or --
cargo binstall trunk
```

## How to run in debug mode

```sh
# Builds the project and opens it in a new browser tab. Auto-reloads when the project changes.
trunk serve --open
```

## How to build in release mode

```sh
# Builds the project in release mode and places it into the `dist` folder.
trunk build --release
```

## What does each file do?

* `Cargo.toml` contains the standard Rust metadata. You put your Rust dependencies in here. You must change this file with your details (name, description, version, authors, categories)

* The `src` folder contains your Rust code.
9 changes: 9 additions & 0 deletions examples/web-audio-worklet-beep/Trunk.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[build]
target = "index.html"
dist = "dist"

[serve.headers]
# see ./assets/_headers for more documentation
"cross-origin-embedder-policy" = "require-corp"
"cross-origin-opener-policy" = "same-origin"
"cross-origin-resource-policy" = "same-site"
14 changes: 14 additions & 0 deletions examples/web-audio-worklet-beep/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>cpal AudioWorklet beep example</title>
</head>

<body>
<input id="play" type="button" value="beep" />
<input id="stop" type="button" value="stop" />
</body>

</html>
114 changes: 114 additions & 0 deletions examples/web-audio-worklet-beep/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use std::{cell::Cell, rc::Rc};

use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Stream,
};
use wasm_bindgen::prelude::*;
use web_sys::console;

// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
// This provides better error messages in debug mode.
// It's disabled in release mode, so it doesn't bloat up the file size.
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();

let document = gloo::utils::document();
let play_button = document.get_element_by_id("play").unwrap();
let stop_button = document.get_element_by_id("stop").unwrap();

// stream needs to be referenced from the "play" and "stop" closures
let stream = Rc::new(Cell::new(None));

// set up play button
{
let stream = stream.clone();
let closure = Closure::<dyn FnMut(_)>::new(move |_event: web_sys::MouseEvent| {
stream.set(Some(beep()));
});
play_button
.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
closure.forget();
}

// set up stop button
{
let closure = Closure::<dyn FnMut(_)>::new(move |_event: web_sys::MouseEvent| {
// stop the stream by dropping it
stream.take();
});
stop_button
.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
closure.forget();
}

Ok(())
}

fn beep() -> Stream {
let host = cpal::host_from_id(cpal::HostId::WebAudioWorklet)
.expect("WebAudioWorklet host not available");

let device = host
.default_output_device()
.expect("failed to find a default output device");
let config = device.default_output_config().unwrap();

match config.sample_format() {
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()),
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()),
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()),
_ => panic!("unsupported sample format"),
}
}

fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Stream
where
T: cpal::Sample + cpal::SizedSample + cpal::FromSample<f32>,
{
let sample_rate = config.sample_rate.0 as f32;
let channels = config.channels as usize;

// Produce a sinusoid of maximum amplitude.
let mut sample_clock = 0f32;
let mut next_value = move || {
sample_clock = (sample_clock + 1.0) % sample_rate;
(sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin()
};

let err_fn = |err| console::error_1(&format!("an error occurred on stream: {}", err).into());

let stream = device
.build_output_stream(
config,
move |data: &mut [T], _| write_data(data, channels, &mut next_value),
err_fn,
None,
)
.unwrap();
stream.play().unwrap();
stream
}

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
where
T: cpal::Sample + cpal::FromSample<f32>,
{
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let value = T::from_sample::<f32>(sample);
for sample in frame.iter_mut() {
*sample = value;
}
}
}
7 changes: 7 additions & 0 deletions src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,12 @@ pub(crate) mod null;
pub(crate) mod oboe;
#[cfg(windows)]
pub(crate) mod wasapi;
#[cfg(all(
target_arch = "wasm32",
feature = "wasm-bindgen",
feature = "web_audio_worklet",
target_feature = "atomics"
))]
pub(crate) mod web_audio_worklet;
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
pub(crate) mod webaudio;
50 changes: 50 additions & 0 deletions src/host/web_audio_worklet/dependent_module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// This file is taken from here: https://github.com/rustwasm/wasm-bindgen/blob/main/examples/wasm-audio-worklet/src/dependent_module.rs
// See this issue for a further explanation of what this file does: https://github.com/rustwasm/wasm-bindgen/issues/3019

use js_sys::{wasm_bindgen, 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(thread_local_v2, 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.with(ImportMeta::url),
);

let options = BlobPropertyBag::new();
options.set_type("text/javascript");
Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options(
&Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)),
&options,
)?)
}

// 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::host::web_audio_worklet::dependent_module::on_the_fly(include_str!($file_name))
};
}
Loading

0 comments on commit 58ee0ed

Please sign in to comment.