Skip to content

Instantly share code, notes, and snippets.

@trentgill
Created September 16, 2021 17:48
Show Gist options
  • Save trentgill/66b90114a3acaae23682098e5519d990 to your computer and use it in GitHub Desktop.
Save trentgill/66b90114a3acaae23682098e5519d990 to your computer and use it in GitHub Desktop.
some feedback on chilly-cheese emulation of coldmac
void process(const ProcessArgs& args) override {
// setup macro and macro cv
float macro = params[MACRO_PARAM].getValue();
float macrovolts = (macro * 10) - 5;
float macro_cvin = 0.f;
if ( inputs[MACRO_INPUT].isConnected() ) {
macro_cvin = inputs[MACRO_INPUT].getVoltage();
}
// don't clamp before adding to knob
// the knob & cv are added before clamping in analog.
// this means you can set SURVEY to -5V knob position, and still fully open to +5 with a 10V envelope.
// macro_cvin = clamp(macro_cvin, -5.f, 5.f);
macro += (macro_cvin / 10);
macro = clamp(macro, 0.f, 1.f);
macrovolts += macro_cvin;
macrovolts = clamp(macrovolts, -5.f, 5.f);
// initial jack normalling
float left = -5.f;
float right = 5.f;
float fade = macro;
float or2, and2, slope;
or2 = and2 = slope = macrovolts;
// left and right crossfader calc and normalling
if ( inputs[LEFT_INPUT].isConnected() ) {
left = inputs[LEFT_INPUT].getVoltage();
}
if ( inputs[RIGHT_INPUT].isConnected() ) {
right = inputs[RIGHT_INPUT].getVoltage();
}
if ( inputs[FADE_INPUT].isConnected() ) {
fade = inputs[FADE_INPUT].getVoltage();
fade = (fade / 10.0f) + 0.5f;
fade = clamp(fade, 0.f, 1.f);
}
float offset = inputs[OFFSET_INPUT].getVoltage();
// float leftfaded = ( ( 1.f - fade) * left ) + ( fade * right ) + offset;
// float rightfaded = ( ( 1.f - fade) * right ) + ( fade * left ) + offset;
// just for fun, the optimized algorithm CM uses for the crossfader is:
float fadeddiff = fade * (right - left); // only 1 multply (bc that's expensive in analog)
float leftfaded = left + fadeddiff + offset;
float rightfaded = right - fadeddiff + offset;
// no need to include this in the module, but thought it could be interesting from an algorithm perspective.
leftfaded = clamp(leftfaded, -5.f, 5.f);
rightfaded = clamp(rightfaded, -5.f, 5.f);
outputs[LEFT_OUTPUT].setVoltage(leftfaded);
outputs[RIGHT_OUTPUT].setVoltage(rightfaded);
// begin the cheese mixture
float cheese = dc1.process(left) + dc2.process(right);
// or + and calc and normalling
float or1, or_output, and1, and_output;
or1 = and1 = inputs[OR1_INPUT].getVoltage();
if ( inputs[OR2_INPUT].isConnected() ) {
// and2 is not normalled from or2. if you patch to OR2, AND2 will still be normalled to SURVEY unless you patch to it
// or2 = and2 = inputs[OR2_INPUT].getVoltage();
or2 = inputs[OR2_INPUT].getVoltage();
}
// i think a ternary op is far more readable here
or_output = (or1 >= or2) ? or1 : or2;
// if (or1 >= or2) {
// or_output = or1;
// } else {
// or_output = or2;
// }
or_output = clamp(or_output, -5.f, 5.f);
outputs[OR_OUTPUT].setVoltage(or_output);
if ( inputs[AND1_INPUT].isConnected() ) {
and1 = inputs[AND1_INPUT].getVoltage();
}
if ( inputs[AND2_INPUT].isConnected() ) {
and2 = inputs[AND2_INPUT].getVoltage();
}
if (and1 <= and2) {
and_output = and1;
} else {
and_output = and2;
}
and_output = clamp(and_output, -5.f, 5.f);
outputs[AND_OUTPUT].setVoltage(and_output);
// continue the cheese mixture
cheese = cheese + dc3.process(or1) + dc4.process(and1);
// slope and follow calc
if ( inputs[SLOPE_INPUT].isConnected() ) {
slope = inputs[SLOPE_INPUT].getVoltage();
}
cheese += dc5.process(slope); // penultimate cheese ingredient added
float crease = slope;
slope = abs(slope);
slope = clamp(slope, 0.f, 5.f);
// the analog implementation is a more complex filter network based on the arp2600 envelope follower
// it's a passive network of 4 RC pairs: 3x @60Hz and 1x @72Hz
// then a buffered (ie active) 1pole LPF at 16Hz
// you don't need to emulate it exactly, but perhaps a cascade of a 2POLE @60Hz into a 1POLE at 16Hz would give better transient response
// also, you can probably put this setParamaters call in the setup so it's not happening on every sample as the rate is fixed.
slew.setParameters(dsp::BiquadFilter::LOWPASS_1POLE, 5.f / args.sampleRate, 0.f, 1.f);
float follow = slew.process(slope);
// i'm not sure if this 1.5x gain is 'correct', but the analog version does have a similar gain boost to compensate for filter losses.
follow = clamp(follow * 1.5, 0.f, 10.f);
outputs[SLOPE_OUTPUT].setVoltage(slope);
outputs[FOLLOW_OUTPUT].setVoltage(follow);
// crease/location calc and normalling
if ( inputs[CREASE_INPUT].isConnected() ) {
crease = inputs[CREASE_INPUT].getVoltage();
}
// "location" aka Integrator has 6 different modes - default, glacial, sluggish, slowish, quickish, and snappy
// might i recommend a const array to avoid the big switch:
// const float CREASES[] = {25000, 300000, 100000, 50000, 12500, 6250};
// location += crease/CREASES[mode];
// or you could factor the division into the const array so it happens at compile time, and you just need a multiply.
const float CREASES_[] = {1.f/25000, 1.f/300000, 1.f/100000, 1.f/50000, 1.f/12500, 1.f/6250};
location += crease * CREASES_[mode];
// i did some quick simulations and it seems the time to rise 10V == 4.8s when crease is at +5V.
// so if crease==1V, risetime is (4.8/10)*5 == 2.4s/V == 0.41667V/s
// assume sample_rate == 48kHz, V/sample = 0.41667/48000 == 0.00000868 V/sample
// which is 1/115200, so quite close to your 'sluggish' setting.
// i'm not precious about the time-constant here (and the capacitor in the integrator has a +/-20% tolerance range)
// but again, just getting you some more specific numbers.
// switch(mode) {
// case 0 :
// location += (crease/25000);
// break;
// case 1 :
// location += (crease/300000);
// break;
// case 2 :
// location += (crease/100000);
// break;
// case 3 :
// location += (crease/50000);
// break;
// case 4 :
// location += (crease/12500);
// break;
// case 5 :
// location += (crease/6250);
// break;
// }
location = clamp(location, -5.f, 5.f);
outputs[LOCATION_OUTPUT].setVoltage(location);
cheese += dc6.process(crease); // final cheese ingredient added
if ( crease >= 0.f ) {
crease -= 5.f;
} else {
crease += 5.f;
}
crease = clamp(crease, -5.f, 5.f);
outputs[CREASE_OUTPUT].setVoltage(crease);
// PREPARE THE CHEESE!!!
// while this clamp does practically happen in analog when the mix hits the power rails,
// i'd probably not put it in here, so you can have non-distorted (but very loud) signal generation
// if you do want to clip the signal, i'd encourage a soft-clipper, rather than a brickwall clamp.
cheese = clamp(cheese*macro, -10.0f, 10.0f);
outputs[CHEESE_OUTPUT].setVoltage(cheese);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment