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 ?? ''); } /**