From a286e460c5e5c16c38493cb606d481dcb8bff422 Mon Sep 17 00:00:00 2001 From: zheng Date: Sat, 30 May 2026 09:54:58 +0800 Subject: [PATCH 1/2] feat(audio): mute system output while recording Silence other apps' playback for the duration of a recording so it neither distracts the speaker nor bleeds into the mic. Mutes the current default output device on capture start and restores exactly that device on stop; leaves an already-user-muted device untouched (and skips the restore). --- KoeApp/Koe/Audio/SPAudioCaptureManager.m | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/KoeApp/Koe/Audio/SPAudioCaptureManager.m b/KoeApp/Koe/Audio/SPAudioCaptureManager.m index 1b36b3b..a8c3ac8 100644 --- a/KoeApp/Koe/Audio/SPAudioCaptureManager.m +++ b/KoeApp/Koe/Audio/SPAudioCaptureManager.m @@ -19,8 +19,35 @@ @interface SPAudioCaptureManager () @property (nonatomic, strong) NSMutableData *accumBuffer; @property (nonatomic, assign) AudioDeviceID pendingDeviceID; +// Output muting during recording: silence other apps' playback so it neither +// distracts the speaker nor bleeds into the mic. Restored on stopCapture. +@property (nonatomic, assign) BOOL didMuteOutput; +@property (nonatomic, assign) AudioObjectID mutedOutputDevice; + +- (void)muteSystemOutput; +- (void)restoreSystemOutput; + @end +// --------------------------------------------------------------------------- +// System output muting — silence other playback while recording +// --------------------------------------------------------------------------- + +static AudioObjectID koeDefaultOutputDevice(void) { + AudioObjectID device = kAudioObjectUnknown; + UInt32 size = sizeof(device); + AudioObjectPropertyAddress addr = { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain + }; + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, + &size, &device) != noErr) { + return kAudioObjectUnknown; + } + return device; +} + // --------------------------------------------------------------------------- // AudioQueue callback — runs on an AudioQueue internal thread // --------------------------------------------------------------------------- @@ -76,6 +103,8 @@ - (instancetype)init { _isCapturing = NO; _accumBuffer = [NSMutableData data]; _pendingDeviceID = kAudioObjectUnknown; + _didMuteOutput = NO; + _mutedOutputDevice = kAudioObjectUnknown; } return self; } @@ -162,6 +191,7 @@ - (BOOL)startCaptureWithAudioCallback:(SPAudioFrameCallback)callback { self.audioQueue = queue; self.isCapturing = YES; + [self muteSystemOutput]; NSLog(@"[Koe] Audio capture started (AudioQueue 16kHz mono Float32, 200ms frames)"); return YES; } @@ -187,7 +217,63 @@ - (void)stopCapture { AudioQueueDispose(self.audioQueue, true); self.audioQueue = NULL; self.audioCallback = nil; + [self restoreSystemOutput]; NSLog(@"[Koe] Audio capture stopped"); } +#pragma mark - System Output Muting + +// Mute the current default output device so other apps' audio is silenced for +// the duration of the recording. The device we mute is remembered so we restore +// exactly that one even if the default route changes mid-session. If the device +// was already muted by the user, we leave it untouched and skip the restore. +- (void)muteSystemOutput { + self.didMuteOutput = NO; + self.mutedOutputDevice = kAudioObjectUnknown; + + AudioObjectID device = koeDefaultOutputDevice(); + if (device == kAudioObjectUnknown) return; + + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyMute, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + if (!AudioObjectHasProperty(device, &addr)) { + NSLog(@"[Koe] Default output device has no master mute; skipping output mute"); + return; + } + + UInt32 muted = 0; + UInt32 size = sizeof(muted); + if (AudioObjectGetPropertyData(device, &addr, 0, NULL, &size, &muted) != noErr) return; + if (muted) return; // already muted by the user — don't touch, don't restore + + UInt32 on = 1; + if (AudioObjectSetPropertyData(device, &addr, 0, NULL, sizeof(on), &on) == noErr) { + self.mutedOutputDevice = device; + self.didMuteOutput = YES; + NSLog(@"[Koe] Muted system output during recording (device %u)", (unsigned)device); + } +} + +- (void)restoreSystemOutput { + if (!self.didMuteOutput) return; + self.didMuteOutput = NO; + + AudioObjectID device = self.mutedOutputDevice; + self.mutedOutputDevice = kAudioObjectUnknown; + if (device == kAudioObjectUnknown) return; + + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyMute, + kAudioDevicePropertyScopeOutput, + kAudioObjectPropertyElementMain + }; + UInt32 off = 0; + if (AudioObjectSetPropertyData(device, &addr, 0, NULL, sizeof(off), &off) == noErr) { + NSLog(@"[Koe] Restored system output after recording"); + } +} + @end From 95152b6b84f049883d6d84551cd5b5a7e46322e3 Mon Sep 17 00:00:00 2001 From: zheng Date: Sat, 30 May 2026 11:34:35 +0800 Subject: [PATCH 2/2] docs(changelog): note system output muting during recording --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae9d4a..ccbb9e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable user-facing changes to Koe are documented here. +## Unreleased + +### Added + +- Added automatic muting of system audio output while recording, so other apps' playback no longer distracts the speaker or bleeds into the microphone. The exact device is restored on stop, and a device the user had already muted is left untouched. + ## 1.0.14 - 2026-04-09 ### Added