Skip to content

Commit 663b8ae

Browse files
committed
After each OfflineAudioContext render quantum, spin the event loop
1 parent b3977f2 commit 663b8ae

File tree

5 files changed

+92
-55
lines changed

5 files changed

+92
-55
lines changed

src/context/concrete_base.rs

Lines changed: 4 additions & 9 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

@@ -126,13 +126,11 @@ impl ConcreteBaseAudioContext {
126126
state: Arc<AtomicU8>,
127127
frames_played: Arc<AtomicU64>,
128128
render_channel: Sender<ControlMessage>,
129-
event_channel: (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) = event_channel;
135-
136134
let audio_node_id_provider = AudioNodeIdProvider::new(node_id_consumer);
137135

138136
let base_inner = ConcreteBaseAudioContextInner {
@@ -147,7 +145,7 @@ impl ConcreteBaseAudioContext {
147145
listener_params: None,
148146
offline,
149147
state,
150-
event_loop: event_loop.clone(),
148+
event_loop,
151149
event_send,
152150
};
153151
let base = Self {
@@ -216,9 +214,6 @@ impl ConcreteBaseAudioContext {
216214
crate::node::load_hrtf_processor(sample_rate as u32);
217215
}
218216

219-
// Boot the event loop thread that handles the events spawned by the render thread
220-
event_loop.run(event_recv);
221-
222217
base
223218
}
224219

src/context/offline.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::render::RenderThread;
99
use crate::{assert_valid_sample_rate, RENDER_QUANTUM_SIZE};
1010
use crate::{Event, OfflineAudioCompletionEvent};
1111

12+
use crate::events::EventLoop;
1213
use futures_channel::{mpsc, oneshot};
1314
use futures_util::SinkExt as _;
1415

@@ -52,6 +53,8 @@ struct OfflineAudioContextRenderer {
5253
onstatechange_handler: Option<Box<dyn FnMut(Event) + Send + 'static>>,
5354
/// event handler for complete event
5455
oncomplete_handler: Option<Box<dyn FnOnce(OfflineAudioCompletionEvent) + Send + 'static>>,
56+
/// event loop to run after each render quantum
57+
event_loop: EventLoop,
5558
}
5659

5760
impl BaseAudioContext for OfflineAudioContext {
@@ -103,6 +106,7 @@ impl OfflineAudioContext {
103106
// Communication channel for events from the render thread to the control thread.
104107
// Use an unbounded channel because we do not require real-time safety.
105108
let (event_send, event_recv) = crossbeam_channel::unbounded();
109+
let event_loop = EventLoop::new(event_recv);
106110

107111
// setup the render 'thread', which will run inside the control thread
108112
let renderer = RenderThread::new(
@@ -121,7 +125,8 @@ impl OfflineAudioContext {
121125
state,
122126
frames_played,
123127
sender,
124-
(event_send, event_recv),
128+
event_send,
129+
event_loop.clone(),
125130
true,
126131
node_id_consumer,
127132
);
@@ -135,6 +140,7 @@ impl OfflineAudioContext {
135140
resume_receiver,
136141
onstatechange_handler: None,
137142
oncomplete_handler: None,
143+
event_loop,
138144
};
139145

140146
Self {
@@ -169,13 +175,14 @@ impl OfflineAudioContext {
169175
suspend_callbacks,
170176
oncomplete_handler,
171177
mut onstatechange_handler,
178+
event_loop,
172179
..
173180
} = renderer;
174181

175182
self.base.set_state(AudioContextState::Running);
176183
Self::emit_statechange(&mut onstatechange_handler);
177184

178-
let result = renderer.render_audiobuffer_sync(self.length, suspend_callbacks, self);
185+
let result = renderer.render_audiobuffer_sync(self, suspend_callbacks, event_loop);
179186

180187
self.base.set_state(AudioContextState::Closed);
181188
Self::emit_statechange(&mut onstatechange_handler);

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-
(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),

src/events.rs

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{AudioBuffer, AudioRenderCapacityEvent};
44
use std::any::Any;
55
use std::collections::HashMap;
66
use std::hash::Hash;
7+
use std::ops::ControlFlow;
78
use std::sync::{Arc, Mutex};
89

910
use crossbeam_channel::Receiver;
@@ -120,52 +121,69 @@ pub(crate) enum EventHandler {
120121
Multiple(Box<dyn FnMut(EventPayload) + Send + 'static>),
121122
}
122123

123-
#[derive(Clone, Default)]
124+
#[derive(Clone)]
124125
pub(crate) struct EventLoop {
126+
event_recv: Receiver<EventDispatch>,
125127
event_handlers: Arc<Mutex<HashMap<EventType, EventHandler>>>,
126128
}
127129

128130
impl EventLoop {
129-
pub fn new() -> Self {
130-
Self::default()
131+
pub fn new(event_recv: Receiver<EventDispatch>) -> Self {
132+
Self {
133+
event_recv,
134+
event_handlers: Default::default(),
135+
}
131136
}
132137

133-
pub fn run(&self, event_channel: Receiver<EventDispatch>) {
134-
log::debug!("Entering event loop");
135-
let self_clone = self.clone();
138+
fn handle_event(&self, mut event: EventDispatch) -> ControlFlow<()> {
139+
// Terminate the event loop when the audio context is closing
140+
let mut result = ControlFlow::Continue(());
141+
if matches!(
142+
event.payload,
143+
EventPayload::AudioContextState(AudioContextState::Closed)
144+
) {
145+
event.payload = EventPayload::None; // the statechange handler takes no argument
146+
result = ControlFlow::Break(());
147+
}
136148

137-
std::thread::spawn(move || {
138-
// This thread is dedicated to event handling so we can block
139-
for mut event in event_channel.iter() {
140-
// Terminate the event loop when the audio context is closing
141-
let mut terminate = false;
142-
if matches!(
143-
event.payload,
144-
EventPayload::AudioContextState(AudioContextState::Closed)
145-
) {
146-
event.payload = EventPayload::None; // the statechange handler takes no argument
147-
terminate = true;
149+
let mut event_handler_lock = self.event_handlers.lock().unwrap();
150+
let callback_option = event_handler_lock.remove(&event.type_);
151+
drop(event_handler_lock); // release Mutex while running callback
152+
153+
if let Some(callback) = callback_option {
154+
match callback {
155+
EventHandler::Once(f) => (f)(event.payload),
156+
EventHandler::Multiple(mut f) => {
157+
(f)(event.payload);
158+
self.event_handlers
159+
.lock()
160+
.unwrap()
161+
.insert(event.type_, EventHandler::Multiple(f));
148162
}
163+
};
164+
}
149165

150-
let mut event_handler_lock = self_clone.event_handlers.lock().unwrap();
151-
let callback_option = event_handler_lock.remove(&event.type_);
152-
drop(event_handler_lock); // release Mutex while running callback
153-
154-
if let Some(callback) = callback_option {
155-
match callback {
156-
EventHandler::Once(f) => (f)(event.payload),
157-
EventHandler::Multiple(mut f) => {
158-
(f)(event.payload);
159-
self_clone
160-
.event_handlers
161-
.lock()
162-
.unwrap()
163-
.insert(event.type_, EventHandler::Multiple(f));
164-
}
165-
};
166-
}
166+
result
167+
}
167168

168-
if terminate {
169+
pub fn handle_pending_events(&self) {
170+
// try_iter will yield all pending events, but does not block
171+
for event in self.event_recv.try_iter() {
172+
self.handle_event(event);
173+
}
174+
}
175+
176+
pub fn run_in_thread(&self) {
177+
log::debug!("Entering event thread");
178+
179+
// split borrows to help compiler
180+
let self_clone = self.clone();
181+
182+
std::thread::spawn(move || {
183+
// This thread is dedicated to event handling, so we can block
184+
for event in self_clone.event_recv.iter() {
185+
let result = self_clone.handle_event(event);
186+
if result.is_break() {
169187
break;
170188
}
171189
}

src/render/thread.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::buffer::{AudioBuffer, AudioBufferOptions};
1717
use crate::context::{
1818
AudioContextState, AudioNodeId, OfflineAudioContext, OfflineAudioContextCallback,
1919
};
20-
use crate::events::EventDispatch;
20+
use crate::events::{EventDispatch, EventLoop};
2121
use crate::message::ControlMessage;
2222
use crate::node::ChannelInterpretation;
2323
use crate::render::AudioWorkletGlobalScope;
@@ -225,10 +225,12 @@ impl RenderThread {
225225
// cf. https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-startrendering
226226
pub fn render_audiobuffer_sync(
227227
mut self,
228-
length: usize,
229-
mut suspend_callbacks: Vec<(usize, Box<OfflineAudioContextCallback>)>,
230228
context: &mut OfflineAudioContext,
229+
mut suspend_callbacks: Vec<(usize, Box<OfflineAudioContextCallback>)>,
230+
event_loop: EventLoop,
231231
) -> AudioBuffer {
232+
let length = context.length();
233+
232234
let options = AudioBufferOptions {
233235
number_of_channels: self.number_of_channels,
234236
length,
@@ -246,12 +248,14 @@ impl RenderThread {
246248
if suspend_callbacks.first().map(|&(q, _)| q) == Some(quantum) {
247249
let callback = suspend_callbacks.remove(0).1;
248250
(callback)(context);
249-
250-
// Handle addition/removal of nodes/edges
251-
self.handle_control_messages();
252251
}
253252

253+
// Handle addition/removal of nodes/edges
254+
self.handle_control_messages();
255+
254256
self.render_offline_quantum(&mut buffer);
257+
258+
event_loop.handle_pending_events();
255259
}
256260

257261
buffer

0 commit comments

Comments
 (0)