Browse Source

Change metronome example to output sine wave

pull/218/head
cu 5 years ago
parent
commit
ca821aa979
2 changed files with 190 additions and 13 deletions
  1. +180
    -12
      examples/Metronome/ExamplePluginMetronome.cpp
  2. +10
    -1
      examples/Metronome/README.md

+ 180
- 12
examples/Metronome/ExamplePluginMetronome.cpp View File

@@ -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.


+ 10
- 1
examples/Metronome/README.md View File

@@ -2,7 +2,16 @@

This example will show tempo sync in DPF.<br/>

This plugin will output impulse the start of every beat.<br/>
This plugin will output sine wave at the start of every beat.<br/>
The pitch of sine wave is 1 octave higher at the start of every bar.<br/>

4 parameters are avaialble:

- Gain
- Decay time
- Semitone
- Cent

To calculate exact frames to the next beat from the start of current audio buffer, `TimePosition::BarBeatTick.barBeat` is used.<br/>

Reference:


Loading…
Cancel
Save