From afd6919d387887d59ac3be189e01b4b78965d71f Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Fri, 25 Sep 2026 20:47:34 +0000 Subject: [PATCH] perf: eliminate ~1s input latency via warm audio pipeline, persistent WebSocket heartbeat pool, and 10ms paste pacing --- src/AppDelegate.swift | 3 ++ src/AudioRecorder.swift | 38 ++++++++++----------- src/InputBoxManager.swift | 2 +- src/SonioxClient.swift | 72 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 90 insertions(+), 25 deletions(-) diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index 96c0cb0..c9d0374 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -82,6 +82,9 @@ 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() + // Check permissions on start checkInitialPermissions() } diff --git a/src/AudioRecorder.swift b/src/AudioRecorder.swift index 9eaf25f..ba57042 100644 --- a/src/AudioRecorder.swift +++ b/src/AudioRecorder.swift @@ -58,37 +58,39 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer return AVCaptureDevice.default(for: .audio) ?? all.first } - public func startRecording() throws { - lock.lock() - defer { lock.unlock() } - - if isRecording { return } - pcmBuffer.removeAll() - + public func ensureSessionRunning() throws { + if let session = captureSession, session.isRunning { + return + } guard let device = resolveCaptureDevice() else { throw NSError(domain: "AudioRecorder", code: 1, userInfo: [NSLocalizedDescriptionKey: "No microphone device found"]) } - let session = AVCaptureSession() let input = try AVCaptureDeviceInput(device: device) - if session.canAddInput(input) { session.addInput(input) } - let output = AVCaptureAudioDataOutput() output.setSampleBufferDelegate(self, queue: captureQueue) - if session.canAddOutput(output) { session.addOutput(output) } - self.captureSession = session self.audioOutput = output - session.startRunning() + print("AudioRecorder: Pre-warmed audio session primed with device:", device.localizedName) + } + + public func startRecording() throws { + lock.lock() + defer { lock.unlock() } + + if isRecording { return } + pcmBuffer.removeAll() + + try ensureSessionRunning() isRecording = true - print("AudioRecorder: Started recording with device:", device.localizedName) + print("AudioRecorder: Instant 0ms recording started.") } public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { @@ -176,12 +178,8 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer if !isRecording { return pcmBuffer } isRecording = false - captureSession?.stopRunning() - captureSession = nil - audioOutput = nil - audioConverter = nil - - print("AudioRecorder: Stopped. Total PCM captured: \(pcmBuffer.count) bytes (\(Double(pcmBuffer.count)/32000.0) seconds)") + // 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.") return pcmBuffer } } diff --git a/src/InputBoxManager.swift b/src/InputBoxManager.swift index b555c5d..48d8602 100644 --- a/src/InputBoxManager.swift +++ b/src/InputBoxManager.swift @@ -77,7 +77,7 @@ public final class InputBoxManager { pb.clearContents() pb.setString(text, forType: .string) self.postCleanCmdV() - usleep(25000) // 25ms pacing between chunks + usleep(10000) // Ultra-fast 10ms pacing between streaming chunks } } diff --git a/src/SonioxClient.swift b/src/SonioxClient.swift index 46cc6dc..dadbf91 100644 --- a/src/SonioxClient.swift +++ b/src/SonioxClient.swift @@ -26,6 +26,7 @@ public final class SonioxLiveSession { public private(set) var hasNetworkFailure: Bool = false private var lastDataTime: Date = Date() + private var heartbeatTimer: Timer? public var isReady: Bool { lock.lock() @@ -33,6 +34,42 @@ public final class SonioxLiveSession { return isConnected && !isClosed && !isFinalizing } + public func startKeepAliveHeartbeat() { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.heartbeatTimer?.invalidate() + self.heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 6.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.lock.lock() + guard !self.isClosed, !self.isFinalizing, let task = self.webSocketTask else { + self.lock.unlock() + return + } + self.lock.unlock() + + task.sendPing { [weak self] error in + guard let self = self else { return } + if let error = error { + print("SonioxLiveSession: Heartbeat ping failed (\(error)). Marking stale.") + self.lock.lock() + self.isConnected = false + self.hasNetworkFailure = true + self.isClosed = true + self.lock.unlock() + SonioxSessionPool.shared.prewarmNextSession() + } + } + } + } + } + + public func stopKeepAliveHeartbeat() { + DispatchQueue.main.async { [weak self] in + self?.heartbeatTimer?.invalidate() + self?.heartbeatTimer = nil + } + } + public init(languageHints: [String] = ["fa", "en", "ar"], endpoint: String = SonioxLiveSession.primaryWsBase) { let hints = languageHints.map { "language_hints=\($0)" }.joined(separator: "&") let urlStr = "\(endpoint)?\(hints)&enable_speaker_diarization=false&enable_language_identification=true&enable_endpoint_detection=false&providers=soniox" @@ -493,6 +530,7 @@ public final class SonioxSessionPool { private var prewarmedSession: SonioxLiveSession? private let lock = NSLock() + private var isPrewarming = false private init() { prewarmNextSession() @@ -506,15 +544,37 @@ public final class SonioxSessionPool { return } - let hints = SonioxSettings.shared.language.hints - let session = SonioxLiveSession(languageHints: hints) - self.prewarmedSession = session + if isPrewarming { return } + isPrewarming = true + + prewarmedSession?.stopKeepAliveHeartbeat() + prewarmedSession?.cancel() + prewarmedSession = nil + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + let hints = SonioxSettings.shared.language.hints + let session = SonioxLiveSession(languageHints: hints) + + // Allow 400ms for connection & initial ping + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in + guard let self = self else { return } + self.lock.lock() + session.startKeepAliveHeartbeat() + self.prewarmedSession = session + self.isPrewarming = false + self.lock.unlock() + print("SonioxSessionPool: Fresh prewarmed session initialized & heartbeat started.") + } + } } public func reconnectAll() { lock.lock() + prewarmedSession?.stopKeepAliveHeartbeat() prewarmedSession?.cancel() prewarmedSession = nil + isPrewarming = false lock.unlock() prewarmNextSession() } @@ -525,14 +585,18 @@ public final class SonioxSessionPool { prewarmedSession = nil lock.unlock() - DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { [weak self] in + // Immediately start pre-warming the NEXT session in the background + DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.prewarmNextSession() } if let session = session, session.isReady { + session.stopKeepAliveHeartbeat() + print("SonioxSessionPool: Acquired HOT prewarmed session (0ms connect delay!)") return session } + print("SonioxSessionPool: Prewarmed session not ready yet, creating immediate on-demand session.") let hints = SonioxSettings.shared.language.hints return SonioxLiveSession(languageHints: hints) }