Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -39,7 +26,7 @@ void AudioParamEventQueue::cancelScheduledValues(double cancelTime) {
}
if (back.getStartTime() >= cancelTime ||
back.getType() == ParamChangeEventType::SET_VALUE_CURVE) {
eventQueue_.popBack();
eventQueue_.pop();
}
}
}
Expand All @@ -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()) {
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

#include <audioapi/core/types/ParamChangeEventType.h>
#include <audioapi/core/utils/ParamChangeEvent.hpp>
#include <audioapi/utils/RingBiDirectionalBuffer.hpp>
#include <audioapi/utils/BoundedPriorityQueue.hpp>

namespace audioapi {

Expand All @@ -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.
Expand Down Expand Up @@ -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<ParamChangeEvent, 32> eventQueue_;
};
struct ParamEventComparator {
bool operator()(const ParamChangeEvent &a, const ParamChangeEvent &b) const {
return a.getAutomationEventTime() < b.getAutomationEventTime();
}
};

BoundedPriorityQueue<ParamChangeEvent, 32, ParamEventComparator> eventQueue_;

inline void setEventEndValueToCurrentValue(ParamChangeEvent &event);
};
} // namespace audioapi
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#pragma once

#include <bit>
#include <functional>
#include <new>
#include <type_traits>
#include <utility>

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<T> (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 <typename T, size_t capacity_, typename Compare = std::less<T>>
class BoundedPriorityQueue {
public:
explicit BoundedPriorityQueue() : size_(0) {
static_assert(isPowerOfTwo(capacity_), "BoundedPriorityQueue's capacity must be a power of 2");
buffer_ = static_cast<TimestampedElement *>(::operator new[](
capacity_ * sizeof(TimestampedElement),
static_cast<std::align_val_t>(alignof(TimestampedElement))));
}

~BoundedPriorityQueue() {
for (size_t i = 0; i < size_; ++i) {
buffer_[i].data.~T();
}
::operator delete[](
buffer_,
capacity_ * sizeof(TimestampedElement),
static_cast<std::align_val_t>(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 <typename U>
bool push(U &&value) noexcept(std::is_nothrow_constructible_v<T, U &&>) {
if (isFull()) [[unlikely]] {
return false;
}
new (&buffer_[size_]) TimestampedElement(std::forward<U>(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<T> && std::is_nothrow_destructible_v<T>) {
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<T>) {
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
Loading
Loading