Skip to content

feat(android): AudioRecorder input preset option (androidInputPreset) - #1210

Open
lpmv wants to merge 3 commits into
software-mansion:mainfrom
lpmv:feat/android-recorder-input-preset
Open

feat(android): AudioRecorder input preset option (androidInputPreset)#1210
lpmv wants to merge 3 commits into
software-mansion:mainfrom
lpmv:feat/android-recorder-input-preset

Conversation

@lpmv

@lpmv lpmv commented Aug 2, 2026

Copy link
Copy Markdown

Why

The Oboe input stream builder in AndroidAudioRecorder::openAudioStream never calls setInputPreset, so every capture stream runs on Oboe's implicit default, InputPreset::VoiceRecognition — the speech-recognition preprocessing chain, which applies no acoustic echo cancellation. For duplex voice apps (playing TTS/voice through the same device while recording, VoIP-style — the use case asked about in #670), that makes Android capture echo-raw, while the equivalent iOS setup gets AEC from the voiceChat session mode.

What

An optional constructor option on AudioRecorder:

const recorder = new AudioRecorder({ androidInputPreset: 'voiceCommunication' });

mapped to Oboe's InputPreset on the capture stream builder. voiceCommunication engages the platform AEC/NS chain — the Android twin of iOS voiceChat.

  • Backwards compatible: when the option is omitted (or names an unknown preset) no setInputPreset call is made, so existing behavior is preserved exactly.
  • iOS ignores the option (its input chain is selected by the AVAudioSession mode); the host object comment says so.
  • Presets exposed: generic, camcorder, voiceRecognition, voiceCommunication, unprocessed, voicePerformance.

Notes

We measured the difference in a duplex probe (1024-sample pcm16 frames at 24 kHz, speaker playback at full volume while recording): on iOS under voiceChat the played audio re-enters the mic at +1.9 dB over the room floor; Android capture through the current builder has no platform AEC path at all. Happy to adjust the option's shape (e.g. a general AudioRecorderOptions bag was chosen so future per-platform stream options have a home).

The Oboe input stream builder never calls setInputPreset, so every capture
stream runs on Oboe's implicit default, InputPreset::VoiceRecognition - the
speech-recognition preprocessing chain, which applies no acoustic echo
cancellation. For duplex voice apps (playing audio through the same device
while recording, VoIP-style) that makes Android capture echo-raw, while the
equivalent iOS setup gets AEC from the voiceChat session mode.

This adds an optional constructor option to AudioRecorder:

  new AudioRecorder({ androidInputPreset: 'voiceCommunication' })

mapping to Oboe's InputPreset on the capture stream builder. When the option
is omitted (or names an unknown preset) no setInputPreset call is made, so
existing behavior is preserved exactly. iOS ignores the option; its input
chain is selected by the AVAudioSession mode instead.

Presets exposed: generic, camcorder, voiceRecognition, voiceCommunication,
unprocessed, voicePerformance.
@closetcaiman closetcaiman added the feature New user-facing features or major capabilities label Aug 5, 2026
@mdydek

mdydek commented Aug 11, 2026

Copy link
Copy Markdown
Member

looks similar to #1216, will probably try to merge your both approaches into one common api, but need some time to test it

Comment on lines +20 to +23
var createAudioRecorder: (
androidInputPreset: string,
iosVoiceProcessing: boolean
) => IAudioRecorder;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better if we passed an object instead of individual props? In case we would add more options.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withDefaultOptions helper seems ambiguous now. I think it could handle both file and preset options or split into two verbose helpers then merge, but now the logic is a bit scattered.

IOSAudioRecorder::IOSAudioRecorder(
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry)
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
bool voiceProcessingEnabled)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, shoudn't this be a member of some struct IOSAudioRecorderOptions?

@@ -29,7 +29,9 @@ class AudioFileWriter;

class IOSAudioRecorder : public AudioRecorder {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule of 5.

Comment on lines +122 to +137
std::string androidInputPreset;
if (count > 0 && args[0].isString()) {
androidInputPreset = args[0].getString(runtime).utf8(runtime);
}

bool iosVoiceProcessing = false;
if (count > 1 && args[1].isBool()) {
iosVoiceProcessing = args[1].getBool();
}

auto audioRecorderHostObject = std::make_shared<AudioRecorderHostObject>(
audioEventHandlerRegistry, &runtime, jsCallInvoker);
audioEventHandlerRegistry,
&runtime,
jsCallInvoker,
androidInputPreset,
iosVoiceProcessing);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to do this parsing inline? Maybe we could extract this logic?

explicit AndroidAudioRecorder(
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry);
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
const std::string &inputPreset = "");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a reference? If AndroidAudioRecorder is the utlimate owner of inputPreset I think it should just use std::move here.

| `options` <Optional /> | [`AudioRecorderStartOptions`](#audiorecorderstartoptions) | Optional recording start configuration. |

#### Returns `Promise<Result<{}>>`.
###### Returns `Promise<Result<{}>>`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for incrementing the heading depth?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, with those options

---
sidebar_position: 4
toc_min_heading_level: 2
toc_max_heading_level: 5
---

h5 also is rendered on the right side as "clickable" shortcut which for sure we don't want to have

| 'voicePerformance';
```

Maps to the Android [audio input preset](https://developer.android.com/ndk/reference/group/audio#anonymous-enum-9) of the capture stream.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a useful information? Besides, don't we actually use oboe API? I know they basically wrap the AAudio, but even so the reported number of options do differ: oboe vs ndk

Comment on lines 38 to 47
export { default as Audio } from './Audio';
export { default as AudioControls } from './Audio/controls/AudioControls';
export type { MediaElementAudioSourceOptions } from './core/MediaElementAudioSourceNode';
export type {
AudioRecorderOptions,
AndroidInputPreset,
} from './core/AudioRecorder';
export type { default as AudioEventSubscription } from './events/AudioEventSubscription';
export { default as FilePreset } from './utils/filePresets';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really a react component used in <audio>?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New user-facing features or major capabilities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants