This commit is contained in:
Armin 2026-07-23 23:40:48 +02:00
commit d21bc831e1
178 changed files with 24136 additions and 0 deletions

View file

@ -0,0 +1,635 @@
/*
==============================================================================
CustomSamplerVoice.cpp
Created: 5 Sep 2023 3:35:03pm
Author: binya
==============================================================================
*/
#include "CustomSamplerVoice.h"
#include "../Utilities/BufferUtils.h"
#include "Effects/BandEQ.h"
#include "Effects/Chorus.h"
#include "Effects/Distortion.h"
#include "Effects/Reverb.h"
CustomSamplerVoice::CustomSamplerVoice(const SamplerParameters& samplerSound, MTSClient* client, double applicationSampleRate, int expectedBlockSize, bool initSample) :
expectedBlockSize(expectedBlockSize), sampleSound(samplerSound),
mainStretcher(samplerSound.sample, samplerSound.sampleRate),
loopStretcher(samplerSound.sample, samplerSound.sampleRate),
endStretcher(samplerSound.sample, samplerSound.sampleRate),
mtsClient(client)
{
SynthesiserVoice::setCurrentPlaybackSampleRate(applicationSampleRate);
if (expectedBlockSize <= 0)
this->expectedBlockSize = 512; // In case a DAW reports this incorrectly at the time of prepareToPlay
if (initSample)
initializeSample();
}
void CustomSamplerVoice::initializeSample()
{
if (sampleSound.sample.getNumChannels() <= 0)
return;
mainStretcher = BungeeStretcher(sampleSound.sample, sampleSound.sampleRate);
loopStretcher = BungeeStretcher(sampleSound.sample, sampleSound.sampleRate);
endStretcher = BungeeStretcher(sampleSound.sample, sampleSound.sampleRate);
const int sampleRate = int(getSampleRate());
if (sampleRate > 0)
{
mainStretcher.preallocateStretcher(sampleRate);
loopStretcher.preallocateStretcher(sampleRate);
endStretcher.preallocateStretcher(sampleRate);
}
tempOutputBuffer.setSize(sampleSound.sample.getNumChannels(), expectedBlockSize * 2);
envelopeBuffer.setSize(sampleSound.sample.getNumChannels(), expectedBlockSize * 2);
tailOffBuffer.setSize(2, TAIL_OFF, false, true);
mainStretcherBuffer.setSize(sampleSound.sample.getNumChannels(), expectedBlockSize * 2);
loopStretcherBuffer.setSize(sampleSound.sample.getNumChannels() - 1, expectedBlockSize * 2);
endStretcherBuffer.setSize(sampleSound.sample.getNumChannels() - 1, expectedBlockSize * 2);
mainLowpass.clear();
loopLowpass.clear();
endLowpass.clear();
for (int i = 0; i < sampleSound.sample.getNumChannels(); i++)
{
mainLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
loopLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
endLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
}
}
void CustomSamplerVoice::startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition)
{
if (midiNoteNumber < sampleSound.midiStart->get() || midiNoteNumber > sampleSound.midiEnd->get() || MTS_ShouldFilterNote(mtsClient, char(midiNoteNumber), -1))
return;
if (!sampleSound.disableVelocity->get())
noteVelocity = velocity;
else
noteVelocity = 1.f;
if (sound)
{
sampleRateConversion = float(sampleSound.sampleRate / getSampleRate());
sampleStart = sampleSound.sampleStart; // Note this implicitly loads the atomic value
sampleEnd = sampleSound.sampleEnd;
wavetableMode = isWavetableModeAvailable(float(sampleSound.sampleRate), sampleStart, sampleEnd) && !sampleSound.disableWavetableMode->get();
playbackMode = wavetableMode ? PluginParameters::BASIC : sampleSound.getPlaybackMode();
playUntilEnd = sampleSound.playUntilEnd->get();
isLooping = sampleSound.isLooping->get() || wavetableMode;
loopingHasStart = isLooping && sampleSound.loopingHasStart->get() && sampleSound.loopStart < sampleSound.sampleStart && !wavetableMode;
loopStart = sampleSound.loopStart;
loopingHasEnd = isLooping && sampleSound.loopingHasEnd->get() && sampleSound.loopEnd > sampleSound.sampleEnd && !wavetableMode;
loopEnd = sampleSound.loopEnd;
effectiveStart = loopingHasStart ? loopStart : sampleStart;
effectiveEnd = loopingHasEnd ? loopEnd : sampleEnd;
// While release can be applied before or after the FX, a minimum number of attack smoothing always needs to be applied to the sample before FX
attackSmoothing = sampleSound.attack->get() * float(getSampleRate()) / 1000.f;
releaseSmoothing = sampleSound.release->get() * float(getSampleRate()) / 1000.f;
attackShape = sampleSound.attackShape->get();
releaseShape = sampleSound.releaseShape->get();
if (loopingHasEnd) // Keep release smoothing within end portion
releaseSmoothing = juce::jmin<float>(releaseSmoothing, float(loopEnd - sampleEnd));
crossfade = juce::jmin<float>(float(sampleSound.crossfadeSamples->get()), (sampleEnd - sampleStart + 1) / 2.f + 1);
vc = VoiceContext();
midiReleased = false;
doLowpass = false;
vc.currentPosition = effectiveStart;
updateSpeedAndPitch(midiNoteNumber, currentPitchWheelPosition);
if (playbackMode == PluginParameters::BUNGEE)
mainStretcher.initialize(effectiveStart, tuning, speedFactor);
effects.clear();
initializeFx();
for (auto& effect : effects)
{
effect.fx->initialize(sampleSound.sample.getNumChannels(), int(getSampleRate()));
effect.fx->updateParams(sampleSound);
}
updateFXParamsTimer = 0;
// Set the initial state (vc.currentPosition is set before updateSpeedAndPitch)
vc.state = PLAYING;
vc.isSmoothingAttack = attackSmoothing > 0;
}
}
void CustomSamplerVoice::stopNote(float /*velocity*/, bool allowTailOff)
{
if (allowTailOff)
{
if (!playUntilEnd || isLooping)
midiReleased = true;
}
else
{
// We render a quick tail-off to avoid clicks
juce::AudioBuffer<float> temp{ tailOffBuffer.getNumChannels(), tailOffBuffer.getNumSamples() };
temp.clear();
renderNextBlock(temp, 0, TAIL_OFF);
tailOffBuffer = temp;
tailOff = 0;
vc.state = STOPPED;
clearCurrentNote();
}
}
void CustomSamplerVoice::immediateHalt()
{
vc.state = STOPPED;
clearCurrentNote();
}
void CustomSamplerVoice::pitchWheelMoved(int newPitchWheelValue)
{
updateSpeedAndPitch(getCurrentlyPlayingNote(), newPitchWheelValue);
}
void CustomSamplerVoice::updateSpeedAndPitch(int currentNote, int pitchWheelPosition)
{
pitchWheel = pitchWheelPosition;
// Account for tuning adjustments
float tuningRatio = 1.f;
if (playbackMode == PluginParameters::BASIC && wavetableMode)
tuningRatio *= std::pow(2.f, (sampleSound.waveformSemitoneTuning->get() + sampleSound.waveformCentTuning->get() / 100.f) / 12.f);
else
tuningRatio *= std::pow(2.f, (sampleSound.semitoneTuning->get() + sampleSound.centTuning->get() / 100.f) / 12.f);
float wheelRange = sampleSound.pitchWheelRange->get();
tuningRatio *= std::pow(2.f, juce::jmap<float>(float(pitchWheelPosition), 0.f, 16383.f, -wheelRange, wheelRange) / 12.f);
tuningRatio *= std::pow(2.f, sampleSound.wideTuningControl->get() / 12.f);
// Account for MIDI note
if (sampleSound.followMidiPitch->get())
{
int rootNote = sampleSound.midiRoot->get();
tuningRatio *= std::pow(2.f, (currentNote - rootNote) / 12.f);
tuningRatio *= float(MTS_RetuningAsRatio(mtsClient, char(currentNote), -1));
}
tuning = tuningRatio;
speedFactor = sampleSound.speedFactor->get();
speedFactor *= 1.f + (tuning - 1.f) * sampleSound.octaveSpeedFactor->get();
if (playbackMode == PluginParameters::BASIC)
{
// In wavetable mode, the sample is treated as a single cycle
// Otherwise, the sample is simply sped up according to the tuning
float a4_hz = sampleSound.a4_freq->get();
speed = wavetableMode ? (tuning * a4_hz) * (sampleEnd - sampleStart + 1 - crossfade) / float(getSampleRate()) / 2.f
: tuning * sampleRateConversion;
// Configure the filters
auto frequency = sampleSound.sampleRate / 2.f / speed;
auto filterLimit = sampleSound.sampleRate / 2.f - 10.f; // We've run into some issues when the filter is too close to the Nyquist frequency
bool wasLowpass = doLowpass;
doLowpass = speed > 1.f && frequency < filterLimit;
if (!wasLowpass && doLowpass)
{
for (int ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
mainLowpass[ch]->resetProcessing(int(vc.currentPosition));
}
if (doLowpass)
{
for (int ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
{
mainLowpass[ch]->setCoefficients(sampleSound.sampleRate, frequency);
loopLowpass[ch]->setCoefficients(sampleSound.sampleRate, frequency);
endLowpass[ch]->setCoefficients(sampleSound.sampleRate, frequency);
}
}
}
else
{
// Update the stretchers
mainStretcher.setPitchAndSpeed(tuning, speedFactor);
loopStretcher.setPitchAndSpeed(tuning, speedFactor);
endStretcher.setPitchAndSpeed(tuning, speedFactor);
speed = speedFactor * sampleRateConversion;
}
}
//==============================================================================
void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
{
if (vc.state == STOPPED && (!doFxTailOff || !getCurrentlyPlayingSound()))
{
clearCurrentNote();
return;
}
// These resizes will happen rarely, if at all
if (tempOutputBuffer.getNumSamples() < numSamples)
{
tempOutputBuffer.setSize(tempOutputBuffer.getNumChannels(), numSamples);
envelopeBuffer.setSize(envelopeBuffer.getNumChannels(), numSamples);
}
tempOutputBuffer.clear();
envelopeBuffer.clear();
if (playbackMode == PluginParameters::BUNGEE && loopStretcherBuffer.getNumSamples() < numSamples)
{
mainStretcherBuffer.setSize(mainStretcherBuffer.getNumChannels(), numSamples);
loopStretcherBuffer.setSize(loopStretcherBuffer.getNumChannels(), numSamples);
endStretcherBuffer.setSize(endStretcherBuffer.getNumChannels(), numSamples);
}
updateSpeedAndPitch(getCurrentlyPlayingNote(), pitchWheel);
bool someFXEnabled{ false };
for (const auto& effect : effects)
{
someFXEnabled = someFXEnabled || effect.enablementSource->get();
}
// Main processing loop
VoiceContext con;
for (auto ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
{
// This struct is used to easily process channel by channel
con = vc;
for (auto i = 0; i < numSamples; i++)
{
if (con.state == STOPPED)
{
con.samplesSinceStopped++;
continue;
}
// Fetch the sample according to the playback mode
float sample = playbackMode == PluginParameters::BASIC ?
fetchSample(ch, con.currentPosition, mainLowpass) :
nextSample(ch, &mainStretcher, mainStretcherBuffer, i);
// Crossfading
if (con.isCrossfadingLoop)
{
double crossfadePosition = con.currentPosition - sampleStart;
if (crossfadePosition >= crossfade)
{
con.isCrossfadingLoop = false;
}
else
{
// Power preserving crossfade (https://www.youtube.com/watch?v=-5cB3rec2T0)
float crossfadeIncrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade + juce::MathConstants<float>::pi)));
float crossfadeDecrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade)));
float next = playbackMode == PluginParameters::BASIC ?
fetchSample(ch, con.currentPosition + sampleEnd - sampleStart - crossfade, loopLowpass) :
nextSample(ch, &loopStretcher, loopStretcherBuffer, i);
sample = sample * crossfadeIncrease + next * crossfadeDecrease;
}
}
if (con.isCrossfadingEnd)
{
double crossfadePosition = con.currentPosition - sampleEnd;
if (crossfadePosition >= crossfade)
{
con.isCrossfadingEnd = false;
}
else
{
float crossfadeIncrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade + juce::MathConstants<float>::pi)));
float crossfadeDecrease = float(std::sqrt(0.5f + 0.5f * std::cos(juce::MathConstants<float>::pi * crossfadePosition / crossfade)));
float next = playbackMode == PluginParameters::BASIC ?
fetchSample(ch, con.crossfadeEndPosition, endLowpass) :
nextSample(ch, &endStretcher, endStretcherBuffer, i);
sample = sample * crossfadeIncrease + next * crossfadeDecrease;
con.crossfadeEndPosition += speed;
}
}
// Attack and release envelopes
envelopeBuffer.setSample(ch, i, 1.f);
if (con.isSmoothingAttack)
{
if (con.speedMovedSinceStart >= attackSmoothing)
con.isSmoothingAttack = false;
else
envelopeBuffer.setSample(ch, i, exponentialCurve(attackShape, con.speedMovedSinceStart / attackSmoothing));
}
if (con.isReleasing)
envelopeBuffer.setSample(ch, i, envelopeBuffer.getSample(ch, i) * exponentialCurve(releaseShape, 1 - con.speedMovedSinceRelease / releaseSmoothing));
// Update the position
con.currentPosition += speed;
con.speedMovedSinceStart += 1;
if (con.isReleasing)
con.speedMovedSinceRelease += 1;
// Handle transitions
if (con.state == PLAYING && isLooping && con.currentPosition >= sampleEnd - crossfade) // Loop crossfade
{
con.currentPosition -= (sampleEnd - sampleStart + 1) - crossfade;
con.isCrossfadingLoop = true;
if (playbackMode == PluginParameters::BUNGEE && ch == 0)
{
std::swap(mainStretcher, loopStretcher);
mainStretcher.initialize(con.currentPosition, tuning, speedFactor); // This could also be done at note start
}
else
{
std::swap(mainLowpass[ch], loopLowpass[ch]);
mainLowpass[ch]->resetProcessing(int(con.currentPosition));
}
}
if (midiReleased && !con.isReleasing && con.state == PLAYING) // Midi release, end crossfade
{
if (loopingHasEnd)
{
con.crossfadeEndPosition = con.currentPosition;
con.currentPosition = sampleEnd + 1;
con.state = PLAYING_END;
con.isCrossfadingEnd = true;
if (playbackMode == PluginParameters::BUNGEE && ch == 0)
{
std::swap(mainStretcher, endStretcher);
mainStretcher.initialize(con.currentPosition, tuning, speedFactor); // This could also be done at note start
}
else
{
std::swap(mainLowpass[ch], endLowpass[ch]);
mainLowpass[ch]->resetProcessing(int(con.currentPosition));
}
}
else
{
con.isReleasing = true;
}
}
if (((con.state == PLAYING && !isLooping) || con.state == PLAYING_END) && !con.isReleasing &&
con.currentPosition > effectiveEnd - releaseSmoothing * speed) // Release smoothing
{
con.isReleasing = true;
}
if (con.currentPosition > effectiveEnd || (con.isReleasing && con.speedMovedSinceRelease >= releaseSmoothing)) // End of playback reached
con.state = STOPPED;
// Scale with standard velocity curve
sample *= juce::Decibels::decibelsToGain(40 * log10(noteVelocity));
sample *= juce::Decibels::decibelsToGain(float(sampleSound.gain->get()));
tempOutputBuffer.setSample(ch, i, sample);
}
}
vc = con;
// Check for updated FX order
if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH)
initializeFx();
// Apply envelope here or after FX if PRE_FX is enabled
if (!sampleSound.applyFXPre->get())
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
juce::FloatVectorOperations::multiply(tempOutputBuffer.getWritePointer(ch), tempOutputBuffer.getReadPointer(ch), envelopeBuffer.getReadPointer(ch), numSamples);
// Apply FX
int reverbSampleDelay = int(1000.f + sampleSound.reverbPredelay->get() * float(getSampleRate()) / 1000.f); // the 1000.f is approximate
someFXEnabled = false;
for (auto& effect : effects)
{
// Check for updated enablement
bool enablement = effect.enablementSource->get();
if (!effect.enabled && enablement)
{
effect.fx->initialize(sampleSound.sample.getNumChannels(), int(getSampleRate()));
effect.fx->updateParams(sampleSound, false);
}
effect.enabled = enablement;
someFXEnabled = someFXEnabled || effect.enabled;
if (effect.enabled && !effect.locallyDisabled)
{
// Update params every UPDATE_PARAMS_LENGTH calls to process
if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH)
effect.fx->updateParams(sampleSound, true);
effect.fx->process(tempOutputBuffer, numSamples);
// Check if an effect should be locally disabled. Note that reverb can only be disabled after a certain delay
if (con.state == STOPPED && numSamples > 10 && !(effect.fxType == PluginParameters::REVERB && con.samplesSinceStopped <= reverbSampleDelay))
{
bool disable{ true };
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
{
float level = tempOutputBuffer.getRMSLevel(ch, 0, numSamples);
if (level > 0)
{
disable = false;
break;
}
}
if (disable)
effect.locallyDisabled = true;
}
}
}
if (sampleSound.applyFXPre->get())
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
juce::FloatVectorOperations::multiply(tempOutputBuffer.getWritePointer(ch), tempOutputBuffer.getReadPointer(ch), envelopeBuffer.getReadPointer(ch), numSamples);
updateFXParamsTimer--;
if (updateFXParamsTimer <= 0)
updateFXParamsTimer = UPDATE_PARAMS_LENGTH;
doFxTailOff = !sampleSound.applyFXPre->get() && someFXEnabled && !effects.empty();
// Check RMS level to see if a voice should be ended despite tailing off effects
if (con.state == STOPPED && someFXEnabled && numSamples > 10 && con.samplesSinceStopped > reverbSampleDelay)
{
bool end{ true }; // Whether all channels are below the threshold
for (int ch = 0; ch < tempOutputBuffer.getNumChannels(); ch++)
{
float level = tempOutputBuffer.getRMSLevel(ch, 0, numSamples);
if (level >= PluginParameters::FX_TAIL_OFF_MAX)
{
end = false;
break;
}
}
if (end)
{
clearCurrentNote();
return;
}
}
mixToBuffer(tempOutputBuffer, outputBuffer, startSample, numSamples, sampleSound.monoOutput->get());
// Add the previous tail-off samples to the output buffer
tailOffBuffer.setSize(tempOutputBuffer.getNumChannels(), tailOffBuffer.getNumSamples(), true, true);
int i = 0;
for (; tailOff < TAIL_OFF; tailOff++)
{
if (i >= numSamples)
break;
for (int ch = 0; ch < tailOffBuffer.getNumChannels(); ch++)
{
float sample = tailOffBuffer.getSample(ch, tailOff) * (TAIL_OFF - tailOff) / TAIL_OFF;
outputBuffer.addSample(ch, startSample + i, sample);
}
i++;
}
}
float CustomSamplerVoice::fetchSample(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const
{
if (0 > position || position >= float(sampleSound.sample.getNumSamples()))
return 0.f;
if (sampleSound.skipAntialiasing->get())
{
return sampleSound.sample.getSample(channel, int(position));
}
else
{
return lanczosInterpolate(channel, position, lowpassStreams);
}
}
float CustomSamplerVoice::nextSample(int channel, BungeeStretcher* stretcher, juce::AudioBuffer<float>& channelBuffer, int i) const
{
if (channel == 0)
{
for (int ch = 1; ch < sampleSound.sample.getNumChannels(); ch++)
channelBuffer.setSample(ch - 1, i, stretcher->nextSample(ch, false));
return stretcher->nextSample(0);
}
else
{
return channelBuffer.getSample(channel - 1, i);
}
}
float CustomSamplerVoice::getEnvelopeGain() const
{
float gain = 1.f;
if (vc.isSmoothingAttack)
gain *= exponentialCurve(attackShape, vc.speedMovedSinceStart / attackSmoothing);
if (vc.isReleasing)
gain *= exponentialCurve(releaseShape, 1 - vc.speedMovedSinceRelease / releaseSmoothing);
return gain;
}
//==============================================================================
void CustomSamplerVoice::initializeFx()
{
auto fxOrder = sampleSound.getFxOrder();
bool changed = false;
for (size_t i = 0; i < fxOrder.size(); i++)
{
if (effects.size() <= i || effects[i].fxType != fxOrder[i])
{
changed = true;
break;
}
}
if (changed)
{
effects.clear();
for (auto& fxType : fxOrder)
{
switch (fxType)
{
case PluginParameters::DISTORTION:
effects.emplace_back(PluginParameters::DISTORTION, std::make_unique<Distortion>(), sampleSound.distortionEnabled);
break;
case PluginParameters::REVERB:
effects.emplace_back(PluginParameters::REVERB, std::make_unique<Reverb>(), sampleSound.reverbEnabled);
break;
case PluginParameters::CHORUS:
effects.emplace_back(PluginParameters::CHORUS, std::make_unique<Chorus>(expectedBlockSize), sampleSound.chorusEnabled);
break;
case PluginParameters::EQ:
effects.emplace_back(PluginParameters::EQ, std::make_unique<BandEQ>(), sampleSound.eqEnabled);
break;
}
}
}
}
//==============================================================================
/** Thank god for Wikipedia, I don't really know why this works. https://en.wikipedia.org/wiki/Lanczos_resampling
The technical details of resampling elude me, but JUCE's filters seem to work well enough for this...
*/
float CustomSamplerVoice::lanczosInterpolate(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const
{
// First, process the lowpass filter
auto& lowpassStream = *lowpassStreams[channel];
if (doLowpass && lowpassStream.getNextSample() < sampleSound.sample.getNumSamples())
{
int lastWindowSample = juce::jmin(int(std::floor(position)) + LANCZOS_WINDOW_SIZE, sampleSound.sample.getNumSamples() - 1);
lowpassStream.processSamples(sampleSound.sample.getReadPointer(channel, lowpassStream.getNextSample()), lastWindowSample - lowpassStream.getNextSample() + 1);
}
// Then, interpolate
int floorIndex = int(std::floor(position));
float result = 0.f;
for (int i = -LANCZOS_WINDOW_SIZE + 1; i <= LANCZOS_WINDOW_SIZE; i++)
{
int iPlus = i + floorIndex;
float sample = 0.f;
if (0 <= iPlus && iPlus < sampleSound.sample.getNumSamples()) // Bounds checking is a bit awkward here but handles some edge cases
{
if (doLowpass)
{
if (iPlus >= lowpassStream.getStartSample())
sample = lowpassStream.getProcessedSample(iPlus);
}
else
{
sample = sampleSound.sample.getSample(channel, iPlus);
}
}
float window = lanczosWindow(position - floorIndex - i);
result += sample * window;
}
return result;
}
float CustomSamplerVoice::lanczosWindow(double x)
{
return x == 0.f ? 1.f : float(LANCZOS_WINDOW_SIZE * std::sin(juce::MathConstants<float>::pi * x) * std::sin(juce::MathConstants<float>::pi * x / LANCZOS_WINDOW_SIZE) * INVERSE_SIN_SQUARED / (x * x));
}

View file

@ -0,0 +1,262 @@
/*
==============================================================================
CustomSamplerVoice.h
Created: 5 Sep 2023 3:35:03pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "SamplerParameters.h"
#include "Effects/Effect.h"
#include "Stretcher.h"
#include <libMTSClient.h>
/** This enum includes the different states a voice can be in */
enum VoiceState
{
PLAYING, // The voice is still before or during the loop
PLAYING_END, // The voice is continuing after the loop
STOPPED
};
/** The context information for sample by sample processing is stored in its own struct. This
is primarily to allow for easy multichannel processing but also encapsulates the state nicely.
Note that the smoothing variables are an important part of the state transition logic.
*/
struct VoiceContext
{
VoiceState state{ STOPPED };
double currentPosition{ 0 }; // Fractional positions are necessary
bool isSmoothingAttack{ false }; // The initial attack curve
bool isCrossfadingLoop{ false };
bool isCrossfadingEnd{ false }; // Crossfading between looping and the end part of the sample
bool isReleasing{ false }; // Active when the note is released or when it nears the end of the sample
double crossfadeEndPosition{ 0 }; // The current position of the end crossfade
float speedMovedSinceStart{ 0 }; // Used to time the attack envelope, note this is in terms of time passed, not position
float speedMovedSinceRelease{ 0 }; // Used to time the release envelope
int samplesSinceStopped{ 0 }; // This is needed to time the RMS measurements for reverb tail off (since it has a delay)
};
/** This class is used to store the state of the lowpass filter for a channel / stream
Because of our use case, a circular buffer is used to store past samples, large enough for the size of the lanczos window.
*/
class LowpassStream
{
public:
explicit LowpassStream(int bufferSize) : intermediateBuffer(1, bufferSize) {}
/** Reset the processing state of the stream to a new sample position */
void resetProcessing(int nextSampleToProcess)
{
filter1.reset();
filter2.reset();
filter3.reset();
filter4.reset();
bufferLoc = 0;
startSample = nextSampleToProcess;
nextSample = nextSampleToProcess;
}
/** Process a block of samples, storing the recent result in the intermediate buffer.
nextSample is the index of the next sample that should be processed.
*/
void processSamples(const float* samples, int numSamples)
{
for (int i = 0; i < numSamples; ++i)
{
float processedSample = filter1.processSingleSampleRaw(samples[i]);
processedSample = filter2.processSingleSampleRaw(processedSample);
processedSample = filter3.processSingleSampleRaw(processedSample);
processedSample = filter4.processSingleSampleRaw(processedSample);
intermediateBuffer.setSample(0, bufferLoc, processedSample);
nextSample++;
bufferLoc = (bufferLoc + 1) % intermediateBuffer.getNumSamples();
}
}
/** Get the processed sample at a given index. Asserts the sample is contained. */
float getProcessedSample(int sampleIndex) const
{
jassert(sampleIndex >= startSample && sampleIndex < nextSample && sampleIndex >= nextSample - intermediateBuffer.getNumSamples());
int bufferIndex = (sampleIndex - startSample) % intermediateBuffer.getNumSamples();
return intermediateBuffer.getSample(0, bufferIndex);
}
int getNextSample() const { return nextSample; }
/** Following juce::dsp::FilterDesign::designIIRLowpassHighOrderButterworthMethod(), this is theoretically -48db above 20khz */
void setCoefficients(int sampleRate, float frequency)
{
float order = 8.f;
filter1.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(1.f * juce::MathConstants<float>::pi / (order * 2.f)))));
filter2.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(3.f * juce::MathConstants<float>::pi / (order * 2.f)))));
filter3.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(5.f * juce::MathConstants<float>::pi / (order * 2.f)))));
filter4.setCoefficients(juce::IIRCoefficients::makeLowPass(sampleRate, frequency, 1.f / (2.f * std::cos(7.f * juce::MathConstants<float>::pi / (order * 2.f)))));
}
int getStartSample() const { return startSample; }
private:
juce::SingleThreadedIIRFilter filter1;
juce::SingleThreadedIIRFilter filter2;
juce::SingleThreadedIIRFilter filter3;
juce::SingleThreadedIIRFilter filter4;
juce::AudioBuffer<float> intermediateBuffer;
int startSample{ 0 };
int bufferLoc{ 0 }; // Location in the buffer to write to (circular buffer)
int nextSample{ 0 }; // The next sample to be processed
};
/** This struct serves to separate per instance enablement of effects from the effect classes themselves */
struct Fx
{
Fx(PluginParameters::FxTypes fxType, std::unique_ptr<Effect> fx, juce::AudioParameterBool* enablementSource) :
fxType(fxType), fx(std::move(fx)), enablementSource(enablementSource) {}
PluginParameters::FxTypes fxType;
std::unique_ptr<Effect> fx;
juce::AudioParameterBool* enablementSource;
bool enabled{ false };
bool locallyDisabled{ false }; // used to avoid empty processing
};
//==============================================================================
/** The CustomSamplerVoice is the main DSP logic of this plugin. It can pitch shift directly or integrate with a 3rd party
algorithm. It supports antialiasing, an FX chain, different looping modes, attack and release, and smooth crossfading.
*/
class CustomSamplerVoice final : public juce::SynthesiserVoice
{
public:
CustomSamplerVoice(const SamplerParameters& samplerSound, MTSClient* client, double applicationSampleRate, int expectedBlockSize, bool initSample = true);
/** For general convenience, we'd like to be able to initialize all voices at plugin start */
void initializeSample();
/** Updates the speed and pitch, setting stretchers and filter cutoffs correctly.
Before calling this the first time, set doLowpass = false so that it resets the lowpass filters.
*/
void updateSpeedAndPitch(int currentNote, int pitchWheelPosition);
//==============================================================================
/** Returns whether the voice is actively playing (not stopped or tailing off) */
bool isPlaying() const { return getCurrentlyPlayingSound() && vc.state != STOPPED; }
/** This is the condition for wavetable mode */
static bool isWavetableModeAvailable(float sampleRate, int sampleStart, int sampleEnd)
{
return float(sampleRate) / (sampleEnd - sampleStart + 1) > PluginParameters::WAVETABLE_CUTOFF_HZ;
}
/** Get the effective location of the sampler voice relative to the original sample, not precise in ADVANCED mode */
double getPosition() const { return vc.currentPosition; }
/** Get the current gain of the voice in the attack and release envelopes, for visualization */
float getEnvelopeGain() const;
/** x should be [0, 1] */
static const float exponentialCurve(float a, float x) { return juce::approximatelyEqual(a, 0.f, juce::Tolerance<float>().withAbsolute(0.001f)) ? x : (std::exp(a * x) - 1) / (std::exp(a) - 1); }
void stopNote(float velocity, bool allowTailOff) override;
void immediateHalt();
private:
bool canPlaySound(juce::SynthesiserSound*) override { return true; }
void startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition) override;
void pitchWheelMoved(int newPitchWheelValue) override;
void controllerMoved(int, int) override {}
void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override;
//==============================================================================
/** Fetch a sample at a given position, in BASIC mode.
Provide a vector of lowpass streams to apply lowpass filtering before interpolation (if doLowpass).
This is necessary to avoid frequencies going above the Nyquist frequency.
*/
float fetchSample(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const;
/** Fetches the next sample from a stretcher, in ADVANCED mode. Note that on channel 0, the stretcher
advances and stores the other channels' output in the channel buffer at index i. Then it's fetched
from there when nextSample is called with the later channel.
*/
float nextSample(int channel, BungeeStretcher* stretcher, juce::AudioBuffer<float>& channelBuffer, int i) const;
/** Use a Lanczos kernel to calculate fractional sample indices. Applies a lowpass filter beforehand, if doLowpass. */
float lanczosInterpolate(int channel, double position, std::vector<std::unique_ptr<LowpassStream>>& lowpassStreams) const;
inline static float lanczosWindow(double x);
static constexpr int LANCZOS_WINDOW_SIZE{ 5 };
/** Initialize or updates (by reinitializing) the effect chain. This is not real-time safe, but I don't think reordering needs to be. */
void initializeFx();
//==============================================================================
int expectedBlockSize;
const SamplerParameters& sampleSound;
float sampleRateConversion{ 0 }; // Loaded sample rate / application sample rate
float speed{ 0 }; // Used in BASIC mode
int effectiveStart{ 0 };
int effectiveEnd{ 0 };
/** "Wavetable mode" activates when the bounds are very short and can act as a waveform cycle. */
bool wavetableMode{ false };
// Unchanging sampler sound parameters
PluginParameters::PLAYBACK_MODES playbackMode{ PluginParameters::PLAYBACK_MODES::BASIC };
float tuning{ 0.f };
int pitchWheel{ 0 };
float speedFactor{ 0.f }; // Used in ADVANCED mode
float noteVelocity{ 0.f };
bool playUntilEnd{ false };
bool isLooping{ false }, loopingHasStart{ false }, loopingHasEnd{ false };
int sampleStart{ 0 }, sampleEnd{ 0 }, loopStart{ 0 }, loopEnd{ 0 };
/** We call this "smoothing" but it's a pretty normal attack/release envelope. */
float attackSmoothing{ 0.f }, releaseSmoothing{ 0.f };
float attackShape{ 0.f }, releaseShape{ 0.f };
float crossfade{ 0.f };
VoiceContext vc;
bool midiReleased{ false };
juce::AudioBuffer<float> tempOutputBuffer;
juce::AudioBuffer<float> envelopeBuffer; // To enable the PRE_FX option, we store the envelope gain here before applying
static constexpr int TAIL_OFF = 50;
int tailOff{ 0 };
juce::AudioBuffer<float> tailOffBuffer; // To avoid clicks on voice-stealing, we render a tail
BungeeStretcher mainStretcher;
BungeeStretcher loopStretcher;
BungeeStretcher endStretcher;
// Since the stretchers process channels together, buffers are needed to store the output
juce::AudioBuffer<float> mainStretcherBuffer;
juce::AudioBuffer<float> loopStretcherBuffer;
juce::AudioBuffer<float> endStretcherBuffer;
bool doLowpass{ false };
std::vector<std::unique_ptr<LowpassStream>> mainLowpass;
std::vector<std::unique_ptr<LowpassStream>> loopLowpass;
std::vector<std::unique_ptr<LowpassStream>> endLowpass;
//==============================================================================
bool doFxTailOff{ false };
static constexpr int UPDATE_PARAMS_LENGTH{ 4 }; // After how many process calls should we query for FX params
int updateFXParamsTimer{ 0 };
std::vector<Fx> effects;
MTSClient* mtsClient{ nullptr };
};
static constexpr float INVERSE_SIN_SQUARED{ 1.f / (juce::MathConstants<float>::pi * juce::MathConstants<float>::pi) };

