fix(macos): guard voice audio paths with no input device (#25817)

Co-authored-by: Stefan Förster <103369858+sfo2001@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-25 00:09:57 +00:00
parent e11e510f5b
commit 236b22b6a2
8 changed files with 68 additions and 0 deletions

View File

@@ -53,6 +53,15 @@ final class AudioInputDeviceObserver {
return output
}
/// Returns true when the system default input device exists and is alive with input channels.
/// Use this preflight before accessing `AVAudioEngine.inputNode` to avoid SIGABRT on Macs
/// without a built-in microphone (Mac mini, Mac Pro, Mac Studio) or when an external mic
/// is disconnected.
static func hasUsableDefaultInputDevice() -> Bool {
guard let uid = self.defaultInputDeviceUID() else { return false }
return self.aliveInputDeviceUIDs().contains(uid)
}
static func defaultInputDeviceSummary() -> String {
let systemObject = AudioObjectID(kAudioObjectSystemObject)
var address = AudioObjectPropertyAddress(

View File

@@ -14,6 +14,13 @@ actor MicLevelMonitor {
if self.running { return }
self.logger.info(
"mic level monitor start (\(AudioInputDeviceObserver.defaultInputDeviceSummary(), privacy: .public))")
guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else {
self.engine = nil
throw NSError(
domain: "MicLevelMonitor",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "No usable audio input device available"])
}
let engine = AVAudioEngine()
self.engine = engine
let input = engine.inputNode

View File

@@ -185,6 +185,12 @@ actor TalkModeRuntime {
}
guard let audioEngine = self.audioEngine else { return }
guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else {
self.audioEngine = nil
self.logger.error("talk mode: no usable audio input device")
return
}
let input = audioEngine.inputNode
let format = input.outputFormat(forBus: 0)
input.removeTap(onBus: 0)

View File

@@ -244,6 +244,14 @@ actor VoicePushToTalk {
}
guard let audioEngine = self.audioEngine else { return }
guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else {
self.audioEngine = nil
throw NSError(
domain: "VoicePushToTalk",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "No usable audio input device available"])
}
let input = audioEngine.inputNode
let format = input.outputFormat(forBus: 0)
if self.tapInstalled {

View File

@@ -166,6 +166,14 @@ actor VoiceWakeRuntime {
}
guard let audioEngine = self.audioEngine else { return }
guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else {
self.audioEngine = nil
throw NSError(
domain: "VoiceWakeRuntime",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "No usable audio input device available"])
}
let input = audioEngine.inputNode
let format = input.outputFormat(forBus: 0)
guard format.channelCount > 0, format.sampleRate > 0 else {

View File

@@ -89,6 +89,14 @@ final class VoiceWakeTester {
self.logInputSelection(preferredMicID: micID)
self.configureSession(preferredMicID: micID)
guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else {
self.audioEngine = nil
throw NSError(
domain: "VoiceWakeTester",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "No usable audio input device available"])
}
let engine = AVAudioEngine()
self.audioEngine = engine