diff --git a/apps/macos/Package.swift b/apps/macos/Package.swift index 1289345ea..16e797344 100644 --- a/apps/macos/Package.swift +++ b/apps/macos/Package.swift @@ -37,7 +37,6 @@ let package = Package( ], resources: [ .copy("Resources/Clawdis.icns"), - .copy("Resources/Sounds"), .copy("Resources/WebChat"), ], swiftSettings: [ diff --git a/apps/macos/Sources/Clawdis/AppState.swift b/apps/macos/Sources/Clawdis/AppState.swift index 1e5076bc8..b17873434 100644 --- a/apps/macos/Sources/Clawdis/AppState.swift +++ b/apps/macos/Sources/Clawdis/AppState.swift @@ -43,10 +43,6 @@ final class AppState: ObservableObject { } } - @Published var voiceWakeChimeEnabled: Bool { - didSet { UserDefaults.standard.set(self.voiceWakeChimeEnabled, forKey: voiceWakeChimeEnabledKey) } - } - @Published var voiceWakeTriggerChime: VoiceWakeChime { didSet { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } } @@ -152,14 +148,12 @@ final class AppState: ObservableObject { self.swabbleEnabled = voiceWakeSupported ? savedVoiceWake : false self.swabbleTriggerWords = UserDefaults.standard .stringArray(forKey: swabbleTriggersKey) ?? defaultVoiceWakeTriggers - self.voiceWakeChimeEnabled = UserDefaults.standard - .object(forKey: voiceWakeChimeEnabledKey) as? Bool ?? true self.voiceWakeTriggerChime = Self.loadChime( key: voiceWakeTriggerChimeKey, - fallback: .system(name: defaultVoiceWakeChimeName)) + fallback: .system(name: "Glass")) self.voiceWakeSendChime = Self.loadChime( key: voiceWakeSendChimeKey, - fallback: .system(name: defaultVoiceWakeChimeName)) + fallback: .system(name: "Glass")) if let storedIconAnimations = UserDefaults.standard.object(forKey: iconAnimationsEnabledKey) as? Bool { self.iconAnimationsEnabled = storedIconAnimations } else { diff --git a/apps/macos/Sources/Clawdis/Constants.swift b/apps/macos/Sources/Clawdis/Constants.swift index b3e8711ba..e703d3ee3 100644 --- a/apps/macos/Sources/Clawdis/Constants.swift +++ b/apps/macos/Sources/Clawdis/Constants.swift @@ -8,13 +8,10 @@ let pauseDefaultsKey = "clawdis.pauseEnabled" let iconAnimationsEnabledKey = "clawdis.iconAnimationsEnabled" let swabbleEnabledKey = "clawdis.swabbleEnabled" let swabbleTriggersKey = "clawdis.swabbleTriggers" -let voiceWakeChimeEnabledKey = "clawdis.voiceWakeChimeEnabled" let voiceWakeTriggerChimeKey = "clawdis.voiceWakeTriggerChime" let voiceWakeSendChimeKey = "clawdis.voiceWakeSendChime" let showDockIconKey = "clawdis.showDockIcon" let defaultVoiceWakeTriggers = ["clawd", "claude"] -let defaultVoiceWakeChimeName = "startrek-computer" -let defaultVoiceWakeChimeExtension = "wav" let voiceWakeMicKey = "clawdis.voiceWakeMicID" let voiceWakeLocaleKey = "clawdis.voiceWakeLocaleID" let voiceWakeAdditionalLocalesKey = "clawdis.voiceWakeAdditionalLocaleIDs" diff --git a/apps/macos/Sources/Clawdis/Resources/Sounds/startrek-computer.wav b/apps/macos/Sources/Clawdis/Resources/Sounds/startrek-computer.wav deleted file mode 100644 index 395c360ea..000000000 Binary files a/apps/macos/Sources/Clawdis/Resources/Sounds/startrek-computer.wav and /dev/null differ diff --git a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift index e6b87a2fd..f6180f7fe 100644 --- a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift +++ b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift @@ -84,7 +84,6 @@ actor VoicePushToTalk { let micID: String? let localeID: String? let forwardConfig: VoiceWakeForwardConfig - let chimeEnabled: Bool let triggerChime: VoiceWakeChime let sendChime: VoiceWakeChime } @@ -100,7 +99,7 @@ actor VoicePushToTalk { let config = await MainActor.run { self.makeConfig() } self.activeConfig = config self.isCapturing = true - if config.chimeEnabled { + if config.triggerChime != .none { await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) } } await VoiceWakeRuntime.shared.pauseForPushToTalk() @@ -138,7 +137,7 @@ actor VoicePushToTalk { forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } } - if self.activeConfig?.chimeEnabled == true, let chime = self.activeConfig?.sendChime { + if let chime = self.activeConfig?.sendChime, chime != .none { await MainActor.run { VoiceWakeChimePlayer.play(chime) } } @@ -224,7 +223,6 @@ actor VoicePushToTalk { micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID, localeID: state.voiceWakeLocaleID, forwardConfig: state.voiceWakeForwardConfig, - chimeEnabled: state.voiceWakeChimeEnabled, triggerChime: state.voiceWakeTriggerChime, sendChime: state.voiceWakeSendChime) } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeChime.swift b/apps/macos/Sources/Clawdis/VoiceWakeChime.swift index 25fbcd4c2..fef4d4f97 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeChime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeChime.swift @@ -2,6 +2,7 @@ import AppKit import Foundation enum VoiceWakeChime: Codable, Equatable { + case none case system(name: String) case custom(displayName: String, bookmark: Data) @@ -14,6 +15,8 @@ enum VoiceWakeChime: Codable, Equatable { var displayLabel: String { switch self { + case .none: + return "No Sound" case let .system(name): return VoiceWakeChimeCatalog.displayName(for: name) case let .custom(displayName, _): @@ -23,12 +26,11 @@ enum VoiceWakeChime: Codable, Equatable { } struct VoiceWakeChimeCatalog { - /// Options shown in the picker; first entry is the default bundled tone. + /// Options shown in the picker. static let systemOptions: [String] = [ - defaultVoiceWakeChimeName, + "Glass", // default "Ping", "Pop", - "Glass", "Frog", "Submarine", "Funk", @@ -36,7 +38,6 @@ struct VoiceWakeChimeCatalog { ] static func displayName(for raw: String) -> String { - if raw == defaultVoiceWakeChimeName { return "Startrek Computer" } return raw } } @@ -50,11 +51,9 @@ enum VoiceWakeChimePlayer { private static func sound(for chime: VoiceWakeChime) -> NSSound? { switch chime { + case .none: + return nil case let .system(name): - // Prefer bundled tone if present. - if let bundled = bundledSound(named: name) { - return bundled - } return NSSound(named: NSSound.Name(name)) case let .custom(_, bookmark): @@ -70,13 +69,4 @@ enum VoiceWakeChimePlayer { return NSSound(contentsOf: url, byReference: false) } } - - private static func bundledSound(named name: String) -> NSSound? { - guard let url = Bundle.main.url( - forResource: name, - withExtension: defaultVoiceWakeChimeExtension, - subdirectory: "Resources/Sounds") - else { return nil } - return NSSound(contentsOf: url, byReference: false) - } } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift index a7a08b17c..0e8e14e28 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift @@ -40,7 +40,6 @@ actor VoiceWakeRuntime { let triggers: [String] let micID: String? let localeID: String? - let chimeEnabled: Bool let triggerChime: VoiceWakeChime let sendChime: VoiceWakeChime } @@ -52,7 +51,6 @@ actor VoiceWakeRuntime { triggers: sanitizeVoiceWakeTriggers(state.swabbleTriggerWords), micID: state.voiceWakeMicID.isEmpty ? nil : state.voiceWakeMicID, localeID: state.voiceWakeLocaleID.isEmpty ? nil : state.voiceWakeLocaleID, - chimeEnabled: state.voiceWakeChimeEnabled, triggerChime: state.voiceWakeTriggerChime, sendChime: state.voiceWakeSendChime) return (enabled, config) @@ -205,7 +203,7 @@ actor VoiceWakeRuntime { private func beginCapture(transcript: String, config: RuntimeConfig) async { self.isCapturing = true - if config.chimeEnabled { + if config.triggerChime != .none { await MainActor.run { VoiceWakeChimePlayer.play(config.triggerChime) } } let trimmed = Self.trimmedAfterTrigger(transcript, triggers: config.triggers) @@ -277,7 +275,7 @@ actor VoiceWakeRuntime { committed: finalTranscript, volatile: "", isFinal: true) - if config.chimeEnabled { + if config.sendChime != .none { await MainActor.run { VoiceWakeChimePlayer.play(config.sendChime) } } await MainActor.run { diff --git a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift index 7ff94fb58..c4ebf9cb7 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift @@ -148,28 +148,18 @@ struct VoiceWakeSettings: View { private var chimeSection: some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .firstTextBaseline, spacing: 10) { - Toggle(isOn: self.$state.voiceWakeChimeEnabled) { - VStack(alignment: .leading, spacing: 2) { - Text("Play sounds") - .font(.callout.weight(.semibold)) - Text("Chimes for wake-word and push-to-talk events.") - .font(.footnote) - .foregroundStyle(.secondary) - } - } - .toggleStyle(.switch) + Text("Sounds") + .font(.callout.weight(.semibold)) Spacer() } self.chimeRow( title: "Trigger sound", selection: self.$state.voiceWakeTriggerChime) - .disabled(!self.state.voiceWakeChimeEnabled) self.chimeRow( title: "Send sound", selection: self.$state.voiceWakeSendChime) - .disabled(!self.state.voiceWakeChimeEnabled) } .padding(.top, 4) } @@ -245,14 +235,19 @@ struct VoiceWakeSettings: View { .frame(width: self.fieldLabelWidth, alignment: .leading) Menu { + Button("No Sound") { selection.wrappedValue = .none } + Divider() ForEach(VoiceWakeChimeCatalog.systemOptions, id: \.self) { option in Button(VoiceWakeChimeCatalog.displayName(for: option)) { selection.wrappedValue = .system(name: option) } } + Divider() + Button("Choose file…") { self.chooseCustomChime(for: selection) } } label: { HStack(spacing: 6) { Text(selection.wrappedValue.displayLabel) + Spacer() Image(systemName: "chevron.down") .font(.caption) .foregroundStyle(.secondary) @@ -266,11 +261,7 @@ struct VoiceWakeSettings: View { .clipShape(RoundedRectangle(cornerRadius: 6)) } - Button("Choose file…") { - self.chooseCustomChime(for: selection) - } - - Button("Test") { + Button("Play") { VoiceWakeChimePlayer.play(selection.wrappedValue) } .keyboardShortcut(.space, modifiers: [.command]) diff --git a/docs/mac/voicewake.md b/docs/mac/voicewake.md index 793b2d0e6..a08b37fde 100644 --- a/docs/mac/voicewake.md +++ b/docs/mac/voicewake.md @@ -25,7 +25,7 @@ Updated: 2025-12-08 · Owners: mac app - **Voice Wake** toggle: enables wake-word runtime. - **Hold Cmd+Fn to talk**: enables the push-to-talk monitor. Disabled on macOS < 26. - Language & mic pickers, live level meter, trigger-word table, tester, forward target/command all remain unchanged. -- **Sounds**: optional chimes on trigger detect and on send; defaults to a bundled `startrek-computer.wav`. You can pick any `NSSound`-loadable file (e.g. MP3/WAV/AIFF) for each event. +- **Sounds**: chimes on trigger detect and on send; defaults to the macOS “Glass” system sound. You can pick any `NSSound`-loadable file (e.g. MP3/WAV/AIFF) for each event or choose **No Sound**. ## Forwarding payload - `VoiceWakeForwarder.prefixedTranscript(_:)` prepends the machine hint before sending. Shared between wake-word and push-to-talk paths.