View file

@ -0,0 +1,31 @@
/*
==============================================================================
CustomSynthesizer.h
Created: 9 Jun 2024 10:37:38am
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
/** JUCE's voice and sound paradigm is not so helpful for us, so we use a blank sound class and pass in our parameters directly to the voices. */
class BlankSynthesizerSound final : public juce::SynthesiserSound
{
public:
bool appliesToNote(int) override { return true; }
bool appliesToChannel(int) override { return true; }
};
/** We add some custom methods because our Synthesizer does not own its voices */
class CustomSynthesizer final : public juce::Synthesiser
{
public:
juce::SynthesiserVoice* removeVoiceWithoutDeleting(const int index)
{
const juce::ScopedLock sl(lock);
return voices.removeAndReturn(index);
}
};

View file

@ -0,0 +1,98 @@
/*
==============================================================================
BandEQ.h
Created: 2 Jan 2024 5:02:27pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "Effect.h"
/** This is a sample 3-band EQ effect, inspired by the Kiloheart's free 3-Band EQ plugin. */
class BandEQ final : public Effect
{
public:
void initialize(int numChannels, int fxSampleRate) override
{
sampleRate = fxSampleRate;
juce::dsp::ProcessSpec spec{};
spec.numChannels = numChannels;
spec.sampleRate = sampleRate;
filterChain.reset();
filterChain.prepare(spec);
}
void updateParams(float lowFreq, float highFreq, float lowGain, float midGain, float highGain)
{
// This possibly has an issue where the frequencies are outside of range on plugin initialization
auto coeffLow = juce::dsp::IIR::Coefficients<float>::makeLowShelf(sampleRate, lowFreq, Q, juce::Decibels::decibelsToGain(lowGain));
auto coeffMid1 = juce::dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, lowFreq, Q, juce::Decibels::decibelsToGain(midGain));
auto coeffMid2 = juce::dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, highFreq, Q, juce::Decibels::decibelsToGain(-midGain));
auto coeffHigh = juce::dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, highFreq, Q, juce::Decibels::decibelsToGain(highGain));
*filterChain.get<0>().state = *coeffLow;
*filterChain.get<1>().state = *coeffMid1;
*filterChain.get<2>().state = *coeffMid2;
*filterChain.get<3>().state = *coeffHigh;
}
void updateParams(const SamplerParameters& samplerSound, bool) override
{
updateParams(
samplerSound.eqLowFreq->get(), samplerSound.eqHighFreq->get(),
samplerSound.eqLowGain->get(), samplerSound.eqMidGain->get(), samplerSound.eqHighGain->get()
);
}
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample = 0) override
{
juce::dsp::AudioBlock block{ buffer.getArrayOfWritePointers(), size_t(buffer.getNumChannels()), size_t(startSample), size_t(numSamples) };
juce::dsp::ProcessContextReplacing context{ block };
filterChain.process(context);
}
juce::Array<double> getMagnitudeForFrequencyArray(juce::Array<double> frequencies)
{
std::array filters{
&filterChain.get<0>(),
&filterChain.get<1>(),
&filterChain.get<2>(),
&filterChain.get<3>()
};
juce::AudioBuffer<double> magnitudes{1, frequencies.size()};
magnitudes.clear();
bool empty{ true };
for (const auto& filter : filters)
{
juce::AudioBuffer<double> temp{ 1, frequencies.size()};
filter->state->getMagnitudeForFrequencyArray(frequencies.getRawDataPointer(), temp.getWritePointer(0), frequencies.size(), 48000);
if (empty)
{
magnitudes.addFrom(0, 0, temp.getReadPointer(0), frequencies.size());
empty = false;
}
else
{
for (int i = 0; i < temp.getNumSamples(); i++)
magnitudes.setSample(0, i, magnitudes.getSample(0, i) * temp.getSample(0, i));
}
}
return juce::Array<double>{magnitudes.getReadPointer(0), frequencies.size()};
}
private:
using Filter = juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>>;
using FilterChain = juce::dsp::ProcessorChain<Filter, Filter, Filter, Filter>;
static constexpr float Q{ 0.6f }; // Magic number I saw online for the response curve
int sampleRate{ 0 };
FilterChain filterChain;
};

