Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions KoeApp/Koe/Audio/SPAudioCaptureManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -76,6 +103,8 @@ - (instancetype)init {
_isCapturing = NO;
_accumBuffer = [NSMutableData data];
_pendingDeviceID = kAudioObjectUnknown;
_didMuteOutput = NO;
_mutedOutputDevice = kAudioObjectUnknown;
}
return self;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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