@@ -1,3 +1,6 @@ | |||
### 1.3.0 (2020-05-29) | |||
- Add EQ Filter. | |||
### 1.2.1 (2020-04-23) | |||
- Multiples | |||
- Make polyphonic. | |||
@@ -239,7 +239,8 @@ | |||
"modularGridUrl": "https://www.modulargrid.net/e/mutable-instruments-shelves-2015", | |||
"tags": [ | |||
"Equalizer", | |||
"Filter" | |||
"Filter", | |||
"Polyphonic" | |||
] | |||
} | |||
] |
@@ -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<MediumLight<RedLight>>(mm2px(Vec(53.629, 109.475)), module, Shelves::CLIP_LIGHT)); | |||
} | |||
void appendContextMenu(Menu* menu) override { | |||
Shelves* module = dynamic_cast<Shelves*>(this->module); | |||
menu->addChild(new MenuSeparator); | |||
struct PreGainItem : MenuItem { | |||
Shelves* module; | |||
void onAction(const event::Action& e) override { | |||
module->preGain ^= true; | |||
} | |||
}; | |||
PreGainItem* preGainItem = createMenuItem<PreGainItem>("Pad input by -6dB", CHECKMARK(module->preGain)); | |||
preGainItem->module = module; | |||
menu->addChild(preGainItem); | |||
} | |||
}; | |||
Model* modelShelves = createModel<Shelves, ShelvesWidget>("Shelves"); | |||
Model* modelShelves = createModel<Shelves, ShelvesWidget>("Shelves"); |
@@ -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 <https://www.gnu.org/licenses/>. | |||
#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 <typename T> | |||
class AAFilter | |||
{ | |||
public: | |||
void Init(float sample_rate) | |||
{ | |||
InitFilter(sample_rate); | |||
} | |||
T Process(T in) | |||
{ | |||
return filter_.Process(in); | |||
} | |||
protected: | |||
SOSFilter<T, kMaxNumSections> filter_; | |||
virtual void InitFilter(float sample_rate) = 0; | |||
}; | |||
template <typename T> | |||
class UpsamplingAAFilter : public AAFilter<T> | |||
{ | |||
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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::filter_.Init(2, kFilter768000x1); | |||
break; | |||
} | |||
//[[[end]]] | |||
} | |||
} | |||
}; | |||
template <typename T> | |||
class DownsamplingAAFilter : public AAFilter<T> | |||
{ | |||
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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::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<T>::filter_.Init(1, kFilter768000x1); | |||
break; | |||
} | |||
//[[[end]]] | |||
} | |||
} | |||
}; | |||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||
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<T>::filter_.Init({}, {});' | |||
.format(num_sections, name)) | |||
cog.outl(' break;') | |||
cog.outl('}') |
@@ -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 <https://www.gnu.org/licenses/>. | |||
#include <cmath> | |||
#include <algorithm> | |||
#include <rack.hpp> | |||
#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 <typename T, typename F> | |||
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 <int len, typename T, typename F> | |||
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 <typename T> | |||
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 <typename T> | |||
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<simd::float_4> up_filter_[3]; | |||
DownsamplingAAFilter<simd::float_4> down_filter_[2]; | |||
LPFilter<simd::float_4> low_high_; | |||
SVFilter<simd::float_4> mid_; | |||
dsp::TRCFilter<simd::float_4> freq_lpf_; | |||
dsp::TRCFilter<simd::float_4> q_lpf_; | |||
dsp::TRCFilter<float> clip_hpf_; | |||
dsp::SlewLimiter clip_slew_; | |||
template <typename T> | |||
T FreqVCALevel(T v_oct) | |||
{ | |||
v_oct = simd::clamp(v_oct, kMinVOct, 0.f); | |||
return simd::pow(2.f, v_oct); | |||
} | |||
template <typename T> | |||
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 <typename T> | |||
T GainVCALevel(T gain_db) | |||
{ | |||
gain_db = simd::fmin(gain_db, kMaximumGain); | |||
return simd::pow(10.f, gain_db / 20.f); | |||
} | |||
}; | |||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||
#pragma once | |||
namespace shelves | |||
{ | |||
struct SOSCoefficients | |||
{ | |||
float b[3]; | |||
float a[2]; | |||
}; | |||
template <typename T, int max_num_sections> | |||
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]; | |||
}; | |||
} |
@@ -1,4 +1,4 @@ | |||
#include "rack.hpp" | |||
#include <rack.hpp> | |||
using namespace rack; | |||