View file

@ -0,0 +1,57 @@
/*
==============================================================================
Chorus.h
Created: 4 Jan 2024 7:22:36pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "Effect.h"
/** This is a simple wrapper around the JUCE Chorus class */
class Chorus final : public Effect
{
public:
explicit Chorus(int expectedBlockSize=MAX_BLOCK_SIZE) : expectedBlockSize(expectedBlockSize) {}
void initialize(int numChannels, int fxSampleRate) override
{
juce::dsp::ProcessSpec processSpec{ double(fxSampleRate), juce::uint32(expectedBlockSize), juce::uint32(numChannels) };
chorus.reset();
chorus.prepare(processSpec);
}
void updateParams(const SamplerParameters& sampleSound, bool realtime) override
{
chorus.setRate(sampleSound.chorusRate->get());
chorus.setDepth(sampleSound.chorusDepth->get());
chorus.setFeedback(sampleSound.chorusFeedback->get());
if (!realtime) // Center delay cannot be modulated
chorus.setCentreDelay(juce::jmin<float>(sampleSound.chorusCenterDelay->get(), 99.9f));
chorus.setMix(sampleSound.chorusMix->get());
}
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample = 0) override
{
while (numSamples > 0)
{
juce::dsp::AudioBlock<float> block{ buffer.getArrayOfWritePointers(), size_t(buffer.getNumChannels()), size_t(startSample), size_t(juce::jmin(MAX_BLOCK_SIZE, numSamples)) };
juce::dsp::ProcessContextReplacing<float> context{ block };
chorus.process(context);
startSample += MAX_BLOCK_SIZE;
numSamples -= MAX_BLOCK_SIZE;
}
}
private:
static constexpr int MAX_BLOCK_SIZE{ 1024 };
juce::dsp::Chorus<float> chorus{};
int expectedBlockSize;
};

