-
-
Notifications
You must be signed in to change notification settings - Fork 49
Refactor/communication between audio and js thread #942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
maciejmakowski2003
wants to merge
82
commits into
main
Choose a base branch
from
refactor/communication-between-audio-and-js-thread
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
82 commits
Select commit
Hold shift + click to select a range
43fed07
refactor: audio array
6ad3a2b
feat: audio bus
f32a9f7
chore: added docs for both utils
91503c4
ci: lint
456b139
refactor: audio array and fft
8f439a6
chore: requested changes
e74317f
refactor: next part for audio bus and audio array
d1dede6
refactor: wip
1eca174
Merge branch 'main' into refactor/internal-tools
55bb04b
fix: a few fixes
e3b937c
fix: lint
83e8fdd
fix: fixed tests
122b9e0
fix: todos
a87f2f7
refactor: nits
58471e4
fix: nitpick
1b51ce6
refactor: added copyWithin - memmove to audio array
8127fa2
fix: memmove
2b01ac3
chore: updated custom processor template
dd2bf6b
refactor: optimized interleaveTo function
40d3ce7
refactor: renaming
7703767
refactor: renaming
a39e955
refactor: renaming
f9a3ac2
refactor: doxygen for AudioBuffer
a92bbf9
refactor: deinterleave audio data using AudioBuffer method
49537d0
refactor: broader support for circular data structures operations and…
7a13612
chore: requuested changes from code review
8587663
fix: nits
2e7538a
Merge branch 'main' into refactor/internal-tools
a033e12
ci: lint
e43d34b
refactor: fat function
ae6b384
refactor: audio param
6c55954
refactor: audio node
7e67258
ci: lint
9b8d15c
refactor: destination
592139a
refactor: audio scheduled source node
af56e5e
refactor: make isInitialized atomic variable
03e2d07
refactor: streamer node
a3547b1
refactor: oscillator and constant source node
14b377e
refactor: buffer base source node
269f56a
fix: nitpicks
f7f063e
refactor: gain, delay, stereo panner nodes
ff82771
ci: lint
60eb11e
refactor: audio buffer source node and related cleanups
1612d47
refactor: audio buffer queue source node
699f75a
fix: nitpicks
9c65120
refactor: wave shaper node
1375a9b
refactor: iir filter node
0298c33
refactor: biquad filter node
1ca5d4c
refactor: convolver node
3def7d7
refactor: part of analyser node
56719b0
Merge branch 'main' into refactor/communication-between-audio-and-js-…
40f2f43
fix: nitpicks
7f84ecf
ci: lint
7d07947
fix: nitpicks
4cc732d
Merge branch 'main' into refactor/communication-between-audio-and-js-…
0552760
ci: lint
8522dd8
refactor: thread-safe setFFTSize and drop support for window types
7173b3d
refactor: analyser node thread safety with lock-free triple buffer
ccd6478
ci: lint
fc26f66
fix: fixed negative latency values in AudioBufferSourceNode
9356844
refactor: moved stretch init and preset to JS thread
0e7e17c
docs: added audio node docs
c799908
refactor: biquad filter thread safety improvements
9bfd34a
ci: lint
edda4bc
docs: doxygen comments for AudioThread-only methods
3a75582
refactor: removed unnecessary headers
mdydek a798e89
ci: lint
9796e1d
Merge branch 'refactor/communication-between-audio-and-js-thread' of …
f7624e1
fix: nitpicks
c03c941
Merge branch 'main' into refactor/communication-between-audio-and-js-…
cb0252e
fix: nitpicks
03f7ee7
refactor: refined TripleBuffer and AnalyserNode for better performanc…
0ed1093
test: tests for TripleBuffer utility class
48f15d7
refactor: thread safe convolver setup
00e9d03
Merge branch 'main' into refactor/communication-between-audio-and-js-…
d4dc870
Merge branch 'main' into refactor/communication-between-audio-and-js-…
9da5b7b
fix: triple buffer polishing
e65aa82
refactor: added concept to ensure TripleBuffer is only instantiated w…
6c36e5a
ci: lint
76220fd
chore: requested changes
maciejmakowski2003 23cb062
chore: yarn format
maciejmakowski2003 1ec4751
Merge branch 'main' into refactor/communication-between-audio-and-js-…
maciejmakowski2003 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
maciejmakowski2003 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.