Skip to content

Commit

Permalink
Copy over LadderFilter implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
khrykin committed Feb 26, 2024
1 parent b38076e commit a067634
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 2 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ target_include_directories(BlackBird
target_sources(BlackBird
PRIVATE
source/dsp/DSPParameters.cpp
source/dsp/LadderFilter.cpp
source/ui/EditorHeader.cpp
source/ui/Knob.cpp
source/ui/PluginEditor.cpp
Expand Down
173 changes: 173 additions & 0 deletions source/dsp/LadderFilter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#include "LadderFilter.h"

namespace blackbird::dsp {

//==============================================================================
template <typename SampleType>
LadderFilter<SampleType>::LadderFilter() : state(2) {
setSampleRate(
SampleType(1000)); // intentionally setting unrealistic default
// sample rate to catch missing initialisation bugs
setResonance(SampleType(0));
setDrive(SampleType(1.2));

mode = Mode::LPF24;
setMode(Mode::LPF12);
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::setMode(Mode newMode) noexcept {
if (newMode == mode)
return;

switch (newMode) {
case Mode::LPF12:
A = {{SampleType(0), SampleType(0), SampleType(1), SampleType(0),
SampleType(0)}};
comp = SampleType(0.5);
break;
case Mode::HPF12:
A = {{SampleType(1), SampleType(-2), SampleType(1), SampleType(0),
SampleType(0)}};
comp = SampleType(0);
break;
case Mode::BPF12:
A = {{SampleType(0), SampleType(0), SampleType(-1), SampleType(1),
SampleType(0)}};
comp = SampleType(0.5);
break;
case Mode::LPF24:
A = {{SampleType(0), SampleType(0), SampleType(0), SampleType(0),
SampleType(1)}};
comp = SampleType(0.5);
break;
case Mode::HPF24:
A = {{SampleType(1), SampleType(-4), SampleType(6), SampleType(-4),
SampleType(1)}};
comp = SampleType(0);
break;
case Mode::BPF24:
A = {{SampleType(0), SampleType(0), SampleType(1), SampleType(-2),
SampleType(1)}};
comp = SampleType(0.5);
break;
default:
jassertfalse;
break;
}

static constexpr auto outputGain = SampleType(1.2);

for (auto &a : A)
a *= outputGain;

mode = newMode;
reset();
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::prepare(const ProcessSpec &spec) {
setSampleRate(SampleType(spec.sampleRate));
setNumChannels(spec.numChannels);
reset();
}

//==============================================================================
template <typename SampleType> void LadderFilter<SampleType>::reset() noexcept {
for (auto &s : state)
s.fill(SampleType(0));

cutoffTransformSmoother.setCurrentAndTargetValue(
cutoffTransformSmoother.getTargetValue());
scaledResonanceSmoother.setCurrentAndTargetValue(
scaledResonanceSmoother.getTargetValue());
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::setCutoffFrequencyHz(
SampleType newCutoff) noexcept {
jassert(newCutoff > SampleType(0));
cutoffFreqHz = newCutoff;
updateCutoffFreq();
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::setResonance(SampleType newResonance) noexcept {
jassert(newResonance >= SampleType(0) && newResonance <= SampleType(1));
resonance = newResonance;
updateResonance();
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::setDrive(SampleType newDrive) noexcept {
jassert(newDrive >= SampleType(1));

drive = newDrive;
gain = std::pow(drive, SampleType(-2.642)) * SampleType(0.6103) +
SampleType(0.3903);
drive2 = drive * SampleType(0.04) + SampleType(0.96);
gain2 = std::pow(drive2, SampleType(-2.642)) * SampleType(0.6103) +
SampleType(0.3903);
}

//==============================================================================
template <typename SampleType>
SampleType
LadderFilter<SampleType>::processSample(SampleType inputValue,
size_t channelToUse) noexcept {
auto &s = state[channelToUse];

const auto a1 = cutoffTransformValue;
const auto g = a1 * SampleType(-1) + SampleType(1);
const auto b0 = g * SampleType(0.76923076923);
const auto b1 = g * SampleType(0.23076923076);

const auto dx = gain * saturationLUT(drive * inputValue);
const auto a = dx + scaledResonanceValue * SampleType(-4) *
(gain2 * saturationLUT(drive2 * s[4]) - dx * comp);

const auto b = b1 * s[0] + a1 * s[1] + b0 * a;
const auto c = b1 * s[1] + a1 * s[2] + b0 * b;
const auto d = b1 * s[2] + a1 * s[3] + b0 * c;
const auto e = b1 * s[3] + a1 * s[4] + b0 * d;

s[0] = a;
s[1] = b;
s[2] = c;
s[3] = d;
s[4] = e;

return a * A[0] + b * A[1] + c * A[2] + d * A[3] + e * A[4];
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::updateSmoothers() noexcept {
cutoffTransformValue = cutoffTransformSmoother.getNextValue();
scaledResonanceValue = scaledResonanceSmoother.getNextValue();
}

//==============================================================================
template <typename SampleType>
void LadderFilter<SampleType>::setSampleRate(SampleType newValue) noexcept {
jassert(newValue > SampleType(0));
cutoffFreqScaler =
SampleType(-2.0 * juce::MathConstants<double>::pi) / newValue;

static constexpr SampleType smootherRampTimeSec = SampleType(0.05);
cutoffTransformSmoother.reset(newValue, smootherRampTimeSec);
scaledResonanceSmoother.reset(newValue, smootherRampTimeSec);

updateCutoffFreq();
}

//==============================================================================
template class LadderFilter<float>;
template class LadderFilter<double>;

} // namespace blackbird::dsp
127 changes: 127 additions & 0 deletions source/dsp/LadderFilter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#pragma once

#include <juce_dsp/juce_dsp.h>

namespace blackbird::dsp {

using namespace juce;
using namespace juce::dsp;

/**
Multi-mode filter based on the Moog ladder filter.
@tags{DSP}
*/
template <typename SampleType> class LadderFilter {
public:
//==============================================================================
using Mode = LadderFilterMode;

//==============================================================================
/** Creates an uninitialised filter. Call prepare() before first use. */
LadderFilter();

/** Enables or disables the filter. If disabled it will simply pass through
* the input signal. */
void setEnabled(bool isEnabled) noexcept { enabled = isEnabled; }

/** Sets filter mode. */
void setMode(Mode newMode) noexcept;

/** Initialises the filter. */
void prepare(const ProcessSpec &spec);

/** Returns the current number of channels. */
size_t getNumChannels() const noexcept { return state.size(); }

/** Resets the internal state variables of the filter. */
void reset() noexcept;

/** Sets the cutoff frequency of the filter.
@param newCutoff cutoff frequency in Hz
*/
void setCutoffFrequencyHz(SampleType newCutoff) noexcept;

/** Sets the resonance of the filter.
@param newResonance a value between 0 and 1; higher values increase the
resonance and can result in self oscillation!
*/
void setResonance(SampleType newResonance) noexcept;

/** Sets the amount of saturation in the filter.
@param newDrive saturation amount; it can be any number greater than or
equal to one. Higher values result in more distortion.
*/
void setDrive(SampleType newDrive) noexcept;

//==============================================================================
template <typename ProcessContext>
void process(const ProcessContext &context) noexcept {
const auto &inputBlock = context.getInputBlock();
auto &outputBlock = context.getOutputBlock();
const auto numChannels = outputBlock.getNumChannels();
const auto numSamples = outputBlock.getNumSamples();

jassert(inputBlock.getNumChannels() <= getNumChannels());
jassert(inputBlock.getNumChannels() == numChannels);
jassert(inputBlock.getNumSamples() == numSamples);

if (!enabled || context.isBypassed) {
outputBlock.copyFrom(inputBlock);
return;
}

for (size_t n = 0; n < numSamples; ++n) {
updateSmoothers();

for (size_t ch = 0; ch < numChannels; ++ch)
outputBlock.getChannelPointer(ch)[n] =
processSample(inputBlock.getChannelPointer(ch)[n], ch);
}
}

protected:
//==============================================================================
SampleType processSample(SampleType inputValue, size_t channelToUse) noexcept;
void updateSmoothers() noexcept;

private:
//==============================================================================
void setSampleRate(SampleType newValue) noexcept;
void setNumChannels(size_t newValue) { state.resize(newValue); }
void updateCutoffFreq() noexcept {
cutoffTransformSmoother.setTargetValue(
std::exp(cutoffFreqHz * cutoffFreqScaler));
}
void updateResonance() noexcept {
scaledResonanceSmoother.setTargetValue(
jmap(resonance, SampleType(0.1), SampleType(1.0)));
}

//==============================================================================
SampleType drive, drive2, gain, gain2, comp;

static constexpr size_t numStates = 5;
std::vector<std::array<SampleType, numStates>> state;
std::array<SampleType, numStates> A;

SmoothedValue<SampleType> cutoffTransformSmoother, scaledResonanceSmoother;
SampleType cutoffTransformValue, scaledResonanceValue;

LookupTableTransform<SampleType> saturationLUT{
[](SampleType x) { return std::tanh(x); }, SampleType(-5), SampleType(5),
128};

SampleType cutoffFreqHz{SampleType(200)};
SampleType resonance;

SampleType cutoffFreqScaler;

Mode mode;
bool enabled = true;
};

} // namespace blackbird::dsp
5 changes: 3 additions & 2 deletions source/dsp/Voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#pragma once

#include "DSPParameters.h"
#include "LadderFilter.h"
#include "LookupTablesBank.h"
#include "VCAOscillator.h"
#include <juce_audio_basics/juce_audio_basics.h>
Expand Down Expand Up @@ -222,7 +223,7 @@ class Voice : public SynthesiserVoice, private Timer {
};

dsp::ProcessorChain<VCAOscillator<float>, VCAOscillator<float>,
dsp::LadderFilter<float>, dsp::Gain<float>>
blackbird::dsp::LadderFilter<float>, dsp::Gain<float>>
processorChain;

dsp::Oscillator<float> lfo;
Expand All @@ -241,7 +242,7 @@ class Voice : public SynthesiserVoice, private Timer {

dsp::Gain<float> &gainProcessor() { return processorChain.get<gainIndex>(); }

dsp::LadderFilter<float> &filter() {
blackbird::dsp::LadderFilter<float> &filter() {
return processorChain.get<filterIndex>();
}

Expand Down

0 comments on commit a067634

Please sign in to comment.