Fix macOS input device selection without voice processing in AudioEngineDevice - #261
Draft
hiroshihorie wants to merge 2 commits into
Draft
Fix macOS input device selection without voice processing in AudioEngineDevice#261hiroshihorie wants to merge 2 commits into
hiroshihorie wants to merge 2 commits into
Conversation
…ineDevice Without voice processing, AVAudioEngine's input and output nodes share a single HAL I/O unit, so the previous per-direction device configuration could not work: setting the input device on the shared unit failed with kAudioUnitErr_InvalidPropertyValue once the graph was wired, and any earlier set re-routed both directions, collapsing output formats when the input device has no output streams. In practice selecting a non-default recording device always failed InitRecording in this mode. The shared unit is now configured in a dedicated step that runs before the graph is wired, and when the effective input and output devices differ, a private aggregate device combining them is created (output as clock master, drift compensation on the input sub device) and set as the unit's device. The aggregate is destroyed on engine release and recreation. The existing per-direction configuration is kept for the voice processing path, where the separate I/O units accept it.
The HAL-level stream readiness poll after aggregate creation was subsumed by the I/O unit format wait in the engine, which is where readiness actually matters for consumers. Replace the loop with a one-shot stream count log for error attribution. Also switch the deprecated Master sub device key spelling to Main, which maps to the same underlying dictionary key.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On macOS with voice processing disabled, selecting a non-default recording device always fails at
InitRecording. Without voice processing, AVAudioEngine backs the input and output nodes with a single shared HAL I/O unit, and a HAL unit talks to exactly one CoreAudio device (kAudioOutputUnitProperty_CurrentDeviceis one global slot). Per-direction device selection is therefore unsatisfiable in that shape:kAudioUnitErr_InvalidPropertyValue(-10851) once the graph is wired.The voice processing path is unaffected: VPIO is a full-duplex unit designed for per-direction device control (it manages an aggregate internally), which is why the Swift SDK (VP on by default) never hit this. The Rust SDK runs this engine with voice processing off by default on macOS, where "select a microphone" failed out of the box.
Fix
A new engine setup step configures the shared unit's device in the non-VP path, before the graph is wired (the HAL unit rejects device changes afterwards, and the node formats read during enable must reflect the target device):
One readiness wait proved necessary in testing: the I/O unit renegotiates its node formats asynchronously after a device change, so the engine waits (bounded ~1s, rollback on timeout) until channel counts are valid before wiring the graph. HAL-level aggregate composition turned out to be synchronous in practice - the stream counts are logged at creation for error attribution.
New helpers
CreatePrivateAggregateDevice/DestroyAggregateDevicelive inmac_audio_utils. The existing per-direction configuration is kept for the voice processing path, now explicitly gated to it.Testing
Verified on macOS arm64 via the LiveKit Rust SDK's
platform_audioexerciser against a local build:Not yet verified: audio quality under long-session clock drift through the aggregate (drift compensation is enabled, but deserves a listen test), and the default-output-change-while-aggregated scenario (should recreate via the existing default device listeners).