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 bb440803b..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(); @@ -115,6 +115,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; } @@ -143,6 +147,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; } 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..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 @@ -6,29 +6,16 @@ namespace audioapi { AudioParamEventQueue::AudioParamEventQueue() : eventQueue_() {} -void AudioParamEventQueue::pushBack(ParamChangeEvent &&event) { +void AudioParamEventQueue::push(ParamChangeEvent &&event) { if (eventQueue_.isEmpty()) { - eventQueue_.pushBack(std::move(event)); - return; + eventQueue_.push(std::move(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()); - eventQueue_.pushBack(std::move(event)); + setEventEndValueToCurrentValue(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 +26,7 @@ void AudioParamEventQueue::cancelScheduledValues(double cancelTime) { } if (back.getStartTime() >= cancelTime || back.getType() == ParamChangeEventType::SET_VALUE_CURVE) { - eventQueue_.popBack(); + eventQueue_.pop(); } } } @@ -50,7 +37,7 @@ void AudioParamEventQueue::cancelAndHoldAtTime(double cancelTime, double &endTim if (back.getEndTime() < cancelTime || back.getStartTime() <= cancelTime) { break; } - eventQueue_.popBack(); + eventQueue_.pop(); } if (eventQueue_.isEmpty()) { @@ -68,4 +55,20 @@ 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()); +} + +} // 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 37abe6ef8..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 @@ -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,14 @@ 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_; + inline void setEventEndValueToCurrentValue(ParamChangeEvent &event); +}; } // 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_; } 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..f8eace9bc --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -0,0 +1,215 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace audioapi { + +/// @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). 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 > +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(TimestampedElement), + static_cast(alignof(TimestampedElement)))); + } + + ~BoundedPriorityQueue() { + for (size_t i = 0; i < size_; ++i) { + buffer_[i].data.~T(); + } + ::operator delete[]( + buffer_, + capacity_ * sizeof(TimestampedElement), + static_cast(alignof(TimestampedElement))); + } + + 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_]) TimestampedElement(std::forward(value), globalCounter_++); + 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].data); + buffer_[0].~TimestampedElement(); + --size_; + if (size_ > 0) { + new (&buffer_[0]) TimestampedElement(std::move(buffer_[size_])); + buffer_[size_].~TimestampedElement(); + 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].~TimestampedElement(); + --size_; + if (size_ > 0) { + new (&buffer_[0]) TimestampedElement(std::move(buffer_[size_])); + buffer_[size_].~TimestampedElement(); + 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 &peekFront() const noexcept { + 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].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].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].data; + } + + /// @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_; + } + + /// @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 { + 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_; + uint64_t globalCounter_ = 0; + typename TimestampedElement::InternalCompare 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 { + 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)); + } +}; + +} // 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 3f5d1ee19..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 { @@ -56,6 +57,10 @@ export default class AudioParam { value: number, endTime: number ): AudioParam { + if (value === 0) { + throw new RangeError(`value must be a non-zero number: ${value}`); + } + if (endTime <= 0) { throw new RangeError( `endTime must be a finite non-negative number: ${endTime}` 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; + } +}