diff --git a/CHANGELOG.md b/CHANGELOG.md index f055f2a..89378e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 1.3.0 (2020-05-29) +- Add EQ Filter. + ### 1.2.1 (2020-04-23) - Multiples - Make polyphonic. diff --git a/plugin.json b/plugin.json index 8740603..226b509 100644 --- a/plugin.json +++ b/plugin.json @@ -239,7 +239,8 @@ "modularGridUrl": "https://www.modulargrid.net/e/mutable-instruments-shelves-2015", "tags": [ "Equalizer", - "Filter" + "Filter", + "Polyphonic" ] } ] diff --git a/src/Shelves.cpp b/src/Shelves.cpp index 5bfa1c6..d5d5761 100644 --- a/src/Shelves.cpp +++ b/src/Shelves.cpp @@ -1,4 +1,15 @@ #include "plugin.hpp" +#include "Shelves/shelves.hpp" + + +static const float freqMin = std::log2(shelves::kFreqKnobMin); +static const float freqMax = std::log2(shelves::kFreqKnobMax); +static const float freqInit = (freqMin + freqMax) / 2; +static const float gainMin = -shelves::kGainKnobRange; +static const float gainMax = shelves::kGainKnobRange; +static const float qMin = std::log2(shelves::kQKnobMin); +static const float qMax = std::log2(shelves::kQKnobMax); +static const float qInit = (qMin + qMax) / 2; struct Shelves : Module { @@ -46,21 +57,122 @@ struct Shelves : Module { NUM_LIGHTS }; + shelves::ShelvesEngine engines[16]; + bool preGain; + Shelves() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configParam(HS_FREQ_PARAM, 0.f, 1.f, 0.f, "High-shelf frequency"); - configParam(HS_GAIN_PARAM, 0.f, 1.f, 0.f, "High-shelf gain"); - configParam(P1_FREQ_PARAM, 0.f, 1.f, 0.f, "Parametric 1 frequency"); - configParam(P1_GAIN_PARAM, 0.f, 1.f, 0.f, "Parametric 1 gain"); - configParam(P1_Q_PARAM, 0.f, 1.f, 0.f, "Parametric 1 quality"); - configParam(P2_FREQ_PARAM, 0.f, 1.f, 0.f, "Parametric 2 frequency"); - configParam(P2_GAIN_PARAM, 0.f, 1.f, 0.f, "Parametric 2 gain"); - configParam(P2_Q_PARAM, 0.f, 1.f, 0.f, "Parametric 2 quality"); - configParam(LS_FREQ_PARAM, 0.f, 1.f, 0.f, "Low-shelf frequency"); - configParam(LS_GAIN_PARAM, 0.f, 1.f, 0.f, "Low-shelf gain"); + + configParam(HS_FREQ_PARAM, freqMin, freqMax, freqInit, "High-shelf frequency", " Hz", 2.f); + configParam(P1_FREQ_PARAM, freqMin, freqMax, freqInit, "Parametric 1 frequency", " Hz", 2.f); + configParam(P2_FREQ_PARAM, freqMin, freqMax, freqInit, "Parametric 2 frequency", " Hz", 2.f); + configParam(LS_FREQ_PARAM, freqMin, freqMax, freqInit, "Low-shelf frequency", " Hz", 2.f); + + configParam(HS_GAIN_PARAM, gainMin, gainMax, 0.f, "High-shelf gain", " dB"); + configParam(P1_GAIN_PARAM, gainMin, gainMax, 0.f, "Parametric 1 gain", " dB"); + configParam(P2_GAIN_PARAM, gainMin, gainMax, 0.f, "Parametric 2 gain", " dB"); + configParam(LS_GAIN_PARAM, gainMin, gainMax, 0.f, "Low-shelf gain", " dB"); + + configParam(P1_Q_PARAM, qMin, qMax, qInit, "Parametric 1 quality", "", 2.f); + configParam(P2_Q_PARAM, qMin, qMax, qInit, "Parametric 2 quality", "", 2.f); + + onReset(); + } + + void onReset() override { + preGain = false; + onSampleRateChange(); + } + + void onSampleRateChange() override { + // TODO In Rack v2, replace with args.sampleRate + for (int c = 0; c < 16; c++) { + engines[c].setSampleRate(APP->engine->getSampleRate()); + } } void process(const ProcessArgs& args) override { + int channels = std::max(inputs[IN_INPUT].getChannels(), 1); + + // Reuse the same frame object for multiple engines because the params aren't touched. + shelves::ShelvesEngine::Frame frame; + frame.pre_gain = preGain; + + frame.hs_freq_knob = rescale(params[HS_FREQ_PARAM].getValue(), freqMin, freqMax, 0.f, 1.f); + frame.p1_freq_knob = rescale(params[P1_FREQ_PARAM].getValue(), freqMin, freqMax, 0.f, 1.f); + frame.p2_freq_knob = rescale(params[P2_FREQ_PARAM].getValue(), freqMin, freqMax, 0.f, 1.f); + frame.ls_freq_knob = rescale(params[LS_FREQ_PARAM].getValue(), freqMin, freqMax, 0.f, 1.f); + + frame.hs_gain_knob = params[HS_GAIN_PARAM].getValue() / shelves::kGainKnobRange; + frame.p1_gain_knob = params[P1_GAIN_PARAM].getValue() / shelves::kGainKnobRange; + frame.p2_gain_knob = params[P2_GAIN_PARAM].getValue() / shelves::kGainKnobRange; + frame.ls_gain_knob = params[LS_GAIN_PARAM].getValue() / shelves::kGainKnobRange; + + frame.p1_q_knob = rescale(params[P1_Q_PARAM].getValue(), qMin, qMax, 0.f, 1.f); + frame.p2_q_knob = rescale(params[P2_Q_PARAM].getValue(), qMin, qMax, 0.f, 1.f); + + frame.hs_freq_cv_connected = inputs[HS_FREQ_INPUT].isConnected(); + frame.hs_gain_cv_connected = inputs[HS_GAIN_INPUT].isConnected(); + frame.p1_freq_cv_connected = inputs[P1_FREQ_INPUT].isConnected(); + frame.p1_gain_cv_connected = inputs[P1_GAIN_INPUT].isConnected(); + frame.p1_q_cv_connected = inputs[P1_Q_INPUT].isConnected(); + frame.p2_freq_cv_connected = inputs[P2_FREQ_INPUT].isConnected(); + frame.p2_gain_cv_connected = inputs[P2_GAIN_INPUT].isConnected(); + frame.p2_q_cv_connected = inputs[P2_Q_INPUT].isConnected(); + frame.ls_freq_cv_connected = inputs[LS_FREQ_INPUT].isConnected(); + frame.ls_gain_cv_connected = inputs[LS_GAIN_INPUT].isConnected(); + frame.global_freq_cv_connected = inputs[FREQ_INPUT].isConnected(); + frame.global_gain_cv_connected = inputs[GAIN_INPUT].isConnected(); + + frame.p1_hp_out_connected = outputs[P1_HP_OUTPUT].isConnected(); + frame.p1_bp_out_connected = outputs[P1_BP_OUTPUT].isConnected(); + frame.p1_lp_out_connected = outputs[P1_LP_OUTPUT].isConnected(); + frame.p2_hp_out_connected = outputs[P2_HP_OUTPUT].isConnected(); + frame.p2_bp_out_connected = outputs[P2_BP_OUTPUT].isConnected(); + frame.p2_lp_out_connected = outputs[P2_LP_OUTPUT].isConnected(); + + float clipLight = 0.f; + + for (int c = 0; c < channels; c++) { + frame.main_in = inputs[IN_INPUT].getVoltage(c); + frame.hs_freq_cv = inputs[HS_FREQ_INPUT].getPolyVoltage(c); + frame.hs_gain_cv = inputs[HS_GAIN_INPUT].getPolyVoltage(c); + frame.p1_freq_cv = inputs[P1_FREQ_INPUT].getPolyVoltage(c); + frame.p1_gain_cv = inputs[P1_GAIN_INPUT].getPolyVoltage(c); + frame.p1_q_cv = inputs[P1_Q_INPUT].getPolyVoltage(c); + frame.p2_freq_cv = inputs[P2_FREQ_INPUT].getPolyVoltage(c); + frame.p2_gain_cv = inputs[P2_GAIN_INPUT].getPolyVoltage(c); + frame.p2_q_cv = inputs[P2_Q_INPUT].getPolyVoltage(c); + frame.ls_freq_cv = inputs[LS_FREQ_INPUT].getPolyVoltage(c); + frame.ls_gain_cv = inputs[LS_GAIN_INPUT].getPolyVoltage(c); + frame.global_freq_cv = inputs[FREQ_INPUT].getPolyVoltage(c); + frame.global_gain_cv = inputs[GAIN_INPUT].getPolyVoltage(c); + + engines[c].process(frame); + + outputs[P1_HP_OUTPUT].setVoltage(frame.p1_hp_out); + outputs[P1_BP_OUTPUT].setVoltage(frame.p1_bp_out); + outputs[P1_LP_OUTPUT].setVoltage(frame.p1_lp_out); + outputs[P2_HP_OUTPUT].setVoltage(frame.p2_hp_out); + outputs[P2_BP_OUTPUT].setVoltage(frame.p2_bp_out); + outputs[P2_LP_OUTPUT].setVoltage(frame.p2_lp_out); + outputs[OUT_OUTPUT].setVoltage(frame.main_out); + clipLight += frame.clip; + } + + lights[CLIP_LIGHT].setSmoothBrightness(clipLight, args.sampleTime); + } + + json_t* dataToJson() override { + json_t* root_j = json_object(); + json_object_set_new(root_j, "preGain", json_boolean(preGain)); + return root_j; + } + + void dataFromJson(json_t* root_j) override { + json_t* preGainJ = json_object_get(root_j, "preGain"); + if (preGainJ) + preGain = json_boolean_value(preGainJ); } }; @@ -110,7 +222,24 @@ struct ShelvesWidget : ModuleWidget { addChild(createLightCentered>(mm2px(Vec(53.629, 109.475)), module, Shelves::CLIP_LIGHT)); } + + void appendContextMenu(Menu* menu) override { + Shelves* module = dynamic_cast(this->module); + + menu->addChild(new MenuSeparator); + + struct PreGainItem : MenuItem { + Shelves* module; + void onAction(const event::Action& e) override { + module->preGain ^= true; + } + }; + + PreGainItem* preGainItem = createMenuItem("Pad input by -6dB", CHECKMARK(module->preGain)); + preGainItem->module = module; + menu->addChild(preGainItem); + } }; -Model* modelShelves = createModel("Shelves"); \ No newline at end of file +Model* modelShelves = createModel("Shelves"); diff --git a/src/Shelves/aafilter.hpp b/src/Shelves/aafilter.hpp new file mode 100644 index 0000000..3073a22 --- /dev/null +++ b/src/Shelves/aafilter.hpp @@ -0,0 +1,535 @@ +// Anti-aliasing filters for common sample rates +// Copyright (C) 2020 Tyler Coy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "sos.hpp" + +namespace shelves +{ + +/*[[[cog +import math +import aafilter + +# We design our filters to keep aliasing out of this band +audio_bw = 20000 + +# We assume the client process generates no frequency content above this +# multiple of the original bandwidth +max_bw_mult = 3 + +rpass = 0.1 # Maximum passband ripple in dB +rstop = 100 # Minimum stopband attenuation in dB + +# Generate filters for these sampling rates +common_rates = [ + 8000, + 11025, 12000, + 22050, 24000, + 44100, 48000, + 88200, 96000, + 176400, 192000, + 352800, 384000, + 705600, 768000 +] + +# Oversample to at least this frequency +min_oversampled_rate = audio_bw * 2 * 3 + +up_filters = list() +down_filters = list() +oversampling_factors = dict() +max_num_sections = 0 + +# For each sample rate, design a pair of upsampling and downsampling filters. +# For the upsampling filter, the stopband must be placed such that the client's +# multiplied bandwidth won't reach into the aliased audio band. For the +# downsampling filter, the stopband must be placed such that all foldover falls +# above the audio band. +for fs in common_rates: + os = math.ceil(min_oversampled_rate / fs) + oversampling_factors[fs] = os + fpass = min(audio_bw, 0.475 * fs) + critical_bw = fpass if fpass >= audio_bw else fs / 2 + up_fstop = min(fs * os / 2, (fs * os - critical_bw) / max_bw_mult) + down_fstop = min(fs * os / 2, fs - critical_bw) + + up = aafilter.design(fs, os, fpass, up_fstop, rpass, rstop) + down = aafilter.design(fs, os, fpass, down_fstop, rpass, rstop) + max_num_sections = max(max_num_sections, len(up.sections), len(down.sections)) + up_filters.append(up) + down_filters.append(down) + +cog.outl('static constexpr int kMaxNumSections = {};' + .format(max_num_sections)) +]]]*/ +static constexpr int kMaxNumSections = 8; +//[[[end]]] + +inline int SampleRateID(float sample_rate) +{ + if (false) {} + /*[[[cog + for fs in sorted(common_rates, reverse=True): + cog.outl('else if ({} <= sample_rate) return {};'.format(fs, fs)) + cog.outl('else return {};'.format(min(common_rates))) + ]]]*/ + else if (768000 <= sample_rate) return 768000; + else if (705600 <= sample_rate) return 705600; + else if (384000 <= sample_rate) return 384000; + else if (352800 <= sample_rate) return 352800; + else if (192000 <= sample_rate) return 192000; + else if (176400 <= sample_rate) return 176400; + else if (96000 <= sample_rate) return 96000; + else if (88200 <= sample_rate) return 88200; + else if (48000 <= sample_rate) return 48000; + else if (44100 <= sample_rate) return 44100; + else if (24000 <= sample_rate) return 24000; + else if (22050 <= sample_rate) return 22050; + else if (12000 <= sample_rate) return 12000; + else if (11025 <= sample_rate) return 11025; + else if (8000 <= sample_rate) return 8000; + else return 8000; + //[[[end]]] +} + +inline int OversamplingFactor(float sample_rate) +{ + switch (SampleRateID(sample_rate)) + { + default: + /*[[[cog + for fs in sorted(common_rates): + cog.outl('case {}: return {};'.format(fs, oversampling_factors[fs])) + ]]]*/ + case 8000: return 15; + case 11025: return 11; + case 12000: return 10; + case 22050: return 6; + case 24000: return 5; + case 44100: return 3; + case 48000: return 3; + case 88200: return 2; + case 96000: return 2; + case 176400: return 1; + case 192000: return 1; + case 352800: return 1; + case 384000: return 1; + case 705600: return 1; + case 768000: return 1; + //[[[end]]] + } +} + +template +class AAFilter +{ +public: + void Init(float sample_rate) + { + InitFilter(sample_rate); + } + + T Process(T in) + { + return filter_.Process(in); + } + +protected: + SOSFilter filter_; + + virtual void InitFilter(float sample_rate) = 0; +}; + +template +class UpsamplingAAFilter : public AAFilter +{ + void InitFilter(float sample_rate) override + { + switch (SampleRateID(sample_rate)) + { + default: + /*[[[cog + aafilter.print_filter_cases(up_filters) + ]]]*/ + case 8000: // o = 15, fp = 3800, fst = 38666, cost = 240000 + { + const SOSCoefficients kFilter8000x15[2] = + { + { {1.44208376e-04, 2.15422675e-04, 1.44208376e-04, }, {-1.75298317e+00, 7.75007227e-01, } }, + { {1.00000000e+00, 1.72189731e-01, 1.00000000e+00, }, {-1.85199502e+00, 9.01687724e-01, } }, + }; + AAFilter::filter_.Init(2, kFilter8000x15); + break; + } + case 11025: // o = 11, fp = 5236, fst = 38587, cost = 242550 + { + const SOSCoefficients kFilter11025x11[2] = + { + { {3.47236726e-04, 5.94611382e-04, 3.47236726e-04, }, {-1.66651262e+00, 7.05884392e-01, } }, + { {1.00000000e+00, 7.58730216e-01, 1.00000000e+00, }, {-1.77900341e+00, 8.69327961e-01, } }, + }; + AAFilter::filter_.Init(2, kFilter11025x11); + break; + } + case 12000: // o = 10, fp = 5699, fst = 38000, cost = 240000 + { + const SOSCoefficients kFilter12000x10[2] = + { + { {4.63786610e-04, 8.16220909e-04, 4.63786610e-04, }, {-1.63450649e+00, 6.81471340e-01, } }, + { {1.00000000e+00, 9.17818354e-01, 1.00000000e+00, }, {-1.74936370e+00, 8.57701633e-01, } }, + }; + AAFilter::filter_.Init(2, kFilter12000x10); + break; + } + case 22050: // o = 6, fp = 10473, fst = 40425, cost = 396900 + { + const SOSCoefficients kFilter22050x6[3] = + { + { {1.95909107e-04, 3.07811266e-04, 1.95909107e-04, }, {-1.58181808e+00, 6.40141057e-01, } }, + { {1.00000000e+00, 1.34444168e-01, 1.00000000e+00, }, {-1.58691814e+00, 7.40684153e-01, } }, + { {1.00000000e+00, -4.56209108e-01, 1.00000000e+00, }, {-1.64635749e+00, 9.03421507e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter22050x6); + break; + } + case 24000: // o = 5, fp = 11399, fst = 36000, cost = 360000 + { + const SOSCoefficients kFilter24000x5[3] = + { + { {3.60375579e-04, 6.11714197e-04, 3.60375579e-04, }, {-1.50089044e+00, 5.82797128e-01, } }, + { {1.00000000e+00, 5.06808919e-01, 1.00000000e+00, }, {-1.48367876e+00, 6.99513376e-01, } }, + { {1.00000000e+00, -8.08861216e-02, 1.00000000e+00, }, {-1.52492835e+00, 8.87536413e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter24000x5); + break; + } + case 44100: // o = 3, fp = 20000, fst = 37433, cost = 529200 + { + const SOSCoefficients kFilter44100x3[4] = + { + { {6.47358611e-04, 1.15520581e-03, 6.47358611e-04, }, {-1.35050917e+00, 4.84676642e-01, } }, + { {1.00000000e+00, 7.82770646e-01, 1.00000000e+00, }, {-1.24212580e+00, 6.01760550e-01, } }, + { {1.00000000e+00, 9.46030879e-02, 1.00000000e+00, }, {-1.12297856e+00, 7.63193697e-01, } }, + { {1.00000000e+00, -1.84341946e-01, 1.00000000e+00, }, {-1.08165394e+00, 9.20980215e-01, } }, + }; + AAFilter::filter_.Init(4, kFilter44100x3); + break; + } + case 48000: // o = 3, fp = 20000, fst = 41333, cost = 576000 + { + const SOSCoefficients kFilter48000x3[4] = + { + { {4.56315687e-04, 7.94441994e-04, 4.56315687e-04, }, {-1.40446545e+00, 5.18222739e-01, } }, + { {1.00000000e+00, 6.11274299e-01, 1.00000000e+00, }, {-1.31956356e+00, 6.25927896e-01, } }, + { {1.00000000e+00, -1.00659178e-01, 1.00000000e+00, }, {-1.22823335e+00, 7.76420985e-01, } }, + { {1.00000000e+00, -3.75767056e-01, 1.00000000e+00, }, {-1.20548228e+00, 9.25277956e-01, } }, + }; + AAFilter::filter_.Init(4, kFilter48000x3); + break; + } + case 88200: // o = 2, fp = 20000, fst = 52133, cost = 529200 + { + const SOSCoefficients kFilter88200x2[3] = + { + { {6.91751141e-04, 1.23689749e-03, 6.91751141e-04, }, {-1.40714871e+00, 5.20902227e-01, } }, + { {1.00000000e+00, 8.42431018e-01, 1.00000000e+00, }, {-1.35717505e+00, 6.56002263e-01, } }, + { {1.00000000e+00, 2.97097489e-01, 1.00000000e+00, }, {-1.36759134e+00, 8.70920336e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter88200x2); + break; + } + case 96000: // o = 2, fp = 20000, fst = 57333, cost = 576000 + { + const SOSCoefficients kFilter96000x2[3] = + { + { {5.02504803e-04, 8.78421990e-04, 5.02504803e-04, }, {-1.45413648e+00, 5.51330003e-01, } }, + { {1.00000000e+00, 6.85942380e-01, 1.00000000e+00, }, {-1.42143582e+00, 6.77242054e-01, } }, + { {1.00000000e+00, 1.15756990e-01, 1.00000000e+00, }, {-1.44850505e+00, 8.78995879e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter96000x2); + break; + } + case 176400: // o = 1, fp = 20000, fst = 52133, cost = 529200 + { + const SOSCoefficients kFilter176400x1[3] = + { + { {6.91751141e-04, 1.23689749e-03, 6.91751141e-04, }, {-1.40714871e+00, 5.20902227e-01, } }, + { {1.00000000e+00, 8.42431018e-01, 1.00000000e+00, }, {-1.35717505e+00, 6.56002263e-01, } }, + { {1.00000000e+00, 2.97097489e-01, 1.00000000e+00, }, {-1.36759134e+00, 8.70920336e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter176400x1); + break; + } + case 192000: // o = 1, fp = 20000, fst = 57333, cost = 576000 + { + const SOSCoefficients kFilter192000x1[3] = + { + { {5.02504803e-04, 8.78421990e-04, 5.02504803e-04, }, {-1.45413648e+00, 5.51330003e-01, } }, + { {1.00000000e+00, 6.85942380e-01, 1.00000000e+00, }, {-1.42143582e+00, 6.77242054e-01, } }, + { {1.00000000e+00, 1.15756990e-01, 1.00000000e+00, }, {-1.44850505e+00, 8.78995879e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter192000x1); + break; + } + case 352800: // o = 1, fp = 20000, fst = 110933, cost = 1058400 + { + const SOSCoefficients kFilter352800x1[3] = + { + { {7.63562466e-05, 9.37911276e-05, 7.63562466e-05, }, {-1.69760825e+00, 7.28764991e-01, } }, + { {1.00000000e+00, -5.40096033e-01, 1.00000000e+00, }, {-1.72321786e+00, 8.05120281e-01, } }, + { {1.00000000e+00, -1.04012920e+00, 1.00000000e+00, }, {-1.79287839e+00, 9.28245030e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter352800x1); + break; + } + case 384000: // o = 1, fp = 20000, fst = 121333, cost = 1152000 + { + const SOSCoefficients kFilter384000x1[3] = + { + { {6.23104401e-05, 6.94740629e-05, 6.23104401e-05, }, {-1.72153665e+00, 7.48079159e-01, } }, + { {1.00000000e+00, -6.96283878e-01, 1.00000000e+00, }, {-1.74951535e+00, 8.19207305e-01, } }, + { {1.00000000e+00, -1.16050137e+00, 1.00000000e+00, }, {-1.81879173e+00, 9.33631596e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter384000x1); + break; + } + case 705600: // o = 1, fp = 20000, fst = 228533, cost = 1411200 + { + const SOSCoefficients kFilter705600x1[2] = + { + { {1.08339911e-04, 1.50243615e-04, 1.08339911e-04, }, {-1.77824462e+00, 7.96098482e-01, } }, + { {1.00000000e+00, -5.03405956e-02, 1.00000000e+00, }, {-1.87131112e+00, 9.11379528e-01, } }, + }; + AAFilter::filter_.Init(2, kFilter705600x1); + break; + } + case 768000: // o = 1, fp = 20000, fst = 249333, cost = 1536000 + { + const SOSCoefficients kFilter768000x1[2] = + { + { {8.80491172e-05, 1.13851506e-04, 8.80491172e-05, }, {-1.79584317e+00, 8.11038264e-01, } }, + { {1.00000000e+00, -2.19769620e-01, 1.00000000e+00, }, {-1.88421935e+00, 9.18189356e-01, } }, + }; + AAFilter::filter_.Init(2, kFilter768000x1); + break; + } + //[[[end]]] + } + } +}; + +template +class DownsamplingAAFilter : public AAFilter +{ + void InitFilter(float sample_rate) override + { + switch (SampleRateID(sample_rate)) + { + default: + /*[[[cog + aafilter.print_filter_cases(down_filters) + ]]]*/ + case 8000: // o = 15, fp = 3800, fst = 4000, cost = 960000 + { + const SOSCoefficients kFilter8000x15[8] = + { + { {1.27849152e-05, -1.15294016e-05, 1.27849152e-05, }, {-1.89076082e+00, 8.94920241e-01, } }, + { {1.00000000e+00, -1.81550212e+00, 1.00000000e+00, }, {-1.90419428e+00, 9.15590704e-01, } }, + { {1.00000000e+00, -1.91311657e+00, 1.00000000e+00, }, {-1.92211660e+00, 9.43157527e-01, } }, + { {1.00000000e+00, -1.93984732e+00, 1.00000000e+00, }, {-1.93701740e+00, 9.66048056e-01, } }, + { {1.00000000e+00, -1.95004731e+00, 1.00000000e+00, }, {-1.94692651e+00, 9.81207030e-01, } }, + { {1.00000000e+00, -1.95451979e+00, 1.00000000e+00, }, {-1.95288929e+00, 9.90199673e-01, } }, + { {1.00000000e+00, -1.95654696e+00, 1.00000000e+00, }, {-1.95649904e+00, 9.95393001e-01, } }, + { {1.00000000e+00, -1.95734415e+00, 1.00000000e+00, }, {-1.95907829e+00, 9.98656952e-01, } }, + }; + AAFilter::filter_.Init(8, kFilter8000x15); + break; + } + case 11025: // o = 11, fp = 5236, fst = 5512, cost = 970200 + { + const SOSCoefficients kFilter11025x11[8] = + { + { {1.59399541e-05, -5.45523304e-06, 1.59399541e-05, }, {-1.85152256e+00, 8.59147179e-01, } }, + { {1.00000000e+00, -1.66827517e+00, 1.00000000e+00, }, {-1.86567107e+00, 8.86607422e-01, } }, + { {1.00000000e+00, -1.84052903e+00, 1.00000000e+00, }, {-1.88464921e+00, 9.23416484e-01, } }, + { {1.00000000e+00, -1.88895850e+00, 1.00000000e+00, }, {-1.90052671e+00, 9.54145238e-01, } }, + { {1.00000000e+00, -1.90758521e+00, 1.00000000e+00, }, {-1.91115958e+00, 9.74577353e-01, } }, + { {1.00000000e+00, -1.91577845e+00, 1.00000000e+00, }, {-1.91763851e+00, 9.86729328e-01, } }, + { {1.00000000e+00, -1.91949726e+00, 1.00000000e+00, }, {-1.92169110e+00, 9.93757870e-01, } }, + { {1.00000000e+00, -1.92096059e+00, 1.00000000e+00, }, {-1.92481123e+00, 9.98179459e-01, } }, + }; + AAFilter::filter_.Init(8, kFilter11025x11); + break; + } + case 12000: // o = 10, fp = 5699, fst = 6000, cost = 960000 + { + const SOSCoefficients kFilter12000x10[8] = + { + { {1.74724987e-05, -2.65793181e-06, 1.74724987e-05, }, {-1.83684224e+00, 8.46022748e-01, } }, + { {1.00000000e+00, -1.60455772e+00, 1.00000000e+00, }, {-1.85073181e+00, 8.75957566e-01, } }, + { {1.00000000e+00, -1.80816772e+00, 1.00000000e+00, }, {-1.86939499e+00, 9.16147406e-01, } }, + { {1.00000000e+00, -1.86608225e+00, 1.00000000e+00, }, {-1.88504252e+00, 9.49754529e-01, } }, + { {1.00000000e+00, -1.88843627e+00, 1.00000000e+00, }, {-1.89555097e+00, 9.72128817e-01, } }, + { {1.00000000e+00, -1.89828300e+00, 1.00000000e+00, }, {-1.90199243e+00, 9.85446639e-01, } }, + { {1.00000000e+00, -1.90275515e+00, 1.00000000e+00, }, {-1.90608719e+00, 9.93153182e-01, } }, + { {1.00000000e+00, -1.90451538e+00, 1.00000000e+00, }, {-1.90935079e+00, 9.98002792e-01, } }, + }; + AAFilter::filter_.Init(8, kFilter12000x10); + break; + } + case 22050: // o = 6, fp = 10473, fst = 11025, cost = 1058400 + { + const SOSCoefficients kFilter22050x6[8] = + { + { {3.67003458e-05, 3.08516252e-05, 3.67003458e-05, }, {-1.72921734e+00, 7.53994379e-01, } }, + { {1.00000000e+00, -1.04633213e+00, 1.00000000e+00, }, {-1.73301180e+00, 8.01279004e-01, } }, + { {1.00000000e+00, -1.49728136e+00, 1.00000000e+00, }, {-1.73817883e+00, 8.65169236e-01, } }, + { {1.00000000e+00, -1.64018498e+00, 1.00000000e+00, }, {-1.74263646e+00, 9.18956353e-01, } }, + { {1.00000000e+00, -1.69729414e+00, 1.00000000e+00, }, {-1.74585766e+00, 9.54949897e-01, } }, + { {1.00000000e+00, -1.72280865e+00, 1.00000000e+00, }, {-1.74827060e+00, 9.76444779e-01, } }, + { {1.00000000e+00, -1.73447030e+00, 1.00000000e+00, }, {-1.75063420e+00, 9.88907702e-01, } }, + { {1.00000000e+00, -1.73907302e+00, 1.00000000e+00, }, {-1.75392950e+00, 9.96761482e-01, } }, + }; + AAFilter::filter_.Init(8, kFilter22050x6); + break; + } + case 24000: // o = 5, fp = 11399, fst = 12000, cost = 960000 + { + const SOSCoefficients kFilter24000x5[8] = + { + { {5.41421251e-05, 6.11551260e-05, 5.41421251e-05, }, {-1.67503641e+00, 7.10371798e-01, } }, + { {1.00000000e+00, -7.40935436e-01, 1.00000000e+00, }, {-1.66871015e+00, 7.66060345e-01, } }, + { {1.00000000e+00, -1.30326567e+00, 1.00000000e+00, }, {-1.66021936e+00, 8.41290550e-01, } }, + { {1.00000000e+00, -1.49333046e+00, 1.00000000e+00, }, {-1.65322192e+00, 9.04610823e-01, } }, + { {1.00000000e+00, -1.57100117e+00, 1.00000000e+00, }, {-1.64887008e+00, 9.46976897e-01, } }, + { {1.00000000e+00, -1.60602637e+00, 1.00000000e+00, }, {-1.64694927e+00, 9.72274830e-01, } }, + { {1.00000000e+00, -1.62210241e+00, 1.00000000e+00, }, {-1.64717215e+00, 9.86942309e-01, } }, + { {1.00000000e+00, -1.62845914e+00, 1.00000000e+00, }, {-1.64981608e+00, 9.96186562e-01, } }, + }; + AAFilter::filter_.Init(8, kFilter24000x5); + break; + } + case 44100: // o = 3, fp = 20000, fst = 24100, cost = 793800 + { + const SOSCoefficients kFilter44100x3[6] = + { + { {2.68627470e-04, 4.49235868e-04, 2.68627470e-04, }, {-1.45093297e+00, 5.48077112e-01, } }, + { {1.00000000e+00, 3.56445341e-01, 1.00000000e+00, }, {-1.37442858e+00, 6.39226382e-01, } }, + { {1.00000000e+00, -4.09182122e-01, 1.00000000e+00, }, {-1.27479281e+00, 7.60081618e-01, } }, + { {1.00000000e+00, -7.45642800e-01, 1.00000000e+00, }, {-1.19642609e+00, 8.60924455e-01, } }, + { {1.00000000e+00, -8.92243997e-01, 1.00000000e+00, }, {-1.15251661e+00, 9.30694207e-01, } }, + { {1.00000000e+00, -9.48436919e-01, 1.00000000e+00, }, {-1.14204907e+00, 9.79130351e-01, } }, + }; + AAFilter::filter_.Init(6, kFilter44100x3); + break; + } + case 48000: // o = 3, fp = 20000, fst = 28000, cost = 720000 + { + const SOSCoefficients kFilter48000x3[5] = + { + { {2.57287527e-04, 4.26397322e-04, 2.57287527e-04, }, {-1.46657488e+00, 5.58547936e-01, } }, + { {1.00000000e+00, 3.12318565e-01, 1.00000000e+00, }, {-1.39841450e+00, 6.48946069e-01, } }, + { {1.00000000e+00, -4.43959552e-01, 1.00000000e+00, }, {-1.31299240e+00, 7.70865691e-01, } }, + { {1.00000000e+00, -7.61106497e-01, 1.00000000e+00, }, {-1.25520703e+00, 8.77567308e-01, } }, + { {1.00000000e+00, -8.77468526e-01, 1.00000000e+00, }, {-1.24463600e+00, 9.61716067e-01, } }, + }; + AAFilter::filter_.Init(5, kFilter48000x3); + break; + } + case 88200: // o = 2, fp = 20000, fst = 68200, cost = 529200 + { + const SOSCoefficients kFilter88200x2[3] = + { + { {6.91751141e-04, 1.23689749e-03, 6.91751141e-04, }, {-1.40714871e+00, 5.20902227e-01, } }, + { {1.00000000e+00, 8.42431018e-01, 1.00000000e+00, }, {-1.35717505e+00, 6.56002263e-01, } }, + { {1.00000000e+00, 2.97097489e-01, 1.00000000e+00, }, {-1.36759134e+00, 8.70920336e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter88200x2); + break; + } + case 96000: // o = 2, fp = 20000, fst = 76000, cost = 576000 + { + const SOSCoefficients kFilter96000x2[3] = + { + { {5.02504803e-04, 8.78421990e-04, 5.02504803e-04, }, {-1.45413648e+00, 5.51330003e-01, } }, + { {1.00000000e+00, 6.85942380e-01, 1.00000000e+00, }, {-1.42143582e+00, 6.77242054e-01, } }, + { {1.00000000e+00, 1.15756990e-01, 1.00000000e+00, }, {-1.44850505e+00, 8.78995879e-01, } }, + }; + AAFilter::filter_.Init(3, kFilter96000x2); + break; + } + case 176400: // o = 1, fp = 20000, fst = 88200, cost = 176400 + { + const SOSCoefficients kFilter176400x1[1] = + { + { {1.95938020e-01, 3.91858763e-01, 1.95938020e-01, }, {-4.62313019e-01, 2.46047822e-01, } }, + }; + AAFilter::filter_.Init(1, kFilter176400x1); + break; + } + case 192000: // o = 1, fp = 20000, fst = 96000, cost = 192000 + { + const SOSCoefficients kFilter192000x1[1] = + { + { {1.74603587e-01, 3.49188678e-01, 1.74603587e-01, }, {-5.65216145e-01, 2.63611998e-01, } }, + }; + AAFilter::filter_.Init(1, kFilter192000x1); + break; + } + case 352800: // o = 1, fp = 20000, fst = 176400, cost = 352800 + { + const SOSCoefficients kFilter352800x1[1] = + { + { {6.99874107e-02, 1.39948456e-01, 6.99874107e-02, }, {-1.16347041e+00, 4.43393682e-01, } }, + }; + AAFilter::filter_.Init(1, kFilter352800x1); + break; + } + case 384000: // o = 1, fp = 20000, fst = 192000, cost = 384000 + { + const SOSCoefficients kFilter384000x1[1] = + { + { {6.09620331e-02, 1.21896769e-01, 6.09620331e-02, }, {-1.22760212e+00, 4.71422957e-01, } }, + }; + AAFilter::filter_.Init(1, kFilter384000x1); + break; + } + case 705600: // o = 1, fp = 20000, fst = 352800, cost = 705600 + { + const SOSCoefficients kFilter705600x1[1] = + { + { {2.13438638e-02, 4.26550556e-02, 2.13438638e-02, }, {-1.57253460e+00, 6.57877382e-01, } }, + }; + AAFilter::filter_.Init(1, kFilter705600x1); + break; + } + case 768000: // o = 1, fp = 20000, fst = 384000, cost = 768000 + { + const SOSCoefficients kFilter768000x1[1] = + { + { {1.83197956e-02, 3.66063440e-02, 1.83197956e-02, }, {-1.60702602e+00, 6.80271956e-01, } }, + }; + AAFilter::filter_.Init(1, kFilter768000x1); + break; + } + //[[[end]]] + } + } +}; + +} diff --git a/src/Shelves/aafilter.py b/src/Shelves/aafilter.py new file mode 100644 index 0000000..91ce6e2 --- /dev/null +++ b/src/Shelves/aafilter.py @@ -0,0 +1,89 @@ +# Anti-aliasing filter design +# Copyright (C) 2020 Tyler Coy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from scipy import signal +import math +import cog + +# fs: base sampling rate in Hz +# os: oversampling factor +# fpass: passband corner in Hz +# fstop: stopband corner in Hz +# rpass: passband ripple in dB +# rstop: stopband attenuation in dB +def design(fs, os, fpass, fstop, rpass, rstop): + wp = fpass / (fs * os) + ws = min(0.5, fstop / (fs * os)) + + n, wc = signal.ellipord(wp*2, ws*2, rpass, rstop) + + # We are using second-order sections, so if the filter order would have + # been odd, we can bump it up by 1 for 'free' + n = 2 * int(math.ceil(n / 2)) + + # Non-oversampled sampling rates result in 0-order filters, since there + # is no spectral content above fs/2. Bump these up to order 2 so we + # get some rolloff. + n = max(2, n) + z, p, k = signal.ellip(n, rpass, rstop, wc, output='zpk') + + if n % 2 == 0: + # DC gain is -rpass for even-order filters, so amplify by rpass + k *= math.pow(10, rpass / 20) + sos = signal.zpk2sos(z, p, k) + + cascade = FilterDescription((fs, os, n, wc/2, ws, sos)) + return cascade + +class FilterDescription(tuple): + def __init__(self, desc): + (fs, os, n, wc, ws, sos) = desc + self.sample_rate = fs + self.oversampling = os + self.order = n + self.wpass = wc + self.wstop = ws + self.fpass = wc * fs * os + self.fstop = ws * fs * os + self.sections = sos + +def print_filter_cases(filters): + for f in filters: + fs = f.sample_rate + factor = f.oversampling + num_sections = len(f.sections) + name = 'kFilter{}x{}'.format(fs, factor) + cost = fs * factor * num_sections + + cog.outl('case {}: // o = {}, fp = {}, fst = {}, cost = {}' + .format(fs, factor, int(f.fpass), int(f.fstop), cost)) + cog.outl('{') + + cog.outl(' const SOSCoefficients {}[{}] =' + .format(name, num_sections)) + cog.outl(' {') + print_coeff = lambda c: '{:.8e},'.format(c).ljust(17) + for s in f.sections: + b = ''.join([print_coeff(c) for c in s[:3]]) + a = ''.join([print_coeff(c) for c in s[4:]]) + cog.outl(' { {' + b + '}, {' + a + '} },') + cog.outl(' };') + + cog.outl(' AAFilter::filter_.Init({}, {});' + .format(num_sections, name)) + + cog.outl(' break;') + cog.outl('}') diff --git a/src/Shelves/shelves.hpp b/src/Shelves/shelves.hpp new file mode 100644 index 0000000..35b7577 --- /dev/null +++ b/src/Shelves/shelves.hpp @@ -0,0 +1,529 @@ +// Mutable Instruments Shelves emulation for VCV Rack +// Copyright (C) 2020 Tyler Coy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include +#include "aafilter.hpp" + +namespace shelves +{ + +using namespace rack; + +// Knob ranges +static const float kFreqKnobMin = 20.f; +static const float kFreqKnobMax = 20e3f; +static const float kGainKnobRange = 18.f; +static const float kQKnobMin = 0.5f; +static const float kQKnobMax = 40.f; + +// Opamp saturation voltage +static const float kClampVoltage = 10.5f; + +// Filter core +static const float kFilterMaxCutoff = kFreqKnobMax; +static const float kFilterR = 100e3f; +static const float kFilterRC = 1.f / (2.f * M_PI * kFilterMaxCutoff); +static const float kFilterC = kFilterRC / kFilterR; + +// Frequency CV amplifier +static const float kFreqAmpR = 18e3f; +static const float kFreqAmpC = 560e-12f; +static const float kFreqAmpInputR = 100e3f; +static const float kMinVOct = -kClampVoltage * kFreqAmpInputR / kFreqAmpR; +static const float kFreqKnobVoltage = std::log2f(kFreqKnobMax / kFreqKnobMin); + +// The 2164's gain constant is -33mV/dB +static const float kVCAGainConstant = -33e-3f; + +inline float QFactorToVoltage(float q_factor) +{ + // Calculate the voltage at the VCA control port for a given Q factor + return 20.f * -kVCAGainConstant * std::log10(2.f * q_factor); +} + +// Q CV amplifier +static const float kQAmpR = 22e3f; +static const float kQAmpC = 560e-12f; +static const float kQAmpGain = -kQAmpR / 150e3f; +static const float kQKnobMinVoltage = QFactorToVoltage(kQKnobMin) / kQAmpGain; +static const float kQKnobMaxVoltage = QFactorToVoltage(kQKnobMax) / kQAmpGain; + +// Gain CV amplifier +static const float kGainPerVolt = 10.f * std::log10(2.f); // 3dB per volt +static const float kMaximumGain = 24.f; + +// Clipping indicator +static const float kClipLEDThreshold = 7.86f; +static const float kClipInputR = 150e3f; +static const float kClipInputC = 100e-9f; +static const float kClipLEDRiseTime = 2e-3f; +static const float kClipLEDFallTime = 10e-3f; + +// Solves an ODE system using the 2nd order Runge-Kutta method +template +inline T StepRK2(float dt, T y, F f) +{ + T k1 = f(y); + T k2 = f(y + k1 * (dt / 2.f)); + return y + dt * k2; +} + +template +inline void StepRK2(float dt, T y[], F f) +{ + T k1[len]; + T k2[len]; + T yi[len]; + + f(y, k1); + + for (int i = 0; i < len; i++) + { + yi[i] = y[i] + k1[i] * (dt / 2.f); + } + + f(yi, k2); + + for (int i = 0; i < len; i++) + { + y[i] += k2[i] * dt; + } +} + +template +class LPFilter +{ +public: + void Init(void) + { + voltage_ = 0.f; + } + + T Process(float timestep, T v_in, T vca_level) + { + T rad_per_s = -vca_level / kFilterRC; + + voltage_ = StepRK2(timestep, voltage_, [&](T v_state) + { + return rad_per_s * (v_in + v_state); + }); + + voltage_ = simd::clamp(voltage_, -kClampVoltage, kClampVoltage); + return voltage_; + } + + T voltage(void) + { + return voltage_; + } + +protected: + T voltage_; +}; + +template +class SVFilter +{ +public: + void Init(void) + { + bp_ = 0.f; + lp_ = 0.f; + hp_ = 0.f; + } + + T Process(float timestep, T in, T vca_level, T q_level) + { + // Thanks Émilie! + // https://mutable-instruments.net/archive/documents/svf_analysis.pdf + // + // For the bandpass integrator, + // dv/dt = -A/RC * hp + // For the lowpass integrator, + // dv/dt = -A/RC * bp + // with + // hp = -(in + lp - 2*Q*bp) + + T rad_per_s = -vca_level / kFilterRC; + + StepRK2<2>(timestep, voltage_, [&](const T v_state[], T v_stepped[]) + { + T lp = v_state[0]; + T bp = v_state[1]; + T hp = -(in + lp - 2.f * q_level * bp); + v_stepped[0] = rad_per_s * bp; + v_stepped[1] = rad_per_s * hp; + }); + + lp_ = simd::clamp(lp_, -kClampVoltage, kClampVoltage); + bp_ = simd::clamp(bp_, -kClampVoltage, kClampVoltage); + T out = bp() * -2.f * q_level; + hp_ = -(in + lp() + out); + return out; + } + + T hp(void) + { + return hp_; + } + + T bp(void) + { + return bp_; + } + + T lp(void) + { + return lp_; + } + +protected: + union + { + T voltage_[2]; + struct + { + T lp_; + T bp_; + }; + }; + T hp_; +}; + +class ShelvesEngine +{ +public: + struct Frame + { + // Parameters + float hs_freq_knob; // 0 to 1 linear + float hs_gain_knob; // -1 to 1 linear + float p1_freq_knob; // 0 to 1 linear + float p1_gain_knob; // -1 to 1 linear + float p1_q_knob; // 0 to 1 linear + float p2_freq_knob; // 0 to 1 linear + float p2_gain_knob; // -1 to 1 linear + float p2_q_knob; // 0 to 1 linear + float ls_freq_knob; // 0 to 1 linear + float ls_gain_knob; // -1 to 1 linear + + // Inputs + float hs_freq_cv; + float hs_gain_cv; + float p1_freq_cv; + float p1_gain_cv; + float p1_q_cv; + float p2_freq_cv; + float p2_gain_cv; + float p2_q_cv; + float ls_freq_cv; + float ls_gain_cv; + float global_freq_cv; + float global_gain_cv; + float main_in; + + bool hs_freq_cv_connected; + bool hs_gain_cv_connected; + bool p1_freq_cv_connected; + bool p1_gain_cv_connected; + bool p1_q_cv_connected; + bool p2_freq_cv_connected; + bool p2_gain_cv_connected; + bool p2_q_cv_connected; + bool ls_freq_cv_connected; + bool ls_gain_cv_connected; + bool global_freq_cv_connected; + bool global_gain_cv_connected; + + // Outputs + float p1_hp_out; + float p1_bp_out; + float p1_lp_out; + float p2_hp_out; + float p2_bp_out; + float p2_lp_out; + float main_out; + + bool p1_hp_out_connected; + bool p1_bp_out_connected; + bool p1_lp_out_connected; + bool p2_hp_out_connected; + bool p2_bp_out_connected; + bool p2_lp_out_connected; + + // Lights + float clip; + + // Options + bool pre_gain; // True = -6dB, False = 0dB + }; + + ShelvesEngine() + { + setSampleRate(1.f); + } + + void setSampleRate(float sample_rate) + { + sample_time_ = 1.f / sample_rate; + oversampling_ = OversamplingFactor(sample_rate); + + up_filter_[0].Init(sample_rate); + up_filter_[1].Init(sample_rate); + up_filter_[2].Init(sample_rate); + down_filter_[0].Init(sample_rate); + down_filter_[1].Init(sample_rate); + + low_high_.Init(); + mid_.Init(); + + float freq_cut = 1.f / (2.f * M_PI * kFreqAmpR * kFreqAmpC); + freq_lpf_.reset(); + freq_lpf_.setCutoffFreq(freq_cut / sample_rate); + + float q_cut = 1.f / (2.f * M_PI * kQAmpR * kQAmpC); + q_lpf_.reset(); + q_lpf_.setCutoffFreq(q_cut / sample_rate); + + float clip_in_cut = 1.f / (2.f * M_PI * kClipInputR * kClipInputC); + clip_hpf_.reset(); + clip_hpf_.setCutoffFreq(clip_in_cut / sample_rate); + + float rise = 1.f / kClipLEDRiseTime; + float fall = 1.f / kClipLEDFallTime; + clip_slew_.reset(); + clip_slew_.setRiseFall(rise, fall); + } + + void process(Frame& frame) + { + auto f_knob = simd::float_4( + frame.ls_freq_knob, + frame.p1_freq_knob, + frame.p2_freq_knob, + frame.hs_freq_knob); + + auto f_cv = simd::float_4( + frame.ls_freq_cv, + frame.p1_freq_cv, + frame.p2_freq_cv, + frame.hs_freq_cv); + + bool f_cv_exists = + frame.hs_freq_cv_connected || + frame.p1_freq_cv_connected || + frame.p2_freq_cv_connected || + frame.ls_freq_cv_connected || + frame.global_freq_cv_connected; + + auto q_knob = simd::float_4( + 0.f, + frame.p1_q_knob, + frame.p2_q_knob, + 0.f); + + auto q_cv = simd::float_4( + 0.f, + frame.p1_q_cv, + frame.p2_q_cv, + 0.f); + + bool q_cv_exists = frame.p1_q_cv_connected || frame.p2_q_cv_connected; + + auto gain_knob = simd::float_4( + frame.ls_gain_knob, + frame.p1_gain_knob, + frame.p2_gain_knob, + frame.hs_gain_knob); + + auto gain_cv = simd::float_4( + frame.ls_gain_cv, + frame.p1_gain_cv, + frame.p2_gain_cv, + frame.hs_gain_cv); + + bool gain_cv_exists = + frame.hs_gain_cv_connected || + frame.p1_gain_cv_connected || + frame.p2_gain_cv_connected || + frame.ls_gain_cv_connected || + frame.global_gain_cv_connected; + + f_cv += frame.global_freq_cv; + gain_cv += frame.global_gain_cv; + + // V/oct + simd::float_4 v_oct = f_cv + kFreqKnobVoltage * (f_knob - 1.f); + freq_lpf_.process(v_oct); + v_oct = freq_lpf_.lowpass(); + + // Q CV + q_cv -= simd::rescale(q_knob, + 0.f, 1.f, kQKnobMinVoltage, kQKnobMaxVoltage); + q_cv *= -kQAmpGain; + q_lpf_.process(q_cv); + q_cv = q_lpf_.lowpass(); + + // Gain CV + simd::float_4 gain_db = + gain_knob * kGainKnobRange + gain_cv * kGainPerVolt; + + // Stuff input into unused element of Q CV vector + q_cv[0] = frame.main_in * (frame.pre_gain ? 0.25f : 0.5f); + + float timestep = sample_time_ / oversampling_; + + // If a CV input is not connected, we can perform the expensive + // exponential calculation here only once instead of inside the loop, + // since we needn't apply oversampling and anti-aliasing to a low-rate + // UI control. + simd::float_4 f_level; + simd::float_4 q_level; + simd::float_4 gain_level; + + if (!f_cv_exists) + { + f_level = FreqVCALevel(v_oct); + } + + if (!q_cv_exists) + { + q_level = QVCALevel(q_cv); + } + + if (!gain_cv_exists) + { + gain_level = GainVCALevel(gain_db); + } + + // Outputs + simd::float_4 out1; + simd::float_4 out2; + bool out2_connected = + frame.p2_hp_out_connected || + frame.p2_bp_out_connected || + frame.p2_lp_out_connected; + + for (int i = 0; i < oversampling_; i++) + { + // Upsample and apply anti-aliasing filters if needed + if (f_cv_exists) + { + v_oct = up_filter_[0].Process( + (i == 0) ? (v_oct * oversampling_) : 0.f); + f_level = FreqVCALevel(v_oct); + } + + // We can't skip this one since it contains the input signal + q_cv = up_filter_[1].Process( + (i == 0) ? (q_cv * oversampling_) : 0.f); + if (q_cv_exists) + { + q_level = QVCALevel(q_cv); + } + + if (gain_cv_exists) + { + gain_db = up_filter_[2].Process( + (i == 0) ? (gain_db * oversampling_) : 0.f); + gain_level = GainVCALevel(gain_db); + } + + // Unpack input from Q CV vector + simd::float_4 in = + _mm_shuffle_ps(q_cv.v, q_cv.v, _MM_SHUFFLE(0, 0, 0, 0)); + + // Process VCFs + low_high_.Process(timestep, in, f_level); + float low = low_high_.voltage()[0]; + float high = low_high_.voltage()[3]; + simd::float_4 mid = mid_.Process(timestep, in, f_level, q_level); + + // Calculate output + low *= 1.f - gain_level[0]; + mid *= 1.f - gain_level; + high = -high + (high + in[0]) * gain_level[3]; + float sum = 2.f * (low + mid[1] + mid[2] + high); + + out1 = simd::float_4(sum, mid_.lp()[1], mid_.bp()[1], mid_.hp()[1]); + out1 = simd::clamp(out1, -kClampVoltage, kClampVoltage); + + // Pre-downsample anti-alias filtering + out1 = down_filter_[0].Process(out1); + + if (out2_connected) + { + out2 = simd::float_4(0.f, mid_.lp()[2], mid_.bp()[2], mid_.hp()[2]); + out2 = simd::clamp(out2, -kClampVoltage, kClampVoltage); + out2 = down_filter_[1].Process(out2); + } + } + + frame.main_out = out1[0]; + + clip_hpf_.process(out1[0]); + float clip = 1.f * (std::abs(clip_hpf_.highpass()) > kClipLEDThreshold); + frame.clip = clip_slew_.process(sample_time_, clip); + + frame.p1_lp_out = out1[1]; + frame.p1_bp_out = out1[2]; + frame.p1_hp_out = out1[3]; + + if (out2_connected) + { + frame.p2_lp_out = out2[1]; + frame.p2_bp_out = out2[2]; + frame.p2_hp_out = out2[3]; + } + } + +protected: + float sample_time_; + int oversampling_; + UpsamplingAAFilter up_filter_[3]; + DownsamplingAAFilter down_filter_[2]; + LPFilter low_high_; + SVFilter mid_; + dsp::TRCFilter freq_lpf_; + dsp::TRCFilter q_lpf_; + dsp::TRCFilter clip_hpf_; + dsp::SlewLimiter clip_slew_; + + template + T FreqVCALevel(T v_oct) + { + v_oct = simd::clamp(v_oct, kMinVOct, 0.f); + return simd::pow(2.f, v_oct); + } + + template + T QVCALevel(T q_cv) + { + q_cv = simd::clamp(q_cv, 0.f, kClampVoltage); + return simd::pow(10.f, q_cv / kVCAGainConstant / 20.f); + } + + template + T GainVCALevel(T gain_db) + { + gain_db = simd::fmin(gain_db, kMaximumGain); + return simd::pow(10.f, gain_db / 20.f); + } +}; + +} diff --git a/src/Shelves/sos.hpp b/src/Shelves/sos.hpp new file mode 100644 index 0000000..0e0554c --- /dev/null +++ b/src/Shelves/sos.hpp @@ -0,0 +1,118 @@ +// Cascaded second-order sections IIR filter +// Copyright (C) 2020 Tyler Coy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +namespace shelves +{ + +struct SOSCoefficients +{ + float b[3]; + float a[2]; +}; + +template +class SOSFilter +{ +public: + SOSFilter() + { + Init(0); + } + + SOSFilter(int num_sections) + { + Init(num_sections); + } + + void Init(int num_sections) + { + num_sections_ = num_sections; + Reset(); + } + + void Init(int num_sections, const SOSCoefficients* sections) + { + num_sections_ = num_sections; + Reset(); + SetCoefficients(sections); + } + + void Reset() + { + for (int n = 0; n < num_sections_; n++) + { + x_[n][0] = 0.f; + x_[n][1] = 0.f; + x_[n][2] = 0.f; + } + + x_[num_sections_][0] = 0.f; + x_[num_sections_][1] = 0.f; + x_[num_sections_][2] = 0.f; + } + + void SetCoefficients(const SOSCoefficients* sections) + { + for (int n = 0; n < num_sections_; n++) + { + sections_[n].b[0] = sections[n].b[0]; + sections_[n].b[1] = sections[n].b[1]; + sections_[n].b[2] = sections[n].b[2]; + + sections_[n].a[0] = sections[n].a[0]; + sections_[n].a[1] = sections[n].a[1]; + } + } + + T Process(T in) + { + for (int n = 0; n < num_sections_; n++) + { + // Shift x state + x_[n][2] = x_[n][1]; + x_[n][1] = x_[n][0]; + x_[n][0] = in; + + T out = 0.f; + + // Add x state + out += sections_[n].b[0] * x_[n][0]; + out += sections_[n].b[1] * x_[n][1]; + out += sections_[n].b[2] * x_[n][2]; + + // Subtract y state + out -= sections_[n].a[0] * x_[n+1][0]; + out -= sections_[n].a[1] * x_[n+1][1]; + in = out; + } + + // Shift final section x state + x_[num_sections_][2] = x_[num_sections_][1]; + x_[num_sections_][1] = x_[num_sections_][0]; + x_[num_sections_][0] = in; + + return in; + } + +protected: + int num_sections_; + SOSCoefficients sections_[max_num_sections]; + T x_[max_num_sections + 1][3]; +}; + +} diff --git a/src/plugin.hpp b/src/plugin.hpp index cc306ba..d5b9478 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -1,4 +1,4 @@ -#include "rack.hpp" +#include using namespace rack;