Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,14 @@ const AudioVisualizer: React.FC = () => {
}
/>
<View
style={{ flex: 0.5, justifyContent: 'center', alignItems: 'center' }}
>
style={{ flex: 0.5, justifyContent: 'center', alignItems: 'center' }}>
{isLoading && <ActivityIndicator color="#FFFFFF" />}
<View
style={{
justifyContent: 'center',
flexDirection: 'row',
marginTop: layout.spacing * 2,
}}
>
}}>
<Button
onPress={handlePlayPause}
title={isPlaying ? 'Pause' : 'Play'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,32 @@ It inherits all properties from [`AudioScheduledSourceNode`](/docs/sources/audio

## Methods

`AudioBufferBaseSourceNode` does not define any additional methods.
It inherits all methods from [`AudioScheduledSourceNode`](/docs/sources/audio-scheduled-source-node#methods).

### `getLatency`

Returns the playback latency introduced by the pitch correction algorithm, in seconds.
When scheduling precise playback times, start input samples this many seconds earlier to compensate for processing delay.
Typically around `0.06s` when pitch correction is enabled, and `0` otherwise.

#### Returns `number`.

<details>
<summary>Example usage</summary>
```tsx
const source = audioContext.createBufferSource({ pitchCorrection: true });
source.buffer = buffer;
source.connect(audioContext.destination);

const latency = source.getLatency();

// Schedule playback slightly earlier to compensate for latency
const startTime = audioContext.currentTime + 1.0; // play in 1 second
source.start(startTime - latency);
```
</details>


## Events

### `onPositionChanged` <MobileOnly />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ interface AudioBufferBaseSourceNodeOptions {
}
```

:::caution
The pitch correction algorithm introduces processing latency.
As a result, when scheduling precise playback times, you should start input samples slightly ahead of the intended playback time.
For more details, see [getLatency()](/docs/sources/audio-buffer-base-source-node#getlatency).
:::

## Example

```tsx
Expand All @@ -39,8 +45,8 @@ function App() {
const audioBufferQueue = audioContextRef.current.createBufferQueueSource();
const buffer1 = ...; // Load your audio buffer here
const buffer2 = ...; // Load another audio buffer if needed
audioBufferQueue.enqueueBuffer(buffer1, false);
audioBufferQueue.enqueueBuffer(buffer2, true); // Last buffer should be marked as is
audioBufferQueue.enqueueBuffer(buffer1);
audioBufferQueue.enqueueBuffer(buffer2);
audioBufferQueue.connect(audioContextRef.current.destination);
audioBufferQueue.start(audioContextRef.current.currentTime);
}
Expand Down
10 changes: 9 additions & 1 deletion packages/audiodocs/docs/sources/audio-buffer-source-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { useGainAdsrPlayground } from '@site/src/components/InteractivePlaygroun

# AudioBufferSourceNode

The `AudioBufferSourceNode` is an [`AudioBufferBaseSourceNode`](/docs/sources/audio-buffer-base-source-node) which represents audio source with in-memory audio data, stored in
The `AudioBufferSourceNode` is an [`AudioBufferBaseSourceNode`](/docs/sources/audio-buffer-base-source-node) which represents audio source with in-memory audio data, stored in
[`AudioBuffer`](/docs/sources/audio-buffer). You can use it for audio playback, including standard pause and resume functionalities.

Expand Down Expand Up @@ -40,6 +39,14 @@ interface AudioBufferBaseSourceNodeOptions {
}
```

:::caution
The pitch correction algorithm introduces processing latency.
As a result, when scheduling precise playback times, you should start input samples slightly ahead of the intended playback time.
For more details, see [getLatency()](/docs/sources/audio-buffer-base-source-node#getlatency).

If you plan to play multiple buffers one after another, consider using [AudioBufferQueueSourceNode](/docs/sources/audio-buffer-queue-source-node)
:::

## Example

```tsx
Expand Down Expand Up @@ -100,6 +107,7 @@ Schedules the `AudioBufferSourceNode` to start playback of audio data contained

#### Returns `undefined`.


## Events

### `onLoopEnded`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ AudioBufferBaseSourceNodeHostObject::AudioBufferBaseSourceNodeHostObject(
AudioBufferBaseSourceNodeHostObject, onPositionChanged),
JSI_EXPORT_PROPERTY_SETTER(
AudioBufferBaseSourceNodeHostObject, onPositionChangedInterval));

