Browse Source

perf: eliminate ~1s input latency via warm audio pipeline, persistent WebSocket heartbeat pool, and 10ms paste pacing

main
Ali Alavi 2 weeks ago
parent
commit
afd6919d38
  1. 3
      src/AppDelegate.swift
  2. 38
      src/AudioRecorder.swift
  3. 2
      src/InputBoxManager.swift
  4. 72
      src/SonioxClient.swift

3
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()
}

38
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
}
}

2
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
}
}

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

Loading…
Cancel
Save