diff --git a/packages/audiodocs/docs/core/base-audio-context.mdx b/packages/audiodocs/docs/core/base-audio-context.mdx index dd0dc9fa2..2e6a098bf 100644 --- a/packages/audiodocs/docs/core/base-audio-context.mdx +++ b/packages/audiodocs/docs/core/base-audio-context.mdx @@ -120,6 +120,16 @@ Creates [`ConvolverNode`](/docs/effects/convolver-node). #### Returns `ConvolverNode`. +### `createDelay` + +Creates [`DelayNode`](/docs/effects/delay-node) + +| Parameter | Type | Description | +| :---: | :---: | :---- | +| `maxDelayTime` | `number` | Maximum amount of time to buffer delayed values| + +#### Returns `DelayNode` + ### `createGain` Creates [`GainNode`](/docs/effects/gain-node). diff --git a/packages/audiodocs/docs/effects/delay-node.mdx b/packages/audiodocs/docs/effects/delay-node.mdx new file mode 100644 index 000000000..c77309c7d --- /dev/null +++ b/packages/audiodocs/docs/effects/delay-node.mdx @@ -0,0 +1,50 @@ +--- +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 + + + +:::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) + +## Properties + +It inherits all properties from [`AudioNode`](/docs/core/audio-node#properties). + +| 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 + +`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 f273bddb5..2eac36708 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 @@ -20,6 +21,7 @@ sidebar_position: 2 | BiquadFilterNode | ✅ | | ConstantSourceNode | ✅ | | ConvolverNode | ✅ | +| DelayNode | ✅ | | GainNode | ✅ | | IIRFilterNode | ✅ | | OfflineAudioContext | ✅ | @@ -36,7 +38,6 @@ sidebar_position: 2 | AudioWorkletProcessor | ❌ | | ChannelMergerNode | ❌ | | ChannelSplitterNode | ❌ | -| DelayNode | ❌ | | DynamicsCompressorNode | ❌ | | MediaElementAudioSourceNode | ❌ | | MediaStreamAudioDestinationNode | ❌ | 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 529782ee0..8487e859c 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 @@ -47,6 +48,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, createIIRFilter), @@ -184,6 +186,15 @@ 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, delayNodeHostObject->getSizeInBytes()); + return jsiObject; +} + JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createStereoPanner) { auto stereoPanner = context_->createStereoPanner(); auto stereoPannerHostObject = std::make_shared(stereoPanner); 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 c2680bb7a..37daf4e04 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 @@ -43,6 +43,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..92e66d8b2 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.cpp @@ -0,0 +1,27 @@ +#include + +#include +#include +#include +#include + +namespace audioapi { + +DelayNodeHostObject::DelayNodeHostObject(const std::shared_ptr &node) + : AudioNodeHostObject(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 = 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..379c8e5f6 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/DelayNodeHostObject.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include +#include + +namespace audioapi { +using namespace facebook; + +class DelayNode; + +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.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 3673fd81e..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(); @@ -45,6 +46,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_; @@ -64,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/BaseAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp index 2e4723c2a..afebaf2b5 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 @@ -140,6 +141,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 6f40ad7ee..b17304977 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; @@ -69,6 +70,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 createIIRFilter( 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 new file mode 100644 index 000000000..cf2e44c5b --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp @@ -0,0 +1,101 @@ +#include +#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()); + requiresTailProcessing_ = true; + 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(); + } +} + +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) { + 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 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; +} + +} // 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..15ab28f10 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#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; + 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; + bool signalledToStop_ = false; + int remainingFrames_ = 0; +}; + +} // namespace audioapi 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 eae7498e4..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 @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -219,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) { + } 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; 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..8f247607f --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/test/src/DelayTest.cpp @@ -0,0 +1,108 @@ +#include +#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 + } + } +} diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 2e19d51d1..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 | AudioParam { + 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; } 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 8b51daf0e..248cba436 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -30,6 +30,7 @@ import RecorderAdapterNode from './RecorderAdapterNode'; import StereoPannerNode from './StereoPannerNode'; import StreamerNode from './StreamerNode'; import WorkletNode from './WorkletNode'; +import DelayNode from './DelayNode'; import IIRFilterNode from './IIRFilterNode'; import { decodeAudioData, decodePCMInBase64 } from './AudioDecoder'; @@ -206,6 +207,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 342588fe9..d06e5173e 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; createIIRFilter: ( @@ -112,6 +113,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 6e704882e..f2770ef11 100644 --- a/packages/react-native-audio-api/src/web-core/AudioContext.tsx +++ b/packages/react-native-audio-api/src/web-core/AudioContext.tsx @@ -18,6 +18,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'; @@ -66,6 +67,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 bc04f0e91..c9341289f 100644 --- a/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx +++ b/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx @@ -15,6 +15,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; @@ -27,6 +28,7 @@ export default interface BaseAudioContext { createOscillator(): OscillatorNode; createConstantSource(): ConstantSourceNode; createGain(): GainNode; + createDelay(maxDelayTime?: number): DelayNode; createStereoPanner(): StereoPannerNode; createBiquadFilter(): BiquadFilterNode; createIIRFilter(options: IIRFilterNodeOptions): IIRFilterNode; 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 86fbae9f4..54c7f9288 100644 --- a/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx +++ b/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx @@ -22,6 +22,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; @@ -72,6 +73,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()); }