Skip to content

Commit c384fba

Browse files
committed
Fix hanging UI thread for certain AudioContext::suspend/stop actions
There were quite a few issues with - multiple subsequent suspend() calls - multiple subsequent close() calls - calling close() on a suspended context There are regression tests for these cases now. Fixes #496
1 parent 3364d53 commit c384fba

File tree

3 files changed

+158
-34
lines changed

3 files changed

+158
-34
lines changed

src/context/concrete_base.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,8 @@ impl ConcreteBaseAudioContext {
351351

352352
/// Updates state of current context
353353
pub(super) fn set_state(&self, state: AudioContextState) {
354-
// Only to be used from OfflineAudioContext, the online one should emit the state changes
355-
// from the render thread
356-
debug_assert!(self.offline());
357-
354+
// Only used from OfflineAudioContext or suspended AudioContext, otherwise the state
355+
// changed ar spawned from the render thread
358356
let current_state = self.state();
359357
if current_state != state {
360358
self.inner.state.store(state as u8, Ordering::Release);

src/context/online.rs

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,15 @@ impl AudioContext {
435435
/// * The audio device is not available
436436
/// * For a `BackendSpecificError`
437437
pub async fn suspend(&self) {
438+
// Don't lock the backend manager because we can't hold is across the await point
438439
log::debug!("Suspend called");
439-
// First, pause rendering via a control message
440+
441+
if self.state() != AudioContextState::Running {
442+
log::debug!("Suspend no-op - context is not running");
443+
return;
444+
}
445+
446+
// Pause rendering via a control message
440447
let (sender, receiver) = oneshot::channel();
441448
let notify = OneshotNotify::Async(sender);
442449
self.base
@@ -450,6 +457,7 @@ impl AudioContext {
450457
// Then ask the audio host to suspend the stream
451458
log::debug!("Suspended audio graph. Suspending audio stream..");
452459
self.backend_manager.lock().unwrap().suspend();
460+
453461
log::debug!("Suspended audio stream");
454462
}
455463

@@ -462,10 +470,19 @@ impl AudioContext {
462470
///
463471
/// * The audio device is not available
464472
/// * For a `BackendSpecificError`
473+
#[allow(clippy::await_holding_lock)] // false positive due to explicit drop
465474
pub async fn resume(&self) {
466-
log::debug!("Resume called");
467-
// First ask the audio host to resume the stream
468-
self.backend_manager.lock().unwrap().resume();
475+
// Lock the backend manager mutex to avoid concurrent calls
476+
log::debug!("Resume called, locking backend manager");
477+
let backend_manager_guard = self.backend_manager.lock().unwrap();
478+
479+
if self.state() != AudioContextState::Suspended {
480+
log::debug!("Resume no-op - context is not suspended");
481+
return;
482+
}
483+
484+
// Ask the audio host to resume the stream
485+
backend_manager_guard.resume();
469486

470487
// Then, ask to resume rendering via a control message
471488
log::debug!("Resumed audio stream, waking audio graph");
@@ -474,6 +491,9 @@ impl AudioContext {
474491
self.base
475492
.send_control_msg(ControlMessage::Resume { notify });
476493

494+
// Drop the Mutex guard so we won't hold it across an await
495+
drop(backend_manager_guard);
496+
477497
// Wait for the render thread to have processed the resume message
478498
// The AudioContextState will be updated by the render thread.
479499
receiver.await.unwrap();
@@ -489,17 +509,28 @@ impl AudioContext {
489509
///
490510
/// Will panic when this function is called multiple times
491511
pub async fn close(&self) {
512+
// Don't lock the backend manager because we can't hold is across the await point
492513
log::debug!("Close called");
493514

494-
// First, stop rendering via a control message
495-
let (sender, receiver) = oneshot::channel();
496-
let notify = OneshotNotify::Async(sender);
497-
self.base.send_control_msg(ControlMessage::Close { notify });
515+
if self.state() == AudioContextState::Closed {
516+
log::debug!("Close no-op - context is already closed");
517+
return;
518+
}
498519

499-
// Wait for the render thread to have processed the suspend message.
500-
// The AudioContextState will be updated by the render thread.
501-
log::debug!("Suspending audio graph, waiting for signal..");
502-
receiver.await.unwrap();
520+
if self.state() == AudioContextState::Running {
521+
// First, stop rendering via a control message
522+
let (sender, receiver) = oneshot::channel();
523+
let notify = OneshotNotify::Async(sender);
524+
self.base.send_control_msg(ControlMessage::Close { notify });
525+
526+
// Wait for the render thread to have processed the suspend message.
527+
// The AudioContextState will be updated by the render thread.
528+
log::debug!("Suspending audio graph, waiting for signal..");
529+
receiver.await.unwrap();
530+
} else {
531+
// if the context is not running, change the state manually
532+
self.base.set_state(AudioContextState::Closed);
533+
}
503534

504535
// Then ask the audio host to close the stream
505536
log::debug!("Suspended audio graph. Closing audio stream..");
@@ -526,8 +557,16 @@ impl AudioContext {
526557
/// * The audio device is not available
527558
/// * For a `BackendSpecificError`
528559
pub fn suspend_sync(&self) {
529-
log::debug!("Suspend_sync called");
530-
// First, pause rendering via a control message
560+
// Lock the backend manager mutex to avoid concurrent calls
561+
log::debug!("Suspend_sync called, locking backend manager");
562+
let backend_manager_guard = self.backend_manager.lock().unwrap();
563+
564+
if self.state() != AudioContextState::Running {
565+
log::debug!("Suspend_sync no-op - context is not running");
566+
return;
567+
}
568+
569+
// Pause rendering via a control message
531570
let (sender, receiver) = crossbeam_channel::bounded(0);
532571
let notify = OneshotNotify::Sync(sender);
533572
self.base
@@ -537,10 +576,11 @@ impl AudioContext {
537576
// The AudioContextState will be updated by the render thread.
538577
log::debug!("Suspending audio graph, waiting for signal..");
539578
receiver.recv().ok();
540-
log::debug!("Suspended audio graph. Suspending audio stream..");
541579

542580
// Then ask the audio host to suspend the stream
543-
self.backend_manager.lock().unwrap().suspend();
581+
log::debug!("Suspended audio graph. Suspending audio stream..");
582+
backend_manager_guard.suspend();
583+
544584
log::debug!("Suspended audio stream");
545585
}
546586

@@ -557,9 +597,17 @@ impl AudioContext {
557597
/// * The audio device is not available
558598
/// * For a `BackendSpecificError`
559599
pub fn resume_sync(&self) {
560-
log::debug!("Resume_sync called");
561-
// First ask the audio host to resume the stream
562-
self.backend_manager.lock().unwrap().resume();
600+
// Lock the backend manager mutex to avoid concurrent calls
601+
log::debug!("Resume_sync called, locking backend manager");
602+
let backend_manager_guard = self.backend_manager.lock().unwrap();
603+
604+
if self.state() != AudioContextState::Suspended {
605+
log::debug!("Resume no-op - context is not suspended");
606+
return;
607+
}
608+
609+
// Ask the audio host to resume the stream
610+
backend_manager_guard.resume();
563611

564612
// Then, ask to resume rendering via a control message
565613
log::debug!("Resumed audio stream, waking audio graph");
@@ -586,21 +634,33 @@ impl AudioContext {
586634
///
587635
/// Will panic when this function is called multiple times
588636
pub fn close_sync(&self) {
589-
log::debug!("Close_sync called");
637+
// Lock the backend manager mutex to avoid concurrent calls
638+
log::debug!("Close_sync called, locking backend manager");
639+
let backend_manager_guard = self.backend_manager.lock().unwrap();
590640

591-
// First, stop rendering via a control message
592-
let (sender, receiver) = crossbeam_channel::bounded(0);
593-
let notify = OneshotNotify::Sync(sender);
594-
self.base.send_control_msg(ControlMessage::Close { notify });
641+
if self.state() == AudioContextState::Closed {
642+
log::debug!("Close no-op - context is already closed");
643+
return;
644+
}
595645

596-
// Wait for the render thread to have processed the suspend message.
597-
// The AudioContextState will be updated by the render thread.
598-
log::debug!("Suspending audio graph, waiting for signal..");
599-
receiver.recv().ok();
646+
// First, stop rendering via a control message
647+
if self.state() == AudioContextState::Running {
648+
let (sender, receiver) = crossbeam_channel::bounded(0);
649+
let notify = OneshotNotify::Sync(sender);
650+
self.base.send_control_msg(ControlMessage::Close { notify });
651+
652+
// Wait for the render thread to have processed the suspend message.
653+
// The AudioContextState will be updated by the render thread.
654+
log::debug!("Suspending audio graph, waiting for signal..");
655+
receiver.recv().ok();
656+
} else {
657+
// if the context is not running, change the state manually
658+
self.base.set_state(AudioContextState::Closed);
659+
}
600660

601661
// Then ask the audio host to close the stream
602662
log::debug!("Suspended audio graph. Closing audio stream..");
603-
self.backend_manager.lock().unwrap().close();
663+
backend_manager_guard.close();
604664

605665
// Stop the AudioRenderCapacity collection thread
606666
self.render_capacity.stop();

tests/online.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,78 @@ fn test_closed() {
219219
let context = AudioContext::new(options);
220220
let node = context.create_gain();
221221

222-
// close the context, and drop it as well (otherwise the comms channel is kept alive)
222+
// Close the context
223223
context.close_sync();
224+
assert_eq!(context.state(), AudioContextState::Closed);
225+
226+
// Should not be able to resume
227+
context.resume_sync();
228+
assert_eq!(context.state(), AudioContextState::Closed);
229+
230+
// Drop the context (otherwise the comms channel is kept alive)
224231
drop(context);
225232

226233
// allow some time for the render thread to drop
227234
std::thread::sleep(Duration::from_millis(10));
228235

229236
node.disconnect(); // should not panic
230237
}
238+
239+
#[test]
240+
fn test_double_suspend() {
241+
let options = AudioContextOptions {
242+
sink_id: "none".into(),
243+
..AudioContextOptions::default()
244+
};
245+
let context = AudioContext::new(options);
246+
247+
context.suspend_sync();
248+
assert_eq!(context.state(), AudioContextState::Suspended);
249+
context.suspend_sync();
250+
assert_eq!(context.state(), AudioContextState::Suspended);
251+
context.resume_sync();
252+
assert_eq!(context.state(), AudioContextState::Running);
253+
}
254+
255+
#[test]
256+
fn test_double_resume() {
257+
let options = AudioContextOptions {
258+
sink_id: "none".into(),
259+
..AudioContextOptions::default()
260+
};
261+
let context = AudioContext::new(options);
262+
263+
context.suspend_sync();
264+
assert_eq!(context.state(), AudioContextState::Suspended);
265+
context.resume_sync();
266+
assert_eq!(context.state(), AudioContextState::Running);
267+
context.resume_sync();
268+
assert_eq!(context.state(), AudioContextState::Running);
269+
}
270+
271+
#[test]
272+
fn test_double_close() {
273+
let options = AudioContextOptions {
274+
sink_id: "none".into(),
275+
..AudioContextOptions::default()
276+
};
277+
let context = AudioContext::new(options);
278+
279+
context.close_sync();
280+
assert_eq!(context.state(), AudioContextState::Closed);
281+
context.close_sync();
282+
assert_eq!(context.state(), AudioContextState::Closed);
283+
}
284+
285+
#[test]
286+
fn test_suspend_then_close() {
287+
let options = AudioContextOptions {
288+
..AudioContextOptions::default()
289+
};
290+
let context = AudioContext::new(options);
291+
292+
context.suspend_sync();
293+
assert_eq!(context.state(), AudioContextState::Suspended);
294+
context.close_sync();
295+
assert_eq!(context.state(), AudioContextState::Closed);
296+
}

0 commit comments

Comments
 (0)