addFunctions(
JSI_EXPORT_FUNCTION(AudioBufferBaseSourceNodeHostObject, getInputLatency),
JSI_EXPORT_FUNCTION(
AudioBufferBaseSourceNodeHostObject, getOutputLatency));
}

AudioBufferBaseSourceNodeHostObject::~AudioBufferBaseSourceNodeHostObject() {
Expand Down Expand Up @@ -70,4 +75,18 @@ JSI_PROPERTY_SETTER_IMPL(
sourceNode->setOnPositionChangedInterval(static_cast<int>(value.getNumber()));
}

JSI_HOST_FUNCTION_IMPL(AudioBufferBaseSourceNodeHostObject, getInputLatency) {
auto audioBufferBaseSourceNode =
std::static_pointer_cast<AudioBufferBaseSourceNode>(node_);

return audioBufferBaseSourceNode->getInputLatency();
}

JSI_HOST_FUNCTION_IMPL(AudioBufferBaseSourceNodeHostObject, getOutputLatency) {
auto audioBufferBaseSourceNode =
std::static_pointer_cast<AudioBufferBaseSourceNode>(node_);

return audioBufferBaseSourceNode->getOutputLatency();
}

} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class AudioBufferBaseSourceNodeHostObject

JSI_PROPERTY_SETTER_DECL(onPositionChanged);
JSI_PROPERTY_SETTER_DECL(onPositionChangedInterval);

JSI_HOST_FUNCTION_DECL(getInputLatency);
JSI_HOST_FUNCTION_DECL(getOutputLatency);
};

} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ std::mutex &AudioBufferBaseSourceNode::getBufferLock() {
return bufferLock_;
}

double AudioBufferBaseSourceNode::getInputLatency() const {
if (pitchCorrection_) {
return static_cast<double>(stretch_->inputLatency()) /
context_->getSampleRate();
}
return 0;
}

double AudioBufferBaseSourceNode::getOutputLatency() const {
if (pitchCorrection_) {
return static_cast<double>(stretch_->outputLatency()) /
context_->getSampleRate();
}
return 0;
}

