react-native-audio-api 0.7.2-nightly-31b46b8-20250820 → 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.
- package/common/cpp/audioapi/core/effects/BiquadFilterNode.cpp +44 -49
- package/common/cpp/audioapi/core/effects/BiquadFilterNode.h +2 -7
- package/common/cpp/audioapi/core/utils/AudioNodeDestructor.cpp +25 -35
- package/common/cpp/audioapi/core/utils/AudioNodeDestructor.h +23 -11
- package/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +211 -84
- package/common/cpp/audioapi/core/utils/AudioNodeManager.h +89 -25
- package/common/cpp/audioapi/utils/Benchmark.hpp +58 -0
- package/common/cpp/audioapi/utils/SpscChannel.hpp +397 -0
- package/common/cpp/test/CMakeLists.txt +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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::
|
|
317
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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_ =
|
|
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
|
|
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_
|
|
9
|
-
|
|
10
|
-
|
|
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_
|
|
19
|
+
isExiting_.store(true, std::memory_order_release);
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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::
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
std::
|
|
41
|
-
while (!isExiting_) {
|
|
42
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
sender_.send(std::move(event));
|
|
29
111
|
}
|
|
30
112
|
|
|
31
113
|
void AudioNodeManager::preProcessGraph() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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> ¶m) {
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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::
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
251
|
+
for (int i = begin; i < vec.size(); i++) {
|
|
252
|
+
if (vec[i])
|
|
253
|
+
vec[i]->cleanup();
|
|
128
254
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> ¶m);
|
|
38
97
|
|
|
39
98
|
void cleanup();
|
|
40
99
|
|
|
41
100
|
private:
|
|
42
|
-
std::mutex graphLock_;
|
|
43
101
|
AudioNodeDestructor nodeDeconstructor_;
|
|
44
102
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
66
|
-
void
|
|
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
|
|
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-
|
|
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"
|