diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp index 6d306b68c..6d74bc732 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.cpp @@ -1,10 +1,13 @@ #include #include +#include #include #include +#include #include +#include "audioapi/jsi/JsiHostObject.h" namespace audioapi { @@ -26,7 +29,8 @@ AudioParamHostObject::AudioParamHostObject(const std::shared_ptr &pa JSI_EXPORT_FUNCTION(AudioParamHostObject, setTargetAtTime), JSI_EXPORT_FUNCTION(AudioParamHostObject, setValueCurveAtTime), JSI_EXPORT_FUNCTION(AudioParamHostObject, cancelScheduledValues), - JSI_EXPORT_FUNCTION(AudioParamHostObject, cancelAndHoldAtTime)); + JSI_EXPORT_FUNCTION(AudioParamHostObject, cancelAndHoldAtTime), + JSI_EXPORT_FUNCTION(AudioParamHostObject, checkCurveExclusion)); addSetters(JSI_EXPORT_PROPERTY_SETTER(AudioParamHostObject, value)); } @@ -56,9 +60,11 @@ JSI_PROPERTY_SETTER_IMPL(AudioParamHostObject, value) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setValueAtTime) { - auto event = [param = param_, - value = static_cast(args[0].getNumber()), - startTime = args[1].getNumber()](BaseAudioContext &) { + auto startTime = args[1].getNumber(); + controlQueue_.push(AutomationEvent(AutomationEventType::SET_VALUE, startTime)); + + auto event = [param = param_, value = static_cast(args[0].getNumber()), startTime]( + BaseAudioContext &) { param->setValueAtTime(value, startTime); }; @@ -67,9 +73,11 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setValueAtTime) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, linearRampToValueAtTime) { - auto event = [param = param_, - value = static_cast(args[0].getNumber()), - endTime = args[1].getNumber()](BaseAudioContext &) { + auto endTime = args[1].getNumber(); + controlQueue_.push(AutomationEvent(AutomationEventType::LINEAR_RAMP, endTime)); + + auto event = [param = param_, value = static_cast(args[0].getNumber()), endTime]( + BaseAudioContext &) { param->linearRampToValueAtTime(value, endTime); }; @@ -78,9 +86,11 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, linearRampToValueAtTime) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, exponentialRampToValueAtTime) { - auto event = [param = param_, - value = static_cast(args[0].getNumber()), - endTime = args[1].getNumber()](BaseAudioContext &) { + auto endTime = args[1].getNumber(); + controlQueue_.push(AutomationEvent(AutomationEventType::EXPONENTIAL_RAMP, endTime)); + + auto event = [param = param_, value = static_cast(args[0].getNumber()), endTime]( + BaseAudioContext &) { param->exponentialRampToValueAtTime(value, endTime); }; @@ -89,9 +99,12 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, exponentialRampToValueAtTime) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setTargetAtTime) { + auto startTime = args[1].getNumber(); + controlQueue_.push(AutomationEvent(AutomationEventType::SET_TARGET, startTime)); + auto event = [param = param_, target = static_cast(args[0].getNumber()), - startTime = args[1].getNumber(), + startTime, timeConstant = args[2].getNumber()](BaseAudioContext &) { param->setTargetAtTime(target, startTime, timeConstant); }; @@ -101,17 +114,18 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setTargetAtTime) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setValueCurveAtTime) { + auto startTime = args[1].getNumber(); + auto duration = args[2].getNumber(); + controlQueue_.push( + AutomationEvent(AutomationEventType::SET_VALUE_CURVE, startTime, startTime + duration)); + auto arrayBuffer = args[0].getObject(runtime).getPropertyAsObject(runtime, "buffer").getArrayBuffer(runtime); auto *rawValues = reinterpret_cast(arrayBuffer.data(runtime)); auto length = static_cast(arrayBuffer.size(runtime) / sizeof(float)); auto values = std::make_shared(rawValues, length); - auto event = [param = param_, - values, - length, - startTime = args[1].getNumber(), - duration = args[2].getNumber()](BaseAudioContext &) { + auto event = [param = param_, values, length, startTime, duration](BaseAudioContext &) { param->setValueCurveAtTime(values, length, startTime, duration); }; @@ -120,7 +134,10 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setValueCurveAtTime) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelScheduledValues) { - auto event = [param = param_, cancelTime = args[0].getNumber()](BaseAudioContext &) { + auto cancelTime = args[0].getNumber(); + controlQueue_.cancelScheduledValues(cancelTime); + + auto event = [param = param_, cancelTime](BaseAudioContext &) { param->cancelScheduledValues(cancelTime); }; @@ -129,7 +146,10 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelScheduledValues) { } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelAndHoldAtTime) { - auto event = [param = param_, cancelTime = args[0].getNumber()](BaseAudioContext &) { + auto cancelTime = args[0].getNumber(); + controlQueue_.cancelScheduledValues(cancelTime); + + auto event = [param = param_, cancelTime](BaseAudioContext &) { param->cancelAndHoldAtTime(cancelTime); }; @@ -137,4 +157,40 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelAndHoldAtTime) { return jsi::Value::undefined(); } +JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, checkCurveExclusion) { + + auto checkExclusionResult = checkCurveExclusionFromJSI(runtime, args); + + auto jsResult = jsi::Object(runtime); + jsResult.setProperty( + runtime, + "status", + jsi::String::createFromUtf8(runtime, checkExclusionResult.is_ok() ? "success" : "error")); + if (checkExclusionResult.is_err()) { + jsResult.setProperty( + runtime, + "message", + jsi::String::createFromUtf8(runtime, checkExclusionResult.unwrap_err())); + } + return jsResult; +} + +Result AudioParamHostObject::checkCurveExclusionFromJSI( + jsi::Runtime &runtime, + const jsi::Value *args) { + auto arg = args[0].getObject(runtime); + auto type = static_cast(arg.getProperty(runtime, "type").getNumber()); + auto automationTime = arg.getProperty(runtime, "automationTime").getNumber(); + + AutomationEvent event; + if (type == AutomationEventType::SET_VALUE_CURVE) { + auto duration = arg.getProperty(runtime, "duration").getNumber(); + event = AutomationEvent(type, automationTime, automationTime + duration); + } else { + event = AutomationEvent(type, automationTime); + } + + return controlQueue_.checkCurveExclusion(event); +} + } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h index 64cd8b8fe..e3006228e 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/AudioParamHostObject.h @@ -5,6 +5,9 @@ #include #include #include +#include +#include "audioapi/core/utils/automation/AutomationControlQueue.h" +#include "audioapi/utils/Result.hpp" namespace audioapi { using namespace facebook; @@ -30,12 +33,19 @@ class AudioParamHostObject : public JsiHostObject { JSI_HOST_FUNCTION_DECL(cancelScheduledValues); JSI_HOST_FUNCTION_DECL(cancelAndHoldAtTime); + JSI_HOST_FUNCTION_DECL(checkCurveExclusion); + private: friend class AudioNodeHostObject; std::shared_ptr param_; + AutomationControlQueue controlQueue_; float defaultValue_; float minValue_; float maxValue_; + + Result checkCurveExclusionFromJSI( + jsi::Runtime &runtime, + const jsi::Value *args); }; } // namespace audioapi 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 396b79942..93fe11c34 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 @@ -5,6 +5,7 @@ #include #include #include +#include "audioapi/core/utils/automation/AutomationRenderEventFactory.hpp" namespace audioapi { @@ -18,153 +19,37 @@ AudioParam::AudioParam( defaultValue_(defaultValue), minValue_(minValue), maxValue_(maxValue), - startTime_(0), - endTime_(0), - startValue_(defaultValue), - endValue_(defaultValue), + eventRenderQueue_(defaultValue), audioBuffer_( std::make_shared(RENDER_QUANTUM_SIZE, 1, context->getSampleRate())) { inputBuffers_.reserve(4); inputNodes_.reserve(4); - // Default calculation function just returns the static value - calculateValue_ = [this](double, double, float, float, double) { - return value_.load(std::memory_order_relaxed); - }; } float AudioParam::getValueAtTime(double time) { - // Check if current automation segment has ended and we need to advance to - // next event - if (endTime_ < time && !eventsQueue_.isEmpty()) { - ParamChangeEvent event; - eventsQueue_.popFront(event); - startTime_ = event.getStartTime(); - endTime_ = event.getEndTime(); - startValue_ = event.getStartValue(); - endValue_ = event.getEndValue(); - calculateValue_ = event.getCalculateValue(); + auto value = eventRenderQueue_.computeValueAtTime(time); + if (!value.has_value()) { + return value_.load(std::memory_order_relaxed); } - - // Calculate value using the current automation function and clamp to valid - auto value = calculateValue_(startTime_, endTime_, startValue_, endValue_, time); - setValue(value); - return value; + setValue(value.value()); + return value.value(); } void AudioParam::setValueAtTime(float value, double startTime) { - // Ignore events scheduled before the end of existing automation - if (startTime < this->getQueueEndTime()) { - return; - } - - // Step function: instant change at startTime - auto calculateValue = - [](double startTime, double /* endTime */, float startValue, float endValue, double time) { - if (time < startTime) { - return startValue; - } - - return endValue; - }; - - this->updateQueue(ParamChangeEvent( - startTime, - startTime, - this->getQueueEndValue(), - value, - std::move(calculateValue), - ParamChangeEventType::SET_VALUE)); + this->updateQueue(AutomationRenderEventFactory::createSetValueEvent(value, startTime)); } void AudioParam::linearRampToValueAtTime(float value, double endTime) { - // Ignore events scheduled before the end of existing automation - if (endTime < this->getQueueEndTime()) { - return; - } - - // Linear interpolation function - auto calculateValue = - [](double startTime, double endTime, float startValue, float endValue, double time) { - if (time < startTime) { - return startValue; - } - - if (time < endTime) { - return static_cast( - startValue + (endValue - startValue) * (time - startTime) / (endTime - startTime)); - } - - return endValue; - }; - - this->updateQueue(ParamChangeEvent( - this->getQueueEndTime(), - endTime, - this->getQueueEndValue(), - value, - std::move(calculateValue), - ParamChangeEventType::LINEAR_RAMP)); + this->updateQueue(AutomationRenderEventFactory::createLinearRampEvent(value, endTime)); } void AudioParam::exponentialRampToValueAtTime(float value, double endTime) { - if (endTime <= this->getQueueEndTime()) { - return; - } - - // 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; - } - - if (time < endTime) { - return static_cast( - startValue * pow(endValue / startValue, (time - startTime) / (endTime - startTime))); - } - - return endValue; - }; - - this->updateQueue(ParamChangeEvent( - this->getQueueEndTime(), - endTime, - this->getQueueEndValue(), - value, - std::move(calculateValue), - ParamChangeEventType::EXPONENTIAL_RAMP)); + this->updateQueue(AutomationRenderEventFactory::createExponentialRampEvent(value, endTime)); } void AudioParam::setTargetAtTime(float target, double startTime, double timeConstant) { - if (startTime <= this->getQueueEndTime()) { - return; - } - // 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; - } - - return static_cast( - target + (startValue - target) * exp(-(time - startTime) / timeConstant)); - }; - this->updateQueue(ParamChangeEvent( - startTime, - startTime, // SetTarget events have infinite duration conceptually - this->getQueueEndValue(), - this->getQueueEndValue(), // End value is not meaningful for - // infinite events - std::move(calculateValue), - ParamChangeEventType::SET_TARGET)); + this->updateQueue( + AutomationRenderEventFactory::createSetTargetEvent(target, startTime, timeConstant)); } void AudioParam::setValueCurveAtTime( @@ -172,45 +57,16 @@ void AudioParam::setValueCurveAtTime( size_t length, double startTime, double duration) { - if (startTime <= this->getQueueEndTime()) { - return; - } - - auto calculateValue = - [values, length]( - double startTime, double endTime, float startValue, float endValue, double time) { - if (time < startTime) { - return startValue; - } - - if (time < endTime) { - // Calculate position in the array based on time progress - auto k = static_cast(std::floor( - static_cast(length - 1) / (endTime - startTime) * (time - startTime))); - // Calculate interpolation factor between adjacent array elements - auto factor = static_cast( - (time - startTime) * static_cast(length - 1) / (endTime - startTime) - k); - return dsp::linearInterpolate(values->span(), k, k + 1, factor); - } - - return endValue; - }; - - this->updateQueue(ParamChangeEvent( - startTime, - startTime + duration, - this->getQueueEndValue(), - values->span()[length - 1], - std::move(calculateValue), - ParamChangeEventType::SET_VALUE_CURVE)); + this->updateQueue( + AutomationRenderEventFactory::createSetValueCurveEvent(values, length, startTime, duration)); } void AudioParam::cancelScheduledValues(double cancelTime) { - this->eventsQueue_.cancelScheduledValues(cancelTime); + eventRenderQueue_.cancelScheduledValues(cancelTime); } void AudioParam::cancelAndHoldAtTime(double cancelTime) { - this->eventsQueue_.cancelAndHoldAtTime(cancelTime, this->endTime_); + eventRenderQueue_.cancelAndHoldAtTime(cancelTime); } void AudioParam::addInputNode(AudioNode *node) { 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 6606bc8c6..84001bb14 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 @@ -2,9 +2,9 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include @@ -68,10 +68,10 @@ class AudioParam { /// @note Audio Thread only void cancelAndHoldAtTime(double cancelTime); - template < - typename F, - typename = std::enable_if_t, BaseAudioContext &>>> - bool scheduleAudioEvent(F &&event) noexcept { + template + bool scheduleAudioEvent(F &&event) noexcept + requires(std::is_invocable_r_v, BaseAudioContext &>) + { if (std::shared_ptr context = context_.lock()) { return context->scheduleAudioEvent(std::forward(event)); } @@ -102,44 +102,20 @@ class AudioParam { float minValue_; float maxValue_; - AudioParamEventQueue eventsQueue_; - - // Current automation state (cached for performance) - double startTime_; - double endTime_; - float startValue_; - float endValue_; - std::function calculateValue_; + AutomationRenderQueue eventRenderQueue_; // Input modulation system std::vector inputNodes_; std::shared_ptr audioBuffer_; std::vector> inputBuffers_; - /// @brief Get the end time of the parameter queue. - /// @return The end time of the parameter queue or last endTime_ if queue is empty. - [[nodiscard]] double getQueueEndTime() const noexcept { - if (eventsQueue_.isEmpty()) { - return endTime_; - } - return eventsQueue_.back().getEndTime(); - } - - /// @brief Get the end value of the parameter queue. - /// @return The end value of the parameter queue or last endValue_ if queue is empty. - [[nodiscard]] float getQueueEndValue() const noexcept { - if (eventsQueue_.isEmpty()) { - return endValue_; - } - return eventsQueue_.back().getEndValue(); - } - /// @brief Update the parameter queue with a new event. /// @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. - void updateQueue(ParamChangeEvent &&event) { - eventsQueue_.pushBack(std::move(event)); + void updateQueue(RenderAutomationEvent &&event) { + eventRenderQueue_.push(std::move(event)); } + float getValueAtTime(double time); void processInputs( const std::shared_ptr &outputBuffer, diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/types/AutomationEventType.h b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AutomationEventType.h new file mode 100644 index 000000000..2e7ef5aef --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AutomationEventType.h @@ -0,0 +1,30 @@ +#pragma once + +#include +namespace audioapi { + +enum class AutomationEventType { + LINEAR_RAMP, + EXPONENTIAL_RAMP, + SET_VALUE, + SET_TARGET, + SET_VALUE_CURVE, +}; + +inline std::string_view toString(AutomationEventType type) { + switch (type) { + case AutomationEventType::LINEAR_RAMP: + return "LinearRampToValueAtTime"; + case AutomationEventType::EXPONENTIAL_RAMP: + return "ExponentialRampToValueAtTime"; + case AutomationEventType::SET_VALUE: + return "SetValueAtTime"; + case AutomationEventType::SET_TARGET: + return "SetTargetAtTime"; + case AutomationEventType::SET_VALUE_CURVE: + return "SetValueCurveAtTime"; + } + return "Unknown"; +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/types/ParamChangeEventType.h b/packages/react-native-audio-api/common/cpp/audioapi/core/types/ParamChangeEventType.h deleted file mode 100644 index dab646ba9..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/types/ParamChangeEventType.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include - -namespace audioapi { - -enum class ParamChangeEventType : std::uint8_t { - LINEAR_RAMP, - EXPONENTIAL_RAMP, - SET_VALUE, - SET_TARGET, - SET_VALUE_CURVE, -}; - -} // namespace audioapi 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 deleted file mode 100644 index a42c6c1f4..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include -#include - -namespace audioapi { - -AudioParamEventQueue::AudioParamEventQueue() = default; - -void AudioParamEventQueue::pushBack(ParamChangeEvent &&event) { - if (eventQueue_.isEmpty()) { - eventQueue_.pushBack(std::move(event)); - return; - } - 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)); -} - -bool AudioParamEventQueue::popFront(ParamChangeEvent &event) { - return eventQueue_.popFront(event); -} - -void AudioParamEventQueue::cancelScheduledValues(double cancelTime) { - while (!eventQueue_.isEmpty()) { - const auto &back = eventQueue_.peekBack(); - if (back.getEndTime() < cancelTime) { - break; - } - if (back.getStartTime() >= cancelTime || - back.getType() == ParamChangeEventType::SET_VALUE_CURVE) { - eventQueue_.popBack(); - } - } -} - -void AudioParamEventQueue::cancelAndHoldAtTime(double cancelTime, double &endTimeCache) { - while (!eventQueue_.isEmpty()) { - const auto &back = eventQueue_.peekBack(); - if (back.getEndTime() < cancelTime || back.getStartTime() <= cancelTime) { - break; - } - eventQueue_.popBack(); - } - - if (eventQueue_.isEmpty()) { - endTimeCache = cancelTime; - return; - } - - auto &back = eventQueue_.peekBackMut(); - back.setEndValue(back.getCalculateValue()( - back.getStartTime(), - back.getEndTime(), - back.getStartValue(), - back.getEndValue(), - cancelTime)); - back.setEndTime(std::min(cancelTime, back.getEndTime())); -} - -} // namespace audioapi 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 deleted file mode 100644 index 1dd95c570..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioParamEventQueue.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace audioapi { - -/// @brief A queue for managing audio parameter change events. -/// @note The invariant of the queue is that its internal buffer always contains non-overlapping events. -class AudioParamEventQueue { - public: - /// @brief Constructor for AudioParamEventQueue. - /// @note Capacity must be valid power of two. - explicit 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); - - /// @brief Pop the front event from the queue. - /// @return The front event in the queue. - bool popFront(ParamChangeEvent &event); - - /// @brief Cancel scheduled parameter changes at or after the given time. - /// @param cancelTime The time at which to cancel scheduled changes. - void cancelScheduledValues(double cancelTime); - - /// @brief Cancel scheduled parameter changes and hold the current value at the given time. - /// @param cancelTime The time at which to cancel scheduled changes. - void cancelAndHoldAtTime(double cancelTime, double &endTimeCache); - - /// @brief Get the first event in the queue. - /// @return The first event in the queue. - [[nodiscard]] const ParamChangeEvent &front() const noexcept { - return eventQueue_.peekFront(); - } - - /// @brief Get the last event in the queue. - /// @return The last event in the queue. - [[nodiscard]] const ParamChangeEvent &back() const noexcept { - return eventQueue_.peekBack(); - } - - /// @brief Check if the event queue is empty. - /// @return True if the queue is empty, false otherwise. - [[nodiscard]] bool isEmpty() const noexcept { - return eventQueue_.isEmpty(); - } - - /// @brief Check if the event queue is full. - /// @return True if the queue is full, false otherwise. - [[nodiscard]] bool isFull() const noexcept { - return eventQueue_.isFull(); - } - - private: - /// @brief The queue of parameter change events. - /// @note INVARIANT it always holds non-overlapping events sorted by start time. - RingBiDirectionalBuffer eventQueue_; -}; - -} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h index 1d6897af0..90360e62c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/Constants.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -44,4 +45,7 @@ using std::hardware_destructive_interference_size; constexpr std::size_t hardware_constructive_interference_size = 64; constexpr std::size_t hardware_destructive_interference_size = 64; #endif + +// audio param +inline constexpr size_t AUDIO_PARAM_MAX_QUEUED_EVENTS = 32; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationControlQueue.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationControlQueue.cpp new file mode 100644 index 000000000..dbdeb9455 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationControlQueue.cpp @@ -0,0 +1,68 @@ +#include "audioapi/core/utils/automation/AutomationControlQueue.h" +#include +#include +#include +#include "audioapi/core/types/AutomationEventType.h" +#include "audioapi/core/utils/automation/AutomationEvent.hpp" +#include "audioapi/utils/Result.hpp" + +namespace audioapi { + +Result AutomationControlQueue::checkCurveExclusion( + const AutomationEvent &event) { + if (event.getType() == AutomationEventType::SET_VALUE_CURVE) { + const auto *conflict = findEventInInterval(event.getStartTime(), event.getEndTime()); + if (conflict != nullptr) { + return Err( + std::format( + "Cannot schedule curve event from time {} to {} because it conflicts with an existing event of type {} at time {}.", + event.getStartTime(), + event.getEndTime(), + toString(conflict->getType()), + conflict->getAutomationTime())); + } + } else { + const auto *conflict = findEventAtTime(event.getAutomationTime()); + if ((conflict != nullptr) && conflict->getType() == AutomationEventType::SET_VALUE_CURVE) { + return Err( + std::format( + "Cannot schedule event of type {} at time {} because it conflicts with an existing curve event from time {} to {}.", + toString(event.getType()), + event.getAutomationTime(), + conflict->getStartTime(), + conflict->getEndTime())); + } + } + return Ok(None); +} + +void AutomationControlQueue::cancelScheduledValues(double cancelTime) { + while (!eventQueue_.isEmpty() && eventQueue_.peekBack().getAutomationTime() >= cancelTime) { + eventQueue_.popBack(); + } +} + +// TODO: these lookups can be optimized using multiset interface of the underlying queue +const AutomationEvent *AutomationControlQueue::findEventAtTime(double automationTime) const { + for (const auto &event : eventQueue_) { + if ((event.getType() == AutomationEventType::SET_VALUE_CURVE && + event.getStartTime() <= automationTime && automationTime <= event.getEndTime()) || + event.getAutomationTime() == automationTime) { + return &event; + } + } + return nullptr; +} + +// TODO: these lookups can be optimized using multiset interface of the underlying queue +const AutomationEvent *AutomationControlQueue::findEventInInterval(double startTime, double endTime) + const { + for (const auto &event : eventQueue_) { + if (event.getStartTime() >= startTime && event.getStartTime() < endTime) { + return &event; + } + } + return nullptr; +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationControlQueue.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationControlQueue.h new file mode 100644 index 000000000..37aa33c80 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationControlQueue.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace audioapi { + +/// @brief A queue for managing audio parameter change events on the JS/control thread. +/// @note The invariant of the queue is that its internal buffer always contains non-overlapping events. +class AutomationControlQueue : public AutomationQueueBase { + public: + explicit AutomationControlQueue() = default; + + /// @brief Validate if a new event can be added to the queue without violating curve exclusion rules. + /// @param event The new event to validate. + /// @return Ok if the event can be added, Err with a message if it cannot be added. + [[nodiscard]] Result checkCurveExclusion(const AutomationEvent &event); + + /// @brief Cancel scheduled parameter changes at or after the given time. + /// @param cancelTime The time at which to cancel scheduled changes. + void cancelScheduledValues(double cancelTime) override; + + private: + [[nodiscard]] const AutomationEvent *findEventAtTime(double time) const; + [[nodiscard]] const AutomationEvent *findEventInInterval(double startTime, double endTime) const; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationEvent.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationEvent.hpp new file mode 100644 index 000000000..b3ae708b2 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationEvent.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include + +namespace audioapi { + +class AutomationEvent { + public: + AutomationEvent() = default; + ~AutomationEvent() = default; + + explicit AutomationEvent(AutomationEventType type, double startTime, double endTime) + : type_(type), startTime_(startTime), endTime_(endTime) {} + + /// @brief Construct from a single automationTime value, setting startTime or endTime based on type. + /// Ramp events (LINEAR_RAMP, EXPONENTIAL_RAMP) store automationTime as endTime. + /// All other types store it as startTime. + explicit AutomationEvent(AutomationEventType type, double automationTime) + : type_(type), + startTime_(isRamp(type) ? 0.0 : automationTime), + endTime_(isRamp(type) ? automationTime : 0.0) {} + + AutomationEvent(const AutomationEvent &) = delete; + AutomationEvent &operator=(const AutomationEvent &) = delete; + + AutomationEvent(AutomationEvent &&other) noexcept + : type_(other.type_), startTime_(other.startTime_), endTime_(other.endTime_) {} + + AutomationEvent &operator=(AutomationEvent &&other) noexcept { + if (this != &other) { + type_ = other.type_; + startTime_ = other.startTime_; + endTime_ = other.endTime_; + } + return *this; + } + + [[nodiscard]] double getAutomationTime() const noexcept { + return isRamp(type_) ? endTime_ : startTime_; + } + + [[nodiscard]] double getStartTime() const noexcept { + return startTime_; + } + + [[nodiscard]] double getEndTime() const noexcept { + return endTime_; + } + + [[nodiscard]] AutomationEventType getType() const noexcept { + return type_; + } + + [[nodiscard]] bool isRampType() const noexcept { + return isRamp(type_); + } + + void setEndTime(double endTime) noexcept { + endTime_ = endTime; + } + + void setStartTime(double startTime) noexcept { + startTime_ = startTime; + } + + protected: + double startTime_ = 0.0; + double endTime_ = 0.0; + AutomationEventType type_ = AutomationEventType::SET_VALUE; + + private: + static bool isRamp(AutomationEventType type) noexcept { + return type == AutomationEventType::LINEAR_RAMP || + type == AutomationEventType::EXPONENTIAL_RAMP; + } +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationQueueBase.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationQueueBase.hpp new file mode 100644 index 000000000..cdab91ce0 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationQueueBase.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include "audioapi/core/utils/Constants.h" +#include "audioapi/utils/BoundedPriorityQueue.hpp" + +namespace audioapi { + +template +concept AutomationEventConcept = requires(TEvent event) { + { event.getAutomationTime() } -> std::convertible_to; +}; +template +class AutomationQueueBase { + public: + AutomationQueueBase() = default; + AutomationQueueBase(const AutomationQueueBase &) = delete; + AutomationQueueBase &operator=(const AutomationQueueBase &) = delete; + AutomationQueueBase(AutomationQueueBase &&) noexcept = delete; + AutomationQueueBase &operator=(AutomationQueueBase &&) noexcept = delete; + virtual ~AutomationQueueBase() = default; + + /// @brief Cancel scheduled parameter changes at or after the given time. + /// @param cancelTime The time at which to cancel scheduled changes. + virtual void cancelScheduledValues(double cancelTime) = 0; + + virtual bool push(TEvent &&event) { + return eventQueue_.push(std::move(event)); + } + + virtual bool pop(TEvent &event) { + return eventQueue_.pop(event); + } + + /// @brief Check if the event queue is empty. + [[nodiscard]] bool isEmpty() const noexcept { + return eventQueue_.isEmpty(); + } + + /// @brief Check if the event queue is full. + [[nodiscard]] bool isFull() const noexcept { + return eventQueue_.isFull(); + } + + /// @brief Get the first event in the queue. + /// @return The first event in the queue. + [[nodiscard]] const TEvent &front() const noexcept { + return eventQueue_.peekFront(); + } + + /// @brief Get the last event in the queue. + /// @return The last event in the queue. + [[nodiscard]] const TEvent &back() const noexcept { + return eventQueue_.peekBack(); + } + + protected: + struct EventComparator { + using is_transparent = void; + bool operator()(const TEvent &a, const TEvent &b) const { + return a.getAutomationTime() < b.getAutomationTime(); + } + bool operator()(const TEvent &a, double time) const { + return a.getAutomationTime() < time; + } + bool operator()(double time, const TEvent &b) const { + return time < b.getAutomationTime(); + } + }; + + BoundedPriorityQueue eventQueue_; +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderEventFactory.hpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderEventFactory.hpp new file mode 100644 index 000000000..405df5f52 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderEventFactory.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include "audioapi/core/utils/automation/RenderAutomationEvent.hpp" +#include "audioapi/dsp/AudioUtils.hpp" +#include "audioapi/utils/AudioArray.hpp" + +namespace audioapi { + +class AutomationRenderEventFactory { + public: + static RenderAutomationEvent createSetValueEvent(float value, double startTime) { + auto calculateValue = + [](double startTime, double /* endTime */, float startValue, float endValue, double time) { + if (time < startTime) { + return startValue; + } + + return endValue; + }; + + return RenderAutomationEvent( + startTime, + startTime, + value, + value, + std::move(calculateValue), + AutomationEventType::SET_VALUE); + } + + static RenderAutomationEvent createLinearRampEvent(float value, double endTime) { + auto calculateValue = + [](double startTime, double endTime, float startValue, float endValue, double time) { + if (time < startTime) { + return startValue; + } + + if (time < endTime) { + return static_cast( + startValue + (endValue - startValue) * (time - startTime) / (endTime - startTime)); + } + + return endValue; + }; + + return RenderAutomationEvent( + 0.0, endTime, 0.0f, value, std::move(calculateValue), AutomationEventType::LINEAR_RAMP); + } + + static RenderAutomationEvent createExponentialRampEvent(float value, double endTime) { + 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; + } + + if (time < endTime) { + return static_cast( + startValue * + pow(endValue / startValue, (time - startTime) / (endTime - startTime))); + } + + return endValue; + }; + + return RenderAutomationEvent( + 0.0, + endTime, + 0.0f, + value, + std::move(calculateValue), + AutomationEventType::EXPONENTIAL_RAMP); + } + + static RenderAutomationEvent + createSetTargetEvent(float target, double startTime, double timeConstant) { + auto calculateValue = [timeConstant, target]( + double startTime, + double /* endTime */, + float startValue, + float /* endValue */, + double time) { + if (timeConstant == 0) { + return target; + } + + if (time < startTime) { + return startValue; + } + + return static_cast( + target + (startValue - target) * exp(-(time - startTime) / timeConstant)); + }; + + return RenderAutomationEvent( + startTime, + startTime, // SetTarget events have infinite duration conceptually + 0.0f, + 0.0f, // End value is not meaningful for infinite events + std::move(calculateValue), + AutomationEventType::SET_TARGET); + } + + static RenderAutomationEvent createSetValueCurveEvent( + const std::shared_ptr &values, + size_t length, + double startTime, + double duration) { + auto calculateValue = + [values, length]( + double startTime, double endTime, float startValue, float endValue, double time) { + if (time < startTime) { + return startValue; + } + + if (time < endTime) { + // Calculate position in the array based on time progress + auto k = static_cast(std::floor( + static_cast(length - 1) / (endTime - startTime) * (time - startTime))); + // Calculate interpolation factor between adjacent array elements + auto factor = static_cast( + (time - startTime) * static_cast(length - 1) / (endTime - startTime) - k); + return dsp::linearInterpolate(values->span(), k, k + 1, factor); + } + + return endValue; + }; + + return RenderAutomationEvent( + startTime, + startTime + duration, + 0.0f, + values->span()[length - 1], + std::move(calculateValue), + AutomationEventType::SET_VALUE_CURVE); + } +}; + +} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderQueue.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderQueue.cpp new file mode 100644 index 000000000..1965af636 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderQueue.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include +#include "audioapi/core/types/AutomationEventType.h" + +namespace audioapi { + +std::optional AutomationRenderQueue::computeValueAtTime(double time) { + while ( + !eventQueue_.isEmpty() && + (!currentEvent_ || + (time >= currentEvent_->getEndTime() && eventQueue_.peekFront().getStartTime() <= time))) { + RenderAutomationEvent next; + eventQueue_.pop(next); + currentEvent_ = std::move(next); + } + + if (!currentEvent_) { + return std::nullopt; + } + + return currentEvent_->getCalculateValue()( + currentEvent_->getStartTime(), + currentEvent_->getEndTime(), + currentEvent_->getStartValue(), + currentEvent_->getEndValue(), + time); +} + +bool AutomationRenderQueue::push(RenderAutomationEvent &&event) { + resolveEventValues(event); + return eventQueue_.push(std::move(event)); +} + +void AutomationRenderQueue::resolveEventValues(RenderAutomationEvent &event) { + auto it = eventQueue_.upper_bound(event.getAutomationTime()); + + RenderAutomationEvent *predecessor = nullptr; + if (it != eventQueue_.begin()) { + predecessor = &eventQueue_.deref_mut(std::prev(it)); + } else if (currentEvent_) { + predecessor = ¤tEvent_.value(); + } + + if (predecessor != nullptr) { + // Set startTime BEFORE startValue for ramps — startValue depends on the resolved startTime + if (event.isRampType()) { + event.setStartTime(predecessor->getEndTime()); + } + + event.setStartValue(getValueOfPreviousEventAt(*predecessor, event.getStartTime())); + + if (predecessor->getType() == AutomationEventType::SET_TARGET) { + predecessor->setEndTime(event.getStartTime()); + predecessor->setEndValue(getValueOfPreviousEventAt(*predecessor, predecessor->getEndTime())); + } + } else { + event.setStartValue(defaultValue_); + } + + if (it != eventQueue_.end() && it->isRampType()) { + auto *successor = &eventQueue_.deref_mut(it); + successor->setStartTime(event.getEndTime()); + successor->setStartValue(event.getEndValue()); + } +} + +float AutomationRenderQueue::getValueOfPreviousEventAt(RenderAutomationEvent &event, double time) { + if (event.getType() == AutomationEventType::SET_TARGET) { + return event.getCalculateValue()( + event.getStartTime(), event.getEndTime(), event.getStartValue(), event.getEndValue(), time); + } + return event.getEndValue(); +} + +void AutomationRenderQueue::cancelScheduledValues(double cancelTime) { + while (!eventQueue_.isEmpty() && eventQueue_.peekBack().getAutomationTime() >= cancelTime) { + eventQueue_.popBack(); + } +} + +void AutomationRenderQueue::cancelAndHoldAtTime(double cancelTime) { + float holdValue = currentEvent_ ? currentEvent_->getStartValue() : 0.0f; + + // Check E2: first event with automationTime > cancelTime + auto e2It = eventQueue_.upper_bound(cancelTime); + + if (e2It != eventQueue_.end() && e2It->isRampType()) { + // E2 is a ramp — compute its value at cancelTime + const auto &e2 = *e2It; + holdValue = e2.getCalculateValue()( + e2.getStartTime(), e2.getEndTime(), e2.getStartValue(), e2.getEndValue(), cancelTime); + } else { + // Hold value comes from E1 or currentEvent_ + auto e1It = (e2It != eventQueue_.begin()) ? std::prev(e2It) : eventQueue_.end(); + if (e1It != eventQueue_.end()) { + holdValue = getValueOfPreviousEventAt(eventQueue_.deref_mut(e1It), cancelTime); + } else if (currentEvent_) { + holdValue = getValueOfPreviousEventAt(*currentEvent_, cancelTime); + } + } + + // Remove all events after cancelTime + while (!eventQueue_.isEmpty() && eventQueue_.peekBack().getAutomationTime() > cancelTime) { + eventQueue_.popBack(); + } + + // Insert hold event — resolveEventValues will set startValue from E1 + this->push(std::move(AutomationRenderEventFactory::createSetValueEvent(holdValue, cancelTime))); +} + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderQueue.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderQueue.h new file mode 100644 index 000000000..b45c27240 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/AutomationRenderQueue.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +namespace audioapi { + +/// @brief A queue for managing audio parameter change events on the audio render thread. +/// @note The invariant of the queue is that its internal buffer always contains non-overlapping events. +class AutomationRenderQueue : public AutomationQueueBase { + public: + explicit AutomationRenderQueue(float defaultValue) : defaultValue_(defaultValue) {} + + void cancelScheduledValues(double cancelTime) override; + + /// @brief Cancel scheduled parameter changes and hold the current value at the given time. + /// @param cancelTime The time at which to cancel scheduled changes. + void cancelAndHoldAtTime(double cancelTime); + + bool push(RenderAutomationEvent &&event) override; + + [[nodiscard]] std::optional computeValueAtTime(double time); + + private: + float defaultValue_; + + void resolveEventValues(RenderAutomationEvent &event); + static float getValueOfPreviousEventAt(RenderAutomationEvent &event, double time); + std::optional currentEvent_ = std::nullopt; +}; + +} // 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/automation/RenderAutomationEvent.hpp similarity index 51% rename from packages/react-native-audio-api/common/cpp/audioapi/core/utils/ParamChangeEvent.hpp rename to packages/react-native-audio-api/common/cpp/audioapi/core/utils/automation/RenderAutomationEvent.hpp index fb95e024c..4aa29ef48 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/automation/RenderAutomationEvent.hpp @@ -1,89 +1,74 @@ #pragma once -#include +#include #include #include +#include "audioapi/core/types/AutomationEventType.h" namespace audioapi { -class ParamChangeEvent { +class RenderAutomationEvent : public AutomationEvent { public: - ParamChangeEvent() = default; - explicit ParamChangeEvent( + RenderAutomationEvent() = default; + ~RenderAutomationEvent() = default; + + explicit RenderAutomationEvent( double startTime, double endTime, float startValue, float endValue, std::function &&calculateValue, - ParamChangeEventType type) - : startTime_(startTime), - endTime_(endTime), + AutomationEventType type) + : AutomationEvent(type, startTime, endTime), calculateValue_(std::move(calculateValue)), startValue_(startValue), - endValue_(endValue), - type_(type) {} + endValue_(endValue) {} - ParamChangeEvent(const ParamChangeEvent &other) = delete; - ParamChangeEvent &operator=(const ParamChangeEvent &other) = delete; + RenderAutomationEvent(const RenderAutomationEvent &) = delete; + RenderAutomationEvent &operator=(const RenderAutomationEvent &) = delete; - ParamChangeEvent(ParamChangeEvent &&other) noexcept - : startTime_(other.startTime_), - endTime_(other.endTime_), + RenderAutomationEvent(RenderAutomationEvent &&other) noexcept + : AutomationEvent(std::move(other)), calculateValue_(std::move(other.calculateValue_)), startValue_(other.startValue_), - endValue_(other.endValue_), - type_(other.type_) {} + endValue_(other.endValue_) {} - ParamChangeEvent &operator=(ParamChangeEvent &&other) noexcept { + RenderAutomationEvent &operator=(RenderAutomationEvent &&other) noexcept { if (this != &other) { - startTime_ = other.startTime_; - endTime_ = other.endTime_; + AutomationEvent::operator=(std::move(other)); calculateValue_ = std::move(other.calculateValue_); startValue_ = other.startValue_; endValue_ = other.endValue_; - type_ = other.type_; } return *this; } - [[nodiscard]] double getEndTime() const noexcept { - return endTime_; - } - [[nodiscard]] double getStartTime() const noexcept { - return startTime_; - } [[nodiscard]] float getEndValue() const noexcept { return endValue_; } + [[nodiscard]] float getStartValue() const noexcept { return startValue_; } + [[nodiscard]] const std::function & getCalculateValue() const noexcept { return calculateValue_; } - [[nodiscard]] ParamChangeEventType getType() const noexcept { - return type_; - } - void setEndTime(double endTime) noexcept { - endTime_ = endTime; - } void setStartValue(float startValue) noexcept { startValue_ = startValue; } + void setEndValue(float endValue) noexcept { endValue_ = endValue; } private: - double startTime_ = 0.0; - double endTime_ = 0.0; std::function calculateValue_; float startValue_ = 0.0f; float endValue_ = 0.0f; - ParamChangeEventType type_ = ParamChangeEventType::SET_VALUE; }; } // 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 new file mode 100644 index 000000000..9a045d585 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace audioapi { + +/// @brief A bounded priority queue with fixed capacity backed by a static pool allocator. +/// Elements are kept in ascending sorted order (smallest element at front). +/// All operations avoid heap allocation. +/// @tparam T The type of elements stored. Must be move-constructible. +/// @tparam Capacity The maximum number of elements. +/// @tparam Compare Comparator type. Defaults to std::less (smallest element at front). +/// @note Stable: for equal keys, insertion order is preserved by std::multiset. +/// @note This implementation is NOT thread-safe. +template > +class BoundedPriorityQueue { + private: + using SetType = std::pmr::multiset; + + // Conservative RB-tree node size: value + 3 pointers + color, aligned to pointer size. + static constexpr size_t kNodeOverhead = 4 * sizeof(void *); + static constexpr size_t kNodeSize = sizeof(T) + kNodeOverhead; + // Extra headroom for pool resource bookkeeping structures. + static constexpr size_t kBufferSize = Capacity * kNodeSize + 256; + + // Members must be declared in this order: buffer_ → mono_ → pool_ → set_. + alignas(std::max_align_t) std::array buffer_; + std::pmr::monotonic_buffer_resource mono_{ + buffer_.data(), + sizeof(buffer_), + std::pmr::null_memory_resource()}; + std::pmr::unsynchronized_pool_resource pool_{ + std::pmr::pool_options{ + .max_blocks_per_chunk = Capacity, + .largest_required_pool_block = kNodeSize}, + &mono_}; + SetType set_{Compare{}, &pool_}; + + public: + explicit BoundedPriorityQueue() = default; + ~BoundedPriorityQueue() = default; + + BoundedPriorityQueue(const BoundedPriorityQueue &) = delete; + BoundedPriorityQueue &operator=(const BoundedPriorityQueue &) = delete; + BoundedPriorityQueue(BoundedPriorityQueue &&) noexcept = delete; + BoundedPriorityQueue &operator=(BoundedPriorityQueue &&) noexcept = delete; + + /// @brief Insert a value in sorted order. Amortized O(1) when inserting the largest element + /// (common case: events scheduled in chronological order), O(log n) otherwise. + /// @return True if inserted, false if full. + template + bool push(U &&value) { + if (isFull()) { + [[unlikely]] return false; + } + // Hint with end(): amortized O(1) when the new event has the largest key (in-order scheduling). + set_.insert(set_.end(), std::forward(value)); + return true; + } + + /// @brief Remove and return the smallest element (front). Amortized O(1). + /// @return True if successful, false if empty. + bool pop(T &out) { + if (isEmpty()) { + [[unlikely]] return false; + } + auto node = set_.extract(set_.begin()); + out = std::move(node.value()); + return true; + } + + /// @brief Remove the smallest element (front) without retrieving it. Amortized O(1). + /// @return True if successful, false if empty. + bool pop() { + if (isEmpty()) { + [[unlikely]] return false; + } + set_.erase(set_.begin()); + return true; + } + + /// @brief Remove the largest element (back). Amortized O(1). + /// @return True if successful, false if empty. + bool popBack() { + if (isEmpty()) { + [[unlikely]] return false; + } + set_.erase(std::prev(set_.end())); + return true; + } + + /// @brief Peek at the smallest element (front). + [[nodiscard]] const T &peekFront() const noexcept { + return *set_.begin(); + } + + /// @brief Peek at the smallest element (front), mutable. + [[nodiscard]] T &peekFrontMut() noexcept { + return const_cast(*set_.begin()); + } + + /// @brief Peek at the largest element (back). + [[nodiscard]] const T &peekBack() const noexcept { + return *std::prev(set_.end()); + } + + /// @brief Peek at the largest element (back), mutable. + [[nodiscard]] T &peekBackMut() noexcept { + return const_cast(*std::prev(set_.end())); + } + + [[nodiscard]] bool isEmpty() const noexcept { + return set_.empty(); + } + + [[nodiscard]] bool isFull() const noexcept { + return set_.size() >= Capacity; + } + + [[nodiscard]] size_t size() const noexcept { + return set_.size(); + } + + [[nodiscard]] size_t getCapacity() const noexcept { + return Capacity; + } + + [[nodiscard]] SetType::const_iterator begin() const noexcept { + return set_.begin(); + } + + [[nodiscard]] SetType::const_iterator end() const noexcept { + return set_.end(); + } + + template + [[nodiscard]] SetType::iterator lower_bound(const Key &key) noexcept { + return set_.lower_bound(key); + } + + template + [[nodiscard]] SetType::iterator upper_bound(const Key &key) noexcept { + return set_.upper_bound(key); + } + + [[nodiscard]] T &deref_mut(SetType::const_iterator it) noexcept { + return const_cast(*it); + } +}; + +} // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/RingBiDirectionalBuffer.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/RingBiDirectionalBuffer.hpp deleted file mode 100644 index a3989455b..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/RingBiDirectionalBuffer.hpp +++ /dev/null @@ -1,198 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -namespace audioapi { - -/// @brief A ring buffer implementation (non thread safe). -/// @tparam T The type of elements stored in the buffer. -/// @tparam capacity_ The maximum number of elements that can be held in the buffer. -/// @note This implementation is NOT thread-safe. -/// @note Can be refered as bounded queue -/// @note Capacity must be a valid power of two and must be greater than zero. -template -class RingBiDirectionalBuffer { - public: - /// @brief Constructor for RingBuffer. - RingBiDirectionalBuffer() - : buffer_( - static_cast(::operator new[]( - capacity_ * sizeof(T), - static_cast(alignof(T))))) { - static_assert(isPowerOfTwo(capacity_), "RingBiDirectionalBuffer's capacity must be power of 2"); - } - - /// @brief Destructor for RingBuffer. - ~RingBiDirectionalBuffer() { - for (int i = headIndex_; i != tailIndex_; i = nextIndex(i)) { - buffer_[i].~T(); - } - ::operator delete[](buffer_, capacity_ * sizeof(T), static_cast(alignof(T))); - } - - DELETE_COPY_AND_MOVE(RingBiDirectionalBuffer); - - /// @brief Push a value into the ring buffer. - /// @tparam U The type of the value to push. - /// @param value The value to push. - /// @return True if the value was pushed successfully, false if the buffer is full. - template - bool pushBack(U &&value) noexcept(std::is_nothrow_constructible_v) { - if (isFull()) [[unlikely]] { - return false; - } - new (&buffer_[tailIndex_]) T(std::forward(value)); - tailIndex_ = nextIndex(tailIndex_); - return true; - } - - /// @brief Push a value to the front of the buffer. - /// @tparam U The type of the value to push. - /// @param value The value to push. - /// @return True if the value was pushed successfully, false if the buffer is full. - template - bool pushFront(U &&value) noexcept(std::is_nothrow_constructible_v) { - if (isFull()) [[unlikely]] { - return false; - } - headIndex_ = prevIndex(headIndex_); - new (&buffer_[headIndex_]) T(std::forward(value)); - return true; - } - - /// @brief Pop a value from the front of the buffer. - /// @param out The value popped from the buffer. - /// @return True if the value was popped successfully, false if the buffer is empty. - bool popFront(T &out) noexcept( - std::is_nothrow_move_constructible_v && std::is_nothrow_destructible_v) { - if (isEmpty()) [[unlikely]] { - return false; - } - out = std::move(buffer_[headIndex_]); - buffer_[headIndex_].~T(); - headIndex_ = nextIndex(headIndex_); - return true; - } - - /// @brief Pop a value from the front of the buffer. - /// @return True if the value was popped successfully, false if the buffer is empty. - bool popFront() noexcept(std::is_nothrow_destructible_v) { - if (isEmpty()) [[unlikely]] { - return false; - } - buffer_[headIndex_].~T(); - headIndex_ = nextIndex(headIndex_); - return true; - } - - /// @brief Pop a value from the back of the buffer. - /// @param out The value popped from the buffer. - /// @return True if the value was popped successfully, false if the buffer is empty. - bool popBack(T &out) noexcept( - std::is_nothrow_move_constructible_v && std::is_nothrow_destructible_v) { - if (isEmpty()) [[unlikely]] { - return false; - } - tailIndex_ = prevIndex(tailIndex_); - out = std::move(buffer_[tailIndex_]); - buffer_[tailIndex_].~T(); - return true; - } - - /// @brief Pop a value from the back of the buffer. - /// @return True if the value was popped successfully, false if the buffer is empty. - bool popBack() noexcept(std::is_nothrow_destructible_v) { - if (isEmpty()) [[unlikely]] { - return false; - } - tailIndex_ = prevIndex(tailIndex_); - buffer_[tailIndex_].~T(); - return true; - } - - /// @brief Peek at the front of the buffer. - /// @return A const reference to the front element of the buffer. - [[nodiscard]] const T &peekFront() const noexcept { - return buffer_[headIndex_]; - } - - /// @brief Peek at the back of the buffer. - /// @return A const reference to the back element of the buffer. - [[nodiscard]] const T &peekBack() const noexcept { - return buffer_[prevIndex(tailIndex_)]; - } - - /// @brief Peek at the front of the buffer. - /// @return A mutable reference to the front element of the buffer. - [[nodiscard]] T &peekFrontMut() noexcept { - return buffer_[headIndex_]; - } - - /// @brief Peek at the back of the buffer. - /// @return A mutable reference to the back element of the buffer. - [[nodiscard]] T &peekBackMut() noexcept { - return buffer_[prevIndex(tailIndex_)]; - } - - /// @brief Check if the buffer is empty. - /// @return True if the buffer is empty, false otherwise. - [[nodiscard]] bool isEmpty() const noexcept { - return headIndex_ == tailIndex_; - } - - /// @brief Check if the buffer is full. - /// @return True if the buffer is full, false otherwise. - [[nodiscard]] bool isFull() const noexcept { - return nextIndex(tailIndex_) == headIndex_; - } - - /// @brief Get the capacity of the buffer. - /// @return The capacity of the buffer. - [[nodiscard]] size_t getCapacity() const noexcept { - return capacity_; - } - - /// @brief Get the real capacity of the buffer (excluding one slot for the empty state). - /// @return The real capacity of the buffer. - [[nodiscard]] size_t getRealCapacity() const noexcept { - return capacity_ - 1; - } - - /// @brief Get the number of elements in the buffer. - /// @return The number of elements in the buffer. - [[nodiscard]] size_t size() const noexcept { - return (capacity_ + tailIndex_ - headIndex_) & (capacity_ - 1); - } - - private: - T *buffer_; - size_t headIndex_{0}; - size_t tailIndex_{0}; - - /// @brief Get the next index in the buffer. - /// @param n The current index. - /// @return The next index in the buffer. - [[nodiscard]] size_t nextIndex(const size_t n) const noexcept { - return (n + 1) & (capacity_ - 1); - } - - /// @brief Get the previous index in the buffer. - /// @param n The current index. - /// @return The previous index in the buffer. - [[nodiscard]] size_t prevIndex(const size_t n) const noexcept { - return (n - 1) & (capacity_ - 1); - } - - /// @brief Check if a number is a power of two. - /// @param n The number to check. - /// @return True if n is a power of two, false otherwise. - static constexpr bool isPowerOfTwo(size_t n) { - return std::has_single_bit(n); - } -}; - -}; // 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..41264f811 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -1,21 +1,21 @@ +import { AutomationEventType } from '../api'; +import { InvalidStateError, NotSupportedError, RangeError } from '../errors'; import { IAudioParam } from '../interfaces'; -import { RangeError, InvalidStateError } from '../errors'; import BaseAudioContext from './BaseAudioContext'; 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; + + 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 { @@ -33,6 +33,15 @@ export default class AudioParam { ); } + const checkExclusionResult = this.audioParam.checkCurveExclusion({ + type: AutomationEventType.SET_VALUE, + automationTime: startTime, + }); + + if (checkExclusionResult.status === 'error') { + throw new NotSupportedError(checkExclusionResult.message); + } + const clampedTime = Math.max(startTime, this.context.currentTime); this.audioParam.setValueAtTime(value, clampedTime); @@ -46,6 +55,15 @@ export default class AudioParam { ); } + const checkExclusionResult = this.audioParam.checkCurveExclusion({ + type: AutomationEventType.LINEAR_RAMP, + automationTime: endTime, + }); + + if (checkExclusionResult.status === 'error') { + throw new NotSupportedError(checkExclusionResult.message); + } + const clampedTime = Math.max(endTime, this.context.currentTime); this.audioParam.linearRampToValueAtTime(value, clampedTime); @@ -66,6 +84,15 @@ export default class AudioParam { ); } + const checkExclusionResult = this.audioParam.checkCurveExclusion({ + type: AutomationEventType.EXPONENTIAL_RAMP, + automationTime: endTime, + }); + + if (checkExclusionResult.status === 'error') { + throw new NotSupportedError(checkExclusionResult.message); + } + const clampedTime = Math.max(endTime, this.context.currentTime); this.audioParam.exponentialRampToValueAtTime(value, clampedTime); @@ -89,6 +116,15 @@ export default class AudioParam { ); } + const checkExclusionResult = this.audioParam.checkCurveExclusion({ + type: AutomationEventType.SET_TARGET, + automationTime: startTime, + }); + + if (checkExclusionResult.status === 'error') { + throw new NotSupportedError(checkExclusionResult.message); + } + const clampedTime = Math.max(startTime, this.context.currentTime); this.audioParam.setTargetAtTime(target, clampedTime, timeConstant); @@ -116,6 +152,16 @@ export default class AudioParam { throw new InvalidStateError(`values must contain at least two values`); } + const checkExclusionResult = this.audioParam.checkCurveExclusion({ + type: AutomationEventType.SET_VALUE_CURVE, + automationTime: startTime, + duration, + }); + + if (checkExclusionResult.status === 'error') { + throw new NotSupportedError(checkExclusionResult.message); + } + const clampedTime = Math.max(startTime, this.context.currentTime); this.audioParam.setValueCurveAtTime(values, clampedTime, duration); diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts index bc3ff0481..2480ef080 100644 --- a/packages/react-native-audio-api/src/interfaces.ts +++ b/packages/react-native-audio-api/src/interfaces.ts @@ -1,25 +1,26 @@ import { AudioEventCallback, AudioEventName } from './events/types'; import type { + AnalyserOptions, AudioRecorderCallbackOptions, AudioRecorderFileOptions, + AutomationEventData, + BaseAudioBufferSourceOptions, + BiquadFilterOptions, BiquadFilterType, ChannelCountMode, ChannelInterpretation, - ContextState, - FileInfo, - OscillatorType, - OverSampleType, - Result, - AnalyserOptions, - BaseAudioBufferSourceOptions, - BiquadFilterOptions, ConstantSourceOptions, + ContextState, DelayOptions, + FileInfo, GainOptions, IAudioBufferSourceOptions, IConvolverOptions, IIRFilterOptions, OscillatorOptions, + OscillatorType, + OverSampleType, + Result, StereoPannerOptions, StreamerOptions, WaveShaperOptions, @@ -142,10 +143,10 @@ export interface IStereoPannerNode extends IAudioNode { } export interface IBiquadFilterNode extends IAudioNode { - readonly frequency: AudioParam; - readonly detune: AudioParam; - readonly Q: AudioParam; - readonly gain: AudioParam; + readonly frequency: IAudioParam; + readonly detune: IAudioParam; + readonly Q: IAudioParam; + readonly gain: IAudioParam; type: BiquadFilterType; getFrequencyResponse( @@ -275,6 +276,7 @@ export interface IAudioParam { ) => void; cancelScheduledValues: (cancelTime: number) => void; cancelAndHoldAtTime: (cancelTime: number) => void; + checkCurveExclusion: (eventData: AutomationEventData) => Result; } export interface IPeriodicWave {} diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index 4fe3ed539..06a5976b9 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -249,3 +249,28 @@ export type DecodeDataInput = number | string | ArrayBuffer; export interface AudioRecorderStartOptions { fileNameOverride?: string; } + +export enum AutomationEventType { + LINEAR_RAMP, + EXPONENTIAL_RAMP, + SET_VALUE, + SET_TARGET, + SET_VALUE_CURVE, +} + +export type AutomationEventData = + | { type: AutomationEventType.SET_VALUE; automationTime: number } + | { type: AutomationEventType.LINEAR_RAMP; automationTime: number } + | { + type: AutomationEventType.EXPONENTIAL_RAMP; + automationTime: number; + } + | { + type: AutomationEventType.SET_TARGET; + automationTime: number; + } + | { + type: AutomationEventType.SET_VALUE_CURVE; + automationTime: number; + duration: number; + };