Browse Source

Add Ripples.

tags/v1.2.0
Andrew Belt 2 years ago
parent
commit
16184aa411
9 changed files with 4343 additions and 22 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +1
    -1
      LICENSE.md
  3. +4
    -4
      README.md
  4. +3
    -3
      plugin.json
  5. +3475
    -11
      res/Ripples.svg
  6. +55
    -3
      src/Ripples.cpp
  7. +329
    -0
      src/Ripples/aafilter.hpp
  8. +355
    -0
      src/Ripples/ripples.hpp
  9. +118
    -0
      src/Ripples/sos.hpp

+ 3
- 0
CHANGELOG.md View File

@@ -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
- 1
LICENSE.md View File

@@ -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.



+ 4
- 4
README.md View File

@@ -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


+ 3
- 3
plugin.json View File

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

+ 3475
- 11
res/Ripples.svg
File diff suppressed because it is too large
View File


+ 55
- 3
src/Ripples.cpp View File

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



+ 329
- 0
src/Ripples/aafilter.hpp View File

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

}

+ 355
- 0
src/Ripples/ripples.hpp View File

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

+ 118
- 0
src/Ripples/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 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];
};

}

Loading…
Cancel
Save