justasample/Source/Sampler/CustomSamplerVoice.h

274 lines
12 KiB
C
Raw Permalink Normal View History

2026-07-23 23:40:48 +02:00
/*
==============================================================================
CustomSamplerVoice.h
Created: 5 Sep 2023 3:35:03pm
Author: binya
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#include "SamplerParameters.h"
#include "Effects/Effect.h"
2026-07-24 18:03:15 +02:00
#include "Effects/ModFilter.h"
#include "Effects/Phaser.h"
#include "Effects/RingMod.h"
#include "LFO.h"
2026-07-23 23:40:48 +02:00
#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 }, isPingPong{ false };
2026-07-23 23:40:48 +02:00
int sampleStart{ 0 }, sampleEnd{ 0 }, loopStart{ 0 }, loopEnd{ 0 };
float pingPongDirection{ 1.0f };
2026-07-23 23:40:48 +02:00
/** 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;
2026-07-24 18:03:15 +02:00
// Modulation section
ModLFO modLFO;
ModFilter modFilter;
PhaserEffect modPhaser;
RingModEffect modRingMod;
juce::AudioBuffer<float> modLfoBuffer;
2026-07-23 23:40:48 +02:00
MTSClient* mtsClient{ nullptr };
};
static constexpr float INVERSE_SIN_SQUARED{ 1.f / (juce::MathConstants<float>::pi * juce::MathConstants<float>::pi) };