Skip to content

Commit 0a23a8e

Browse files
Refactor/communication between audio and js thread (#942)
* refactor: audio array * feat: audio bus * chore: added docs for both utils * ci: lint * refactor: audio array and fft * chore: requested changes * refactor: next part for audio bus and audio array * refactor: wip * fix: a few fixes * fix: lint * fix: fixed tests * fix: todos * refactor: nits * fix: nitpick * refactor: added copyWithin - memmove to audio array * fix: memmove * chore: updated custom processor template * refactor: optimized interleaveTo function * refactor: renaming * refactor: renaming * refactor: renaming * refactor: doxygen for AudioBuffer * refactor: deinterleave audio data using AudioBuffer method * refactor: broader support for circular data structures operations and deinterleaving * chore: requuested changes from code review * fix: nits * ci: lint * refactor: fat function * refactor: audio param * refactor: audio node * ci: lint * refactor: destination * refactor: audio scheduled source node * refactor: make isInitialized atomic variable * refactor: streamer node * refactor: oscillator and constant source node * refactor: buffer base source node * fix: nitpicks * refactor: gain, delay, stereo panner nodes * ci: lint * refactor: audio buffer source node and related cleanups * refactor: audio buffer queue source node * fix: nitpicks * refactor: wave shaper node * refactor: iir filter node * refactor: biquad filter node * refactor: convolver node * refactor: part of analyser node * fix: nitpicks * ci: lint * fix: nitpicks * ci: lint * refactor: thread-safe setFFTSize and drop support for window types * refactor: analyser node thread safety with lock-free triple buffer * ci: lint * fix: fixed negative latency values in AudioBufferSourceNode * refactor: moved stretch init and preset to JS thread * docs: added audio node docs * refactor: biquad filter thread safety improvements * ci: lint * docs: doxygen comments for AudioThread-only methods * refactor: removed unnecessary headers * ci: lint * fix: nitpicks * fix: nitpicks * refactor: refined TripleBuffer and AnalyserNode for better performance and correctness * test: tests for TripleBuffer utility class * refactor: thread safe convolver setup * fix: triple buffer polishing * refactor: added concept to ensure TripleBuffer is only instantiated with copyable types * ci: lint * chore: requested changes * chore: yarn format --------- Co-authored-by: maciejmakowski2003 <maciej.makowski2608@gmail.com> Co-authored-by: michal <dydmichal@gmail.com> Co-authored-by: maciejmakowski2003 <maciejmakowski2003@users.noreply.github.com>
1 parent dc87254 commit 0a23a8e

124 files changed

Lines changed: 2595 additions & 1932 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,6 @@ packages/react-native-audio-api/android/src/main/jniLibs/
9797
packages/react-native-audio-api/common/cpp/audioapi/external/**/*.a
9898
packages/react-native-audio-api/common/cpp/audioapi/external/*.xcframework
9999
packages/react-native-audio-api/common/cpp/audioapi/external/ffmpeg_ios/
100+
101+
# Clangd cache
100102
.cache

apps/common-app/src/examples/Streaming/Streaming.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,8 @@ const Streaming: FC = () => {
3030
console.error('StreamerNode is already initialized');
3131
return;
3232
}
33-
streamerRef.current = aCtxRef.current.createStreamer();
33+
streamerRef.current = aCtxRef.current.createStreamer('https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8');
3434

35-
streamerRef.current.initialize(
36-
'https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8'
37-
);
3835
streamerRef.current.connect(gainRef.current);
3936
gainRef.current.connect(aCtxRef.current.destination);
4037
streamerRef.current.start(aCtxRef.current.currentTime);

apps/fabric-example/ios/FabricExample.xcodeproj/project.pbxproj

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,10 @@
191191
inputFileListPaths = (
192192
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
193193
);
194-
inputPaths = (
195-
);
196194
name = "[CP] Embed Pods Frameworks";
197195
outputFileListPaths = (
198196
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
199197
);
200-
outputPaths = (
201-
);
202198
runOnlyForDeploymentPostprocessing = 0;
203199
shellPath = /bin/sh;
204200
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks.sh\"\n";
@@ -234,14 +230,10 @@
234230
inputFileListPaths = (
235231
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources-${CONFIGURATION}-input-files.xcfilelist",
236232
);
237-
inputPaths = (
238-
);
239233
name = "[CP] Copy Pods Resources";
240234
outputFileListPaths = (
241235
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources-${CONFIGURATION}-output-files.xcfilelist",
242236
);
243-
outputPaths = (
244-
);
245237
runOnlyForDeploymentPostprocessing = 0;
246238
shellPath = /bin/sh;
247239
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources.sh\"\n";
@@ -424,7 +416,10 @@
424416
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
425417
"-DRCT_REMOVE_LEGACY_ARCH=1",
426418
);
427-
OTHER_LDFLAGS = "$(inherited) ";
419+
OTHER_LDFLAGS = (
420+
"$(inherited)",
421+
" ",
422+
);
428423
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
429424
SDKROOT = iphoneos;
430425
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -512,7 +507,10 @@
512507
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
513508
"-DRCT_REMOVE_LEGACY_ARCH=1",
514509
);
515-
OTHER_LDFLAGS = "$(inherited) ";
510+
OTHER_LDFLAGS = (
511+
"$(inherited)",
512+
" ",
513+
);
516514
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
517515
SDKROOT = iphoneos;
518516
SWIFT_ENABLE_EXPLICIT_MODULES = NO;

apps/fabric-example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2553,7 +2553,7 @@ SPEC CHECKSUMS:
25532553
React-microtasksnativemodule: d1956f0eec54c619b63a379520fb4c618a55ccb9
25542554
react-native-background-timer: 4638ae3bee00320753647900b21260b10587b6f7
25552555
react-native-safe-area-context: ae7587b95fb580d1800c5b0b2a7bd48c2868e67a
2556-
react-native-skia: 268f7c9942c00dcecc58fae9758b7833e3d246f2
2556+
react-native-skia: 5f68d3c3749bfb4f726e408410b8be5999392cd9
25572557
React-NativeModulesApple: 5ba0903927f6b8d335a091700e9fda143980f819
25582558
React-networking: 3a4b7f9ed2b2d1c0441beacb79674323a24bcca6
25592559
React-oscompat: ff26abf0ae3e3fdbe47b44224571e3fc7226a573

ghdocs/audio-node.md

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# How to create new AudioNode
2+
3+
In this docs we present recommended patterns for creating new AudioNodes.
4+
5+
## Layers
6+
7+
Ususally, each AudioNode has three layers:
8+
9+
- Core (C++)
10+
11+
Class implementing core audio processing logic. Should be implemented in highly performant manner using internal data structures (if possible).
12+
13+
```cpp
14+
class GainNode : public AudioNode {
15+
public:
16+
explicit GainNode(const std::shared_ptr<BaseAudioContext> &context, const GainOptions &options);
17+
18+
[[nodiscard]] std::shared_ptr<AudioParam> getGainParam() const;
19+
20+
protected:
21+
std::shared_ptr<AudioBuffer> processNode(
22+
const std::shared_ptr<AudioBuffer> &processingBuffer,
23+
int framesToProcess) override;
24+
25+
private:
26+
std::shared_ptr<AudioParam> gainParam_;
27+
};
28+
```
29+
30+
- Host Object (HO)
31+
32+
Interop class between C++ and JS, implemented on C++ side. HO is returned from C++ to JS from BaseAudioContext factory methods. JS has its own interfaces that works as a counterpart of C++ HO. There is no strong typing mechanism between C++ and JS. Implementation is based on the alignment between C++ HO and JS interface.
33+
34+
```cpp
35+
class GainNodeHostObject : public AudioNodeHostObject {
36+
public:
37+
explicit GainNodeHostObject(
38+
const std::shared_ptr<BaseAudioContext> &context,
39+
const GainOptions &options);
40+
41+
JSI_PROPERTY_GETTER_DECL(gain);
42+
43+
private:
44+
std::shared_ptr<AudioParamHostObject> gainParam_;
45+
};
46+
```
47+
```ts
48+
export interface IGainNode extends IAudioNode {
49+
readonly gain: IAudioParam;
50+
}
51+
```
52+
53+
- Typescript (JS)
54+
55+
Elegant typescript wrapper around JS HO interface.
56+
57+
```ts
58+
class GainNode extends AudioNode {
59+
readonly gain: AudioParam;
60+
61+
constructor(context: BaseAudioContext, options?: TGainOptions) {
62+
const gainNode: IGainNode = context.context.createGain(options || {}); // context.context is C++ HO
63+
super(context, gainNode);
64+
this.gain = new AudioParam(gainNode.gain, context);
65+
}
66+
}
67+
```
68+
69+
## Core (C++) implementation
70+
71+
Each AudioNode should implement one virtual method:
72+
```cpp
73+
std::shared_ptr<AudioBuffer> processNode(
74+
const std::shared_ptr<AudioBuffer> &processingBuffer,
75+
int framesToProcess)
76+
```
77+
78+
It is responsible for AudioNode's processing logic. It gets input buffer as argument - `processingBus` and should return processed buffer.
79+
80+
```cpp
81+
std::shared_ptr<AudioBuffer> GainNode::processNode(
82+
const std::shared_ptr<AudioBuffer> &processingBuffer,
83+
int framesToProcess) {
84+
std::shared_ptr<BaseAudioContext> context = context_.lock();
85+
if (context == nullptr)
86+
return processingBuffer;
87+
double time = context->getCurrentTime();
88+
auto gainParamValues = gainParam_->processARateParam(framesToProcess, time);
89+
auto gainValues = gainParamValues->getChannel(0);
90+
91+
for (size_t i = 0; i < processingBuffer->getNumberOfChannels(); i++) {
92+
auto channel = processingBuffer->getChannel(i);
93+
channel->multiply(*gainValues, framesToProcess);
94+
}
95+
96+
return processingBuffer;
97+
}
98+
```
99+
100+
There are a few rules that should be followed when implementing C++ AudioNode core.
101+
102+
- **Thread safety**: Each AudioNode should be created in thread safe manner.
103+
104+
- **Heap allocations**: Heap allocations are not allowed on the Audio Thread, so all necessary data should be allocated in constructor, or pre-allocated on other thread and passed to AudioNode.
105+
106+
- **Destructions** No destructions are allowed to happen on the Audio Thread. AudioNode destruction are handled by already active AudioDestructor. If you need to perform some cleanup, you have to delegate it to AudioDestructor.
107+
108+
- **No locks on Audio Thread**: Locks are not allowed on the Audio Thread. Audio procssing have to be highly performant and efficient.
109+
110+
- **No syscalls** Syscalls are not allowed on the Audio Thread, so if you need to perform some work that requires syscalls, you have to delegate it to other thread.
111+
112+
## HostObject implementation
113+
114+
We can distinguish three types of AudioNode's JS methods:
115+
116+
1. **getter**
117+
118+
```ts
119+
const fftSize = analyserNode.fftSize;
120+
```
121+
122+
C++ counter part is `JSI_PROPERTY_GETTER`. It just returns some value.
123+
124+
```cpp
125+
JSI_PROPERTY_GETTER_IMPL(AnalyserNodeHostObject, fftSize) {
126+
return {fftSize_};
127+
}
128+
```
129+
130+
2. **setter**
131+
132+
C++ counterpart is `JSI_PROPERTY_SETTER`. It just receives some value.
133+
134+
```ts
135+
analyserNode.fftSize = 2048;
136+
```
137+
138+
```cpp
139+
JSI_PROPERTY_SETTER_IMPL(AnalyserNodeHostObject, minDecibels) {
140+
auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_);
141+
auto minDecibels = static_cast<float>(value.getNumber());
142+
auto event = [analyserNode, minDecibels](BaseAudioContext&) {
143+
analyserNode->setMinDecibels(minDecibels);
144+
};
145+
analyserNode->scheduleAudioEvent(std::move(event));
146+
minDecibels_ = minDecibels;
147+
}
148+
```
149+
150+
151+
3. **function**
152+
153+
C++ counterpart is `JSI_HOST_FUNCTION`. It is a common function that can receive arguments and return some value.
154+
155+
```ts
156+
const fftOutput = new Uint8Array(analyser.frequencyBinCount);
157+
analyserNode.getByteFrequencyData(fftOutput);
158+
```
159+
160+
```cpp
161+
JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) {
162+
auto arrayBuffer =
163+
args[0].getObject(runtime).getPropertyAsObject(runtime, "buffer").getArrayBuffer(runtime);
164+
auto data = arrayBuffer.data(runtime);
165+
auto length = static_cast<int>(arrayBuffer.size(runtime));
166+
167+
auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_);
168+
analyserNode->getByteFrequencyData(data, length);
169+
170+
return jsi::Value::undefined();
171+
}
172+
```
173+
174+
All methods should be registerd in C++ HO constructor:
175+
176+
```cpp
177+
AnalyserNodeHostObject::AnalyserNodeHostObject(const std::shared_ptr<BaseAudioContext>& context, const AnalyserOptions &options)
178+
: /* ... */ {
179+
addGetters(JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, fftSize));
180+
addSetters(JSI_EXPORT_PROPERTY_SETTER(AnalyserNodeHostObject, fftSize));
181+
addFunctions(JSI_EXPORT_FUNCTION(AnalyserNodeHostObject, getByteFrequencyData),);
182+
}
183+
```
184+
185+
#### Shadow state (C++)
186+
187+
Shadow state is a mechanism introduced in order to make communication between JS and the Audio Thread lock-free. AudioNodeHostObject stores the set of properties, which are modified only by JS thread (the same set C++ AudioNode has). Everytime we want to access some property from JS, we can just return property from shadow state, when we modify some property we have to update shadow state and schedule update event on Audio Event Loop (SPSC). By following that manner we can skip accessing AudioNode state, that is also accessed by the Audio Thread - no need to lock or use atomic variables.
188+
189+
```cpp
190+
class OscillatorNodeHostObject : public AudioScheduledSourceNodeHostObject {
191+
public:
192+
/* ... */
193+
194+
JSI_PROPERTY_GETTER_DECL(type);
195+
JSI_PROPERTY_SETTER_DECL(type);
196+
197+
private:
198+
/* ... */
199+
OscillatorType type_;
200+
};
201+
```
202+
203+
```cpp
204+
JSI_PROPERTY_GETTER_IMPL(OscillatorNodeHostObject, type) {
205+
return jsi::String::createFromUtf8(runtime, js_enum_parser::oscillatorTypeToString(type_));
206+
}
207+
208+
JSI_PROPERTY_SETTER_IMPL(OscillatorNodeHostObject, type) {
209+
auto oscillatorNode = std::static_pointer_cast<OscillatorNode>(node_);
210+
auto type = js_enum_parser::oscillatorTypeFromString(value.asString(runtime).utf8(runtime));
211+
212+
auto event = [oscillatorNode, type](BaseAudioContext &) {
213+
oscillatorNode->setType(type);
214+
};
215+
type_ = type;
216+
217+
oscillatorNode->scheduleAudioEvent(std::move(event));
218+
}
219+
```
220+
221+
#### Communication between JS Thread and Audio Thread
222+
223+
**getters** and **setters**
224+
225+
1. Property is primitive and is not modified by the Audio Thread.
226+
227+
Shadow state design pattern should be followed.
228+
229+
2. Property is not primitive and is not modified by the Audio Thread.
230+
231+
It should be stored in TS layer and copied to AudioNode
232+
233+
3. Property is primitive and can be modified by the Audio Thread.
234+
235+
In C++ core it should be an atomic variable that allows to access it in thread-safe manner from both threads.
236+
237+
```cpp
238+
class AudioParam {
239+
public:
240+
/* ... */
241+
242+
[[nodiscard]] inline float getValue() const noexcept {
243+
return value_.load(std::memory_order_relaxed);
244+
}
245+
246+
inline void setValue(float value) {
247+
value_.store(std::clamp(value, minValue_, maxValue_), std::memory_order_release);
248+
}
249+
250+
/* ... */
251+
252+
private:
253+
std::atomic<float> value_;
254+
255+
/* ... */
256+
};
257+
```
258+
259+
```cpp
260+
JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, value) {
261+
return {param_->getValue()};
262+
}
263+
264+
JSI_PROPERTY_SETTER_IMPL(AudioParamHostObject, value) {
265+
auto event = [param = param_, value = static_cast<float>(value.getNumber())](BaseAudioContext &) {
266+
param->setValue(value);
267+
};
268+
269+
param_->scheduleAudioEvent(std::move(event));
270+
}
271+
```
272+
273+
4. Property is not primitive and can be modified by the Audio Thread.
274+
In C++ core triple buffer pattern should be followed. It allows to have one copy of property for reader, one for writer and one for pending update. On each update we just swap pending update with writer, and on each read we just read from reader. In that manner we can skip locks and just operate on atomic indices.
275+
276+
Check AnalyserNode implementation for example or this [article](https://medium.com/@sgn00/triple-buffer-lock-free-concurrency-primitive-611848627a1e) for more details.
277+
278+
**functions**
279+
280+
Function's should follow the same thread-safe lock-free patterns as getters/setters. Set of properties read/write by the function determines mechanisms that should be used in implementation.

packages/audiodocs/docs/analysis/analyser-node.mdx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,8 @@ It inherits all properties from [`AudioNode`](/docs/core/audio-node#properties).
5151
| `minDecibels` | `number` | Float value representing the minimum value for the range of results from [`getByteFrequencyData()`](/docs/analysis/analyser-node#getbytefrequencydata). |
5252
| `maxDecibels` | `number` | Float value representing the maximum value for the range of results from [`getByteFrequencyData()`](/docs/analysis/analyser-node#getbytefrequencydata). |
5353
| `smoothingTimeConstant` | `number` | Float value representing averaging constant with the last analysis frame. In general the higher value the smoother is the transition between values over time. |
54-
| `window` | [`WindowType`](/docs/types/window-type) | Enumerated value that specifies the type of window function applied when extracting frequency data. |
5554
| `frequencyBinCount` | `number` | Integer value representing amount of the data obtained in frequency domain, half of the `fftSize` property. | <ReadOnly /> |
5655

57-
:::caution
58-
59-
On `Web`, the value of `window` is permanently `'blackman'`, and it cannot be set like on the `Android` or `iOS`.
60-
61-
:::
62-
6356
## Methods
6457

6558
It inherits all methods from [`AudioNode`](/docs/core/audio-node#methods).
@@ -128,6 +121,3 @@ Each value in the array is within the range 0 to 255, where value of 127 indicat
128121
- Nominal range is 0 to 1.
129122
- 0 means no averaging, 1 means "overlap the previous and current buffer quite a lot while computing the value".
130123
- Throws `IndexSizeError` if set value is outside the allowed range.
131-
132-
#### `window`
133-
- Default value is `'blackman'`

packages/audiodocs/docs/core/base-audio-context.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ Creates [`StereoPannerNode`](/docs/effects/stereo-panner-node).
172172

173173
Creates [`StreamerNode`](/docs/sources/streamer-node).
174174

175+
| Parameter | Type | Description |
176+
| :---: | :---: | :---- |
177+
| `options` <Optional /> | [`StreamerOptions`](/docs/sources/streamer-node#streameroptions) | Streamer options to initialize. |
178+
175179
#### Returns `StreamerNode`.
176180

177181
### `createWaveShaper`

0 commit comments

Comments
 (0)