From e31383a8f17657248bebeb39e3b9177f30cbfb89 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 12 Dec 2025 21:59:04 +0000 Subject: [PATCH] fix(ios): harden voice wake callbacks --- apps/ios/Sources/Voice/VoiceWakeManager.swift | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift index 30d6d8282..f9cc8bc3b 100644 --- a/apps/ios/Sources/Voice/VoiceWakeManager.swift +++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift @@ -106,33 +106,46 @@ final class VoiceWakeManager: NSObject, ObservableObject { self.recognitionTask = self.speechRecognizer?.recognitionTask(with: request) { [weak self] result, error in guard let self else { return } - if let error { - self.statusText = "Recognizer error: \(error.localizedDescription)" - self.isListening = false - if self.isEnabled { - Task { - try? await Task.sleep(nanoseconds: 700_000_000) - await self.start() - } - } - return + Task { @MainActor in + self.handleRecognitionCallback(result: result, error: error) } - guard let result else { return } + } + } - let transcript = result.bestTranscription.formattedString - if let cmd = self.extractCommand(from: transcript) { - if cmd != self.lastDispatched { - self.lastDispatched = cmd - self.statusText = "Triggered" - Task { [weak self] in - guard let self else { return } - await self.onCommand?(cmd) - if self.isEnabled { - await self.start() - } - } + private func handleRecognitionCallback(result: SFSpeechRecognitionResult?, error: Error?) { + if let error { + self.statusText = "Recognizer error: \(error.localizedDescription)" + self.isListening = false + + let shouldRestart = self.isEnabled + if shouldRestart { + Task { + try? await Task.sleep(nanoseconds: 700_000_000) + await self.start() } } + return + } + + guard let result else { return } + let transcript = result.bestTranscription.formattedString + guard let cmd = self.extractCommand(from: transcript) else { return } + + if cmd == self.lastDispatched { return } + self.lastDispatched = cmd + self.statusText = "Triggered" + + Task { [weak self] in + guard let self else { return } + await self.onCommand?(cmd) + await self.startIfEnabled() + } + } + + private func startIfEnabled() async { + let shouldRestart = self.isEnabled + if shouldRestart { + await self.start() } } @@ -150,7 +163,7 @@ final class VoiceWakeManager: NSObject, ObservableObject { try session.setCategory(.playAndRecord, mode: .measurement, options: [ .duckOthers, .mixWithOthers, - .allowBluetooth, + .allowBluetoothHFP, .defaultToSpeaker, ]) try session.setActive(true, options: []) @@ -158,8 +171,10 @@ final class VoiceWakeManager: NSObject, ObservableObject { private static func requestMicrophonePermission() async -> Bool { await withCheckedContinuation { cont in - AVAudioSession.sharedInstance().requestRecordPermission { ok in - cont.resume(returning: ok) + AVAudioApplication.requestRecordPermission { ok in + Task { @MainActor in + cont.resume(returning: ok) + } } } } @@ -167,7 +182,9 @@ final class VoiceWakeManager: NSObject, ObservableObject { private static func requestSpeechPermission() async -> Bool { await withCheckedContinuation { cont in SFSpeechRecognizer.requestAuthorization { status in - cont.resume(returning: status == .authorized) + Task { @MainActor in + cont.resume(returning: status == .authorized) + } } } }