Skip to content

Commit 217ed1a

Browse files
committed
BaseAudioContext::state set by render thread to signal device readiness
Fixes #410 Except for suspending/resuming, I don't have a solution for that yet. In summary: - A context is instantiated in Suspended state - Online context will change to Running at the first render quantum - Online context will change to Closed when the render thread is dropped after a `close` call - Online context suspend/resume state is still managed by the control thread - TODO - Offline context will change to Running after startRendering
1 parent 585f8aa commit 217ed1a

File tree

9 files changed

+66
-24
lines changed

9 files changed

+66
-24
lines changed

src/context/concrete_base.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ struct ConcreteBaseAudioContextInner {
9191
listener_params: Option<AudioListenerParams>,
9292
/// Denotes if this AudioContext is offline or not
9393
offline: bool,
94-
/// Describes the current state of the `ConcreteBaseAudioContext`
95-
state: AtomicU8,
94+
/// Current state of the `ConcreteBaseAudioContext`, shared with the RenderThread
95+
state: Arc<AtomicU8>,
9696
/// Stores the event handlers
9797
event_loop: EventLoop,
9898
/// Sender for events that will be handled by the EventLoop
@@ -150,6 +150,7 @@ impl ConcreteBaseAudioContext {
150150
pub(super) fn new(
151151
sample_rate: f32,
152152
max_channel_count: usize,
153+
state: Arc<AtomicU8>,
153154
frames_played: Arc<AtomicU64>,
154155
render_channel: Sender<ControlMessage>,
155156
event_channel: Option<(Sender<EventDispatch>, Receiver<EventDispatch>)>,
@@ -175,7 +176,7 @@ impl ConcreteBaseAudioContext {
175176
queued_audio_listener_msgs: Mutex::new(Vec::new()),
176177
listener_params: None,
177178
offline,
178-
state: AtomicU8::new(AudioContextState::Suspended as u8),
179+
state,
179180
event_loop: event_loop.clone(),
180181
event_send,
181182
};
@@ -263,7 +264,6 @@ impl ConcreteBaseAudioContext {
263264
if self.state() != AudioContextState::Closed {
264265
let result = self.inner.render_channel.read().unwrap().send(msg);
265266
if result.is_err() {
266-
self.set_state(AudioContextState::Closed);
267267
log::warn!("Discarding control message - render thread is closed");
268268
}
269269
}
@@ -333,14 +333,14 @@ impl ConcreteBaseAudioContext {
333333
/// Returns state of current context
334334
#[must_use]
335335
pub(super) fn state(&self) -> AudioContextState {
336-
self.inner.state.load(Ordering::SeqCst).into()
336+
self.inner.state.load(Ordering::Acquire).into()
337337
}
338338

339339
/// Updates state of current context
340340
pub(super) fn set_state(&self, state: AudioContextState) {
341341
let current_state = self.state();
342342
if current_state != state {
343-
self.inner.state.store(state as u8, Ordering::SeqCst);
343+
self.inner.state.store(state as u8, Ordering::Release);
344344
let _ = self.send_event(EventDispatch::state_change());
345345
}
346346
}

src/context/offline.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
//! The `OfflineAudioContext` type
22
3-
use std::sync::atomic::AtomicU64;
3+
use std::sync::atomic::{AtomicU64, AtomicU8};
44
use std::sync::{Arc, Mutex};
55

66
use crate::buffer::AudioBuffer;
7-
use crate::context::{BaseAudioContext, ConcreteBaseAudioContext};
7+
use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
88
use crate::render::RenderThread;
99
use crate::{assert_valid_sample_rate, RENDER_QUANTUM_SIZE};
1010

@@ -71,19 +71,23 @@ impl OfflineAudioContext {
7171
// track number of frames - synced from render thread to control thread
7272
let frames_played = Arc::new(AtomicU64::new(0));
7373
let frames_played_clone = Arc::clone(&frames_played);
74+
let state = Arc::new(AtomicU8::new(AudioContextState::Suspended as u8));
75+
let state_clone = Arc::clone(&state);
7476

7577
// setup the render 'thread', which will run inside the control thread
7678
let renderer = RenderThread::new(
7779
sample_rate,
7880
number_of_channels,
7981
receiver,
82+
state_clone,
8083
frames_played_clone,
8184
);
8285

8386
// first, setup the base audio context
8487
let base = ConcreteBaseAudioContext::new(
8588
sample_rate,
8689
number_of_channels,
90+
state,
8791
frames_played,
8892
sender,
8993
None,

src/context/online.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ impl AudioContext {
166166
let backend = io::build_output(options, render_thread_init.clone());
167167

168168
let ControlThreadInit {
169+
state,
169170
frames_played,
170171
ctrl_msg_send,
171172
load_value_recv,
@@ -181,13 +182,13 @@ impl AudioContext {
181182
let base = ConcreteBaseAudioContext::new(
182183
backend.sample_rate(),
183184
backend.number_of_channels(),
185+
state,
184186
frames_played,
185187
ctrl_msg_send,
186188
Some((event_send, event_recv)),
187189
false,
188190
node_id_consumer,
189191
);
190-
base.set_state(AudioContextState::Running);
191192

192193
// setup AudioRenderCapacity for this context
193194
let base_clone = base.clone();
@@ -255,9 +256,6 @@ impl AudioContext {
255256
return Ok(());
256257
}
257258

258-
// Temporarily set the state to Suspended, resume after the new backend is up
259-
self.base().set_state(AudioContextState::Suspended);
260-
261259
// Acquire exclusive lock on ctrl msg sender
262260
let ctrl_msg_send = self.base.lock_control_msg_sender();
263261

@@ -286,6 +284,8 @@ impl AudioContext {
286284
graph_recv.recv().unwrap()
287285
};
288286

287+
backend_manager_guard.close();
288+
289289
// hotswap the backend
290290
let options = AudioContextOptions {
291291
sample_rate: Some(self.sample_rate()),
@@ -304,10 +304,6 @@ impl AudioContext {
304304
let message = ControlMessage::Startup { graph };
305305
ctrl_msg_send.send(message).unwrap();
306306

307-
if original_state == AudioContextState::Running {
308-
self.base().set_state(AudioContextState::Running);
309-
}
310-
311307
// flush the cached msgs
312308
pending_msgs
313309
.into_iter()
@@ -394,6 +390,8 @@ impl AudioContext {
394390
#[allow(clippy::missing_const_for_fn, clippy::unused_self)]
395391
pub fn suspend_sync(&self) {
396392
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.
397395
self.base().set_state(AudioContextState::Suspended);
398396
}
399397
}
@@ -413,6 +411,8 @@ impl AudioContext {
413411
#[allow(clippy::missing_const_for_fn, clippy::unused_self)]
414412
pub fn resume_sync(&self) {
415413
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.
416416
self.base().set_state(AudioContextState::Running);
417417
}
418418
}
@@ -432,7 +432,6 @@ impl AudioContext {
432432
pub fn close_sync(&self) {
433433
self.backend_manager.lock().unwrap().close();
434434
self.render_capacity.stop();
435-
self.base().set_state(AudioContextState::Closed);
436435
}
437436

438437
/// Creates a [`MediaStreamAudioSourceNode`](node::MediaStreamAudioSourceNode) from a

src/io/cpal.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ impl AudioBackendManager for CpalBackend {
123123
log::info!("Audio Output Host: cpal {:?}", host.id());
124124

125125
let RenderThreadInit {
126+
state,
126127
frames_played,
127128
ctrl_msg_recv,
128129
load_value_send,
@@ -190,6 +191,7 @@ impl AudioBackendManager for CpalBackend {
190191
sample_rate,
191192
preferred_config.channels as usize,
192193
ctrl_msg_recv.clone(),
194+
Arc::clone(&state),
193195
Arc::clone(&frames_played),
194196
);
195197
renderer.set_event_channels(load_value_send.clone(), event_send.clone());
@@ -231,6 +233,7 @@ impl AudioBackendManager for CpalBackend {
231233
sample_rate,
232234
supported_config.channels as usize,
233235
ctrl_msg_recv,
236+
state,
234237
frames_played,
235238
);
236239
renderer.set_event_channels(load_value_send, event_send);

src/io/cubeb.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ impl AudioBackendManager for CubebBackend {
151151
Self: Sized,
152152
{
153153
let RenderThreadInit {
154+
state,
154155
frames_played,
155156
ctrl_msg_recv,
156157
load_value_send,
@@ -186,6 +187,7 @@ impl AudioBackendManager for CubebBackend {
186187
sample_rate,
187188
number_of_channels,
188189
ctrl_msg_recv,
190+
state,
189191
frames_played,
190192
);
191193
renderer.set_event_channels(load_value_send, event_send);

src/io/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Audio input/output interfaces
22
3-
use std::sync::atomic::AtomicU64;
3+
use std::sync::atomic::{AtomicU64, AtomicU8};
44
use std::sync::Arc;
55

66
use crossbeam_channel::{Receiver, Sender};
77

88
use crate::buffer::AudioBuffer;
9-
use crate::context::{AudioContextLatencyCategory, AudioContextOptions};
9+
use crate::context::{AudioContextLatencyCategory, AudioContextOptions, AudioContextState};
1010
use crate::events::EventDispatch;
1111
use crate::media_devices::MediaDeviceInfo;
1212
use crate::media_streams::{MediaStream, MediaStreamTrack};
@@ -26,6 +26,7 @@ mod microphone;
2626

2727
#[derive(Debug)]
2828
pub(crate) struct ControlThreadInit {
29+
pub state: Arc<AtomicU8>,
2930
pub frames_played: Arc<AtomicU64>,
3031
pub ctrl_msg_send: Sender<ControlMessage>,
3132
pub load_value_recv: Receiver<AudioRenderCapacityLoad>,
@@ -35,13 +36,17 @@ pub(crate) struct ControlThreadInit {
3536

3637
#[derive(Clone, Debug)]
3738
pub(crate) struct RenderThreadInit {
39+
pub state: Arc<AtomicU8>,
3840
pub frames_played: Arc<AtomicU64>,
3941
pub ctrl_msg_recv: Receiver<ControlMessage>,
4042
pub load_value_send: Sender<AudioRenderCapacityLoad>,
4143
pub event_send: Sender<EventDispatch>,
4244
}
4345

4446
pub(crate) fn thread_init() -> (ControlThreadInit, RenderThreadInit) {
47+
// Track audio context state - synced from render thread to control thread
48+
let state = Arc::new(AtomicU8::new(AudioContextState::Suspended as u8));
49+
4550
// Track number of frames - synced from render thread to control thread
4651
let frames_played = Arc::new(AtomicU64::new(0));
4752

@@ -61,6 +66,7 @@ pub(crate) fn thread_init() -> (ControlThreadInit, RenderThreadInit) {
6166
let (event_send, event_recv) = crossbeam_channel::bounded(256);
6267

6368
let control_thread_init = ControlThreadInit {
69+
state: Arc::clone(&state),
6470
frames_played: Arc::clone(&frames_played),
6571
ctrl_msg_send,
6672
load_value_recv,
@@ -69,6 +75,7 @@ pub(crate) fn thread_init() -> (ControlThreadInit, RenderThreadInit) {
6975
};
7076

7177
let render_thread_init = RenderThreadInit {
78+
state,
7279
frames_played,
7380
ctrl_msg_recv,
7481
load_value_send,

src/io/none.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,20 @@ impl AudioBackendManager for NoneBackend {
7171
let sample_rate = options.sample_rate.unwrap_or(48000.);
7272

7373
let RenderThreadInit {
74+
state,
7475
frames_played,
7576
ctrl_msg_recv,
7677
load_value_send,
7778
event_send,
7879
} = render_thread_init;
7980

80-
let mut render_thread =
81-
RenderThread::new(sample_rate, MAX_CHANNELS, ctrl_msg_recv, frames_played);
81+
let mut render_thread = RenderThread::new(
82+
sample_rate,
83+
MAX_CHANNELS,
84+
ctrl_msg_recv,
85+
state,
86+
frames_played,
87+
);
8288
render_thread.set_event_channels(load_value_send, event_send);
8389
render_thread.spawn_garbage_collector_thread();
8490

src/render/thread.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::any::Any;
44
use std::cell::Cell;
55

6-
use std::sync::atomic::{AtomicU64, Ordering};
6+
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
77
use std::sync::Arc;
88
use std::time::{Duration, Instant};
99

@@ -14,7 +14,9 @@ use futures::stream::StreamExt;
1414

1515
use super::AudioRenderQuantum;
1616
use crate::buffer::{AudioBuffer, AudioBufferOptions};
17-
use crate::context::{AudioNodeId, OfflineAudioContext, OfflineAudioContextCallback};
17+
use crate::context::{
18+
AudioContextState, AudioNodeId, OfflineAudioContext, OfflineAudioContextCallback,
19+
};
1820
use crate::events::EventDispatch;
1921
use crate::message::ControlMessage;
2022
use crate::node::ChannelInterpretation;
@@ -31,6 +33,7 @@ pub(crate) struct RenderThread {
3133
/// number of channels of the backend stream, i.e. sound card number of
3234
/// channels clamped to MAX_CHANNELS
3335
number_of_channels: usize,
36+
state: Arc<AtomicU8>,
3437
frames_played: Arc<AtomicU64>,
3538
receiver: Option<Receiver<ControlMessage>>,
3639
buffer_offset: Option<(usize, AudioRenderQuantum)>,
@@ -66,13 +69,15 @@ impl RenderThread {
6669
sample_rate: f32,
6770
number_of_channels: usize,
6871
receiver: Receiver<ControlMessage>,
72+
state: Arc<AtomicU8>,
6973
frames_played: Arc<AtomicU64>,
7074
) -> Self {
7175
Self {
7276
graph: None,
7377
sample_rate,
7478
buffer_size: 0,
7579
number_of_channels,
80+
state,
7681
frames_played,
7782
receiver: Some(receiver),
7883
buffer_offset: None,
@@ -150,13 +155,15 @@ impl RenderThread {
150155
self.graph.as_mut().unwrap().mark_cycle_breaker(id);
151156
}
152157
Shutdown { sender } => {
158+
self.set_state(AudioContextState::Suspended);
153159
let _ = sender.send(self.graph.take().unwrap());
154160
self.receiver = None;
155161
return; // no further handling of ctrl msgs
156162
}
157163
Startup { graph } => {
158164
debug_assert!(self.graph.is_none());
159165
self.graph = Some(graph);
166+
self.set_state(AudioContextState::Running);
160167
}
161168
NodeMessage { id, mut msg } => {
162169
self.graph.as_mut().unwrap().route_message(id, msg.as_mut());
@@ -415,10 +422,23 @@ impl RenderThread {
415422
self.handle_control_messages();
416423
}
417424
}
425+
426+
fn set_state(&self, state: AudioContextState) {
427+
self.state.store(state as u8, Ordering::Relaxed);
428+
if let Some(sender) = self.event_sender.as_ref() {
429+
sender.try_send(EventDispatch::state_change()).ok();
430+
}
431+
}
418432
}
419433

420434
impl Drop for RenderThread {
421435
fn drop(&mut self) {
436+
// If the audio graph is still there, this is an actual shutdown.
437+
// When the graph is None, this means we are only changing the sink - not shutting down.
438+
if self.graph.is_some() {
439+
self.set_state(AudioContextState::Closed);
440+
}
441+
422442
if let Some(gc) = self.garbage_collector.as_mut() {
423443
gc.push(llq::Node::new(Box::new(TerminateGarbageCollectorThread)))
424444
}

tests/online.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ fn test_none_sink_id() {
106106
assert_eq!(context.sink_id(), "none");
107107

108108
context.suspend_sync();
109-
assert_eq!(context.state(), AudioContextState::Suspended);
110109

111110
// give event thread some time to pick up events
112111
std::thread::sleep(std::time::Duration::from_millis(20));
112+
assert_eq!(context.state(), AudioContextState::Suspended);
113113
assert_eq!(state_changes.load(Ordering::Relaxed), 2); // suspended
114114

115115
context.resume_sync();
@@ -120,6 +120,7 @@ fn test_none_sink_id() {
120120
assert_eq!(state_changes.load(Ordering::Relaxed), 3); // resumed
121121

122122
context.close_sync();
123+
std::thread::sleep(std::time::Duration::from_millis(20));
123124
assert_eq!(context.state(), AudioContextState::Closed);
124125

125126
// give event thread some time to pick up events

0 commit comments

Comments
 (0)