From d810a2536006363dd4eda0fb3877ee25c00796df Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Tue, 10 Mar 2026 14:44:08 +0100 Subject: [PATCH 01/11] fix: missing error in exponentialRampToValue --- packages/react-native-audio-api/src/core/AudioParam.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-native-audio-api/src/core/AudioParam.ts b/packages/react-native-audio-api/src/core/AudioParam.ts index 3f5d1ee19..ba375c17f 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -56,6 +56,10 @@ export default class AudioParam { value: number, endTime: number ): AudioParam { + if (value === 0) { + throw new RangeError(`value must be a finite positive number: ${value}`); + } + if (endTime <= 0) { throw new RangeError( `endTime must be a finite non-negative number: ${endTime}` From eb0804f31ef14aa8187a48318f62fe905acc97f0 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Tue, 10 Mar 2026 14:46:19 +0100 Subject: [PATCH 02/11] fix: timeConstant edgecase --- .../common/cpp/audioapi/core/AudioParam.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp index be3b6f82c..b06cfcc44 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp @@ -154,6 +154,10 @@ void AudioParam::setTargetAtTime(float target, double startTime, double timeCons // Exponential decay function towards target value auto calculateValue = [timeConstant, target]( double startTime, double, float startValue, float, double time) { + if (timeConstant == 0) { + return target; + } + if (time < startTime) { return startValue; } From bac2dc1907dab18605e60563718b931c8426250f Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Tue, 10 Mar 2026 14:47:52 +0100 Subject: [PATCH 03/11] fix: startValue and endValue edgecases --- .../common/cpp/audioapi/core/AudioParam.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp index b06cfcc44..dddd0d2fc 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp @@ -122,6 +122,10 @@ void AudioParam::exponentialRampToValueAtTime(float value, double endTime) { // Exponential curve function using power law auto calculateValue = [](double startTime, double endTime, float startValue, float endValue, double time) { + if (startValue * endValue < 0 || startValue == 0) { + return startValue; + } + if (time < startTime) { return startValue; } From 01666b8dc95a4214f29f6d2367a974eddb26f5de Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Tue, 10 Mar 2026 17:28:03 +0100 Subject: [PATCH 04/11] fix: error message --- packages/react-native-audio-api/src/core/AudioParam.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-audio-api/src/core/AudioParam.ts b/packages/react-native-audio-api/src/core/AudioParam.ts index ba375c17f..e74f192d1 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -57,7 +57,7 @@ export default class AudioParam { endTime: number ): AudioParam { if (value === 0) { - throw new RangeError(`value must be a finite positive number: ${value}`); + throw new RangeError(`value must be a non-zero number: ${value}`); } if (endTime <= 0) { From 76df938b9dcba940d129f8e6ecc66c79c6603ce8 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Wed, 11 Mar 2026 16:16:51 +0100 Subject: [PATCH 05/11] feat: add BoundedPriorityQueue --- .../audioapi/utils/BoundedPriorityQueue.hpp | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp new file mode 100644 index 000000000..6b4df0612 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -0,0 +1,172 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace audioapi { + +/// @brief A bounded priority queue (min-heap) with fixed capacity. +/// @tparam T The type of elements stored in the queue. +/// @tparam capacity_ The maximum number of elements. Must be a power of two greater than zero. +/// @tparam Compare Comparator type. Defaults to std::less (min-heap: smallest element at top). +/// @note This implementation is NOT thread-safe. +/// @note Capacity must be a power of two and greater than zero. +template > +class BoundedPriorityQueue { + public: + explicit BoundedPriorityQueue() : size_(0) { + static_assert(isPowerOfTwo(capacity_), "BoundedPriorityQueue's capacity must be a power of 2"); + buffer_ = static_cast( + ::operator new[](capacity_ * sizeof(T), static_cast(alignof(T)))); + } + + ~BoundedPriorityQueue() { + for (size_t i = 0; i < size_; ++i) { + buffer_[i].~T(); + } + ::operator delete[](buffer_, capacity_ * sizeof(T), static_cast(alignof(T))); + } + + BoundedPriorityQueue(const BoundedPriorityQueue &) = delete; + BoundedPriorityQueue &operator=(const BoundedPriorityQueue &) = delete; + + /// @brief Push a value into the priority queue. + /// @tparam U The type of the value to push. + /// @param value The value to push. + /// @return True if pushed successfully, false if the queue is full. + template + bool push(U &&value) noexcept(std::is_nothrow_constructible_v) { + if (isFull()) [[unlikely]] { + return false; + } + new (&buffer_[size_]) T(std::forward(value)); + siftUp(size_); + ++size_; + return true; + } + + /// @brief Pop the top (highest priority) element and retrieve it. + /// @param out The popped element. + /// @return True if popped successfully, false if the queue is empty. + bool pop(T &out) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_destructible_v) { + if (isEmpty()) [[unlikely]] { + return false; + } + out = std::move(buffer_[0]); + buffer_[0].~T(); + --size_; + if (size_ > 0) { + new (&buffer_[0]) T(std::move(buffer_[size_])); + buffer_[size_].~T(); + siftDown(0); + } + return true; + } + + /// @brief Pop the top element without retrieving it. + /// @return True if popped successfully, false if the queue is empty. + bool pop() noexcept(std::is_nothrow_destructible_v) { + if (isEmpty()) [[unlikely]] { + return false; + } + buffer_[0].~T(); + --size_; + if (size_ > 0) { + new (&buffer_[0]) T(std::move(buffer_[size_])); + buffer_[size_].~T(); + siftDown(0); + } + return true; + } + + /// @brief Peek at the top (highest priority) element without removing it. + /// @return A const reference to the top element. + [[nodiscard]] inline const T &peek() const noexcept { + return buffer_[0]; + } + + /// @brief Peek at the top (highest priority) element without removing it. + /// @return A mutable reference to the top element. + [[nodiscard]] inline T &peekMut() noexcept { + return buffer_[0]; + } + + /// @brief Check if the queue is empty. + /// @return True if the queue is empty, false otherwise. + [[nodiscard]] inline bool isEmpty() const noexcept { + return size_ == 0; + } + + /// @brief Check if the queue is full. + /// @return True if the queue is full, false otherwise. + [[nodiscard]] inline bool isFull() const noexcept { + return size_ == capacity_; + } + + /// @brief Get the number of elements in the queue. + /// @return The current number of elements. + [[nodiscard]] inline size_t size() const noexcept { + return size_; + } + + /// @brief Get the maximum capacity of the queue. + /// @return The capacity. + [[nodiscard]] inline size_t getCapacity() const noexcept { + return capacity_; + } + + private: + T *buffer_; + size_t size_; + Compare compare_; + + static constexpr bool isPowerOfTwo(size_t n) { + return std::has_single_bit(n); + } + + void siftUp(size_t index) noexcept { + while (index > 0) { + size_t parent = (index - 1) / 2; + if (compare_(buffer_[index], buffer_[parent])) { + swapAt(index, parent); + index = parent; + } else { + break; + } + } + } + + void siftDown(size_t index) noexcept { + while (true) { + size_t left = 2 * index + 1; + size_t right = 2 * index + 2; + size_t top = index; + + if (left < size_ && compare_(buffer_[left], buffer_[top])) { + top = left; + } + if (right < size_ && compare_(buffer_[right], buffer_[top])) { + top = right; + } + if (top == index) { + break; + } + swapAt(index, top); + index = top; + } + } + + void swapAt(size_t a, size_t b) noexcept { + T tmp(std::move(buffer_[a])); + buffer_[a].~T(); + new (&buffer_[a]) T(std::move(buffer_[b])); + buffer_[b].~T(); + new (&buffer_[b]) T(std::move(tmp)); + } +}; + +} // namespace audioapi From 1a2c9c2e6efd2ef80b6daa33d30743900087e406 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 12 Mar 2026 17:36:31 +0100 Subject: [PATCH 06/11] feat: make BoundedPriorityQueue stable --- .../audioapi/utils/BoundedPriorityQueue.hpp | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp index 6b4df0612..d5128d77c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -8,10 +8,10 @@ namespace audioapi { -/// @brief A bounded priority queue (min-heap) with fixed capacity. +/// @brief A bounded priority queue (min-heap) with fixed capacity. When full, new elements are rejected. When popping, the highest priority element is removed and returned. /// @tparam T The type of elements stored in the queue. /// @tparam capacity_ The maximum number of elements. Must be a power of two greater than zero. -/// @tparam Compare Comparator type. Defaults to std::less (min-heap: smallest element at top). +/// @tparam Compare Comparator type. Defaults to std::less (min-heap: smallest element at top). The queue implements a stable priority order meaning that if two elements compare equal, the one that was inserted earlier will be popped first. /// @note This implementation is NOT thread-safe. /// @note Capacity must be a power of two and greater than zero. template > @@ -42,7 +42,7 @@ class BoundedPriorityQueue { if (isFull()) [[unlikely]] { return false; } - new (&buffer_[size_]) T(std::forward(value)); + new (&buffer_[size_]) TimestampedElement(std::forward(value), globalCounter_++); siftUp(size_); ++size_; return true; @@ -51,8 +51,8 @@ class BoundedPriorityQueue { /// @brief Pop the top (highest priority) element and retrieve it. /// @param out The popped element. /// @return True if popped successfully, false if the queue is empty. - bool pop(T &out) noexcept(std::is_nothrow_move_constructible_v && - std::is_nothrow_destructible_v) { + bool pop(T &out) noexcept( + std::is_nothrow_move_constructible_v && std::is_nothrow_destructible_v) { if (isEmpty()) [[unlikely]] { return false; } @@ -85,14 +85,26 @@ class BoundedPriorityQueue { /// @brief Peek at the top (highest priority) element without removing it. /// @return A const reference to the top element. - [[nodiscard]] inline const T &peek() const noexcept { + [[nodiscard]] inline const T &peekFront() const noexcept { return buffer_[0]; } - /// @brief Peek at the top (highest priority) element without removing it. - /// @return A mutable reference to the top element. - [[nodiscard]] inline T &peekMut() noexcept { - return buffer_[0]; + /// @brief Peek at the last (lowest priority) element without removing it. + /// @return A reference to the last element. + [[nodiscard]] inline T &peekFrontMut() noexcept { + return buffer_[size_ - 1]; + } + + /// @brief Peek at the last (lowest priority) element without removing it. + /// @return A reference to the last element. + [[nodiscard]] inline const T &peekBack() const noexcept { + return buffer_[size_ - 1]; + } + + /// @brief Peek at the last (lowest priority) element without removing it. + /// @return A reference to the last element. + [[nodiscard]] inline T &peekBackMut() noexcept { + return buffer_[size_ - 1]; } /// @brief Check if the queue is empty. @@ -120,9 +132,30 @@ class BoundedPriorityQueue { } private: - T *buffer_; + // Internal wrapper to track arrival order + struct TimestampedElement { + T data; + uint64_t insertionOrder; + + // Use the provided Compare for T, but fall back to insertionOrder for ties + struct InternalCompare { + Compare userComp; + bool operator()(const TimestampedElement &a, const TimestampedElement &b) const { + if (userComp(a.data, b.data)) { + return true; + } + if (userComp(b.data, a.data)) { + return false; + } + return a.insertionOrder < b.insertionOrder; + } + }; + }; + + TimestampedElement *buffer_; size_t size_; - Compare compare_; + uint64_t globalCounter_ = 0; + typename TimestampedElement::InternalCompare compare_; static constexpr bool isPowerOfTwo(size_t n) { return std::has_single_bit(n); From 107f3380227b887394232ba8bfeaf9ac1f5005f3 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 12 Mar 2026 18:13:45 +0100 Subject: [PATCH 07/11] feat: integrate new queue --- .../common/cpp/audioapi/core/AudioParam.cpp | 2 +- .../common/cpp/audioapi/core/AudioParam.h | 2 +- .../audioapi/core/utils/AudioParamEventQueue.cpp | 14 +++++++------- .../audioapi/core/utils/AudioParamEventQueue.h | 16 ++++++++++------ .../cpp/audioapi/core/utils/ParamChangeEvent.hpp | 6 ++++++ 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp index 19219dd5e..6d1c7160a 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.cpp @@ -38,7 +38,7 @@ float AudioParam::getValueAtTime(double time) { // next event if (endTime_ < time && !eventsQueue_.isEmpty()) { ParamChangeEvent event; - eventsQueue_.popFront(event); + eventsQueue_.pop(event); startTime_ = event.getStartTime(); endTime_ = event.getEndTime(); startValue_ = event.getStartValue(); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h index 06abccab8..0038a2c0d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioParam.h @@ -138,7 +138,7 @@ class AudioParam { /// @param event The new event to add to the queue. /// @note Handles connecting start value of the new event to the end value of the previous event. inline void updateQueue(ParamChangeEvent &&event) { - eventsQueue_.pushBack(std::move(event)); + eventsQueue_.push(std::move(event)); } float getValueAtTime(double time); void processInputs( diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp index 7270a5a9c..84d887130 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp @@ -6,9 +6,9 @@ namespace audioapi { AudioParamEventQueue::AudioParamEventQueue() : eventQueue_() {} -void AudioParamEventQueue::pushBack(ParamChangeEvent &&event) { +void AudioParamEventQueue::push(ParamChangeEvent &&event) { if (eventQueue_.isEmpty()) { - eventQueue_.pushBack(std::move(event)); + eventQueue_.push(std::move(event)); return; } auto &prev = eventQueue_.peekBackMut(); @@ -24,11 +24,11 @@ void AudioParamEventQueue::pushBack(ParamChangeEvent &&event) { event.getStartTime())); } event.setStartValue(prev.getEndValue()); - eventQueue_.pushBack(std::move(event)); + eventQueue_.push(std::move(event)); } -bool AudioParamEventQueue::popFront(ParamChangeEvent &event) { - return eventQueue_.popFront(event); +bool AudioParamEventQueue::pop(ParamChangeEvent &event) { + return eventQueue_.pop(event); } void AudioParamEventQueue::cancelScheduledValues(double cancelTime) { @@ -39,7 +39,7 @@ void AudioParamEventQueue::cancelScheduledValues(double cancelTime) { } if (back.getStartTime() >= cancelTime || back.getType() == ParamChangeEventType::SET_VALUE_CURVE) { - eventQueue_.popBack(); + eventQueue_.pop(); } } } @@ -50,7 +50,7 @@ void AudioParamEventQueue::cancelAndHoldAtTime(double cancelTime, double &endTim if (back.getEndTime() < cancelTime || back.getStartTime() <= cancelTime) { break; } - eventQueue_.popBack(); + eventQueue_.pop(); } if (eventQueue_.isEmpty()) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h index 37abe6ef8..d4b4ba38b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h @@ -2,7 +2,7 @@ #include #include -#include +#include namespace audioapi { @@ -16,11 +16,11 @@ class AudioParamEventQueue { /// @brief Push a new event to the back of the queue. /// @note Handles connecting the start value of the new event to the end value of the last event in the queue. - void pushBack(ParamChangeEvent &&event); + void push(ParamChangeEvent &&event); /// @brief Pop the front event from the queue. /// @return The front event in the queue. - bool popFront(ParamChangeEvent &event); + bool pop(ParamChangeEvent &event); /// @brief Cancel scheduled parameter changes at or after the given time. /// @param cancelTime The time at which to cancel scheduled changes. @@ -55,9 +55,13 @@ class AudioParamEventQueue { } private: - /// @brief The queue of parameter change events. - /// @note INVARIANT it always holds non-overlapping events sorted by start time. - RingBiDirectionalBuffer eventQueue_; + struct ParamEventComparator { + bool operator()(const ParamChangeEvent &a, const ParamChangeEvent &b) const { + return a.getAutomationEventTime() < b.getAutomationEventTime(); + } + }; + + BoundedPriorityQueue eventQueue_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/ParamChangeEvent.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/ParamChangeEvent.hpp index 22b35a647..af594946c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/ParamChangeEvent.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/ParamChangeEvent.hpp @@ -47,6 +47,12 @@ class ParamChangeEvent { return *this; } + [[nodiscard]] inline double getAutomationEventTime() const noexcept { + bool isRamp = type_ == ParamChangeEventType::LINEAR_RAMP || + type_ == ParamChangeEventType::EXPONENTIAL_RAMP; + + return isRamp ? endTime_ : startTime_; + } [[nodiscard]] inline double getEndTime() const noexcept { return endTime_; } From 8a5903533cb295813a83bbf27e5d2f819b278c12 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 12 Mar 2026 20:02:20 +0100 Subject: [PATCH 08/11] feat: update queue to support curve exclusion --- .../core/utils/AudioParamEventQueue.cpp | 86 +++++++++++++++---- .../core/utils/AudioParamEventQueue.h | 7 +- .../audioapi/utils/BoundedPriorityQueue.hpp | 6 ++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp index 84d887130..74b15bb72 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp @@ -1,30 +1,28 @@ #include #include +#include +#include #include +#include "audioapi/utils/Result.hpp" namespace audioapi { AudioParamEventQueue::AudioParamEventQueue() : eventQueue_() {} -void AudioParamEventQueue::push(ParamChangeEvent &&event) { +Result AudioParamEventQueue::push(ParamChangeEvent &&event) { if (eventQueue_.isEmpty()) { eventQueue_.push(std::move(event)); - return; + return Ok(None); } - auto &prev = eventQueue_.peekBackMut(); - if (prev.getType() == ParamChangeEventType::SET_TARGET) { - prev.setEndTime(event.getStartTime()); - // Calculate what the SET_TARGET value would be at the new event's start - // time - prev.setEndValue(prev.getCalculateValue()( - prev.getStartTime(), - prev.getEndTime(), - prev.getStartValue(), - prev.getEndValue(), - event.getStartTime())); + + auto isValid = satisfiesCurveExclusion(event); + if (isValid.is_err()) { + return Err(isValid.unwrap_err()); } - event.setStartValue(prev.getEndValue()); + + setEventEndValueToCurrentValue(event); eventQueue_.push(std::move(event)); + return Ok(None); } bool AudioParamEventQueue::pop(ParamChangeEvent &event) { @@ -68,4 +66,62 @@ void AudioParamEventQueue::cancelAndHoldAtTime(double cancelTime, double &endTim back.setEndTime(std::min(cancelTime, back.getEndTime())); } -} // namespace audioapi +void AudioParamEventQueue::setEventEndValueToCurrentValue(ParamChangeEvent &event) { + auto &prev = eventQueue_.peekBackMut(); + if (prev.getType() == ParamChangeEventType::SET_TARGET) { + prev.setEndTime(event.getStartTime()); + // Calculate what the SET_TARGET value would be at the new event's start + // time + prev.setEndValue(prev.getCalculateValue()( + prev.getStartTime(), + prev.getEndTime(), + prev.getStartValue(), + prev.getEndValue(), + event.getStartTime())); + } + event.setStartValue(prev.getEndValue()); +} + +Result AudioParamEventQueue::satisfiesCurveExclusion( + const ParamChangeEvent &event) const { + double newT = event.getStartTime(); + bool isSetValueCurveAtTime = (event.getType() == ParamChangeEventType::SET_VALUE_CURVE); + double newD = isSetValueCurveAtTime ? event.getEndTime() - event.getStartTime() : 0.0; + + for (size_t i = 0; i < eventQueue_.size(); ++i) { + const auto &existing = eventQueue_.peekAt(i); + double existingT = existing.getStartTime(); + + // 1. Check if existing curve blocks the new event + // Any automation method called at a time in [T, T+D) of an existing curve is not allowed + if (existing.getType() == ParamChangeEventType::SET_VALUE_CURVE) { + double existingD = existing.getEndTime() - + existing.getStartTime(); // TODO: is arthimetic on floating point safe here? + if (newT >= existingT && newT < (existingT + existingD)) { + return Err( + std::format( + "Cannot schedule event {} at time {} because it overlaps with an existing SetValueCurveAtTime event from {} to {}", + static_cast(event.getType()), // TODO add event type to string conversion + newT, + existingT, + existingT + existingD)); + } + } + + // 2. If new event is a curve, existing events strictly inside (T, T+D) are not allowed + if (isSetValueCurveAtTime) { + if (existingT > newT && existingT < (newT + newD)) { + return Err( + std::format( + "Cannot schedule SetValueCurveAtTime event from {} to {} because it overlaps with an existing event {} at time {}", + newT, + newT + newD, + static_cast(existing.getType()), // TODO add event type to string conversion + existingT)); + } + } + } + + return Ok(None); +} +} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h index d4b4ba38b..46eb690e1 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include "audioapi/utils/Result.hpp" namespace audioapi { @@ -16,7 +18,7 @@ class AudioParamEventQueue { /// @brief Push a new event to the back of the queue. /// @note Handles connecting the start value of the new event to the end value of the last event in the queue. - void push(ParamChangeEvent &&event); + Result push(ParamChangeEvent &&event); /// @brief Pop the front event from the queue. /// @return The front event in the queue. @@ -62,6 +64,9 @@ class AudioParamEventQueue { }; BoundedPriorityQueue eventQueue_; + + Result satisfiesCurveExclusion(const ParamChangeEvent &event) const; + inline void setEventEndValueToCurrentValue(ParamChangeEvent &event); }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp index d5128d77c..eeaabd06b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -131,6 +131,12 @@ class BoundedPriorityQueue { return capacity_; } + /// @brief Peek at the i-th element in the internal buffer (heap order, not sorted). + /// @note Intended for iterating over all elements without removing them. + [[nodiscard]] inline const T &peekAt(size_t i) const noexcept { + return buffer_[i].data; + } + private: // Internal wrapper to track arrival order struct TimestampedElement { From ee635d3fe0ebd6c312717b399181f1d13e908f67 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Fri, 13 Mar 2026 18:08:05 +0100 Subject: [PATCH 09/11] fix: curve exclusion time windows --- .../cpp/audioapi/core/utils/AudioParamEventQueue.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp index 74b15bb72..a76f70223 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp @@ -95,16 +95,15 @@ Result AudioParamEventQueue::satisfiesCurveExclusion( // 1. Check if existing curve blocks the new event // Any automation method called at a time in [T, T+D) of an existing curve is not allowed if (existing.getType() == ParamChangeEventType::SET_VALUE_CURVE) { - double existingD = existing.getEndTime() - - existing.getStartTime(); // TODO: is arthimetic on floating point safe here? - if (newT >= existingT && newT < (existingT + existingD)) { + double existingEndTime = existing.getEndTime(); + if (newT >= existingT && newT < existingEndTime) { return Err( std::format( "Cannot schedule event {} at time {} because it overlaps with an existing SetValueCurveAtTime event from {} to {}", static_cast(event.getType()), // TODO add event type to string conversion newT, existingT, - existingT + existingD)); + existingEndTime)); } } From 6c5cfa88a6195471879d3460ac87e14d5c6bd761 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Mon, 16 Mar 2026 10:56:50 +0100 Subject: [PATCH 10/11] fix: queue element handling --- .../audioapi/utils/BoundedPriorityQueue.hpp | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp index eeaabd06b..f8eace9bc 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -19,15 +19,19 @@ class BoundedPriorityQueue { public: explicit BoundedPriorityQueue() : size_(0) { static_assert(isPowerOfTwo(capacity_), "BoundedPriorityQueue's capacity must be a power of 2"); - buffer_ = static_cast( - ::operator new[](capacity_ * sizeof(T), static_cast(alignof(T)))); + buffer_ = static_cast(::operator new[]( + capacity_ * sizeof(TimestampedElement), + static_cast(alignof(TimestampedElement)))); } ~BoundedPriorityQueue() { for (size_t i = 0; i < size_; ++i) { - buffer_[i].~T(); + buffer_[i].data.~T(); } - ::operator delete[](buffer_, capacity_ * sizeof(T), static_cast(alignof(T))); + ::operator delete[]( + buffer_, + capacity_ * sizeof(TimestampedElement), + static_cast(alignof(TimestampedElement))); } BoundedPriorityQueue(const BoundedPriorityQueue &) = delete; @@ -56,12 +60,12 @@ class BoundedPriorityQueue { if (isEmpty()) [[unlikely]] { return false; } - out = std::move(buffer_[0]); - buffer_[0].~T(); + out = std::move(buffer_[0].data); + buffer_[0].~TimestampedElement(); --size_; if (size_ > 0) { - new (&buffer_[0]) T(std::move(buffer_[size_])); - buffer_[size_].~T(); + new (&buffer_[0]) TimestampedElement(std::move(buffer_[size_])); + buffer_[size_].~TimestampedElement(); siftDown(0); } return true; @@ -73,11 +77,11 @@ class BoundedPriorityQueue { if (isEmpty()) [[unlikely]] { return false; } - buffer_[0].~T(); + buffer_[0].~TimestampedElement(); --size_; if (size_ > 0) { - new (&buffer_[0]) T(std::move(buffer_[size_])); - buffer_[size_].~T(); + new (&buffer_[0]) TimestampedElement(std::move(buffer_[size_])); + buffer_[size_].~TimestampedElement(); siftDown(0); } return true; @@ -86,25 +90,25 @@ class BoundedPriorityQueue { /// @brief Peek at the top (highest priority) element without removing it. /// @return A const reference to the top element. [[nodiscard]] inline const T &peekFront() const noexcept { - return buffer_[0]; + return buffer_[0].data; } /// @brief Peek at the last (lowest priority) element without removing it. /// @return A reference to the last element. [[nodiscard]] inline T &peekFrontMut() noexcept { - return buffer_[size_ - 1]; + return buffer_[size_ - 1].data; } /// @brief Peek at the last (lowest priority) element without removing it. /// @return A reference to the last element. [[nodiscard]] inline const T &peekBack() const noexcept { - return buffer_[size_ - 1]; + return buffer_[size_ - 1].data; } /// @brief Peek at the last (lowest priority) element without removing it. /// @return A reference to the last element. [[nodiscard]] inline T &peekBackMut() noexcept { - return buffer_[size_ - 1]; + return buffer_[size_ - 1].data; } /// @brief Check if the queue is empty. @@ -200,11 +204,11 @@ class BoundedPriorityQueue { } void swapAt(size_t a, size_t b) noexcept { - T tmp(std::move(buffer_[a])); - buffer_[a].~T(); - new (&buffer_[a]) T(std::move(buffer_[b])); - buffer_[b].~T(); - new (&buffer_[b]) T(std::move(tmp)); + TimestampedElement tmp(std::move(buffer_[a])); + buffer_[a].~TimestampedElement(); + new (&buffer_[a]) TimestampedElement(std::move(buffer_[b])); + buffer_[b].~TimestampedElement(); + new (&buffer_[b]) TimestampedElement(std::move(tmp)); } }; From 4c1c8dfe282da16bc7d4c72c397f4f3afa9db99f Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Mon, 23 Mar 2026 14:08:10 +0100 Subject: [PATCH 11/11] feat: add JS priority queue --- .../core/utils/AudioParamEventQueue.cpp | 54 +---- .../core/utils/AudioParamEventQueue.h | 6 +- .../src/core/AudioParam.ts | 19 +- packages/react-native-audio-api/src/types.ts | 15 ++ .../src/utils/audio-param/AutomationEvent.ts | 17 ++ .../utils/audio-param/AutomationEventQueue.ts | 79 +++++++ .../utils/audio-param/BoundedPriorityQueue.ts | 201 ++++++++++++++++++ 7 files changed, 324 insertions(+), 67 deletions(-) create mode 100644 packages/react-native-audio-api/src/utils/audio-param/AutomationEvent.ts create mode 100644 packages/react-native-audio-api/src/utils/audio-param/AutomationEventQueue.ts create mode 100644 packages/react-native-audio-api/src/utils/audio-param/BoundedPriorityQueue.ts diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp index a76f70223..581d3587a 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp @@ -1,28 +1,17 @@ #include #include -#include -#include #include -#include "audioapi/utils/Result.hpp" namespace audioapi { AudioParamEventQueue::AudioParamEventQueue() : eventQueue_() {} -Result AudioParamEventQueue::push(ParamChangeEvent &&event) { +void AudioParamEventQueue::push(ParamChangeEvent &&event) { if (eventQueue_.isEmpty()) { eventQueue_.push(std::move(event)); - return Ok(None); } - - auto isValid = satisfiesCurveExclusion(event); - if (isValid.is_err()) { - return Err(isValid.unwrap_err()); - } - setEventEndValueToCurrentValue(event); eventQueue_.push(std::move(event)); - return Ok(None); } bool AudioParamEventQueue::pop(ParamChangeEvent &event) { @@ -82,45 +71,4 @@ void AudioParamEventQueue::setEventEndValueToCurrentValue(ParamChangeEvent &even event.setStartValue(prev.getEndValue()); } -Result AudioParamEventQueue::satisfiesCurveExclusion( - const ParamChangeEvent &event) const { - double newT = event.getStartTime(); - bool isSetValueCurveAtTime = (event.getType() == ParamChangeEventType::SET_VALUE_CURVE); - double newD = isSetValueCurveAtTime ? event.getEndTime() - event.getStartTime() : 0.0; - - for (size_t i = 0; i < eventQueue_.size(); ++i) { - const auto &existing = eventQueue_.peekAt(i); - double existingT = existing.getStartTime(); - - // 1. Check if existing curve blocks the new event - // Any automation method called at a time in [T, T+D) of an existing curve is not allowed - if (existing.getType() == ParamChangeEventType::SET_VALUE_CURVE) { - double existingEndTime = existing.getEndTime(); - if (newT >= existingT && newT < existingEndTime) { - return Err( - std::format( - "Cannot schedule event {} at time {} because it overlaps with an existing SetValueCurveAtTime event from {} to {}", - static_cast(event.getType()), // TODO add event type to string conversion - newT, - existingT, - existingEndTime)); - } - } - - // 2. If new event is a curve, existing events strictly inside (T, T+D) are not allowed - if (isSetValueCurveAtTime) { - if (existingT > newT && existingT < (newT + newD)) { - return Err( - std::format( - "Cannot schedule SetValueCurveAtTime event from {} to {} because it overlaps with an existing event {} at time {}", - newT, - newT + newD, - static_cast(existing.getType()), // TODO add event type to string conversion - existingT)); - } - } - } - - return Ok(None); -} } // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h index 46eb690e1..03f152a0b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h @@ -3,8 +3,6 @@ #include #include #include -#include -#include "audioapi/utils/Result.hpp" namespace audioapi { @@ -18,7 +16,7 @@ class AudioParamEventQueue { /// @brief Push a new event to the back of the queue. /// @note Handles connecting the start value of the new event to the end value of the last event in the queue. - Result push(ParamChangeEvent &&event); + void push(ParamChangeEvent &&event); /// @brief Pop the front event from the queue. /// @return The front event in the queue. @@ -65,8 +63,6 @@ class AudioParamEventQueue { BoundedPriorityQueue eventQueue_; - Result satisfiesCurveExclusion(const ParamChangeEvent &event) const; inline void setEventEndValueToCurrentValue(ParamChangeEvent &event); }; - } // namespace audioapi diff --git a/packages/react-native-audio-api/src/core/AudioParam.ts b/packages/react-native-audio-api/src/core/AudioParam.ts index e74f192d1..a305b0a51 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -1,21 +1,22 @@ import { IAudioParam } from '../interfaces'; import { RangeError, InvalidStateError } from '../errors'; import BaseAudioContext from './BaseAudioContext'; +import { AutomationEventQueue } from '../utils/audio-param/AutomationEventQueue'; export default class AudioParam { - readonly defaultValue: number; - readonly minValue: number; - readonly maxValue: number; - readonly audioParam: IAudioParam; - readonly context: BaseAudioContext; - - constructor(audioParam: IAudioParam, context: BaseAudioContext) { - this.audioParam = audioParam; + public readonly defaultValue: number; + public readonly minValue: number; + public readonly maxValue: number; + public readonly eventQueue = new AutomationEventQueue(); + + constructor( + public readonly audioParam: IAudioParam, + public readonly context: BaseAudioContext + ) { this.value = audioParam.value; this.defaultValue = audioParam.defaultValue; this.minValue = audioParam.minValue; this.maxValue = audioParam.maxValue; - this.context = context; } public get value(): number { diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index 67450bf0b..9a1b4d2d9 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -247,3 +247,18 @@ export type DecodeDataInput = number | string | ArrayBuffer; export interface AudioRecorderStartOptions { fileNameOverride?: string; } + +export type TimestampedElement = { + data: T; + insertionOrder: number; +}; + +export type Comparator = (a: T, b: T) => boolean; + +export enum AutomationEventType { + SetValueAtTime = 'setValueAtTime', + LinearRampToValueAtTime = 'linearRampToValueAtTime', + ExponentialRampToValueAtTime = 'exponentialRampToValueAtTime', + SetTargetAtTime = 'setTargetAtTime', + SetValueCurveAtTime = 'setValueCurveAtTime', +} diff --git a/packages/react-native-audio-api/src/utils/audio-param/AutomationEvent.ts b/packages/react-native-audio-api/src/utils/audio-param/AutomationEvent.ts new file mode 100644 index 000000000..6da76a985 --- /dev/null +++ b/packages/react-native-audio-api/src/utils/audio-param/AutomationEvent.ts @@ -0,0 +1,17 @@ +import { AutomationEventType } from '../../types'; + +export class AutomationEvent { + // eslint-disable-next-line no-useless-constructor + constructor( + public readonly type: AutomationEventType, + public readonly startTime: number, + public readonly endTime: number + ) {} + + public get automationTime(): number { + const isRamp = + this.type === AutomationEventType.LinearRampToValueAtTime || + this.type === AutomationEventType.ExponentialRampToValueAtTime; + return isRamp ? this.endTime : this.startTime; + } +} diff --git a/packages/react-native-audio-api/src/utils/audio-param/AutomationEventQueue.ts b/packages/react-native-audio-api/src/utils/audio-param/AutomationEventQueue.ts new file mode 100644 index 000000000..4a171df09 --- /dev/null +++ b/packages/react-native-audio-api/src/utils/audio-param/AutomationEventQueue.ts @@ -0,0 +1,79 @@ +import { AutomationEventType, Result } from '../../types'; +import { AutomationEvent } from './AutomationEvent'; +import { BoundedPriorityQueue } from './BoundedPriorityQueue'; + +/** + * A priority queue of automation events, ordered by start time. Enforces the + * curve exclusion rule from the Web Audio spec: + * https://webaudio.github.io/web-audio-api/#AudioParam. The queue is bounded to + * a fixed capacity, and rejects new events when full. This is a utility used by + * AudioParam for scheduling automation events. + * + * @param capacity - Maximum number of events the queue can hold. Must be a + * power of two greater than zero. + * @throws NotSupportedError if a new event violates the curve exclusion rule + * with existing events in the queue. + */ +export class AutomationEventQueue { + private readonly queue: BoundedPriorityQueue; + + constructor(capacity: number = 32) { + this.queue = new BoundedPriorityQueue( + capacity, + (a, b) => a.automationTime < b.automationTime + ); + } + + /** + * Push a new automation event into the queue, enforcing the curve exclusion + * rule. + * + * @param event - The automation event to be scheduled. + * @returns True if the event was successfully scheduled, false if the queue + * is full. + * @throws NotSupportedError if the event violates the curve exclusion rule + * with existing events in the queue. + */ + public push(event: AutomationEvent): Result<{ value: boolean }> { + const curveExclusionResult = this.satisfiesCurveExclusion(event); + if (curveExclusionResult.status === 'error') { + return curveExclusionResult; + } + return { status: 'success', value: this.queue.push(event) }; + } + + // the curve exclusion rule (Web Audio spec ยง1.6). + // https://webaudio.github.io/web-audio-api/#AudioParam + private satisfiesCurveExclusion(newEvent: AutomationEvent): Result<{}> { + const newT = newEvent.startTime; + const isSetValueCurve = + newEvent.type === AutomationEventType.SetValueCurveAtTime; + const newD = isSetValueCurve ? newEvent.endTime - newEvent.startTime : 0; + + for (let i = 0; i < this.queue.size; i++) { + const existing = this.queue.peekAt(i); + const existingT = existing.startTime; + + // 1. Any event scheduled inside an existing curve's [T, T+D) is not allowed. + if (existing.type === AutomationEventType.SetValueCurveAtTime) { + const existingEndTime = existing.endTime; + if (newT >= existingT && newT < existingEndTime) { + return { + status: 'error', + message: `Cannot schedule event ${newEvent.type} at time ${newT} because it overlaps with an existing SetValueCurveAtTime event from ${existingT} to ${existingEndTime}`, + }; + } + } + + // 2. A new curve may not contain any existing event in its open interval (T, T+D). + if (isSetValueCurve && existingT > newT && existingT < newT + newD) { + return { + status: 'error', + message: `Cannot schedule SetValueCurveAtTime event from ${newT} to ${newT + newD} because it overlaps with an existing event ${existing.type} at time ${existingT}`, + }; + } + } + + return { status: 'success' }; + } +} diff --git a/packages/react-native-audio-api/src/utils/audio-param/BoundedPriorityQueue.ts b/packages/react-native-audio-api/src/utils/audio-param/BoundedPriorityQueue.ts new file mode 100644 index 000000000..e86c4a4bc --- /dev/null +++ b/packages/react-native-audio-api/src/utils/audio-param/BoundedPriorityQueue.ts @@ -0,0 +1,201 @@ +import { Comparator, TimestampedElement } from '../../types'; + +/** + * A bounded priority queue (min-heap) with fixed capacity. When full, new + * elements are rejected. Implements stable priority order: equal-priority + * elements are popped in insertion order. + * + * @param capacity - Maximum number of elements. Must be a power of two greater + * than zero. + * @param compare - Returns true if `a` has higher priority than `b` (default: + * min-heap by value). + */ +export class BoundedPriorityQueue { + private readonly buffer: Array | undefined>; + private size_: number; + private globalCounter: number; + + constructor( + readonly capacity: number = 32, + private readonly compare: Comparator + ) { + if (!BoundedPriorityQueue.isPowerOfTwo(capacity)) { + throw new Error( + `BoundedPriorityQueue capacity must be a power of two, got: ${capacity}` + ); + } + this.buffer = new Array(capacity).fill(undefined); + this.size_ = 0; + this.globalCounter = 0; + } + + /** + * Push a value into the priority queue. + * + * @returns True if pushed successfully, false if the queue is full. + */ + public push(value: T): boolean { + if (this.isFull()) { + return false; + } + + this.buffer[this.size_] = { + data: value, + insertionOrder: this.globalCounter++, + }; + this.siftUp(this.size_); + this.size_++; + + return true; + } + + /** + * Pop the top (highest priority) element and return it. + * + * @returns The popped element, or undefined if the queue is empty. + */ + public pop(): T | undefined { + if (this.isEmpty()) { + return undefined; + } + + const top = this.buffer[0]!.data; + + this.size_--; + + if (this.size_ > 0) { + this.buffer[0] = this.buffer[this.size_]; + this.buffer[this.size_] = undefined; + this.siftDown(0); + } else { + this.buffer[0] = undefined; + } + + return top; + } + + /** + * Peek at the top (highest priority / front) element without removing it. + * + * @returns The front element, or undefined if the queue is empty. + */ + public peekFront(): T { + return this.buffer[0]!.data; + } + + /** + * Peek at the last element in the internal buffer without removing it. This + * is the element at index size-1 in heap storage, not necessarily the lowest + * priority. + * + * @returns The back element, or undefined if the queue is empty. + */ + public peekBack(): T { + return this.buffer[this.size_ - 1]!.data; + } + + /** + * Peek at the i-th element in the internal buffer (heap order, not sorted). + * Intended for iterating over all elements without removing them. + * + * @param i - Index in the internal buffer. + * @returns The element at index i, or undefined if out of bounds. + */ + public peekAt(i: number): T { + return this.buffer[i]!.data; + } + + /** + * Check if the queue is empty. + * + * @returns True if the queue is empty, false otherwise. + */ + public isEmpty(): boolean { + return this.size_ === 0; + } + + /** + * Check if the queue is full. + * + * @returns True if the queue is full, false otherwise. + */ + public isFull(): boolean { + return this.size_ === this.capacity; + } + + /** + * Get the number of elements currently in the queue. + * + * @returns The number of elements in the queue. + */ + public get size(): number { + return this.size_; + } + + private internalCompare( + a: TimestampedElement, + b: TimestampedElement + ): boolean { + if (this.compare(a.data, b.data)) { + return true; + } + + if (this.compare(b.data, a.data)) { + return false; + } + + return a.insertionOrder < b.insertionOrder; + } + + private siftUp(index: number): void { + while (index > 0) { + const parent = Math.floor((index - 1) / 2); + + if (this.internalCompare(this.buffer[index]!, this.buffer[parent]!)) { + this.swapAt(index, parent); + index = parent; + } else { + break; + } + } + } + + private siftDown(index: number): void { + while (true) { + const left = 2 * index + 1; + const right = 2 * index + 2; + let top = index; + + if ( + left < this.size_ && + this.internalCompare(this.buffer[left]!, this.buffer[top]!) + ) { + top = left; + } + + if ( + right < this.size_ && + this.internalCompare(this.buffer[right]!, this.buffer[top]!) + ) { + top = right; + } + + if (top === index) { + break; + } + + this.swapAt(index, top); + index = top; + } + } + + private swapAt(a: number, b: number): void { + const tmp = this.buffer[a]; + this.buffer[a] = this.buffer[b]; + this.buffer[b] = tmp; + } + + private static isPowerOfTwo(n: number): boolean { + return n > 0 && (n & (n - 1)) === 0; + } +}