VoiceWake: streamline chimes, default to Glass
This commit is contained in:
@@ -37,7 +37,6 @@ let package = Package(
|
||||
],
|
||||
resources: [
|
||||
.copy("Resources/Clawdis.icns"),
|
||||
.copy("Resources/Sounds"),
|
||||
.copy("Resources/WebChat"),
|
||||
],
|
||||
swiftSettings: [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user