From e5b468f0117d58dd7d6e27c5f2e0bbed979cae19 Mon Sep 17 00:00:00 2001 From: Louis Vetter Date: Sun, 2 Aug 2026 14:45:37 +0200 Subject: [PATCH] feat(android): recorder input preset option (androidInputPreset) 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. --- .../android/core/AndroidAudioRecorder.cpp | 36 ++++++++++++++++++- .../android/core/AndroidAudioRecorder.h | 4 ++- .../cpp/audioapi/AudioAPIModuleInstaller.h | 6 +++- .../inputs/AudioRecorderHostObject.cpp | 7 ++-- .../inputs/AudioRecorderHostObject.h | 4 ++- .../src/AudioAPIModule/globals.d.ts | 2 +- packages/react-native-audio-api/src/api.ts | 1 + .../src/core/AudioRecorder.ts | 27 ++++++++++++-- 8 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp index c7403429b..e4b0e3ac6 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp @@ -18,15 +18,45 @@ #include #include +#include #include #include #include namespace audioapi { +namespace { +/// Maps the JS-facing preset name to Oboe's InputPreset. An unknown or empty +/// name yields no preset call, preserving Oboe's own default +/// (InputPreset::VoiceRecognition) exactly as before this option existed. +std::optional inputPresetFromString(const std::string &name) { + if (name == "generic") { + return oboe::InputPreset::Generic; + } + if (name == "camcorder") { + return oboe::InputPreset::Camcorder; + } + if (name == "voiceRecognition") { + return oboe::InputPreset::VoiceRecognition; + } + if (name == "voiceCommunication") { + return oboe::InputPreset::VoiceCommunication; + } + if (name == "unprocessed") { + return oboe::InputPreset::Unprocessed; + } + if (name == "voicePerformance") { + return oboe::InputPreset::VoicePerformance; + } + return std::nullopt; +} +} // namespace + AndroidAudioRecorder::AndroidAudioRecorder( - const std::shared_ptr &audioEventHandlerRegistry) + const std::shared_ptr &audioEventHandlerRegistry, + const std::string &inputPreset) : AudioRecorder(audioEventHandlerRegistry), + inputPreset_(inputPreset), streamSampleRate_(0.0), streamChannelCount_(0), streamMaxBufferSizeInFrames_(0) {} @@ -74,6 +104,10 @@ Result AndroidAudioRecorder::openAudioStream() { ->setDataCallback(shared_from_this()) ->setErrorCallback(shared_from_this()); + if (auto preset = inputPresetFromString(inputPreset_)) { + builder.setInputPreset(*preset); + } + auto result = builder.openStream(mStream_); if (result != oboe::Result::OK || mStream_ == nullptr) { diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h index 5f311434f..680c11ed7 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h @@ -25,7 +25,8 @@ class AndroidAudioRecorder : public oboe::AudioStreamCallback, public std::enable_shared_from_this { public: explicit AndroidAudioRecorder( - const std::shared_ptr &audioEventHandlerRegistry); + const std::shared_ptr &audioEventHandlerRegistry, + const std::string &inputPreset = ""); ~AndroidAudioRecorder() override; void cleanup(); @@ -63,6 +64,7 @@ class AndroidAudioRecorder : public oboe::AudioStreamCallback, private: std::shared_ptr deinterleavingBuffer_; + std::string inputPreset_; std::atomic streamSampleRate_; int32_t streamChannelCount_; int32_t streamMaxBufferSizeInFrames_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h index 762c4ce20..805744fe6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h @@ -117,8 +117,12 @@ class AudioAPIModuleInstaller { const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { + std::string androidInputPreset; + if (count > 0 && args[0].isString()) { + androidInputPreset = args[0].getString(runtime).utf8(runtime); + } auto audioRecorderHostObject = std::make_shared( - audioEventHandlerRegistry, &runtime, jsCallInvoker); + audioEventHandlerRegistry, &runtime, jsCallInvoker, androidInputPreset); auto jsiObject = jsi::Object::createFromHostObject(runtime, audioRecorderHostObject); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp index 5fe0476c4..53301d898 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp @@ -22,10 +22,13 @@ namespace audioapi { AudioRecorderHostObject::AudioRecorderHostObject( const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, - const std::shared_ptr &callInvoker) { + const std::shared_ptr &callInvoker, + const std::string &androidInputPreset) { #ifdef ANDROID - audioRecorder_ = std::make_shared(audioEventHandlerRegistry); + audioRecorder_ = std::make_shared(audioEventHandlerRegistry, androidInputPreset); #else + // The input preset is an Android concept (Oboe); iOS configures its input + // through the AVAudioSession mode instead. audioRecorder_ = std::make_shared(audioEventHandlerRegistry); #endif diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h index 6fda9690e..a9530d4d7 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h @@ -5,6 +5,7 @@ #include #include +#include namespace audioapi { using namespace facebook; @@ -17,7 +18,8 @@ class AudioRecorderHostObject : public HostObject { AudioRecorderHostObject( const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, - const std::shared_ptr &callInvoker); + const std::shared_ptr &callInvoker, + const std::string &androidInputPreset); JSI_HOST_FUNCTION_DECL(start); JSI_HOST_FUNCTION_DECL(stop); diff --git a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts index 655cde36b..60794261a 100644 --- a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts +++ b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts @@ -17,7 +17,7 @@ declare global { sampleRate: number ) => IOfflineAudioContext; - var createAudioRecorder: () => IAudioRecorder; + var createAudioRecorder: (androidInputPreset: string) => IAudioRecorder; var createAudioBuffer: ( numberOfChannels: number, diff --git a/packages/react-native-audio-api/src/api.ts b/packages/react-native-audio-api/src/api.ts index 5aa212b3b..40ea82d94 100644 --- a/packages/react-native-audio-api/src/api.ts +++ b/packages/react-native-audio-api/src/api.ts @@ -38,6 +38,7 @@ export * from './Audio'; 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'; diff --git a/packages/react-native-audio-api/src/core/AudioRecorder.ts b/packages/react-native-audio-api/src/core/AudioRecorder.ts index 7c9256cfb..9f03b3335 100644 --- a/packages/react-native-audio-api/src/core/AudioRecorder.ts +++ b/packages/react-native-audio-api/src/core/AudioRecorder.ts @@ -39,6 +39,29 @@ function withDefaultOptions( }; } +/** + * Options applied when the recorder is created. + */ +export interface AudioRecorderOptions { + /** + * Android only: the Oboe input preset for the capture stream. Android + * routes the microphone through a preprocessing chain selected by this + * preset; Oboe's implicit default is `voiceRecognition`, which applies NO + * acoustic echo cancellation. Duplex voice use cases (VoIP-style apps that + * play audio while recording) should pass `voiceCommunication` to engage + * the platform AEC/NS chain. Has no effect on iOS. + */ + androidInputPreset?: AndroidInputPreset; +} + +export type AndroidInputPreset = + | 'generic' + | 'camcorder' + | 'voiceRecognition' + | 'voiceCommunication' + | 'unprocessed' + | 'voicePerformance'; + export default class AudioRecorder { protected onAudioReadySubscription: AudioEventSubscription | null = null; protected onErrorSubscription: AudioEventSubscription | null = null; @@ -51,8 +74,8 @@ export default class AudioRecorder { globalThis.AudioEventEmitter ); - constructor() { - this.recorder = globalThis.createAudioRecorder(); + constructor(options?: AudioRecorderOptions) { + this.recorder = globalThis.createAudioRecorder(options?.androidInputPreset ?? ''); } /**