Skip to content

AudioWorklet based Host for web when Wasm atomics are enabled #958

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ 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]
dasp_sample = "0.11"
Expand Down Expand Up @@ -65,6 +76,7 @@ web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOption

[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", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] }

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
Loading