View file

@ -0,0 +1,73 @@
/*
==============================================================================
Distortion.h
Created: 30 Dec 2023 8:39:04pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "Effect.h"
#include <gin_distortion.h>
/** This is a simple Distortion class that wraps around gin::AirWindowsDistortion */
class Distortion final : public Effect
{
public:
void initialize(int numChannels, int fxSampleRate) override
{
int numEffects = numChannels / 2 + numChannels % 2;
channelDistortions.resize(numEffects);
for (int ch = 0; ch < numEffects; ch++)
{
channelDistortions[ch] = std::make_unique<gin::AirWindowsDistortion>();
channelDistortions[ch]->setSampleRate(fxSampleRate);
}
}
void updateParams(float density, float highpass, float mix)
{
float mappedDensity = density >= 0.f ? juce::jmap<float>(density, 0.2f, 1.f) : juce::jmap<float>(density, -0.5f, 0.f, 0.f, 0.2f);
// We try to keep the gain of the distortion constant:
// This is a sigmoid function found manually from graphing the output of the distortion. When the mapped density < 0.2f,
// a different function needs to be used, since the distortion actually behaves differently according to that threshold.
// Note that the gain parameter only applies if it's less than 1.f, so we need to increase the gain ourselves after processing.
float gainChange = mappedDensity >= 0.2f ? (0.2f * (1.f + expf(-7.f * (mappedDensity - 0.5f)))) : 1.f;
postGain = mappedDensity < 0.2f ? 1.f / (4.f * mappedDensity + 0.2f) : 1.f;
for (const auto& channelDistortion : channelDistortions)
channelDistortion->setParams(mappedDensity, highpass, gainChange, mix);
}
void updateParams(const SamplerParameters& sampleSound, bool) override
{
updateParams(
sampleSound.distortionDensity->get(),
sampleSound.distortionHighpass->get(),
sampleSound.distortionMix->get()
);
}
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample=0) override
{
bool lastIsMono = buffer.getNumChannels() % 2 == 1;
for (int ch = 0; ch < buffer.getNumChannels(); ch += 2)
{
// Note that I modified the gin header to make this more straightforward
if (lastIsMono && ch == buffer.getNumChannels() - 1)
channelDistortions[ch / 2]->process(buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch, startSample), numSamples);
else
channelDistortions[ch / 2]->process(buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch + 1, startSample), numSamples);
}
buffer.applyGain(startSample, numSamples, postGain);
}
private:
std::vector<std::unique_ptr<gin::AirWindowsDistortion>> channelDistortions;
float postGain{ 0. };
};

