| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | //! Web Audio backend implementation. |
| 2 | //! |
| 3 | //! Default backend on WebAssembly. |
| 4 | |
| 5 | extern crate js_sys; |
| 6 | extern crate wasm_bindgen; |
| 7 | extern crate web_sys; |
| 8 | |
| 9 | use self::wasm_bindgen::prelude::*; |
| 10 | use self::wasm_bindgen::JsCast; |
| 11 | use self::web_sys::{AudioContext, AudioContextOptions}; |
| 12 | use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; |
| 13 | use crate::{ |
| 14 | BackendSpecificError, BufferSize, BuildStreamError, 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 | use std::ops::DerefMut; |
| 21 | use std::sync::{Arc, Mutex, RwLock}; |
| 22 | use std::time::Duration; |
| 23 | |
| 24 | /// Type alias for shared closure handles used in audio callbacks |
| 25 | type ClosureHandle = Arc<RwLock<Option<Closure<dyn FnMut()>>>>; |
| 26 | |
| 27 | /// Content is false if the iterator is empty. |
| 28 | pub struct Devices(bool); |
| 29 | |
| 30 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] |
| 31 | pub struct Device; |
| 32 | |
| 33 | pub struct Host; |
| 34 | |
| 35 | pub struct Stream { |
| 36 | ctx: Arc<AudioContext>, |
| 37 | on_ended_closures: Vec<ClosureHandle>, |
| 38 | config: StreamConfig, |
| 39 | buffer_size_frames: usize, |
| 40 | } |
| 41 | |
| 42 | // WASM runs in a single-threaded environment, so Send and Sync are safe by design. |
| 43 | unsafe impl Send for Stream {} |
| 44 | unsafe impl Sync for Stream {} |
| 45 | |
| 46 | // Compile-time assertion that Stream is Send and Sync |
| 47 | crate::assert_stream_send!(Stream); |
| 48 | crate::assert_stream_sync!(Stream); |
| 49 | |
| 50 | pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; |
| 51 | |
| 52 | const MIN_CHANNELS: u16 = 1; |
| 53 | const MAX_CHANNELS: u16 = 32; |
| 54 | const MIN_SAMPLE_RATE: SampleRate = 8_000; |
| 55 | const MAX_SAMPLE_RATE: SampleRate = 96_000; |
| 56 | const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; |
| 57 | const MIN_BUFFER_SIZE: u32 = 1; |
| 58 | const MAX_BUFFER_SIZE: u32 = u32::MAX; |
| 59 | const DEFAULT_BUFFER_SIZE: usize = 2048; |
| 60 | const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; |
| 61 | |
| 62 | impl Host { |
| 63 | pub fn new() -> Result<Self, crate::HostUnavailable> { |
| 64 | Ok(Host) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | impl HostTrait for Host { |
| 69 | type Devices = Devices; |
| 70 | type Device = Device; |
| 71 | |
| 72 | fn is_available() -> bool { |
| 73 | // Assume this host is always available on webaudio. |
| 74 | true |
| 75 | } |
| 76 | |
| 77 | fn devices(&self) -> Result<Self::Devices, DevicesError> { |
| 78 | Devices::new() |
| 79 | } |
| 80 | |
| 81 | fn default_input_device(&self) -> Option<Self::Device> { |
| 82 | default_input_device() |
| 83 | } |
| 84 | |
| 85 | fn default_output_device(&self) -> Option<Self::Device> { |
| 86 | default_output_device() |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | impl Devices { |
| 91 | fn new() -> Result<Self, DevicesError> { |
| 92 | Ok(Self::default()) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | impl Device { |
| 97 | fn description(&self) -> Result<DeviceDescription, DeviceNameError> { |
| 98 | Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) |
| 99 | .direction(crate::DeviceDirection::Output) |
| 100 | .build()) |
| 101 | } |
| 102 | |
| 103 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 104 | Ok(DeviceId( |
| 105 | crate::platform::HostId::WebAudio, |
| 106 | "default".to_string(), |
| 107 | )) |
| 108 | } |
| 109 | |
| 110 | fn supported_input_configs( |
| 111 | &self, |
| 112 | ) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> { |
| 113 | // TODO |
| 114 | Ok(Vec::new().into_iter()) |
| 115 | } |
| 116 | |
| 117 | fn supported_output_configs( |
| 118 | &self, |
| 119 | ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 120 | let buffer_size = SupportedBufferSize::Range { |
| 121 | min: MIN_BUFFER_SIZE, |
| 122 | max: MAX_BUFFER_SIZE, |
| 123 | }; |
| 124 | let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) |
| 125 | .map(|channels| SupportedStreamConfigRange { |
| 126 | channels, |
| 127 | min_sample_rate: MIN_SAMPLE_RATE, |
| 128 | max_sample_rate: MAX_SAMPLE_RATE, |
| 129 | buffer_size, |
| 130 | sample_format: SUPPORTED_SAMPLE_FORMAT, |
| 131 | }) |
| 132 | .collect(); |
| 133 | Ok(configs.into_iter()) |
| 134 | } |
| 135 | |
| 136 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 137 | // TODO |
| 138 | Err(DefaultStreamConfigError::StreamTypeNotSupported) |
| 139 | } |
| 140 | |
| 141 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 142 | const EXPECT: &str = "expected at least one valid webaudio stream config"; |
| 143 | let config = self |
| 144 | .supported_output_configs() |
| 145 | .expect(EXPECT) |
| 146 | .max_by(|a, b| a.cmp_default_heuristics(b)) |
| 147 | .unwrap() |
| 148 | .with_sample_rate(DEFAULT_SAMPLE_RATE); |
| 149 | |
| 150 | Ok(config) |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | impl DeviceTrait for Device { |
| 155 | type SupportedInputConfigs = SupportedInputConfigs; |
| 156 | type SupportedOutputConfigs = SupportedOutputConfigs; |
| 157 | type Stream = Stream; |
| 158 | |
| 159 | fn description(&self) -> Result<DeviceDescription, DeviceNameError> { |
| 160 | Device::description(self) |
| 161 | } |
| 162 | |
| 163 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 164 | Device::id(self) |
| 165 | } |
| 166 | |
| 167 | fn supported_input_configs( |
| 168 | &self, |
| 169 | ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> { |
| 170 | Device::supported_input_configs(self) |
| 171 | } |
| 172 | |
| 173 | fn supported_output_configs( |
| 174 | &self, |
| 175 | ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 176 | Device::supported_output_configs(self) |
| 177 | } |
| 178 | |
| 179 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 180 | Device::default_input_config(self) |
| 181 | } |
| 182 | |
| 183 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 184 | Device::default_output_config(self) |
| 185 | } |
| 186 | |
| 187 | fn build_input_stream_raw<D, E>( |
| 188 | &self, |
| 189 | _config: StreamConfig, |
| 190 | _sample_format: SampleFormat, |
| 191 | _data_callback: D, |
| 192 | _error_callback: E, |
| 193 | _timeout: Option<Duration>, |
| 194 | ) -> Result<Self::Stream, BuildStreamError> |
| 195 | where |
| 196 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, |
| 197 | E: FnMut(StreamError) + Send + 'static, |
| 198 | { |
| 199 | // TODO |
| 200 | Err(BuildStreamError::StreamConfigNotSupported) |
| 201 | } |
| 202 | |
| 203 | /// Create an output stream. |
| 204 | fn build_output_stream_raw<D, E>( |
| 205 | &self, |
| 206 | config: StreamConfig, |
| 207 | sample_format: SampleFormat, |
| 208 | data_callback: D, |
| 209 | _error_callback: E, |
| 210 | _timeout: Option<Duration>, |
| 211 | ) -> Result<Self::Stream, BuildStreamError> |
| 212 | where |
| 213 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 214 | E: FnMut(StreamError) + Send + 'static, |
| 215 | { |
| 216 | if !valid_config(config, sample_format) { |
| 217 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 218 | } |
| 219 | |
| 220 | let n_channels = config.channels as usize; |
| 221 | |
| 222 | let buffer_size_frames = match config.buffer_size { |
| 223 | BufferSize::Fixed(v) => { |
| 224 | if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { |
| 225 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 226 | } |
| 227 | v as usize |
| 228 | } |
| 229 | BufferSize::Default => DEFAULT_BUFFER_SIZE, |
| 230 | }; |
| 231 | let buffer_size_samples = buffer_size_frames * n_channels; |
| 232 | let buffer_time_step_secs = buffer_time_step_secs(buffer_size_frames, config.sample_rate); |
| 233 | |
| 234 | let data_callback = Arc::new(Mutex::new(Box::new(data_callback))); |
| 235 | |
| 236 | // Create the WebAudio stream. |
| 237 | let stream_opts = AudioContextOptions::new(); |
| 238 | stream_opts.set_sample_rate(config.sample_rate as f32); |
| 239 | let ctx = AudioContext::new_with_context_options(&stream_opts).map_err( |
| 240 | |err| -> BuildStreamError { |
| 241 | let description = format!("{:?}", err); |
| 242 | let err = BackendSpecificError { description }; |
| 243 | err.into() |
| 244 | }, |
| 245 | )?; |
| 246 | |
| 247 | let destination = ctx.destination(); |
| 248 | |
| 249 | // If possible, set the destination's channel_count to the given config.channel. |
| 250 | // If not, fallback on the default destination channel_count to keep previous behavior |
| 251 | // and do not return an error. |
| 252 | if config.channels as u32 <= destination.max_channel_count() { |
| 253 | destination.set_channel_count(config.channels as u32); |
| 254 | } |
| 255 | |
| 256 | // SAFETY: WASM is single-threaded, so Arc is safe even though AudioContext is not Send/Sync |
| 257 | #[allow(clippy::arc_with_non_send_sync)] |
| 258 | let ctx = Arc::new(ctx); |
| 259 | |
| 260 | // A container for managing the lifecycle of the audio callbacks. |
| 261 | let mut on_ended_closures: Vec<ClosureHandle> = Vec::new(); |
| 262 | |
| 263 | // A cursor keeping track of the current time at which new frames should be scheduled. |
| 264 | let time = Arc::new(RwLock::new(0f64)); |
| 265 | |
| 266 | // baseLatency is fixed for the lifetime of the AudioContext. |
| 267 | let base_latency_secs = js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("baseLatency")) |
| 268 | .ok() |
| 269 | .and_then(|v| v.as_f64()) |
| 270 | .unwrap_or(0.0); |
| 271 | |
| 272 | // Create a set of closures / callbacks which will continuously fetch and schedule sample |
| 273 | // playback. Starting with two workers, e.g. a front and back buffer so that audio frames |
| 274 | // can be fetched in the background. |
| 275 | for _i in 0..2 { |
| 276 | let data_callback_handle = data_callback.clone(); |
| 277 | let ctx_handle = ctx.clone(); |
| 278 | let time_handle = time.clone(); |
| 279 | |
| 280 | // A set of temporary buffers to be used for intermediate sample transformation steps. |
| 281 | let mut temporary_buffer = vec![0f32; buffer_size_samples]; |
| 282 | let mut temporary_channel_buffer = vec![0f32; buffer_size_frames]; |
| 283 | |
| 284 | #[cfg(target_feature = "atomics")] |
| 285 | let temporary_channel_array_view: js_sys::Float32Array; |
| 286 | #[cfg(target_feature = "atomics")] |
| 287 | { |
| 288 | let temporary_channel_array = js_sys::ArrayBuffer::new( |
| 289 | (std::mem::size_of::<f32>() * buffer_size_frames) as u32, |
| 290 | ); |
| 291 | temporary_channel_array_view = js_sys::Float32Array::new(&temporary_channel_array); |
| 292 | } |
| 293 | |
| 294 | // Create a webaudio buffer which will be reused to avoid allocations. |
| 295 | let ctx_buffer = ctx |
| 296 | .create_buffer( |
| 297 | config.channels as u32, |
| 298 | buffer_size_frames as u32, |
| 299 | config.sample_rate as f32, |
| 300 | ) |
| 301 | .map_err(|err| -> BuildStreamError { |
| 302 | let description = format!("{:?}", err); |
| 303 | let err = BackendSpecificError { description }; |
| 304 | err.into() |
| 305 | })?; |
| 306 | |
| 307 | // A self reference to this closure for passing to future audio event calls. |
| 308 | // SAFETY: WASM is single-threaded, so Arc is safe even though Closure is not Send/Sync |
| 309 | #[allow(clippy::arc_with_non_send_sync)] |
| 310 | let on_ended_closure: ClosureHandle = Arc::new(RwLock::new(None)); |
| 311 | let on_ended_closure_handle = on_ended_closure.clone(); |
| 312 | |
| 313 | on_ended_closure |
| 314 | .write() |
| 315 | .unwrap() |
| 316 | .replace(Closure::wrap(Box::new(move || { |
| 317 | let now = ctx_handle.current_time(); |
| 318 | let time_at_start_of_buffer = { |
| 319 | let time_at_start_of_buffer = time_handle |
| 320 | .read() |
| 321 | .expect("Unable to get a read lock on the time cursor"); |
| 322 | // Synchronise first buffer as necessary (eg. keep the time value |
| 323 | // referenced to the context clock). |
| 324 | if *time_at_start_of_buffer > 0.0 { |
| 325 | *time_at_start_of_buffer |
| 326 | } else { |
| 327 | // Schedule the first buffer far enough ahead for the browser's |
| 328 | // internal audio pipeline (baseLatency) plus one full buffer of |
| 329 | // data, so playback starts underrun-free at any buffer size. |
| 330 | now + base_latency_secs + buffer_time_step_secs |
| 331 | } |
| 332 | }; |
| 333 | |
| 334 | // Populate the sample data into an interleaved temporary buffer. |
| 335 | { |
| 336 | let len = temporary_buffer.len(); |
| 337 | let data = temporary_buffer.as_mut_ptr() as *mut (); |
| 338 | let mut data = unsafe { Data::from_parts(data, len, sample_format) }; |
| 339 | let mut data_callback = data_callback_handle.lock().unwrap(); |
| 340 | // outputLatency can change at runtime, so read it each callback. |
| 341 | let output_latency_secs = js_sys::Reflect::get( |
| 342 | ctx_handle.as_ref(), |
| 343 | &JsValue::from("outputLatency"), |
| 344 | ) |
| 345 | .ok() |
| 346 | .and_then(|v| v.as_f64()) |
| 347 | .unwrap_or(0.0); |
| 348 | let total_hw_latency_secs = { |
| 349 | let sum = base_latency_secs + output_latency_secs; |
| 350 | if sum.is_finite() { |
| 351 | sum.max(0.0) |
| 352 | } else { |
| 353 | 0.0 |
| 354 | } |
| 355 | }; |
| 356 | let callback = crate::StreamInstant::from_secs_f64(now); |
| 357 | let playback = crate::StreamInstant::from_secs_f64( |
| 358 | time_at_start_of_buffer + total_hw_latency_secs, |
| 359 | ); |
| 360 | let timestamp = crate::OutputStreamTimestamp { callback, playback }; |
| 361 | let info = OutputCallbackInfo { timestamp }; |
| 362 | (data_callback.deref_mut())(&mut data, &info); |
| 363 | } |
| 364 | |
| 365 | // Deinterleave the sample data and copy into the audio context buffer. |
| 366 | // We do not reference the audio context buffer directly e.g. getChannelData. |
| 367 | // As wasm-bindgen only gives us a copy, not a direct reference. |
| 368 | for channel in 0..n_channels { |
| 369 | for i in 0..buffer_size_frames { |
| 370 | temporary_channel_buffer[i] = |
| 371 | temporary_buffer[n_channels * i + channel]; |
| 372 | } |
| 373 | |
| 374 | #[cfg(not(target_feature = "atomics"))] |
| 375 | { |
| 376 | ctx_buffer |
| 377 | .copy_to_channel(&temporary_channel_buffer, channel as i32) |
| 378 | .expect( |
| 379 | "Unable to write sample data into the audio context buffer", |
| 380 | ); |
| 381 | } |
| 382 | |
| 383 | // copyToChannel cannot be directly copied into from a SharedArrayBuffer, |
| 384 | // which WASM memory is backed by if the 'atomics' flag is enabled. |
| 385 | // This workaround copies the data into an intermediary buffer first. |
| 386 | // There's a chance browsers may eventually relax that requirement. |
| 387 | // See this issue: https://github.com/WebAudio/web-audio-api/issues/2565 |
| 388 | #[cfg(target_feature = "atomics")] |
| 389 | { |
| 390 | temporary_channel_array_view.copy_from(&temporary_channel_buffer); |
| 391 | ctx_buffer |
| 392 | .unchecked_ref::<ExternalArrayAudioBuffer>() |
| 393 | .copy_to_channel(&temporary_channel_array_view, channel as i32) |
| 394 | .expect( |
| 395 | "Unable to write sample data into the audio context buffer", |
| 396 | ); |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | // Create an AudioBufferSourceNode, schedule it to playback the reused buffer |
| 401 | // in the future. |
| 402 | let source = ctx_handle |
| 403 | .create_buffer_source() |
| 404 | .expect("Unable to create a webaudio buffer source"); |
| 405 | source.set_buffer(Some(&ctx_buffer)); |
| 406 | source |
| 407 | .connect_with_audio_node(&ctx_handle.destination()) |
| 408 | .expect( |
| 409 | "Unable to connect the web audio buffer source to the context destination", |
| 410 | ); |
| 411 | source |
| 412 | .add_event_listener_with_callback( |
| 413 | "ended", |
| 414 | on_ended_closure_handle |
| 415 | .read() |
| 416 | .unwrap() |
| 417 | .as_ref() |
| 418 | .unwrap() |
| 419 | .as_ref() |
| 420 | .unchecked_ref(), |
| 421 | ) |
| 422 | .expect("Failed to add ended event listener"); |
| 423 | |
| 424 | source |
| 425 | .start_with_when(time_at_start_of_buffer) |
| 426 | .expect("Unable to start the webaudio buffer source"); |
| 427 | |
| 428 | // Keep track of when the next buffer worth of samples should be played. |
| 429 | *time_handle.write().unwrap() = time_at_start_of_buffer + buffer_time_step_secs; |
| 430 | }) as Box<dyn FnMut()>)); |
| 431 | |
| 432 | on_ended_closures.push(on_ended_closure); |
| 433 | } |
| 434 | |
| 435 | Ok(Stream { |
| 436 | ctx, |
| 437 | on_ended_closures, |
| 438 | config, |
| 439 | buffer_size_frames, |
| 440 | }) |
| 441 | } |
| 442 | } |
| 443 | |
| 444 | impl Stream { |
| 445 | /// Return the [`AudioContext`](https://developer.mozilla.org/docs/Web/API/AudioContext) used |
| 446 | /// by this stream. |
| 447 | pub fn audio_context(&self) -> &AudioContext { |
| 448 | &self.ctx |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | impl StreamTrait for Stream { |
| 453 | fn play(&self) -> Result<(), PlayStreamError> { |
| 454 | let window = web_sys::window().unwrap(); |
| 455 | match self.ctx.resume() { |
| 456 | Ok(_) => { |
| 457 | // Begin webaudio playback, initially scheduling the closures to fire on a timeout |
| 458 | // event. |
| 459 | let mut offset_ms = 10; |
| 460 | let time_step_secs = |
| 461 | buffer_time_step_secs(self.buffer_size_frames, self.config.sample_rate); |
| 462 | let time_step_ms = (time_step_secs * 1_000.0) as i32; |
| 463 | for on_ended_closure in self.on_ended_closures.iter() { |
| 464 | window |
| 465 | .set_timeout_with_callback_and_timeout_and_arguments_0( |
| 466 | on_ended_closure |
| 467 | .read() |
| 468 | .unwrap() |
| 469 | .as_ref() |
| 470 | .unwrap() |
| 471 | .as_ref() |
| 472 | .unchecked_ref(), |
| 473 | offset_ms, |
| 474 | ) |
| 475 | .unwrap(); |
| 476 | offset_ms += time_step_ms; |
| 477 | } |
| 478 | Ok(()) |
| 479 | } |
| 480 | Err(err) => { |
| 481 | let description = format!("{:?}", err); |
| 482 | let err = BackendSpecificError { description }; |
| 483 | Err(err.into()) |
| 484 | } |
| 485 | } |
| 486 | } |
| 487 | |
| 488 | fn pause(&self) -> Result<(), PauseStreamError> { |
| 489 | match self.ctx.suspend() { |
| 490 | Ok(_) => Ok(()), |
| 491 | Err(err) => { |
| 492 | let description = format!("{:?}", err); |
| 493 | let err = BackendSpecificError { description }; |
| 494 | Err(err.into()) |
| 495 | } |
| 496 | } |
| 497 | } |
| 498 | |
| 499 | fn buffer_size(&self) -> Option<crate::FrameCount> { |
| 500 | Some(self.buffer_size_frames as crate::FrameCount) |
| 501 | } |
| 502 | } |
| 503 | |
| 504 | impl Drop for Stream { |
| 505 | fn drop(&mut self) { |
| 506 | let _ = self.ctx.close(); |
| 507 | } |
| 508 | } |
| 509 | |
| 510 | impl Default for Devices { |
| 511 | fn default() -> Devices { |
| 512 | // We produce an empty iterator if the WebAudio API isn't available. |
| 513 | Devices(is_webaudio_available()) |
| 514 | } |
| 515 | } |
| 516 | |
| 517 | impl Iterator for Devices { |
| 518 | type Item = Device; |
| 519 | |
| 520 | #[inline] |
| 521 | fn next(&mut self) -> Option<Device> { |
| 522 | if self.0 { |
| 523 | self.0 = false; |
| 524 | Some(Device) |
| 525 | } else { |
| 526 | None |
| 527 | } |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | fn default_input_device() -> Option<Device> { |
| 532 | // TODO |
| 533 | None |
| 534 | } |
| 535 | |
| 536 | fn default_output_device() -> Option<Device> { |
| 537 | if is_webaudio_available() { |
| 538 | Some(Device) |
| 539 | } else { |
| 540 | None |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | // Detects whether the `AudioContext` global variable is available. |
| 545 | fn is_webaudio_available() -> bool { |
| 546 | js_sys::Reflect::get(&js_sys::global(), &JsValue::from("AudioContext")) |
| 547 | .unwrap() |
| 548 | .is_truthy() |
| 549 | } |
| 550 | |
| 551 | // Whether or not the given stream configuration is valid for building a stream. |
| 552 | fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool { |
| 553 | conf.channels <= MAX_CHANNELS |
| 554 | && conf.channels >= MIN_CHANNELS |
| 555 | && conf.sample_rate <= MAX_SAMPLE_RATE |
| 556 | && conf.sample_rate >= MIN_SAMPLE_RATE |
| 557 | && sample_format == SUPPORTED_SAMPLE_FORMAT |
| 558 | } |
| 559 | |
| 560 | fn buffer_time_step_secs(buffer_size_frames: usize, sample_rate: SampleRate) -> f64 { |
| 561 | buffer_size_frames as f64 / sample_rate as f64 |
| 562 | } |
| 563 | |
| 564 | #[cfg(target_feature = "atomics")] |
| 565 | #[wasm_bindgen] |
| 566 | extern "C" { |
| 567 | #[wasm_bindgen(js_name = AudioBuffer)] |
| 568 | type ExternalArrayAudioBuffer; |
| 569 | |
| 570 | # [wasm_bindgen(catch, method, structural, js_class = "AudioBuffer", js_name = copyToChannel)] |
| 571 | pub fn copy_to_channel( |
| 572 | this: &ExternalArrayAudioBuffer, |
| 573 | source: &js_sys::Float32Array, |
| 574 | channel_number: i32, |
| 575 | ) -> Result<(), JsValue>; |
| 576 | } |