feat(android): implement setInputDevice, which was a no-op - #1255
Open
kimchouard wants to merge 1 commit into
Open
feat(android): implement setInputDevice, which was a no-op#1255kimchouard wants to merge 1 commit into
kimchouard wants to merge 1 commit into
Conversation
`AudioManager.setInputDevice` was implemented on iOS and did nothing on Android: the module method carried a `// TODO: noop for now` and resolved unconditionally, so a caller could not tell that the selection had been ignored and capture stayed on the platform default. The selection now reaches the recorder. `AudioInputSelection` holds the preferred device id for the process, since Android routes capture per process and the selection arrives through the `AudioAPIModule` TurboModule, which holds no reference to any recorder. `AndroidAudioRecorder::openAudioStream()` reads it and passes it to `oboe::AudioStreamBuilder::setDeviceId()`. Nothing in the shared `common/cpp` layer depends on any of it. Two properties are worth calling out, because both are easy to get wrong: A stream that opened on a different device is replaced rather than reused. `stop()` stops the stream without closing it and `openAudioStream()` returned early whenever a stream already existed, so a recorder reused its first stream for every later `start()`. Applying the device only in the builder would have worked on the first recording and never again. A device that was not honoured fails the open. Oboe applies `setDeviceId` on the AAudio backend only and reports `kUnspecified` under OpenSL ES, so the opened stream's `getDeviceId()` is compared against the request and a mismatch closes the stream and reports an error. Recording from a device the caller did not choose is worse than not recording. Callers that never select a device are unaffected: the selection is `oboe::kUnspecified`, `setDeviceId` is never called, the readback comparison is skipped and the early return behaves exactly as before. Also adds the three `TYPE_USB_*` categories to `parseDeviceCategory`, which previously rendered a USB interface as "Other (11)", and populates `currentInputs`, which was always empty so `useAudioInput` could not report the current device across a remount.
maciejmakowski2003
self-requested a review
August 27, 2026 08:24
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.
The bug
AudioManager.setInputDeviceworks on iOS and does nothing on Android — the module method is a// TODO: noop for nowthat resolves unconditionally. Capture stays on the platform default while the API reports success.Known gap (the docs say so, and #734 was closed by #926 which shipped iOS only). I hit it building a feature that records a USB-connected instrument on a phone.
The fix
AudioInputSelectionholds the preferred device id for the process;AndroidAudioRecorder::openAudioStream()reads it and passes it tooboe::AudioStreamBuilder::setDeviceId().Process-wide rather than per-recorder because Android routes capture per process, and the selection arrives through the
AudioAPIModuleTurboModule, which holds no reference to any recorder. Nothing incommon/cppdepends on it. I chose to make the existing cross-platform API work rather than add aRecorderOptions.deviceIdthat would diverge from iOS.Two parts look like more than the job needs, so briefly:
A stale stream is replaced, not reused.
stop()never closes the stream andopenAudioStream()returned early whenever one existed — so a recorder reuses its first stream for every laterstart(). Setting the device only in the builder would work on the first recording and never again.A device that wasn't honoured fails the open. Oboe applies
setDeviceIdon AAudio only and reportskUnspecifiedunder OpenSL ES, so the opened stream'sgetDeviceId()is checked against the request and a mismatch closes the stream and errors. Recording from a device the caller didn't pick seemed worse than not recording. (Deliberately not forcingsetAudioApi(AAudio)— Oboe's header calls that "extremely risky" on 8.0.)Nothing changes for callers who don't use it: with no selection the value is
oboe::kUnspecified,setDeviceIdis never called, the readback is skipped, and the early return behaves exactly as before.Also:
TYPE_USB_DEVICE/TYPE_USB_HEADSET/TYPE_USB_ACCESSORYadded toparseDeviceCategory(a USB interface enumerated as"Other (11)"), andcurrentInputspopulated (it was always empty, souseAudioInputcould never report the current device).Questions for you
prepare()ing the callback at a possibly different sample rate and re-init()ing the adapter node while the audio thread is inonAudioReady— which looked like the duplex-stream work the TODO defers. Say the word and I'll implement the reroute instead.setPreferredInput:nil; Android now mirrors that). I had a cross-platform fix and pulled it out — wideningdeviceIdtostring | nullin the spec is your API call, not a bug fix. Happy to open it separately.android/src/oldarch/NativeAudioAPIModuleSpec.javais hand-maintained and nothing in the repo compiles it. Untouched here, but it can drift from codegen silently.Verification
All green:
validate:android(409 C++ tests, NDK build),lint:cpp,lint:kotlin,lint:js,format:check:js,format:check:android:kotlin,typecheck,test:js(79),check-audio-enum-sync.Not tested on a device. The readback assert, the reconnect path and reject-while-paused are compiled and reasoned about, never executed on hardware. Happy to check anything specific if you can point me at it.
format:checkfor C++ is red, but not from this PR — android 24 / ios 50 / common 1325, identical counts at base commitc12bd7c9. My files appear in none of the lists. Measured with clang-format 23.1.0; older releases may report differently.Left out to keep this focused:
TYPE_BLE_HEADSETis API 31 andparseDeviceCategoryis@RequiresApi(O), so it needs an annotation bump.