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:
32
PROGRESS.md
32
PROGRESS.md
@@ -10,6 +10,38 @@ up instantly. Newest status at the top.
|
||||
|
||||
## ▶ Where we left off / next action
|
||||
|
||||
- **Awaiting on-device verification:** **iOS stereo mic kills headphone/A2DP output — REAL
|
||||
root cause found & fixed** (2026-06-20, on Windows; verify on Mac). All prior "fixes" (the
|
||||
2026-06-19 entries below) targeted the Swift `IOSAudioRouter` on the false premise that
|
||||
"miniaudio does NOT touch AVAudioSession on iOS." **It does.** The core opened its miniaudio
|
||||
devices with `ma_device_init(nullptr, ...)`; with a NULL context, miniaudio (0.11.25) runs an
|
||||
iOS "hack" (`miniaudio.h` ~44057) that picks a session category by device type, then
|
||||
`ma_context_init__coreaudio` (~36552) calls `setCategory()` + `setActive()` on **every device
|
||||
open** — capture → `AVAudioSessionCategoryRecord` with **zero options**. That wiped the
|
||||
`.playAndRecord` category, the mode, and `.allowBluetoothA2DP`/`.mixWithOthers`/`.allowAirPlay`
|
||||
that `IOSAudioRouter` had just configured → headphone/A2DP (and even wired) output died. The
|
||||
stereo presets broke worst because they depend on the A2DP output route the wipe removed.
|
||||
TeamTalk never hits this: its SDK opens RemoteIO/VPIO AudioUnits directly and leaves the
|
||||
session entirely to the app (`UtilSound.swift`); miniaudio insists on managing it.
|
||||
- **Fix (core, cross-platform safe):** `AudioEngine` now owns a `ma_context` built by
|
||||
`make_context_config()` with `coreaudio.sessionCategory = ma_ios_session_category_none` +
|
||||
`noAudioSessionActivate`/`noAudioSessionDeactivate = MA_TRUE`, and passes it to **all**
|
||||
`ma_device_init` calls (playback, capture, loopback) and to `enumerate_devices`'s context.
|
||||
miniaudio now never touches AVAudioSession; the Swift `IOSAudioRouter` is the sole owner
|
||||
(session is already activated on connect in `AppState.swift:authResult`, before any device
|
||||
opens, so removing miniaudio's self-activation is safe). Context is lazily inited in
|
||||
`start()`, reused across restarts, uninited in `~AudioEngine`.
|
||||
Files: `core/src/audio/audio_engine.{h,cpp}`.
|
||||
- **TEMP diagnostics (remove after verification):** `AudioSessionManager.logSessionState(_:)`
|
||||
logs category/mode/options/route; called after `ensureSessionActive`, on every route change,
|
||||
and on `.streamStarted` (right after the core opens its devices). On Mac, watch the log when
|
||||
joining voice with the Stereo Mic preset: category must stay `…PlayAndRecord` with
|
||||
`allowBluetoothA2DP` and the output route must remain the headphones/A2DP device — NOT flip
|
||||
to `…Record`. If confirmed, delete the `logSessionState` calls + method and the prior
|
||||
band-aid comments in `IOSAudioRouter`/`audio_engine.cpp` can be trimmed.
|
||||
- **Verified on Windows:** `cmake --build --preset dev` clean, `ctest --preset dev` 21/21.
|
||||
iOS build & on-device run still to be done by the user on the Mac.
|
||||
|
||||
- **Planned (not started):** **External PCM feed/tap API (`vc_stream_feed_pcm` +
|
||||
`vc_set_pcm_sink`)** (2026-06-19, plan written on Windows; implement on Mac). A public,
|
||||
documented API for driving audio streams with externally-provided PCM instead of (or in
|
||||
|
||||
Reference in New Issue
Block a user