void AudioBufferBaseSourceNode::sendOnPositionChangedEvent() {
auto onPositionChangedCallbackId =
onPositionChangedCallbackId_.load(std::memory_order_acquire);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class AudioBufferBaseSourceNode : public AudioScheduledSourceNode {
void setOnPositionChangedCallbackId(uint64_t callbackId);
void setOnPositionChangedInterval(int interval);
[[nodiscard]] int getOnPositionChangedInterval() const;
[[nodiscard]] double getInputLatency() const;
[[nodiscard]] double getOutputLatency() const;

protected:
// pitch correction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ AudioBufferQueueSourceNode::AudioBufferQueueSourceNode(
buffers_ = {};
stretch_->presetDefault(channelCount_, context_->getSampleRate());

if (pitchCorrection) {
// If pitch correction is enabled, add extra frames at the end
// to compensate for processing latency.
addExtraTailFrames_ = true;

int extraTailFrames =
static_cast<int>(stretch_->inputLatency() + stretch_->outputLatency());
tailBuffer_ = std::make_shared<AudioBuffer>(
channelCount_, extraTailFrames, context_->getSampleRate());

tailBuffer_->bus_->zero();
}

isInitialized_ = true;
}

Expand Down Expand Up @@ -169,10 +182,17 @@ void AudioBufferQueueSourceNode::processWithoutInterpolation(
"ended", onEndedCallbackId_, body);

if (buffers_.empty()) {
processingBus->zero(writeIndex, framesLeft);
readIndex = 0;
if (addExtraTailFrames_) {
buffers_.emplace(bufferId_, tailBuffer_);
bufferId_++;

break;
addExtraTailFrames_ = false;
} else if (buffers_.empty()) {
processingBus->zero(writeIndex, framesLeft);
readIndex = 0;

break;
}
}

data = buffers_.front();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class AudioBufferQueueSourceNode : public AudioBufferBaseSourceNode {
size_t bufferId_ = 0;

bool isPaused_ = false;
bool addExtraTailFrames_ = false;
std::shared_ptr<AudioBuffer> tailBuffer_;

double playedBuffersDuration_ = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,30 @@ void AudioBufferSourceNode::setBuffer(
}

buffer_ = buffer;
alignedBus_ = std::make_shared<AudioBus>(*buffer_->bus_);
channelCount_ = buffer_->getNumberOfChannels();

stretch_->presetDefault(channelCount_, buffer_->getSampleRate());

if (pitchCorrection_) {
int extraTailFrames = static_cast<int>(
(getInputLatency() + getOutputLatency()) * context_->getSampleRate());
size_t totalSize = buffer_->getLength() + extraTailFrames;

alignedBus_ = std::make_shared<AudioBus>(
totalSize, channelCount_, buffer_->getSampleRate());
alignedBus_->copy(buffer_->bus_.get(), 0, 0, buffer_->getLength());

alignedBus_->zero(buffer_->getLength(), extraTailFrames);
} else {
alignedBus_ = std::make_shared<AudioBus>(*buffer_->bus_);
}

audioBus_ = std::make_shared<AudioBus>(
RENDER_QUANTUM_SIZE, channelCount_, context_->getSampleRate());
playbackRateBus_ = std::make_shared<AudioBus>(
RENDER_QUANTUM_SIZE * 3, channelCount_, context_->getSampleRate());

loopEnd_ = buffer_->getDuration();

stretch_->presetDefault(channelCount_, buffer_->getSampleRate());
}

void AudioBufferSourceNode::start(double when, double offset, double duration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
#include <audioapi/core/sources/AudioBufferBaseSourceNode.h>
#include <audioapi/libs/signalsmith-stretch/signalsmith-stretch.h>

#include <memory>
#include <cstddef>
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>

namespace audioapi {
Expand Down Expand Up @@ -37,7 +37,7 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode {
void setOnLoopEndedCallbackId(uint64_t callbackId);

protected:
std::shared_ptr<AudioBus> processNode(const std::shared_ptr<AudioBus>& processingBus, int framesToProcess) override;
std::shared_ptr<AudioBus> processNode(const std::shared_ptr<AudioBus> &processingBus, int framesToProcess) override;
double getCurrentPosition() const override;

private:
Expand All @@ -55,13 +55,13 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode {
void sendOnLoopEndedEvent();

void processWithoutInterpolation(
const std::shared_ptr<AudioBus>& processingBus,
const std::shared_ptr<AudioBus> &processingBus,
size_t startOffset,
size_t offsetLength,
float playbackRate) override;

void processWithInterpolation(
const std::shared_ptr<AudioBus>& processingBus,
const std::shared_ptr<AudioBus> &processingBus,
size_t startOffset,
size_t offsetLength,
float playbackRate) override;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,12 @@ export default class AudioBufferBaseSourceNode extends AudioScheduledSourceNode
public set onPositionChangedInterval(value: number) {
(this.node as IAudioBufferBaseSourceNode).onPositionChangedInterval = value;
}

public getLatency(): number {
return (
(this.node as IAudioBufferBaseSourceNode).getOutputLatency() +
(this.node as IAudioBufferBaseSourceNode).getInputLatency() *
(this.node as IAudioBufferBaseSourceNode).playbackRate.value
);
}
}
3 changes: 3 additions & 0 deletions packages/react-native-audio-api/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ export interface IAudioBufferBaseSourceNode extends IAudioScheduledSourceNode {
detune: IAudioParam;
playbackRate: IAudioParam;

getInputLatency: () => number;
getOutputLatency: () => number;

// passing subscriptionId(uint_64 in cpp, string in js) to the cpp
onPositionChanged: string;
// set how often the onPositionChanged event is called
Expand Down