@@ -14,6 +14,7 @@ use crate::spatial::AudioListenerParams;
1414use crate :: AudioListener ;
1515
1616use crossbeam_channel:: { SendError , Sender } ;
17+ use std:: collections:: HashSet ;
1718use std:: sync:: atomic:: { AtomicU64 , AtomicU8 , Ordering } ;
1819use 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
114117impl 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) ]
471513mod 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}
0 commit comments