@@ -14,6 +14,7 @@ use crate::spatial::AudioListenerParams;
14
14
use crate :: AudioListener ;
15
15
16
16
use crossbeam_channel:: { SendError , Sender } ;
17
+ use std:: collections:: HashSet ;
17
18
use std:: sync:: atomic:: { AtomicU64 , AtomicU8 , Ordering } ;
18
19
use std:: sync:: { Arc , Mutex , RwLock , RwLockWriteGuard } ;
19
20
@@ -109,6 +110,8 @@ struct ConcreteBaseAudioContextInner {
109
110
event_loop : EventLoop ,
110
111
/// Sender for events that will be handled by the EventLoop
111
112
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 ) > > ,
112
115
}
113
116
114
117
impl BaseAudioContext for ConcreteBaseAudioContext {
@@ -147,6 +150,7 @@ impl ConcreteBaseAudioContext {
147
150
state,
148
151
event_loop,
149
152
event_send,
153
+ connections : Mutex :: new ( HashSet :: new ( ) ) ,
150
154
} ;
151
155
let base = Self {
152
156
inner : Arc :: new ( base_inner) ,
@@ -283,17 +287,23 @@ impl ConcreteBaseAudioContext {
283
287
self . inner . render_channel . write ( ) . unwrap ( )
284
288
}
285
289
286
- /// Inform render thread that the control thread `AudioNode` no langer has any handles
287
290
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 ;
296
295
}
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) ;
297
307
}
298
308
299
309
/// Inform render thread that this node can act as a cycle breaker
@@ -393,6 +403,11 @@ impl ConcreteBaseAudioContext {
393
403
394
404
/// Connects the output of the `from` audio node to the input of the `to` audio node
395
405
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) ) ;
396
411
let message = ControlMessage :: ConnectNode {
397
412
from,
398
413
to,
@@ -406,6 +421,8 @@ impl ConcreteBaseAudioContext {
406
421
///
407
422
/// It is not performed immediately as the `AudioNode` is not registered at this point.
408
423
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
+
409
426
let message = ControlMessage :: ConnectNode {
410
427
from : param. registration ( ) . id ( ) ,
411
428
to : audio_node,
@@ -415,16 +432,41 @@ impl ConcreteBaseAudioContext {
415
432
self . inner . queued_messages . lock ( ) . unwrap ( ) . push ( message) ;
416
433
}
417
434
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
+ } ) ;
423
463
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
+ }
428
470
}
429
471
430
472
/// Connect the `AudioListener` to a `PannerNode`
@@ -470,6 +512,7 @@ impl ConcreteBaseAudioContext {
470
512
#[ cfg( test) ]
471
513
mod tests {
472
514
use super :: * ;
515
+ use crate :: context:: OfflineAudioContext ;
473
516
474
517
#[ test]
475
518
fn test_provide_node_id ( ) {
@@ -481,4 +524,54 @@ mod tests {
481
524
assert_eq ! ( provider. get( ) . 0 , 0 ) ; // reused
482
525
assert_eq ! ( provider. get( ) . 0 , 2 ) ; // newly assigned
483
526
}
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
+ }
484
577
}
0 commit comments