View file

@ -0,0 +1,23 @@
/*
==============================================================================
Effect.h
Created: 30 Dec 2023 8:35:28pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../SamplerParameters.h"
class Effect
{
public:
virtual ~Effect() = default;
virtual void initialize(int numChannels, int fxSampleRate) = 0;
virtual void updateParams(const SamplerParameters& sampleSound, bool modulating = false) = 0;
virtual void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample=0) = 0;
};

View file

@ -0,0 +1,85 @@
/*
==============================================================================
Reverb.h
Created: 30 Dec 2023 8:38:55pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "Effect.h"
#include <gin_simpleverb.h>
/** This is a simple Effect class that wraps around Gin's SimpleVerb implementation */
class Reverb final : public Effect
{
public:
Reverb() = default;
void initialize(int numChannels, int fxSampleRate) override
{
int numEffects = numChannels / 2 + numChannels % 2;
channelGinReverbs.resize(numEffects);
for (int ch = 0; ch < numEffects; ch++)
{
channelGinReverbs[ch] = std::make_unique<gin::SimpleVerb>();
channelGinReverbs[ch]->setSampleRate(float(fxSampleRate));
channelGinReverbs[ch]->setParameters(0.f, 0.f, 1.f, 0.f, 1.f, 0.f, 0.f);
}
}
// The intended ranges of these values are in PluginParameters.h
void updateParams(float size, float damping, float predelay, float lows, float highs, float mix) const
{
for (const auto& channelGinReverb : channelGinReverbs)
{
channelGinReverb->setParameters(
juce::jmap<float>(size, PluginParameters::REVERB_SIZE_RANGE.getStart(), PluginParameters::REVERB_SIZE_RANGE.getEnd(), 0.f, 1.f),
juce::jmap<float>(damping, PluginParameters::REVERB_DAMPING_RANGE.getStart(), PluginParameters::REVERB_DAMPING_RANGE.getEnd(), 0.f, 1.f),
float(sqrtf(predelay / 250.f)), // conversions to counteract the faders in this algorithm
juce::jmap<float>(highs, PluginParameters::REVERB_HIGHS_RANGE.getStart(), PluginParameters::REVERB_HIGHS_RANGE.getEnd(), 0.3f, 1.f),
1.f - juce::jmap<float>(lows, PluginParameters::REVERB_LOWS_RANGE.getStart(), PluginParameters::REVERB_LOWS_RANGE.getEnd(), 0.3f, 1.f),
mix,
(1.f - mix) / 2.f
);
}
}
void updateParams(const SamplerParameters& sampleSound, bool) override
{
updateParams(
sampleSound.reverbSize->get(),
sampleSound.reverbDamping->get(),
sampleSound.reverbPredelay->get(),
sampleSound.reverbLows->get(),
sampleSound.reverbHighs->get(),
sampleSound.reverbMix->get()
);
}
void process(juce::AudioBuffer<float>& buffer, int numSamples, int startSample = 0) override
{
bool lastIsMono = buffer.getNumChannels() % 2 == 1;
for (int ch = 0; ch < buffer.getNumChannels(); ch += 2)
{
// I modified the header of the process method to work easier with this code, you'll need to do the same to get it to compile
// void SimpleVerb::process (const float* in1, const float* in2, float* out1, float* out2, int numSamples)
if (lastIsMono && ch == buffer.getNumChannels() - 1)
channelGinReverbs[ch / 2]->process(
buffer.getReadPointer(ch, startSample), buffer.getReadPointer(ch, startSample),
buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch, startSample), numSamples);
else
channelGinReverbs[ch / 2]->process(
buffer.getReadPointer(ch, startSample), buffer.getReadPointer(ch + 1, startSample),
buffer.getWritePointer(ch, startSample), buffer.getWritePointer(ch + 1, startSample), numSamples);
}
}
private:
std::vector<std::unique_ptr<gin::SimpleVerb>> channelGinReverbs;
};

