Skip to content

Commit 50e115f

Browse files
committed
Ensure AudioContext suspend/resume_sync block control thread until completed
1 parent e54c5b7 commit 50e115f

File tree

4 files changed

+67
-26
lines changed

4 files changed

+67
-26
lines changed

src/context/online.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::events::{EventDispatch, EventHandler, EventPayload, EventType};
77
use crate::io::{self, AudioBackendManager, ControlThreadInit, RenderThreadInit};
88
use crate::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind};
99
use crate::media_streams::{MediaStream, MediaStreamTrack};
10-
use crate::message::ControlMessage;
10+
use crate::message::{ControlMessage, OneshotNotify};
1111
use crate::node::{self, ChannelConfigOptions};
1212
use crate::render::graph::Graph;
1313
use crate::MediaElement;
@@ -387,13 +387,19 @@ impl AudioContext {
387387
///
388388
/// * The audio device is not available
389389
/// * For a `BackendSpecificError`
390-
#[allow(clippy::missing_const_for_fn, clippy::unused_self)]
391390
pub fn suspend_sync(&self) {
392-
if self.backend_manager.lock().unwrap().suspend() {
393-
// TODO: state should signal device readiness hence should be updated by the render
394-
// thread, not the control thread.
395-
self.base().set_state(AudioContextState::Suspended);
396-
}
391+
// First, pause rendering via a control message
392+
let (sender, receiver) = crossbeam_channel::bounded(0);
393+
let notify = OneshotNotify::Sync(sender);
394+
let suspend_msg = ControlMessage::Suspend { notify };
395+
self.base.send_control_msg(suspend_msg);
396+
397+
// Wait for the render thread to have processed the suspend message.
398+
// The AudioContextState will be updated by the render thread.
399+
receiver.recv().ok();
400+
401+
// Then ask the audio host to suspend the stream
402+
self.backend_manager.lock().unwrap().suspend();
397403
}
398404

399405
/// Resumes the progression of time in an audio context that has previously been
@@ -408,13 +414,19 @@ impl AudioContext {
408414
///
409415
/// * The audio device is not available
410416
/// * For a `BackendSpecificError`
411-
#[allow(clippy::missing_const_for_fn, clippy::unused_self)]
412417
pub fn resume_sync(&self) {
413-
if self.backend_manager.lock().unwrap().resume() {
414-
// TODO: state should signal device readiness hence should be updated by the render
415-
// thread, not the control thread.
416-
self.base().set_state(AudioContextState::Running);
417-
}
418+
// First ask the audio host to resume the stream
419+
self.backend_manager.lock().unwrap().resume();
420+
421+
// Then, ask to resume rendering via a control message
422+
let (sender, receiver) = crossbeam_channel::bounded(0);
423+
let notify = OneshotNotify::Sync(sender);
424+
let suspend_msg = ControlMessage::Resume { notify };
425+
self.base.send_control_msg(suspend_msg);
426+
427+
// Wait for the render thread to have processed the resume message
428+
// The AudioContextState will be updated by the render thread.
429+
receiver.recv().ok();
418430
}
419431

420432
/// Closes the `AudioContext`, releasing the system resources being used.

src/message.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ use crate::node::ChannelConfig;
77
use crate::render::graph::Graph;
88
use crate::render::AudioProcessor;
99

10-
use crossbeam_channel::Sender;
11-
1210
/// Commands from the control thread to the render thread
1311
pub(crate) enum ControlMessage {
1412
/// Register a new node in the audio graph
@@ -42,11 +40,19 @@ pub(crate) enum ControlMessage {
4240
MarkCycleBreaker { id: AudioNodeId },
4341

4442
/// Shut down and recycle the audio graph
45-
Shutdown { sender: Sender<Graph> },
43+
Shutdown {
44+
sender: crossbeam_channel::Sender<Graph>,
45+
},
4646

4747
/// Start rendering with given audio graph
4848
Startup { graph: Graph },
4949

50+
/// Suspend and pause audio processing
51+
Suspend { notify: OneshotNotify },
52+
53+
/// Resume audio processing after suspending
54+
Resume { notify: OneshotNotify },
55+
5056
/// Generic message to be handled by AudioProcessor
5157
NodeMessage {
5258
id: AudioNodeId,
@@ -56,3 +62,21 @@ pub(crate) enum ControlMessage {
5662
/// Request a diagnostic report of the audio graph
5763
RunDiagnostics { buffer: Vec<u8> },
5864
}
65+
66+
/// Helper object to emit single notification
67+
pub(crate) enum OneshotNotify {
68+
/// A synchronous oneshot sender
69+
Sync(crossbeam_channel::Sender<()>),
70+
/// An asynchronous oneshot sender
71+
Async(futures::channel::oneshot::Sender<()>),
72+
}
73+
74+
impl OneshotNotify {
75+
/// Emit the notification
76+
pub fn send(self) {
77+
match self {
78+
Self::Sync(s) => s.send(()).ok(),
79+
Self::Async(s) => s.send(()).ok(),
80+
};
81+
}
82+
}

src/render/thread.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub(crate) struct RenderThread {
3333
/// number of channels of the backend stream, i.e. sound card number of
3434
/// channels clamped to MAX_CHANNELS
3535
number_of_channels: usize,
36+
suspended: bool,
3637
state: Arc<AtomicU8>,
3738
frames_played: Arc<AtomicU64>,
3839
receiver: Option<Receiver<ControlMessage>>,
@@ -77,6 +78,7 @@ impl RenderThread {
7778
sample_rate,
7879
buffer_size: 0,
7980
number_of_channels,
81+
suspended: false,
8082
state,
8183
frames_played,
8284
receiver: Some(receiver),
@@ -181,6 +183,16 @@ impl RenderThread {
181183
.expect("Unable to send diagnostics - channel is full");
182184
}
183185
}
186+
Suspend { notify } => {
187+
self.suspended = true;
188+
self.set_state(AudioContextState::Suspended);
189+
notify.send();
190+
}
191+
Resume { notify } => {
192+
self.suspended = false;
193+
self.set_state(AudioContextState::Running);
194+
notify.send();
195+
}
184196
}
185197
}
186198
}
@@ -366,8 +378,8 @@ impl RenderThread {
366378
// handle addition/removal of nodes/edges
367379
self.handle_control_messages();
368380

369-
// if the thread is still booting, or shutting down, fill with silence
370-
if !self.graph.as_ref().is_some_and(Graph::is_active) {
381+
// if the thread is still booting, suspended, or shutting down, fill with silence
382+
if self.suspended || !self.graph.as_ref().is_some_and(Graph::is_active) {
371383
output_buffer.fill(S::from_sample_(0.));
372384
return;
373385
}

tests/online.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,25 +106,18 @@ fn test_none_sink_id() {
106106
assert_eq!(context.sink_id(), "none");
107107

108108
context.suspend_sync();
109-
110-
// give event thread some time to pick up events
111-
std::thread::sleep(std::time::Duration::from_millis(20));
112109
assert_eq!(context.state(), AudioContextState::Suspended);
113110
assert_eq!(state_changes.load(Ordering::Relaxed), 2); // suspended
114111

115112
context.resume_sync();
116113
assert_eq!(context.state(), AudioContextState::Running);
117-
118-
// give event thread some time to pick up events
119-
std::thread::sleep(std::time::Duration::from_millis(20));
120114
assert_eq!(state_changes.load(Ordering::Relaxed), 3); // resumed
121115

122116
context.close_sync();
123-
std::thread::sleep(std::time::Duration::from_millis(20));
124-
assert_eq!(context.state(), AudioContextState::Closed);
125117

126118
// give event thread some time to pick up events
127119
std::thread::sleep(std::time::Duration::from_millis(20));
120+
assert_eq!(context.state(), AudioContextState::Closed);
128121
assert!(sink_stable.load(Ordering::SeqCst));
129122
assert_eq!(state_changes.load(Ordering::Relaxed), 4); // closed
130123
}

0 commit comments

Comments
 (0)