Voice: fix audio-session leak causing insufficientPriority (561017449)
Decoded the device error: NSOSStatusErrorDomain 561017449 = '!pri' = AVAudioSession insufficientPriority — the mic was denied because a session was left active (which also blocks the keyboard's own dictation). Root cause: the early-return guards (recognizer unavailable / invalid format) ran AFTER setActive(true) but never deactivated, leaking an active session. Also the .measurement + .duckOthers config is more prone to priority denials. Fixes: - Deactivate any stale active session before starting. - Use .record / .default (least-exclusive recording config). - stopEngine() now always releases the session (no early-out), and the bailout guards call it so the session is never left active. - Map 561017449 to an actionable message (close other audio apps / restart). Device-specific; not reproducible on the sim (blocks at the permission dialog). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,12 +69,19 @@ final class SpeechRecognizer: ObservableObject {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let session = AVAudioSession.sharedInstance()
|
let session = AVAudioSession.sharedInstance()
|
||||||
try session.setCategory(.playAndRecord, mode: .measurement, options: [.duckOthers, .defaultToSpeaker])
|
// Release any session left active by a prior attempt — a stuck-active
|
||||||
|
// session causes error 561017449 ('!pri', insufficientPriority) and can
|
||||||
|
// also block the keyboard's own dictation from grabbing the mic.
|
||||||
|
try? session.setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
// Plain .record/.default is the least-exclusive recording config; the
|
||||||
|
// earlier .measurement + .duckOthers combo is more prone to priority denials.
|
||||||
|
try session.setCategory(.record, mode: .default)
|
||||||
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
||||||
|
|
||||||
guard let recognizer, recognizer.isAvailable else {
|
guard let recognizer, recognizer.isAvailable else {
|
||||||
error = "Speech recognition is not available right now."
|
error = "Speech recognition is not available right now."
|
||||||
print("[VOICE] Recognizer unavailable")
|
print("[VOICE] Recognizer unavailable")
|
||||||
|
stopEngine() // release the session we just activated
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +93,7 @@ final class SpeechRecognizer: ObservableObject {
|
|||||||
guard fmt.sampleRate > 0, fmt.channelCount > 0 else {
|
guard fmt.sampleRate > 0, fmt.channelCount > 0 else {
|
||||||
error = "No microphone input available on this device."
|
error = "No microphone input available on this device."
|
||||||
print("[VOICE] Invalid input format: \(fmt)")
|
print("[VOICE] Invalid input format: \(fmt)")
|
||||||
|
stopEngine() // release the session we just activated
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +133,13 @@ final class SpeechRecognizer: ObservableObject {
|
|||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
let ns = error as NSError
|
let ns = error as NSError
|
||||||
self.error = "Could not start recording. (\(ns.domain) \(ns.code))"
|
// 561017449 = '!pri' = AVAudioSession insufficientPriority: another app/
|
||||||
|
// process holds the mic, or the audio system is in a stuck state.
|
||||||
|
if ns.code == 561017449 {
|
||||||
|
self.error = "Microphone is busy. Close other audio apps (calls, Voice Memos), then try again. If it persists, restart your phone."
|
||||||
|
} else {
|
||||||
|
self.error = "Could not start recording. (\(ns.code))"
|
||||||
|
}
|
||||||
print("[VOICE] Engine start failed: \(ns.domain) \(ns.code) — \(error.localizedDescription)")
|
print("[VOICE] Engine start failed: \(ns.domain) \(ns.code) — \(error.localizedDescription)")
|
||||||
stopEngine()
|
stopEngine()
|
||||||
}
|
}
|
||||||
@@ -137,14 +151,15 @@ final class SpeechRecognizer: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func stopEngine() {
|
private func stopEngine() {
|
||||||
guard audioEngine.isRunning || task != nil else { return }
|
if audioEngine.isRunning { audioEngine.stop() }
|
||||||
audioEngine.stop()
|
|
||||||
audioEngine.inputNode.removeTap(onBus: 0)
|
audioEngine.inputNode.removeTap(onBus: 0)
|
||||||
request?.endAudio()
|
request?.endAudio()
|
||||||
task?.finish()
|
task?.finish()
|
||||||
request = nil
|
request = nil
|
||||||
task = nil
|
task = nil
|
||||||
isRecording = false
|
isRecording = false
|
||||||
|
// Always release the audio session, even on a failed/partial start — a
|
||||||
|
// session left active denies the mic to us and to keyboard dictation.
|
||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user