View file

@ -0,0 +1,94 @@
/*
==============================================================================
CustomSamplerSound.cpp
Created: 5 Sep 2023 3:35:11pm
Author: binya
==============================================================================
*/
#include "SamplerParameters.h"
SamplerParameters::SamplerParameters(const juce::AudioProcessorValueTreeState& apvts, PluginParameters::State& pluginState, const juce::AudioBuffer<float>& sample, int sampleRate) :
sample(sample), sampleRate(sampleRate),
gain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::SAMPLE_GAIN))),
speedFactor(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::SPEED_FACTOR))),
octaveSpeedFactor(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::OCTAVE_SPEED_FACTOR))),
attack(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::ATTACK))),
release(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::RELEASE))),
attackShape(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::ATTACK_SHAPE))),
releaseShape(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::RELEASE_SHAPE))),
a4_freq(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::A4_HZ))),
pitchWheelRange(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::PITCH_WHEEL_RANGE))),
wideTuningControl(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::WIDE_TUNING))),
semitoneTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::SEMITONE_TUNING))),
centTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::CENT_TUNING))),
waveformSemitoneTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::WAVEFORM_SEMITONE_TUNING))),
waveformCentTuning(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::WAVEFORM_CENT_TUNING))),
crossfadeSamples(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::CROSSFADE_SAMPLES))),
monoOutput(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::MONO_OUTPUT))),
disableVelocity(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISABLE_VELOCITY))),
skipAntialiasing(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::SKIP_ANTIALIASING))),
applyFXPre(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::PRE_FX))),
playUntilEnd(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::PLAY_UNTIL_END))),
disableWavetableMode(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISABLE_WAVETABLE_MODE))),
isLooping(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::IS_LOOPING))),
loopingHasStart(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_START))),
loopingHasEnd(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_END))),
sampleStart(pluginState.sampleStart), sampleEnd(pluginState.sampleEnd),
loopStart(pluginState.loopStart), loopEnd(pluginState.loopEnd),
midiStart(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MIDI_START))),
midiEnd(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MIDI_END))),
midiRoot(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MIDI_ROOT))),
followMidiPitch(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::FOLLOW_MIDI_PITCH))),
reverbEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::REVERB_ENABLED))),
distortionEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISTORTION_ENABLED))),
eqEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::EQ_ENABLED))),
chorusEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::CHORUS_ENABLED))),
reverbMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_MIX))),
reverbSize(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_SIZE))),
reverbDamping(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_DAMPING))),
reverbLows(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_LOWS))),
reverbHighs(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_HIGHS))),
reverbPredelay(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::REVERB_PREDELAY))),
distortionMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::DISTORTION_MIX))),
distortionDensity(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::DISTORTION_DENSITY))),
distortionHighpass(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::DISTORTION_HIGHPASS))),
eqLowGain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_LOW_GAIN))),
eqMidGain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_MID_GAIN))),
eqHighGain(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_HIGH_GAIN))),
eqLowFreq(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_LOW_FREQ))),
eqHighFreq(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::EQ_HIGH_FREQ))),
chorusMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_MIX))),
chorusRate(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_RATE))),
chorusDepth(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_DEPTH))),
chorusFeedback(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_FEEDBACK))),
chorusCenterDelay(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_CENTER_DELAY))),
playbackMode(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::PLAYBACK_MODE))),
fxOrder(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::FX_PERM)))
{
}
void SamplerParameters::sampleChanged(const int newSampleRate)
{
sampleRate = newSampleRate;
}
PluginParameters::PLAYBACK_MODES SamplerParameters::getPlaybackMode() const
{
return PluginParameters::getPlaybackMode(playbackMode->getIndex());
}
std::array<PluginParameters::FxTypes, 4> SamplerParameters::getFxOrder() const
{
return PluginParameters::paramToPerm(fxOrder->get());
}

