From aff8dacc2d98dd441c4e8546a152222031e9372a Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Fri, 10 Oct 2025 08:33:53 +0200 Subject: [PATCH 01/38] feat: prepare headers for indexed connect --- .../common/cpp/audioapi/core/AudioNode.cpp | 438 +++++++++--------- .../common/cpp/audioapi/core/AudioNode.h | 106 +++-- .../audioapi/core/utils/AudioNodeManager.cpp | 26 +- .../audioapi/core/utils/AudioNodeManager.h | 8 +- .../audioapi/core/utils/NodeConnections.cpp | 0 .../cpp/audioapi/core/utils/NodeConnections.h | 102 ++++ 6 files changed, 412 insertions(+), 268 deletions(-) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h 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 dcc9d34a3..1f7b70b76 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,15 +1,24 @@ -#include +#include "AudioNode.h" #include #include #include -#include #include +#include +#include "NodeConnections.h" +// mostly wrong, left it as a template namespace audioapi { AudioNode::AudioNode(BaseAudioContext *context) : context_(context) { - audioBus_ = std::make_shared( - RENDER_QUANTUM_SIZE, channelCount_, context->getSampleRate()); + m_connections = std::make_unique(this, context); + isInitialized_ = true; + + // Initialize output buses for a standard node + m_outputBuses.resize(numberOfOutputs_); + for (unsigned int i = 0; i < numberOfOutputs_; ++i) { + m_outputBuses[i] = std::make_shared( + RENDER_QUANTUM_SIZE, channelCount_, context->getSampleRate()); + } } AudioNode::~AudioNode() { @@ -18,300 +27,283 @@ AudioNode::~AudioNode() { } } -int AudioNode::getNumberOfInputs() const { - return numberOfInputs_; -} - -int AudioNode::getNumberOfOutputs() const { - return numberOfOutputs_; -} - -int AudioNode::getChannelCount() const { - return channelCount_; -} - -std::string AudioNode::getChannelCountMode() const { - return AudioNode::toString(channelCountMode_); -} - -std::string AudioNode::getChannelInterpretation() const { - return AudioNode::toString(channelInterpretation_); -} +// --- Public API --- -void AudioNode::connect(const std::shared_ptr &node) { +void AudioNode::connect( + const std::shared_ptr &destination, + unsigned int outputIndex, + unsigned int inputIndex) { context_->getNodeManager()->addPendingNodeConnection( - shared_from_this(), node, AudioNodeManager::ConnectionType::CONNECT); + shared_from_this(), + destination, + outputIndex, + inputIndex, + AudioNodeManager::ConnectionType::CONNECT, ); } -void AudioNode::connect(const std::shared_ptr ¶m) { +void AudioNode::connect( + const std::shared_ptr ¶m, + unsigned int outputIndex) { context_->getNodeManager()->addPendingParamConnection( - shared_from_this(), param, AudioNodeManager::ConnectionType::CONNECT); + shared_from_this(), + param, + outputIndex, + AudioNodeManager::ConnectionType::CONNECT); } void AudioNode::disconnect() { + m_connections->disconnectAll(); + // context_->getNodeManager()->addPendingNodeConnection( + // shared_from_this(), + // nullptr, + // 0, + // 0, + // AudioNodeManager::ConnectionType::DISCONNECT_ALL); +} + +// disconnect a single output from any other nodes or params +void AudioNode::disconnect(unsigned int outputIndex) { + if (outputIndex >= numberOfOutputs_) + throw std::out_of_range("Output index out of bounds."); context_->getNodeManager()->addPendingNodeConnection( shared_from_this(), nullptr, - AudioNodeManager::ConnectionType::DISCONNECT_ALL); + outputIndex, + 0, + AudioNodeManager::ConnectionType::DISCONNECT); } -void AudioNode::disconnect(const std::shared_ptr &node) { +// disconnect all connections to a specific node param / node connections +void AudioNode::disconnect(const std::shared_ptr &destination) { + if (!destination) + return; context_->getNodeManager()->addPendingNodeConnection( - shared_from_this(), node, AudioNodeManager::ConnectionType::DISCONNECT); -} - -void AudioNode::disconnect(const std::shared_ptr ¶m) { - context_->getNodeManager()->addPendingParamConnection( - shared_from_this(), param, AudioNodeManager::ConnectionType::DISCONNECT); + shared_from_this(), + destination, + AudioNodeManager::ConnectionType::DISCONNECT, + ALL_INDICES, + ALL_INDICES); } -bool AudioNode::isEnabled() const { - return isEnabled_; +void AudioNode::disconnect( + const std::shared_ptr &destination, + unsigned int outputIndex) { + if (!destination) + return; + if (outputIndex >= numberOfOutputs_) + throw std::out_of_range("Output index out of bounds."); + context_->getNodeManager()->addPendingNodeConnection( + shared_from_this(), + destination, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex, + ALL_INDICES); } -void AudioNode::enable() { - if (isEnabled()) { +void AudioNode::disconnect( + const std::shared_ptr &destination, + unsigned int outputIndex, + unsigned int inputIndex) { + if (!destination) return; - } - - isEnabled_ = true; - - for (auto it = outputNodes_.begin(), end = outputNodes_.end(); it != end; - ++it) { - it->get()->onInputEnabled(); - } + if (outputIndex >= numberOfOutputs_) + throw std::out_of_range("Output index out of bounds."); + if (inputIndex >= destination->getNumberOfInputs()) + throw std::out_of_range("Input index out of bounds."); + context_->getNodeManager()->addPendingNodeConnection( + shared_from_this(), + destination, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex, + inputIndex); } -void AudioNode::disable() { - if (!isEnabled()) { +void AudioNode::disconnect(const std::shared_ptr ¶m) { + if (!param) return; - } - - isEnabled_ = false; - - for (auto it = outputNodes_.begin(), end = outputNodes_.end(); it != end; - ++it) { - it->get()->onInputDisabled(); - } + // This would require a more detailed event or a new ConnectionType + // For now, we can simplify and require an output index. + // disconnect(param, 0); } -std::string AudioNode::toString(ChannelCountMode mode) { - switch (mode) { - case ChannelCountMode::MAX: - return "max"; - case ChannelCountMode::CLAMPED_MAX: - return "clamped-max"; - case ChannelCountMode::EXPLICIT: - return "explicit"; - default: - throw std::invalid_argument("Unknown channel count mode"); - } +void AudioNode::disconnect( + const std::shared_ptr ¶m, + unsigned int outputIndex) { + if (!param) + return; + if (outputIndex >= numberOfOutputs_) + throw std::out_of_range("Output index out of bounds."); + context_->getNodeManager()->addPendingParamConnection( + shared_from_this(), + param, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex); } -std::string AudioNode::toString(ChannelInterpretation interpretation) { - switch (interpretation) { - case ChannelInterpretation::SPEAKERS: - return "speakers"; - case ChannelInterpretation::DISCRETE: - return "discrete"; - default: - throw std::invalid_argument("Unknown channel interpretation"); - } -} +// --- Processing --- std::shared_ptr AudioNode::processAudio( - const std::shared_ptr &outputBus, int framesToProcess, bool checkIsAlreadyProcessed) { - if (!isInitialized_) { - return outputBus; - } - - if (checkIsAlreadyProcessed && isAlreadyProcessed()) { - return audioBus_; - } - - // Process inputs and return the bus with the most channels. - auto processingBus = - processInputs(outputBus, framesToProcess, checkIsAlreadyProcessed); + if (!isInitialized_) + return getOutputBus(0); + if (checkIsAlreadyProcessed && isAlreadyProcessed()) + return getOutputBus(0); - // Apply channel count mode. - processingBus = applyChannelCountMode(processingBus); + const auto &processedInputs = + m_connections->processAllInputs(framesToProcess, checkIsAlreadyProcessed); + processNode(processedInputs, framesToProcess); - // Mix all input buses into the processing bus. - mixInputsBuses(processingBus); - - assert(processingBus != nullptr); - - // Finally, process the node itself. - return processNode(processingBus, framesToProcess); - ; + return getOutputBus(0); } -bool AudioNode::isAlreadyProcessed() { - assert(context_ != nullptr); - - std::size_t currentSampleFrame = context_->getCurrentSampleFrame(); - - // check if the node has already been processed for this rendering quantum - if (currentSampleFrame == lastRenderedFrame_) { - return true; +std::shared_ptr AudioNode::getOutputBus(unsigned int outputIndex) { + if (outputIndex < m_outputBuses.size()) { + return m_outputBuses[outputIndex]; } - - // Update the last rendered frame before processing node and its inputs. - lastRenderedFrame_ = currentSampleFrame; - - return false; + return nullptr; } -std::shared_ptr AudioNode::processInputs( - const std::shared_ptr &outputBus, - int framesToProcess, - bool checkIsAlreadyProcessed) { - auto processingBus = audioBus_; - processingBus->zero(); - - int maxNumberOfChannels = 0; - for (auto it = inputNodes_.begin(), end = inputNodes_.end(); it != end; - ++it) { - auto inputNode = *it; - assert(inputNode != nullptr); - - if (!inputNode->isEnabled()) { - continue; - } - - auto inputBus = inputNode->processAudio( - outputBus, framesToProcess, checkIsAlreadyProcessed); - inputBuses_.push_back(inputBus); - - if (maxNumberOfChannels < inputBus->getNumberOfChannels()) { - maxNumberOfChannels = inputBus->getNumberOfChannels(); - processingBus = inputBus; - } - } +// --- Getters & State --- +int AudioNode::getNumberOfInputs() const { + return numberOfInputs_; +} +int AudioNode::getNumberOfOutputs() const { + return numberOfOutputs_; +} +int AudioNode::getChannelCount() const { + return channelCount_; +} +bool AudioNode::isEnabled() const { + return isEnabled_; +} +std::string AudioNode::getChannelCountMode() const { + return "max"; +} // Placeholder +std::string AudioNode::getChannelInterpretation() const { + return "speakers"; +} // Placeholder - return processingBus; +void AudioNode::enable() { + if (isEnabled_) + return; + isEnabled_ = true; + // This logic now needs to iterate through m_connections's outputs + // m_connections->propagateEnable(); } -std::shared_ptr AudioNode::applyChannelCountMode( - const std::shared_ptr &processingBus) { - // If the channelCountMode is EXPLICIT, the node should output the number of - // channels specified by the channelCount. - if (channelCountMode_ == ChannelCountMode::EXPLICIT) { - return audioBus_; - } +void AudioNode::disable() { + if (!isEnabled_) + return; + isEnabled_ = false; + // m_connections->propagateDisable(); +} - // If the channelCountMode is CLAMPED_MAX, the node should output the maximum - // number of channels clamped to channelCount. - if (channelCountMode_ == ChannelCountMode::CLAMPED_MAX && - processingBus->getNumberOfChannels() >= channelCount_) { - return audioBus_; - } +// --- Handshake Methods --- - return processingBus; +void AudioNode::connectNode( + const std::shared_ptr &destination, + unsigned int outputIndex, + unsigned int inputIndex) { + m_connections->connectNode(destination, outputIndex, inputIndex); } -void AudioNode::mixInputsBuses(const std::shared_ptr &processingBus) { - assert(processingBus != nullptr); - - for (auto it = inputBuses_.begin(), end = inputBuses_.end(); it != end; - ++it) { - processingBus->sum(it->get(), channelInterpretation_); +void AudioNode::onInputConnected( + AudioNode *source, + unsigned int outputIndexFromSource, + unsigned int inputIndex) { + m_connections->onInputConnected(source, outputIndexFromSource, inputIndex); + if (source->isEnabled()) { + onInputEnabled(); } +} - inputBuses_.clear(); +void AudioNode::disconnectAll() { + m_connections->disconnectAll(); } -void AudioNode::connectNode(const std::shared_ptr &node) { - auto position = outputNodes_.find(node); +void AudioNode::disconnectNode( + const std::shared_ptr &destination, + unsigned int outputIndex, + unsigned int inputIndex) { + m_connections->disconnectNode(destination, outputIndex, inputIndex); +} - if (position == outputNodes_.end()) { - outputNodes_.insert(node); - node->onInputConnected(this); +void AudioNode::onInputDisconnected( + AudioNode *source, + unsigned int outputIndexFromSource, + unsigned int inputIndex) { + m_connections->onInputDisconnected(source, outputIndexFromSource, inputIndex); + if (source->isEnabled()) { + onInputDisabled(); } } -void AudioNode::connectParam(const std::shared_ptr ¶m) { - auto position = outputParams_.find(param); +void AudioNode::connectParam( + const std::shared_ptr ¶m, + unsigned int outputIndex) { + m_connections->connectParam(param, outputIndex); +} - if (position == outputParams_.end()) { - outputParams_.insert(param); - param->addInputNode(this); - } +void AudioNode::disconnectParam( + const std::shared_ptr ¶m, + unsigned int outputIndex) { + m_connections->disconnectParam(param, outputIndex); } -void AudioNode::disconnectNode(const std::shared_ptr &node) { - auto position = outputNodes_.find(node); +// --- Processing & State --- + +void AudioNode::processNode( + const std::vector> &inputBuses, + int framesToProcess) { + // Default bridge implementation for backward compatibility + auto firstInputBus = inputBuses.empty() ? nullptr : inputBuses[0]; + auto outputBus = getOutputBus(0); - if (position != outputNodes_.end()) { - node->onInputDisconnected(this); - outputNodes_.erase(node); + if (firstInputBus && outputBus) { + if (outputBus.get() != firstInputBus.get()) { + outputBus->copy(firstInputBus.get()); + } + } else if (outputBus) { + outputBus->zero(); } -} -void AudioNode::disconnectParam(const std::shared_ptr ¶m) { - auto position = outputParams_.find(param); + // Call the old version + processNode(outputBus, framesToProcess); +} - if (position != outputParams_.end()) { - param->removeInputNode(this); - outputParams_.erase(param); - } +void AudioNode::processNode( + std::shared_ptr processingBus, + int framesToProcess) { + // Base implementation does nothing. To be overridden by simple nodes. } void AudioNode::onInputEnabled() { - numberOfEnabledInputNodes_ += 1; - - if (!isEnabled()) { + numberOfEnabledInputNodes_++; + if (!isEnabled_) { enable(); } } void AudioNode::onInputDisabled() { - numberOfEnabledInputNodes_ -= 1; - - if (isEnabled() && numberOfEnabledInputNodes_ == 0) { + numberOfEnabledInputNodes_--; + if (isEnabled_ && numberOfEnabledInputNodes_ == 0) { disable(); } } -void AudioNode::onInputConnected(AudioNode *node) { - if (!isInitialized_) { - return; - } - - inputNodes_.insert(node); - - if (node->isEnabled()) { - onInputEnabled(); - } -} - -void AudioNode::onInputDisconnected(AudioNode *node) { - if (!isInitialized_) { - return; - } - - if (node->isEnabled()) { - onInputDisabled(); - } - - auto position = inputNodes_.find(node); - - if (position != inputNodes_.end()) { - inputNodes_.erase(position); +bool AudioNode::isAlreadyProcessed() { + std::size_t currentSampleFrame = context_->getCurrentSampleFrame(); + if (currentSampleFrame == lastRenderedFrame_) { + return true; } + lastRenderedFrame_ = currentSampleFrame; + return false; } void AudioNode::cleanup() { isInitialized_ = false; - - for (auto it = outputNodes_.begin(), end = outputNodes_.end(); it != end; - ++it) { - it->get()->onInputDisconnected(this); - } - - outputNodes_.clear(); + m_connections->disconnectAll(); } -} // namespace audioapi +} // namespace audioapi \ No newline at end of file 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 098a62896..134c30ba9 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 @@ -1,90 +1,116 @@ + #pragma once #include #include #include +#include +#include +#include #include #include -#include -#include #include -#include namespace audioapi { class AudioBus; class BaseAudioContext; class AudioParam; +class NodeConnections; class AudioNode : public std::enable_shared_from_this { public: + // allocates correct amount of space for m_outputBuses explicit AudioNode(BaseAudioContext *context); virtual ~AudioNode(); + // --- Public Connection API --- + // just delegates it to the NodeManager + void + connect(const std::shared_ptr &destination, unsigned int outputIndex = 0, unsigned int inputIndex = 0); + // just delegates it to the NodeManager + void connect(const std::shared_ptr ¶m, unsigned int outputIndex = 0); + + // Overloaded Disconnect Methode - these just call equivalent methods in //NodeConnection + void disconnect(); + void disconnect(unsigned int outputIndex); + void disconnect(const std::shared_ptr &destination); + void disconnect(const std::shared_ptr &destination, unsigned int outputIndex); + void disconnect(const std::shared_ptr &destination, unsigned int outputIndex, unsigned int inputIndex); + void disconnect(const std::shared_ptr ¶m); + void disconnect(const std::shared_ptr ¶m, unsigned int outputIndex); + + // calls NodeConnections::processAllInputs + virtual std::shared_ptr processAudio(int framesToProcess, bool checkIsAlreadyProcessed); + + std::shared_ptr getOutputBus(unsigned int outputIndex); + + // --- Getters & State --- int getNumberOfInputs() const; int getNumberOfOutputs() const; int getChannelCount() const; + bool isEnabled() const; std::string getChannelCountMode() const; std::string getChannelInterpretation() 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 &outputBus, int framesToProcess, bool checkIsAlreadyProcessed); - - bool isEnabled() const; + BaseAudioContext *getContext() const { + return context_; + } + ChannelCountMode getChannelCountModeEnum() const { + return channelCountMode_; + } + ChannelInterpretation getChannelInterpretationEnum() const { + return channelInterpretation_; + } + // Sets the node's state to enabled and propagates this change to its outputs. void enable(); + // Sets the node's state to disabled and propagates this change to its outputs. virtual void disable(); protected: friend class AudioNodeManager; - friend class AudioDestinationNode; + friend class NodeConnections; - BaseAudioContext *context_; - std::shared_ptr audioBus_; + // these are forwarders to NodeConnections + void connectNode(const std::shared_ptr &destination, unsigned int outputIndex, unsigned int inputIndex); + void onInputConnected(AudioNode *source, unsigned int outputIndexFromSource, unsigned int inputIndex); + + void disconnectAll(); + void disconnectNode(const std::shared_ptr &destination, unsigned int outputIndex, unsigned int inputIndex); + void onInputDisconnected(AudioNode *source, unsigned int outputIndexFromSource, unsigned int inputIndex); + + void connectParam(const std::shared_ptr ¶m, unsigned int outputIndex); + void disconnectParam(const std::shared_ptr ¶m, unsigned int outputIndex); + // inputBuses is a vector with size == numberOfInputs_ and contains a + // mixed AudioBus for each input index (may be a silent bus). Implementors + // must write into this node's output buses (accessible via getOutputBus()). + virtual void processNode(const std::vector> &inputBuses, int framesToProcess); + + // Called by an upstream node when it becomes enabled. Increments the active input counter. + void onInputEnabled(); + // Called by an upstream node when it becomes disabled. Decrements the active input counter. + void onInputDisabled(); + + BaseAudioContext *context_; int numberOfInputs_ = 1; int numberOfOutputs_ = 1; int channelCount_ = 2; ChannelCountMode channelCountMode_ = ChannelCountMode::MAX; - ChannelInterpretation channelInterpretation_ = - ChannelInterpretation::SPEAKERS; + ChannelInterpretation channelInterpretation_ = ChannelInterpretation::SPEAKERS; - std::unordered_set inputNodes_ = {}; - std::unordered_set> outputNodes_ = {}; - std::unordered_set> outputParams_ = {}; + std::vector> m_outputBuses; + std::unique_ptr m_connections; int numberOfEnabledInputNodes_ = 0; bool isInitialized_ = false; bool isEnabled_ = true; - std::size_t lastRenderedFrame_{SIZE_MAX}; private: - std::vector> inputBuses_ = {}; - + bool isAlreadyProcessed(); static std::string toString(ChannelCountMode mode); static std::string toString(ChannelInterpretation interpretation); - - virtual std::shared_ptr processNode(const std::shared_ptr&, int) = 0; - - bool isAlreadyProcessed(); - std::shared_ptr processInputs(const std::shared_ptr& outputBus, int framesToProcess, bool checkIsAlreadyProcessed); - std::shared_ptr applyChannelCountMode(const std::shared_ptr &processingBus); - void mixInputsBuses(const std::shared_ptr& processingBus); - - 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(); - void onInputDisabled(); - void onInputConnected(AudioNode *node); - void onInputDisconnected(AudioNode *node); - void cleanup(); }; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp index ec1c0c47f..05ebc74de 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp @@ -87,12 +87,16 @@ AudioNodeManager::~AudioNodeManager() { void AudioNodeManager::addPendingNodeConnection( const std::shared_ptr &from, const std::shared_ptr &to, + unsigned int outputIndex, + unsigned int inputIndex, ConnectionType type) { auto event = std::make_unique(); event->type = type; event->payloadType = EventPayloadType::NODES; event->payload.nodes.from = from; event->payload.nodes.to = to; + event->payload.nodes.outputIndex = outputIndex; + event->payload.nodes.inputIndex = inputIndex; sender_.send(std::move(event)); } @@ -100,12 +104,14 @@ void AudioNodeManager::addPendingNodeConnection( void AudioNodeManager::addPendingParamConnection( const std::shared_ptr &from, const std::shared_ptr &to, + unsigned int outputIndex, ConnectionType type) { auto event = std::make_unique(); event->type = type; event->payloadType = EventPayloadType::PARAMS; event->payload.params.from = from; event->payload.params.to = to; + event->payload.params.outputIndex = outputIndex; sender_.send(std::move(event)); } @@ -168,9 +174,13 @@ void AudioNodeManager::settlePendingConnections() { void AudioNodeManager::handleConnectEvent(std::unique_ptr event) { if (event->payloadType == EventPayloadType::NODES) { - event->payload.nodes.from->connectNode(event->payload.nodes.to); + event->payload.nodes.from->connectNode( + event->payload.nodes.to, + event->payload.nodes.outputIndex, + event->payload.nodes.inputIndex); } else if (event->payloadType == EventPayloadType::PARAMS) { - event->payload.params.from->connectParam(event->payload.params.to); + event->payload.params.from->connectParam( + event->payload.params.to, event->payload.params.outputIndex); } else { assert(false && "Invalid payload type for connect event"); } @@ -178,14 +188,21 @@ void AudioNodeManager::handleConnectEvent(std::unique_ptr event) { void AudioNodeManager::handleDisconnectEvent(std::unique_ptr event) { if (event->payloadType == EventPayloadType::NODES) { - event->payload.nodes.from->disconnectNode(event->payload.nodes.to); + event->payload.nodes.from->disconnectNode( + event->payload.nodes.to, + event->payload.nodes.outputIndex, + event->payload.nodes.inputIndex); } else if (event->payloadType == EventPayloadType::PARAMS) { - event->payload.params.from->disconnectParam(event->payload.params.to); + event->payload.params.from->disconnectParam( + event->payload.params.to, event->payload.params.outputIndex); } else { assert(false && "Invalid payload type for disconnect event"); } } +// wont be needed, NodeDestination::disconnectAll() does the job by dispatching +// multiple disconnect events +/* void AudioNodeManager::handleDisconnectAllEvent(std::unique_ptr event) { assert(event->payloadType == EventPayloadType::NODES); for (auto it = event->payload.nodes.from->outputNodes_.begin(); @@ -195,6 +212,7 @@ void AudioNodeManager::handleDisconnectAllEvent(std::unique_ptr event) { it = next; } } +*/ void AudioNodeManager::handleAddToDeconstructionEvent( std::unique_ptr event) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h index a92613214..9bdec6323 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h @@ -29,10 +29,13 @@ class AudioNodeManager { struct { std::shared_ptr from; std::shared_ptr to; + unsigned int outputIndex; + unsigned int inputIndex; } nodes; struct { std::shared_ptr from; std::shared_ptr to; + unsigned int outputIndex; } params; std::shared_ptr sourceNode; std::shared_ptr audioParam; @@ -68,6 +71,8 @@ class AudioNodeManager { void addPendingNodeConnection( const std::shared_ptr &from, const std::shared_ptr &to, + unsigned int outputIndex = 0, + unsigned int inputIndex = 0, ConnectionType type); /// @brief Adds a pending connection between an audio node and an audio parameter. @@ -78,7 +83,8 @@ class AudioNodeManager { void addPendingParamConnection( const std::shared_ptr &from, const std::shared_ptr &to, - ConnectionType type); + unsigned int outputIndex = 0, + ConnectionType type); /// @brief Adds a processing node to the manager. /// @param node The processing node to add. diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h new file mode 100644 index 000000000..2ffc80cbe --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h @@ -0,0 +1,102 @@ + +#pragma once + +#include +#include +#include + +namespace audioapi { + +class AudioNode; +class AudioBus; +class AudioParam; +class BaseAudioContext; + +// Describes an INCOMING connection from a source node's output port. +struct InputConnection { + AudioNode *sourceNode; + unsigned int outputIndexFromSource; +}; + +// Describes an OUTGOING connection to a destination node's input port. +struct OutputConnection { + std::shared_ptr destinationNode; + unsigned int inputIndexAtDestination; +}; + +// Manages ALL connectivity (in and out) and input processing logic for an AudioNode. +class NodeConnections { + public: + explicit NodeConnections(AudioNode *owner, BaseAudioContext *context); + + // --- Public Disconnection Logic (called by AudioNode's public API) --- + // disconnect all + void disconnect(); + // disconnect everything (params and nodes) from an output index + void disconnect(unsigned int outputIndex); + // disconnects all outputs of the node which go to a specific desitination //Audio Node + void disconnect(const std::shared_ptr &node); + // disconnects a specific output of the node from + // any and all inputs of some destination node + void disconnect(const std::shared_ptr &node, unsigned int outputIndex); + /* +Disconnects a specific output of the node from a specific input of some destination node + */ + void disconnect(const std::shared_ptr &node, unsigned int output, unsigned int input); + /* + Disconnects all outputs of the node that go to a specific destination node. The contribution of this node to the + computed parameter value goes to 0 when this operation takes effect. The intrinsic parameter value is not affected + by this operation. + */ + void disconnect(const std::shared_ptr ¶m); + /* + Disconnects a specific output of the AudioNode from a specific destination AudioParam. The contribution of this + AudioNode to the computed parameter value goes to 0 when this operation takes effect. The intrinsic parameter + value is not affected by this operation. + */ + void disconnect(const std::shared_ptr ¶m, unsigned int output); + + // --- Input Processing --- + const std::vector> &processAllInputs(int framesToProcess, bool checkIsAlreadyProcessed); + + // --- Handshake Methods (called by AudioNode's protected methods) --- + // adds a node to the output nodes map + void connectNode(const std::shared_ptr &destination, unsigned int outputIndex, unsigned int inputIndex); + // inserts a node into the inputs map + void onInputConnected(AudioNode *sourceNode, unsigned int outputIndexFromSource, unsigned int inputIndex); + // deletes all nodes from the output map + void disconnectAll(); + // deletes the node from the output map + void disconnectNode(const std::shared_ptr &destination, unsigned int outputIndex, unsigned int inputIndex); + // deletes the node from the input map + void onInputDisconnected(AudioNode *sourceNode, unsigned int outputIndexFromSource, unsigned int inputIndex); + // adds param to the output param map + void connectParam(const std::shared_ptr ¶m, unsigned int outputIndex); + // deletes param from the output param map + void disconnectParam(const std::shared_ptr ¶m, unsigned int outputIndex); + // clears the output map and calls onInputDisconnected on nodes inside the map + void cleanup(); + // Iterates through all outputs and calls onInputEnabled() on connected nodes. + void propagateEnable(); + // Iterates through all outputs and calls onInputDisabled() on connected nodes. + void propagateDisable(); + + private: + AudioNode *m_owner; + BaseAudioContext *m_context; + + // --- Connection Maps --- + std::map> m_indexedInputs; + std::map> m_indexedOutputs; + std::map>> m_indexedOutputParams; + + // used for calculations inside processnputAtIndex + std::vector> m_processingInputBuses; + + // --- Internal Helpers --- + std::shared_ptr processInputAtIndex(unsigned int index, int framesToProcess, bool checkIsAlreadyProcessed); + std::shared_ptr applyChannelCountMode(const std::shared_ptr &processingBus); + void mixSourceBuses(const std::shared_ptr &processingBus); +}; + +} // namespace audioapi From fb20be1ed3f44521311f286ccc3e9b1005c355ca Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:28:31 +0200 Subject: [PATCH 02/38] feat: implement NodeConnections --- .../common/cpp/audioapi/core/AudioNode.cpp | 229 +++++---- .../common/cpp/audioapi/core/AudioNode.h | 11 +- .../common/cpp/audioapi/core/AudioParam.cpp | 9 +- .../audioapi/core/utils/AudioNodeManager.cpp | 16 +- .../audioapi/core/utils/AudioNodeManager.h | 9 +- .../audioapi/core/utils/NodeConnections.cpp | 470 ++++++++++++++++++ .../cpp/audioapi/core/utils/NodeConnections.h | 7 +- 7 files changed, 629 insertions(+), 122 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.cpp index 1f7b70b76..00cb8ba14 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 @@ -2,11 +2,10 @@ #include #include #include +#include #include #include -#include "NodeConnections.h" -// mostly wrong, left it as a template namespace audioapi { AudioNode::AudioNode(BaseAudioContext *context) : context_(context) { @@ -27,8 +26,6 @@ AudioNode::~AudioNode() { } } -// --- Public API --- - void AudioNode::connect( const std::shared_ptr &destination, unsigned int outputIndex, @@ -36,9 +33,9 @@ void AudioNode::connect( context_->getNodeManager()->addPendingNodeConnection( shared_from_this(), destination, + AudioNodeManager::ConnectionType::CONNECT, outputIndex, - inputIndex, - AudioNodeManager::ConnectionType::CONNECT, ); + inputIndex); } void AudioNode::connect( @@ -47,42 +44,27 @@ void AudioNode::connect( context_->getNodeManager()->addPendingParamConnection( shared_from_this(), param, - outputIndex, - AudioNodeManager::ConnectionType::CONNECT); + AudioNodeManager::ConnectionType::CONNECT, + outputIndex); } void AudioNode::disconnect() { - m_connections->disconnectAll(); - // context_->getNodeManager()->addPendingNodeConnection( - // shared_from_this(), - // nullptr, - // 0, - // 0, - // AudioNodeManager::ConnectionType::DISCONNECT_ALL); + m_connections->disconnect(); } // disconnect a single output from any other nodes or params void AudioNode::disconnect(unsigned int outputIndex) { + // ask if we want to do index checks here or just in the ts layer if (outputIndex >= numberOfOutputs_) throw std::out_of_range("Output index out of bounds."); - context_->getNodeManager()->addPendingNodeConnection( - shared_from_this(), - nullptr, - outputIndex, - 0, - AudioNodeManager::ConnectionType::DISCONNECT); + m_connections->disconnect(outputIndex); } // disconnect all connections to a specific node param / node connections void AudioNode::disconnect(const std::shared_ptr &destination) { if (!destination) return; - context_->getNodeManager()->addPendingNodeConnection( - shared_from_this(), - destination, - AudioNodeManager::ConnectionType::DISCONNECT, - ALL_INDICES, - ALL_INDICES); + m_connections->disconnect(destination); } void AudioNode::disconnect( @@ -92,12 +74,7 @@ void AudioNode::disconnect( return; if (outputIndex >= numberOfOutputs_) throw std::out_of_range("Output index out of bounds."); - context_->getNodeManager()->addPendingNodeConnection( - shared_from_this(), - destination, - AudioNodeManager::ConnectionType::DISCONNECT, - outputIndex, - ALL_INDICES); + m_connections->disconnect(destination, outputIndex); } void AudioNode::disconnect( @@ -110,20 +87,13 @@ void AudioNode::disconnect( throw std::out_of_range("Output index out of bounds."); if (inputIndex >= destination->getNumberOfInputs()) throw std::out_of_range("Input index out of bounds."); - context_->getNodeManager()->addPendingNodeConnection( - shared_from_this(), - destination, - AudioNodeManager::ConnectionType::DISCONNECT, - outputIndex, - inputIndex); + m_connections->disconnect(destination, outputIndex, inputIndex); } void AudioNode::disconnect(const std::shared_ptr ¶m) { if (!param) return; - // This would require a more detailed event or a new ConnectionType - // For now, we can simplify and require an output index. - // disconnect(param, 0); + m_connections->disconnect(param); } void AudioNode::disconnect( @@ -133,33 +103,51 @@ void AudioNode::disconnect( return; if (outputIndex >= numberOfOutputs_) throw std::out_of_range("Output index out of bounds."); - context_->getNodeManager()->addPendingParamConnection( - shared_from_this(), - param, - AudioNodeManager::ConnectionType::DISCONNECT, - outputIndex); + m_connections->disconnect(param, outputIndex); } -// --- Processing --- - std::shared_ptr AudioNode::processAudio( int framesToProcess, bool checkIsAlreadyProcessed) { - if (!isInitialized_) - return getOutputBus(0); - if (checkIsAlreadyProcessed && isAlreadyProcessed()) - return getOutputBus(0); + if (!isInitialized_ || framesToProcess <= 0) { + return nullptr; + } + + if (checkIsAlreadyProcessed && isAlreadyProcessed()) { + if (!m_outputBuses.empty()) { + return m_outputBuses[0]; + } + return nullptr; + } + + if (m_outputBuses.size() != static_cast(numberOfOutputs_)) { + m_outputBuses.resize(numberOfOutputs_); + } + for (int i = 0; i < numberOfOutputs_; ++i) { + if (!m_outputBuses[i]) { + m_outputBuses[i] = std::make_shared( + static_cast(framesToProcess), + channelCount_, + context_->getSampleRate()); + } else { + if (m_outputBuses[i]->getSize() != static_cast(framesToProcess) || + m_outputBuses[i]->getSampleRate() != context_->getSampleRate()) { + m_outputBuses[i] = std::make_shared( + static_cast(framesToProcess), + channelCount_, + context_->getSampleRate()); + } + m_outputBuses[i]->zero(); + } + } - const auto &processedInputs = + const auto &inputBuses = m_connections->processAllInputs(framesToProcess, checkIsAlreadyProcessed); - processNode(processedInputs, framesToProcess); - return getOutputBus(0); -} + processNode(inputBuses, framesToProcess); -std::shared_ptr AudioNode::getOutputBus(unsigned int outputIndex) { - if (outputIndex < m_outputBuses.size()) { - return m_outputBuses[outputIndex]; + if (!m_outputBuses.empty()) { + return m_outputBuses[0]; } return nullptr; } @@ -178,29 +166,50 @@ bool AudioNode::isEnabled() const { return isEnabled_; } std::string AudioNode::getChannelCountMode() const { - return "max"; -} // Placeholder + return AudioNode::toString(channelCountMode_); +} std::string AudioNode::getChannelInterpretation() const { - return "speakers"; -} // Placeholder + return AudioNode::toString(channelInterpretation_); +} + +std::string AudioNode::toString(ChannelCountMode mode) { + switch (mode) { + case ChannelCountMode::MAX: + return "max"; + case ChannelCountMode::CLAMPED_MAX: + return "clamped-max"; + case ChannelCountMode::EXPLICIT: + return "explicit"; + default: + throw std::invalid_argument("Unknown channel count mode"); + } +} + +std::string AudioNode::toString(ChannelInterpretation interpretation) { + switch (interpretation) { + case ChannelInterpretation::SPEAKERS: + return "speakers"; + case ChannelInterpretation::DISCRETE: + return "discrete"; + default: + throw std::invalid_argument("Unknown channel interpretation"); + } +} void AudioNode::enable() { if (isEnabled_) return; isEnabled_ = true; - // This logic now needs to iterate through m_connections's outputs - // m_connections->propagateEnable(); + m_connections->propagateEnable(); } void AudioNode::disable() { if (!isEnabled_) return; isEnabled_ = false; - // m_connections->propagateDisable(); + m_connections->propagateDisable(); } -// --- Handshake Methods --- - void AudioNode::connectNode( const std::shared_ptr &destination, unsigned int outputIndex, @@ -212,6 +221,10 @@ void AudioNode::onInputConnected( AudioNode *source, unsigned int outputIndexFromSource, unsigned int inputIndex) { + if (!isInitialized_) { + return; + } + m_connections->onInputConnected(source, outputIndexFromSource, inputIndex); if (source->isEnabled()) { onInputEnabled(); @@ -233,6 +246,10 @@ void AudioNode::onInputDisconnected( AudioNode *source, unsigned int outputIndexFromSource, unsigned int inputIndex) { + if (!isInitialized_) { + return; + } + m_connections->onInputDisconnected(source, outputIndexFromSource, inputIndex); if (source->isEnabled()) { onInputDisabled(); @@ -251,33 +268,6 @@ void AudioNode::disconnectParam( m_connections->disconnectParam(param, outputIndex); } -// --- Processing & State --- - -void AudioNode::processNode( - const std::vector> &inputBuses, - int framesToProcess) { - // Default bridge implementation for backward compatibility - auto firstInputBus = inputBuses.empty() ? nullptr : inputBuses[0]; - auto outputBus = getOutputBus(0); - - if (firstInputBus && outputBus) { - if (outputBus.get() != firstInputBus.get()) { - outputBus->copy(firstInputBus.get()); - } - } else if (outputBus) { - outputBus->zero(); - } - - // Call the old version - processNode(outputBus, framesToProcess); -} - -void AudioNode::processNode( - std::shared_ptr processingBus, - int framesToProcess) { - // Base implementation does nothing. To be overridden by simple nodes. -} - void AudioNode::onInputEnabled() { numberOfEnabledInputNodes_++; if (!isEnabled_) { @@ -303,7 +293,52 @@ bool AudioNode::isAlreadyProcessed() { void AudioNode::cleanup() { isInitialized_ = false; - m_connections->disconnectAll(); + m_connections->cleanup(); +} + +std::shared_ptr AudioNode::processNode( + const std::shared_ptr &processingBus, + int /*framesToProcess*/) { + if (!processingBus) { + return nullptr; + } + processingBus->zero(); + return processingBus; +} + +void AudioNode::processNode( + const std::vector> &inputBuses, + int framesToProcess) { + std::shared_ptr processingBus; + + if (inputBuses.empty() || !inputBuses[0]) { + processingBus = std::make_shared( + static_cast(framesToProcess), + channelCount_, + context_->getSampleRate()); + processingBus->zero(); + } else { + processingBus = inputBuses[0]; + } + + std::shared_ptr returnedBus = + processNode(processingBus, framesToProcess); + + if (!m_outputBuses.empty()) { + auto outBus = getOutputBus(0); + if (returnedBus && returnedBus != outBus) { + outBus->copy(returnedBus.get()); + } else if (!returnedBus) { + outBus->zero(); + } + } +} + +std::shared_ptr AudioNode::getOutputBus(unsigned int index) { + if (index >= m_outputBuses.size()) { + throw std::out_of_range("Output index out of bounds."); + } + return m_outputBuses[index]; } } // namespace audioapi \ No newline at end of file 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 134c30ba9..b17fdcce1 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,8 +4,8 @@ #include #include #include -#include +#include #include #include #include @@ -25,14 +25,13 @@ class AudioNode : public std::enable_shared_from_this { explicit AudioNode(BaseAudioContext *context); virtual ~AudioNode(); - // --- Public Connection API --- // just delegates it to the NodeManager void connect(const std::shared_ptr &destination, unsigned int outputIndex = 0, unsigned int inputIndex = 0); // just delegates it to the NodeManager void connect(const std::shared_ptr ¶m, unsigned int outputIndex = 0); - // Overloaded Disconnect Methode - these just call equivalent methods in //NodeConnection + // Overloaded Disconnect Methode - these just call equivalent methods in NodeConnection void disconnect(); void disconnect(unsigned int outputIndex); void disconnect(const std::shared_ptr &destination); @@ -46,7 +45,6 @@ class AudioNode : public std::enable_shared_from_this { std::shared_ptr getOutputBus(unsigned int outputIndex); - // --- Getters & State --- int getNumberOfInputs() const; int getNumberOfOutputs() const; int getChannelCount() const; @@ -82,9 +80,8 @@ class AudioNode : public std::enable_shared_from_this { void connectParam(const std::shared_ptr ¶m, unsigned int outputIndex); void disconnectParam(const std::shared_ptr ¶m, unsigned int outputIndex); - // inputBuses is a vector with size == numberOfInputs_ and contains a - // mixed AudioBus for each input index (may be a silent bus). Implementors - // must write into this node's output buses (accessible via getOutputBus()). + // created for backwards compatibility + virtual std::shared_ptr processNode(const std::shared_ptr &processingBus, int framesToProcess); virtual void processNode(const std::vector> &inputBuses, int framesToProcess); // Called by an upstream node when it becomes enabled. Increments the active input counter. 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 8db460f28..0e76aa1ad 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 @@ -322,9 +322,12 @@ void AudioParam::processInputs( continue; } - // Process this input node and store its output bus - auto inputBus = inputNode->processAudio( - outputBus, framesToProcess, checkIsAlreadyProcessed); + // probably wrong and needs to be adjusted to the indexed connect + + inputNode->processAudio(framesToProcess, checkIsAlreadyProcessed); + + auto inputBus = inputNode->getOutputBus(0); + inputBuses_.emplace_back(inputBus); } } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp index 05ebc74de..977adb154 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp @@ -87,9 +87,9 @@ AudioNodeManager::~AudioNodeManager() { void AudioNodeManager::addPendingNodeConnection( const std::shared_ptr &from, const std::shared_ptr &to, - unsigned int outputIndex, - unsigned int inputIndex, - ConnectionType type) { + ConnectionType type, + unsigned int outputIndex, + unsigned int inputIndex) { auto event = std::make_unique(); event->type = type; event->payloadType = EventPayloadType::NODES; @@ -104,8 +104,8 @@ void AudioNodeManager::addPendingNodeConnection( void AudioNodeManager::addPendingParamConnection( const std::shared_ptr &from, const std::shared_ptr &to, - unsigned int outputIndex, - ConnectionType type) { + ConnectionType type, + unsigned int outputIndex) { auto event = std::make_unique(); event->type = type; event->payloadType = EventPayloadType::PARAMS; @@ -162,9 +162,9 @@ void AudioNodeManager::settlePendingConnections() { case ConnectionType::DISCONNECT: handleDisconnectEvent(std::move(value)); break; - case ConnectionType::DISCONNECT_ALL: - handleDisconnectAllEvent(std::move(value)); - break; + // case ConnectionType::DISCONNECT_ALL: + // handleDisconnectAllEvent(std::move(value)); + // break; case ConnectionType::ADD: handleAddToDeconstructionEvent(std::move(value)); break; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h index 9bdec6323..fac2df40e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.h @@ -71,9 +71,9 @@ class AudioNodeManager { void addPendingNodeConnection( const std::shared_ptr &from, const std::shared_ptr &to, + ConnectionType type, unsigned int outputIndex = 0, - unsigned int inputIndex = 0, - ConnectionType type); + unsigned int inputIndex = 0); /// @brief Adds a pending connection between an audio node and an audio parameter. /// @param from The source audio node. @@ -83,8 +83,8 @@ class AudioNodeManager { void addPendingParamConnection( const std::shared_ptr &from, const std::shared_ptr &to, - unsigned int outputIndex = 0, - ConnectionType type); + ConnectionType type, + unsigned int outputIndex = 0 ); /// @brief Adds a processing node to the manager. /// @param node The processing node to add. @@ -127,7 +127,6 @@ class AudioNodeManager { void settlePendingConnections(); void handleConnectEvent(std::unique_ptr event); void handleDisconnectEvent(std::unique_ptr event); - void handleDisconnectAllEvent(std::unique_ptr event); void handleAddToDeconstructionEvent(std::unique_ptr event); template diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp index e69de29bb..19feeab5c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.cpp @@ -0,0 +1,470 @@ +#include "NodeConnections.h" +#include +#include +#include +#include +#include +#include +#include + +namespace audioapi { + +NodeConnections::NodeConnections(AudioNode *owner, BaseAudioContext *context) + : m_owner(owner), m_context(context) {} + +void NodeConnections::disconnect() { + // Disconnect from all nodes + for (std::map>::iterator it = + m_indexedOutputs.begin(); + it != m_indexedOutputs.end(); + ++it) { + unsigned int outputIndex = it->first; + std::vector &connections = it->second; + for (std::vector::iterator conn_it = connections.begin(); + conn_it != connections.end(); + ++conn_it) { + m_context->getNodeManager()->addPendingNodeConnection( + m_owner->shared_from_this(), + conn_it->destinationNode, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex, + conn_it->inputIndexAtDestination); + } + } + + // Disconnect from all params + for (std::map>>:: + iterator it = m_indexedOutputParams.begin(); + it != m_indexedOutputParams.end(); + ++it) { + unsigned int outputIndex = it->first; + std::vector> ¶ms = it->second; + for (std::vector>::iterator param_it = + params.begin(); + param_it != params.end(); + ++param_it) { + m_context->getNodeManager()->addPendingParamConnection( + m_owner->shared_from_this(), + *param_it, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex); + } + } +} + +void NodeConnections::disconnect(unsigned int outputIndex) { + // Disconnect all nodes from a specific output + if (m_indexedOutputs.count(outputIndex)) { + std::vector &connections = + m_indexedOutputs.at(outputIndex); + for (std::vector::iterator conn_it = connections.begin(); + conn_it != connections.end(); + ++conn_it) { + m_context->getNodeManager()->addPendingNodeConnection( + m_owner->shared_from_this(), + conn_it->destinationNode, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex, + conn_it->inputIndexAtDestination); + } + } + + // Disconnect all params from a specific output + if (m_indexedOutputParams.count(outputIndex)) { + std::vector> ¶ms = + m_indexedOutputParams.at(outputIndex); + for (std::vector>::iterator param_it = + params.begin(); + param_it != params.end(); + ++param_it) { + m_context->getNodeManager()->addPendingParamConnection( + m_owner->shared_from_this(), + *param_it, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex); + } + } +} + +void NodeConnections::disconnect(const std::shared_ptr &node) { + for (std::map>::iterator it = + m_indexedOutputs.begin(); + it != m_indexedOutputs.end(); + ++it) { + unsigned int outputIndex = it->first; + std::vector &connections = it->second; + for (std::vector::iterator conn_it = connections.begin(); + conn_it != connections.end(); + ++conn_it) { + if (conn_it->destinationNode == node) { + m_context->getNodeManager()->addPendingNodeConnection( + m_owner->shared_from_this(), + conn_it->destinationNode, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex, + conn_it->inputIndexAtDestination); + } + } + } +} + +void NodeConnections::disconnect( + const std::shared_ptr &node, + unsigned int outputIndex) { + if (m_indexedOutputs.count(outputIndex)) { + std::vector &connections = + m_indexedOutputs.at(outputIndex); + for (std::vector::iterator conn_it = connections.begin(); + conn_it != connections.end(); + ++conn_it) { + if (conn_it->destinationNode == node) { + m_context->getNodeManager()->addPendingNodeConnection( + m_owner->shared_from_this(), + conn_it->destinationNode, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex, + conn_it->inputIndexAtDestination); + } + } + } +} + +void NodeConnections::disconnect( + const std::shared_ptr &node, + unsigned int output, + unsigned int input) { + // This is a specific request, so we can dispatch a single event. + m_context->getNodeManager()->addPendingNodeConnection( + m_owner->shared_from_this(), + node, + AudioNodeManager::ConnectionType::DISCONNECT, + output, + input); +} + +void NodeConnections::disconnect(const std::shared_ptr ¶m) { + for (std::map>>:: + iterator it = m_indexedOutputParams.begin(); + it != m_indexedOutputParams.end(); + ++it) { + unsigned int outputIndex = it->first; + std::vector> ¶ms = it->second; + for (std::vector>::iterator param_it = + params.begin(); + param_it != params.end(); + ++param_it) { + if (*param_it == param) { + m_context->getNodeManager()->addPendingParamConnection( + m_owner->shared_from_this(), + *param_it, + AudioNodeManager::ConnectionType::DISCONNECT, + outputIndex); + } + } + } +} + +void NodeConnections::disconnect( + const std::shared_ptr ¶m, + unsigned int output) { + m_context->getNodeManager()->addPendingParamConnection( + m_owner->shared_from_this(), + param, + AudioNodeManager::ConnectionType::DISCONNECT, + output); +} + +void NodeConnections::connectNode( + const std::shared_ptr &destination, + unsigned int outputIndex, + unsigned int inputIndex) { + OutputConnection newConnection{destination, inputIndex}; + m_indexedOutputs[outputIndex].push_back(newConnection); + destination->onInputConnected(m_owner, outputIndex, inputIndex); +} + +void NodeConnections::onInputConnected( + AudioNode *sourceNode, + unsigned int outputIndexFromSource, + unsigned int inputIndex) { + InputConnection newConnection{sourceNode, outputIndexFromSource}; + m_indexedInputs[inputIndex].push_back(newConnection); +} + +// not sure if this is needed/valid - probably should be handled in cleanup() +void NodeConnections::disconnectAll() { + // This internal method is called on cleanup. It should directly notify nodes. + for (std::map>::iterator it = + m_indexedOutputs.begin(); + it != m_indexedOutputs.end(); + ++it) { + const unsigned int outputIndex = it->first; + const std::vector &connections = it->second; + for (std::vector::const_iterator conn_it = + connections.begin(); + conn_it != connections.end(); + ++conn_it) { + conn_it->destinationNode->onInputDisconnected( + m_owner, outputIndex, conn_it->inputIndexAtDestination); + } + } + m_indexedOutputs.clear(); + + m_indexedOutputParams.clear(); +} + +void NodeConnections::disconnectNode( + const std::shared_ptr &destination, + unsigned int outputIndex, + unsigned int inputIndex) { + if (!m_indexedOutputs.count(outputIndex)) { + return; + } + auto &connections = m_indexedOutputs.at(outputIndex); + auto it = std::remove_if( + connections.begin(), + connections.end(), + [&](const OutputConnection &conn) { + return conn.destinationNode == destination && + conn.inputIndexAtDestination == inputIndex; + }); + if (it != connections.end()) { + connections.erase(it, connections.end()); + destination->onInputDisconnected(m_owner, outputIndex, inputIndex); + } +} + +void NodeConnections::onInputDisconnected( + AudioNode *sourceNode, + unsigned int outputIndexFromSource, + unsigned int inputIndex) { + if (!m_indexedInputs.count(inputIndex)) { + return; + } + auto &connections = m_indexedInputs.at(inputIndex); + auto it = std::remove_if( + connections.begin(), connections.end(), [&](const InputConnection &conn) { + return conn.sourceNode == sourceNode && + conn.outputIndexFromSource == outputIndexFromSource; + }); + if (it != connections.end()) { + connections.erase(it, connections.end()); + } +} + +void NodeConnections::connectParam( + const std::shared_ptr ¶m, + unsigned int outputIndex) { + m_indexedOutputParams[outputIndex].push_back(param); + param->addInputNode(m_owner); +} + +void NodeConnections::disconnectParam( + const std::shared_ptr ¶m, + unsigned int outputIndex) { + if (!m_indexedOutputParams.count(outputIndex)) + return; + auto ¶ms = m_indexedOutputParams.at(outputIndex); + auto it = std::remove(params.begin(), params.end(), param); + if (it != params.end()) { + param->removeInputNode(m_owner); + params.erase(it, params.end()); + } +} + +// og version only notified the output nodes and ignored params +void NodeConnections::cleanup() { + for (std::map>::iterator map_it = + m_indexedOutputs.begin(); + map_it != m_indexedOutputs.end(); + ++map_it) { + unsigned int outputIndex = map_it->first; + std::vector &connections = map_it->second; + for (std::vector::iterator conn_it = connections.begin(); + conn_it != connections.end(); + ++conn_it) { + const std::shared_ptr &destinationNode = + conn_it->destinationNode; + unsigned int inputIndexAtDestination = conn_it->inputIndexAtDestination; + if (destinationNode) { + destinationNode->onInputDisconnected( + m_owner, outputIndex, inputIndexAtDestination); + } + } + } + + for (std::map>>:: + iterator map_it = m_indexedOutputParams.begin(); + map_it != m_indexedOutputParams.end(); + ++map_it) { + unsigned int outputIndex = map_it->first; + std::vector> ¶ms = map_it->second; + for (std::vector>::iterator param_it = + params.begin(); + param_it != params.end(); + ++param_it) { + const std::shared_ptr &destinationParam = *param_it; + if (destinationParam) { + destinationParam->removeInputNode(m_owner); + } + } + } + + m_indexedInputs.clear(); + m_indexedOutputs.clear(); + m_indexedOutputParams.clear(); +} + +void NodeConnections::propagateEnable() { + for (std::map>::const_iterator + it = m_indexedOutputs.begin(); + it != m_indexedOutputs.end(); + ++it) { + const std::vector &connections = it->second; + for (std::vector::const_iterator conn_it = + connections.begin(); + conn_it != connections.end(); + ++conn_it) { + conn_it->destinationNode->onInputEnabled(); + } + } +} + +void NodeConnections::propagateDisable() { + for (std::map>::const_iterator + it = m_indexedOutputs.begin(); + it != m_indexedOutputs.end(); + ++it) { + const std::vector &connections = it->second; + for (std::vector::const_iterator conn_it = + connections.begin(); + conn_it != connections.end(); + ++conn_it) { + conn_it->destinationNode->onInputDisabled(); + } + } +} + +unsigned int NodeConnections::computeNumberOfChannelsForInput( + unsigned int index) const { + const ChannelCountMode mode = m_owner->getChannelCountModeEnum(); + + if (mode == ChannelCountMode::EXPLICIT) { + return m_owner->getChannelCount(); + } + + unsigned int maxInputChannels = 0; + if (m_indexedInputs.count(index)) { + const std::vector &connections = m_indexedInputs.at(index); + for (const InputConnection &ic : connections) { + if (ic.sourceNode) { + maxInputChannels = std::max( + maxInputChannels, (unsigned int)ic.sourceNode->getChannelCount()); + } + } + } + + unsigned int computedChannels = 0; + if (mode == ChannelCountMode::MAX) { + computedChannels = maxInputChannels; + } else { // CLAMPED_MAX + computedChannels = + std::min((unsigned int)m_owner->getChannelCount(), maxInputChannels); + } + + return computedChannels > 0 ? computedChannels : 1; +} + +const std::vector> &NodeConnections::processAllInputs( + int framesToProcess, + bool checkIsAlreadyProcessed) { + int numInputs = m_owner->getNumberOfInputs(); + + m_processingInputBuses.resize(static_cast(numInputs)); + + for (int i = 0; i < numInputs; ++i) { + m_processingInputBuses[i] = processInputAtIndex( + static_cast(i), framesToProcess, checkIsAlreadyProcessed); + } + + return m_processingInputBuses; +} + +std::shared_ptr NodeConnections::processInputAtIndex( + unsigned int index, + int framesToProcess, + bool checkIsAlreadyProcessed) { + if (!m_indexedInputs.count(index) || m_indexedInputs.at(index).empty()) { + unsigned int channels = m_owner->getChannelCount(); + auto bus = getProcessingBusForIndex(index, channels, framesToProcess); + bus->zero(); + return bus; + } + + const std::vector &connections = m_indexedInputs.at(index); + const ChannelCountMode mode = m_owner->getChannelCountModeEnum(); + + if (connections.size() == 1 && mode == ChannelCountMode::MAX) { + AudioNode *sourceNode = connections[0].sourceNode; + if (sourceNode) { + sourceNode->processAudio(framesToProcess, checkIsAlreadyProcessed); + try { + return sourceNode->getOutputBus(connections[0].outputIndexFromSource); + } catch (...) { + } + } + // fallback to silent bus + auto fallback = getProcessingBusForIndex(index, 1, framesToProcess); + fallback->zero(); + return fallback; + } + + unsigned int computedChannels = computeNumberOfChannelsForInput(index); + auto sumBus = + getProcessingBusForIndex(index, computedChannels, framesToProcess); + + sumBus->zero(); + + for (const InputConnection &ic : connections) { + AudioNode *sourceNode = ic.sourceNode; + if (!sourceNode) + continue; + + sourceNode->processAudio(framesToProcess, checkIsAlreadyProcessed); + + std::shared_ptr srcBus; + try { + srcBus = sourceNode->getOutputBus(ic.outputIndexFromSource); + } catch (...) { + srcBus = nullptr; + } + + if (srcBus) { + sumBus->sum(srcBus.get(), m_owner->getChannelInterpretationEnum()); + } + } + + return sumBus; +} + +std::shared_ptr NodeConnections::getProcessingBusForIndex( + unsigned int inputIndex, + unsigned int numChannels, + int framesToProcess) { + if (m_processingInputBuses.size() <= inputIndex) { + m_processingInputBuses.resize(inputIndex + 1); + } + + auto &bus = m_processingInputBuses[inputIndex]; + if (!bus || bus->getSize() != static_cast(framesToProcess) || + bus->getNumberOfChannels() != numChannels || + bus->getSampleRate() != m_context->getSampleRate()) { + bus = std::make_shared( + static_cast(framesToProcess), + numChannels, + m_context->getSampleRate()); + } + return bus; +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h index 2ffc80cbe..6e642b218 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/NodeConnections.h @@ -95,8 +95,11 @@ Disconnects a specific output of the node from a specific input of some destin // --- Internal Helpers --- std::shared_ptr processInputAtIndex(unsigned int index, int framesToProcess, bool checkIsAlreadyProcessed); - std::shared_ptr applyChannelCountMode(const std::shared_ptr &processingBus); - void mixSourceBuses(const std::shared_ptr &processingBus); + unsigned int computeNumberOfChannelsForInput(unsigned int index) const; + + std::shared_ptr getProcessingBusForIndex(unsigned int inputIndex, + unsigned int numChannels, + int framesToProcess); }; } // namespace audioapi From 445194d83631fdd42a51142d2595bf57260710ad Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:32:55 +0200 Subject: [PATCH 03/38] feat: adjusted cpp layer to indexed connect --- .../cpp/audioapi/core/BaseAudioContext.cpp | 18 +++ .../cpp/audioapi/core/BaseAudioContext.h | 4 + .../destinations/AudioDestinationNode.cpp | 23 +++- .../core/destinations/AudioDestinationNode.h | 4 +- .../core/effects/StereoPannerNode.cpp | 129 +++++++++++++----- .../core/sources/AudioBufferSourceNode.cpp | 13 +- .../audioapi/core/sources/OscillatorNode.cpp | 3 - .../audioapi/core/sources/StreamerNode.cpp | 2 - .../audioapi/core/utils/AudioNodeManager.cpp | 4 +- 9 files changed, 149 insertions(+), 51 deletions(-) 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 7a63739cc..f35519be9 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 @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -139,6 +141,22 @@ std::shared_ptr BaseAudioContext::createBiquadFilter() { return biquadFilter; } +std::shared_ptr BaseAudioContext::createChannelSplitter( + unsigned numberOfOutputs) { + auto channelSplitter = + std::make_shared(this, numberOfOutputs); + nodeManager_->addProcessingNode(channelSplitter); + return channelSplitter; +} + +std::shared_ptr BaseAudioContext::createChannelMerger( + unsigned numberOfInputs) { + auto channelMerger = + std::make_shared(this, numberOfInputs); + nodeManager_->addProcessingNode(channelMerger); + return channelMerger; +} + std::shared_ptr BaseAudioContext::createBufferSource( bool pitchCorrection) { auto bufferSource = 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 69ae155be..fe622f111 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 @@ -34,6 +34,8 @@ class WorkletSourceNode; class WorkletNode; class WorkletProcessingNode; class StreamerNode; +class ChannelSplitterNode; +class ChannelMergerNode; class BaseAudioContext { public: @@ -58,6 +60,8 @@ class BaseAudioContext { std::shared_ptr createBiquadFilter(); std::shared_ptr createBufferSource(bool pitchCorrection); std::shared_ptr createBufferQueueSource(bool pitchCorrection); + std::shared_ptr createChannelSplitter(unsigned numberOfOutputs = 6); + std::shared_ptr createChannelMerger(unsigned numberOfInputs = 6); static std::shared_ptr createBuffer(int numberOfChannels, size_t length, float sampleRate); std::shared_ptr createPeriodicWave( 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 index 8147c4733..a87a44889 100644 --- 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 @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace audioapi { @@ -22,21 +23,35 @@ double AudioDestinationNode::getCurrentTime() const { return static_cast(currentSampleFrame_) / context_->getSampleRate(); } +void AudioDestinationNode::processNode( + const std::vector> &inputBuses, + int framesToProcess) { + if (inputBuses.empty() || !inputBuses[0]) { + return; + } + + + getOutputBus(0)->copy(inputBuses[0].get()); +} + void AudioDestinationNode::renderAudio( const std::shared_ptr &destinationBus, int numFrames) { - if (numFrames < 0 || !destinationBus || !isInitialized_) { + if (numFrames <= 0 || !destinationBus || !isInitialized_) { return; } + context_->getNodeManager()->preProcessGraph(); + destinationBus->zero(); - auto processedBus = processAudio(destinationBus, numFrames, true); - if (processedBus && processedBus != destinationBus) { - destinationBus->copy(processedBus.get()); + const auto &inputBuses = m_connections->processAllInputs(numFrames, true); + + if (!inputBuses.empty() && inputBuses[0]) { + destinationBus->copy(inputBuses[0].get()); } destinationBus->normalize(); 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 5488e6e7c..5588a95c9 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 @@ -22,9 +22,7 @@ class AudioDestinationNode : public AudioNode { void renderAudio(const std::shared_ptr& audioData, int numFrames); 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& processingBus, int) final { return processingBus; }; + void processNode(const std::vector> &inputBuses, int framesToProcess) override; private: std::size_t currentSampleFrame_; 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 74532ef2e..529fa9e79 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 @@ -25,54 +25,115 @@ std::shared_ptr StereoPannerNode::processNode( double time = context_->getCurrentTime(); double deltaTime = 1.0 / context_->getSampleRate(); - auto *inputLeft = processingBus->getChannelByType(AudioBus::ChannelLeft); - auto panParamValues = panParam_->processARateParam(framesToProcess, time) - ->getChannel(0) - ->getData(); + // needs to be tested and probably fixed + auto panValuesArray = panParam_->processARateParam(framesToProcess, time); + const float *panParamValues = panValuesArray->getChannel(0)->getData(); + + + std::shared_ptr outBus = nullptr; + std::shared_ptr tempBus = nullptr; + bool usedTemp = false; + +. + try { + outBus = getOutputBus(0); + } catch (...) { + outBus = nullptr; + } + + if (!outBus || outBus->getNumberOfChannels() < 2) { + tempBus = std::make_shared( + static_cast(framesToProcess), 2, context_->getSampleRate()); + tempBus->zero(); + outBus = tempBus; + usedTemp = true; + } else { + outBus->zero(); + } - auto *outputLeft = audioBus_->getChannelByType(AudioBus::ChannelLeft); - auto *outputRight = audioBus_->getChannelByType(AudioBus::ChannelRight); + + auto *outputLeft = outBus->getChannelByType(AudioBus::ChannelLeft); + auto *outputRight = outBus->getChannelByType(AudioBus::ChannelRight); + + + auto numInChannels = processingBus ? processingBus->getNumberOfChannels() : 0; + auto *inputLeft = processingBus + ? processingBus->getChannelByType(AudioBus::ChannelLeft) + : nullptr; + auto *inputRight = processingBus + ? processingBus->getChannelByType(AudioBus::ChannelRight) + : nullptr; + + + if (!inputLeft) { + + if (usedTemp) { + + if (!m_outputBuses.empty() && m_outputBuses[0]) { + m_outputBuses[0]->copy(outBus.get()); + return m_outputBuses[0]; + } + } + return outBus; + } - // Input is mono - if (processingBus->getNumberOfChannels() == 1) { - for (int i = 0; i < framesToProcess; i++) { - auto pan = std::clamp(panParamValues[i], -1.0f, 1.0f); - auto x = (pan + 1) / 2; + + if (numInChannels <= 1 || inputRight == nullptr) { + + const float *inL = inputLeft->getData(); + float *outL = outputLeft->getData(); + float *outR = outputRight->getData(); - auto gainL = static_cast(cos(x * PI / 2)); - auto gainR = static_cast(sin(x * PI / 2)); + for (int i = 0; i < framesToProcess; ++i) { + float pan = std::clamp(panParamValues[i], -1.0f, 1.0f); + float x = (pan + 1.0f) * 0.5f; + float gainL = static_cast(cos(x * PI / 2.0)); + float gainR = static_cast(sin(x * PI / 2.0)); + float inputSample = inL[i]; - float input = (*inputLeft)[i]; + outL[i] = inputSample * gainL; + outR[i] = inputSample * gainR; - (*outputLeft)[i] = input * gainL; - (*outputRight)[i] = input * gainR; time += deltaTime; } - } else { // Input is stereo - auto *inputRight = processingBus->getChannelByType(AudioBus::ChannelRight); - for (int i = 0; i < framesToProcess; i++) { - auto pan = std::clamp(panParamValues[i], -1.0f, 1.0f); - auto x = (pan <= 0 ? pan + 1 : pan); - - auto gainL = static_cast(cos(x * PI / 2)); - auto gainR = static_cast(sin(x * PI / 2)); - - float inputL = (*inputLeft)[i]; - float inputR = (*inputRight)[i]; - - if (pan <= 0) { - (*outputLeft)[i] = inputL + inputR * gainL; - (*outputRight)[i] = inputR * gainR; + } else { + + const float *inL = inputLeft->getData(); + const float *inR = inputRight->getData(); + float *outL = outputLeft->getData(); + float *outR = outputRight->getData(); + + for (int i = 0; i < framesToProcess; ++i) { + float pan = std::clamp(panParamValues[i], -1.0f, 1.0f); + float x = (pan <= 0.0f ? pan + 1.0f : pan); + float gainL = static_cast(cos(x * PI / 2.0)); + float gainR = static_cast(sin(x * PI / 2.0)); + + float sampleL = inL[i]; + float sampleR = inR[i]; + + if (pan <= 0.0f) { + outL[i] = sampleL + sampleR * gainL; + outR[i] = sampleR * gainR; } else { - (*outputLeft)[i] = inputL * gainL; - (*outputRight)[i] = inputR + inputL * gainR; + outL[i] = sampleL * gainL; + outR[i] = sampleR + sampleL * gainR; } time += deltaTime; } } - return audioBus_; + + if (usedTemp) { + if (!m_outputBuses.empty() && m_outputBuses[0]) { + m_outputBuses[0]->copy(outBus.get()); + return m_outputBuses[0]; + } + } + + + return outBus; } } // namespace audioapi 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 d7fc5d102..5b29c312c 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 @@ -83,10 +83,17 @@ void AudioBufferSourceNode::setBuffer( buffer_ = buffer; alignedBus_ = std::make_shared(*buffer_->bus_); - channelCount_ = buffer_->getNumberOfChannels(); + int newChannelCount = buffer_->getNumberOfChannels(); - audioBus_ = std::make_shared( - RENDER_QUANTUM_SIZE, channelCount_, context_->getSampleRate()); + if (channelCount_ != newChannelCount) { + channelCount_ = newChannelCount; + + // Re-initialize output buses with the correct channel count. + for (unsigned int i = 0; i < numberOfOutputs_; ++i) { + m_outputBuses[i] = std::make_shared( + RENDER_QUANTUM_SIZE, channelCount_, context_->getSampleRate()); + } + } playbackRateBus_ = std::make_shared( RENDER_QUANTUM_SIZE * 3, channelCount_, context_->getSampleRate()); 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 832a452a7..aeb44a804 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 @@ -21,9 +21,6 @@ OscillatorNode::OscillatorNode(BaseAudioContext *context) type_ = OscillatorType::SINE; periodicWave_ = context_->getBasicWaveForm(type_); - audioBus_ = std::make_shared( - RENDER_QUANTUM_SIZE, 1, context_->getSampleRate()); - isInitialized_ = true; } 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 f7b031e97..a9f03b983 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 @@ -66,8 +66,6 @@ bool StreamerNode::initialize(const std::string &input_url) { maxBufferSize_, codecpar_->ch_layout.nb_channels, codecCtx_->sample_rate); channelCount_ = codecpar_->ch_layout.nb_channels; - audioBus_ = std::make_shared( - RENDER_QUANTUM_SIZE, channelCount_, context_->getSampleRate()); streamingThread_ = std::thread(&StreamerNode::streamAudio, this); streamFlag.store(true); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp index 977adb154..d8dffca2e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioNodeManager.cpp @@ -88,7 +88,7 @@ void AudioNodeManager::addPendingNodeConnection( const std::shared_ptr &from, const std::shared_ptr &to, ConnectionType type, - unsigned int outputIndex, + unsigned int outputIndex, unsigned int inputIndex) { auto event = std::make_unique(); event->type = type; @@ -105,7 +105,7 @@ void AudioNodeManager::addPendingParamConnection( const std::shared_ptr &from, const std::shared_ptr &to, ConnectionType type, - unsigned int outputIndex) { + unsigned int outputIndex) { auto event = std::make_unique(); event->type = type; event->payloadType = EventPayloadType::PARAMS; From 9221b153d7bfbb24b8860dc9e21e593d9cbfb5dc Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:35:02 +0200 Subject: [PATCH 04/38] feat: implement channel merger and splitter cpp layer --- .../core/effects/ChannelMergerNode.cpp | 44 +++++++++++++++ .../audioapi/core/effects/ChannelMergerNode.h | 17 ++++++ .../core/effects/ChannelSplitterNode.cpp | 54 +++++++++++++++++++ .../core/effects/ChannelSplitterNode.h | 17 ++++++ 4 files changed, 132 insertions(+) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.h create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.h diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.cpp new file mode 100644 index 000000000..3e01f7fad --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.cpp @@ -0,0 +1,44 @@ +#include "ChannelMergerNode.h" +#include +#include +#include + +namespace audioapi { + +ChannelMergerNode::ChannelMergerNode( + BaseAudioContext *context, + unsigned numberOfInputs) + : AudioNode(context) { + numberOfInputs_ = numberOfInputs; + numberOfOutputs_ = 1; + + channelCount_ = 1; + channelCountMode_ = ChannelCountMode::EXPLICIT; + channelInterpretation_ = ChannelInterpretation::SPEAKERS; + + m_outputBuses.clear(); + m_outputBuses.push_back( + std::make_shared( + RENDER_QUANTUM_SIZE, numberOfInputs_, context->getSampleRate())); + + isInitialized_ = true; +} + +void ChannelMergerNode::processNode( + const std::vector> &inputBuses, + int framesToProcess) { + auto outputBus = getOutputBus(0); + + for (unsigned i = 0; i < numberOfInputs_; ++i) { + bool isInputConnected = i < inputBuses.size() && inputBuses[i]; + + if (isInputConnected) { + const auto &sourceBus = inputBuses[i]; + outputBus->getChannel(i)->copy(sourceBus->getChannel(0)); + } else { + outputBus->getChannel(i)->zero(); + } + } +} + +} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.h new file mode 100644 index 000000000..72eb3449e --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelMergerNode.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace audioapi { + +class ChannelMergerNode : public AudioNode { + public: + explicit ChannelMergerNode(BaseAudioContext *context, unsigned numberOfInputs = 6); + virtual ~ChannelMergerNode() = default; + + protected: + void processNode(const std::vector> &inputBuses, int framesToProcess) override; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.cpp new file mode 100644 index 000000000..c5a7486fe --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.cpp @@ -0,0 +1,54 @@ +#include "ChannelSplitterNode.h" +#include +#include +#include +#include + +namespace audioapi { + +ChannelSplitterNode::ChannelSplitterNode( + BaseAudioContext *context, + unsigned numberOfOutputs) + : AudioNode(context) { + numberOfInputs_ = 1; + numberOfOutputs_ = numberOfOutputs; + + channelCount_ = numberOfOutputs; + channelCountMode_ = ChannelCountMode::EXPLICIT; + channelInterpretation_ = ChannelInterpretation::DISCRETE; + + m_outputBuses.clear(); + for (unsigned i = 0; i < numberOfOutputs_; ++i) { + m_outputBuses.push_back( + std::make_shared( + RENDER_QUANTUM_SIZE, 1, context->getSampleRate())); + } + + isInitialized_ = true; +} + +void ChannelSplitterNode::processNode( + const std::vector> &inputBuses, + int framesToProcess) { + if (inputBuses.empty() || !inputBuses[0]) { + for (unsigned i = 0; i < numberOfOutputs_; ++i) { + getOutputBus(i)->zero(); + } + return; + } + + const auto &sourceBus = inputBuses[0]; + unsigned numberOfSourceChannels = sourceBus->getNumberOfChannels(); + + for (unsigned i = 0; i < numberOfOutputs_; ++i) { + auto destinationBus = getOutputBus(i); + + if (i < numberOfSourceChannels) { + destinationBus->getChannel(0)->copy(sourceBus->getChannel(i)); + } else { + destinationBus->zero(); + } + } +} + +} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.h new file mode 100644 index 000000000..462f9faed --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/effects/ChannelSplitterNode.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace audioapi { + +class ChannelSplitterNode : public AudioNode { + public: + explicit ChannelSplitterNode(BaseAudioContext *context, unsigned numberOfOutputs = 6); + virtual ~ChannelSplitterNode() = default; + + protected: + void processNode(const std::vector> &inputBuses, int framesToProcess) override; +}; + +} // namespace audioapi From 612464b39c58fc3bc783027803e70c8a01d56a34 Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:37:54 +0200 Subject: [PATCH 05/38] feat: implement jsi layer for merger/splitter and indexed connect --- .../HostObjects/AudioNodeHostObject.cpp | 105 +++++++++++++++--- .../BaseAudioContextHostObject.cpp | 32 ++++++ .../HostObjects/BaseAudioContextHostObject.h | 2 + .../effects/ChannelMergerNodeHostObject.cpp | 20 ++++ .../effects/ChannelMergerNodeHostObject.h | 19 ++++ .../effects/ChannelSplitterNodeHostObject.cpp | 21 ++++ .../effects/ChannelSplitterNodeHostObject.h | 19 ++++ 7 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.h create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.h 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 d5d3ce4bf..64386eded 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 @@ -42,32 +42,109 @@ JSI_PROPERTY_GETTER_IMPL(AudioNodeHostObject, channelInterpretation) { JSI_HOST_FUNCTION_IMPL(AudioNodeHostObject, connect) { auto obj = args[0].getObject(runtime); + if (obj.isHostObject(runtime)) { auto node = obj.getHostObject(runtime); - node_->connect(std::shared_ptr(node)->node_); + unsigned int outputIndex = args[1].asNumber(); + unsigned int inputIndex = args[2].asNumber(); + node_->connect( + std::shared_ptr(node)->node_, + outputIndex, + inputIndex); } if (obj.isHostObject(runtime)) { auto param = obj.getHostObject(runtime); - node_->connect(std::shared_ptr(param)->param_); + unsigned int outputIndex = args[1].asNumber(); + node_->connect( + std::shared_ptr(param)->param_, outputIndex); } return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioNodeHostObject, disconnect) { - if (args[0].isUndefined()) { - node_->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_); - } + switch (count) { + case 0: { + // Signature: disconnect() + node_->disconnect(); + break; + } - if (obj.isHostObject(runtime)) { - auto param = obj.getHostObject(runtime); - node_->disconnect(std::shared_ptr(param)->param_); + case 1: { + const auto &arg = args[0]; + if (arg.isNumber()) { + // Signature: disconnect(outputIndex) + node_->disconnect(static_cast(arg.asNumber())); + } else if (arg.isObject()) { + auto obj = arg.asObject(runtime); + if (obj.isHostObject(runtime)) { + // Signature: disconnect(destinationNode) + auto destHostObject = obj.getHostObject(runtime); + node_->disconnect(destHostObject->node_); + } else if (obj.isHostObject(runtime)) { + // Signature: disconnect(destinationParam) + auto destHostObject = + obj.getHostObject(runtime); + node_->disconnect(destHostObject->param_); + } else { + throw jsi::JSError( + runtime, + "disconnect: Argument 1 must be a number, AudioNode, or AudioParam."); + } + } else { + throw jsi::JSError(runtime, "disconnect: Invalid argument."); + } + break; + } + + case 2: { + if (!args[0].isObject() || !args[1].isNumber()) { + throw jsi::JSError( + runtime, + "disconnect: Expected arguments of type (AudioNode | AudioParam, number)."); + } + auto obj = args[0].asObject(runtime); + auto outputIndex = static_cast(args[1].asNumber()); + if (obj.isHostObject(runtime)) { + // Signature: disconnect(destinationNode, outputIndex) + auto destHostObject = obj.getHostObject(runtime); + node_->disconnect(destHostObject->node_, outputIndex); + } else if (obj.isHostObject(runtime)) { + // Signature: disconnect(destinationParam, outputIndex) + auto destHostObject = obj.getHostObject(runtime); + node_->disconnect(destHostObject->param_, outputIndex); + } else { + throw jsi::JSError( + runtime, + "disconnect: Argument 1 must be an AudioNode or AudioParam."); + } + break; + } + + case 3: { + if (!args[0].isObject() || !args[1].isNumber() || !args[2].isNumber()) { + throw jsi::JSError( + runtime, + "disconnect: Expected arguments of type (AudioNode, number, number)."); + } + auto obj = args[0].asObject(runtime); + if (!obj.isHostObject(runtime)) { + throw jsi::JSError( + runtime, + "disconnect: With 3 arguments, the first must be an AudioNode."); + } + auto destHostObject = obj.getHostObject(runtime); + auto outputIndex = static_cast(args[1].asNumber()); + auto inputIndex = static_cast(args[2].asNumber()); + // Signature: disconnect(destinationNode, outputIndex, inputIndex) + node_->disconnect(destHostObject->node_, outputIndex, inputIndex); + break; + } + + default: { + throw jsi::JSError(runtime, "disconnect: Invalid number of arguments."); + } } + return jsi::Value::undefined(); } } // namespace audioapi 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 e08fe834d..65ebf9a02 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 @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -45,6 +47,8 @@ BaseAudioContextHostObject::BaseAudioContextHostObject( JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createGain), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createStereoPanner), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createBiquadFilter), + JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createChannelSplitter), + JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createChannelMerger), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createBufferSource), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createBufferQueueSource), JSI_EXPORT_FUNCTION(BaseAudioContextHostObject, createBuffer), @@ -194,6 +198,34 @@ JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createBiquadFilter) { return jsi::Object::createFromHostObject(runtime, biquadFilterHostObject); } +JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createChannelSplitter) { + unsigned numberOfOutputs = 6; + if (count > 0 && args[0].isNumber()) { + numberOfOutputs = static_cast(args[0].asNumber()); + } + + auto channelSplitter = context_->createChannelSplitter(numberOfOutputs); + + auto channelSplitterHostObject = + std::make_shared(channelSplitter); + + return jsi::Object::createFromHostObject(runtime, channelSplitterHostObject); +} + +JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createChannelMerger) { + unsigned numberOfInputs = 6; + if (count > 0 && args[0].isNumber()) { + numberOfInputs = static_cast(args[0].asNumber()); + } + + auto channelMerger = context_->createChannelMerger(numberOfInputs); + + auto channelMergerHostObject = + std::make_shared(channelMerger); + + return jsi::Object::createFromHostObject(runtime, channelMergerHostObject); +} + JSI_HOST_FUNCTION_IMPL(BaseAudioContextHostObject, createBufferSource) { auto pitchCorrection = args[0].asBool(); auto bufferSource = context_->createBufferSource(pitchCorrection); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h index cc9694d7e..2c08a7392 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/BaseAudioContextHostObject.h @@ -36,6 +36,8 @@ class BaseAudioContextHostObject : public JsiHostObject { JSI_HOST_FUNCTION_DECL(createGain); JSI_HOST_FUNCTION_DECL(createStereoPanner); JSI_HOST_FUNCTION_DECL(createBiquadFilter); + JSI_HOST_FUNCTION_DECL(createChannelSplitter); + JSI_HOST_FUNCTION_DECL(createChannelMerger); JSI_HOST_FUNCTION_DECL(createBufferSource); JSI_HOST_FUNCTION_DECL(createBufferQueueSource); JSI_HOST_FUNCTION_DECL(createBuffer); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.cpp new file mode 100644 index 000000000..368c826ad --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.cpp @@ -0,0 +1,20 @@ +#include + +#include +#include + +namespace audioapi { + +ChannelMergerNodeHostObject::ChannelMergerNodeHostObject( + const std::shared_ptr &node) + : AudioNodeHostObject(node) { + addGetters( + JSI_EXPORT_PROPERTY_GETTER(ChannelMergerNodeHostObject, numberOfInputs)); +} + +JSI_PROPERTY_GETTER_IMPL(ChannelMergerNodeHostObject, numberOfInputs) { + auto channelMergerNode = std::static_pointer_cast(node_); + return jsi::Value(channelMergerNode->getNumberOfInputs()); +} + +} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.h new file mode 100644 index 000000000..948812f5a --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelMergerNodeHostObject.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include +#include + +namespace audioapi { +using namespace facebook; + +class ChannelMergerNode; + +class ChannelMergerNodeHostObject : public AudioNodeHostObject { + public: + explicit ChannelMergerNodeHostObject(const std::shared_ptr &node); + + JSI_PROPERTY_GETTER_DECL(numberOfInputs); +}; +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.cpp new file mode 100644 index 000000000..52080343c --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.cpp @@ -0,0 +1,21 @@ +#include + +#include +#include + +namespace audioapi { + +ChannelSplitterNodeHostObject::ChannelSplitterNodeHostObject( + const std::shared_ptr &node) + : AudioNodeHostObject(node) { + addGetters(JSI_EXPORT_PROPERTY_GETTER( + ChannelSplitterNodeHostObject, numberOfOutputs)); +} + +JSI_PROPERTY_GETTER_IMPL(ChannelSplitterNodeHostObject, numberOfOutputs) { + auto channelSplitterNode = + std::static_pointer_cast(node_); + return jsi::Value(channelSplitterNode->getNumberOfOutputs()); +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.h new file mode 100644 index 000000000..3c1de4b33 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/effects/ChannelSplitterNodeHostObject.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include +#include + +namespace audioapi { +using namespace facebook; + +class ChannelSplitterNode; + +class ChannelSplitterNodeHostObject : public AudioNodeHostObject { + public: + explicit ChannelSplitterNodeHostObject(const std::shared_ptr &node); + + JSI_PROPERTY_GETTER_DECL(numberOfOutputs); +}; +} // namespace audioapi From 0e05c4e4c2d2c7945dfd530a2d977b985545f2a6 Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:39:13 +0200 Subject: [PATCH 06/38] fix: lint --- .../destinations/AudioDestinationNode.cpp | 4 ---- .../core/effects/StereoPannerNode.cpp | 20 +++---------------- 2 files changed, 3 insertions(+), 21 deletions(-) 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 index a87a44889..10bf4a693 100644 --- 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 @@ -30,7 +30,6 @@ void AudioDestinationNode::processNode( return; } - getOutputBus(0)->copy(inputBuses[0].get()); } @@ -41,13 +40,10 @@ void AudioDestinationNode::renderAudio( return; } - context_->getNodeManager()->preProcessGraph(); - destinationBus->zero(); - const auto &inputBuses = m_connections->processAllInputs(numFrames, true); if (!inputBuses.empty() && inputBuses[0]) { 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 529fa9e79..5cc7fd9e5 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 @@ -25,19 +25,15 @@ std::shared_ptr StereoPannerNode::processNode( double time = context_->getCurrentTime(); double deltaTime = 1.0 / context_->getSampleRate(); - // needs to be tested and probably fixed + // needs to be tested and probably fixed auto panValuesArray = panParam_->processARateParam(framesToProcess, time); const float *panParamValues = panValuesArray->getChannel(0)->getData(); - std::shared_ptr outBus = nullptr; std::shared_ptr tempBus = nullptr; bool usedTemp = false; -. - try { - outBus = getOutputBus(0); - } catch (...) { + try { outBus = getOutputBus(0); } catch (...) { outBus = nullptr; } @@ -48,14 +44,12 @@ std::shared_ptr StereoPannerNode::processNode( outBus = tempBus; usedTemp = true; } else { - outBus->zero(); + outBus->zero(); } - auto *outputLeft = outBus->getChannelByType(AudioBus::ChannelLeft); auto *outputRight = outBus->getChannelByType(AudioBus::ChannelRight); - auto numInChannels = processingBus ? processingBus->getNumberOfChannels() : 0; auto *inputLeft = processingBus ? processingBus->getChannelByType(AudioBus::ChannelLeft) @@ -64,11 +58,8 @@ std::shared_ptr StereoPannerNode::processNode( ? processingBus->getChannelByType(AudioBus::ChannelRight) : nullptr; - if (!inputLeft) { - if (usedTemp) { - if (!m_outputBuses.empty() && m_outputBuses[0]) { m_outputBuses[0]->copy(outBus.get()); return m_outputBuses[0]; @@ -77,9 +68,7 @@ std::shared_ptr StereoPannerNode::processNode( return outBus; } - if (numInChannels <= 1 || inputRight == nullptr) { - const float *inL = inputLeft->getData(); float *outL = outputLeft->getData(); float *outR = outputRight->getData(); @@ -97,7 +86,6 @@ std::shared_ptr StereoPannerNode::processNode( time += deltaTime; } } else { - const float *inL = inputLeft->getData(); const float *inR = inputRight->getData(); float *outL = outputLeft->getData(); @@ -124,7 +112,6 @@ std::shared_ptr StereoPannerNode::processNode( } } - if (usedTemp) { if (!m_outputBuses.empty() && m_outputBuses[0]) { m_outputBuses[0]->copy(outBus.get()); @@ -132,7 +119,6 @@ std::shared_ptr StereoPannerNode::processNode( } } - return outBus; } From e09fcd851baa86975e9d96c13c0464f9c92d3835 Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:41:10 +0200 Subject: [PATCH 07/38] feat: implement ts layer --- .../core/effects/StereoPannerNode.cpp | 4 +- packages/react-native-audio-api/src/api.ts | 2 + .../src/core/AudioNode.ts | 72 +++++++++++++++++-- .../src/core/BaseAudioContext.ts | 16 +++++ .../src/core/ChannelMergerNode.ts | 12 ++++ .../src/core/ChannelSplitterNode.ts | 12 ++++ .../react-native-audio-api/src/interfaces.ts | 23 +++++- 7 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 packages/react-native-audio-api/src/core/ChannelMergerNode.ts create mode 100644 packages/react-native-audio-api/src/core/ChannelSplitterNode.ts 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 5cc7fd9e5..8a0cb902f 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 @@ -33,7 +33,9 @@ std::shared_ptr StereoPannerNode::processNode( std::shared_ptr tempBus = nullptr; bool usedTemp = false; - try { outBus = getOutputBus(0); } catch (...) { + try { + outBus = getOutputBus(0); + } catch (...) { outBus = nullptr; } diff --git a/packages/react-native-audio-api/src/api.ts b/packages/react-native-audio-api/src/api.ts index 3fb9918bd..396ffd837 100644 --- a/packages/react-native-audio-api/src/api.ts +++ b/packages/react-native-audio-api/src/api.ts @@ -71,6 +71,8 @@ export { default as BiquadFilterNode } from './core/BiquadFilterNode'; export { default as GainNode } from './core/GainNode'; export { default as OscillatorNode } from './core/OscillatorNode'; export { default as StereoPannerNode } from './core/StereoPannerNode'; +export { default as ChannelSplitterNode } from './core/ChannelSplitterNode'; +export { default as ChannelMergerNode } from './core/ChannelMergerNode'; export { default as AudioRecorder } from './core/AudioRecorder'; export { default as StreamerNode } from './core/StreamerNode'; export { default as ConstantSourceNode } from './core/ConstantSourceNode'; diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 2e19d51d1..100a403ff 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -2,7 +2,11 @@ import { IAudioNode } from '../interfaces'; import AudioParam from './AudioParam'; import { ChannelCountMode, ChannelInterpretation } from '../types'; import BaseAudioContext from './BaseAudioContext'; -import { InvalidAccessError } from '../errors'; +import { + InvalidAccessError, + IndexSizeError, + NotSupportedError, +} from '../errors'; export default class AudioNode { readonly context: BaseAudioContext; @@ -23,27 +27,81 @@ export default class AudioNode { this.channelInterpretation = this.node.channelInterpretation; } - public connect(destination: AudioNode | AudioParam): AudioNode | AudioParam { + public connect( + destination: AudioNode | AudioParam, + outputIndex: number = 0, + inputIndex: number = 0 + ): AudioNode | AudioParam | undefined { if (this.context !== destination.context) { throw new InvalidAccessError( 'Source and destination are from different BaseAudioContexts' ); } + if (outputIndex < 0 || outputIndex >= this.numberOfOutputs) { + throw new IndexSizeError('Output index is out of range'); + } + if (destination instanceof AudioNode) { + if (inputIndex < 0 || inputIndex >= destination.numberOfInputs) { + throw new IndexSizeError('Input index is out of range'); + } + } + if (destination instanceof AudioParam && inputIndex !== 0) { + throw new NotSupportedError( + 'Cannot specify input index when connecting to an AudioParam' + ); + } if (destination instanceof AudioParam) { - this.node.connect(destination.audioParam); + this.node.connect(destination.audioParam, outputIndex); + return undefined; } else { - this.node.connect(destination.node); + this.node.connect(destination.node, outputIndex, inputIndex); } return destination; } - public disconnect(destination?: AudioNode | AudioParam): void { + public disconnect( + destinationOrOutput?: AudioNode | AudioParam | number, + output?: number, + input?: number + ): void { + // probably needs a switch statement + // Case 1: disconnect() + if (destinationOrOutput === undefined) { + this.node.disconnect(); + return; + } + + // Case 2: disconnect(outputIndex: number) + if (typeof destinationOrOutput === 'number') { + this.node.disconnect(destinationOrOutput); + return; + } + + const destination = destinationOrOutput; + if (destination instanceof AudioParam) { - this.node.disconnect(destination.audioParam); + // Destination is an AudioParam + if (output === undefined) { + // disconnect(destination: AudioParam) + this.node.disconnect(destination.audioParam); + } else { + // disconnect(destination: AudioParam, output: number) + this.node.disconnect(destination.audioParam, output); + } } else { - this.node.disconnect(destination?.node); + // Destination is an AudioNode + if (output === undefined) { + // disconnect(destination: AudioNode) + this.node.disconnect(destination.node); + } else if (input === undefined) { + // disconnect(destination: AudioNode, output: number) + this.node.disconnect(destination.node, output); + } else { + // disconnect(destination: AudioNode, output: number, input: number) + this.node.disconnect(destination.node, output, input); + } } } } diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index 8482ac61e..06f0219a5 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -23,6 +23,8 @@ import RecorderAdapterNode from './RecorderAdapterNode'; import StereoPannerNode from './StereoPannerNode'; import StreamerNode from './StreamerNode'; import WorkletNode from './WorkletNode'; +import ChannelSplitterNode from './ChannelSplitterNode'; +import ChannelMergerNode from './ChannelMergerNode'; import { decodeAudioData, decodePCMInBase64 } from './AudioDecoder'; export default class BaseAudioContext { @@ -218,6 +220,20 @@ export default class BaseAudioContext { return new BiquadFilterNode(this, this.context.createBiquadFilter()); } + createChannelSplitter(numberOfOutputs: number = 6): ChannelSplitterNode { + return new ChannelSplitterNode( + this, + this.context.createChannelSplitter(numberOfOutputs) + ); + } + + createChannelMerger(numberOfInputs: number = 6): ChannelSplitterNode { + return new ChannelMergerNode( + this, + this.context.createChannelMerger(numberOfInputs) + ); + } + createBufferSource( options?: AudioBufferBaseSourceNodeOptions ): AudioBufferSourceNode { diff --git a/packages/react-native-audio-api/src/core/ChannelMergerNode.ts b/packages/react-native-audio-api/src/core/ChannelMergerNode.ts new file mode 100644 index 000000000..8e8474252 --- /dev/null +++ b/packages/react-native-audio-api/src/core/ChannelMergerNode.ts @@ -0,0 +1,12 @@ +import { IChannelMergerNode } from '../interfaces'; +import AudioNode from './AudioNode'; +import BaseAudioContext from './BaseAudioContext'; + +export default class ChannelMergerNode extends AudioNode { + readonly numberOfInputs: number; + + constructor(context: BaseAudioContext, merger: IChannelMergerNode) { + super(context, merger); + this.numberOfInputs = merger.numberOfInputs; + } +} diff --git a/packages/react-native-audio-api/src/core/ChannelSplitterNode.ts b/packages/react-native-audio-api/src/core/ChannelSplitterNode.ts new file mode 100644 index 000000000..9f3507ee8 --- /dev/null +++ b/packages/react-native-audio-api/src/core/ChannelSplitterNode.ts @@ -0,0 +1,12 @@ +import { IChannelSplitterNode } from '../interfaces'; +import AudioNode from './AudioNode'; +import BaseAudioContext from './BaseAudioContext'; + +export default class ChannelSplitterNode extends AudioNode { + readonly numberOfOutputs: number; + + constructor(context: BaseAudioContext, splitter: IChannelSplitterNode) { + super(context, splitter); + this.numberOfOutputs = splitter.numberOfOutputs; + } +} diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index 67c5d0299..b07796f54 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -60,6 +60,8 @@ export interface IBaseAudioContext { createGain(): IGainNode; createStereoPanner(): IStereoPannerNode; createBiquadFilter: () => IBiquadFilterNode; + createChannelSplitter: (numberOfOutputs?: number) => IChannelSplitterNode; + createChannelMerger: (numberOfInputs?: number) => IChannelMergerNode; createBufferSource: (pitchCorrection: boolean) => IAudioBufferSourceNode; createBufferQueueSource: ( pitchCorrection: boolean @@ -98,8 +100,17 @@ export interface IAudioNode { readonly channelCountMode: ChannelCountMode; readonly channelInterpretation: ChannelInterpretation; - connect: (destination: IAudioNode | IAudioParam) => void; - disconnect: (destination?: IAudioNode | IAudioParam) => void; + connect( + destination: IAudioNode | IAudioParam, + output?: number, + input?: number + ): void; + + disconnect(): void; + disconnect(output: number): void; + disconnect(destination: IAudioNode | IAudioParam): void; + disconnect(destination: IAudioNode | IAudioParam, output: number): void; + disconnect(destination: IAudioNode, output: number, input: number): void; } export interface IGainNode extends IAudioNode { @@ -124,6 +135,14 @@ export interface IBiquadFilterNode extends IAudioNode { ): void; } +export interface IChannelSplitterNode extends IAudioNode { + readonly numberOfOutputs: number; +} + +export interface IChannelMergerNode extends IAudioNode { + readonly numberOfInputs: number; +} + export interface IAudioDestinationNode extends IAudioNode {} export interface IAudioScheduledSourceNode extends IAudioNode { From e5bc9e2e1537a14c3fcb984968b7aa98b05e0a5e Mon Sep 17 00:00:00 2001 From: miloszwielgus Date: Thu, 16 Oct 2025 10:42:51 +0200 Subject: [PATCH 08/38] feat: implement example for testing --- .../MergerSplitter/MergerSplitter.tsx | 194 ++++++++++++++++++ .../src/examples/MergerSplitter/index.tsx | 1 + apps/common-app/src/examples/index.ts | 10 +- apps/fabric-example/ios/Podfile.lock | 12 +- 4 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 apps/common-app/src/examples/MergerSplitter/MergerSplitter.tsx create mode 100644 apps/common-app/src/examples/MergerSplitter/index.tsx diff --git a/apps/common-app/src/examples/MergerSplitter/MergerSplitter.tsx b/apps/common-app/src/examples/MergerSplitter/MergerSplitter.tsx new file mode 100644 index 000000000..8abeb07f7 --- /dev/null +++ b/apps/common-app/src/examples/MergerSplitter/MergerSplitter.tsx @@ -0,0 +1,194 @@ +import React, { useRef, useState, useEffect, FC } from 'react'; +import { Alert, ActivityIndicator } from 'react-native'; +import { + AudioContext, + GainNode, + AudioBufferSourceNode, + ChannelSplitterNode, + ChannelMergerNode, +} from 'react-native-audio-api'; + +import { Container, Slider, Spacer, Button } from '../../components'; + +// test url pointing to my public repo, to be changed / deleted +const AUDIO_URL = + 'https://github.com/miloszwielgus/test-files/raw/refs/heads/main/example-music-01.ogg'; + +const INITIAL_GAIN = 0.5; +const labelWidth = 100; + +const SplitterMerger: FC = () => { + const [isPlaying, setIsPlaying] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [gain1, setGain1] = useState(INITIAL_GAIN); + const [gain2, setGain2] = useState(INITIAL_GAIN); + const [gain3, setGain3] = useState(INITIAL_GAIN); + const [gain4, setGain4] = useState(INITIAL_GAIN); + + const audioContextRef = useRef(null); + const sourceNodeRef = useRef(null); + const splitterRef = useRef(null); + const mergerRef = useRef(null); + const audioBufferRef = useRef(null); + + const gainNode1Ref = useRef(null); + const gainNode2Ref = useRef(null); + const gainNode3Ref = useRef(null); + const gainNode4Ref = useRef(null); + + const setupAndPlay = async () => { + const context = audioContextRef.current; + if (!context) return; + + if (!audioBufferRef.current) { + setIsLoading(true); + try { + const response = await fetch(AUDIO_URL); + const arrayBuffer = await response.arrayBuffer(); + audioBufferRef.current = await context.decodeAudioData(arrayBuffer); + } catch (error) { + console.error('Failed to fetch or decode audio:', error); + Alert.alert( + 'Error', + 'Could not load the audio file. Check network and URL.' + ); + setIsLoading(false); + return; + } finally { + setIsLoading(false); + } + } + + sourceNodeRef.current = context.createBufferSource(); + sourceNodeRef.current.buffer = audioBufferRef.current; + sourceNodeRef.current.loop = true; + + splitterRef.current = context.createChannelSplitter(4); + mergerRef.current = context.createChannelMerger(4); + + gainNode1Ref.current = context.createGain(); + gainNode1Ref.current.gain.value = gain1; + splitterRef.current.connect(gainNode1Ref.current, 0, 0); + gainNode1Ref.current.connect(mergerRef.current, 0, 0); + + gainNode2Ref.current = context.createGain(); + gainNode2Ref.current.gain.value = gain2; + splitterRef.current.connect(gainNode2Ref.current, 1, 0); + gainNode2Ref.current.connect(mergerRef.current, 0, 1); + + gainNode3Ref.current = context.createGain(); + gainNode3Ref.current.gain.value = gain3; + splitterRef.current.connect(gainNode3Ref.current, 2, 0); + gainNode3Ref.current.connect(mergerRef.current, 0, 2); + + gainNode4Ref.current = context.createGain(); + gainNode4Ref.current.gain.value = gain4; + splitterRef.current.connect(gainNode4Ref.current, 3, 0); + gainNode4Ref.current.connect(mergerRef.current, 0, 3); + + + sourceNodeRef.current.connect(splitterRef.current); + mergerRef.current.connect(context.destination); + sourceNodeRef.current.start(0); + setIsPlaying(true); + }; + + const stopPlayback = () => { + if (sourceNodeRef.current) { + sourceNodeRef.current.stop(0); + sourceNodeRef.current.disconnect(); + sourceNodeRef.current = null; + } + setIsPlaying(false); + }; + + const handlePlayPause = async () => { + if (isPlaying) { + stopPlayback(); + } else { + await setupAndPlay(); + } + }; + + const handleGain1Change = (value: number) => { + setGain1(value); + if (gainNode1Ref.current) gainNode1Ref.current.gain.value = value; + }; + const handleGain2Change = (value: number) => { + setGain2(value); + if (gainNode2Ref.current) gainNode2Ref.current.gain.value = value; + }; + const handleGain3Change = (value: number) => { + setGain3(value); + if (gainNode3Ref.current) gainNode3Ref.current.gain.value = value; + }; + const handleGain4Change = (value: number) => { + setGain4(value); + if (gainNode4Ref.current) gainNode4Ref.current.gain.value = value; + }; + + useEffect(() => { + if (!audioContextRef.current) { + audioContextRef.current = new AudioContext(); + } + return () => { + stopPlayback(); + audioContextRef.current?.close(); + }; + }, []); + + return ( + + {isLoading && } +