Skip to content

Commit 26b41da

Browse files
committed
Introduce AudioProcessor::before_drop pre-destructor hook
This allows the processor to send some final events, or move some items to the garbage collector thread. In this commit we use the before_drop hook to ensure that the ended event is sent for all AudioScheduledSourceNodes when the stop time is implicit. This fixes #489
1 parent 6eb3801 commit 26b41da

File tree

7 files changed

+280
-2
lines changed

7 files changed

+280
-2
lines changed

src/node/audio_buffer_source.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,13 +771,23 @@ impl AudioProcessor for AudioBufferSourceRenderer {
771771

772772
log::warn!("AudioBufferSourceRenderer: Dropping incoming message {msg:?}");
773773
}
774+
775+
fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
776+
if !self.ended_triggered && scope.current_time >= self.start_time {
777+
scope.send_ended_event();
778+
self.ended_triggered = true;
779+
}
780+
}
774781
}
775782

776783
#[cfg(test)]
777784
mod tests {
778785
use float_eq::assert_float_eq;
779786
use std::f32::consts::PI;
780787

788+
use std::sync::atomic::{AtomicBool, Ordering};
789+
use std::sync::Arc;
790+
781791
use crate::context::{BaseAudioContext, OfflineAudioContext};
782792
use crate::RENDER_QUANTUM_SIZE;
783793

@@ -1458,4 +1468,72 @@ mod tests {
14581468
src.stop();
14591469
src.stop();
14601470
}
1471+
1472+
#[test]
1473+
fn test_ended_event() {
1474+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1475+
let mut src = context.create_buffer_source();
1476+
src.start_at(0.);
1477+
src.stop_at(0.5);
1478+
1479+
let ended = Arc::new(AtomicBool::new(false));
1480+
let ended_clone = Arc::clone(&ended);
1481+
src.set_onended(move |_event| {
1482+
ended_clone.store(true, Ordering::Relaxed);
1483+
});
1484+
1485+
let _ = context.start_rendering_sync();
1486+
assert!(ended.load(Ordering::Relaxed));
1487+
}
1488+
1489+
#[test]
1490+
fn test_no_ended_event() {
1491+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1492+
let src = context.create_constant_source();
1493+
1494+
// do not start the node
1495+
1496+
let ended = Arc::new(AtomicBool::new(false));
1497+
let ended_clone = Arc::clone(&ended);
1498+
src.set_onended(move |_event| {
1499+
ended_clone.store(true, Ordering::Relaxed);
1500+
});
1501+
1502+
let _ = context.start_rendering_sync();
1503+
assert!(!ended.load(Ordering::Relaxed)); // should not have triggered
1504+
}
1505+
1506+
#[test]
1507+
fn test_exact_ended_event() {
1508+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1509+
let mut src = context.create_buffer_source();
1510+
src.start_at(0.);
1511+
src.stop_at(1.); // end right at the end of the offline buffer
1512+
1513+
let ended = Arc::new(AtomicBool::new(false));
1514+
let ended_clone = Arc::clone(&ended);
1515+
src.set_onended(move |_event| {
1516+
ended_clone.store(true, Ordering::Relaxed);
1517+
});
1518+
1519+
let _ = context.start_rendering_sync();
1520+
assert!(ended.load(Ordering::Relaxed));
1521+
}
1522+
1523+
#[test]
1524+
fn test_implicit_ended_event() {
1525+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1526+
let mut src = context.create_buffer_source();
1527+
src.start_at(0.);
1528+
// no explicit stop, so we stop at end of offline context
1529+
1530+
let ended = Arc::new(AtomicBool::new(false));
1531+
let ended_clone = Arc::clone(&ended);
1532+
src.set_onended(move |_event| {
1533+
ended_clone.store(true, Ordering::Relaxed);
1534+
});
1535+
1536+
let _ = context.start_rendering_sync();
1537+
assert!(ended.load(Ordering::Relaxed));
1538+
}
14611539
}

src/node/constant_source.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,13 @@ impl AudioProcessor for ConstantSourceRenderer {
255255

256256
log::warn!("ConstantSourceRenderer: Dropping incoming message {msg:?}");
257257
}
258+
259+
fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
260+
if !self.ended_triggered && scope.current_time >= self.start_time {
261+
scope.send_ended_event();
262+
self.ended_triggered = true;
263+
}
264+
}
258265
}
259266

