Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions runtime/elem/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ namespace elem
: _data(__data), _size(__size)
{}

static BufferView<FloatType> subview(FloatType* __data, size_t start, size_t length) {
return BufferView<FloatType>(__data + start, length);
}

FloatType operator[] (size_t i) {
return _data[i];
}
Expand Down
122 changes: 30 additions & 92 deletions runtime/elem/builtins/Sample.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
#include "../Types.h"

#include "./helpers/Change.h"
#include "elem/builtins/helpers/BufferReader.h"


namespace elem
{

template <typename FloatType>
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 <typename FloatType, typename ReaderType = VariablePitchLerpReader<float>>
template <typename FloatType>
struct SampleNode : public GraphNode<FloatType> {
using GraphNode<FloatType>::GraphNode;
using ReaderContext = typename BufferReader<FloatType>::template ReadContext<FloatType>;

static constexpr double FadeTime = 8.0;

SampleNode(NodeId id, double sr, size_t blockSize)
: GraphNode<FloatType>::GraphNode(id, sr, blockSize)
, readers({BufferReader<FloatType>(sr, FadeTime), BufferReader<FloatType>(sr, FadeTime)})
{
}

int setProperty(std::string const& key, js::Value const& val, SharedResourceMap& resources) override
{
Expand Down Expand Up @@ -76,8 +83,8 @@ namespace elem
}

void reset() override {
readers[0].noteOff();
readers[1].noteOff();
readers[0].disengage();
readers[1].disengage();
}

void process (BlockContext<FloatType> const& ctx) override {
Expand All @@ -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
Expand All @@ -119,25 +123,37 @@ 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
));
});
}
}

SingleWriterSingleReaderQueue<SharedResourcePtr> bufferQueue;
SharedResourcePtr activeBuffer;

Change<FloatType> change;
std::array<ReaderType, 2> readers;
std::array<BufferReader<FloatType>, 2> readers;
size_t currentReader = 0;

enum class Mode
Expand All @@ -152,82 +168,4 @@ namespace elem
std::atomic<size_t> stopOffset = 0;
};

// A helper struct for reading from sample data with variable rate using
// linear interpolation.
template <typename FloatType>
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<size_t>(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<FloatType>::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
Loading