Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/audiodocs/docs/core/audio-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ constructor(options?: AudioContextOptions)
| Parameter | Type | Default | |
| :---: | :---: | :----: | :---- |
| `sampleRate` <Optional /> | `number` | - | The preferred sample rate for the context. |
| `latencyHint` <Optional /> | `'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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(DSPAudioBuffer *, int)> &renderAudio,
float sampleRate,
int channelCount,
std::mutex *driverMutex,
const std::shared_ptr<AudioContext> &context,
std::atomic<uint32_t> &currentRenders)
std::atomic<uint32_t> &currentRenders,
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_);
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <mutex>

#include <audioapi/core/CommonPlayer.h>
#include <audioapi/core/types/AudioContextLatencyHint.h>
#include <audioapi/utils/AudioBuffer.hpp>

namespace audioapi {
Expand All @@ -29,7 +30,8 @@ class AudioPlayer : public CommonPlayer,
int channelCount,
std::mutex *driverMutex,
const std::shared_ptr<AudioContext> &context,
std::atomic<uint32_t> &currentRenders);
std::atomic<uint32_t> &currentRenders,
AudioContextLatencyHint latencyHint = AudioContextLatencyHint::INTERACTIVE);

~AudioPlayer() override {
cleanup();
Expand Down Expand Up @@ -67,6 +69,7 @@ class AudioPlayer : public CommonPlayer,
std::atomic<int32_t> lastCallbackFrameCount_{0};
std::mutex *driverMutex_;
std::weak_ptr<AudioContext> context_;
AudioContextLatencyHint latencyHint_;

bool openAudioStream();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,28 @@ class AudioAPIModuleInstaller {
return jsi::Function::createFromHostFunction(
*jsiRuntime,
jsi::PropNameID::forAscii(*jsiRuntime, "createAudioContext"),
1,
2,
[jsCallInvoker, audioEventHandlerRegistry](
jsi::Runtime &runtime,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count) -> jsi::Value {
auto sampleRate = static_cast<float>(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<AudioContextHostObject>(
sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker);
sampleRate, audioEventHandlerRegistry, &runtime, jsCallInvoker, latencyHint);

return jsi::Object::createFromHostObject(runtime, audioContextHostObject);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ AudioContextHostObject::AudioContextHostObject(
float sampleRate,
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
jsi::Runtime *runtime,
const std::shared_ptr<react::CallInvoker> &callInvoker)
const std::shared_ptr<react::CallInvoker> &callInvoker,
AudioContextLatencyHint latencyHint)
: BaseAudioContextHostObject(
std::make_shared<AudioContext>(sampleRate, audioEventHandlerRegistry),
std::make_shared<AudioContext>(sampleRate, audioEventHandlerRegistry, latencyHint),
runtime,
callInvoker) {
addGetters(JSI_EXPORT_PROPERTY_GETTER(AudioContextHostObject, outputLatency));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <audioapi/HostObjects/BaseAudioContextHostObject.h>
#include <audioapi/core/types/AudioContextLatencyHint.h>
#include <audioapi/events/IAudioEventHandlerRegistry.h>

#include <jsi/jsi.h>
Expand All @@ -17,7 +18,8 @@ class AudioContextHostObject : public BaseAudioContextHostObject {
float sampleRate,
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
jsi::Runtime *runtime,
const std::shared_ptr<react::CallInvoker> &callInvoker);
const std::shared_ptr<react::CallInvoker> &callInvoker,
AudioContextLatencyHint latencyHint = AudioContextLatencyHint::INTERACTIVE);

JSI_HOST_FUNCTION_DECL(close);
JSI_HOST_FUNCTION_DECL(resume);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
namespace audioapi {
AudioContext::AudioContext(
float sampleRate,
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry)
: BaseAudioContext(sampleRate, audioEventHandlerRegistry), isInitialized_(false) {
const std::shared_ptr<IAudioEventHandlerRegistry> &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).
Expand Down Expand Up @@ -44,7 +47,8 @@ void AudioContext::initialize(const AudioDestinationNode *destination) {
destination_->getChannelCount(),
&driverMutex_,
std::static_pointer_cast<AudioContext>(shared_from_this()),
currentRenders_);
currentRenders_,
latencyHint_);
#else
audioPlayer_ = std::make_shared<IOSAudioPlayer>(
[this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <audioapi/core/BaseAudioContext.h>
#include <audioapi/core/CommonPlayer.h>
#include <audioapi/core/types/AudioContextLatencyHint.h>
#include <audioapi/jsi/ContextPromiseResolver.hpp>
#include <audioapi/utils/AudioBuffer.hpp>
#include <audioapi/utils/Macros.h>
Expand All @@ -15,7 +16,8 @@ class AudioContext : public BaseAudioContext {
public:
explicit AudioContext(
float sampleRate,
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry);
const std::shared_ptr<IAudioEventHandlerRegistry> &audioEventHandlerRegistry,
AudioContextLatencyHint latencyHint = AudioContextLatencyHint::INTERACTIVE);
~AudioContext() override;
DELETE_COPY_AND_MOVE(AudioContext);

Expand All @@ -39,6 +41,7 @@ class AudioContext : public BaseAudioContext {

private:
std::shared_ptr<CommonPlayer> audioPlayer_;
AudioContextLatencyHint latencyHint_;
std::atomic<bool> isInitialized_{false};
/// Audio I/O callback thread increments around each platform render callback;
/// control thread waits on suspend/close.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include <cstdint>

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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-audio-api/src/core/AudioContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export default class AudioContext extends BaseAudioContext {

super(
globalThis.createAudioContext(
options?.sampleRate || AudioManager.getDevicePreferredSampleRate()
options?.sampleRate || AudioManager.getDevicePreferredSampleRate(),
options?.latencyHint
)
);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/react-native-audio-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down