Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
43fed07
refactor: audio array
Feb 2, 2026
6ad3a2b
feat: audio bus
Feb 2, 2026
f32a9f7
chore: added docs for both utils
Feb 2, 2026
91503c4
ci: lint
Feb 2, 2026
456b139
refactor: audio array and fft
Feb 2, 2026
8f439a6
chore: requested changes
Feb 3, 2026
e74317f
refactor: next part for audio bus and audio array
Feb 3, 2026
d1dede6
refactor: wip
Feb 3, 2026
1eca174
Merge branch 'main' into refactor/internal-tools
Feb 3, 2026
55bb04b
fix: a few fixes
Feb 3, 2026
e3b937c
fix: lint
Feb 3, 2026
83e8fdd
fix: fixed tests
Feb 3, 2026
122b9e0
fix: todos
Feb 4, 2026
a87f2f7
refactor: nits
Feb 4, 2026
58471e4
fix: nitpick
Feb 4, 2026
1b51ce6
refactor: added copyWithin - memmove to audio array
Feb 4, 2026
8127fa2
fix: memmove
Feb 4, 2026
2b01ac3
chore: updated custom processor template
Feb 4, 2026
dd2bf6b
refactor: optimized interleaveTo function
Feb 4, 2026
40d3ce7
refactor: renaming
Feb 4, 2026
7703767
refactor: renaming
Feb 4, 2026
a39e955
refactor: renaming
Feb 4, 2026
f9a3ac2
refactor: doxygen for AudioBuffer
Feb 4, 2026
a92bbf9
refactor: deinterleave audio data using AudioBuffer method
Feb 5, 2026
49537d0
refactor: broader support for circular data structures operations and…
Feb 5, 2026
7a13612
chore: requuested changes from code review
Feb 5, 2026
8587663
fix: nits
Feb 5, 2026
2e7538a
Merge branch 'main' into refactor/internal-tools
Feb 6, 2026
a033e12
ci: lint
Feb 6, 2026
e43d34b
refactor: fat function
Feb 6, 2026
ae6b384
refactor: audio param
Feb 6, 2026
6c55954
refactor: audio node
Feb 6, 2026
7e67258
ci: lint
Feb 6, 2026
9b8d15c
refactor: destination
Feb 6, 2026
592139a
refactor: audio scheduled source node
Feb 6, 2026
af56e5e
refactor: make isInitialized atomic variable
Feb 6, 2026
03e2d07
refactor: streamer node
Feb 6, 2026
a3547b1
refactor: oscillator and constant source node
Feb 6, 2026
14b377e
refactor: buffer base source node
Feb 6, 2026
269f56a
fix: nitpicks
Feb 6, 2026
f7f063e
refactor: gain, delay, stereo panner nodes
Feb 6, 2026
ff82771
ci: lint
Feb 6, 2026
60eb11e
refactor: audio buffer source node and related cleanups
Feb 7, 2026
1612d47
refactor: audio buffer queue source node
Feb 7, 2026
699f75a
fix: nitpicks
Feb 7, 2026
9c65120
refactor: wave shaper node
Feb 7, 2026
1375a9b
refactor: iir filter node
Feb 7, 2026
0298c33
refactor: biquad filter node
Feb 7, 2026
1ca5d4c
refactor: convolver node
Feb 7, 2026
3def7d7
refactor: part of analyser node
Feb 9, 2026
56719b0
Merge branch 'main' into refactor/communication-between-audio-and-js-…
Feb 9, 2026
40f2f43
fix: nitpicks
Feb 9, 2026
7f84ecf
ci: lint
Feb 9, 2026
7d07947
fix: nitpicks
Feb 9, 2026
4cc732d
Merge branch 'main' into refactor/communication-between-audio-and-js-…
Feb 26, 2026
0552760
ci: lint
Feb 26, 2026
8522dd8
refactor: thread-safe setFFTSize and drop support for window types
Feb 27, 2026
7173b3d
refactor: analyser node thread safety with lock-free triple buffer
Feb 27, 2026
ccd6478
ci: lint
Feb 27, 2026
fc26f66
fix: fixed negative latency values in AudioBufferSourceNode
Feb 27, 2026
9356844
refactor: moved stretch init and preset to JS thread
Mar 2, 2026
0e7e17c
docs: added audio node docs
Mar 2, 2026
c799908
refactor: biquad filter thread safety improvements
Mar 2, 2026
9bfd34a
ci: lint
Mar 2, 2026
edda4bc
docs: doxygen comments for AudioThread-only methods
Mar 2, 2026
3a75582
refactor: removed unnecessary headers
mdydek Mar 2, 2026
a798e89
ci: lint
Mar 3, 2026
9796e1d
Merge branch 'refactor/communication-between-audio-and-js-thread' of …
Mar 3, 2026
f7624e1
fix: nitpicks
Mar 3, 2026
c03c941
Merge branch 'main' into refactor/communication-between-audio-and-js-…
Mar 3, 2026
cb0252e
fix: nitpicks
Mar 3, 2026
03f7ee7
refactor: refined TripleBuffer and AnalyserNode for better performanc…
Mar 3, 2026
0ed1093
test: tests for TripleBuffer utility class
Mar 3, 2026
48f15d7
refactor: thread safe convolver setup
Mar 3, 2026
00e9d03
Merge branch 'main' into refactor/communication-between-audio-and-js-…
Mar 3, 2026
d4dc870
Merge branch 'main' into refactor/communication-between-audio-and-js-…
Mar 4, 2026
9da5b7b
fix: triple buffer polishing
Mar 4, 2026
e65aa82
refactor: added concept to ensure TripleBuffer is only instantiated w…
Mar 5, 2026
6c36e5a
ci: lint
Mar 5, 2026
76220fd
chore: requested changes
maciejmakowski2003 Mar 5, 2026
23cb062
chore: yarn format
maciejmakowski2003 Mar 5, 2026
1ec4751
Merge branch 'main' into refactor/communication-between-audio-and-js-…
maciejmakowski2003 Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ packages/react-native-audio-api/common/cpp/audioapi/external/**/*.a
packages/react-native-audio-api/common/cpp/audioapi/external/*.xcframework
packages/react-native-audio-api/common/cpp/audioapi/external/ffmpeg_ios/

# Clangd cache
.cache
5 changes: 1 addition & 4 deletions apps/common-app/src/examples/Streaming/Streaming.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,8 @@ const Streaming: FC = () => {
console.error('StreamerNode is already initialized');
return;
}
streamerRef.current = aCtxRef.current.createStreamer();
streamerRef.current = aCtxRef.current.createStreamer('https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8');

streamerRef.current.initialize(
'https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8'
);
streamerRef.current.connect(gainRef.current);
gainRef.current.connect(aCtxRef.current.destination);
streamerRef.current.start(aCtxRef.current.currentTime);
Expand Down
18 changes: 8 additions & 10 deletions apps/fabric-example/ios/FabricExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-frameworks.sh\"\n";
Expand Down Expand Up @@ -234,14 +230,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FabricExample/Pods-FabricExample-resources.sh\"\n";
Expand Down Expand Up @@ -424,7 +416,10 @@
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
Expand Down Expand Up @@ -512,7 +507,10 @@
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
Expand Down
2 changes: 1 addition & 1 deletion apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2553,7 +2553,7 @@ SPEC CHECKSUMS:
React-microtasksnativemodule: d1956f0eec54c619b63a379520fb4c618a55ccb9
react-native-background-timer: 4638ae3bee00320753647900b21260b10587b6f7
react-native-safe-area-context: ae7587b95fb580d1800c5b0b2a7bd48c2868e67a
react-native-skia: 268f7c9942c00dcecc58fae9758b7833e3d246f2
react-native-skia: 5f68d3c3749bfb4f726e408410b8be5999392cd9
React-NativeModulesApple: 5ba0903927f6b8d335a091700e9fda143980f819
React-networking: 3a4b7f9ed2b2d1c0441beacb79674323a24bcca6
React-oscompat: ff26abf0ae3e3fdbe47b44224571e3fc7226a573
Expand Down
280 changes: 280 additions & 0 deletions ghdocs/audio-node.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
# How to create new AudioNode

In this docs we present recommended patterns for creating new AudioNodes.

## Layers

Ususally, each AudioNode has three layers:

- Core (C++)

Class implementing core audio processing logic. Should be implemented in highly performant manner using internal data structures (if possible).

```cpp
class GainNode : public AudioNode {
public:
explicit GainNode(const std::shared_ptr<BaseAudioContext> &context, const GainOptions &options);

[[nodiscard]] std::shared_ptr<AudioParam> getGainParam() const;

protected:
std::shared_ptr<AudioBuffer> processNode(
const std::shared_ptr<AudioBuffer> &processingBuffer,
int framesToProcess) override;

private:
std::shared_ptr<AudioParam> gainParam_;
};
```

- Host Object (HO)

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.

```cpp
class GainNodeHostObject : public AudioNodeHostObject {
public:
explicit GainNodeHostObject(
const std::shared_ptr<BaseAudioContext> &context,
const GainOptions &options);

JSI_PROPERTY_GETTER_DECL(gain);

private:
std::shared_ptr<AudioParamHostObject> gainParam_;
};
```
```ts
export interface IGainNode extends IAudioNode {
readonly gain: IAudioParam;
}
```

- Typescript (JS)

Elegant typescript wrapper around JS HO interface.

```ts
class GainNode extends AudioNode {
readonly gain: AudioParam;

constructor(context: BaseAudioContext, options?: TGainOptions) {
const gainNode: IGainNode = context.context.createGain(options || {}); // context.context is C++ HO
super(context, gainNode);
this.gain = new AudioParam(gainNode.gain, context);
}
}
```

## Core (C++) implementation

Each AudioNode should implement one virtual method:
```cpp
std::shared_ptr<AudioBuffer> processNode(
const std::shared_ptr<AudioBuffer> &processingBuffer,
int framesToProcess)
```

It is responsible for AudioNode's processing logic. It gets input buffer as argument - `processingBus` and should return processed buffer.

```cpp
std::shared_ptr<AudioBuffer> GainNode::processNode(
const std::shared_ptr<AudioBuffer> &processingBuffer,
int framesToProcess) {
std::shared_ptr<BaseAudioContext> context = context_.lock();
if (context == nullptr)
return processingBuffer;
double time = context->getCurrentTime();
auto gainParamValues = gainParam_->processARateParam(framesToProcess, time);
auto gainValues = gainParamValues->getChannel(0);

for (size_t i = 0; i < processingBuffer->getNumberOfChannels(); i++) {
auto channel = processingBuffer->getChannel(i);
channel->multiply(*gainValues, framesToProcess);
}

return processingBuffer;
}
```

There are a few rules that should be followed when implementing C++ AudioNode core.

- **Thread safety**: Each AudioNode should be created in thread safe manner.

- **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.

- **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.

- **No locks on Audio Thread**: Locks are not allowed on the Audio Thread. Audio procssing have to be highly performant and efficient.

- **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.

## HostObject implementation

We can distinguish three types of AudioNode's JS methods:

1. **getter**

```ts
const fftSize = analyserNode.fftSize;
```

C++ counter part is `JSI_PROPERTY_GETTER`. It just returns some value.

```cpp
JSI_PROPERTY_GETTER_IMPL(AnalyserNodeHostObject, fftSize) {
return {fftSize_};
}
```

2. **setter**

C++ counterpart is `JSI_PROPERTY_SETTER`. It just receives some value.

```ts
analyserNode.fftSize = 2048;
```

```cpp
JSI_PROPERTY_SETTER_IMPL(AnalyserNodeHostObject, minDecibels) {
auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_);
auto minDecibels = static_cast<float>(value.getNumber());
auto event = [analyserNode, minDecibels](BaseAudioContext&) {
analyserNode->setMinDecibels(minDecibels);
};
analyserNode->scheduleAudioEvent(std::move(event));
minDecibels_ = minDecibels;
}
```


3. **function**

C++ counterpart is `JSI_HOST_FUNCTION`. It is a common function that can receive arguments and return some value.

```ts
const fftOutput = new Uint8Array(analyser.frequencyBinCount);
analyserNode.getByteFrequencyData(fftOutput);
```

```cpp
JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) {
auto arrayBuffer =
args[0].getObject(runtime).getPropertyAsObject(runtime, "buffer").getArrayBuffer(runtime);
auto data = arrayBuffer.data(runtime);
auto length = static_cast<int>(arrayBuffer.size(runtime));

auto analyserNode = std::static_pointer_cast<AnalyserNode>(node_);
analyserNode->getByteFrequencyData(data, length);

return jsi::Value::undefined();
}
```

All methods should be registerd in C++ HO constructor:

```cpp
AnalyserNodeHostObject::AnalyserNodeHostObject(const std::shared_ptr<BaseAudioContext>& context, const AnalyserOptions &options)
: /* ... */ {
addGetters(JSI_EXPORT_PROPERTY_GETTER(AnalyserNodeHostObject, fftSize));
addSetters(JSI_EXPORT_PROPERTY_SETTER(AnalyserNodeHostObject, fftSize));
addFunctions(JSI_EXPORT_FUNCTION(AnalyserNodeHostObject, getByteFrequencyData),);
}
```

#### Shadow state (C++)

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.

```cpp
class OscillatorNodeHostObject : public AudioScheduledSourceNodeHostObject {
public:
/* ... */

JSI_PROPERTY_GETTER_DECL(type);
JSI_PROPERTY_SETTER_DECL(type);

private:
/* ... */
OscillatorType type_;
};
```

```cpp
JSI_PROPERTY_GETTER_IMPL(OscillatorNodeHostObject, type) {
return jsi::String::createFromUtf8(runtime, js_enum_parser::oscillatorTypeToString(type_));
}

