| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | //! Audio Worklet backend implementation. |
| 2 | //! |
| 3 | //! Available on WebAssembly with the `audioworklet` feature. Requires atomics support. |
| 4 | //! See the `audioworklet-beep` example for setup instructions. |
| 5 | |
| 6 | mod dependent_module; |
| 7 | use js_sys::wasm_bindgen; |
| 8 | |
| 9 | use crate::dependent_module; |
| 10 | use wasm_bindgen::prelude::*; |
| 11 | |
| 12 | use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; |
| 13 | use crate::{ |
| 14 | BackendSpecificError, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, |
| 15 | DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, |
| 16 | DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, |
| 17 | SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, |
| 18 | SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, |
| 19 | }; |
| 20 | |
| 21 | use std::time::Duration; |
| 22 | |
| 23 | /// Content is false if the iterator is empty. |
| 24 | pub struct Devices(bool); |
| 25 | |
| 26 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] |
| 27 | pub struct Device; |
| 28 | |
| 29 | pub struct Host; |
| 30 | |
| 31 | pub struct Stream { |
| 32 | audio_context: web_sys::AudioContext, |
| 33 | } |
| 34 | |
| 35 | pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; |
| 36 | |
| 37 | const MIN_CHANNELS: ChannelCount = 1; |
| 38 | const MAX_CHANNELS: ChannelCount = 32; |
| 39 | const MIN_SAMPLE_RATE: SampleRate = 8_000; |
| 40 | const MAX_SAMPLE_RATE: SampleRate = 96_000; |
| 41 | const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; |
| 42 | const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; |
| 43 | |
| 44 | impl Host { |
| 45 | pub fn new() -> Result<Self, crate::HostUnavailable> { |
| 46 | if Self::is_available() { |
| 47 | Ok(Host) |
| 48 | } else { |
| 49 | Err(crate::HostUnavailable) |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | impl HostTrait for Host { |
| 55 | type Devices = Devices; |
| 56 | type Device = Device; |
| 57 | |
| 58 | fn is_available() -> bool { |
| 59 | if let Some(window) = web_sys::window() { |
| 60 | let has_audio_worklet = |
| 61 | js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")).unwrap_or(false); |
| 62 | |
| 63 | let cross_origin_isolated = |
| 64 | js_sys::Reflect::get(&window, &JsValue::from_str("crossOriginIsolated")) |
| 65 | .ok() |
| 66 | .and_then(|v| v.as_bool()) |
| 67 | .unwrap_or(false); |
| 68 | |
| 69 | has_audio_worklet && cross_origin_isolated |
| 70 | } else { |
| 71 | false |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | fn devices(&self) -> Result<Self::Devices, DevicesError> { |
| 76 | Devices::new() |
| 77 | } |
| 78 | |
| 79 | fn default_input_device(&self) -> Option<Self::Device> { |
| 80 | // TODO |
| 81 | None |
| 82 | } |
| 83 | |
| 84 | fn default_output_device(&self) -> Option<Self::Device> { |
| 85 | Some(Device) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | impl Devices { |
| 90 | fn new() -> Result<Self, DevicesError> { |
| 91 | Ok(Self::default()) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | impl DeviceTrait for Device { |
| 96 | type SupportedInputConfigs = SupportedInputConfigs; |
| 97 | type SupportedOutputConfigs = SupportedOutputConfigs; |
| 98 | type Stream = Stream; |
| 99 | |
| 100 | #[inline] |
| 101 | fn description(&self) -> Result<DeviceDescription, DeviceNameError> { |
| 102 | Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) |
| 103 | .direction(crate::DeviceDirection::Output) |
| 104 | .build()) |
| 105 | } |
| 106 | |
| 107 | #[inline] |
| 108 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 109 | Ok(DeviceId( |
| 110 | crate::platform::HostId::AudioWorklet, |
| 111 | "default".to_string(), |
| 112 | )) |
| 113 | } |
| 114 | |
| 115 | #[inline] |
| 116 | fn supported_input_configs( |
| 117 | &self, |
| 118 | ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> { |
| 119 | // TODO |
| 120 | Ok(Vec::new().into_iter()) |
| 121 | } |
| 122 | |
| 123 | #[inline] |
| 124 | fn supported_output_configs( |
| 125 | &self, |
| 126 | ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 127 | let buffer_size = SupportedBufferSize::Unknown; |
| 128 | |
| 129 | // In actuality the number of supported channels cannot be fully known until |
| 130 | // the browser attempts to initialized the AudioWorklet. |
| 131 | |
| 132 | let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) |
| 133 | .map(|channels| SupportedStreamConfigRange { |
| 134 | channels, |
| 135 | min_sample_rate: MIN_SAMPLE_RATE, |
| 136 | max_sample_rate: MAX_SAMPLE_RATE, |
| 137 | buffer_size, |
| 138 | sample_format: SUPPORTED_SAMPLE_FORMAT, |
| 139 | }) |
| 140 | .collect(); |
| 141 | Ok(configs.into_iter()) |
| 142 | } |
| 143 | |
| 144 | #[inline] |
| 145 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 146 | // TODO |
| 147 | Err(DefaultStreamConfigError::StreamTypeNotSupported) |
| 148 | } |
| 149 | |
| 150 | #[inline] |
| 151 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 152 | const EXPECT: &str = "expected at least one valid webaudio stream config"; |
| 153 | let config = self |
| 154 | .supported_output_configs() |
| 155 | .expect(EXPECT) |
| 156 | .max_by(|a, b| a.cmp_default_heuristics(b)) |
| 157 | .unwrap() |
| 158 | .with_sample_rate(DEFAULT_SAMPLE_RATE); |
| 159 | |
| 160 | Ok(config) |
| 161 | } |
| 162 | |
| 163 | fn build_input_stream_raw<D, E>( |
| 164 | &self, |
| 165 | _config: StreamConfig, |
| 166 | _sample_format: SampleFormat, |
| 167 | _data_callback: D, |
| 168 | _error_callback: E, |
| 169 | _timeout: Option<Duration>, |
| 170 | ) -> Result<Self::Stream, BuildStreamError> |
| 171 | where |
| 172 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, |
| 173 | E: FnMut(StreamError) + Send + 'static, |
| 174 | { |
| 175 | // TODO |
| 176 | Err(BuildStreamError::StreamConfigNotSupported) |
| 177 | } |
| 178 | |
| 179 | /// Create an output stream. |
| 180 | fn build_output_stream_raw<D, E>( |
| 181 | &self, |
| 182 | config: StreamConfig, |
| 183 | sample_format: SampleFormat, |
| 184 | mut data_callback: D, |
| 185 | mut error_callback: E, |
| 186 | _timeout: Option<Duration>, |
| 187 | ) -> Result<Self::Stream, BuildStreamError> |
| 188 | where |
| 189 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 190 | E: FnMut(StreamError) + Send + 'static, |
| 191 | { |
| 192 | if !valid_config(config, sample_format) { |
| 193 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 194 | } |
| 195 | |
| 196 | let stream_opts = web_sys::AudioContextOptions::new(); |
| 197 | stream_opts.set_sample_rate(config.sample_rate as f32); |
| 198 | |
| 199 | let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( |
| 200 | |err| -> BuildStreamError { |
| 201 | let description = format!("{err:?}"); |
| 202 | let err = BackendSpecificError { description }; |
| 203 | err.into() |
| 204 | }, |
| 205 | )?; |
| 206 | |
| 207 | let destination = audio_context.destination(); |
| 208 | |
| 209 | // If possible, set the destination's channel_count to the given config.channel. |
| 210 | // If not, fallback on the default destination channel_count to keep previous behavior |
| 211 | // and do not return an error. |
| 212 | if config.channels as u32 <= destination.max_channel_count() { |
| 213 | destination.set_channel_count(config.channels as u32); |
| 214 | } |
| 215 | |
| 216 | let ctx = audio_context.clone(); |
| 217 | wasm_bindgen_futures::spawn_local(async move { |
| 218 | let result: Result<(), JsValue> = async move { |
| 219 | let mod_url = dependent_module!("worklet.js")?; |
| 220 | wasm_bindgen_futures::JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?) |
| 221 | .await?; |
| 222 | |
| 223 | let options = web_sys::AudioWorkletNodeOptions::new(); |
| 224 | |
| 225 | let js_array = js_sys::Array::new(); |
| 226 | js_array.push(&JsValue::from_f64(destination.channel_count() as _)); |
| 227 | |
| 228 | options.set_output_channel_count(&js_array); |
| 229 | options.set_number_of_inputs(0); |
| 230 | |
| 231 | // Capture audio output latency here: the closure runs in a separate worker and cannot access AudioContext properties directly. |
| 232 | // While baseLatency is fixed for the context lifetime, outputLatency can change but not be re-read from inside the worklet; |
| 233 | // we snapshot it here. |
| 234 | let base_latency_secs = |
| 235 | js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("baseLatency")) |
| 236 | .ok() |
| 237 | .and_then(|v| v.as_f64()) |
| 238 | .unwrap_or(0.0); |
| 239 | let output_latency_secs = |
| 240 | js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("outputLatency")) |
| 241 | .ok() |
| 242 | .and_then(|v| v.as_f64()) |
| 243 | .unwrap_or(0.0); |
| 244 | let total_output_latency_secs = { |
| 245 | let sum = base_latency_secs + output_latency_secs; |
| 246 | if sum.is_finite() { sum.max(0.0) } else { 0.0 } |
| 247 | }; |
| 248 | |
| 249 | options.set_processor_options(Some(&js_sys::Array::of3( |
| 250 | &wasm_bindgen::module(), |
| 251 | &wasm_bindgen::memory(), |
| 252 | &WasmAudioProcessor::new(Box::new( |
| 253 | move |interleaved_data, frame_size, sample_rate, now| { |
| 254 | let data = interleaved_data.as_mut_ptr() as *mut (); |
| 255 | let mut data = unsafe { |
| 256 | Data::from_parts(data, interleaved_data.len(), sample_format) |
| 257 | }; |
| 258 | |
| 259 | let callback = crate::StreamInstant::from_secs_f64(now); |
| 260 | let buffer_duration = frames_to_duration(frame_size as _, sample_rate); |
| 261 | let playback = callback |
| 262 | .add(buffer_duration + Duration::from_secs_f64(total_output_latency_secs)) |
| 263 | .expect( |
| 264 | "`playback` occurs beyond representation supported by `StreamInstant`", |
| 265 | ); |
| 266 | let timestamp = crate::OutputStreamTimestamp { callback, playback }; |
| 267 | let info = OutputCallbackInfo { timestamp }; |
| 268 | (data_callback)(&mut data, &info); |
| 269 | }, |
| 270 | )) |
| 271 | .pack() |
| 272 | .into(), |
| 273 | ))); |
| 274 | // This name 'CpalProcessor' must match the name registered in worklet.js |
| 275 | let audio_worklet_node = |
| 276 | web_sys::AudioWorkletNode::new_with_options(&ctx, "CpalProcessor", &options)?; |
| 277 | |
| 278 | audio_worklet_node.connect_with_audio_node(&destination)?; |
| 279 | Ok(()) |
| 280 | } |
| 281 | .await; |
| 282 | |
| 283 | if let Err(err) = result { |
| 284 | let description = if let Some(string_value) = err.as_string() { |
| 285 | string_value |
| 286 | } else { |
| 287 | format!("Browser error initializing stream: {err:?}") |
| 288 | }; |
| 289 | |
| 290 | error_callback(StreamError::BackendSpecific { |
| 291 | err: BackendSpecificError { description }, |
| 292 | }) |
| 293 | } |
| 294 | }); |
| 295 | |
| 296 | Ok(Stream { audio_context }) |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | impl StreamTrait for Stream { |
| 301 | fn play(&self) -> Result<(), PlayStreamError> { |
| 302 | match self.audio_context.resume() { |
| 303 | Ok(_) => Ok(()), |
| 304 | Err(err) => { |
| 305 | let description = format!("{err:?}"); |
| 306 | let err = BackendSpecificError { description }; |
| 307 | Err(err.into()) |
| 308 | } |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | fn pause(&self) -> Result<(), PauseStreamError> { |
| 313 | match self.audio_context.suspend() { |
| 314 | Ok(_) => Ok(()), |
| 315 | Err(err) => { |
| 316 | let description = format!("{err:?}"); |
| 317 | let err = BackendSpecificError { description }; |
| 318 | Err(err.into()) |
| 319 | } |
| 320 | } |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | impl Drop for Stream { |
| 325 | fn drop(&mut self) { |
| 326 | let _ = self.audio_context.close(); |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | impl Default for Devices { |
| 331 | fn default() -> Devices { |
| 332 | Devices(true) |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | impl Iterator for Devices { |
| 337 | type Item = Device; |
| 338 | #[inline] |
| 339 | fn next(&mut self) -> Option<Device> { |
| 340 | if self.0 { |
| 341 | self.0 = false; |
| 342 | Some(Device) |
| 343 | } else { |
| 344 | None |
| 345 | } |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | // Whether or not the given stream configuration is valid for building a stream. |
| 350 | fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool { |
| 351 | conf.channels <= MAX_CHANNELS |
| 352 | && conf.channels >= MIN_CHANNELS |
| 353 | && conf.sample_rate <= MAX_SAMPLE_RATE |
| 354 | && conf.sample_rate >= MIN_SAMPLE_RATE |
| 355 | && sample_format == SUPPORTED_SAMPLE_FORMAT |
| 356 | } |
| 357 | |
| 358 | // Convert the given duration in frames at the given sample rate to a `std::time::Duration`. |
| 359 | fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { |
| 360 | let secsf = frames as f64 / rate as f64; |
| 361 | let secs = secsf as u64; |
| 362 | let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; |
| 363 | std::time::Duration::new(secs, nanos) |
| 364 | } |
| 365 | |
| 366 | type AudioProcessorCallback = Box<dyn FnMut(&mut [f32], u32, u32, f64)>; |
| 367 | |
| 368 | /// WasmAudioProcessor provides an interface for the Javascript code |
| 369 | /// running in the AudioWorklet to interact with Rust. |
| 370 | #[wasm_bindgen] |
| 371 | pub struct WasmAudioProcessor { |
| 372 | #[wasm_bindgen(skip)] |
| 373 | interleaved_buffer: Vec<f32>, |
| 374 | #[wasm_bindgen(skip)] |
| 375 | // Passes in an interleaved scratch buffer, frame size, sample rate, and current time. |
| 376 | callback: AudioProcessorCallback, |
| 377 | } |
| 378 | |
| 379 | impl WasmAudioProcessor { |
| 380 | pub fn new(callback: AudioProcessorCallback) -> Self { |
| 381 | Self { |
| 382 | interleaved_buffer: Vec::new(), |
| 383 | callback, |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | #[wasm_bindgen] |
| 389 | impl WasmAudioProcessor { |
| 390 | pub fn process( |
| 391 | &mut self, |
| 392 | channels: u32, |
| 393 | frame_size: u32, |
| 394 | sample_rate: u32, |
| 395 | current_time: f64, |
| 396 | ) -> u32 { |
| 397 | let frame_size = frame_size as usize; |
| 398 | |
| 399 | // Ensure there's enough space in the output buffer |
| 400 | // This likely only occurs once, or very few times. |
| 401 | let interleaved_buffer_size = channels as usize * frame_size; |
| 402 | self.interleaved_buffer.resize( |
| 403 | interleaved_buffer_size.max(self.interleaved_buffer.len()), |
| 404 | 0.0, |
| 405 | ); |
| 406 | |
| 407 | (self.callback)( |
| 408 | &mut self.interleaved_buffer[..interleaved_buffer_size], |
| 409 | frame_size as u32, |
| 410 | sample_rate, |
| 411 | current_time, |
| 412 | ); |
| 413 | |
| 414 | // Returns a pointer to the raw interleaved buffer to Javascript so |
| 415 | // it can deinterleave it into the output buffers. |
| 416 | // |
| 417 | // Deinterleaving is done on the Javascript side because it's simpler and it may be faster. |
| 418 | // Doing it this way avoids an extra copy and the JS deinterleaving code |
| 419 | // is likely heavily optimized by the browser's JS engine, |
| 420 | // although I have not tested that assumption. |
| 421 | self.interleaved_buffer.as_mut_ptr() as _ |
| 422 | } |
| 423 | |
| 424 | /// Converts this `WasmAudioProcessor` into a raw pointer (as `usize`) for FFI use. |
| 425 | /// |
| 426 | /// # Purpose |
| 427 | /// This function is intended to transfer ownership of the processor instance to the caller, |
| 428 | /// typically for passing between Rust and JavaScript via WebAssembly. |
| 429 | /// |
| 430 | /// # Relationship with [`unpack`] |
| 431 | /// The returned pointer must be passed to [`unpack`] exactly once to recover the original |
| 432 | /// `WasmAudioProcessor` instance. Failing to do so will result in a memory leak. Calling |
| 433 | /// [`unpack`] more than once or using the pointer after it has been unpacked will result in |
| 434 | /// undefined behavior. |
| 435 | /// |
| 436 | /// # Safety and Lifetime |
| 437 | /// After calling `pack`, the caller is responsible for ensuring that `unpack` is called |
| 438 | /// exactly once, and that the pointer is not used after being unpacked. This function |
| 439 | /// should be used with care, as improper use can lead to memory safety issues. |
| 440 | /// |
| 441 | /// [`unpack`]: Self::unpack |
| 442 | pub fn pack(self) -> usize { |
| 443 | Box::into_raw(Box::new(self)) as usize |
| 444 | } |
| 445 | /// # Safety |
| 446 | /// |
| 447 | /// The `val` parameter must be a value previously returned by `Self::pack`. |
| 448 | /// It must not have already been unpacked or deallocated, and must not be used after this call. |
| 449 | /// Using an invalid or already-consumed pointer will result in undefined behavior. |
| 450 | pub unsafe fn unpack(val: usize) -> Self { |
| 451 | *Box::from_raw(val as *mut _) |
| 452 | } |
| 453 | } |