// Mutable Instruments Streams 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 .
#pragma once
#include
#include "aafilter.hpp"
namespace streams
{
using namespace rack;
class AnalogEngine
{
public:
struct ChannelFrame
{
// Parameters
float level_mod_knob;
float response_knob;
// Inputs
float signal_in;
float level_cv;
float dac_cv;
float pwm_cv;
// Outputs
float signal_out;
float adc_out;
};
struct Frame
{
ChannelFrame ch1;
ChannelFrame ch2;
};
AnalogEngine()
{
Reset();
SetSampleRate(1.f);
}
void Reset(void)
{
v_out_ = 0.f;
}
void SetSampleRate(float sample_rate)
{
sample_time_ = 1.f / sample_rate;
oversampling_ = OversamplingFactor(sample_rate);
float oversampled_rate = sample_rate * oversampling_;
up_filter_[0].Init(sample_rate);
up_filter_[1].Init(sample_rate);
down_filter_.Init(sample_rate);
auto cutoff = float_4(kDACFilterCutoff,
kDACFilterCutoff,
kPWMFilterCutoff,
kPWMFilterCutoff);
rc_lpf_.setCutoffFreq(cutoff / oversampled_rate);
}
void Process(Frame& frame)
{
auto a_inputs = float_4(frame.ch1.signal_in,
frame.ch2.signal_in,
frame.ch1.level_cv,
frame.ch2.level_cv);
auto d_inputs = float_4(frame.ch1.dac_cv,
frame.ch2.dac_cv,
frame.ch1.pwm_cv,
frame.ch2.pwm_cv);
auto level_mod = float_4(frame.ch1.level_mod_knob,
frame.ch2.level_mod_knob, 0.f, 0.f);
auto response = float_4(frame.ch1.response_knob,
frame.ch2.response_knob, 0.f, 0.f);
float_4 output;
float timestep = sample_time_ / oversampling_;
for (int i = 0; i < oversampling_; i++)
{
// Upsample and apply anti-aliasing filters
a_inputs = up_filter_[0].Process(
(i == 0) ? (a_inputs * oversampling_) : 0.f);
d_inputs = up_filter_[1].Process(
(i == 0) ? (d_inputs * oversampling_) : 0.f);
rc_lpf_.process(d_inputs);
d_inputs = rc_lpf_.lowpass();
float_4 signal_in = a_inputs;
float_4 level_cv = _mm_movehl_ps(a_inputs.v, a_inputs.v);
float_4 dac_cv = d_inputs;
auto level = CalculateLevel(dac_cv, level_cv, level_mod, response);
signal_in *= level;
float_4 pwm_cv = _mm_movehl_ps(d_inputs.v, d_inputs.v);
pwm_cv *= kPWMCVInputR / (kPWMCVInputR + kPWMCVOutputR);
auto rad_per_s = -PinVoltageToLevel(pwm_cv) / kFilterCoreRC;
// Solve each VCF cell using the backward Euler method.
float_4 v_in = _mm_movelh_ps(signal_in.v, v_out_.v);
v_out_ = (v_in + v_out_) * simd::exp(rad_per_s * timestep) - v_in;
v_out_ = simd::clamp(v_out_, -kClampVoltage, kClampVoltage);
// Pre-downsample anti-alias filtering
// output will contain the lower two elements from level and the
// upper two elements from v_out_
output =
_mm_shuffle_ps(level.v, v_out_.v, _MM_SHUFFLE(3, 2, 1, 0));
output = down_filter_.Process(output);
}
frame.ch1.signal_out = output[2];
frame.ch2.signal_out = output[3];
// We don't care too much about aliasing in this signal, since it's
// only fed back to the digital section for VU metering.
output = LevelToPinVoltage(output);
frame.ch1.adc_out = output[0];
frame.ch2.adc_out = output[1];
}
protected:
using float_4 = simd::float_4;
float sample_time_;
int oversampling_;
UpsamplingAAFilter up_filter_[2];
DownsamplingAAFilter down_filter_;
dsp::TRCFilter rc_lpf_;
float_4 v_out_;
// Calculate level from the VCA control pin voltage
template
T PinVoltageToLevel(T v_control)
{
return simd::pow(10.f, v_control / (kVCAGainConstant * 20.f));
}
// Calculate VCA control pin voltage from level
template
T LevelToPinVoltage(T level)
{
T volts = kVCAGainConstant * 20.f * simd::log10(level);
return simd::ifelse(level > 0.f, volts, kClampVoltage);
}
// Calculate level from the CV inputs and pots
template
T CalculateLevel(T dac_cv, T level_cv, T level_mod, T response)
{
T power = (kLevelResponseMinR + kLevelResponsePotR) /
(kLevelResponseMinR + (kLevelResponsePotR * response));
T i_level = level_mod * level_cv / kLevelCVInputR;
T i_dac = dac_cv / (kDACCVOutputR + kDACCVInputR);
T i_in = i_level + i_dac + kVCAOffsetI;
T base = -i_in / kLevelRefI;
T level = simd::pow(base, power);
level = simd::ifelse(base > 0.f, level, 0.f);
return simd::fmin(level, kVCAMaxLevel);
}
// The 2164's gain constant is -33mV/dB
static constexpr float kVCAGainConstant = -33e-3f;
// The 2164's maximum gain is +20dB
static constexpr float kVCAMaxLevel = 10.f;
static constexpr float kLevelCVInputR = 100e3f;
static constexpr float kDACCVOutputR = 11e3f;
static constexpr float kDACCVInputR = 14e3f;
static constexpr float kVCAOffsetI = -10.f / 10e6f;
static constexpr float kPWMCVOutputR = 1.2e3f;
static constexpr float kPWMCVInputR = 2.5e3f;
// Level response VCA input current
static constexpr float kLevelRefI = -10.f / 200e3f;
// Level response potentiometer and series resistor
static constexpr float kLevelResponsePotR = 10e3f;
static constexpr float kLevelResponseMinR = 510.f;
// Opamp saturation voltage
static constexpr float kClampVoltage = 10.5f;
// 1N4148 forward voltage
static constexpr float kDiodeVoltage = 745e-3f;
static constexpr float kDACFilterCutoff = 12.7e3f;
static constexpr float kPWMFilterCutoff = 242.f;
static constexpr float kFilterCoreR = 100e3f;
static constexpr float kFilterCoreC = 33e-12f;
static constexpr float kFilterCoreRC = kFilterCoreR * kFilterCoreC;
};
}