Skip to content

Commit 53ae244

Browse files
committed
Restore microphone state on exit
1 parent f07158e commit 53ae244

3 files changed

Lines changed: 127 additions & 10 deletions

File tree

src/event_loop.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ fn update_mic(
6464
}
6565
}
6666

67+
pub fn restore_microphone_on_exit(controller: &Arc<RwLock<MicController>>) {
68+
if let Err(err) = controller.write().unwrap().restore_on_exit() {
69+
log::error!("Failed to restore microphone state on exit: {}", err);
70+
}
71+
}
72+
6773
pub fn start(
6874
mut event_loop: EventLoop<Message>,
6975
event_ids: EventIds,
@@ -230,6 +236,7 @@ pub fn start(
230236
}
231237

232238
if exit_requested {
239+
restore_microphone_on_exit(&controller);
233240
*control_flow = ControlFlow::Exit;
234241
} else {
235242
// Sleep until the next scheduled check rather than spinning.

src/main.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extern crate objc;
1919

2020
use crate::camera::CameraController;
2121
use crate::config::AppVars;
22-
use crate::event_loop::start;
22+
use crate::event_loop::{restore_microphone_on_exit, start};
2323
use crate::mic::MicController;
2424
use crate::settings::Settings;
2525
use crate::ui::UI;
@@ -57,7 +57,7 @@ fn main() {
5757
trace!("Mic controller initialized {:?}", controller);
5858

5959
// Register SIGTERM/SIGINT handlers. The signal handler only sets a flag;
60-
// a background thread exits without changing the current microphone state.
60+
// a background thread performs microphone cleanup before exiting.
6161
unsafe {
6262
libc::signal(
6363
libc::SIGTERM,
@@ -68,10 +68,12 @@ fn main() {
6868
handle_signal as *const () as libc::sighandler_t,
6969
);
7070
}
71+
let shutdown_controller = controller.clone();
7172
std::thread::spawn(move || loop {
7273
std::thread::sleep(Duration::from_millis(100));
7374
if SHUTDOWN_REQUESTED.load(Ordering::SeqCst) {
74-
info!("Signal received — exiting without changing microphone state");
75+
info!("Signal received — restoring microphone state before exit");
76+
restore_microphone_on_exit(&shutdown_controller);
7577
std::process::exit(0);
7678
}
7779
});

src/mic.rs

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)