diff --git a/vendor/web-audio-api-1.7.0/src/context/online.rs b/vendor/web-audio-api-1.7.0/src/context/online.rs index a461db904b..7d9117282e 100644 --- a/vendor/web-audio-api-1.7.0/src/context/online.rs +++ b/vendor/web-audio-api-1.7.0/src/context/online.rs @@ -619,7 +619,11 @@ impl AudioContext { // Stop AudioRenderCapacity before closing so no capacity events are queued during shutdown. self.render_capacity.stop(); - if self.state() == AudioContextState::Running { + // Startup can still be queued while the initial state is Suspended. + // Serialize Close after it before shutting down the backend. + if self.startup_pending.load(Ordering::Acquire) + || self.state() == AudioContextState::Running + { // First, stop rendering via a control message let (sender, receiver) = oneshot::channel(); let notify = OneshotNotify::Async(sender); @@ -759,8 +763,11 @@ impl AudioContext { // Stop AudioRenderCapacity before closing so no capacity events are queued during shutdown. self.render_capacity.stop(); - // First, stop rendering via a control message - if self.state() == AudioContextState::Running { + // Startup must settle before the backend stops, even if the initial + // Suspended state has not yet transitioned to Running. + if self.startup_pending.load(Ordering::Acquire) + || self.state() == AudioContextState::Running + { let (sender, receiver) = crossbeam_channel::bounded(0); let notify = OneshotNotify::Sync(sender); self.base.send_control_msg(ControlMessage::Close { notify }); @@ -918,6 +925,38 @@ mod tests { fn require_send_sync(_: T) {} + #[test] + fn test_close_sync_during_startup_is_terminal() { + let context = AudioContext::new(AudioContextOptions { + sink_id: "none".into(), + ..AudioContextOptions::default() + }); + context.close_sync(); + assert!( + !context.startup_pending.load(Ordering::Acquire), + "close must settle the queued Startup before stopping its backend" + ); + assert_eq!(context.state(), AudioContextState::Closed); + context.close_sync(); + assert_eq!(context.state(), AudioContextState::Closed); + } + + #[test] + fn test_close_during_startup_is_terminal() { + let context = AudioContext::new(AudioContextOptions { + sink_id: "none".into(), + ..AudioContextOptions::default() + }); + executor::block_on(context.close()); + assert!( + !context.startup_pending.load(Ordering::Acquire), + "close must settle the queued Startup before stopping its backend" + ); + assert_eq!(context.state(), AudioContextState::Closed); + context.close_sync(); + assert_eq!(context.state(), AudioContextState::Closed); + } + #[test] fn test_all_futures_thread_safe() { let options = AudioContextOptions { diff --git a/vendor/web-audio-api-1.7.0/src/render/thread.rs b/vendor/web-audio-api-1.7.0/src/render/thread.rs index c684954968..20353812d3 100644 --- a/vendor/web-audio-api-1.7.0/src/render/thread.rs +++ b/vendor/web-audio-api-1.7.0/src/render/thread.rs @@ -191,10 +191,11 @@ impl RenderThread { Startup { graph } => { debug_assert!(self.graph.is_none()); self.graph = Some(graph); + self.set_state(AudioContextState::Running); if let Some(startup_pending) = self.startup_pending.as_ref() { + // Observing startup completion must also observe Running. startup_pending.store(false, Ordering::Release); } - self.set_state(AudioContextState::Running); } NodeMessage { id, mut msg } => { self.graph.as_mut().unwrap().route_message(id, msg.as_mut());