Skip to content

Commit d92903a

Browse files
authored
Merge pull request #480 from orottier/feature/offline-event-loop
Also run the EventLoop for OfflineAudioContext
2 parents 7ce4f09 + dc3fab7 commit d92903a

File tree

10 files changed

+353
-286
lines changed

10 files changed

+353
-286
lines changed

src/context/concrete_base.rs

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::spatial::AudioListenerParams;
1313

1414
use crate::AudioListener;
1515

16-
use crossbeam_channel::{Receiver, SendError, Sender};
16+
use crossbeam_channel::{SendError, Sender};
1717
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
1818
use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard};
1919

@@ -108,7 +108,7 @@ struct ConcreteBaseAudioContextInner {
108108
/// Stores the event handlers
109109
event_loop: EventLoop,
110110
/// Sender for events that will be handled by the EventLoop
111-
event_send: Option<Sender<EventDispatch>>,
111+
event_send: Sender<EventDispatch>,
112112
}
113113

114114
impl BaseAudioContext for ConcreteBaseAudioContext {
@@ -126,16 +126,11 @@ impl ConcreteBaseAudioContext {
126126
state: Arc<AtomicU8>,
127127
frames_played: Arc<AtomicU64>,
128128
render_channel: Sender<ControlMessage>,
129-
event_channel: Option<(Sender<EventDispatch>, Receiver<EventDispatch>)>,
129+
event_send: Sender<EventDispatch>,
130+
event_loop: EventLoop,
130131
offline: bool,
131132
node_id_consumer: llq::Consumer<AudioNodeId>,
132133
) -> Self {
133-
let event_loop = EventLoop::new();
134-
let (event_send, event_recv) = match event_channel {
135-
None => (None, None),
136-
Some((send, recv)) => (Some(send), Some(recv)),
137-
};
138-
139134
let audio_node_id_provider = AudioNodeIdProvider::new(node_id_consumer);
140135

141136
let base_inner = ConcreteBaseAudioContextInner {
@@ -150,7 +145,7 @@ impl ConcreteBaseAudioContext {
150145
listener_params: None,
151146
offline,
152147
state,
153-
event_loop: event_loop.clone(),
148+
event_loop,
154149
event_send,
155150
};
156151
let base = Self {
@@ -219,13 +214,6 @@ impl ConcreteBaseAudioContext {
219214
crate::node::load_hrtf_processor(sample_rate as u32);
220215
}
221216

222-
// Boot the event loop thread that handles the events spawned by the render thread
223-
// (we don't do this for offline rendering because it makes little sense, the graph cannot
224-
// be mutated once rendering has started anyway)
225-
if let Some(event_channel) = event_recv {
226-
event_loop.run(event_channel);
227-
}
228-
229217
base
230218
}
231219

@@ -288,10 +276,7 @@ impl ConcreteBaseAudioContext {
288276
}
289277

290278
pub(crate) fn send_event(&self, msg: EventDispatch) -> Result<(), SendError<EventDispatch>> {
291-
match self.inner.event_send.as_ref() {
292-
Some(s) => s.send(msg),
293-
None => Err(SendError(msg)),
294-
}
279+
self.inner.event_send.send(msg)
295280
}
296281

297282
pub(crate) fn lock_control_msg_sender(&self) -> RwLockWriteGuard<'_, Sender<ControlMessage>> {

src/context/offline.rs

Lines changed: 79 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ use std::sync::{Arc, Mutex};
55

66
use crate::buffer::AudioBuffer;
77
use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
8+
use crate::events::{
9+
Event, EventDispatch, EventHandler, EventPayload, EventType, OfflineAudioCompletionEvent,
10+
};
811
use crate::render::RenderThread;
912
use crate::{assert_valid_sample_rate, RENDER_QUANTUM_SIZE};
10-
use crate::{Event, OfflineAudioCompletionEvent};
1113

14+
use crate::events::EventLoop;
1215
use futures_channel::{mpsc, oneshot};
1316
use futures_util::SinkExt as _;
1417

@@ -48,28 +51,14 @@ struct OfflineAudioContextRenderer {
4851
suspend_callbacks: Vec<(usize, Box<OfflineAudioContextCallback>)>,
4952
/// channel to listen for `resume` calls on a suspended context
5053
resume_receiver: mpsc::Receiver<()>,
51-
/// event handler for statechange event
52-
onstatechange_handler: Option<Box<dyn FnMut(Event) + Send + 'static>>,
53-
/// event handler for complete event
54-
oncomplete_handler: Option<Box<dyn FnOnce(OfflineAudioCompletionEvent) + Send + 'static>>,
54+
/// event loop to run after each render quantum
55+
event_loop: EventLoop,
5556
}
5657

5758
impl BaseAudioContext for OfflineAudioContext {
5859
fn base(&self) -> &ConcreteBaseAudioContext {
5960
&self.base
6061
}
61-
62-
fn set_onstatechange<F: FnMut(Event) + Send + 'static>(&self, callback: F) {
63-
if let Some(renderer) = self.renderer.lock().unwrap().as_mut() {
64-
renderer.onstatechange_handler = Some(Box::new(callback));
65-
}
66-
}
67-
68-
fn clear_onstatechange(&self) {
69-
if let Some(renderer) = self.renderer.lock().unwrap().as_mut() {
70-
renderer.onstatechange_handler = None;
71-
}
72-
}
7362
}
7463

7564
impl OfflineAudioContext {
@@ -100,13 +89,19 @@ impl OfflineAudioContext {
10089
let state = Arc::new(AtomicU8::new(AudioContextState::Suspended as u8));
10190
let state_clone = Arc::clone(&state);
10291

92+
// Communication channel for events from the render thread to the control thread.
93+
// Use an unbounded channel because we do not require real-time safety.
94+
let (event_send, event_recv) = crossbeam_channel::unbounded();
95+
let event_loop = EventLoop::new(event_recv);
96+
10397
// setup the render 'thread', which will run inside the control thread
10498
let renderer = RenderThread::new(
10599
sample_rate,
106100
number_of_channels,
107101
receiver,
108102
state_clone,
109103
frames_played_clone,
104+
event_send.clone(),
110105
);
111106

112107
// first, setup the base audio context
@@ -116,7 +111,8 @@ impl OfflineAudioContext {
116111
state,
117112
frames_played,
118113
sender,
119-
None,
114+
event_send,
115+
event_loop.clone(),
120116
true,
121117
node_id_consumer,
122118
);
@@ -128,8 +124,7 @@ impl OfflineAudioContext {
128124
suspend_promises: Vec::new(),
129125
suspend_callbacks: Vec::new(),
130126
resume_receiver,
131-
onstatechange_handler: None,
132-
oncomplete_handler: None,
127+
event_loop,
133128
};
134129

135130
Self {
@@ -159,23 +154,25 @@ impl OfflineAudioContext {
159154
.unwrap()
160155
.take()
161156
.expect("InvalidStateError - Cannot call `startRendering` twice");
157+
162158
let OfflineAudioContextRenderer {
163159
renderer,
164160
suspend_callbacks,
165-
oncomplete_handler,
166-
mut onstatechange_handler,
161+
event_loop,
167162
..
168163
} = renderer;
169164

170165
self.base.set_state(AudioContextState::Running);
171-
Self::emit_statechange(&mut onstatechange_handler);
172166

173-
let result = renderer.render_audiobuffer_sync(self.length, suspend_callbacks, self);
167+
let result = renderer.render_audiobuffer_sync(self, suspend_callbacks, &event_loop);
174168

175169
self.base.set_state(AudioContextState::Closed);
176-
Self::emit_statechange(&mut onstatechange_handler);
170+
let _ = self
171+
.base
172+
.send_event(EventDispatch::complete(result.clone()));
177173

178-
Self::emit_complete(oncomplete_handler, &result);
174+
// spin the event loop once more to handle the statechange/complete events
175+
event_loop.handle_pending_events();
179176

180177
result
181178
}
@@ -204,48 +201,27 @@ impl OfflineAudioContext {
204201
renderer,
205202
suspend_promises,
206203
resume_receiver,
207-
oncomplete_handler,
208-
mut onstatechange_handler,
204+
event_loop,
209205
..
210206
} = renderer;
211207

212208
self.base.set_state(AudioContextState::Running);
213-
Self::emit_statechange(&mut onstatechange_handler);
214209

215210
let result = renderer
216-
.render_audiobuffer(self.length, suspend_promises, resume_receiver)
211+
.render_audiobuffer(self.length, suspend_promises, resume_receiver, &event_loop)
217212
.await;
218213

219214
self.base.set_state(AudioContextState::Closed);
220-
Self::emit_statechange(&mut onstatechange_handler);
215+
let _ = self
216+
.base
217+
.send_event(EventDispatch::complete(result.clone()));
221218

222-
Self::emit_complete(oncomplete_handler, &result);
219+
// spin the event loop once more to handle the statechange/complete events
220+
event_loop.handle_pending_events();
223221

224222
result
225223
}
226224

227-
fn emit_complete(
228-
oncomplete_handler: Option<Box<dyn FnOnce(OfflineAudioCompletionEvent) + Send>>,
229-
result: &AudioBuffer,
230-
) {
231-
if let Some(callback) = oncomplete_handler {
232-
let event = OfflineAudioCompletionEvent {
233-
rendered_buffer: result.clone(),
234-
event: Event { type_: "complete" },
235-
};
236-
(callback)(event);
237-
}
238-
}
239-
240-
fn emit_statechange(onstatechange_handler: &mut Option<Box<dyn FnMut(Event) + Send>>) {
241-
if let Some(callback) = onstatechange_handler.as_mut() {
242-
let event = Event {
243-
type_: "statechange",
244-
};
245-
(callback)(event);
246-
}
247-
}
248-
249225
/// get the length of rendering audio buffer
250226
// false positive: OfflineAudioContext is not const
251227
#[allow(clippy::missing_const_for_fn, clippy::unused_self)]
@@ -422,17 +398,24 @@ impl OfflineAudioContext {
422398
&self,
423399
callback: F,
424400
) {
425-
if let Some(renderer) = self.renderer.lock().unwrap().as_mut() {
426-
renderer.oncomplete_handler = Some(Box::new(callback));
427-
}
401+
let callback = move |v| match v {
402+
EventPayload::Complete(v) => {
403+
let event = OfflineAudioCompletionEvent {
404+
rendered_buffer: v,
405+
event: Event { type_: "complete" },
406+
};
407+
callback(event)
408+
}
409+
_ => unreachable!(),
410+
};
411+
412+
self.base()
413+
.set_event_handler(EventType::Complete, EventHandler::Once(Box::new(callback)));
428414
}
429415

430416
/// Unset the callback to run when the rendering has completed
431-
#[allow(clippy::missing_panics_doc)]
432417
pub fn clear_oncomplete(&self) {
433-
if let Some(renderer) = self.renderer.lock().unwrap().as_mut() {
434-
renderer.oncomplete_handler = None;
435-
}
418+
self.base().clear_event_handler(EventType::Complete);
436419
}
437420
}
438421

@@ -587,6 +570,23 @@ mod tests {
587570
assert!(changed.load(Ordering::Relaxed));
588571
}
589572

573+
#[test]
574+
fn test_onstatechange_async() {
575+
use futures::executor;
576+
577+
let context = OfflineAudioContext::new(2, 555, 44_100.);
578+
579+
let changed = Arc::new(AtomicBool::new(false));
580+
let changed_clone = Arc::clone(&changed);
581+
context.set_onstatechange(move |_event| {
582+
changed_clone.store(true, Ordering::Relaxed);
583+
});
584+
585+
let _ = executor::block_on(context.start_rendering());
586+
587+
assert!(changed.load(Ordering::Relaxed));
588+
}
589+
590590
#[test]
591591
fn test_oncomplete() {
592592
let mut context = OfflineAudioContext::new(2, 555, 44_100.);
@@ -602,4 +602,22 @@ mod tests {
602602

603603
assert!(complete.load(Ordering::Relaxed));
604604
}
605+
606+
#[test]
607+
fn test_oncomplete_async() {
608+
use futures::executor;
609+
610+
let context = OfflineAudioContext::new(2, 555, 44_100.);
611+
612+
let complete = Arc::new(AtomicBool::new(false));
613+
let complete_clone = Arc::clone(&complete);
614+
context.set_oncomplete(move |event| {
615+
assert_eq!(event.rendered_buffer.length(), 555);
616+
complete_clone.store(true, Ordering::Relaxed);
617+
});
618+
619+
let _ = executor::block_on(context.start_rendering());
620+
621+
assert!(complete.load(Ordering::Relaxed));
622+
}
605623
}

src/context/online.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::error::Error;
33
use std::sync::Mutex;
44

55
use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
6-
use crate::events::{EventDispatch, EventHandler, EventPayload, EventType};
6+
use crate::events::{EventDispatch, EventHandler, EventLoop, EventPayload, EventType};
77
use crate::io::{self, AudioBackendManager, ControlThreadInit, NoneBackend, RenderThreadInit};
88
use crate::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind};
99
use crate::media_streams::{MediaStream, MediaStreamTrack};
@@ -181,11 +181,13 @@ impl AudioContext {
181181
#[allow(clippy::needless_pass_by_value)]
182182
#[must_use]
183183
pub fn new(mut options: AudioContextOptions) -> Self {
184+
// Log, but ignore invalid sinks
184185
if !is_valid_sink_id(&options.sink_id) {
185186
log::error!("NotFoundError: invalid sinkId {:?}", options.sink_id);
186187
options.sink_id = String::from("");
187188
}
188189

190+
// Set up the audio output thread
189191
let (control_thread_init, render_thread_init) = io::thread_init();
190192
let backend = io::build_output(options, render_thread_init.clone());
191193

@@ -198,26 +200,37 @@ impl AudioContext {
198200
event_recv,
199201
} = control_thread_init;
200202

203+
// Construct the audio Graph and hand it to the render thread
201204
let (node_id_producer, node_id_consumer) = llq::Queue::new().split();
202205
let graph = Graph::new(node_id_producer);
203206
let message = ControlMessage::Startup { graph };
204207
ctrl_msg_send.send(message).unwrap();
205208

209+
// Set up the event loop thread that handles the events spawned by the render thread
210+
let event_loop = EventLoop::new(event_recv);
211+
212+
// Put everything together in the BaseAudioContext (shared with offline context)
206213
let base = ConcreteBaseAudioContext::new(
207214
backend.sample_rate(),
208215
backend.number_of_channels(),
209216
state,
210217
frames_played,
211218
ctrl_msg_send,
212-
Some((event_send, event_recv)),
219+
event_send,
220+
event_loop.clone(),
213221
false,
214222
node_id_consumer,
215223
);
216224

217-
// setup AudioRenderCapacity for this context
225+
// Setup AudioRenderCapacity for this context
218226
let base_clone = base.clone();
219227
let render_capacity = AudioRenderCapacity::new(base_clone, load_value_recv);
220228

229+
// As the final step, spawn a thread for the event loop. If we do this earlier we may miss
230+
// event handling of the initial events that are emitted right after render thread
231+
// construction.
232+
event_loop.run_in_thread();
233+
221234
Self {
222235
base,
223236
backend_manager: Mutex::new(backend),

0 commit comments

Comments
 (0)