260267
#[cfg(test)]
@@ -264,6 +271,9 @@ mod tests {
264271

265272
use float_eq::assert_float_eq;
266273

274+
use std::sync::atomic::{AtomicBool, Ordering};
275+
use std::sync::Arc;
276+
267277
use super::*;
268278

269279
#[test]
@@ -369,4 +379,72 @@ mod tests {
369379
src.stop();
370380
src.stop();
371381
}
382+
383+
#[test]
384+
fn test_ended_event() {
385+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
386+
let mut src = context.create_constant_source();
387+
src.start_at(0.);
388+
src.stop_at(0.5);
389+
390+
let ended = Arc::new(AtomicBool::new(false));
391+
let ended_clone = Arc::clone(&ended);
392+
src.set_onended(move |_event| {
393+
ended_clone.store(true, Ordering::Relaxed);
394+
});
395+
396+
let _ = context.start_rendering_sync();
397+
assert!(ended.load(Ordering::Relaxed));
398+
}
399+
400+
#[test]
401+
fn test_no_ended_event() {
402+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
403+
let src = context.create_constant_source();
404+
405+
// do not start the node
406+
407+
let ended = Arc::new(AtomicBool::new(false));
408+
let ended_clone = Arc::clone(&ended);
409+
src.set_onended(move |_event| {
410+
ended_clone.store(true, Ordering::Relaxed);
411+
});
412+
413+
let _ = context.start_rendering_sync();
414+
assert!(!ended.load(Ordering::Relaxed)); // should not have triggered
415+
}
416+
417+
#[test]
418+
fn test_exact_ended_event() {
419+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
420+
let mut src = context.create_constant_source();
421+
src.start_at(0.);
422+
src.stop_at(1.); // end right at the end of the offline buffer
423+
424+
let ended = Arc::new(AtomicBool::new(false));
425+
let ended_clone = Arc::clone(&ended);
426+
src.set_onended(move |_event| {
427+
ended_clone.store(true, Ordering::Relaxed);
428+
});
429+
430+
let _ = context.start_rendering_sync();
431+
assert!(ended.load(Ordering::Relaxed));
432+
}
433+
434+
#[test]
435+
fn test_implicit_ended_event() {
436+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
437+
let mut src = context.create_constant_source();
438+
src.start_at(0.);
439+
// no explicit stop, so we stop at end of offline context
440+
441+
let ended = Arc::new(AtomicBool::new(false));
442+
let ended_clone = Arc::clone(&ended);
443+
src.set_onended(move |_event| {
444+
ended_clone.store(true, Ordering::Relaxed);
445+
});
446+
447+
let _ = context.start_rendering_sync();
448+
assert!(ended.load(Ordering::Relaxed));
449+
}
372450
}

src/node/oscillator.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,13 @@ impl AudioProcessor for OscillatorRenderer {
457457

458458
log::warn!("OscillatorRenderer: Dropping incoming message {msg:?}");
459459
}
460+
461+
fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
462+
if !self.ended_triggered && scope.current_time >= self.start_time {
463+
scope.send_ended_event();
464+
self.ended_triggered = true;
465+
}
466+
}
460467
}
461468