JSI_PROPERTY_SETTER_IMPL(OscillatorNodeHostObject, type) {
auto oscillatorNode = std::static_pointer_cast<OscillatorNode>(node_);
auto type = js_enum_parser::oscillatorTypeFromString(value.asString(runtime).utf8(runtime));

auto event = [oscillatorNode, type](BaseAudioContext &) {
oscillatorNode->setType(type);
};
type_ = type;

oscillatorNode->scheduleAudioEvent(std::move(event));
}
```

#### Communication between JS Thread and Audio Thread

**getters** and **setters**

1. Property is primitive and is not modified by the Audio Thread.

Shadow state design pattern should be followed.

2. Property is not primitive and is not modified by the Audio Thread.

It should be stored in TS layer and copied to AudioNode

3. Property is primitive and can be modified by the Audio Thread.

In C++ core it should be an atomic variable that allows to access it in thread-safe manner from both threads.

```cpp
class AudioParam {
public:
/* ... */

[[nodiscard]] inline float getValue() const noexcept {
return value_.load(std::memory_order_relaxed);
}

inline void setValue(float value) {
value_.store(std::clamp(value, minValue_, maxValue_), std::memory_order_release);
}

/* ... */

private:
std::atomic<float> value_;

/* ... */
};
```

```cpp
JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, value) {
return {param_->getValue()};
}

