Skip to content

Refactor/communication between audio and js thread#942

Open
maciejmakowski2003 wants to merge 65 commits intomainfrom
refactor/communication-between-audio-and-js-thread
Open

Refactor/communication between audio and js thread#942
maciejmakowski2003 wants to merge 65 commits intomainfrom
refactor/communication-between-audio-and-js-thread

Conversation

@maciejmakowski2003
Copy link
Collaborator

@maciejmakowski2003 maciejmakowski2003 commented Feb 6, 2026

Closes RNAA-311
Closes RNAA-250
Closes RNAA-410
Part of RNAA-433

⚠️ Breaking changes ⚠️

  • Replaced initialize method from StreamerNode with passing url via ctor or factory method.
  • Drop support for hann window in AnalyserNode

Introduced changes

  • Enhanced communication between JS thread and Audio thread. Almost all of node's modifications from JS thread are scheduled on CrossThreadEventScheduler wrapper to be executed on Audio thread. HostObject plays a ShadowAudioNode role - it stores a copy of node's state. There are some exceptions like using atomics ot triple buffer pattern.

Checklist

  • Linked relevant issue
  • Updated relevant documentation
  • Added/Conducted relevant tests
  • Performed self-review of the code
  • Updated Web Audio API coverage
  • Added support for web
  • Updated old arch android spec file

Base automatically changed from refactor/internal-tools to main February 9, 2026 11:16
@maciejmakowski2003 maciejmakowski2003 self-assigned this Feb 9, 2026
@maciejmakowski2003 maciejmakowski2003 marked this pull request as ready for review March 2, 2026 12:07
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to update this doc in future and include few repeating solutions for most common problems that are used in our codebase aka:

  • prealocating
  • SPSC with worker thread
  • how to communicate Audio <-> JSI

Also it is worth noting that we do not block syscalls or allocations on audio thread it is just strongly unrecommended to do but in case if someone is crafting custom node processor for his custom needs it might work fine

Comment on lines +40 to +41
/// @note Thread safe, because does not access state of the node.
void unregisterOnBufferEndedCallback(uint64_t callbackId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does not but might be inheritly unsafe through registry handler

@@ -50,7 +50,7 @@

if (isConnected()) {
if (auto lock = Locker::tryLock(adapterNodeMutex_)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lock missed ?

Comment on lines +40 to +41
/// @note Thread safe, because does not access state of the node.
void unregisterOnBufferEndedCallback(uint64_t callbackId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its thread safety depends on the thread safety of eventRegistryHandle. Besides these methods do not need to be thread safe as they are used on AudioThread through scheduled async events

std::shared_ptr<IOSAudioPlayer> audioPlayer_;
#endif
bool isInitialized_;
std::atomic<bool> isInitialized_ = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be hard to swallow but i doubt it should be atomic as for node to be connected to graph thus processed it needs to be sent through spsc ... sooo its creation is done on the JS thread

Comment on lines +46 to +48
audioEventScheduler_(
std::make_unique<CrossThreadEventScheduler<BaseAudioContext>>(AUDIO_SCHEDULER_CAPACITY)) {
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it need to be unique_ptr?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks really good but few neatpicks and overall good job but write tests for it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and i am kind of against puting something like this in such a big PR, cuz whole PR LGTM besides this part

Comment on lines +43 to +46
T buffers_[3];
int frontIndex_ = 0;
std::atomic<State> state_{{1, false}};
int backIndex_ = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False sharing occurs here at alignas as in SPSC to prevent it. Also it might be better to use size_t instead of int but that is minor thing

bool hasUpdate;
};

T buffers_[3];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buffers should also be alligned to cache line sizes, and u can maybe enchance it to use emplace new operator to allow T types which are not defaultly constructible

Comment on lines +38 to +41
struct State {
int index;
bool hasUpdate;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use bitfields to fit this in 32 bits

Comment on lines +16 to +24
T *getForWriter() {
return &buffers_[backIndex_];
}

void publish() {
State newState{backIndex_, true};
auto prevState = state_.exchange(newState, std::memory_order_acq_rel);
backIndex_ = prevState.index;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might have interface read(), write() instead of this it would be better for RAII i guess as u never forget to publish

Copy link
Contributor

@mdydek mdydek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure to align with current formatter, because some files are wrongly formatted and git diff is going crazy

@@ -0,0 +1,280 @@
# How to create new AudioNode

In this docs we present recommended design pattern for creating new AudioNode.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In this docs we present recommended design pattern for creating new AudioNode.
In this docs we present recommended patterns for creating new AudioNodes.


## Layers

In common each AudioNode has three layers:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In common each AudioNode has three layers:
Usually, each AudioNode has three layers:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤓

readonly gain: AudioParam;

constructor(context: BaseAudioContext, options?: TGainOptions) {
const gainNode: IGainNode = context.context.createGain(options || {});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emphasize that context.context is underlying c++ object

```
1. **function**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. **function**
3. **function**

```

```cpp
SI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) {
JSI_HOST_FUNCTION_IMPL(AnalyserNodeHostObject, getByteFrequencyData) {

Comment on lines +40 to +44
minDecibels_.store(minDecibels, std::memory_order_release);
}

void AnalyserNode::setMaxDecibels(float maxDecibels) {
maxDecibels_ = maxDecibels;
maxDecibels_.store(maxDecibels, std::memory_order_release);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where minDecibels_ and maxDecibels_ can be accessed outside of js thread, thus there is no necessity for atomic variables

Comment on lines +40 to +46
float getContextSampleRate() const {
if (std::shared_ptr<BaseAudioContext> context = context_.lock()) {
return context->getSampleRate();
}

return DEFAULT_SAMPLE_RATE;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this idea, it can be very misleading since system can output correct amount even though context is not valid

static FilterCoefficients
setNormalizedCoefficients(float b0, float b1, float b2, float a0, float a1, float a2);
FilterCoefficients
applyFilter(float frequency, float Q, float gain, float detune, BiquadFilterType type);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need type as parameter? In all use cases it is property of a class, so we can retrieve its value inside a function

Comment on lines +57 to +58
template <typename F>
bool inline scheduleAudioEvent(F &&event) noexcept {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add more constraints what can and what cannot be passed as an event, right now we can pass this event

auto event = []() {
   return;
 };

but it will fail, because context will call this event like that

event(*this);

createAnalyser: (analyserOptions: AnalyserOptions) => IAnalyserNode;
createConvolver: (convolverOptions?: IConvolverOptions) => IConvolverNode;
createStreamer: (streamerOptions?: StreamerOptions) => IStreamerNode | null; // null when FFmpeg is not enabled
createStreamer: (streamerOptions: StreamerOptions) => IStreamerNode | null; // null when FFmpeg is not enabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update docs accordingly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants