Browse Source

fix: stop mic hardware immediately when idle, make HUD always on top, and dual-layer hotkey listener

main
Ali Alavi 2 weeks ago
parent
commit
e1ce9b9b33
  1. 13
      src/AppDelegate.swift
  2. 19
      src/AudioRecorder.swift
  3. 22
      src/HUDOverlay.swift
  4. 74
      src/HotkeyManager.swift

13
src/AppDelegate.swift

@ -82,8 +82,8 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
// Pre-warm the background WebSocket session for 0ms startup delay
SonioxSessionPool.shared.prewarmNextSession()
// Pre-warm audio hardware capture pipeline for instant 0ms recording
try? audioRecorder.ensureSessionRunning()
// Pre-configure audio hardware capture graph (kept STOPPED until key is pressed)
try? audioRecorder.ensureSessionConfigured()
// Check permissions on start
checkInitialPermissions()
@ -119,7 +119,11 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
}
public func startRecording() {
if audioRecorder.isRecording || isBusyFinalizing { return }
if audioRecorder.isRecording { return }
if isBusyFinalizing {
print("AppDelegate: isBusyFinalizing was active when startRecording invoked. Clearing stale lock.")
isBusyFinalizing = false
}
isNetworkDegraded = false
lastRecordedAudio.removeAll()
@ -219,6 +223,9 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak self] in
guard let self = self, self.isBusyFinalizing else { return }
print("AppDelegate: Finalize safety watchdog triggered — recovering transcript.")
self.isBusyFinalizing = false
self.statusBarController.updateIcon(state: .idle)
HUDOverlayController.shared.hide()
self.activeSession?.completeWithCurrentText()
}

19
src/AudioRecorder.swift

@ -58,8 +58,8 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer
return AVCaptureDevice.default(for: .audio) ?? all.first
}
public func ensureSessionRunning() throws {
if let session = captureSession, session.isRunning {
public func ensureSessionConfigured() throws {
if captureSession != nil {
return
}
guard let device = resolveCaptureDevice() else {
@ -77,8 +77,7 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer
}
self.captureSession = session
self.audioOutput = output
session.startRunning()
print("AudioRecorder: Pre-warmed audio session primed with device:", device.localizedName)
print("AudioRecorder: Capture session graph configured for device:", device.localizedName)
}
public func startRecording() throws {
@ -88,9 +87,12 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer
if isRecording { return }
pcmBuffer.removeAll()
try ensureSessionRunning()
try ensureSessionConfigured()
if let session = captureSession, !session.isRunning {
session.startRunning()
}
isRecording = true
print("AudioRecorder: Instant 0ms recording started.")
print("AudioRecorder: Fast recording started (mic hardware ON).")
}
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
@ -178,8 +180,9 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer
if !isRecording { return pcmBuffer }
isRecording = false
// Keep captureSession running so subsequent presses start in 0ms!
print("AudioRecorder: Stopped. Total PCM captured: \(pcmBuffer.count) bytes (\(Double(pcmBuffer.count)/32000.0) seconds). Audio engine kept hot.")
// Immediately stop hardware capture so macOS orange mic indicator turns OFF
captureSession?.stopRunning()
print("AudioRecorder: Stopped. Microphone hardware OFF. Total PCM captured: \(pcmBuffer.count) bytes.")
return pcmBuffer
}
}

22
src/HUDOverlay.swift

@ -35,7 +35,7 @@ public final class HUDOverlayController {
)
panel.isOpaque = false
panel.backgroundColor = .clear
panel.level = .floating
panel.level = .statusBar
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
panel.hasShadow = false
panel.ignoresMouseEvents = true
@ -54,7 +54,7 @@ public final class HUDOverlayController {
}
private func reposition() {
guard let window = self.window, let screen = NSScreen.main else { return }
guard let window = self.window, let screen = NSScreen.main ?? NSScreen.screens.first else { return }
let screenRect = screen.visibleFrame
// Center horizontally, positioned comfortably higher above dock
let x = screenRect.midX - (canvasSize / 2.0)
@ -76,17 +76,17 @@ public final class HUDOverlayController {
self.hide()
case .recording(let level, let isLagging, let pingQuality):
if !self.isVisible {
if !self.isVisible || window.alphaValue < 0.95 {
window.orderFrontRegardless()
window.animator().alphaValue = 1.0
window.alphaValue = 1.0
self.isVisible = true
}
orb.updateAudioLevel(level, isLagging: isLagging, pingQuality: pingQuality)
case .retrying:
if !self.isVisible {
if !self.isVisible || window.alphaValue < 0.95 {
window.orderFrontRegardless()
window.animator().alphaValue = 1.0
window.alphaValue = 1.0
self.isVisible = true
}
orb.showRetryState()
@ -112,13 +112,15 @@ public final class HUDOverlayController {
public func hide() {
DispatchQueue.main.async { [weak self] in
guard let self = self, let window = self.window, self.isVisible else { return }
self.isVisible = false
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.18
context.duration = 0.15
window.animator().alphaValue = 0.0
}, completionHandler: {
window.orderOut(nil)
self.isVisible = false
self.orbView?.reset()
if !self.isVisible {
window.orderOut(nil)
self.orbView?.reset()
}
})
}
}

74
src/HotkeyManager.swift

@ -148,13 +148,29 @@ public final class HotkeyManager {
// 2. Global Event Tap for high-priority interception (Microphone Key, Option, CapsLock, Fn/Globe)
setupEventTap()
// 3. Fallback NSEvent monitor
// 3. Dual-layer fallback NSEvent monitor (catches flagsChanged/Option even after wake/sleep)
setupGlobalMonitor()
// 4. Watch for system and display sleep/wake to ensure event tap is always alive
NotificationCenter.default.removeObserver(self, name: NSWorkspace.didWakeNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { [weak self] _ in
print("HotkeyManager: System woke from sleep. Re-registering hotkeys...")
self?.registerHotkeys()
}
NotificationCenter.default.removeObserver(self, name: NSWorkspace.screensDidWakeNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.screensDidWakeNotification, object: nil, queue: .main) { [weak self] _ in
print("HotkeyManager: Screens woke. Verifying hotkey event tap...")
if let tap = self?.eventTapPort {
CGEvent.tapEnable(tap: tap, enable: true)
} else {
self?.registerHotkeys()
}
}
}
private func triggerAction() {
let now = Date().timeIntervalSince1970
guard (now - lastTriggerTime) > 0.25 else { return }
guard (now - lastTriggerTime) > 0.10 else { return }
lastTriggerTime = now
DispatchQueue.main.async { [weak self] in
self?.onHotkeyPressed?()
@ -169,6 +185,15 @@ public final class HotkeyManager {
guard let refcon = refcon else { return Unmanaged.passRetained(event) }
let manager = Unmanaged<HotkeyManager>.fromOpaque(refcon).takeUnretainedValue()
// Auto-recover if macOS disabled tap on timeout or user input
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
print("HotkeyManager: EventTap was disabled by OS (\(type.rawValue)). Re-enabling now...")
if let tap = manager.eventTapPort {
CGEvent.tapEnable(tap: tap, enable: true)
}
return Unmanaged.passRetained(event)
}
let flags = event.flags.rawValue
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
@ -294,13 +319,50 @@ public final class HotkeyManager {
}
private func setupGlobalMonitor() {
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
guard let self = self, self.eventTapPort == nil else { return }
if let mon = globalMonitor {
NSEvent.removeMonitor(mon)
self.globalMonitor = nil
}
// Active backup monitor that never dies on sleep/wake
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged, .keyDown, .keyUp]) { [weak self] event in
guard let self = self else { return }
if self.currentPreset == .controlSpace {
if event.keyCode == 49 && event.modifierFlags.contains(.control) {
let flags = event.modifierFlags
let keyCode = event.keyCode
if self.currentPreset == .option {
if keyCode == 58 || keyCode == 61 { // Left or Right Option
let isAlt = flags.contains(.option)
if isAlt != self.isOptionPhysicallyDown {
self.isOptionPhysicallyDown = isAlt
if isAlt {
self.triggerAction()
} else if self.currentMode == .pushToTalk {
DispatchQueue.main.async {
self.onHotkeyReleased?()
}
}
}
}
} else if self.currentPreset == .controlSpace {
if keyCode == 49 && flags.contains(.control) {
self.triggerAction()
}
} else if self.currentPreset == .capsLock {
if keyCode == 57 {
let isCaps = flags.contains(.capsLock)
if isCaps != self.isCapsLockPhysicallyDown {
self.isCapsLockPhysicallyDown = isCaps
if isCaps {
self.triggerAction()
} else if self.currentMode == .pushToTalk {
DispatchQueue.main.async {
self.onHotkeyReleased?()
}
}
}
}
}
}
}

Loading…
Cancel
Save