Created
September 16, 2021 17:48
-
-
Save trentgill/66b90114a3acaae23682098e5519d990 to your computer and use it in GitHub Desktop.
some feedback on chilly-cheese emulation of coldmac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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