fix(ios): stop miniaudio from clobbering AVAudioSession (stereo->A2DP output death)
The real root cause of "selecting Stereo Mic kills headphone/A2DP output on Join Voice." Every prior fix worked on the Swift IOSAudioRouter under the false premise that "miniaudio does NOT touch AVAudioSession on iOS." It does: the core opened devices via ma_device_init(nullptr, ...), and with a NULL context miniaudio 0.11.25 runs an iOS "hack" that sets the session category by device type, then ma_context_init__coreaudio calls setCategory()+setActive() on every device open -- capture -> AVAudioSessionCategoryRecord with zero options. That wipes the .playAndRecord category, the mode, and .allowBluetoothA2DP / .mixWithOthers / .allowAirPlay that IOSAudioRouter had just configured, killing headphone/A2DP (and even wired) output. Stereo presets break worst because they rely on the A2DP output route the wipe removes. TeamTalk avoids this by opening RemoteIO/VPIO AudioUnits directly and leaving the session entirely to the app. Fix (core, cross-platform safe): AudioEngine now owns a ma_context built by make_context_config() with coreaudio.sessionCategory = ma_ios_session_category_none and noAudioSessionActivate/Deactivate = MA_TRUE, and routes all ma_device_init calls (playback, capture, loopback) plus enumerate_devices through it. miniaudio no longer touches AVAudioSession; IOSAudioRouter is the sole owner (the session is already activated on connect in AppState before any device opens). Context is lazily inited in start(), reused across restarts, uninited in ~AudioEngine. Adds TEMP AudioSessionManager.logSessionState() diagnostics (after activate, on route change, on .streamStarted) to verify on-device that the category stays PlayAndRecord+allowBluetoothA2DP instead of flipping to Record. Remove once confirmed. Windows: cmake --build --preset dev clean; ctest --preset dev 21/21. iOS build + on-device verification pending on Mac. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -292,6 +292,26 @@ class AudioEngine {
|
||||
void on_capture(const int16_t* pcm, ma_uint32 frames);
|
||||
void on_playback(int16_t* out, ma_uint32 frames);
|
||||
|
||||
// Build the ma_context config that keeps miniaudio from managing AVAudioSession on iOS.
|
||||
// With a NULL context, ma_device_init runs miniaudio's iOS "hack" (miniaudio.h ~44057)
|
||||
// that calls setCategory()/setActive() on EVERY device open — capture →
|
||||
// AVAudioSessionCategoryRecord with zero options. That obliterates the category/mode/
|
||||
// options the Swift IOSAudioRouter configured (notably .playAndRecord and
|
||||
// .allowBluetoothA2DP), which is what killed headphone/A2DP output when stereo was
|
||||
// selected. The iOS Swift layer is the SOLE owner of the audio session (activated in
|
||||
// AppState on connect, configured by IOSAudioRouter); miniaudio must only open the
|
||||
// AudioUnit against the already-configured, already-active route. These coreaudio fields
|
||||
// are no-ops on non-Apple backends. See PROGRESS.md / the "stereo mic kills output"
|
||||
// investigation.
|
||||
static ma_context_config make_context_config();
|
||||
|
||||
// Owned context, shared by the playback, capture, and loopback devices so they all honor
|
||||
// the no-session-management config above. Lives for the engine's lifetime (init lazily in
|
||||
// start(), reused across stop()/start() restarts, uninit in the destructor) — loopback has
|
||||
// an independent start/stop lifecycle, so the context must outlive a single stop().
|
||||
ma_context context_{};
|
||||
bool context_inited_ = false;
|
||||
|
||||
ma_device capture_device_{};
|
||||
ma_device playback_device_{};
|
||||
bool capture_started_ = false;
|
||||
|
||||
Reference in New Issue
Block a user