diff --git a/packages/audiodocs/docs/core/audio-context.mdx b/packages/audiodocs/docs/core/audio-context.mdx
index 6e775e96b..2d8ef128f 100644
--- a/packages/audiodocs/docs/core/audio-context.mdx
+++ b/packages/audiodocs/docs/core/audio-context.mdx
@@ -21,6 +21,7 @@ constructor(options?: AudioContextOptions)
| Parameter | Type | Default | |
| :---: | :---: | :----: | :---- |
| `sampleRate` | `number` | - | The preferred sample rate for the context. |
+| `latencyHint` | `'interactive' \| 'balanced' \| 'playback'` | `'interactive'` | What the context should optimize its output stream for. `interactive` requests the lowest latency the platform offers; `balanced` and `playback` trade output latency for a deeper buffer, which favours glitch-free sustained playback (many simultaneous sources, low-end devices). On Android these map to Oboe's `LowLatency`, `None` and `PowerSaving` performance modes; on iOS the hint is currently accepted but does not change the stream; on web it is passed to the browser's `AudioContext`. Numeric hints are not supported yet. |
#### 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..a80537e75 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
@@ -13,20 +13,38 @@
namespace audioapi {
+namespace {
+
+PerformanceMode performanceModeFor(AudioContextLatencyHint latencyHint) {
+ switch (latencyHint) {
+ case AudioContextLatencyHint::BALANCED:
+ return PerformanceMode::None;
+ case AudioContextLatencyHint::PLAYBACK:
+ return PerformanceMode::PowerSaving;
+ case AudioContextLatencyHint::INTERACTIVE:
+ return PerformanceMode::LowLatency;
+ }
+ return PerformanceMode::LowLatency;
+}
+
+} // namespace
+
AudioPlayer::AudioPlayer(
const std::function &renderAudio,
float sampleRate,
int channelCount,
std::mutex *driverMutex,
const std::shared_ptr &context,
- std::atomic ¤tRenders)
+ std::atomic ¤tRenders,
+ AudioContextLatencyHint latencyHint)
: renderAudio_(renderAudio),
currentRenders_(currentRenders),
sampleRate_(sampleRate),
channelCount_(channelCount),
isRunning_(false),
driverMutex_(driverMutex),
- context_(context) {}
+ context_(context),
+ latencyHint_(latencyHint) {}
bool AudioPlayer::openAudioStream() {
std::scoped_lock lock(streamMutex_);
@@ -35,7 +53,7 @@ bool AudioPlayer::openAudioStream() {
builder.setSharingMode(SharingMode::Exclusive)
->setFormat(AudioFormat::Float)
->setFormatConversionAllowed(true)
- ->setPerformanceMode(PerformanceMode::LowLatency)
+ ->setPerformanceMode(performanceModeFor(latencyHint_))
->setChannelCount(channelCount_)
->setSampleRateConversionQuality(SampleRateConversionQuality::Medium)
->setFramesPerDataCallback(RENDER_QUANTUM_SIZE)
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..fe43d1cbb 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
@@ -10,6 +10,7 @@
#include
#include
+#include
#include
namespace audioapi {
@@ -29,7 +30,8 @@ class AudioPlayer : public CommonPlayer,
int channelCount,
std::mutex *driverMutex,
const std::shared_ptr &context,
- std::atomic ¤tRenders);
+ std::atomic ¤tRenders,
+ AudioContextLatencyHint latencyHint = AudioContextLatencyHint::INTERACTIVE);
~AudioPlayer() override {
cleanup();
@@ -67,6 +69,7 @@ class AudioPlayer : public CommonPlayer,
std::atomic lastCallbackFrameCount_{0};
std::mutex *driverMutex_;
std::weak_ptr context_;
+ AudioContextLatencyHint latencyHint_;
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 e4dc7cbce..5b6a68587 100644
--- a/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h
+++ b/packages/react-native-audio-api/common/cpp/audioapi/AudioAPIModuleInstaller.h
@@ -60,7 +60,7 @@ class AudioAPIModuleInstaller {
return jsi::Function::createFromHostFunction(
*jsiRuntime,
jsi::PropNameID::forAscii(*jsiRuntime, "createAudioContext"),
- 1,
+ 2,
[jsCallInvoker, audioEventHandlerRegistry](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
@@ -68,8 +68,20 @@ class AudioAPIModuleInstaller {
size_t count) -> jsi::Value {
auto sampleRate = static_cast(args[0].getNumber());
+ // Unknown strings fall back to INTERACTIVE, matching how browsers
+ // treat an unrecognised latencyHint.
+ auto latencyHint = AudioContextLatencyHint::INTERACTIVE;
+ if (count > 1 && args[1].isString()) {
+ auto hint = args[1].getString(runtime).utf8(runtime);
+ if (hint == "balanced") {
+ latencyHint = AudioContextLatencyHint::BALANCED;
+ } else if (hint == "playback") {
+ latencyHint = AudioContextLatencyHint::PLAYBACK;
+ }
+ }
+
auto audioContextHostObject = std::make_shared(
- sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker);
+ sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker, latencyHint);
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..73c682332 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
@@ -13,9 +13,10 @@ AudioContextHostObject::AudioContextHostObject(
float sampleRate,
const std::shared_ptr &audioEventHandlerRegistry,
jsi::Runtime *runtime,
- const std::shared_ptr &callInvoker)
+ const std::shared_ptr &callInvoker,
+ AudioContextLatencyHint latencyHint)
: BaseAudioContextHostObject(
- std::make_shared(sampleRate, audioEventHandlerRegistry),
+ std::make_shared(sampleRate, audioEventHandlerRegistry, latencyHint),
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..4934ddf5c 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
@@ -17,7 +18,8 @@ class AudioContextHostObject : public BaseAudioContextHostObject {
float sampleRate,
const std::shared_ptr &audioEventHandlerRegistry,
jsi::Runtime *runtime,
- const std::shared_ptr &callInvoker);
+ const std::shared_ptr &callInvoker,
+ AudioContextLatencyHint latencyHint = AudioContextLatencyHint::INTERACTIVE);
JSI_HOST_FUNCTION_DECL(close);
JSI_HOST_FUNCTION_DECL(resume);
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 fed66eb8a..8d88a8f39 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,
- const std::shared_ptr &audioEventHandlerRegistry)
- : BaseAudioContext(sampleRate, audioEventHandlerRegistry), isInitialized_(false) {
+ const std::shared_ptr &audioEventHandlerRegistry,
+ AudioContextLatencyHint latencyHint)
+ : BaseAudioContext(sampleRate, audioEventHandlerRegistry),
+ latencyHint_(latencyHint),
+ isInitialized_(false) {
// Context starts SUSPENDED with no audio-thread consumer. Let the producer
// drain Channel A itself until start()/resume() hands draining to the
// audio callback (same pattern as OfflineAudioContext before rendering).
@@ -44,7 +47,8 @@ void AudioContext::initialize(const AudioDestinationNode *destination) {
destination_->getChannelCount(),
&driverMutex_,
std::static_pointer_cast(shared_from_this()),
- currentRenders_);
+ currentRenders_,
+ latencyHint_);
#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..1cf77e813 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
@@ -2,6 +2,7 @@
#include
#include
+#include
#include
#include
#include
@@ -15,7 +16,8 @@ class AudioContext : public BaseAudioContext {
public:
explicit AudioContext(
float sampleRate,
- const std::shared_ptr &audioEventHandlerRegistry);
+ const std::shared_ptr &audioEventHandlerRegistry,
+ AudioContextLatencyHint latencyHint = AudioContextLatencyHint::INTERACTIVE);
~AudioContext() override;
DELETE_COPY_AND_MOVE(AudioContext);
@@ -39,6 +41,7 @@ class AudioContext : public BaseAudioContext {
private:
std::shared_ptr audioPlayer_;
+ AudioContextLatencyHint latencyHint_;
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/types/AudioContextLatencyHint.h b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioContextLatencyHint.h
new file mode 100644
index 000000000..ecfc036e5
--- /dev/null
+++ b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioContextLatencyHint.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include
+
+namespace audioapi {
+
+/// Web Audio's AudioContextOptions.latencyHint categories: what the context should
+/// optimize its output stream for. Platform backends map these to their own stream
+/// configuration; INTERACTIVE preserves the pre-hint behaviour and is the default.
+enum class AudioContextLatencyHint : std::uint8_t { INTERACTIVE, BALANCED, PLAYBACK };
+
+} // 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 655cde36b..6e762cc1c 100644
--- a/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts
+++ b/packages/react-native-audio-api/src/AudioAPIModule/globals.d.ts
@@ -7,10 +7,14 @@ import type {
IAudioBuffer,
IOfflineAudioContext,
} from '../jsi-interfaces';
+import type { AudioContextLatencyCategory } from '../types';
/* eslint-disable no-var */
declare global {
- var createAudioContext: (sampleRate: number) => IAudioContext;
+ var createAudioContext: (
+ sampleRate: number,
+ latencyHint?: AudioContextLatencyCategory
+ ) => 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 8bf61ba42..6a01ad4f0 100644
--- a/packages/react-native-audio-api/src/core/AudioContext.ts
+++ b/packages/react-native-audio-api/src/core/AudioContext.ts
@@ -14,7 +14,8 @@ export default class AudioContext extends BaseAudioContext {
super(
globalThis.createAudioContext(
- options?.sampleRate || AudioManager.getDevicePreferredSampleRate()
+ options?.sampleRate || AudioManager.getDevicePreferredSampleRate(),
+ options?.latencyHint
)
);
}
diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts
index 7afdae22c..c173f8e5c 100644
--- a/packages/react-native-audio-api/src/types.ts
+++ b/packages/react-native-audio-api/src/types.ts
@@ -45,8 +45,21 @@ export type OscillatorType =
| 'triangle'
| 'custom';
+export type AudioContextLatencyCategory =
+ | 'balanced'
+ | 'interactive'
+ | 'playback';
+
export interface AudioContextOptions {
sampleRate?: number;
+ /**
+ * What the context should optimize its output stream for. Defaults to
+ * `interactive` (the lowest latency the platform offers). `balanced` and
+ * `playback` trade output latency for a deeper buffer, which on Android moves
+ * multi-source playback off the underrun-prone low-latency path. Numeric
+ * hints are not supported yet.
+ */
+ latencyHint?: AudioContextLatencyCategory;
}
export interface OfflineAudioContextOptions {
diff --git a/packages/react-native-audio-api/src/web-core/AudioContext.web.ts b/packages/react-native-audio-api/src/web-core/AudioContext.web.ts
index 220b4a85d..fb7e62ed6 100644
--- a/packages/react-native-audio-api/src/web-core/AudioContext.web.ts
+++ b/packages/react-native-audio-api/src/web-core/AudioContext.web.ts
@@ -33,7 +33,10 @@ export default class AudioContext implements BaseAudioContext {
assertSupportedSampleRate(options.sampleRate);
}
- this.context = new window.AudioContext({ sampleRate: options?.sampleRate });
+ this.context = new window.AudioContext({
+ sampleRate: options?.sampleRate,
+ latencyHint: options?.latencyHint,
+ });
this.sampleRate = this.context.sampleRate;
this.destination = new AudioDestinationNode(this, this.context.destination);