Browse Source

Add Shelves library and implement Shelves::process().

tags/v1.4.0
Andrew Belt 2 years ago
parent
commit
d957fb82eb
8 changed files with 1417 additions and 13 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +2
    -1
      plugin.json
  3. +140
    -11
      src/Shelves.cpp
  4. +535
    -0
      src/Shelves/aafilter.hpp
  5. +89
    -0
      src/Shelves/aafilter.py
  6. +529
    -0
      src/Shelves/shelves.hpp
  7. +118
    -0
      src/Shelves/sos.hpp
  8. +1
    -1
      src/plugin.hpp

+ 3
- 0
CHANGELOG.md View File

@@ -1,3 +1,6 @@
### 1.3.0 (2020-05-29)
- Add EQ Filter.

### 1.2.1 (2020-04-23)
- Multiples
- Make polyphonic.


+ 2
- 1
plugin.json View File

@@ -239,7 +239,8 @@
"modularGridUrl": "https://www.modulargrid.net/e/mutable-instruments-shelves-2015",
"tags": [
"Equalizer",
"Filter"
"Filter",
"Polyphonic"
]
}
]

+ 140
- 11
src/Shelves.cpp View File

@@ -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");

+ 535
- 0
src/Shelves/aafilter.hpp View File

@@ -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]]]
}
}
};

}

+ 89
- 0
src/Shelves/aafilter.py View File

@@ -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('}')

+ 529
- 0
src/Shelves/shelves.hpp View File

@@ -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);
}
};

}

+ 118
- 0
src/Shelves/sos.hpp View File

@@ -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
- 1
src/plugin.hpp View File

@@ -1,4 +1,4 @@
#include "rack.hpp"
#include <rack.hpp>


using namespace rack;


Loading…
Cancel
Save