diff --git a/packages/audiodocs/docs/core/audio-context.mdx b/packages/audiodocs/docs/core/audio-context.mdx index 45eaebeca..071856960 100644 --- a/packages/audiodocs/docs/core/audio-context.mdx +++ b/packages/audiodocs/docs/core/audio-context.mdx @@ -21,6 +21,9 @@ constructor(options?: AudioContextOptions) | Parameter | Type | Default | | | :---: | :---: | :----: | :---- | | `sampleRate` | `number` | - | The preferred sample rate for the context. | +| `androidOutputProfile` | `'media'` \| `'voiceCommunication'` | `'media'` | Android output classification. Use `'voiceCommunication'` for interactive voice calls. | + +On Android, `voiceCommunication` configures the output stream with voice-communication usage and speech content type. It does not guarantee echo cancellation, noise suppression, or automatic gain control; those remain device- and route-dependent. #### Errors diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index 38371018f..4bbdf0217 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -19,14 +19,16 @@ AudioPlayer::AudioPlayer( int channelCount, std::mutex *driverMutex, const std::shared_ptr &context, - std::atomic ¤tRenders) + std::atomic ¤tRenders, + AndroidOutputProfile outputProfile) : renderAudio_(renderAudio), currentRenders_(currentRenders), sampleRate_(sampleRate), channelCount_(channelCount), isRunning_(false), driverMutex_(driverMutex), - context_(context) {} + context_(context), + outputProfile_(outputProfile) {} bool AudioPlayer::openAudioStream() { std::scoped_lock lock(streamMutex_); @@ -43,6 +45,10 @@ bool AudioPlayer::openAudioStream() { ->setErrorCallback(shared_from_this()) ->setSampleRate(static_cast(sampleRate_)); + if (outputProfile_ == AndroidOutputProfile::VoiceCommunication) { + builder.setUsage(Usage::VoiceCommunication)->setContentType(ContentType::Speech); + } + auto result = builder.openStream(mStream_); if (result != oboe::Result::OK || mStream_ == nullptr) { __android_log_print( diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 2f9a4ac01..ee919a743 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -29,7 +30,8 @@ class AudioPlayer : public CommonPlayer, int channelCount, std::mutex *driverMutex, const std::shared_ptr &context, - std::atomic ¤tRenders); + std::atomic ¤tRenders, + AndroidOutputProfile outputProfile); ~AudioPlayer() override { cleanup(); @@ -67,6 +69,7 @@ class AudioPlayer : public CommonPlayer, std::atomic lastCallbackFrameCount_{0}; std::mutex *driverMutex_; std::weak_ptr context_; + AndroidOutputProfile outputProfile_; bool openAudioStream(); }; 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 ba15c738b..89c91d261 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h @@ -76,9 +76,15 @@ class AudioAPIModuleInstaller { const jsi::Value *args, size_t count) -> jsi::Value { auto sampleRate = static_cast(args[0].getNumber()); + auto androidOutputProfile = AndroidOutputProfile::Media; + + if (count > 1 && args[1].isString() && + args[1].getString(runtime).utf8(runtime) == "voiceCommunication") { + androidOutputProfile = AndroidOutputProfile::VoiceCommunication; + } auto audioContextHostObject = std::make_shared( - sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker); + sampleRate, androidOutputProfile, audioEventHandlerRegistry, &runtime, jsCallInvoker); return jsi::Object::createFromHostObject(runtime, audioContextHostObject); }); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp index 4bc1c6843..3f2d2ab9f 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp @@ -11,11 +11,15 @@ namespace audioapi { AudioContextHostObject::AudioContextHostObject( float sampleRate, + AndroidOutputProfile androidOutputProfile, const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker) : BaseAudioContextHostObject( - std::make_shared(sampleRate, audioEventHandlerRegistry), + std::make_shared( + sampleRate, + androidOutputProfile, + audioEventHandlerRegistry), runtime, callInvoker) { addGetters(JSI_EXPORT_PROPERTY_GETTER(AudioContextHostObject, outputLatency)); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h index 4952c6c85..7da9cf909 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -15,6 +16,7 @@ class AudioContextHostObject : public BaseAudioContextHostObject { public: explicit AudioContextHostObject( float sampleRate, + AndroidOutputProfile androidOutputProfile, const std::shared_ptr &audioEventHandlerRegistry, jsi::Runtime *runtime, const std::shared_ptr &callInvoker); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index 6721bb9bf..0e175db41 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -15,8 +15,11 @@ namespace audioapi { AudioContext::AudioContext( float sampleRate, + AndroidOutputProfile androidOutputProfile, const std::shared_ptr &audioEventHandlerRegistry) - : BaseAudioContext(sampleRate, audioEventHandlerRegistry), isInitialized_(false) { + : BaseAudioContext(sampleRate, audioEventHandlerRegistry), + androidOutputProfile_(androidOutputProfile), + isInitialized_(false) { // Context starts SUSPENDED with no audio-thread consumer. Let the producer // drain the channels itself until start()/resume() hands draining to the // audio callback (same pattern as OfflineAudioContext before rendering). @@ -45,7 +48,8 @@ void AudioContext::initialize(const AudioDestinationNode *destination) { destination_->getChannelCount(), &driverMutex_, std::static_pointer_cast(shared_from_this()), - currentRenders_); + currentRenders_, + androidOutputProfile_); #else audioPlayer_ = std::make_shared( [this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); }, diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h index 0cad163a2..a7b5dfcf9 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -15,6 +16,7 @@ class AudioContext : public BaseAudioContext { public: explicit AudioContext( float sampleRate, + AndroidOutputProfile androidOutputProfile, const std::shared_ptr &audioEventHandlerRegistry); ~AudioContext() override; DELETE_COPY_AND_MOVE(AudioContext); @@ -39,6 +41,7 @@ class AudioContext : public BaseAudioContext { private: std::shared_ptr audioPlayer_; + AndroidOutputProfile androidOutputProfile_; std::atomic isInitialized_{false}; /// Audio I/O callback thread increments around each platform render callback; /// control thread waits on suspend/close. diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContextOptions.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContextOptions.h new file mode 100644 index 000000000..9bc5f0c21 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContextOptions.h @@ -0,0 +1,10 @@ +#pragma once + +namespace audioapi { + +enum class AndroidOutputProfile { + Media, + VoiceCommunication, +}; + +} // namespace audioapi 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 a3f23c4d4..b8482baa2 100644 --- a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts +++ b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts @@ -7,11 +7,18 @@ import type { IAudioBuffer, IOfflineAudioContext, } from '../jsi-interfaces'; -import type { AudioRecorderOptions, FileInfo } from '../types'; +import type { + AndroidOutputProfile, + AudioRecorderOptions, + FileInfo, +} from '../types'; /* eslint-disable no-var */ declare global { - var createAudioContext: (sampleRate: number) => IAudioContext; + var createAudioContext: ( + sampleRate: number, + androidOutputProfile?: AndroidOutputProfile + ) => IAudioContext; var createOfflineAudioContext: ( numberOfChannels: number, length: number, diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index f0b075bda..d22a5ec9a 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -15,7 +15,8 @@ export default class AudioContext extends BaseAudioContext { super( globalThis.createAudioContext( - options?.sampleRate || AudioManager.getDevicePreferredSampleRate() + options?.sampleRate || AudioManager.getDevicePreferredSampleRate(), + options?.androidOutputProfile ) ); } diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index d5db672e5..6c0d5f842 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -47,8 +47,11 @@ export type OscillatorType = export interface AudioContextOptions { sampleRate?: number; + androidOutputProfile?: AndroidOutputProfile; } +export type AndroidOutputProfile = 'media' | 'voiceCommunication'; + export interface OfflineAudioContextOptions { numberOfChannels: number; length: number; diff --git a/packages/react-native-audio-api/tests/audio-context-options.test.ts b/packages/react-native-audio-api/tests/audio-context-options.test.ts new file mode 100644 index 000000000..bd5c857e9 --- /dev/null +++ b/packages/react-native-audio-api/tests/audio-context-options.test.ts @@ -0,0 +1,40 @@ +jest.mock('react-native', () => ({ + TurboModuleRegistry: { + get: jest.fn(() => ({ + getDevicePreferredSampleRate: jest.fn(() => 44100), + })), + }, +})); + +jest.mock('../src/core/BaseAudioContext', () => { + return class BaseAudioContext { + constructor(_context: unknown) {} + }; +}); + +import type { IAudioContext } from '../src/jsi-interfaces'; + +describe('AudioContext options', () => { + const createAudioContext = globalThis.createAudioContext; + + afterEach(() => { + globalThis.createAudioContext = createAudioContext; + }); + + it('forwards the Android voice communication output profile to native', () => { + const nativeContext = {} as IAudioContext; + globalThis.createAudioContext = jest.fn(() => nativeContext); + const AudioContext = require('../src/core/AudioContext') + .default as typeof import('../src/core/AudioContext').default; + + new AudioContext({ + sampleRate: 24000, + androidOutputProfile: 'voiceCommunication', + }); + + expect(globalThis.createAudioContext).toHaveBeenCalledWith( + 24000, + 'voiceCommunication' + ); + }); +});