diff --git a/runtime/elem/Types.h b/runtime/elem/Types.h index c0206bf4..eb4a5e41 100644 --- a/runtime/elem/Types.h +++ b/runtime/elem/Types.h @@ -115,6 +115,10 @@ namespace elem : _data(__data), _size(__size) {} + static BufferView subview(FloatType* __data, size_t start, size_t length) { + return BufferView(__data + start, length); + } + FloatType operator[] (size_t i) { return _data[i]; } diff --git a/runtime/elem/builtins/Sample.h b/runtime/elem/builtins/Sample.h index 7f86dc90..260434fc 100644 --- a/runtime/elem/builtins/Sample.h +++ b/runtime/elem/builtins/Sample.h @@ -5,22 +5,29 @@ #include "../Types.h" #include "./helpers/Change.h" +#include "elem/builtins/helpers/BufferReader.h" namespace elem { - template - struct VariablePitchLerpReader; - // SampleNode is a core builtin for sample playback. // // The sample file is loaded from disk or from virtual memory with a path set by the `path` property. // The sample is then triggered on the rising edge of an incoming pulse train, so // this node expects a single child node delivering that train. - template > + template struct SampleNode : public GraphNode { using GraphNode::GraphNode; + using ReaderContext = typename BufferReader::template ReadContext; + + static constexpr double FadeTime = 8.0; + + SampleNode(NodeId id, double sr, size_t blockSize) + : GraphNode::GraphNode(id, sr, blockSize) + , readers({BufferReader(sr, FadeTime), BufferReader(sr, FadeTime)}) + { + } int setProperty(std::string const& key, js::Value const& val, SharedResourceMap& resources) override { @@ -76,8 +83,8 @@ namespace elem } void reset() override { - readers[0].noteOff(); - readers[1].noteOff(); + readers[0].disengage(); + readers[1].disengage(); } void process (BlockContext const& ctx) override { @@ -93,9 +100,6 @@ namespace elem // while playing the sample will cause a discontinuity. while (bufferQueue.size() > 0) { bufferQueue.pop(activeBuffer); - - readers[0] = ReaderType(sampleRate, activeBuffer); - readers[1] = ReaderType(sampleRate, activeBuffer); } // If we don't have an input trigger or an active buffer, we can just return here @@ -119,17 +123,29 @@ namespace elem // Rising edge if (cv > FloatType(0.5)) { - readers[currentReader & 1].noteOff(); - readers[++currentReader & 1].noteOn(ostart); + readers[currentReader & 1].disengage(); + readers[++currentReader & 1].engage(0); } // If we're in trigger mode then we can ignore falling edges if (cv < FloatType(-0.5) && playbackMode != Mode::Trigger) { - readers[currentReader & 1].noteOff(); + readers[currentReader & 1].disengage(); } + auto outputChannels = std::array{&outputData[i]}; // Process both readers for the current sample - outputData[i] = readers[0].tick(ostart, ostop, rate, wantsLoop) + readers[1].tick(ostart, ostop, rate, wantsLoop); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + outputChannels.data(), + 1, + 1, + ostart, + ostop, + wantsLoop, + rate + )); + }); } } @@ -137,7 +153,7 @@ namespace elem SharedResourcePtr activeBuffer; Change change; - std::array readers; + std::array, 2> readers; size_t currentReader = 0; enum class Mode @@ -152,82 +168,4 @@ namespace elem std::atomic stopOffset = 0; }; - // A helper struct for reading from sample data with variable rate using - // linear interpolation. - template - struct VariablePitchLerpReader - { - VariablePitchLerpReader() = default; - - VariablePitchLerpReader(FloatType _sampleRate, SharedResourcePtr _sourceBuffer) - : sourceBuffer(_sourceBuffer), sampleRate(_sampleRate), gainSmoothAlpha(1.0 - std::exp(-1.0 / (0.01 * _sampleRate))) {} - - VariablePitchLerpReader(VariablePitchLerpReader& other) - : sourceBuffer(other.sourceBuffer), sampleRate(other.sampleRate), gainSmoothAlpha(1.0 - std::exp(-1.0 / (0.01 * other.sampleRate))) {} - - void noteOn(size_t const startOffset) - { - targetGain = FloatType(1); - pos = FloatType(startOffset); - } - - void noteOff() - { - targetGain = FloatType(0); - } - - FloatType tick (size_t const startOffset, size_t const stopOffset, FloatType const stepSize, bool const wantsLoop) - { - if (sourceBuffer == nullptr || pos < 0.0 || (gain == FloatType(0) && targetGain == FloatType(0))) - return FloatType(0); - - auto bufferView = sourceBuffer->getChannelData(0); - auto* sourceData = bufferView.data(); - size_t const sourceLength = bufferView.size(); - - if (pos >= (double) (sourceLength - stopOffset)) { - if (!wantsLoop) { - return FloatType(0); - } - - pos = (double) startOffset; - } - - // Linear interpolation on the buffer read - auto readLeft = static_cast(pos); - auto readRight = readLeft + 1; - auto const frac = FloatType(pos - (double) readLeft); - - if (readLeft >= sourceLength) - readLeft -= sourceLength; - - if (readRight >= sourceLength) - readRight -= sourceLength; - - auto const left = sourceData[readLeft]; - auto const right = sourceData[readRight]; - - // Now we can read the next sample out of the buffer with linear - // interpolation for sub-sample reads. - auto const out = gain * (left + frac * (right - left)); - auto const gainSettled = std::abs(targetGain - gain) <= std::numeric_limits::epsilon(); - - // Update our state - pos = pos + (double) stepSize; - gain = gainSettled ? targetGain : gain + gainSmoothAlpha * (targetGain - gain); - gain = std::clamp(gain, FloatType(0), FloatType(1)); - - // And return - return out; - } - - SharedResourcePtr sourceBuffer; - - FloatType sampleRate = 0; - FloatType gainSmoothAlpha = 0; - FloatType targetGain = 0; - FloatType gain = 0; - double pos = 0; - }; - } // namespace elem diff --git a/runtime/elem/builtins/SampleSeq.h b/runtime/elem/builtins/SampleSeq.h index 8027ecca..783b08f1 100644 --- a/runtime/elem/builtins/SampleSeq.h +++ b/runtime/elem/builtins/SampleSeq.h @@ -1,175 +1,30 @@ #pragma once +#include +#include + #include "../GraphNode.h" #include "../SingleWriterSingleReaderQueue.h" #include "../Types.h" - +#include "helpers/BufferReader.h" #include "helpers/RefCountedPool.h" -#include "../third-party/signalsmith-stretch/signalsmith-stretch.h" - -#include -#include +#include "../third-party/signalsmith-stretch/signalsmith-stretch.h" namespace elem { - namespace detail - { - template - FloatType lerp (FloatType alpha, FloatType x, FloatType y) { - return x + alpha * (y - x); - } - - template - FloatType fpEqual (FloatType x, FloatType y) { - return std::abs(x - y) <= FloatType(1e-6); - } - - template - struct GainFade { - GainFade() = default; - - void setTargetGain (FloatType g) { - targetGain = g; - - if (targetGain < currentGain) { - step = FloatType(-1) * std::abs(step); - } else { - step = std::abs(step); - } - } - - FloatType operator() (FloatType x) { - if (currentGain == targetGain) - return (currentGain * x); - - auto y = x * currentGain; - currentGain = std::clamp(currentGain + step, FloatType(0), FloatType(1)); - - return y; - } - - bool on() { - return fpEqual(targetGain, FloatType(1)); - } - - bool silent() { - return fpEqual(targetGain, FloatType(0)) && fpEqual(currentGain, FloatType(0)); - } - - void reset() { - currentGain = FloatType(0); - targetGain = FloatType(0); - } - - FloatType currentGain = 0; - FloatType targetGain = 0; - FloatType step = 0.02; // TODO - }; - - template - struct BufferReader { - BufferReader() = default; - - void engage (double start, double currentTime, FloatType* _buffer, size_t _size) { - startTime = start; - buffer = _buffer; - bufferSize = _size; - fade.setTargetGain(FloatType(1)); - - position = static_cast(((currentTime - startTime) / sampleDuration) * (double) (bufferSize - 1u)); - position = std::clamp(position, 0, bufferSize); - } - - void disengage() { - fade.setTargetGain(FloatType(0)); - } - - // Does the incoming time match what this reader is expecting? - // - // If we're not engaged, we don't have any expectations so we just say sure. - // If we are engaged, we try to map the incoming time onto a position in the - // buffer and see if that's far off from where we currently are. - bool isAlignedWithTime(double t) { - if (!fade.on()) - return true; - - size_t newPos = static_cast(((t - startTime) / sampleDuration) * (double) (bufferSize - 1u)); - int delta = static_cast(position) - static_cast(newPos); - bool aligned = std::abs(delta) < 16; - - return aligned; - } - - template - void readAdding(DestType* outputData, size_t numSamples) { - for (size_t i = 0; (i < numSamples) && (position < bufferSize); ++i) { - outputData[i] += static_cast(fade(buffer[position++])); - } - } - - FloatType read (FloatType const* buffer, size_t size, double t) - { - if (fade.silent() || sampleDuration <= FloatType(0)) - return FloatType(0); - - // An allocated but inactive reader is currently fading out at the point in time - // from which we jumped to allocate a new reader - double const pos = fade.on() - ? (t - startTime) / sampleDuration - : (stepStopTime() - startTime) / sampleDuration; - - // While we're still active, track last position so that we can stop effectively - if (fade.on()) { - dt = t - lastTimeStep; - lastTimeStep = t; - } - - // Deallocate if we've run out of bounds - if (pos < 0.0 || pos >= 1.0) { - disengage(); - return FloatType(0); - } - - // Instead of clamping here, we could accept loop points in the sample and - // mod the playback position within those loop points. Property loop: [start, stop] - auto l = static_cast(pos * (double) (size - 1u)); - auto r = std::min(size, l + 1u); - auto const alpha = FloatType((pos * (double) (size - 1u)) - static_cast(l)); - - return fade(lerp(alpha, buffer[l], buffer[r])); - } - - FloatType stepStopTime() { - lastTimeStep += dt; - return lastTimeStep; - } - - void reset (double sampleDur) { - fade.reset(); - - sampleDuration = sampleDur; - startTime = 0.0; - dt = 0.0; - } - - GainFade fade; - FloatType* buffer = nullptr; - size_t bufferSize = 0; - size_t position = 0; - - double sampleDuration = 0; - double startTime = 0; - double lastTimeStep = 0; - double dt = 0; - }; - } - template struct SampleSeqNode : public GraphNode { + using ReaderContext = typename BufferReader::template ReadContext; + + // Note: this is set to 1.1 ms to preserve backwards compatibility. Historically, a gain fade with + // a step size of 0.02 was used for SampleSeqNode, which comes out to roughly 1.1 ms at 44100 Hz. + static constexpr double FadeTime = 1.1; + SampleSeqNode(NodeId id, FloatType const sr, int const blockSize) : GraphNode::GraphNode(id, sr, blockSize) + , readers({BufferReader(sr, FadeTime), BufferReader(sr, FadeTime)}) { if constexpr (WithStretch) { stretch.presetDefault(1, sr); @@ -273,9 +128,8 @@ namespace elem // Here a value of 1.0 is considered an onset, and anything else // considered an offset. - if (detail::fpEqual(prevEvent->second, FloatType(1.0))) { - auto const bufferView = activeBuffer->getChannelData(0); - readers[activeReader].engage(prevEvent->first, t, const_cast(bufferView.data()), bufferView.size()); + if (fpEqual(prevEvent->second, FloatType(1.0))) { + readers[activeReader].engage(0); } } } @@ -290,8 +144,8 @@ namespace elem auto const sampleDur = sampleDuration.load(); if (sampleDur != rtSampleDuration) { - readers[0].reset(sampleDur); - readers[1].reset(sampleDur); + readers[0].reset(); + readers[1].reset(); rtSampleDuration = sampleDur; } @@ -299,8 +153,8 @@ namespace elem while (bufferQueue.size() > 0) { bufferQueue.pop(activeBuffer); - readers[0].reset(sampleDur); - readers[1].reset(sampleDur); + readers[0].reset(); + readers[1].reset(); } // Pull newest seq from queue @@ -338,10 +192,15 @@ namespace elem || (prevEvent != seqEnd && before(t, prevEvent->first)) || (nextEvent != seqEnd && after(t, nextEvent->first)); + double const timeUnitsPerSample = sampleDur / (double) activeBuffer->numSamples(); + int64_t const sampleTime = t / timeUnitsPerSample; + bool const significantTimeChange = std::abs(sampleTime - nextExpectedBlockStart) > 16; + nextExpectedBlockStart = sampleTime + numSamples; + // TODO: if the input time has changed significantly, need to address the input latency of // the phase vocoder by resetting it and then pushing stretch.inputLatency * stretchFactor // samples ahead of `timeInSamples(t)` - if (shouldUpdateBounds || !readers[activeReader].isAlignedWithTime(t)) { + if (shouldUpdateBounds || significantTimeChange) { updateEventBoundaries(t); } @@ -365,16 +224,28 @@ namespace elem // Clear and read std::fill_n(scratchData, numSourceSamples, FloatType(0)); - readers[0].readAdding(scratchData, numSourceSamples); - readers[1].readAdding(scratchData, numSourceSamples); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + &scratchData, + 1, + numSourceSamples + )); + }); stretch.process(&scratchData, numSourceSamples, &outputData, numSamples); } else { // Clear and read std::fill_n(outputData, numSamples, FloatType(0)); - readers[0].readAdding(outputData, numSamples); - readers[1].readAdding(outputData, numSamples); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + &outputData, + 1, + numSamples + )); + }); } } @@ -390,7 +261,7 @@ namespace elem SingleWriterSingleReaderQueue bufferQueue; SharedResourcePtr activeBuffer; - std::array, 2> readers; + std::array, 2> readers; size_t activeReader = 0; int64_t nextExpectedBlockStart = 0; diff --git a/runtime/elem/builtins/helpers/BufferReader.h b/runtime/elem/builtins/helpers/BufferReader.h new file mode 100644 index 00000000..fe30978b --- /dev/null +++ b/runtime/elem/builtins/helpers/BufferReader.h @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include +#include + +#include "GainFade.h" +#include "elem/SharedResource.h" + +namespace elem +{ + template + class BufferReader { + public: + BufferReader(double sampleRate, double fadeTime) + : fade(sampleRate, fadeTime, fadeTime) + {} + + void engage (double _position) { + fade.fadeIn(); + position = _position; + } + + void disengage() { + fade.fadeOut(); + } + + template + struct ReadContext { + ReadContext( + SharedResource* source, + DestType** outputData, + size_t numChannels, + size_t numSamples, + std::optional startOffsetSamples = std::nullopt, + std::optional stopOffsetSamples = std::nullopt, + bool shouldLoop = false, + double playbackRate = 1.0, + size_t writeOffset = 0 + ) + : source(source) + , outputData(outputData) + , numChannels(numChannels) + , numSamples(numSamples) + , startOffsetSamples(startOffsetSamples) + , stopOffsetSamples(stopOffsetSamples) + , shouldLoop(shouldLoop) + , playbackRate(playbackRate) + , writeOffset(writeOffset) + {} + + SharedResource* source; + DestType** outputData; + size_t numChannels; + size_t numSamples; + std::optional startOffsetSamples; + std::optional stopOffsetSamples; + bool shouldLoop; + double playbackRate; + size_t writeOffset; + }; + + template + void readAdding(ReadContext const& ctx) { + if (ctx.source == nullptr || fade.fadedOut()) { + return; + } + + auto const numChannels = std::min(ctx.numChannels, ctx.source->numChannels()); + auto const bufferSize = ctx.source->numSamples(); + if (numChannels == 0 || bufferSize == 0) { + return; + } + + auto const _startOffset = ctx.startOffsetSamples.value_or(0); + auto const _stopOffset = ctx.stopOffsetSamples.value_or(0); + auto const startOffset = _startOffset >= 0 ? + std::min(_startOffset, static_cast(bufferSize)) : 0; + auto const stopOffset = _stopOffset >= 0 ? + std::min(_stopOffset, static_cast(bufferSize)) : 0; + auto const sampleLength = bufferSize - startOffset - stopOffset; + + elem::GainFade localFade(fade); + double pos = position; + + for (size_t j = 0; j < numChannels; ++j) { + pos = position; + localFade = fade; + + // Here we take a subview of the buffer that ignores samples before the start offset and after the stop offset. + // This view then gets passed into lerpRead() below. This means we can treat a pos of 0 as `startOffset` and a + // pos of 1 as `startOffset + sampleLength`. + auto bufferView = BufferView::subview(ctx.source->getChannelData(j).data(), + startOffset, sampleLength); + for (size_t i = 0; i < ctx.numSamples; ++i) { + if (pos >= 1.0) { + if (!ctx.shouldLoop) { + break; + } + // Restart the loop. Note there is no crossfade happening yet, + // so loops may be discontinuous. + pos = pos - 1.0; + } + + auto const out = static_cast(localFade(lerpRead(bufferView, pos))); + ctx.outputData[j][i + ctx.writeOffset] += out; + + pos += (ctx.playbackRate / static_cast(sampleLength)); + } + } + + // Update the fade member to have the latest state + fade = localFade; + position = pos; + } + + // Linearly interpolates between the two samples adjacent to the given position. + // @param pos must be a normalized value between 0 and 1. + static FloatType lerpRead(BufferView const& view, double pos) + { + assert(pos >= 0.0 && pos <= 1.0); + + auto* data = view.data(); + auto size = view.size(); + + auto const realPos = pos * view.size(); + auto left = static_cast(realPos); + auto right = std::min(left + 1, size - 1); + auto alpha = realPos - (double) left; + + if (left >= size) + return FloatType(0); + + if (right >= size) + return data[left]; + + return lerp(static_cast(alpha), data[left], data[right]); + } + + void reset () { + fade.reset(); + } + + private: + elem::GainFade fade; + + double position = 0; + }; +} // namespace elem diff --git a/runtime/elem/builtins/helpers/GainFade.h b/runtime/elem/builtins/helpers/GainFade.h index c79efe98..a9057a51 100644 --- a/runtime/elem/builtins/helpers/GainFade.h +++ b/runtime/elem/builtins/helpers/GainFade.h @@ -99,6 +99,10 @@ namespace elem return (targetGain.load() > FloatType(0.5)); } + bool fadedOut() const { + return targetGain.load() == FloatType(0) && currentGain.load() == FloatType(0); + } + bool settled() { return fpEqual(targetGain.load(), currentGain.load()); } diff --git a/runtime/elem/builtins/mc/Sample.h b/runtime/elem/builtins/mc/Sample.h index ae7b1c51..557e7303 100644 --- a/runtime/elem/builtins/mc/Sample.h +++ b/runtime/elem/builtins/mc/Sample.h @@ -5,8 +5,8 @@ #include "../../Types.h" #include "../helpers/Change.h" -#include "../helpers/GainFade.h" -#include "../helpers/FloatUtils.h" +#include "elem/builtins/helpers/BufferReader.h" +#include namespace elem @@ -23,6 +23,15 @@ namespace elem template struct MCSampleNode : public GraphNode { using GraphNode::GraphNode; + using ReaderContext = typename BufferReader::template ReadContext; + + static constexpr double FadeTime = 4.0; + + MCSampleNode(NodeId id, double sr, size_t blockSize) + : GraphNode::GraphNode(id, sr, blockSize) + , readers({BufferReader(sr, FadeTime), BufferReader(sr, FadeTime)}) + { + } int setProperty(std::string const& key, js::Value const& val, SharedResourceMap& resources) override { @@ -85,8 +94,8 @@ namespace elem } void reset() override { - readers[0].noteOff(); - readers[1].noteOff(); + readers[0].disengage(); + readers[1].disengage(); } void process (BlockContext const& ctx) override { @@ -96,16 +105,14 @@ namespace elem auto numOuts = ctx.numOutputChannels; auto numSamples = ctx.numSamples; - auto const sampleRate = GraphNode::getSampleRate(); - // First order of business: grab the most recent sample buffer to use if // there's anything in the queue. This behavior means that changing the buffer // while playing the sample will cause a discontinuity. while (bufferQueue.size() > 0) { bufferQueue.pop(activeBuffer); - readers[0] = MCVariablePitchReader(sampleRate, activeBuffer); - readers[1] = MCVariablePitchReader(sampleRate, activeBuffer); + readers[0].reset(); + readers[1].reset(); } // First we clear the output buffers @@ -136,12 +143,23 @@ namespace elem if (cv > FloatType(0.5)) { // Read from [i, j] - readers[0].sumInto(outputData, numOuts, i, j - i, rate); - readers[1].sumInto(outputData, numOuts, i, j - i, rate); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + outputData, + numOuts, + j - i, + ostart, + ostop, + wantsLoop, + rate, + i + )); + }); // Update voice state - readers[currentReader & 1].noteOff(); - readers[++currentReader & 1].noteOn(ostart, ostop, wantsLoop); + readers[currentReader & 1].disengage(); + readers[++currentReader & 1].engage(0); // Update counters i = j; @@ -150,11 +168,22 @@ namespace elem // If we're in trigger mode then we can ignore falling edges if (cv < FloatType(-0.5) && playbackMode != Mode::Trigger) { // Read from [i, j] - readers[0].sumInto(outputData, numOuts, i, j - i, rate); - readers[1].sumInto(outputData, numOuts, i, j - i, rate); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + outputData, + numOuts, + j - i, + ostart, + ostop, + wantsLoop, + rate, + i + )); + }); // Update voice state - readers[currentReader & 1].noteOff(); + readers[currentReader & 1].disengage(); // Break so we can update our sample counters // Update counters @@ -162,15 +191,26 @@ namespace elem } } - readers[0].sumInto(outputData, numOuts, i, j - i, rate); - readers[1].sumInto(outputData, numOuts, i, j - i, rate); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + outputData, + numOuts, + j - i, + ostart, + ostop, + wantsLoop, + rate, + i + )); + }); } SingleWriterSingleReaderQueue bufferQueue; SharedResourcePtr activeBuffer; Change change; - std::array, 2> readers; + std::array, 2> readers; size_t currentReader = 0; enum class Mode @@ -186,106 +226,4 @@ namespace elem std::atomic playbackRate = 1.0; }; - // A helper struct for reading from sample data with variable rate using - // linear interpolation. - template - struct MCVariablePitchReader - { - MCVariablePitchReader() - : sourceBuffer(nullptr), gainFade(44100.0, 4.0, 4.0) - {} - - MCVariablePitchReader(FloatType _sampleRate, SharedResourcePtr _sourceBuffer) - : sourceBuffer(_sourceBuffer), gainFade(_sampleRate, 4.0, 4.0) - {} - - MCVariablePitchReader(MCVariablePitchReader& other) - : sourceBuffer(other.sourceBuffer), gainFade(other.gainFade) - {} - - void noteOn(size_t _startOffset, size_t _stopOffset, bool wantsLoop) - { - gainFade.fadeIn(); - - startOffset = (double) _startOffset; - stopOffset = (double) _stopOffset; - shouldLoop = wantsLoop; - pos = startOffset; - } - - void noteOff() - { - gainFade.fadeOut(); - } - - FloatType lerpRead(BufferView const& view, double pos) - { - auto* data = view.data(); - auto size = view.size(); - - auto left = static_cast(pos); - auto right = left + 1; - auto alpha = pos - (double) left; - - if (left >= size) - return FloatType(0); - - if (right >= size) - return data[left]; - - return lerp(static_cast(alpha), data[left], data[right]); - } - - void sumInto(FloatType** outputData, size_t numOuts, size_t writeOffset, size_t numSamples, double playbackRate) - { - elem::GainFade localFade(gainFade); - - double readStart = startOffset; - double readStop = 0; - - for (size_t i = 0; i < std::min(numOuts, sourceBuffer->numChannels()); ++i) { - auto bufferView = sourceBuffer->getChannelData(i); - size_t const sourceLength = bufferView.size(); - - readStop = static_cast(sourceLength) - stopOffset; - - // Reinitialize the local copy to match our member instance - localFade = gainFade; - - for (size_t j = 0; j < numSamples; ++j) { - double readPos = pos + static_cast(j) * playbackRate; - - if (readPos >= readStop) { - if (shouldLoop) { - readPos = readStart + std::fmod(readPos - readStart, readStop - readStart); - } else { - continue; - } - } - - outputData[i][writeOffset + j] += localFade(lerpRead(bufferView, readPos)); - } - } - - // Here we have a localFade instance that has finished running over a block, which - // represents where our class instance should now be - gainFade = localFade; - - // And update our position - pos += static_cast(numSamples) * playbackRate; - - if (pos >= readStop && shouldLoop) { - pos = readStart + std::fmod(pos - readStart, readStop - readStart); - } - } - - SharedResourcePtr sourceBuffer; - - GainFade gainFade; - bool shouldLoop = false; - double stopOffset = 0; - double startOffset = 0; - double pos = 0; - }; - } // namespace elem diff --git a/runtime/elem/builtins/mc/SampleSeq.h b/runtime/elem/builtins/mc/SampleSeq.h index c4e151b8..245146c9 100644 --- a/runtime/elem/builtins/mc/SampleSeq.h +++ b/runtime/elem/builtins/mc/SampleSeq.h @@ -1,97 +1,24 @@ #pragma once -#include "../helpers/FloatUtils.h" -#include "../helpers/GainFade.h" - +#include "elem/GraphNode.h" +#include "elem/SingleWriterSingleReaderQueue.h" +#include "elem/Types.h" +#include "elem/builtins/helpers/BufferReader.h" +#include "elem/builtins/helpers/RefCountedPool.h" +#include "elem/third-party/signalsmith-stretch/signalsmith-stretch.h" namespace elem { - namespace detail - { - - template - struct MCBufferReader { - MCBufferReader(double sampleRate, double fadeTime) - : fade(sampleRate, fadeTime, fadeTime) - { - } - - void engage (double start, double currentTime, size_t _bufferSize) { - startTime = start; - bufferSize = _bufferSize; - fade.fadeIn(); - - position = static_cast(((currentTime - startTime) / sampleDuration) * (double) (bufferSize - 1u)); - position = std::clamp(position, 0, bufferSize); - } - - void disengage() { - fade.fadeOut(); - } - - // Does the incoming time match what this reader is expecting? - // - // If we're not engaged, we don't have any expectations so we just say sure. - // If we are engaged, we try to map the incoming time onto a position in the - // buffer and see if that's far off from where we currently are. - bool isAlignedWithTime(double t) { - if (!fade.on()) - return true; - - size_t newPos = static_cast(((t - startTime) / sampleDuration) * (double) (bufferSize - 1u)); - int delta = static_cast(position) - static_cast(newPos); - bool aligned = std::abs(delta) < 16; - - return aligned; - } - - template - void readAdding(SharedResource* resource, DestType** outputData, size_t numChannels, size_t numSamples) { - elem::GainFade localFade(fade); - - for (size_t j = 0; j < std::min(numChannels, resource->numChannels()); ++j) { - auto bufferView = resource->getChannelData(j); - auto bufferSize = bufferView.size(); - auto* sourceData = bufferView.data(); - - // Reinitialize the local copy to match our member instance - localFade = fade; - - for (size_t i = 0; (i < numSamples) && ((position + i) < bufferSize); ++i) { - outputData[j][i] += static_cast(localFade(sourceData[position + i])); - } - } - - // Here we have a localFade instance that has finished running over a block, which - // represents where our class instance should now be - fade = localFade; - - // And update our position - position += numSamples; - } - - void reset (double sampleDur) { - fade.reset(); - - sampleDuration = sampleDur; - startTime = 0.0; - } - - elem::GainFade fade; - size_t bufferSize = 0; - size_t position = 0; - - double sampleDuration = 0; - double startTime = 0; - }; - } - template struct StereoSampleSeqNode : public GraphNode { + using ReaderContext = typename BufferReader::template ReadContext; + + static constexpr double FadeTime = 8.0; + StereoSampleSeqNode(NodeId id, FloatType const sr, int const blockSize) : GraphNode::GraphNode(id, sr, blockSize) - , readers({detail::MCBufferReader(sr, 8.0), detail::MCBufferReader(sr, 8.0)}) + , readers({BufferReader(sr, FadeTime), BufferReader(sr, FadeTime)}) { if constexpr (WithStretch) { stretch.presetDefault(2, sr); @@ -199,8 +126,8 @@ namespace elem // Here a value of 1.0 is considered an onset, and anything else // considered an offset. - if (detail::fpEqual(prevEvent->second, FloatType(1.0))) { - readers[activeReader].engage(prevEvent->first, t, activeBuffer->numSamples()); + if (fpEqual(prevEvent->second, FloatType(1.0))) { + readers[activeReader].engage(0); } } } @@ -214,8 +141,8 @@ namespace elem auto const sampleDur = sampleDuration.load(); if (sampleDur != rtSampleDuration) { - readers[0].reset(sampleDur); - readers[1].reset(sampleDur); + readers[0].reset(); + readers[1].reset(); rtSampleDuration = sampleDur; } @@ -223,8 +150,8 @@ namespace elem while (bufferQueue.size() > 0) { bufferQueue.pop(activeBuffer); - readers[0].reset(sampleDur); - readers[1].reset(sampleDur); + readers[0].reset(); + readers[1].reset(); } // Pull newest seq from queue @@ -304,8 +231,14 @@ namespace elem std::array ptrs {{scratchData, scratchData + (numSamples * 4)}}; auto** scratchPtrs = ptrs.data(); - readers[0].readAdding(activeBuffer.get(), scratchPtrs, ctx.numOutputChannels, numSourceSamples); - readers[1].readAdding(activeBuffer.get(), scratchPtrs, ctx.numOutputChannels, numSourceSamples); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + scratchPtrs, + ctx.numOutputChannels, + numSourceSamples + )); + }); stretch.process(scratchPtrs, static_cast(numSourceSamples), outputData, static_cast(numSamples)); } else { @@ -314,8 +247,14 @@ namespace elem std::fill_n(outputData[i], numSamples, FloatType(0)); } - readers[0].readAdding(activeBuffer.get(), outputData, ctx.numOutputChannels, numSamples); - readers[1].readAdding(activeBuffer.get(), outputData, ctx.numOutputChannels, numSamples); + std::for_each(readers.begin(), readers.end(), [&](auto& reader) { + reader.readAdding(ReaderContext( + activeBuffer.get(), + outputData, + ctx.numOutputChannels, + numSamples + )); + }); } } @@ -331,7 +270,7 @@ namespace elem SingleWriterSingleReaderQueue bufferQueue; SharedResourcePtr activeBuffer; - std::array, 2> readers; + std::array, 2> readers; size_t activeReader = 0; int64_t nextExpectedBlockStart = 0;