@@ -1,3 +1,6 @@ | |||
### 1.2.0 (2020-04-20) | |||
- Add Liquid Filter | |||
### 1.1.0 (2020-04-16) | |||
- Macro Oscillator | |||
- Change range of frequency parameter to [-4, 4] octaves in order to match hardware. | |||
@@ -1,4 +1,4 @@ | |||
All **source code** in the `src/` folder is copyright © 2017-2020 Andrew Belt and Audible Instruments contributers. | |||
All **source code** in the `src/` folder is copyright © 2016-2020 Andrew Belt and Audible Instruments contributers. | |||
This program is free software: you can redistribute it and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.en.html) as published by the [Free Software Foundation](https://www.fsf.org/), either version 3 of the License, or (at your option) any later version. | |||
@@ -52,16 +52,16 @@ Based on [Blinds](https://mutable-instruments.net/modules/blinds), [Manual](http | |||
### Quad VCA | |||
Based on [Veils](https://mutable-instruments.net/modules/veils), [Manual](https://mutable-instruments.net/modules/veils/manual/) | |||
### Liquid Filter | |||
Based on [Ripples](https://mutable-instruments.net/modules/ripples), [Manual](https://mutable-instruments.net/modules/ripples/manual/) | |||
- Virtual analog model provided by [Alright Devices](https://www.alrightdevices.com/) after successful crowdfunding. | |||
## Not yet ported | |||
### [Peaks](https://mutable-instruments.net/modules/peaks) | |||
[Manual](https://mutable-instruments.net/modules/peaks/manual/) | |||
### [Ripples](https://mutable-instruments.net/modules/ripples) | |||
[Manual](https://mutable-instruments.net/modules/ripples/manual/) | |||
- analog, but I own a unit. need a free afternoon to port | |||
### [Shelves](https://mutable-instruments.net/modules/shelves) | |||
[Manual](https://mutable-instruments.net/modules/shelves/manual/) | |||
- analog, will not port unless I buy/borrow one or someone contributes a technical model | |||
@@ -1,6 +1,6 @@ | |||
{ | |||
"slug": "AudibleInstruments", | |||
"version": "1.1.0", | |||
"version": "1.2.0", | |||
"license": "GPL-3.0-or-later", | |||
"name": "Audible Instruments", | |||
"author": "VCV", | |||
@@ -202,14 +202,14 @@ | |||
}, | |||
{ | |||
"slug": "Ripples", | |||
"disabled": true, | |||
"name": "Liquid Filter", | |||
"description": "Based on Mutable Instruments Ripples", | |||
"modularGridUrl": "https://www.modulargrid.net/e/mutable-instruments-ripples", | |||
"tags": [ | |||
"Filter", | |||
"Voltage-controlled amplifier", | |||
"Hardware clone" | |||
"Hardware clone", | |||
"Polyphonic" | |||
] | |||
} | |||
] |
@@ -1,4 +1,5 @@ | |||
#include "plugin.hpp" | |||
#include "Ripples/ripples.hpp" | |||
struct Ripples : Module { | |||
@@ -27,14 +28,65 @@ struct Ripples : Module { | |||
NUM_LIGHTS | |||
}; | |||
RipplesEngine engines[16]; | |||
Ripples() { | |||
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
configParam(RES_PARAM, 0.f, 1.f, 0.5f, "Resonance"); | |||
configParam(FREQ_PARAM, 0.f, 1.f, 0.5f, "Frequency"); | |||
configParam(FM_PARAM, 0.f, 1.f, 0.5f, "Frequency modulation"); | |||
configParam(RES_PARAM, 0.f, 1.f, 0.f, "Resonance", "%", 0, 100); | |||
configParam(FREQ_PARAM, std::log2(RipplesEngine::kFreqKnobMin), std::log2(RipplesEngine::kFreqKnobMax), std::log2(RipplesEngine::kFreqKnobMax), "Frequency", " Hz", 2.f); | |||
configParam(FM_PARAM, -1.f, 1.f, 0.f, "Frequency modulation", "%", 0, 100); | |||
onSampleRateChange(); | |||
} | |||
void onReset() override { | |||
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. | |||
RipplesEngine::Frame frame; | |||
frame.res_knob = params[RES_PARAM].getValue(); | |||
frame.freq_knob = rescale(params[FREQ_PARAM].getValue(), std::log2(RipplesEngine::kFreqKnobMin), std::log2(RipplesEngine::kFreqKnobMax), 0.f, 1.f); | |||
frame.fm_knob = params[FM_PARAM].getValue(); | |||
frame.gain_cv_present = inputs[GAIN_INPUT].isConnected(); | |||
for (int c = 0; c < channels; c++) { | |||
frame.res_cv = inputs[RES_INPUT].getPolyVoltage(c); | |||
frame.freq_cv = inputs[FREQ_INPUT].getPolyVoltage(c); | |||
frame.fm_cv = inputs[FM_INPUT].getPolyVoltage(c); | |||
frame.input = inputs[IN_INPUT].getVoltage(c); | |||
frame.gain_cv = inputs[GAIN_INPUT].getPolyVoltage(c); | |||
engines[c].process(frame); | |||
// Although rare, using extreme parameters, I've been able to produce nonfinite floats with the filter model, so detect them and reset the state. | |||
if (!std::isfinite(frame.bp2)) { | |||
// A reset() method would be nice, but we can just reinitialize it. | |||
engines[c] = RipplesEngine(); | |||
engines[c].setSampleRate(args.sampleRate); | |||
} | |||
else { | |||
outputs[BP2_OUTPUT].setVoltage(frame.bp2, c); | |||
outputs[LP2_OUTPUT].setVoltage(frame.lp2, c); | |||
outputs[LP4_OUTPUT].setVoltage(frame.lp4, c); | |||
outputs[LP4VCA_OUTPUT].setVoltage(frame.lp4vca, c); | |||
} | |||
} | |||
outputs[BP2_OUTPUT].setChannels(channels); | |||
outputs[LP2_OUTPUT].setChannels(channels); | |||
outputs[LP4_OUTPUT].setChannels(channels); | |||
outputs[LP4VCA_OUTPUT].setChannels(channels); | |||
} | |||
}; | |||
@@ -0,0 +1,329 @@ | |||
// 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 ripples | |||
{ | |||
template <typename T> | |||
class AAFilter | |||
{ | |||
public: | |||
void Init(float sample_rate) | |||
{ | |||
InitFilter(sample_rate); | |||
} | |||
T ProcessUp(T in) | |||
{ | |||
return up_filter_.Process(in); | |||
} | |||
T ProcessDown(T in) | |||
{ | |||
return down_filter_.Process(in); | |||
} | |||
int GetOversamplingFactor(void) | |||
{ | |||
return oversampling_factor_; | |||
} | |||
protected: | |||
struct CascadedSOS | |||
{ | |||
float sample_rate; | |||
int oversampling_factor; | |||
int num_sections; | |||
const SOSCoefficients* coeffs; | |||
}; | |||
/*[[[cog | |||
from scipy import signal | |||
import math | |||
min_oversampled_rate = 20000 * 6 | |||
common_rates = [ | |||
8000, | |||
11025, 12000, | |||
22050, 24000, | |||
44100, 48000, | |||
88200, 96000, | |||
176400, 192000, | |||
352800, 384000, | |||
705600, 768000 | |||
] | |||
fp = 20000 # passband corner in Hz | |||
rp = 0.1 # passband ripple in dB | |||
rs = 100 # stopband attenuation in dB | |||
array_name = 'kFilter' | |||
cascades = [] | |||
max_num_sections = 0 | |||
for fs in common_rates: | |||
factor = math.ceil(min_oversampled_rate / fs) | |||
wp = fp / fs | |||
ws = 0.5 | |||
n, wc = signal.ellipord(wp*2/factor, ws*2/factor, rp, rs) | |||
# 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, rp, rs, wc, output='zpk') | |||
if n % 2 == 0: | |||
# DC gain is -rp for even-order filters, so amplify by rp | |||
k *= math.pow(10, rp / 20) | |||
sos = signal.zpk2sos(z, p, k) | |||
max_num_sections = max(max_num_sections, len(sos)) | |||
cascade = (fs, factor, n, wc, sos) | |||
cascades.append(cascade) | |||
cog.outl('static constexpr int kMaxNumSections = {};' | |||
.format(max_num_sections)) | |||
]]]*/ | |||
static constexpr int kMaxNumSections = 7; | |||
//[[[end]]] | |||
SOSFilter<T, kMaxNumSections> up_filter_; | |||
SOSFilter<T, kMaxNumSections> down_filter_; | |||
int oversampling_factor_; | |||
void InitFilter(float sample_rate) | |||
{ | |||
if (false) {} | |||
/*[[[cog | |||
for cascade in reversed(cascades): | |||
(fs, factor, order, wc, sos) = cascade | |||
num_sections = len(sos) | |||
name = '{:s}{:d}x{:d}'.format(array_name, fs, factor) | |||
cost = fs * factor * num_sections | |||
cog.outl('else if ({} <= sample_rate)'.format(fs)) | |||
cog.outl('{') | |||
cog.outl(' const SOSCoefficients {:s}[{:d}] =' | |||
' // n = {:d}, wc = {:f}, cost = {:d}' | |||
.format(name, num_sections, order, wc, cost)) | |||
cog.outl(' {') | |||
for sec in sos: | |||
b = ''.join(['{:.8e},'.format(c).ljust(17) for c in sec[:3]]) | |||
a = ''.join(['{:.8e},'.format(c).ljust(17) for c in sec[4:]]) | |||
cog.outl(' { {' + b + '}, {' + a + '} },') | |||
cog.outl(' };') | |||
cog.outl(' up_filter_.Init({}, {});'.format(num_sections, name)) | |||
cog.outl(' down_filter_.Init({}, {});'.format(num_sections, name)) | |||
cog.outl(' oversampling_factor_ = {};'.format(factor)) | |||
cog.outl('}') | |||
cog.outl('else {{ InitFilter({}); }}'.format(*cascades[0])) | |||
]]]*/ | |||
else if (768000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter768000x1[1] = // n = 2, wc = 0.052083, cost = 768000 | |||
{ | |||
{ {1.83197956e-02, 3.66063440e-02, 1.83197956e-02, }, {-1.60702602e+00, 6.80271956e-01, } }, | |||
}; | |||
up_filter_.Init(1, kFilter768000x1); | |||
down_filter_.Init(1, kFilter768000x1); | |||
oversampling_factor_ = 1; | |||
} | |||
else if (705600 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter705600x1[1] = // n = 2, wc = 0.056689, cost = 705600 | |||
{ | |||
{ {2.13438638e-02, 4.26550556e-02, 2.13438638e-02, }, {-1.57253460e+00, 6.57877382e-01, } }, | |||
}; | |||
up_filter_.Init(1, kFilter705600x1); | |||
down_filter_.Init(1, kFilter705600x1); | |||
oversampling_factor_ = 1; | |||
} | |||
else if (384000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter384000x1[1] = // n = 2, wc = 0.104167, cost = 384000 | |||
{ | |||
{ {6.09620331e-02, 1.21896769e-01, 6.09620331e-02, }, {-1.22760212e+00, 4.71422957e-01, } }, | |||
}; | |||
up_filter_.Init(1, kFilter384000x1); | |||
down_filter_.Init(1, kFilter384000x1); | |||
oversampling_factor_ = 1; | |||
} | |||
else if (352800 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter352800x1[1] = // n = 2, wc = 0.113379, cost = 352800 | |||
{ | |||
{ {6.99874107e-02, 1.39948456e-01, 6.99874107e-02, }, {-1.16347041e+00, 4.43393682e-01, } }, | |||
}; | |||
up_filter_.Init(1, kFilter352800x1); | |||
down_filter_.Init(1, kFilter352800x1); | |||
oversampling_factor_ = 1; | |||
} | |||
else if (192000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter192000x1[1] = // n = 2, wc = 0.208333, cost = 192000 | |||
{ | |||
{ {1.74603587e-01, 3.49188678e-01, 1.74603587e-01, }, {-5.65216145e-01, 2.63611998e-01, } }, | |||
}; | |||
up_filter_.Init(1, kFilter192000x1); | |||
down_filter_.Init(1, kFilter192000x1); | |||
oversampling_factor_ = 1; | |||
} | |||
else if (176400 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter176400x1[1] = // n = 2, wc = 0.226757, cost = 176400 | |||
{ | |||
{ {1.95938020e-01, 3.91858763e-01, 1.95938020e-01, }, {-4.62313019e-01, 2.46047822e-01, } }, | |||
}; | |||
up_filter_.Init(1, kFilter176400x1); | |||
down_filter_.Init(1, kFilter176400x1); | |||
oversampling_factor_ = 1; | |||
} | |||
else if (96000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter96000x2[4] = // n = 8, wc = 0.208333, cost = 768000 | |||
{ | |||
{ {1.61637850e-04, 2.48564833e-04, 1.61637850e-04, }, {-1.55379599e+00, 6.19242969e-01, } }, | |||
{ {1.00000000e+00, -3.56106191e-03, 1.00000000e+00, }, {-1.52397985e+00, 7.01779035e-01, } }, | |||
{ {1.00000000e+00, -7.04269454e-01, 1.00000000e+00, }, {-1.49925562e+00, 8.20191196e-01, } }, | |||
{ {1.00000000e+00, -9.36222412e-01, 1.00000000e+00, }, {-1.51854586e+00, 9.39911675e-01, } }, | |||
}; | |||
up_filter_.Init(4, kFilter96000x2); | |||
down_filter_.Init(4, kFilter96000x2); | |||
oversampling_factor_ = 2; | |||
} | |||
else if (88200 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter88200x2[4] = // n = 8, wc = 0.226757, cost = 705600 | |||
{ | |||
{ {2.14361684e-04, 3.44618768e-04, 2.14361684e-04, }, {-1.51452462e+00, 5.91486912e-01, } }, | |||
{ {1.00000000e+00, 1.79381294e-01, 1.00000000e+00, }, {-1.47183116e+00, 6.80568376e-01, } }, | |||
{ {1.00000000e+00, -5.38705333e-01, 1.00000000e+00, }, {-1.43146550e+00, 8.07687680e-01, } }, | |||
{ {1.00000000e+00, -7.87002288e-01, 1.00000000e+00, }, {-1.44140131e+00, 9.35689662e-01, } }, | |||
}; | |||
up_filter_.Init(4, kFilter88200x2); | |||
down_filter_.Init(4, kFilter88200x2); | |||
oversampling_factor_ = 2; | |||
} | |||
else if (48000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter48000x3[6] = // n = 12, wc = 0.277778, cost = 864000 | |||
{ | |||
{ {1.96007199e-04, 3.15285921e-04, 1.96007199e-04, }, {-1.49750952e+00, 5.79487424e-01, } }, | |||
{ {1.00000000e+00, 1.64502383e-01, 1.00000000e+00, }, {-1.43900370e+00, 6.63196513e-01, } }, | |||
{ {1.00000000e+00, -5.92180251e-01, 1.00000000e+00, }, {-1.36241892e+00, 7.75058824e-01, } }, | |||
{ {1.00000000e+00, -9.07488127e-01, 1.00000000e+00, }, {-1.30223398e+00, 8.69165582e-01, } }, | |||
{ {1.00000000e+00, -1.04177534e+00, 1.00000000e+00, }, {-1.26951947e+00, 9.34679234e-01, } }, | |||
{ {1.00000000e+00, -1.09276235e+00, 1.00000000e+00, }, {-1.26454687e+00, 9.80322986e-01, } }, | |||
}; | |||
up_filter_.Init(6, kFilter48000x3); | |||
down_filter_.Init(6, kFilter48000x3); | |||
oversampling_factor_ = 3; | |||
} | |||
else if (44100 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter44100x3[7] = // n = 14, wc = 0.302343, cost = 926100 | |||
{ | |||
{ {2.33467524e-04, 3.85146244e-04, 2.33467524e-04, }, {-1.46779940e+00, 5.59300587e-01, } }, | |||
{ {1.00000000e+00, 2.84344987e-01, 1.00000000e+00, }, {-1.39743012e+00, 6.47280334e-01, } }, | |||
{ {1.00000000e+00, -4.81735913e-01, 1.00000000e+00, }, {-1.30466696e+00, 7.63828718e-01, } }, | |||
{ {1.00000000e+00, -8.14458422e-01, 1.00000000e+00, }, {-1.22921466e+00, 8.60153843e-01, } }, | |||
{ {1.00000000e+00, -9.63424410e-01, 1.00000000e+00, }, {-1.18164620e+00, 9.24279595e-01, } }, | |||
{ {1.00000000e+00, -1.03102512e+00, 1.00000000e+00, }, {-1.15782377e+00, 9.63657309e-01, } }, | |||
{ {1.00000000e+00, -1.05757483e+00, 1.00000000e+00, }, {-1.15253824e+00, 9.89272846e-01, } }, | |||
}; | |||
up_filter_.Init(7, kFilter44100x3); | |||
down_filter_.Init(7, kFilter44100x3); | |||
oversampling_factor_ = 3; | |||
} | |||
else if (24000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter24000x5[4] = // n = 8, wc = 0.333333, cost = 480000 | |||
{ | |||
{ {9.93374792e-04, 1.81504524e-03, 9.93374792e-04, }, {-1.28123502e+00, 4.43830055e-01, } }, | |||
{ {1.00000000e+00, 9.69736619e-01, 1.00000000e+00, }, {-1.14056361e+00, 5.73274737e-01, } }, | |||
{ {1.00000000e+00, 3.23593812e-01, 1.00000000e+00, }, {-9.84074266e-01, 7.48267989e-01, } }, | |||
{ {1.00000000e+00, 4.69137219e-02, 1.00000000e+00, }, {-9.17508757e-01, 9.16260523e-01, } }, | |||
}; | |||
up_filter_.Init(4, kFilter24000x5); | |||
down_filter_.Init(4, kFilter24000x5); | |||
oversampling_factor_ = 5; | |||
} | |||
else if (22050 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter22050x6[4] = // n = 8, wc = 0.302343, cost = 529200 | |||
{ | |||
{ {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, } }, | |||
}; | |||
up_filter_.Init(4, kFilter22050x6); | |||
down_filter_.Init(4, kFilter22050x6); | |||
oversampling_factor_ = 6; | |||
} | |||
else if (12000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter12000x10[3] = // n = 6, wc = 0.333333, cost = 360000 | |||
{ | |||
{ {3.42306291e-03, 6.53522273e-03, 3.42306291e-03, }, {-1.13209947e+00, 3.65774415e-01, } }, | |||
{ {1.00000000e+00, 1.42136933e+00, 1.00000000e+00, }, {-9.55595652e-01, 5.55195466e-01, } }, | |||
{ {1.00000000e+00, 1.05842861e+00, 1.00000000e+00, }, {-8.35474882e-01, 8.34840828e-01, } }, | |||
}; | |||
up_filter_.Init(3, kFilter12000x10); | |||
down_filter_.Init(3, kFilter12000x10); | |||
oversampling_factor_ = 10; | |||
} | |||
else if (11025 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter11025x11[3] = // n = 6, wc = 0.329829, cost = 363825 | |||
{ | |||
{ {3.26702718e-03, 6.22983576e-03, 3.26702718e-03, }, {-1.14130758e+00, 3.70354990e-01, } }, | |||
{ {1.00000000e+00, 1.40863044e+00, 1.00000000e+00, }, {-9.69538649e-01, 5.57917370e-01, } }, | |||
{ {1.00000000e+00, 1.03994151e+00, 1.00000000e+00, }, {-8.54328717e-01, 8.35728285e-01, } }, | |||
}; | |||
up_filter_.Init(3, kFilter11025x11); | |||
down_filter_.Init(3, kFilter11025x11); | |||
oversampling_factor_ = 11; | |||
} | |||
else if (8000 <= sample_rate) | |||
{ | |||
const SOSCoefficients kFilter8000x15[3] = // n = 6, wc = 0.333333, cost = 360000 | |||
{ | |||
{ {3.42306291e-03, 6.53522273e-03, 3.42306291e-03, }, {-1.13209947e+00, 3.65774415e-01, } }, | |||
{ {1.00000000e+00, 1.42136933e+00, 1.00000000e+00, }, {-9.55595652e-01, 5.55195466e-01, } }, | |||
{ {1.00000000e+00, 1.05842861e+00, 1.00000000e+00, }, {-8.35474882e-01, 8.34840828e-01, } }, | |||
}; | |||
up_filter_.Init(3, kFilter8000x15); | |||
down_filter_.Init(3, kFilter8000x15); | |||
oversampling_factor_ = 15; | |||
} | |||
else { InitFilter(8000); } | |||
//[[[end]]] | |||
} | |||
}; | |||
} |
@@ -0,0 +1,355 @@ | |||
// Mutable Instruments Ripples 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 <random> | |||
#include "rack.hpp" | |||
#include "aafilter.hpp" | |||
using namespace rack; | |||
class RipplesEngine | |||
{ | |||
public: | |||
struct Frame | |||
{ | |||
// Parameters | |||
float res_knob; // 0 to 1 linear | |||
float freq_knob; // 0 to 1 linear | |||
float fm_knob; // -1 to 1 linear | |||
// Inputs | |||
float res_cv; | |||
float freq_cv; | |||
float fm_cv; | |||
float input; | |||
float gain_cv; | |||
bool gain_cv_present; | |||
// Outputs | |||
float bp2; | |||
float lp2; | |||
float lp4; | |||
float lp4vca; | |||
}; | |||
// Frequency knob | |||
static constexpr float kFreqKnobMin = 20.f; | |||
static constexpr float kFreqKnobMax = 20000.f; | |||
static constexpr float kFreqKnobVoltage = | |||
std::log2f(kFreqKnobMax / kFreqKnobMin); | |||
// Calculate base and multiplier values to pass to configParam so that the | |||
// knob value is labeled in Hz. | |||
// Model the knob as a generic V/oct input with 100k input impedance. | |||
// Assume the internal knob voltage `v` is on the interval [0, vmax] and | |||
// let `p` be the position of the knob varying linearly along [0, 1]. Then, | |||
// freq = fmin * 2^v | |||
// v = vmax * p | |||
// vmax = log2(fmax / fmin) | |||
// freq = fmin * 2^(log2(fmax / fmin) * p) | |||
// = fmin * (fmax / fmin)^p | |||
static constexpr float kFreqKnobDisplayBase = kFreqKnobMax / kFreqKnobMin; | |||
static constexpr float kFreqKnobDisplayMultiplier = kFreqKnobMin; | |||
// Frequency CV amplifier | |||
// The 2164's gain constant is -33mV/dB. Multiply by 6dB/1V to find the | |||
// nominal gain of the amplifier. | |||
static constexpr float kVCAGainConstant = -33e-3f; | |||
static constexpr float kPlus6dB = 20.f * std::log10(2.f); | |||
static constexpr float kFreqAmpGain = kVCAGainConstant * kPlus6dB; | |||
static constexpr float kFreqInputR = 100e3f; | |||
static constexpr float kFreqAmpR = -kFreqAmpGain * kFreqInputR; | |||
static constexpr float kFreqAmpC = 560e-12f; | |||
// Resonance CV amplifier | |||
static constexpr float kResInputR = 22e3f; | |||
static constexpr float kResKnobV = 12.f; | |||
static constexpr float kResKnobR = 62e3f; | |||
static constexpr float kResAmpR = 47e3f; | |||
static constexpr float kResAmpC = 560e-12f; | |||
// Gain CV amplifier | |||
static constexpr float kGainInputR = 27e3f; | |||
static constexpr float kGainNormalV = 12.f; | |||
static constexpr float kGainNormalR = 15e3f; | |||
static constexpr float kGainAmpR = 47e3f; | |||
static constexpr float kGainAmpC = 560e-12f; | |||
// Filter core | |||
static constexpr float kFilterMaxCutoff = kFreqKnobMax; | |||
static constexpr float kFilterCellR = 33e3f; | |||
static constexpr float kFilterCellRC = | |||
1.f / (2.f * M_PI * kFilterMaxCutoff); | |||
static constexpr float kFilterCellC = kFilterCellRC / kFilterCellR; | |||
static constexpr float kFilterInputR = 100e3f; | |||
static constexpr float kFilterInputGain = kFilterCellR / kFilterInputR; | |||
static constexpr float kFilterCellSelfModulation = 0.01f; | |||
// Filter core feedback path | |||
static constexpr float kFeedbackRt = 22e3f; | |||
static constexpr float kFeedbackRb = 1e3f; | |||
static constexpr float kFeedbackR = kFeedbackRt + kFeedbackRb; | |||
static constexpr float kFeedbackGain = kFeedbackRb / kFeedbackR; | |||
// Filter core feedforward path | |||
static constexpr float kFeedforwardRt = 300e3f; | |||
static constexpr float kFeedforwardRb = 1e3f; | |||
static constexpr float kFeedforwardR = kFeedforwardRt + kFeedforwardRb; | |||
static constexpr float kFeedforwardGain = kFeedforwardRb / kFeedforwardR; | |||
static constexpr float kFeedforwardC = 220e-9f; | |||
// Filter output amplifiers | |||
static constexpr float kLP2Gain = -100e3f / 39e3f; | |||
static constexpr float kLP4Gain = -100e3f / 33e3f; | |||
static constexpr float kBP2Gain = -100e3f / 39e3f; | |||
// VCA | |||
static constexpr float kVCAInputC = 4.7e-6f; | |||
static constexpr float kVCAInputRt = 100e3f; | |||
static constexpr float kVCAInputRb = 1e3f; | |||
static constexpr float kVCAInputR = kVCAInputRt + kVCAInputRb; | |||
static constexpr float kVCAInputGain = kVCAInputRb / kVCAInputR; | |||
static constexpr float kVCAOutputR = 100e3f; | |||
// Voltage-to-current converters | |||
// Saturation voltage at BJT collector | |||
static constexpr float kVtoICollectorVSat = -10.f; | |||
RipplesEngine() | |||
{ | |||
setSampleRate(1.f); | |||
} | |||
void setSampleRate(float sample_rate) | |||
{ | |||
sample_time_ = 1.f / sample_rate; | |||
cell_voltage_ = 0.f; | |||
aa_filter_.Init(sample_rate); | |||
float oversample_rate = | |||
sample_rate * aa_filter_.GetOversamplingFactor(); | |||
float freq_cut = 1.f / (2.f * M_PI * kFreqAmpR * kFreqAmpC); | |||
float res_cut = 1.f / (2.f * M_PI * kResAmpR * kResAmpC); | |||
float gain_cut = 1.f / (2.f * M_PI * kGainAmpR * kGainAmpC); | |||
float ff_cut = 1.f / (2.f * M_PI * kFeedforwardR * kFeedforwardC); | |||
auto cutoffs = simd::float_4(ff_cut, freq_cut, res_cut, gain_cut); | |||
rc_filters_.setCutoffFreq(cutoffs / oversample_rate); | |||
float vca_cut = 1.f / (2.f * M_PI * kVCAInputR * kVCAInputC); | |||
vca_hpf_.setCutoffFreq(vca_cut / oversample_rate); | |||
} | |||
void process(Frame& frame) | |||
{ | |||
// Calculate equivalent frequency CV | |||
float v_oct = 0.f; | |||
v_oct += (frame.freq_knob - 1.f) * kFreqKnobVoltage; | |||
v_oct += frame.freq_cv; | |||
v_oct += frame.fm_cv * frame.fm_knob; | |||
v_oct = std::min(v_oct, 0.f); | |||
// Calculate resonance control current | |||
float i_reso = VtoIConverter(kResAmpR, frame.res_cv, kResInputR, | |||
frame.res_knob * kResKnobV, kResKnobR); | |||
// Calculate gain control current | |||
float gain_cv = frame.gain_cv; | |||
float gain_input_r = kGainInputR; | |||
if (!frame.gain_cv_present) | |||
{ | |||
gain_cv = kGainNormalV; | |||
gain_input_r += kGainNormalR; | |||
} | |||
float i_vca = VtoIConverter(kGainAmpR, gain_cv, gain_input_r); | |||
// Pack and upsample inputs | |||
int oversampling_factor = aa_filter_.GetOversamplingFactor(); | |||
float timestep = sample_time_ / oversampling_factor; | |||
// Add noise to input to bootstrap self-oscillation | |||
float input = frame.input + 1e-6 * (random::uniform() - 0.5f); | |||
auto inputs = simd::float_4(input, v_oct, i_reso, i_vca); | |||
inputs *= oversampling_factor; | |||
simd::float_4 outputs; | |||
for (int i = 0; i < oversampling_factor; i++) | |||
{ | |||
inputs = aa_filter_.ProcessUp((i == 0) ? inputs : 0.f); | |||
outputs = CoreProcess(inputs, timestep); | |||
outputs = aa_filter_.ProcessDown(outputs); | |||
} | |||
frame.bp2 = outputs[0]; | |||
frame.lp2 = outputs[1]; | |||
frame.lp4 = outputs[2]; | |||
frame.lp4vca = outputs[3]; | |||
} | |||
protected: | |||
float sample_time_; | |||
simd::float_4 cell_voltage_; | |||
ripples::AAFilter<simd::float_4> aa_filter_; | |||
dsp::TRCFilter<simd::float_4> rc_filters_; | |||
dsp::TRCFilter<float> vca_hpf_; | |||
// High-rate processing core | |||
// inputs: vector containing (input, v_oct, i_reso, i_vca) | |||
// returns: vector containing (bp2, lp2, lp4, lp4vca) | |||
simd::float_4 CoreProcess(simd::float_4 inputs, float timestep) | |||
{ | |||
rc_filters_.process(inputs); | |||
// Lowpass the control signals | |||
simd::float_4 control = rc_filters_.lowpass(); | |||
float v_oct = control[1]; | |||
float i_reso = control[2]; | |||
float i_vca = control[3]; | |||
// Highpass the input signal to generate the resonance feedforward | |||
float feedforward = rc_filters_.highpass()[0]; | |||
// The 2164's input terminal is a virtual ground, so we can model the | |||
// vca-integrator cell like so: | |||
// ___ | |||
// ┌───────────┤___├───────────┐ | |||
// │ R │ | |||
// │ ┌───┤├───┤ | |||
// │ A*ix │ C │ | |||
// ___ │ ┌───┐ │ │╲ │ | |||
// vin ──┤___├──┤ ┌─┤ → ├───┴─┤-╲____├── vout | |||
// R │ ↓ix │ └───┘ ┌─┤+╱ | |||
// ╧ ╧ ╧ │╱ | |||
// | |||
// We can see that: | |||
// ix = (vin + vout) / R | |||
// A*ix = -C * dvout/dt | |||
// Thus, | |||
// dvout/dt = -A/(RC) * (vin + vout) | |||
// Calculate -A / RC | |||
simd::float_4 rad_per_s = -std::exp2f(v_oct) / kFilterCellRC; | |||
// Emulate the filter core | |||
cell_voltage_ = StepRK2(timestep, cell_voltage_, [&](simd::float_4 vout) | |||
{ | |||
// vout contains the initial cell voltages (v0, v1 v2, v3) | |||
// Rotate cell voltages. vin will contain (v3, v0, v1, v2) | |||
simd::float_4 vin = | |||
_mm_shuffle_ps(vout.v, vout.v, _MM_SHUFFLE(2, 1, 0, 3)); | |||
// The core input is the filter input plus the resonance signal | |||
float vp = feedforward * kFeedforwardGain; | |||
float vn = vout[3] * kFeedbackGain; | |||
float res = kFilterCellR * OTAVCA(vp, vn, i_reso); | |||
simd::float_4 in = inputs[0] * kFilterInputGain + res; | |||
// Replace lowest element of vin with lowest element from in | |||
vin = _mm_move_ss(vin.v, in.v); | |||
// Now, vin contains (in, v0, v1, v2) | |||
// and vout contains (v0, v1, v2, v3) | |||
// Their sum gives us vin + vout for each cell | |||
simd::float_4 vsum = vin + vout; | |||
simd::float_4 dvout = rad_per_s * vsum; | |||
// Generate some even-order harmonics via self-modulation | |||
dvout *= (1.f + vsum * kFilterCellSelfModulation); | |||
return dvout; | |||
}); | |||
float lp1 = cell_voltage_[0]; | |||
float lp2 = cell_voltage_[1]; | |||
float lp4 = cell_voltage_[3]; | |||
float bp2 = (lp1 + lp2) * kBP2Gain; | |||
vca_hpf_.process(lp4); | |||
float lp4vca = vca_hpf_.highpass(); | |||
lp4vca = -kVCAOutputR * OTAVCA(0.f, lp4vca * kVCAInputGain, i_vca); | |||
lp2 *= kLP2Gain; | |||
lp4 *= kLP4Gain; | |||
return simd::float_4(bp2, lp2, lp4, lp4vca); | |||
} | |||
// Solves an ODE system using the 2nd order Runge-Kutta method | |||
template <typename T, typename F> | |||
T StepRK2(float dt, T y, F f) | |||
{ | |||
T k1 = f(y); | |||
T k2 = f(y + k1 * dt / 2.f); | |||
return y + dt * k2; | |||
} | |||
// Model of Ripples nonlinear CV voltage-to-current converters | |||
float VtoIConverter( | |||
float rfb, // Amplifier feedback resistor | |||
float vc, float rc, // CV voltage and input resistor | |||
float vp = 0.f, float rp = 1e12f) // Knob voltage and resistor | |||
{ | |||
// Find nominal voltage at the BJT collector, ignoring nonlinearity | |||
float vnom = -(vc * rfb / rc + vp * rfb / rp); | |||
// Apply clipping - naive for now | |||
float vout = std::max(vnom, kVtoICollectorVSat); | |||
// Find voltage at the opamp's negative terminal | |||
float nrc = rp * rfb; | |||
float nrp = rc * rfb; | |||
float nrfb = rc * rp; | |||
float vneg = (vc * nrc + vp * nrp + vout * nrfb) / (nrc + nrp + nrfb); | |||
// Find output current | |||
float iout = (vneg - vout) / rfb; | |||
return std::max(iout, 0.f); | |||
} | |||
// Model of LM13700 OTA VCA, neglecting linearizing diodes | |||
// vp: voltage at positive input terminal | |||
// vn: voltage at negative input terminal | |||
// i_abc: amplifier bias current | |||
// returns: OTA output current | |||
template <typename T> | |||
T OTAVCA(T vp, T vn, T i_abc) | |||
{ | |||
// For the derivation of this equation, see this fantastic paper: | |||
// http://www.openmusiclabs.com/files/otadist.pdf | |||
// Thanks guest! | |||
// | |||
// i_out = i_abc * (e^(vi/vt) - 1) / (e^(vi/vt) + 1) | |||
// or equivalently, | |||
// i_out = i_abc * tanh(vi / (2vt)) | |||
constexpr float kTemperature = 40.f; // Silicon temperature in Celsius | |||
constexpr float kKoverQ = 8.617333262145e-5; | |||
constexpr float kKelvin = 273.15f; // 0C in K | |||
constexpr float kVt = kKoverQ * (kTemperature + kKelvin); | |||
T vi = vp - vn; | |||
T z = vi / (2 * kVt); | |||
// Pade approximant of tanh(z) | |||
T z2 = z * z; | |||
T q = 12.f + z2; | |||
T p = 12.f * z * q / (36.f * z2 + q * q); | |||
return i_abc * p; | |||
} | |||
}; |
@@ -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 ripples | |||
{ | |||
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]; | |||
}; | |||
} |