mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 04:10:48 +02:00
init
This commit is contained in:
commit
d21bc831e1
178 changed files with 24136 additions and 0 deletions
635
Source/Sampler/CustomSamplerVoice.cpp
Normal file
635
Source/Sampler/CustomSamplerVoice.cpp
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue