| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | //! Emscripten backend implementation. |
| 2 | //! |
| 3 | //! Default backend on Emscripten. |
| 4 | |
| 5 | use js_sys::Float32Array; |
| 6 | use std::panic::AssertUnwindSafe; |
| 7 | use std::time::Duration; |
| 8 | use wasm_bindgen::prelude::*; |
| 9 | use wasm_bindgen::JsCast; |
| 10 | use wasm_bindgen_futures::{spawn_local, JsFuture}; |
| 11 | use web_sys::AudioContext; |
| 12 | |
| 13 | use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; |
| 14 | use crate::{ |
| 15 | BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, |
| 16 | DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError, |
| 17 | InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, |
| 18 | SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, |
| 19 | SupportedStreamConfigRange, SupportedStreamConfigsError, |
| 20 | }; |
| 21 | |
| 22 | // The emscripten backend currently works by instantiating an `AudioContext` object per `Stream`. |
| 23 | // Creating a stream creates a new `AudioContext`. Destroying a stream destroys it. Creation of a |
| 24 | // `Host` instance initializes the `stdweb` context. |
| 25 | |
| 26 | /// The default emscripten host type. |
| 27 | #[derive(Debug)] |
| 28 | pub struct Host; |
| 29 | |
| 30 | /// Content is false if the iterator is empty. |
| 31 | pub struct Devices(bool); |
| 32 | |
| 33 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 34 | pub struct Device; |
| 35 | |
| 36 | #[wasm_bindgen] |
| 37 | #[derive(Clone)] |
| 38 | pub struct Stream { |
| 39 | // A reference to an `AudioContext` object. |
| 40 | audio_ctxt: AudioContext, |
| 41 | } |
| 42 | |
| 43 | // WASM runs in a single-threaded environment, so Send and Sync are safe by design. |
| 44 | unsafe impl Send for Stream {} |
| 45 | unsafe impl Sync for Stream {} |
| 46 | |
| 47 | // Compile-time assertion that Stream is Send and Sync |
| 48 | crate::assert_stream_send!(Stream); |
| 49 | crate::assert_stream_sync!(Stream); |
| 50 | |
| 51 | pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; |
| 52 | |
| 53 | const MIN_CHANNELS: u16 = 1; |
| 54 | const MAX_CHANNELS: u16 = 32; |
| 55 | const MIN_SAMPLE_RATE: SampleRate = 8_000; |
| 56 | const MAX_SAMPLE_RATE: SampleRate = 96_000; |
| 57 | const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; |
| 58 | const MIN_BUFFER_SIZE: u32 = 1; |
| 59 | const MAX_BUFFER_SIZE: u32 = u32::MAX; |
| 60 | const DEFAULT_BUFFER_SIZE: usize = 2048; |
| 61 | const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; |
| 62 | |
| 63 | impl Host { |
| 64 | pub fn new() -> Result<Self, crate::HostUnavailable> { |
| 65 | Ok(Host) |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | impl Devices { |
| 70 | fn new() -> Result<Self, DevicesError> { |
| 71 | Ok(Self::default()) |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | impl Device { |
| 76 | fn description(&self) -> Result<DeviceDescription, DeviceNameError> { |
| 77 | Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) |
| 78 | .direction(crate::DeviceDirection::Output) |
| 79 | .build()) |
| 80 | } |
| 81 | |
| 82 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 83 | Ok(DeviceId( |
| 84 | crate::platform::HostId::Emscripten, |
| 85 | "default".to_string(), |
| 86 | )) |
| 87 | } |
| 88 | |
| 89 | fn supported_input_configs( |
| 90 | &self, |
| 91 | ) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> { |
| 92 | unimplemented!(); |
| 93 | } |
| 94 | |
| 95 | fn supported_output_configs( |
| 96 | &self, |
| 97 | ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 98 | let buffer_size = SupportedBufferSize::Range { |
| 99 | min: MIN_BUFFER_SIZE, |
| 100 | max: MAX_BUFFER_SIZE, |
| 101 | }; |
| 102 | let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) |
| 103 | .map(|channels| SupportedStreamConfigRange { |
| 104 | channels, |
| 105 | min_sample_rate: MIN_SAMPLE_RATE, |
| 106 | max_sample_rate: MAX_SAMPLE_RATE, |
| 107 | buffer_size, |
| 108 | sample_format: SUPPORTED_SAMPLE_FORMAT, |
| 109 | }) |
| 110 | .collect(); |
| 111 | Ok(configs.into_iter()) |
| 112 | } |
| 113 | |
| 114 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 115 | unimplemented!(); |
| 116 | } |
| 117 | |
| 118 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 119 | const EXPECT: &str = "expected at least one valid webaudio stream config"; |
| 120 | let config = self |
| 121 | .supported_output_configs() |
| 122 | .expect(EXPECT) |
| 123 | .max_by(|a, b| a.cmp_default_heuristics(b)) |
| 124 | .unwrap() |
| 125 | .with_sample_rate(DEFAULT_SAMPLE_RATE); |
| 126 | |
| 127 | Ok(config) |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | impl HostTrait for Host { |
| 132 | type Devices = Devices; |
| 133 | type Device = Device; |
| 134 | |
| 135 | fn is_available() -> bool { |
| 136 | // Assume this host is always available on emscripten. |
| 137 | true |
| 138 | } |
| 139 | |
| 140 | fn devices(&self) -> Result<Self::Devices, DevicesError> { |
| 141 | Devices::new() |
| 142 | } |
| 143 | |
| 144 | fn default_input_device(&self) -> Option<Self::Device> { |
| 145 | default_input_device() |
| 146 | } |
| 147 | |
| 148 | fn default_output_device(&self) -> Option<Self::Device> { |
| 149 | default_output_device() |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | impl DeviceTrait for Device { |
| 154 | type SupportedInputConfigs = SupportedInputConfigs; |
| 155 | type SupportedOutputConfigs = SupportedOutputConfigs; |
| 156 | type Stream = Stream; |
| 157 | |
| 158 | fn description(&self) -> Result<DeviceDescription, DeviceNameError> { |
| 159 | Device::description(self) |
| 160 | } |
| 161 | |
| 162 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 163 | Device::id(self) |
| 164 | } |
| 165 | |
| 166 | fn supported_input_configs( |
| 167 | &self, |
| 168 | ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> { |
| 169 | Device::supported_input_configs(self) |
| 170 | } |
| 171 | |
| 172 | fn supported_output_configs( |
| 173 | &self, |
| 174 | ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 175 | Device::supported_output_configs(self) |
| 176 | } |
| 177 | |
| 178 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 179 | Device::default_input_config(self) |
| 180 | } |
| 181 | |
| 182 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 183 | Device::default_output_config(self) |
| 184 | } |
| 185 | |
| 186 | fn build_input_stream_raw<D, E>( |
| 187 | &self, |
| 188 | _config: StreamConfig, |
| 189 | _sample_format: SampleFormat, |
| 190 | _data_callback: D, |
| 191 | _error_callback: E, |
| 192 | _timeout: Option<Duration>, |
| 193 | ) -> Result<Self::Stream, BuildStreamError> |
| 194 | where |
| 195 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, |
| 196 | E: FnMut(StreamError) + Send + 'static, |
| 197 | { |
| 198 | unimplemented!() |
| 199 | } |
| 200 | |
| 201 | fn build_output_stream_raw<D, E>( |
| 202 | &self, |
| 203 | config: StreamConfig, |
| 204 | sample_format: SampleFormat, |
| 205 | data_callback: D, |
| 206 | _error_callback: E, |
| 207 | _timeout: Option<Duration>, |
| 208 | ) -> Result<Self::Stream, BuildStreamError> |
| 209 | where |
| 210 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 211 | E: FnMut(StreamError) + Send + 'static, |
| 212 | { |
| 213 | if !valid_config(config, sample_format) { |
| 214 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 215 | } |
| 216 | |
| 217 | let buffer_size_frames = match config.buffer_size { |
| 218 | BufferSize::Fixed(v) => { |
| 219 | if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { |
| 220 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 221 | } |
| 222 | v as usize |
| 223 | } |
| 224 | BufferSize::Default => DEFAULT_BUFFER_SIZE, |
| 225 | }; |
| 226 | |
| 227 | // Create the stream. |
| 228 | let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system"); |
| 229 | let stream = Stream { audio_ctxt }; |
| 230 | |
| 231 | // Use `set_timeout` to invoke a Rust callback repeatedly. |
| 232 | // |
| 233 | // The job of this callback is to fill the content of the audio buffers. |
| 234 | // |
| 235 | // See also: The call to `set_timeout` at the end of the `audio_callback_fn` which creates |
| 236 | // the loop. |
| 237 | let data_callback = AssertUnwindSafe(data_callback); |
| 238 | set_timeout( |
| 239 | 10, |
| 240 | stream.clone(), |
| 241 | data_callback, |
| 242 | config, |
| 243 | sample_format, |
| 244 | buffer_size_frames as u32, |
| 245 | ); |
| 246 | |
| 247 | Ok(stream) |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | impl StreamTrait for Stream { |
| 252 | fn play(&self) -> Result<(), PlayStreamError> { |
| 253 | let future = JsFuture::from( |
| 254 | self.audio_ctxt |
| 255 | .resume() |
| 256 | .expect("Could not resume the stream"), |
| 257 | ); |
| 258 | spawn_local(async { |
| 259 | match future.await { |
| 260 | Ok(value) => assert!(value.is_undefined()), |
| 261 | Err(value) => panic!("AudioContext.resume() promise was rejected: {:?}", value), |
| 262 | } |
| 263 | }); |
| 264 | Ok(()) |
| 265 | } |
| 266 | |
| 267 | fn pause(&self) -> Result<(), PauseStreamError> { |
| 268 | let future = JsFuture::from( |
| 269 | self.audio_ctxt |
| 270 | .suspend() |
| 271 | .expect("Could not suspend the stream"), |
| 272 | ); |
| 273 | spawn_local(async { |
| 274 | match future.await { |
| 275 | Ok(value) => assert!(value.is_undefined()), |
| 276 | Err(value) => panic!("AudioContext.suspend() promise was rejected: {:?}", value), |
| 277 | } |
| 278 | }); |
| 279 | Ok(()) |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | fn audio_callback_fn<D>( |
| 284 | mut data_callback: AssertUnwindSafe<D>, |
| 285 | ) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32) |
| 286 | where |
| 287 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 288 | { |
| 289 | |stream, config, sample_format, buffer_size_frames| { |
| 290 | let sample_rate = config.sample_rate; |
| 291 | let buffer_size_samples = buffer_size_frames * config.channels as u32; |
| 292 | let audio_ctxt = &stream.audio_ctxt; |
| 293 | |
| 294 | // TODO: We should be re-using a buffer. |
| 295 | let mut temporary_buffer = vec![0f32; buffer_size_samples as usize]; |
| 296 | |
| 297 | { |
| 298 | let len = temporary_buffer.len(); |
| 299 | let data = temporary_buffer.as_mut_ptr() as *mut (); |
| 300 | let mut data = unsafe { Data::from_parts(data, len, sample_format) }; |
| 301 | let now_secs: f64 = audio_ctxt.current_time(); |
| 302 | let callback = crate::StreamInstant::from_secs_f64(now_secs); |
| 303 | // TODO: Use proper latency instead. Currently, unsupported on most browsers though, so |
| 304 | // we estimate based on buffer size instead. Probably should use this, but it's only |
| 305 | // supported by firefox (2020-04-28). |
| 306 | // let latency_secs: f64 = audio_ctxt.outputLatency.try_into().unwrap(); |
| 307 | let buffer_duration = frames_to_duration(len, sample_rate as usize); |
| 308 | let playback = callback |
| 309 | .add(buffer_duration) |
| 310 | .expect("`playback` occurs beyond representation supported by `StreamInstant`"); |
| 311 | let timestamp = crate::OutputStreamTimestamp { callback, playback }; |
| 312 | let info = OutputCallbackInfo { timestamp }; |
| 313 | data_callback(&mut data, &info); |
| 314 | } |
| 315 | |
| 316 | let typed_array: Float32Array = temporary_buffer.as_slice().into(); |
| 317 | |
| 318 | debug_assert_eq!(temporary_buffer.len() % config.channels as usize, 0); |
| 319 | |
| 320 | let src_buffer = Float32Array::new(typed_array.buffer().as_ref()); |
| 321 | let context = audio_ctxt; |
| 322 | let buffer = context |
| 323 | .create_buffer( |
| 324 | config.channels as u32, |
| 325 | buffer_size_frames, |
| 326 | sample_rate as f32, |
| 327 | ) |
| 328 | .expect("Buffer could not be created"); |
| 329 | for channel in 0..config.channels { |
| 330 | let mut buffer_content = buffer |
| 331 | .get_channel_data(channel as u32) |
| 332 | .expect("Should be impossible"); |
| 333 | for (i, buffer_content_item) in buffer_content.iter_mut().enumerate() { |
| 334 | *buffer_content_item = |
| 335 | src_buffer.get_index(i as u32 * config.channels as u32 + channel as u32); |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | let node = context |
| 340 | .create_buffer_source() |
| 341 | .expect("The buffer source node could not be created"); |
| 342 | node.set_buffer(Some(&buffer)); |
| 343 | context |
| 344 | .destination() |
| 345 | .connect_with_audio_node(&node) |
| 346 | .expect("Could not connect the audio node to the destination"); |
| 347 | node.start().expect("Could not start the audio node"); |
| 348 | |
| 349 | // TODO: handle latency better ; right now we just use setInterval with the amount of sound |
| 350 | // data that is in each buffer ; this is obviously bad, and also the schedule is too tight |
| 351 | // and there may be underflows |
| 352 | set_timeout( |
| 353 | 1000 * buffer_size_frames as i32 / sample_rate as i32, |
| 354 | stream.clone().clone(), |
| 355 | data_callback, |
| 356 | config, |
| 357 | sample_format, |
| 358 | buffer_size_frames, |
| 359 | ); |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | fn set_timeout<D>( |
| 364 | time: i32, |
| 365 | stream: Stream, |
| 366 | data_callback: AssertUnwindSafe<D>, |
| 367 | config: StreamConfig, |
| 368 | sample_format: SampleFormat, |
| 369 | buffer_size_frames: u32, |
| 370 | ) where |
| 371 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 372 | { |
| 373 | let window = web_sys::window().expect("Not in a window somehow?"); |
| 374 | window |
| 375 | .set_timeout_with_callback_and_timeout_and_arguments_4( |
| 376 | Closure::once_into_js(audio_callback_fn(data_callback)) |
| 377 | .dyn_ref::<js_sys::Function>() |
| 378 | .expect("The function was somehow not a function"), |
| 379 | time, |
| 380 | &stream.into(), |
| 381 | &config.into(), |
| 382 | &Closure::once_into_js(move || sample_format), |
| 383 | &buffer_size_frames.into(), |
| 384 | ) |
| 385 | .expect("The timeout could not be set"); |
| 386 | } |
| 387 | |
| 388 | impl Default for Devices { |
| 389 | fn default() -> Devices { |
| 390 | // We produce an empty iterator if the WebAudio API isn't available. |
| 391 | Devices(is_webaudio_available()) |
| 392 | } |
| 393 | } |
| 394 | impl Iterator for Devices { |
| 395 | type Item = Device; |
| 396 | |
| 397 | fn next(&mut self) -> Option<Device> { |
| 398 | if self.0 { |
| 399 | self.0 = false; |
| 400 | Some(Device) |
| 401 | } else { |
| 402 | None |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | fn default_input_device() -> Option<Device> { |
| 408 | unimplemented!(); |
| 409 | } |
| 410 | |
| 411 | fn default_output_device() -> Option<Device> { |
| 412 | if is_webaudio_available() { |
| 413 | Some(Device) |
| 414 | } else { |
| 415 | None |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | // Detects whether the `AudioContext` global variable is available. |
| 420 | fn is_webaudio_available() -> bool { |
| 421 | AudioContext::new().is_ok() |
| 422 | } |
| 423 | |
| 424 | // Whether or not the given stream configuration is valid for building a stream. |
| 425 | fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool { |
| 426 | conf.channels <= MAX_CHANNELS |
| 427 | && conf.channels >= MIN_CHANNELS |
| 428 | && conf.sample_rate <= MAX_SAMPLE_RATE |
| 429 | && conf.sample_rate >= MIN_SAMPLE_RATE |
| 430 | && sample_format == SUPPORTED_SAMPLE_FORMAT |
| 431 | } |
| 432 | |
| 433 | // Convert the given duration in frames at the given sample rate to a `std::time::Duration`. |
| 434 | fn frames_to_duration(frames: usize, rate: usize) -> std::time::Duration { |
| 435 | let secsf = frames as f64 / rate as f64; |
| 436 | let secs = secsf as u64; |
| 437 | let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; |
| 438 | std::time::Duration::new(secs, nanos) |
| 439 | } |