|
|
|
@@ -20,16 +20,74 @@ START_NAMESPACE_DISTRHO |
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
1-pole lowpass filter to smooth out parameters and envelopes.
|
|
|
|
This filter is guaranteed not to overshoot.
|
|
|
|
*/
|
|
|
|
class Smoother {
|
|
|
|
private:
|
|
|
|
float kp;
|
|
|
|
|
|
|
|
public:
|
|
|
|
float value;
|
|
|
|
|
|
|
|
Smoother()
|
|
|
|
: kp(0.0f),
|
|
|
|
value(0.0f) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Set kp from cutoff frequency in Hz.
|
|
|
|
For derivation, see the answer of Matt L. on the url below. Equation 3 is used.
|
|
|
|
|
|
|
|
Computation is done on double for accuracy. When using float, kp will be inaccurate
|
|
|
|
if the cutoffHz is below around 3.0 to 4.0 Hz.
|
|
|
|
|
|
|
|
Reference:
|
|
|
|
- [Single-pole IIR low-pass filter - which is the correct formula for the decay coefficient? - Signal Processing Stack Exchange](https://dsp.stackexchange.com/questions/54086/single-pole-iir-low-pass-filter-which-is-the-correct-formula-for-the-decay-coe)
|
|
|
|
*/
|
|
|
|
void setCutoff(float sampleRate, float cutoffHz) {
|
|
|
|
double omega_c = 2.0 * M_PI * cutoffHz / sampleRate;
|
|
|
|
double y = 1.0 - cos(omega_c);
|
|
|
|
kp = float(-y + sqrt((y + 2.0) * y));
|
|
|
|
}
|
|
|
|
|
|
|
|
float process(float input) {
|
|
|
|
return value += kp * (input - value);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
Plugin that demonstrates tempo sync in DPF.
|
|
|
|
The tempo sync implementation is on the first if branch in run() method.
|
|
|
|
*/
|
|
|
|
class ExamplePluginMetronome : public Plugin
|
|
|
|
{
|
|
|
|
private:
|
|
|
|
enum ParameterIndex : uint32_t {
|
|
|
|
pGain,
|
|
|
|
pDecayTime,
|
|
|
|
pSemitone,
|
|
|
|
pCent,
|
|
|
|
|
|
|
|
N_PARAMETERS,
|
|
|
|
};
|
|
|
|
|
|
|
|
public:
|
|
|
|
ExamplePluginMetronome()
|
|
|
|
: Plugin(0, 0, 0), // 0 parameters, 0 programs, 0 states
|
|
|
|
: Plugin(N_PARAMETERS, 0, 0), // 4 parameters, 0 programs, 0 states
|
|
|
|
sampleRate(44100.0f),
|
|
|
|
counter(0) {}
|
|
|
|
counter(0),
|
|
|
|
phase(0.0f),
|
|
|
|
decay(0.0f),
|
|
|
|
gain(0.5f),
|
|
|
|
semitone(72),
|
|
|
|
cent(0),
|
|
|
|
decayTime(0.2f)
|
|
|
|
{
|
|
|
|
sampleRateChanged(sampleRate);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
/* --------------------------------------------------------------------------------------------------------
|
|
|
|
@@ -101,26 +159,88 @@ protected: |
|
|
|
Initialize the parameter @a index.
|
|
|
|
This function will be called once, shortly after the plugin is created.
|
|
|
|
*/
|
|
|
|
void initParameter(uint32_t /* index */, Parameter& /* parameter */) override
|
|
|
|
void initParameter(uint32_t index, Parameter& parameter) override
|
|
|
|
{
|
|
|
|
parameter.hints = kParameterIsAutomable;
|
|
|
|
|
|
|
|
switch (index)
|
|
|
|
{
|
|
|
|
case pGain:
|
|
|
|
parameter.name = "Gain";
|
|
|
|
parameter.hints |= kParameterIsLogarithmic;
|
|
|
|
parameter.ranges.min = 0.0f;
|
|
|
|
parameter.ranges.max = 1.0f;
|
|
|
|
parameter.ranges.def = 0.5f;
|
|
|
|
break;
|
|
|
|
case pDecayTime:
|
|
|
|
parameter.name = "DecayTime";
|
|
|
|
parameter.hints |= kParameterIsLogarithmic;
|
|
|
|
parameter.ranges.min = 0.001f;
|
|
|
|
parameter.ranges.max = 1.0f;
|
|
|
|
parameter.ranges.def = 0.2f;
|
|
|
|
break;
|
|
|
|
case pSemitone:
|
|
|
|
parameter.name = "Semitone";
|
|
|
|
parameter.hints |= kParameterIsInteger;
|
|
|
|
parameter.ranges.min = 0;
|
|
|
|
parameter.ranges.max = 127;
|
|
|
|
parameter.ranges.def = 72;
|
|
|
|
break;
|
|
|
|
case pCent:
|
|
|
|
parameter.name = "Cent";
|
|
|
|
parameter.hints |= kParameterIsInteger;
|
|
|
|
parameter.ranges.min = -100;
|
|
|
|
parameter.ranges.max = 100;
|
|
|
|
parameter.ranges.def = 0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
parameter.symbol = parameter.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------------------------------------------------
|
|
|
|
* Internal data */
|
|
|
|
|
|
|
|
/**
|
|
|
|
Get the current value of a parameter.
|
|
|
|
*/
|
|
|
|
float getParameterValue(uint32_t /* index */) const override
|
|
|
|
float getParameterValue(uint32_t index) const override
|
|
|
|
{
|
|
|
|
switch (index)
|
|
|
|
{
|
|
|
|
case pGain:
|
|
|
|
return gain;
|
|
|
|
case pDecayTime:
|
|
|
|
return decayTime;
|
|
|
|
case pSemitone:
|
|
|
|
return semitone;
|
|
|
|
case pCent:
|
|
|
|
return cent;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Change a parameter value.
|
|
|
|
*/
|
|
|
|
void setParameterValue(uint32_t /* index */, float /* value */) override
|
|
|
|
void setParameterValue(uint32_t index, float value) override
|
|
|
|
{
|
|
|
|
switch (index)
|
|
|
|
{
|
|
|
|
case pGain:
|
|
|
|
gain = value;
|
|
|
|
break;
|
|
|
|
case pDecayTime:
|
|
|
|
decayTime = value;
|
|
|
|
break;
|
|
|
|
case pSemitone:
|
|
|
|
semitone = value;
|
|
|
|
break;
|
|
|
|
case pCent:
|
|
|
|
cent = value;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------------------------------------------------
|
|
|
|
@@ -135,33 +255,62 @@ protected: |
|
|
|
const TimePosition& timePos(getTimePosition());
|
|
|
|
|
|
|
|
if (timePos.playing && timePos.bbt.valid) {
|
|
|
|
|
|
|
|
// Better to use double when manipulating time.
|
|
|
|
double secondsPerBeat = 60.0 / timePos.bbt.beatsPerMinute;
|
|
|
|
double framesPerBeat = sampleRate * secondsPerBeat;
|
|
|
|
double beatFraction = timePos.bbt.tick / timePos.bbt.ticksPerBeat;
|
|
|
|
|
|
|
|
|
|
|
|
// If beatFraction == 0.0, next beat is exactly at the start of currenct cycle.
|
|
|
|
// Otherwise, reset counter to the frames to the next beat.
|
|
|
|
counter = beatFraction == 0.0
|
|
|
|
? 0
|
|
|
|
: static_cast<uint32_t>(framesPerBeat * (1.0 - beatFraction));
|
|
|
|
|
|
|
|
// Compute deltaPhase in normalized frequency.
|
|
|
|
// semitone is midi note number, which is A4 (440Hz at standard tuning) at 69.
|
|
|
|
// Frequency goes up to 1 octave higher at the start of bar.
|
|
|
|
float frequency = 440.0f * powf(2.0f, (100.0f * (semitone - 69.0f) + cent) / 1200.0f);
|
|
|
|
float deltaPhase = frequency / sampleRate;
|
|
|
|
float octave = timePos.bbt.beat == 1 ? 2.0f : 1.0f;
|
|
|
|
|
|
|
|
// Envelope reaches 1e-5 at decayTime after triggering.
|
|
|
|
decay = pow(1e-5, 1.0 / (decayTime * sampleRate));
|
|
|
|
|
|
|
|
// Reset phase and frequency at the start of transpose.
|
|
|
|
if (!wasPlaying) {
|
|
|
|
phase = 0.0f;
|
|
|
|
|
|
|
|
deltaPhaseSmoother.value = deltaPhase;
|
|
|
|
gainSmoother.value = 1.0f;
|
|
|
|
envelopeSmoother.value = 0.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < frames; ++i) {
|
|
|
|
if (counter <= 0) {
|
|
|
|
outputs[0][i] = 1.0f;
|
|
|
|
envelope = 1.0f;
|
|
|
|
counter = uint32_t(framesPerBeat);
|
|
|
|
} else {
|
|
|
|
outputs[0][i] = 0.0f;
|
|
|
|
octave = !wasPlaying || timePos.bbt.beat == timePos.bbt.beatsPerBar ? 2.0f : 1.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
--counter;
|
|
|
|
|
|
|
|
envelope *= decay;
|
|
|
|
|
|
|
|
phase += octave * deltaPhaseSmoother.process(deltaPhase);
|
|
|
|
phase -= floorf(phase);
|
|
|
|
|
|
|
|
outputs[0][i] = gainSmoother.process(gain)
|
|
|
|
* envelopeSmoother.process(envelope)
|
|
|
|
* sinf(float(2.0 * M_PI) * phase);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Stop metronome if not playing or timePos.bbt is invalid.
|
|
|
|
for (uint32_t i = 0; i < frames; ++i) outputs[0][i] = 0.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
wasPlaying = timePos.playing;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------------------------------------------------------
|
|
|
|
* Callbacks (optional) */
|
|
|
|
|
|
|
|
@@ -172,6 +321,11 @@ protected: |
|
|
|
void sampleRateChanged(double newSampleRate) override
|
|
|
|
{
|
|
|
|
sampleRate = newSampleRate;
|
|
|
|
|
|
|
|
// Cutoff value was tuned manually.
|
|
|
|
deltaPhaseSmoother.setCutoff(sampleRate, 100.0f);
|
|
|
|
gainSmoother.setCutoff(sampleRate, 500.0f);
|
|
|
|
envelopeSmoother.setCutoff(sampleRate, 250.0f);
|
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------------------------------------
|
|
|
|
@@ -179,6 +333,20 @@ protected: |
|
|
|
private:
|
|
|
|
float sampleRate;
|
|
|
|
uint32_t counter; // Stores number of frames to the next beat.
|
|
|
|
bool wasPlaying; // Used to reset phase and frequency at the start of transpose.
|
|
|
|
float phase; // Sine wave phase. Normalized in [0, 1).
|
|
|
|
float envelope; // Current value of gain envelope.
|
|
|
|
float decay; // Coefficient to decay envelope in a frame.
|
|
|
|
|
|
|
|
Smoother deltaPhaseSmoother;
|
|
|
|
Smoother gainSmoother;
|
|
|
|
Smoother envelopeSmoother;
|
|
|
|
|
|
|
|
// Parameters.
|
|
|
|
float gain;
|
|
|
|
float semitone;
|
|
|
|
float cent;
|
|
|
|
float decayTime;
|
|
|
|
|
|
|
|
/**
|
|
|
|
Set our plugin class as non-copyable and add a leak detector just in case.
|
|
|
|
|