diff --git a/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.cpp b/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.cpp index bab0b68b..cb6163ff 100644 --- a/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.cpp @@ -4,13 +4,14 @@ #include "AudioBufferSourceNode.h" #include "AudioBus.h" #include "AudioParam.h" +#include "AudioUtils.h" #include "BaseAudioContext.h" #include "Constants.h" namespace audioapi { AudioBufferSourceNode::AudioBufferSourceNode(BaseAudioContext *context) - : AudioScheduledSourceNode(context), loop_(false), bufferIndex_(0) { + : AudioScheduledSourceNode(context), loop_(false), vReadIndex_(0.0), loopStart_(0), loopEnd_(0) { numberOfInputs_ = 0; buffer_ = std::shared_ptr(nullptr); @@ -65,116 +66,179 @@ void AudioBufferSourceNode::setBuffer( const std::shared_ptr &buffer) { if (!buffer) { buffer_ = std::shared_ptr(nullptr); + alignedBus_ = std::shared_ptr(nullptr); return; } buffer_ = buffer; + alignedBus_ = std::make_shared(context_->getSampleRate(), buffer_->getLength()); + + alignedBus_->zero(); + alignedBus_->sum(buffer_->bus_.get()); } -// Note: AudioBus copy method will use memcpy if the source buffer and system -// processing bus have same channel count, otherwise it will use the summing -// function taking care of up/down mixing. -void AudioBufferSourceNode::processNode( - AudioBus *processingBus, - int framesToProcess) { +void AudioBufferSourceNode::processNode(AudioBus *processingBus, int framesToProcess) { + size_t startOffset = 0; + size_t offsetLength = 0; + + updatePlaybackInfo(processingBus, framesToProcess, startOffset, offsetLength); + float playbackRate = getPlaybackRateValue(startOffset); + // No audio data to fill, zero the output and return. - if (!isPlaying() || !buffer_ || buffer_->getLength() == 0) { + if (!isPlaying() || !alignedBus_ || alignedBus_->getSize() == 0 || !playbackRate) { processingBus->zero(); return; } - // Easiest case, the buffer is the same length as the number of frames to - // process, just copy the data. - if (framesToProcess == buffer_->getLength()) { - processingBus->copy(buffer_->bus_.get()); + // // Wrap to the start of the loop if necessary + // if (loop_ && vReadIndex_ >= vFrameEnd) { + // vReadIndex_ = vFrameStart + std::fmod(vReadIndex_ - vFrameStart, vFrameDelta); + // } + + if (std::fabs(playbackRate) == 1.0) { + processWithoutInterpolation(processingBus, startOffset, offsetLength, playbackRate); + } else { + processWithInterpolation(processingBus, startOffset, offsetLength, playbackRate); + } +} - if (!loop_) { - playbackState_ = PlaybackState::FINISHED; - disable(); +/** + * Helper functions + */ + +void AudioBufferSourceNode::processWithoutInterpolation( + AudioBus *processingBus, + size_t startOffset, + size_t offsetLength, + float playbackRate +) { + size_t direction = playbackRate < 0 ? -1 : 1; + + size_t readIndex = static_cast(vReadIndex_); + size_t writeIndex = startOffset; + + size_t frameStart = static_cast(getVirtualStartFrame()); + size_t frameEnd = static_cast(getVirtualEndFrame()); + size_t frameDelta = frameEnd - frameStart; + + size_t framesLeft = offsetLength; + + while (framesLeft > 0) { + size_t framesToEnd = frameEnd - readIndex; + size_t framesToCopy = std::min(framesToEnd, framesLeft); + framesToCopy = std::max(framesToCopy, 0ul); + + // Direction is forward, we can normally copy the data + if (direction == 1) { + processingBus->copy(alignedBus_.get(), readIndex, writeIndex, framesToCopy); + } else { + for (int i = 0; i < framesToCopy; i += 1) { + for (int j = 0; j < processingBus->getNumberOfChannels(); j += 1) { + (*processingBus->getChannel(j))[writeIndex + i] = (*alignedBus_->getChannel(j))[readIndex - i]; + } + } } - return; + writeIndex += framesToCopy; + readIndex += framesToCopy * direction; + framesLeft -= framesToCopy; + + if (readIndex >= frameEnd || readIndex < frameStart) { + readIndex += direction * frameDelta; + + if (!loop_) { + processingBus->zero(writeIndex, framesLeft); + playbackState_ = PlaybackState::FINISHED; + disable(); + break; + } + } } - // The buffer is longer than the number of frames to process. - // We have to keep track of where we are in the buffer. - if (framesToProcess < buffer_->getLength()) { - int outputBusIndex = 0; - int framesToCopy = 0; + // update reading index for next render quantum + vReadIndex_ = readIndex; +} + +void AudioBufferSourceNode::processWithInterpolation( + AudioBus *processingBus, + size_t startOffset, + size_t offsetLength, + float playbackRate +) { + size_t direction = playbackRate < 0 ? -1 : 1; - while (framesToProcess - outputBusIndex > 0) { - framesToCopy = std::min( - framesToProcess - outputBusIndex, - buffer_->getLength() - bufferIndex_); + size_t writeIndex = startOffset; - processingBus->copy( - buffer_->bus_.get(), bufferIndex_, outputBusIndex, framesToCopy); + double vFrameStart = getVirtualStartFrame(); + double vFrameEnd = getVirtualEndFrame(); + double vFrameDelta = vFrameEnd - vFrameStart; - bufferIndex_ += framesToCopy; - outputBusIndex += framesToCopy; + size_t frameStart = static_cast(vFrameStart); + size_t frameEnd = static_cast(vFrameEnd); - if (bufferIndex_ < buffer_->getLength()) { - continue; - } + size_t framesLeft = offsetLength; - bufferIndex_ %= buffer_->getLength(); + while (framesLeft > 0) { + size_t readIndex = static_cast(vReadIndex_); + size_t nextReadIndex = readIndex + 1; + float factor = vReadIndex_ - readIndex; + + if (nextReadIndex >= frameEnd) { + nextReadIndex = loop_ ? frameStart : readIndex; + } + + for (int i = 0; i < processingBus->getNumberOfChannels(); i += 1) { + float* destination = processingBus->getChannel(i)->getData(); + const float* source = alignedBus_->getChannel(i)->getData(); + + destination[writeIndex] = AudioUtils::linearInterpolate( + source, + readIndex, + nextReadIndex, + factor + ); + } + + writeIndex += 1; + vReadIndex_ += playbackRate; + framesLeft -= 1; + + if (vReadIndex_ < vFrameStart || vReadIndex_ >= vFrameEnd) { + vReadIndex_ += vFrameDelta * direction; if (!loop_) { + processingBus->zero(writeIndex, framesLeft); playbackState_ = PlaybackState::FINISHED; disable(); - - if (framesToProcess - outputBusIndex > 0) { - processingBus->zero(outputBusIndex, framesToProcess - outputBusIndex); - } + break; } } - - return; } +} - // processing bus is longer than the source buffer - if (!loop_) { - // If we don't loop the buffer, copy it once and zero the remaining - // processing bus frames. - processingBus->copy(buffer_->bus_.get()); - processingBus->zero( - buffer_->getLength(), framesToProcess - buffer_->getLength()); +float AudioBufferSourceNode::getPlaybackRateValue(size_t& startOffset) { + double time = context_->getCurrentTime() + startOffset / context_->getSampleRate(); - playbackState_ = PlaybackState::FINISHED; - disable(); + return playbackRateParam_->getValueAtTime(time) + * std::pow(2.0f, detuneParam_->getValueAtTime(time) / 1200.0f); +} - return; - } +double AudioBufferSourceNode::getVirtualStartFrame() { + double inputBufferLength = alignedBus_->getSize(); + double loopStartFrame = loopStart_ * context_->getSampleRate(); - // If we loop the buffer, we need to loop the buffer framesToProcess / - // bufferSize times There might also be a remainder of frames to copy after - // the loop, which will also carry over some buffer frames to the next render - // quantum. - int processingBusPosition = 0; - int bufferSize = buffer_->getLength(); - int remainingFrames = framesToProcess - framesToProcess / bufferSize; - - // Do we have some frames left in the buffer from the previous render quantum, - // if yes copy them over and reset the buffer position. - if (bufferIndex_ > 0) { - processingBus->copy(buffer_->bus_.get(), 0, bufferIndex_); - processingBusPosition += bufferIndex_; - bufferIndex_ = 0; - } + return loop_ && loopStartFrame >= 0 && loopStart_ < loopEnd_ + ? loopStartFrame + : 0.0; +} - // Copy the entire buffer n times to the processing bus. - while (processingBusPosition + bufferSize <= framesToProcess) { - processingBus->copy(buffer_->bus_.get()); - processingBusPosition += bufferSize; - } +double AudioBufferSourceNode::getVirtualEndFrame() { + double inputBufferLength = alignedBus_->getSize(); + double loopEndFrame = loopEnd_ * context_->getSampleRate(); - // Fill in the remaining frames from the processing buffer and update buffer - // index for next render quantum. - if (remainingFrames > 0) { - processingBus->copy( - buffer_->bus_.get(), 0, processingBusPosition, remainingFrames); - bufferIndex_ = remainingFrames; - } + return loop_ && loopEndFrame > 0 && loopStart_ < loopEnd_ + ? std::min(loopEndFrame, inputBufferLength) + : inputBufferLength; } } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.h b/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.h index f89da44e..2c6f9102 100644 --- a/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/core/AudioBufferSourceNode.h @@ -35,18 +35,36 @@ class AudioBufferSourceNode : public AudioScheduledSourceNode { private: // Looping related properties bool loop_; - double loopEnd_; double loopStart_; + double loopEnd_; // playback rate aka pitch change params std::shared_ptr detuneParam_; std::shared_ptr playbackRateParam_; - // internal helpers - int bufferIndex_; + // internal helper + double vReadIndex_; // User provided buffer std::shared_ptr buffer_; + std::shared_ptr alignedBus_; + + float getPlaybackRateValue(size_t& startOffset); + + double getVirtualStartFrame(); + double getVirtualEndFrame(); + + void processWithoutInterpolation( + AudioBus *processingBus, + size_t startOffset, + size_t offsetLength, + float playbackRate); + + void processWithInterpolation( + AudioBus *processingBus, + size_t startOffset, + size_t offsetLength, + float playbackRate); }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.cpp b/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.cpp index e7d3ebab..3b6c9c02 100644 --- a/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.cpp @@ -8,18 +8,19 @@ namespace audioapi { AudioScheduledSourceNode::AudioScheduledSourceNode(BaseAudioContext *context) - : AudioNode(context), playbackState_(PlaybackState::UNSCHEDULED), startFrame_(-1), stopFrame_(-1) { + : AudioNode(context), playbackState_(PlaybackState::UNSCHEDULED), startTime_(-1.0), stopTime_(-1) { numberOfInputs_ = 0; } void AudioScheduledSourceNode::start(double time) { playbackState_ = PlaybackState::SCHEDULED; - startFrame_ = AudioUtils::timeToSampleFrame(time, context_->getSampleRate()); + startTime_ = time; + context_->getNodeManager()->addSourceNode(shared_from_this()); } void AudioScheduledSourceNode::stop(double time) { - stopFrame_ = AudioUtils::timeToSampleFrame(time, context_->getSampleRate()); + stopTime_ = time; } bool AudioScheduledSourceNode::isUnscheduled() { @@ -40,10 +41,13 @@ bool AudioScheduledSourceNode::isFinished() { } void AudioScheduledSourceNode::updatePlaybackInfo(AudioBus *processingBus, int framesToProcess, size_t& startOffset, size_t& nonSilentFramesToProcess) { + int sampleRate = context_->getSampleRate(); + size_t firstFrame = context_->getCurrentSampleFrame(); size_t lastFrame = firstFrame + framesToProcess; - size_t startFrame = std::max(startFrame_, firstFrame); - size_t stopFrame = stopFrame_; + + size_t startFrame = std::max(AudioUtils::timeToSampleFrame(startTime_, sampleRate), firstFrame); + size_t stopFrame = AudioUtils::timeToSampleFrame(stopTime_, sampleRate); if (isUnscheduled() || isFinished()) { startOffset = 0; diff --git a/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.h b/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.h index 778c8a1d..c0b1e5b7 100644 --- a/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/core/AudioScheduledSourceNode.h @@ -29,8 +29,8 @@ class AudioScheduledSourceNode : public AudioNode { void updatePlaybackInfo(AudioBus *processingBus, int framesToProcess, size_t& startOffset, size_t& nonSilentFramesToProcess); private: - size_t startFrame_; - size_t stopFrame_; + double startTime_; + double stopTime_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/utils/AudioUtils.h b/packages/react-native-audio-api/common/cpp/utils/AudioUtils.h index 578a9735..d3453fc9 100644 --- a/packages/react-native-audio-api/common/cpp/utils/AudioUtils.h +++ b/packages/react-native-audio-api/common/cpp/utils/AudioUtils.h @@ -7,4 +7,13 @@ namespace audioapi::AudioUtils { double sampleFrameToTime(int sampleFrame, int sampleRate) { return static_cast(sampleFrame) / sampleRate; } + + float linearInterpolate(const float* source, size_t firstIndex, size_t secondIndex, float factor) { + if (firstIndex == secondIndex && firstIndex >= 1) { + return source[firstIndex] + factor * (source[firstIndex] - source[firstIndex - 1]); + } + + return source[firstIndex] + factor * (source[secondIndex] - source[firstIndex]); + } + } // namespace audioapi::AudioUtils