Skip to content

feat(android): implement setInputDevice, which was a no-op - #1255

Open
kimchouard wants to merge 1 commit into
software-mansion:mainfrom
kimchouard:feat/android-input-device-selection
Open

feat(android): implement setInputDevice, which was a no-op#1255
kimchouard wants to merge 1 commit into
software-mansion:mainfrom
kimchouard:feat/android-input-device-selection

Conversation

@kimchouard

@kimchouard kimchouard commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The bug

AudioManager.setInputDevice works on iOS and does nothing on Android — the module method is a // TODO: noop for now that 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

AudioInputSelection holds the preferred device id for the process; AndroidAudioRecorder::openAudioStream() reads it and passes it to oboe::AudioStreamBuilder::setDeviceId().

Process-wide rather than per-recorder because Android routes capture per process, and the selection arrives through the AudioAPIModule TurboModule, which holds no reference to any recorder. Nothing in common/cpp depends on it. I chose to make the existing cross-platform API work rather than add a RecorderOptions.deviceId that 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 and openAudioStream() returned early whenever one existed — so a recorder reuses its first stream for every later start(). 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 setDeviceId on AAudio only and reports kUnspecified under OpenSL ES, so the opened stream's getDeviceId() 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 forcing setAudioApi(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, setDeviceId is never called, the readback is skipped, and the early return behaves exactly as before.

Also: TYPE_USB_DEVICE / TYPE_USB_HEADSET / TYPE_USB_ACCESSORY added to parseDeviceCategory (a USB interface enumerated as "Other (11)"), and currentInputs populated (it was always empty, so useAudioInput could never report the current device).

Questions for you

  1. What should this do while a recorder is running? iOS reroutes live. Android binds the device as the stream opens, so it can't. This rejects, telling the caller to stop → select → start, on the reasoning that resolving while silently deferring is the worse failure. A live restart means reopening the file, re-prepare()ing the callback at a possibly different sample rate and re-init()ing the adapter node while the audio thread is in onAudioReady — which looked like the duplex-stream work the TODO defers. Say the word and I'll implement the reroute instead.
  2. Clearing the preference isn't possible on either platform today (iOS rejects a nil id before reaching setPreferredInput:nil; Android now mirrors that). I had a cross-platform fix and pulled it out — widening deviceId to string | null in the spec is your API call, not a bug fix. Happy to open it separately.
  3. android/src/oldarch/NativeAudioAPIModuleSpec.java is 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:check for C++ is red, but not from this PR — android 24 / ios 50 / common 1325, identical counts at base commit c12bd7c9. 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_HEADSET is API 31 and parseDeviceCategory is @RequiresApi(O), so it needs an annotation bump.

`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.
@closetcaiman closetcaiman added feature New user-facing features or major capabilities android Native Android implementation, C++/Java/Kotlin bindings, or Android-specific issues labels Aug 26, 2026
@maciejmakowski2003
maciejmakowski2003 self-requested a review August 27, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android Native Android implementation, C++/Java/Kotlin bindings, or Android-specific issues feature New user-facing features or major capabilities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants