@@ -360,6 +360,7 @@ pub struct MicController<B = CoreAudioBackend> {
360360 /// Keyed by AudioDeviceID; value is the volume scalar (0.0–1.0) before muting.
361361 saved_volumes : HashMap < AudioDeviceID , f32 > ,
362362 volume_fallback_devices : HashSet < AudioDeviceID > ,
363+ native_muted_devices : HashSet < AudioDeviceID > ,
363364 backend : B ,
364365}
365366
@@ -370,6 +371,7 @@ impl<B: Default> Default for MicController<B> {
370371 desired_muted : false ,
371372 saved_volumes : HashMap :: new ( ) ,
372373 volume_fallback_devices : HashSet :: new ( ) ,
374+ native_muted_devices : HashSet :: new ( ) ,
373375 backend : B :: default ( ) ,
374376 }
375377 }
@@ -398,6 +400,7 @@ impl<B: AudioBackend> MicController<B> {
398400 desired_muted : false ,
399401 saved_volumes : HashMap :: new ( ) ,
400402 volume_fallback_devices : HashSet :: new ( ) ,
403+ native_muted_devices : HashSet :: new ( ) ,
401404 backend,
402405 } ;
403406 trace ! ( "Creating audio controller" ) ;
@@ -515,6 +518,7 @@ impl<B: AudioBackend> MicController<B> {
515518 audio_device_id : AudioDeviceID ,
516519 state : bool ,
517520 ) -> Result < Option < AudioDeviceID > > {
521+ let was_muted = self . is_muted ( audio_device_id) ?;
518522 let set_result = self . backend . set_mute ( audio_device_id, state) ?;
519523 if set_result. is_none ( ) {
520524 trace ! (
@@ -537,6 +541,11 @@ impl<B: AudioBackend> MicController<B> {
537541 state
538542 ) ) ;
539543 }
544+ if state && was_muted == Some ( false ) {
545+ self . native_muted_devices . insert ( audio_device_id) ;
546+ } else if !state {
547+ self . native_muted_devices . remove ( & audio_device_id) ;
548+ }
540549 }
541550
542551 let state_text = match self . is_muted ( audio_device_id) ? {
@@ -554,12 +563,14 @@ impl<B: AudioBackend> MicController<B> {
554563 let Some ( current_vol) = self . backend . get_volume ( audio_device_id) ? else {
555564 return Ok ( false ) ;
556565 } ;
557- trace ! (
558- "Saving volume {:.3} for device {} before muting" ,
559- current_vol,
560- audio_device_id
561- ) ;
562- self . saved_volumes . insert ( audio_device_id, current_vol) ;
566+ if !is_volume_muted ( current_vol) {
567+ trace ! (
568+ "Saving volume {:.3} for device {} before muting" ,
569+ current_vol,
570+ audio_device_id
571+ ) ;
572+ self . saved_volumes . insert ( audio_device_id, current_vol) ;
573+ }
563574 }
564575 if self . backend . set_volume ( audio_device_id, 0.0 ) ?. is_none ( ) {
565576 return Ok ( false ) ;
@@ -574,7 +585,9 @@ impl<B: AudioBackend> MicController<B> {
574585 volume
575586 ) ) ;
576587 }
577- self . volume_fallback_devices . insert ( audio_device_id) ;
588+ if self . saved_volumes . contains_key ( & audio_device_id) {
589+ self . volume_fallback_devices . insert ( audio_device_id) ;
590+ }
578591 } else {
579592 let restore_vol = self
580593 . saved_volumes
@@ -659,6 +672,47 @@ impl<B: AudioBackend> MicController<B> {
659672 Ok ( self )
660673 }
661674
675+ pub fn restore_on_exit ( & mut self ) -> Result < ( ) > {
676+ let native_devices: Vec < _ > = self . native_muted_devices . iter ( ) . copied ( ) . collect ( ) ;
677+ let volume_devices: Vec < _ > = self . saved_volumes . keys ( ) . copied ( ) . collect ( ) ;
678+ let mut failures = Vec :: new ( ) ;
679+
680+ for id in native_devices {
681+ match self . backend . set_mute ( id, false ) {
682+ Ok ( Some ( ( ) ) ) => match self . wait_for_device_state ( id, false ) {
683+ Ok ( true ) => {
684+ self . native_muted_devices . remove ( & id) ;
685+ }
686+ Ok ( false ) => failures. push ( format ! ( "{} (native mute remained enabled)" , id) ) ,
687+ Err ( err) => failures. push ( format ! ( "{} ({})" , id, err) ) ,
688+ } ,
689+ Ok ( None ) => failures. push ( format ! ( "{} (native mute unavailable)" , id) ) ,
690+ Err ( err) => failures. push ( format ! ( "{} ({})" , id, err) ) ,
691+ }
692+ }
693+
694+ for id in volume_devices {
695+ match self . mute_via_volume ( id, false ) {
696+ Ok ( true ) => { }
697+ Ok ( false ) => failures. push ( format ! ( "{} (input volume unavailable)" , id) ) ,
698+ Err ( err) => failures. push ( format ! ( "{} ({})" , id, err) ) ,
699+ }
700+ }
701+
702+ self . muted = self . is_muted_all ( ) . unwrap_or ( false ) ;
703+ self . desired_muted = self . muted ;
704+
705+ if failures. is_empty ( ) {
706+ Ok ( ( ) )
707+ } else {
708+ Err ( anyhow ! (
709+ "failed to restore {} input device(s) on exit: {}" ,
710+ failures. len( ) ,
711+ failures. join( "; " )
712+ ) )
713+ }
714+ }
715+
662716 pub fn toggle ( & mut self , state : Option < bool > ) -> Result < & Self > {
663717 let state = target_state ( state, self . desired_muted ) ;
664718 self . mute_all ( state)
@@ -867,6 +921,32 @@ mod tests {
867921 assert ! ( controller. should_enforce_mute( ) ) ;
868922 }
869923
924+ #[ test]
925+ fn restore_on_exit_unmutes_native_devices_muted_by_app ( ) {
926+ let backend = FakeBackend :: with_devices ( vec ! [ ( 1 , Device :: native( "Built-in" , false ) ) ] ) ;
927+ let mut controller = MicController :: with_backend ( backend) . unwrap ( ) ;
928+ controller. mute_all ( true ) . unwrap ( ) ;
929+
930+ controller. restore_on_exit ( ) . unwrap ( ) ;
931+
932+ assert ! ( !controller. muted) ;
933+ assert_eq ! ( controller. backend. device( 1 ) . unwrap( ) . mute, Some ( false ) ) ;
934+ assert ! ( controller. native_muted_devices. is_empty( ) ) ;
935+ }
936+
937+ #[ test]
938+ fn restore_on_exit_leaves_preexisting_native_mute_unchanged ( ) {
939+ let backend = FakeBackend :: with_devices ( vec ! [ ( 1 , Device :: native( "Built-in" , true ) ) ] ) ;
940+ let mut controller = MicController :: with_backend ( backend) . unwrap ( ) ;
941+ controller. mute_all ( true ) . unwrap ( ) ;
942+
943+ controller. restore_on_exit ( ) . unwrap ( ) ;
944+
945+ assert ! ( controller. muted) ;
946+ assert_eq ! ( controller. backend. device( 1 ) . unwrap( ) . mute, Some ( true ) ) ;
947+ assert ! ( controller. native_muted_devices. is_empty( ) ) ;
948+ }
949+
870950 #[ test]
871951 fn native_mute_failure_does_not_claim_muted_but_keeps_enforcing ( ) {
872952 let mut device = Device :: native ( "Built-in" , false ) ;
@@ -908,6 +988,34 @@ mod tests {
908988 assert ! ( controller. volume_fallback_devices. contains( & 1 ) ) ;
909989 }
910990
991+ #[ test]
992+ fn restore_on_exit_restores_volume_fallback_devices_muted_by_app ( ) {
993+ let backend = FakeBackend :: with_devices ( vec ! [ ( 1 , Device :: fallback( "Continuity" , 0.65 ) ) ] ) ;
994+ let mut controller = MicController :: with_backend ( backend) . unwrap ( ) ;
995+ controller. mute_all ( true ) . unwrap ( ) ;
996+
997+ controller. restore_on_exit ( ) . unwrap ( ) ;
998+
999+ assert ! ( !controller. muted) ;
1000+ assert_eq ! ( controller. backend. device( 1 ) . unwrap( ) . volume, Some ( 0.65 ) ) ;
1001+ assert ! ( controller. saved_volumes. is_empty( ) ) ;
1002+ assert ! ( controller. volume_fallback_devices. is_empty( ) ) ;
1003+ }
1004+
1005+ #[ test]
1006+ fn restore_on_exit_leaves_preexisting_volume_mute_unchanged ( ) {
1007+ let backend = FakeBackend :: with_devices ( vec ! [ ( 1 , Device :: fallback( "Continuity" , 0.0 ) ) ] ) ;
1008+ let mut controller = MicController :: with_backend ( backend) . unwrap ( ) ;
1009+ controller. mute_all ( true ) . unwrap ( ) ;
1010+
1011+ controller. restore_on_exit ( ) . unwrap ( ) ;
1012+
1013+ assert ! ( controller. muted) ;
1014+ assert_eq ! ( controller. backend. device( 1 ) . unwrap( ) . volume, Some ( 0.0 ) ) ;
1015+ assert ! ( controller. saved_volumes. is_empty( ) ) ;
1016+ assert ! ( controller. volume_fallback_devices. is_empty( ) ) ;
1017+ }
1018+
9111019 #[ test]
9121020 fn volume_fallback_failure_does_not_claim_muted ( ) {
9131021 let mut device = Device :: fallback ( "Continuity" , 0.65 ) ;
0 commit comments