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));
|
||||
}
|
||||
262
Source/Sampler/CustomSamplerVoice.h
Normal file
262
Source/Sampler/CustomSamplerVoice.h
Normal 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) };
|
||||
31
Source/Sampler/CustomSynthesizer.h
Normal file
31
Source/Sampler/CustomSynthesizer.h
Normal 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);
|
||||
}
|
||||
};
|
||||
98
Source/Sampler/Effects/BandEQ.h
Normal file
98
Source/Sampler/Effects/BandEQ.h
Normal 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;
|
||||
};
|
||||
57
Source/Sampler/Effects/Chorus.h
Normal file
57
Source/Sampler/Effects/Chorus.h
Normal 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;
|
||||
};
|
||||
73
Source/Sampler/Effects/Distortion.h
Normal file
73
Source/Sampler/Effects/Distortion.h
Normal 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. };
|
||||
};
|
||||
23
Source/Sampler/Effects/Effect.h
Normal file
23
Source/Sampler/Effects/Effect.h
Normal 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;
|
||||
};
|
||||
85
Source/Sampler/Effects/Reverb.h
Normal file
85
Source/Sampler/Effects/Reverb.h
Normal 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;
|
||||
};
|
||||
94
Source/Sampler/SamplerParameters.cpp
Normal file
94
Source/Sampler/SamplerParameters.cpp
Normal 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());
|
||||
}
|
||||
53
Source/Sampler/SamplerParameters.h
Normal file
53
Source/Sampler/SamplerParameters.h
Normal 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
161
Source/Sampler/Stretcher.h
Normal 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 };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue