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
7 changes: 7 additions & 0 deletions Wave/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ final class AppState {
var includePunctuation: Bool {
didSet { UserDefaults.standard.set(includePunctuation, forKey: "includePunctuation") }
}
var muteSystemAudio: Bool {
didSet { UserDefaults.standard.set(muteSystemAudio, forKey: "muteSystemAudio") }
}

// MARK: - Services
let modelManager = ModelManager()
Expand Down Expand Up @@ -66,6 +69,7 @@ final class AppState {
} else {
includePunctuation = UserDefaults.standard.bool(forKey: "includePunctuation")
}
muteSystemAudio = UserDefaults.standard.bool(forKey: "muteSystemAudio")

// Default shortcut: Control + Space
if hotkeyKeyCode == 0 && hotkeyModifiers == 0 {
Expand Down Expand Up @@ -130,8 +134,10 @@ final class AppState {
do {
status = .recording
showOverlay()
if muteSystemAudio { SystemAudioDucker.duck() }
try await transcriptionService.startRecording()
} catch {
if muteSystemAudio { SystemAudioDucker.restore() }
status = .error("Recording failed")
hideOverlay()
try? await Task.sleep(for: .seconds(2))
Expand All @@ -144,6 +150,7 @@ final class AppState {
updateOverlay()

let text = await transcriptionService.stopRecordingAndTranscribe(includePunctuation: includePunctuation)
if muteSystemAudio { SystemAudioDucker.restore() }

hideOverlay()

Expand Down
60 changes: 60 additions & 0 deletions Wave/Utilities/SystemAudioDucker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import CoreAudio

/// Saves and restores the default output device's mute state around a dictation session.
/// Uses CoreAudio's `kAudioDevicePropertyMute` rather than changing volume so the
/// user's volume setting is never touched.
enum SystemAudioDucker {
private static var savedMuteState: Bool = false

/// Snapshot the current mute state, then mute system output.
static func duck() {
savedMuteState = isMuted()
setMuted(true)
}

/// Restore the mute state captured at the last `duck()` call.
static func restore() {
setMuted(savedMuteState)
}

// MARK: - Private

private static func isMuted() -> Bool {
guard let device = defaultOutputDevice() else { return false }
var mute: UInt32 = 0
var size = UInt32(MemoryLayout<UInt32>.size)
var address = mutePropertyAddress()
let status = AudioObjectGetPropertyData(device, &address, 0, nil, &size, &mute)
return status == noErr && mute != 0
}

private static func setMuted(_ muted: Bool) {
guard let device = defaultOutputDevice() else { return }
var mute = UInt32(muted ? 1 : 0)
var address = mutePropertyAddress()
// Silently ignore devices that don't support the mute property (e.g. some BT sinks)
AudioObjectSetPropertyData(device, &address, 0, nil, UInt32(MemoryLayout<UInt32>.size), &mute)
}

private static func defaultOutputDevice() -> AudioDeviceID? {
var device = AudioDeviceID(kAudioObjectUnknown)
var size = UInt32(MemoryLayout<AudioDeviceID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &device
)
return (status == noErr && device != kAudioObjectUnknown) ? device : nil
}

private static func mutePropertyAddress() -> AudioObjectPropertyAddress {
AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyMute,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain
)
}
}
1 change: 1 addition & 0 deletions Wave/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct HomeView: View {
Text("Transcription")
.font(.headline)
Toggle("Include punctuation", isOn: $state.includePunctuation)
Toggle("Mute system audio while dictating", isOn: $state.muteSystemAudio)
}

// Model section
Expand Down