react-native-audio-api 0.7.2-nightly-31b46b8-20250819 → 0.7.2-nightly-c06331b-20250821

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,6 +77,8 @@ void BiquadFilterNode::getFrequencyResponse(
77
77
  float *magResponseOutput,
78
78
  float *phaseResponseOutput,
79
79
  const int length) {
80
+ applyFilter();
81
+
80
82
  // Local copies for micro-optimization
81
83
  float b0 = b0_;
82
84
  float b1 = b1_;
@@ -91,7 +93,7 @@ void BiquadFilterNode::getFrequencyResponse(
91
93
  continue;
92
94
  }
93
95
 
94
- auto omega = -PI * frequencyArray[i];
96
+ auto omega = -PI * frequencyArray[i] / context_->getNyquistFrequency();
95
97
  auto z = std::complex<float>(cos(omega), sin(omega));
96
98
  auto response = (b0 + (b1 + b2 * z) * z) /
97
99
  (std::complex<float>(1, 0) + (a1 + a2 * z) * z);
@@ -101,13 +103,6 @@ void BiquadFilterNode::getFrequencyResponse(
101
103
  }
102
104
  }
103
105
 
104
- void BiquadFilterNode::resetCoefficients() {
105
- x1_ = 0.0;
106
- x2_ = 0.0;
107
- y1_ = 0.0;
108
- y2_ = 0.0;
109
- }
110
-
111
106
  void BiquadFilterNode::setNormalizedCoefficients(
112
107
  float b0,
113
108
  float b1,
@@ -163,7 +158,7 @@ void BiquadFilterNode::setHighpassCoefficients(float frequency, float Q) {
163
158
  float theta = PI * frequency;
164
159
  float alpha = std::sin(theta) / (2 * g);
165
160
  float cosW = std::cos(theta);
166
- float beta = (1 - cosW) / 2;
161
+ float beta = (1 + cosW) / 2;
167
162
 
168
163
  setNormalizedCoefficients(
169
164
  beta, -2 * beta, beta, 1 + alpha, -2 * cosW, 1 - alpha);
@@ -313,13 +308,17 @@ void BiquadFilterNode::setAllpassCoefficients(float frequency, float Q) {
313
308
  1 - alpha, -2 * cosW, 1 + alpha, 1 + alpha, -2 * cosW, 1 - alpha);
314
309
  }
315
310
 
316
- void BiquadFilterNode::updateCoefficientsForFrame(
317
- float frequency,
318
- float detune,
319
- float Q,
320
- float gain) {
321
- float normalizedFrequency = frequency / context_->getNyquistFrequency();
311
+ void BiquadFilterNode::applyFilter() {
312
+ double currentTime = context_->getCurrentTime();
322
313
 
314
+ float frequency =
315
+ frequencyParam_->processKRateParam(RENDER_QUANTUM_SIZE, currentTime);
316
+ float detune =
317
+ detuneParam_->processKRateParam(RENDER_QUANTUM_SIZE, currentTime);
318
+ auto Q = QParam_->processKRateParam(RENDER_QUANTUM_SIZE, currentTime);
319
+ auto gain = gainParam_->processKRateParam(RENDER_QUANTUM_SIZE, currentTime);
320
+
321
+ float normalizedFrequency = frequency / context_->getNyquistFrequency();
323
322
  if (detune != 0.0f) {
324
323
  normalizedFrequency *= std::pow(2.0f, detune / 1200.0f);
325
324
  }
@@ -357,47 +356,43 @@ void BiquadFilterNode::updateCoefficientsForFrame(
357
356
  void BiquadFilterNode::processNode(
358
357
  const std::shared_ptr<AudioBus> &processingBus,
359
358
  int framesToProcess) {
360
- double currentTime = context_->getCurrentTime();
361
- auto frequencyValues =
362
- frequencyParam_->processARateParam(framesToProcess, currentTime)
363
- ->getChannel(0)
364
- ->getData();
365
- auto detuneValues =
366
- detuneParam_->processARateParam(framesToProcess, currentTime)
367
- ->getChannel(0)
368
- ->getData();
369
- auto qValues = QParam_->processARateParam(framesToProcess, currentTime)
370
- ->getChannel(0)
371
- ->getData();
372
- auto gainValues = gainParam_->processARateParam(framesToProcess, currentTime)
373
- ->getChannel(0)
374
- ->getData();
375
-
376
- for (int c = 0; c < processingBus->getNumberOfChannels(); c++) {
377
- float x1 = x1_;
378
- float x2 = x2_;
379
- float y1 = y1_;
380
- float y2 = y2_;
381
-
382
- for (int i = 0; i < framesToProcess; i++) {
383
- updateCoefficientsForFrame(
384
- frequencyValues[i], detuneValues[i], qValues[i], gainValues[i]);
385
-
386
- float input = (*processingBus->getChannel(c))[i];
387
- float output = b0_ * input + b1_ * x1 + b2_ * x2 - a1_ * y1 - a2_ * y2;
388
-
389
- (*processingBus->getChannel(c))[i] = output;
359
+ int numChannels = processingBus->getNumberOfChannels();
360
+
361
+ applyFilter();
362
+
363
+ // local copies for micro-optimization
364
+ float b0 = b0_;
365
+ float b1 = b1_;
366
+ float b2 = b2_;
367
+ float a1 = a1_;
368
+ float a2 = a2_;
369
+
370
+ float x1, x2, y1, y2;
371
+
372
+ for (int c = 0; c < numChannels; ++c) {
373
+ auto channelData = processingBus->getChannel(c)->getData();
374
+
375
+ x1 = x1_;
376
+ x2 = x2_;
377
+ y1 = y1_;
378
+ y2 = y2_;
379
+
380
+ for (int i = 0; i < framesToProcess; ++i) {
381
+ float input = channelData[i];
382
+ float output = b0 * input + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
383
+
384
+ channelData[i] = output;
390
385
 
391
386
  x2 = x1;
392
387
  x1 = input;
393
388
  y2 = y1;
394
389
  y1 = output;
395
390
  }
396
- x1_ = x1;
397
- x2_ = x2;
398
- y1_ = y1;
399
- y2_ = y2;
400
391
  }
392
+ x1_ = x1;
393
+ x2_ = x2;
394
+ y1_ = y1;
395
+ y2_ = y2;
401
396
  }
402
397
 
403
398
  } // namespace audioapi
@@ -54,7 +54,7 @@ class BiquadFilterNode : public AudioNode {
54
54
  float b0_ = 1.0;
55
55
  float b1_ = 0;
56
56
  float b2_ = 0;
57
- float a1_ = 1.0;
57
+ float a1_ = 0;
58
58
  float a2_ = 0;
59
59
 
60
60
  static BiquadFilterType fromString(const std::string &type) {
@@ -105,7 +105,6 @@ class BiquadFilterNode : public AudioNode {
105
105
  }
106
106
  }
107
107
 
108
- void resetCoefficients();
109
108
  void setNormalizedCoefficients(
110
109
  float b0,
111
110
  float b1,
@@ -121,11 +120,7 @@ class BiquadFilterNode : public AudioNode {
121
120
  void setPeakingCoefficients(float frequency, float Q, float gain);
122
121
  void setNotchCoefficients(float frequency, float Q);
123
122
  void setAllpassCoefficients(float frequency, float Q);
124
- void updateCoefficientsForFrame(
125
- float frequency,
126
- float detune,
127
- float q,
128
- float gain);
123
+ void applyFilter();
129
124
  };
130
125
 
131
126
  } // namespace audioapi
@@ -5,50 +5,40 @@
5
5
  namespace audioapi {
6
6
 
7
7
  AudioNodeDestructor::AudioNodeDestructor() {
8
- isExiting_ = false;
9
- nodesForDeconstruction_.reserve(10);
10
- thread_ = std::thread(&AudioNodeDestructor::process, this);
8
+ isExiting_.store(false, std::memory_order_release);
9
+ auto [sender, receiver] = channels::spsc::channel<
10
+ std::shared_ptr<AudioNode>,
11
+ channels::spsc::OverflowStrategy::WAIT_ON_FULL,
12
+ channels::spsc::WaitStrategy::ATOMIC_WAIT>(kChannelCapacity);
13
+ sender_ = std::move(sender);
14
+ workerHandle_ =
15
+ std::thread(&AudioNodeDestructor::process, this, std::move(receiver));
11
16
  }
12
17
 
13
18
  AudioNodeDestructor::~AudioNodeDestructor() {
14
- isExiting_ = true;
19
+ isExiting_.store(true, std::memory_order_release);
15
20
 
16
- cv_.notify_one(); // call process for the last time
17
- if (thread_.joinable()) {
18
- thread_.join();
21
+ // We need to send a nullptr to unblock the receiver
22
+ sender_.send(nullptr);
23
+ if (workerHandle_.joinable()) {
24
+ workerHandle_.join();
19
25
  }
20
26
  }
21
27
 
22
- void AudioNodeDestructor::tryCallWithLock(
23
- const std::function<void()> &callback) {
24
- if (auto lock = Locker::tryLock(mutex_)) {
25
- callback();
26
- }
27
- }
28
-
29
- void AudioNodeDestructor::addNodeForDeconstruction(
30
- const std::shared_ptr<AudioNode> &node) {
31
- // NOTE: this method must be called within `tryCallWithLock`
32
- nodesForDeconstruction_.emplace_back(node);
28
+ bool AudioNodeDestructor::tryAddNodeForDeconstruction(
29
+ std::shared_ptr<AudioNode> &&node) {
30
+ return sender_.try_send(std::move(node)) ==
31
+ channels::spsc::ResponseStatus::SUCCESS;
33
32
  }
34
33
 
35
- void AudioNodeDestructor::notify() {
36
- cv_.notify_one();
37
- }
38
-
39
- void AudioNodeDestructor::process() {
40
- std::unique_lock<std::mutex> lock(mutex_);
41
- while (!isExiting_) {
42
- cv_.wait(lock, [this] {
43
- return isExiting_ || !nodesForDeconstruction_.empty();
44
- });
45
-
46
- if (isExiting_)
47
- break;
48
-
49
- if (!isExiting_ && !nodesForDeconstruction_.empty()) {
50
- nodesForDeconstruction_.clear();
51
- }
34
+ void AudioNodeDestructor::process(
35
+ channels::spsc::Receiver<
36
+ std::shared_ptr<AudioNode>,
37
+ channels::spsc::OverflowStrategy::WAIT_ON_FULL,
38
+ channels::spsc::WaitStrategy::ATOMIC_WAIT> &&receiver) {
39
+ auto rcv = std::move(receiver);
40
+ while (!isExiting_.load(std::memory_order_acquire)) {
41
+ rcv.receive();
52
42
  }
53
43
  }
54
44
 
@@ -1,34 +1,46 @@
1
1
  #pragma once
2
2
 
3
- #include <condition_variable>
4
- #include <mutex>
5
3
  #include <thread>
6
4
  #include <atomic>
7
- #include <functional>
8
5
  #include <vector>
9
6
  #include <memory>
7
+ #include <audioapi/utils/SpscChannel.hpp>
10
8
 
11
9
  namespace audioapi {
12
10
 
13
11
  class AudioNode;
14
12
 
13
+ #define AUDIO_NODE_DESTRUCTOR_SPSC_OPTIONS \
14
+ std::shared_ptr<AudioNode>, \
15
+ channels::spsc::OverflowStrategy::WAIT_ON_FULL, \
16
+ channels::spsc::WaitStrategy::ATOMIC_WAIT
17
+
15
18
  class AudioNodeDestructor {
16
19
  public:
17
20
  AudioNodeDestructor();
18
21
  ~AudioNodeDestructor();
19
22
 
20
- void tryCallWithLock(const std::function<void()> &callback);
21
- void addNodeForDeconstruction(const std::shared_ptr<AudioNode> &node);
22
- void notify();
23
+ /// @brief Adds a node to the deconstruction queue.
24
+ /// @param node The audio node to be deconstructed.
25
+ /// @return True if the node was successfully added, false otherwise.
26
+ /// @note node does NOT get moved out if it is not successfully added.
27
+ bool tryAddNodeForDeconstruction(std::shared_ptr<AudioNode> &&node);
23
28
 
24
29
  private:
25
- mutable std::mutex mutex_;
26
- std::thread thread_;
27
- std::condition_variable cv_;
28
- std::vector<std::shared_ptr<AudioNode>> nodesForDeconstruction_;
30
+ static constexpr size_t kChannelCapacity = 1024;
29
31
 
32
+ std::thread workerHandle_;
30
33
  std::atomic<bool> isExiting_;
31
34
 
32
- void process();
35
+ channels::spsc::Sender<
36
+ AUDIO_NODE_DESTRUCTOR_SPSC_OPTIONS> sender_;
37
+
38
+ /// @brief Processes audio nodes for deconstruction.
39
+ /// @param receiver The receiver channel for incoming audio nodes.
40
+ void process(channels::spsc::Receiver<
41
+ AUDIO_NODE_DESTRUCTOR_SPSC_OPTIONS> &&receiver);
33
42
  };
43
+
44
+ #undef AUDIO_NODE_DESTRUCTOR_SPSC_OPTIONS
45
+
34
46
  } // namespace audioapi
@@ -6,6 +6,80 @@
6
6
 
7
7
  namespace audioapi {
8
8
 
9
+ AudioNodeManager::Event::Event(Event &&other) {
10
+ *this = std::move(other);
11
+ }
12
+
13
+ AudioNodeManager::Event &AudioNodeManager::Event::operator=(Event &&other) {
14
+ if (this != &other) {
15
+ // Clean up current resources
16
+ this->~Event();
17
+
18
+ // Move resources from the other event
19
+ type = other.type;
20
+ payloadType = other.payloadType;
21
+ switch (payloadType) {
22
+ case EventPayloadType::NODES:
23
+ payload.nodes.from = std::move(other.payload.nodes.from);
24
+ payload.nodes.to = std::move(other.payload.nodes.to);
25
+ break;
26
+ case EventPayloadType::PARAMS:
27
+ payload.params.from = std::move(other.payload.params.from);
28
+ payload.params.to = std::move(other.payload.params.to);
29
+ break;
30
+ case EventPayloadType::SOURCE_NODE:
31
+ payload.sourceNode = std::move(other.payload.sourceNode);
32
+ break;
33
+ case EventPayloadType::AUDIO_PARAM:
34
+ payload.audioParam = std::move(other.payload.audioParam);
35
+ break;
36
+ case EventPayloadType::NODE:
37
+ payload.node = std::move(other.payload.node);
38
+ break;
39
+
40
+ default:
41
+ break;
42
+ }
43
+ }
44
+ return *this;
45
+ }
46
+
47
+ AudioNodeManager::Event::~Event() {
48
+ switch (payloadType) {
49
+ case EventPayloadType::NODES:
50
+ payload.nodes.from.~shared_ptr();
51
+ payload.nodes.to.~shared_ptr();
52
+ break;
53
+ case EventPayloadType::PARAMS:
54
+ payload.params.from.~shared_ptr();
55
+ payload.params.to.~shared_ptr();
56
+ break;
57
+ case EventPayloadType::SOURCE_NODE:
58
+ payload.sourceNode.~shared_ptr();
59
+ break;
60
+ case EventPayloadType::AUDIO_PARAM:
61
+ payload.audioParam.~shared_ptr();
62
+ break;
63
+ case EventPayloadType::NODE:
64
+ payload.node.~shared_ptr();
65
+ break;
66
+ }
67
+ }
68
+
69
+ AudioNodeManager::AudioNodeManager() {
70
+ sourceNodes_.reserve(kInitialCapacity);
71
+ processingNodes_.reserve(kInitialCapacity);
72
+ audioParams_.reserve(kInitialCapacity);
73
+
74
+ auto channel_pair = channels::spsc::channel<
75
+ std::unique_ptr<Event>,
76
+ channels::spsc::OverflowStrategy::WAIT_ON_FULL,
77
+ channels::spsc::WaitStrategy::BUSY_LOOP>(kChannelCapacity);
78
+
79
+ sender_ = std::move(channel_pair.first);
80
+ receiver_ = std::move(channel_pair.second);
81
+ }
82
+
9
83
  AudioNodeManager::~AudioNodeManager() {
10
84
  cleanup();
11
85
  }
@@ -14,133 +88,185 @@ void AudioNodeManager::addPendingNodeConnection(
14
88
  const std::shared_ptr<AudioNode> &from,
15
89
  const std::shared_ptr<AudioNode> &to,
16
90
  ConnectionType type) {
17
- Locker lock(getGraphLock());
91
+ auto event = std::make_unique<Event>();
92
+ event->type = type;
93
+ event->payloadType = EventPayloadType::NODES;
94
+ event->payload.nodes.from = from;
95
+ event->payload.nodes.to = to;
18
96
 
19
- audioNodesToConnect_.emplace_back(from, to, type);
97
+ sender_.send(std::move(event));
20
98
  }
21
99
 
22
100
  void AudioNodeManager::addPendingParamConnection(
23
101
  const std::shared_ptr<AudioNode> &from,
24
102
  const std::shared_ptr<AudioParam> &to,
25
103
  ConnectionType type) {
26
- Locker lock(getGraphLock());
104
+ auto event = std::make_unique<Event>();
105
+ event->type = type;
106
+ event->payloadType = EventPayloadType::PARAMS;
107
+ event->payload.params.from = from;
108
+ event->payload.params.to = to;
27
109
 
28
- audioParamToConnect_.emplace_back(from, to, type);
110
+ sender_.send(std::move(event));
29
111
  }
30
112
 
31
113
  void AudioNodeManager::preProcessGraph() {
32
- if (auto locker = Locker::tryLock(getGraphLock())) {
33
- settlePendingConnections();
34
- prepareNodesForDestruction();
35
- }
36
- }
37
-
38
- std::mutex &AudioNodeManager::getGraphLock() {
39
- return graphLock_;
114
+ settlePendingConnections();
115
+ prepareNodesForDestruction(sourceNodes_);
116
+ prepareNodesForDestruction(processingNodes_);
40
117
  }
41
118
 
42
119
  void AudioNodeManager::addProcessingNode(
43
120
  const std::shared_ptr<AudioNode> &node) {
44
- Locker lock(getGraphLock());
45
- processingNodes_.insert(node);
121
+ auto event = std::make_unique<Event>();
122
+ event->type = ConnectionType::ADD;
123
+ event->payloadType = EventPayloadType::NODE;
124
+ event->payload.node = node;
125
+
126
+ sender_.send(std::move(event));
46
127
  }
47
128
 
48
129
  void AudioNodeManager::addSourceNode(
49
130
  const std::shared_ptr<AudioScheduledSourceNode> &node) {
50
- Locker lock(getGraphLock());
51
- sourceNodes_.insert(node);
131
+ auto event = std::make_unique<Event>();
132
+ event->type = ConnectionType::ADD;
133
+ event->payloadType = EventPayloadType::SOURCE_NODE;
134
+ event->payload.sourceNode = node;
135
+
136
+ sender_.send(std::move(event));
52
137
  }
53
138
 
54
139
  void AudioNodeManager::addAudioParam(const std::shared_ptr<AudioParam> &param) {
55
- Locker lock(getGraphLock());
56
- audioParams_.insert(param);
140
+ auto event = std::make_unique<Event>();
141
+ event->type = ConnectionType::ADD;
142
+ event->payloadType = EventPayloadType::AUDIO_PARAM;
143
+ event->payload.audioParam = param;
144
+
145
+ sender_.send(std::move(event));
57
146
  }
58
147
 
59
148
  void AudioNodeManager::settlePendingConnections() {
60
- for (int i = 0; i < audioNodesToConnect_.size(); ++i) {
61
- auto &connection = audioNodesToConnect_[i];
62
- std::shared_ptr<AudioNode> from = std::get<0>(connection);
63
- std::shared_ptr<AudioNode> to = std::get<1>(connection);
64
- ConnectionType type = std::get<2>(connection);
65
-
66
- assert(from != nullptr);
67
-
68
- if (type == ConnectionType::CONNECT) {
69
- assert(to != nullptr);
70
- from->connectNode(to);
71
- } else if (type == ConnectionType::DISCONNECT) {
72
- assert(to != nullptr);
73
- from->disconnectNode(to);
74
- } else {
75
- for (auto it = from->outputNodes_.begin();
76
- it != from->outputNodes_.end();) {
77
- auto next = std::next(it);
78
- from->disconnectNode(*it);
79
- it = next;
80
- }
149
+ std::unique_ptr<Event> value;
150
+ while (receiver_.try_receive(value) !=
151
+ channels::spsc::ResponseStatus::CHANNEL_EMPTY) {
152
+ switch (value->type) {
153
+ case ConnectionType::CONNECT:
154
+ handleConnectEvent(std::move(value));
155
+ break;
156
+ case ConnectionType::DISCONNECT:
157
+ handleDisconnectEvent(std::move(value));
158
+ break;
159
+ case ConnectionType::DISCONNECT_ALL:
160
+ handleDisconnectAllEvent(std::move(value));
161
+ break;
162
+ case ConnectionType::ADD:
163
+ handleAddToDeconstructionEvent(std::move(value));
164
+ break;
81
165
  }
82
166
  }
167
+ }
83
168
 
84
- audioNodesToConnect_.clear();
85
-
86
- for (auto it = audioParamToConnect_.begin(), end = audioParamToConnect_.end();
87
- it != end;
88
- ++it) {
89
- std::shared_ptr<AudioNode> from = std::get<0>(*it);
90
- std::shared_ptr<AudioParam> to = std::get<1>(*it);
91
- ConnectionType type = std::get<2>(*it);
92
-
93
- assert(from != nullptr);
94
- assert(to != nullptr);
169
+ void AudioNodeManager::handleConnectEvent(std::unique_ptr<Event> event) {
170
+ if (event->payloadType == EventPayloadType::NODES) {
171
+ event->payload.nodes.from->connectNode(event->payload.nodes.to);
172
+ } else if (event->payloadType == EventPayloadType::PARAMS) {
173
+ event->payload.params.from->connectParam(event->payload.params.to);
174
+ } else {
175
+ assert(false && "Invalid payload type for connect event");
176
+ }
177
+ }
95
178
 
96
- if (type == ConnectionType::CONNECT) {
97
- from->connectParam(to);
98
- } else {
99
- from->disconnectParam(to);
100
- }
179
+ void AudioNodeManager::handleDisconnectEvent(std::unique_ptr<Event> event) {
180
+ if (event->payloadType == EventPayloadType::NODES) {
181
+ event->payload.nodes.from->disconnectNode(event->payload.nodes.to);
182
+ } else if (event->payloadType == EventPayloadType::PARAMS) {
183
+ event->payload.params.from->disconnectParam(event->payload.params.to);
184
+ } else {
185
+ assert(false && "Invalid payload type for disconnect event");
101
186
  }
187
+ }
102
188
 
103
- audioParamToConnect_.clear();
189
+ void AudioNodeManager::handleDisconnectAllEvent(std::unique_ptr<Event> event) {
190
+ assert(event->payloadType == EventPayloadType::NODES);
191
+ for (auto it = event->payload.nodes.from->outputNodes_.begin();
192
+ it != event->payload.nodes.from->outputNodes_.end();) {
193
+ auto next = std::next(it);
194
+ event->payload.nodes.from->disconnectNode(*it);
195
+ it = next;
196
+ }
104
197
  }
105
198
 
106
- void AudioNodeManager::cleanupNode(const std::shared_ptr<AudioNode> &node) {
107
- nodeDeconstructor_.addNodeForDeconstruction(node);
108
- node->cleanup();
199
+ void AudioNodeManager::handleAddToDeconstructionEvent(
200
+ std::unique_ptr<Event> event) {
201
+ switch (event->payloadType) {
202
+ case EventPayloadType::NODE:
203
+ processingNodes_.push_back(event->payload.node);
204
+ break;
205
+ case EventPayloadType::SOURCE_NODE:
206
+ sourceNodes_.push_back(event->payload.sourceNode);
207
+ break;
208
+ case EventPayloadType::AUDIO_PARAM:
209
+ audioParams_.push_back(event->payload.audioParam);
210
+ break;
211
+ default:
212
+ assert(false && "Unknown event payload type");
213
+ }
109
214
  }
110
215
 
111
- void AudioNodeManager::prepareNodesForDestruction() {
112
- nodeDeconstructor_.tryCallWithLock([this]() {
113
- auto sNodesIt = sourceNodes_.begin();
216
+ template <typename U>
217
+ void AudioNodeManager::prepareNodesForDestruction(
218
+ std::vector<std::shared_ptr<U>> &vec) {
219
+ if (vec.empty()) {
220
+ return;
221
+ }
222
+ /// An example of input-output
223
+ /// for simplicity we will be considering vector where each value represents
224
+ /// use_count() of an element vec = [1, 2, 1, 3, 1] our end result will be vec
225
+ /// = [2, 3, 1, 1, 1] After this operation all nodes with use_count() == 1
226
+ /// will be at the end and we will try to send them After sending, we will
227
+ /// only keep nodes with use_count() > 1 or which failed vec = [2, 3, failed,
228
+ /// sent, sent] // failed will be always before sents vec = [2, 3, failed] and
229
+ /// we resize
230
+ /// @note if there are no nodes with use_count() == 1 `begin` will be equal to
231
+ /// vec.size()
232
+ /// @note if all nodes have use_count() == 1 `begin` will be 0
114
233
 
115
- while (sNodesIt != sourceNodes_.end()) {
116
- // we don't want to destroy nodes that are still playing or will be
117
- // playing
118
- if (sNodesIt->use_count() == 1 &&
119
- (sNodesIt->get()->isUnscheduled() || sNodesIt->get()->isFinished())) {
120
- cleanupNode(*sNodesIt);
121
- sNodesIt = sourceNodes_.erase(sNodesIt);
122
- } else {
123
- ++sNodesIt;
124
- }
234
+ int begin = 0;
235
+ int end = vec.size() - 1; // can be -1 (edge case)
236
+
237
+ // Moves all nodes with use_count() == 1 to the end
238
+ // nodes in range [begin, vec.size()) should be deleted
239
+ // so new size of the vector will be `begin`
240
+ while (begin <= end) {
241
+ while (begin < end && vec[end].use_count() == 1) {
242
+ end--;
243
+ }
244
+ if (vec[begin].use_count() == 1) {
245
+ std::swap(vec[begin], vec[end]);
246
+ end--;
125
247
  }
248
+ begin++;
249
+ }
126
250
 
127
- auto pNodesIt = processingNodes_.begin();
251
+ for (int i = begin; i < vec.size(); i++) {
252
+ if (vec[i])
253
+ vec[i]->cleanup();
128
254
 
129
- while (pNodesIt != processingNodes_.end()) {
130
- if (pNodesIt->use_count() == 1) {
131
- cleanupNode(*pNodesIt);
132
- pNodesIt = processingNodes_.erase(pNodesIt);
133
- } else {
134
- ++pNodesIt;
135
- }
255
+ /// If we fail to add we can't safely remove the node from the vector
256
+ /// so we swap it and advance begin cursor
257
+ /// @note vec[i] does NOT get moved out if it is not successfully added.
258
+ if (!nodeDeconstructor_.tryAddNodeForDeconstruction(std::move(vec[i]))) {
259
+ std::swap(vec[i], vec[begin]);
260
+ begin++;
136
261
  }
137
- });
138
- nodeDeconstructor_.notify();
262
+ }
263
+ if (begin < vec.size()) {
264
+ // it does not realocate if newer size is < current size
265
+ vec.resize(begin);
266
+ }
139
267
  }
140
268
 
141
269
  void AudioNodeManager::cleanup() {
142
- Locker lock(getGraphLock());
143
-
144
270
  for (auto it = sourceNodes_.begin(), end = sourceNodes_.end(); it != end;
145
271
  ++it) {
146
272
  it->get()->cleanup();
@@ -154,6 +280,7 @@ void AudioNodeManager::cleanup() {
154
280
 
155
281
  sourceNodes_.clear();
156
282
  processingNodes_.clear();
283
+ audioParams_.clear();
157
284
  }
158
285
 
159
286
  } // namespace audioapi
@@ -6,7 +6,8 @@
6
6
  #include <mutex>
7
7
  #include <tuple>
8
8
  #include <vector>
9
- #include <unordered_set>
9
+
10
+ #include <audioapi/utils/SpscChannel.hpp>
10
11
 
11
12
  namespace audioapi {
12
13
 
@@ -14,56 +15,119 @@ class AudioNode;
14
15
  class AudioScheduledSourceNode;
15
16
  class AudioParam;
16
17
 
18
+ #define AUDIO_NODE_MANAGER_SPSC_OPTIONS \
19
+ std::unique_ptr<Event>, \
20
+ channels::spsc::OverflowStrategy::WAIT_ON_FULL, \
21
+ channels::spsc::WaitStrategy::BUSY_LOOP
22
+
17
23
  class AudioNodeManager {
18
24
  public:
19
- enum class ConnectionType { CONNECT, DISCONNECT, DISCONNECT_ALL };
20
- AudioNodeManager() = default;
25
+ enum class ConnectionType { CONNECT, DISCONNECT, DISCONNECT_ALL, ADD };
26
+ typedef ConnectionType EventType; // for backwards compatibility
27
+ enum class EventPayloadType { NODES, PARAMS, SOURCE_NODE, AUDIO_PARAM, NODE };
28
+ union EventPayload {
29
+ struct {
30
+ std::shared_ptr<AudioNode> from;
31
+ std::shared_ptr<AudioNode> to;
32
+ } nodes;
33
+ struct {
34
+ std::shared_ptr<AudioNode> from;
35
+ std::shared_ptr<AudioParam> to;
36
+ } params;
37
+ std::shared_ptr<AudioScheduledSourceNode> sourceNode;
38
+ std::shared_ptr<AudioParam> audioParam;
39
+ std::shared_ptr<AudioNode> node;
40
+
41
+ // Default constructor that initializes the first member
42
+ EventPayload() : nodes{} {}
43
+
44
+ // Destructor - we'll handle cleanup explicitly in Event destructor
45
+ ~EventPayload() {}
46
+ };
47
+ struct Event {
48
+ EventType type;
49
+ EventPayloadType payloadType;
50
+ EventPayload payload;
51
+
52
+ Event(Event&& other);
53
+ Event& operator=(Event&& other);
54
+ Event() : type(ConnectionType::CONNECT), payloadType(EventPayloadType::NODES), payload() {}
55
+ ~Event();
56
+ };
57
+
58
+ AudioNodeManager();
21
59
  ~AudioNodeManager();
22
60
 
23
61
  void preProcessGraph();
24
62
 
63
+ /// @brief Adds a pending connection between two audio nodes.
64
+ /// @param from The source audio node.
65
+ /// @param to The destination audio node.
66
+ /// @param type The type of connection (connect/disconnect).
67
+ /// @note Should be only used from JavaScript/HostObjects thread
25
68
  void addPendingNodeConnection(
26
69
  const std::shared_ptr<AudioNode> &from,
27
70
  const std::shared_ptr<AudioNode> &to,
28
71
  ConnectionType type);
29
72
 
73
+ /// @brief Adds a pending connection between an audio node and an audio parameter.
74
+ /// @param from The source audio node.
75
+ /// @param to The destination audio parameter.
76
+ /// @param type The type of connection (connect/disconnect).
77
+ /// @note Should be only used from JavaScript/HostObjects thread
30
78
  void addPendingParamConnection(
31
79
  const std::shared_ptr<AudioNode> &from,
32
80
  const std::shared_ptr<AudioParam> &to,
33
81
  ConnectionType type);
34
82
 
83
+ /// @brief Adds a processing node to the manager.
84
+ /// @param node The processing node to add.
85
+ /// @note Should be only used from JavaScript/HostObjects thread
35
86
  void addProcessingNode(const std::shared_ptr<AudioNode> &node);
87
+
88
+ /// @brief Adds a source node to the manager.
89
+ /// @param node The source node to add.
90
+ /// @note Should be only used from JavaScript/HostObjects thread
36
91
  void addSourceNode(const std::shared_ptr<AudioScheduledSourceNode> &node);
92
+
93
+ /// @brief Adds an audio parameter to the manager.
94
+ /// @param param The audio parameter to add.
95
+ /// @note Should be only used from JavaScript/HostObjects thread
37
96
  void addAudioParam(const std::shared_ptr<AudioParam> &param);
38
97
 
39
98
  void cleanup();
40
99
 
41
100
  private:
42
- std::mutex graphLock_;
43
101
  AudioNodeDestructor nodeDeconstructor_;
44
102
 
45
- // all nodes created in the context
46
- std::unordered_set<std::shared_ptr<AudioScheduledSourceNode>> sourceNodes_;
47
- std::unordered_set<std::shared_ptr<AudioNode>> processingNodes_;
48
- std::unordered_set<std::shared_ptr<AudioParam>> audioParams_;
49
-
50
- // connections to be settled
51
- std::vector<std::tuple<
52
- std::shared_ptr<AudioNode>,
53
- std::shared_ptr<AudioNode>,
54
- ConnectionType>>
55
- audioNodesToConnect_;
56
-
57
- std::vector<std::tuple<
58
- std::shared_ptr<AudioNode>,
59
- std::shared_ptr<AudioParam>,
60
- ConnectionType>>
61
- audioParamToConnect_;
62
-
63
- std::mutex &getGraphLock();
103
+ /// @brief Initial capacity for various node types for deletion
104
+ /// @note Higher capacity decreases number of reallocations at runtime (can be easily adjusted to 128 if needed)
105
+ static constexpr size_t kInitialCapacity = 32;
106
+
107
+ /// @brief Initial capacity for event passing channel
108
+ /// @note High value reduces wait time for sender (JavaScript/HostObjects thread here)
109
+ static constexpr size_t kChannelCapacity = 1024;
110
+
111
+ std::vector<std::shared_ptr<AudioScheduledSourceNode>> sourceNodes_;
112
+ std::vector<std::shared_ptr<AudioNode>> processingNodes_;
113
+ std::vector<std::shared_ptr<AudioParam>> audioParams_;
114
+
115
+ channels::spsc::Receiver<
116
+ AUDIO_NODE_MANAGER_SPSC_OPTIONS> receiver_;
117
+
118
+ channels::spsc::Sender<
119
+ AUDIO_NODE_MANAGER_SPSC_OPTIONS> sender_;
120
+
64
121
  void settlePendingConnections();
65
- void cleanupNode(const std::shared_ptr<AudioNode> &node);
66
- void prepareNodesForDestruction();
122
+ void handleConnectEvent(std::unique_ptr<Event> event);
123
+ void handleDisconnectEvent(std::unique_ptr<Event> event);
124
+ void handleDisconnectAllEvent(std::unique_ptr<Event> event);
125
+ void handleAddToDeconstructionEvent(std::unique_ptr<Event> event);
126
+
127
+ template <typename U>
128
+ void prepareNodesForDestruction(std::vector<std::shared_ptr<U>> &vec);
67
129
  };
68
130
 
131
+ #undef AUDIO_NODE_MANAGER_SPSC_OPTIONS
132
+
69
133
  } // namespace audioapi
@@ -0,0 +1,58 @@
1
+ #pragma once
2
+
3
+ /// Benchmarking utilities
4
+ #include <chrono>
5
+ #include <unordered_map>
6
+
7
+ #ifdef ANDROID
8
+ #include <android/log.h>
9
+ #endif
10
+
11
+ namespace audioapi::benchmarks {
12
+
13
+ /// @brief Gets the execution time of a function
14
+ /// @tparam Func The type of the function to benchmark
15
+ /// @param func The function to benchmark
16
+ /// @return The duration of the function execution in nanoseconds
17
+ /// @note This function is safe to use across threads
18
+ template <typename Func>
19
+ double getExecutionTime(Func&& func) {
20
+ auto start = std::chrono::high_resolution_clock::now();
21
+ std::forward<Func>(func)();
22
+ auto end = std::chrono::high_resolution_clock::now();
23
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
24
+ }
25
+
26
+ /// @brief Logs the average execution time of a function
27
+ /// @tparam Func The type of the function to benchmark
28
+ /// @param msg The message to log
29
+ /// @param func The function to benchmark
30
+ /// @return The duration of the function execution in nanoseconds
31
+ /// @note This function should not be used in production
32
+ /// @note This function is not thread-safe and should be used preferably once in a codebase when you need to measure performance
33
+ template <typename Func>
34
+ double logAvgExecutionTime(const std::string& msg, Func&& func) {
35
+ double duration = getExecutionTime(std::forward<Func>(func));
36
+
37
+ static std::unordered_map<std::string, double> durationsSum;
38
+ static std::unordered_map<std::string, int> durationsCount;
39
+
40
+ // Ensure initialization for first time
41
+ if (durationsSum.find(msg) == durationsSum.end()) {
42
+ durationsSum[msg] = 0.0;
43
+ durationsCount[msg] = 0;
44
+ }
45
+
46
+ durationsSum[msg] += duration;
47
+ durationsCount[msg]++;
48
+
49
+ long long avgDuration = static_cast<long long>(durationsSum[msg] / durationsCount[msg]);
50
+
51
+ #ifdef ANDROID
52
+ __android_log_print(ANDROID_LOG_INFO, "AudioAPI", "%s: %lld ns", msg.c_str(), avgDuration);
53
+ #else
54
+ printf("%s: %lld ns\n", msg.c_str(), avgDuration);
55
+ #endif
56
+ return duration;
57
+ }
58
+ }
@@ -0,0 +1,397 @@
1
+ #pragma once
2
+
3
+ #include <atomic>
4
+ #include <memory>
5
+ #include <algorithm>
6
+ #include <thread>
7
+ #include <type_traits>
8
+
9
+
10
+ namespace audioapi::channels::spsc {
11
+
12
+ /// @brief Overflow strategy for sender when the channel is full
13
+ enum class OverflowStrategy {
14
+ /// @brief Block and wait for space (default behavior)
15
+ WAIT_ON_FULL,
16
+
17
+ /// @brief Overwrite the oldest unread element
18
+ OVERWRITE_ON_FULL
19
+ };
20
+
21
+ /// @brief Wait strategy for receiver and sender when looping and trying
22
+ enum class WaitStrategy {
23
+ /// @brief Busy loop waiting strategy
24
+ /// @note should be used when low latency is required and channel is not expected to wait
25
+ /// @note should be definitely used with OverflowStrategy::OVERWRITE_ON_FULL
26
+ /// @note it uses `asm volatile ("" ::: "memory")` to prevent harmful compiler optimizations
27
+ BUSY_LOOP,
28
+
29
+ /// @brief Yielding waiting strategy
30
+ /// @note should be used when low latency is not critical and channel is expected to wait
31
+ /// @note it uses std::this_thread::yield under the hood
32
+ YIELD,
33
+
34
+ /// @brief Atomic waiting strategy
35
+ /// @note should be used when low latency is required and channel is expected to wait for longer
36
+ /// @note it uses std::atomic_wait under the hood
37
+ ATOMIC_WAIT,
38
+ };
39
+
40
+ /// @brief Response status for channel operations
41
+ enum class ResponseStatus {
42
+ SUCCESS,
43
+ CHANNEL_FULL,
44
+ CHANNEL_EMPTY,
45
+
46
+ /// @brief Indicates that the last value is being overwritten or read so try fails
47
+ /// @note This status is only returned if given channel supports OVERWRITE_ON_FULL strategy
48
+ SKIP_DUE_TO_OVERWRITE
49
+ };
50
+
51
+ template<typename T, OverflowStrategy Strategy, WaitStrategy Wait>
52
+ class Sender;
53
+ template<typename T, OverflowStrategy Strategy, WaitStrategy Wait>
54
+ class Receiver;
55
+ template<typename T, OverflowStrategy Strategy, WaitStrategy Wait>
56
+ class InnerChannel;
57
+
58
+ /// @brief Create a bounded single-producer, single-consumer channel
59
+ /// @param capacity The minimum capacity of the channel, real capacity will be equal to the closest higher or equal power of two - 1. So for example, if capacity = 12 then channel will hold 15 elements.
60
+ /// @tparam T The type of values sent through the channel
61
+ /// @tparam Strategy The overflow strategy (default: WAIT_ON_FULL)
62
+ /// @tparam Wait The wait strategy used when looping and trying to send or receive (default: BUSY_LOOP)
63
+ /// @return A pair of sender and receiver for the channel
64
+ template <typename T, OverflowStrategy Strategy = OverflowStrategy::WAIT_ON_FULL, WaitStrategy Wait = WaitStrategy::BUSY_LOOP>
65
+ std::pair<Sender<T, Strategy, Wait>, Receiver<T, Strategy, Wait>> channel(size_t capacity) {
66
+ auto channel = std::make_shared<InnerChannel<T, Strategy, Wait>>(capacity);
67
+ return { Sender<T, Strategy, Wait>(channel), Receiver<T, Strategy, Wait>(channel) };
68
+ }
69
+
70
+ /// @brief Sender for a single-producer, single-consumer channel
71
+ /// @tparam T The type of values sent through the channel
72
+ /// @tparam Strategy The overflow strategy used by the channel
73
+ /// It allows to send values to the channel. It is designed to be used only from one thread at a time.
74
+ template <typename T, OverflowStrategy Strategy = OverflowStrategy::WAIT_ON_FULL, WaitStrategy Wait = WaitStrategy::BUSY_LOOP>
75
+ class Sender {
76
+ /// Disallows sender creation outside of channel function
77
+ explicit Sender(std::shared_ptr<InnerChannel<T, Strategy, Wait>> chan) : channel_(chan) {}
78
+ public:
79
+ /// @brief Default constructor
80
+ /// @note required to have sender as class member
81
+ Sender() = default;
82
+ Sender(const Sender&) = delete;
83
+ Sender& operator=(const Sender&) = delete;
84
+
85
+ Sender& operator=(Sender&& other) noexcept {
86
+ channel_ = std::move(other.channel_);
87
+ return *this;
88
+ }
89
+ Sender(Sender&& other) noexcept : channel_(std::move(other.channel_)) {}
90
+
91
+ /// @brief Try to send a value to the channel
92
+ /// @param value The value to send
93
+ /// @return ResponseStatus indicating the result of the operation
94
+ /// @note this function is lock-free and wait-free
95
+ template<typename U>
96
+ ResponseStatus try_send(U&& value) noexcept(std::is_nothrow_constructible_v<T, U&&>) {
97
+ return channel_->try_send(std::forward<U>(value));
98
+ }
99
+
100
+ /// @brief Send a value to the channel (copy version)
101
+ /// @param value The value to send
102
+ /// @note This function is lock-free but may block if the channel is full
103
+ void send(const T& value) noexcept(std::is_nothrow_constructible_v<T, const T&>) {
104
+ while (channel_->try_send(value) != ResponseStatus::SUCCESS) {
105
+ if constexpr (Wait == WaitStrategy::YIELD) {
106
+ std::this_thread::yield(); // Yield to allow other threads to run
107
+ } else if constexpr (Wait == WaitStrategy::BUSY_LOOP) {
108
+ asm volatile ("" ::: "memory"); // Busy loop, just spin with compiler barrier
109
+ } else if constexpr (Wait == WaitStrategy::ATOMIC_WAIT) {
110
+ channel_->rcvCursor_.wait(channel_->rcvCursorCache_, std::memory_order_acquire);
111
+ }
112
+ }
113
+ }
114
+
115
+ /// @brief Send a value to the channel (move version)
116
+ /// @param value The value to send
117
+ /// @note This function is lock-free but may block if the channel is full.
118
+ void send(T&& value) noexcept(std::is_nothrow_constructible_v<T, T&&>) {
119
+ while (channel_->try_send(std::move(value)) != ResponseStatus::SUCCESS) {
120
+ if constexpr (Wait == WaitStrategy::YIELD) {
121
+ std::this_thread::yield(); // Yield to allow other threads to run
122
+ } else if constexpr (Wait == WaitStrategy::BUSY_LOOP) {
123
+ asm volatile ("" ::: "memory"); // Busy loop, just spin with compiler barrier
124
+ } else if constexpr (Wait == WaitStrategy::ATOMIC_WAIT) {
125
+ channel_->rcvCursor_.wait(channel_->rcvCursorCache_, std::memory_order_acquire);
126
+ }
127
+ }
128
+ }
129
+
130
+ private:
131
+ std::shared_ptr<InnerChannel<T, Strategy, Wait>> channel_;
132
+
133
+ friend std::pair<Sender<T, Strategy, Wait>, Receiver<T, Strategy, Wait>> channel<T, Strategy, Wait>(size_t capacity);
134
+ };
135
+
136
+ /// @brief Receiver for a single-producer, single-consumer channel
137
+ /// @tparam T The type of values sent through the channel
138
+ /// @tparam Strategy The overflow strategy used by the channel
139
+ /// It allows to receive values from the channel. It is designed to be used only from one thread at a time.
140
+ template <typename T, OverflowStrategy Strategy = OverflowStrategy::WAIT_ON_FULL, WaitStrategy Wait = WaitStrategy::BUSY_LOOP>
141
+ class Receiver {
142
+ /// Disallows receiver creation outside of channel function
143
+ explicit Receiver(std::shared_ptr<InnerChannel<T, Strategy, Wait>> chan) : channel_(chan) {}
144
+ public:
145
+ /// @brief Default constructor
146
+ /// @note required to have receiver as class member
147
+ Receiver() = default;
148
+ Receiver(const Receiver&) = delete;
149
+ Receiver& operator=(const Receiver&) = delete;
150
+
151
+ Receiver& operator=(Receiver&& other) noexcept {
152
+ channel_ = std::move(other.channel_);
153
+ return *this;
154
+ }
155
+ Receiver(Receiver&& other) noexcept : channel_(std::move(other.channel_)) {}
156
+
157
+ /// @brief Try to receive a value from the channel
158
+ /// @param value The received value
159
+ /// @return ResponseStatus indicating the result of the operation
160
+ /// @note This function is lock-free and wait-free.
161
+ ResponseStatus try_receive(T& value) noexcept(std::is_nothrow_move_assignable_v<T> && std::is_nothrow_destructible_v<T>) {
162
+ return channel_->try_receive(value);
163
+ }
164
+
165
+ /// @brief Receive a value from the channel
166
+ /// @return The received value
167
+ /// @note This function is lock-free but may block if the channel is empty.
168
+ T receive() noexcept(std::is_nothrow_default_constructible_v<T> && std::is_nothrow_move_assignable_v<T> && std::is_nothrow_destructible_v<T>) {
169
+ T value;
170
+ while (channel_->try_receive(value) != ResponseStatus::SUCCESS) {
171
+ if constexpr (Wait == WaitStrategy::YIELD) {
172
+ std::this_thread::yield(); // Yield to allow other threads to run
173
+ } else if constexpr (Wait == WaitStrategy::BUSY_LOOP) {
174
+ asm volatile ("" ::: "memory"); // Busy loop, just spin with compiler barrier
175
+ } else if constexpr (Wait == WaitStrategy::ATOMIC_WAIT) {
176
+ channel_->sendCursor_.wait(channel_->sendCursorCache_, std::memory_order_acquire);
177
+ }
178
+ }
179
+ return value;
180
+ }
181
+
182
+ private:
183
+ std::shared_ptr<InnerChannel<T, Strategy, Wait>> channel_;
184
+
185
+ friend std::pair<Sender<T, Strategy, Wait>, Receiver<T, Strategy, Wait>> channel<T, Strategy, Wait>(size_t capacity);
186
+ };
187
+
188
+ /// @brief Inner channel implementation for the SPSC queue
189
+ /// @tparam T The type of values sent through the channel
190
+ /// @tparam Strategy The overflow strategy to use when the channel is full
191
+ /// @tparam Wait The wait strategy used for internal operations
192
+ /// This class is not intended to be used directly by users.
193
+ /// @note this class is not thread safe and should be wrapped in std::shared_ptr
194
+ template <typename T, OverflowStrategy Strategy = OverflowStrategy::WAIT_ON_FULL, WaitStrategy Wait = WaitStrategy::BUSY_LOOP>
195
+ class InnerChannel {
196
+ public:
197
+ /// @brief Construct a channel with a given capacity
198
+ /// @param capacity The minimum capacity of the channel, for performance it will be allocated with next power of 2
199
+ /// Uses raw memory allocation so the T type is not required to provide default constructors
200
+ /// alignment is the key for performance it makes sure that objects are properly aligned in memory for faster access
201
+ explicit InnerChannel(size_t capacity) :
202
+ capacity_(next_power_of_2(capacity)),
203
+ capacity_mask_(capacity_ - 1),
204
+ buffer_(static_cast<T*>(operator new[](capacity_ * sizeof(T), std::align_val_t{alignof(T)}))) {
205
+
206
+ // Initialize cache values for better performance
207
+ rcvCursorCache_ = 0;
208
+ sendCursorCache_ = 0;
209
+
210
+ // Initialize reader state for overwrite strategy
211
+ if constexpr (Strategy == OverflowStrategy::OVERWRITE_ON_FULL) {
212
+ oldestOccupied_.store(false, std::memory_order_relaxed);
213
+ }
214
+ }
215
+
216
+ /// This should not be called if there is existing handle to reader or writer
217
+ ~InnerChannel() {
218
+ size_t sendCursor = sendCursor_.load(std::memory_order_seq_cst);
219
+ size_t rcvCursor = rcvCursor_.load(std::memory_order_seq_cst);
220
+
221
+ // Call destructors for all elements in the buffer
222
+ size_t i = rcvCursor;
223
+ while (i != sendCursor) {
224
+ buffer_[i].~T();
225
+ i = next_index(i);
226
+ }
227
+
228
+ // Deallocate the buffer
229
+ ::operator delete[](buffer_);
230
+ }
231
+
232
+ /// @brief Try to send a value to the channel
233
+ /// @param value The value to send
234
+ /// @return ResponseStatus indicating the result of the operation
235
+ /// @note This function is lock-free and wait-free
236
+ template<typename U>
237
+ ResponseStatus try_send(U&& value) noexcept(std::is_nothrow_constructible_v<T, U&&>) {
238
+ if constexpr (Strategy == OverflowStrategy::WAIT_ON_FULL) {
239
+ return try_send_wait_on_full(std::forward<U>(value));
240
+ } else {
241
+ return try_send_overwrite_on_full(std::forward<U>(value));
242
+ }
243
+ }
244
+
245
+ /// @brief Try to receive a value from the channel
246
+ /// @param value The variable to store the received value
247
+ /// @return ResponseStatus indicating the result of the operation
248
+ /// @note This function is lock-free and wait-free
249
+ ResponseStatus try_receive(T& value) noexcept(std::is_nothrow_move_assignable_v<T> && std::is_nothrow_destructible_v<T>) {
250
+ if constexpr (Strategy == OverflowStrategy::OVERWRITE_ON_FULL) {
251
+ // Set reader active flag to prevent overwrites during read
252
+ bool isOccupied = oldestOccupied_.exchange(true, std::memory_order_acq_rel);
253
+ if (isOccupied) {
254
+ // It means that the oldest element is being overwritten so we cannot read
255
+ return ResponseStatus::SKIP_DUE_TO_OVERWRITE;
256
+ }
257
+ }
258
+
259
+ size_t rcvCursor = rcvCursor_.load(std::memory_order_relaxed); // only receiver thread reads this
260
+
261
+ if (rcvCursor == sendCursorCache_) {
262
+ // Refresh cache
263
+ sendCursorCache_ = sendCursor_.load(std::memory_order_acquire);
264
+ if (rcvCursor == sendCursorCache_) {
265
+ if constexpr (Strategy == OverflowStrategy::OVERWRITE_ON_FULL) {
266
+ oldestOccupied_.store(false, std::memory_order_release);
267
+ }
268
+
269
+ return ResponseStatus::CHANNEL_EMPTY;
270
+ }
271
+ }
272
+
273
+ value = std::move(buffer_[rcvCursor]);
274
+ buffer_[rcvCursor].~T(); // Call destructor
275
+
276
+ rcvCursor_.store(next_index(rcvCursor), std::memory_order_release);
277
+
278
+ if constexpr (Wait == WaitStrategy::ATOMIC_WAIT) {
279
+ rcvCursor_.notify_one(); // Notify sender that a value has been received
280
+ }
281
+
282
+ if constexpr (Strategy == OverflowStrategy::OVERWRITE_ON_FULL) {
283
+ oldestOccupied_.store(false, std::memory_order_release);
284
+ }
285
+
286
+ return ResponseStatus::SUCCESS;
287
+ }
288
+
289
+ private:
290
+ /// @brief Try to send with WAIT_ON_FULL strategy (original behavior)
291
+ template<typename U>
292
+ inline ResponseStatus try_send_wait_on_full(U&& value) noexcept(std::is_nothrow_constructible_v<T, U&&>) {
293
+ size_t sendCursor = sendCursor_.load(std::memory_order_relaxed); // only sender thread writes this
294
+ size_t next_sendCursor = next_index(sendCursor);
295
+
296
+ if (next_sendCursor == rcvCursorCache_) {
297
+ // Refresh the cache
298
+ rcvCursorCache_ = rcvCursor_.load(std::memory_order_acquire);
299
+ if (next_sendCursor == rcvCursorCache_) return ResponseStatus::CHANNEL_FULL;
300
+ }
301
+
302
+ // Construct the new element in place
303
+ new (&buffer_[sendCursor]) T(std::forward<U>(value));
304
+
305
+ sendCursor_.store(next_sendCursor, std::memory_order_release);
306
+
307
+ if constexpr (Wait == WaitStrategy::ATOMIC_WAIT) {
308
+ sendCursor_.notify_one(); // Notify receiver that a value has been sent
309
+ }
310
+
311
+ return ResponseStatus::SUCCESS;
312
+ }
313
+
314
+ /// @brief Try to send with OVERWRITE_ON_FULL strategy
315
+ template<typename U>
316
+ inline ResponseStatus try_send_overwrite_on_full(U&& value) noexcept(std::is_nothrow_constructible_v<T, U&&>) {
317
+ size_t sendCursor = sendCursor_.load(std::memory_order_relaxed); // only sender thread writes this
318
+ size_t next_sendCursor = next_index(sendCursor);
319
+
320
+ if (next_sendCursor == rcvCursorCache_) {
321
+ // Refresh the cache
322
+ rcvCursorCache_ = rcvCursor_.load(std::memory_order_acquire);
323
+ if (next_sendCursor == rcvCursorCache_) {
324
+ bool isOldestOccupied = oldestOccupied_.exchange(true, std::memory_order_acq_rel);
325
+ if (isOldestOccupied) {
326
+ // If the oldest element is occupied, we cannot overwrite
327
+ return ResponseStatus::SKIP_DUE_TO_OVERWRITE;
328
+ }
329
+
330
+ size_t newestRcvCursor = rcvCursor_.load(std::memory_order_acquire);
331
+
332
+ /// If the receiver did not advance, we can safely advance the cursor
333
+ if (rcvCursorCache_ == newestRcvCursor) {
334
+ rcvCursorCache_ = next_index(newestRcvCursor);
335
+ rcvCursor_.store(rcvCursorCache_, std::memory_order_release);
336
+ } else {
337
+ rcvCursorCache_ = newestRcvCursor;
338
+ }
339
+
340
+ oldestOccupied_.store(false, std::memory_order_release);
341
+ }
342
+ }
343
+
344
+ // Normal case: buffer not full
345
+ new (&buffer_[sendCursor]) T(std::forward<U>(value));
346
+ sendCursor_.store(next_sendCursor, std::memory_order_release);
347
+
348
+ if constexpr (Wait == WaitStrategy::ATOMIC_WAIT) {
349
+ sendCursor_.notify_one(); // Notify receiver that a value has been sent
350
+ }
351
+
352
+ return ResponseStatus::SUCCESS;
353
+ }
354
+
355
+ /// @brief Calculate the next power of 2 greater than or equal to n
356
+ /// @param n The input value
357
+ /// @return The next power of 2
358
+ static constexpr size_t next_power_of_2(const size_t n) noexcept {
359
+ if (n <= 1) return 1;
360
+
361
+ // Use bit manipulation for efficiency
362
+ size_t power = 1;
363
+ while (power < n) {
364
+ power <<= 1;
365
+ }
366
+ return power;
367
+ }
368
+
369
+ /// @brief Get the next index in a circular buffer
370
+ /// @param val The current index
371
+ /// @return The next index
372
+ /// @note it might not be used for performance but it is a good reference
373
+ inline size_t next_index(const size_t val) const noexcept {
374
+ return (val + 1) & capacity_mask_;
375
+ }
376
+
377
+ const size_t capacity_;
378
+ const size_t capacity_mask_; // mask for bitwise next_index
379
+ T* buffer_;
380
+
381
+ /// Producer-side data (accessed by sender thread)
382
+ alignas(64) std::atomic<size_t> sendCursor_{0};
383
+ alignas(64) size_t rcvCursorCache_{0}; // reduces cache coherency
384
+
385
+ /// Consumer-side data (accessed by receiver thread)
386
+ alignas(64) std::atomic<size_t> rcvCursor_{0};
387
+ alignas(64) size_t sendCursorCache_{0}; // reduces cache coherency
388
+
389
+ /// Flag indicating if the oldest element is occupied
390
+ alignas(64) std::atomic<bool> oldestOccupied_{false};
391
+
392
+ friend class Sender<T, Strategy, Wait>;
393
+ friend class Receiver<T, Strategy, Wait>;
394
+ };
395
+
396
+
397
+ } // namespace channels::spsc
@@ -1,7 +1,7 @@
1
1
  cmake_minimum_required(VERSION 3.14)
2
2
  project(rnaudioapi_test)
3
3
 
4
- set(CMAKE_CXX_STANDARD 17)
4
+ set(CMAKE_CXX_STANDARD 20)
5
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
6
  set(ROOT ${CMAKE_SOURCE_DIR}/../../../../..)
7
7
  set(REACT_NATIVE_DIR "${ROOT}/node_modules/react-native")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-api",
3
- "version": "0.7.2-nightly-31b46b8-20250819",
3
+ "version": "0.7.2-nightly-c06331b-20250821",
4
4
  "description": "react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification",
5
5
  "bin": {
6
6
  "setup-rn-audio-api-web": "./scripts/setup-rn-audio-api-web.js"