diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp index b3955cdaa..6183dcbb2 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp @@ -15,7 +15,10 @@ AudioPlayer::AudioPlayer( const std::function, int)> &renderAudio, float sampleRate, int channelCount) - : renderAudio_(renderAudio), sampleRate_(sampleRate), channelCount_(channelCount) { + : renderAudio_(renderAudio), + sampleRate_(sampleRate), + channelCount_(channelCount), + isRunning_(false) { isInitialized_ = openAudioStream(); nativeAudioPlayer_ = jni::make_global(NativeAudioPlayer::create()); @@ -41,15 +44,16 @@ bool AudioPlayer::openAudioStream() { return false; } - mBus_ = std::make_shared(RENDER_QUANTUM_SIZE, channelCount_, sampleRate_); + audioBus_ = std::make_shared(RENDER_QUANTUM_SIZE, channelCount_, sampleRate_); return true; } bool AudioPlayer::start() { if (mStream_) { jni::ThreadScope::WithClassLoader([this]() { nativeAudioPlayer_->start(); }); - auto result = mStream_->requestStart(); - return result == oboe::Result::OK; + auto result = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(result, std::memory_order_release); + return result; } return false; @@ -58,14 +62,20 @@ bool AudioPlayer::start() { void AudioPlayer::stop() { if (mStream_) { jni::ThreadScope::WithClassLoader([this]() { nativeAudioPlayer_->stop(); }); + isRunning_.store(false, std::memory_order_release); mStream_->requestStop(); } } bool AudioPlayer::resume() { + if (isRunning()) { + return true; + } + if (mStream_) { - auto result = mStream_->requestStart(); - return result == oboe::Result::OK; + auto result = mStream_->requestStart() == oboe::Result::OK; + isRunning_.store(result, std::memory_order_release); + return result; } return false; @@ -73,6 +83,7 @@ bool AudioPlayer::resume() { void AudioPlayer::suspend() { if (mStream_) { + isRunning_.store(false, std::memory_order_release); mStream_->requestPause(); } } @@ -87,7 +98,8 @@ void AudioPlayer::cleanup() { } bool AudioPlayer::isRunning() const { - return mStream_ && mStream_->getState() == oboe::StreamState::Started; + return mStream_ && mStream_->getState() == oboe::StreamState::Started && + isRunning_.load(std::memory_order_acquire); } DataCallbackResult @@ -99,17 +111,19 @@ AudioPlayer::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numF auto buffer = static_cast(audioData); int processedFrames = 0; - assert(buffer != nullptr); - while (processedFrames < numFrames) { int framesToProcess = std::min(numFrames - processedFrames, RENDER_QUANTUM_SIZE); - renderAudio_(mBus_, framesToProcess); - // TODO: optimize this with SIMD? + if (isRunning_.load(std::memory_order_acquire)) { + renderAudio_(audioBus_, framesToProcess); + } else { + audioBus_->zero(); + } + for (int i = 0; i < framesToProcess; i++) { for (int channel = 0; channel < channelCount_; channel += 1) { buffer[(processedFrames + i) * channelCount_ + channel] = - mBus_->getChannel(channel)->getData()[i]; + audioBus_->getChannel(channel)->getData()[i]; } } diff --git a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h index 5283a4316..c93c7ebe7 100644 --- a/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +++ b/packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AudioPlayer.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -45,10 +44,11 @@ class AudioPlayer : public AudioStreamDataCallback, AudioStreamErrorCallback { private: std::function, int)> renderAudio_; std::shared_ptr mStream_; - std::shared_ptr mBus_; + std::shared_ptr audioBus_; bool isInitialized_ = false; float sampleRate_; int channelCount_; + std::atomic isRunning_; bool openAudioStream(); 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 45508e702..b3f3c26ec 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 @@ -7,7 +7,7 @@ namespace audioapi { AudioParamHostObject::AudioParamHostObject(const std::shared_ptr ¶m) - : param_(param) { + : param_(param), defaultValue_(param->getDefaultValue()), minValue_(param->getMinValue()), maxValue_(param->getMaxValue()) { addGetters( JSI_EXPORT_PROPERTY_GETTER(AudioParamHostObject, value), JSI_EXPORT_PROPERTY_GETTER(AudioParamHostObject, defaultValue), @@ -31,47 +31,58 @@ JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, value) { } JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, defaultValue) { - return {param_->getDefaultValue()}; + return {defaultValue_}; } JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, minValue) { - return {param_->getMinValue()}; + return {minValue_}; } JSI_PROPERTY_GETTER_IMPL(AudioParamHostObject, maxValue) { - return {param_->getMaxValue()}; + return {maxValue_}; } JSI_PROPERTY_SETTER_IMPL(AudioParamHostObject, value) { - param_->setValue(static_cast(value.getNumber())); + auto event = [param = param_, value = static_cast(value.getNumber())](BaseAudioContext &context) { + param->setValue(value); + }; + + param_->scheduleAudioEvent(std::move(event)); } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setValueAtTime) { - auto value = static_cast(args[0].getNumber()); - double startTime = args[1].getNumber(); - param_->setValueAtTime(value, startTime); + auto event = [param = param_, value = static_cast(args[0].getNumber()), startTime = args[1].getNumber()](BaseAudioContext &context) { + param->setValueAtTime(value, startTime); + }; + + param_->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, linearRampToValueAtTime) { - auto value = static_cast(args[0].getNumber()); - double endTime = args[1].getNumber(); - param_->linearRampToValueAtTime(value, endTime); + auto event = [param = param_, value = static_cast(args[0].getNumber()), endTime = args[1].getNumber()](BaseAudioContext &context) { + param->linearRampToValueAtTime(value, endTime); + }; + + param_->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, exponentialRampToValueAtTime) { - auto value = static_cast(args[0].getNumber()); - double endTime = args[1].getNumber(); - param_->exponentialRampToValueAtTime(value, endTime); + auto event = [param = param_, value = static_cast(args[0].getNumber()), endTime = args[1].getNumber()](BaseAudioContext &context) { + param->exponentialRampToValueAtTime(value, endTime); + }; + + param_->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setTargetAtTime) { - auto target = static_cast(args[0].getNumber()); - double startTime = args[1].getNumber(); - double timeConstant = args[2].getNumber(); - param_->setTargetAtTime(target, startTime, timeConstant); + auto event = [param = param_, target = static_cast(args[0].getNumber()), startTime = args[1].getNumber(), timeConstant = args[2].getNumber()](BaseAudioContext &context) { + param->setTargetAtTime(target, startTime, timeConstant); + }; + + param_->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } @@ -80,24 +91,32 @@ JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, setValueCurveAtTime) { args[0].getObject(runtime).getPropertyAsObject(runtime, "buffer").getArrayBuffer(runtime); auto rawValues = reinterpret_cast(arrayBuffer.data(runtime)); auto length = static_cast(arrayBuffer.size(runtime)); - auto values = std::make_unique>(rawValues, rawValues + length); + auto values = std::make_shared>(rawValues, rawValues + length); - double startTime = args[1].getNumber(); - double duration = args[2].getNumber(); - param_->setValueCurveAtTime(std::move(values), length, startTime, duration); + auto event = [param = param_, values, length, startTime = args[1].getNumber(), duration = args[2].getNumber()](BaseAudioContext &context) { + param->setValueCurveAtTime(values, length, startTime, duration); + }; + + param_->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelScheduledValues) { - double cancelTime = args[0].getNumber(); - param_->cancelScheduledValues(cancelTime); + auto event = [param = param_, cancelTime = args[0].getNumber()](BaseAudioContext &context) { + param->cancelScheduledValues(cancelTime); + }; + + param_->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } JSI_HOST_FUNCTION_IMPL(AudioParamHostObject, cancelAndHoldAtTime) { - double cancelTime = args[0].getNumber(); - param_->cancelAndHoldAtTime(cancelTime); - return jsi::Value::undefined(); + auto event = [param = param_, cancelTime = args[0].getNumber()](BaseAudioContext &context) { + param->cancelAndHoldAtTime(cancelTime); + }; + + param_->scheduleAudioEvent(std::move(event)); + return jsi::Value::undefined(); } } // 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 8afbfaa10..363ec7bcc 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 @@ -36,5 +36,8 @@ class AudioParamHostObject : public JsiHostObject { friend class AudioNodeHostObject; std::shared_ptr param_; + float defaultValue_; + float minValue_; + float maxValue_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp index 202a4643b..7e791aee8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.cpp @@ -1,14 +1,17 @@ #include - #include #include +#include + #include +#include namespace audioapi { AudioBufferSourceNodeHostObject::AudioBufferSourceNodeHostObject( const std::shared_ptr &node) - : AudioBufferBaseSourceNodeHostObject(node) { + : AudioBufferBaseSourceNodeHostObject(node), loop_(false), loopSkip_(false), loopStart_(0), loopEnd_(0), buffer_(nullptr) { + // TODO: init with AudioBufferSourceOptions when PR with new ctors will be merged addGetters( JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, loop), JSI_EXPORT_PROPERTY_GETTER(AudioBufferSourceNodeHostObject, loopSkip), @@ -32,93 +35,104 @@ AudioBufferSourceNodeHostObject::AudioBufferSourceNodeHostObject( } AudioBufferSourceNodeHostObject::~AudioBufferSourceNodeHostObject() { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - // When JSI object is garbage collected (together with the eventual callback), // underlying source node might still be active and try to call the // non-existing callback. - audioBufferSourceNode->setOnLoopEndedCallbackId(0); + setOnLoopEndedCallbackId(0); } JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, loop) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - auto loop = audioBufferSourceNode->getLoop(); - return {loop}; + return {loop_}; } JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, loopSkip) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - auto loopSkip = audioBufferSourceNode->getLoopSkip(); - return {loopSkip}; + return {loopSkip_}; } JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, buffer) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - auto buffer = audioBufferSourceNode->getBuffer(); - - if (!buffer) { + if (!buffer_) { return jsi::Value::null(); } - auto bufferHostObject = std::make_shared(buffer); - auto jsiObject = jsi::Object::createFromHostObject(runtime, bufferHostObject); - jsiObject.setExternalMemoryPressure(runtime, bufferHostObject->getSizeInBytes() + 16); + auto jsiObject = jsi::Object::createFromHostObject(runtime, buffer_); + jsiObject.setExternalMemoryPressure(runtime, buffer_->getSizeInBytes() + 16); return jsiObject; } JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, loopStart) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - auto loopStart = audioBufferSourceNode->getLoopStart(); - return {loopStart}; + return {loopStart_}; } JSI_PROPERTY_GETTER_IMPL(AudioBufferSourceNodeHostObject, loopEnd) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - auto loopEnd = audioBufferSourceNode->getLoopEnd(); - return {loopEnd}; + return {loopEnd_}; } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loop) { auto audioBufferSourceNode = std::static_pointer_cast(node_); - audioBufferSourceNode->setLoop(value.getBool()); + auto loop = value.getBool(); + + auto event = [audioBufferSourceNode, loop](BaseAudioContext &context) { + audioBufferSourceNode->setLoop(loop); + }; + + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + loop_ = loop; } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopSkip) { auto audioBufferSourceNode = std::static_pointer_cast(node_); - audioBufferSourceNode->setLoopSkip(value.getBool()); + auto loopSkip = value.getBool(); + + auto event = [audioBufferSourceNode, loopSkip](BaseAudioContext &context) { + audioBufferSourceNode->setLoopSkip(loopSkip); + }; + + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + loopSkip_ = loopSkip; } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopStart) { auto audioBufferSourceNode = std::static_pointer_cast(node_); - audioBufferSourceNode->setLoopStart(value.getNumber()); + auto loopStart = value.getNumber(); + + auto event = [audioBufferSourceNode, loopStart](BaseAudioContext &context) { + audioBufferSourceNode->setLoopStart(loopStart); + }; + + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + loopStart_ = loopStart; } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, loopEnd) { auto audioBufferSourceNode = std::static_pointer_cast(node_); - audioBufferSourceNode->setLoopEnd(value.getNumber()); + auto loopEnd = value.getNumber(); + + auto event = [audioBufferSourceNode, loopEnd](BaseAudioContext &context) { + audioBufferSourceNode->setLoopEnd(loopEnd); + }; + + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + loopEnd_ = loopEnd; } JSI_PROPERTY_SETTER_IMPL(AudioBufferSourceNodeHostObject, onLoopEnded) { - auto audioBufferSourceNode = std::static_pointer_cast(node_); - - audioBufferSourceNode->setOnLoopEndedCallbackId( - std::stoull(value.getString(runtime).utf8(runtime))); + auto callbackId = std::stoull(value.getString(runtime).utf8(runtime)); + setOnLoopEndedCallbackId(callbackId); } JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, start) { - auto when = args[0].getNumber(); - auto offset = args[1].getNumber(); - auto audioBufferSourceNode = std::static_pointer_cast(node_); - if (args[2].isUndefined()) { - audioBufferSourceNode->start(when, offset); - - return jsi::Value::undefined(); - } + auto event = [ + audioBufferSourceNode, + when = args[0].getNumber(), + offset = args[1].getNumber(), + duration = args[2].isUndefined() ? -1 : args[2].getNumber() + ](BaseAudioContext &context) { + audioBufferSourceNode->start(when, offset, duration); + }; - auto duration = args[2].getNumber(); - audioBufferSourceNode->start(when, offset, duration); + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); return jsi::Value::undefined(); } @@ -126,16 +140,38 @@ JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, start) { JSI_HOST_FUNCTION_IMPL(AudioBufferSourceNodeHostObject, setBuffer) { auto audioBufferSourceNode = std::static_pointer_cast(node_); - if (args[0].isNull()) { - audioBufferSourceNode->setBuffer(std::shared_ptr(nullptr)); - return jsi::Value::undefined(); - } + auto bufferHostObject = args[0].isNull() ? std::shared_ptr(nullptr) : + args[0].getObject(runtime).asHostObject(runtime); + + if (bufferHostObject != nullptr) { + thisValue.asObject(runtime).setExternalMemoryPressure( + runtime, bufferHostObject->getSizeInBytes() + 16); + } + + auto event = [ + audioBufferSourceNode, + buffer = bufferHostObject ? bufferHostObject->audioBuffer_ : nullptr + ](BaseAudioContext &context) { + audioBufferSourceNode->setBuffer(buffer); + }; + + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + buffer_ = bufferHostObject; + loopEnd_ = bufferHostObject ? bufferHostObject->audioBuffer_->getDuration() : 0.0; - auto bufferHostObject = args[0].getObject(runtime).asHostObject(runtime); - thisValue.asObject(runtime).setExternalMemoryPressure( - runtime, bufferHostObject->getSizeInBytes() + 16); - audioBufferSourceNode->setBuffer(bufferHostObject->audioBuffer_); return jsi::Value::undefined(); } +void AudioBufferSourceNodeHostObject::setOnLoopEndedCallbackId(uint64_t callbackId) { + auto audioBufferSourceNode = std::static_pointer_cast(node_); + + auto event = [audioBufferSourceNode, callbackId](BaseAudioContext &context) { + audioBufferSourceNode->setOnLoopEndedCallbackId(callbackId); + }; + + audioBufferSourceNode->unregisterOnLoopEndedCallback(onLoopEndedCallbackId_); + audioBufferSourceNode->scheduleAudioEvent(std::move(event)); + onLoopEndedCallbackId_ = callbackId; +} + } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h index 57fcbee11..cb3f21008 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioBufferSourceNodeHostObject.h @@ -9,6 +9,7 @@ namespace audioapi { using namespace facebook; class AudioBufferSourceNode; +class AudioBufferHostObject; class AudioBufferSourceNodeHostObject : public AudioBufferBaseSourceNodeHostObject { public: @@ -30,6 +31,16 @@ class AudioBufferSourceNodeHostObject : public AudioBufferBaseSourceNodeHostObje JSI_HOST_FUNCTION_DECL(start); JSI_HOST_FUNCTION_DECL(setBuffer); + + private: + bool loop_; + bool loopSkip_; + double loopStart_; + double loopEnd_; + uint64_t onLoopEndedCallbackId_ = 0; + std::shared_ptr buffer_; + + void setOnLoopEndedCallbackId(uint64_t callbackId); }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp index a2c2d19b2..5910bdc46 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioContext.cpp @@ -15,8 +15,7 @@ AudioContext::AudioContext( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, const RuntimeRegistry &runtimeRegistry) - : BaseAudioContext(audioEventHandlerRegistry, runtimeRegistry), isInitialized_(false) { - sampleRate_ = sampleRate; + : BaseAudioContext(sampleRate, audioEventHandlerRegistry, runtimeRegistry), isInitialized_(false) { state_ = ContextState::SUSPENDED; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h index 2b6edfcbe..f3edb4798 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/AudioNode.h @@ -1,20 +1,22 @@ #pragma once +#include #include #include #include #include #include +#include #include #include #include +#include #include namespace audioapi { class AudioBus; -class BaseAudioContext; class AudioParam; class AudioNode : public std::enable_shared_from_this { @@ -42,6 +44,17 @@ class AudioNode : public std::enable_shared_from_this { void enable(); virtual void disable(); + template < + typename F, + typename = std::enable_if_t, BaseAudioContext &>>> + bool inline scheduleAudioEvent(F &&event) noexcept { + if (std::shared_ptr context = context_.lock()) { + return context->scheduleAudioEvent(std::forward(event)); + } + + return false; + } + protected: friend class AudioNodeManager; friend class AudioDestinationNode; 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 f1847550c..6db11ff6e 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 @@ -19,7 +19,6 @@ AudioParam::AudioParam( minValue_(minValue), maxValue_(maxValue), eventsQueue_(), - eventScheduler_(32), startTime_(0), endTime_(0), startValue_(defaultValue), @@ -29,7 +28,7 @@ AudioParam::AudioParam( inputNodes_.reserve(4); // Default calculation function just returns the static value calculateValue_ = [this](double, double, float, float, double) { - return value_; + return value_.load(std::memory_order_relaxed); }; } @@ -48,13 +47,12 @@ float AudioParam::getValueAtTime(double time) { // Calculate value using the current automation function and clamp to valid setValue(calculateValue_(startTime_, endTime_, startValue_, endValue_, time)); - return value_; + return value_.load(std::memory_order_relaxed); } void AudioParam::setValueAtTime(float value, double startTime) { - auto event = [value, startTime](AudioParam ¶m) { // Ignore events scheduled before the end of existing automation - if (startTime < param.getQueueEndTime()) { + if (startTime < this->getQueueEndTime()) { return; } @@ -68,21 +66,18 @@ void AudioParam::setValueAtTime(float value, double startTime) { return endValue; }; - param.updateQueue(ParamChangeEvent( + this->updateQueue(ParamChangeEvent( startTime, startTime, - param.getQueueEndValue(), + this->getQueueEndValue(), value, std::move(calculateValue), ParamChangeEventType::SET_VALUE)); - }; - eventScheduler_.scheduleEvent(std::move(event)); } void AudioParam::linearRampToValueAtTime(float value, double endTime) { - auto event = [value, endTime](AudioParam ¶m) { // Ignore events scheduled before the end of existing automation - if (endTime < param.getQueueEndTime()) { + if (endTime < this->getQueueEndTime()) { return; } @@ -101,20 +96,17 @@ void AudioParam::linearRampToValueAtTime(float value, double endTime) { return endValue; }; - param.updateQueue(ParamChangeEvent( - param.getQueueEndTime(), + this->updateQueue(ParamChangeEvent( + this->getQueueEndTime(), endTime, - param.getQueueEndValue(), + this->getQueueEndValue(), value, std::move(calculateValue), ParamChangeEventType::LINEAR_RAMP)); - }; - eventScheduler_.scheduleEvent(std::move(event)); } void AudioParam::exponentialRampToValueAtTime(float value, double endTime) { - auto event = [value, endTime](AudioParam ¶m) { - if (endTime <= param.getQueueEndTime()) { + if (endTime <= this->getQueueEndTime()) { return; } @@ -134,20 +126,17 @@ void AudioParam::exponentialRampToValueAtTime(float value, double endTime) { return endValue; }; - param.updateQueue(ParamChangeEvent( - param.getQueueEndTime(), + this->updateQueue(ParamChangeEvent( + this->getQueueEndTime(), endTime, - param.getQueueEndValue(), + this->getQueueEndValue(), value, std::move(calculateValue), ParamChangeEventType::EXPONENTIAL_RAMP)); - }; - eventScheduler_.scheduleEvent(std::move(event)); } void AudioParam::setTargetAtTime(float target, double startTime, double timeConstant) { - auto event = [target, startTime, timeConstant](AudioParam ¶m) { - if (startTime <= param.getQueueEndTime()) { + if (startTime <= this->getQueueEndTime()) { return; } // Exponential decay function towards target value @@ -160,17 +149,14 @@ void AudioParam::setTargetAtTime(float target, double startTime, double timeCons return static_cast( target + (startValue - target) * exp(-(time - startTime) / timeConstant)); }; - param.updateQueue(ParamChangeEvent( + this->updateQueue(ParamChangeEvent( startTime, startTime, // SetTarget events have infinite duration conceptually - param.getQueueEndValue(), - param.getQueueEndValue(), // End value is not meaningful for + this->getQueueEndValue(), + this->getQueueEndValue(), // End value is not meaningful for // infinite events std::move(calculateValue), ParamChangeEventType::SET_TARGET)); - }; - - eventScheduler_.scheduleEvent(std::move(event)); } void AudioParam::setValueCurveAtTime( @@ -178,8 +164,7 @@ void AudioParam::setValueCurveAtTime( size_t length, double startTime, double duration) { - auto event = [values, length, startTime, duration](AudioParam ¶m) { - if (startTime <= param.getQueueEndTime()) { + if (startTime <= this->getQueueEndTime()) { return; } @@ -203,29 +188,21 @@ void AudioParam::setValueCurveAtTime( return endValue; }; - param.updateQueue(ParamChangeEvent( + this->updateQueue(ParamChangeEvent( startTime, startTime + duration, - param.getQueueEndValue(), + this->getQueueEndValue(), values->at(length - 1), std::move(calculateValue), ParamChangeEventType::SET_VALUE_CURVE)); - }; - - /// Schedules an event that modifies this param - /// It will be executed on next audio render cycle - eventScheduler_.scheduleEvent(std::move(event)); } void AudioParam::cancelScheduledValues(double cancelTime) { - eventScheduler_.scheduleEvent( - [cancelTime](AudioParam ¶m) { param.eventsQueue_.cancelScheduledValues(cancelTime); }); + this->eventsQueue_.cancelScheduledValues(cancelTime); } void AudioParam::cancelAndHoldAtTime(double cancelTime) { - eventScheduler_.scheduleEvent([cancelTime](AudioParam ¶m) { - param.eventsQueue_.cancelAndHoldAtTime(cancelTime, param.endTime_); - }); + this->eventsQueue_.cancelAndHoldAtTime(cancelTime, this->endTime_); } void AudioParam::addInputNode(AudioNode *node) { @@ -255,7 +232,6 @@ std::shared_ptr AudioParam::calculateInputs( } std::shared_ptr AudioParam::processARateParam(int framesToProcess, double time) { - processScheduledEvents(); auto processingBus = calculateInputs(audioBus_, framesToProcess); std::shared_ptr context = context_.lock(); @@ -277,7 +253,6 @@ std::shared_ptr AudioParam::processARateParam(int framesToProcess, dou } float AudioParam::processKRateParam(int framesToProcess, double time) { - processScheduledEvents(); auto processingBus = calculateInputs(audioBus_, framesToProcess); // Return block-rate parameter value plus first sample of input modulation 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 af1d7fcb6..4c7a8ed87 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 @@ -23,84 +23,64 @@ class AudioParam { float maxValue, std::shared_ptr context); - /// JS-Thread only methods - /// These methods are called only from HostObjects invoked on the JS thread. - - // JS-Thread only + /// @note Can be invoked from any thread [[nodiscard]] inline float getValue() const noexcept { - return value_; + return value_.load(std::memory_order_relaxed); } - // JS-Thread only [[nodiscard]] inline float getDefaultValue() const noexcept { return defaultValue_; } - // JS-Thread only [[nodiscard]] inline float getMinValue() const noexcept { return minValue_; } - // JS-Thread only [[nodiscard]] inline float getMaxValue() const noexcept { return maxValue_; } - // JS-Thread only inline void setValue(float value) { - value_ = std::clamp(value, minValue_, maxValue_); + value_.store(std::clamp(value, minValue_, maxValue_), std::memory_order_relaxed); } - // JS-Thread only void setValueAtTime(float value, double startTime); - - // JS-Thread only void linearRampToValueAtTime(float value, double endTime); - - // JS-Thread only void exponentialRampToValueAtTime(float value, double endTime); - - // JS-Thread only void setTargetAtTime(float target, double startTime, double timeConstant); - - // JS-Thread only void setValueCurveAtTime( std::shared_ptr> values, size_t length, double startTime, double duration); - - // JS-Thread only void cancelScheduledValues(double cancelTime); - - // JS-Thread only void cancelAndHoldAtTime(double cancelTime); - /// Audio-Thread only methods - /// These methods are called only from the Audio rendering thread. - - // Audio-Thread only (indirectly through AudioNode::connectParam by AudioNodeManager) void addInputNode(AudioNode *node); - - // Audio-Thread only (indirectly through AudioNode::disconnectParam by AudioNodeManager) void removeInputNode(AudioNode *node); - - // Audio-Thread only std::shared_ptr processARateParam(int framesToProcess, double time); - - // Audio-Thread only float processKRateParam(int framesToProcess, double time); + template < + typename F, + typename = std::enable_if_t, BaseAudioContext &>>> + bool inline scheduleAudioEvent(F &&event) noexcept { + if (std::shared_ptr context = context_.lock()) { + return context->scheduleAudioEvent(std::forward(event)); + } + + return false; + } + private: // Core parameter state std::weak_ptr context_; - float value_; + std::atomic value_; float defaultValue_; float minValue_; float maxValue_; AudioParamEventQueue eventsQueue_; - CrossThreadEventScheduler eventScheduler_; // Current automation state (cached for performance) double startTime_; @@ -116,7 +96,7 @@ class AudioParam { /// @brief Get the end time of the parameter queue. /// @return The end time of the parameter queue or last endTime_ if queue is empty. - inline double getQueueEndTime() const noexcept { + [[nodiscard]] inline double getQueueEndTime() const noexcept { if (eventsQueue_.isEmpty()) { return endTime_; } @@ -125,18 +105,13 @@ class AudioParam { /// @brief Get the end value of the parameter queue. /// @return The end value of the parameter queue or last endValue_ if queue is empty. - inline float getQueueEndValue() const noexcept { + [[nodiscard]] inline float getQueueEndValue() const noexcept { if (eventsQueue_.isEmpty()) { return endValue_; } return eventsQueue_.back().getEndValue(); } - /// @brief Process all scheduled events. - inline void processScheduledEvents() noexcept(noexcept(eventScheduler_.processAllEvents(*this))) { - eventScheduler_.processAllEvents(*this); - } - /// @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. diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp index 35313bdfd..d935a97d6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.cpp @@ -35,9 +35,12 @@ namespace audioapi { BaseAudioContext::BaseAudioContext( + float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, const RuntimeRegistry &runtimeRegistry) - : nodeManager_(std::make_shared()), + : sampleRate_ {sampleRate}, + nodeManager_(std::make_shared()), + audioEventScheduler_(std::make_unique>(1024)), audioEventHandlerRegistry_(audioEventHandlerRegistry), runtimeRegistry_(runtimeRegistry) {} diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h index d97523518..b40fd6581 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/BaseAudioContext.h @@ -3,12 +3,15 @@ #include #include #include +#include + #include #include #include #include #include #include +#include #include #include @@ -42,6 +45,7 @@ class WaveShaperNode; class BaseAudioContext : public std::enable_shared_from_this { public: explicit BaseAudioContext( + float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, const RuntimeRegistry &runtimeRegistry); virtual ~BaseAudioContext() = default; @@ -101,12 +105,28 @@ class BaseAudioContext : public std::enable_shared_from_this { [[nodiscard]] bool isSuspended() const; [[nodiscard]] bool isClosed() const; + void inline processAudioEvents() { + audioEventScheduler_->processAllEvents(*this); + } + + template < + typename F, + typename = std::enable_if_t, BaseAudioContext &>>> + bool inline scheduleAudioEvent(F &&event) noexcept { + if (!isRunning()) { + processAudioEvents(); + event(*this); + return true; + } + + return audioEventScheduler_->scheduleEvent(std::forward(event)); + } + protected: static std::string toString(ContextState state); std::shared_ptr destination_; - // init in AudioContext or OfflineContext constructor - float sampleRate_{}; + float sampleRate_; ContextState state_ = ContextState::RUNNING; std::shared_ptr nodeManager_; @@ -116,6 +136,8 @@ class BaseAudioContext : public std::enable_shared_from_this { std::shared_ptr cachedSawtoothWave_ = nullptr; std::shared_ptr cachedTriangleWave_ = nullptr; + std::unique_ptr> audioEventScheduler_; + [[nodiscard]] virtual bool isDriverRunning() const = 0; public: diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp index 6d38f87d6..ace04ad2f 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/OfflineAudioContext.cpp @@ -24,11 +24,10 @@ OfflineAudioContext::OfflineAudioContext( float sampleRate, const std::shared_ptr &audioEventHandlerRegistry, const RuntimeRegistry &runtimeRegistry) - : BaseAudioContext(audioEventHandlerRegistry, runtimeRegistry), + : BaseAudioContext(sampleRate, audioEventHandlerRegistry, runtimeRegistry), length_(length), numberOfChannels_(numberOfChannels), currentSampleFrame_(0) { - sampleRate_ = sampleRate; resultBus_ = std::make_shared(static_cast(length_), numberOfChannels_, sampleRate_); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp index 3bc7da0d0..d41bb9d6a 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/destinations/AudioDestinationNode.cpp @@ -35,6 +35,7 @@ void AudioDestinationNode::renderAudio( } if (std::shared_ptr context = context_.lock()) { + context->processAudioEvents(); context->getNodeManager()->preProcessGraph(); } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp index 3a3507c49..56cc6a4a6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp @@ -20,38 +20,14 @@ AudioBufferSourceNode::AudioBufferSourceNode( loopSkip_(false), loopStart_(0), loopEnd_(0), - buffer_(nullptr), alignedBus_(nullptr) { isInitialized_ = true; } AudioBufferSourceNode::~AudioBufferSourceNode() { - Locker locker(getBufferLock()); - - buffer_.reset(); alignedBus_.reset(); } -bool AudioBufferSourceNode::getLoop() const { - return loop_; -} - -bool AudioBufferSourceNode::getLoopSkip() const { - return loopSkip_; -} - -double AudioBufferSourceNode::getLoopStart() const { - return loopStart_; -} - -double AudioBufferSourceNode::getLoopEnd() const { - return loopEnd_; -} - -std::shared_ptr AudioBufferSourceNode::getBuffer() const { - return buffer_; -} - void AudioBufferSourceNode::setLoop(bool loop) { loop_ = loop; } @@ -74,39 +50,36 @@ void AudioBufferSourceNode::setLoopEnd(double loopEnd) { } void AudioBufferSourceNode::setBuffer(const std::shared_ptr &buffer) { - Locker locker(getBufferLock()); std::shared_ptr context = context_.lock(); if (buffer == nullptr || context == nullptr) { - buffer_ = std::shared_ptr(nullptr); alignedBus_ = std::shared_ptr(nullptr); loopEnd_ = 0; return; } - buffer_ = buffer; - channelCount_ = buffer_->getNumberOfChannels(); + channelCount_ = buffer->getNumberOfChannels(); - stretch_->presetDefault(channelCount_, buffer_->getSampleRate()); + stretch_->presetDefault(channelCount_, buffer->getSampleRate()); if (pitchCorrection_) { int extraTailFrames = static_cast((getInputLatency() + getOutputLatency()) * context->getSampleRate()); - size_t totalSize = buffer_->getLength() + extraTailFrames; + size_t totalSize = buffer->getLength() + extraTailFrames; - alignedBus_ = std::make_shared(totalSize, channelCount_, buffer_->getSampleRate()); - alignedBus_->copy(buffer_->bus_.get(), 0, 0, buffer_->getLength()); + alignedBus_ = std::make_shared(totalSize, channelCount_, buffer->getSampleRate()); + alignedBus_->copy(buffer->bus_.get(), 0, 0, buffer->getLength()); - alignedBus_->zero(buffer_->getLength(), extraTailFrames); + alignedBus_->zero(buffer->getLength(), extraTailFrames); } else { - alignedBus_ = std::make_shared(*buffer_->bus_); + alignedBus_ = std::make_shared(*buffer->bus_); } audioBus_ = std::make_shared(RENDER_QUANTUM_SIZE, channelCount_, context->getSampleRate()); playbackRateBus_ = std::make_shared( RENDER_QUANTUM_SIZE * 3, channelCount_, context->getSampleRate()); - loopEnd_ = buffer_->getDuration(); + loopEnd_ = buffer->getDuration(); } void AudioBufferSourceNode::start(double when, double offset, double duration) { @@ -136,11 +109,11 @@ void AudioBufferSourceNode::disable() { } void AudioBufferSourceNode::setOnLoopEndedCallbackId(uint64_t callbackId) { - auto oldCallbackId = onLoopEndedCallbackId_.exchange(callbackId, std::memory_order_acq_rel); + onLoopEndedCallbackId_ = callbackId; +} - if (oldCallbackId != 0) { - audioEventHandlerRegistry_->unregisterHandler("loopEnded", oldCallbackId); - } +void AudioBufferSourceNode::unregisterOnLoopEndedCallback(uint64_t callbackId) { + audioEventHandlerRegistry_->unregisterHandler("loopEnded", callbackId); } std::shared_ptr AudioBufferSourceNode::processNode( @@ -168,13 +141,12 @@ std::shared_ptr AudioBufferSourceNode::processNode( } double AudioBufferSourceNode::getCurrentPosition() const { - return dsp::sampleFrameToTime(static_cast(vReadIndex_), buffer_->getSampleRate()); + return dsp::sampleFrameToTime(static_cast(vReadIndex_), alignedBus_->getSampleRate()); } void AudioBufferSourceNode::sendOnLoopEndedEvent() { - auto onLoopEndedCallbackId = onLoopEndedCallbackId_.load(std::memory_order_acquire); - if (onLoopEndedCallbackId != 0) { - audioEventHandlerRegistry_->invokeHandlerWithEventBody("loopEnded", onLoopEndedCallbackId, {}); + if (onLoopEndedCallbackId_ != 0) { + audioEventHandlerRegistry_->invokeHandlerWithEventBody("loopEnded", onLoopEndedCallbackId_, {}); } } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h index 9ea731c32..5b4a8cab3 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioBufferSourceNode.h @@ -19,12 +19,6 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode { explicit AudioBufferSourceNode(std::shared_ptr context, bool pitchCorrection); ~AudioBufferSourceNode() override; - [[nodiscard]] bool getLoop() const; - [[nodiscard]] bool getLoopSkip() const; - [[nodiscard]] double getLoopStart() const; - [[nodiscard]] double getLoopEnd() const; - [[nodiscard]] std::shared_ptr getBuffer() const; - void setLoop(bool loop); void setLoopSkip(bool loopSkip); void setLoopStart(double loopStart); @@ -37,6 +31,10 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode { void setOnLoopEndedCallbackId(uint64_t callbackId); + /// @note JS Thread only. + /// Thread safe, because does not access state of the node. + void unregisterOnLoopEndedCallback(uint64_t callbackId); + protected: std::shared_ptr processNode( const std::shared_ptr &processingBus, @@ -51,10 +49,9 @@ class AudioBufferSourceNode : public AudioBufferBaseSourceNode { double loopEnd_; // User provided buffer - std::shared_ptr buffer_; std::shared_ptr alignedBus_; - std::atomic onLoopEndedCallbackId_ = 0; // 0 means no callback + uint64_t onLoopEndedCallbackId_ = 0; // 0 means no callback void sendOnLoopEndedEvent(); void processWithoutInterpolation( diff --git a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.cpp b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.cpp index 5f4b76b58..832898b68 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.cpp @@ -29,7 +29,7 @@ AudioEventHandlerRegistry::~AudioEventHandlerRegistry() { uint64_t AudioEventHandlerRegistry::registerHandler( const std::string &eventName, const std::shared_ptr &handler) { - uint64_t listenerId = listenerIdCounter_++; + uint64_t listenerId = listenerIdCounter_.fetch_add(1, std::memory_order_relaxed); if (callInvoker_ == nullptr || runtime_ == nullptr) { // If callInvoker or runtime is not valid, we cannot register the handler diff --git a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h index 30c94a27f..76af87dfc 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/events/AudioEventHandlerRegistry.h @@ -16,6 +16,10 @@ using namespace facebook; using EventValue = std::variant>; +/// @brief A registry for audio event handlers. +/// It allows registering, unregistering, and invoking event handlers for audio events. +/// State changes are performed only on the JS thread. +/// State access is thread-safe via the RN CallInvoker. class AudioEventHandlerRegistry : public IAudioEventHandlerRegistry { public: explicit AudioEventHandlerRegistry( @@ -23,14 +27,24 @@ class AudioEventHandlerRegistry : public IAudioEventHandlerRegistry { const std::shared_ptr &callInvoker); ~AudioEventHandlerRegistry() override; + /// @brief Registers an event handler for a specific event. + /// @note Can be used from any thread. uint64_t registerHandler( const std::string &eventName, const std::shared_ptr &handler) override; + + /// @brief Unregisters an event handler for a specific event. + /// @note Can be used from any thread. void unregisterHandler(const std::string &eventName, uint64_t listenerId) override; + /// @brief Invokes the event handler(s) for a specific event with the provided event body. + /// @note Can be used from any thread. void invokeHandlerWithEventBody( const std::string &eventName, const std::unordered_map &body) override; + + /// @brief Invokes a specific event handler by listener ID with the provided event body. + /// @note Can be used from any thread. void invokeHandlerWithEventBody( const std::string &eventName, uint64_t listenerId, diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp index 0d47e5d8a..ed30bccb8 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/CrossThreadEventScheduler.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -24,11 +25,13 @@ using namespace channels::spsc; /// In this setup no locking happens and modifications can be seen by Audio thread. /// @note it is intended to be used for two threads one which schedules events and one which processes them /// @note it is not safe to be copied across two threads use std::shared_ptr if you need to share data -template +template class CrossThreadEventScheduler { + using EventType = FatFunction; + public: explicit CrossThreadEventScheduler(size_t capacity) { - auto [sender, receiver] = channel>(capacity); + auto [sender, receiver] = channel(capacity); eventSender_ = std::move(sender); eventReceiver_ = std::move(receiver); } @@ -36,26 +39,26 @@ class CrossThreadEventScheduler { CrossThreadEventScheduler &operator=(const CrossThreadEventScheduler &) = delete; /// @brief Schedules an event to be processed on the audio thread. - /// @param event The event to schedule. - /// @return True if the event was successfully scheduled, false if the queue is full. - bool scheduleEvent(std::function &&event) noexcept( + /// @param event The event to schedule. Implicitly converts from lambdas. + /// @return True if scheduled, false if the queue is full. + /// @note Requires that sizeof(lambda) <= FunctionSize. + bool scheduleEvent(EventType &&event) noexcept( noexcept(eventSender_.try_send(std::move(event)))) { return eventSender_.try_send(std::move(event)) == ResponseStatus::SUCCESS; } /// @brief Processes all scheduled events. /// @param data The data to pass to each event. - void processAllEvents(T &data) noexcept( - noexcept(eventReceiver_.try_receive(std::declval &>()))) { - std::function event; + void processAllEvents(T &data) noexcept { + EventType event; while (eventReceiver_.try_receive(event) == ResponseStatus::SUCCESS) { event(data); } } private: - Sender> eventSender_; - Receiver> eventReceiver_; + Sender eventSender_; + Receiver eventReceiver_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/FatFunction.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/FatFunction.hpp new file mode 100644 index 000000000..376a51c46 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/FatFunction.hpp @@ -0,0 +1,145 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace audioapi { + +template +class FatFunction; + +/// @brief FatFunction is a fixed-size function wrapper that can store callable objects +/// of a specific size N without dynamic memory allocation. +/// @tparam N Size in bytes to allocate for the callable object +/// @tparam _Fp The function signature (e.g., void(), int(int), etc.) +template +class FatFunction { + + private: + using _InvokerType = _FpReturnType (*)(const std::byte *storage, _FpArgs... args); + using _DeleterType = void (*)(std::byte *storage); + using _MoverType = void (*)(std::byte *dest, std::byte *src); + + public: + FatFunction() = default; + FatFunction(std::nullptr_t) : FatFunction() {} + + /// @brief Constructs a FatFunction from a callable object. + /// @tparam _Callable The type of the callable object + /// @tparam (enable_if) Ensures that the callable fits within the allocated size N + /// and is invocable with the specified signature. + /// @param callable The callable object to store + template < + typename _Callable, + typename = std::enable_if_t< + sizeof(_Callable) <= N && std::is_invocable_r_v<_FpReturnType, _Callable, _FpArgs...>>> + FatFunction(_Callable &&callable) { + using DecayedCallable = std::decay_t<_Callable>; + new (storage_.data()) DecayedCallable(std::forward<_Callable>(callable)); + invoker_ = [](const std::byte *storage, _FpArgs... args) -> _FpReturnType { + const DecayedCallable *callablePtr = reinterpret_cast(storage); + return (*callablePtr)(std::forward<_FpArgs>(args)...); + }; + if constexpr (std::is_trivially_destructible_v) { + // No custom deleter needed for trivially destructible types + deleter_ = nullptr; + } else { + deleter_ = [](std::byte *storage) { + DecayedCallable *callablePtr = reinterpret_cast(storage); + callablePtr->~DecayedCallable(); + }; + } + if constexpr (std::is_trivially_move_constructible_v) { + // No custom mover needed for trivially moveable types as memcpy is a fallback + mover_ = nullptr; + } else { + mover_ = [](std::byte *dest, std::byte *src) { + DecayedCallable *srcPtr = reinterpret_cast(src); + new (dest) DecayedCallable(std::move(*srcPtr)); + }; + } + } + + /// @brief Move constructor + /// @param other + FatFunction(FatFunction &&other) noexcept { + if (other.invoker_) { + if (other.mover_) { + other.mover_(storage_.data(), other.storage_.data()); + } else { + std::memcpy(storage_.data(), other.storage_.data(), N); + } + invoker_ = other.invoker_; + deleter_ = other.deleter_; + mover_ = other.mover_; + other.reset(); + } + } + + /// @brief Move assignment operator + /// @param other + FatFunction &operator=(FatFunction &&other) noexcept { + if (this != &other) { + reset(); + if (other.invoker_) { + if (other.mover_) { + other.mover_(storage_.data(), other.storage_.data()); + } else { + std::memcpy(storage_.data(), other.storage_.data(), N); + } + invoker_ = other.invoker_; + deleter_ = other.deleter_; + mover_ = other.mover_; + other.reset(); + } + } + return *this; + } + + /// @brief Call operator to invoke the stored callable + /// @param ...args Arguments to pass to the callable + /// @return The result of the callable invocation + _FpReturnType operator()(_FpArgs &&...args) const { + if (!invoker_) { + throw std::bad_function_call(); + } + return invoker_(storage_.data(), std::forward<_FpArgs>(args)...); + } + + /// @brief Destructor + ~FatFunction() { + reset(); + } + + /// @brief Releases the stored callable and returns its storage and deleter. + /// @return A pair containing the storage array and the deleter function + /// @note To clear resources properly after release, the user must call the deleter on the storage. + std::pair, _DeleterType> release() { + std::array storageCopy; + std::memcpy(storageCopy.data(), storage_.data(), N); + _DeleterType deleterCopy = deleter_; + deleter_ = nullptr; + invoker_ = nullptr; + mover_ = nullptr; + return {std::move(storageCopy), deleterCopy}; + } + + private: + alignas(std::max_align_t) std::array storage_; + _InvokerType invoker_ = nullptr; // Function pointer to invoke the stored callable + _DeleterType deleter_ = nullptr; // Function pointer to delete the stored callable + _MoverType mover_ = nullptr; // Function pointer to move the stored callable + + void reset() { + if (deleter_) { + deleter_(storage_.data()); + } + deleter_ = nullptr; + invoker_ = nullptr; + mover_ = nullptr; + } +}; + +} // namespace audioapi