From 3f728362bce211217c0c4022d81cb9a13c9bd192 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Tue, 13 Feb 2024 17:50:14 -0500 Subject: [PATCH 01/12] Fix crash on Web / Wasm when 'atomics' flag is enabled --- src/host/webaudio/mod.rs | 52 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 8c7efd777..dc9c0d9bf 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -257,6 +257,16 @@ impl DeviceTrait for Device { let mut temporary_buffer = vec![0f32; buffer_size_samples]; let mut temporary_channel_buffer = vec![0f32; buffer_size_frames]; + #[cfg(target_feature = "atomics")] + let temporary_channel_array_view: js_sys::Float32Array; + #[cfg(target_feature = "atomics")] + { + let temporary_channel_array = js_sys::ArrayBuffer::new( + (std::mem::size_of::() * buffer_size_frames) as u32, + ); + temporary_channel_array_view = js_sys::Float32Array::new(&temporary_channel_array); + } + // Create a webaudio buffer which will be reused to avoid allocations. let ctx_buffer = ctx .create_buffer( @@ -316,9 +326,31 @@ impl DeviceTrait for Device { temporary_channel_buffer[i] = temporary_buffer[n_channels * i + channel]; } - ctx_buffer - .copy_to_channel(&mut temporary_channel_buffer, channel as i32) - .expect("Unable to write sample data into the audio context buffer"); + + #[cfg(not(target_feature = "atomics"))] + { + ctx_buffer + .copy_to_channel(&mut temporary_channel_buffer, channel as i32) + .expect( + "Unable to write sample data into the audio context buffer", + ); + } + + // copyToChannel cannot be directly copied into from a SharedArrayBuffer, + // which WASM memory is backed by if the 'atomics' flag is enabled. + // This workaround copies the data into an intermediary buffer first. + // There's a chance browsers may eventually relax that requirement. + // See this issue: https://github.com/WebAudio/web-audio-api/issues/2565 + #[cfg(target_feature = "atomics")] + { + temporary_channel_array_view.copy_from(&mut temporary_channel_buffer); + ctx_buffer + .unchecked_ref::() + .copy_to_channel(&temporary_channel_array_view, channel as i32) + .expect( + "Unable to write sample data into the audio context buffer", + ); + } } // Create an AudioBufferSourceNode, schedule it to playback the reused buffer @@ -480,3 +512,17 @@ fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool { fn buffer_time_step_secs(buffer_size_frames: usize, sample_rate: SampleRate) -> f64 { buffer_size_frames as f64 / sample_rate.0 as f64 } + +#[cfg(target_feature = "atomics")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = AudioBuffer)] + type ExternalArrayAudioBuffer; + + # [wasm_bindgen(catch, method, structural, js_class = "AudioBuffer", js_name = copyToChannel)] + pub fn copy_to_channel( + this: &ExternalArrayAudioBuffer, + source: &js_sys::Float32Array, + channel_number: i32, + ) -> Result<(), JsValue>; +} From 58ee0edd8d133358bfe4b55c0c2048ab3b70c3d6 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Sat, 1 Mar 2025 14:07:58 -0500 Subject: [PATCH 02/12] AudioWorklet based host for when atomics are enabled --- Cargo.toml | 12 + examples/web-audio-worklet-beep/.gitignore | 3 + examples/web-audio-worklet-beep/Cargo.toml | 40 ++ examples/web-audio-worklet-beep/README.md | 39 ++ examples/web-audio-worklet-beep/Trunk.toml | 9 + examples/web-audio-worklet-beep/index.html | 14 + examples/web-audio-worklet-beep/src/lib.rs | 114 ++++++ src/host/mod.rs | 7 + .../web_audio_worklet/dependent_module.rs | 50 +++ src/host/web_audio_worklet/mod.rs | 383 ++++++++++++++++++ src/host/web_audio_worklet/worklet.js | 30 ++ src/platform/mod.rs | 10 +- 12 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 examples/web-audio-worklet-beep/.gitignore create mode 100644 examples/web-audio-worklet-beep/Cargo.toml create mode 100644 examples/web-audio-worklet-beep/README.md create mode 100644 examples/web-audio-worklet-beep/Trunk.toml create mode 100644 examples/web-audio-worklet-beep/index.html create mode 100644 examples/web-audio-worklet-beep/src/lib.rs create mode 100644 src/host/web_audio_worklet/dependent_module.rs create mode 100644 src/host/web_audio_worklet/mod.rs create mode 100644 src/host/web_audio_worklet/worklet.js diff --git a/Cargo.toml b/Cargo.toml index ecf4cf002..52432757c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/examples/web-audio-worklet-beep/.gitignore b/examples/web-audio-worklet-beep/.gitignore new file mode 100644 index 000000000..25545b464 --- /dev/null +++ b/examples/web-audio-worklet-beep/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +/dist +/target diff --git a/examples/web-audio-worklet-beep/Cargo.toml b/examples/web-audio-worklet-beep/Cargo.toml new file mode 100644 index 000000000..783902777 --- /dev/null +++ b/examples/web-audio-worklet-beep/Cargo.toml @@ -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"] diff --git a/examples/web-audio-worklet-beep/README.md b/examples/web-audio-worklet-beep/README.md new file mode 100644 index 000000000..7eacb6ee0 --- /dev/null +++ b/examples/web-audio-worklet-beep/README.md @@ -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. diff --git a/examples/web-audio-worklet-beep/Trunk.toml b/examples/web-audio-worklet-beep/Trunk.toml new file mode 100644 index 000000000..bb27e6853 --- /dev/null +++ b/examples/web-audio-worklet-beep/Trunk.toml @@ -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" diff --git a/examples/web-audio-worklet-beep/index.html b/examples/web-audio-worklet-beep/index.html new file mode 100644 index 000000000..6a748f05b --- /dev/null +++ b/examples/web-audio-worklet-beep/index.html @@ -0,0 +1,14 @@ + + + + + + cpal AudioWorklet beep example + + + + + + + + \ No newline at end of file diff --git a/examples/web-audio-worklet-beep/src/lib.rs b/examples/web-audio-worklet-beep/src/lib.rs new file mode 100644 index 000000000..36fd2e63d --- /dev/null +++ b/examples/web-audio-worklet-beep/src/lib.rs @@ -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::::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::::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::(&device, &config.into()), + cpal::SampleFormat::I16 => run::(&device, &config.into()), + cpal::SampleFormat::U16 => run::(&device, &config.into()), + _ => panic!("unsupported sample format"), + } +} + +fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Stream +where + T: cpal::Sample + cpal::SizedSample + cpal::FromSample, +{ + 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(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) +where + T: cpal::Sample + cpal::FromSample, +{ + for frame in output.chunks_mut(channels) { + let sample = next_sample(); + let value = T::from_sample::(sample); + for sample in frame.iter_mut() { + *sample = value; + } + } +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 8de06cbe0..7be297573 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -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; diff --git a/src/host/web_audio_worklet/dependent_module.rs b/src/host/web_audio_worklet/dependent_module.rs new file mode 100644 index 000000000..8fd14d58d --- /dev/null +++ b/src/host/web_audio_worklet/dependent_module.rs @@ -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 { + // 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)) + }; +} diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs new file mode 100644 index 000000000..2e029f7a9 --- /dev/null +++ b/src/host/web_audio_worklet/mod.rs @@ -0,0 +1,383 @@ +mod dependent_module; +use js_sys::wasm_bindgen; + +use crate::dependent_module; +use wasm_bindgen::prelude::*; + +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; +use std::time::Duration; + +/// Content is false if the iterator is empty. +pub struct Devices(bool); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Device; + +pub struct Host; + +pub struct Stream { + audio_context: web_sys::AudioContext, +} + +pub type SupportedInputConfigs = ::std::vec::IntoIter; +pub type SupportedOutputConfigs = ::std::vec::IntoIter; + +const MIN_CHANNELS: u16 = 1; +const MAX_CHANNELS: u16 = 32; +const MIN_SAMPLE_RATE: SampleRate = SampleRate(8_000); +const MAX_SAMPLE_RATE: SampleRate = SampleRate(96_000); +const DEFAULT_SAMPLE_RATE: SampleRate = SampleRate(44_100); +const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + if let Ok(audio_context_is_defined) = js_sys::eval("typeof AudioWorklet !== 'undefined'") { + audio_context_is_defined.as_bool().unwrap() + } else { + false + } + } + + fn devices(&self) -> Result { + Devices::new() + } + + fn default_input_device(&self) -> Option { + // TODO + None + } + + fn default_output_device(&self) -> Option { + Some(Device) + } +} + +impl Devices { + fn new() -> Result { + Ok(Self::default()) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + #[inline] + fn name(&self) -> Result { + Ok("Default Device".to_owned()) + } + + #[inline] + fn supported_input_configs( + &self, + ) -> Result { + // TODO + Ok(Vec::new().into_iter()) + } + + #[inline] + fn supported_output_configs( + &self, + ) -> Result { + let buffer_size = SupportedBufferSize::Unknown; + + // In actuality the number of supported channels cannot be fully known until + // the browser attempts to initialized the AudioWorklet. + + let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: MIN_SAMPLE_RATE, + max_sample_rate: MAX_SAMPLE_RATE, + buffer_size: buffer_size.clone(), + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + .collect(); + Ok(configs.into_iter()) + } + + #[inline] + fn default_input_config(&self) -> Result { + // TODO + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + + #[inline] + fn default_output_config(&self) -> Result { + const EXPECT: &str = "expected at least one valid webaudio stream config"; + let config = self + .supported_output_configs() + .expect(EXPECT) + .max_by(|a, b| a.cmp_default_heuristics(b)) + .unwrap() + .with_sample_rate(DEFAULT_SAMPLE_RATE); + + Ok(config) + } + + fn build_input_stream_raw( + &self, + _config: &StreamConfig, + _sample_format: SampleFormat, + _data_callback: D, + _error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + // TODO + Err(BuildStreamError::StreamConfigNotSupported) + } + + /// Create an output stream. + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + mut error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + if !valid_config(config, sample_format) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + let config = config.clone(); + + let stream_opts = web_sys::AudioContextOptions::new(); + stream_opts.set_sample_rate(config.sample_rate.0 as f32); + + let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( + |err| -> BuildStreamError { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + err.into() + }, + )?; + + let destination = audio_context.destination(); + + // If possible, set the destination's channel_count to the given config.channel. + // If not, fallback on the default destination channel_count to keep previous behavior + // and do not return an error. + if config.channels as u32 <= destination.max_channel_count() { + destination.set_channel_count(config.channels as u32); + } + + let ctx = audio_context.clone(); + wasm_bindgen_futures::spawn_local(async move { + let result: Result<(), JsValue> = (async move || { + let mod_url = dependent_module!("worklet.js")?; + wasm_bindgen_futures::JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?) + .await?; + + let options = web_sys::AudioWorkletNodeOptions::new(); + + let js_array = js_sys::Array::new(); + js_array.push(&JsValue::from_f64(destination.channel_count() as _)); + + options.set_output_channel_count(&js_array); + options.set_number_of_inputs(0); + + options.set_processor_options(Some(&js_sys::Array::of3( + &wasm_bindgen::module(), + &wasm_bindgen::memory(), + &WasmAudioProcessor::new(Box::new( + move |interleaved_data, frame_size, sample_rate, now| { + let data = interleaved_data.as_mut_ptr() as *mut (); + let mut data = unsafe { + Data::from_parts(data, interleaved_data.len(), sample_format) + }; + + let callback = crate::StreamInstant::from_secs_f64(now); + + let buffer_duration = + frames_to_duration(frame_size as _, SampleRate(sample_rate as u32)); + let playback = callback.add(buffer_duration).expect( + "`playback` occurs beyond representation supported by `StreamInstant`", + ); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + (data_callback)(&mut data, &info); + }, + )) + .pack() + .into(), + ))); + // This name 'CpalProcessor' must match the name registered in worklet.js + let audio_worklet_node = + web_sys::AudioWorkletNode::new_with_options(&ctx, "CpalProcessor", &options)?; + + audio_worklet_node.connect_with_audio_node(&destination)?; + Ok(()) + })() + .await; + + if let Err(e) = result { + let description = if let Some(string_value) = e.as_string() { + string_value + } else { + format!("Browser error initializing stream: {:?}", e) + }; + + error_callback(StreamError::BackendSpecific { + err: BackendSpecificError { description }, + }) + } + }); + + Ok(Stream { audio_context }) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + match self.audio_context.resume() { + Ok(_) => Ok(()), + Err(err) => { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } + + fn pause(&self) -> Result<(), PauseStreamError> { + match self.audio_context.suspend() { + Ok(_) => Ok(()), + Err(err) => { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } +} + +impl Drop for Stream { + fn drop(&mut self) { + let _ = self.audio_context.close(); + } +} + +impl Default for Devices { + fn default() -> Devices { + Devices(true) + } +} + +impl Iterator for Devices { + type Item = Device; + #[inline] + fn next(&mut self) -> Option { + if self.0 { + self.0 = false; + Some(Device) + } else { + None + } + } +} + +// Whether or not the given stream configuration is valid for building a stream. +fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool { + conf.channels <= MAX_CHANNELS + && conf.channels >= MIN_CHANNELS + && conf.sample_rate <= MAX_SAMPLE_RATE + && conf.sample_rate >= MIN_SAMPLE_RATE + && sample_format == SUPPORTED_SAMPLE_FORMAT +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate.0 as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +/// WasmAudioProcessor provides an interface for the Javascript code +/// running in the AudioWorklet to interact with Rust. +#[wasm_bindgen] +pub struct WasmAudioProcessor { + #[wasm_bindgen(skip)] + interleaved_buffer: Vec, + #[wasm_bindgen(skip)] + // Passes in an interleaved scratch buffer, frame size, sample rate, and current time. + callback: Box, +} + +impl WasmAudioProcessor { + pub fn new(callback: Box) -> Self { + Self { + interleaved_buffer: Vec::new(), + callback, + } + } +} + +#[wasm_bindgen] +impl WasmAudioProcessor { + pub fn process( + &mut self, + channels: u32, + frame_size: u32, + sample_rate: u32, + current_time: f64, + ) -> u32 { + let frame_size = frame_size as usize; + + // Ensure there's enough space in the output buffer + // This likely only occurs once, or very few times. + let interleaved_buffer_size = channels as usize * frame_size; + self.interleaved_buffer.resize( + interleaved_buffer_size.max(self.interleaved_buffer.len()), + 0.0, + ); + + (self.callback)( + &mut self.interleaved_buffer[..interleaved_buffer_size], + frame_size as u32, + sample_rate, + current_time, + ); + + // Returns a pointer to the raw interleaved buffer to Javascript so + // it can deinterleave it into the output buffers. + // + // Deinterleaving is done on the Javascript side because it's simpler and it may be faster. + // Doing it this way avoids an extra copy and the JS deinterleaving code + // is likely heavily optimized by the browser's JS engine, + // although I have not tested that assumption. + self.interleaved_buffer.as_mut_ptr() as _ + } + + pub fn pack(self) -> usize { + Box::into_raw(Box::new(self)) as usize + } + pub unsafe fn unpack(val: usize) -> Self { + *Box::from_raw(val as *mut _) + } +} diff --git a/src/host/web_audio_worklet/worklet.js b/src/host/web_audio_worklet/worklet.js new file mode 100644 index 000000000..234707a8a --- /dev/null +++ b/src/host/web_audio_worklet/worklet.js @@ -0,0 +1,30 @@ +registerProcessor("CpalProcessor", class WasmProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + let [module, memory, handle] = options.processorOptions; + bindgen.initSync({ module, memory }); + this.processor = bindgen.WasmAudioProcessor.unpack(handle); + this.wasm_memory = new Float32Array(memory.buffer); + } + process(inputs, outputs) { + const channels = outputs[0]; + const channels_count = channels.length; + const frame_size = channels[0].length; + + const interleaved_ptr = this.processor.process(channels_count, frame_size, sampleRate, currentTime); + + const FLOAT32_SIZE_BYTES = 4; + const interleaved_start = interleaved_ptr / FLOAT32_SIZE_BYTES; + const interleaved = this.wasm_memory.subarray(interleaved_start, interleaved_start + channels_count * frame_size); + + for (let ch = 0; ch < channels_count; ch++) { + const channel = channels[ch]; + + for (let i = 0, j = ch; i < frame_size; i++, j += channels_count) { + channel[i] = interleaved[j]; + } + } + + return true; + } +}); \ No newline at end of file diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 65d77ca40..18f4e0894 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -662,7 +662,15 @@ mod platform_impl { SupportedOutputConfigs as WebAudioSupportedOutputConfigs, }; - impl_platform_host!(WebAudio webaudio "WebAudio"); + #[cfg(feature = "web_audio_worklet")] + pub use crate::host::webaudio::{ + Device as WebAudioWorkletDevice, Devices as WebAudioWorkletDevices, + Host as WebAudioWorkletHost, Stream as WebAudioWorkletStream, + SupportedInputConfigs as WebAudioWorkletSupportedInputConfigs, + SupportedOutputConfigs as WebAudioWorkletSupportedOutputConfigs, + }; + + impl_platform_host!(#[cfg(feature = "web_audio_worklet")] WebAudioWorklet web_audio_worklet "WebAudioWorklet", WebAudio webaudio "WebAudio"); /// The default host for the current compilation target platform. pub fn default_host() -> Host { From 04695027d286777e8e1ecf176c8de0f6087a0ef5 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Sun, 2 Mar 2025 20:48:04 -0500 Subject: [PATCH 03/12] Adjust cargo.toml to avoid adding unnecessary dependencies on non-Wasm platforms --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 52432757c..1a3afe079 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ web_audio_worklet = [ ] [dependencies] -wasm-bindgen-futures = {version = "0.4.33", optional = true} dasp_sample = "0.11" [dev-dependencies] @@ -77,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"] } From 314c9e4b29689711bd5bdc7011962e00fea984bf Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Thu, 7 Aug 2025 00:18:27 -0400 Subject: [PATCH 04/12] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/host/web_audio_worklet/dependent_module.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/host/web_audio_worklet/dependent_module.rs b/src/host/web_audio_worklet/dependent_module.rs index 8fd14d58d..ceaca3e38 100644 --- a/src/host/web_audio_worklet/dependent_module.rs +++ b/src/host/web_audio_worklet/dependent_module.rs @@ -1,4 +1,16 @@ -// This file is taken from here: https://github.com/rustwasm/wasm-bindgen/blob/main/examples/wasm-audio-worklet/src/dependent_module.rs +// 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 terms of those licenses. +// 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}; From 991d86ed21f67f1e4e6d917e2378328b758c833f Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Thu, 7 Aug 2025 00:19:00 -0400 Subject: [PATCH 05/12] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/host/web_audio_worklet/mod.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index 2e029f7a9..b360e6136 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -374,6 +374,24 @@ impl WasmAudioProcessor { self.interleaved_buffer.as_mut_ptr() as _ } + /// Converts this `WasmAudioProcessor` into a raw pointer (as `usize`) for FFI use. + /// + /// # Purpose + /// This function is intended to transfer ownership of the processor instance to the caller, + /// typically for passing between Rust and JavaScript via WebAssembly. + /// + /// # Relationship with [`unpack`] + /// The returned pointer must be passed to [`unpack`] exactly once to recover the original + /// `WasmAudioProcessor` instance. Failing to do so will result in a memory leak. Calling + /// [`unpack`] more than once or using the pointer after it has been unpacked will result in + /// undefined behavior. + /// + /// # Safety and Lifetime + /// After calling `pack`, the caller is responsible for ensuring that `unpack` is called + /// exactly once, and that the pointer is not used after being unpacked. This function + /// should be used with care, as improper use can lead to memory safety issues. + /// + /// [`unpack`]: Self::unpack pub fn pack(self) -> usize { Box::into_raw(Box::new(self)) as usize } From 60733aba450931b9b1d6346e19daf2f203417868 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Thu, 7 Aug 2025 00:19:21 -0400 Subject: [PATCH 06/12] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/host/web_audio_worklet/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index b360e6136..b5826717d 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -395,6 +395,11 @@ impl WasmAudioProcessor { pub fn pack(self) -> usize { Box::into_raw(Box::new(self)) as usize } + /// # Safety + /// + /// The `val` parameter must be a value previously returned by `Self::pack`. + /// It must not have already been unpacked or deallocated, and must not be used after this call. + /// Using an invalid or already-consumed pointer will result in undefined behavior. pub unsafe fn unpack(val: usize) -> Self { *Box::from_raw(val as *mut _) } From 3d7e31fe08ef6961221b10b310090e164d4fa34e Mon Sep 17 00:00:00 2001 From: Ian Kettlewell Date: Thu, 7 Aug 2025 00:33:27 -0400 Subject: [PATCH 07/12] Adapted copilot recommendation to remove eval --- src/host/web_audio_worklet/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index b5826717d..fe931b39a 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -46,11 +46,15 @@ impl HostTrait for Host { type Device = Device; fn is_available() -> bool { - if let Ok(audio_context_is_defined) = js_sys::eval("typeof AudioWorklet !== 'undefined'") { - audio_context_is_defined.as_bool().unwrap() - } else { - false + if let Some(window) = web_sys::window() { + // Check if 'AudioWorklet' exists on the window object + if let Ok(has_audio_worklet) = + js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")) + { + return has_audio_worklet; + } } + false } fn devices(&self) -> Result { From 57f28e1579fd5716081715a015c2d7691566b545 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell <4565191+kettle11@users.noreply.github.com> Date: Thu, 7 Aug 2025 01:14:30 -0400 Subject: [PATCH 08/12] Exempt from .gitignore .cargo/config.toml necessary for AudioWorklet example --- .gitignore | 14 ++++++++------ examples/web-audio-worklet-beep/.cargo/config.toml | 5 +++++ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 examples/web-audio-worklet-beep/.cargo/config.toml diff --git a/.gitignore b/.gitignore index 0289afe13..161f7ce94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -/target -/Cargo.lock -.cargo/ -.DS_Store -recorded.wav -rls*.log +/target +/Cargo.lock +.cargo/ +.DS_Store +recorded.wav +rls*.log + +!/examples/web-audio-worklet-beep/.cargo/ diff --git a/examples/web-audio-worklet-beep/.cargo/config.toml b/examples/web-audio-worklet-beep/.cargo/config.toml new file mode 100644 index 000000000..456c37896 --- /dev/null +++ b/examples/web-audio-worklet-beep/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.wasm32-unknown-unknown] +rustflags = ["-C", "target-feature=+atomics"] + +[unstable] +build-std = ["std", "panic_abort"] From 190048f95021e4b95c198675c96dd6ec7ecabe72 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell <4565191+kettle11@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:08:42 -0400 Subject: [PATCH 09/12] Incorporate code review feedback: * Update README to reference AudioWorklet usage * Attribute authorship of AudioWorklet example * Remove unwraps in example code * Choose Apache 2.0 license for copied code * Correctly report if the AudioWorklet host is available * Fix incorrect module reference --- README.md | 3 +++ examples/web-audio-worklet-beep/Cargo.toml | 1 + examples/web-audio-worklet-beep/src/lib.rs | 6 ++--- .../web_audio_worklet/dependent_module.rs | 2 +- src/host/web_audio_worklet/mod.rs | 25 +++++++++++++------ src/platform/mod.rs | 2 +- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b5ac67097..6cf4316b5 100644 --- a/README.md +++ b/README.md @@ -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. Oboe can either use a shared or static runtime. The static runtime is used by default, but activating the `oboe-shared-stdcxx` feature makes it use the shared runtime, which requires `libc++_shared.so` from the Android NDK to diff --git a/examples/web-audio-worklet-beep/Cargo.toml b/examples/web-audio-worklet-beep/Cargo.toml index 783902777..a4660131e 100644 --- a/examples/web-audio-worklet-beep/Cargo.toml +++ b/examples/web-audio-worklet-beep/Cargo.toml @@ -3,6 +3,7 @@ name = "web-audio-worklet-beep" description = "cpal beep example for WebAssembly on an AudioWorklet" version = "0.1.0" edition = "2018" +authors = ["Ian Kettlewell "] [lib] crate-type = ["cdylib"] diff --git a/examples/web-audio-worklet-beep/src/lib.rs b/examples/web-audio-worklet-beep/src/lib.rs index 36fd2e63d..8237fd99b 100644 --- a/examples/web-audio-worklet-beep/src/lib.rs +++ b/examples/web-audio-worklet-beep/src/lib.rs @@ -24,8 +24,8 @@ pub fn main_js() -> Result<(), JsValue> { 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(); + let play_button = document.get_element_by_id("play")?; + let stop_button = document.get_element_by_id("stop")?; // stream needs to be referenced from the "play" and "stop" closures let stream = Rc::new(Cell::new(None)); @@ -83,7 +83,7 @@ where 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() + (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()); diff --git a/src/host/web_audio_worklet/dependent_module.rs b/src/host/web_audio_worklet/dependent_module.rs index ceaca3e38..42bb6ebf4 100644 --- a/src/host/web_audio_worklet/dependent_module.rs +++ b/src/host/web_audio_worklet/dependent_module.rs @@ -8,7 +8,7 @@ // // Copyright (c) 2017-2024 The wasm-bindgen Developers // -// This file incorporates code from the above source under the terms of those licenses. +// 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 diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index fe931b39a..1ed358f2b 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -37,7 +37,11 @@ const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; impl Host { pub fn new() -> Result { - Ok(Host) + if Self::is_available() { + Ok(Host) + } else { + Err(crate::HostUnavailable) + } } } @@ -47,14 +51,19 @@ impl HostTrait for Host { fn is_available() -> bool { if let Some(window) = web_sys::window() { - // Check if 'AudioWorklet' exists on the window object - if let Ok(has_audio_worklet) = - js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")) - { - return has_audio_worklet; - } + let has_audio_worklet = + js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")).unwrap_or(false); + + let cross_origin_isolated = + js_sys::Reflect::get(&window, &JsValue::from_str("crossOriginIsolated")) + .ok() + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + has_audio_worklet && cross_origin_isolated + } else { + false } - false } fn devices(&self) -> Result { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 18f4e0894..a118669a0 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -663,7 +663,7 @@ mod platform_impl { }; #[cfg(feature = "web_audio_worklet")] - pub use crate::host::webaudio::{ + pub use crate::host::web_audio_worklet::{ Device as WebAudioWorkletDevice, Devices as WebAudioWorkletDevices, Host as WebAudioWorkletHost, Stream as WebAudioWorkletStream, SupportedInputConfigs as WebAudioWorkletSupportedInputConfigs, From c3118ffaa8126c2beda50c79b614670c277fe7f8 Mon Sep 17 00:00:00 2001 From: Ian Kettlewell <4565191+kettle11@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:13:37 -0400 Subject: [PATCH 10/12] Add spaces to Cargo.toml formatting --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1a3afe079..26772fc93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +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} +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"] } From 3aa5968f16b4f74496bacea73d09f039a074085c Mon Sep 17 00:00:00 2001 From: Ian Kettlewell <4565191+kettle11@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:37:39 -0400 Subject: [PATCH 11/12] Fix a few clippy suggestions and switch format! to preferred version --- src/host/web_audio_worklet/mod.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index 1ed358f2b..8c2d484ad 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -118,7 +118,7 @@ impl DeviceTrait for Device { channels, min_sample_rate: MIN_SAMPLE_RATE, max_sample_rate: MAX_SAMPLE_RATE, - buffer_size: buffer_size.clone(), + buffer_size, sample_format: SUPPORTED_SAMPLE_FORMAT, }) .collect(); @@ -184,7 +184,7 @@ impl DeviceTrait for Device { let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( |err| -> BuildStreamError { - let description = format!("{:?}", err); + let description = format!("{err:?}", err); let err = BackendSpecificError { description }; err.into() }, @@ -227,7 +227,7 @@ impl DeviceTrait for Device { let callback = crate::StreamInstant::from_secs_f64(now); let buffer_duration = - frames_to_duration(frame_size as _, SampleRate(sample_rate as u32)); + frames_to_duration(frame_size as _, SampleRate(sample_rate)); let playback = callback.add(buffer_duration).expect( "`playback` occurs beyond representation supported by `StreamInstant`", ); @@ -248,11 +248,11 @@ impl DeviceTrait for Device { })() .await; - if let Err(e) = result { - let description = if let Some(string_value) = e.as_string() { + if let Err(err) = result { + let description = if let Some(string_value) = err.as_string() { string_value } else { - format!("Browser error initializing stream: {:?}", e) + format!("Browser error initializing stream: {err:?}") }; error_callback(StreamError::BackendSpecific { @@ -270,7 +270,7 @@ impl StreamTrait for Stream { match self.audio_context.resume() { Ok(_) => Ok(()), Err(err) => { - let description = format!("{:?}", err); + let description = format!("{err:?}"); let err = BackendSpecificError { description }; Err(err.into()) } @@ -281,7 +281,7 @@ impl StreamTrait for Stream { match self.audio_context.suspend() { Ok(_) => Ok(()), Err(err) => { - let description = format!("{:?}", err); + let description = format!("{err:?}"); let err = BackendSpecificError { description }; Err(err.into()) } @@ -331,6 +331,8 @@ fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Dura std::time::Duration::new(secs, nanos) } +type AudioProcessorCallback = Box; + /// WasmAudioProcessor provides an interface for the Javascript code /// running in the AudioWorklet to interact with Rust. #[wasm_bindgen] @@ -339,11 +341,11 @@ pub struct WasmAudioProcessor { interleaved_buffer: Vec, #[wasm_bindgen(skip)] // Passes in an interleaved scratch buffer, frame size, sample rate, and current time. - callback: Box, + callback: AudioProcessorCallback, } impl WasmAudioProcessor { - pub fn new(callback: Box) -> Self { + pub fn new(callback: AudioProcessorCallback) -> Self { Self { interleaved_buffer: Vec::new(), callback, From 73afd827f76f0252ddaea1622e27c2477fb7410c Mon Sep 17 00:00:00 2001 From: Ian Kettlewell <4565191+kettle11@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:46:24 -0400 Subject: [PATCH 12/12] * Switch u16 to use cpal::ChannelCount * Correct 2 errors introduced in previous commit --- examples/web-audio-worklet-beep/src/lib.rs | 4 ++-- src/host/web_audio_worklet/mod.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/web-audio-worklet-beep/src/lib.rs b/examples/web-audio-worklet-beep/src/lib.rs index 8237fd99b..dcb998da9 100644 --- a/examples/web-audio-worklet-beep/src/lib.rs +++ b/examples/web-audio-worklet-beep/src/lib.rs @@ -24,8 +24,8 @@ pub fn main_js() -> Result<(), JsValue> { console_error_panic_hook::set_once(); let document = gloo::utils::document(); - let play_button = document.get_element_by_id("play")?; - let stop_button = document.get_element_by_id("stop")?; + 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)); diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index 8c2d484ad..789c7f7ec 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -6,9 +6,9 @@ use wasm_bindgen::prelude::*; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + BackendSpecificError, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use std::time::Duration; @@ -28,8 +28,8 @@ pub struct Stream { pub type SupportedInputConfigs = ::std::vec::IntoIter; pub type SupportedOutputConfigs = ::std::vec::IntoIter; -const MIN_CHANNELS: u16 = 1; -const MAX_CHANNELS: u16 = 32; +const MIN_CHANNELS: ChannelCount = 1; +const MAX_CHANNELS: ChannelCount = 32; const MIN_SAMPLE_RATE: SampleRate = SampleRate(8_000); const MAX_SAMPLE_RATE: SampleRate = SampleRate(96_000); const DEFAULT_SAMPLE_RATE: SampleRate = SampleRate(44_100); @@ -184,7 +184,7 @@ impl DeviceTrait for Device { let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( |err| -> BuildStreamError { - let description = format!("{err:?}", err); + let description = format!("{err:?}"); let err = BackendSpecificError { description }; err.into() },