feat(clients): persist input settings, add mic input gain, fix iOS chat + VoiceOver
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

Input mode (VAD/PTT/Always-On), VAD threshold, and the new mic gain were
applied to the core + UI but never saved, so every relaunch reset to VAD
defaults. Each client now persists them and re-applies on connect:
  - iOS: UserDefaults (SessionState.loadAndApplyVoiceSettings + setter writes)
  - macOS: UserDefaults via MainWindowController didSet + loadPersistedAudioSettings
    (settings window also restores the VAD slider from the stored threshold)
  - Windows: new Models/VoiceSettings.cs (JSON at %AppData%\VoiceCat\voice.json,
    mirrors FeedbackSettings) loaded/applied in MainForm

Add global send-side mic gain API vc_set_input_gain (applied to MIC PCM in
on_capture_frame before the VAD gate, clamped to int16) + Swift/C# bindings,
and a 0-300% (default 100%) mic-volume slider on all three clients.

Fix iOS chat: ChatView called sendText(scope:.channel) with no targetId (0),
so channel messages went nowhere; now passes session.currentChannelId.

Fix iOS per-user tuning for VoiceOver: the tuning sheet was long-press
.contextMenu only (invisible to VoiceOver); UserRow now also exposes the same
buttons via .accessibilityActions (no visual change).

Verified: core builds clean; ctest 24/27 (3 pre-existing teardown crashes,
reproduced with changes stashed); VoiceCatMac + VoiceCatiOS (arm64 sim) build
SUCCEEDED; VoiceCat.Interop dotnet build succeeded. Windows App not built
(WinForms can't build on macOS) — follows existing patterns.
This commit is contained in:
2026-06-23 03:35:26 +02:00
parent d30c4ee2f5
commit 95f1fb70b0
17 changed files with 354 additions and 6 deletions

View File

@@ -33,7 +33,9 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
private var screenCapture: ScreenAudioCapture?
// Last app/exclusion choice from the share picker; reused as the default next time.
private var screenAudioSelection: ScreenAudioSelection = .default
internal var pttKeyCode: UInt16 = 0x60 // F8
internal var pttKeyCode: UInt16 = 0x60 { // F8
didSet { UserDefaults.standard.set(Int(pttKeyCode), forKey: AudioDefaults.pttKeyCode) }
}
private var pttMonitor: Any?
private var pttEngaged = false // guards the PTT cue against key-repeat
private var serverMuted = false
@@ -49,9 +51,26 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
private var settingsWindowController: SettingsWindowController?
// MARK: - Audio settings state (source of truth read/written by SettingsWindowController)
// The input mode / VAD threshold / mic gain / PTT key persist via UserDefaults (didSet below)
// so they survive relaunch; loadPersistedAudioSettings() restores them at startup and they are
// pushed into the core when the mic stream starts (micToggleClicked).
internal var selectedInputMode: VoiceCatInputMode = .voiceActivation
internal var vadThresholdValue: Float = 0.05
enum AudioDefaults {
static let inputMode = "voice.inputMode"
static let vadThreshold = "voice.vadThreshold"
static let inputGain = "voice.inputGain"
static let pttKeyCode = "voice.pttKeyCode"
}
internal var selectedInputMode: VoiceCatInputMode = .voiceActivation {
didSet { UserDefaults.standard.set(Int(selectedInputMode.rawValue), forKey: AudioDefaults.inputMode) }
}
internal var vadThresholdValue: Float = 0.05 {
didSet { UserDefaults.standard.set(vadThresholdValue, forKey: AudioDefaults.vadThreshold) }
}
internal var inputGain: Float = 1.0 {
didSet { UserDefaults.standard.set(inputGain, forKey: AudioDefaults.inputGain) }
}
internal var selectedInputDeviceId: String?
// MARK: - UI components
@@ -97,12 +116,32 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
super.init(window: window)
window.delegate = self
loadPersistedAudioSettings()
buildUI()
buildToolbar()
wireEvents()
bootstrap()
}
/// Restore the saved input mode / VAD threshold / mic gain / PTT key from UserDefaults so a
/// relaunch keeps the user's transmission settings instead of resetting to VAD defaults.
private func loadPersistedAudioSettings() {
let d = UserDefaults.standard
if d.object(forKey: AudioDefaults.inputMode) != nil {
let raw = UInt32(d.integer(forKey: AudioDefaults.inputMode))
selectedInputMode = VoiceCatInputMode(rawValue: raw) ?? .voiceActivation
}
if d.object(forKey: AudioDefaults.vadThreshold) != nil {
vadThresholdValue = d.float(forKey: AudioDefaults.vadThreshold)
}
if d.object(forKey: AudioDefaults.inputGain) != nil {
inputGain = d.float(forKey: AudioDefaults.inputGain)
}
if d.object(forKey: AudioDefaults.pttKeyCode) != nil {
pttKeyCode = UInt16(d.integer(forKey: AudioDefaults.pttKeyCode))
}
}
required init?(coder: NSCoder) { fatalError() }
deinit {
@@ -712,6 +751,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
if selectedInputMode == .voiceActivation {
client.setVadThreshold(vadThresholdValue)
}
client.setInputGain(inputGain)
setVoiceJoinedState(true)
addActivity("Joined voice — microphone active")
EventFeedback.shared.play(.voiceOn)