justasample/Source/Sampler/Stretcher.h

161 lines
6.3 KiB
C
Raw Normal View History

2026-07-23 23:40:48 +02:00
/*
==============================================================================
Stretcher.h
Created: 27 May 2024 4:09:49pm
Author: binya
==============================================================================
*/
#pragma once
#include <Bungee.h>
// Bungee sets a hard limit on the pitch ratio to simplify memory management. We can increase this limit before building
// and use a resampling hack when necessary (the hack is not great because it requires reallocation of the stretcher).
// This must be set to the value in Timing.cpp (internal to Bungee)
static constexpr int bungeeMaxPitchOctaves = BUNGEE_MAX_OCTAVES;
static constexpr float bungeeMinimumRatio = 1.f / (1 << bungeeMaxPitchOctaves);
class BungeeStretcher
{
public:
explicit BungeeStretcher(const juce::AudioBuffer<float>& sampleBuffer, int sampleRate) : buffer(&sampleBuffer),
bufferSampleRate(sampleRate)
{
}
/** Allocates a new stretcher and the input buffer. */
void preallocateStretcher(int appSampleRate)
{
if (appSampleRate == 0 || appSampleRate == applicationSampleRate)
return;
bungee = std::make_unique<Bungee::Stretcher<Bungee::Basic>>(Bungee::SampleRates{ bufferSampleRate, appSampleRate }, buffer->getNumChannels());
inputData.setSize(1, buffer->getNumChannels() * bungee->maxInputFrameCount(), false, false, true); // Note, maxInputFrameCount has a reported overflow issue
previousInputRate = bufferSampleRate;
applicationSampleRate = appSampleRate;
}
void initialize(long double sampleStart, float initialRatio = 1, float initialSpeed = 1)
{
setPitchAndSpeed(initialRatio, initialSpeed);
// We only reallocate when necessary (if resamplingHack requires it, as it's not good for real-time performance)
int inputRate = int(bufferSampleRate / resamplingHack);
if (inputRate != previousInputRate)
{
bungee = std::make_unique<Bungee::Stretcher<Bungee::Basic>>(Bungee::SampleRates{ inputRate, applicationSampleRate }, buffer->getNumChannels());
inputData.setSize(1, buffer->getNumChannels() * bungee->maxInputFrameCount(), false, false, true); // maxInputFrameCount has a reported overflow issue
previousInputRate = inputRate;
}
output = Bungee::OutputChunk{};
outputIndex = 0;
preroll(double(sampleStart));
}
/** Pre-rolls the stretcher to a new position. Use this before you plan to move the position. */
void preroll(double newPosition)
{
request = Bungee::Request{ newPosition, speedFactor * resamplingHack, pitchRatio, true };
bungee->preroll(request);
while (!output.data || std::isnan(output.request[Bungee::OutputChunk::begin]->position) ||
newPosition > output.request[Bungee::OutputChunk::end]->position || newPosition < output.request[Bungee::OutputChunk::begin]->position)
{
auto input = bungee->specifyGrain(request);
if (buffer->getNumChannels() * (input.end - input.begin) >= inputData.getNumSamples()) // Can happen with extreme ratios
inputData.setSize(1, buffer->getNumChannels() * (input.end - input.begin));
int begin = juce::jlimit<int>(int(std::ceil(newPosition)), buffer->getNumSamples(), input.begin);
int end = juce::jlimit<int>(int(std::ceil(newPosition)), buffer->getNumSamples(), input.end);
inputData.clear();
if (begin < end)
{
for (int ch = 0; ch < buffer->getNumChannels(); ch++)
inputData.copyFrom(0, ch * (input.end - input.begin) + begin - input.begin, *buffer, ch, begin, end - begin);
}
bungee->analyseGrain(inputData.getReadPointer(0), (input.end - input.begin));
bungee->synthesiseGrain(output);
bungee->next(request);
outputIndex = int(std::round((newPosition - output.request[Bungee::OutputChunk::begin]->position) / positionSpeed));
}
}
/** Fetches the next sample. Call this for each channel before advancing. */
float nextSample(int channel, bool advance = true)
{
if (outputIndex >= output.frameCount)
{
request.pitch = pitchRatio;
request.speed = speedFactor * resamplingHack;
auto [begin, end] = bungee->specifyGrain(request);
begin = juce::jlimit<int>(0, buffer->getNumSamples(), begin);
end = juce::jlimit<int>(0, buffer->getNumSamples(), end);
inputData.clear(); // Preferring simplicity over maximum efficiency
for (int ch = 0; ch < buffer->getNumChannels(); ch++)
inputData.copyFrom(0, ch * (end - begin), *buffer, ch, begin, end - begin);
bungee->analyseGrain(inputData.getReadPointer(0), end - begin);
bungee->synthesiseGrain(output);
bungee->next(request);
outputIndex = 0;
}
float result = output.data[channel * output.channelStride + outputIndex];
if (advance)
outputIndex++;
return result;
}
void setPitchAndSpeed(float newPitchRatio, float newSpeedFactor)
{
pitchRatio = newPitchRatio;
speedFactor = newSpeedFactor;
if (newPitchRatio < bungeeMinimumRatio)
{
pitchRatio = bungeeMinimumRatio;
resamplingHack = bungeeMinimumRatio / newPitchRatio;
}
else
{
resamplingHack = 1.f;
}
positionSpeed = speedFactor * bufferSampleRate / applicationSampleRate;
}
/** Returns the speed at which the stretcher advances through the buffer, relative to the buffer's sample rate. */
float getPositionSpeed() const { return positionSpeed; }
private:
const juce::AudioBuffer<float>* buffer{ nullptr };
int bufferSampleRate{ 0 };
int applicationSampleRate{ 0 };
float pitchRatio{ 1. };
float speedFactor{ 1. };
float positionSpeed{ 0. }; // speedFactor * bufferSampleRate / applicationSampleRate
// We use a little hack to ignore Bungee's maxPitchOctaves limit
float resamplingHack{ 1.f };
int previousInputRate{ 0 };
std::unique_ptr<Bungee::Stretcher<Bungee::Basic>> bungee;
Bungee::Request request{};
Bungee::OutputChunk output{};
juce::AudioBuffer<float> inputData;
int outputIndex{ 0 };
};