-
Notifications
You must be signed in to change notification settings - Fork 397
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AudioWorklet based host for when atomics are enabled
- Loading branch information
Showing
12 changed files
with
710 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Cargo.lock | ||
/dist | ||
/target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
}; | ||
} |
Oops, something went wrong.