-
Notifications
You must be signed in to change notification settings - Fork 443
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
base: master
Are you sure you want to change the base?
Changes from all commits
f0019b3
9cdadb2
b23061b
2787c91
4154e13
9567de7
7ca10a2
fa5e2e1
801b360
703e9d0
1a03027
1d8014b
ae645f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/ | ||
|
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"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, thanks for looking into it. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Cargo.lock | ||
/dist | ||
/target |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
[package] | ||
name = "web-audio-worklet-beep" | ||
kettle11 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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"] |
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. |
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" |
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> |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you're return a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} |
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)) | ||
}; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.