From 26b756e38a648eda04fe7c49a120e92736fae8a1 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Wed, 22 Jul 2026 11:45:59 +0200 Subject: [PATCH 1/5] fix: offset default value in QueueSourceNode.start --- .../src/core/AudioBufferQueueSourceNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts index e1f747bb2..0f85465e4 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts @@ -37,7 +37,7 @@ export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNod (this.node as IAudioBufferQueueSourceNode).clearBuffers(); } - public override start(when: number = 0, offset: number = -1): void { + public override start(when: number = 0, offset: number = 0): void { if (when < 0) { throw new RangeError( `when must be a finite non-negative number: ${when}` From 2014328b1a5f1cb6efa875221c8c357640271482 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 23 Jul 2026 16:20:06 +0200 Subject: [PATCH 2/5] feat: add resume to AudioBufferQueueSourceNode --- .../AudioBufferBaseSourceNodeHostObject.cpp | 1 - .../AudioBufferQueueSourceNodeHostObject.cpp | 15 +++++++- .../AudioBufferQueueSourceNodeHostObject.h | 6 ++- .../AudioScheduledSourceNodeHostObject.h | 4 +- .../sources/AudioBufferQueueSourceNode.cpp | 4 ++ .../core/sources/AudioBufferQueueSourceNode.h | 2 + .../src/core/AudioBufferQueueSourceNode.ts | 38 ++++++++++++++++++- .../src/jsi-interfaces.ts | 1 + packages/react-native-audio-api/src/types.ts | 7 ++++ 9 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.cpp index 96f751a39..febedfec9 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.cpp index 96cde93ad..90df30583 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -28,7 +30,8 @@ AudioBufferQueueSourceNodeHostObject::AudioBufferQueueSourceNodeHostObject( JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, enqueueBuffer), JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, dequeueBuffer), JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, clearBuffers), - JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, pause)); + JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, pause), + JSI_EXPORT_FUNCTION(AudioBufferQueueSourceNodeHostObject, resume)); } AudioBufferQueueSourceNodeHostObject::~AudioBufferQueueSourceNodeHostObject() { @@ -63,6 +66,16 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, pause) { return jsi::Value::undefined(); } +JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, resume) { + auto handle = node_->handle; + auto event = + [handle, node = bufferQueueSourceNode_, when = args[0].getNumber()](BaseAudioContext &) { + node->resume(when); + }; + bufferQueueSourceNode_->scheduleAudioEvent(std::move(event)); + return jsi::Value::undefined(); +} + JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, enqueueBuffer) { auto audioBufferHostObject = args[0].getObject(runtime).asHostObject(runtime); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.h index 12018b0f1..c84de7eee 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferQueueSourceNodeHostObject.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include namespace audioapi { @@ -19,11 +21,13 @@ class AudioBufferQueueSourceNodeHostObject : public AudioBufferBaseSourceNodeHos const BaseAudioBufferSourceOptions &options); ~AudioBufferQueueSourceNodeHostObject() override; + DELETE_COPY_AND_MOVE(AudioBufferQueueSourceNodeHostObject); JSI_PROPERTY_SETTER_DECL(onBufferEnded); - JSI_HOST_FUNCTION_DECL(start); + JSI_HOST_FUNCTION_DECL(start) override; JSI_HOST_FUNCTION_DECL(pause); + JSI_HOST_FUNCTION_DECL(resume); JSI_HOST_FUNCTION_DECL(enqueueBuffer); JSI_HOST_FUNCTION_DECL(dequeueBuffer); JSI_HOST_FUNCTION_DECL(clearBuffers); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.h index 470a295d1..e78a23f53 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace audioapi { @@ -18,10 +19,11 @@ class AudioScheduledSourceNodeHostObject : public AudioNodeHostObject { const AudioScheduledSourceNodeOptions &options = AudioScheduledSourceNodeOptions()); ~AudioScheduledSourceNodeHostObject() override; + DELETE_COPY_AND_MOVE(AudioScheduledSourceNodeHostObject); JSI_PROPERTY_SETTER_DECL(onEnded); - JSI_HOST_FUNCTION_DECL(start); + virtual JSI_HOST_FUNCTION_DECL(start); JSI_HOST_FUNCTION_DECL(stop); protected: diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp index 7c869fca7..9cceccc61 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.cpp @@ -65,6 +65,10 @@ void AudioBufferQueueSourceNode::start(double when, double offset) { vReadIndex_ = static_cast(buffers_.front().second->getSampleRate() * offset); } +void AudioBufferQueueSourceNode::resume(double when) { + start(when); +} + void AudioBufferQueueSourceNode::pause() { AudioScheduledSourceNode::stop(0.0); isPaused_ = true; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h index c5c9bc368..2574c0617 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferQueueSourceNode.h @@ -29,6 +29,8 @@ class AudioBufferQueueSourceNode : public AudioBufferBaseSourceNode { /// @note Audio Thread only void pause(); + void resume(double when); + /// @note Audio Thread only void enqueueBuffer( const std::shared_ptr &buffer, diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts index 0f85465e4..0a3a7f957 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts @@ -1,13 +1,17 @@ import { IAudioBufferQueueSourceNode } from '../jsi-interfaces'; import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; import AudioBuffer from './AudioBuffer'; -import { RangeError } from '../errors'; +import { InvalidStateError, RangeError } from '../errors'; import type BaseAudioContext from './BaseAudioContext'; -import { AudioBufferQueueSourceOptions } from '../types'; +import { + AudioBufferQueueSourceOptions, + AudioBufferQueueSourceState, +} from '../types'; import { OnBufferEndEventType } from '../events/types'; export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNode { private onBufferEndedCallback?: (event: OnBufferEndEventType) => void; + private state: AudioBufferQueueSourceState = AudioBufferQueueSourceState.IDLE; constructor( context: BaseAudioContext, @@ -50,6 +54,11 @@ export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNod ); } + if (this.state !== AudioBufferQueueSourceState.IDLE) { + throw new InvalidStateError('Cannot call start more than once'); + } + this.state = AudioBufferQueueSourceState.PLAYING; + (this.node as IAudioBufferQueueSourceNode).start(when, offset); } @@ -59,6 +68,7 @@ export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNod `when must be a finite non-negative number: ${when}` ); } + this.state = AudioBufferQueueSourceState.STOPPED; (this.node as IAudioBufferQueueSourceNode).stop(when); } @@ -89,6 +99,30 @@ export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNod } public pause(): void { + if (this.state !== AudioBufferQueueSourceState.PLAYING) { + throw new InvalidStateError( + 'Cannot call pause when the node is not playing' + ); + } + this.state = AudioBufferQueueSourceState.PAUSED; + (this.node as IAudioBufferQueueSourceNode).pause(); } + + public resume(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + if (this.state !== AudioBufferQueueSourceState.PAUSED) { + throw new InvalidStateError( + 'Cannot call resume without calling pause first' + ); + } + this.state = AudioBufferQueueSourceState.PLAYING; + + (this.node as IAudioBufferQueueSourceNode).resume(when); + } } diff --git a/packages/react-native-audio-api/src/jsi-interfaces.ts b/packages/react-native-audio-api/src/jsi-interfaces.ts index 1857124f8..46760253c 100644 --- a/packages/react-native-audio-api/src/jsi-interfaces.ts +++ b/packages/react-native-audio-api/src/jsi-interfaces.ts @@ -200,6 +200,7 @@ export interface IAudioBufferQueueSourceNode extends IAudioBufferBaseSourceNode enqueueBuffer: (audioBuffer: IAudioBuffer) => string; start: (when?: number, offset?: number) => void; pause: () => void; + resume: (when?: number) => void; // passing subscriptionId(uint_64 in cpp, string in js) to the cpp onBufferEnded: string; diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index f712f81b4..3b1e55409 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -172,6 +172,13 @@ interface BaseAudioBufferSourceOptions extends AudioNodeOptions { export type AudioBufferQueueSourceOptions = BaseAudioBufferSourceOptions; +export enum AudioBufferQueueSourceState { + IDLE, + PLAYING, + PAUSED, + STOPPED, +} + export interface AudioBufferSourceOptions extends BaseAudioBufferSourceOptions { buffer?: AudioBufferLike; loop?: boolean; From ff8a86719ec37c076ebb5fabf70dfeec3322a5d0 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 23 Jul 2026 16:20:46 +0200 Subject: [PATCH 3/5] docs: update AudioBufferQueueSourceNode interface --- .../audio-buffer-queue-source-node.mdx | 42 +++++++++++++++++++ .../media-element-audio-source-node.mdx | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/audiodocs/docs/sources/audio-buffer-queue-source-node.mdx b/packages/audiodocs/docs/sources/audio-buffer-queue-source-node.mdx index 0d89cddab..fe17e5aa3 100644 --- a/packages/audiodocs/docs/sources/audio-buffer-queue-source-node.mdx +++ b/packages/audiodocs/docs/sources/audio-buffer-queue-source-node.mdx @@ -136,14 +136,56 @@ Schedules the `AudioBufferQueueSourceNode` to start playback of enqueued [`Audio | `when` | `number` | The time, in seconds, at which playback is scheduled to start. If `when` is less than [`AudioContext.currentTime`](/docs/core/base-audio-context#properties) or set to `0.0`, the node starts playing immediately.
Default: `0.0`. | | `offset` | `number` | The position, in seconds, within the first enqueued audio buffer where playback begins.
The default value is `0.0`, which starts playback from the beginning of the first enqueued buffer. If the offset exceeds the buffer’s [`duration`](/docs/sources/audio-buffer#properties), it’s automatically clamped to the valid range. | +#### Errors: + +| Error type | Description | +| :---: | :---- | +| `RangeError` | `when` or `offset` is negative number. | +| `InvalidStateError` | If the node has already been started once. | + +#### Returns `undefined`. + +#### Errors: + +| Error type | Description | +| :---: | :---- | +| `RangeError` | `when` is negative number. | +| `InvalidStateError` | If the node has not been started yet or has already been stopped | + +#### Returns `undefined`. ### `pause` Stops audio immediately. Unlike [`stop()`](/docs/sources/audio-scheduled-source-node#stop), which fully stops playback and clears the queued buffers, pause() halts the audio while keeping the current playback position, allowing you to resume from the same point later. +#### Errors: + +| Error type | Description | +| :---: | :---- | +| `InvalidStateError` | If the node has not been started yet, has already been stopped, or is already paused. | + #### Returns `undefined`. +### `resume` + +Resumes audio playback with a specified delay. If no time is given, it resumes immediately. + +| Parameter | Type | Description | +| :---: | :---: | :---- | +| `when` | `number` | The time, in seconds, at which playback is scheduled to resume. If `when` is less than [`AudioContext.currentTime`](/docs/core/base-audio-context#properties) or set to `0.0`, the node resumes playing immediately.
Default: `0.0`. | + +#### Errors: + +| Error type | Description | +| :---: | :---- | +| `RangeError` | `when` is negative number. | +| `InvalidStateError` | If the node has not been started yet, has already been stopped, or is not currently paused. | + +#### Returns `undefined`. + + + ## Events It inherits all events from [`AudioScheduledSourceNode`](/docs/sources/audio-scheduled-source-node#events). diff --git a/packages/audiodocs/docs/sources/media-element-audio-source-node.mdx b/packages/audiodocs/docs/sources/media-element-audio-source-node.mdx index b4aff3de7..49d651a3f 100644 --- a/packages/audiodocs/docs/sources/media-element-audio-source-node.mdx +++ b/packages/audiodocs/docs/sources/media-element-audio-source-node.mdx @@ -10,7 +10,7 @@ import { ReadOnly } from '@site/src/components/Badges'; The `MediaElementAudioSourceNode` is an [`AudioNode`](/docs/core/audio-node) that routes media playback into the audio graph. On the web it mirrors [`AudioContext.createMediaElementSource()`](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaElementSource) and accepts an `HTMLMediaElement`. -On the native it wraps a file-backed source created by the experimental [`