justasample/Source/Sampler/CustomSamplerVoice.cpp

654 lines
26 KiB
C++
Raw Normal View History

2026-07-23 23:40:48 +02:00
/*
==============================================================================
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;
isPingPong = isLooping && sampleSound.isPingPong->get() && !wavetableMode;
loopingHasStart = isLooping && sampleSound.loopingHasStart->get() && sampleSound.loopStart < sampleSound.sampleStart && !wavetableMode && !isPingPong;
2026-07-23 23:40:48 +02:00
loopStart = sampleSound.loopStart;
loopingHasEnd = isLooping && sampleSound.loopingHasEnd->get() && sampleSound.loopEnd > sampleSound.sampleEnd && !wavetableMode && !isPingPong;
2026-07-23 23:40:48 +02:00
loopEnd = sampleSound.loopEnd;
if (isPingPong)
playbackMode = PluginParameters::BASIC; // BungeeStretcher is forward-only
pingPongDirection = 1.0f;
2026-07-23 23:40:48 +02:00
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);
if (isPingPong && isLooping)
speed *= pingPongDirection;
2026-07-23 23:40:48 +02:00
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 && isPingPong && pingPongDirection > 0 && con.currentPosition >= sampleEnd) // Ping-pong: reverse at end
{
pingPongDirection = -1.0f;
speed = std::abs(speed) * -1.0f;
}
else if (con.state == PLAYING && isLooping && isPingPong && pingPongDirection < 0 && con.currentPosition <= sampleStart) // Ping-pong: reverse at start
{
pingPongDirection = 1.0f;
speed = std::abs(speed);
}
else if (con.state == PLAYING && isLooping && !isPingPong && con.currentPosition >= sampleEnd - crossfade) // Loop crossfade
2026-07-23 23:40:48 +02:00
{
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));
}