Skip to content

Commit 3b5a485

Browse files
authored
Merge pull request #465 from orottier/feature/audioscheduledsourcenode-start
AudioScheduledSourceNode can return tail_time false when not scheduled
2 parents 64f7cc0 + 9947a73 commit 3b5a485

File tree

7 files changed

+67
-62
lines changed

7 files changed

+67
-62
lines changed

src/context/concrete_base.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ impl ConcreteBaseAudioContext {
306306
|| LISTENER_PARAM_IDS.contains(&id.0);
307307

308308
if !magic {
309-
let message = ControlMessage::FreeWhenFinished { id };
309+
let message = ControlMessage::ControlHandleDropped { id };
310310
self.send_control_msg(message);
311311
}
312312
}

src/message.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub(crate) enum ControlMessage {
3434
DisconnectAll { from: AudioNodeId },
3535

3636
/// Notify the render thread this node is dropped in the control thread
37-
FreeWhenFinished { id: AudioNodeId },
37+
ControlHandleDropped { id: AudioNodeId },
3838

3939
/// Mark node as a cycle breaker (DelayNode only)
4040
MarkCycleBreaker { id: AudioNodeId },

src/node/audio_buffer_source.rs

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ struct PlaybackInfo {
5555
k: f32,
5656
}
5757

58-
#[derive(Debug, Clone)]
58+
#[derive(Debug, Clone, Copy)]
5959
struct LoopState {
6060
pub is_looping: bool,
6161
pub start: f64,
@@ -214,7 +214,7 @@ impl AudioBufferSourceNode {
214214
buffer: None,
215215
detune: d_proc,
216216
playback_rate: pr_proc,
217-
loop_state: loop_state.clone(),
217+
loop_state,
218218
render_state: AudioBufferRendererState::default(),
219219
ended_triggered: false,
220220
};
@@ -411,39 +411,43 @@ impl AudioProcessor for AudioBufferSourceRenderer {
411411
params: AudioParamValues<'_>,
412412
scope: &AudioWorkletGlobalScope,
413413
) -> bool {
414-
// single output node
414+
// Single output node
415415
let output = &mut outputs[0];
416416

417417
let sample_rate = scope.sample_rate as f64;
418418
let dt = 1. / sample_rate;
419419
let block_duration = dt * RENDER_QUANTUM_SIZE as f64;
420420
let next_block_time = scope.current_time + block_duration;
421421

422-
let LoopState {
423-
is_looping,
424-
start: loop_start,
425-
end: loop_end,
426-
} = self.loop_state.clone();
427-
428-
// these will only be used if `loop_` is true, so no need for `Option`
429-
let mut actual_loop_start = 0.;
430-
let mut actual_loop_end = 0.;
431-
432-
// return early if start_time is beyond this block
422+
// Return early if start_time is beyond this block
433423
if self.start_time >= next_block_time {
434424
output.make_silent();
435-
return true;
425+
// #462 AudioScheduledSourceNodes that have not been scheduled to start can safely
426+
// return tail_time false in order to be collected if their control handle drops.
427+
return self.start_time != f64::MAX;
436428
}
437429

438430
// If the buffer has not been set wait for it.
439431
let buffer = match &self.buffer {
440432
None => {
441433
output.make_silent();
442-
return true;
434+
// #462 like the above arm, we can safely return tail_time false if this node has
435+
// no buffer set.
436+
return false;
443437
}
444438
Some(b) => b,
445439
};
446440

441+
let LoopState {
442+
is_looping,
443+
start: loop_start,
444+
end: loop_end,
445+
} = self.loop_state;
446+
447+
// these will only be used if `loop_` is true, so no need for `Option`
448+
let mut actual_loop_start = 0.;
449+
let mut actual_loop_end = 0.;
450+
447451
// compute compound parameter at k-rate, these parameters have constraints
448452
// https://webaudio.github.io/web-audio-api/#audioparam-automation-rate-constraints
449453
let detune = params.get(&self.detune)[0];
@@ -456,15 +460,20 @@ impl AudioProcessor for AudioBufferSourceRenderer {
456460
// we just linearly interpolate, thus favoring performance vs quality
457461
let sampling_ratio = buffer.sample_rate() as f64 / sample_rate;
458462

459-
// In addition, if the buffer has more than one channel, then the
460-
// AudioBufferSourceNode output must change to a single channel of silence
461-
// at the beginning of a render quantum after the time at which any one of
462-
// the following conditions holds:
463+
// Load the buffer time from the render state.
464+
// The render state has to be updated before leaving this method!
465+
let mut buffer_time = self.render_state.buffer_time.load(Ordering::Relaxed);
463466

467+
// The output must change to a single channel of silence at the beginning of a render
468+
// quantum after the time at which any one of the following conditions holds:
464469
// 1. the stop time has been reached.
465470
// 2. the duration has been reached.
471+
// 3. the end of the buffer has been reached.
466472
if scope.current_time >= self.stop_time
467473
|| self.render_state.buffer_time_elapsed >= self.duration
474+
|| !is_looping
475+
&& (computed_playback_rate > 0. && buffer_time >= buffer_duration
476+
|| computed_playback_rate < 0. && buffer_time < 0.)
468477
{
469478
output.make_silent(); // also converts to mono
470479

@@ -477,31 +486,6 @@ impl AudioProcessor for AudioBufferSourceRenderer {
477486
return false;
478487
}
479488

480-
// Load the buffer time from the render state.
481-
// The render state has to be updated before leaving this method!
482-
let mut buffer_time = self.render_state.buffer_time.load(Ordering::Relaxed);
483-
484-
// 3. the end of the buffer has been reached.
485-
if !is_looping {
486-
if computed_playback_rate > 0. && buffer_time >= buffer_duration {
487-
output.make_silent(); // also converts to mono
488-
if !self.ended_triggered {
489-
scope.send_ended_event();
490-
self.ended_triggered = true;
491-
}
492-
return false;
493-
}
494-
495-
if computed_playback_rate < 0. && buffer_time < 0. {
496-
output.make_silent(); // also converts to mono
497-
if !self.ended_triggered {
498-
scope.send_ended_event();
499-
self.ended_triggered = true;
500-
}
501-
return false;
502-
}
503-
}
504-
505489
output.set_number_of_channels(buffer.number_of_channels());
506490

507491
// go through the algorithm described in the spec

src/node/constant_source.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ impl AudioProcessor for ConstantSourceRenderer {
192192

193193
if self.start_time >= next_block_time {
194194
output.make_silent();
195-
return true;
195+
// #462 AudioScheduledSourceNodes that have not been scheduled to start can safely
196+
// return tail_time false in order to be collected if their control handle drops.
197+
return self.start_time != f64::MAX;
196198
}
197199

198200
output.force_mono();
@@ -314,6 +316,23 @@ mod tests {
314316
assert_float_eq!(channel[128..], vec![1.; 128][..], abs_all <= 0.);
315317
}
316318

319+
#[test]
320+
fn test_start_in_the_future_while_dropped() {
321+
let sample_rate = 48000.;
322+
let mut context = OfflineAudioContext::new(1, 4 * 128, sample_rate);
323+
324+
let mut src = context.create_constant_source();
325+
src.connect(&context.destination());
326+
src.start_at(258. / sample_rate as f64); // in 3rd block
327+
drop(src); // explicit drop
328+
329+
let buffer = context.start_rendering_sync();
330+
let channel = buffer.get_channel_data(0);
331+
332+
assert_float_eq!(channel[0..258], vec![0.; 258][..], abs_all <= 0.);
333+
assert_float_eq!(channel[258..], vec![1.; 254][..], abs_all <= 0.);
334+
}
335+
317336
#[test]
318337
#[should_panic]
319338
fn test_start_twice() {

src/node/oscillator.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,9 @@ impl AudioProcessor for OscillatorRenderer {
373373

374374
if self.start_time >= next_block_time {
375375
output.make_silent();
376-
return true;
376+
// #462 AudioScheduledSourceNodes that have not been scheduled to start can safely
377+
// return tail_time false in order to be collected if their control handle drops.
378+
return self.start_time != f64::MAX;
377379
} else if self.stop_time < scope.current_time {
378380
output.make_silent();
379381

src/render/graph.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub struct Node {
5454
/// Outgoing edges: tuple of outcoming node reference, our output index and their input index
5555
outgoing_edges: SmallVec<[OutgoingEdge; 2]>,
5656
/// Indicates if the control thread has dropped this Node
57-
free_when_finished: bool,
57+
control_handle_dropped: bool,
5858
/// Indicates if the node has any incoming connections (for lifecycle management)
5959
has_inputs_connected: bool,
6060
/// Indicates if the node can act as a cycle breaker (only DelayNode for now)
@@ -68,7 +68,7 @@ impl std::fmt::Debug for Node {
6868
.field("processor", &self.processor)
6969
.field("channel_config", &self.channel_config)
7070
.field("outgoing_edges", &self.outgoing_edges)
71-
.field("free_when_finished", &self.free_when_finished)
71+
.field("control_handle_dropped", &self.control_handle_dropped)
7272
.field("cycle_breaker", &self.cycle_breaker)
7373
.finish_non_exhaustive()
7474
}
@@ -85,7 +85,7 @@ impl Node {
8585
fn can_free(&self, tail_time: bool) -> bool {
8686
// Only drop when the Control thread has dropped its handle.
8787
// Otherwise the node can be reconnected/restarted etc.
88-
if !self.free_when_finished {
88+
if !self.control_handle_dropped {
8989
return false;
9090
}
9191

@@ -185,7 +185,7 @@ impl Graph {
185185
outputs,
186186
channel_config,
187187
outgoing_edges: smallvec![],
188-
free_when_finished: false,
188+
control_handle_dropped: false,
189189
has_inputs_connected: false,
190190
cycle_breaker: false,
191191
}),
@@ -231,12 +231,12 @@ impl Graph {
231231
self.ordered.clear(); // void current ordering
232232
}
233233

234-
pub fn mark_free_when_finished(&mut self, index: AudioNodeId) {
234+
pub fn mark_control_handle_dropped(&mut self, index: AudioNodeId) {
235235
// Issue #92, a race condition can occur for AudioParams. They may have already been
236236
// removed from the audio graph if the node they feed into was dropped.
237237
// Therefore, do not assume this node still exists:
238238
if let Some(node) = self.nodes.get_mut(index) {
239-
node.get_mut().free_when_finished = true;
239+
node.get_mut().control_handle_dropped = true;
240240
}
241241
}
242242

@@ -509,7 +509,7 @@ impl Graph {
509509
// - special node (destination = id 0, listener = id 1), or
510510
// - not connected to this dropped node, or
511511
// - if the control thread still has a handle to it.
512-
let retain = id.0 < 2 || !was_connected || !node.free_when_finished;
512+
let retain = id.0 < 2 || !was_connected || !node.control_handle_dropped;
513513

514514
if !retain {
515515
self.reclaim_id_channel
@@ -735,7 +735,7 @@ mod tests {
735735
// dropped and the AudioNodeId(3) should be reclaimed
736736
add_node(&mut graph, 2, node.clone());
737737
// Mark the node as 'detached from the control thread', so it is allowed to drop
738-
graph.nodes[AudioNodeId(2)].get_mut().free_when_finished = true;
738+
graph.nodes[AudioNodeId(2)].get_mut().control_handle_dropped = true;
739739

740740
// Connect the regular node to the AudioDestinationNode
741741
add_edge(&mut graph, 2, 0);
@@ -777,7 +777,7 @@ mod tests {
777777
// dropped and the AudioNodeId(3) should be reclaimed
778778
add_node(&mut graph, 2, node.clone());
779779
// Mark the node as 'detached from the control thread', so it is allowed to drop
780-
graph.nodes[AudioNodeId(2)].get_mut().free_when_finished = true;
780+
graph.nodes[AudioNodeId(2)].get_mut().control_handle_dropped = true;
781781

782782
// Connect the regular node to the AudioDestinationNode
783783
add_edge(&mut graph, 2, 0);
@@ -786,7 +786,7 @@ mod tests {
786786
let param = Box::new(TestNode { tail_time: true }); // audio params have tail time true
787787
add_node(&mut graph, 3, param);
788788
// Mark the node as 'detached from the control thread', so it is allowed to drop
789-
graph.nodes[AudioNodeId(3)].get_mut().free_when_finished = true;
789+
graph.nodes[AudioNodeId(3)].get_mut().control_handle_dropped = true;
790790

791791
// Connect the audioparam to the regular node
792792
add_audioparam(&mut graph, 3, 2);
@@ -828,7 +828,7 @@ mod tests {
828828
add_node(&mut graph, 2, node);
829829

830830
// Mark the node as 'detached from the control thread', so it is allowed to drop
831-
graph.nodes[AudioNodeId(2)].get_mut().free_when_finished = true;
831+
graph.nodes[AudioNodeId(2)].get_mut().control_handle_dropped = true;
832832

833833
// Render a single quantum
834834
let scope = AudioWorkletGlobalScope {

src/render/thread.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ impl RenderThread {
150150
DisconnectAll { from } => {
151151
self.graph.as_mut().unwrap().remove_edges_from(from);
152152
}
153-
FreeWhenFinished { id } => {
154-
self.graph.as_mut().unwrap().mark_free_when_finished(id);
153+
ControlHandleDropped { id } => {
154+
self.graph.as_mut().unwrap().mark_control_handle_dropped(id);
155155
}
156156
MarkCycleBreaker { id } => {
157157
self.graph.as_mut().unwrap().mark_cycle_breaker(id);

0 commit comments

Comments
 (0)