Skip to content

Commit 727e8da

Browse files
authored
Merge pull request #493 from orottier/feature/disconnect-methods
AudioNode disconnect methods
2 parents 25f46ed + 4d0553c commit 727e8da

File tree

12 files changed

+354
-113
lines changed

12 files changed

+354
-113
lines changed

examples/constant_source.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ fn main() {
3333
// left branch
3434
let gain_left = context.create_gain();
3535
gain_left.gain().set_value(0.);
36-
gain_left.connect_at(&merger, 0, 0);
36+
gain_left.connect_from_output_to_input(&merger, 0, 0);
3737

3838
let mut src_left = context.create_oscillator();
3939
src_left.frequency().set_value(200.);
@@ -43,7 +43,7 @@ fn main() {
4343
// right branch
4444
let gain_right = context.create_gain();
4545
gain_right.gain().set_value(0.);
46-
gain_right.connect_at(&merger, 0, 1);
46+
gain_right.connect_from_output_to_input(&merger, 0, 1);
4747

4848
let mut src_right = context.create_oscillator();
4949
src_right.frequency().set_value(300.);

examples/merger.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ fn main() {
4646
let merger = context.create_channel_merger(2);
4747

4848
// connect left osc to left input of the merger
49-
left.connect_at(&merger, 0, 0);
49+
left.connect_from_output_to_input(&merger, 0, 0);
5050
// connect right osc to left input of the merger
51-
right.connect_at(&merger, 0, 1);
51+
right.connect_from_output_to_input(&merger, 0, 1);
5252

5353
// Connect the merger to speakers
5454
merger.connect(&context.destination());

examples/multichannel.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fn main() {
5757
let now = context.current_time();
5858

5959
let mut osc = context.create_oscillator();
60-
osc.connect_at(&merger, 0, output_channel);
60+
osc.connect_from_output_to_input(&merger, 0, output_channel);
6161
osc.frequency().set_value(200.);
6262
osc.start_at(now);
6363
osc.stop_at(now + 1.);

src/context/concrete_base.rs

Lines changed: 111 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::spatial::AudioListenerParams;
1414
use crate::AudioListener;
1515

1616
use crossbeam_channel::{SendError, Sender};
17+
use std::collections::HashSet;
1718
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
1819
use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard};
1920

@@ -109,6 +110,8 @@ struct ConcreteBaseAudioContextInner {
109110
event_loop: EventLoop,
110111
/// Sender for events that will be handled by the EventLoop
111112
event_send: Sender<EventDispatch>,
113+
/// Current audio graph connections (from node, output port, to node, input port)
114+
connections: Mutex<HashSet<(AudioNodeId, usize, AudioNodeId, usize)>>,
112115
}
113116

114117
impl BaseAudioContext for ConcreteBaseAudioContext {
@@ -147,6 +150,7 @@ impl ConcreteBaseAudioContext {
147150
state,
148151
event_loop,
149152
event_send,
153+
connections: Mutex::new(HashSet::new()),
150154
};
151155
let base = Self {
152156
inner: Arc::new(base_inner),
@@ -283,17 +287,23 @@ impl ConcreteBaseAudioContext {
283287
self.inner.render_channel.write().unwrap()
284288
}
285289

286-
/// Inform render thread that the control thread `AudioNode` no langer has any handles
287290
pub(super) fn mark_node_dropped(&self, id: AudioNodeId) {
288-
// do not drop magic nodes
289-
let magic = id == DESTINATION_NODE_ID
290-
|| id == LISTENER_NODE_ID
291-
|| LISTENER_PARAM_IDS.contains(&id.0);
292-
293-
if !magic {
294-
let message = ControlMessage::ControlHandleDropped { id };
295-
self.send_control_msg(message);
291+
// Ignore magic nodes
292+
if id == DESTINATION_NODE_ID || id == LISTENER_NODE_ID || LISTENER_PARAM_IDS.contains(&id.0)
293+
{
294+
return;
296295
}
296+
297+
// Inform render thread that the control thread AudioNode no longer has any handles
298+
let message = ControlMessage::ControlHandleDropped { id };
299+
self.send_control_msg(message);
300+
301+
// Clear the connection administration for this node, the node id may be recycled later
302+
self.inner
303+
.connections
304+
.lock()
305+
.unwrap()
306+
.retain(|&(from, _output, to, _input)| from != id && to != id);
297307
}
298308

299309
/// Inform render thread that this node can act as a cycle breaker
@@ -393,6 +403,11 @@ impl ConcreteBaseAudioContext {
393403

394404
/// Connects the output of the `from` audio node to the input of the `to` audio node
395405
pub(crate) fn connect(&self, from: AudioNodeId, to: AudioNodeId, output: usize, input: usize) {
406+
self.inner
407+
.connections
408+
.lock()
409+
.unwrap()
410+
.insert((from, output, to, input));
396411
let message = ControlMessage::ConnectNode {
397412
from,
398413
to,
@@ -406,6 +421,8 @@ impl ConcreteBaseAudioContext {
406421
///
407422
/// It is not performed immediately as the `AudioNode` is not registered at this point.
408423
pub(super) fn queue_audio_param_connect(&self, param: &AudioParam, audio_node: AudioNodeId) {
424+
// no need to store these type of connections in self.inner.connections
425+
409426
let message = ControlMessage::ConnectNode {
410427
from: param.registration().id(),
411428
to: audio_node,
@@ -415,16 +432,41 @@ impl ConcreteBaseAudioContext {
415432
self.inner.queued_messages.lock().unwrap().push(message);
416433
}
417434

418-
/// Disconnects all outputs of the audio node that go to a specific destination node.
419-
pub(crate) fn disconnect_from(&self, from: AudioNodeId, to: AudioNodeId) {
420-
let message = ControlMessage::DisconnectNode { from, to };
421-
self.send_control_msg(message);
422-
}
435+
/// Disconnects outputs of the audio node, possibly filtered by output node, input, output.
436+
pub(crate) fn disconnect(
437+
&self,
438+
from: AudioNodeId,
439+
output: Option<usize>,
440+
to: Option<AudioNodeId>,
441+
input: Option<usize>,
442+
) {
443+
// check if the node was connected, otherwise panic
444+
let mut has_disconnected = false;
445+
let mut connections = self.inner.connections.lock().unwrap();
446+
connections.retain(|&(c_from, c_output, c_to, c_input)| {
447+
let retain = c_from != from
448+
|| c_output != output.unwrap_or(c_output)
449+
|| c_to != to.unwrap_or(c_to)
450+
|| c_input != input.unwrap_or(c_input);
451+
if !retain {
452+
has_disconnected = true;
453+
let message = ControlMessage::DisconnectNode {
454+
from,
455+
to: c_to,
456+
input: c_input,
457+
output: c_output,
458+
};
459+
self.send_control_msg(message);
460+
}
461+
retain
462+
});
423463

424-
/// Disconnects all outgoing connections from the audio node.
425-
pub(crate) fn disconnect(&self, from: AudioNodeId) {
426-
let message = ControlMessage::DisconnectAll { from };
427-
self.send_control_msg(message);
464+
// make sure to drop the MutexGuard before the panic to avoid poisoning
465+
drop(connections);
466+
467+
if !has_disconnected && to.is_some() {
468+
panic!("InvalidAccessError - attempting to disconnect unconnected nodes");
469+
}
428470
}
429471

430472
/// Connect the `AudioListener` to a `PannerNode`
@@ -470,6 +512,7 @@ impl ConcreteBaseAudioContext {
470512
#[cfg(test)]
471513
mod tests {
472514
use super::*;
515+
use crate::context::OfflineAudioContext;
473516

474517
#[test]
475518
fn test_provide_node_id() {
@@ -481,4 +524,54 @@ mod tests {
481524
assert_eq!(provider.get().0, 0); // reused
482525
assert_eq!(provider.get().0, 2); // newly assigned
483526
}
527+
528+
#[test]
529+
fn test_connect_disconnect() {
530+
let context = OfflineAudioContext::new(1, 128, 48000.);
531+
let node1 = context.create_constant_source();
532+
let node2 = context.create_gain();
533+
534+
// connection list starts empty
535+
assert!(context.base().inner.connections.lock().unwrap().is_empty());
536+
537+
node1.disconnect(); // never panic for plain disconnect calls
538+
539+
node1.connect(&node2);
540+
541+
// connection should be registered
542+
assert_eq!(context.base().inner.connections.lock().unwrap().len(), 1);
543+
544+
node1.disconnect();
545+
assert!(context.base().inner.connections.lock().unwrap().is_empty());
546+
547+
node1.connect(&node2);
548+
assert_eq!(context.base().inner.connections.lock().unwrap().len(), 1);
549+
550+
node1.disconnect_dest(&node2);
551+
assert!(context.base().inner.connections.lock().unwrap().is_empty());
552+
}
553+
554+
#[test]
555+
#[should_panic]
556+
fn test_disconnect_not_existing() {
557+
let context = OfflineAudioContext::new(1, 128, 48000.);
558+
let node1 = context.create_constant_source();
559+
let node2 = context.create_gain();
560+
561+
node1.disconnect_dest(&node2);
562+
}
563+
564+
#[test]
565+
fn test_mark_node_dropped() {
566+
let context = OfflineAudioContext::new(1, 128, 48000.);
567+
568+
let node1 = context.create_constant_source();
569+
let node2 = context.create_gain();
570+
571+
node1.connect(&node2);
572+
context.base().mark_node_dropped(node1.registration().id());
573+
574+
// dropping should clear connections administration
575+
assert!(context.base().inner.connections.lock().unwrap().is_empty());
576+
}
484577
}

src/context/offline.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,21 +459,26 @@ mod tests {
459459

460460
#[test]
461461
fn test_suspend_sync() {
462+
use crate::node::ConstantSourceNode;
463+
use std::sync::OnceLock;
464+
462465
let len = RENDER_QUANTUM_SIZE * 4;
463466
let sample_rate = 48000_f64;
464467

465468
let mut context = OfflineAudioContext::new(1, len, sample_rate as f32);
469+
static SOURCE: OnceLock<ConstantSourceNode> = OnceLock::new();
466470

467471
context.suspend_sync(RENDER_QUANTUM_SIZE as f64 / sample_rate, |context| {
468472
assert_eq!(context.state(), AudioContextState::Suspended);
469473
let mut src = context.create_constant_source();
470474
src.connect(&context.destination());
471475
src.start();
476+
SOURCE.set(src).unwrap();
472477
});
473478

474479
context.suspend_sync((3 * RENDER_QUANTUM_SIZE) as f64 / sample_rate, |context| {
475480
assert_eq!(context.state(), AudioContextState::Suspended);
476-
context.destination().disconnect();
481+
SOURCE.get().unwrap().disconnect();
477482
});
478483

479484
let output = context.start_rendering_sync();

src/message.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ pub(crate) enum ControlMessage {
2828
},
2929

3030
/// Clear the connection between two given nodes in the audio graph
31-
DisconnectNode { from: AudioNodeId, to: AudioNodeId },
32-
33-
/// Disconnect this node from the audio graph (drop all its connections)
34-
DisconnectAll { from: AudioNodeId },
31+
DisconnectNode {
32+
from: AudioNodeId,
33+
to: AudioNodeId,
34+
input: usize,
35+
output: usize,
36+
},
3537

3638
/// Notify the render thread this node is dropped in the control thread
3739
ControlHandleDropped { id: AudioNodeId },

0 commit comments

Comments
 (0)