From 57ce36d51f36a863fed49821fc2c6e603c9b73a1 Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 12 Nov 2025 15:05:27 +0100 Subject: [PATCH 1/7] feat: delay node --- .../BaseAudioContextHostObject.cpp | 13 +++ .../HostObjects/BaseAudioContextHostObject.h | 1 + .../effects/DelayNodeHostObject.cpp | 20 ++++ .../HostObjects/effects/DelayNodeHostObject.h | 19 ++++ .../cpp/audioapi/core/BaseAudioContext.cpp | 7 ++ .../cpp/audioapi/core/BaseAudioContext.h | 2 + .../cpp/audioapi/core/effects/DelayNode.cpp | 91 +++++++++++++++++++ .../cpp/audioapi/core/effects/DelayNode.h | 30 ++++++ .../src/core/AudioNode.ts | 4 +- .../src/core/BaseAudioContext.ts | 6 ++ .../src/core/DelayNode.ts | 13 +++ .../react-native-audio-api/src/interfaces.ts | 6 ++ .../src/web-core/AudioContext.tsx | 5 + .../src/web-core/BaseAudioContext.tsx | 2 + .../src/web-core/DelayNode.tsx | 12 +++ .../src/web-core/OfflineAudioContext.tsx | 5 + 16 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h create mode 100644 packages/react-native-audio-api/src/core/DelayNode.ts create mode 100644 packages/react-native-audio-api/src/web-core/DelayNode.tsx diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp index 0f7565926..f54bd61d0 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ BaseAudioContextHostObject::BaseAudioContextHostObject( JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createStreamer), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createConstantSource), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createGain), + JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createDelay), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createStereoPanner), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createBiquadFilter), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createBufferSource), @@ -189,6 +191,17 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createGain) { return jsi::Object::createFromHostObject(runtime, gainHostObject); } +JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createDelay) { + auto maxDelayTime = static_cast(args[0].getNumber()); + auto delayNode = context_->createDelay(maxDelayTime); + auto delayNodeHostObject = std::make_shared(delayNode); + auto jsiObject = + jsi::Object::createFromHostObject(runtime, delayNodeHostObject); + jsiObject.setExternalMemoryPressure( + runtime, sizeof(float) * this->context_->getSampleRate() * maxDelayTime); + return jsiObject; +} + JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createStereoPanner) { auto stereoPanner = context_->createStereoPanner(); auto stereoPannerHostObject = diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h index 4ab17b29f..f988bff37 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h @@ -42,6 +42,7 @@ class BaseAudioContextHostObject : public JsiHostObject { JSI_HOST_FUNCTION_DECL(createPeriodicWave); JSI_HOST_FUNCTION_DECL(createAnalyser); JSI_HOST_FUNCTION_DECL(createConvolver); + JSI_HOST_FUNCTION_DECL(createDelay); std::shared_ptr context_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp new file mode 100644 index 000000000..9567e6ae4 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp @@ -0,0 +1,20 @@ +#include + +#include +#include + +namespace audioapi { + +DelayNodeHostObject::DelayNodeHostObject(const std::shared_ptr &node) + : AudioNodeHostObject(node) { + addGetters(JSI_EXPORT_PROPERTY_GETTER(DelayNodeHostObject, delayTime)); +} + +JSI_PROPERTY_GETTER_IMPL(DelayNodeHostObject, delayTime) { + auto delayNode = std::static_pointer_cast(node_); + auto delayTimeParam = + std::make_shared(delayNode->getDelayTimeParam()); + return jsi::Object::createFromHostObject(runtime, delayTimeParam); +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h new file mode 100644 index 000000000..46282dcf7 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include +#include + +namespace audioapi { +using namespace facebook; + +class DelayNode; + +class DelayNodeHostObject : public AudioNodeHostObject { + public: + explicit DelayNodeHostObject(const std::shared_ptr &node); + + JSI_PROPERTY_GETTER_DECL(delayTime); +}; +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp index fa05c13e2..ddaa7739a 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +135,12 @@ std::shared_ptr BaseAudioContext::createGain() { return gain; } +std::shared_ptr BaseAudioContext::createDelay(float maxDelayTime) { + auto delay = std::make_shared(this, maxDelayTime); + nodeManager_->addProcessingNode(delay); + return delay; +} + std::shared_ptr BaseAudioContext::createStereoPanner() { auto stereoPanner = std::make_shared(this); nodeManager_->addProcessingNode(stereoPanner); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h index eba8b3460..cd4f1ef6d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h @@ -16,6 +16,7 @@ namespace audioapi { class AudioBus; class GainNode; +class DelayNode; class AudioBuffer; class PeriodicWave; class OscillatorNode; @@ -66,6 +67,7 @@ class BaseAudioContext { std::shared_ptr createConstantSource(); std::shared_ptr createStreamer(); std::shared_ptr createGain(); + std::shared_ptr createDelay(float maxDelayTime); std::shared_ptr createStereoPanner(); std::shared_ptr createBiquadFilter(); std::shared_ptr createBufferSource(bool pitchCorrection); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp new file mode 100644 index 000000000..5ea58c107 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include + +namespace audioapi { + +DelayNode::DelayNode(BaseAudioContext *context, float maxDelayTime) + : AudioNode(context) { + delayTimeParam_ = std::make_shared(0, 0, maxDelayTime, context); + delayBuffer_ = std::make_shared( + static_cast( + maxDelayTime * context->getSampleRate() + + 1), // +1 to enable delayTime equal to maxDelayTime + channelCount_, + context->getSampleRate()); + isInitialized_ = true; +} + +std::shared_ptr DelayNode::getDelayTimeParam() const { + return delayTimeParam_; +} + +void DelayNode::onInputDisabled() { + numberOfEnabledInputNodes_ -= 1; + if (isEnabled() && numberOfEnabledInputNodes_ == 0) { + signalledToStop_ = true; + remainingFrames_ = delayTimeParam_->getValue() * context_->getSampleRate(); + } +} + +std::shared_ptr DelayNode::processNode( + const std::shared_ptr &processingBus, + int framesToProcess) { + // Mismatched channel count, mix delay buffer to match processing bus + if (processingBus->getNumberOfChannels() != + delayBuffer_->getNumberOfChannels()) { + AudioBus mixedDelayBuffer( + delayBuffer_->getSize(), + processingBus->getNumberOfChannels(), + context_->getSampleRate()); + mixedDelayBuffer.zero(); + mixedDelayBuffer.sum(delayBuffer_.get()); + delayBuffer_ = std::make_shared(mixedDelayBuffer); + } + if (signalledToStop_) { + if (remainingFrames_ > 0) { + for (int frame = 0; frame < std::min(framesToProcess, remainingFrames_); + ++frame) { + for (int channel = 0; channel < processingBus->getNumberOfChannels(); + ++channel) { + processingBus->getChannel(channel)->getData()[frame] = + delayBuffer_->getChannel(channel)->getData()[readIndex_]; + } + readIndex_ = (readIndex_ + 1) % delayBuffer_->getSize(); + } + remainingFrames_ -= framesToProcess; + } else { + disable(); + signalledToStop_ = false; + } + return processingBus; + } + double time = context_->getCurrentTime(); + auto delayTimeParamValues = + delayTimeParam_->processARateParam(framesToProcess, time); + auto sampleRate = context_->getSampleRate(); + for (int frame = 0; frame < framesToProcess; ++frame) { + float delayTime = (*delayTimeParamValues->getChannel(0))[frame]; + size_t delaySamples = static_cast(delayTime * sampleRate); + size_t writeIndex = (readIndex_ + delaySamples) % delayBuffer_->getSize(); + + for (int channel = 0; channel < processingBus->getNumberOfChannels(); + ++channel) { + // Write the current input sample into the delay buffer + float inputSample = processingBus->getChannel(channel)->getData()[frame]; + delayBuffer_->getChannel(channel)->getData()[writeIndex] = inputSample; + + // Output the delayed sample + float delayedSample = + delayBuffer_->getChannel(channel)->getData()[readIndex_]; + processingBus->getChannel(channel)->getData()[frame] = delayedSample; + } + + readIndex_ = (readIndex_ + 1) % delayBuffer_->getSize(); + } + return processingBus; +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h new file mode 100644 index 000000000..ca117db4f --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +namespace audioapi { + +class AudioBus; + +class DelayNode : public AudioNode { + public: + explicit DelayNode(BaseAudioContext *context, float maxDelayTime); + + [[nodiscard]] std::shared_ptr getDelayTimeParam() const; + + protected: + std::shared_ptr processNode(const std::shared_ptr& processingBus, int framesToProcess) override; + + private: + void onInputDisabled() override; + std::shared_ptr delayTimeParam_; + std::shared_ptr delayBuffer_; + size_t readIndex_ = 0; + bool signalledToStop_ = false; + int remainingFrames_ = 0; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 2e19d51d1..f803806d6 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -23,7 +23,7 @@ export default class AudioNode { this.channelInterpretation = this.node.channelInterpretation; } - public connect(destination: AudioNode | AudioParam): AudioNode | AudioParam { + public connect(destination: AudioNode | AudioParam): AudioNode { if (this.context !== destination.context) { throw new InvalidAccessError( 'Source and destination are from different BaseAudioContexts' @@ -36,7 +36,7 @@ export default class AudioNode { this.node.connect(destination.node); } - return destination; + return destination instanceof AudioNode ? destination : this; } public disconnect(destination?: AudioNode | AudioParam): void { diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index 86938d40c..8b0aa870e 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -25,6 +25,7 @@ import RecorderAdapterNode from './RecorderAdapterNode'; import StereoPannerNode from './StereoPannerNode'; import StreamerNode from './StreamerNode'; import WorkletNode from './WorkletNode'; +import DelayNode from './DelayNode'; import { decodeAudioData, decodePCMInBase64 } from './AudioDecoder'; export default class BaseAudioContext { @@ -196,6 +197,11 @@ export default class BaseAudioContext { return new GainNode(this, this.context.createGain()); } + createDelay(maxDelayTime?: number): DelayNode { + const maxTime = maxDelayTime ?? 1.0; + return new DelayNode(this, this.context.createDelay(maxTime)); + } + createStereoPanner(): StereoPannerNode { return new StereoPannerNode(this, this.context.createStereoPanner()); } diff --git a/packages/react-native-audio-api/src/core/DelayNode.ts b/packages/react-native-audio-api/src/core/DelayNode.ts new file mode 100644 index 000000000..ac4ab9d86 --- /dev/null +++ b/packages/react-native-audio-api/src/core/DelayNode.ts @@ -0,0 +1,13 @@ +import { IDelayNode } from '../interfaces'; +import AudioNode from './AudioNode'; +import AudioParam from './AudioParam'; +import BaseAudioContext from './BaseAudioContext'; + +export default class DelayNode extends AudioNode { + readonly delayTime: AudioParam; + + constructor(context: BaseAudioContext, delay: IDelayNode) { + super(context, delay); + this.delayTime = new AudioParam(delay.delayTime, context); + } +} diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index 8133ddcfd..62db3daf3 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -60,6 +60,7 @@ export interface IBaseAudioContext { createOscillator(): IOscillatorNode; createConstantSource(): IConstantSourceNode; createGain(): IGainNode; + createDelay(maxDelayTime: number): IDelayNode; createStereoPanner(): IStereoPannerNode; createBiquadFilter: () => IBiquadFilterNode; createBufferSource: (pitchCorrection: boolean) => IAudioBufferSourceNode; @@ -108,6 +109,11 @@ export interface IAudioNode { disconnect: (destination?: IAudioNode | IAudioParam) => void; } +export interface IDelayNode extends IAudioNode { + readonly delayTime: IAudioParam; + maxDelayTime: number; +} + export interface IGainNode extends IAudioNode { readonly gain: IAudioParam; } diff --git a/packages/react-native-audio-api/src/web-core/AudioContext.tsx b/packages/react-native-audio-api/src/web-core/AudioContext.tsx index d7ab486a4..3d6a9c463 100644 --- a/packages/react-native-audio-api/src/web-core/AudioContext.tsx +++ b/packages/react-native-audio-api/src/web-core/AudioContext.tsx @@ -16,6 +16,7 @@ import OscillatorNode from './OscillatorNode'; import PeriodicWave from './PeriodicWave'; import StereoPannerNode from './StereoPannerNode'; import ConvolverNode from './ConvolverNode'; +import DelayNode from './DelayNode'; import { ConvolverNodeOptions } from './ConvolverNodeOptions'; import { globalWasmPromise, globalTag } from './custom/LoadCustomWasm'; @@ -64,6 +65,10 @@ export default class AudioContext implements BaseAudioContext { return new GainNode(this, this.context.createGain()); } + createDelay(maxDelayTime?: number): DelayNode { + return new DelayNode(this, this.context.createDelay(maxDelayTime)); + } + createStereoPanner(): StereoPannerNode { return new StereoPannerNode(this, this.context.createStereoPanner()); } diff --git a/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx b/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx index 5d2fa9a74..69529f071 100644 --- a/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx +++ b/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx @@ -10,6 +10,7 @@ import PeriodicWave from './PeriodicWave'; import StereoPannerNode from './StereoPannerNode'; import ConstantSourceNode from './ConstantSourceNode'; import ConvolverNode from './ConvolverNode'; +import DelayNode from './DelayNode'; export default interface BaseAudioContext { readonly context: globalThis.BaseAudioContext; @@ -22,6 +23,7 @@ export default interface BaseAudioContext { createOscillator(): OscillatorNode; createConstantSource(): ConstantSourceNode; createGain(): GainNode; + createDelay(maxDelayTime?: number): DelayNode; createStereoPanner(): StereoPannerNode; createBiquadFilter(): BiquadFilterNode; createConvolver(): ConvolverNode; diff --git a/packages/react-native-audio-api/src/web-core/DelayNode.tsx b/packages/react-native-audio-api/src/web-core/DelayNode.tsx new file mode 100644 index 000000000..68e8e54a6 --- /dev/null +++ b/packages/react-native-audio-api/src/web-core/DelayNode.tsx @@ -0,0 +1,12 @@ +import BaseAudioContext from './BaseAudioContext'; +import AudioNode from './AudioNode'; +import AudioParam from './AudioParam'; + +export default class DelayNode extends AudioNode { + readonly delayTime: AudioParam; + + constructor(context: BaseAudioContext, delay: globalThis.DelayNode) { + super(context, delay); + this.delayTime = new AudioParam(delay.delayTime, context); + } +} diff --git a/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx b/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx index cf9eec733..c58679393 100644 --- a/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx +++ b/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx @@ -20,6 +20,7 @@ import ConstantSourceNode from './ConstantSourceNode'; import { globalWasmPromise, globalTag } from './custom/LoadCustomWasm'; import ConvolverNode from './ConvolverNode'; import { ConvolverNodeOptions } from './ConvolverNodeOptions'; +import DelayNode from './DelayNode'; export default class OfflineAudioContext implements BaseAudioContext { readonly context: globalThis.OfflineAudioContext; @@ -70,6 +71,10 @@ export default class OfflineAudioContext implements BaseAudioContext { return new GainNode(this, this.context.createGain()); } + createDelay(maxDelayTime?: number): DelayNode { + return new DelayNode(this, this.context.createDelay(maxDelayTime)); + } + createStereoPanner(): StereoPannerNode { return new StereoPannerNode(this, this.context.createStereoPanner()); } From 0dbb0d0c7004c18b20acd190fadab4a02ecef18c Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 12 Nov 2025 15:31:33 +0100 Subject: [PATCH 2/7] docs: docs for delay node --- .../docs/core/base-audio-context.mdx | 9 +++++ .../audiodocs/docs/effects/delay-node.mdx | 38 +++++++++++++++++++ .../docs/other/web-audio-api-coverage.mdx | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/audiodocs/docs/effects/delay-node.mdx diff --git a/packages/audiodocs/docs/core/base-audio-context.mdx b/packages/audiodocs/docs/core/base-audio-context.mdx index 8823f992d..9c5a81797 100644 --- a/packages/audiodocs/docs/core/base-audio-context.mdx +++ b/packages/audiodocs/docs/core/base-audio-context.mdx @@ -159,6 +159,15 @@ Creates [`GainNode`](/docs/effects/gain-node). #### Returns `GainNode`. +### `createDelay` + +Creates [`DelayNode`](/docs/effects/delay-node) + +| Parameter | Type | Description | +| :---: | :---: | :---- | +| `maxDelayTime` | `number` | Maximum amount of time to buffer delayed values| + +#### Returns `DelayNode` ### `createConvolver` diff --git a/packages/audiodocs/docs/effects/delay-node.mdx b/packages/audiodocs/docs/effects/delay-node.mdx new file mode 100644 index 000000000..30fbf007d --- /dev/null +++ b/packages/audiodocs/docs/effects/delay-node.mdx @@ -0,0 +1,38 @@ +--- +sidebar_position: 5 +--- + +import AudioNodePropsTable from "@site/src/components/AudioNodePropsTable" +import { ReadOnly } from '@site/src/components/Badges'; + +# DelayNode + +The `DelayNode` interface represents the latency of the audio signal by given time. It is an [`AudioNode`](/docs/core/audio-node) that applies time shift to incoming signal f.e. +if `delayTime` value is 0.5, it means that audio will be played after 0.5 seconds. + +#### [`AudioNode`](/docs/core/audio-node#properties) properties + + + +## Constructor + +[`BaseAudioContext.createDelay(maxDelayTime?: number)`](/docs/core/base-audio-context#createdelay) + +## Properties + +It inherits all properties from [`AudioNode`](/docs/core/audio-node#properties). + +| Name | Type | Description | | +| :----: | :----: | :-------- | :-: | +| `delayTime` | [`AudioParam`](/docs/core/audio-param) | [`a-rate`](/docs/core/audio-param#a-rate-vs-k-rate) `AudioParam` representing value of time shift to apply. | + +## Methods + +`GainNode` does not define any additional methods. +It inherits all methods from [`AudioNode`](/docs/core/audio-node#methods). + +## Remarks + +#### `delayTime` +- Default value is 0. +- Nominal range is 0 - `maxDelayTime`. diff --git a/packages/audiodocs/docs/other/web-audio-api-coverage.mdx b/packages/audiodocs/docs/other/web-audio-api-coverage.mdx index 3854d8eda..e1d33d16b 100644 --- a/packages/audiodocs/docs/other/web-audio-api-coverage.mdx +++ b/packages/audiodocs/docs/other/web-audio-api-coverage.mdx @@ -25,6 +25,7 @@ sidebar_position: 2 | PeriodicWave | ✅ | | StereoPannerNode | ✅ | | ConvolverNode | ✅ | +| DelayNode | ✅ | | AudioContext | 🚧 | Available props and methods: `close`, `suspend`, `resume` | | BaseAudioContext | 🚧 | Available props and methods: `currentTime`, `destination`, `sampleRate`, `state`, `decodeAudioData`, all create methods for available or partially implemented nodes | | AudioListener | ❌ | @@ -35,7 +36,6 @@ sidebar_position: 2 | AudioWorkletProcessor | ❌ | | ChannelMergerNode | ❌ | | ChannelSplitterNode | ❌ | -| DelayNode | ❌ | | DynamicsCompressorNode | ❌ | | IIRFilterNode | ❌ | | MediaElementAudioSourceNode | ❌ | From f6bc80c6ddbbd6e1ec16fbc117e231ce1fab9394 Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 12 Nov 2025 15:49:17 +0100 Subject: [PATCH 3/7] test: delaynode --- .../common/cpp/test/src/DelayTest.cpp | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 packages/react-native-audio-api/common/cpp/test/src/DelayTest.cpp diff --git a/packages/react-native-audio-api/common/cpp/test/src/DelayTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/DelayTest.cpp new file mode 100644 index 000000000..563d7542d --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/test/src/DelayTest.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace audioapi; + +class DelayTest : public ::testing::Test { + protected: + std::shared_ptr eventRegistry; + std::unique_ptr context; + static constexpr int sampleRate = 44100; + + void SetUp() override { + eventRegistry = std::make_shared(); + context = std::make_unique( + 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + } +}; + +class TestableDelayNode : public DelayNode { + public: + explicit TestableDelayNode(BaseAudioContext *context) + : DelayNode(context, 1) {} + + void setDelayTimeParam(float value) { + getDelayTimeParam()->setValue(value); + } + + std::shared_ptr processNode( + const std::shared_ptr &processingBus, + int framesToProcess) override { + return DelayNode::processNode(processingBus, framesToProcess); + } +}; + +TEST_F(DelayTest, DelayCanBeCreated) { + auto delay = context->createDelay(1.0f); + ASSERT_NE(delay, nullptr); +} + +TEST_F(DelayTest, DelayWithZeroDelayOutputsInputSignal) { + static constexpr float DELAY_TIME = 0.0f; + static constexpr int FRAMES_TO_PROCESS = 4; + auto delayNode = std::make_shared(context.get()); + delayNode->setDelayTimeParam(DELAY_TIME); + + auto bus = + std::make_shared(FRAMES_TO_PROCESS, 1, sampleRate); + for (size_t i = 0; i < bus->getSize(); ++i) { + bus->getChannel(0)->getData()[i] = i + 1; + } + + auto resultBus = delayNode->processNode(bus, FRAMES_TO_PROCESS); + for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { + EXPECT_FLOAT_EQ((*resultBus->getChannel(0))[i], static_cast(i + 1)); + } +} + +TEST_F(DelayTest, DelayAppliesTimeShiftCorrectly) { + float DELAY_TIME = (128.0 / context->getSampleRate()) * 0.5; + static constexpr int FRAMES_TO_PROCESS = 128; + auto delayNode = std::make_shared(context.get()); + delayNode->setDelayTimeParam(DELAY_TIME); + + auto bus = + std::make_shared(FRAMES_TO_PROCESS, 1, sampleRate); + for (size_t i = 0; i < bus->getSize(); ++i) { + bus->getChannel(0)->getData()[i] = i + 1; + } + + auto resultBus = delayNode->processNode(bus, FRAMES_TO_PROCESS); + for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { + if (i < + FRAMES_TO_PROCESS / 2) { // First 64 samples should be zero due to delay + EXPECT_FLOAT_EQ((*resultBus->getChannel(0))[i], 0.0f); + } else { + EXPECT_FLOAT_EQ( + (*resultBus->getChannel(0))[i], + static_cast( + i + 1 - + FRAMES_TO_PROCESS / + 2)); // Last 64 samples should be 1st part of bus + } + } +} + +TEST_F(DelayTest, DelayHandlesTailCorrectly) { + float DELAY_TIME = (128.0 / context->getSampleRate()) * 0.5; + static constexpr int FRAMES_TO_PROCESS = 128; + auto delayNode = std::make_shared(context.get()); + delayNode->setDelayTimeParam(DELAY_TIME); + + auto bus = + std::make_shared(FRAMES_TO_PROCESS, 1, sampleRate); + for (size_t i = 0; i < bus->getSize(); ++i) { + bus->getChannel(0)->getData()[i] = i + 1; + } + + delayNode->processNode(bus, FRAMES_TO_PROCESS); + auto resultBus = delayNode->processNode(bus, FRAMES_TO_PROCESS); + for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { + if (i < + FRAMES_TO_PROCESS / 2) { // First 64 samples should be 2nd part of bus + EXPECT_FLOAT_EQ( + (*resultBus->getChannel(0))[i], + static_cast(i + 1 + FRAMES_TO_PROCESS / 2)); + } else { + EXPECT_FLOAT_EQ( + (*resultBus->getChannel(0))[i], + 0.0f); // Last 64 samples should be zero + } + } +} From 5d6239252e58702ba0399a61177278c5081c8a6f Mon Sep 17 00:00:00 2001 From: michal Date: Mon, 17 Nov 2025 11:30:36 +0100 Subject: [PATCH 4/7] feat: removed audio thread allocations --- .../cpp/audioapi/core/effects/DelayNode.cpp | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp index 5ea58c107..330ab6e29 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp @@ -13,7 +13,7 @@ DelayNode::DelayNode(BaseAudioContext *context, float maxDelayTime) static_cast( maxDelayTime * context->getSampleRate() + 1), // +1 to enable delayTime equal to maxDelayTime - channelCount_, + 2, context->getSampleRate()); isInitialized_ = true; } @@ -30,61 +30,58 @@ void DelayNode::onInputDisabled() { } } +// delay buffer always has 2 channels, mix if needed std::shared_ptr DelayNode::processNode( const std::shared_ptr &processingBus, int framesToProcess) { - // Mismatched channel count, mix delay buffer to match processing bus - if (processingBus->getNumberOfChannels() != - delayBuffer_->getNumberOfChannels()) { - AudioBus mixedDelayBuffer( - delayBuffer_->getSize(), - processingBus->getNumberOfChannels(), - context_->getSampleRate()); - mixedDelayBuffer.zero(); - mixedDelayBuffer.sum(delayBuffer_.get()); - delayBuffer_ = std::make_shared(mixedDelayBuffer); - } if (signalledToStop_) { if (remainingFrames_ > 0) { - for (int frame = 0; frame < std::min(framesToProcess, remainingFrames_); - ++frame) { - for (int channel = 0; channel < processingBus->getNumberOfChannels(); - ++channel) { - processingBus->getChannel(channel)->getData()[frame] = - delayBuffer_->getChannel(channel)->getData()[readIndex_]; - } - readIndex_ = (readIndex_ + 1) % delayBuffer_->getSize(); + if (readIndex_ + framesToProcess >= delayBuffer_->getSize()) { + size_t framesToEnd = delayBuffer_->getSize() - readIndex_; + processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToEnd); + delayBuffer_->zero(readIndex_, framesToEnd); + readIndex_ = 0; + framesToProcess -= framesToEnd; + remainingFrames_ -= framesToEnd; } + processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToProcess); + delayBuffer_->zero(readIndex_, framesToProcess); remainingFrames_ -= framesToProcess; + readIndex_ += framesToProcess; } else { disable(); signalledToStop_ = false; } return processingBus; } - double time = context_->getCurrentTime(); - auto delayTimeParamValues = - delayTimeParam_->processARateParam(framesToProcess, time); - auto sampleRate = context_->getSampleRate(); - for (int frame = 0; frame < framesToProcess; ++frame) { - float delayTime = (*delayTimeParamValues->getChannel(0))[frame]; - size_t delaySamples = static_cast(delayTime * sampleRate); - size_t writeIndex = (readIndex_ + delaySamples) % delayBuffer_->getSize(); - - for (int channel = 0; channel < processingBus->getNumberOfChannels(); - ++channel) { - // Write the current input sample into the delay buffer - float inputSample = processingBus->getChannel(channel)->getData()[frame]; - delayBuffer_->getChannel(channel)->getData()[writeIndex] = inputSample; - - // Output the delayed sample - float delayedSample = - delayBuffer_->getChannel(channel)->getData()[readIndex_]; - processingBus->getChannel(channel)->getData()[frame] = delayedSample; - } - - readIndex_ = (readIndex_ + 1) % delayBuffer_->getSize(); + auto delayTime = delayTimeParam_->processKRateParam( + framesToProcess, context_->getCurrentTime()); + size_t processingBusStartIndex = 0; + size_t writeIndex = + static_cast(readIndex_ + delayTime * context_->getSampleRate()) % + delayBuffer_->getSize(); + int framesToWrite = framesToProcess; + if (writeIndex + framesToWrite >= delayBuffer_->getSize()) { + int framesToCopy = writeIndex + framesToWrite - delayBuffer_->getSize(); + delayBuffer_->sum( + processingBus.get(), processingBusStartIndex, writeIndex, framesToCopy); + writeIndex = 0; + processingBusStartIndex += framesToCopy; + framesToWrite -= framesToCopy; + } + delayBuffer_->sum( + processingBus.get(), processingBusStartIndex, writeIndex, framesToWrite); + processingBus->zero(); + if (readIndex_ + framesToProcess >= delayBuffer_->getSize()) { + size_t framesToEnd = delayBuffer_->getSize() - readIndex_; + processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToEnd); + readIndex_ = 0; + framesToProcess -= framesToEnd; + delayBuffer_->zero(readIndex_, framesToEnd); } + processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToProcess); + delayBuffer_->zero(readIndex_, framesToProcess); + readIndex_ += framesToProcess; return processingBus; } From 96c36c0fe43723ae0362913dd0d3854a038f3054 Mon Sep 17 00:00:00 2001 From: michal Date: Mon, 17 Nov 2025 12:09:24 +0100 Subject: [PATCH 5/7] feat: added size method --- .../audiodocs/docs/effects/delay-node.mdx | 20 +++++++++++++++---- .../docs/other/web-audio-api-coverage.mdx | 5 +++-- .../BaseAudioContextHostObject.cpp | 2 +- .../effects/DelayNodeHostObject.cpp | 7 +++++++ .../HostObjects/effects/DelayNodeHostObject.h | 2 ++ .../common/cpp/audioapi/core/AudioNode.h | 1 + .../audioapi/core/utils/AudioNodeManager.cpp | 6 +++--- .../src/core/AudioNode.ts | 7 ++++--- 8 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/audiodocs/docs/effects/delay-node.mdx b/packages/audiodocs/docs/effects/delay-node.mdx index 30fbf007d..c77309c7d 100644 --- a/packages/audiodocs/docs/effects/delay-node.mdx +++ b/packages/audiodocs/docs/effects/delay-node.mdx @@ -14,6 +14,10 @@ if `delayTime` value is 0.5, it means that audio will be played after 0.5 second +:::info +Delay is a node with tail-time, which means, that it continues to output non-silent audio with zero input for the duration of `delayTime`. +::: + ## Constructor [`BaseAudioContext.createDelay(maxDelayTime?: number)`](/docs/core/base-audio-context#createdelay) @@ -22,17 +26,25 @@ if `delayTime` value is 0.5, it means that audio will be played after 0.5 second It inherits all properties from [`AudioNode`](/docs/core/audio-node#properties). -| Name | Type | Description | | -| :----: | :----: | :-------- | :-: | -| `delayTime` | [`AudioParam`](/docs/core/audio-param) | [`a-rate`](/docs/core/audio-param#a-rate-vs-k-rate) `AudioParam` representing value of time shift to apply. | +| Name | Type | Description | +| :----: | :----: | :-------- | +| `delayTime`| [`AudioParam`](/docs/core/audio-param) | [`k-rate`](/docs/core/audio-param#a-rate-vs-k-rate) `AudioParam` representing value of time shift to apply. | + +:::warning +In web audio api specs `delayTime` is an `a-rate` param. +::: ## Methods -`GainNode` does not define any additional methods. +`DelayNode` does not define any additional methods. It inherits all methods from [`AudioNode`](/docs/core/audio-node#methods). ## Remarks +#### `maxDelayTime` +- Default value is 1.0. +- Nominal range is 0 - 180. + #### `delayTime` - Default value is 0. - Nominal range is 0 - `maxDelayTime`. diff --git a/packages/audiodocs/docs/other/web-audio-api-coverage.mdx b/packages/audiodocs/docs/other/web-audio-api-coverage.mdx index e1d33d16b..2c028152c 100644 --- a/packages/audiodocs/docs/other/web-audio-api-coverage.mdx +++ b/packages/audiodocs/docs/other/web-audio-api-coverage.mdx @@ -1,3 +1,4 @@ + --- id: web-audio-api-coverage sidebar_label: Web Audio API coverage @@ -19,13 +20,13 @@ sidebar_position: 2 | AudioScheduledSourceNode | ✅ | | BiquadFilterNode | ✅ | | ConstantSourceNode | ✅ | +| ConvolverNode | ✅ | +| DelayNode | ✅ | | GainNode | ✅ | | OfflineAudioContext | ✅ | | OscillatorNode | ✅ | | PeriodicWave | ✅ | | StereoPannerNode | ✅ | -| ConvolverNode | ✅ | -| DelayNode | ✅ | | AudioContext | 🚧 | Available props and methods: `close`, `suspend`, `resume` | | BaseAudioContext | 🚧 | Available props and methods: `currentTime`, `destination`, `sampleRate`, `state`, `decodeAudioData`, all create methods for available or partially implemented nodes | | AudioListener | ❌ | diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp index f54bd61d0..1283797a2 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.cpp @@ -198,7 +198,7 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createDelay) { auto jsiObject = jsi::Object::createFromHostObject(runtime, delayNodeHostObject); jsiObject.setExternalMemoryPressure( - runtime, sizeof(float) * this->context_->getSampleRate() * maxDelayTime); + runtime, delayNodeHostObject->getSizeInBytes()); return jsiObject; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp index 9567e6ae4..821ba96ba 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp @@ -1,6 +1,7 @@ #include #include +#include #include namespace audioapi { @@ -10,6 +11,12 @@ DelayNodeHostObject::DelayNodeHostObject(const std::shared_ptr &node) addGetters(JSI_EXPORT_PROPERTY_GETTER(DelayNodeHostObject, delayTime)); } +size_t DelayNodeHostObject::getSizeInBytes() const { + auto delayNode = std::static_pointer_cast(node_); + return sizeof(float) * delayNode->context_->getSampleRate() * + delayNode->getDelayTimeParam()->getMaxValue(); +} + JSI_PROPERTY_GETTER_IMPL(DelayNodeHostObject, delayTime) { auto delayNode = std::static_pointer_cast(node_); auto delayTimeParam = diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h index 46282dcf7..379c8e5f6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h @@ -14,6 +14,8 @@ class DelayNodeHostObject : public AudioNodeHostObject { public: explicit DelayNodeHostObject(const std::shared_ptr &node); + [[nodiscard]] size_t getSizeInBytes() const; + JSI_PROPERTY_GETTER_DECL(delayTime); }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h index 4b726ff1b..99f778664 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h @@ -42,6 +42,7 @@ class AudioNode : public std::enable_shared_from_this { friend class AudioNodeManager; friend class AudioDestinationNode; friend class ConvolverNode; + friend class DelayNodeHostObject; BaseAudioContext *context_; std::shared_ptr audioBus_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp index 94ac0732a..22aa4299e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -222,9 +223,8 @@ inline bool AudioNodeManager::nodeCanBeDestructed( if constexpr (std::is_base_of_v) { return node.use_count() == 1 && (node->isUnscheduled() || node->isFinished()); - } else if constexpr (std::is_base_of_v< - ConvolverNode, - U>) { // convolver overrides disabling behavior + } else if constexpr ( + std::is_base_of_v || std::is_base_of_v) { return node.use_count() == 1 && !node->isEnabled(); } return node.use_count() == 1; diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index f803806d6..7990f4ce3 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -23,7 +23,9 @@ export default class AudioNode { this.channelInterpretation = this.node.channelInterpretation; } - public connect(destination: AudioNode | AudioParam): AudioNode { + public connect(destination: AudioNode): AudioNode; + public connect(destination: AudioParam): void; + public connect(destination: AudioNode | AudioParam): AudioNode | void { if (this.context !== destination.context) { throw new InvalidAccessError( 'Source and destination are from different BaseAudioContexts' @@ -34,9 +36,8 @@ export default class AudioNode { this.node.connect(destination.audioParam); } else { this.node.connect(destination.node); + return destination; } - - return destination instanceof AudioNode ? destination : this; } public disconnect(destination?: AudioNode | AudioParam): void { From ed9ac5d449c37777ee2337c405e48110c4d26b2d Mon Sep 17 00:00:00 2001 From: michal Date: Fri, 21 Nov 2025 16:26:15 +0100 Subject: [PATCH 6/7] feat: changes after review --- .../common/cpp/audioapi/core/AudioNode.cpp | 4 + .../common/cpp/audioapi/core/AudioNode.h | 2 + .../audioapi/core/effects/ConvolverNode.cpp | 1 + .../cpp/audioapi/core/effects/DelayNode.cpp | 83 ++++++++++--------- .../cpp/audioapi/core/effects/DelayNode.h | 7 ++ .../audioapi/core/utils/AudioNodeManager.cpp | 3 +- 6 files changed, 62 insertions(+), 38 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.cpp index 9035ab02a..47ff549b3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.cpp @@ -70,6 +70,10 @@ bool AudioNode::isEnabled() const { return isEnabled_; } +bool AudioNode::requiresTailProcessing() const { + return requiresTailProcessing_; +} + void AudioNode::enable() { if (isEnabled()) { return; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h index bc2e2a6b6..a56e79454 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h @@ -38,6 +38,7 @@ class AudioNode : public std::enable_shared_from_this { bool checkIsAlreadyProcessed); bool isEnabled() const; + bool requiresTailProcessing() const; void enable(); virtual void disable(); @@ -65,6 +66,7 @@ class AudioNode : public std::enable_shared_from_this { int numberOfEnabledInputNodes_ = 0; bool isInitialized_ = false; bool isEnabled_ = true; + bool requiresTailProcessing_ = false; std::size_t lastRenderedFrame_{SIZE_MAX}; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.cpp index a09bc0ced..2692beeab 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.cpp @@ -30,6 +30,7 @@ ConvolverNode::ConvolverNode( setBuffer(buffer); audioBus_ = std::make_shared(RENDER_QUANTUM_SIZE, channelCount_, context->getSampleRate()); + requiresTailProcessing_ = true; isInitialized_ = true; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp index 9bf2695e6..f782aaca3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp @@ -13,8 +13,9 @@ DelayNode::DelayNode(BaseAudioContext *context, float maxDelayTime) : AudioNode( static_cast( maxDelayTime * context->getSampleRate() + 1), // +1 to enable delayTime equal to maxDelayTime - 2, + channelCount_, context->getSampleRate()); + requiresTailProcessing_ = true; isInitialized_ = true; } @@ -30,54 +31,62 @@ void DelayNode::onInputDisabled() { } } -// delay buffer always has 2 channels, mix if needed +void DelayNode::delayBufferOperation( + const std::shared_ptr &processingBus, + int framesToProcess, + size_t &operationStartingIndex, + DelayNode::BufferAction action) { + size_t processingBusStartIndex = 0; + // handle buffer wrap around + if (operationStartingIndex + framesToProcess > delayBuffer_->getSize()) { + int framesToEnd = operationStartingIndex + framesToProcess - delayBuffer_->getSize(); + if (action == BufferAction::WRITE) { + delayBuffer_->sum( + processingBus.get(), processingBusStartIndex, operationStartingIndex, framesToEnd); + } else { // READ + processingBus->sum( + delayBuffer_.get(), operationStartingIndex, processingBusStartIndex, framesToEnd); + } + operationStartingIndex = 0; + processingBusStartIndex += framesToEnd; + framesToProcess -= framesToEnd; + } + if (action == BufferAction::WRITE) { + delayBuffer_->sum( + processingBus.get(), processingBusStartIndex, operationStartingIndex, framesToProcess); + processingBus->zero(); + } else { // READ + processingBus->sum( + delayBuffer_.get(), operationStartingIndex, processingBusStartIndex, framesToProcess); + delayBuffer_->zero(operationStartingIndex, framesToProcess); + } + operationStartingIndex += framesToProcess; +} + +// delay buffer always has channelCount_ channels +// processing is split into two parts +// 1. writing to delay buffer (mixing if needed) from processing bus +// 2. reading from delay buffer to processing bus (mixing if needed) with delay std::shared_ptr DelayNode::processNode( const std::shared_ptr &processingBus, int framesToProcess) { + // handling tail processing if (signalledToStop_) { - if (remainingFrames_ > 0) { - if (readIndex_ + framesToProcess >= delayBuffer_->getSize()) { - size_t framesToEnd = delayBuffer_->getSize() - readIndex_; - processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToEnd); - delayBuffer_->zero(readIndex_, framesToEnd); - readIndex_ = 0; - framesToProcess -= framesToEnd; - remainingFrames_ -= framesToEnd; - } - processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToProcess); - delayBuffer_->zero(readIndex_, framesToProcess); - remainingFrames_ -= framesToProcess; - readIndex_ += framesToProcess; - } else { + if (remainingFrames_ <= 0) { disable(); signalledToStop_ = false; + return processingBus; } + delayBufferOperation(processingBus, framesToProcess, readIndex_, DelayNode::BufferAction::READ); + remainingFrames_ -= framesToProcess; return processingBus; } + // normal processing auto delayTime = delayTimeParam_->processKRateParam(framesToProcess, context_->getCurrentTime()); - size_t processingBusStartIndex = 0; size_t writeIndex = static_cast(readIndex_ + delayTime * context_->getSampleRate()) % delayBuffer_->getSize(); - int framesToWrite = framesToProcess; - if (writeIndex + framesToWrite >= delayBuffer_->getSize()) { - int framesToCopy = writeIndex + framesToWrite - delayBuffer_->getSize(); - delayBuffer_->sum(processingBus.get(), processingBusStartIndex, writeIndex, framesToCopy); - writeIndex = 0; - processingBusStartIndex += framesToCopy; - framesToWrite -= framesToCopy; - } - delayBuffer_->sum(processingBus.get(), processingBusStartIndex, writeIndex, framesToWrite); - processingBus->zero(); - if (readIndex_ + framesToProcess >= delayBuffer_->getSize()) { - size_t framesToEnd = delayBuffer_->getSize() - readIndex_; - processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToEnd); - readIndex_ = 0; - framesToProcess -= framesToEnd; - delayBuffer_->zero(readIndex_, framesToEnd); - } - processingBus->sum(delayBuffer_.get(), readIndex_, 0, framesToProcess); - delayBuffer_->zero(readIndex_, framesToProcess); - readIndex_ += framesToProcess; + delayBufferOperation(processingBus, framesToProcess, writeIndex, DelayNode::BufferAction::WRITE); + delayBufferOperation(processingBus, framesToProcess, readIndex_, DelayNode::BufferAction::READ); return processingBus; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h index 1023f59af..15ab28f10 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace audioapi { @@ -22,6 +23,12 @@ class DelayNode : public AudioNode { private: void onInputDisabled() override; + enum class BufferAction { READ, WRITE }; + void delayBufferOperation( + const std::shared_ptr &processingBus, + int framesToProcess, + size_t &operationStartingIndex, + BufferAction action); std::shared_ptr delayTimeParam_; std::shared_ptr delayBuffer_; size_t readIndex_ = 0; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp index 8f8494913..ca4a182de 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp @@ -220,7 +220,8 @@ inline bool AudioNodeManager::nodeCanBeDestructed(std::shared_ptr const &node // playing if constexpr (std::is_base_of_v) { return node.use_count() == 1 && (node->isUnscheduled() || node->isFinished()); - } else if constexpr (std::is_base_of_v || std::is_base_of_v) { + } else if (node->requiresTailProcessing()) { + // if the node requires tail processing, its own implementation handles disabling it at the right time return node.use_count() == 1 && !node->isEnabled(); } return node.use_count() == 1; From eadc5d0ff4748f11408105f7c92802686434cba2 Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 26 Nov 2025 12:28:11 +0100 Subject: [PATCH 7/7] feat: few empty lines to improve readability --- .../common/cpp/audioapi/core/effects/DelayNode.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp index f782aaca3..cf2e44c5b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp @@ -37,9 +37,11 @@ void DelayNode::delayBufferOperation( size_t &operationStartingIndex, DelayNode::BufferAction action) { size_t processingBusStartIndex = 0; + // handle buffer wrap around if (operationStartingIndex + framesToProcess > delayBuffer_->getSize()) { int framesToEnd = operationStartingIndex + framesToProcess - delayBuffer_->getSize(); + if (action == BufferAction::WRITE) { delayBuffer_->sum( processingBus.get(), processingBusStartIndex, operationStartingIndex, framesToEnd); @@ -47,10 +49,12 @@ void DelayNode::delayBufferOperation( processingBus->sum( delayBuffer_.get(), operationStartingIndex, processingBusStartIndex, framesToEnd); } + operationStartingIndex = 0; processingBusStartIndex += framesToEnd; framesToProcess -= framesToEnd; } + if (action == BufferAction::WRITE) { delayBuffer_->sum( processingBus.get(), processingBusStartIndex, operationStartingIndex, framesToProcess); @@ -60,6 +64,7 @@ void DelayNode::delayBufferOperation( delayBuffer_.get(), operationStartingIndex, processingBusStartIndex, framesToProcess); delayBuffer_->zero(operationStartingIndex, framesToProcess); } + operationStartingIndex += framesToProcess; } @@ -77,16 +82,19 @@ std::shared_ptr DelayNode::processNode( signalledToStop_ = false; return processingBus; } + delayBufferOperation(processingBus, framesToProcess, readIndex_, DelayNode::BufferAction::READ); remainingFrames_ -= framesToProcess; return processingBus; } + // normal processing auto delayTime = delayTimeParam_->processKRateParam(framesToProcess, context_->getCurrentTime()); size_t writeIndex = static_cast(readIndex_ + delayTime * context_->getSampleRate()) % delayBuffer_->getSize(); delayBufferOperation(processingBus, framesToProcess, writeIndex, DelayNode::BufferAction::WRITE); delayBufferOperation(processingBus, framesToProcess, readIndex_, DelayNode::BufferAction::READ); + return processingBus; }