pedalboard/plugins/IIRFilters.h (194 lines of code) (raw):
/*
* pedalboard
* Copyright 2021 Spotify AB
*
* Licensed under the GNU Public License, Version 3.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.gnu.org/licenses/gpl-3.0.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
#include "../JucePlugin.h"
namespace Pedalboard {
/**
* Given a cutoff frequency and sample rate, clamp the cutoff frequency to
* [e, sr / 2 - e] for some small epsilon, to ensure that filter frequency
* response is stable.
*/
inline float clampCutoffFrequency(float cutoffFrequencyHz, float sampleRate) {
return juce::jlimit(1e-2f, (sampleRate / 2) - 1e2f, cutoffFrequencyHz);
}
/**
* A base class for all IIR filter classes.
*/
template <typename SampleType>
class IIRFilter : public JucePlugin<juce::dsp::ProcessorDuplicator<
juce::dsp::IIR::Filter<SampleType>,
juce::dsp::IIR::Coefficients<SampleType>>> {
public:
void setCutoffFrequencyHz(float f) {
if (f <= 0)
throw std::domain_error("Cutoff frequency must be greater than 0Hz.");
cutoffFrequencyHz = f;
}
float getCutoffFrequencyHz() const noexcept { return cutoffFrequencyHz; }
void setQ(float q) {
if (q <= 0)
throw std::domain_error("Q value must be greater than 0.");
Q = q;
}
float getQ() const noexcept { return Q; }
void setGainDecibels(float dB) {
gainFactor = juce::Decibels::decibelsToGain<SampleType>(dB);
}
float getGainDecibels() const noexcept {
return juce::Decibels::gainToDecibels<SampleType>(gainFactor);
}
virtual void prepare(const juce::dsp::ProcessSpec &spec) override {
if (this->lastSpec.sampleRate != spec.sampleRate ||
this->lastSpec.maximumBlockSize < spec.maximumBlockSize ||
spec.numChannels != this->lastSpec.numChannels) {
JucePlugin<juce::dsp::ProcessorDuplicator<
juce::dsp::IIR::Filter<SampleType>,
juce::dsp::IIR::Coefficients<SampleType>>>::prepare(spec);
this->lastSpec = spec;
}
}
protected:
float cutoffFrequencyHz;
float Q;
float gainFactor;
};
template <typename SampleType>
class HighShelfFilter : public IIRFilter<SampleType> {
public:
virtual void prepare(const juce::dsp::ProcessSpec &spec) override {
*this->getDSP().state =
*juce::dsp::IIR::Coefficients<SampleType>::makeHighShelf(
spec.sampleRate,
clampCutoffFrequency(this->cutoffFrequencyHz, spec.sampleRate),
this->Q, this->gainFactor);
IIRFilter<SampleType>::prepare(spec);
}
};
template <typename SampleType>
class LowShelfFilter : public IIRFilter<SampleType> {
public:
virtual void prepare(const juce::dsp::ProcessSpec &spec) override {
*this->getDSP().state =
*juce::dsp::IIR::Coefficients<SampleType>::makeLowShelf(
spec.sampleRate,
clampCutoffFrequency(this->cutoffFrequencyHz, spec.sampleRate),
this->Q, this->gainFactor);
IIRFilter<SampleType>::prepare(spec);
}
};
template <typename SampleType> class PeakFilter : public IIRFilter<SampleType> {
public:
virtual void prepare(const juce::dsp::ProcessSpec &spec) override {
*this->getDSP().state =
*juce::dsp::IIR::Coefficients<SampleType>::makePeakFilter(
spec.sampleRate,
clampCutoffFrequency(this->cutoffFrequencyHz, spec.sampleRate),
this->Q, this->gainFactor);
IIRFilter<SampleType>::prepare(spec);
}
};
inline void init_iir_filters(py::module &m) {
py::class_<IIRFilter<float>, Plugin, std::shared_ptr<IIRFilter<float>>>(
m, "IIRFilter",
"An abstract class that implements various kinds of infinite impulse "
"response (IIR) filter designs. This should not be used directly; use "
":class:`HighShelfFilter`, :class:`LowShelfFilter`, or "
":class:`PeakFilter` directly instead.")
.def(py::init([]() {
throw std::runtime_error(
"IIRFilter is not designed to be instantiated directly: "
"use HighShelfFilter, LowShelfFilter, or PeakFilter instead.");
return nullptr;
}));
py::class_<HighShelfFilter<float>, IIRFilter<float>,
std::shared_ptr<HighShelfFilter<float>>>(
m, "HighShelfFilter",
"A high shelf filter plugin with variable Q and gain, as would be used "
"in an equalizer. Frequencies above the cutoff frequency will be boosted "
"(or cut) by the provided gain (in decibels).")
.def(py::init([](float cutoffFrequencyHz, float gaindB, float Q) {
auto plugin = std::make_unique<HighShelfFilter<float>>();
plugin->setCutoffFrequencyHz(cutoffFrequencyHz);
plugin->setGainDecibels(gaindB);
plugin->setQ(Q);
return plugin;
}),
py::arg("cutoff_frequency_hz") = 440, py::arg("gain_db") = 0.0,
py::arg("q") = (juce::MathConstants<float>::sqrt2 / 2.0))
.def("__repr__",
[](const HighShelfFilter<float> &plugin) {
std::ostringstream ss;
ss << "<pedalboard.HighShelfFilter";
ss << " cutoff_frequency_hz=" << plugin.getCutoffFrequencyHz();
ss << " gain_db=" << plugin.getGainDecibels();
ss << " q=" << plugin.getQ();
ss << " at " << &plugin;
ss << ">";
return ss.str();
})
.def_property("cutoff_frequency_hz",
&HighShelfFilter<float>::getCutoffFrequencyHz,
&HighShelfFilter<float>::setCutoffFrequencyHz)
.def_property("gain_db", &HighShelfFilter<float>::getGainDecibels,
&HighShelfFilter<float>::setGainDecibels)
.def_property("q", &HighShelfFilter<float>::getQ,
&HighShelfFilter<float>::setQ);
py::class_<LowShelfFilter<float>, IIRFilter<float>,
std::shared_ptr<LowShelfFilter<float>>>(
m, "LowShelfFilter",
"A low shelf filter with variable Q and gain, as would be used in an "
"equalizer. Frequencies below the cutoff frequency will be boosted (or "
"cut) by the provided gain value.")
.def(py::init([](float cutoffFrequencyHz, float gaindB, float Q) {
auto plugin = std::make_unique<LowShelfFilter<float>>();
plugin->setCutoffFrequencyHz(cutoffFrequencyHz);
plugin->setGainDecibels(gaindB);
plugin->setQ(Q);
return plugin;
}),
py::arg("cutoff_frequency_hz") = 440, py::arg("gain_db") = 0.0,
py::arg("q") = (juce::MathConstants<float>::sqrt2 / 2.0))
.def("__repr__",
[](const LowShelfFilter<float> &plugin) {
std::ostringstream ss;
ss << "<pedalboard.LowShelfFilter";
ss << " cutoff_frequency_hz=" << plugin.getCutoffFrequencyHz();
ss << " gain_db=" << plugin.getGainDecibels();
ss << " q=" << plugin.getQ();
ss << " at " << &plugin;
ss << ">";
return ss.str();
})
.def_property("cutoff_frequency_hz",
&LowShelfFilter<float>::getCutoffFrequencyHz,
&LowShelfFilter<float>::setCutoffFrequencyHz)
.def_property("gain_db", &LowShelfFilter<float>::getGainDecibels,
&LowShelfFilter<float>::setGainDecibels)
.def_property("q", &LowShelfFilter<float>::getQ,
&LowShelfFilter<float>::setQ);
py::class_<PeakFilter<float>, IIRFilter<float>,
std::shared_ptr<PeakFilter<float>>>(
m, "PeakFilter",
"A peak (or notch) filter with variable Q and gain, as would be used in "
"an equalizer. Frequencies around the cutoff frequency will be boosted "
"(or cut) by the provided gain value.")
.def(py::init([](float cutoffFrequencyHz, float gaindB, float Q) {
auto plugin = std::make_unique<PeakFilter<float>>();
plugin->setCutoffFrequencyHz(cutoffFrequencyHz);
plugin->setGainDecibels(gaindB);
plugin->setQ(Q);
return plugin;
}),
py::arg("cutoff_frequency_hz") = 440, py::arg("gain_db") = 0.0,
py::arg("q") = (juce::MathConstants<float>::sqrt2 / 2.0))
.def("__repr__",
[](const PeakFilter<float> &plugin) {
std::ostringstream ss;
ss << "<pedalboard.PeakFilter";
ss << " cutoff_frequency_hz=" << plugin.getCutoffFrequencyHz();
ss << " gain_db=" << plugin.getGainDecibels();
ss << " q=" << plugin.getQ();
ss << " at " << &plugin;
ss << ">";
return ss.str();
})
.def_property("cutoff_frequency_hz",
&PeakFilter<float>::getCutoffFrequencyHz,
&PeakFilter<float>::setCutoffFrequencyHz)
.def_property("gain_db", &PeakFilter<float>::getGainDecibels,
&PeakFilter<float>::setGainDecibels)
.def_property("q", &PeakFilter<float>::getQ, &PeakFilter<float>::setQ);
}
}; // namespace Pedalboard