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'
+ );
+ });
+});