@@ -279,21 +279,26 @@ impl AudioContext {
279279 /// is currently not implemented.
280280 #[ allow( clippy:: needless_collect, clippy:: missing_panics_doc) ]
281281 pub fn set_sink_id_sync ( & self , sink_id : String ) -> Result < ( ) , Box < dyn Error > > {
282+ log:: debug!( "SinkChange requested" ) ;
282283 if self . sink_id ( ) == sink_id {
284+ log:: debug!( "SinkChange: no-op" ) ;
283285 return Ok ( ( ) ) ; // sink is already active
284286 }
285287
286288 if !is_valid_sink_id ( & sink_id) {
287289 Err ( format ! ( "NotFoundError: invalid sinkId {sink_id}" ) ) ?;
288290 } ;
289291
292+ log:: debug!( "SinkChange: locking backend manager" ) ;
290293 let mut backend_manager_guard = self . backend_manager . lock ( ) . unwrap ( ) ;
291294 let original_state = self . state ( ) ;
292295 if original_state == AudioContextState :: Closed {
296+ log:: debug!( "SinkChange: context is closed" ) ;
293297 return Ok ( ( ) ) ;
294298 }
295299
296300 // Acquire exclusive lock on ctrl msg sender
301+ log:: debug!( "SinkChange: locking message channel" ) ;
297302 let ctrl_msg_send = self . base . lock_control_msg_sender ( ) ;
298303
299304 // Flush out the ctrl msg receiver, cache
@@ -303,13 +308,17 @@ impl AudioContext {
303308 let graph = if matches ! ( pending_msgs. first( ) , Some ( ControlMessage :: Startup { .. } ) ) {
304309 // Handle the edge case where the previous backend was suspended for its entire lifetime.
305310 // In this case, the `Startup` control message was never processed.
311+ log:: debug!( "SinkChange: recover unstarted graph" ) ;
312+
306313 let msg = pending_msgs. remove ( 0 ) ;
307314 match msg {
308315 ControlMessage :: Startup { graph } => graph,
309316 _ => unreachable ! ( ) ,
310317 }
311318 } else {
312319 // Acquire the audio graph from the current render thread, shutting it down
320+ log:: debug!( "SinkChange: recover graph from render thread" ) ;
321+
313322 let ( graph_send, graph_recv) = crossbeam_channel:: bounded ( 1 ) ;
314323 let message = ControlMessage :: CloseAndRecycle { sender : graph_send } ;
315324 ctrl_msg_send. send ( message) . unwrap ( ) ;
@@ -321,6 +330,7 @@ impl AudioContext {
321330 graph_recv. recv ( ) . unwrap ( )
322331 } ;
323332
333+ log:: debug!( "SinkChange: closing audio stream" ) ;
324334 backend_manager_guard. close ( ) ;
325335
326336 // hotswap the backend
@@ -330,10 +340,12 @@ impl AudioContext {
330340 sink_id,
331341 render_size_hint : AudioContextRenderSizeCategory :: default ( ) , // todo reuse existing setting
332342 } ;
343+ log:: debug!( "SinkChange: starting audio stream" ) ;
333344 * backend_manager_guard = io:: build_output ( options, self . render_thread_init . clone ( ) ) ;
334345
335346 // if the previous backend state was suspend, suspend the new one before shipping the graph
336347 if original_state == AudioContextState :: Suspended {
348+ log:: debug!( "SinkChange: suspending audio stream" ) ;
337349 backend_manager_guard. suspend ( ) ;
338350 }
339351
@@ -352,6 +364,7 @@ impl AudioContext {
352364 // trigger event when all the work is done
353365 let _ = self . base . send_event ( EventDispatch :: sink_change ( ) ) ;
354366
367+ log:: debug!( "SinkChange: done" ) ;
355368 Ok ( ( ) )
356369 }
357370
@@ -422,18 +435,30 @@ impl AudioContext {
422435 /// * The audio device is not available
423436 /// * For a `BackendSpecificError`
424437 pub async fn suspend ( & self ) {
425- // First, pause rendering via a control message
438+ // Don't lock the backend manager because we can't hold is across the await point
439+ log:: debug!( "Suspend called" ) ;
440+
441+ if self . state ( ) != AudioContextState :: Running {
442+ log:: debug!( "Suspend no-op - context is not running" ) ;
443+ return ;
444+ }
445+
446+ // Pause rendering via a control message
426447 let ( sender, receiver) = oneshot:: channel ( ) ;
427448 let notify = OneshotNotify :: Async ( sender) ;
428449 self . base
429450 . send_control_msg ( ControlMessage :: Suspend { notify } ) ;
430451
431452 // Wait for the render thread to have processed the suspend message.
432453 // The AudioContextState will be updated by the render thread.
454+ log:: debug!( "Suspending audio graph, waiting for signal.." ) ;
433455 receiver. await . unwrap ( ) ;
434456
435457 // Then ask the audio host to suspend the stream
458+ log:: debug!( "Suspended audio graph. Suspending audio stream.." ) ;
436459 self . backend_manager . lock ( ) . unwrap ( ) . suspend ( ) ;
460+
461+ log:: debug!( "Suspended audio stream" ) ;
437462 }
438463
439464 /// Resumes the progression of time in an audio context that has previously been
@@ -445,19 +470,34 @@ impl AudioContext {
445470 ///
446471 /// * The audio device is not available
447472 /// * For a `BackendSpecificError`
473+ #[ allow( clippy:: await_holding_lock) ] // false positive due to explicit drop
448474 pub async fn resume ( & self ) {
449- // First ask the audio host to resume the stream
450- self . backend_manager . lock ( ) . unwrap ( ) . resume ( ) ;
475+ // Lock the backend manager mutex to avoid concurrent calls
476+ log:: debug!( "Resume called, locking backend manager" ) ;
477+ let backend_manager_guard = self . backend_manager . lock ( ) . unwrap ( ) ;
478+
479+ if self . state ( ) != AudioContextState :: Suspended {
480+ log:: debug!( "Resume no-op - context is not suspended" ) ;
481+ return ;
482+ }
483+
484+ // Ask the audio host to resume the stream
485+ backend_manager_guard. resume ( ) ;
451486
452487 // Then, ask to resume rendering via a control message
488+ log:: debug!( "Resumed audio stream, waking audio graph" ) ;
453489 let ( sender, receiver) = oneshot:: channel ( ) ;
454490 let notify = OneshotNotify :: Async ( sender) ;
455491 self . base
456492 . send_control_msg ( ControlMessage :: Resume { notify } ) ;
457493
494+ // Drop the Mutex guard so we won't hold it across an await
495+ drop ( backend_manager_guard) ;
496+
458497 // Wait for the render thread to have processed the resume message
459498 // The AudioContextState will be updated by the render thread.
460499 receiver. await . unwrap ( ) ;
500+ log:: debug!( "Resumed audio graph" ) ;
461501 }
462502
463503 /// Closes the `AudioContext`, releasing the system resources being used.
@@ -469,22 +509,37 @@ impl AudioContext {
469509 ///
470510 /// Will panic when this function is called multiple times
471511 pub async fn close ( & self ) {
472- // First, stop rendering via a control message
473- let ( sender, receiver) = oneshot:: channel ( ) ;
474- let notify = OneshotNotify :: Async ( sender) ;
475- self . base . send_control_msg ( ControlMessage :: Close { notify } ) ;
512+ // Don't lock the backend manager because we can't hold is across the await point
513+ log:: debug!( "Close called" ) ;
476514
477- // Wait for the render thread to have processed the suspend message.
478- // The AudioContextState will be updated by the render thread.
479- receiver. await . unwrap ( ) ;
515+ if self . state ( ) == AudioContextState :: Closed {
516+ log:: debug!( "Close no-op - context is already closed" ) ;
517+ return ;
518+ }
519+
520+ if self . state ( ) == AudioContextState :: Running {
521+ // First, stop rendering via a control message
522+ let ( sender, receiver) = oneshot:: channel ( ) ;
523+ let notify = OneshotNotify :: Async ( sender) ;
524+ self . base . send_control_msg ( ControlMessage :: Close { notify } ) ;
525+
526+ // Wait for the render thread to have processed the suspend message.
527+ // The AudioContextState will be updated by the render thread.
528+ log:: debug!( "Suspending audio graph, waiting for signal.." ) ;
529+ receiver. await . unwrap ( ) ;
530+ } else {
531+ // if the context is not running, change the state manually
532+ self . base . set_state ( AudioContextState :: Closed ) ;
533+ }
480534
481535 // Then ask the audio host to close the stream
536+ log:: debug!( "Suspended audio graph. Closing audio stream.." ) ;
482537 self . backend_manager . lock ( ) . unwrap ( ) . close ( ) ;
483538
484539 // Stop the AudioRenderCapacity collection thread
485540 self . render_capacity . stop ( ) ;
486541
487- // TODO stop the event loop <https://github.com/orottier/web- audio-api-rs/issues/421>
542+ log :: debug! ( "Closed audio stream" ) ;
488543 }
489544
490545 /// Suspends the progression of time in the audio context.
@@ -502,18 +557,31 @@ impl AudioContext {
502557 /// * The audio device is not available
503558 /// * For a `BackendSpecificError`
504559 pub fn suspend_sync ( & self ) {
505- // First, pause rendering via a control message
560+ // Lock the backend manager mutex to avoid concurrent calls
561+ log:: debug!( "Suspend_sync called, locking backend manager" ) ;
562+ let backend_manager_guard = self . backend_manager . lock ( ) . unwrap ( ) ;
563+
564+ if self . state ( ) != AudioContextState :: Running {
565+ log:: debug!( "Suspend_sync no-op - context is not running" ) ;
566+ return ;
567+ }
568+
569+ // Pause rendering via a control message
506570 let ( sender, receiver) = crossbeam_channel:: bounded ( 0 ) ;
507571 let notify = OneshotNotify :: Sync ( sender) ;
508572 self . base
509573 . send_control_msg ( ControlMessage :: Suspend { notify } ) ;
510574
511575 // Wait for the render thread to have processed the suspend message.
512576 // The AudioContextState will be updated by the render thread.
577+ log:: debug!( "Suspending audio graph, waiting for signal.." ) ;
513578 receiver. recv ( ) . ok ( ) ;
514579
515580 // Then ask the audio host to suspend the stream
516- self . backend_manager . lock ( ) . unwrap ( ) . suspend ( ) ;
581+ log:: debug!( "Suspended audio graph. Suspending audio stream.." ) ;
582+ backend_manager_guard. suspend ( ) ;
583+
584+ log:: debug!( "Suspended audio stream" ) ;
517585 }
518586
519587 /// Resumes the progression of time in an audio context that has previously been
@@ -529,10 +597,20 @@ impl AudioContext {
529597 /// * The audio device is not available
530598 /// * For a `BackendSpecificError`
531599 pub fn resume_sync ( & self ) {
532- // First ask the audio host to resume the stream
533- self . backend_manager . lock ( ) . unwrap ( ) . resume ( ) ;
600+ // Lock the backend manager mutex to avoid concurrent calls
601+ log:: debug!( "Resume_sync called, locking backend manager" ) ;
602+ let backend_manager_guard = self . backend_manager . lock ( ) . unwrap ( ) ;
603+
604+ if self . state ( ) != AudioContextState :: Suspended {
605+ log:: debug!( "Resume no-op - context is not suspended" ) ;
606+ return ;
607+ }
608+
609+ // Ask the audio host to resume the stream
610+ backend_manager_guard. resume ( ) ;
534611
535612 // Then, ask to resume rendering via a control message
613+ log:: debug!( "Resumed audio stream, waking audio graph" ) ;
536614 let ( sender, receiver) = crossbeam_channel:: bounded ( 0 ) ;
537615 let notify = OneshotNotify :: Sync ( sender) ;
538616 self . base
@@ -541,6 +619,7 @@ impl AudioContext {
541619 // Wait for the render thread to have processed the resume message
542620 // The AudioContextState will be updated by the render thread.
543621 receiver. recv ( ) . ok ( ) ;
622+ log:: debug!( "Resumed audio graph" ) ;
544623 }
545624
546625 /// Closes the `AudioContext`, releasing the system resources being used.
@@ -555,22 +634,38 @@ impl AudioContext {
555634 ///
556635 /// Will panic when this function is called multiple times
557636 pub fn close_sync ( & self ) {
558- // First, stop rendering via a control message
559- let ( sender, receiver) = crossbeam_channel:: bounded ( 0 ) ;
560- let notify = OneshotNotify :: Sync ( sender) ;
561- self . base . send_control_msg ( ControlMessage :: Close { notify } ) ;
637+ // Lock the backend manager mutex to avoid concurrent calls
638+ log:: debug!( "Close_sync called, locking backend manager" ) ;
639+ let backend_manager_guard = self . backend_manager . lock ( ) . unwrap ( ) ;
562640
563- // Wait for the render thread to have processed the suspend message.
564- // The AudioContextState will be updated by the render thread.
565- receiver. recv ( ) . ok ( ) ;
641+ if self . state ( ) == AudioContextState :: Closed {
642+ log:: debug!( "Close no-op - context is already closed" ) ;
643+ return ;
644+ }
645+
646+ // First, stop rendering via a control message
647+ if self . state ( ) == AudioContextState :: Running {
648+ let ( sender, receiver) = crossbeam_channel:: bounded ( 0 ) ;
649+ let notify = OneshotNotify :: Sync ( sender) ;
650+ self . base . send_control_msg ( ControlMessage :: Close { notify } ) ;
651+
652+ // Wait for the render thread to have processed the suspend message.
653+ // The AudioContextState will be updated by the render thread.
654+ log:: debug!( "Suspending audio graph, waiting for signal.." ) ;
655+ receiver. recv ( ) . ok ( ) ;
656+ } else {
657+ // if the context is not running, change the state manually
658+ self . base . set_state ( AudioContextState :: Closed ) ;
659+ }
566660
567661 // Then ask the audio host to close the stream
568- self . backend_manager . lock ( ) . unwrap ( ) . close ( ) ;
662+ log:: debug!( "Suspended audio graph. Closing audio stream.." ) ;
663+ backend_manager_guard. close ( ) ;
569664
570665 // Stop the AudioRenderCapacity collection thread
571666 self . render_capacity . stop ( ) ;
572667
573- // TODO stop the event loop <https://github.com/orottier/web- audio-api-rs/issues/421>
668+ log :: debug! ( "Closed audio stream" ) ;
574669 }
575670
576671 /// Creates a [`MediaStreamAudioSourceNode`](node::MediaStreamAudioSourceNode) from a
0 commit comments