462469
impl OscillatorRenderer {
@@ -608,6 +615,9 @@ mod tests {
608615
use float_eq::assert_float_eq;
609616
use std::f64::consts::PI;
610617

618+
use std::sync::atomic::{AtomicBool, Ordering};
619+
use std::sync::Arc;
620+
611621
use crate::context::{BaseAudioContext, OfflineAudioContext};
612622
use crate::node::{AudioNode, AudioScheduledSourceNode};
613623
use crate::periodic_wave::{PeriodicWave, PeriodicWaveOptions};
@@ -1247,4 +1257,72 @@ mod tests {
12471257
osc.stop();
12481258
osc.stop();
12491259
}
1260+
1261+
#[test]
1262+
fn test_ended_event() {
1263+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1264+
let mut src = context.create_oscillator();
1265+
src.start_at(0.);
1266+
src.stop_at(0.5);
1267+
1268+
let ended = Arc::new(AtomicBool::new(false));
1269+
let ended_clone = Arc::clone(&ended);
1270+
src.set_onended(move |_event| {
1271+
ended_clone.store(true, Ordering::Relaxed);
1272+
});
1273+
1274+
let _ = context.start_rendering_sync();
1275+
assert!(ended.load(Ordering::Relaxed));
1276+
}
1277+
1278+
#[test]
1279+
fn test_no_ended_event() {
1280+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1281+
let src = context.create_oscillator();
1282+
1283+
// do not start the node
1284+
1285+
let ended = Arc::new(AtomicBool::new(false));
1286+
let ended_clone = Arc::clone(&ended);
1287+
src.set_onended(move |_event| {
1288+
ended_clone.store(true, Ordering::Relaxed);
1289+
});
1290+
1291+
let _ = context.start_rendering_sync();
1292+
assert!(!ended.load(Ordering::Relaxed)); // should not have triggered
1293+
}
1294+
1295+
#[test]
1296+
fn test_exact_ended_event() {
1297+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1298+
let mut src = context.create_oscillator();
1299+
src.start_at(0.);
1300+
src.stop_at(1.); // end right at the end of the offline buffer
1301+
1302+
let ended = Arc::new(AtomicBool::new(false));
1303+
let ended_clone = Arc::clone(&ended);
1304+
src.set_onended(move |_event| {
1305+
ended_clone.store(true, Ordering::Relaxed);
1306+
});
1307+
1308+
let _ = context.start_rendering_sync();
1309+
assert!(ended.load(Ordering::Relaxed));
1310+
}
1311+
1312+
#[test]
1313+
fn test_implicit_ended_event() {
1314+
let mut context = OfflineAudioContext::new(2, 44_100, 44_100.);
1315+
let mut src = context.create_oscillator();
1316+
src.start_at(0.);
1317+
// no explicit stop, so we stop at end of offline context
1318+
1319+
let ended = Arc::new(AtomicBool::new(false));
1320+
let ended_clone = Arc::clone(&ended);
1321+
src.set_onended(move |_event| {
1322+
ended_clone.store(true, Ordering::Relaxed);
1323+
});
1324+
1325+
let _ = context.start_rendering_sync();
1326+
assert!(ended.load(Ordering::Relaxed));
1327+
}
12501328
}

src/render/graph.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ impl Graph {
503503
let mut node = self.nodes.remove(*index).into_inner();
504504
self.reclaim_id_channel
505505
.push(node.reclaim_id.take().unwrap());
506+
node.processor.before_drop(scope);
506507
drop(node);
507508

508509
// And remove it from the ordering after we have processed all nodes
@@ -536,6 +537,13 @@ impl Graph {
536537
// Return the output buffer of destination node
537538
&self.nodes.get_unchecked_mut(AudioNodeId(0)).outputs[0]
538539
}
540+
541+
pub fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
542+
self.nodes.iter_mut().for_each(|(id, node)| {
543+
scope.node_id.set(id);
544+
node.get_mut().processor.before_drop(scope);
545+
});
546+
}
539547
}
540548

541549
#[cfg(test)]

src/render/node_collection.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ impl NodeCollection {
5757
self.nodes.iter_mut().filter_map(Option::as_mut)
5858
}
5959

60+
#[inline(always)]
61+
pub fn iter_mut(&mut self) -> impl Iterator<Item = (AudioNodeId, &mut RefCell<Node>)> {
62+
self.nodes
63+
.iter_mut()
64+
.enumerate()
65+
.filter_map(|(i, v)| v.as_mut().map(|m| (AudioNodeId(i as u64), m)))
66+
}
67+
6068
#[inline(always)]
6169
pub fn contains(&self, index: AudioNodeId) -> bool {
6270
self.nodes[index.0 as usize].is_some()

src/render/processor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ pub trait AudioProcessor: Send {
172172
fn has_side_effects(&self) -> bool {
173173
false
174174
}
175+
176+
fn before_drop(&mut self, _scope: &AudioWorkletGlobalScope) {}
175177
}
176178

177179
impl std::fmt::Debug for dyn AudioProcessor {

0 commit comments

Comments
 (0)