diff --git a/apps/common-app/src/demos/PedalBoard/ReverbPedal.tsx b/apps/common-app/src/demos/PedalBoard/ReverbPedal.tsx index ed58844eb..23ed7a9d2 100644 --- a/apps/common-app/src/demos/PedalBoard/ReverbPedal.tsx +++ b/apps/common-app/src/demos/PedalBoard/ReverbPedal.tsx @@ -47,7 +47,7 @@ export default function ReverbPedal({ } else if (level < 0.66) { desiredDuration = 0.7; } else { - desiredDuration = 1; + desiredDuration = 5; } if (convolverNodeRef.current?.buffer) { if (convolverNodeRef.current.buffer.duration === desiredDuration) { diff --git a/apps/common-app/src/examples/AudioFile/AudioPlayer.ts b/apps/common-app/src/examples/AudioFile/AudioPlayer.ts index 2a0cafa07..805a1a918 100644 --- a/apps/common-app/src/examples/AudioFile/AudioPlayer.ts +++ b/apps/common-app/src/examples/AudioFile/AudioPlayer.ts @@ -50,6 +50,10 @@ class AudioPlayer { }); this.sourceNode.buffer = this.audioBuffer; this.sourceNode.playbackRate.value = this.playbackRate; + const volume1 = this.audioContext.createGain(); + const volume2 = this.audioContext.createGain(); + volume1.gain.value = 0.5; + volume2.gain.value = 0.5; this.volumeNode = this.audioContext.createGain(); this.volumeNode.gain.value = this.volume; diff --git a/apps/common-app/src/examples/ConvolverIR/ConvolverIR.tsx b/apps/common-app/src/examples/ConvolverIR/ConvolverIR.tsx index e59012f42..2f831a4b9 100644 --- a/apps/common-app/src/examples/ConvolverIR/ConvolverIR.tsx +++ b/apps/common-app/src/examples/ConvolverIR/ConvolverIR.tsx @@ -72,7 +72,6 @@ const ConvolverIR: FC = () => { return; } bufferSourceRef.current?.stop(0); - await AudioManager.setAudioSessionActivity(false); setIsPlaying(false); }, []); diff --git a/packages/audiodocs/docs/guides/create-your-own-effect.mdx b/packages/audiodocs/docs/guides/create-your-own-effect.mdx index d40fae945..a6cc3d4fa 100644 --- a/packages/audiodocs/docs/guides/create-your-own-effect.mdx +++ b/packages/audiodocs/docs/guides/create-your-own-effect.mdx @@ -52,9 +52,7 @@ public: explicit MyProcessorNode(const std::shared_ptr &context); protected: - std::shared_ptr - processNode(const std::shared_ptr &buffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; // highlight-start private: @@ -79,14 +77,11 @@ private: namespace audioapi { MyProcessorNode::MyProcessorNode(const std::shared_ptr &context) //highlight-next-line - : AudioNode(context), gain(0.5) { - isInitialized_.store(true, std::memory_order_release); - } + : AudioNode(context), gain(0.5) {} - std::shared_ptr MyProcessorNode::processNode(const std::shared_ptr &buffer, - int framesToProcess) { + void MyProcessorNode::processNode(int framesToProcess) { // highlight-start - for (int channel = 0; channel < buffer->getNumberOfChannels(); ++channel) { + for (int channel = 0; channel < audioBuffer_->getNumberOfChannels(); ++channel) { auto *audioArray = bus->getChannel(channel); for (size_t i = 0; i < framesToProcess; ++i) { // Apply gain to each sample in the audio array diff --git a/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.cpp b/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.cpp index f47c58cf9..a21571872 100644 --- a/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.cpp +++ b/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.cpp @@ -3,13 +3,9 @@ namespace audioapi { MyProcessorNode::MyProcessorNode( const std::shared_ptr &context) - : AudioNode(context) { - isInitialized_.store(true, std::memory_order_release); -} + : AudioNode(context) {} -std::shared_ptr -MyProcessorNode::processNode(const std::shared_ptr &buffer, - int framesToProcess) { +void MyProcessorNode::processNode(int framesToProcess) { // put your processing logic here } } // namespace audioapi diff --git a/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.h b/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.h index 076ea9ab4..063807c0a 100644 --- a/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.h +++ b/packages/custom-node-generator/templates/basic/shared/MyProcessorNode.h @@ -10,8 +10,6 @@ class MyProcessorNode : public AudioNode { explicit MyProcessorNode(const std::shared_ptr &context); protected: - std::shared_ptr - processNode(const std::shared_ptr &buffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp index 14665f8f0..6403032ce 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp @@ -445,7 +445,7 @@ void AndroidAudioRecorder::clearOnAudioReadyCallback() { /// If the recorder is already active, it will initialize the adapter node immediately. /// This method should be called from the JS thread only. /// @param node Shared pointer to the RecorderAdapterNode to connect. -void AndroidAudioRecorder::connect(const std::shared_ptr &node) { +void AndroidAudioRecorder::connect(const std::shared_ptr &node) { std::scoped_lock adapterLock(adapterNodeMutex_); adapterNode_ = node; isConnected_.store(true, std::memory_order_release); diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h index 2e6e6d7a3..fd4224e4d 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h @@ -44,7 +44,7 @@ class AndroidAudioRecorder : public oboe::AudioStreamCallback, public AudioRecor uint64_t callbackId) override; void clearOnAudioReadyCallback() override; - void connect(const std::shared_ptr &node) override; + void connect(const std::shared_ptr &node) override; void disconnect() override; oboe::DataCallbackResult diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index a88c634c7..0a30b150a 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -12,14 +12,11 @@ namespace audioapi { AudioPlayer::AudioPlayer( - const std::function, int)> &renderAudio, + const std::function &renderAudio, float sampleRate, int channelCount) - : renderAudio_(renderAudio), - sampleRate_(sampleRate), - channelCount_(channelCount), - isRunning_(false) { - isInitialized_ = openAudioStream(); + : renderAudio_(renderAudio), sampleRate_(sampleRate), channelCount_(channelCount) { + isInitialized_.store(openAudioStream(), std::memory_order_release); } bool AudioPlayer::openAudioStream() { @@ -86,7 +83,7 @@ void AudioPlayer::suspend() { } void AudioPlayer::cleanup() { - isInitialized_ = false; + isInitialized_.store(false, std::memory_order_release); if (mStream_ != nullptr) { mStream_->close(); @@ -101,7 +98,7 @@ bool AudioPlayer::isRunning() const { DataCallbackResult AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) { - if (!isInitialized_) { + if (!isInitialized_.load(std::memory_order_acquire)) { return DataCallbackResult::Continue; } @@ -112,7 +109,7 @@ AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numF auto framesToProcess = std::min(numFrames - processedFrames, RENDER_QUANTUM_SIZE); if (isRunning_.load(std::memory_order_acquire)) { - renderAudio_(buffer_, framesToProcess); + renderAudio_(buffer_.get(), framesToProcess); } else { buffer_->zero(); } @@ -130,7 +127,7 @@ void AudioPlayer::onErrorAfterClose(oboe::AudioStream *stream, oboe::Result erro if (error == oboe::Result::ErrorDisconnected) { cleanup(); if (openAudioStream()) { - isInitialized_ = true; + isInitialized_.store(true, std::memory_order_release); resume(); } } diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 878ce7b14..b3c951a21 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -17,7 +17,7 @@ class AudioContext; class AudioPlayer : public AudioStreamDataCallback, AudioStreamErrorCallback { public: AudioPlayer( - const std::function, int)> &renderAudio, + const std::function &renderAudio, float sampleRate, int channelCount); @@ -40,13 +40,13 @@ class AudioPlayer : public AudioStreamDataCallback, AudioStreamErrorCallback { void onErrorAfterClose(AudioStream * /* audioStream */, Result /* error */) override; private: - std::function, int)> renderAudio_; + std::function renderAudio_; std::shared_ptr mStream_; std::shared_ptr buffer_; - bool isInitialized_ = false; float sampleRate_; int channelCount_; - std::atomic isRunning_; + std::atomic isInitialized_{false}; + std::atomic isRunning_{false}; bool openAudioStream(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp index c76067bb7..91dbb34a5 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioContextHostObject.cpp @@ -23,6 +23,7 @@ AudioContextHostObject::AudioContextHostObject( } JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, close) { + context_->getGraph()->collectDisposedNodes(); auto audioContext = std::static_pointer_cast(context_); auto promise = promiseVendor_->createAsyncPromise([audioContext = std::move(audioContext)]() { return [audioContext](jsi::Runtime &runtime) { @@ -35,6 +36,7 @@ JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, close) { } JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, resume) { + context_->getGraph()->collectDisposedNodes(); auto audioContext = std::static_pointer_cast(context_); auto promise = promiseVendor_->createAsyncPromise([audioContext = std::move(audioContext)]() { auto result = audioContext->resume(); @@ -46,6 +48,7 @@ JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, resume) { } JSI_HOST_FUNCTION_IMPL(AudioContextHostObject, suspend) { + context_->getGraph()->collectDisposedNodes(); auto audioContext = std::static_pointer_cast(context_); auto promise = promiseVendor_->createAsyncPromise([audioContext = std::move(audioContext)]() { auto result = audioContext->suspend(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.cpp index c00abef23..24b5a7d38 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.cpp @@ -1,16 +1,22 @@ #include #include +#include +#include #include #include +#include +#include #include +#include namespace audioapi { AudioNodeHostObject::AudioNodeHostObject( - const std::shared_ptr &node, + const std::shared_ptr &graph, + std::unique_ptr node, const AudioNodeOptions &options) - : node_(node), + : utils::graph::HostNode(graph, std::move(node)), numberOfInputs_(options.numberOfInputs), numberOfOutputs_(options.numberOfOutputs), channelCount_(options.channelCount), @@ -58,32 +64,51 @@ JSI_PROPERTY_GETTER_IMPL(AudioNodeHostObject, channelInterpretation) { JSI_HOST_FUNCTION_IMPL(AudioNodeHostObject, connect) { auto obj = args[0].getObject(runtime); + + // delay node is being connected to something, use delay reader + if (auto *fromNode = dynamic_cast(this); fromNode != nullptr) { + auto delayReader = fromNode->delayReaderHostNode_; + auto toNode = obj.getHostObject(runtime); + delayReader->connect(*toNode); + return jsi::Value::undefined(); + } + + // something is being connected to a delay node, use delay writer + if (auto *toNode = dynamic_cast( + obj.getHostObject(runtime).get()); + toNode != nullptr) { + auto delayWriter = toNode->delayWriterHostNode_; + connect(*delayWriter); + return jsi::Value::undefined(); + } + if (obj.isHostObject(runtime)) { auto node = obj.getHostObject(runtime); - node_->connect(std::shared_ptr(node)->node_); - } - if (obj.isHostObject(runtime)) { + connect(*node); + } else if (obj.isHostObject(runtime)) { auto param = obj.getHostObject(runtime); - node_->connect(std::shared_ptr(param)->param_); + param->connectToGraph(); + graph_->addEdge(node_, param->bridgeNode()); } return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioNodeHostObject, disconnect) { if (args[0].isUndefined()) { - node_->disconnect(); + disconnect(); return jsi::Value::undefined(); } + auto obj = args[0].getObject(runtime); if (obj.isHostObject(runtime)) { auto node = obj.getHostObject(runtime); - node_->disconnect(std::shared_ptr(node)->node_); - } - - if (obj.isHostObject(runtime)) { + disconnect(*node); + } else if (obj.isHostObject(runtime)) { auto param = obj.getHostObject(runtime); - node_->disconnect(std::shared_ptr(param)->param_); + // Disconnect source → bridge + graph_->removeEdge(node_, param->bridgeNode()); } + return jsi::Value::undefined(); } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.h index 5b8c12d3c..998e7a894 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioNodeHostObject.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -13,10 +15,11 @@ using namespace facebook; class AudioNode; -class AudioNodeHostObject : public JsiHostObject { +class AudioNodeHostObject : public JsiHostObject, public utils::graph::HostNode { public: explicit AudioNodeHostObject( - const std::shared_ptr &node, + const std::shared_ptr &graph, + std::unique_ptr node, const AudioNodeOptions &options = AudioNodeOptions()); ~AudioNodeHostObject() override; @@ -26,12 +29,17 @@ class AudioNodeHostObject : public JsiHostObject { JSI_PROPERTY_GETTER_DECL(channelCountMode); JSI_PROPERTY_GETTER_DECL(channelInterpretation); + using utils::graph::HostNode::connect; + using utils::graph::HostNode::disconnect; + JSI_HOST_FUNCTION_DECL(connect); JSI_HOST_FUNCTION_DECL(disconnect); - protected: - std::shared_ptr node_; + virtual size_t getMemoryPressure() { + return 350'000; + } + protected: const int numberOfInputs_; const int numberOfOutputs_; size_t channelCount_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp index 96b46e3cf..322780923 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -9,8 +10,13 @@ namespace audioapi { -AudioParamHostObject::AudioParamHostObject(const std::shared_ptr ¶m) - : param_(param), +AudioParamHostObject::AudioParamHostObject( + std::shared_ptr graph, + HNode *ownerNode, + const std::shared_ptr ¶m) + : graph_(std::move(graph)), + ownerNode_(ownerNode), + param_(param), defaultValue_(param->getDefaultValue()), minValue_(param->getMinValue()), maxValue_(param->getMaxValue()) { @@ -33,6 +39,15 @@ AudioParamHostObject::AudioParamHostObject(const std::shared_ptr &pa addSetters(JSI_EXPORT_PROPERTY_SETTER(AudioParamHostObject, value)); } +AudioParamHostObject::~AudioParamHostObject() { + if (graph_ && bridgeNode_ != nullptr) { + // Remove the bridge node itself + (void)graph_->removeNode(bridgeNode_); + bridgeNode_ = nullptr; + ownerNode_ = nullptr; + } +} + JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, value) { return {param_->getValue()}; } @@ -159,6 +174,18 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelAndHoldAtTime) { return jsi::Value::undefined(); } +void AudioParamHostObject::connectToGraph() { + if (isConnectedToGraph_ || graph_ == nullptr) { + return; + } + + auto bridgeGraphObject = std::make_unique(param_.get()); + bridgeNode_ = graph_->addNode(std::move(bridgeGraphObject)); + // Connect bridge → owner so topological sort orders correctly + (void)graph_->addEdge(bridgeNode_, ownerNode_); + isConnectedToGraph_ = true; +} + JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, checkCurveExclusion) { auto checkExclusionResult = checkCurveExclusionFromJSI(runtime, args); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h index 3b1d54fab..eb5083ca8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,9 +14,27 @@ using namespace facebook; class AudioParam; +/// @brief Host object for AudioParam that owns its BridgeNode. +/// +/// The BridgeNode is created lazily on the first connectToGraph() call +/// and connected to the owner node (bridge → owner). Sources connecting +/// to this param connect to the bridge node (source → bridge). +/// +/// When destroyed, the BridgeNode is removed from the graph. class AudioParamHostObject : public JsiHostObject { public: - explicit AudioParamHostObject(const std::shared_ptr ¶m); + using HNode = utils::graph::HostGraph::Node; + + /// @brief Creates an AudioParamHostObject with its BridgeNode. + /// @param graph The audio graph + /// @param ownerNode The HNode* of the AudioNode that owns this param + /// @param param The AudioParam this host object represents + explicit AudioParamHostObject( + std::shared_ptr graph, + HNode *ownerNode, + const std::shared_ptr ¶m); + + ~AudioParamHostObject() override; JSI_PROPERTY_GETTER_DECL(value); JSI_PROPERTY_GETTER_DECL(defaultValue); @@ -33,9 +52,20 @@ class AudioParamHostObject : public JsiHostObject { JSI_HOST_FUNCTION_DECL(cancelAndHoldAtTime); JSI_HOST_FUNCTION_DECL(checkCurveExclusion); + /// @brief Returns the bridge node for this param (for source → bridge connections). + [[nodiscard]] HNode *bridgeNode() const { + return bridgeNode_; + } + + void connectToGraph(); + private: friend class AudioNodeHostObject; + std::shared_ptr graph_; + HNode *ownerNode_ = nullptr; + HNode *bridgeNode_ = nullptr; + bool isConnectedToGraph_ = false; std::shared_ptr param_; ParamControlQueue controlQueue_; float defaultValue_; 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 14d999109..4bd372135 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 @@ -37,8 +37,7 @@ BaseAudioContextHostObject::BaseAudioContextHostObject( : context_(context), promiseVendor_(std::make_shared(runtime, callInvoker)), callInvoker_(callInvoker) { - context_->initialize(); - destination_ = std::make_shared(context_->getDestination()); + destination_ = std::make_shared(context_); addGetters( JSI_EXPORT_PROPERTY_GETTER(BaseAudioContextHostObject, destination), @@ -95,19 +94,13 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWorkletSourceNode) { #if RN_AUDIO_API_ENABLE_WORKLETS auto shareableWorklet = worklets::extractSerializableOrThrow(runtime, args[0]); - std::weak_ptr workletRuntime; auto shouldUseUiRuntime = args[1].getBool(); - auto shouldLockRuntime = shouldUseUiRuntime; - if (shouldUseUiRuntime) { - workletRuntime = context_->getRuntimeRegistry().uiRuntime; - } else { - workletRuntime = context_->getRuntimeRegistry().audioRuntime; - } + std::weak_ptr workletRuntime = shouldUseUiRuntime + ? context_->getRuntimeRegistry().uiRuntime + : context_->getRuntimeRegistry().audioRuntime; - auto workletSourceNode = - context_->createWorkletSourceNode(shareableWorklet, workletRuntime, shouldLockRuntime); - auto workletSourceNodeHostObject = - std::make_shared(workletSourceNode); + auto workletSourceNodeHostObject = std::make_shared( + context_->getGraph(), context_, workletRuntime, shareableWorklet, shouldUseUiRuntime); return jsi::Object::createFromHostObject(runtime, workletSourceNodeHostObject); #endif return jsi::Value::undefined(); @@ -117,21 +110,21 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWorkletNode) { #if RN_AUDIO_API_ENABLE_WORKLETS auto shareableWorklet = worklets::extractSerializableOrThrow(runtime, args[0]); - - std::weak_ptr workletRuntime; auto shouldUseUiRuntime = args[1].getBool(); - auto shouldLockRuntime = shouldUseUiRuntime; - if (shouldUseUiRuntime) { - workletRuntime = context_->getRuntimeRegistry().uiRuntime; - } else { - workletRuntime = context_->getRuntimeRegistry().audioRuntime; - } + std::weak_ptr workletRuntime = shouldUseUiRuntime + ? context_->getRuntimeRegistry().uiRuntime + : context_->getRuntimeRegistry().audioRuntime; auto bufferLength = static_cast(args[2].getNumber()); auto inputChannelCount = static_cast(args[3].getNumber()); - auto workletNode = context_->createWorkletNode( - shareableWorklet, workletRuntime, bufferLength, inputChannelCount, shouldLockRuntime); - auto workletNodeHostObject = std::make_shared(workletNode); + auto workletNodeHostObject = std::make_shared( + context_->getGraph(), + context_, + workletRuntime, + shareableWorklet, + shouldUseUiRuntime, + bufferLength, + inputChannelCount); auto jsiObject = jsi::Object::createFromHostObject(runtime, workletNodeHostObject); jsiObject.setExternalMemoryPressure( runtime, @@ -145,28 +138,20 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWorkletProcessingNode) #if RN_AUDIO_API_ENABLE_WORKLETS auto shareableWorklet = worklets::extractSerializableOrThrow(runtime, args[0]); - - std::weak_ptr workletRuntime; auto shouldUseUiRuntime = args[1].getBool(); - auto shouldLockRuntime = shouldUseUiRuntime; - if (shouldUseUiRuntime) { - workletRuntime = context_->getRuntimeRegistry().uiRuntime; - } else { - workletRuntime = context_->getRuntimeRegistry().audioRuntime; - } + std::weak_ptr workletRuntime = shouldUseUiRuntime + ? context_->getRuntimeRegistry().uiRuntime + : context_->getRuntimeRegistry().audioRuntime; - auto workletProcessingNode = - context_->createWorkletProcessingNode(shareableWorklet, workletRuntime, shouldLockRuntime); - auto workletProcessingNodeHostObject = - std::make_shared(workletProcessingNode); + auto workletProcessingNodeHostObject = std::make_shared( + context_->getGraph(), context_, workletRuntime, shareableWorklet, shouldUseUiRuntime); return jsi::Object::createFromHostObject(runtime, workletProcessingNodeHostObject); #endif return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createRecorderAdapter) { - auto recorderAdapter = context_->createRecorderAdapter(); - auto recorderAdapterHostObject = std::make_shared(recorderAdapter); + auto recorderAdapterHostObject = std::make_shared(context_); return jsi::Object::createFromHostObject(runtime, recorderAdapterHostObject); } @@ -175,7 +160,9 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createOscillator) { const auto oscillatorOptions = audioapi::option_parser::parseOscillatorOptions(runtime, options); auto oscillatorHostObject = std::make_shared(context_, oscillatorOptions); - return jsi::Object::createFromHostObject(runtime, oscillatorHostObject); + auto object = jsi::Object::createFromHostObject(runtime, oscillatorHostObject); + object.setExternalMemoryPressure(runtime, oscillatorHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createStreamer) { @@ -200,14 +187,18 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createConstantSource) { audioapi::option_parser::parseConstantSourceOptions(runtime, options); auto constantSourceHostObject = std::make_shared(context_, constantSourceOptions); - return jsi::Object::createFromHostObject(runtime, constantSourceHostObject); + auto object = jsi::Object::createFromHostObject(runtime, constantSourceHostObject); + object.setExternalMemoryPressure(runtime, constantSourceHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createGain) { const auto options = args[0].asObject(runtime); const auto gainOptions = audioapi::option_parser::parseGainOptions(runtime, options); auto gainHostObject = std::make_shared(context_, gainOptions); - return jsi::Object::createFromHostObject(runtime, gainHostObject); + auto object = jsi::Object::createFromHostObject(runtime, gainHostObject); + object.setExternalMemoryPressure(runtime, gainHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createDelay) { @@ -215,7 +206,7 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createDelay) { const auto delayOptions = audioapi::option_parser::parseDelayOptions(runtime, options); auto delayNodeHostObject = std::make_shared(context_, delayOptions); auto jsiObject = jsi::Object::createFromHostObject(runtime, delayNodeHostObject); - jsiObject.setExternalMemoryPressure(runtime, delayNodeHostObject->getSizeInBytes()); + jsiObject.setExternalMemoryPressure(runtime, delayNodeHostObject->getMemoryPressure()); return jsiObject; } @@ -225,7 +216,9 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createStereoPanner) { audioapi::option_parser::parseStereoPannerOptions(runtime, options); auto stereoPannerHostObject = std::make_shared(context_, stereoPannerOptions); - return jsi::Object::createFromHostObject(runtime, stereoPannerHostObject); + auto object = jsi::Object::createFromHostObject(runtime, stereoPannerHostObject); + object.setExternalMemoryPressure(runtime, stereoPannerHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createBiquadFilter) { @@ -234,14 +227,18 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createBiquadFilter) { audioapi::option_parser::parseBiquadFilterOptions(runtime, options); auto biquadFilterHostObject = std::make_shared(context_, biquadFilterOptions); - return jsi::Object::createFromHostObject(runtime, biquadFilterHostObject); + auto object = jsi::Object::createFromHostObject(runtime, biquadFilterHostObject); + object.setExternalMemoryPressure(runtime, biquadFilterHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createIIRFilter) { const auto options = args[0].asObject(runtime); const auto iirFilterOptions = audioapi::option_parser::parseIIRFilterOptions(runtime, options); auto iirFilterHostObject = std::make_shared(context_, iirFilterOptions); - return jsi::Object::createFromHostObject(runtime, iirFilterHostObject); + auto object = jsi::Object::createFromHostObject(runtime, iirFilterHostObject); + object.setExternalMemoryPressure(runtime, iirFilterHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createBufferSource) { @@ -262,7 +259,9 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createFileSource) { #endif // RN_AUDIO_API_FFMPEG_DISABLED const auto fileSourceHostObject = std::make_shared(context_, opts); - return jsi::Object::createFromHostObject(runtime, fileSourceHostObject); + auto object = jsi::Object::createFromHostObject(runtime, fileSourceHostObject); + object.setExternalMemoryPressure(runtime, fileSourceHostObject->getMemoryPressure()); + return object; }; const auto options = args[0].asObject(runtime); @@ -278,7 +277,9 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createBufferQueueSource) { audioapi::option_parser::parseBaseAudioBufferSourceOptions(runtime, options); auto bufferStreamSourceHostObject = std::make_shared( context_, baseAudioBufferSourceOptions); - return jsi::Object::createFromHostObject(runtime, bufferStreamSourceHostObject); + auto object = jsi::Object::createFromHostObject(runtime, bufferStreamSourceHostObject); + object.setExternalMemoryPressure(runtime, bufferStreamSourceHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createPeriodicWave) { @@ -309,7 +310,9 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createAnalyser) { const auto options = args[0].asObject(runtime); const auto analyserOptions = audioapi::option_parser::parseAnalyserOptions(runtime, options); auto analyserHostObject = std::make_shared(context_, analyserOptions); - return jsi::Object::createFromHostObject(runtime, analyserHostObject); + auto object = jsi::Object::createFromHostObject(runtime, analyserHostObject); + object.setExternalMemoryPressure(runtime, analyserHostObject->getMemoryPressure()); + return object; } JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createConvolver) { @@ -323,6 +326,7 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createConvolver) { .asHostObject(runtime); jsiObject.setExternalMemoryPressure(runtime, bufferHostObject->getSizeInBytes()); } + jsiObject.setExternalMemoryPressure(runtime, convolverHostObject->getMemoryPressure()); return jsiObject; } @@ -331,6 +335,8 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createWaveShaper) { const auto waveShaperOptions = audioapi::option_parser::parseWaveShaperOptions(runtime, options); auto waveShaperHostObject = std::make_shared(context_, waveShaperOptions); - return jsi::Object::createFromHostObject(runtime, waveShaperHostObject); + auto object = jsi::Object::createFromHostObject(runtime, waveShaperHostObject); + object.setExternalMemoryPressure(runtime, waveShaperHostObject->getMemoryPressure()); + return object; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp index ee9fee5ef..189f88f93 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/OfflineAudioContextHostObject.cpp @@ -31,6 +31,7 @@ OfflineAudioContextHostObject::OfflineAudioContextHostObject( } JSI_HOST_FUNCTION_IMPL(OfflineAudioContextHostObject, resume) { + context_->getGraph()->collectDisposedNodes(); auto audioContext = std::static_pointer_cast(context_); auto promise = promiseVendor_->createAsyncPromise([audioContext]() { audioContext->resume(); @@ -43,6 +44,7 @@ JSI_HOST_FUNCTION_IMPL(OfflineAudioContextHostObject, resume) { } JSI_HOST_FUNCTION_IMPL(OfflineAudioContextHostObject, suspend) { + context_->getGraph()->collectDisposedNodes(); double when = args[0].getNumber(); auto audioContext = std::static_pointer_cast(context_); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/analysis/AnalyserNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/analysis/AnalyserNodeHostObject.cpp index 2ba7ce41d..9740f3692 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/analysis/AnalyserNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/analysis/AnalyserNodeHostObject.cpp @@ -11,7 +11,10 @@ namespace audioapi { AnalyserNodeHostObject::AnalyserNodeHostObject( const std::shared_ptr &context, const AnalyserOptions &options) - : AudioNodeHostObject(context->createAnalyser(options), options) { + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { addGetters( JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, fftSize), JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, minDecibels), @@ -32,48 +35,43 @@ AnalyserNodeHostObject::AnalyserNodeHostObject( } JSI_PROPERTY_GETTER_IMPL(AnalyserNodeHostObject, fftSize) { - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); return {analyserNode->getFFTSize()}; } JSI_PROPERTY_GETTER_IMPL(AnalyserNodeHostObject, minDecibels) { - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); return {analyserNode->getMinDecibels()}; } JSI_PROPERTY_GETTER_IMPL(AnalyserNodeHostObject, maxDecibels) { - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); return {analyserNode->getMaxDecibels()}; } JSI_PROPERTY_GETTER_IMPL(AnalyserNodeHostObject, smoothingTimeConstant) { - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); return {analyserNode->getSmoothingTimeConstant()}; } JSI_PROPERTY_SETTER_IMPL(AnalyserNodeHostObject, fftSize) { - auto analyserNode = std::static_pointer_cast(node_); - - auto fftSize = static_cast(value.getNumber()); - analyserNode->setFFTSize(fftSize); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); + analyserNode->setFFTSize(static_cast(value.getNumber())); } JSI_PROPERTY_SETTER_IMPL(AnalyserNodeHostObject, minDecibels) { - auto analyserNode = std::static_pointer_cast(node_); - auto minDecibels = static_cast(value.getNumber()); - analyserNode->setMinDecibels(minDecibels); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); + analyserNode->setMinDecibels(static_cast(value.getNumber())); } JSI_PROPERTY_SETTER_IMPL(AnalyserNodeHostObject, maxDecibels) { - auto analyserNode = std::static_pointer_cast(node_); - auto maxDecibels = static_cast(value.getNumber()); - analyserNode->setMaxDecibels(maxDecibels); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); + analyserNode->setMaxDecibels(static_cast(value.getNumber())); } JSI_PROPERTY_SETTER_IMPL(AnalyserNodeHostObject, smoothingTimeConstant) { - auto analyserNode = std::static_pointer_cast(node_); - auto smoothingTimeConstant = static_cast(value.getNumber()); - analyserNode->setSmoothingTimeConstant(smoothingTimeConstant); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); + analyserNode->setSmoothingTimeConstant(static_cast(value.getNumber())); } JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getFloatFrequencyData) { @@ -82,7 +80,7 @@ JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getFloatFrequencyData) { auto data = reinterpret_cast(arrayBuffer.data(runtime)); auto length = static_cast(arrayBuffer.size(runtime) / sizeof(float)); - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); analyserNode->getFloatFrequencyData(data, length); return jsi::Value::undefined(); @@ -94,7 +92,7 @@ JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) { auto data = arrayBuffer.data(runtime); auto length = static_cast(arrayBuffer.size(runtime)); - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); analyserNode->getByteFrequencyData(data, length); return jsi::Value::undefined(); @@ -106,7 +104,7 @@ JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getFloatTimeDomainData) { auto data = reinterpret_cast(arrayBuffer.data(runtime)); auto length = static_cast(arrayBuffer.size(runtime) / sizeof(float)); - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); analyserNode->getFloatTimeDomainData(data, length); return jsi::Value::undefined(); @@ -118,7 +116,7 @@ JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteTimeDomainData) { auto data = arrayBuffer.data(runtime); auto length = static_cast(arrayBuffer.size(runtime)); - auto analyserNode = std::static_pointer_cast(node_); + auto analyserNode = static_cast(node_->handle->audioNode->asAudioNode()); analyserNode->getByteTimeDomainData(data, length); return jsi::Value::undefined(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/destinations/AudioDestinationNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/destinations/AudioDestinationNodeHostObject.h index 05fa272b9..e89cb1082 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/destinations/AudioDestinationNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/destinations/AudioDestinationNodeHostObject.h @@ -11,7 +11,15 @@ using namespace facebook; class AudioDestinationNodeHostObject : public AudioNodeHostObject { public: - explicit AudioDestinationNodeHostObject(const std::shared_ptr &node) - : AudioNodeHostObject(node, AudioDestinationOptions()) {} + explicit AudioDestinationNodeHostObject( + const std::shared_ptr &context, + const AudioDestinationOptions &options = AudioDestinationOptions()) + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context), + options) { + context->initialize(static_cast(node_->handle->audioNode.get())); + } }; + } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/BiquadFilterNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/BiquadFilterNodeHostObject.cpp index dd555eece..0e1c2d6a7 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/BiquadFilterNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/BiquadFilterNodeHostObject.cpp @@ -14,12 +14,19 @@ namespace audioapi { BiquadFilterNodeHostObject::BiquadFilterNodeHostObject( const std::shared_ptr &context, const BiquadFilterOptions &options) - : AudioNodeHostObject(context->createBiquadFilter(options), options), type_(options.type) { - auto biquadFilterNode = std::static_pointer_cast(node_); - frequencyParam_ = std::make_shared(biquadFilterNode->getFrequencyParam()); - detuneParam_ = std::make_shared(biquadFilterNode->getDetuneParam()); - QParam_ = std::make_shared(biquadFilterNode->getQParam()); - gainParam_ = std::make_shared(biquadFilterNode->getGainParam()); + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options), + type_(options.type) { + auto biquadFilterNode = static_cast(node_->handle->audioNode->asAudioNode()); + frequencyParam_ = + std::make_shared(graph_, node_, biquadFilterNode->getFrequencyParam()); + detuneParam_ = + std::make_shared(graph_, node_, biquadFilterNode->getDetuneParam()); + QParam_ = std::make_shared(graph_, node_, biquadFilterNode->getQParam()); + gainParam_ = + std::make_shared(graph_, node_, biquadFilterNode->getGainParam()); addGetters( JSI_EXPORT_PROPERTY_GETTER(BiquadFilterNodeHostObject, frequency), @@ -54,11 +61,12 @@ JSI_PROPERTY_GETTER_IMPL(BiquadFilterNodeHostObject, type) { } JSI_PROPERTY_SETTER_IMPL(BiquadFilterNodeHostObject, type) { - auto biquadFilterNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto biquadFilterNode = static_cast(handle->audioNode->asAudioNode()); auto type = js_enum_parser::filterTypeFromString(value.asString(runtime).utf8(runtime)); - auto event = [biquadFilterNode, type](BaseAudioContext &) { - biquadFilterNode->setType(type); + auto event = [handle, type](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setType(type); }; biquadFilterNode->scheduleAudioEvent(std::move(event)); type_ = type; @@ -78,7 +86,7 @@ JSI_HOST_FUNCTION_IMPL(BiquadFilterNodeHostObject, getFrequencyResponse) { args[2].getObject(runtime).getPropertyAsObject(runtime, "buffer").getArrayBuffer(runtime); auto phaseResponseOut = reinterpret_cast(arrayBufferPhase.data(runtime)); - auto biquadFilterNode = std::static_pointer_cast(node_); + auto biquadFilterNode = static_cast(node_->handle->audioNode->asAudioNode()); biquadFilterNode->getFrequencyResponse( frequencyArray, magResponseOut, phaseResponseOut, length, type_); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ConvolverNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ConvolverNodeHostObject.cpp index 0340c4513..52d4e124f 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ConvolverNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ConvolverNodeHostObject.cpp @@ -18,7 +18,10 @@ namespace audioapi { ConvolverNodeHostObject::ConvolverNodeHostObject( const std::shared_ptr &context, const ConvolverOptions &options) - : AudioNodeHostObject(context->createConvolver(options), options), + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options), normalize_(!options.disableNormalization) { if (options.buffer != nullptr) { setBuffer(options.buffer); @@ -30,7 +33,7 @@ ConvolverNodeHostObject::ConvolverNodeHostObject( } JSI_PROPERTY_GETTER_IMPL(ConvolverNodeHostObject, normalize) { - return jsi::Value(normalize_); + return {normalize_}; } JSI_PROPERTY_SETTER_IMPL(ConvolverNodeHostObject, normalize) { @@ -57,7 +60,8 @@ void ConvolverNodeHostObject::setBuffer(const std::shared_ptr &buff return; } - auto convolverNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *convolverNode = static_cast(handle->audioNode->asAudioNode()); auto copiedBuffer = std::make_shared(*buffer); @@ -102,7 +106,8 @@ void ConvolverNodeHostObject::setBuffer(const std::shared_ptr &buff .intermediateBuffer = intermediateBuffer, .scaleFactor = scaleFactor}); - auto event = [convolverNode, setupData](BaseAudioContext &) { + auto event = [handle, setupData](BaseAudioContext &) { + auto *convolverNode = static_cast(handle->audioNode->asAudioNode()); convolverNode->setBuffer( setupData->buffer, std::move(setupData->convolvers), 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 49220a93e..ad8ac5587 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 @@ -2,18 +2,47 @@ #include #include #include +#include +#include +#include #include #include +#include namespace audioapi { DelayNodeHostObject::DelayNodeHostObject( const std::shared_ptr &context, const DelayOptions &options) - : AudioNodeHostObject(context->createDelay(options), options) { - auto delayNode = std::static_pointer_cast(node_); - delayTimeParam_ = std::make_shared(delayNode->getDelayTimeParam()); + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { + auto *delayNode = static_cast(node_->handle->audioNode->asAudioNode()); + delayTimeParam_ = + std::make_shared(graph_, node_, delayNode->getDelayTimeParam()); + + auto delayBuffer = std::make_shared( + static_cast(options.maxDelayTime * context->getSampleRate() + 1), + channelCount_, + context->getSampleRate()); + + // order has to be preserved because adding cycle would not change their order in the graph + delayReaderHostNode_ = + std::make_shared(graph_, std::move(delayNode->delayReader_)); + delayWriterHostNode_ = + std::make_shared(graph_, std::move(delayNode->delayWriter_)); + + // Reader and writer communicate via the delay line (a ring buffer), not via + // an audio edge. Linking their processable state ensures that when the + // reader becomes processable (because something downstream pulls from it), + // the writer — and everything feeding the writer — is also marked + // processable. The same link carries the transition back to NOT_PROCESSABLE + // on disconnect. + audioapi::utils::graph::Graph::linkNodes( + delayReaderHostNode_->rawNode(), delayWriterHostNode_->rawNode()); + addGetters(JSI_EXPORT_PROPERTY_GETTER(DelayNodeHostObject, delayTime)); } @@ -22,9 +51,9 @@ JSI_PROPERTY_GETTER_IMPL(DelayNodeHostObject, delayTime) { } size_t DelayNodeHostObject::getSizeInBytes() const { - auto delayNode = std::static_pointer_cast(node_); + auto *delayNode = static_cast(node_->handle->audioNode->asAudioNode()); auto base = sizeof(float) * delayNode->getDelayTimeParam()->getMaxValue(); - return base * delayNode->getContextSampleRate(); + return static_cast(base * delayNode->getContextSampleRate()); } } // 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 index df72a99bf..2b44dc123 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 @@ -10,6 +10,9 @@ using namespace facebook; struct DelayOptions; class BaseAudioContext; class AudioParamHostObject; +class DelayLine; +class DelayWriterHostNode; +class DelayReaderHostNode; class DelayNodeHostObject : public AudioNodeHostObject { public: @@ -20,8 +23,11 @@ class DelayNodeHostObject : public AudioNodeHostObject { [[nodiscard]] size_t getSizeInBytes() const; JSI_PROPERTY_GETTER_DECL(delayTime); + std::shared_ptr delayWriterHostNode_; + std::shared_ptr delayReaderHostNode_; private: std::shared_ptr delayTimeParam_; + std::shared_ptr delayLine_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/GainNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/GainNodeHostObject.cpp index ecacc7dc5..e99133738 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/GainNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/GainNodeHostObject.cpp @@ -11,9 +11,12 @@ namespace audioapi { GainNodeHostObject::GainNodeHostObject( const std::shared_ptr &context, const GainOptions &options) - : AudioNodeHostObject(context->createGain(options), options) { - auto gainNode = std::static_pointer_cast(node_); - gainParam_ = std::make_shared(gainNode->getGainParam()); + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { + auto gainNode = static_cast(node_->handle->audioNode->asAudioNode()); + gainParam_ = std::make_shared(graph_, node_, gainNode->getGainParam()); addGetters(JSI_EXPORT_PROPERTY_GETTER(GainNodeHostObject, gain)); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/IIRFilterNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/IIRFilterNodeHostObject.cpp index 4299665fa..7d6923b7c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/IIRFilterNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/IIRFilterNodeHostObject.cpp @@ -9,7 +9,10 @@ namespace audioapi { IIRFilterNodeHostObject::IIRFilterNodeHostObject( const std::shared_ptr &context, const IIRFilterOptions &options) - : AudioNodeHostObject(context->createIIRFilter(options), options) { + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { addFunctions(JSI_EXPORT_FUNCTION(IIRFilterNodeHostObject, getFrequencyResponse)); } @@ -28,7 +31,7 @@ JSI_HOST_FUNCTION_IMPL(IIRFilterNodeHostObject, getFrequencyResponse) { args[2].getObject(runtime).getPropertyAsObject(runtime, "buffer").getArrayBuffer(runtime); auto phaseResponseOut = reinterpret_cast(arrayBufferPhase.data(runtime)); - auto iirFilterNode = std::static_pointer_cast(node_); + auto iirFilterNode = static_cast(node_->handle->audioNode->asAudioNode()); iirFilterNode->getFrequencyResponse(frequencyArray, magResponseOut, phaseResponseOut, length); return jsi::Value::undefined(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/StereoPannerNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/StereoPannerNodeHostObject.cpp index f9e50b9cd..45a666779 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/StereoPannerNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/StereoPannerNodeHostObject.cpp @@ -11,9 +11,13 @@ namespace audioapi { StereoPannerNodeHostObject::StereoPannerNodeHostObject( const std::shared_ptr &context, const StereoPannerOptions &options) - : AudioNodeHostObject(context->createStereoPanner(options), options) { - auto stereoPannerNode = std::static_pointer_cast(node_); - panParam_ = std::make_shared(stereoPannerNode->getPanParam()); + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { + auto stereoPannerNode = static_cast(node_->handle->audioNode->asAudioNode()); + panParam_ = + std::make_shared(graph_, node_, stereoPannerNode->getPanParam()); addGetters(JSI_EXPORT_PROPERTY_GETTER(StereoPannerNodeHostObject, pan)); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WaveShaperNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WaveShaperNodeHostObject.cpp index 27d26d7bc..f61381c8d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WaveShaperNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WaveShaperNodeHostObject.cpp @@ -12,7 +12,10 @@ namespace audioapi { WaveShaperNodeHostObject::WaveShaperNodeHostObject( const std::shared_ptr &context, const WaveShaperOptions &options) - : AudioNodeHostObject(context->createWaveShaper(options), options), + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options), oversample_(options.oversample) { addGetters(JSI_EXPORT_PROPERTY_GETTER(WaveShaperNodeHostObject, oversample)); addSetters(JSI_EXPORT_PROPERTY_SETTER(WaveShaperNodeHostObject, oversample)); @@ -24,18 +27,20 @@ JSI_PROPERTY_GETTER_IMPL(WaveShaperNodeHostObject, oversample) { } JSI_PROPERTY_SETTER_IMPL(WaveShaperNodeHostObject, oversample) { - auto waveShaperNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto waveShaperNode = static_cast(handle->audioNode->asAudioNode()); auto oversample = js_enum_parser::overSampleTypeFromString(value.asString(runtime).utf8(runtime)); - auto event = [waveShaperNode, oversample](BaseAudioContext &) { - waveShaperNode->setOversample(oversample); + auto event = [handle, oversample](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setOversample(oversample); }; waveShaperNode->scheduleAudioEvent(std::move(event)); oversample_ = oversample; } JSI_HOST_FUNCTION_IMPL(WaveShaperNodeHostObject, setCurve) { - auto waveShaperNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto waveShaperNode = static_cast(handle->audioNode->asAudioNode()); std::shared_ptr curve = nullptr; @@ -50,8 +55,8 @@ JSI_HOST_FUNCTION_IMPL(WaveShaperNodeHostObject, setCurve) { std::make_shared(reinterpret_cast(arrayBuffer.data(runtime)), size); } - auto event = [waveShaperNode, curve](BaseAudioContext &) { - waveShaperNode->setCurve(curve); + auto event = [handle, curve](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setCurve(curve); }; waveShaperNode->scheduleAudioEvent(std::move(event)); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h index 1ee3e2e4e..e3d16ef1a 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletNodeHostObject.h @@ -2,6 +2,7 @@ #include #include +#include #include @@ -10,7 +11,20 @@ using namespace facebook; class WorkletNodeHostObject : public AudioNodeHostObject { public: - explicit WorkletNodeHostObject(const std::shared_ptr &node) - : AudioNodeHostObject(node) {} + explicit WorkletNodeHostObject( + const std::shared_ptr &graph, + const std::shared_ptr &context, + std::weak_ptr workletRuntime, + const std::shared_ptr &shareableWorklet, + bool shouldLockRuntime, + size_t bufferLength, + size_t inputChannelCount) + : AudioNodeHostObject( + graph, + std::make_unique( + context, + bufferLength, + inputChannelCount, + WorkletsRunner(workletRuntime, shareableWorklet, shouldLockRuntime))) {} }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h index 0c17b3cdd..c74f55e0d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/WorkletProcessingNodeHostObject.h @@ -2,6 +2,7 @@ #include #include +#include #include @@ -10,7 +11,16 @@ using namespace facebook; class WorkletProcessingNodeHostObject : public AudioNodeHostObject { public: - explicit WorkletProcessingNodeHostObject(const std::shared_ptr &node) - : AudioNodeHostObject(node) {} + explicit WorkletProcessingNodeHostObject( + const std::shared_ptr &graph, + const std::shared_ptr &context, + std::weak_ptr workletRuntime, + const std::shared_ptr &shareableWorklet, + bool shouldLockRuntime) + : AudioNodeHostObject( + graph, + std::make_unique( + context, + WorkletsRunner(workletRuntime, shareableWorklet, shouldLockRuntime))) {} }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp index da4465e54..802993c77 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp @@ -134,8 +134,7 @@ JSI_HOST_FUNCTION_IMPL(AudioRecorderHostObject, connect) { auto adapterNodeHostObject = args[0].getObject(runtime).getHostObject(runtime); - audioRecorder_->connect( - std::static_pointer_cast(adapterNodeHostObject->node_)); + audioRecorder_->connect(adapterNodeHostObject->node_->handle); return jsi::Value::undefined(); } 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 8f54eac8a..41f5b49a3 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 @@ -11,14 +11,18 @@ namespace audioapi { AudioBufferBaseSourceNodeHostObject::AudioBufferBaseSourceNodeHostObject( - const std::shared_ptr &node, + const std::shared_ptr &graph, + std::unique_ptr node, const BaseAudioBufferSourceOptions &options) - : AudioScheduledSourceNodeHostObject(node, options), + : AudioScheduledSourceNodeHostObject(graph, std::move(node), options), onPositionChangedInterval_(options.onPositionChangedInterval), pitchCorrection_(options.pitchCorrection) { - auto sourceNode = std::static_pointer_cast(node_); - detuneParam_ = std::make_shared(sourceNode->getDetuneParam()); - playbackRateParam_ = std::make_shared(sourceNode->getPlaybackRateParam()); + auto sourceNode = + static_cast(node_->handle->audioNode->asAudioNode()); + detuneParam_ = + std::make_shared(graph_, node_, sourceNode->getDetuneParam()); + playbackRateParam_ = + std::make_shared(graph_, node_, sourceNode->getPlaybackRateParam()); addGetters( JSI_EXPORT_PROPERTY_GETTER(AudioBufferBaseSourceNodeHostObject, detune), @@ -59,11 +63,13 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferBaseSourceNodeHostObject, onPositionChanged) } JSI_PROPERTY_SETTER_IMPL(AudioBufferBaseSourceNodeHostObject, onPositionChangedInterval) { - auto sourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto sourceNode = static_cast(handle->audioNode->asAudioNode()); auto interval = static_cast(value.getNumber()); - auto event = [sourceNode, interval](BaseAudioContext &) { - sourceNode->setOnPositionChangedInterval(interval); + auto event = [handle, interval](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->setOnPositionChangedInterval(interval); }; sourceNode->scheduleAudioEvent(std::move(event)); @@ -79,10 +85,12 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferBaseSourceNodeHostObject, getOutputLatency) { } void AudioBufferBaseSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_t callbackId) { - auto sourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto sourceNode = static_cast(handle->audioNode->asAudioNode()); - auto event = [sourceNode, callbackId](BaseAudioContext &) { - sourceNode->setOnPositionChangedCallbackId(callbackId); + auto event = [handle, callbackId](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->setOnPositionChangedCallbackId(callbackId); }; sourceNode->unregisterOnPositionChangedCallback(onPositionChangedCallbackId_); @@ -91,19 +99,21 @@ void AudioBufferBaseSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_ } void AudioBufferBaseSourceNodeHostObject::initStretch(int channelCount, float sampleRate) { - auto sourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto sourceNode = static_cast(handle->audioNode->asAudioNode()); auto stretch = std::make_shared>(); stretch->presetDefault(channelCount, sampleRate); - inputLatency_ = - std::max(dsp::sampleFrameToTime(stretch->inputLatency(), node_->getContextSampleRate()), 0.0); + inputLatency_ = std::max( + dsp::sampleFrameToTime(stretch->inputLatency(), sourceNode->getContextSampleRate()), 0.0); outputLatency_ = std::max( - dsp::sampleFrameToTime(stretch->outputLatency(), node_->getContextSampleRate()), 0.0); + dsp::sampleFrameToTime(stretch->outputLatency(), sourceNode->getContextSampleRate()), 0.0); auto playbackRateBuffer = std::make_shared(3 * RENDER_QUANTUM_SIZE, channelCount, sampleRate); - auto event = [sourceNode, stretch, playbackRateBuffer](BaseAudioContext &) { - sourceNode->initStretch(stretch, playbackRateBuffer); + auto event = [handle, stretch, playbackRateBuffer](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->initStretch(stretch, playbackRateBuffer); }; sourceNode->scheduleAudioEvent(std::move(event)); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.h index 8f27cf2a6..cf1b7065e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferBaseSourceNodeHostObject.h @@ -14,7 +14,8 @@ class AudioParamHostObject; class AudioBufferBaseSourceNodeHostObject : public AudioScheduledSourceNodeHostObject { public: explicit AudioBufferBaseSourceNodeHostObject( - const std::shared_ptr &node, + const std::shared_ptr &graph, + std::unique_ptr node, const BaseAudioBufferSourceOptions &options); ~AudioBufferBaseSourceNodeHostObject() override; 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 ec1d063b1..a606f00cc 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 @@ -13,7 +13,10 @@ namespace audioapi { AudioBufferQueueSourceNodeHostObject::AudioBufferQueueSourceNodeHostObject( const std::shared_ptr &context, const BaseAudioBufferSourceOptions &options) - : AudioBufferBaseSourceNodeHostObject(context->createBufferQueueSource(options), options) { + : AudioBufferBaseSourceNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { functions_->erase("start"); addSetters(JSI_EXPORT_PROPERTY_SETTER(AudioBufferQueueSourceNodeHostObject, onBufferEnded)); @@ -39,23 +42,27 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferQueueSourceNodeHostObject, onBufferEnded) { } JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, start) { - auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); - - auto event = [audioBufferQueueSourceNode, - when = args[0].getNumber(), - offset = args[1].getNumber()](BaseAudioContext &) { - audioBufferQueueSourceNode->start(when, offset); - }; + auto handle = node_->handle; + auto audioBufferQueueSourceNode = + static_cast(handle->audioNode->asAudioNode()); + + auto event = + [handle, when = args[0].getNumber(), offset = args[1].getNumber()](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->start(when, offset); + }; audioBufferQueueSourceNode->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, pause) { - auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferQueueSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioBufferQueueSourceNode](BaseAudioContext &) { - audioBufferQueueSourceNode->pause(); + auto event = [handle](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->pause(); }; audioBufferQueueSourceNode->scheduleAudioEvent(std::move(event)); @@ -63,7 +70,9 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, pause) { } JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, enqueueBuffer) { - auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferQueueSourceNode = + static_cast(handle->audioNode->asAudioNode()); auto audioBufferHostObject = args[0].getObject(runtime).asHostObject(runtime); @@ -84,9 +93,9 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, enqueueBuffer) { stretchHasBeenInit_ = true; } - auto event = [audioBufferQueueSourceNode, copiedBuffer, bufferId = bufferId_, tailBuffer]( - BaseAudioContext &) { - audioBufferQueueSourceNode->enqueueBuffer(copiedBuffer, bufferId, tailBuffer); + auto event = [handle, copiedBuffer, bufferId = bufferId_, tailBuffer](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->enqueueBuffer(copiedBuffer, bufferId, tailBuffer); }; audioBufferQueueSourceNode->scheduleAudioEvent(std::move(event)); @@ -94,11 +103,13 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, enqueueBuffer) { } JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, dequeueBuffer) { - auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferQueueSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioBufferQueueSourceNode, - bufferId = static_cast(args[0].getNumber())](BaseAudioContext &) { - audioBufferQueueSourceNode->dequeueBuffer(bufferId); + auto event = [handle, bufferId = static_cast(args[0].getNumber())](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->dequeueBuffer(bufferId); }; audioBufferQueueSourceNode->scheduleAudioEvent(std::move(event)); @@ -106,10 +117,12 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, dequeueBuffer) { } JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, clearBuffers) { - auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferQueueSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioBufferQueueSourceNode](BaseAudioContext &) { - audioBufferQueueSourceNode->clearBuffers(); + auto event = [handle](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->clearBuffers(); }; audioBufferQueueSourceNode->scheduleAudioEvent(std::move(event)); @@ -117,10 +130,13 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferQueueSourceNodeHostObject, clearBuffers) { } void AudioBufferQueueSourceNodeHostObject::setOnBufferEndedCallbackId(uint64_t callbackId) { - auto audioBufferQueueSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferQueueSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioBufferQueueSourceNode, callbackId](BaseAudioContext &) { - audioBufferQueueSourceNode->setOnBufferEndedCallbackId(callbackId); + auto event = [handle, callbackId](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->setOnBufferEndedCallbackId(callbackId); }; audioBufferQueueSourceNode->unregisterOnBufferEndedCallback(onBufferEndedCallbackId_); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp index 9c863fdd1..0c58b5ab1 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp @@ -14,7 +14,10 @@ namespace audioapi { AudioBufferSourceNodeHostObject::AudioBufferSourceNodeHostObject( const std::shared_ptr &context, const AudioBufferSourceOptions &options) - : AudioBufferBaseSourceNodeHostObject(context->createBufferSource(options), options), + : AudioBufferBaseSourceNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options), loop_(options.loop), loopSkip_(options.loopSkip), loopStart_(options.loopStart), @@ -68,11 +71,13 @@ JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, loopEnd) { } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loop) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); auto loop = value.getBool(); - auto event = [audioBufferSourceNode, loop](BaseAudioContext &) { - audioBufferSourceNode->setLoop(loop); + auto event = [handle, loop](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setLoop(loop); }; audioBufferSourceNode->scheduleAudioEvent(std::move(event)); @@ -80,11 +85,13 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loop) { } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopSkip) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); auto loopSkip = value.getBool(); - auto event = [audioBufferSourceNode, loopSkip](BaseAudioContext &) { - audioBufferSourceNode->setLoopSkip(loopSkip); + auto event = [handle, loopSkip](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setLoopSkip(loopSkip); }; audioBufferSourceNode->scheduleAudioEvent(std::move(event)); @@ -92,11 +99,13 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopSkip) { } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopStart) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); auto loopStart = value.getNumber(); - auto event = [audioBufferSourceNode, loopStart](BaseAudioContext &) { - audioBufferSourceNode->setLoopStart(loopStart); + auto event = [handle, loopStart](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setLoopStart(loopStart); }; audioBufferSourceNode->scheduleAudioEvent(std::move(event)); @@ -104,11 +113,13 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopStart) { } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopEnd) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); auto loopEnd = value.getNumber(); - auto event = [audioBufferSourceNode, loopEnd](BaseAudioContext &) { - audioBufferSourceNode->setLoopEnd(loopEnd); + auto event = [handle, loopEnd](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setLoopEnd(loopEnd); }; audioBufferSourceNode->scheduleAudioEvent(std::move(event)); @@ -121,13 +132,16 @@ JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, onLoopEnded) { } JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, start) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioBufferSourceNode, + auto event = [handle, when = args[0].getNumber(), offset = args[1].getNumber(), duration = args[2].isUndefined() ? -1 : args[2].getNumber()](BaseAudioContext &) { - audioBufferSourceNode->start(when, offset, duration); + static_cast(handle->audioNode->asAudioNode()) + ->start(when, offset, duration); }; audioBufferSourceNode->scheduleAudioEvent(std::move(event)); @@ -135,8 +149,6 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, start) { } JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, setBuffer) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - if (args[0].isNull()) { setBuffer(nullptr); } else { @@ -151,14 +163,21 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, setBuffer) { } void AudioBufferSourceNodeHostObject::setOnLoopEndedCallbackId(uint64_t callbackId) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioBufferSourceNode, callbackId](BaseAudioContext &) { - audioBufferSourceNode->setOnLoopEndedCallbackId(callbackId); + auto event = [handle, callbackId](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->setOnLoopEndedCallbackId(callbackId); }; audioBufferSourceNode->unregisterOnLoopEndedCallback(onLoopEndedCallbackId_); - audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + if (callbackId == 0) { + audioBufferSourceNode->scheduleGCEvent(std::move(event)); + } else { + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + } onLoopEndedCallbackId_ = callbackId; } @@ -166,7 +185,9 @@ void AudioBufferSourceNodeHostObject::setBuffer(const std::shared_ptr(node_); + auto handle = node_->handle; + auto audioBufferSourceNode = + static_cast(handle->audioNode->asAudioNode()); std::shared_ptr copiedBuffer; std::shared_ptr audioBuffer; @@ -195,8 +216,9 @@ void AudioBufferSourceNodeHostObject::setBuffer(const std::shared_ptrgetContextSampleRate()); } - auto event = [audioBufferSourceNode, copiedBuffer, audioBuffer](BaseAudioContext &) { - audioBufferSourceNode->setBuffer(copiedBuffer, audioBuffer); + auto event = [handle, copiedBuffer, audioBuffer](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->setBuffer(copiedBuffer, audioBuffer); }; audioBufferSourceNode->scheduleAudioEvent(std::move(event)); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp index e8e19745f..c87b62492 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp @@ -1,21 +1,26 @@ #include +#include #include #include #include #include #include -#include "audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.h" namespace audioapi { AudioFileSourceNodeHostObject::AudioFileSourceNodeHostObject( const std::shared_ptr &context, const AudioFileSourceOptions &options) - : AudioScheduledSourceNodeHostObject(context->createFileSource(options), options), + : AudioScheduledSourceNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options), loop_(options.loop), - volume_(options.volume), - duration_(std::static_pointer_cast(node_)->getDuration()) { + duration_( + static_cast(node_->handle->audioNode->asAudioNode()) + ->getDuration()), + volume_(options.volume) { addGetters( JSI_EXPORT_PROPERTY_GETTER(AudioFileSourceNodeHostObject, volume), JSI_EXPORT_PROPERTY_GETTER(AudioFileSourceNodeHostObject, loop), @@ -43,7 +48,8 @@ JSI_PROPERTY_GETTER_IMPL(AudioFileSourceNodeHostObject, volume) { } JSI_PROPERTY_SETTER_IMPL(AudioFileSourceNodeHostObject, volume) { - auto node = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *node = static_cast(handle->audioNode->asAudioNode()); volume_ = static_cast(value.getNumber()); auto event = [node, volume = this->volume_](BaseAudioContext &ctx) { node->setVolume(volume); @@ -56,7 +62,8 @@ JSI_PROPERTY_GETTER_IMPL(AudioFileSourceNodeHostObject, loop) { } JSI_PROPERTY_SETTER_IMPL(AudioFileSourceNodeHostObject, loop) { - auto node = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *node = static_cast(handle->audioNode->asAudioNode()); loop_ = value.getBool(); auto event = [node, loop = this->loop_](BaseAudioContext &ctx) { node->setLoop(loop); @@ -65,7 +72,8 @@ JSI_PROPERTY_SETTER_IMPL(AudioFileSourceNodeHostObject, loop) { } JSI_PROPERTY_GETTER_IMPL(AudioFileSourceNodeHostObject, currentTime) { - auto node = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *node = static_cast(handle->audioNode->asAudioNode()); return {node->getCurrentTime()}; } @@ -74,7 +82,8 @@ JSI_PROPERTY_GETTER_IMPL(AudioFileSourceNodeHostObject, duration) { } JSI_HOST_FUNCTION_IMPL(AudioFileSourceNodeHostObject, pause) { - auto audioFileSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *audioFileSourceNode = static_cast(handle->audioNode->asAudioNode()); auto event = [audioFileSourceNode](BaseAudioContext &ctx) { audioFileSourceNode->pause(); }; @@ -83,7 +92,8 @@ JSI_HOST_FUNCTION_IMPL(AudioFileSourceNodeHostObject, pause) { } JSI_HOST_FUNCTION_IMPL(AudioFileSourceNodeHostObject, seekToTime) { - auto audioFileSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *audioFileSourceNode = static_cast(handle->audioNode->asAudioNode()); if (count < 1 || !args[0].isNumber()) { return jsi::Value::undefined(); } @@ -103,7 +113,8 @@ JSI_PROPERTY_SETTER_IMPL(AudioFileSourceNodeHostObject, onPositionChanged) { } void AudioFileSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_t callbackId) { - auto sourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *sourceNode = static_cast(handle->audioNode->asAudioNode()); auto event = [sourceNode, callbackId](BaseAudioContext &) { sourceNode->setOnPositionChangedCallbackId(callbackId); @@ -115,7 +126,8 @@ void AudioFileSourceNodeHostObject::setOnPositionChangedCallbackId(uint64_t call } void AudioFileSourceNodeHostObject::setOnEndedCallbackId(uint64_t callbackId) { - auto sourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto *sourceNode = static_cast(handle->audioNode->asAudioNode()); auto event = [sourceNode, callbackId](BaseAudioContext &) { sourceNode->setOnEndedCallbackId(callbackId); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.cpp index 099be5b6f..fb848a0a6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.cpp @@ -8,9 +8,10 @@ namespace audioapi { AudioScheduledSourceNodeHostObject::AudioScheduledSourceNodeHostObject( - const std::shared_ptr &node, + const std::shared_ptr &graph, + std::unique_ptr node, const AudioScheduledSourceNodeOptions &options) - : AudioNodeHostObject(node) { + : AudioNodeHostObject(graph, std::move(node), options) { addSetters(JSI_EXPORT_PROPERTY_SETTER(AudioScheduledSourceNodeHostObject, onEnded)); addFunctions( @@ -31,10 +32,12 @@ JSI_PROPERTY_SETTER_IMPL(AudioScheduledSourceNodeHostObject, onEnded) { } JSI_HOST_FUNCTION_IMPL(AudioScheduledSourceNodeHostObject, start) { - auto audioScheduleSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioScheduleSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioScheduleSourceNode, when = args[0].getNumber()](BaseAudioContext &) { - audioScheduleSourceNode->start(when); + auto event = [handle, when = args[0].getNumber()](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->start(when); }; audioScheduleSourceNode->scheduleAudioEvent(std::move(event)); @@ -42,10 +45,12 @@ JSI_HOST_FUNCTION_IMPL(AudioScheduledSourceNodeHostObject, start) { } JSI_HOST_FUNCTION_IMPL(AudioScheduledSourceNodeHostObject, stop) { - auto audioScheduleSourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto audioScheduleSourceNode = + static_cast(handle->audioNode->asAudioNode()); - auto event = [audioScheduleSourceNode, when = args[0].getNumber()](BaseAudioContext &) { - audioScheduleSourceNode->stop(when); + auto event = [handle, when = args[0].getNumber()](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->stop(when); }; audioScheduleSourceNode->scheduleAudioEvent(std::move(event)); @@ -53,14 +58,20 @@ JSI_HOST_FUNCTION_IMPL(AudioScheduledSourceNodeHostObject, stop) { } void AudioScheduledSourceNodeHostObject::setOnEndedCallbackId(uint64_t callbackId) { - auto sourceNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto sourceNode = static_cast(handle->audioNode->asAudioNode()); - auto event = [sourceNode, callbackId](BaseAudioContext &) { - sourceNode->setOnEndedCallbackId(callbackId); + auto event = [handle, callbackId](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode()) + ->setOnEndedCallbackId(callbackId); }; sourceNode->unregisterOnEndedCallback(onEndedCallbackId_); - sourceNode->scheduleAudioEvent(std::move(event)); + if (callbackId == 0) { + sourceNode->scheduleGCEvent(std::move(event)); + } else { + sourceNode->scheduleAudioEvent(std::move(event)); + } onEndedCallbackId_ = callbackId; } 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 717853115..c56db8119 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 @@ -8,12 +8,11 @@ namespace audioapi { using namespace facebook; -class AudioScheduledSourceNode; - class AudioScheduledSourceNodeHostObject : public AudioNodeHostObject { public: explicit AudioScheduledSourceNodeHostObject( - const std::shared_ptr &node, + const std::shared_ptr &graph, + std::unique_ptr node, const AudioScheduledSourceNodeOptions &options = AudioScheduledSourceNodeOptions()); ~AudioScheduledSourceNodeHostObject() override; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/ConstantSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/ConstantSourceNodeHostObject.cpp index b4522932b..c592661a6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/ConstantSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/ConstantSourceNodeHostObject.cpp @@ -10,9 +10,14 @@ namespace audioapi { ConstantSourceNodeHostObject::ConstantSourceNodeHostObject( const std::shared_ptr &context, const ConstantSourceOptions &options) - : AudioScheduledSourceNodeHostObject(context->createConstantSource(options), options) { - auto constantSourceNode = std::static_pointer_cast(node_); - offsetParam_ = std::make_shared(constantSourceNode->getOffsetParam()); + : AudioScheduledSourceNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) { + auto constantSourceNode = + static_cast(node_->handle->audioNode->asAudioNode()); + offsetParam_ = + std::make_shared(graph_, node_, constantSourceNode->getOffsetParam()); addGetters(JSI_EXPORT_PROPERTY_GETTER(ConstantSourceNodeHostObject, offset)); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/OscillatorNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/OscillatorNodeHostObject.cpp index c88189572..d3adc038b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/OscillatorNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/OscillatorNodeHostObject.cpp @@ -14,11 +14,16 @@ namespace audioapi { OscillatorNodeHostObject::OscillatorNodeHostObject( const std::shared_ptr &context, const OscillatorOptions &options) - : AudioScheduledSourceNodeHostObject(context->createOscillator(options), options), + : AudioScheduledSourceNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options), type_(options.type) { - auto oscillatorNode = std::static_pointer_cast(node_); - frequencyParam_ = std::make_shared(oscillatorNode->getFrequencyParam()); - detuneParam_ = std::make_shared(oscillatorNode->getDetuneParam()); + auto *oscillatorNode = static_cast(node_->handle->audioNode->asAudioNode()); + frequencyParam_ = + std::make_shared(graph_, node_, oscillatorNode->getFrequencyParam()); + detuneParam_ = + std::make_shared(graph_, node_, oscillatorNode->getDetuneParam()); addGetters( JSI_EXPORT_PROPERTY_GETTER(OscillatorNodeHostObject, frequency), @@ -43,11 +48,12 @@ JSI_PROPERTY_GETTER_IMPL(OscillatorNodeHostObject, type) { } JSI_HOST_FUNCTION_IMPL(OscillatorNodeHostObject, setPeriodicWave) { - auto oscillatorNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto oscillatorNode = static_cast(handle->audioNode->asAudioNode()); auto periodicWave = args[0].getObject(runtime).getHostObject(runtime); - auto event = [oscillatorNode, periodicWave = periodicWave->periodicWave_](BaseAudioContext &) { - oscillatorNode->setPeriodicWave(periodicWave); + auto event = [handle, periodicWave = periodicWave->periodicWave_](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setPeriodicWave(periodicWave); }; oscillatorNode->scheduleAudioEvent(std::move(event)); @@ -55,11 +61,12 @@ JSI_HOST_FUNCTION_IMPL(OscillatorNodeHostObject, setPeriodicWave) { } JSI_PROPERTY_SETTER_IMPL(OscillatorNodeHostObject, type) { - auto oscillatorNode = std::static_pointer_cast(node_); + auto handle = node_->handle; + auto oscillatorNode = static_cast(handle->audioNode->asAudioNode()); auto type = js_enum_parser::oscillatorTypeFromString(value.asString(runtime).utf8(runtime)); - auto event = [oscillatorNode, type](BaseAudioContext &) { - oscillatorNode->setType(type); + auto event = [handle, type](BaseAudioContext &) { + static_cast(handle->audioNode->asAudioNode())->setType(type); }; type_ = type; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/RecorderAdapterNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/RecorderAdapterNodeHostObject.h index 8b257025b..4da28fb5b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/RecorderAdapterNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/RecorderAdapterNodeHostObject.h @@ -2,6 +2,7 @@ #include #include +#include #include @@ -12,8 +13,13 @@ class AudioRecorderHostObject; class RecorderAdapterNodeHostObject : public AudioNodeHostObject { public: - explicit RecorderAdapterNodeHostObject(const std::shared_ptr &node) - : AudioNodeHostObject(node) {} + explicit RecorderAdapterNodeHostObject( + const std::shared_ptr &context, + const AudioScheduledSourceNodeOptions &options = AudioScheduledSourceNodeOptions()) + : AudioNodeHostObject( + context->getGraph(), + std::make_unique(context), + options) {} private: friend class AudioRecorderHostObject; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/StreamerNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/StreamerNodeHostObject.h index 58352f693..9c0f65826 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/StreamerNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/StreamerNodeHostObject.h @@ -18,7 +18,10 @@ class StreamerNodeHostObject : public AudioScheduledSourceNodeHostObject { explicit StreamerNodeHostObject( const std::shared_ptr &context, const StreamerOptions &options) - : AudioScheduledSourceNodeHostObject(context->createStreamer(options), options) {} + : AudioScheduledSourceNodeHostObject( + context->getGraph(), + std::make_unique(context, options), + options) {} [[nodiscard]] static size_t getSizeInBytes() { return SIZE; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h index 648b53334..a9ae0e484 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/WorkletSourceNodeHostObject.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include @@ -10,7 +12,18 @@ using namespace facebook; class WorkletSourceNodeHostObject : public AudioScheduledSourceNodeHostObject { public: - explicit WorkletSourceNodeHostObject(const std::shared_ptr &node) - : AudioScheduledSourceNodeHostObject(node) {} + explicit WorkletSourceNodeHostObject( + const std::shared_ptr &graph, + const std::shared_ptr &context, + std::weak_ptr workletRuntime, + const std::shared_ptr &shareableWorklet, + bool shouldLockRuntime, + const AudioScheduledSourceNodeOptions &options = AudioScheduledSourceNodeOptions()) + : AudioScheduledSourceNodeHostObject( + graph, + std::make_unique( + context, + WorkletsRunner(workletRuntime, shareableWorklet, shouldLockRuntime)), + options) {} }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index cc7497116..4c20dc556 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -6,7 +6,6 @@ #include #include -#include #include namespace audioapi { @@ -23,14 +22,18 @@ AudioContext::~AudioContext() { } } -void AudioContext::initialize() { - BaseAudioContext::initialize(); +void AudioContext::initialize(const AudioDestinationNode *destination) { + BaseAudioContext::initialize(destination); #ifdef ANDROID audioPlayer_ = std::make_shared( - this->renderAudio(), getSampleRate(), destination_->getChannelCount()); + [this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); }, + getSampleRate(), + destination_->getChannelCount()); #else audioPlayer_ = std::make_shared( - this->renderAudio(), getSampleRate(), destination_->getChannelCount()); + [this](DSPAudioBuffer *buf, int n) { processGraph(buf, n); }, + getSampleRate(), + destination_->getChannelCount()); #endif } @@ -39,7 +42,6 @@ void AudioContext::close() { audioPlayer_->stop(); audioPlayer_->cleanup(); - getGraphManager()->cleanup(); } bool AudioContext::resume() { @@ -89,12 +91,6 @@ bool AudioContext::start() { return false; } -std::function, int)> AudioContext::renderAudio() { - return [this](const std::shared_ptr &data, int frames) { - destination_->renderAudio(data, frames); - }; -} - bool AudioContext::isDriverRunning() const { return audioPlayer_->isRunning(); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h index 456986312..93f443de8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.h @@ -5,7 +5,6 @@ #include #include -#include #include namespace audioapi { @@ -28,7 +27,11 @@ class AudioContext : public BaseAudioContext { bool resume(); bool suspend(); bool start(); - void initialize() override; + + /// @brief Initializes native audio player and assigns the audio destination node to the context. + /// @param destination The audio destination node to be associated with the context. + /// @note This method must be called before the audio context can be used for processing audio. + void initialize(const AudioDestinationNode *destination) final; private: #ifdef ANDROID @@ -39,8 +42,6 @@ class AudioContext : public BaseAudioContext { std::atomic isInitialized_{false}; bool isDriverRunning() const override; - - std::function, int)> renderAudio(); }; } // 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 0436c6f53..8c1a0a120 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 @@ -1,11 +1,13 @@ #include #include #include -#include #include #include +#include +#include #include +#include namespace audioapi { @@ -23,285 +25,108 @@ AudioNode::AudioNode( RENDER_QUANTUM_SIZE, channelCount_, context->getSampleRate()); } -AudioNode::~AudioNode() { - if (isInitialized_.load(std::memory_order_acquire)) { - cleanup(); - } -} - bool AudioNode::canBeDestructed() const { - return true; -} - -size_t AudioNode::getChannelCount() const { - return channelCount_; -} - -void AudioNode::connect( - const std::shared_ptr - &node) { // NOLINT(readability-convert-member-functions-to-static) - if (std::shared_ptr context = context_.lock()) { - context->getGraphManager()->addPendingNodeConnection( - shared_from_this(), node, AudioGraphManager::ConnectionType::CONNECT); - } -} - -void AudioNode::connect( - const std::shared_ptr - ¶m) { // NOLINT(readability-convert-member-functions-to-static) - if (std::shared_ptr context = context_.lock()) { - context->getGraphManager()->addPendingParamConnection( - shared_from_this(), param, AudioGraphManager::ConnectionType::CONNECT); - } -} - -void AudioNode::disconnect() { // NOLINT(readability-convert-member-functions-to-static) - if (std::shared_ptr context = context_.lock()) { - context->getGraphManager()->addPendingNodeConnection( - shared_from_this(), nullptr, AudioGraphManager::ConnectionType::DISCONNECT_ALL); - } -} - -void AudioNode::disconnect( - const std::shared_ptr - &node) { // NOLINT(readability-convert-member-functions-to-static) - if (std::shared_ptr context = context_.lock()) { - context->getGraphManager()->addPendingNodeConnection( - shared_from_this(), node, AudioGraphManager::ConnectionType::DISCONNECT); - } + // Tail-bearing nodes must not be destroyed mid-tail: even if the node is + // orphaned and has no live inputs, audio that has not yet been rendered + // would otherwise be lost. Once the tail has fully drained, the node + // becomes destructible. + return !requiresTailProcessing_ || tailState_ == TailState::FINISHED; } -void AudioNode::disconnect( - const std::shared_ptr - ¶m) { // NOLINT(readability-convert-member-functions-to-static) - if (std::shared_ptr context = context_.lock()) { - context->getGraphManager()->addPendingParamConnection( - shared_from_this(), param, AudioGraphManager::ConnectionType::DISCONNECT); +bool AudioNode::isProcessable() const { + if (GraphObject::isProcessable()) { + return true; } + // Even after a downstream disconnect flips processableState_ to + // NOT_PROCESSABLE, a tail-bearing node must keep being scheduled until its + // impulse response has decayed. Otherwise the tail would never play out. + return requiresTailProcessing_ && tailState_ != TailState::FINISHED; } -bool AudioNode::isEnabled() const { - return isEnabled_; +size_t AudioNode::getChannelCount() const { + return channelCount_; } bool AudioNode::requiresTailProcessing() const { return requiresTailProcessing_; } -void AudioNode::enable() { - if (isEnabled()) { - return; - } - - isEnabled_ = true; - - for (auto it = outputNodes_.begin(), end = outputNodes_.end(); it != end; ++it) { - it->get()->onInputEnabled(); - } -} - -void AudioNode::disable() { - if (!isEnabled()) { - return; - } - - isEnabled_ = false; +namespace { - for (auto it = outputNodes_.begin(), end = outputNodes_.end(); it != end; ++it) { - it->get()->onInputDisabled(); +// Branch-free silence check over a contiguous float span. Reinterprets each +// sample as its IEEE-754 bit pattern and ORs them together with the sign bit +// masked out, so `-0.0f` is treated as silent (matching `== 0.0f`). The lack +// of an in-loop branch lets the compiler auto-vectorize this tight loop. +[[nodiscard]] inline bool spanIsSilent(const float *data, size_t count) noexcept { + constexpr std::uint32_t kSignMask = 0x7FFFFFFFu; + std::uint32_t acc = 0; + for (size_t i = 0; i < count; ++i) { + std::uint32_t bits; + std::memcpy(&bits, data + i, sizeof(bits)); + acc |= (bits & kSignMask); } + return acc == 0; } -std::shared_ptr AudioNode::processAudio( - const std::shared_ptr &outputBuffer, - int framesToProcess, - bool checkIsAlreadyProcessed) { - if (!isInitialized_.load(std::memory_order_acquire)) { - return outputBuffer; - } - - if (checkIsAlreadyProcessed && isAlreadyProcessed()) { - return audioBuffer_; - } - - // Process inputs and return the buffer with the most channels. - auto processingBuffer = processInputs(outputBuffer, framesToProcess, checkIsAlreadyProcessed); - - // Apply channel count mode. - processingBuffer = applyChannelCountMode(processingBuffer); - - // Mix all input buffers into the processing buffer. - mixInputsBuffers(processingBuffer); - - assert(processingBuffer != nullptr); - - // Finally, process the node itself. - return processNode(processingBuffer, framesToProcess); -} - -bool AudioNode::isAlreadyProcessed() { // NOLINT(readability-convert-member-functions-to-static) - if (std::shared_ptr context = context_.lock()) { - std::size_t currentSampleFrame = context->getCurrentSampleFrame(); +} // namespace - // check if the node has already been processed for this rendering quantum - if (currentSampleFrame == lastRenderedFrame_) { - return true; - } - - // Update the last rendered frame before processing node and its inputs. - lastRenderedFrame_ = currentSampleFrame; - - return false; +bool AudioNode::isInputSilent(const std::vector &inputs) const { + // No inputs means upstream is gone (or this node never had any). Either + // way the mixer below would feed silence to processNode, so treat it as + // silent input here. + if (inputs.empty()) { + return true; } - - // If context is invalid, consider it as already processed to avoid processing - return true; // NOLINT(readability-simplify-boolean-expr) -} - -std::shared_ptr AudioNode::processInputs( - const std::shared_ptr &outputBuffer, - int framesToProcess, - bool checkIsAlreadyProcessed) { // NOLINT(readability-convert-member-functions-to-static) - auto processingBuffer = audioBuffer_; - processingBuffer->zero(); - - size_t maxNumberOfChannels = 0; - for (auto *inputNode : inputNodes_) { - assert(inputNode != nullptr); - - if (!inputNode->isEnabled()) { + for (const DSPAudioBuffer *input : inputs) { + if (input == nullptr) { continue; } - - auto inputBuffer = - inputNode->processAudio(outputBuffer, framesToProcess, checkIsAlreadyProcessed); - inputBuffers_.push_back(inputBuffer); - - if (maxNumberOfChannels < inputBuffer->getNumberOfChannels()) { - maxNumberOfChannels = inputBuffer->getNumberOfChannels(); - processingBuffer = inputBuffer; + const size_t channels = input->getNumberOfChannels(); + for (size_t c = 0; c < channels; ++c) { + auto *channel = input->getChannel(c); + if (channel == nullptr) { + continue; + } + const auto span = channel->span(); + if (!spanIsSilent(span.data(), span.size())) { + return false; + } } } - - return processingBuffer; -} - -std::shared_ptr AudioNode::applyChannelCountMode( - const std::shared_ptr &processingBuffer) { - // If the channelCountMode is EXPLICIT, the node should output the number of - // channels specified by the channelCount. - if (channelCountMode_ == ChannelCountMode::EXPLICIT) { - return audioBuffer_; - } - - // If the channelCountMode is CLAMPED_MAX, the node should output the maximum - // number of channels clamped to channelCount. - if (channelCountMode_ == ChannelCountMode::CLAMPED_MAX && - processingBuffer->getNumberOfChannels() >= channelCount_) { - return audioBuffer_; - } - - return processingBuffer; -} - -void AudioNode::mixInputsBuffers(const std::shared_ptr &processingBuffer) { - assert(processingBuffer != nullptr); - - for (auto it = inputBuffers_.begin(), end = inputBuffers_.end(); it != end; ++it) { - processingBuffer->sum(**it, channelInterpretation_); - } - - inputBuffers_.clear(); -} - -void AudioNode::connectNode(const std::shared_ptr &node) { - auto position = outputNodes_.find(node); - - if (position == outputNodes_.end()) { - outputNodes_.insert(node); - node->onInputConnected(this); - } -} - -void AudioNode::connectParam(const std::shared_ptr ¶m) { - auto position = outputParams_.find(param); - - if (position == outputParams_.end()) { - outputParams_.insert(param); - param->addInputNode(this); - } -} - -void AudioNode::disconnectNode(const std::shared_ptr &node) { - auto position = outputNodes_.find(node); - - if (position != outputNodes_.end()) { - node->onInputDisconnected(this); - outputNodes_.erase(node); - } -} - -void AudioNode::disconnectParam(const std::shared_ptr ¶m) { - auto position = outputParams_.find(param); - - if (position != outputParams_.end()) { - param->removeInputNode(this); - outputParams_.erase(param); - } -} - -void AudioNode::onInputEnabled() { - numberOfEnabledInputNodes_ += 1; - - if (!isEnabled()) { - enable(); - } + return true; } -void AudioNode::onInputDisabled() { - numberOfEnabledInputNodes_ -= 1; +void AudioNode::updateTailStateForQuantum( + const std::vector &inputs, + int numFrames) { + const bool silent = isInputSilent(inputs); - if (isEnabled() && numberOfEnabledInputNodes_ == 0) { - disable(); - } -} - -void AudioNode::onInputConnected(AudioNode *node) { - if (!isInitialized_.load(std::memory_order_acquire)) { + if (!silent) { + // Any non-silent sample re-arms the tail: the node is being driven again + // and we must not stop it mid-stream. + tailState_ = TailState::ACTIVE; + tailFramesRemaining_ = 0; return; } - inputNodes_.insert(node); - - if (node->isEnabled()) { - onInputEnabled(); + switch (tailState_) { + case TailState::ACTIVE: + tailState_ = TailState::SIGNALLED_TO_STOP; + tailFramesRemaining_ = computeTailFrames(); + if (tailFramesRemaining_ <= 0) { + tailState_ = TailState::FINISHED; + } + break; + case TailState::SIGNALLED_TO_STOP: + tailFramesRemaining_ -= numFrames; + if (tailFramesRemaining_ <= 0) { + tailState_ = TailState::FINISHED; + } + break; + case TailState::FINISHED: + // Stay finished; will be reset to ACTIVE if non-silent input returns. + break; } } -void AudioNode::onInputDisconnected(AudioNode *node) { - if (!isInitialized_.load(std::memory_order_acquire)) { - return; - } - - if (node->isEnabled()) { - onInputDisabled(); - } - - auto position = inputNodes_.find(node); - - if (position != inputNodes_.end()) { - inputNodes_.erase(position); - } -} - -void AudioNode::cleanup() { - isInitialized_.store(false, std::memory_order_release); - - for (auto it = outputNodes_.begin(), end = outputNodes_.end(); it != end; ++it) { - it->get()->onInputDisconnected(this); - } - - outputNodes_.clear(); -} - } // 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 d887a92d0..cfd8cdc2a 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 @@ -4,14 +4,15 @@ #include #include #include +#include #include #include #include #include #include +#include #include -#include #include #include @@ -19,25 +20,22 @@ namespace audioapi { class AudioParam; -class AudioNode : public std::enable_shared_from_this { +class AudioNode : public utils::graph::GraphObject, public std::enable_shared_from_this { public: explicit AudioNode( const std::shared_ptr &context, const AudioNodeOptions &options = AudioNodeOptions()); - virtual ~AudioNode(); - + ~AudioNode() override = default; DELETE_COPY_AND_MOVE(AudioNode); [[nodiscard]] size_t getChannelCount() const; - void connect(const std::shared_ptr &node); - void connect(const std::shared_ptr ¶m); - void disconnect(); - void disconnect(const std::shared_ptr &node); - void disconnect(const std::shared_ptr ¶m); - virtual std::shared_ptr processAudio( - const std::shared_ptr &outputBuffer, - int framesToProcess, - bool checkIsAlreadyProcessed); + + /// @brief Returns this node's `channelCountMode` attribute. + /// @note The value is immutable after construction, so this is safe to call + /// from any thread. + [[nodiscard]] ChannelCountMode getChannelCountMode() const { + return channelCountMode_; + } [[nodiscard]] float getContextSampleRate() const { if (std::shared_ptr context = context_.lock()) { @@ -52,11 +50,64 @@ class AudioNode : public std::enable_shared_from_this { return getContextSampleRate() / kNyquistDivisor; } - /// @note JS Thread only - [[nodiscard]] bool isEnabled() const; + /// @brief Returns the output buffer for this node. + /// @note Audio Thread only. + [[nodiscard]] const DSPAudioBuffer *getOutput() const override { + return audioBuffer_.get(); + } + + /// @brief Returns the input buffer for this node. By default, this is the same as the output buffer. + /// @note Audio Thread only. + /// @note For StereoPannerNode and PannerNode due to channel limitations - + /// https://webaudio.github.io/web-audio-api/#StereoPanner-channel-limitations + /// the input buffer is negotiate with inputs, but output buffer is always stereo. + std::shared_ptr getInputBuffer() const { + return audioBuffer_; + } + + /// @brief Returns the output buffer for this node. By default, this is the same as the input buffer. + /// @note Audio Thread only. + /// @note For StereoPannerNode and PannerNode due to channel limitations - + /// https://webaudio.github.io/web-audio-api/#StereoPanner-channel-limitations + /// the input buffer is negotiate with inputs, but output buffer is always stereo. + virtual std::shared_ptr getOutputBuffer() const { + return audioBuffer_; + } + + virtual void setOutputBuffer(const std::shared_ptr &buffer) { + audioBuffer_ = buffer; + } + /// @note JS Thread only [[nodiscard]] bool requiresTailProcessing() const; + /// @brief Tail-processing lifecycle state. + /// + /// Audio-thread only. Drives the "node has a non-zero impulse response that + /// must be played out after its inputs go silent" behavior required by the + /// Web Audio spec for nodes such as BiquadFilter, Delay and Convolver. + /// + /// Transitions (in `processInputs`, gated by `requiresTailProcessing_`): + /// - any quantum with non-silent input : * -> ACTIVE (re-arm) + /// - first silent quantum after ACTIVE : ACTIVE -> SIGNALLED_TO_STOP, + /// tailFramesRemaining_ = computeTailFrames() + /// - subsequent silent quanta : tailFramesRemaining_ -= numFrames + /// once <= 0, SIGNALLED_TO_STOP -> FINISHED + enum class TailState : std::uint8_t { + ACTIVE, + SIGNALLED_TO_STOP, + FINISHED, + }; + + /// @brief Returns whether this node should be processed during audio iteration. + /// + /// Extends the base GraphObject rule with: a tail-bearing node remains + /// processable until its tail has fully drained, even after its + /// `processableState_` was flipped to NOT_PROCESSABLE by a downstream + /// disconnect. + /// @note Audio Thread only. + [[nodiscard]] bool isProcessable() const override; + template bool scheduleAudioEvent(F &&event) noexcept { if (std::shared_ptr context = context_.lock()) { @@ -66,13 +117,32 @@ class AudioNode : public std::enable_shared_from_this { return false; } - virtual bool canBeDestructed() const; + /// @brief Schedule an audio event from the JS runtime's finalizer (GC) + /// thread. Use this from `~HostObject` code paths — `scheduleAudioEvent` + /// would race because its SPSC channel's single producer is the JS + /// thread. + template + bool scheduleGCEvent(F &&event) noexcept { + if (std::shared_ptr context = context_.lock()) { + return context->scheduleGCEvent(std::forward(event)); + } + + return false; + } + + bool canBeDestructed() const override; + + [[nodiscard]] AudioNode *asAudioNode() override { + return this; + } + + [[nodiscard]] const AudioNode *asAudioNode() const override { + return this; + } protected: - friend class AudioGraphManager; - friend class AudioDestinationNode; - friend class ConvolverNode; friend class DelayNodeHostObject; + friend class utils::graph::HostGraph; std::weak_ptr context_; std::shared_ptr audioBuffer_; @@ -84,46 +154,55 @@ class AudioNode : public std::enable_shared_from_this { const ChannelInterpretation channelInterpretation_ = ChannelInterpretation::SPEAKERS; const bool requiresTailProcessing_; - std::unordered_set inputNodes_; - std::unordered_set> outputNodes_; - std::unordered_set> outputParams_; + /// @brief Tail-processing audio-thread state. Only mutated when + /// `requiresTailProcessing_` is true. + TailState tailState_ = TailState::ACTIVE; + int tailFramesRemaining_ = 0; + + /// @brief Implementation of processing logic for AudioNode. + /// Mixes input buffers, runs the tail-state transition (when this node + /// requires tail processing), and calls processNode. + void processInputs(const std::vector &inputs, int numFrames) override { + if (requiresTailProcessing_) { + updateTailStateForQuantum(inputs, numFrames); + } + + getInputBuffer()->zero(); + + for (const DSPAudioBuffer *input : inputs) { + getInputBuffer()->sum(*input, channelInterpretation_); + } - int numberOfEnabledInputNodes_{0}; - std::atomic isInitialized_{false}; + processNode(numFrames); + } + + /// @brief Returns the tail length in audio frames for the current node + /// state. Called when transitioning into `SIGNALLED_TO_STOP` so that the + /// frame counter starts from a fresh, up-to-date value. + /// + /// Default 0 — overriders are tail-bearing nodes (Biquad, Convolver, + /// DelayWriter). The implementation may read audio-thread-owned state + /// (filter coefficients, IR length, max delay). + /// + /// @note Audio Thread only. + [[nodiscard]] virtual int computeTailFrames() const { + return 0; + } - std::size_t lastRenderedFrame_{SIZE_MAX}; + /// @brief Returns whether the set of input buffers carries no signal this + /// quantum. Default scans every sample. Tail-bearing nodes whose audio + /// inputs come from outside the graph (none today, but plausible) may + /// override. + /// + /// @note Audio Thread only. + [[nodiscard]] virtual bool isInputSilent(const std::vector &inputs) const; - void enable(); - virtual void disable(); + virtual void processNode(int) = 0; private: - bool isEnabled_ = true; - std::vector> inputBuffers_; - - virtual std::shared_ptr processInputs( - const std::shared_ptr &outputBuffer, - int framesToProcess, - bool checkIsAlreadyProcessed); - virtual std::shared_ptr processNode( - const std::shared_ptr &, - int) = 0; - - bool isAlreadyProcessed(); - std::shared_ptr applyChannelCountMode( - const std::shared_ptr &processingBuffer); - void mixInputsBuffers(const std::shared_ptr &processingBuffer); - - void connectNode(const std::shared_ptr &node); - void disconnectNode(const std::shared_ptr &node); - void connectParam(const std::shared_ptr ¶m); - void disconnectParam(const std::shared_ptr ¶m); - - void onInputEnabled(); - virtual void onInputDisabled(); - void onInputConnected(AudioNode *node); - void onInputDisconnected(AudioNode *node); - - void cleanup(); + /// @brief Drives `tailState_` for one render quantum. Called from + /// `processInputs` only when `requiresTailProcessing_` is true. + void updateTailStateForQuantum(const std::vector &inputs, int numFrames); }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp index a552c3869..b0512ddaa 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp @@ -19,12 +19,11 @@ AudioParam::AudioParam( defaultValue_(defaultValue), minValue_(minValue), maxValue_(maxValue), - eventRenderQueue_(defaultValue), - audioBuffer_( - std::make_shared(RENDER_QUANTUM_SIZE, 1, context->getSampleRate())) { - inputBuffers_.reserve(4); - inputNodes_.reserve(4); -} + inputBuffer_( + std::make_shared(RENDER_QUANTUM_SIZE, 1, context->getSampleRate())), + outputBuffer_( + std::make_shared(RENDER_QUANTUM_SIZE, 1, context->getSampleRate())), + eventRenderQueue_(defaultValue) {} float AudioParam::getValueAtTime(double time) { auto value = eventRenderQueue_.computeValueAtTime(time); @@ -68,89 +67,41 @@ void AudioParam::cancelAndHoldAtTime(double cancelTime) { eventRenderQueue_.cancelAndHoldAtTime(cancelTime); } -void AudioParam::addInputNode(AudioNode *node) { - inputNodes_.emplace_back(node); -} - -void AudioParam::removeInputNode(AudioNode *node) { - for (int i = 0; i < inputNodes_.size(); i++) { - if (inputNodes_[i] == node) { - std::swap(inputNodes_[i], inputNodes_.back()); - inputNodes_.resize(inputNodes_.size() - 1); - break; - } - } -} - -std::shared_ptr AudioParam::calculateInputs( - const std::shared_ptr &processingBuffer, - int framesToProcess) { - processingBuffer->zero(); - if (inputNodes_.empty()) { - return processingBuffer; - } - processInputs(processingBuffer, framesToProcess, true); - mixInputsBuffers(processingBuffer); - return processingBuffer; -} - std::shared_ptr AudioParam::processARateParam(int framesToProcess, double time) { - auto processingBuffer = calculateInputs(audioBuffer_, framesToProcess); - std::shared_ptr context = context_.lock(); if (context == nullptr) { - return processingBuffer; + outputBuffer_->zero(); + return outputBuffer_; } + float sampleRate = context->getSampleRate(); - auto bufferData = processingBuffer->getChannel(0)->span(); double timeCache = time; - float timeStep = 1.0f / sampleRate; - float sample = 0.0f; + double timeStep = 1.0 / sampleRate; - // Add automated parameter value to each sample - for (int i = 0; i < framesToProcess; i++, timeCache += timeStep) { - sample = getValueAtTime(timeCache); - bufferData[i] += sample; - } - // processingBuffer is a mono buffer containing per-sample parameter values - return processingBuffer; -} + // Read modulation from input buffer (filled by BridgeNode if connected, otherwise zeros) + auto inputData = inputBuffer_->getChannel(0)->span(); + auto outputData = outputBuffer_->getChannel(0)->span(); -float AudioParam::processKRateParam(int framesToProcess, double time) { - auto processingBuffer = calculateInputs(audioBuffer_, framesToProcess); + // Compute: modulation + automated parameter value → output buffer + for (size_t i = 0; i < framesToProcess; i++, timeCache += timeStep) { + outputData[i] = inputData[i] + getValueAtTime(timeCache); + } - // Return block-rate parameter value plus first sample of input modulation - return processingBuffer->getChannel(0)->span()[0] + getValueAtTime(time); -} + // Zero the input buffer so next frame starts clean if no BridgeNode refills it + inputBuffer_->zero(); -void AudioParam::processInputs( - const std::shared_ptr &outputBuffer, - int framesToProcess, - bool checkIsAlreadyProcessed) { - for (auto *inputNode : inputNodes_) { - assert(inputNode != nullptr); - - if (!inputNode->isEnabled()) { - continue; - } - - // Process this input node and store its output buffer - auto inputBuffer = - inputNode->processAudio(outputBuffer, framesToProcess, checkIsAlreadyProcessed); - inputBuffers_.emplace_back(inputBuffer); - } + return outputBuffer_; } -void AudioParam::mixInputsBuffers(const std::shared_ptr &processingBuffer) { - assert(processingBuffer != nullptr); +float AudioParam::processKRateParam(int framesToProcess, double time) { + // Return block-rate parameter value plus first sample of input modulation + float modulation = inputBuffer_->getChannel(0)->span()[0]; + float result = modulation + getValueAtTime(time); - // Sum all input buffers into the processing buffer - for (auto &inputBuffer : inputBuffers_) { - processingBuffer->sum(*inputBuffer, ChannelInterpretation::SPEAKERS); - } + // Zero the input buffer so next frame starts clean if no BridgeNode refills it + inputBuffer_->zero(); - // Clear for next processing cycle - inputBuffers_.clear(); + return result; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h index bb5702b0d..db990799b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -11,7 +10,6 @@ #include #include #include -#include namespace audioapi { @@ -90,11 +88,11 @@ class AudioParam { /// Audio-Thread only methods /// These methods are called only from the Audio rendering thread. + /// @brief Returns the input buffer where BridgeNode stores mixed modulation signals. /// @note Audio Thread only - void addInputNode(AudioNode *node); - - /// @note Audio Thread only - void removeInputNode(AudioNode *node); + [[nodiscard]] std::shared_ptr getInputBuffer() const { + return inputBuffer_; + } /// @note Audio Thread only std::shared_ptr processARateParam(int framesToProcess, double time); @@ -112,10 +110,10 @@ class AudioParam { ParamRenderQueue eventRenderQueue_; - // Input modulation system - std::vector inputNodes_; - std::shared_ptr audioBuffer_; - std::vector> inputBuffers_; + // Input modulation buffer - filled by BridgeNode during graph processing + std::shared_ptr inputBuffer_; + // Output buffer for a-rate processing - contains modulation + param value + std::shared_ptr outputBuffer_; /// @brief Update the parameter queue with a new event. /// @param event The new event to add to the queue. @@ -126,14 +124,6 @@ class AudioParam { } float getValueAtTime(double time); - void processInputs( - const std::shared_ptr &outputBuffer, - int framesToProcess, - bool checkIsAlreadyProcessed); - void mixInputsBuffers(const std::shared_ptr &processingBuffer); - std::shared_ptr calculateInputs( - const std::shared_ptr &processingBuffer, - int framesToProcess); }; } // 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 41e95a136..8aa41b010 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 @@ -1,34 +1,11 @@ #include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if !RN_AUDIO_API_FFMPEG_DISABLED -#include -#endif // RN_AUDIO_API_FFMPEG_DISABLED -#include #include -#include #include #include #include #include #include -#include #include namespace audioapi { @@ -39,13 +16,16 @@ BaseAudioContext::BaseAudioContext( const RuntimeRegistry &runtimeRegistry) : state_(ContextState::SUSPENDED), sampleRate_(sampleRate), - graphManager_(std::make_shared()), audioEventHandlerRegistry_(audioEventHandlerRegistry), runtimeRegistry_(runtimeRegistry), - audioEventScheduler_(AUDIO_SCHEDULER_CAPACITY) {} + audioEventScheduler_(AUDIO_SCHEDULER_CAPACITY), + gcAudioEventScheduler_(GC_AUDIO_SCHEDULER_CAPACITY), + disposer_( + std::make_unique>(AUDIO_SCHEDULER_CAPACITY)), + graph_(std::make_shared(AUDIO_SCHEDULER_CAPACITY, disposer_.get())) {} -void BaseAudioContext::initialize() { - destination_ = std::make_shared(shared_from_this()); +void BaseAudioContext::initialize(const AudioDestinationNode *destination) { + destination_ = destination; } ContextState BaseAudioContext::getState() { @@ -63,143 +43,17 @@ float BaseAudioContext::getSampleRate() const { } std::size_t BaseAudioContext::getCurrentSampleFrame() const { - assert(destination_ != nullptr); - return destination_->getCurrentSampleFrame(); + return currentSampleFrame_.load(std::memory_order_acquire); } double BaseAudioContext::getCurrentTime() const { - assert(destination_ != nullptr); - return destination_->getCurrentTime(); -} - -std::shared_ptr BaseAudioContext::getDestination() const { - return destination_; + return static_cast(getCurrentSampleFrame()) / getSampleRate(); } void BaseAudioContext::setState(audioapi::ContextState state) { state_.store(state, std::memory_order_release); } -std::shared_ptr BaseAudioContext::createWorkletSourceNode( - std::shared_ptr &shareableWorklet, - std::weak_ptr runtime, - bool shouldLockRuntime) { - WorkletsRunner workletRunner(runtime, shareableWorklet, shouldLockRuntime); - auto workletSourceNode = - std::make_shared(shared_from_this(), std::move(workletRunner)); - graphManager_->addSourceNode(workletSourceNode); - return workletSourceNode; -} - -std::shared_ptr BaseAudioContext::createWorkletNode( - std::shared_ptr &shareableWorklet, - std::weak_ptr runtime, - size_t bufferLength, - size_t inputChannelCount, - bool shouldLockRuntime) { - WorkletsRunner workletRunner(runtime, shareableWorklet, shouldLockRuntime); - auto workletNode = std::make_shared( - shared_from_this(), bufferLength, inputChannelCount, std::move(workletRunner)); - graphManager_->addProcessingNode(workletNode); - return workletNode; -} - -std::shared_ptr BaseAudioContext::createWorkletProcessingNode( - std::shared_ptr &shareableWorklet, - std::weak_ptr runtime, - bool shouldLockRuntime) { - WorkletsRunner workletRunner(runtime, shareableWorklet, shouldLockRuntime); - auto workletProcessingNode = - std::make_shared(shared_from_this(), std::move(workletRunner)); - graphManager_->addProcessingNode(workletProcessingNode); - return workletProcessingNode; -} - -std::shared_ptr BaseAudioContext::createRecorderAdapter() { - auto recorderAdapter = std::make_shared(shared_from_this()); - graphManager_->addProcessingNode(recorderAdapter); - return recorderAdapter; -} - -std::shared_ptr BaseAudioContext::createOscillator( - const OscillatorOptions &options) { - auto oscillator = std::make_shared(shared_from_this(), options); - graphManager_->addSourceNode(oscillator); - return oscillator; -} - -std::shared_ptr BaseAudioContext::createConstantSource( - const ConstantSourceOptions &options) { - auto constantSource = std::make_shared(shared_from_this(), options); - graphManager_->addSourceNode(constantSource); - return constantSource; -} - -std::shared_ptr BaseAudioContext::createStreamer(const StreamerOptions &options) { -#if !RN_AUDIO_API_FFMPEG_DISABLED - auto streamer = std::make_shared(shared_from_this(), options); - graphManager_->addSourceNode(streamer); - return streamer; -#else - return nullptr; -#endif // RN_AUDIO_API_FFMPEG_DISABLED -} - -std::shared_ptr BaseAudioContext::createGain(const GainOptions &options) { - auto gain = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(gain); - return gain; -} - -std::shared_ptr BaseAudioContext::createStereoPanner( - const StereoPannerOptions &options) { - auto stereoPanner = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(stereoPanner); - return stereoPanner; -} - -std::shared_ptr BaseAudioContext::createDelay(const DelayOptions &options) { - auto delay = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(delay); - return delay; -} - -std::shared_ptr BaseAudioContext::createBiquadFilter( - const BiquadFilterOptions &options) { - auto biquadFilter = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(biquadFilter); - return biquadFilter; -} - -std::shared_ptr BaseAudioContext::createBufferSource( - const AudioBufferSourceOptions &options) { - auto bufferSource = std::make_shared(shared_from_this(), options); - graphManager_->addSourceNode(bufferSource); - return bufferSource; -} - -#if !RN_AUDIO_API_TEST -std::shared_ptr BaseAudioContext::createFileSource( - const AudioFileSourceOptions &options) { - auto fileSource = std::make_shared(shared_from_this(), options); - graphManager_->addSourceNode(fileSource); - return fileSource; -} -#endif // RN_AUDIO_API_TEST - -std::shared_ptr BaseAudioContext::createIIRFilter(const IIRFilterOptions &options) { - auto iirFilter = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(iirFilter); - return iirFilter; -} - -std::shared_ptr BaseAudioContext::createBufferQueueSource( - const BaseAudioBufferSourceOptions &options) { - auto bufferSource = std::make_shared(shared_from_this(), options); - graphManager_->addSourceNode(bufferSource); - return bufferSource; -} - std::shared_ptr BaseAudioContext::createPeriodicWave( const std::vector> &complexData, bool disableNormalization, @@ -207,25 +61,6 @@ std::shared_ptr BaseAudioContext::createPeriodicWave( return std::make_shared(getSampleRate(), complexData, length, disableNormalization); } -std::shared_ptr BaseAudioContext::createAnalyser(const AnalyserOptions &options) { - auto analyser = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(analyser); - return analyser; -} - -std::shared_ptr BaseAudioContext::createConvolver(const ConvolverOptions &options) { - auto convolver = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(convolver); - return convolver; -} - -std::shared_ptr BaseAudioContext::createWaveShaper( - const WaveShaperOptions &options) { - auto waveShaper = std::make_shared(shared_from_this(), options); - graphManager_->addProcessingNode(waveShaper); - return waveShaper; -} - std::shared_ptr BaseAudioContext::getBasicWaveForm(OscillatorType type) { switch (type) { case OscillatorType::SINE: @@ -254,8 +89,8 @@ std::shared_ptr BaseAudioContext::getBasicWaveForm(OscillatorType } } -std::shared_ptr BaseAudioContext::getGraphManager() const { - return graphManager_; +std::shared_ptr BaseAudioContext::getGraph() const { + return graph_; } std::shared_ptr BaseAudioContext::getAudioEventHandlerRegistry() const { @@ -266,4 +101,25 @@ const RuntimeRegistry &BaseAudioContext::getRuntimeRegistry() const { return runtimeRegistry_; } +utils::DisposerImpl *BaseAudioContext::getDisposer() const { + return disposer_.get(); +} + +void BaseAudioContext::processGraph(DSPAudioBuffer *buffer, int numFrames) { + processAudioEvents(); + graph_->processEvents(); + graph_->process(); + + for (auto &&[node, inputs] : graph_->iter()) { + node.process(inputs, numFrames); + + // Copy destination output to the final buffer + if (auto *audioNode = node.asAudioNode(); audioNode == destination_) { + buffer->copy(*audioNode->getOutputBuffer(), 0, 0, numFrames); + } + } + + currentSampleFrame_.fetch_add(numFrames, std::memory_order_release); +} + } // namespace audioapi 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 1cbf100b8..3e8dab563 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 @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include #include #include @@ -17,110 +20,51 @@ namespace audioapi { -class GainNode; -class DelayNode; -class PeriodicWave; -class OscillatorNode; -class ConstantSourceNode; -class StereoPannerNode; -class AudioGraphManager; -class BiquadFilterNode; -class IIRFilterNode; -class AudioDestinationNode; -class AudioBufferSourceNode; -class AudioBufferQueueSourceNode; -class AudioFileSourceNode; -class AnalyserNode; class AudioEventHandlerRegistry; -class ConvolverNode; class IAudioEventHandlerRegistry; -class RecorderAdapterNode; -class WaveShaperNode; -class WorkletSourceNode; -class WorkletNode; -class WorkletProcessingNode; -class StreamerNode; -struct GainOptions; -struct StereoPannerOptions; -struct ConvolverOptions; -struct ConstantSourceOptions; -struct AnalyserOptions; -struct BiquadFilterOptions; -struct OscillatorOptions; -struct BaseAudioBufferSourceOptions; -struct AudioBufferSourceOptions; -struct AudioFileSourceOptions; -struct StreamerOptions; -struct DelayOptions; -struct IIRFilterOptions; -struct WaveShaperOptions; +class PeriodicWave; +class AudioDestinationNode; class BaseAudioContext : public std::enable_shared_from_this { public: - DELETE_COPY_AND_MOVE(BaseAudioContext); - explicit BaseAudioContext( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, const RuntimeRegistry &runtimeRegistry); virtual ~BaseAudioContext() = default; + DELETE_COPY_AND_MOVE(BaseAudioContext); ContextState getState(); [[nodiscard]] float getSampleRate() const; [[nodiscard]] double getCurrentTime() const; [[nodiscard]] std::size_t getCurrentSampleFrame() const; - [[nodiscard]] std::shared_ptr getDestination() const; void setState(ContextState state); - std::shared_ptr createRecorderAdapter(); - std::shared_ptr createWorkletSourceNode( - std::shared_ptr &shareableWorklet, - std::weak_ptr runtime, - bool shouldLockRuntime = true); - std::shared_ptr createWorkletNode( - std::shared_ptr &shareableWorklet, - std::weak_ptr runtime, - size_t bufferLength, - size_t inputChannelCount, - bool shouldLockRuntime = true); - std::shared_ptr createWorkletProcessingNode( - std::shared_ptr &shareableWorklet, - std::weak_ptr runtime, - bool shouldLockRuntime = true); - std::shared_ptr createDelay(const DelayOptions &options); - std::shared_ptr createIIRFilter(const IIRFilterOptions &options); - std::shared_ptr createOscillator(const OscillatorOptions &options); - std::shared_ptr createConstantSource(const ConstantSourceOptions &options); - std::shared_ptr createStreamer(const StreamerOptions &options); - std::shared_ptr createGain(const GainOptions &options); - std::shared_ptr createStereoPanner(const StereoPannerOptions &options); - std::shared_ptr createBiquadFilter(const BiquadFilterOptions &options); - std::shared_ptr createBufferSource( - const AudioBufferSourceOptions &options); -#if !RN_AUDIO_API_TEST - std::shared_ptr createFileSource(const AudioFileSourceOptions &options); -#endif // RN_AUDIO_API_TEST - std::shared_ptr createBufferQueueSource( - const BaseAudioBufferSourceOptions &options); - [[nodiscard]] std::shared_ptr createPeriodicWave( - const std::vector> - &complexData, // NOLINT(readability-avoid-const-params-in-decls) + std::shared_ptr createPeriodicWave( + const std::vector> &complexData, bool disableNormalization, int length) const; - std::shared_ptr createAnalyser(const AnalyserOptions &options); - std::shared_ptr createConvolver(const ConvolverOptions &options); - std::shared_ptr createWaveShaper(const WaveShaperOptions &options); std::shared_ptr getBasicWaveForm(OscillatorType type); - [[nodiscard]] std::shared_ptr getGraphManager() const; - [[nodiscard]] std::shared_ptr getAudioEventHandlerRegistry() const; - [[nodiscard]] const RuntimeRegistry &getRuntimeRegistry() const; + std::shared_ptr getGraph() const; + std::shared_ptr getAudioEventHandlerRegistry() const; + const RuntimeRegistry &getRuntimeRegistry() const; + utils::DisposerImpl *getDisposer() const; - virtual void initialize(); + /// @brief Assigns the audio destination node to the context. + /// @param destination The audio destination node to be associated with the context. + /// @note This method must be called before the audio context can be used for processing audio. + virtual void initialize(const AudioDestinationNode *destination); void processAudioEvents() { + // Drain JS-thread scheduler first, then the GC-thread scheduler. + // This preserves causality for the common case where a JS-thread + // produced event logically happens-before a GC-thread cleanup event + // on the same node (e.g. `onEnded = id` followed by HostObject + // finalization that clears the callback id). audioEventScheduler_.processAllEvents(*this); + gcAudioEventScheduler_.processAllEvents(*this); } template @@ -134,13 +78,33 @@ class BaseAudioContext : public std::enable_shared_from_this { return audioEventScheduler_.scheduleEvent(std::forward(event)); } + /// @brief Schedule an audio event produced on the JS runtime's finalizer + /// (GC) thread. Uses a dedicated SPSC channel so the main + /// `audioEventScheduler_` stays single-producer (JS thread). + /// + /// Call this from JSI HostObject destructors (which on Hermes fire on the + /// concurrent GC thread) instead of `scheduleAudioEvent`, otherwise the + /// GC thread and the JS thread will race on the same SPSC channel. + /// + /// @note Unlike `scheduleAudioEvent`, this does NOT fall through to a + /// synchronous `event(*this)` when the context is not RUNNING. The + /// finalizer thread must never touch context state directly, and the + /// event handler itself must be safe to skip if the audio thread never + /// drains it (e.g. context already closed). + template + bool scheduleGCEvent(F &&event) noexcept { // NOLINT(cppcoreguidelines-missing-std-forward) + return gcAudioEventScheduler_.scheduleEvent(std::forward(event)); + } + + void processGraph(DSPAudioBuffer *buffer, int numFrames); + protected: - std::shared_ptr destination_; + std::atomic currentSampleFrame_{0}; + const AudioDestinationNode *destination_; private: std::atomic state_; std::atomic sampleRate_; - std::shared_ptr graphManager_; std::shared_ptr audioEventHandlerRegistry_; RuntimeRegistry runtimeRegistry_; @@ -150,7 +114,15 @@ class BaseAudioContext : public std::enable_shared_from_this { std::shared_ptr cachedTriangleWave_ = nullptr; static constexpr size_t AUDIO_SCHEDULER_CAPACITY = 1024; + static constexpr size_t GC_AUDIO_SCHEDULER_CAPACITY = 256; CrossThreadEventScheduler audioEventScheduler_; + /// Dedicated single-producer SPSC scheduler for events produced on the + /// JS runtime's finalizer (GC) thread. Keeps `audioEventScheduler_` + /// single-producer (JS thread) and avoids the MPSC-on-SPSC data race. + CrossThreadEventScheduler gcAudioEventScheduler_; + + std::unique_ptr> disposer_; + std::shared_ptr graph_; [[nodiscard]] virtual bool isDriverRunning() const = 0; }; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp index 6aca0206d..133ce6cb4 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -29,10 +28,6 @@ OfflineAudioContext::OfflineAudioContext( std::make_shared(RENDER_QUANTUM_SIZE, numberOfChannels, sampleRate)), resultBuffer_(std::make_shared(length, numberOfChannels, sampleRate)) {} -OfflineAudioContext::~OfflineAudioContext() { - getGraphManager()->cleanup(); -} - void OfflineAudioContext::resume() { Locker locker(mutex_); @@ -70,7 +65,7 @@ void OfflineAudioContext::renderAudio() { std::min(static_cast(length_ - currentSampleFrame_), RENDER_QUANTUM_SIZE); audioBuffer_->zero(); - destination_->renderAudio(audioBuffer_, framesToProcess); + processGraph(audioBuffer_.get(), framesToProcess); resultBuffer_->copy(*audioBuffer_, 0, currentSampleFrame_, framesToProcess); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h index 95a7c0017..dc5eadcbb 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.h @@ -22,7 +22,7 @@ class OfflineAudioContext : public BaseAudioContext { float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, const RuntimeRegistry &runtimeRegistry); - ~OfflineAudioContext() override; + ~OfflineAudioContext() override = default; DELETE_COPY_AND_MOVE(OfflineAudioContext); /// @note JS Thread only diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.cpp index 6c32c1c73..d6ec30849 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.cpp @@ -21,7 +21,7 @@ AnalyserNode::AnalyserNode( maxDecibels_(options.maxDecibels), smoothingTimeConstant_(options.smoothingTimeConstant) { setFFTSize(options.fftSize); - isInitialized_.store(true, std::memory_order_release); + setProcessableState(GraphObject::PROCESSABLE_STATE::ALWAYS_PROCESSABLE); } void AnalyserNode::setFFTSize(int fftSize) { @@ -88,14 +88,12 @@ void AnalyserNode::getByteTimeDomainData(uint8_t *data, int length) { } } -std::shared_ptr AnalyserNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void AnalyserNode::processNode(int framesToProcess) { // Analyser should behave like a sniffer node, it should not modify the - // processingBuffer but instead copy the data to its own input buffer. + // audioBuffer_ but instead copy the data to its own input buffer. // Down mix the input buffer to mono - downMixBuffer_->copy(*processingBuffer); + downMixBuffer_->copy(*audioBuffer_); // Copy the down mixed buffer to the input buffer (circular buffer) inputArray_->push_back(*downMixBuffer_->getChannel(0), framesToProcess, true); @@ -106,8 +104,6 @@ std::shared_ptr AnalyserNode::processNode( frame->sequenceNumber = ++publishSequence_; inputArray_->pop_back(frame->timeDomain, fftSize, 0, true); analysisBuffer_.publish(); - - return processingBuffer; } void AnalyserNode::doFFTAnalysis() { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.h index b38ce3010..b6d653576 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/analysis/AnalyserNode.h @@ -75,9 +75,7 @@ class AnalyserNode : public AudioNode { void getByteTimeDomainData(uint8_t *data, int length); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: std::atomic fftSize_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp deleted file mode 100644 index e7fcd1d40..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -namespace audioapi { - -AudioDestinationNode::AudioDestinationNode(const std::shared_ptr &context) - : AudioNode(context, AudioDestinationOptions()), currentSampleFrame_(0) { - isInitialized_.store(true, std::memory_order_release); -} - -std::size_t AudioDestinationNode::getCurrentSampleFrame() const { - return currentSampleFrame_.load(std::memory_order_acquire); -} - -double AudioDestinationNode::getCurrentTime() const { - return static_cast(getCurrentSampleFrame()) / getContextSampleRate(); -} - -void AudioDestinationNode::renderAudio( - const std::shared_ptr &destinationBuffer, - int numFrames) { - if (numFrames < 0 || !destinationBuffer || !isInitialized_.load(std::memory_order_acquire)) { - return; - } - - if (std::shared_ptr context = context_.lock()) { - context->processAudioEvents(); - context->getGraphManager()->preProcessGraph(); - } - - destinationBuffer->zero(); - - auto processedBuffer = processAudio(destinationBuffer, numFrames, true); - - if (processedBuffer && processedBuffer != destinationBuffer) { - destinationBuffer->copy(*processedBuffer); - } - - destinationBuffer->normalize(); - - currentSampleFrame_.fetch_add(numFrames, std::memory_order_release); -} - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.h index 2a61c844e..221726113 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.h @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -14,28 +13,15 @@ class BaseAudioContext; class AudioDestinationNode : public AudioNode { public: - explicit AudioDestinationNode(const std::shared_ptr &context); - - /// @note Thread safe - std::size_t getCurrentSampleFrame() const; - - /// @note Thread safe - double getCurrentTime() const; - - /// @note Audio Thread only - void renderAudio(const std::shared_ptr &audioData, int numFrames); + explicit AudioDestinationNode(const std::shared_ptr &context) + : AudioNode(context, AudioDestinationOptions()) { + processableState_ = GraphObject::PROCESSABLE_STATE::ALWAYS_PROCESSABLE; + } protected: - // DestinationNode is triggered by AudioContext using renderAudio - // processNode function is not necessary and is never called. - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) final { - return processingBuffer; + void processNode(int) final { + audioBuffer_->normalize(); }; - - private: - std::atomic currentSampleFrame_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.cpp index c7dc8fb3a..0f4071b75 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.cpp @@ -32,6 +32,8 @@ #include #include +#include +#include #include // https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html - math @@ -68,9 +70,7 @@ BiquadFilterNode::BiquadFilterNode( x1_(MAX_CHANNEL_COUNT), x2_(MAX_CHANNEL_COUNT), y1_(MAX_CHANNEL_COUNT), - y2_(MAX_CHANNEL_COUNT) { - isInitialized_.store(true, std::memory_order_release); -} + y2_(MAX_CHANNEL_COUNT) {} void BiquadFilterNode::setType(BiquadFilterType type) { type_ = type; @@ -381,9 +381,7 @@ BiquadFilterNode::FilterCoefficients BiquadFilterNode::applyFilter( return coeffs; } -std::shared_ptr BiquadFilterNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void BiquadFilterNode::processNode(int framesToProcess) { if (std::shared_ptr context = context_.lock()) { auto currentTime = context->getCurrentTime(); float frequency = frequencyParam_->processKRateParam(RENDER_QUANTUM_SIZE, currentTime); @@ -393,12 +391,15 @@ std::shared_ptr BiquadFilterNode::processNode( auto coeffs = applyFilter(frequency, Q, gain, detune, type_); + // Cache for computeTailFrames(); read on the same thread. + lastA2_ = coeffs.a2; + float x1, x2, y1, y2; // NOLINT(cppcoreguidelines-init-variables) - auto numChannels = processingBuffer->getNumberOfChannels(); + auto numChannels = audioBuffer_->getNumberOfChannels(); for (size_t c = 0; c < numChannels; ++c) { - auto channel = processingBuffer->getChannel(c)->subSpan(framesToProcess); + auto channel = audioBuffer_->getChannel(c)->subSpan(framesToProcess); x1 = x1_[c]; x2 = x2_[c]; @@ -429,10 +430,26 @@ std::shared_ptr BiquadFilterNode::processNode( y2_[c] = y2; } } else { - processingBuffer->zero(); + audioBuffer_->zero(); } +} - return processingBuffer; +int BiquadFilterNode::computeTailFrames() const { + // For a stable biquad H(z) = (b0 + b1 z^-1 + b2 z^-2) / (1 + a1 z^-1 + a2 z^-2), + // the poles are roots of z^2 + a1 z + a2. Their product equals a2, so the + // larger pole magnitude is bounded by sqrt(|a2|). Using that as `r`, the + // impulse decays roughly like `r^n`; the number of frames until it drops + // below kTailEpsilon is `ceil(log(eps)/log(r))`. + const double r = std::min(std::sqrt(std::abs(lastA2_)), 0.999); + if (r <= 0.0) { + return 0; + } + const double frames = std::ceil(std::log(kTailEpsilon) / std::log(r)); + const int cap = static_cast(kMaxTailSeconds * getContextSampleRate()); + if (!std::isfinite(frames) || frames <= 0.0) { + return 0; + } + return std::min(static_cast(frames), cap); } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.h index 47288f131..f9adbf8e1 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/BiquadFilterNode.h @@ -68,17 +68,31 @@ class BiquadFilterNode : public AudioNode { BiquadFilterType type); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; + + /// @brief IIR tail length, derived from the dominant pole magnitude of the + /// most recently applied filter coefficients (`r ≈ sqrt(|a2|)`). Returns + /// the number of frames until the impulse response decays below + /// `kTailEpsilon`, capped at `kMaxTailSeconds * sampleRate`. + /// @note Audio Thread only. + [[nodiscard]] int computeTailFrames() const override; private: + static constexpr double kTailEpsilon = 1e-4; // -80 dB + static constexpr float kMaxTailSeconds = 0.5f; // safety cap + const std::shared_ptr frequencyParam_; const std::shared_ptr detuneParam_; const std::shared_ptr QParam_; const std::shared_ptr gainParam_; BiquadFilterType type_; + /// Most recently applied `a2` coefficient, cached on the audio thread by + /// `processNode`. Used by `computeTailFrames` to estimate decay length. + /// `r = sqrt(|a2|)` is a conservative upper bound on the dominant pole + /// magnitude for stable biquads. + double lastA2_ = 0.0; + // delayed samples, one per channel DSPAudioArray x1_; DSPAudioArray x2_; 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 fa659999e..8ff0f898d 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 @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -16,15 +15,11 @@ ConvolverNode::ConvolverNode( const ConvolverOptions &options) : AudioNode(context, options), gainCalibrationSampleRate_(context->getSampleRate()), - remainingSegments_(0), internalBufferIndex_(0), - signalledToStop_(false), scaleFactor_(1.0f), intermediateBuffer_(nullptr), buffer_(nullptr), - internalBuffer_(nullptr) { - isInitialized_.store(true, std::memory_order_release); -} + internalBuffer_(nullptr) {} void ConvolverNode::setBuffer( const std::shared_ptr &buffer, @@ -38,13 +33,25 @@ void ConvolverNode::setBuffer( return; } - auto graphManager = context->getGraphManager(); + if (buffer_ != nullptr) { + context->getDisposer()->dispose(std::move(buffer_)); + } + + if (threadPool_ != nullptr) { + context->getDisposer()->dispose(std::move(threadPool_)); + } + + for (auto &convolver : convolvers_) { + context->getDisposer()->dispose(std::move(convolver)); + } - if (buffer_) { - graphManager->addAudioBufferForDestruction(std::move(buffer_)); + if (internalBuffer_ != nullptr) { + context->getDisposer()->dispose(std::move(internalBuffer_)); } - // TODO move convolvers, thread pool and DSPAudioBuffers destruction to graph manager as well + if (intermediateBuffer_ != nullptr) { + context->getDisposer()->dispose(std::move(intermediateBuffer_)); + } buffer_ = buffer; convolvers_ = std::move(convolvers); @@ -53,6 +60,13 @@ void ConvolverNode::setBuffer( intermediateBuffer_ = intermediateBuffer; scaleFactor_ = scaleFactor; internalBufferIndex_ = 0; + + // Re-arm the tail: a brand-new IR may have a completely different length, + // and any pending tail countdown from the previous IR is now meaningless. + // The base-class state machine will recompute computeTailFrames() the next + // time the input goes silent. + tailState_ = TailState::ACTIVE; + tailFramesRemaining_ = 0; } float ConvolverNode::calculateNormalizationScale(const std::shared_ptr &buffer) const { @@ -80,40 +94,34 @@ float ConvolverNode::calculateNormalizationScale(const std::shared_ptrgetSegCount(); +// processing pipeline: audioBuffer_ (input) -> intermediateBuffer_ -> audioBuffer_ (output) +void ConvolverNode::processNode(int framesToProcess) { + if (buffer_ == nullptr) { + return; } -} -// processing pipeline: processingBuffer -> intermediateBuffer_ -> audioBuffer_ (mixing -// with intermediateBuffer_) -std::shared_ptr ConvolverNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { - if (processingBuffer->getSize() != RENDER_QUANTUM_SIZE) { + if (framesToProcess != RENDER_QUANTUM_SIZE) { printf( "[AUDIOAPI WARN] convolver requires 128 buffer size for each render quantum, otherwise quality of convolution is very poor\n"); } - if (signalledToStop_) { - if (remainingSegments_ > 0) { - remainingSegments_--; - } else { - disable(); - signalledToStop_ = false; - internalBufferIndex_ = 0; - return processingBuffer; - } + + // Once the base-class tail counter has fully drained, stop convolving and + // emit silence; the IR's contribution has decayed beyond audibility. + if (tailState_ == TailState::FINISHED) { + audioBuffer_->zero(); + internalBufferIndex_ = 0; + return; } + if (internalBufferIndex_ < framesToProcess) { - performConvolution(processingBuffer); // result returned to intermediateBuffer_ + performConvolution(audioBuffer_); // reads from audioBuffer_, result goes to intermediateBuffer_ + audioBuffer_->zero(); audioBuffer_->sum(*intermediateBuffer_); internalBuffer_->copy(*audioBuffer_, 0, internalBufferIndex_, RENDER_QUANTUM_SIZE); internalBufferIndex_ += RENDER_QUANTUM_SIZE; } + audioBuffer_->zero(); audioBuffer_->copy(*internalBuffer_, 0, 0, framesToProcess); auto remainingFrames = static_cast(internalBufferIndex_ - framesToProcess); @@ -128,8 +136,13 @@ std::shared_ptr ConvolverNode::processNode( for (int i = 0; i < audioBuffer_->getNumberOfChannels(); ++i) { audioBuffer_->getChannel(i)->scale(scaleFactor_); } +} - return audioBuffer_; +int ConvolverNode::computeTailFrames() const { + // The convolver's impulse response equals the IR buffer itself, so a full + // tail equals one IR length of samples. If no IR has been set yet, there + // is nothing to ring out. + return buffer_ ? static_cast(buffer_->getSize()) : 0; } void ConvolverNode::performConvolution(const std::shared_ptr &processingBuffer) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.h index 832dd5c57..5c1486822 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ConvolverNode.h @@ -36,16 +36,17 @@ class ConvolverNode : public AudioNode { float calculateNormalizationScale(const std::shared_ptr &buffer) const; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; + + /// @brief Tail length equals the impulse-response length in frames. A + /// freshly loaded IR makes the convolver ring for at least its own length + /// after the input goes silent. + /// @note Audio Thread only. + [[nodiscard]] int computeTailFrames() const override; private: - void onInputDisabled() override; const float gainCalibrationSampleRate_; - size_t remainingSegments_; size_t internalBufferIndex_; - bool signalledToStop_; float scaleFactor_; std::shared_ptr intermediateBuffer_; 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 61c41b9f4..7dd37cd4f 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 @@ -1,5 +1,9 @@ #include #include +#include +#include +#include +#include #include #include #include @@ -19,97 +23,27 @@ DelayNode::DelayNode(const std::shared_ptr &context, const Del 1), // +1 to enable delayTime equal to maxDelayTime channelCount_, context->getSampleRate())) { - isInitialized_.store(true, std::memory_order_release); + delayLine_ = std::make_shared(delayBuffer_, delayTimeParam_); + + // Tail processing belongs to the writer: when the writer's upstream goes + // silent, the writer must keep filling the ring with zeros for one full + // `maxDelayTime` so that the reader naturally drains to silence. The + // reader has no audio inputs (its data comes from the shared ring) and is + // GC'd via the linkNodes(reader, writer) propagation, so it sets + // `requiresTailProcessing = false` to opt out of the base-class state + // machine that would otherwise consider it permanently "silent". + AudioNodeOptions readerOptions = options; + // Reader has no audio inputs (its data comes from the shared ring) and + // does not own the tail — see comment above. + readerOptions.numberOfInputs = 0; + readerOptions.requiresTailProcessing = false; + + delayReader_ = std::make_unique(context, readerOptions, delayLine_); + delayWriter_ = std::make_unique(context, options, delayLine_); } std::shared_ptr DelayNode::getDelayTimeParam() const { return delayTimeParam_; } -void DelayNode::onInputDisabled() { - numberOfEnabledInputNodes_ -= 1; - if (isEnabled() && numberOfEnabledInputNodes_ == 0) { - signalledToStop_ = true; - remainingFrames_ = static_cast(delayTimeParam_->getValue() * getContextSampleRate()); - } -} - -void DelayNode::delayBufferOperation( - const std::shared_ptr &processingBuffer, - int framesToProcess, - size_t &operationStartingIndex, - DelayNode::BufferAction action) { - size_t processingBufferStartIndex = 0; - - // handle buffer wrap around - if (operationStartingIndex + framesToProcess > delayBuffer_->getSize()) { - auto framesToEnd = static_cast(delayBuffer_->getSize() - operationStartingIndex); - - if (action == BufferAction::WRITE) { - delayBuffer_->sum( - *processingBuffer, processingBufferStartIndex, operationStartingIndex, framesToEnd); - } else { // READ - processingBuffer->sum( - *delayBuffer_, operationStartingIndex, processingBufferStartIndex, framesToEnd); - delayBuffer_->zero(operationStartingIndex, framesToEnd); - } - - operationStartingIndex = 0; - processingBufferStartIndex += framesToEnd; - framesToProcess -= framesToEnd; - } - - if (action == BufferAction::WRITE) { - delayBuffer_->sum( - *processingBuffer, processingBufferStartIndex, operationStartingIndex, framesToProcess); - processingBuffer->zero(); - } else { // READ - processingBuffer->sum( - *delayBuffer_, operationStartingIndex, processingBufferStartIndex, 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 buffer -// 2. reading from delay buffer to processing buffer (mixing if needed) with delay -std::shared_ptr DelayNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { - // handling tail processing - if (signalledToStop_) { - if (remainingFrames_ <= 0) { - disable(); - signalledToStop_ = false; - return processingBuffer; - } - - delayBufferOperation( - processingBuffer, framesToProcess, readIndex_, DelayNode::BufferAction::READ); - remainingFrames_ -= framesToProcess; - return processingBuffer; - } - - // normal processing - std::shared_ptr context = context_.lock(); - if (context == nullptr) { - processingBuffer->zero(); - return processingBuffer; - } - - auto delayTime = delayTimeParam_->processKRateParam(framesToProcess, context->getCurrentTime()); - size_t writeIndex = - static_cast(static_cast(readIndex_) + delayTime * context->getSampleRate()) % - delayBuffer_->getSize(); - delayBufferOperation( - processingBuffer, framesToProcess, writeIndex, DelayNode::BufferAction::WRITE); - delayBufferOperation( - processingBuffer, framesToProcess, readIndex_, DelayNode::BufferAction::READ); - - return processingBuffer; -} - } // 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 index 48d705bf8..7f3949146 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 @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -9,6 +11,7 @@ namespace audioapi { struct DelayOptions; +class DelayLine; class DelayNode : public AudioNode { public: @@ -16,24 +19,18 @@ class DelayNode : public AudioNode { [[nodiscard]] std::shared_ptr getDelayTimeParam() const; + std::shared_ptr delayLine_; + std::unique_ptr delayReader_; + std::unique_ptr delayWriter_; + protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override { + // noop + }; private: - void onInputDisabled() override; - enum class BufferAction : uint8_t { READ, WRITE }; - void delayBufferOperation( - const std::shared_ptr &processingBuffer, - int framesToProcess, - size_t &operationStartingIndex, - BufferAction action); const 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/effects/GainNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.cpp index 5d0b8b822..18a8255e1 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.cpp @@ -15,31 +15,25 @@ GainNode::GainNode(const std::shared_ptr &context, const GainO options.gain, MOST_NEGATIVE_SINGLE_FLOAT, MOST_POSITIVE_SINGLE_FLOAT, - context)) { - isInitialized_.store(true, std::memory_order_release); -} + context)) {} std::shared_ptr GainNode::getGainParam() const { return gainParam_; } -std::shared_ptr GainNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void GainNode::processNode(int framesToProcess) { std::shared_ptr context = context_.lock(); if (context == nullptr) { - return processingBuffer; + return; } double time = context->getCurrentTime(); auto gainParamValues = gainParam_->processARateParam(framesToProcess, time); auto *gainValues = gainParamValues->getChannel(0); - for (size_t i = 0; i < processingBuffer->getNumberOfChannels(); i++) { - auto *channel = processingBuffer->getChannel(i); + for (size_t i = 0; i < audioBuffer_->getNumberOfChannels(); i++) { + auto *channel = audioBuffer_->getChannel(i); channel->multiply(*gainValues, framesToProcess); } - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.h index 797a44c5a..37df303c3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/GainNode.h @@ -17,9 +17,7 @@ class GainNode : public AudioNode { [[nodiscard]] std::shared_ptr getGainParam() const; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: const std::shared_ptr gainParam_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.cpp index 7c8734aa9..025d519bd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.cpp @@ -41,9 +41,7 @@ IIRFilterNode::IIRFilterNode( feedforward_(createNormalizedArray(options.feedforward, options.feedback[0])), feedback_(createNormalizedArray(options.feedback, options.feedback[0])), xBuffers_(BUFFER_LENGTH, MAX_CHANNEL_COUNT, context->getSampleRate()), - yBuffers_(BUFFER_LENGTH, MAX_CHANNEL_COUNT, context->getSampleRate()) { - isInitialized_.store(true, std::memory_order_release); -} + yBuffers_(BUFFER_LENGTH, MAX_CHANNEL_COUNT, context->getSampleRate()) {} // Compute Z-transform of the filter // @@ -98,10 +96,8 @@ void IIRFilterNode::getFrequencyResponse( // TODO: tail -std::shared_ptr IIRFilterNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { - auto numChannels = static_cast(processingBuffer->getNumberOfChannels()); +void IIRFilterNode::processNode(int framesToProcess) { + int numChannels = static_cast(audioBuffer_->getNumberOfChannels()); size_t feedforwardLength = feedforward_.getSize(); size_t feedbackLength = feedback_.getSize(); @@ -110,7 +106,7 @@ std::shared_ptr IIRFilterNode::processNode( constexpr int mask = BUFFER_LENGTH - 1; for (int c = 0; c < numChannels; ++c) { - auto channel = processingBuffer->getChannel(c)->subSpan(framesToProcess); + auto channel = audioBuffer_->getChannel(c)->subSpan(framesToProcess); auto &x = xBuffers_[c]; auto &y = yBuffers_[c]; @@ -149,7 +145,6 @@ std::shared_ptr IIRFilterNode::processNode( } bufferIndices_[c] = bufferIndex; } - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.h index 9289725c7..f9aa64ab7 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/IIRFilterNode.h @@ -54,9 +54,7 @@ class IIRFilterNode : public AudioNode { size_t length) const; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: static constexpr size_t BUFFER_LENGTH = 32; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.cpp index ba8b818d5..58ea85519 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.cpp @@ -14,32 +14,38 @@ StereoPannerNode::StereoPannerNode( const std::shared_ptr &context, const StereoPannerOptions &options) : AudioNode(context, options), - panParam_(std::make_shared(options.pan, -1.0f, 1.0f, context)) { - isInitialized_.store(true, std::memory_order_release); -} + panParam_(std::make_shared(options.pan, -1.0f, 1.0f, context)), + outputBuffer_( + std::make_shared( + RENDER_QUANTUM_SIZE, + channelCount_, + context->getSampleRate())) {} std::shared_ptr StereoPannerNode::getPanParam() const { return panParam_; } -std::shared_ptr StereoPannerNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +std::shared_ptr StereoPannerNode::getOutputBuffer() const { + return outputBuffer_; +} + +void StereoPannerNode::processNode(int framesToProcess) { std::shared_ptr context = context_.lock(); if (context == nullptr) { - return processingBuffer; + return; } + double time = context->getCurrentTime(); double deltaTime = 1.0 / context->getSampleRate(); auto panParamValues = panParam_->processARateParam(framesToProcess, time)->getChannel(0)->span(); - auto outputLeft = audioBuffer_->getChannelByType(AudioBuffer::ChannelLeft)->span(); - auto outputRight = audioBuffer_->getChannelByType(AudioBuffer::ChannelRight)->span(); + auto outputLeft = outputBuffer_->getChannelByType(AudioBuffer::ChannelLeft)->span(); + auto outputRight = outputBuffer_->getChannelByType(AudioBuffer::ChannelRight)->span(); // Input is mono - if (processingBuffer->getNumberOfChannels() == 1) { - auto inputLeft = processingBuffer->getChannelByType(AudioBuffer::ChannelMono)->span(); + if (audioBuffer_->getNumberOfChannels() == 1) { + auto inputLeft = audioBuffer_->getChannelByType(AudioBuffer::ChannelMono)->span(); for (int i = 0; i < framesToProcess; i++) { const auto pan = std::clamp(panParamValues[i], -1.0f, 1.0f); @@ -52,8 +58,8 @@ std::shared_ptr StereoPannerNode::processNode( time += deltaTime; } } else { // Input is stereo - auto inputLeft = processingBuffer->getChannelByType(AudioBuffer::ChannelLeft)->span(); - auto inputRight = processingBuffer->getChannelByType(AudioBuffer::ChannelRight)->span(); + auto inputLeft = audioBuffer_->getChannelByType(AudioBuffer::ChannelLeft)->span(); + auto inputRight = audioBuffer_->getChannelByType(AudioBuffer::ChannelRight)->span(); for (int i = 0; i < framesToProcess; i++) { const auto pan = std::clamp(panParamValues[i], -1.0f, 1.0f); @@ -74,8 +80,6 @@ std::shared_ptr StereoPannerNode::processNode( time += deltaTime; } } - - return audioBuffer_; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.h index f54f26682..5f699cfe8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/StereoPannerNode.h @@ -18,14 +18,17 @@ class StereoPannerNode : public AudioNode { const StereoPannerOptions &options); [[nodiscard]] std::shared_ptr getPanParam() const; + [[nodiscard]] std::shared_ptr getOutputBuffer() const override; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; + [[nodiscard]] const DSPAudioBuffer *getOutput() const override { + return outputBuffer_.get(); + } private: const std::shared_ptr panParam_; + const std::shared_ptr outputBuffer_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.cpp index 7886face4..981842d7d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.cpp @@ -17,7 +17,6 @@ WaveShaperNode::WaveShaperNode( waveShapers_.emplace_back(std::make_unique(nullptr, context->getSampleRate())); } setCurve(options.curve); - isInitialized_.store(true, std::memory_order_release); } void WaveShaperNode::setOversample(OverSampleType type) { @@ -36,20 +35,16 @@ void WaveShaperNode::setCurve(const std::shared_ptr &curve) { } } -std::shared_ptr WaveShaperNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void WaveShaperNode::processNode(int framesToProcess) { if (curve_ == nullptr) { - return processingBuffer; + return; } - for (size_t channel = 0; channel < processingBuffer->getNumberOfChannels(); channel++) { - auto *channelData = processingBuffer->getChannel(channel); + for (size_t channel = 0; channel < audioBuffer_->getNumberOfChannels(); channel++) { + auto *channelData = audioBuffer_->getChannel(channel); waveShapers_[channel]->process(*channelData, framesToProcess); } - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.h index e0e6f697f..0c022c494 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WaveShaperNode.h @@ -26,9 +26,7 @@ class WaveShaperNode : public AudioNode { void setCurve(const std::shared_ptr &curve); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: OverSampleType oversample_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp index f1da29450..7c034abbd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.cpp @@ -16,15 +16,12 @@ WorkletNode::WorkletNode( std::make_shared(bufferLength, inputChannelCount, context->getSampleRate())), bufferLength_(bufferLength), inputChannelCount_(inputChannelCount), - curBuffIndex_(0) { - isInitialized_.store(true, std::memory_order_release); -} + curBuffIndex_(0) {} -std::shared_ptr WorkletNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void WorkletNode::processNode(int framesToProcess) { int processed = 0; - size_t channelCount_ = std::min(inputChannelCount_, processingBuffer->getNumberOfChannels()); + size_t channelCount_ = + std::min(inputChannelCount_, static_cast(audioBuffer_->getNumberOfChannels())); while (processed < framesToProcess) { size_t framesToWorkletInvoke = bufferLength_ - curBuffIndex_; size_t needsToProcess = framesToProcess - processed; @@ -33,7 +30,7 @@ std::shared_ptr WorkletNode::processNode( /// here we copy /// to [curBuffIndex_, curBuffIndex_ + shouldProcess] /// from [processed, processed + shouldProcess] - buffer_->copy(*processingBuffer, processed, curBuffIndex_, shouldProcess); + buffer_->copy(*audioBuffer_, processed, curBuffIndex_, shouldProcess); processed += static_cast(shouldProcess); curBuffIndex_ += shouldProcess; @@ -65,8 +62,6 @@ std::shared_ptr WorkletNode::processNode( return jsi::Value::undefined(); }); } - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h index 9e0b462e6..df0bc61b6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletNode.h @@ -24,11 +24,7 @@ class WorkletNode : public AudioNode { : AudioNode(context) {} protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return processingBuffer; - } + void processNode(int framesToProcess) override {} }; #else @@ -45,9 +41,7 @@ class WorkletNode : public AudioNode { ~WorkletNode() override = default; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: WorkletsRunner workletRunner_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp index df875a6ed..ae974b502 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.cpp @@ -19,20 +19,16 @@ WorkletProcessingNode::WorkletProcessingNode( inputBuffsHandles_[i] = std::make_shared(RENDER_QUANTUM_SIZE); outputBuffsHandles_[i] = std::make_shared(RENDER_QUANTUM_SIZE); } - - isInitialized_.store(true, std::memory_order_release); } -std::shared_ptr WorkletProcessingNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void WorkletProcessingNode::processNode(int framesToProcess) { size_t channelCount = std::min( static_cast(2), // Fixed to stereo for now - processingBuffer->getNumberOfChannels()); + audioBuffer_->getNumberOfChannels()); // Copy input data to pre-allocated input buffers for (size_t ch = 0; ch < channelCount; ch++) { - inputBuffsHandles_[ch]->copy(*processingBuffer->getChannel(ch), 0, 0, framesToProcess); + inputBuffsHandles_[ch]->copy(*audioBuffer_->getChannel(ch), 0, 0, framesToProcess); } // Execute the worklet @@ -67,7 +63,7 @@ std::shared_ptr WorkletProcessingNode::processNode( // Copy processed output data back to the processing buffer or zero on failure for (size_t ch = 0; ch < channelCount; ch++) { - auto *channelData = processingBuffer->getChannel(ch); + auto *channelData = audioBuffer_->getChannel(ch); if (result.has_value()) { // Copy processed output data @@ -77,8 +73,6 @@ std::shared_ptr WorkletProcessingNode::processNode( channelData->zero(0, framesToProcess); } } - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h index ffd7473a3..6e917c7c9 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/WorkletProcessingNode.h @@ -22,11 +22,7 @@ class WorkletProcessingNode : public AudioNode { : AudioNode(context) {} protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return processingBuffer; - } + void processNode(int framesToProcess) override {} }; #else @@ -39,9 +35,7 @@ class WorkletProcessingNode : public AudioNode { WorkletsRunner &&workletRunner); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: WorkletsRunner workletRunner_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayLine.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayLine.h new file mode 100644 index 000000000..31e578272 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayLine.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace audioapi { + +/// Shared delay ring + delay-time param. +/// +/// Two logical read positions: +/// - `readIndex_` — advanced by DelayReader after each READ (live read head). +/// - `writeIndex_` — copy of `readIndex_` at the start of each audio quantum +/// (`syncReadSnapshotForQuantum` keyed by context sample frame). DelayWriter uses this to +/// compute write position so writer/reader order in the graph does not matter. +class DelayLine { + public: + DelayLine(std::shared_ptr buffer, const std::shared_ptr &delayTimeParam) + : buffer_(std::move(buffer)), delayTimeParam_(delayTimeParam) {} + + [[nodiscard]] std::shared_ptr getBuffer() const { + return buffer_; + } + + [[nodiscard]] std::shared_ptr getDelayTimeParam() const { + return delayTimeParam_; + } + + /// Call at the start of DelayReader/DelayWriter `processNode` (same `currentSampleFrame` for + /// the whole graph pass). First touch each quantum refreshes `readSnapshotForWrite_` from + /// `readIndex_`. + void syncReadSnapshotForQuantum(size_t currentSampleFrame) { + if (currentSampleFrame != quantumSampleFrame_) { + writeIndex_ = readIndex_; + quantumSampleFrame_ = currentSampleFrame; + } + } + + /// Read head used by DelayWriter for `(snapshot + delaySamples) % N` (not `readIndex_` after + /// reader may have advanced). + [[nodiscard]] size_t readSnapshotForWrite() const { + return writeIndex_; + } + + [[nodiscard]] size_t &readIndex() { + return readIndex_; + } + + [[nodiscard]] size_t readIndex() const { + return readIndex_; + } + + private: + std::shared_ptr buffer_; + std::shared_ptr delayTimeParam_; + + size_t readIndex_{0}; + size_t writeIndex_{0}; + size_t quantumSampleFrame_{std::numeric_limits::max()}; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayReader.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayReader.cpp new file mode 100644 index 000000000..20636840c --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayReader.cpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace audioapi { + +DelayReader::DelayReader( + const std::shared_ptr &context, + const AudioNodeOptions &options, + std::shared_ptr delayLine) + : AudioNode(context, options), delayLine_(std::move(delayLine)) {} + +void DelayReader::processNode(int framesToProcess) { + std::shared_ptr context = context_.lock(); + if (context == nullptr) { + audioBuffer_->zero(); + return; + } + + delayLine_->syncReadSnapshotForQuantum(context->getCurrentSampleFrame()); + + auto delayBuffer = delayLine_->getBuffer(); + size_t &readIdx = delayLine_->readIndex(); + + delay_ring::bufferOperation( + delayBuffer, audioBuffer_, framesToProcess, readIdx, delay_ring::BufferAction::READ); +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayReader.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayReader.h new file mode 100644 index 000000000..1fcc88c12 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayReader.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include + +namespace audioapi { + +class BaseAudioContext; + +/// Reads from the delay line at `delayLine->readIndex()` into the node buffer +class DelayReader : public AudioNode { + public: + explicit DelayReader( + const std::shared_ptr &context, + const AudioNodeOptions &options, + std::shared_ptr delayLine); + + void processNode(int framesToProcess) override; + + private: + std::shared_ptr delayLine_; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayRingBufferOp.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayRingBufferOp.h new file mode 100644 index 000000000..f504593ac --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayRingBufferOp.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include +#include +#include + +namespace audioapi::delay_ring { + +enum class BufferAction : std::uint8_t { READ, WRITE }; + +/// Ring-buffer read/write: splits at the end of the ring, then advances the head with wrap. +inline void bufferOperation( + const std::shared_ptr &delayBuffer, + const std::shared_ptr &processingBuffer, + int framesToProcess, + size_t &operationStartingIndex, + BufferAction action) { + size_t processingBufferStartIndex = 0; + + const size_t bufferSize = delayBuffer->getSize(); + if (operationStartingIndex + static_cast(framesToProcess) > bufferSize) { + const int tail = static_cast(bufferSize - operationStartingIndex); + + if (action == BufferAction::WRITE) { + delayBuffer->sum(*processingBuffer, processingBufferStartIndex, operationStartingIndex, tail); + } else { + processingBuffer->sum(*delayBuffer, operationStartingIndex, processingBufferStartIndex, tail); + } + + operationStartingIndex = 0; + processingBufferStartIndex += static_cast(tail); + framesToProcess -= tail; + } + + if (action == BufferAction::WRITE) { + delayBuffer->sum( + *processingBuffer, processingBufferStartIndex, operationStartingIndex, framesToProcess); + processingBuffer->zero(); + } else { + processingBuffer->sum( + *delayBuffer, operationStartingIndex, processingBufferStartIndex, framesToProcess); + delayBuffer->zero(operationStartingIndex, static_cast(framesToProcess)); + } + + operationStartingIndex = + (operationStartingIndex + static_cast(framesToProcess)) % bufferSize; +} + +} // namespace audioapi::delay_ring diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayWriter.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayWriter.cpp new file mode 100644 index 000000000..e653842de --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayWriter.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace audioapi { + +DelayWriter::DelayWriter( + const std::shared_ptr &context, + const AudioNodeOptions &options, + std::shared_ptr delayLine) + : AudioNode(context, options), delayLine_(std::move(delayLine)) {} + +void DelayWriter::processNode(int framesToProcess) { + std::shared_ptr context = context_.lock(); + if (context == nullptr) { + audioBuffer_->zero(); + return; + } + + delayLine_->syncReadSnapshotForQuantum(context->getCurrentSampleFrame()); + + auto delayBuffer = delayLine_->getBuffer(); + auto delayTime = delayLine_->getDelayTimeParam()->processKRateParam( + framesToProcess, context->getCurrentTime()); + const size_t readForWrite = delayLine_->readSnapshotForWrite(); + size_t writeIndex = + static_cast(static_cast(readForWrite) + delayTime * context->getSampleRate()) % + delayBuffer->getSize(); + + delay_ring::bufferOperation( + delayBuffer, audioBuffer_, framesToProcess, writeIndex, delay_ring::BufferAction::WRITE); +} + +int DelayWriter::computeTailFrames() const { + // `getMaxValue()` of the delayTime param was set to `maxDelayTime` by the + // owning DelayNode constructor. + const auto sampleRate = delayLine_->getBuffer() ? delayLine_->getBuffer()->getSampleRate() : 0.0f; + const float maxDelaySeconds = delayLine_->getDelayTimeParam()->getMaxValue(); + return static_cast(maxDelaySeconds * sampleRate); +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayWriter.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayWriter.h new file mode 100644 index 000000000..5abaffffb --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/DelayWriter.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include + +namespace audioapi { + +class BaseAudioContext; + +/// Writes the node’s input into the delay ring at `writeIndex = (readIndex + delaySamples) % N` +/// (same rule as DelayNode). +class DelayWriter : public AudioNode { + public: + explicit DelayWriter( + const std::shared_ptr &context, + const AudioNodeOptions &options, + std::shared_ptr delayLine); + + void processNode(int framesToProcess) override; + + protected: + /// @brief Tail length equals one full `maxDelayTime` worth of frames: the + /// writer must keep filling the ring with zeros for at least that long + /// after upstream goes silent so the reader can drain the audio currently + /// stored in the ring. + /// @note Audio Thread only. + [[nodiscard]] int computeTailFrames() const override; + + private: + std::shared_ptr delayLine_; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayReaderHostNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayReaderHostNode.cpp new file mode 100644 index 000000000..288b66dc0 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayReaderHostNode.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include + +#include +#include + +namespace audioapi { + +class DelayReader; + +DelayReaderHostNode::DelayReaderHostNode( + const std::shared_ptr &graph, + std::unique_ptr delayReader) + : utils::graph::HostNode(graph, std::move(delayReader)) {} +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayReaderHostNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayReaderHostNode.h new file mode 100644 index 000000000..300f66607 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayReaderHostNode.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace audioapi { + +class DelayReader; + +class DelayReaderHostNode : public utils::graph::HostNode { + public: + explicit DelayReaderHostNode( + const std::shared_ptr &graph, + std::unique_ptr delayReader); +}; +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayWriterHostNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayWriterHostNode.cpp new file mode 100644 index 000000000..119f2f9bd --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayWriterHostNode.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include + +#include +#include + +namespace audioapi { + +class DelayWriter; + +DelayWriterHostNode::DelayWriterHostNode( + const std::shared_ptr &graph, + std::unique_ptr delayWriter) + : utils::graph::HostNode(graph, std::move(delayWriter)) {} +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayWriterHostNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayWriterHostNode.h new file mode 100644 index 000000000..820187649 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/delay/host_nodes/DelayWriterHostNode.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace audioapi { + +class DelayWriter; + +class DelayWriterHostNode : public utils::graph::HostNode { + public: + explicit DelayWriterHostNode( + const std::shared_ptr &graph, + std::unique_ptr delayWriter); +}; +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h index 9e0c4248b..534d8e7ee 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/inputs/AudioRecorder.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -11,7 +12,6 @@ namespace audioapi { class AudioFileWriter; -class RecorderAdapterNode; class AudioFileProperties; class AudioRecorderCallback; class AudioEventHandlerRegistry; @@ -38,7 +38,7 @@ class AudioRecorder { virtual void pause() = 0; virtual void resume() = 0; - virtual void connect(const std::shared_ptr &node) = 0; + virtual void connect(const std::shared_ptr &node) = 0; virtual void disconnect() = 0; virtual Result setOnAudioReadyCallback( @@ -84,7 +84,7 @@ class AudioRecorder { std::string filePath_; std::shared_ptr fileWriter_ = nullptr; - std::shared_ptr adapterNode_ = nullptr; + std::shared_ptr adapterNodeHandle_ = nullptr; std::shared_ptr dataCallback_ = nullptr; std::shared_ptr audioEventHandlerRegistry_; std::shared_ptr fileProperties_ = nullptr; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp index 0d3dac031..c62a6e814 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace audioapi { AudioBufferBaseSourceNode::AudioBufferBaseSourceNode( @@ -37,8 +38,18 @@ AudioBufferBaseSourceNode::AudioBufferBaseSourceNode( void AudioBufferBaseSourceNode::initStretch( const std::shared_ptr> &stretch, const std::shared_ptr &playbackRateBuffer) { - stretch_ = stretch; - playbackRateBuffer_ = playbackRateBuffer; + if (auto context = context_.lock()) { + if (playbackRateBuffer_ != nullptr) { + context->getDisposer()->dispose(std::move(playbackRateBuffer_)); + } + + if (stretch_ != nullptr) { + context->getDisposer()->dispose(std::move(stretch_)); + } + + stretch_ = stretch; + playbackRateBuffer_ = playbackRateBuffer; + } } std::shared_ptr AudioBufferBaseSourceNode::getDetuneParam() const { @@ -63,23 +74,19 @@ void AudioBufferBaseSourceNode::unregisterOnPositionChangedCallback(uint64_t cal audioEventHandlerRegistry_->unregisterHandler(AudioEvent::POSITION_CHANGED, callbackId); } -std::shared_ptr AudioBufferBaseSourceNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void AudioBufferBaseSourceNode::processNode(int framesToProcess) { if (isEmpty()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } if (!pitchCorrection_) { - processWithoutPitchCorrection(processingBuffer, framesToProcess); + processWithoutPitchCorrection(audioBuffer_, framesToProcess); } else { - processWithPitchCorrection(processingBuffer, framesToProcess); + processWithPitchCorrection(audioBuffer_, framesToProcess); } handleStopScheduled(); - - return processingBuffer; } void AudioBufferBaseSourceNode::sendOnPositionChangedEvent() { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.h index a44cbbb84..0920f2a91 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferBaseSourceNode.h @@ -37,9 +37,7 @@ class AudioBufferBaseSourceNode : public AudioScheduledSourceNode { // internal helper double vReadIndex_; - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) final; + void processNode(int framesToProcess) final; virtual double getCurrentPosition() const = 0; 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 478933349..b2a9fbf97 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 @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -26,8 +25,6 @@ AudioBufferQueueSourceNode::AudioBufferQueueSourceNode( // to compensate for processing latency. addExtraTailFrames_ = true; } - - isInitialized_.store(true, std::memory_order_release); } void AudioBufferQueueSourceNode::stop(double when) { @@ -78,10 +75,8 @@ void AudioBufferQueueSourceNode::dequeueBuffer(const size_t bufferId) { return; } - auto graphManager = context->getGraphManager(); - if (buffers_.front().first == bufferId) { - graphManager->addAudioBufferForDestruction(std::move(buffers_.front().second)); + context->getDisposer()->dispose(std::move(buffers_.front().second)); buffers_.pop_front(); vReadIndex_ = 0.0; return; @@ -91,7 +86,7 @@ void AudioBufferQueueSourceNode::dequeueBuffer(const size_t bufferId) { // And keep vReadIndex_ at the same position. for (auto it = std::next(buffers_.begin()); it != buffers_.end(); ++it) { if (it->first == bufferId) { - graphManager->addAudioBufferForDestruction(std::move(it->second)); + context->getDisposer()->dispose(std::move(it->second)); buffers_.erase(it); return; } @@ -102,7 +97,7 @@ void AudioBufferQueueSourceNode::dequeueBuffer(const size_t bufferId) { void AudioBufferQueueSourceNode::clearBuffers() { if (auto context = context_.lock()) { for (auto it = buffers_.begin(); it != buffers_.end(); ++it) { - context->getGraphManager()->addAudioBufferForDestruction(std::move(it->second)); + context->getDisposer()->dispose(std::move(it->second)); } buffers_.clear(); @@ -200,7 +195,7 @@ void AudioBufferQueueSourceNode::processWithoutInterpolation( buffers_.emplace_back(bufferId, tailBuffer_); addExtraTailFrames_ = false; } else { - context->getGraphManager()->addAudioBufferForDestruction(std::move(buffer)); + context->getDisposer()->dispose(std::move(buffer)); processingBuffer->zero(writeIndex, framesLeft); readIndex = 0; @@ -208,7 +203,7 @@ void AudioBufferQueueSourceNode::processWithoutInterpolation( } } - context->getGraphManager()->addAudioBufferForDestruction(std::move(buffer)); + context->getDisposer()->dispose(std::move(buffer)); data = buffers_.front(); bufferId = data.first; buffer = data.second; @@ -282,14 +277,14 @@ void AudioBufferQueueSourceNode::processWithInterpolation( sendOnBufferEndedEvent(bufferId, buffers_.empty()); if (buffers_.empty()) { - context->getGraphManager()->addAudioBufferForDestruction(std::move(buffer)); + context->getDisposer()->dispose(std::move(buffer)); processingBuffer->zero(writeIndex, framesLeft); vReadIndex_ = 0.0; break; } vReadIndex_ = vReadIndex_ - static_cast(buffer->getSize()); - context->getGraphManager()->addAudioBufferForDestruction(std::move(buffer)); + context->getDisposer()->dispose(std::move(buffer)); data = buffers_.front(); bufferId = data.first; buffer = data.second; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp index 5bd78ec42..0ca0fb867 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -22,9 +21,7 @@ AudioBufferSourceNode::AudioBufferSourceNode( loop_(options.loop), loopSkip_(options.loopSkip), loopStart_(options.loopStart), - loopEnd_(options.loopEnd) { - isInitialized_.store(true, std::memory_order_release); -} + loopEnd_(options.loopEnd) {} void AudioBufferSourceNode::setLoop(bool loop) { loop_ = loop; @@ -54,13 +51,13 @@ void AudioBufferSourceNode::setBuffer( return; } - auto graphManager = context->getGraphManager(); - if (buffer_ != nullptr) { - graphManager->addAudioBufferForDestruction(std::move(buffer_)); + context->getDisposer()->dispose(std::move(buffer_)); } - // TODO move DSPAudioBuffers destruction to graph manager as well + if (audioBuffer_ != nullptr) { + context->getDisposer()->dispose(std::move(audioBuffer_)); + } if (buffer == nullptr) { loopEnd_ = 0; @@ -98,6 +95,11 @@ void AudioBufferSourceNode::start(double when, double offset, double duration) { void AudioBufferSourceNode::disable() { AudioScheduledSourceNode::disable(); + + if (auto context = context_.lock()) { + context->getDisposer()->dispose(std::move(buffer_)); + } + buffer_ = nullptr; } void AudioBufferSourceNode::setOnLoopEndedCallbackId(uint64_t callbackId) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp index fd6882060..c05d2db78 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp @@ -52,8 +52,6 @@ AudioFileSourceNode::AudioFileSourceNode( spsc::OverflowStrategy::OVERWRITE_ON_FULL, spsc::WaitStrategy::ATOMIC_WAIT>>( SEEK_OFFLOADER_WORKER_COUNT, [this](OffloadedSeekRequest req) { runOffloadedSeekTask(req); }); - - isInitialized_.store(true, std::memory_order_release); } void AudioFileSourceNode::setOnPositionChangedCallbackId(uint64_t callbackId) { @@ -215,34 +213,32 @@ size_t AudioFileSourceNode::handleEof( return framesRead + extra; } -std::shared_ptr AudioFileSourceNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void AudioFileSourceNode::processNode(int framesToProcess) { if (decoderState_ == nullptr || decoder_ == nullptr || !decoder_->isOpen()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } std::shared_ptr context = context_.lock(); if (context == nullptr) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } if (pendingOffloadedSeeks_.load(std::memory_order_acquire) > 0) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } if (filePaused_) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } size_t startOffset = 0; size_t offsetLength = 0; updatePlaybackInfo( - processingBuffer, + audioBuffer_, framesToProcess, startOffset, offsetLength, @@ -250,8 +246,8 @@ std::shared_ptr AudioFileSourceNode::processNode( context->getCurrentSampleFrame()); if (!isPlaying() && !isStopScheduled()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } auto &state = *decoderState_; @@ -260,7 +256,7 @@ std::shared_ptr AudioFileSourceNode::processNode( sendOnPositionChangedEvent(static_cast(framesRead)); if (volume_ != 0.0f && framesRead > 0) { - writeInterleavedToBufferAtOffset(processingBuffer, state, startOffset, framesRead); + writeInterleavedToBufferAtOffset(audioBuffer_, state, startOffset, framesRead); } if (framesRead < offsetLength) { @@ -270,21 +266,19 @@ std::shared_ptr AudioFileSourceNode::processNode( sendOnPositionChangedEvent(static_cast(offsetLength - framesRead)); filePaused_ = true; playbackState_ = PlaybackState::STOP_SCHEDULED; - processingBuffer->zero(startOffset + framesRead, offsetLength - framesRead); + audioBuffer_->zero(startOffset + framesRead, offsetLength - framesRead); } else { - const size_t totalFilled = handleEof(processingBuffer, offsetLength, framesRead, startOffset); + const size_t totalFilled = handleEof(audioBuffer_, offsetLength, framesRead, startOffset); onPositionChangedFlush_.store(true, std::memory_order_release); currentTime_.store(0, std::memory_order_release); sendOnPositionChangedEvent(static_cast(totalFilled)); - processingBuffer->zero(startOffset + totalFilled, offsetLength - totalFilled); + audioBuffer_->zero(startOffset + totalFilled, offsetLength - totalFilled); } } if (isStopScheduled()) { handleStopScheduled(); } - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h index 0730e4d1b..0eec1340b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h @@ -70,9 +70,7 @@ class AudioFileSourceNode : public AudioScheduledSourceNode { void seekToTime(double seconds); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: void initDecoders( diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp index aa9d12478..6c036443e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -41,23 +40,23 @@ void AudioScheduledSourceNode::stop(double when) { stopTime_ = when; } -bool AudioScheduledSourceNode::isUnscheduled() { +bool AudioScheduledSourceNode::isUnscheduled() const { return playbackState_ == PlaybackState::UNSCHEDULED; } -bool AudioScheduledSourceNode::isScheduled() { +bool AudioScheduledSourceNode::isScheduled() const { return playbackState_ == PlaybackState::SCHEDULED; } -bool AudioScheduledSourceNode::isPlaying() { +bool AudioScheduledSourceNode::isPlaying() const { return playbackState_ == PlaybackState::PLAYING; } -bool AudioScheduledSourceNode::isFinished() { +bool AudioScheduledSourceNode::isFinished() const { return playbackState_ == PlaybackState::FINISHED; } -bool AudioScheduledSourceNode::isStopScheduled() { +bool AudioScheduledSourceNode::isStopScheduled() const { return playbackState_ == PlaybackState::STOP_SCHEDULED; } @@ -69,6 +68,10 @@ void AudioScheduledSourceNode::unregisterOnEndedCallback(uint64_t callbackId) { audioEventHandlerRegistry_->unregisterHandler(AudioEvent::ENDED, callbackId); } +bool AudioScheduledSourceNode::canBeDestructed() const { + return isUnscheduled() || isFinished(); +} + void AudioScheduledSourceNode::updatePlaybackInfo( const std::shared_ptr &processingBuffer, int framesToProcess, @@ -79,7 +82,9 @@ void AudioScheduledSourceNode::updatePlaybackInfo( auto firstFrame = currentSampleFrame; size_t lastFrame = firstFrame + framesToProcess - 1; - size_t startFrame = std::max(dsp::timeToSampleFrame(startTime_, sampleRate), firstFrame); + size_t startFrame = startTime_ == -1.0 + ? firstFrame + : std::max(dsp::timeToSampleFrame(startTime_, sampleRate), firstFrame); size_t stopFrame = stopTime_ == -1.0 ? std::numeric_limits::max() : dsp::timeToSampleFrame(stopTime_, sampleRate); if (isFinished()) { @@ -156,8 +161,6 @@ void AudioScheduledSourceNode::updatePlaybackInfo( } void AudioScheduledSourceNode::disable() { - AudioNode::disable(); - if (onEndedCallbackId_ != 0) { audioEventHandlerRegistry_->invokeHandlerWithEventBody( AudioEvent::ENDED, onEndedCallbackId_, {}); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h index 85c65fd19..aa6e98929 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h @@ -34,27 +34,29 @@ class AudioScheduledSourceNode : public AudioNode { virtual void stop(double when); /// @note Audio Thread only - bool isUnscheduled(); + bool isUnscheduled() const; /// @note Audio Thread only - bool isScheduled(); + bool isScheduled() const; /// @note Audio Thread only - bool isPlaying(); + bool isPlaying() const; /// @note Audio Thread only - bool isFinished(); + bool isFinished() const; /// @note Audio Thread only - bool isStopScheduled(); + bool isStopScheduled() const; /// @note Audio Thread only void setOnEndedCallbackId(uint64_t callbackId); - void disable() override; + virtual void disable(); void unregisterOnEndedCallback(uint64_t callbackId); + bool canBeDestructed() const override; + protected: double startTime_; double stopTime_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.cpp index 0c7fce17d..92675cad2 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.cpp @@ -16,28 +16,24 @@ ConstantSourceNode::ConstantSourceNode( options.offset, MOST_NEGATIVE_SINGLE_FLOAT, MOST_POSITIVE_SINGLE_FLOAT, - context)) { - isInitialized_.store(true, std::memory_order_release); -} + context)) {} std::shared_ptr ConstantSourceNode::getOffsetParam() const { return offsetParam_; } -std::shared_ptr ConstantSourceNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void ConstantSourceNode::processNode(int framesToProcess) { size_t startOffset = 0; size_t offsetLength = 0; std::shared_ptr context = context_.lock(); if (context == nullptr) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } updatePlaybackInfo( - processingBuffer, + audioBuffer_, framesToProcess, startOffset, offsetLength, @@ -45,22 +41,19 @@ std::shared_ptr ConstantSourceNode::processNode( context->getCurrentSampleFrame()); if (!isPlaying() && !isStopScheduled()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } auto *offsetChannel = offsetParam_->processARateParam(framesToProcess, context->getCurrentTime())->getChannel(0); - for (size_t channel = 0; channel < processingBuffer->getNumberOfChannels(); ++channel) { - processingBuffer->getChannel(channel)->copy( - *offsetChannel, startOffset, startOffset, offsetLength); + for (size_t channel = 0; channel < audioBuffer_->getNumberOfChannels(); ++channel) { + audioBuffer_->getChannel(channel)->copy(*offsetChannel, startOffset, startOffset, offsetLength); } if (isStopScheduled()) { handleStopScheduled(); } - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.h index 33b50d35c..f2c5d1f4c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/ConstantSourceNode.h @@ -19,9 +19,7 @@ class ConstantSourceNode : public AudioScheduledSourceNode { [[nodiscard]] std::shared_ptr getOffsetParam() const; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: const std::shared_ptr offsetParam_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.cpp index 60be3d8ae..50ff384c3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.cpp @@ -25,8 +25,6 @@ OscillatorNode::OscillatorNode( } else { periodicWave_ = context->getBasicWaveForm(type_); } - - isInitialized_.store(true, std::memory_order_release); } std::shared_ptr OscillatorNode::getFrequencyParam() const { @@ -49,20 +47,18 @@ void OscillatorNode::setPeriodicWave(const std::shared_ptr &period type_ = OscillatorType::CUSTOM; } -std::shared_ptr OscillatorNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void OscillatorNode::processNode(int framesToProcess) { size_t startOffset = 0; size_t offsetLength = 0; std::shared_ptr context = context_.lock(); if (context == nullptr) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } updatePlaybackInfo( - processingBuffer, + audioBuffer_, framesToProcess, startOffset, offsetLength, @@ -70,8 +66,8 @@ std::shared_ptr OscillatorNode::processNode( context->getCurrentSampleFrame()); if (!isPlaying() && !isStopScheduled()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } auto time = @@ -81,9 +77,9 @@ std::shared_ptr OscillatorNode::processNode( const auto tableSize = static_cast(periodicWave_->getPeriodicWaveSize()); const auto tableScale = periodicWave_->getScale(); - const auto numChannels = processingBuffer->getNumberOfChannels(); + const auto numChannels = audioBuffer_->getNumberOfChannels(); - auto channelSpan = processingBuffer->getChannel(0)->span(); + auto channelSpan = audioBuffer_->getChannel(0)->span(); float currentPhase = phase_; for (size_t i = startOffset; i < offsetLength; i += 1) { @@ -105,11 +101,9 @@ std::shared_ptr OscillatorNode::processNode( phase_ = currentPhase; for (size_t ch = 1; ch < numChannels; ch += 1) { - processingBuffer->getChannel(ch)->copy(*processingBuffer->getChannel(0)); + audioBuffer_->getChannel(ch)->copy(*audioBuffer_->getChannel(0)); } handleStopScheduled(); - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.h index 73d41c25e..7244994e2 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/OscillatorNode.h @@ -27,9 +27,7 @@ class OscillatorNode : public AudioScheduledSourceNode { void setPeriodicWave(const std::shared_ptr &periodicWave); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: std::shared_ptr frequencyParam_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp index 9401b22c9..202bb6045 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp @@ -67,12 +67,10 @@ void RecorderAdapterNode::adapterCleanup() { isInitialized_.store(false, std::memory_order_release); } -std::shared_ptr RecorderAdapterNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void RecorderAdapterNode::processNode(int framesToProcess) { if (!isInitialized_.load(std::memory_order_acquire)) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } if (needsResampling_) { @@ -81,8 +79,7 @@ std::shared_ptr RecorderAdapterNode::processNode( readFrames(*adapterOutputBuffer_, framesToProcess); } - processingBuffer->sum(*adapterOutputBuffer_, ChannelInterpretation::SPEAKERS); - return processingBuffer; + audioBuffer_->sum(*adapterOutputBuffer_, ChannelInterpretation::SPEAKERS); } void RecorderAdapterNode::processResampled(int framesToProcess) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h index 8769d2fac..15fef1c4d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/RecorderAdapterNode.h @@ -33,9 +33,7 @@ class RecorderAdapterNode : public AudioNode { std::vector> buff_; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; std::shared_ptr adapterOutputBuffer_; private: @@ -53,6 +51,8 @@ class RecorderAdapterNode : public AudioNode { // Accumulates resampled output across calls AudioBuffer overflowBuffer_; size_t overflowSize_ = 0; + + std::atomic isInitialized_{false}; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.cpp index 197cc92a4..56a9405a7 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.cpp @@ -56,19 +56,17 @@ StreamerNode::~StreamerNode() { #endif // RN_AUDIO_API_FFMPEG_DISABLED } -std::shared_ptr StreamerNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { +void StreamerNode::processNode(int framesToProcess) { #if !RN_AUDIO_API_FFMPEG_DISABLED size_t startOffset = 0; size_t offsetLength = 0; std::shared_ptr context = context_.lock(); if (context == nullptr) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } updatePlaybackInfo( - processingBuffer, + audioBuffer_, framesToProcess, startOffset, offsetLength, @@ -77,15 +75,15 @@ std::shared_ptr StreamerNode::processNode( isNodeFinished_.store(isFinished(), std::memory_order_release); if (!isPlaying() && !isStopScheduled()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } auto bufferRemaining = static_cast(bufferedAudioData_.size - processedSamples_); int alreadyProcessed = 0; if (bufferRemaining < framesToProcess) { if (hasBufferedAudioData_) { - processingBuffer->copy(bufferedAudioData_.buffer, processedSamples_, 0, bufferRemaining); + audioBuffer_->copy(bufferedAudioData_.buffer, processedSamples_, 0, bufferRemaining); framesToProcess -= bufferRemaining; alreadyProcessed += bufferRemaining; } @@ -99,13 +97,11 @@ std::shared_ptr StreamerNode::processNode( } } if (hasBufferedAudioData_ && framesToProcess > 0) { - processingBuffer->copy( + audioBuffer_->copy( bufferedAudioData_.buffer, processedSamples_, alreadyProcessed, framesToProcess); processedSamples_ += framesToProcess; } #endif // RN_AUDIO_API_FFMPEG_DISABLED - - return processingBuffer; } #if !RN_AUDIO_API_FFMPEG_DISABLED @@ -116,10 +112,6 @@ bool StreamerNode::initialize(const std::string &input_url) { return false; } - if (isInitialized_.load(std::memory_order_acquire)) { - return false; - } - if (!openInput(input_url)) { if (VERBOSE) printf("Failed to open input\n"); @@ -155,7 +147,6 @@ bool StreamerNode::initialize(const std::string &input_url) { receiver_ = std::move(receiver); streamingThread_ = std::thread(&StreamerNode::streamAudio, this); - isInitialized_.store(true, std::memory_order_release); return true; } @@ -348,7 +339,6 @@ void StreamerNode::cleanup() { decoder_ = nullptr; codecpar_ = nullptr; maxResampledSamples_ = 0; - isInitialized_.store(false, std::memory_order_release); } #endif // RN_AUDIO_API_FFMPEG_DISABLED } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.h index 70ae44973..a576d943b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/StreamerNode.h @@ -68,9 +68,7 @@ class StreamerNode : public AudioScheduledSourceNode { ~StreamerNode() override; protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: std::string streamPath_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp index 7f700c74a..c650487aa 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.cpp @@ -15,16 +15,12 @@ WorkletSourceNode::WorkletSourceNode( for (size_t i = 0; i < outputChannelCount; ++i) { outputBuffsHandles_[i] = std::make_shared(RENDER_QUANTUM_SIZE); } - - isInitialized_.store(true, std::memory_order_release); } -std::shared_ptr WorkletSourceNode::processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) { - if (isUnscheduled() || isFinished() || !isEnabled()) { - processingBuffer->zero(); - return processingBuffer; +void WorkletSourceNode::processNode(int framesToProcess) { + if (isUnscheduled() || isFinished()) { + audioBuffer_->zero(); + return; } size_t startOffset = 0; @@ -32,23 +28,23 @@ std::shared_ptr WorkletSourceNode::processNode( std::shared_ptr context = context_.lock(); if (context == nullptr) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } updatePlaybackInfo( - processingBuffer, + audioBuffer_, framesToProcess, startOffset, nonSilentFramesToProcess, context->getSampleRate(), context->getCurrentSampleFrame()); - if (nonSilentFramesToProcess == 0) { - processingBuffer->zero(); - return processingBuffer; + if (!isPlaying() && !isStopScheduled() || nonSilentFramesToProcess == 0) { + audioBuffer_->zero(); + return; } - size_t outputChannelCount = processingBuffer->getNumberOfChannels(); + size_t outputChannelCount = audioBuffer_->getNumberOfChannels(); auto result = workletRunner_.executeOnRuntimeSync( [this, nonSilentFramesToProcess, startOffset, time = context->getCurrentTime()]( @@ -72,19 +68,17 @@ std::shared_ptr WorkletSourceNode::processNode( // If the worklet execution failed, zero the output // It might happen if the runtime is not available if (!result.has_value()) { - processingBuffer->zero(); - return processingBuffer; + audioBuffer_->zero(); + return; } // Copy the processed data back to the AudioBuffer for (size_t i = 0; i < outputChannelCount; ++i) { - processingBuffer->getChannel(i)->copy( + audioBuffer_->getChannel(i)->copy( *outputBuffsHandles_[i], 0, startOffset, nonSilentFramesToProcess); } handleStopScheduled(); - - return processingBuffer; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h index 39ba4d00b..c22d28ad2 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/WorkletSourceNode.h @@ -22,11 +22,7 @@ class WorkletSourceNode : public AudioScheduledSourceNode { : AudioScheduledSourceNode(context) {} protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return processingBuffer; - } + void processNode(int framesToProcess) override {} }; #else @@ -37,9 +33,7 @@ class WorkletSourceNode : public AudioScheduledSourceNode { WorkletsRunner &&workletRunner); protected: - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override; + void processNode(int framesToProcess) override; private: WorkletsRunner workletRunner_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDestructor.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDestructor.hpp deleted file mode 100644 index 8b8da021a..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDestructor.hpp +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace audioapi { - -/// @brief A generic class to offload object destruction to a separate thread. -/// @tparam T The type of object to be destroyed. -template -class AudioDestructor { - public: - DELETE_COPY_AND_MOVE(AudioDestructor); - - AudioDestructor() : isExiting_(false) { - auto [sender, receiver] = channels::spsc::channel< - std::shared_ptr, - channels::spsc::OverflowStrategy::WAIT_ON_FULL, - channels::spsc::WaitStrategy::ATOMIC_WAIT>(kChannelCapacity); - sender_ = std::move(sender); - workerHandle_ = std::thread(&AudioDestructor::process, this, std::move(receiver)); - } - - ~AudioDestructor() { - isExiting_.store(true, std::memory_order_release); - - // We need to send a nullptr to unblock the receiver - sender_.send(nullptr); - if (workerHandle_.joinable()) { - workerHandle_.join(); - } - } - - /// @brief Adds an audio object to the deconstruction queue. - /// @param object The audio object to be deconstructed. - /// @return True if the node was successfully added, false otherwise. - /// @note audio object does NOT get moved out if it is not successfully added. - bool tryAddForDeconstruction(std::shared_ptr &&object) { - return sender_.try_send(std::move(object)) == channels::spsc::ResponseStatus::SUCCESS; - } - - private: - static constexpr size_t kChannelCapacity = 1024; - - std::thread workerHandle_; - std::atomic isExiting_; - - using SenderType = channels::spsc::Sender< - std::shared_ptr, - channels::spsc::OverflowStrategy::WAIT_ON_FULL, - channels::spsc::WaitStrategy::ATOMIC_WAIT>; - - using ReceiverType = channels::spsc::Receiver< - std::shared_ptr, - channels::spsc::OverflowStrategy::WAIT_ON_FULL, - channels::spsc::WaitStrategy::ATOMIC_WAIT>; - - SenderType sender_; - - /// @brief Processes audio objects for deconstruction. - /// @param receiver The receiver channel for incoming audio objects. - void process(ReceiverType &&receiver) { - auto rcv = std::move(receiver); - while (!isExiting_.load(std::memory_order_acquire)) { - rcv.receive(); - } - } -}; - -#undef AUDIO_NODE_DESTRUCTOR_SPSC_OPTIONS - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioGraphManager.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioGraphManager.cpp deleted file mode 100644 index 1689fc6b6..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioGraphManager.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace audioapi { - -AudioGraphManager::Event::Event(Event &&other) noexcept { - *this = std::move(other); -} - -AudioGraphManager::Event &AudioGraphManager::Event::operator=(Event &&other) noexcept { - if (this != &other) { - // Clean up current resources - this->~Event(); - - // Move resources from the other event - type = other.type; - payloadType = other.payloadType; - switch (payloadType) { - case EventPayloadType::NODES: - payload.nodes.from = std::move(other.payload.nodes.from); - payload.nodes.to = std::move(other.payload.nodes.to); - break; - case EventPayloadType::PARAMS: - payload.params.from = std::move(other.payload.params.from); - payload.params.to = std::move(other.payload.params.to); - break; - case EventPayloadType::SOURCE_NODE: - payload.sourceNode = std::move(other.payload.sourceNode); - break; - case EventPayloadType::AUDIO_PARAM: - payload.audioParam = std::move(other.payload.audioParam); - break; - case EventPayloadType::NODE: - payload.node = std::move(other.payload.node); - break; - - default: - break; - } - } - return *this; -} - -AudioGraphManager::Event::~Event() { - switch (payloadType) { - case EventPayloadType::NODES: - payload.nodes.from.~shared_ptr(); - payload.nodes.to.~shared_ptr(); - break; - case EventPayloadType::PARAMS: - payload.params.from.~shared_ptr(); - payload.params.to.~shared_ptr(); - break; - case EventPayloadType::SOURCE_NODE: - payload.sourceNode.~shared_ptr(); - break; - case EventPayloadType::AUDIO_PARAM: - payload.audioParam.~shared_ptr(); - break; - case EventPayloadType::NODE: - payload.node.~shared_ptr(); - break; - } -} - -AudioGraphManager::AudioGraphManager() { - sourceNodes_.reserve(kInitialCapacity); - processingNodes_.reserve(kInitialCapacity); - audioParams_.reserve(kInitialCapacity); - audioBuffers_.reserve(kInitialCapacity); - - auto channel_pair = channels::spsc::channel< - std::unique_ptr, - channels::spsc::OverflowStrategy::WAIT_ON_FULL, - channels::spsc::WaitStrategy::BUSY_LOOP>(kChannelCapacity); - - sender_ = std::move(channel_pair.first); - receiver_ = std::move(channel_pair.second); -} - -AudioGraphManager::~AudioGraphManager() { - cleanup(); -} - -void AudioGraphManager::addPendingNodeConnection( - const std::shared_ptr &from, - const std::shared_ptr &to, - ConnectionType type) { - auto event = std::make_unique(); - event->type = type; - event->payloadType = EventPayloadType::NODES; - event->payload.nodes.from = from; - event->payload.nodes.to = to; - - sender_.send(std::move(event)); -} - -void AudioGraphManager::addPendingParamConnection( - const std::shared_ptr &from, - const std::shared_ptr &to, - ConnectionType type) { - auto event = std::make_unique(); - event->type = type; - event->payloadType = EventPayloadType::PARAMS; - event->payload.params.from = from; - event->payload.params.to = to; - - sender_.send(std::move(event)); -} - -void AudioGraphManager::preProcessGraph() { - settlePendingConnections(); - AudioGraphManager::prepareForDestruction(sourceNodes_, nodeDestructor_); - AudioGraphManager::prepareForDestruction(processingNodes_, nodeDestructor_); - AudioGraphManager::prepareForDestruction(audioBuffers_, bufferDestructor_); -} - -void AudioGraphManager::addProcessingNode(const std::shared_ptr &node) { - auto event = std::make_unique(); - event->type = ConnectionType::ADD; - event->payloadType = EventPayloadType::NODE; - event->payload.node = node; - - sender_.send(std::move(event)); -} - -void AudioGraphManager::addSourceNode(const std::shared_ptr &node) { - auto event = std::make_unique(); - event->type = ConnectionType::ADD; - event->payloadType = EventPayloadType::SOURCE_NODE; - event->payload.sourceNode = node; - - sender_.send(std::move(event)); -} - -void AudioGraphManager::addAudioParam(const std::shared_ptr ¶m) { - auto event = std::make_unique(); - event->type = ConnectionType::ADD; - event->payloadType = EventPayloadType::AUDIO_PARAM; - event->payload.audioParam = param; - - sender_.send(std::move(event)); -} - -void AudioGraphManager::addAudioBufferForDestruction(std::shared_ptr buffer) { - // direct access because this is called from the Audio thread - audioBuffers_.emplace_back(std::move(buffer)); -} - -void AudioGraphManager::settlePendingConnections() { - std::unique_ptr value; - while (receiver_.try_receive(value) != channels::spsc::ResponseStatus::CHANNEL_EMPTY) { - switch (value->type) { - case ConnectionType::CONNECT: - handleConnectEvent(std::move(value)); - break; - case ConnectionType::DISCONNECT: - handleDisconnectEvent(std::move(value)); - break; - case ConnectionType::DISCONNECT_ALL: - handleDisconnectAllEvent(std::move(value)); - break; - case ConnectionType::ADD: - handleAddToDeconstructionEvent(std::move(value)); - break; - } - } -} - -void AudioGraphManager::handleConnectEvent(std::unique_ptr event) { - if (event->payloadType == EventPayloadType::NODES) { - event->payload.nodes.from->connectNode(event->payload.nodes.to); - } else if (event->payloadType == EventPayloadType::PARAMS) { - event->payload.params.from->connectParam(event->payload.params.to); - } else { - assert(false && "Invalid payload type for connect event"); - } -} - -void AudioGraphManager::handleDisconnectEvent(std::unique_ptr event) { - if (event->payloadType == EventPayloadType::NODES) { - event->payload.nodes.from->disconnectNode(event->payload.nodes.to); - } else if (event->payloadType == EventPayloadType::PARAMS) { - event->payload.params.from->disconnectParam(event->payload.params.to); - } else { - assert(false && "Invalid payload type for disconnect event"); - } -} - -void AudioGraphManager::handleDisconnectAllEvent(std::unique_ptr event) { - assert(event->payloadType == EventPayloadType::NODES); - for (auto it = event->payload.nodes.from->outputNodes_.begin(); - it != event->payload.nodes.from->outputNodes_.end();) { - auto next = std::next(it); - event->payload.nodes.from->disconnectNode(*it); - it = next; - } -} - -void AudioGraphManager::handleAddToDeconstructionEvent(std::unique_ptr event) { - switch (event->payloadType) { - case EventPayloadType::NODE: - processingNodes_.push_back(event->payload.node); - break; - case EventPayloadType::SOURCE_NODE: - sourceNodes_.push_back(event->payload.sourceNode); - break; - case EventPayloadType::AUDIO_PARAM: - audioParams_.push_back(event->payload.audioParam); - break; - default: - assert(false && "Unknown event payload type"); - } -} - -void AudioGraphManager::cleanup() { - for (auto it = sourceNodes_.begin(), end = sourceNodes_.end(); it != end; ++it) { - it->get()->cleanup(); - } - - for (auto it = processingNodes_.begin(), end = processingNodes_.end(); it != end; ++it) { - it->get()->cleanup(); - } - - sourceNodes_.clear(); - processingNodes_.clear(); - audioParams_.clear(); - audioBuffers_.clear(); -} - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioGraphManager.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioGraphManager.h deleted file mode 100644 index 419860b89..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioGraphManager.h +++ /dev/null @@ -1,219 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace audioapi { - -class AudioNode; -class AudioScheduledSourceNode; -class AudioParam; - -#define AUDIO_GRAPH_MANAGER_SPSC_OPTIONS \ - std::unique_ptr, channels::spsc::OverflowStrategy::WAIT_ON_FULL, \ - channels::spsc::WaitStrategy::BUSY_LOOP - -template -concept HasCleanupMethod = requires(T t) { - { t.cleanup() }; -}; - -class AudioGraphManager { - public: - enum class ConnectionType : uint8_t { CONNECT, DISCONNECT, DISCONNECT_ALL, ADD }; - using EventType = ConnectionType; // for backwards compatibility - enum class EventPayloadType : uint8_t { NODES, PARAMS, SOURCE_NODE, AUDIO_PARAM, NODE }; - union EventPayload { - struct { - std::shared_ptr from; - std::shared_ptr to; - } nodes; - struct { - std::shared_ptr from; - std::shared_ptr to; - } params; - std::shared_ptr sourceNode; - std::shared_ptr audioParam; - std::shared_ptr node; - - // Default constructor that initializes the first member - EventPayload() : nodes{} {} - - // Destructor - we'll handle cleanup explicitly in Event destructor - ~EventPayload() {} - }; - struct Event { - EventType type = ConnectionType::CONNECT; - EventPayloadType payloadType = EventPayloadType::NODES; - EventPayload payload; - - Event(Event &&other) noexcept; - Event &operator=(Event &&other) noexcept; - Event() = default; - ~Event(); - }; - - AudioGraphManager(); - DELETE_COPY_AND_MOVE(AudioGraphManager); - ~AudioGraphManager(); - - void preProcessGraph(); - - /// @brief Adds a pending connection between two audio nodes. - /// @param from The source audio node. - /// @param to The destination audio node. - /// @param type The type of connection (connect/disconnect). - /// @note Should be only used from JavaScript/HostObjects thread - void addPendingNodeConnection( - const std::shared_ptr &from, - const std::shared_ptr &to, - ConnectionType type); - - /// @brief Adds a pending connection between an audio node and an audio parameter. - /// @param from The source audio node. - /// @param to The destination audio parameter. - /// @param type The type of connection (connect/disconnect). - /// @note Should be only used from JavaScript/HostObjects thread - void addPendingParamConnection( - const std::shared_ptr &from, - const std::shared_ptr &to, - ConnectionType type); - - /// @brief Adds a processing node to the manager. - /// @param node The processing node to add. - /// @note Should be only used from JavaScript/HostObjects thread - void addProcessingNode(const std::shared_ptr &node); - - /// @brief Adds a source node to the manager. - /// @param node The source node to add. - /// @note Should be only used from JavaScript/HostObjects thread - void addSourceNode(const std::shared_ptr &node); - - /// @brief Adds an audio parameter to the manager. - /// @param param The audio parameter to add. - /// @note Should be only used from JavaScript/HostObjects thread - void addAudioParam(const std::shared_ptr ¶m); - - /// @brief Adds an audio buffer to the manager for destruction. - /// @note Called directly from the Audio thread (bypasses SPSC). - void addAudioBufferForDestruction(std::shared_ptr buffer); - - void cleanup(); - - private: - AudioDestructor nodeDestructor_; - AudioDestructor bufferDestructor_; - - /// @brief Initial capacity for various node types for deletion - /// @note Higher capacity decreases number of reallocations at runtime (can be easily adjusted to 128 if needed) - static constexpr size_t kInitialCapacity = 32; - - /// @brief Initial capacity for event passing channel - /// @note High value reduces wait time for sender (JavaScript/HostObjects thread here) - static constexpr size_t kChannelCapacity = 1024; - - std::vector> sourceNodes_; - std::vector> processingNodes_; - std::vector> audioParams_; - std::vector> audioBuffers_; - - channels::spsc::Receiver receiver_; - - channels::spsc::Sender sender_; - - void settlePendingConnections(); - static void handleConnectEvent(std::unique_ptr event); - static void handleDisconnectEvent(std::unique_ptr event); - static void handleDisconnectAllEvent(std::unique_ptr event); - void handleAddToDeconstructionEvent(std::unique_ptr event); - - template - static bool canBeDestructed(const std::shared_ptr &object) { - return object.use_count() == 1; - } - - template - requires std::derived_from - static bool canBeDestructed(std::shared_ptr const &node) { - // If the node is an AudioScheduledSourceNode, we need to check if it is - // playing - if constexpr (std::is_base_of_v) { - return node.use_count() == 1 && (node->isUnscheduled() || node->isFinished()); - } - - 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; - } - - template - requires std::convertible_to - static void prepareForDestruction( - std::vector> &vec, - AudioDestructor &audioDestructor) { - if (vec.empty()) { - return; - } - /// An example of input-output - /// for simplicity we will be considering vector where each value represents - /// use_count() of an element vec = [1, 2, 1, 3, 1] our end result will be vec - /// = [2, 3, 1, 1, 1] After this operation all nodes with use_count() == 1 - /// will be at the end and we will try to send them. After sending, we will - /// only keep audio objects with use_count() > 1 or which failed vec = [2, 3, failed, - /// sent, sent] failed will be always before sents vec = [2, 3, failed] and - /// we resize - /// @note if there are no nodes with use_count() == 1 `begin` will be equal to - /// vec.size() - /// @note if all audio objects have use_count() == 1 `begin` will be 0 - - int begin = 0; - int end = vec.size() - 1; // can be -1 (edge case) - - // Moves all audio objects with use_count() == 1 to the end - // nodes in range [begin, vec.size()) should be deleted - // so new size of the vector will be `begin` - while (begin <= end) { - while (begin < end && AudioGraphManager::canBeDestructed(vec[end])) { - end--; - } - if (AudioGraphManager::canBeDestructed(vec[begin])) { - std::swap(vec[begin], vec[end]); - end--; - } - begin++; - } - - for (int i = begin; i < vec.size(); i++) { - if constexpr (HasCleanupMethod) { - if (vec[i]) { - vec[i]->cleanup(); - } - } - - /// If we fail to add we can't safely remove the node from the vector - /// so we swap it and advance begin cursor - /// @note vec[i] does NOT get moved out if it is not successfully added. - if (!audioDestructor.tryAddForDeconstruction(std::move(vec[i]))) { - std::swap(vec[i], vec[begin]); - begin++; - } - } - if (begin < vec.size()) { - // it does not reallocate if newer size is < current size - vec.resize(begin); - } - } -}; - -#undef AUDIO_GRAPH_MANAGER_SPSC_OPTIONS - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h index a70b2b305..c071ca8fc 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h @@ -36,6 +36,9 @@ inline constexpr size_t PROMISE_VENDOR_THREAD_POOL_WORKER_COUNT = 4; inline constexpr size_t PROMISE_VENDOR_THREAD_POOL_LOAD_BALANCER_QUEUE_SIZE = 32; inline constexpr size_t PROMISE_VENDOR_THREAD_POOL_WORKER_QUEUE_SIZE = 32; +// Disposer payload size (= sizeof(std::vector)) +inline constexpr size_t DISPOSER_PAYLOAD_SIZE = 24; + // Cache line size #ifdef __cpp_lib_hardware_interference_size using std::hardware_constructive_interference_size; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Disposer.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Disposer.hpp similarity index 95% rename from packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Disposer.hpp rename to packages/react-native-audio-api/common/cpp/audioapi/core/utils/Disposer.hpp index 7b2e6f030..6a1883f54 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Disposer.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Disposer.hpp @@ -4,12 +4,11 @@ #include #include -#include #include #include #include -namespace audioapi::utils::graph { +namespace audioapi::utils { /// @brief A disposal payload that can hold any trivially-relocatable or /// move-constructible type up to N bytes. The value is moved into a raw byte @@ -22,7 +21,7 @@ struct DisposalPayload { void (*destructor)(void *); // type-erased destructor /// @brief Sentinel check — a null destructor means "shutdown". - bool isSentinel() const; + [[nodiscard]] bool isSentinel() const; /// @brief Creates a sentinel payload used to signal worker thread shutdown. static DisposalPayload sentinel(); @@ -139,9 +138,9 @@ DisposerImpl::DisposerImpl(size_t channelCapacity) { auto [sender, receiver] = channel(channelCapacity); sender_ = std::move(sender); - workerHandle_ = std::thread([receiver = std::move(receiver)]() mutable { + workerHandle_ = std::thread([receiver_ = std::move(receiver)]() mutable { while (true) { - auto payload = receiver.receive(); + auto payload = receiver_.receive(); if (payload.isSentinel()) { break; } @@ -163,4 +162,4 @@ bool DisposerImpl::doDispose(DisposalPayload &&payload) { return sender_.try_send(std::move(payload)) == audioapi::channels::spsc::ResponseStatus::SUCCESS; } -} // namespace audioapi::utils::graph +} // namespace audioapi::utils diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.cpp new file mode 100644 index 000000000..9b470092e --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.cpp @@ -0,0 +1,210 @@ +#include +#include + +namespace audioapi::utils::graph { + +// ── Accessors ───────────────────────────────────────────────────────────── + +auto AudioGraph::operator[](std::uint32_t index) -> Node & { + return nodes[index]; +} + +auto AudioGraph::operator[](std::uint32_t index) const -> const Node & { + return nodes[index]; +} + +size_t AudioGraph::size() const { + return nodes.size(); +} + +bool AudioGraph::empty() const { + return nodes.empty(); +} + +InputPool &AudioGraph::pool() { + return pool_; +} + +const InputPool &AudioGraph::pool() const { + return pool_; +} + +// ── Mutators & processing ───────────────────────────────────────────────── + +void AudioGraph::reserveNodes(std::uint32_t capacity) { + nodes.reserve(capacity); +} + +void AudioGraph::markDirty() { + topo_order_dirty = true; +} + +void AudioGraph::addNode(std::shared_ptr handle) { + handle->index = static_cast(nodes.size()); + nodes.emplace_back(std::move(handle)); +} + +void AudioGraph::process() { + if (topo_order_dirty) { + topo_order_dirty = false; + kahn_toposort(); + if (topo_order_dirty) { + return; + } + } + + const auto n = static_cast(nodes.size()); + + // ── Pass 1: mark deletions (cascading, left-to-right in topo order) ──── + // A node is deleted when: orphaned && no live inputs && canBeDestructed(). + // Because the array is topologically sorted, removing a source first lets + // its dependents see the updated input set and potentially cascade. + for (auto &node : nodes) { + pool_.removeIf( + node.input_head, [this](std::uint32_t inp) { return nodes[inp].will_be_deleted; }); + + if (node.orphaned && InputPool::isEmpty(node.input_head) && + node.handle->audioNode->canBeDestructed()) { + node.will_be_deleted = true; + } + } + + // ── Compute new-position remap (stored in after_compaction_ind) ───────── + std::uint32_t new_pos = 0; + for (std::uint32_t i = 0; i < n; i++) { + if (!nodes[i].will_be_deleted) { + nodes[i].after_compaction_ind = static_cast(new_pos); + new_pos++; + } + // deleted nodes keep after_compaction_ind == -1 (default) + } + + // ── Pass 2a: remap inputs to post-compaction indices ───────────────────── + // Must happen BEFORE shifting nodes, because shifting invalidates source + // positions that later nodes' inputs may still reference. + for (std::uint32_t e = 0; e < n; e++) { + if (nodes[e].will_be_deleted) { + continue; + } + for (auto &inp : pool_.mutableView(nodes[e].input_head)) { + inp = static_cast(nodes[inp].after_compaction_ind); + } + } + + // ── Pass 2b: compact — shift kept nodes left ─────────────────────────── + std::uint32_t b = 0; + for (std::uint32_t e = 0; e < n; e++) { + if (nodes[e].will_be_deleted) { + continue; + } + if (b != e) { + nodes[b] = std::move(nodes[e]); + nodes[e].input_head = InputPool::kNull; // prevent double-free in truncation + } + nodes[b].handle->index = b; + b++; + } + + // Truncate — dropping shared_ptr decrements refcount (2 → 1); + // HostGraph detects this and destroys the ghost on the main thread. + for (std::uint32_t i = b; i < n; i++) { + // Free any lingering pool slots (should already be empty for deleted nodes) + pool_.freeAll(nodes[i].input_head); + // Handle may have been moved-from during compaction, so just null it + nodes[i].handle = nullptr; + } + nodes.resize(b); + + // Reset scratch fields for next compaction + for (auto &node : nodes) { + node.after_compaction_ind = -1; + node.will_be_deleted = false; + } +} + +// ── Kahn's toposort ─────────────────────────────────────────────────────── + +void AudioGraph::kahn_toposort() { + const auto n = static_cast(nodes.size()); + if (n <= 1) { + return; + } + + // Phase 1: compute out-degree + for (const auto &nd : nodes) { + for (std::uint32_t inp : pool_.view(nd.input_head)) { + nodes[inp].topo_out_degree++; + } + } + + // Phase 2: reverse Kahn BFS — sinks first, sources last in dequeue order. + // FIFO queue embedded as a linked list through after_compaction_ind. + std::int32_t qh = -1, qt = -1; + auto enq = [&](std::uint32_t i) { + nodes[i].after_compaction_ind = -1; + if (qh == -1) [[unlikely]] { + qh = qt = static_cast(i); + } else { + nodes[qt].after_compaction_ind = static_cast(i); + qt = static_cast(i); + } + }; + + for (std::uint32_t i = 0; i < n; i++) { + if (nodes[i].topo_out_degree == 0) { + enq(i); + } + } + + std::uint32_t write = n; + while (qh != -1) { + auto idx = static_cast(qh); + qh = nodes[idx].after_compaction_ind; + nodes[idx].after_compaction_ind = static_cast(--write); + + for (std::uint32_t inp : pool_.view(nodes[idx].input_head)) { + if (--nodes[inp].topo_out_degree == 0) { + enq(inp); + } + } + } + + // Phase 3: remap input indices to new positions (before nodes move) + for (auto &nd : nodes) { + for (std::uint32_t &inp : pool_.mutableView(nd.input_head)) { + inp = static_cast(nodes[inp].after_compaction_ind); + } + } + + // Phase 4: apply permutation in place via cycle sort + for (std::uint32_t i = 0; i < n; i++) { + while (nodes[i].after_compaction_ind != static_cast(i)) { + auto t = static_cast(nodes[i].after_compaction_ind); + std::swap(nodes[i], nodes[t]); + } + } + + // Phase 5: update handle indices & reset scratch + for (std::uint32_t i = 0; i < n; i++) { + if (nodes[i].handle) { + nodes[i].handle->index = i; + } + nodes[i].after_compaction_ind = -1; + } +} + +AudioGraph::NodeBuffer AudioGraph::adoptNodeBuffer(NodeBuffer preAllocated) { + // Move live nodes into the pre-allocated (empty, large-capacity) buffer. + // No reallocation: preAllocated.data.capacity() >= nodes.size() guaranteed + // by the main thread before sending this event. + preAllocated.data.insert( + preAllocated.data.end(), + std::make_move_iterator(nodes.begin()), + std::make_move_iterator(nodes.end())); + std::swap(nodes, preAllocated.data); + // preAllocated.data now holds the old (small) buffer with moved-from nodes. + // Caller disposes it off the audio thread. + return preAllocated; +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.h new file mode 100644 index 000000000..da96b08c2 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.h @@ -0,0 +1,190 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace audioapi::utils::graph { + +/// @brief Cache-friendly, index-stable node storage with in-place topological sort. +/// +/// Nodes are stored in a flat vector that is kept topologically sorted +/// (sources first, sinks last). The graph supports O(V+E) compaction of +/// orphaned nodes and O(1)-extra-space Kahn's toposort. +/// +/// @note Can store at most 2^30 nodes due to bit-packed indices (~10^9). +class AudioGraph { + // ── Node ──────────────────────────────────────────────────────────────── + + struct Node { + Node() = default; + explicit Node(std::shared_ptr handle) : handle(std::move(handle)) {} + + std::shared_ptr handle = nullptr; // owned handle bridging to HostGraph + std::uint32_t input_head = InputPool::kNull; // head of input linked list in pool_ + + std::uint32_t topo_out_degree : 31 = 0; // scratch — Kahn's out-degree counter + unsigned will_be_deleted : 1 = 0; // scratch — marked for compaction removal + std::int32_t after_compaction_ind : 31 = + -1; // scratch — new index after compaction / BFS linked-list next + + /// Node is removed when: orphaned && inputs.empty() && canBeDestructed() + unsigned orphaned : 1 = 0; // means this node was removed from host graph + +#if RN_AUDIO_API_TEST + size_t test_node_identifier__ = 0; +#endif + }; + + public: + AudioGraph() = default; + ~AudioGraph() = default; + + AudioGraph(const AudioGraph &) = delete; + AudioGraph &operator=(const AudioGraph &) = delete; + + AudioGraph(AudioGraph &&) noexcept = default; + AudioGraph &operator=(AudioGraph &&) noexcept = default; + + // ── Node buffer pre-allocation (main-thread → audio-thread handoff) ───── + + /// @brief Opaque pre-allocated node storage. + /// + /// Created on the main thread via makeNodeBuffer(), then handed to the + /// audio thread via adoptNodeBuffer(). The returned (old) buffer must be + /// disposed off the audio thread. + struct NodeBuffer { + std::vector data; + explicit NodeBuffer(std::uint32_t capacity) { + data.reserve(capacity); + } + NodeBuffer() = default; + }; + + /// @brief Allocates a node buffer with the given capacity on the calling thread. + [[nodiscard]] static NodeBuffer makeNodeBuffer(std::uint32_t capacity) { + return NodeBuffer(capacity); + } + + /// @brief Installs a pre-allocated node buffer on the audio thread. + /// + /// Moves all live nodes into the pre-allocated buffer (allocation-free: + /// capacity was ensured on the main thread), swaps it in, and returns + /// the old buffer for disposal off the audio thread. + /// + /// @note Must be called only from the audio thread. + [[nodiscard]] NodeBuffer adoptNodeBuffer(NodeBuffer preAllocated); + + /// @brief Entry returned by iter() — a reference to the graph object and a view of its inputs. + template + struct Entry { + GraphObject &graphObject; + InputsView inputs; + }; + + // ── Accessors ─────────────────────────────────────────────────────────── + + /// @brief Access node by flat-vector index. + [[nodiscard]] Node &operator[](std::uint32_t index); + + /// @brief Access node by flat-vector index (const). + [[nodiscard]] const Node &operator[](std::uint32_t index) const; + + /// @brief Number of live nodes in the graph. + [[nodiscard]] size_t size() const; + + /// @brief Whether the graph is empty. + [[nodiscard]] bool empty() const; + + /// @brief Provides an iterable view of the nodes in topological order. + /// + /// Each entry contains a reference to the GraphObject and an immutable view + /// of its inputs (as references to GraphObject). + /// + /// ## Example usage: + /// ```cpp + /// for (auto [graphObject, inputs] : graph.iter()) { + /// // process graphObject and its inputs + /// } + /// ``` + /// @note Lifetime of entries is bound to this graph — they are not owned. + /// @note Using this iterator after modifying the graph is undefined behavior. + [[nodiscard]] auto iter(); + + /// @brief Returns a reference to the input pool used for edge storage. + [[nodiscard]] InputPool &pool(); + [[nodiscard]] const InputPool &pool() const; + + /// @brief Pre-reserves the internal node vector to at least `capacity`. + /// + /// Call from `processGrowEvents()` (outside the allocation-free zone) + /// so that subsequent `addNode` calls within `processEvents()` do not + /// trigger vector reallocation. + void reserveNodes(std::uint32_t capacity); + + // ── Mutators ──────────────────────────────────────────────────────────── + + /// @brief Marks the topological ordering as dirty so the next process() + /// recomputes it. + void markDirty(); + + /// @brief Adds a new node. AudioGraph takes shared ownership of the handle. + /// @param handle shared NodeHandle bridging to HostGraph + void addNode(std::shared_ptr handle); + + /// @brief Recomputes topological order (if dirty), then compacts the graph + /// by removing orphaned, input-free, destructible nodes. + /// + /// When a node is compacted out its `shared_ptr` is released + /// (refcount drops 2 → 1). HostGraph detects this via `use_count() == 1` + /// and destroys the ghost + GraphObject on the main thread. + /// + /// Uses a two-pass approach: pass 1 marks deletions (cascading in topo + /// order) and computes index remapping; pass 2 remaps inputs and shifts + /// kept nodes left. + /// + /// Time: O(V + E) + /// + /// Extra space: O(1) — everything in place. + void process(); + + private: + std::vector nodes; // always kept topologically sorted + InputPool pool_; // pool backing all input linked lists + bool topo_order_dirty = false; // set by markDirty(), cleared by process() + + /// @brief In-place Kahn's toposort (sources first, sinks last). + /// + /// Uses `after_compaction_ind` as an embedded FIFO linked-list for the + /// BFS queue, and cycle-sort for the final permutation. + /// + /// Time: O(V + E) + /// + /// Extra space: O(1). + void kahn_toposort(); +}; + +inline auto AudioGraph::iter() { + return nodes | + std::views::filter([](const Node &n) { return n.handle->audioNode->isProcessable(); }) | + std::views::transform([this](Node &node) { + return Entry{ + .graphObject = *node.handle->audioNode, + .inputs = pool_.view(node.input_head) | + std::views::transform([this](std::uint32_t idx) -> const GraphObject & { + return *nodes[idx].handle->audioNode; + }), + }; + }); +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.hpp deleted file mode 100644 index 1d56c9322..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.hpp +++ /dev/null @@ -1,360 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace audioapi::utils::graph { - -template -concept AudioGraphNode = std::derived_from; - -/// @brief Cache-friendly, index-stable node storage with in-place topological sort. -/// -/// Nodes are stored in a flat vector that is kept topologically sorted -/// (sources first, sinks last). The graph supports O(V+E) compaction of -/// orphaned nodes and O(1)-extra-space Kahn's toposort. -/// -/// @note Can store at most 2^30 nodes due to bit-packed indices (~10^9). -template -class AudioGraph { - // ── Node ──────────────────────────────────────────────────────────────── - - struct Node { - Node() = default; - explicit Node(std::shared_ptr> handle) : handle(handle) {} - - std::shared_ptr> handle = nullptr; // owned handle bridging to HostGraph - std::uint32_t input_head = InputPool::kNull; // head of input linked list in pool_ - - std::uint32_t topo_out_degree : 31 = 0; // scratch — Kahn's out-degree counter - unsigned will_be_deleted : 1 = 0; // scratch — marked for compaction removal - std::int32_t after_compaction_ind : 31 = - -1; // scratch — new index after compaction / BFS linked-list next - - /// Node is removed when: orphaned && inputs.empty() && canBeDestructed() - unsigned orphaned : 1 = 0; // means this node was removed from host graph - -#if RN_AUDIO_API_TEST - size_t test_node_identifier__ = 0; -#endif - }; - - public: - AudioGraph() = default; - ~AudioGraph() = default; - - AudioGraph(const AudioGraph &) = delete; - AudioGraph &operator=(const AudioGraph &) = delete; - - AudioGraph(AudioGraph &&) noexcept = default; - AudioGraph &operator=(AudioGraph &&) noexcept = default; - - /// @brief Entry returned by iter() — a reference to the audio node and a view of its inputs. - template - struct Entry { - NodeType &audioNode; - InputsView inputs; - }; - - // ── Accessors ─────────────────────────────────────────────────────────── - - /// @brief Access node by flat-vector index. - [[nodiscard]] Node &operator[](std::uint32_t index); - - /// @brief Access node by flat-vector index (const). - [[nodiscard]] const Node &operator[](std::uint32_t index) const; - - /// @brief Number of live nodes in the graph. - [[nodiscard]] size_t size() const; - - /// @brief Whether the graph is empty. - [[nodiscard]] bool empty() const; - - /// @brief Provides an iterable view of the nodes in topological order. - /// - /// Each entry contains a reference to the AudioNode and an immutable view - /// of its inputs (as references to AudioNodes). - /// - /// ## Example usage: - /// ```cpp - /// for (auto [audioNode, inputs] : graph.iter()) { - /// // process audioNode and its inputs - /// } - /// ``` - /// @note Lifetime of entries is bound to this graph — they are not owned. - /// @note Using this iterator after modifying the graph is undefined behavior. - [[nodiscard]] auto iter(); - - /// @brief Returns a reference to the input pool used for edge storage. - [[nodiscard]] InputPool &pool(); - [[nodiscard]] const InputPool &pool() const; - - /// @brief Pre-reserves the internal node vector to at least `capacity`. - /// - /// Call from `processGrowEvents()` (outside the allocation-free zone) - /// so that subsequent `addNode` calls within `processEvents()` do not - /// trigger vector reallocation. - void reserveNodes(std::uint32_t capacity); - - // ── Mutators ──────────────────────────────────────────────────────────── - - /// @brief Marks the topological ordering as dirty so the next process() - /// recomputes it. - void markDirty(); - - /// @brief Adds a new node. AudioGraph takes shared ownership of the handle. - /// @param handle shared NodeHandle bridging to HostGraph - void addNode(std::shared_ptr> handle); - - /// @brief Recomputes topological order (if dirty), then compacts the graph - /// by removing orphaned, input-free, destructible nodes. - /// - /// When a node is compacted out its `shared_ptr` is released - /// (refcount drops 2 → 1). HostGraph detects this via `use_count() == 1` - /// and destroys the ghost + AudioNode on the main thread. - /// - /// Uses a two-pass approach: pass 1 marks deletions (cascading in topo - /// order) and computes index remapping; pass 2 remaps inputs and shifts - /// kept nodes left. - /// - /// Time: O(V + E) - /// - /// Extra space: O(1) — everything in place. - void process(); - - private: - std::vector nodes; // always kept topologically sorted - InputPool pool_; // pool backing all input linked lists - bool topo_order_dirty = false; // set by markDirty(), cleared by process() - - /// @brief In-place Kahn's toposort (sources first, sinks last). - /// - /// Uses `after_compaction_ind` as an embedded FIFO linked-list for the - /// BFS queue, and cycle-sort for the final permutation. - /// - /// Time: O(V + E) - /// - /// Extra space: O(1). - void kahn_toposort(); -}; - -// ========================================================================= -// Implementation -// ========================================================================= - -// ── Accessors ───────────────────────────────────────────────────────────── - -template -auto AudioGraph::operator[](std::uint32_t index) -> Node & { - return nodes[index]; -} - -template -auto AudioGraph::operator[](std::uint32_t index) const -> const Node & { - return nodes[index]; -} - -template -size_t AudioGraph::size() const { - return nodes.size(); -} - -template -bool AudioGraph::empty() const { - return nodes.empty(); -} - -template -auto AudioGraph::iter() { - return nodes | std::views::transform([this](Node &node) { - return Entry{ - *node.handle->audioNode, - pool_.view(node.input_head) | - std::views::transform([this](std::uint32_t idx) -> const NodeType & { - return *nodes[idx].handle->audioNode; - })}; - }); -} - -template -InputPool &AudioGraph::pool() { - return pool_; -} - -template -const InputPool &AudioGraph::pool() const { - return pool_; -} - -template -void AudioGraph::reserveNodes(std::uint32_t capacity) { - nodes.reserve(capacity); -} - -// ── Mutators ────────────────────────────────────────────────────────────── - -template -void AudioGraph::markDirty() { - topo_order_dirty = true; -} - -template -void AudioGraph::addNode(std::shared_ptr> handle) { - handle->index = static_cast(nodes.size()); - nodes.emplace_back(std::move(handle)); -} - -template -void AudioGraph::process() { - if (topo_order_dirty) { - topo_order_dirty = false; - kahn_toposort(); - if (topo_order_dirty) { - return; - } - } - - const auto n = static_cast(nodes.size()); - - // ── Pass 1: mark deletions (cascading, left-to-right in topo order) ──── - // A node is deleted when: orphaned && no live inputs && canBeDestructed(). - // Because the array is topologically sorted, removing a source first lets - // its dependents see the updated input set and potentially cascade. - for (auto &node : nodes) { - pool_.removeIf( - node.input_head, [this](std::uint32_t inp) { return nodes[inp].will_be_deleted; }); - - if (node.orphaned && InputPool::isEmpty(node.input_head) && - node.handle->audioNode->canBeDestructed()) { - node.will_be_deleted = true; - } - } - - // ── Compute new-position remap (stored in after_compaction_ind) ───────── - std::uint32_t new_pos = 0; - for (std::uint32_t i = 0; i < n; i++) { - if (!nodes[i].will_be_deleted) { - nodes[i].after_compaction_ind = static_cast(new_pos); - new_pos++; - } - // deleted nodes keep after_compaction_ind == -1 (default) - } - - // ── Pass 2a: remap inputs to post-compaction indices ───────────────────── - // Must happen BEFORE shifting nodes, because shifting invalidates source - // positions that later nodes' inputs may still reference. - for (std::uint32_t e = 0; e < n; e++) { - if (nodes[e].will_be_deleted) - continue; - for (auto &inp : pool_.mutableView(nodes[e].input_head)) { - inp = static_cast(nodes[inp].after_compaction_ind); - } - } - - // ── Pass 2b: compact — shift kept nodes left ─────────────────────────── - std::uint32_t b = 0; - for (std::uint32_t e = 0; e < n; e++) { - if (nodes[e].will_be_deleted) - continue; - if (b != e) { - nodes[b] = std::move(nodes[e]); - nodes[e].input_head = InputPool::kNull; // prevent double-free in truncation - } - nodes[b].handle->index = b; - b++; - } - - // Truncate — dropping shared_ptr decrements refcount (2 → 1); - // HostGraph detects this and destroys the ghost on the main thread. - for (std::uint32_t i = b; i < n; i++) { - // Free any lingering pool slots (should already be empty for deleted nodes) - pool_.freeAll(nodes[i].input_head); - nodes[i].handle = nullptr; - } - nodes.resize(b); - - // Reset scratch fields for next compaction - for (auto &node : nodes) { - node.after_compaction_ind = -1; - node.will_be_deleted = false; - } -} - -// ── Kahn's toposort ─────────────────────────────────────────────────────── - -template -void AudioGraph::kahn_toposort() { - const auto n = static_cast(nodes.size()); - if (n <= 1) - return; - - // Phase 1: compute out-degree - for (const auto &nd : nodes) { - for (std::uint32_t inp : pool_.view(nd.input_head)) - nodes[inp].topo_out_degree++; - } - - // Phase 2: reverse Kahn BFS — sinks first, sources last in dequeue order. - // FIFO queue embedded as a linked list through after_compaction_ind. - std::int32_t qh = -1, qt = -1; - auto enq = [&](std::uint32_t i) { - nodes[i].after_compaction_ind = -1; - if (qh == -1) [[unlikely]] { - qh = qt = static_cast(i); - } else { - nodes[qt].after_compaction_ind = static_cast(i); - qt = static_cast(i); - } - }; - - for (std::uint32_t i = 0; i < n; i++) { - if (nodes[i].topo_out_degree == 0) - enq(i); - } - - std::uint32_t write = n; - while (qh != -1) { - auto idx = static_cast(qh); - qh = nodes[idx].after_compaction_ind; - nodes[idx].after_compaction_ind = static_cast(--write); - - for (std::uint32_t inp : pool_.view(nodes[idx].input_head)) { - if (--nodes[inp].topo_out_degree == 0) - enq(inp); - } - } - - // Phase 3: remap input indices to new positions (before nodes move) - for (auto &nd : nodes) { - for (std::uint32_t &inp : pool_.mutableView(nd.input_head)) - inp = static_cast(nodes[inp].after_compaction_ind); - } - - // Phase 4: apply permutation in place via cycle sort - for (std::uint32_t i = 0; i < n; i++) { - while (nodes[i].after_compaction_ind != static_cast(i)) { - auto t = static_cast(nodes[i].after_compaction_ind); - std::swap(nodes[i], nodes[t]); - } - } - - // Phase 5: update handle indices & reset scratch - for (std::uint32_t i = 0; i < n; i++) { - if (nodes[i].handle) - nodes[i].handle->index = i; - nodes[i].after_compaction_ind = -1; - } -} - -} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/BridgeNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/BridgeNode.cpp new file mode 100644 index 000000000..e52c0cbd0 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/BridgeNode.cpp @@ -0,0 +1,36 @@ +#include +#include + +namespace audioapi::utils::graph { + +BridgeNode::BridgeNode(AudioParam *param) : param_(param) {} + +bool BridgeNode::canBeDestructed() const { + return true; +} + +const DSPAudioBuffer *BridgeNode::getOutput() const { + return nullptr; +} + +AudioParam *BridgeNode::param() const { + return param_; +} + +void BridgeNode::processInputs(const std::vector &inputs, int numFrames) { + // Skip processing if param is null (e.g., in tests) + if (param_ == nullptr) { + return; + } + + // Get AudioParam's input buffer and fill it with mixed inputs + auto inputBuffer = param_->getInputBuffer(); + inputBuffer->zero(); + + for (const DSPAudioBuffer *input : inputs) { + inputBuffer->sum(*input, ChannelInterpretation::SPEAKERS); + } + (void)numFrames; +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/BridgeNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/BridgeNode.h new file mode 100644 index 000000000..d88e22f0e --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/BridgeNode.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace audioapi { +class AudioParam; +} + +namespace audioapi::utils::graph { + +/// @brief Processable graph node that bridges AudioNode outputs to AudioParam inputs. +/// +/// A BridgeNode sits between source AudioNode(s) and the owner AudioNode of a +/// param, forming the path: source(s) → bridge → owner. This lets the graph +/// system detect cycles and compute correct topological ordering for param +/// connections. +/// +/// BridgeNodes are: +/// - **Processable** — mixes inputs and writes directly to AudioParam's input buffer. +/// - **Not mixable** — getOutput() returns nullptr, so other nodes won't mix it. +/// - **Always destructible** — removed by compaction when orphaned with no inputs. +/// +/// ## Lifetime Management +/// - HostAudioParam owns the BridgeNode +/// - BridgeNode holds raw pointer to AudioParam (lifetime guaranteed by owner) +/// - When HostAudioParam is destructed, BridgeNode becomes orphaned +/// - canBeDestructed() always returns true, so it's cleaned up on next compaction +class BridgeNode final : public GraphObject { + public: + explicit BridgeNode(AudioParam *param); + + [[nodiscard]] bool canBeDestructed() const override; + + /// @brief Returns nullptr - BridgeNode should not be mixed as input for other nodes. + [[nodiscard]] const DSPAudioBuffer *getOutput() const override; + + /// @brief Returns the param this bridge represents a connection to. + [[nodiscard]] AudioParam *param() const; + + protected: + void processInputs(const std::vector &inputs, int numFrames) override; + + private: + AudioParam *param_; +}; + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.cpp new file mode 100644 index 000000000..b409a5897 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.cpp @@ -0,0 +1,158 @@ +// Graph coordinator implementation (formerly inline in Graph.hpp). + +#include +#include + +#include +#include +#include + +namespace audioapi::utils::graph { + +Graph::Graph(size_t eventQueueCapacity, Disposer *disposer) + : disposer_(disposer) { + using namespace audioapi::channels::spsc; + + auto [es, er] = + channel(eventQueueCapacity); + eventSender_ = std::move(es); + eventReceiver_ = std::move(er); + + auto [gs, gr] = + channel(eventQueueCapacity); + gcEventSender_ = std::move(gs); + gcEventReceiver_ = std::move(gr); +} + +Graph::Graph( + size_t eventQueueCapacity, + Disposer *disposer, + std::uint32_t initialNodeCapacity, + std::uint32_t initialEdgeCapacity) + : Graph(eventQueueCapacity, disposer) { + if (initialNodeCapacity > 0) { + audioGraph.reserveNodes(initialNodeCapacity); + nodeCapacity_ = initialNodeCapacity; + } + if (initialEdgeCapacity > 0) { + audioGraph.pool().grow(initialEdgeCapacity); + poolCapacity_ = initialEdgeCapacity; + } +} + +void Graph::processEvents() { + AGEvent event; + // Drain Channel A (JS thread producer: addNode / addEdge / grow / …) + // fully first — this guarantees that any `addNode(X)` pending here is + // applied to AudioGraph before we process a matching `orphan(X)` that + // may already be sitting in Channel B. + while (eventReceiver_.try_receive(event) == audioapi::channels::spsc::ResponseStatus::SUCCESS) { + if (event) { + event(audioGraph, *disposer_); + } + } + // Drain Channel B (finalizer / GC thread producer: removeNode orphan + // events). These are idempotent w.r.t. each other and never require + // capacity growth (they only flip a boolean on an existing node). + while (gcEventReceiver_.try_receive(event) == audioapi::channels::spsc::ResponseStatus::SUCCESS) { + if (event) { + event(audioGraph, *disposer_); + } + } +} + +void Graph::process() { + audioGraph.process(); +} + +Graph::HNode *Graph::addNode(std::unique_ptr audioNode) { + // collectDisposedNodes(); + + auto handle = std::make_shared(0, std::move(audioNode)); + auto [hostNode, event] = hostGraph.addNode(handle); + + sendNodeGrowIfNeeded(); + + eventSender_.send(std::move(event)); + return hostNode; +} + +Graph::Res Graph::removeNode(HNode *node) { + // collectDisposedNodes(); + // Routed through Channel B: HostNode destructors (and therefore this + // call) may fire on the JS runtime's finalizer thread (e.g. Hermes GC). + // Sending through the dedicated SPSC channel keeps the single-producer + // invariant for both channels. + return hostGraph.removeNode(node).map([&](AGEvent event) { + gcEventSender_.send(std::move(event)); + return NoneType{}; + }); +} + +Graph::Res Graph::addEdge(HNode *from, HNode *to) { + // collectDisposedNodes(); + return hostGraph.addEdge(from, to).map([&](AGEvent event) { + sendPoolGrowIfNeeded(); + eventSender_.send(std::move(event)); + return NoneType{}; + }); +} + +void Graph::linkNodes(HNode *from, HNode *to) { + HostGraph::linkNodes(from, to); +} + +Graph::Res Graph::removeEdge(HNode *from, HNode *to) { + // collectDisposedNodes(); + return hostGraph.removeEdge(from, to).map([&](AGEvent event) { + eventSender_.send(std::move(event)); + return NoneType{}; + }); +} + +Graph::Res Graph::removeAllEdges(HNode *from) { + // collectDisposedNodes(); + return hostGraph.removeAllEdges(from).map([&](AGEvent event) { + eventSender_.send(std::move(event)); + return NoneType{}; + }); +} + +void Graph::collectDisposedNodes() { + hostGraph.collectDisposedNodes(); +} + +void Graph::sendPoolGrowIfNeeded() { + auto edges = static_cast(hostGraph.edgeCount()); + // edges > poolCapacity_ / 2 || (poolCapacity_ == 0 && edges > 0) left for clarity + if (edges > poolCapacity_ / 2) { + std::uint32_t newCap = std::max(static_cast(edges * 2), std::uint32_t{64}); + auto buf = std::make_unique(newCap); + eventSender_.send( + [buf = std::move(buf), newCap]( + AudioGraph &graph, Disposer &disposer) mutable { + auto *old = graph.pool().adoptBuffer(buf.release(), newCap); + if (old) { + disposer.dispose(OwnedSlotBuffer(old)); + } + }); + poolCapacity_ = newCap; + } +} + +void Graph::sendNodeGrowIfNeeded() { + auto nodes = static_cast(hostGraph.nodeCount()); + if (nodes > nodeCapacity_) { + std::uint32_t newCap = std::max(static_cast(nodes * 2), std::uint32_t{64}); + auto buf = AudioGraph::makeNodeBuffer(newCap); + eventSender_.send( + [buf = std::move(buf)]( + AudioGraph &graph, Disposer &disposer) mutable { + auto old = graph.adoptNodeBuffer(std::move(buf)); + disposer.dispose(std::move(old)); + }); + nodeCapacity_ = newCap; + } +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.h new file mode 100644 index 000000000..0524ae37e --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.h @@ -0,0 +1,188 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace audioapi::utils::graph { + +/// @brief Thread-safe graph coordinator that bridges HostGraph (main thread) +/// and AudioGraph (audio thread) via two SPSC event channels. +/// +/// Two producer threads are supported: +/// - The JS thread produces structural mutations (`addNode`, `addEdge`, +/// `removeEdge`, `removeAllEdges`) and pre-grow events on **Channel A**. +/// - The JS runtime's finalizer / GC thread produces `removeNode` +/// (orphan) events on **Channel B**. This is the path taken when a +/// HostObject's destructor runs on the Hermes GC finalizer thread. +/// +/// The audio thread drains Channel A fully before Channel B in every +/// `processEvents()` call. That preserves the invariant +/// `addNode(X) happens-before orphan(X)` on the audio side even though +/// the two channels carry no cross-ordering by themselves. +/// +/// Memory pre-growth: the main thread tracks edge and node counts. When +/// growth is needed it sends an inline grow AGEvent immediately followed +/// by the graph-mutation AGEvent through Channel A, guaranteeing FIFO +/// ordering: the audio thread always applies growth before the operation +/// that needs it. +/// +/// ## Audio-thread call order +/// ``` +/// graph.processEvents(); // drain Channel A, then Channel B (FIFO within each) +/// graph.process(); // toposort + compaction +/// for (auto&& [node, inputs] : graph.iter()) { ... } +/// ``` +class Graph { + using AGEvent = HostGraph::AGEvent; + + // ── Event channel (main → audio): grow + graph mutations ─────────────── + + using EventReceiver = audioapi::channels::spsc::Receiver< + AGEvent, + audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL, + audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>; + using EventSender = audioapi::channels::spsc::Sender< + AGEvent, + audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL, + audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>; + + using HNode = HostGraph::Node; + + public: + using ResultError = HostGraph::ResultError; + using Res = Result; + + Graph(size_t eventQueueCapacity, Disposer *disposer); + + Graph( + size_t eventQueueCapacity, + Disposer *disposer, + std::uint32_t initialNodeCapacity, + std::uint32_t initialEdgeCapacity); + + // ── Audio-thread API ──────────────────────────────────────────────────── + + /// @brief Processes all scheduled events (grow + graph-mutation). + /// + /// Grow events (pool buffer adoption, node vector reserve) may allocate, + /// so call this **before** entering the allocation-free zone. + /// Graph-mutation events (addNode, orphan, push, remove, markDirty) are + /// allocation-free because their capacity was ensured by a preceding + /// grow event in the same FIFO. + /// + /// @note Should be called only from the audio thread. + void processEvents(); + + /// @brief Runs toposort + compaction on the audio graph. + /// Allocation-free. + /// @note Should be called only from the audio thread. + void process(); + + /// @brief Returns an iterable view of nodes in topological order. + /// + /// Each entry contains a reference to GraphObject and an immutable view + /// of its inputs (as references to GraphObject). + /// Allocation-free. + /// + /// @note Should be called only from the audio thread, after process(). + [[nodiscard]] auto iter() { + return audioGraph.iter(); + } + + // ── Main-thread API ──────────────────────────────────────────────────── + + /// @brief Adds a new node to the graph and returns a pointer to it. + /// @param audioNode the audio processing node to add (ownership transferred) + /// @return pointer to the newly added HostGraph::Node + HNode *addNode(std::unique_ptr audioNode = nullptr); + + template TObject> + HNode *addNode(std::unique_ptr audioNode) { + return addNode(std::unique_ptr(std::move(audioNode))); + } + + /// @brief Removes a node (marks as ghost). Pointer remains valid until + /// the ghost is collected after AudioGraph releases its shared_ptr. + Res removeNode(HNode *node); + + /// @brief Adds a directed edge from → to. Rejects cycles and duplicates. + Res addEdge(HNode *from, HNode *to); + + /// @brief Links two nodes so that `to` follows the processable state of + /// `from` (one-way). No AudioGraph side effect — purely a host-graph hint + /// for propagating the processable state between nodes that share + /// processing semantics but not an audio edge (e.g. DelayReader → + /// DelayWriter). + static void linkNodes(HNode *from, HNode *to); + + /// @brief Removes a directed edge from → to. + Res removeEdge(HNode *from, HNode *to); + + /// @brief Removes all outgoing edges from `from`. + Res removeAllEdges(HNode *from); + + void collectDisposedNodes(); + + private: + using OwnedSlotBuffer = std::unique_ptr; + using OwnedNodeBuffer = AudioGraph::NodeBuffer; + + // Aligning to cache line size to prevent false sharing between audio and main thread + alignas(hardware_destructive_interference_size) AudioGraph audioGraph; + alignas(hardware_destructive_interference_size) HostGraph hostGraph; + + // ── Channels (immutable after construction — no false sharing) ───────── + // + // Channel A: JS thread producer — addNode / addEdge / removeEdge / + // removeAllEdges / grow events. + // Channel B: finalizer (GC) thread producer — removeNode (orphan) events. + // + // Each channel has a single producer, so SPSC invariants hold. + + EventSender eventSender_; + EventReceiver eventReceiver_; + + EventSender gcEventSender_; + EventReceiver gcEventReceiver_; + + // ── Disposer — destroys old pool buffers off the audio thread ─────────── + + Disposer *disposer_; + + // ── Main-thread tracking for pre-growth ───────────────────────────────── + + std::uint32_t poolCapacity_ = 0; ///< Pool capacity we have ensured + std::uint32_t nodeCapacity_ = 0; ///< Node vector capacity we have ensured + + /// @brief Pre-grows the InputPool when the edge count approaches capacity. + /// + /// Queries HostGraph::edgeCount() for the current truth. Allocates a new + /// slot buffer on the main thread and sends it as an AGEvent through the + /// event channel. The old buffer is sent to the Disposer for deallocation + /// on a separate thread — never on the audio thread. + void sendPoolGrowIfNeeded(); + + /// @brief Pre-reserves the AudioGraph node vector when node count exceeds + /// the last ensured capacity. Allocates a new node buffer on the main + /// thread and sends it as an AGEvent through the event channel. The old + /// buffer is sent to the Disposer for deallocation on a separate thread — + /// never on the audio thread. + void sendNodeGrowIfNeeded(); + + // ── Bridge tracking (main thread only) ────────────────────────────────── + + friend class GraphTest; +}; + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.hpp deleted file mode 100644 index 269f32476..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.hpp +++ /dev/null @@ -1,227 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include -#include - -namespace audioapi::utils::graph { - -/// @brief Thread-safe graph coordinator that bridges HostGraph (main thread) -/// and AudioGraph (audio thread) via a single SPSC event channel. -/// -/// Memory pre-growth: the main thread tracks edge and node counts. When -/// growth is needed it sends an inline grow AGEvent immediately followed -/// by the graph-mutation AGEvent through the **same** channel, guaranteeing -/// FIFO ordering: the audio thread always applies growth before the -/// operation that needs it. -/// -/// ## Audio-thread call order -/// ``` -/// graph.processEvents(); // apply pending graph mutations (if any) — in FIFO order -/// graph.process(); // toposort + compaction -/// for (auto&& [node, inputs] : graph.iter()) { ... } -/// ``` -template -class Graph { - using AGEvent = HostGraph::AGEvent; - - // ── Event channel (main → audio): grow + graph mutations ─────────────── - - using EventReceiver = audioapi::channels::spsc::Receiver< - AGEvent, - audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL, - audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>; - using EventSender = audioapi::channels::spsc::Sender< - AGEvent, - audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL, - audioapi::channels::spsc::WaitStrategy::BUSY_LOOP>; - - using HNode = HostGraph::Node; - - public: - using ResultError = HostGraph::ResultError; - using Res = Result; - - explicit Graph(size_t eventQueueCapacity) { - using namespace audioapi::channels::spsc; - - auto [es, er] = channel( - eventQueueCapacity); - eventSender_ = std::move(es); - eventReceiver_ = std::move(er); - } - - Graph( - size_t eventQueueCapacity, - std::uint32_t initialNodeCapacity, - std::uint32_t initialEdgeCapacity) - : Graph(eventQueueCapacity) { - if (initialNodeCapacity > 0) { - audioGraph.reserveNodes(initialNodeCapacity); - nodeCapacity_ = initialNodeCapacity; - } - if (initialEdgeCapacity > 0) { - audioGraph.pool().grow(initialEdgeCapacity); - poolCapacity_ = initialEdgeCapacity; - } - } - - // ── Audio-thread API ──────────────────────────────────────────────────── - - /// @brief Processes all scheduled events (grow + graph-mutation). - /// - /// Grow events (pool buffer adoption, node vector reserve) may allocate, - /// so call this **before** entering the allocation-free zone. - /// Graph-mutation events (addNode, orphan, push, remove, markDirty) are - /// allocation-free because their capacity was ensured by a preceding - /// grow event in the same FIFO. - /// - /// @note Should be called only from the audio thread. - void processEvents() { - AGEvent event; - while (eventReceiver_.try_receive(event) == audioapi::channels::spsc::ResponseStatus::SUCCESS) { - if (event) { - event(audioGraph, disposer_); - } - } - } - - /// @brief Runs toposort + compaction on the audio graph. - /// Allocation-free. - /// @note Should be called only from the audio thread. - void process() { - audioGraph.process(); - } - - /// @brief Returns an iterable view of nodes in topological order. - /// - /// Each entry contains a reference to the NodeType and an immutable view - /// of its inputs (as references to NodeType). - /// Allocation-free. - /// - /// @note Should be called only from the audio thread, after process(). - [[nodiscard]] auto iter() { - return audioGraph.iter(); - } - - // ── Main-thread API ──────────────────────────────────────────────────── - - /// @brief Adds a new node to the graph and returns a pointer to it. - /// @param audioNode the audio processing node to add (ownership transferred) - /// @return pointer to the newly added HostGraph::Node - HNode *addNode(std::unique_ptr audioNode = nullptr) { - hostGraph.collectDisposedNodes(); - - auto handle = std::make_shared>(0, std::move(audioNode)); - auto [hostNode, event] = hostGraph.addNode(handle); - - sendNodeGrowIfNeeded(); - - eventSender_.send(std::move(event)); - return hostNode; - } - - /// @brief Removes a node (marks as ghost). Pointer remains valid until - /// the ghost is collected after AudioGraph releases its shared_ptr. - Res removeNode(HNode *node) { - hostGraph.collectDisposedNodes(); - return hostGraph.removeNode(node).map([&](AGEvent event) { - eventSender_.send(std::move(event)); - return NoneType{}; - }); - } - - /// @brief Adds a directed edge from → to. Rejects cycles and duplicates. - Res addEdge(HNode *from, HNode *to) { - hostGraph.collectDisposedNodes(); - return hostGraph.addEdge(from, to).map([&](AGEvent event) { - sendPoolGrowIfNeeded(); - eventSender_.send(std::move(event)); - return NoneType{}; - }); - } - - /// @brief Removes a directed edge from → to. - Res removeEdge(HNode *from, HNode *to) { - hostGraph.collectDisposedNodes(); - return hostGraph.removeEdge(from, to).map([&](AGEvent event) { - eventSender_.send(std::move(event)); - return NoneType{}; - }); - } - - private: - static constexpr size_t kDisposerPayloadSize = HostGraph::kDisposerPayloadSize; - - using OwnedSlotBuffer = std::unique_ptr; - - // Aligning to cache line size to prevent false sharing between audio and main thread - alignas(hardware_destructive_interference_size) AudioGraph audioGraph; - alignas(hardware_destructive_interference_size) HostGraph hostGraph; - - // ── Channel (immutable after construction — no false sharing) ─────────── - - EventSender eventSender_; - EventReceiver eventReceiver_; - - // ── Disposer — destroys old pool buffers off the audio thread ─────────── - - DisposerImpl disposer_{64}; - - // ── Main-thread tracking for pre-growth ───────────────────────────────── - - std::uint32_t poolCapacity_ = 0; ///< Pool capacity we have ensured - std::uint32_t nodeCapacity_ = 0; ///< Node vector capacity we have ensured - - /// @brief Pre-grows the InputPool when the edge count approaches capacity. - /// - /// Queries HostGraph::edgeCount() for the current truth. Allocates a new - /// slot buffer on the main thread and sends it as an AGEvent through the - /// event channel. The old buffer is sent to the Disposer for deallocation - /// on a separate thread — never on the audio thread. - void sendPoolGrowIfNeeded() { - auto edges = static_cast(hostGraph.edgeCount()); - // edges > poolCapacity_ / 2 || (poolCapacity_ == 0 && edges > 0) left for clarity - if (edges > poolCapacity_ / 2) { - std::uint32_t newCap = std::max(static_cast(edges * 2), std::uint32_t{64}); - auto buf = std::make_unique(newCap); - eventSender_.send( - [buf = std::move(buf), newCap]( - AudioGraph &graph, Disposer &disposer) mutable { - auto *old = graph.pool().adoptBuffer(buf.release(), newCap); - if (old) { - disposer.dispose(OwnedSlotBuffer(old)); - } - }); - poolCapacity_ = newCap; - } - } - - /// @brief Pre-reserves the AudioGraph node vector when node count exceeds - /// the last ensured capacity. Queries HostGraph::nodeCount() for the - /// current truth. Sends a grow event through the event channel. - void sendNodeGrowIfNeeded() { - auto nodes = static_cast(hostGraph.nodeCount()); - if (nodes > nodeCapacity_) { - std::uint32_t newCap = std::max(static_cast(nodes * 2), std::uint32_t{64}); - eventSender_.send( - [newCap](AudioGraph &graph, auto &) { graph.reserveNodes(newCap); }); - nodeCapacity_ = newCap; - } - } - - friend class GraphTest; -}; - -} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.cpp new file mode 100644 index 000000000..777f593a4 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.cpp @@ -0,0 +1,36 @@ +#include +#include + +namespace audioapi::utils::graph { + +bool GraphObject::canBeDestructed() const { + return true; +} + +bool GraphObject::isProcessable() const { + return processableState_ != PROCESSABLE_STATE::NOT_PROCESSABLE; +} + +void GraphObject::setProcessableState(PROCESSABLE_STATE state) { + processableState_ = state; +} + +const DSPAudioBuffer *GraphObject::getOutput() const { + return nullptr; +} + +AudioNode *GraphObject::asAudioNode() { + return nullptr; +} + +const AudioNode *GraphObject::asAudioNode() const { + return nullptr; +} + +void GraphObject::processInputs(const std::vector &inputs, int numFrames) { + // Default: do nothing. Subclasses override for actual processing. + (void)inputs; + (void)numFrames; +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.h new file mode 100644 index 000000000..c4feb1975 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.h @@ -0,0 +1,98 @@ +#pragma once + +#include + +#include +#include + +#include +#include + +namespace audioapi { +class AudioNode; +} // namespace audioapi + +namespace audioapi::utils::graph { + +/// @brief Base class for graph objects (AudioNode, BridgeNode, etc.). +/// +/// GraphObjects are owned by NodeHandles and stored in AudioGraph's flat vector +/// +/// ## Lifecycle +/// - Created on the main thread as a unique_ptr +/// - Transferred to AudioGraph via NodeHandle on node addition +/// - Accessed on the audio thread during processing +/// - Destroyed when all below conditions are met: +/// 1. The HostNode is removed and the NodeHandle is marked as a ghost +/// 2. The Node has no inputs +/// 3. canBeDestructed() returns true +/// +/// ## Processing Model +/// - `isProcessable()` determines if the node participates in audio processing +/// - `process()` is called during graph iteration for processable nodes +/// - `getOutput()` returns the output buffer; nullptr means not mixable as input +class GraphObject { + public: + GraphObject() = default; + virtual ~GraphObject() = default; + DELETE_COPY_AND_MOVE(GraphObject); + + /// @brief Returns whether this graph object can be safely destroyed. + [[nodiscard]] virtual bool canBeDestructed() const; + + /// @brief Controls how `isProcessable()` is derived for nodes that support conditional processing. + enum class PROCESSABLE_STATE : std::uint8_t { + ALWAYS_PROCESSABLE, + NOT_PROCESSABLE, + CONDITIONAL_PROCESSABLE, + }; + + /// @brief Returns whether this node should be processed during audio iteration. + [[nodiscard]] virtual bool isProcessable() const; + + void setProcessableState(PROCESSABLE_STATE state); + + /// @brief Returns the output buffer for this node. + /// @return Pointer to output buffer, or nullptr if this node should not + /// contribute to input mixing for other nodes. + [[nodiscard]] virtual const DSPAudioBuffer *getOutput() const; + + /// @brief Processes this node with the given inputs. + /// Filters inputs to only those with valid output buffers. + /// @tparam R Range of GraphObject references (inputs from the graph) + /// @param inputs Range of input GraphObjects + /// @param numFrames Number of audio frames to process + template + requires std::same_as, const GraphObject &> + void process(R &&inputs, int numFrames) { + // Collect valid input buffers (those with non-null getOutput) + inputBuffers_.clear(); + for (const GraphObject &input : inputs) { + if (const DSPAudioBuffer *output = input.getOutput()) { + inputBuffers_.push_back(output); + } + } + processInputs(inputBuffers_, numFrames); + } + + /// @brief Downcast helper for JS thread communication with AudioNode. + [[nodiscard]] virtual AudioNode *asAudioNode(); + + /// @brief Downcast helper for JS thread communication with AudioNode. + [[nodiscard]] virtual const AudioNode *asAudioNode() const; + + protected: + friend class HostGraph; + /// @brief Implementation of processing logic with filtered input buffers. + /// @param inputs Vector of pointers to valid input buffers + /// @param numFrames Number of audio frames to process + virtual void processInputs(const std::vector &inputs, int numFrames); + + PROCESSABLE_STATE processableState_ = PROCESSABLE_STATE::NOT_PROCESSABLE; + + private: + // Reusable buffer for collecting inputs (avoids allocation per frame) + std::vector inputBuffers_; +}; + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.cpp new file mode 100644 index 000000000..67f8aa5df --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.cpp @@ -0,0 +1,446 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace audioapi::utils::graph { + +namespace { + +/// @brief Returns the AudioNode associated with `node`, or nullptr if the +/// node has no audio payload (e.g. the lightweight `graph->addNode()` used +/// in tests that exercise topology only). +inline audioapi::AudioNode *audioNodeOf(const HostGraph::Node *node) { + if (node == nullptr || node->handle == nullptr || node->handle->audioNode == nullptr) { + return nullptr; + } + return node->handle->audioNode->asAudioNode(); +} + +/// @brief Computes the channel count that `dest`'s output buffer must carry +/// after the current set of inputs (`dest->inputs`). Follows the Web Audio +/// rules for `channelCountMode`: +/// - EXPLICIT -> `channelCount` attribute (inputs ignored) +/// - MAX -> max over inputs' channel counts +/// - CLAMPED_MAX -> min(channelCount attribute, max over inputs') +/// +/// When there are no inputs the node keeps its own `channelCount` attribute +/// (matches the shape the buffer already had at construction time). +/// +/// Reads only host-thread-owned state: the const `channelCountMode_` and +/// the `channelCount` attributes of the upstream nodes. +size_t negotiateChannelCount(const HostGraph::Node *dest) { + auto *destAudio = audioNodeOf(dest); + if (destAudio == nullptr) { + return 0; + } + + const auto attr = static_cast(destAudio->getChannelCount()); + const auto mode = destAudio->getChannelCountMode(); + + if (mode == audioapi::ChannelCountMode::EXPLICIT || dest->inputs.empty()) { + return attr; + } + + size_t maxInputChannels = 0; + for (const HostGraph::Node *input : dest->inputs) { + auto *inAudio = audioNodeOf(input); + if (inAudio == nullptr) { + continue; + } + const auto c = static_cast(inAudio->getChannelCount()); + maxInputChannels = std::max(c, maxInputChannels); + } + + if (maxInputChannels == 0) { + return attr; + } + + switch (mode) { + case audioapi::ChannelCountMode::MAX: + return maxInputChannels; + case audioapi::ChannelCountMode::CLAMPED_MAX: + return std::min(attr, maxInputChannels); + case audioapi::ChannelCountMode::EXPLICIT: + return attr; + } + return attr; +} + +/// @brief If `dest` is a real AudioNode and the newly negotiated channel +/// count differs from what its output buffer already has, allocates a +/// replacement `DSPAudioBuffer` on the host thread and returns it. +/// Returns `nullptr` when no swap is needed (no audio payload, negotiation +/// converged, or context gone). +std::shared_ptr buildNegotiatedBufferIfNeeded( + const HostGraph::Node *dest) { + auto *destAudio = audioNodeOf(dest); + if (destAudio == nullptr) { + return nullptr; + } + + const size_t desired = negotiateChannelCount(dest); + if (desired == 0) { + return nullptr; + } + + // The current buffer shape is the only authoritative "current state" for + // channel count (the attribute can differ from the effective buffer in + // MAX / CLAMPED_MAX modes). Reading `getNumberOfChannels()` here is a + // plain load of a size_t owned by the shared_ptr we already hold, so it + // is safe on the host thread as long as the buffer pointer itself is + // only ever swapped under an AGEvent (which is the case). + const auto current = destAudio->getOutputBuffer(); + if (current != nullptr && current->getNumberOfChannels() == desired) { + return nullptr; + } + + return std::make_shared( + audioapi::RENDER_QUANTUM_SIZE, static_cast(desired), destAudio->getContextSampleRate()); +} + +} // namespace + +// ========================================================================= +// Implementation +// ========================================================================= + +bool HostGraph::TraversalState::visit(size_t currentTerm) { + if (term == currentTerm) { + return false; + } + term = currentTerm; + return true; +} + +HostGraph::Node::~Node() { + for (Node *input : inputs) { + auto &outs = input->outputs; + outs.erase(std::remove(outs.begin(), outs.end(), this), outs.end()); + } + for (Node *output : outputs) { + auto &inps = output->inputs; + inps.erase(std::remove(inps.begin(), inps.end(), this), inps.end()); + } +} + +HostGraph::HostGraph() = default; + +HostGraph::~HostGraph() { + std::scoped_lock lock(nodesMutex_); + for (Node *n : nodes) { + n->linkedNodes.clear(); + } + for (Node *n : nodes) { + delete n; + } + nodes.clear(); +} + +HostGraph::HostGraph(HostGraph &&other) noexcept + : nodes(std::move(other.nodes)), edgeCount_(other.edgeCount_), last_term(other.last_term) { + other.edgeCount_ = 0; + other.last_term = 0; +} + +auto HostGraph::operator=(HostGraph &&other) noexcept -> HostGraph & { + if (this != &other) { + std::scoped_lock lock(nodesMutex_, other.nodesMutex_); + for (Node *n : nodes) { + n->linkedNodes.clear(); + } + for (Node *n : nodes) { + delete n; + } + nodes = std::move(other.nodes); + edgeCount_ = other.edgeCount_; + last_term = other.last_term; + other.edgeCount_ = 0; + other.last_term = 0; + } + return *this; +} + +auto HostGraph::addNode(std::shared_ptr handle) -> std::pair { + std::scoped_lock lock(nodesMutex_); + Node *newNode = new Node(); + newNode->handle = handle; + nodes.push_back(newNode); + + auto event = [h = std::move(handle)](auto &graph, auto &) { + graph.addNode(std::move(h)); + }; + + return {newNode, std::move(event)}; +} + +auto HostGraph::removeNode(Node *node) -> Res { + std::scoped_lock lock(nodesMutex_); + auto it = std::ranges::find(nodes, node); + if (it == nodes.end()) { + return Res::Err(ResultError::NODE_NOT_FOUND); + } + + node->ghost = true; + + return Res::Ok( + [h = node->handle](AudioGraph &graph, auto &) { graph[h->index].orphaned = true; }); +} + +void HostGraph::markNodesAsProcessing(Node *node) { + if (node == nullptr) { + return; + } + // Already CONDITIONAL or ALWAYS — do not recurse. Stops infinite recursion + // on cycles (e.g. delay feedback) and avoids redundant walks. + if (node->handle->audioNode->isProcessable()) { + return; + } + node->handle->audioNode->setProcessableState( + GraphObject::PROCESSABLE_STATE::CONDITIONAL_PROCESSABLE); + + for (Node *input : node->inputs) { + markNodesAsProcessing(input); + } + for (Node *linked : node->linkedNodes) { + markNodesAsProcessing(linked); + } +} + +auto HostGraph::addEdge(Node *from, Node *to) -> Res { + std::scoped_lock lock(nodesMutex_); + if (std::ranges::find(nodes, from) == nodes.end() || + std::ranges::find(nodes, to) == nodes.end()) { + return Res::Err(ResultError::NODE_NOT_FOUND); + } + if (from->ghost || to->ghost) { + return Res::Err(ResultError::NODE_NOT_FOUND); + } + + for (Node *out : from->outputs) { + if (out == to) { + return Res::Err(ResultError::EDGE_ALREADY_EXISTS); + } + } + + if (hasPath(to, from)) { + return Res::Err(ResultError::CYCLE_DETECTED); + } + + from->outputs.push_back(to); + to->inputs.push_back(from); + edgeCount_++; + + // Channel-count negotiation: computed + allocated on the host thread, + // applied on the audio thread by the AGEvent below. + auto negotiatedBuffer = buildNegotiatedBufferIfNeeded(to); + + // could be problematic, since we are passing raw pointers to the lambda + return Res::Ok([from, to, negotiatedBuffer = std::move(negotiatedBuffer)]( + AudioGraph &graph, auto &disposer) mutable { + auto *fromNode = from->handle ? from->handle->audioNode.get() : nullptr; + auto *toNode = to->handle ? to->handle->audioNode.get() : nullptr; + if (fromNode && toNode && !fromNode->isProcessable() && toNode->isProcessable()) { + markNodesAsProcessing(from); + } + if (auto *toAudio = (toNode ? toNode->asAudioNode() : nullptr); + toAudio != nullptr && negotiatedBuffer != nullptr) { + auto oldBuffer = toAudio->getOutputBuffer(); + toAudio->setOutputBuffer(std::move(negotiatedBuffer)); + if (oldBuffer != nullptr) { + disposer.dispose(std::move(oldBuffer)); + } + } + graph.pool().push(graph[to->handle->index].input_head, from->handle->index); + graph.markDirty(); + }); +} + +void HostGraph::markNodesAsNotProcessing(Node *node) { + if (node == nullptr || node->handle == nullptr || node->handle->audioNode == nullptr) { + return; + } + auto *audioNode = node->handle->audioNode.get(); + if (!audioNode->isProcessable()) { + return; + } + if (audioNode->processableState_ != GraphObject::PROCESSABLE_STATE::CONDITIONAL_PROCESSABLE) { + return; + } + audioNode->setProcessableState(GraphObject::PROCESSABLE_STATE::NOT_PROCESSABLE); + + for (Node *input : node->inputs) { + markNodesAsNotProcessing(input); + } + for (Node *linked : node->linkedNodes) { + markNodesAsNotProcessing(linked); + } +} + +auto HostGraph::removeEdge(Node *from, Node *to) -> Res { + std::scoped_lock lock(nodesMutex_); + if (std::ranges::find(nodes, from) == nodes.end() || + std::ranges::find(nodes, to) == nodes.end()) { + return Res::Err(ResultError::NODE_NOT_FOUND); + } + if (from->ghost || to->ghost) { + return Res::Err(ResultError::NODE_NOT_FOUND); + } + + auto itOut = std::ranges::find(from->outputs, to); + if (itOut == from->outputs.end()) { + return Res::Err(ResultError::EDGE_NOT_FOUND); + } + + auto itIn = std::ranges::find(to->inputs, from); + if (itIn != to->inputs.end()) { + to->inputs.erase(itIn); + } + from->outputs.erase(itOut); + edgeCount_--; + + // Channel-count negotiation: computed + allocated on the host thread, + // applied on the audio thread by the AGEvent below. + auto negotiatedBuffer = buildNegotiatedBufferIfNeeded(to); + + // could be problematic, since we are passing raw pointers to the lambda + return Res::Ok([from, to, negotiatedBuffer = std::move(negotiatedBuffer)]( + AudioGraph &graph, auto &disposer) mutable { + auto *fromAudio = (from && from->handle) ? from->handle->audioNode.get() : nullptr; + + if (fromAudio != nullptr && + fromAudio->processableState_ == GraphObject::PROCESSABLE_STATE::CONDITIONAL_PROCESSABLE) { + bool updateProcessingNodes = std::ranges::all_of(from->outputs, [](Node *output) { + auto *outAudio = (output && output->handle) ? output->handle->audioNode.get() : nullptr; + return outAudio == nullptr || !outAudio->isProcessable(); + }); + if (updateProcessingNodes) { + HostGraph::markNodesAsNotProcessing(from); + } + } + auto *toNode = to->handle ? to->handle->audioNode.get() : nullptr; + if (auto *toAudio = (toNode ? toNode->asAudioNode() : nullptr); + toAudio != nullptr && negotiatedBuffer != nullptr) { + auto oldBuffer = toAudio->getOutputBuffer(); + toAudio->setOutputBuffer(negotiatedBuffer); + if (oldBuffer != nullptr) { + disposer.dispose(std::move(oldBuffer)); + } + } + graph.pool().remove(graph[to->handle->index].input_head, from->handle->index); + graph.markDirty(); + }); +} + +auto HostGraph::removeAllEdges(Node *from) -> Res { + std::scoped_lock lock(nodesMutex_); + if (std::ranges::find(nodes, from) == nodes.end() || from->ghost) { + return Res::Err(ResultError::NODE_NOT_FOUND); + } + + auto pairs = std::vector>(); + pairs.reserve(from->outputs.size()); + + for (Node *to : from->outputs) { + auto itIn = std::ranges::find(to->inputs, from); + if (itIn != to->inputs.end()) { + to->inputs.erase(itIn); + } + edgeCount_--; + pairs.emplace_back(from->handle->index, to->handle->index); + } + from->outputs.clear(); + + return Res::Ok([pairs = std::move(pairs), from](AudioGraph &graph, auto &disposer) mutable { + auto *fromNode = from->handle->audioNode->asAudioNode(); + if (fromNode != nullptr && + fromNode->processableState_ == GraphObject::PROCESSABLE_STATE::CONDITIONAL_PROCESSABLE) { + HostGraph::markNodesAsNotProcessing(from); + } + for (const auto &[fromIdx, toIdx] : pairs) { + graph.pool().remove(graph[toIdx].input_head, fromIdx); + } + graph.markDirty(); + disposer.dispose(std::move(pairs)); + }); +} + +bool HostGraph::hasPath(Node *start, Node *end) { + if (start == end) { + return true; + } + + last_term++; + size_t term = last_term; + + std::vector stack; + stack.push_back(start); + start->traversalState.term = term; + + while (!stack.empty()) { + Node *curr = stack.back(); + stack.pop_back(); + + if (curr == end) { + return true; + } + + for (Node *out : curr->outputs) { + if (out->traversalState.visit(term)) { + stack.push_back(out); + } + } + } + return false; +} + +void HostGraph::linkNodes(Node *from, Node *to) { + if (from == nullptr || to == nullptr || from == to) { + return; + } + if (std::ranges::find(from->linkedNodes, to) != from->linkedNodes.end()) { + return; + } + from->linkedNodes.push_back(to); +} + +size_t HostGraph::edgeCount() const { + std::scoped_lock lock(nodesMutex_); + return edgeCount_; +} + +size_t HostGraph::nodeCount() const { + std::scoped_lock lock(nodesMutex_); + return nodes.size(); +} + +void HostGraph::collectDisposedNodes() { + std::scoped_lock lock(nodesMutex_); + for (auto it = nodes.begin(); it != nodes.end();) { + Node *n = *it; + if (n->ghost && n->handle.use_count() == 1) { + edgeCount_ -= n->outputs.size(); + for (Node *m : nodes) { + auto &ln = m->linkedNodes; + ln.erase(std::remove(ln.begin(), ln.end(), n), ln.end()); + } + *it = nodes.back(); + nodes.pop_back(); + delete n; + } else { + ++it; + } + } +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.h new file mode 100644 index 000000000..54f8ec4af --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.h @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class GraphCycleDebugTest; + +namespace audioapi::utils::graph { + +class Graph; +class TestGraphUtils; + +/// @brief Main-thread graph mirror that keeps structure in sync with AudioGraph. +/// +/// Maintains adjacency lists (inputs / outputs) for O(V+E) cycle detection +/// via DFS. Every mutation produces an `AGEvent` lambda that, when executed on +/// the audio thread, applies the same structural change to AudioGraph. +/// +/// Ghost nodes: when a node is removed it is marked `ghost = true` but its +/// edges are kept so that `hasPath` still sees paths through nodes that are +/// alive in AudioGraph. Ghosts are collected once AudioGraph releases its +/// shared_ptr (detected via `use_count() == 1`). +/// +/// @note Use through the Graph wrapper for safety. +class HostGraph { + public: + enum class ResultError : uint8_t { + NODE_NOT_FOUND, + CYCLE_DETECTED, + EDGE_NOT_FOUND, + EDGE_ALREADY_EXISTS, + }; + + /// Event that modifies AudioGraph to keep it consistent with HostGraph. + /// The second argument is the Disposer used to offload buffer deallocation. + using AGEvent = FatFunction<32, void(AudioGraph &, Disposer &)>; + + using Res = Result; + + /// Per-node scratch used by graph traversals (e.g. hasPath). + struct TraversalState { + size_t term = 0; + + /// @return true if node was not yet visited in the current traversal term + bool visit(size_t currentTerm); + }; + + /// A single node in the HostGraph. + struct Node { + std::vector inputs; // reversed edges + std::vector outputs; // forward edges + /// Nodes whose processable state should follow this node's state. + /// Used to tie together the state of logically-linked host nodes that do + /// not share a graph edge (e.g. DelayReader → DelayWriter communicate via + /// a ring buffer, so no audio edge exists, but they must be processed + /// together). One-way: when THIS node is marked (not)processing, every + /// entry in `linkedNodes` is marked too (recursively). + std::vector linkedNodes; + TraversalState traversalState; + std::shared_ptr handle; // shared handle bridging to AudioGraph + bool ghost = false; // kept for cycle detection until AudioGraph confirms deletion + +#if RN_AUDIO_API_TEST + size_t test_node_identifier__ = 0; +#endif + + /// Destructor tears down all edges touching this node. + ~Node(); + }; + + // ── Lifecycle ─────────────────────────────────────────────────────────── + + HostGraph(); + ~HostGraph(); + + HostGraph(const HostGraph &) = delete; + HostGraph &operator=(const HostGraph &) = delete; + + HostGraph(HostGraph &&other) noexcept; + HostGraph &operator=(HostGraph &&other) noexcept; + + // ── Public API ────────────────────────────────────────────────────────── + + /// @brief Adds a new node to the graph. + /// @param handle shared handle that bridges HostGraph ↔ AudioGraph + /// @return pair of (raw Node pointer, AGEvent to replay on AudioGraph) + std::pair addNode(std::shared_ptr handle); + + /// @brief Removes a node (marks it as ghost, keeps edges for cycle detection). + /// @return AGEvent that sets `orphaned = true` on the AudioGraph side. + Res removeNode(Node *node); + + /// @brief Adds a directed edge from → to. Rejects cycles and duplicates. + /// @return AGEvent that adds the input on the AudioGraph side. + Res addEdge(Node *from, Node *to); + + /// @brief Links the processable-state of `from` to propagate into `to`. + /// + /// When `from` is marked (not)processing, `to` will be too. The link is + /// one-way and does NOT create a graph edge (no AudioGraph side effect). + /// Intended for host nodes that share processing semantics but do not + /// share an audio edge (e.g. DelayReader → DelayWriter). + /// + /// If the same pair is already linked, this is a no-op. + static void linkNodes(Node *from, Node *to); + + /// @brief Marks this node CONDITIONAL_PROCESSABLE, then recurses into inputs + /// and linkedNodes. Skips nodes that are already processable (terminates + /// cycles and redundant paths). + static void markNodesAsProcessing(Node *node); + + /// @brief Removes a directed edge from → to. + /// @return AGEvent that removes the input on the AudioGraph side. + Res removeEdge(Node *from, Node *to); + + /// @brief Marks a node as not processing and recursively marks all inputs as not processing. + static void markNodesAsNotProcessing(Node *node); + + /// @brief Removes all outgoing edges from `from`. + /// @return single AGEvent that removes all inputs on the AudioGraph side, or NODE_NOT_FOUND. + Res removeAllEdges(Node *from); + + /// @brief Current number of live (non-ghost) edges. + [[nodiscard]] size_t edgeCount() const; + + /// @brief Current number of nodes (including ghosts). + [[nodiscard]] size_t nodeCount() const; + + private: + std::vector nodes; + /// Guards access to `nodes` and the per-node adjacency mutated by the + /// public API (inputs/outputs/ghost). Public API methods do not call one + /// another while holding the lock, so a plain mutex is sufficient. + mutable std::mutex nodesMutex_; + size_t edgeCount_ = 0; + size_t last_term = 0; // monotonic counter for traversal freshness + + /// @brief DFS reachability check (traverses ghosts too). + bool hasPath(Node *start, Node *end); + + /// @brief Scans ghost nodes and deletes those whose handle has + /// `use_count() == 1`, meaning AudioGraph has released its reference. + void collectDisposedNodes(); + + friend class Graph; + friend class TestGraphUtils; + friend class HostGraphTest; + friend class GraphCycleDebugTest; +}; + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.hpp deleted file mode 100644 index c785248a5..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostGraph.hpp +++ /dev/null @@ -1,331 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -class GraphCycleDebugTest; - -namespace audioapi::utils::graph { - -template -class HostGraph; -template -class Graph; -class TestGraphUtils; - -/// @brief Main-thread graph mirror that keeps structure in sync with AudioGraph. -/// -/// Maintains adjacency lists (inputs / outputs) for O(V+E) cycle detection -/// via DFS. Every mutation produces an `AGEvent` lambda that, when executed on -/// the audio thread, applies the same structural change to AudioGraph. -/// -/// Ghost nodes: when a node is removed it is marked `ghost = true` but its -/// edges are kept so that `hasPath` still sees paths through nodes that are -/// alive in AudioGraph. Ghosts are collected once AudioGraph releases its -/// shared_ptr (detected via `use_count() == 1`). -/// -/// @note Use through the Graph wrapper for safety. -template -class HostGraph { - public: - enum class ResultError { - NODE_NOT_FOUND, - CYCLE_DETECTED, - EDGE_NOT_FOUND, - EDGE_ALREADY_EXISTS, - }; - - /// Size of the Disposer payload (= sizeof(std::unique_ptr)). - static constexpr size_t kDisposerPayloadSize = 8; - - /// Event that modifies AudioGraph to keep it consistent with HostGraph. - /// The second argument is the Disposer used to offload buffer deallocation. - using AGEvent = FatFunction<32, void(AudioGraph &, Disposer &)>; - - using Res = Result; - - /// Per-node scratch used by graph traversals (e.g. hasPath). - struct TraversalState { - size_t term = 0; - - /// @return true if node was not yet visited in the current traversal term - bool visit(size_t currentTerm); - }; - - /// A single node in the HostGraph. - struct Node { - std::vector inputs; // reversed edges - std::vector outputs; // forward edges - TraversalState traversalState; - std::shared_ptr> handle; // shared handle bridging to AudioGraph - bool ghost = false; // kept for cycle detection until AudioGraph confirms deletion - -#if RN_AUDIO_API_TEST - size_t test_node_identifier__ = 0; -#endif - - /// Destructor tears down all edges touching this node. - ~Node(); - }; - - // ── Lifecycle ─────────────────────────────────────────────────────────── - - HostGraph() = default; - ~HostGraph(); - - HostGraph(const HostGraph &) = delete; - HostGraph &operator=(const HostGraph &) = delete; - - HostGraph(HostGraph &&other) noexcept; - HostGraph &operator=(HostGraph &&other) noexcept; - - // ── Public API ────────────────────────────────────────────────────────── - - /// @brief Adds a new node to the graph. - /// @param handle shared handle that bridges HostGraph ↔ AudioGraph - /// @return pair of (raw Node pointer, AGEvent to replay on AudioGraph) - std::pair addNode(std::shared_ptr> handle); - - /// @brief Removes a node (marks it as ghost, keeps edges for cycle detection). - /// @return AGEvent that sets `orphaned = true` on the AudioGraph side. - Res removeNode(Node *node); - - /// @brief Adds a directed edge from → to. Rejects cycles and duplicates. - /// @return AGEvent that adds the input on the AudioGraph side. - Res addEdge(Node *from, Node *to); - - /// @brief Removes a directed edge from → to. - /// @return AGEvent that removes the input on the AudioGraph side. - Res removeEdge(Node *from, Node *to); - - /// @brief Current number of live (non-ghost) edges. - [[nodiscard]] size_t edgeCount() const; - - /// @brief Current number of nodes (including ghosts). - [[nodiscard]] size_t nodeCount() const; - - private: - std::vector nodes; - size_t edgeCount_ = 0; - size_t last_term = 0; // monotonic counter for traversal freshness - - /// @brief DFS reachability check (traverses ghosts too). - bool hasPath(Node *start, Node *end); - - /// @brief Scans ghost nodes and deletes those whose handle has - /// `use_count() == 1`, meaning AudioGraph has released its reference. - void collectDisposedNodes(); - - friend class Graph; - friend class TestGraphUtils; - friend class HostGraphTest; - friend class GraphCycleDebugTest; -}; - -// ========================================================================= -// Implementation -// ========================================================================= - -template -bool HostGraph::TraversalState::visit(size_t currentTerm) { - if (term == currentTerm) { - return false; - } - term = currentTerm; - return true; -} - -template -HostGraph::Node::~Node() { - for (Node *input : inputs) { - auto &outs = input->outputs; - outs.erase(std::remove(outs.begin(), outs.end(), this), outs.end()); - } - for (Node *output : outputs) { - auto &inps = output->inputs; - inps.erase(std::remove(inps.begin(), inps.end(), this), inps.end()); - } -} - -// ── Lifecycle ───────────────────────────────────────────────────────────── - -template -HostGraph::~HostGraph() { - for (Node *n : nodes) { - delete n; - } - nodes.clear(); -} - -template -HostGraph::HostGraph(HostGraph &&other) noexcept - : nodes(std::move(other.nodes)), edgeCount_(other.edgeCount_), last_term(other.last_term) { - other.edgeCount_ = 0; - other.last_term = 0; -} - -template -auto HostGraph::operator=(HostGraph &&other) noexcept -> HostGraph & { - if (this != &other) { - for (Node *n : nodes) { - delete n; - } - nodes = std::move(other.nodes); - edgeCount_ = other.edgeCount_; - last_term = other.last_term; - other.edgeCount_ = 0; - other.last_term = 0; - } - return *this; -} - -template -auto HostGraph::addNode(std::shared_ptr> handle) - -> std::pair { - Node *newNode = new Node(); - newNode->handle = handle; - nodes.push_back(newNode); - - auto event = [h = std::move(handle)](auto &graph, auto &) { - graph.addNode(h); - }; - - return {newNode, std::move(event)}; -} - -template -auto HostGraph::removeNode(Node *node) -> Res { - auto it = std::find(nodes.begin(), nodes.end(), node); - if (it == nodes.end()) { - return Res::Err(ResultError::NODE_NOT_FOUND); - } - - node->ghost = true; - - return Res::Ok( - [h = node->handle](AudioGraph &graph, auto &) { graph[h->index].orphaned = true; }); -} - -template -auto HostGraph::addEdge(Node *from, Node *to) -> Res { - if (std::find(nodes.begin(), nodes.end(), from) == nodes.end() || - std::find(nodes.begin(), nodes.end(), to) == nodes.end()) { - return Res::Err(ResultError::NODE_NOT_FOUND); - } - if (from->ghost || to->ghost) { - return Res::Err(ResultError::NODE_NOT_FOUND); - } - - for (Node *out : from->outputs) { - if (out == to) - return Res::Err(ResultError::EDGE_ALREADY_EXISTS); - } - - if (hasPath(to, from)) { - return Res::Err(ResultError::CYCLE_DETECTED); - } - - from->outputs.push_back(to); - to->inputs.push_back(from); - edgeCount_++; - - return Res::Ok([hFrom = from->handle, hTo = to->handle](AudioGraph &graph, auto &) { - graph.pool().push(graph[hTo->index].input_head, hFrom->index); - graph.markDirty(); - }); -} - -template -auto HostGraph::removeEdge(Node *from, Node *to) -> Res { - if (std::find(nodes.begin(), nodes.end(), from) == nodes.end() || - std::find(nodes.begin(), nodes.end(), to) == nodes.end()) { - return Res::Err(ResultError::NODE_NOT_FOUND); - } - if (from->ghost || to->ghost) { - return Res::Err(ResultError::NODE_NOT_FOUND); - } - - auto itOut = std::find(from->outputs.begin(), from->outputs.end(), to); - if (itOut == from->outputs.end()) { - return Res::Err(ResultError::EDGE_NOT_FOUND); - } - - auto itIn = std::find(to->inputs.begin(), to->inputs.end(), from); - if (itIn != to->inputs.end()) { - to->inputs.erase(itIn); - } - from->outputs.erase(itOut); - edgeCount_--; - - return Res::Ok([hFrom = from->handle, hTo = to->handle](AudioGraph &graph, auto &) { - graph.pool().remove(graph[hTo->index].input_head, hFrom->index); - graph.markDirty(); - }); -} - -template -bool HostGraph::hasPath(Node *start, Node *end) { - if (start == end) { - return true; - } - - last_term++; - size_t term = last_term; - - std::vector stack; - stack.push_back(start); - start->traversalState.term = term; - - while (!stack.empty()) { - Node *curr = stack.back(); - stack.pop_back(); - - if (curr == end) { - return true; - } - - for (Node *out : curr->outputs) { - if (out->traversalState.visit(term)) { - stack.push_back(out); - } - } - } - return false; -} - -template -size_t HostGraph::edgeCount() const { - return edgeCount_; -} - -template -size_t HostGraph::nodeCount() const { - return nodes.size(); -} - -template -void HostGraph::collectDisposedNodes() { - for (auto it = nodes.begin(); it != nodes.end();) { - Node *n = *it; - if (n->ghost && n->handle.use_count() == 1) { - // ~Node() tears down edges from neighbor lists. - // We decrement for each unique edge (stored once in outputs). - edgeCount_ -= n->outputs.size(); - *it = nodes.back(); - nodes.pop_back(); - delete n; - } else { - ++it; - } - } -} - -} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.cpp new file mode 100644 index 000000000..59d5bb8aa --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.cpp @@ -0,0 +1,56 @@ +#include + +#include +#include + +namespace audioapi::utils::graph { + +HostNode::HostNode(std::shared_ptr graph, std::unique_ptr graphObject) + : graph_(std::move(graph)), node_(graph_->addNode(std::move(graphObject))) {} + +HostNode::~HostNode() { + if (graph_ && (node_ != nullptr)) { + graph_->collectDisposedNodes(); + (void)graph_->removeNode(node_); + node_ = nullptr; + } +} + +HostNode::HostNode(HostNode &&other) noexcept + : graph_(std::move(other.graph_)), node_(other.node_) { + other.node_ = nullptr; +} + +HostNode &HostNode::operator=(HostNode &&other) noexcept { + if (this != &other) { + if (graph_ && (node_ != nullptr)) { + (void)graph_->removeNode(node_); + } + graph_ = std::move(other.graph_); + node_ = other.node_; + other.node_ = nullptr; + } + return *this; +} + +HostNode::Res HostNode::connect(HostNode &other) { + return graph_->addEdge(node_, other.node_); +} + +HostNode::Res HostNode::disconnect(HostNode &other) { + return graph_->removeEdge(node_, other.node_); +} + +HostNode::Res HostNode::disconnect() { + return graph_->removeAllEdges(node_); +} + +HostNode::HNode *HostNode::rawNode() const { + return node_; +} + +const std::shared_ptr &HostNode::graph() const { + return graph_; +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.h similarity index 55% rename from packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.hpp rename to packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.h index e591ac848..19442cc78 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.h @@ -1,7 +1,10 @@ #pragma once -#include +#include +#include +#include +#include #include #include @@ -9,13 +12,13 @@ namespace audioapi::utils::graph { /// @brief RAII base class for host-side nodes. /// -/// Holds a `shared_ptr>` to keep the graph alive and owns a +/// Holds a `shared_ptr` to keep the graph alive and owns a /// `HostGraph::Node*` managed by that graph. On construction the node is /// registered in the graph (and an event is sent to AudioGraph); on /// destruction the node is removed (scheduling orphan-marking on AudioGraph). /// /// Host objects that represent audio processing nodes should publicly inherit -/// from HostNode and pass their payload (the AudioNode-like object) to the +/// from HostNode and pass their payload (GraphObject-derived object) to the /// constructor. `connect` / `disconnect` provide edge management. /// /// @note HostNode intentionally does NOT prevent cycles — callers must handle @@ -23,9 +26,9 @@ namespace audioapi::utils::graph { /// /// ## Example usage: /// ```cpp -/// class MyGainNode : public HostNode { +/// class MyGainNode : public HostNode { /// public: -/// MyGainNode(std::shared_ptr> g, +/// MyGainNode(std::shared_ptr g, /// std::unique_ptr impl) /// : HostNode(std::move(g), std::move(impl)) {} /// }; @@ -34,77 +37,57 @@ namespace audioapi::utils::graph { /// gain->connect(*destination); /// gain.reset(); // destructor removes the node from the graph /// ``` -template class HostNode { public: - using GraphType = Graph; - using HNode = HostGraph::Node; - using ResultError = HostGraph::ResultError; + using GraphType = Graph; + using HNode = HostGraph::Node; + using ResultError = HostGraph::ResultError; using Res = Result; /// @brief Constructs a HostNode, adding it to the graph. /// @param graph shared ownership of the Graph — prevents the graph from /// being destroyed while any HostNode still references it - /// @param audioNode the audio processing payload (ownership transferred - /// through to AudioGraph via NodeHandle) - explicit HostNode(std::shared_ptr graph, std::unique_ptr audioNode = nullptr) - : graph_(std::move(graph)), node_(graph_->addNode(std::move(audioNode))) {} + /// @param graphObject the payload (ownership transferred through to + /// AudioGraph via NodeHandle) + explicit HostNode( + std::shared_ptr graph, + std::unique_ptr graphObject = nullptr); + + template + requires std::derived_from + explicit HostNode(std::shared_ptr graph, std::unique_ptr graphObject) + : HostNode(std::move(graph), std::unique_ptr(std::move(graphObject))) {} /// @brief Destructor removes the node from the graph. /// This marks the node as a ghost in HostGraph, and schedules an event /// that sets `orphaned = true` on the AudioGraph side. - virtual ~HostNode() { - if (graph_ && node_) { - // Ignore the result — the node should always be found unless the - // graph was already torn down. - (void)graph_->removeNode(node_); - node_ = nullptr; - } - } + virtual ~HostNode(); // Non-copyable (unique graph node identity) HostNode(const HostNode &) = delete; HostNode &operator=(const HostNode &) = delete; // Movable - HostNode(HostNode &&other) noexcept : graph_(std::move(other.graph_)), node_(other.node_) { - other.node_ = nullptr; - } - - HostNode &operator=(HostNode &&other) noexcept { - if (this != &other) { - // Remove current node first - if (graph_ && node_) { - (void)graph_->removeNode(node_); - } - graph_ = std::move(other.graph_); - node_ = other.node_; - other.node_ = nullptr; - } - return *this; - } + HostNode(HostNode &&other) noexcept; + HostNode &operator=(HostNode &&other) noexcept; /// @brief Connects this node's output to another node's input (this → other). /// @return Ok on success, Err on cycle / duplicate / not-found - Res connect(HostNode &other) { - return graph_->addEdge(node_, other.node_); - } + Res connect(HostNode &other); /// @brief Disconnects this node's output from another node's input. /// @return Ok on success, Err on not-found - Res disconnect(HostNode &other) { - return graph_->removeEdge(node_, other.node_); - } + Res disconnect(HostNode &other); + + /// @brief Disconnects all this node's outputs. + /// @return Ok on success, Err on not-found + Res disconnect(); /// @brief Returns the raw HostGraph::Node pointer (for advanced usage / testing). - [[nodiscard]] HNode *rawNode() const { - return node_; - } + [[nodiscard]] HNode *rawNode() const; /// @brief Returns the Graph this node belongs to. - [[nodiscard]] const std::shared_ptr &graph() const { - return graph_; - } + [[nodiscard]] const std::shared_ptr &graph() const; protected: std::shared_ptr graph_; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.cpp new file mode 100644 index 000000000..62705a91f --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.cpp @@ -0,0 +1,123 @@ +// InputPool out-of-line definitions (templates remain in InputPool.h). + +#include + +namespace audioapi::utils::graph { + +// ── Lifecycle ───────────────────────────────────────────────────────────── + +InputPool::InputPool() = default; + +InputPool::~InputPool() { + delete[] slots_; +} + +InputPool::InputPool(InputPool &&other) noexcept + : slots_(other.slots_), capacity_(other.capacity_), free_head_(other.free_head_) { + other.slots_ = nullptr; + other.capacity_ = 0; + other.free_head_ = kNull; +} + +InputPool &InputPool::operator=(InputPool &&other) noexcept { + if (this != &other) { + delete[] slots_; + slots_ = other.slots_; + capacity_ = other.capacity_; + free_head_ = other.free_head_; + other.slots_ = nullptr; + other.capacity_ = 0; + other.free_head_ = kNull; + } + return *this; +} + +// ── Slot allocation ─────────────────────────────────────────────────────── + +std::uint32_t InputPool::alloc() { + if (free_head_ == kNull) { + grow(capacity_ == 0 ? 64 : capacity_ * 2); + } + std::uint32_t idx = free_head_; + free_head_ = slots_[idx].next_free; + return idx; +} + +void InputPool::free(std::uint32_t idx) { + slots_[idx].next_free = free_head_; + free_head_ = idx; +} + +// ── Linked-list operations ──────────────────────────────────────────────── + +void InputPool::push(std::uint32_t &head, std::uint32_t inputVal) { + std::uint32_t idx = alloc(); + slots_[idx].val = inputVal; + slots_[idx].next = head; + head = idx; +} + +bool InputPool::remove(std::uint32_t &head, std::uint32_t inputVal) { + std::uint32_t *prev = &head; + std::uint32_t curr = head; + while (curr != kNull) { + if (slots_[curr].val == inputVal) { + *prev = slots_[curr].next; + free(curr); + return true; + } + prev = &slots_[curr].next; + curr = slots_[curr].next; + } + return false; +} + +void InputPool::freeAll(std::uint32_t &head) { + while (head != kNull) { + std::uint32_t nxt = slots_[head].next; + free(head); + head = nxt; + } +} + +bool InputPool::isEmpty(std::uint32_t head) { + return head == kNull; +} + +// ── Iteration ───────────────────────────────────────────────────────────── + +InputPool::InputView InputPool::view(std::uint32_t head) const { + return {.slots = slots_, .head = head}; +} + +InputPool::InputView InputPool::mutableView(std::uint32_t head) { + return {.slots = slots_, .head = head}; +} + +// ── Pool management ─────────────────────────────────────────────────────── + +std::uint32_t InputPool::capacity() const { + return capacity_; +} + +InputPool::Slot *InputPool::adoptBuffer(Slot *newSlots, std::uint32_t newCapacity) { + if (slots_ != nullptr) { + std::memcpy(newSlots, slots_, capacity_ * sizeof(Slot)); + } + for (std::uint32_t i = capacity_; i < newCapacity; i++) { + newSlots[i].next_free = free_head_; + free_head_ = i; + } + Slot *old = slots_; + slots_ = newSlots; + capacity_ = newCapacity; + return old; +} + +void InputPool::grow(std::uint32_t newCapacity) { + auto *newSlots = new Slot[newCapacity]; + Slot *old = adoptBuffer(newSlots, newCapacity); + delete[] old; +} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.h similarity index 63% rename from packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.hpp rename to packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.h index 000b258e4..aa9724a1c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/InputPool.h @@ -3,7 +3,6 @@ #include #include #include -#include #include namespace audioapi::utils::graph { @@ -78,7 +77,7 @@ class InputPool { // ── Lifecycle ─────────────────────────────────────────────────────────── - InputPool() = default; + InputPool(); ~InputPool(); InputPool(const InputPool &) = delete; @@ -148,12 +147,6 @@ class InputPool { std::uint32_t free_head_ = kNull; }; -// ========================================================================= -// Implementation -// ========================================================================= - -// ── Iterator ────────────────────────────────────────────────────────────── - template auto InputPool::Iterator::operator*() const -> reference { return slots[current].val; @@ -180,82 +173,14 @@ bool InputPool::Iterator::operator!=(const Iterator &other) const { return current != other.current; } -// ── InputView ───────────────────────────────────────────────────────────── - template auto InputPool::InputView::begin() const -> Iterator { - return {slots, head}; + return {.slots = slots, .current = head}; } template auto InputPool::InputView::end() const -> Iterator { - return {slots, kNull}; -} - -// ── Lifecycle ───────────────────────────────────────────────────────────── - -inline InputPool::~InputPool() { - delete[] slots_; -} - -inline InputPool::InputPool(InputPool &&other) noexcept - : slots_(other.slots_), capacity_(other.capacity_), free_head_(other.free_head_) { - other.slots_ = nullptr; - other.capacity_ = 0; - other.free_head_ = kNull; -} - -inline InputPool &InputPool::operator=(InputPool &&other) noexcept { - if (this != &other) { - delete[] slots_; - slots_ = other.slots_; - capacity_ = other.capacity_; - free_head_ = other.free_head_; - other.slots_ = nullptr; - other.capacity_ = 0; - other.free_head_ = kNull; - } - return *this; -} - -// ── Slot allocation ─────────────────────────────────────────────────────── - -inline std::uint32_t InputPool::alloc() { - if (free_head_ == kNull) { - grow(capacity_ == 0 ? 64 : capacity_ * 2); - } - std::uint32_t idx = free_head_; - free_head_ = slots_[idx].next_free; - return idx; -} - -inline void InputPool::free(std::uint32_t idx) { - slots_[idx].next_free = free_head_; - free_head_ = idx; -} - -// ── Linked-list operations ──────────────────────────────────────────────── - -inline void InputPool::push(std::uint32_t &head, std::uint32_t inputVal) { - std::uint32_t idx = alloc(); - slots_[idx].val = inputVal; - slots_[idx].next = head; - head = idx; -} - -inline bool InputPool::remove(std::uint32_t &head, std::uint32_t inputVal) { - std::uint32_t *prev = &head; - std::uint32_t curr = head; - while (curr != kNull) { - if (slots_[curr].val == inputVal) { - *prev = slots_[curr].next; - free(curr); - return true; - } - prev = &slots_[curr].next; - curr = slots_[curr].next; - } - return false; + return {.slots = slots, .current = kNull}; } template @@ -275,52 +200,4 @@ void InputPool::removeIf(std::uint32_t &head, Pred pred) { } } -inline void InputPool::freeAll(std::uint32_t &head) { - while (head != kNull) { - std::uint32_t nxt = slots_[head].next; - free(head); - head = nxt; - } -} - -inline bool InputPool::isEmpty(std::uint32_t head) { - return head == kNull; -} - -// ── Iteration ───────────────────────────────────────────────────────────── - -inline InputPool::InputView InputPool::view(std::uint32_t head) const { - return {slots_, head}; -} - -inline InputPool::InputView InputPool::mutableView(std::uint32_t head) { - return {slots_, head}; -} - -// ── Pool management ─────────────────────────────────────────────────────── - -inline std::uint32_t InputPool::capacity() const { - return capacity_; -} - -inline InputPool::Slot *InputPool::adoptBuffer(Slot *newSlots, std::uint32_t newCapacity) { - if (slots_) { - std::memcpy(newSlots, slots_, capacity_ * sizeof(Slot)); - } - for (std::uint32_t i = capacity_; i < newCapacity; i++) { - newSlots[i].next_free = free_head_; - free_head_ = i; - } - Slot *old = slots_; - slots_ = newSlots; - capacity_ = newCapacity; - return old; -} - -inline void InputPool::grow(std::uint32_t newCapacity) { - auto *newSlots = new Slot[newCapacity]; - Slot *old = adoptBuffer(newSlots, newCapacity); - delete[] old; -} - } // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.cpp new file mode 100644 index 000000000..075afe5e5 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.cpp @@ -0,0 +1,10 @@ +#include +#include +#include + +namespace audioapi::utils::graph { + +NodeHandle::NodeHandle(std::uint32_t index, std::unique_ptr audioNode) + : audioNode(std::move(audioNode)), index(index) {} + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.h similarity index 74% rename from packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.hpp rename to packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.h index 6765677c5..3421b17fd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/NodeHandle.h @@ -1,8 +1,9 @@ #pragma once +#include + #include #include -#include namespace audioapi::utils::graph { @@ -20,13 +21,11 @@ namespace audioapi::utils::graph { /// - When AudioGraph compacts out an orphaned node it releases its shared_ptr /// (refcount 2 → 1). HostGraph detects use_count() == 1 and destroys the /// ghost + payload on the main thread. -template struct NodeHandle { - std::uint32_t index; // current position in AudioGraph::nodes - std::unique_ptr audioNode; // the payload node (may be null in tests) + std::unique_ptr audioNode; // payload graph object (may be null in tests) + std::uint32_t index; // current position in AudioGraph::nodes - NodeHandle(std::uint32_t index, std::unique_ptr audioNode) - : index(index), audioNode(std::move(audioNode)) {} + NodeHandle(std::uint32_t index, std::unique_ptr audioNode); }; } // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/audioapi/jsi/RuntimeLifecycleMonitor.cpp b/packages/react-native-audio-api/common/cpp/audioapi/jsi/RuntimeLifecycleMonitor.cpp index 53d259bfb..ee1b326b2 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/jsi/RuntimeLifecycleMonitor.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/jsi/RuntimeLifecycleMonitor.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -7,22 +8,30 @@ namespace audioapi { static std::unordered_map> listeners; +static std::mutex listenersMutex; struct RuntimeLifecycleMonitorObject : public jsi::HostObject { jsi::Runtime *rt_; explicit RuntimeLifecycleMonitorObject(jsi::Runtime *rt) : rt_(rt) {} ~RuntimeLifecycleMonitorObject() override { - auto listenersSet = listeners.find(rt_); - if (listenersSet != listeners.end()) { - for (auto listener : listenersSet->second) { - listener->onRuntimeDestroyed(rt_); + std::unordered_set toNotify; + { + std::scoped_lock lock(listenersMutex); + auto listenersSet = listeners.find(rt_); + if (listenersSet != listeners.end()) { + toNotify = std::move(listenersSet->second); + listeners.erase(listenersSet); } - listeners.erase(listenersSet); + } + // Notify outside the lock — listener callbacks may re-enter add/remove. + for (auto *listener : toNotify) { + listener->onRuntimeDestroyed(rt_); } } }; void RuntimeLifecycleMonitor::addListener(jsi::Runtime &rt, RuntimeLifecycleListener *listener) { + std::scoped_lock lock(listenersMutex); auto listenersSet = listeners.find(&rt); if (listenersSet == listeners.end()) { // We install a global host object in the provided runtime, this way we can @@ -43,6 +52,7 @@ void RuntimeLifecycleMonitor::addListener(jsi::Runtime &rt, RuntimeLifecycleList } void RuntimeLifecycleMonitor::removeListener(jsi::Runtime &rt, RuntimeLifecycleListener *listener) { + std::scoped_lock lock(listenersMutex); auto listenersSet = listeners.find(&rt); if (listenersSet == listeners.end()) { // nothing to do here diff --git a/packages/react-native-audio-api/common/cpp/audioapi/types/NodeOptions.h b/packages/react-native-audio-api/common/cpp/audioapi/types/NodeOptions.h index 8acbbede4..b19466e10 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/types/NodeOptions.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/types/NodeOptions.h @@ -88,6 +88,14 @@ struct BiquadFilterOptions : AudioNodeOptions { float detune = 0.0f; float Q = 1.0f; float gain = 0.0f; + + BiquadFilterOptions() { + requiresTailProcessing = true; + } + + explicit BiquadFilterOptions(AudioNodeOptions options) : AudioNodeOptions(options) { + requiresTailProcessing = true; + } }; struct OscillatorOptions : AudioScheduledSourceNodeOptions { diff --git a/packages/react-native-audio-api/common/cpp/test/RunTestsGraph.sh b/packages/react-native-audio-api/common/cpp/test/RunTestsGraph.sh index 8088a3d09..658fc72c9 100755 --- a/packages/react-native-audio-api/common/cpp/test/RunTestsGraph.sh +++ b/packages/react-native-audio-api/common/cpp/test/RunTestsGraph.sh @@ -14,7 +14,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" # Allow override of GRAPH_FILTER via environment variable -GRAPH_FILTER="${GRAPH_FILTER:-AudioGraphTest.*:AudioGraphFuzzTest.*:GraphTest.*:GraphFuzzTest.*:GraphCycleDebugTest.*:HostGraphTest.*:Seeds/*}" +GRAPH_FILTER="${GRAPH_FILTER:-AudioGraphTest.*:AudioGraphFuzzTest.*:GraphTest.*:GraphFuzzTest.*:GraphCycleDebugTest.*:HostGraphTest.*:GraphNodeGrowthTest.*:Seeds/*}" cmake -S . -B build -Wno-dev diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp index 511c33beb..389a860a6 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/DelayTest.cpp @@ -1,5 +1,8 @@ #include +#include #include +#include +#include #include #include #include @@ -16,40 +19,81 @@ class DelayTest : public ::testing::Test { protected: std::shared_ptr eventRegistry; std::shared_ptr context; + std::shared_ptr destination; static constexpr int sampleRate = 44100; void SetUp() override { eventRegistry = std::make_shared(); context = std::make_shared( 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); - context->initialize(); + destination = std::make_shared(context); + context->initialize(destination.get()); + } +}; + +class TestableDelayWriter : public DelayWriter { + public: + explicit TestableDelayWriter( + std::shared_ptr context, + const DelayOptions &options, + std::shared_ptr delayLine) + : DelayWriter(context, options, delayLine) {} + + void setInputBuffer(const std::shared_ptr &input) { + audioBuffer_ = input; + } +}; + +class TestableDelayReader : public DelayReader { + public: + explicit TestableDelayReader( + std::shared_ptr context, + const DelayOptions &options, + std::shared_ptr delayLine) + : DelayReader(context, options, delayLine) {} + + auto getOutputBuffer() { + return audioBuffer_; } }; class TestableDelayNode : public DelayNode { public: explicit TestableDelayNode(std::shared_ptr context, const DelayOptions &options) - : DelayNode(context, options) {} + : DelayNode(context, options), + testableDelayReader_(context, options, delayLine_), + testableDelayWriter_(context, options, delayLine_) {} void setDelayTimeParam(float value) { getDelayTimeParam()->setValue(value); } - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return DelayNode::processNode(processingBuffer, framesToProcess); + void setInputBuffer(const std::shared_ptr &input) { + testableDelayWriter_.setInputBuffer(input); } + + auto getOutputBuffer() { + return testableDelayReader_.getOutputBuffer(); + } + + void processNode(int framesToProcess) override { + testableDelayWriter_.processNode(framesToProcess); + testableDelayReader_.processNode(framesToProcess); + } + + private: + TestableDelayWriter testableDelayWriter_; + TestableDelayReader testableDelayReader_; }; TEST_F(DelayTest, DelayCanBeCreated) { - auto delay = context->createDelay(DelayOptions()); + auto delay = std::make_shared(context, DelayOptions()); ASSERT_NE(delay, nullptr); } TEST_F(DelayTest, DelayWithZeroDelayOutputsInputSignal) { static constexpr float DELAY_TIME = 0.0f; - static constexpr int FRAMES_TO_PROCESS = 4; + static constexpr int FRAMES_TO_PROCESS = 128; auto options = DelayOptions(); options.maxDelayTime = 1.0f; auto delayNode = TestableDelayNode(context, options); @@ -60,15 +104,17 @@ TEST_F(DelayTest, DelayWithZeroDelayOutputsInputSignal) { (*buffer->getChannel(0))[i] = i + 1; } - auto resultBuffer = delayNode.processNode(buffer, FRAMES_TO_PROCESS); + delayNode.setInputBuffer(buffer); + delayNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = delayNode.getOutputBuffer(); for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { EXPECT_FLOAT_EQ((*resultBuffer->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; + float DELAY_TIME = (FRAMES_TO_PROCESS / context->getSampleRate()) * 0.5; auto options = DelayOptions(); options.maxDelayTime = 1.0f; auto delayNode = TestableDelayNode(context, options); @@ -79,7 +125,9 @@ TEST_F(DelayTest, DelayAppliesTimeShiftCorrectly) { (*buffer->getChannel(0))[i] = i + 1; } - auto resultBuffer = delayNode.processNode(buffer, FRAMES_TO_PROCESS); + delayNode.setInputBuffer(buffer); + delayNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = delayNode.getOutputBuffer(); 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((*resultBuffer->getChannel(0))[i], 0.0f); @@ -93,10 +141,10 @@ TEST_F(DelayTest, DelayAppliesTimeShiftCorrectly) { } TEST_F(DelayTest, DelayHandlesTailCorrectly) { - float DELAY_TIME = (128.0 / context->getSampleRate()) * 0.5; static constexpr int FRAMES_TO_PROCESS = 128; + float DELAY_TIME = (FRAMES_TO_PROCESS / context->getSampleRate()) * 0.5; auto options = DelayOptions(); - options.maxDelayTime = 1.0f; + options.maxDelayTime = (FRAMES_TO_PROCESS / context->getSampleRate()) * 3; auto delayNode = TestableDelayNode(context, options); delayNode.setDelayTimeParam(DELAY_TIME); @@ -105,15 +153,19 @@ TEST_F(DelayTest, DelayHandlesTailCorrectly) { (*buffer->getChannel(0))[i] = i + 1; } - delayNode.processNode(buffer, FRAMES_TO_PROCESS); - auto resultBuffer = delayNode.processNode(buffer, FRAMES_TO_PROCESS); + delayNode.setInputBuffer(buffer); + delayNode.processNode(FRAMES_TO_PROCESS); + // Second call uses the result of the first call as input + delayNode.getOutputBuffer()->zero(); + delayNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = delayNode.getOutputBuffer(); for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { - if (i < FRAMES_TO_PROCESS / 2) { // First 64 samples should be 2nd part of buffer + if (i < FRAMES_TO_PROCESS / 2) { // First samples should be 2nd part of buffer EXPECT_FLOAT_EQ( (*resultBuffer->getChannel(0))[i], static_cast(i + 1 + FRAMES_TO_PROCESS / 2.0)); } else { EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], - 0.0f); // Last 64 samples should be zero + 0.0f); // Last samples should be zero } } } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp index 46c2bd5ac..3ddac4a25 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/GainTest.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -16,13 +17,15 @@ class GainTest : public ::testing::Test { protected: std::shared_ptr eventRegistry; std::shared_ptr context; + std::shared_ptr destination; static constexpr int sampleRate = 44100; void SetUp() override { eventRegistry = std::make_shared(); context = std::make_shared( 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); - context->initialize(); + destination = std::make_shared(context); + context->initialize(destination.get()); } }; @@ -35,15 +38,17 @@ class TestableGainNode : public GainNode { getGainParam()->setValue(value); } - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return GainNode::processNode(processingBuffer, framesToProcess); + void setInputBuffer(const std::shared_ptr &input) { + audioBuffer_ = input; + } + + void processNode(int framesToProcess) override { + GainNode::processNode(framesToProcess); } }; TEST_F(GainTest, GainCanBeCreated) { - auto gain = context->createGain(GainOptions()); + auto gain = std::make_shared(context, GainOptions()); ASSERT_NE(gain, nullptr); } @@ -58,7 +63,9 @@ TEST_F(GainTest, GainModulatesVolumeCorrectly) { (*buffer->getChannel(0))[i] = i + 1; } - auto resultBuffer = gainNode.processNode(buffer, FRAMES_TO_PROCESS); + gainNode.setInputBuffer(buffer); + gainNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = gainNode.getOutputBuffer(); for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], (i + 1) * GAIN_VALUE); } @@ -76,7 +83,9 @@ TEST_F(GainTest, GainModulatesVolumeCorrectlyMultiChannel) { (*buffer->getChannel(1))[i] = -i - 1; } - auto resultBuffer = gainNode.processNode(buffer, FRAMES_TO_PROCESS); + gainNode.setInputBuffer(buffer); + gainNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = gainNode.getOutputBuffer(); for (size_t i = 0; i < FRAMES_TO_PROCESS; ++i) { EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], (i + 1) * GAIN_VALUE); EXPECT_FLOAT_EQ((*resultBuffer->getChannel(1))[i], (-i - 1) * GAIN_VALUE); diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp index 91fb54ce0..1b400f71a 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/IIRFilterTest.cpp @@ -93,7 +93,7 @@ class IIRFilterTest : public ::testing::Test { TEST_F(IIRFilterTest, IIRFilterCanBeCreated) { const std::vector feedforward = {1.0}; const std::vector feedback = {1.0}; - auto node = context->createIIRFilter(IIRFilterOptions(feedforward, feedback)); + auto node = std::make_shared(context, IIRFilterOptions(feedforward, feedback)); ASSERT_NE(node, nullptr); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp index b0f2649a2..dff1d86d5 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/StereoPannerTest.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -16,13 +17,15 @@ class StereoPannerTest : public ::testing::Test { protected: std::shared_ptr eventRegistry; std::shared_ptr context; + std::shared_ptr destination; static constexpr int sampleRate = 44100; void SetUp() override { eventRegistry = std::make_shared(); context = std::make_shared( 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); - context->initialize(); + destination = std::make_shared(context); + context->initialize(destination.get()); } }; @@ -35,15 +38,17 @@ class TestableStereoPannerNode : public StereoPannerNode { getPanParam()->setValue(value); } - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return StereoPannerNode::processNode(processingBuffer, framesToProcess); + void setInputBuffer(const std::shared_ptr &input) { + audioBuffer_ = input; + } + + void processNode(int framesToProcess) override { + StereoPannerNode::processNode(framesToProcess); } }; TEST_F(StereoPannerTest, StereoPannerCanBeCreated) { - auto panner = context->createStereoPanner(StereoPannerOptions()); + auto panner = std::make_shared(context, StereoPannerOptions()); ASSERT_NE(panner, nullptr); } @@ -58,7 +63,9 @@ TEST_F(StereoPannerTest, PanModulatesInputMonoCorrectly) { (*buffer->getChannelByType(AudioBuffer::ChannelLeft))[i] = i + 1; } - auto resultBuffer = panNode.processNode(buffer, FRAMES_TO_PROCESS); + panNode.setInputBuffer(buffer); + panNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = panNode.getOutputBuffer(); // x = (0.5 + 1) / 2 = 0.75 // gainL = cos(x * (π / 2)) = cos(0.75 * (π / 2)) = 0.38268343236508984 // gainR = sin(x * (π / 2)) = sin(0.75 * (π / 2)) = 0.9238795325112867 @@ -86,7 +93,9 @@ TEST_F(StereoPannerTest, PanModulatesInputStereoCorrectlyWithNegativePan) { (*buffer->getChannelByType(AudioBuffer::ChannelRight))[i] = i + 1; } - auto resultBuffer = panNode.processNode(buffer, FRAMES_TO_PROCESS); + panNode.setInputBuffer(buffer); + panNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = panNode.getOutputBuffer(); // x = -0.5 + 1 = 0.5 // gainL = cos(x * (π / 2)) = cos(0.5 * (π / 2)) = 0.7071067811865476 // gainR = sin(x * (π / 2)) = sin(0.5 * (π / 2)) = 0.7071067811865476 @@ -114,7 +123,9 @@ TEST_F(StereoPannerTest, PanModulatesInputStereoCorrectlyWithPositivePan) { (*buffer->getChannelByType(AudioBuffer::ChannelRight))[i] = i + 1; } - auto resultBuffer = panNode.processNode(buffer, FRAMES_TO_PROCESS); + panNode.setInputBuffer(buffer); + panNode.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = panNode.getOutputBuffer(); // x = 0.75 // gainL = cos(x * (π / 2)) = cos(0.75 * (π / 2)) = 0.38268343236508984 // gainR = sin(x * (π / 2)) = sin(0.75 * (π / 2)) = 0.9238795325112867 diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp new file mode 100644 index 000000000..66844fabd --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/TailProcessingTest.cpp @@ -0,0 +1,284 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace audioapi; + +// NOLINTBEGIN + +namespace { + +constexpr int kQuantum = 128; +constexpr int kSampleRate = 44100; + +// Builds a single-channel buffer that is either fully silent or filled with +// a constant non-zero value. +std::shared_ptr makeBuffer(bool nonZero, int channels = 2) { + auto buf = std::make_shared(kQuantum, channels, kSampleRate); + if (nonZero) { + for (int c = 0; c < channels; ++c) { + for (size_t i = 0; i < buf->getSize(); ++i) { + (*buf->getChannel(c))[i] = 0.5f; + } + } + } else { + buf->zero(); + } + return buf; +} + +// Test harness exposing `processInputs` and the protected tail-state fields +// so we can drive and inspect the state machine deterministically. +template +class TailHarness : public NodeT { + public: + using NodeT::NodeT; + + // Hand-built equivalent of `process(inputs, n)` that does not require a + // full graph: forwards a vector of raw buffer pointers straight into the + // base-class `processInputs`. The mixing path uses these as inputs. + void runQuantum(const std::vector &inputs) { + this->processInputs(inputs, kQuantum); + } + + AudioNode::TailState tailState() const { + return this->tailState_; + } + int tailFramesRemaining() const { + return this->tailFramesRemaining_; + } + int computeTail() const { + return this->computeTailFrames(); + } + using NodeT::canBeDestructed; + using NodeT::isProcessable; +}; + +class TailProcessingTest : public ::testing::Test { + protected: + std::shared_ptr eventRegistry; + std::shared_ptr context; + std::shared_ptr destination; + + void SetUp() override { + eventRegistry = std::make_shared(); + context = std::make_shared( + 2, 5 * kSampleRate, kSampleRate, eventRegistry, RuntimeRegistry{}); + destination = std::make_shared(context); + context->initialize(destination.get()); + } +}; + +// ─── BiquadFilter ───────────────────────────────────────────────────────── + +TEST_F(TailProcessingTest, BiquadFilterDeclaresTailProcessing) { + BiquadFilterOptions opts{}; + EXPECT_TRUE(opts.requiresTailProcessing); + + auto biquad = std::make_shared(context, opts); + EXPECT_TRUE(biquad->requiresTailProcessing()); +} + +TEST_F(TailProcessingTest, BiquadFilterStartsActiveAndStaysSoUnderInput) { + TailHarness biquad(context, BiquadFilterOptions()); + + auto nonSilent = makeBuffer(/*nonZero=*/true); + std::vector inputs{nonSilent.get()}; + + EXPECT_EQ(biquad.tailState(), AudioNode::TailState::ACTIVE); + + for (int q = 0; q < 4; ++q) { + biquad.runQuantum(inputs); + } + + EXPECT_EQ(biquad.tailState(), AudioNode::TailState::ACTIVE); + EXPECT_TRUE(biquad.isProcessable()); + EXPECT_FALSE(biquad.canBeDestructed()) << "An ACTIVE tail-bearing node must not be destructible"; +} + +TEST_F(TailProcessingTest, BiquadFilterDrainsTailAfterInputGoesSilent) { + TailHarness biquad(context, BiquadFilterOptions()); + + auto nonSilent = makeBuffer(/*nonZero=*/true); + auto silent = makeBuffer(/*nonZero=*/false); + + // One non-silent quantum primes the filter coefficients (lastA2_). + biquad.runQuantum({nonSilent.get()}); + ASSERT_EQ(biquad.tailState(), AudioNode::TailState::ACTIVE); + + // First silent quantum signals stop and seeds the countdown. + biquad.runQuantum({silent.get()}); + EXPECT_EQ(biquad.tailState(), AudioNode::TailState::SIGNALLED_TO_STOP); + const int initialTail = biquad.tailFramesRemaining(); + EXPECT_GT(initialTail, 0); + + // The countdown decreases by `kQuantum` per silent quantum. + biquad.runQuantum({silent.get()}); + if (biquad.tailState() == AudioNode::TailState::SIGNALLED_TO_STOP) { + EXPECT_LE(biquad.tailFramesRemaining(), initialTail - kQuantum); + } + + // Drain the rest until FINISHED. + for (int q = 0; q < 1000 && biquad.tailState() != AudioNode::TailState::FINISHED; ++q) { + biquad.runQuantum({silent.get()}); + } + EXPECT_EQ(biquad.tailState(), AudioNode::TailState::FINISHED); + EXPECT_TRUE(biquad.canBeDestructed()); + // Once finished and downstream is gone, isProcessable() falls back to the + // base GraphObject rule (NOT_PROCESSABLE by default). + EXPECT_FALSE(biquad.isProcessable()); +} + +TEST_F(TailProcessingTest, BiquadFilterReArmsOnNewInput) { + TailHarness biquad(context, BiquadFilterOptions()); + + auto nonSilent = makeBuffer(/*nonZero=*/true); + auto silent = makeBuffer(/*nonZero=*/false); + + biquad.runQuantum({nonSilent.get()}); + biquad.runQuantum({silent.get()}); + ASSERT_EQ(biquad.tailState(), AudioNode::TailState::SIGNALLED_TO_STOP); + + // A non-silent quantum should restore ACTIVE and reset the counter. + biquad.runQuantum({nonSilent.get()}); + EXPECT_EQ(biquad.tailState(), AudioNode::TailState::ACTIVE); + EXPECT_EQ(biquad.tailFramesRemaining(), 0); +} + +TEST_F(TailProcessingTest, BiquadFilterTailLengthFollowsCoefficients) { + TailHarness biquad(context, BiquadFilterOptions()); + + // Without ever running processNode, lastA2_ is 0 → tail is 0 frames. + EXPECT_EQ(biquad.computeTail(), 0); + + // After processing one quantum of audio the coefficients are populated and + // the tail length should be a positive, finite number of frames bounded + // by the 0.5-second cap. + auto nonSilent = makeBuffer(/*nonZero=*/true); + biquad.runQuantum({nonSilent.get()}); + + const int tail = biquad.computeTail(); + EXPECT_GE(tail, 0); + EXPECT_LE(tail, static_cast(0.5f * kSampleRate)); +} + +// ─── DelayWriter ────────────────────────────────────────────────────────── + +TEST_F(TailProcessingTest, DelayWriterTailEqualsMaxDelayInFrames) { + DelayOptions opts{}; + opts.maxDelayTime = 0.25f; + auto delayNode = std::make_shared(context, opts); + + // The DelayNode's writer is tail-bearing; query computeTailFrames via the + // public AudioNode contract by reaching through `delayWriter_`. + auto *writer = static_cast(delayNode->delayWriter_.get()); + + // We can't call computeTailFrames directly (protected), but we can verify + // requiresTailProcessing is on and exercise the state machine via the + // base-class hooks instead. The numerical equivalence of the tail length + // is covered by the DrainsTail test below. + EXPECT_TRUE(writer->requiresTailProcessing()); +} + +TEST_F(TailProcessingTest, DelayWriterDrainsTailAfterInputSilences) { + DelayOptions opts{}; + opts.maxDelayTime = (10.0f * kQuantum) / kSampleRate; // 10 quanta of tail + auto delayNode = std::make_shared(context, opts); + + // Re-create the writer as a TailHarness so we can poke its private state. + // The writer just needs the same DelayLine; share it. + TailHarness writer(context, opts, delayNode->delayLine_); + + auto nonSilent = makeBuffer(/*nonZero=*/true); + auto silent = makeBuffer(/*nonZero=*/false); + + writer.runQuantum({nonSilent.get()}); + ASSERT_EQ(writer.tailState(), AudioNode::TailState::ACTIVE); + + // First silent quantum starts the countdown of ~10 quanta. + writer.runQuantum({silent.get()}); + ASSERT_EQ(writer.tailState(), AudioNode::TailState::SIGNALLED_TO_STOP); + EXPECT_GT(writer.tailFramesRemaining(), 0); + EXPECT_LE(writer.tailFramesRemaining(), 10 * kQuantum); + + for (int q = 0; q < 64 && writer.tailState() != AudioNode::TailState::FINISHED; ++q) { + writer.runQuantum({silent.get()}); + } + EXPECT_EQ(writer.tailState(), AudioNode::TailState::FINISHED); + EXPECT_TRUE(writer.canBeDestructed()); +} + +// ─── ConvolverNode ──────────────────────────────────────────────────────── + +TEST_F(TailProcessingTest, ConvolverDeclaresTailProcessing) { + ConvolverOptions opts{}; + EXPECT_TRUE(opts.requiresTailProcessing); + + auto conv = std::make_shared(context, opts); + EXPECT_TRUE(conv->requiresTailProcessing()); +} + +TEST_F(TailProcessingTest, ConvolverWithoutBufferHasZeroTail) { + TailHarness conv(context, ConvolverOptions()); + + // No IR loaded yet → no tail, regardless of input history. + EXPECT_EQ(conv.computeTail(), 0); + + auto nonSilent = makeBuffer(/*nonZero=*/true); + auto silent = makeBuffer(/*nonZero=*/false); + conv.runQuantum({nonSilent.get()}); + conv.runQuantum({silent.get()}); + + // SIGNALLED_TO_STOP with a 0-frame countdown collapses to FINISHED in the + // same quantum. + EXPECT_EQ(conv.tailState(), AudioNode::TailState::FINISHED); + EXPECT_TRUE(conv.canBeDestructed()); +} + +TEST_F(TailProcessingTest, ConvolverTailLengthEqualsIRLength) { + TailHarness conv(context, ConvolverOptions()); + + // Build a minimal IR buffer. We deliberately avoid constructing real + // convolvers/threadpool/internal buffers: setBuffer only reads the IR's + // length and resets tail bookkeeping — the convolution path (processNode) + // is not exercised here. + constexpr size_t kIrFrames = 2048; + auto ir = std::make_shared(kIrFrames, 1, kSampleRate); + + conv.setBuffer( + ir, + /*convolvers=*/{}, + /*threadPool=*/nullptr, + /*internalBuffer=*/nullptr, + /*intermediateBuffer=*/nullptr, + /*scaleFactor=*/1.0f); + + EXPECT_EQ(conv.computeTail(), static_cast(kIrFrames)); + + // setBuffer must also re-arm the tail state machine. + EXPECT_EQ(conv.tailState(), AudioNode::TailState::ACTIVE); + EXPECT_EQ(conv.tailFramesRemaining(), 0); + EXPECT_FALSE(conv.canBeDestructed()) << "A newly-armed convolver with a tail must stay alive"; + + // Swapping to a shorter IR updates the tail length on next silence. + constexpr size_t kShorterFrames = 512; + auto shorterIr = std::make_shared(kShorterFrames, 1, kSampleRate); + conv.setBuffer(shorterIr, {}, nullptr, nullptr, nullptr, 1.0f); + EXPECT_EQ(conv.computeTail(), static_cast(kShorterFrames)); +} + +// NOLINTEND +} // namespace diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp index 2dc398fcb..a23575814 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/effects/WaveShaperNodeTest.cpp @@ -37,22 +37,24 @@ class TestableWaveShaperNode : public WaveShaperNode { data[2] = 2.0f; } - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return WaveShaperNode::processNode(processingBuffer, framesToProcess); + void setInputBuffer(const std::shared_ptr &input) { + audioBuffer_ = input; + } + + void processNode(int framesToProcess) override { + WaveShaperNode::processNode(framesToProcess); } std::shared_ptr testCurve_; }; TEST_F(WaveShaperNodeTest, WaveShaperNodeCanBeCreated) { - auto waveShaper = context->createWaveShaper(WaveShaperOptions()); + auto waveShaper = std::make_shared(context, WaveShaperOptions()); ASSERT_NE(waveShaper, nullptr); } TEST_F(WaveShaperNodeTest, NullCanBeAsignedToCurve) { - auto waveShaper = context->createWaveShaper(WaveShaperOptions()); + auto waveShaper = std::make_shared(context, WaveShaperOptions()); ASSERT_NO_THROW(waveShaper->setCurve(nullptr)); } @@ -67,7 +69,9 @@ TEST_F(WaveShaperNodeTest, NoneOverSamplingProcessesCorrectly) { (*buffer->getChannel(0))[i] = -1.0f + i * 0.5f; } - auto resultBuffer = waveShaper->processNode(buffer, FRAMES_TO_PROCESS); + waveShaper->setInputBuffer(buffer); + waveShaper->processNode(FRAMES_TO_PROCESS); + auto resultBuffer = waveShaper->getOutputBuffer(); auto curveData = waveShaper->testCurve_->span(); auto resultData = resultBuffer->getChannel(0)->span(); diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp index 36039816c..e47203fa7 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/AudioScheduledSourceTest.cpp @@ -19,21 +19,22 @@ class AudioScheduledSourceTest : public ::testing::Test { protected: std::shared_ptr eventRegistry; std::shared_ptr context; + std::shared_ptr destination; + static constexpr int sampleRate = 44100; void SetUp() override { eventRegistry = std::make_shared(); context = std::make_shared( - 2, 5 * SAMPLE_RATE, SAMPLE_RATE, eventRegistry, RuntimeRegistry{}); - context->initialize(); + 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); + destination = std::make_shared(context); + context->initialize(destination.get()); } }; class TestableAudioScheduledSourceNode : public AudioScheduledSourceNode { public: explicit TestableAudioScheduledSourceNode(std::shared_ptr context) - : AudioScheduledSourceNode(context) { - isInitialized_.store(true, std::memory_order_release); - } + : AudioScheduledSourceNode(context) {} void updatePlaybackInfo( const std::shared_ptr &processingBuffer, @@ -51,10 +52,7 @@ class TestableAudioScheduledSourceNode : public AudioScheduledSourceNode { currentSampleFrame); } - std::shared_ptr processNode(const std::shared_ptr &, int) - override { - return nullptr; - } + void processNode(int) override {} PlaybackState getPlaybackState() const { return playbackState_; @@ -73,7 +71,7 @@ class TestableAudioScheduledSourceNode : public AudioScheduledSourceNode { nonSilentFramesToProcess, context->getSampleRate(), context->getCurrentSampleFrame()); - context->getDestination()->renderAudio(processingBuffer, frames); + context->processGraph(processingBuffer.get(), frames); } } }; diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp index f0486260d..c1ce53543 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/ConstantSourceTest.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -16,13 +17,15 @@ class ConstantSourceTest : public ::testing::Test { protected: std::shared_ptr eventRegistry; std::shared_ptr context; + std::shared_ptr destination; static constexpr int sampleRate = 44100; void SetUp() override { eventRegistry = std::make_shared(); context = std::make_shared( 2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{}); - context->initialize(); + destination = std::make_shared(context); + context->initialize(destination.get()); } }; @@ -35,15 +38,13 @@ class TestableConstantSourceNode : public ConstantSourceNode { getOffsetParam()->setValue(value); } - std::shared_ptr processNode( - const std::shared_ptr &processingBuffer, - int framesToProcess) override { - return ConstantSourceNode::processNode(processingBuffer, framesToProcess); + void processNode(int framesToProcess) override { + ConstantSourceNode::processNode(framesToProcess); } }; TEST_F(ConstantSourceTest, ConstantSourceCanBeCreated) { - auto constantSource = context->createConstantSource(ConstantSourceOptions()); + auto constantSource = std::make_shared(context, ConstantSourceOptions()); ASSERT_NE(constantSource, nullptr); } @@ -52,18 +53,20 @@ TEST_F(ConstantSourceTest, ConstantSourceOutputsConstantValue) { auto buffer = std::make_shared(FRAMES_TO_PROCESS, 1, sampleRate); auto constantSource = TestableConstantSourceNode(context); - // constantSource.start(context->getCurrentTime()); - // auto resultBuffer = constantSource.processNode(buffer, FRAMES_TO_PROCESS); + constantSource.start(context->getCurrentTime()); + constantSource.processNode(FRAMES_TO_PROCESS); + auto resultBuffer = constantSource.getOutputBuffer(); - // for (int i = 0; i < FRAMES_TO_PROCESS; ++i) { - // EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], 1.0f); - // } + for (int i = 0; i < FRAMES_TO_PROCESS; ++i) { + EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], 1.0f); + } - // constantSource.setOffsetParam(0.5f); - // resultBuffer = constantSource.processNode(buffer, FRAMES_TO_PROCESS); - // for (int i = 0; i < FRAMES_TO_PROCESS; ++i) { - // EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], 0.5f); - // } + constantSource.setOffsetParam(0.5f); + constantSource.processNode(FRAMES_TO_PROCESS); + resultBuffer = constantSource.getOutputBuffer(); + for (int i = 0; i < FRAMES_TO_PROCESS; ++i) { + EXPECT_FLOAT_EQ((*resultBuffer->getChannel(0))[i], 0.5f); + } } // NOLINTEND diff --git a/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp index 0bcd1fff4..5da78c421 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/core/sources/OscillatorTest.cpp @@ -24,7 +24,7 @@ class OscillatorTest : public ::testing::Test { }; TEST_F(OscillatorTest, OscillatorCanBeCreated) { - auto osc = context->createOscillator(OscillatorOptions()); + auto osc = std::make_shared(context, OscillatorOptions()); ASSERT_NE(osc, nullptr); } diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphFuzzTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphFuzzTest.cpp index 941ca6296..31ee82fb8 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphFuzzTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphFuzzTest.cpp @@ -1,14 +1,14 @@ -#include -#include +#include +#include #include #include "TestGraphUtils.h" -#include #include #include #include #include #include +#include #include namespace audioapi::utils::graph { @@ -24,11 +24,12 @@ class AudioGraphFuzzTest : public ::testing::TestWithParam { protected: using MNode = MockNode; - AudioGraph graph; + DisposerImpl disposer_{64}; + AudioGraph graph; std::mt19937_64 rng; // Track live handles so we can reference them - std::vector>> handles; + std::vector> handles; size_t nextId = 0; void SetUp() override { @@ -37,8 +38,9 @@ class AudioGraphFuzzTest : public ::testing::TestWithParam { // ── Helpers ───────────────────────────────────────────────────────────── - std::shared_ptr> doAddNode() { - auto h = std::make_shared>(0, std::make_unique()); + std::shared_ptr doAddNode() { + std::unique_ptr obj = std::make_unique(); + auto h = std::make_shared(0, std::move(obj)); graph.addNode(h); graph[h->index].test_node_identifier__ = nextId++; handles.push_back(h); @@ -52,7 +54,7 @@ class AudioGraphFuzzTest : public ::testing::TestWithParam { // pickLive() will skip it because it checks orphaned status. } - void doAddEdge(std::shared_ptr> &from, std::shared_ptr> &to) { + void doAddEdge(std::shared_ptr &from, std::shared_ptr &to) { auto fromIdx = from->index; auto toIdx = to->index; // Verify at point-of-add that this edge doesn't create a duplicate @@ -67,9 +69,7 @@ class AudioGraphFuzzTest : public ::testing::TestWithParam { graph.markDirty(); } - void doRemoveEdge( - std::shared_ptr> &from, - std::shared_ptr> &to) { + void doRemoveEdge(std::shared_ptr &from, std::shared_ptr &to) { // Same as what HostGraph's removeEdge event does graph.pool().remove(graph[to->index].input_head, from->index); graph.markDirty(); diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphTest.cpp index e99780ed6..bf9ca3030 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioGraphTest.cpp @@ -1,7 +1,6 @@ -#include -#include +#include +#include #include -#include #include #include #include @@ -14,26 +13,26 @@ namespace audioapi::utils::graph { // --------------------------------------------------------------------------- class AudioGraphTest : public ::testing::Test { protected: - AudioGraph graph; + DisposerImpl disposer_{64}; + AudioGraph graph; // Helpers ---------------------------------------------------------------- - /// @brief Creates a shared NodeHandle with no node (for structural tests) - std::shared_ptr> makeHandle(size_t testId = 0) { - auto h = std::make_shared>(0, nullptr); + /// @brief Creates a shared NodeHandle with no node (for structural tests) + std::shared_ptr makeHandle(size_t testId = 0) { + auto h = std::make_shared(0, nullptr); return h; } - /// @brief Creates a shared NodeHandle with a MockNode - std::shared_ptr> makeHandleWithNode(bool destructible = true) { - return std::make_shared>(0, std::make_unique(destructible)); + /// @brief Creates a shared NodeHandle with a MockNode + std::shared_ptr makeHandleWithNode(bool destructible = true) { + std::unique_ptr obj = std::make_unique(destructible); + return std::make_shared(0, std::move(obj)); } /// @brief Adds N nodes with test identifiers 0..N-1 and returns their handles - std::vector>> addNodes( - size_t n, - bool withAudioNode = false) { - std::vector>> handles; + std::vector> addNodes(size_t n, bool withAudioNode = false) { + std::vector> handles; handles.reserve(n); for (size_t i = 0; i < n; i++) { auto h = withAudioNode ? makeHandleWithNode() : makeHandle(i); @@ -293,7 +292,9 @@ TEST_F(AudioGraphTest, Compact_RemovesOnceDestructible) { EXPECT_EQ(graph.size(), 2u); // Now make it destructible - h1->audioNode->setDestructible(true); + auto *mockNode = dynamic_cast(h1->audioNode.get()); + ASSERT_NE(mockNode, nullptr); + mockNode->setDestructible(true); graph.process(); // second pass: node 1 should be removed EXPECT_EQ(graph.size(), 1u); diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.cpp index eb2d96d47..e0917f449 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.cpp @@ -100,6 +100,21 @@ bool AudioThreadGuard::Scope::clean() const { #define __has_feature(x) 0 #endif +#if !defined(__SANITIZE_ADDRESS__) && !defined(__SANITIZE_THREAD__) && \ + !__has_feature(address_sanitizer) && !__has_feature(thread_sanitizer) + +bool audioapi::test::AudioThreadGuard::isEnabled() { + return true; +} + +#else + +bool audioapi::test::AudioThreadGuard::isEnabled() { + return false; +} + +#endif + #if !defined(__SANITIZE_ADDRESS__) && !defined(__SANITIZE_THREAD__) && \ !__has_feature(address_sanitizer) && !__has_feature(thread_sanitizer) diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.h b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.h index 332c5c77e..0bce89fc9 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.h +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/AudioThreadGuard.h @@ -54,6 +54,14 @@ class AudioThreadGuard { /// Takes a snapshot of context-switch counters. static ContextSwitchSnapshot contextSwitches(); + // ── Sanitizer-awareness ───────────────────────────────────────────────── + + /// Returns true if the operator new/delete overrides are active. + /// When building with ASan or TSan the overrides are disabled (the + /// sanitizer provides its own interceptors), so allocation tracking + /// is unavailable and any test relying on it should be skipped. + static bool isEnabled(); + // ── Internal — called by operator new/delete overrides ────────────────── static void recordAllocation(); diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/BridgeNodeTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/BridgeNodeTest.cpp new file mode 100644 index 000000000..4f9c9c424 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/BridgeNodeTest.cpp @@ -0,0 +1,672 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "MockGraphProcessor.h" +#include "TestGraphUtils.h" + +namespace audioapi::utils::graph { + +// ========================================================================= +// A. isProcessable contract +// ========================================================================= + +TEST(BridgeNodeContract, MockNodeIsNotProcessable) { + MockNode node; + EXPECT_FALSE(node.isProcessable()); +} + +TEST(BridgeNodeContract, BridgeNodeIsNotProcessable) { + BridgeNode bridge(nullptr); + EXPECT_FALSE(bridge.isProcessable()); +} + +TEST(BridgeNodeContract, BridgeNodeIsAlwaysDestructible) { + BridgeNode bridge(nullptr); + EXPECT_TRUE(bridge.canBeDestructed()); +} + +TEST(BridgeNodeContract, NonProcessableMockNodeIsNotProcessable) { + NonProcessableMockNode node; + EXPECT_FALSE(node.isProcessable()); + EXPECT_TRUE(node.canBeDestructed()); +} + +TEST(BridgeNodeContract, BridgeNodeStoresParam) { + // Use a real AudioParam to verify storage + auto param = createMockAudioParam(); + BridgeNode bridge(param.get()); + EXPECT_EQ(bridge.param(), param.get()); +} + +// ========================================================================= +// B. Graph structural tests (HostGraph + AudioGraph) +// ========================================================================= + +class BridgeGraphTest : public ::testing::Test { + protected: + using HNode = HostGraph::Node; + using AGEvent = HostGraph::AGEvent; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + + AudioGraph audioGraph; + HostGraph hostGraph; + DisposerImpl disposer_{64}; + + HNode *addMockNode() { + auto obj = std::make_unique(); + auto handle = std::make_shared(0, std::move(obj)); + auto [hostNode, event] = hostGraph.addNode(handle); + event(audioGraph, disposer_); + return hostNode; + } + + HNode *addBridgeNode(AudioParam *param = nullptr) { + auto obj = std::make_unique(param); + auto handle = std::make_shared(0, std::move(obj)); + auto [hostNode, event] = hostGraph.addNode(handle); + event(audioGraph, disposer_); + return hostNode; + } + + bool addEdge(HNode *from, HNode *to) { + auto result = hostGraph.addEdge(from, to); + if (result.is_ok()) { + auto event = std::move(result).unwrap(); + event(audioGraph, disposer_); + return true; + } + return false; + } + + bool removeEdge(HNode *from, HNode *to) { + auto result = hostGraph.removeEdge(from, to); + if (result.is_ok()) { + auto event = std::move(result).unwrap(); + event(audioGraph, disposer_); + return true; + } + return false; + } + + bool removeNode(HNode *node) { + auto result = hostGraph.removeNode(node); + if (result.is_ok()) { + auto event = std::move(result).unwrap(); + event(audioGraph, disposer_); + return true; + } + return false; + } +}; + +TEST_F(BridgeGraphTest, BridgeCreatesThreeNodePath) { + auto *source = addMockNode(); + auto *owner = addMockNode(); + auto *bridge = addBridgeNode(); + + ASSERT_TRUE(addEdge(source, bridge)); + ASSERT_TRUE(addEdge(bridge, owner)); + + // 3 nodes in graph + EXPECT_EQ(audioGraph.size(), 3u); + + // Topo sort should place them: source, bridge, owner + audioGraph.process(); + + // Verify source comes before bridge comes before owner + auto srcIdx = source->handle->index; + auto bridgeIdx = bridge->handle->index; + auto ownerIdx = owner->handle->index; + EXPECT_LT(srcIdx, bridgeIdx); + EXPECT_LT(bridgeIdx, ownerIdx); +} + +TEST_F(BridgeGraphTest, CycleDetectionThroughBridges) { + // Create: A → bridge → B → A would be a cycle + auto *nodeA = addMockNode(); + auto *nodeB = addMockNode(); + auto *bridge = addBridgeNode(); + + ASSERT_TRUE(addEdge(nodeA, bridge)); + ASSERT_TRUE(addEdge(bridge, nodeB)); + + // Now B → A should be rejected as a cycle + auto result = hostGraph.addEdge(nodeB, nodeA); + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::CYCLE_DETECTED); +} + +TEST_F(BridgeGraphTest, DuplicateEdgeRejectionWithBridges) { + auto *source = addMockNode(); + auto *bridge = addBridgeNode(); + + ASSERT_TRUE(addEdge(source, bridge)); + // Same edge again should be rejected + auto result = hostGraph.addEdge(source, bridge); + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::EDGE_ALREADY_EXISTS); +} + +// ========================================================================= +// C. AudioGraph::iter() filtering +// ========================================================================= + +class BridgeIterTest : public ::testing::Test { + protected: + using HNode = HostGraph::Node; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + + AudioGraph audioGraph; + HostGraph hostGraph; + DisposerImpl disposer_{64}; + + HNode *addNode(std::unique_ptr obj) { + auto handle = std::make_shared(0, std::move(obj)); + auto [hostNode, event] = hostGraph.addNode(handle); + event(audioGraph, disposer_); + return hostNode; + } + + bool addEdge(HNode *from, HNode *to) { + auto result = hostGraph.addEdge(from, to); + if (result.is_ok()) { + std::move(result).unwrap()(audioGraph, disposer_); + return true; + } + return false; + } +}; + +TEST_F(BridgeIterTest, IterSkipsNonProcessableNodes) { + auto node = std::make_unique(); + node->setProcessable(); + auto *processable1 = addNode(std::move(node)); + auto *nonProcessable = addNode(std::make_unique()); + auto node2 = std::make_unique(); + node2->setProcessable(); + auto *processable2 = addNode(std::move(node2)); + + ASSERT_TRUE(addEdge(processable1, nonProcessable)); + ASSERT_TRUE(addEdge(nonProcessable, processable2)); + audioGraph.process(); + + // iter() should only yield 2 nodes (skip the non-processable one) + size_t count = 0; + for (auto &&[graphObject, inputs] : audioGraph.iter()) { + EXPECT_TRUE(graphObject.isProcessable()); + count++; + } + EXPECT_EQ(count, 2u); +} + +TEST_F(BridgeIterTest, AllProcessableNodesInTopoOrder) { + // A → bridge → B → C + auto *a = addNode(std::make_unique(nullptr, 1)); + auto *bridge = addNode(std::make_unique(nullptr)); + auto *b = addNode(std::make_unique(nullptr, 2)); + auto *c = addNode(std::make_unique(nullptr, 3)); + + ASSERT_TRUE(addEdge(a, bridge)); + ASSERT_TRUE(addEdge(bridge, b)); + ASSERT_TRUE(addEdge(b, c)); + audioGraph.process(); + + // Should yield A, bridge, B, C in topo order (bridge is now processable) + size_t count = 0; + for (auto &&[graphObject, inputs] : audioGraph.iter()) { + count++; + } + EXPECT_EQ(count, 4u); +} + +TEST_F(BridgeIterTest, InputsViewMayReferenceBridgeIndices) { + // source → bridge → owner + // iter() yields all processable nodes including bridge + auto *source = addNode(std::make_unique()); + auto *bridge = addNode(std::make_unique(nullptr)); + auto node = std::make_unique(); + node->setProcessable(); + auto *owner = addNode(std::move(node)); + + ASSERT_TRUE(addEdge(source, bridge)); + ASSERT_TRUE(addEdge(bridge, owner)); + audioGraph.process(); + + size_t processableCount = 0; + for (auto &&[graphObject, inputs] : audioGraph.iter()) { + processableCount++; + // Input could be bridge or source — both are valid GraphObjects + for (const auto &input : inputs) { + (void)input; + } + } + EXPECT_EQ(processableCount, 3u); // source + bridge + owner +} + +// ========================================================================= +// D. Compaction tests +// ========================================================================= + +TEST_F(BridgeGraphTest, OrphanedBridgeWithNoInputsRemoved) { + auto *bridge = addBridgeNode(); + EXPECT_EQ(audioGraph.size(), 1u); + + // Mark orphaned + removeNode(bridge); + audioGraph.process(); + + EXPECT_EQ(audioGraph.size(), 0u); +} + +TEST_F(BridgeGraphTest, SourceRemovalCascadesBridgeRemoval) { + auto *source = addMockNode(); + auto *bridge = addBridgeNode(); + auto *owner = addMockNode(); + + ASSERT_TRUE(addEdge(source, bridge)); + ASSERT_TRUE(addEdge(bridge, owner)); + audioGraph.process(); + EXPECT_EQ(audioGraph.size(), 3u); + + // Remove source — bridge loses its only input + removeNode(source); + audioGraph.process(); + + // Source compacted (orphaned, no inputs, destructible) + // Bridge compacted (orphaned via edge removal cascade — its input was removed) + // Owner stays (not orphaned) + // Actually: source is orphaned+no inputs → removed + // Then bridge has no inputs → but bridge is NOT orphaned unless explicitly marked + // Bridge keeps its edge to owner. Bridge itself is not orphaned. + // So only source is removed. + // After first process: source removed, bridge has no inputs but not orphaned. + // Bridge won't be compacted unless it's also orphaned. + // This is correct — bridge removal needs to be done via disconnectParam or removeNodeWithBridges. + EXPECT_EQ(audioGraph.size(), 2u); // bridge + owner remain +} + +TEST_F(BridgeGraphTest, BridgeOrphanedAndNoInputsGetsCompacted) { + auto *source = addMockNode(); + auto *bridge = addBridgeNode(); + auto *owner = addMockNode(); + + ASSERT_TRUE(addEdge(source, bridge)); + ASSERT_TRUE(addEdge(bridge, owner)); + audioGraph.process(); + EXPECT_EQ(audioGraph.size(), 3u); + + // Orphan source and bridge + removeNode(source); + removeEdge(bridge, owner); + removeNode(bridge); + audioGraph.process(); + + // Both source and bridge should be compacted + EXPECT_EQ(audioGraph.size(), 1u); // only owner remains +} + +// ========================================================================= +// E. Full Graph wrapper integration +// ========================================================================= + +class BridgeGraphWrapperTest : public ::testing::Test { + protected: + using HNode = HostGraph::Node; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + DisposerImpl disposer_{64}; + std::shared_ptr graph; + // Hold real AudioParams to avoid fake pointer crashes + std::vector> params_; + + void SetUp() override { + graph = std::make_shared(4096, &disposer_); + } + + AudioParam *createParam() { + params_.push_back(createMockAudioParam()); + return params_.back().get(); + } + + /// Simulates AudioParamHostObject: creates bridge, adds to graph, connects bridge→owner + HNode *createParamBridge(AudioParam *param, HNode *owner) { + auto *bridge = graph->addNode(std::make_unique(param)); + EXPECT_TRUE(graph->addEdge(bridge, owner).is_ok()); + return bridge; + } + + void processAll() { + graph->processEvents(); + graph->process(); + } +}; + +TEST_F(BridgeGraphWrapperTest, ConnectSourceToBridge) { + auto *source = graph->addNode(std::make_unique()); + auto node = std::make_unique(); + // set manually so processable propagates through bridge and source + node->setProcessable(); + auto *owner = graph->addNode(std::move(node)); + auto *param = createParam(); + auto *bridge = createParamBridge(param, owner); + + auto result = graph->addEdge(source, bridge); + ASSERT_TRUE(result.is_ok()); + + processAll(); + + // Should have 3 nodes: source, bridge, owner (all processable now) + size_t iterCount = 0; + for (auto &&[graphObject, inputs] : graph->iter()) { + iterCount++; + } + EXPECT_EQ(iterCount, 3u); +} + +TEST_F(BridgeGraphWrapperTest, DisconnectSourceFromBridge) { + auto *source = graph->addNode(std::make_unique()); + auto node = std::make_unique(); + // set manually so processable propagates through bridge and source + node->setProcessable(); + auto *owner = graph->addNode(std::move(node)); + auto *param = createParam(); + auto *bridge = createParamBridge(param, owner); + + ASSERT_TRUE(graph->addEdge(source, bridge).is_ok()); + processAll(); + + // Disconnect source → bridge + ASSERT_TRUE(graph->removeEdge(source, bridge).is_ok()); + processAll(); + + // Bridge still exists (owned by param host object), but no source connected + size_t iterCount = 0; + for (auto &&[graphObject, inputs] : graph->iter()) { + iterCount++; + } + EXPECT_EQ(iterCount, 2u); // only owner + bridge remains processable +} + +TEST_F(BridgeGraphWrapperTest, DuplicateEdgeToBridgeRejected) { + auto *source = graph->addNode(std::make_unique()); + auto *owner = graph->addNode(std::make_unique()); + auto *param = createParam(); + auto *bridge = createParamBridge(param, owner); + + ASSERT_TRUE(graph->addEdge(source, bridge).is_ok()); + + // Same edge again should fail + auto result = graph->addEdge(source, bridge); + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::EDGE_ALREADY_EXISTS); +} + +TEST_F(BridgeGraphWrapperTest, CycleDetectedThroughBridge) { + auto *nodeA = graph->addNode(std::make_unique()); + auto *nodeB = graph->addNode(std::make_unique()); + auto *param = createParam(); + + // A → B (regular edge) + ASSERT_TRUE(graph->addEdge(nodeA, nodeB).is_ok()); + + // Create bridge for nodeA's param (bridge → nodeA) + auto *bridge = createParamBridge(param, nodeA); + + // Try B → bridge — combined with bridge → A and A → B creates cycle + auto result = graph->addEdge(nodeB, bridge); + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::CYCLE_DETECTED); +} + +TEST_F(BridgeGraphWrapperTest, BridgeRemovalWhenParamDestroyed) { + auto *source = graph->addNode(std::make_unique()); + auto node = std::make_unique(); + // set manually so processable propagates through bridge and source + node->setProcessable(); + auto *owner = graph->addNode(std::move(node)); + auto *param = createParam(); + auto *bridge = createParamBridge(param, owner); + + ASSERT_TRUE(graph->addEdge(source, bridge).is_ok()); + processAll(); + + // Simulate AudioParamHostObject destruction: + // First remove all edges to/from bridge, then remove bridge node + ASSERT_TRUE(graph->removeEdge(source, bridge).is_ok()); + ASSERT_TRUE(graph->removeEdge(bridge, owner).is_ok()); + ASSERT_TRUE(graph->removeNode(bridge).is_ok()); + processAll(); + + // Bridge should be compacted away (orphaned + no inputs) + size_t iterCount = 0; + for (auto &&[graphObject, inputs] : graph->iter()) { + iterCount++; + } + EXPECT_EQ(iterCount, 1u); // only owner remains processable +} + +TEST_F(BridgeGraphWrapperTest, MultipleSourcesConnectToSameBridge) { + auto *source1 = graph->addNode(std::make_unique()); + auto *source2 = graph->addNode(std::make_unique()); + auto node = std::make_unique(); + // set manually so processable propagates through bridge and source + node->setProcessable(); + auto *owner = graph->addNode(std::move(node)); + auto *param = createParam(); + auto *bridge = createParamBridge(param, owner); + + ASSERT_TRUE(graph->addEdge(source1, bridge).is_ok()); + ASSERT_TRUE(graph->addEdge(source2, bridge).is_ok()); + processAll(); + + // Should have 4 nodes: source1, source2, bridge, owner + size_t iterCount = 0; + for (auto &&[graphObject, inputs] : graph->iter()) { + iterCount++; + } + EXPECT_EQ(iterCount, 4u); + + // Disconnect one source + ASSERT_TRUE(graph->removeEdge(source1, bridge).is_ok()); + processAll(); + + // Still 4 nodes + iterCount = 0; + for (auto &&[graphObject, inputs] : graph->iter()) { + iterCount++; + } + EXPECT_EQ(iterCount, 3u); // source1 is unprocessable +} + +TEST_F(BridgeGraphWrapperTest, ConcurrentWithMockGraphProcessor) { + using Processor = audioapi::test::MockGraphProcessor; + // Pre-allocate node/pool capacity to avoid grow-event allocations inside + // the AudioThreadGuard scope. + auto sharedGraph = std::make_shared(4096, &disposer_, 16, 64); + Processor processor(*sharedGraph); + processor.start(); + + auto *source = sharedGraph->addNode(std::make_unique(nullptr, 10)); + auto *owner = sharedGraph->addNode(std::make_unique(nullptr, 20)); + auto *param = createParam(); + + // Create bridge and connect + auto *bridge = sharedGraph->addNode(std::make_unique(param)); + ASSERT_TRUE(sharedGraph->addEdge(bridge, owner).is_ok()); + ASSERT_TRUE(sharedGraph->addEdge(source, bridge).is_ok()); + + // Let processor run a few cycles + while (processor.cyclesCompleted() < 10) { + std::this_thread::yield(); + } + + // Disconnect source from bridge + ASSERT_TRUE(sharedGraph->removeEdge(source, bridge).is_ok()); + + while (processor.cyclesCompleted() < 20) { + std::this_thread::yield(); + } + + processor.stop(); + EXPECT_TRUE(processor.allocationClean()); +} + +// ========================================================================= +// F. Fuzz test with bridge nodes +// ========================================================================= + +class BridgeFuzzTest : public ::testing::TestWithParam { + protected: + using HNode = HostGraph::Node; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + + DisposerImpl disposer_{64}; + std::shared_ptr graph; + std::mt19937_64 rng; + std::vector liveNodes; + std::vector bridgeNodes; + std::vector> params_; // Real params + std::vector paramPtrs_; // Raw pointers for test use + + void SetUp() override { + graph = std::make_shared(4096, &disposer_); + rng.seed(GetParam()); + + // Create real AudioParam objects for testing + for (int i = 1; i <= 8; i++) { + params_.push_back(createMockAudioParam()); + paramPtrs_.push_back(params_.back().get()); + } + } + + void processAll() { + graph->processEvents(); + graph->process(); + } + + HNode *pickRandom() { + if (liveNodes.empty()) + return nullptr; + return liveNodes[std::uniform_int_distribution(0, liveNodes.size() - 1)(rng)]; + } + + HNode *pickBridge() { + if (bridgeNodes.empty()) + return nullptr; + return bridgeNodes[std::uniform_int_distribution(0, bridgeNodes.size() - 1)(rng)]; + } + + AudioParam *pickParam() { + return paramPtrs_[std::uniform_int_distribution(0, paramPtrs_.size() - 1)(rng)]; + } +}; + +TEST_P(BridgeFuzzTest, RandomParamOps) { + size_t initialCount = std::uniform_int_distribution(4, 16)(rng); + size_t opCount = std::uniform_int_distribution(50, 200)(rng); + + // Seed nodes + for (size_t i = 0; i < initialCount; i++) { + liveNodes.push_back(graph->addNode(std::make_unique())); + } + processAll(); + + for (size_t i = 0; i < opCount; i++) { + size_t op = std::uniform_int_distribution(0, 99)(rng); + + if (op < 10) { + // Add node + liveNodes.push_back(graph->addNode(std::make_unique())); + + } else if (op < 25) { + // Add regular edge + auto *a = pickRandom(); + auto *b = pickRandom(); + if (a && b && a != b) { + (void)graph->addEdge(a, b); + } + + } else if (op < 35) { + // Create bridge for a param and connect to owner + auto *owner = pickRandom(); + if (owner) { + auto *bridge = graph->addNode(std::make_unique(pickParam())); + (void)graph->addEdge(bridge, owner); + bridgeNodes.push_back(bridge); + } + + } else if (op < 50) { + // Connect source to bridge + auto *source = pickRandom(); + auto *bridge = pickBridge(); + if (source && bridge) { + (void)graph->addEdge(source, bridge); + } + + } else if (op < 60) { + // Disconnect source from bridge + auto *source = pickRandom(); + auto *bridge = pickBridge(); + if (source && bridge) { + (void)graph->removeEdge(source, bridge); + } + + } else if (op < 70) { + // Remove bridge node (simulating param destruction) + auto *bridge = pickBridge(); + if (bridge) { + (void)graph->removeNode(bridge); + bridgeNodes.erase( + std::remove(bridgeNodes.begin(), bridgeNodes.end(), bridge), bridgeNodes.end()); + } + + } else if (op < 80) { + // Remove regular node + auto *n = pickRandom(); + if (n) { + (void)graph->removeNode(n); + liveNodes.erase(std::remove(liveNodes.begin(), liveNodes.end(), n), liveNodes.end()); + } + + } else if (op < 90) { + // Remove regular edge + auto *a = pickRandom(); + auto *b = pickRandom(); + if (a && b) { + (void)graph->removeEdge(a, b); + } + + } else { + // Process + processAll(); + } + } + + // Final process — should not crash or produce bad state + processAll(); + + // Verify iter doesn't crash + for (auto &&[graphObject, inputs] : graph->iter()) { + (void)graphObject; + for (const auto &input : inputs) { + (void)input; + } + } +} + +INSTANTIATE_TEST_SUITE_P( + Seeds, + BridgeFuzzTest, + ::testing::Range(uint64_t{0}, uint64_t{100}), + [](const ::testing::TestParamInfo &info) { + return "seed_" + std::to_string(info.param); + }); + +} // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphCycleDebugTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphCycleDebugTest.cpp index 6c26d867d..2df07f1e3 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphCycleDebugTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphCycleDebugTest.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -18,14 +18,14 @@ namespace audioapi::utils::graph { class GraphCycleDebugTest : public ::testing::TestWithParam { protected: using MNode = MockNode; - using HNode = HostGraph::Node; - using AGEvent = HostGraph::AGEvent; + using HNode = HostGraph::Node; + using AGEvent = HostGraph::AGEvent; - static constexpr size_t kPayloadSize = HostGraph::kDisposerPayloadSize; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; std::mt19937_64 rng; - AudioGraph audioGraph; - HostGraph hostGraph; + AudioGraph audioGraph; + HostGraph hostGraph; DisposerImpl disposer_{64}; std::vector liveNodes; size_t nextId = 0; @@ -37,7 +37,8 @@ class GraphCycleDebugTest : public ::testing::TestWithParam { // ── Helpers ───────────────────────────────────────────────────────────── HNode *doAddNode() { - auto handle = std::make_shared>(0, std::make_unique()); + std::unique_ptr obj = std::make_unique(); + auto handle = std::make_shared(0, std::move(obj)); auto [hostNode, event] = hostGraph.addNode(handle); size_t id = nextId++; hostNode->test_node_identifier__ = id; diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphFuzzTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphFuzzTest.cpp index 9e5829f3e..483a6171e 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphFuzzTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphFuzzTest.cpp @@ -1,7 +1,5 @@ -#include +#include #include -#include -#include #include #include #include @@ -9,12 +7,12 @@ #include #include #include -#include "AudioThreadGuard.h" #include "MockGraphProcessor.h" #include "TestGraphUtils.h" using namespace audioapi::utils::graph; using audioapi::test::MockGraphProcessor; +using audioapi::utils::DisposerImpl; // ========================================================================= // Fixture — parameterized by seed for reproducible randomized testing @@ -23,12 +21,14 @@ using audioapi::test::MockGraphProcessor; class GraphFuzzTest : public ::testing::TestWithParam { protected: using PNode = ProcessableMockNode; - using HNode = HostGraph::Node; - using Res = Graph::Res; - using ResultError = Graph::ResultError; + using HNode = HostGraph::Node; + using Res = Graph::Res; + using ResultError = Graph::ResultError; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + DisposerImpl disposer_{64}; std::mt19937_64 rng; - std::unique_ptr> graph; + std::unique_ptr graph; std::vector nodes; // tracks live (non-removed) nodes size_t initialNodeCount; size_t operationCount; @@ -49,7 +49,7 @@ class GraphFuzzTest : public ::testing::TestWithParam { // Ensure graph growth does not happen on the audio thread during this fuzz run. const auto maxNodes = static_cast(initialNodeCount + operationCount + 64); const auto maxEdges = static_cast(operationCount * 2 + 64); - graph = std::make_unique>(4096, maxNodes, maxEdges); + graph = std::make_unique(4096, &disposer_, maxNodes, maxEdges); // Randomly partition the range 0..99 into 4 operation weights size_t total = 100; diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphNodeGrowthTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphNodeGrowthTest.cpp new file mode 100644 index 000000000..013035e2b --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphNodeGrowthTest.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include +#include +#include "AudioThreadGuard.h" +#include "MockGraphProcessor.h" +#include "TestGraphUtils.h" + +using namespace audioapi::utils::graph; +using audioapi::test::AudioThreadGuard; +using audioapi::test::MockGraphProcessor; +using audioapi::utils::DisposerImpl; + +// ========================================================================= +// GraphNodeGrowthTest +// +// Verifies the allocation behaviour of sendNodeGrowIfNeeded(). +// +// The method sends a lambda [newCap](...){ graph.reserveNodes(newCap); } +// through the SPSC event channel. That lambda is executed by the audio +// thread inside processEvents() → AudioGraph::reserveNodes() → +// std::vector::reserve(), which allocates heap memory ON the audio thread. +// +// The tests below expose this by running a MockGraphProcessor (audio thread +// instrumented with AudioThreadGuard) while the main thread adds nodes. +// ========================================================================= + +class GraphNodeGrowthTest : public ::testing::Test { + protected: + using PNode = ProcessableMockNode; + using HNode = HostGraph::Node; + + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + DisposerImpl disposer_{64}; +}; + +// ── Test 1: Node grow events must not allocate on the audio thread ───────── +// +// Creates a Graph WITHOUT pre-reserved node capacity (nodeCapacity_ == 0). +// Adding 1000 nodes triggers multiple sendNodeGrowIfNeeded() calls, each +// sending a grow event that the audio thread executes inside processEvents(). +// +// Currently FAILS because the grow event lambda calls reserveNodes() on the +// audio thread → std::vector::reserve() → heap allocation. +// The fix: pre-allocate the buffer on the main thread (same pattern as +// sendPoolGrowIfNeeded) and only hand off the ready buffer via the event. +TEST_F(GraphNodeGrowthTest, NodeGrowEventsDoNotAllocateOnAudioThread) { + if (!audioapi::test::AudioThreadGuard::isEnabled()) { + GTEST_SKIP() << "AudioThreadGuard operator new/delete overrides are disabled " + "under ASan/TSan — allocation tracking unavailable"; + } + + // No initial capacity → nodeCapacity_ = 0, every threshold triggers a grow + auto graph = std::make_unique(4096, &disposer_); + + MockGraphProcessor processor(*graph); + processor.start(); + + constexpr size_t kNodeCount = 1000; + std::vector nodes; + nodes.reserve(kNodeCount); + + for (size_t i = 0; i < kNodeCount; ++i) { + nodes.push_back(graph->addNode(std::make_unique())); + + // Periodic yield so the audio thread can process grow events + // while the main thread is still adding nodes. + if (i % 50 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(500)); + } + } + + // Drain all pending events + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + processor.stop(); + + EXPECT_GT(processor.cyclesCompleted(), 0u) + << "Audio thread should have completed at least one cycle"; + + EXPECT_TRUE(processor.allocationClean()) + << "Audio thread allocated " << processor.allocationViolations() << " times across " + << processor.cyclesCompleted() + << " cycles — sendNodeGrowIfNeeded() must not allocate on the audio thread"; +} + +// ── Test 2: Pre-reserved capacity prevents audio-thread allocations ──────── +// +// When the Graph is constructed with sufficient initial capacity +// (4-arg constructor), sendNodeGrowIfNeeded() never fires and the audio +// thread stays allocation-free — even for 1000 nodes. +// This serves as a control / regression baseline. +TEST_F(GraphNodeGrowthTest, PreReservedCapacityKeepsAudioThreadAllocationFree) { + if (!audioapi::test::AudioThreadGuard::isEnabled()) { + GTEST_SKIP() << "AudioThreadGuard operator new/delete overrides are disabled " + "under ASan/TSan — allocation tracking unavailable"; + } + + constexpr size_t kNodeCount = 1000; + + // Reserve enough upfront so sendNodeGrowIfNeeded() never fires + const auto maxNodes = static_cast(kNodeCount + 64); + const std::uint32_t maxEdges = 64; + auto graph = std::make_unique(4096, &disposer_, maxNodes, maxEdges); + + MockGraphProcessor processor(*graph); + processor.start(); + + std::vector nodes; + nodes.reserve(kNodeCount); + + for (size_t i = 0; i < kNodeCount; ++i) { + nodes.push_back(graph->addNode(std::make_unique())); + if (i % 50 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(500)); + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + processor.stop(); + + EXPECT_GT(processor.cyclesCompleted(), 0u) + << "Audio thread should have completed at least one cycle"; + + EXPECT_TRUE(processor.allocationClean()) + << "Audio thread allocated " << processor.allocationViolations() << " times across " + << processor.cyclesCompleted() << " cycles despite pre-reserved capacity"; +} diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphTest.cpp index 944dd055f..c9f248aba 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/GraphTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/GraphTest.cpp @@ -1,11 +1,15 @@ -#include +#include +#include +#include +#include +#include #include #include #include #include #include -#include #include +#include #include #include "TestGraphUtils.h" @@ -13,26 +17,69 @@ namespace audioapi::utils::graph { class GraphTest : public ::testing::Test { protected: - std::unique_ptr> graph; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; + DisposerImpl disposer_{64}; + std::unique_ptr graph; void SetUp() override { - graph = std::make_unique>(4096); + graph = std::make_unique(4096, &disposer_); } - const AudioGraph &getAudioGraph() { + const AudioGraph &getAudioGraph() { return graph->audioGraph; } - const HostGraph &getHostGraph() { + const HostGraph &getHostGraph() { return graph->hostGraph; } }; +namespace { + +/// Minimal AudioNode used as both a "source" (configurable channel count on +/// its output buffer) and a "destination" (configurable channelCount / +/// channelCountMode whose effective buffer we assert on). +class ChannelCountTestNode : public audioapi::AudioNode { + public: + ChannelCountTestNode( + const std::shared_ptr &context, + const audioapi::AudioNodeOptions &options) + : AudioNode(context, options) {} + + void processNode(int /*framesToProcess*/) override {} +}; + +struct ChannelOpts { + int channelCount; + audioapi::ChannelCountMode mode; +}; + +/// @returns the computed number of channels on the output buffer of the +/// AudioNode that backs `node`. +inline size_t channelsOf(const HostGraph::Node *node) { + auto *audioNode = node->handle->audioNode->asAudioNode(); + return audioNode->getOutputBuffer()->getNumberOfChannels(); +} + +/// Adds a ChannelCountTestNode with the given options to the managed +/// `graph`. Returns the HostGraph::Node pointer; the associated AudioGraph +/// slot is populated once `graph->processEvents()` is called. +inline HostGraph::Node *addChannelCountNode(Graph &graph, const ChannelOpts &opts) { + audioapi::AudioNodeOptions audioNodeOpts; + audioNodeOpts.channelCount = opts.channelCount; + audioNodeOpts.channelCountMode = opts.mode; + + auto audioNode = std::make_unique(getGraphTestContext(), audioNodeOpts); + return graph.addNode(std::move(audioNode)); +} + +} // namespace + TEST_F(GraphTest, EventsAreScheduledButNotExecutedUntilProcess) { auto *node = graph->addNode(); ASSERT_NE(node, nullptr); - // AudioGraph should not be aware of the node yet (event not processed) + // AudioGraph should not be aware of the node yet (event not processed) const auto &ag = getAudioGraph(); size_t sizeBefore = ag.size(); @@ -55,8 +102,7 @@ TEST_F(GraphTest, NoUselessEventsScheduled) { const auto &ag = getAudioGraph(); // Convert to verify auto initialAdj = TestGraphUtils::convertAudioGraphToAdjacencyList( - const_cast &>( - ag)); // casting const away if utils need it, or verifyutils usage + const_cast(ag)); // casting const away if utils need it, or verifyutils usage // Try adding duplicate edge (should fail and NOT schedule event) ASSERT_TRUE(graph->addEdge(node1, node2).is_ok()); // Success first time @@ -64,19 +110,18 @@ TEST_F(GraphTest, NoUselessEventsScheduled) { // Result of valid op auto intermediateAdj = - TestGraphUtils::convertAudioGraphToAdjacencyList(const_cast &>(ag)); + TestGraphUtils::convertAudioGraphToAdjacencyList(const_cast(ag)); // Try adding SAME edge (should fail) auto result = graph->addEdge(node1, node2); EXPECT_TRUE(result.is_err()); - EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::EDGE_ALREADY_EXISTS); + EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::EDGE_ALREADY_EXISTS); // Even if we call processEvents, state should not change (and no event should be consumed ideally, // impossible to check queue count easily without friend or mock, but state check is good enough) graph->processEvents(); - auto finalAdj = - TestGraphUtils::convertAudioGraphToAdjacencyList(const_cast &>(ag)); + auto finalAdj = TestGraphUtils::convertAudioGraphToAdjacencyList(const_cast(ag)); EXPECT_EQ(intermediateAdj, finalAdj); } @@ -86,7 +131,7 @@ TEST_F(GraphTest, ThreadRaceConcurrency) { // One thread processes events (consumer) std::atomic running{true}; - std::vector::Node *> nodes; + std::vector nodes; // Add initial nodes for (int i = 0; i < 10; ++i) { @@ -112,7 +157,7 @@ TEST_F(GraphTest, ThreadRaceConcurrency) { nodes.push_back(n); } else if (op == 1 && nodes.size() > 2) { // Add edge - HostGraph::Node *n1, *n2; + HostGraph::Node *n1, *n2; { n1 = nodes[rand_r(&seed) % nodes.size()]; n2 = nodes[rand_r(&seed) % nodes.size()]; @@ -123,7 +168,7 @@ TEST_F(GraphTest, ThreadRaceConcurrency) { } } else if (op == 2 && nodes.size() > 5) { // Remove edge - HostGraph::Node *n1, *n2; + HostGraph::Node *n1, *n2; { n1 = nodes[rand_r(&seed) % nodes.size()]; n2 = nodes[rand_r(&seed) % nodes.size()]; @@ -146,14 +191,134 @@ TEST_F(GraphTest, ThreadRaceConcurrency) { const auto &ag = getAudioGraph(); const auto &hg = getHostGraph(); - auto audioAdj = - TestGraphUtils::convertAudioGraphToAdjacencyList(const_cast &>(ag)); - auto hostAdj = - TestGraphUtils::convertHostGraphToAdjacencyList(const_cast &>(hg)); + auto audioAdj = TestGraphUtils::convertAudioGraphToAdjacencyList(const_cast(ag)); + auto hostAdj = TestGraphUtils::convertHostGraphToAdjacencyList(const_cast(hg)); // They should match EXPECT_EQ(audioAdj, hostAdj); } } +// ─── Channel-count negotiation on connect/disconnect ───────────────────── +// +// These tests assert the Web Audio contract: the computed number of +// channels on a node's output buffer must follow `channelCountMode` +// - MAX -> max(channelCount of each connected input) +// (the node's channelCount attribute is ignored) +// - CLAMPED_MAX -> min(channelCount attribute, +// max(channelCount of each connected input)) +// - EXPLICIT -> always the channelCount attribute +// +// The computation must happen on the HostGraph side at addEdge/removeEdge +// time, but the actual buffer swap is published through the AGEvent and +// applied on the AudioGraph side — therefore each test calls +// `graph->processEvents()` before inspecting `channelsOf(...)`. + +TEST_F(GraphTest, ChannelCountNegotiation_MaxMode_SingleInput) { + auto *source = + addChannelCountNode(*graph, {.channelCount = 4, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::MAX}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(source, dest).is_ok()); + graph->processEvents(); + + EXPECT_EQ(channelsOf(dest), 4u) + << "MAX mode: after connecting a 4-channel source the downstream buffer " + "must be resized to 4 channels (channelCount attribute is ignored)"; +} + +TEST_F(GraphTest, ChannelCountNegotiation_MaxMode_MultipleInputsTakeMax) { + auto *mono = addChannelCountNode(*graph, {.channelCount = 1, .mode = ChannelCountMode::EXPLICIT}); + auto *six = addChannelCountNode(*graph, {.channelCount = 6, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::MAX}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(mono, dest).is_ok()); + ASSERT_TRUE(graph->addEdge(six, dest).is_ok()); + graph->processEvents(); + + EXPECT_EQ(channelsOf(dest), 6u) + << "MAX mode: the downstream buffer must follow the largest connected input"; +} + +TEST_F(GraphTest, ChannelCountNegotiation_ClampedMaxMode_ClampsAboveAttribute) { + auto *source = + addChannelCountNode(*graph, {.channelCount = 6, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = + addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::CLAMPED_MAX}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(source, dest).is_ok()); + graph->processEvents(); + + EXPECT_EQ(channelsOf(dest), 2u) + << "CLAMPED_MAX should clamp a 6-channel input down to channelCount=2"; +} + +TEST_F(GraphTest, ChannelCountNegotiation_ClampedMaxMode_FollowsInputWhenBelowAttribute) { + auto *source = + addChannelCountNode(*graph, {.channelCount = 1, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = + addChannelCountNode(*graph, {.channelCount = 4, .mode = ChannelCountMode::CLAMPED_MAX}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(source, dest).is_ok()); + graph->processEvents(); + + EXPECT_EQ(channelsOf(dest), 1u) + << "CLAMPED_MAX: mono input with channelCount=4 must still produce a mono buffer"; +} + +TEST_F(GraphTest, ChannelCountNegotiation_ExplicitMode_IgnoresInput) { + auto *source = + addChannelCountNode(*graph, {.channelCount = 6, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = addChannelCountNode(*graph, {.channelCount = 4, .mode = ChannelCountMode::EXPLICIT}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(source, dest).is_ok()); + graph->processEvents(); + + EXPECT_EQ(channelsOf(dest), 4u) << "EXPLICIT must always produce exactly channelCount channels"; +} + +TEST_F(GraphTest, ChannelCountNegotiation_MaxMode_RecomputesOnSecondConnection) { + auto *stereoSource = + addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::EXPLICIT}); + auto *quadSource = + addChannelCountNode(*graph, {.channelCount = 4, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::MAX}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(stereoSource, dest).is_ok()); + graph->processEvents(); + EXPECT_EQ(channelsOf(dest), 2u) + << "MAX: with only a stereo source connected, buffer should be 2 channels"; + + ASSERT_TRUE(graph->addEdge(quadSource, dest).is_ok()); + graph->processEvents(); + EXPECT_EQ(channelsOf(dest), 4u) + << "MAX: connecting a 4-channel source must grow the buffer to 4 channels"; +} + +TEST_F(GraphTest, ChannelCountNegotiation_MaxMode_RecomputesOnDisconnection) { + auto *stereoSource = + addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::EXPLICIT}); + auto *quadSource = + addChannelCountNode(*graph, {.channelCount = 4, .mode = ChannelCountMode::EXPLICIT}); + auto *dest = addChannelCountNode(*graph, {.channelCount = 2, .mode = ChannelCountMode::MAX}); + graph->processEvents(); + + ASSERT_TRUE(graph->addEdge(stereoSource, dest).is_ok()); + ASSERT_TRUE(graph->addEdge(quadSource, dest).is_ok()); + graph->processEvents(); + ASSERT_EQ(channelsOf(dest), 4u); + + ASSERT_TRUE(graph->removeEdge(quadSource, dest).is_ok()); + graph->processEvents(); + + EXPECT_EQ(channelsOf(dest), 2u) + << "MAX: removing the 4-channel source should shrink the buffer back to 2 channels"; +} + } // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/HostGraphTest.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/HostGraphTest.cpp index e796c0003..8fec9e0cf 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/HostGraphTest.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/HostGraphTest.cpp @@ -1,10 +1,9 @@ -#include -#include -#include -#include +#include +#include +#include +#include #include #include -#include #include #include #include "TestGraphUtils.h" @@ -13,19 +12,19 @@ namespace audioapi::utils::graph { class HostGraphTest : public ::testing::Test { protected: - static constexpr size_t kPayloadSize = HostGraph::kDisposerPayloadSize; + static constexpr size_t kPayloadSize = audioapi::DISPOSER_PAYLOAD_SIZE; DisposerImpl disposer_{64}; void verifyAddEdge( - HostGraph &hostGraph, - AudioGraph &audioGraph, + HostGraph &hostGraph, + AudioGraph &audioGraph, size_t fromId, size_t toId, const std::vector> &expectedAdjacencyList) { // Find nodes by ID - HostGraph::Node *fromNode = nullptr; - HostGraph::Node *toNode = nullptr; + HostGraph::Node *fromNode = nullptr; + HostGraph::Node *toNode = nullptr; for (auto *n : hostGraph.nodes) { if (n->test_node_identifier__ == fromId) @@ -44,26 +43,25 @@ class HostGraphTest : public ::testing::Test { auto result = hostGraph.addEdge(fromNode, toNode); ASSERT_TRUE(result.is_ok()) << "addEdge failed"; - // Verify AudioGraph UNCHANGED + // Verify AudioGraph UNCHANGED auto intermediateAudioAdj = TestGraphUtils::convertAudioGraphToAdjacencyList(audioGraph); - EXPECT_EQ(initialAudioAdj, intermediateAudioAdj) - << "AudioGraph changed before event execution"; + EXPECT_EQ(initialAudioAdj, intermediateAudioAdj) << "AudioGraph changed before event execution"; // Perform Event auto event = std::move(result).unwrap(); event(audioGraph, disposer_); - // Verify AudioGraph UPDATED and CONSISTENT + // Verify AudioGraph UPDATED and CONSISTENT auto finalAudioAdj = TestGraphUtils::convertAudioGraphToAdjacencyList(audioGraph); auto finalHostAdj = TestGraphUtils::convertHostGraphToAdjacencyList(hostGraph); EXPECT_EQ(finalAudioAdj, expectedAdjacencyList) - << "AudioGraph does not match expected adjacency list"; + << "AudioGraph does not match expected adjacency list"; EXPECT_EQ(finalHostAdj, expectedAdjacencyList) - << "HostGraph does not match expected adjacency list"; + << "HostGraph does not match expected adjacency list"; } - HostGraph::Node *findNode(const HostGraph &hostGraph, size_t id) { + HostGraph::Node *findNode(const HostGraph &hostGraph, size_t id) { for (auto *n : hostGraph.nodes) { if (n->test_node_identifier__ == id) return n; @@ -79,19 +77,19 @@ TEST_F(HostGraphTest, AddNode) { {} // 2 }); - // Create a new handle and add it via HostGraph - auto handle = std::make_shared>(0, nullptr); + // Create a new handle and add it via HostGraph + auto handle = std::make_shared(0, nullptr); auto [hostNode, event] = hostGraph.addNode(handle); EXPECT_EQ(hostNode->handle, handle); hostNode->test_node_identifier__ = 3; - // AudioGraph unchanged before event + // AudioGraph unchanged before event EXPECT_EQ(audioGraph.size(), 3u); event(audioGraph, disposer_); - // After event: node added to AudioGraph + // After event: node added to AudioGraph EXPECT_EQ(audioGraph.size(), 4u); audioGraph[handle->index].test_node_identifier__ = 3; @@ -200,21 +198,21 @@ TEST_F(HostGraphTest, AddEdge_CycleDetection) { auto hostAdjBefore = TestGraphUtils::convertHostGraphToAdjacencyList(hostGraph); auto audioAdjBefore = TestGraphUtils::convertAudioGraphToAdjacencyList(audioGraph); - HostGraph::Node *node0 = findNode(hostGraph, 0); - HostGraph::Node *node2 = findNode(hostGraph, 2); + HostGraph::Node *node0 = findNode(hostGraph, 0); + HostGraph::Node *node2 = findNode(hostGraph, 2); // Try adding cycle 2->0 auto result = hostGraph.addEdge(node2, node0); EXPECT_TRUE(result.is_err()); - EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::CYCLE_DETECTED); + EXPECT_EQ(result.unwrap_err(), HostGraph::ResultError::CYCLE_DETECTED); - // HostGraph should NOT change + // HostGraph should NOT change auto hostAdjAfter = TestGraphUtils::convertHostGraphToAdjacencyList(hostGraph); - EXPECT_EQ(hostAdjBefore, hostAdjAfter) << "HostGraph modified despite cycle detection"; + EXPECT_EQ(hostAdjBefore, hostAdjAfter) << "HostGraph modified despite cycle detection"; - // AudioGraph should NOT change (no event executed) + // AudioGraph should NOT change (no event executed) auto audioAdjAfter = TestGraphUtils::convertAudioGraphToAdjacencyList(audioGraph); - EXPECT_EQ(audioAdjBefore, audioAdjAfter) << "AudioGraph modified"; + EXPECT_EQ(audioAdjBefore, audioAdjAfter) << "AudioGraph modified"; } TEST_F(HostGraphTest, AddEdge_LargeSpecificGraph) { @@ -267,8 +265,8 @@ TEST_F(HostGraphTest, AddEdge_GridInterconnect) { // If we try 5->0 -> Cycle (5 reachable from 0) auto hostAdjBefore = TestGraphUtils::convertHostGraphToAdjacencyList(hostGraph); - HostGraph::Node *node5 = findNode(hostGraph, 5); - HostGraph::Node *node0 = findNode(hostGraph, 0); + HostGraph::Node *node5 = findNode(hostGraph, 5); + HostGraph::Node *node0 = findNode(hostGraph, 0); auto result = hostGraph.addEdge(node5, node0); EXPECT_TRUE(result.is_err()); @@ -276,17 +274,17 @@ TEST_F(HostGraphTest, AddEdge_GridInterconnect) { } // --------------------------------------------------------------------------- -// BUG demonstration: ghost node in AudioGraph causes accepted cycle +// BUG demonstration: ghost node in AudioGraph causes accepted cycle // --------------------------------------------------------------------------- // -// When a node is removed from HostGraph it is deleted immediately (edges torn -// down, pointer freed). The corresponding AudioGraph event only marks the +// When a node is removed from HostGraph it is deleted immediately (edges torn +// down, pointer freed). The corresponding AudioGraph event only marks the // node as `orphaned` — it stays in the vector with all its edges until // compaction eventually removes it. // -// This creates a window where HostGraph no longer "sees" the node, so its +// This creates a window where HostGraph no longer "sees" the node, so its // cycle-detection (hasPath) can miss paths that still exist in AudioGraph. -// If a new edge is added through that blind-spot, AudioGraph ends up with a +// If a new edge is added through that blind-spot, AudioGraph ends up with a // cycle and toposort produces garbage. // TEST_F(HostGraphTest, RemoveNode_GhostNodeMustNotAllowCycle) { @@ -297,32 +295,31 @@ TEST_F(HostGraphTest, RemoveNode_GhostNodeMustNotAllowCycle) { {} // 2 }); - HostGraph::Node *node0 = findNode(hostGraph, 0); - HostGraph::Node *node1 = findNode(hostGraph, 1); - HostGraph::Node *node2 = findNode(hostGraph, 2); + HostGraph::Node *node0 = findNode(hostGraph, 0); + HostGraph::Node *node1 = findNode(hostGraph, 1); + HostGraph::Node *node2 = findNode(hostGraph, 2); ASSERT_NE(node0, nullptr); ASSERT_NE(node1, nullptr); ASSERT_NE(node2, nullptr); - // ── Step 1: remove node 1 from HostGraph ── + // ── Step 1: remove node 1 from HostGraph ── auto removeResult = hostGraph.removeNode(node1); ASSERT_TRUE(removeResult.is_ok()); - // Execute the remove-event on AudioGraph (only sets orphaned=true). + // Execute the remove-event on AudioGraph (only sets orphaned=true). auto removeEvent = std::move(removeResult).unwrap(); removeEvent(audioGraph, disposer_); - // AudioGraph still has the ghost: 0 → 1(orphaned) → 2 + // AudioGraph still has the ghost: 0 → 1(orphaned) → 2 EXPECT_EQ(audioGraph.size(), 3u); // ── Step 2: add edge 2 → 0 ── - // Because node 1 still bridges 0→2 in AudioGraph, this would create - // a cycle: 0 → 1 → 2 → 0. HostGraph MUST reject it. + // Because node 1 still bridges 0→2 in AudioGraph, this would create + // a cycle: 0 → 1 → 2 → 0. HostGraph MUST reject it. auto addResult = hostGraph.addEdge(node2, node0); - EXPECT_TRUE(addResult.is_err()) - << "HostGraph should detect the cycle through the ghost node"; - EXPECT_EQ(addResult.unwrap_err(), HostGraph::ResultError::CYCLE_DETECTED); + EXPECT_TRUE(addResult.is_err()) << "HostGraph should detect the cycle through the ghost node"; + EXPECT_EQ(addResult.unwrap_err(), HostGraph::ResultError::CYCLE_DETECTED); } } // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/MockGraphProcessor.h b/packages/react-native-audio-api/common/cpp/test/src/graph/MockGraphProcessor.h index 2d296d7f7..7dff81a27 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/MockGraphProcessor.h +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/MockGraphProcessor.h @@ -1,11 +1,12 @@ #pragma once -#include +#include #include "AudioThreadGuard.h" #include #include #include +#include #include #ifdef __APPLE__ @@ -21,15 +22,15 @@ namespace audioapi::test { /// Spawns a dedicated thread that repeatedly: /// 1. Processes SPSC events (graph mutations from the main thread) /// 2. Runs toposort + compaction -/// 3. Iterates nodes in topological order, calling `process()` if the -/// NodeType supports it (SFINAE via `requires`) +/// 3. Iterates graph objects in topological order, downcasts to `NodeType`, +/// then calls `process(inputs)` if available /// /// The thread is instrumented with AudioThreadGuard to detect heap /// allocations / deallocations and sample context-switch counters. /// /// ## Usage /// ```cpp -/// Graph graph(4096); +/// Graph graph(4096); /// MockGraphProcessor processor(graph); /// processor.start(); /// // … mutate graph from main thread … @@ -37,11 +38,11 @@ namespace audioapi::test { /// EXPECT_TRUE(processor.allocationClean()); /// ``` /// -/// @tparam NodeType the audio graph node type (must satisfy AudioGraphNode) -template +/// @tparam NodeType concrete GraphObject subtype expected by this processor. +template class MockGraphProcessor { public: - explicit MockGraphProcessor(audioapi::utils::graph::Graph &graph) : graph_(graph) {} + explicit MockGraphProcessor(audioapi::utils::graph::Graph &graph) : graph_(graph) {} ~MockGraphProcessor() { stop(); @@ -136,16 +137,21 @@ class MockGraphProcessor { cycles_.fetch_add(1, std::memory_order_relaxed); } - /// @brief Iterates nodes in topological order and calls process(inputs). + /// @brief Iterates graph objects in topological order and calls process(inputs). void processNodes() { - for (auto &&[node, inputs] : graph_.iter()) { - if constexpr (requires { node.process(inputs); }) { - node.process(inputs); + for (auto &&[graphObject, inputs] : graph_.iter()) { + auto *node = dynamic_cast(&graphObject); + if (!node) { + continue; + } + + if constexpr (requires { node->process(inputs); }) { + node->process(inputs); } } } - audioapi::utils::graph::Graph &graph_; + audioapi::utils::graph::Graph &graph_; std::thread thread_; std::atomic running_{false}; std::atomic allocationViolations_{0}; diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.cpp b/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.cpp index c092fef18..47b5afc1f 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.cpp +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.cpp @@ -8,15 +8,15 @@ namespace audioapi::utils::graph { -std::pair, HostGraph> TestGraphUtils::createTestGraph( +std::pair TestGraphUtils::createTestGraph( std::vector> adjacencyList) { - HostGraph hostGraph = makeFromAdjacencyList(adjacencyList); - AudioGraph audioGraph = createAudioGraphFromHostGraph(hostGraph); + HostGraph hostGraph = makeFromAdjacencyList(adjacencyList); + AudioGraph audioGraph = createAudioGraphFromHostGraph(hostGraph); return {std::move(audioGraph), std::move(hostGraph)}; } std::vector> TestGraphUtils::convertAudioGraphToAdjacencyList( - const AudioGraph &audioGraph) { + const AudioGraph &audioGraph) { std::vector> adjacencyList; if (audioGraph.size() == 0) return {}; @@ -50,7 +50,7 @@ std::vector> TestGraphUtils::convertAudioGraphToAdjacencyLis } std::vector> TestGraphUtils::convertHostGraphToAdjacencyList( - const HostGraph &hostGraph) { + const HostGraph &hostGraph) { std::vector> adjacencyList; if (hostGraph.nodes.empty()) return {}; @@ -66,7 +66,7 @@ std::vector> TestGraphUtils::convertHostGraphToAdjacencyList for (auto *n : hostGraph.nodes) { size_t nodeId = n->test_node_identifier__; - for (HostGraph::Node *output : n->outputs) { + for (HostGraph::Node *output : n->outputs) { if (output) { adjacencyList[nodeId].push_back(output->test_node_identifier__); } @@ -77,15 +77,15 @@ std::vector> TestGraphUtils::convertHostGraphToAdjacencyList return adjacencyList; } -HostGraph TestGraphUtils::makeFromAdjacencyList( +HostGraph TestGraphUtils::makeFromAdjacencyList( const std::vector> &adjacencyList) { - HostGraph graph; - std::vector::Node *> nodesVec; + HostGraph graph; + std::vector nodesVec; nodesVec.reserve(adjacencyList.size()); for (size_t i = 0; i < adjacencyList.size(); ++i) { - auto handle = std::make_shared>(static_cast(i), nullptr); - auto *node = new HostGraph::Node(); + auto handle = std::make_shared(static_cast(i), nullptr); + auto *node = new HostGraph::Node(); node->handle = handle; node->test_node_identifier__ = i; nodesVec.push_back(node); @@ -95,8 +95,8 @@ HostGraph TestGraphUtils::makeFromAdjacencyList( for (size_t fromIndex = 0; fromIndex < adjacencyList.size(); ++fromIndex) { for (size_t toIndex : adjacencyList[fromIndex]) { if (fromIndex < nodesVec.size() && toIndex < nodesVec.size()) { - HostGraph::Node *fromNode = nodesVec[fromIndex]; - HostGraph::Node *toNode = nodesVec[toIndex]; + HostGraph::Node *fromNode = nodesVec[fromIndex]; + HostGraph::Node *toNode = nodesVec[toIndex]; fromNode->outputs.push_back(toNode); toNode->inputs.push_back(fromNode); } @@ -107,9 +107,8 @@ HostGraph TestGraphUtils::makeFromAdjacencyList( return graph; } -AudioGraph TestGraphUtils::createAudioGraphFromHostGraph( - const HostGraph &hostGraph) { - AudioGraph audioGraph; +AudioGraph TestGraphUtils::createAudioGraphFromHostGraph(const HostGraph &hostGraph) { + AudioGraph audioGraph; if (hostGraph.nodes.empty()) return audioGraph; @@ -122,7 +121,7 @@ AudioGraph TestGraphUtils::createAudioGraphFromHostGraph( audioGraph[idx].test_node_identifier__ = n->test_node_identifier__; audioGraph.pool().freeAll(audioGraph[idx].input_head); - for (HostGraph::Node *input : n->inputs) { + for (HostGraph::Node *input : n->inputs) { audioGraph.pool().push(audioGraph[idx].input_head, input->handle->index); } } diff --git a/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h b/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h index d7fa631a2..ca3f42d53 100644 --- a/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h +++ b/packages/react-native-audio-api/common/cpp/test/src/graph/TestGraphUtils.h @@ -5,10 +5,14 @@ #define RN_AUDIO_API_TEST true // for intellisense #endif +#include +#include #include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include @@ -28,12 +32,18 @@ inline std::shared_ptr getGraphTestContext() { auto eventRegistry = std::make_shared(); auto ctx = std::make_shared(2, 1024, 44100.0f, eventRegistry, RuntimeRegistry{}); - ctx->initialize(); return ctx; }(); return context; } +// ── MockAudioParam ──────────────────────────────────────────────────────── +// Real AudioParam for use in graph tests that need valid param pointers. + +inline std::shared_ptr createMockAudioParam() { + return std::make_shared(0.0f, -1.0f, 1.0f, getGraphTestContext()); +} + struct MockNode : AudioNode { explicit MockNode(bool destructible = true) : AudioNode(getGraphTestContext()), destructible_(destructible) {} @@ -47,22 +57,36 @@ struct MockNode : AudioNode { destructible_.store(value, std::memory_order_release); } - private: - std::shared_ptr processNode( - const std::shared_ptr &processingBus, - int) override { - return processingBus; + void setProcessable() { + setProcessableState(PROCESSABLE_STATE::CONDITIONAL_PROCESSABLE); } + private: + void processNode(int) override {} + std::atomic destructible_; }; +// ── NonProcessableMockNode ──────────────────────────────────────────────── +// Pure GraphObject subclass that is not processable. Used to test +// iter() filtering at the AudioGraph level without depending on BridgeNode. + +struct NonProcessableMockNode : GraphObject { + [[nodiscard]] bool isProcessable() const override { + return false; + } + + [[nodiscard]] bool canBeDestructed() const override { + return true; + } +}; + // ── MockHostNode ────────────────────────────────────────────────────────── -// RAII wrapper around HostNode for testing the HostNode lifecycle. +// RAII wrapper around HostNode for testing the HostNode lifecycle. -class MockHostNode : public HostNode { +class MockHostNode : public HostNode { public: - explicit MockHostNode(std::shared_ptr> graph, bool destructible = true) + explicit MockHostNode(std::shared_ptr graph, bool destructible = true) : HostNode(std::move(graph), std::make_unique(destructible)) {} }; @@ -87,10 +111,14 @@ struct ProcessableMockNode : MockNode { ProcessFn processFn = nullptr, int initialValue = 0, bool destructible = true) - : MockNode(destructible), value(initialValue), processFn_(std::move(processFn)) {} + : MockNode(destructible), value(initialValue), processFn_(std::move(processFn)) { + setProcessable(); + } - /// @brief Called by the audio thread with the inputs range from iter(). - /// Collects input values into a stack buffer — no heap allocation. + /// @brief Called by the audio thread with an input range from `Graph::iter()`. + /// + /// Supports both strongly-typed test ranges and GraphObject-based ranges, + /// collecting values into a stack buffer with no heap allocation. template void process(R &&inputs) { if (!processFn_) @@ -98,8 +126,18 @@ struct ProcessableMockNode : MockNode { int buf[kMaxInputs]; size_t n = 0; for (const auto &input : inputs) { - if (n < kMaxInputs) + if (n >= kMaxInputs) { + continue; + } + + if constexpr (requires { input.value.load(std::memory_order_acquire); }) { buf[n++] = input.value.load(std::memory_order_acquire); + } else { + auto *typed = dynamic_cast(&input); + if (typed) { + buf[n++] = typed->value.load(std::memory_order_acquire); + } + } } value.store(processFn_({buf, n}), std::memory_order_release); } @@ -115,22 +153,21 @@ class TestGraphUtils { /// @brief Creates a paired AudioGraph + HostGraph from an adjacency list. /// @param adjacencyList adjacencyList[i] = {j, k} means edges i→j, i→k /// @return (AudioGraph, HostGraph) pair with consistent structure - static std::pair, HostGraph> createTestGraph( + static std::pair createTestGraph( std::vector> adjacencyList); /// @brief Converts AudioGraph to adjacency list for equality comparison. static std::vector> convertAudioGraphToAdjacencyList( - const AudioGraph &audioGraph); + const AudioGraph &audioGraph); /// @brief Converts HostGraph to adjacency list for equality comparison. static std::vector> convertHostGraphToAdjacencyList( - const HostGraph &hostGraph); + const HostGraph &hostGraph); private: - static HostGraph makeFromAdjacencyList( - const std::vector> &adjacencyList); + static HostGraph makeFromAdjacencyList(const std::vector> &adjacencyList); - static AudioGraph createAudioGraphFromHostGraph(const HostGraph &hostGraph); + static AudioGraph createAudioGraphFromHostGraph(const HostGraph &hostGraph); }; } // namespace audioapi::utils::graph diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h index ced8ce26a..9d628e986 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.h @@ -19,7 +19,7 @@ class AudioContext; class IOSAudioPlayer { public: IOSAudioPlayer( - const std::function, int)> &renderAudio, + const std::function &renderAudio, float sampleRate, int channelCount); ~IOSAudioPlayer(); @@ -41,7 +41,7 @@ class IOSAudioPlayer { std::shared_ptr audioBuffer_; NativeAudioPlayer *audioPlayer_; - std::function, int)> renderAudio_; + std::function renderAudio_; int channelCount_; std::atomic isRunning_; /// Set from main thread on start/resume; consumed on audio thread to drop stale pending audio. diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm index 42e9aa2de..7c0f5d801 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioPlayer.mm @@ -11,7 +11,7 @@ namespace audioapi { IOSAudioPlayer::IOSAudioPlayer( - const std::function, int)> &renderAudio, + const std::function &renderAudio, float sampleRate, int channelCount) : audioBuffer_(nullptr), @@ -84,7 +84,7 @@ continue; } - renderAudio_(audioBuffer_, RENDER_QUANTUM_SIZE); + renderAudio_(audioBuffer_.get(), RENDER_QUANTUM_SIZE); // normal rendering - take RENDER_QUANTUM_SIZE frames from the graph and copy to output const int stillNeed = numFrames - outPos; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h index 57a11f8d3..0d6631518 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.h @@ -10,6 +10,7 @@ typedef struct objc_object NativeAudioRecorder; #endif // __OBJC__ #include +#include #include #include @@ -36,7 +37,7 @@ class IOSAudioRecorder : public AudioRecorder { std::shared_ptr properties) override; void disableFileOutput() override; - void connect(const std::shared_ptr &node) override; + void connect(const std::shared_ptr &node) override; void disconnect() override; void pause() override; diff --git a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm index 6001eef79..1007b489a 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/core/IOSAudioRecorder.mm @@ -3,6 +3,7 @@ #import #import +#include #include #include @@ -122,10 +123,10 @@ static void cleanupStartedRecorder( if (isConnected()) { if (auto lock = Locker::tryLock(adapterNodeMutex_)) { - for (size_t channel = 0; channel < adapterNode_->getChannelCount(); ++channel) { - auto data = (float *)inputBuffer->mBuffers[channel].mData; - - adapterNode_->buff_[channel]->write(data, numFrames); + auto *adapterNode = static_cast(adapterNodeHandle_->audioNode.get()); + for (size_t channel = 0; channel < adapterNode->getChannelCount(); ++channel) { + auto *data = static_cast(inputBuffer->mBuffers[channel].mData); + adapterNode->buff_[channel]->write(data, numFrames); } } } @@ -148,7 +149,7 @@ static void cleanupStartedRecorder( isConnected_.store(false, std::memory_order_release); dataCallback_ = nullptr; fileWriter_ = nullptr; - adapterNode_ = nullptr; + adapterNodeHandle_ = nullptr; } [nativeRecorder_ cleanup]; @@ -269,11 +270,12 @@ static void cleanupStartedRecorder( callbackOutputConfigured_.store(true, std::memory_order_release); } - if (wantsConnection() && adapterNode_ != nullptr) { - adapterNode_->init( - maxInputBufferLength, - recorderFormatChannelCount(inputFormat), - recorderFormatSampleRate(inputFormat)); + if (wantsConnection() && adapterNodeHandle_ != nullptr) { + static_cast(adapterNodeHandle_->audioNode.get()) + ->init( + maxInputBufferLength, + recorderFormatChannelCount(inputFormat), + recorderFormatSampleRate(inputFormat)); connectedConfigured_.store(true, std::memory_order_release); } @@ -290,7 +292,7 @@ static void cleanupStartedRecorder( { std::shared_ptr fileWriter; std::shared_ptr dataCallback; - std::shared_ptr adapterNode; + std::shared_ptr adapterNodeHandle; std::vector outputPaths; std::string filePath; @@ -327,7 +329,7 @@ static void cleanupStartedRecorder( if (hadConnection) { connectedConfigured_.store(false, std::memory_order_release); - adapterNode = std::move(adapterNode_); + adapterNodeHandle = std::move(adapterNodeHandle_); } for (const auto &raw : recordingSegmentPaths_) { @@ -359,8 +361,8 @@ static void cleanupStartedRecorder( dataCallback->cleanup(); } - if (adapterNode != nullptr) { - adapterNode->adapterCleanup(); + if (adapterNodeHandle != nullptr) { + static_cast(adapterNodeHandle->audioNode.get())->adapterCleanup(); } return Result, double, double>, std::string>::Ok( @@ -463,10 +465,10 @@ static void cleanupStartedRecorder( /// If the recorder is already active, it will initialize the adapter node immediately. /// This method should be called from the JS thread only. /// @param node Shared pointer to the RecorderAdapterNode to connect. -void IOSAudioRecorder::connect(const std::shared_ptr &node) +void IOSAudioRecorder::connect(const std::shared_ptr &node) { std::scoped_lock lock(adapterNodeMutex_); - adapterNode_ = node; + adapterNodeHandle_ = node; isConnected_.store(true, std::memory_order_release); connectedConfigured_.store(false, std::memory_order_release); @@ -477,10 +479,11 @@ static void cleanupStartedRecorder( return; } - adapterNode_->init( - [nativeRecorder_ getResolvedBufferSize], - resolvedInputFormat.channelCount, - resolvedInputFormat.sampleRate); + static_cast(adapterNodeHandle_->audioNode.get()) + ->init( + [nativeRecorder_ getBufferSize], + resolvedInputFormat.channelCount, + resolvedInputFormat.sampleRate); connectedConfigured_.store(true, std::memory_order_release); } } @@ -490,18 +493,18 @@ static void cleanupStartedRecorder( /// This method should be called from the JS thread only. void IOSAudioRecorder::disconnect() { - std::shared_ptr adapterNode; + std::shared_ptr adapterNodeHandle; bool hadConnection = false; { std::scoped_lock lock(adapterNodeMutex_); hadConnection = isConnected(); connectedConfigured_.store(false, std::memory_order_release); isConnected_.store(false, std::memory_order_release); - adapterNode = std::move(adapterNode_); + adapterNodeHandle = std::move(adapterNodeHandle_); } - if (hadConnection && adapterNode != nullptr) { - adapterNode->adapterCleanup(); + if (hadConnection && adapterNodeHandle != nullptr) { + static_cast(adapterNodeHandle->audioNode.get())->adapterCleanup(); } } diff --git a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts index 8e34d638b..0fba03302 100644 --- a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts +++ b/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts @@ -14,8 +14,8 @@ export default class AudioBufferBaseSourceNode extends AudioScheduledSourceNode constructor(context: BaseAudioContext, node: IAudioBufferBaseSourceNode) { super(context, node); - this.detune = new AudioParam(node.detune, context); - this.playbackRate = new AudioParam(node.playbackRate, context); + this.detune = new AudioParam(node.detune, context, this); + this.playbackRate = new AudioParam(node.playbackRate, context, this); } public get onPositionChanged(): diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 203e2a12f..f3a60adc8 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -2,7 +2,7 @@ import { IAudioNode } from '../interfaces'; import AudioParam from './AudioParam'; import { ChannelCountMode, ChannelInterpretation } from '../types'; import type BaseAudioContext from './BaseAudioContext'; -import { InvalidAccessError } from '../errors'; +import { IndexSizeError } from '../errors'; export default class AudioNode { readonly context: BaseAudioContext; @@ -27,14 +27,24 @@ export default class AudioNode { public connect(destination: AudioParam): void; public connect(destination: AudioNode | AudioParam): AudioNode | void { if (this.context !== destination.context) { - throw new InvalidAccessError( + throw new IndexSizeError( 'Source and destination are from different BaseAudioContexts' ); } + if (this.numberOfOutputs === 0) { + throw new IndexSizeError('Faild to connect: AudioNode has no output'); + } + if (destination instanceof AudioParam) { this.node.connect(destination.audioParam); } else { + if (destination.numberOfInputs === 0) { + throw new IndexSizeError( + 'Failed to connect: destination AudioNode has no input' + ); + } + this.node.connect(destination.node); return destination; } diff --git a/packages/react-native-audio-api/src/core/AudioParam.ts b/packages/react-native-audio-api/src/core/AudioParam.ts index d836ea2df..334d4fe86 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -2,6 +2,7 @@ import { AutomationEventData, AutomationEventType } from '../types'; import { InvalidStateError, NotSupportedError, RangeError } from '../errors'; import { IAudioParam } from '../interfaces'; import type BaseAudioContext from './BaseAudioContext'; +import type AudioNode from './AudioNode'; export default class AudioParam { public readonly defaultValue: number; @@ -10,12 +11,15 @@ export default class AudioParam { constructor( public readonly audioParam: IAudioParam, - public readonly context: BaseAudioContext + public readonly context: BaseAudioContext, + public readonly owner: AudioNode ) { this.value = audioParam.value; this.defaultValue = audioParam.defaultValue; this.minValue = audioParam.minValue; this.maxValue = audioParam.maxValue; + this.context = context; + this.owner = owner; } public get value(): number { diff --git a/packages/react-native-audio-api/src/core/BiquadFilterNode.ts b/packages/react-native-audio-api/src/core/BiquadFilterNode.ts index f5183dac0..58c5abd77 100644 --- a/packages/react-native-audio-api/src/core/BiquadFilterNode.ts +++ b/packages/react-native-audio-api/src/core/BiquadFilterNode.ts @@ -16,10 +16,10 @@ export default class BiquadFilterNode extends AudioNode { options || {} ); super(context, biquadFilter); - this.frequency = new AudioParam(biquadFilter.frequency, context); - this.detune = new AudioParam(biquadFilter.detune, context); - this.Q = new AudioParam(biquadFilter.Q, context); - this.gain = new AudioParam(biquadFilter.gain, context); + this.frequency = new AudioParam(biquadFilter.frequency, context, this); + this.detune = new AudioParam(biquadFilter.detune, context, this); + this.Q = new AudioParam(biquadFilter.Q, context, this); + this.gain = new AudioParam(biquadFilter.gain, context, this); } public get type(): BiquadFilterType { diff --git a/packages/react-native-audio-api/src/core/ConstantSourceNode.ts b/packages/react-native-audio-api/src/core/ConstantSourceNode.ts index bffb12f23..661bf457e 100644 --- a/packages/react-native-audio-api/src/core/ConstantSourceNode.ts +++ b/packages/react-native-audio-api/src/core/ConstantSourceNode.ts @@ -12,6 +12,6 @@ export default class ConstantSourceNode extends AudioScheduledSourceNode { options || {} ); super(context, node); - this.offset = new AudioParam(node.offset, context); + this.offset = new AudioParam(node.offset, context, this); } } diff --git a/packages/react-native-audio-api/src/core/DelayNode.ts b/packages/react-native-audio-api/src/core/DelayNode.ts index b03396425..d09e72b17 100644 --- a/packages/react-native-audio-api/src/core/DelayNode.ts +++ b/packages/react-native-audio-api/src/core/DelayNode.ts @@ -9,6 +9,6 @@ export default class DelayNode extends AudioNode { constructor(context: BaseAudioContext, options?: DelayOptions) { const delay = context.context.createDelay(options || {}); super(context, delay); - this.delayTime = new AudioParam(delay.delayTime, context); + this.delayTime = new AudioParam(delay.delayTime, context, this); } } diff --git a/packages/react-native-audio-api/src/core/GainNode.ts b/packages/react-native-audio-api/src/core/GainNode.ts index 48f7a1c88..d6eba2112 100644 --- a/packages/react-native-audio-api/src/core/GainNode.ts +++ b/packages/react-native-audio-api/src/core/GainNode.ts @@ -10,6 +10,6 @@ export default class GainNode extends AudioNode { constructor(context: BaseAudioContext, options?: GainOptions) { const gainNode: IGainNode = context.context.createGain(options || {}); super(context, gainNode); - this.gain = new AudioParam(gainNode.gain, context); + this.gain = new AudioParam(gainNode.gain, context, this); } } diff --git a/packages/react-native-audio-api/src/core/OscillatorNode.ts b/packages/react-native-audio-api/src/core/OscillatorNode.ts index 014c9e018..20edce07d 100644 --- a/packages/react-native-audio-api/src/core/OscillatorNode.ts +++ b/packages/react-native-audio-api/src/core/OscillatorNode.ts @@ -17,8 +17,8 @@ export default class OscillatorNode extends AudioScheduledSourceNode { const node = context.context.createOscillator(options || {}); super(context, node); - this.frequency = new AudioParam(node.frequency, context); - this.detune = new AudioParam(node.detune, context); + this.frequency = new AudioParam(node.frequency, context, this); + this.detune = new AudioParam(node.detune, context, this); this.type = node.type; } diff --git a/packages/react-native-audio-api/src/core/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/StereoPannerNode.ts index 9e4a19e11..466572257 100644 --- a/packages/react-native-audio-api/src/core/StereoPannerNode.ts +++ b/packages/react-native-audio-api/src/core/StereoPannerNode.ts @@ -12,6 +12,6 @@ export default class StereoPannerNode extends AudioNode { options || {} ); super(context, pan); - this.pan = new AudioParam(pan.pan, context); + this.pan = new AudioParam(pan.pan, context, this); } } diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index cf9df9837..20f1c5401 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -129,8 +129,8 @@ export interface IAudioNode { readonly channelCountMode: ChannelCountMode; readonly channelInterpretation: ChannelInterpretation; - connect: (destination: IAudioNode | IAudioParam) => void; - disconnect: (destination?: IAudioNode | IAudioParam) => void; + connect(destination: IAudioNode | IAudioParam): void; + disconnect(destination?: IAudioNode | IAudioParam): void; } export interface IDelayNode extends IAudioNode { diff --git a/packages/react-native-audio-api/src/web-core/AudioParam.tsx b/packages/react-native-audio-api/src/web-core/AudioParam.tsx index 341f59e55..306bb7bcc 100644 --- a/packages/react-native-audio-api/src/web-core/AudioParam.tsx +++ b/packages/react-native-audio-api/src/web-core/AudioParam.tsx @@ -5,9 +5,8 @@ export default class AudioParam { readonly defaultValue: number; readonly minValue: number; readonly maxValue: number; - readonly context: BaseAudioContext; - readonly param: globalThis.AudioParam; + readonly context: BaseAudioContext; constructor(param: globalThis.AudioParam, context: BaseAudioContext) { this.param = param;