VoiceWake: streamline chimes, default to Glass

This commit is contained in:
Peter Steinberger
2025-12-08 20:50:34 +01:00
parent feb70aeb6b
commit ffaf968940
9 changed files with 22 additions and 55 deletions

View File

@@ -37,7 +37,6 @@ let package = Package(
],
resources: [
.copy("Resources/Clawdis.icns"),
.copy("Resources/Sounds"),
.copy("Resources/WebChat"),
],
swiftSettings: [

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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])

View File

@@ -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.