View file

@ -0,0 +1,53 @@
/*
==============================================================================
CustomSamplerSound.h
Created: 5 Sep 2023 3:35:11pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "../PluginParameters.h"
/** A class defining all parameters for a note played by CustomSamplerVoice.cpp */
class SamplerParameters final
{
public:
SamplerParameters(const juce::AudioProcessorValueTreeState& apvts, PluginParameters::State& pluginState, const juce::AudioBuffer<float>& sample, int sampleRate);
void sampleChanged(int newSampleRate);
/** Fetch the playback mode, as the proper enum type */
PluginParameters::PLAYBACK_MODES getPlaybackMode() const;
/** Fetch the sound's FX chain permutation */
std::array<PluginParameters::FxTypes, 4> getFxOrder() const;
/** The sound to play */
const juce::AudioBuffer<float>& sample;
int sampleRate;
/** Playback details */
juce::AudioParameterFloat* gain, * speedFactor, * octaveSpeedFactor, * attack, * release, * attackShape, * releaseShape, * a4_freq, * pitchWheelRange, * wideTuningControl;
juce::AudioParameterInt* semitoneTuning, * centTuning, * waveformSemitoneTuning, * waveformCentTuning, * crossfadeSamples;
juce::AudioParameterBool* monoOutput, * disableVelocity, * skipAntialiasing, * applyFXPre, * playUntilEnd, * disableWavetableMode, * isLooping, * loopingHasStart, * loopingHasEnd;
ListenableAtomic<int>& sampleStart, & sampleEnd, & loopStart, & loopEnd;
juce::AudioParameterInt* midiStart, * midiEnd, * midiRoot;
juce::AudioParameterBool* followMidiPitch;
/** FX parameters */
juce::AudioParameterBool* reverbEnabled, * distortionEnabled, * eqEnabled, * chorusEnabled;
juce::AudioParameterFloat* reverbMix, * reverbSize, * reverbDamping, * reverbLows, * reverbHighs, * reverbPredelay;
juce::AudioParameterFloat* distortionMix, * distortionDensity, * distortionHighpass;
juce::AudioParameterFloat* eqLowGain, * eqMidGain, * eqHighGain, * eqLowFreq, * eqHighFreq;
juce::AudioParameterFloat* chorusMix, * chorusRate, * chorusDepth, * chorusFeedback, * chorusCenterDelay;
private:
juce::AudioParameterChoice* playbackMode;
juce::AudioParameterInt* fxOrder;
};

161
Source/Sampler/Stretcher.h Normal file
View file

@ -0,0 +1,161 @@
/*
==============================================================================
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 };
};