fix(ios): VPIO silent playback + quiet speaker in stereo/studio presets
Two on-device bugs in the native iOS Voice-Processing path (Swift-only; no core/ABI change). 1. Voice Chat (VPIO) silent playback: doStartMicStream() called audioRestart() BEFORE startStream, so when the engine was already running (a remote stream had started it) it reopened with external_capture=false and opened a hardware miniaudio capture device. The announce-result restart then early-returns (engine already running) so that device was never dropped and fought the AVAudioEngine VPIO unit, silencing playback. Now: setExternalPlayback first, then startStream (stores external_feed synchronously), THEN audioRestart() — the core reopens in full external mode (no hardware devices). Added VPIO diagnostics: graph/route formats at start, ring written/read totals at teardown. 2. Stereo Mic / Studio quiet earpiece: the .builtInMicBtA2dp presets omit .defaultToSpeaker (it breaks A2DP) and skip forceSpeaker, so with no Bluetooth connected output pinned to the quiet receiver. New IOSAudioRouter.applyA2dpSpeakerFallback() overrides to the built-in speaker when no external (A2DP/wired/AirPlay) output is present and clears the override when one is — called after activation and on device-change route changes.
This commit is contained in:
@@ -689,9 +689,35 @@ final class IOSAudioRouter: ObservableObject {
|
||||
showsA2dpNoAecWarning = (bluetoothMode == .builtInMicBtA2dp)
|
||||
}
|
||||
|
||||
/// Whether the current configuration wants Bluetooth A2DP output. Used after session
|
||||
/// activation to clear any lingering speaker override that would pin output to the speaker.
|
||||
var wantsA2dpOutput: Bool { bluetoothMode == .builtInMicBtA2dp }
|
||||
/// Route fallback for the A2DP-output presets (Stereo Mic / Studio / BT Headphones + Mono
|
||||
/// Mic, all `.builtInMicBtA2dp`). These presets deliberately omit `.defaultToSpeaker` (it
|
||||
/// breaks A2DP routing) and skip the `forceSpeaker` override, so when NO external output
|
||||
/// (Bluetooth A2DP / wired / AirPlay) is connected `.playAndRecord` pins output to the quiet
|
||||
/// built-in receiver (earpiece). This routes to the loud built-in speaker instead via a
|
||||
/// post-activation `overrideOutputAudioPort(.speaker)` — the documented "A2DP if connected,
|
||||
/// else speaker" behavior. When an external output IS present we clear the override so A2DP /
|
||||
/// headphones / AirPlay are honored. No-op outside `.builtInMicBtA2dp` mode (other modes pick
|
||||
/// their route via category options). Must be called AFTER the session is active.
|
||||
func applyA2dpSpeakerFallback() {
|
||||
guard bluetoothMode == .builtInMicBtA2dp else { return }
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
// Treat the built-in receiver and speaker as "internal"; anything else (A2DP, headphones,
|
||||
// USB, AirPlay) is an external output we should defer to.
|
||||
let hasExternalOutput = session.currentRoute.outputs.contains {
|
||||
$0.portType != .builtInReceiver && $0.portType != .builtInSpeaker
|
||||
}
|
||||
do {
|
||||
if hasExternalOutput {
|
||||
try session.overrideOutputAudioPort(.none)
|
||||
logger.info("A2DP mode — external output present, clearing speaker override")
|
||||
} else {
|
||||
try session.overrideOutputAudioPort(.speaker)
|
||||
logger.info("A2DP mode — no external output, routing to built-in speaker")
|
||||
}
|
||||
} catch {
|
||||
logger.error("A2DP speaker fallback failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected input port object, if any.
|
||||
var selectedPort: IOSAudioInputPort? {
|
||||
|
||||
Reference in New Issue
Block a user