Skip to content

Commit a010057

Browse files
committed
feat: added bridge node
1 parent 8274a95 commit a010057

File tree

8 files changed

+865
-2
lines changed

8 files changed

+865
-2
lines changed

packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/AudioGraph.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ inline bool AudioGraph::empty() const {
166166
}
167167

168168
inline auto AudioGraph::iter() {
169-
return nodes | std::views::transform([this](Node &node) {
169+
return nodes |
170+
std::views::filter([](const Node &n) { return n.handle->audioNode->isProcessable(); }) |
171+
std::views::transform([this](Node &node) {
170172
return Entry{
171173
*node.handle->audioNode,
172174
pool_.view(node.input_head) |
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#pragma once
2+
3+
#include <audioapi/core/utils/graph/GraphObject.hpp>
4+
5+
namespace audioapi {
6+
class AudioParam;
7+
}
8+
9+
namespace audioapi::utils::graph {
10+
11+
/// @brief Lightweight graph-only node that represents an AudioParam connection.
12+
///
13+
/// A BridgeNode sits between a source AudioNode and the owner AudioNode of a
14+
/// param, forming the path: source → bridge → owner. This lets the graph
15+
/// system detect cycles and compute correct topological ordering for param
16+
/// connections without creating real ownership dependencies.
17+
///
18+
/// BridgeNodes are:
19+
/// - **Not processable** — skipped by `AudioGraph::iter()`.
20+
/// - **Always destructible** — removed by compaction when orphaned with no inputs.
21+
/// - **Non-owning** — stores a raw `AudioParam*` whose lifetime is guaranteed
22+
/// by the owner node.
23+
class BridgeNode final : public GraphObject {
24+
public:
25+
explicit BridgeNode(AudioParam *param) : param_(param) {}
26+
27+
[[nodiscard]] bool isProcessable() const override {
28+
return false;
29+
}
30+
31+
[[nodiscard]] bool canBeDestructed() const override {
32+
return true;
33+
}
34+
35+
/// @brief Returns the param this bridge represents a connection to.
36+
[[nodiscard]] AudioParam *param() const {
37+
return param_;
38+
}
39+
40+
private:
41+
AudioParam *param_; // non-owning — lifetime guaranteed by owner node
42+
};
43+
44+
} // namespace audioapi::utils::graph

packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/Graph.hpp

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <audioapi/core/utils/graph/AudioGraph.hpp>
4+
#include <audioapi/core/utils/graph/BridgeNode.hpp>
45
#include <audioapi/core/utils/graph/Disposer.hpp>
56
#include <audioapi/core/utils/graph/HostGraph.hpp>
67
#include <audioapi/core/utils/graph/InputPool.hpp>
@@ -13,8 +14,13 @@
1314
#include <algorithm>
1415
#include <cstdint>
1516
#include <memory>
17+
#include <unordered_map>
1618
#include <utility>
1719

20+
namespace audioapi {
21+
class AudioParam;
22+
}
23+
1824
namespace audioapi::utils::graph {
1925

2026
/// @brief Thread-safe graph coordinator that bridges HostGraph (main thread)
@@ -165,6 +171,115 @@ class Graph {
165171
});
166172
}
167173

174+
// ── Param bridge API ───────────────────────────────────────────────────
175+
176+
/// @brief Creates a bridge node representing: source → bridge → owner.
177+
///
178+
/// The bridge encodes a param connection in the graph for cycle detection
179+
/// and topological ordering. The bridge itself is not processable.
180+
///
181+
/// @param source the node whose output feeds the param
182+
/// @param owner the node that owns the param
183+
/// @param param raw pointer to the AudioParam (lifetime guaranteed by owner)
184+
/// @return Ok on success, Err on cycle/duplicate/not-found
185+
Res connectParam(HNode *source, HNode *owner, AudioParam *param) {
186+
hostGraph.collectDisposedNodes();
187+
188+
BridgeKey key{source, param};
189+
if (bridgeMap_.count(key)) {
190+
return Res::Err(ResultError::EDGE_ALREADY_EXISTS);
191+
}
192+
193+
// Create bridge node
194+
auto bridgeObj = std::make_unique<BridgeNode>(param);
195+
auto bridgeHandle = std::make_shared<NodeHandle>(0, std::move(bridgeObj));
196+
auto [bridgeHostNode, addEvent] = hostGraph.addNode(bridgeHandle);
197+
198+
// source → bridge
199+
auto edgeRes1 = hostGraph.addEdge(source, bridgeHostNode);
200+
if (edgeRes1.is_err()) {
201+
// Rollback: remove bridge node
202+
(void)hostGraph.removeNode(bridgeHostNode);
203+
return Res::Err(edgeRes1.unwrap_err());
204+
}
205+
206+
// bridge → owner
207+
auto edgeRes2 = hostGraph.addEdge(bridgeHostNode, owner);
208+
if (edgeRes2.is_err()) {
209+
// Rollback: remove source→bridge edge and bridge node
210+
(void)hostGraph.removeEdge(source, bridgeHostNode);
211+
(void)hostGraph.removeNode(bridgeHostNode);
212+
return Res::Err(edgeRes2.unwrap_err());
213+
}
214+
215+
// All succeeded — send events through SPSC
216+
sendNodeGrowIfNeeded();
217+
eventSender_.send(std::move(addEvent));
218+
219+
sendPoolGrowIfNeeded();
220+
eventSender_.send(std::move(edgeRes1).unwrap());
221+
222+
sendPoolGrowIfNeeded();
223+
eventSender_.send(std::move(edgeRes2).unwrap());
224+
225+
// Track bridge
226+
bridgeMap_[key] = bridgeHostNode;
227+
bridgeOwners_[bridgeHostNode] = owner;
228+
229+
return Res::Ok(NoneType{});
230+
}
231+
232+
/// @brief Removes a bridge node for the given (source, param) pair.
233+
Res disconnectParam(HNode *source, HNode * /*owner*/, AudioParam *param) {
234+
hostGraph.collectDisposedNodes();
235+
236+
BridgeKey key{source, param};
237+
auto it = bridgeMap_.find(key);
238+
if (it == bridgeMap_.end()) {
239+
return Res::Err(ResultError::EDGE_NOT_FOUND);
240+
}
241+
242+
HNode *bridge = it->second;
243+
removeBridge(source, bridge);
244+
bridgeMap_.erase(it);
245+
246+
return Res::Ok(NoneType{});
247+
}
248+
249+
/// @brief Removes a node and cascade-removes any bridges where this node
250+
/// is the source or owner.
251+
Res removeNodeWithBridges(HNode *node) {
252+
hostGraph.collectDisposedNodes();
253+
254+
// Cascade: remove bridges where this node is source
255+
for (auto it = bridgeMap_.begin(); it != bridgeMap_.end();) {
256+
if (it->first.source == node) {
257+
HNode *bridge = it->second;
258+
removeBridge(node, bridge);
259+
bridgeOwners_.erase(bridge);
260+
it = bridgeMap_.erase(it);
261+
} else {
262+
++it;
263+
}
264+
}
265+
266+
// Cascade: remove bridges where this node is owner
267+
for (auto it = bridgeMap_.begin(); it != bridgeMap_.end();) {
268+
auto ownerIt = bridgeOwners_.find(it->second);
269+
if (ownerIt != bridgeOwners_.end() && ownerIt->second == node) {
270+
HNode *bridge = it->second;
271+
HNode *source = it->first.source;
272+
removeBridge(source, bridge);
273+
bridgeOwners_.erase(ownerIt);
274+
it = bridgeMap_.erase(it);
275+
} else {
276+
++it;
277+
}
278+
}
279+
280+
return removeNode(node);
281+
}
282+
168283
private:
169284
static constexpr size_t kDisposerPayloadSize = HostGraph::kDisposerPayloadSize;
170285

@@ -223,6 +338,57 @@ class Graph {
223338
}
224339
}
225340

341+
// ── Bridge tracking (main thread only) ──────────────────────────────────
342+
343+
struct BridgeKey {
344+
HNode *source;
345+
AudioParam *param;
346+
347+
bool operator==(const BridgeKey &other) const {
348+
return source == other.source && param == other.param;
349+
}
350+
};
351+
352+
struct BridgeKeyHash {
353+
size_t operator()(const BridgeKey &k) const {
354+
auto h1 = std::hash<HNode *>{}(k.source);
355+
auto h2 = std::hash<AudioParam *>{}(k.param);
356+
return h1 ^ (h2 << 1);
357+
}
358+
};
359+
360+
/// Maps (source, param) → bridge host node
361+
std::unordered_map<BridgeKey, HNode *, BridgeKeyHash> bridgeMap_;
362+
363+
/// Maps bridge host node → owner host node (for cascade removal)
364+
std::unordered_map<HNode *, HNode *> bridgeOwners_;
365+
366+
/// @brief Removes a bridge node: tears down edges and marks for removal.
367+
void removeBridge(HNode *source, HNode *bridge) {
368+
// Find the owner from bridgeOwners_
369+
auto ownerIt = bridgeOwners_.find(bridge);
370+
HNode *owner = (ownerIt != bridgeOwners_.end()) ? ownerIt->second : nullptr;
371+
372+
// Remove edges: source→bridge, bridge→owner
373+
auto res1 = hostGraph.removeEdge(source, bridge);
374+
if (res1.is_ok()) {
375+
eventSender_.send(std::move(res1).unwrap());
376+
}
377+
378+
if (owner) {
379+
auto res2 = hostGraph.removeEdge(bridge, owner);
380+
if (res2.is_ok()) {
381+
eventSender_.send(std::move(res2).unwrap());
382+
}
383+
}
384+
385+
// Remove bridge node
386+
auto res3 = hostGraph.removeNode(bridge);
387+
if (res3.is_ok()) {
388+
eventSender_.send(std::move(res3).unwrap());
389+
}
390+
}
391+
226392
friend class GraphTest;
227393
};
228394

packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/GraphObject.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ class GraphObject {
3030
return true;
3131
}
3232

33+
/// @brief Returns whether this node should be processed during audio iteration.
34+
///
35+
/// Default is true. BridgeNodes override to return false — they exist only
36+
/// for graph structure (cycle detection, topo ordering) and are skipped
37+
/// by AudioGraph::iter().
38+
[[nodiscard]] virtual bool isProcessable() const {
39+
return true;
40+
}
41+
3342
/// @brief Downcast helper for node-specific handling.
3443
[[nodiscard]] virtual AudioNode *asAudioNode() {
3544
return nullptr;

packages/react-native-audio-api/common/cpp/audioapi/core/utils/graph/HostNode.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ class HostNode {
103103
return graph_->removeEdge(node_, other.node_);
104104
}
105105

106+
/// @brief Connects this node's output to a param on the owner node via a bridge.
107+
/// @return Ok on success, Err on cycle / duplicate / not-found
108+
Res connectParam(HostNode &owner, AudioParam *param) {
109+
return graph_->connectParam(node_, owner.node_, param);
110+
}
111+
112+
/// @brief Disconnects this node's output from a param on the owner node.
113+
/// @return Ok on success, Err on not-found
114+
Res disconnectParam(HostNode &owner, AudioParam *param) {
115+
return graph_->disconnectParam(node_, owner.node_, param);
116+
}
117+
106118
/// @brief Returns the raw HostGraph::Node pointer (for advanced usage / testing).
107119
[[nodiscard]] HNode *rawNode() const {
108120
return node_;

0 commit comments

Comments
 (0)