Skip to content
Open
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
16 changes: 9 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/target
/Cargo.lock
.cargo/
.DS_Store
recorded.wav
rls*.log
/.direnv
/target
/Cargo.lock
.cargo/
.DS_Store
recorded.wav
rls*.log
/.direnv
!/examples/web-audio-worklet-beep/.cargo/

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ asio = [
# Deprecated, the `oboe` backend has been removed
oboe-shared-stdcxx = []

# 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]
dasp_sample = "0.11"

Expand Down Expand Up @@ -99,6 +110,7 @@ web-sys = { version = "0.3.35", features = [

[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
wasm-bindgen = { version = "0.2.58", optional = true }
wasm-bindgen-futures = { version = "0.4.33", optional = true }
js-sys = { version = "0.3.35" }
web-sys = { version = "0.3.35", features = [
"AudioContext",
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ Some audio backends are optional and will only be compiled with a [feature flag]

- JACK (on Linux): `jack`
- ASIO (on Windows): `asio`
- AudioWorklet (on Web): `web_audio_worklet`

For AudioWorklet backend usage see the README for the `web-audio-worklet-beep` example.

## ASIO on Windows

Expand Down
5 changes: 5 additions & 0 deletions examples/web-audio-worklet-beep/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics"]

[unstable]
build-std = ["std", "panic_abort"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it have to abort? Could we leave that choice to the user?

Copy link
Contributor Author

@kettle11 kettle11 Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think right now when using multiple threads and atomics panic has to be set to abort. I can't remember the exact reason, but if I try to disable the flag there are cryptic build errors. Searching online doesn't turn up anything quickly. I think it's an area that's under documented right now because few people are using multithreaded WebAssembly with Rust.

For now I think leaving this as-is makes the most sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for looking into it.

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
41 changes: 41 additions & 0 deletions examples/web-audio-worklet-beep/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[package]
name = "web-audio-worklet-beep"
description = "cpal beep example for WebAssembly on an AudioWorklet"
version = "0.1.0"
edition = "2018"
authors = ["Ian Kettlewell <[email protected]>"]

[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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you're return a Result, could you remove the unwrap here and elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing being unwrapped is an Option and the function error type is a JsValue, so I think it's simplest to just unwrap here.

It matches the other example and the unwrap would only fail if they've modified the associated index.html in which case the unwrap error would be informative.

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 * std::f32::consts::PI / 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 jack;
pub(crate) mod null;
#[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;
62 changes: 62 additions & 0 deletions src/host/web_audio_worklet/dependent_module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// This file is based on code from:
// https://github.com/rustwasm/wasm-bindgen/blob/main/examples/wasm-audio-worklet/src/dependent_module.rs
//
// The original code is licensed under either of:
// - MIT license (https://opensource.org/licenses/MIT)
// - Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
// at your option.
//
// Copyright (c) 2017-2024 The wasm-bindgen Developers
//
// This file incorporates code from the above source under the Apache License, Version 2.0 license.
// Please see the original repository for more details.
//
// 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
Loading