JSI_PROPERTY_SETTER_IMPL(AudioParamHostObject, value) {
auto event = [param = param_, value = static_cast<float>(value.getNumber())](BaseAudioContext &) {
param->setValue(value);
};

param_->scheduleAudioEvent(std::move(event));
}
```

4. Property is not primitive and can be modified by the Audio Thread.
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.

Check AnalyserNode implementation for example or this [article](https://medium.com/@sgn00/triple-buffer-lock-free-concurrency-primitive-611848627a1e) for more details.

**functions**

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.
10 changes: 0 additions & 10 deletions packages/audiodocs/docs/analysis/analyser-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,8 @@ It inherits all properties from [`AudioNode`](/docs/core/audio-node#properties).
| `minDecibels` | `number` | Float value representing the minimum value for the range of results from [`getByteFrequencyData()`](/docs/analysis/analyser-node#getbytefrequencydata). |
| `maxDecibels` | `number` | Float value representing the maximum value for the range of results from [`getByteFrequencyData()`](/docs/analysis/analyser-node#getbytefrequencydata). |
| `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. |
| `window` | [`WindowType`](/docs/types/window-type) | Enumerated value that specifies the type of window function applied when extracting frequency data. |
| `frequencyBinCount` | `number` | Integer value representing amount of the data obtained in frequency domain, half of the `fftSize` property. | <ReadOnly /> |

:::caution

On `Web`, the value of `window` is permanently `'blackman'`, and it cannot be set like on the `Android` or `iOS`.

:::

## Methods

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

#### `window`
- Default value is `'blackman'`
4 changes: 4 additions & 0 deletions packages/audiodocs/docs/core/base-audio-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ Creates [`StereoPannerNode`](/docs/effects/stereo-panner-node).

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

| Parameter | Type | Description |
| :---: | :---: | :---- |
| `options` <Optional /> | [`StreamerOptions`](/docs/sources/streamer-node#streameroptions) | Streamer options to initialize. |

#### Returns `StreamerNode`.

### `createWaveShaper`
Expand Down
Loading