#include "plugin.hpp" #include "ChowDSP.hpp" struct ChoppingKinky : Module { enum ParamIds { FOLD_A_PARAM, FOLD_B_PARAM, CV_A_PARAM, CV_B_PARAM, NUM_PARAMS }; enum InputIds { IN_A_INPUT, IN_B_INPUT, IN_GATE_INPUT, CV_A_INPUT, VCA_CV_A_INPUT, CV_B_INPUT, VCA_CV_B_INPUT, NUM_INPUTS }; enum OutputIds { OUT_CHOPP_OUTPUT, OUT_A_OUTPUT, OUT_B_OUTPUT, NUM_OUTPUTS }; enum LightIds { LED_A_LIGHT, LED_B_LIGHT, NUM_LIGHTS }; enum { CHANNEL_A, CHANNEL_B, CHANNEL_CHOPP, NUM_CHANNELS }; static const int WAVESHAPE_CACHE_SIZE = 256; float waveshapeA[WAVESHAPE_CACHE_SIZE + 1] = {}; float waveshapeBPositive[WAVESHAPE_CACHE_SIZE + 1] = {}; float waveshapeBNegative[WAVESHAPE_CACHE_SIZE + 1] = {}; dsp::SchmittTrigger trigger; bool outputAToChopp = false; float previousA = 0.0; chowdsp::VariableOversampling<6> oversampler[NUM_CHANNELS]; // uses a 2*6=12th order Butterworth filter int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling DCBlocker blockDCFilter; bool blockDC = false; ChoppingKinky() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configParam(FOLD_A_PARAM, 0.f, 2.f, 0.f, "Gain/shape control for channel A"); configParam(FOLD_B_PARAM, 0.f, 2.f, 0.f, "Gain/shape control for channel B"); configParam(CV_A_PARAM, -1.f, 1.f, 0.f, "Channel A CV control attenuverter"); configParam(CV_B_PARAM, -1.f, 1.f, 0.f, "Channel A CV control attenuverter"); configInput(IN_A_INPUT, "A"); configInput(IN_B_INPUT, "B"); configInput(IN_GATE_INPUT, "Chopp"); configInput(CV_A_INPUT, "CV A (with attenuator)"); configInput(VCA_CV_A_INPUT, "CV A"); configInput(CV_B_INPUT, "CV B (with attenuator)"); configInput(VCA_CV_B_INPUT, "CV B"); getInputInfo(CV_B_INPUT)->description = "Normalled to CV A (with attenuator) Input"; configOutput(OUT_CHOPP_OUTPUT, "Chopp"); configOutput(OUT_A_OUTPUT, "A"); configOutput(OUT_B_OUTPUT, "B"); cacheWaveshaperResponses(); // calculate up/downsampling rates onSampleRateChange(); } void onSampleRateChange() override { float sampleRate = APP->engine->getSampleRate(); blockDCFilter.setFrequency(22.05 / sampleRate); for (int channel_idx = 0; channel_idx < NUM_CHANNELS; channel_idx++) { oversampler[channel_idx].setOversamplingIndex(oversamplingIndex); oversampler[channel_idx].reset(sampleRate); } } void process(const ProcessArgs& args) override { float gainA = params[FOLD_A_PARAM].getValue(); gainA += params[CV_A_PARAM].getValue() * inputs[CV_A_INPUT].getVoltage() / 10.f; gainA += inputs[VCA_CV_A_INPUT].getVoltage() / 10.f; gainA = std::max(gainA, 0.f); // CV_B_INPUT is normalled to CV_A_INPUT (input with attenuverter) float gainB = params[FOLD_B_PARAM].getValue(); gainB += params[CV_B_PARAM].getValue() * inputs[CV_B_INPUT].getNormalVoltage(inputs[CV_A_INPUT].getVoltage()) / 10.f; gainB += inputs[VCA_CV_B_INPUT].getVoltage() / 10.f; gainB = std::max(gainB, 0.f); const float inA = inputs[IN_A_INPUT].getVoltageSum(); const float inB = inputs[IN_B_INPUT].getNormalVoltage(inputs[IN_A_INPUT].getVoltageSum()); // if the CHOPP gate is wired in, do chop logic if (inputs[IN_GATE_INPUT].isConnected()) { // TODO: check rescale? trigger.process(rescale(inputs[IN_GATE_INPUT].getVoltageSum(), 0.1f, 2.f, 0.f, 1.f)); outputAToChopp = trigger.isHigh(); } // else zero-crossing detector on input A switches between A and B else { if (previousA > 0 && inA < 0) { outputAToChopp = false; } else if (previousA < 0 && inA > 0) { outputAToChopp = true; } } previousA = inA; const bool choppIsRequired = outputs[OUT_CHOPP_OUTPUT].isConnected(); const bool aIsRequired = outputs[OUT_A_OUTPUT].isConnected() || choppIsRequired; const bool bIsRequired = outputs[OUT_B_OUTPUT].isConnected() || choppIsRequired; if (aIsRequired) { oversampler[CHANNEL_A].upsample(inA * gainA); } if (bIsRequired) { oversampler[CHANNEL_B].upsample(inB * gainB); } if (choppIsRequired) { oversampler[CHANNEL_CHOPP].upsample(outputAToChopp ? 1.f : 0.f); } float* osBufferA = oversampler[CHANNEL_A].getOSBuffer(); float* osBufferB = oversampler[CHANNEL_B].getOSBuffer(); float* osBufferChopp = oversampler[CHANNEL_CHOPP].getOSBuffer(); for (int i = 0; i < oversampler[0].getOversamplingRatio(); i++) { if (aIsRequired) { //osBufferA[i] = wavefolderAResponse(osBufferA[i]); osBufferA[i] = wavefolderAResponseCached(osBufferA[i]); } if (bIsRequired) { //osBufferB[i] = wavefolderBResponse(osBufferB[i]); osBufferB[i] = wavefolderBResponseCached(osBufferB[i]); } if (choppIsRequired) { osBufferChopp[i] = osBufferChopp[i] * osBufferA[i] + (1.f - osBufferChopp[i]) * osBufferB[i]; } } float outA = aIsRequired ? oversampler[CHANNEL_A].downsample() : 0.f; float outB = bIsRequired ? oversampler[CHANNEL_B].downsample() : 0.f; float outChopp = choppIsRequired ? oversampler[CHANNEL_CHOPP].downsample() : 0.f; if (blockDC) { outChopp = blockDCFilter.process(outChopp); } outputs[OUT_A_OUTPUT].setVoltage(outA); outputs[OUT_B_OUTPUT].setVoltage(outB); outputs[OUT_CHOPP_OUTPUT].setVoltage(outChopp); if (inputs[IN_GATE_INPUT].isConnected()) { lights[LED_A_LIGHT].setSmoothBrightness((float) outputAToChopp, args.sampleTime); lights[LED_B_LIGHT].setSmoothBrightness((float)(!outputAToChopp), args.sampleTime); } else { lights[LED_A_LIGHT].setBrightness(0.f); lights[LED_B_LIGHT].setBrightness(0.f); } } float wavefolderAResponseCached(float x) { if (x >= 0) { float j = rescale(clamp(x, 0.f, 10.f), 0.f, 10.f, 0, WAVESHAPE_CACHE_SIZE - 1); return interpolateLinear(waveshapeA, j); } else { return -wavefolderAResponseCached(-x); } } float wavefolderBResponseCached(float x) { if (x >= 0) { float j = rescale(clamp(x, 0.f, 10.f), 0.f, 10.f, 0, WAVESHAPE_CACHE_SIZE - 1); return interpolateLinear(waveshapeBPositive, j); } else { float j = rescale(clamp(-x, 0.f, 10.f), 0.f, 10.f, 0, WAVESHAPE_CACHE_SIZE - 1); return interpolateLinear(waveshapeBNegative, j); } } static float wavefolderAResponse(float x) { if (x < 0) { return -wavefolderAResponse(-x); } float xScaleFactor = 1.f / 20.f; float yScaleFactor = 12.5f; x = x * xScaleFactor; float piecewiseX1 = 0.087; float piecewiseX2 = 0.245; float piecewiseX3 = 0.3252; if (x < piecewiseX1) { float x_ = x / piecewiseX1; return -0.38 * yScaleFactor * (std::sin(M_PI * std::pow(x_, 0.8)) + 1.0 / (3 * 1.6) * std::sin(3 * M_PI * std::pow(x_, 0.8))); } else if (x < piecewiseX2) { float x_ = x - piecewiseX1; return -yScaleFactor * (-0.2 * std::sin(0.5 * M_PI * 12.69 * x_) - 0.24 * std::sin(1.5 * M_PI * 12.69 * x_)); } else if (x < piecewiseX3) { float x_ = 9.8 * (x - piecewiseX2); return -0.33 * yScaleFactor * std::sin(x_ / 0.165) * (1 + 0.9 * std::pow(x_, 3) / (1.0 + 2.0 * std::pow(x_, 6))); } else { float x_ = (x - piecewiseX3) / 0.05; return yScaleFactor * ((0.4274 - 0.031) * std::exp(-std::pow(x_, 2.0)) + 0.031); } } static float wavefolderBResponse(float x) { float xScaleFactor = 1.f / 20.f; float yScaleFactor = 12.5f; x = x * xScaleFactor; // assymetric response if (x > 0) { float piecewiseX1 = 0.117; float piecewiseX2 = 0.2837; if (x < piecewiseX1) { float x_ = x / piecewiseX1; return -0.3 * yScaleFactor * (std::sin(M_PI * std::pow(x_, 0.67)) + 1.0 / (3 * 0.8) * std::sin(3 * M_PI * std::pow(x_, 0.67))); } else if (x < piecewiseX2) { float x_ = x - piecewiseX1; return 0.35 * yScaleFactor * std::sin(12. * M_PI * x_); } else { float x_ = (x - piecewiseX2); return 0.57 * yScaleFactor * std::tanh(x_ / 0.03); } } else { float piecewiseX1 = -0.105; float piecewiseX2 = -0.20722; if (x > piecewiseX1) { float x_ = x / piecewiseX1; return 0.37 * yScaleFactor * (std::sin(M_PI * std::pow(x_, 0.65)) + 1.0 / (3 * 1.2) * std::sin(3 * M_PI * std::pow(x_, 0.65))); } else if (x > piecewiseX2) { float x_ = x - piecewiseX1; return 0.2 * yScaleFactor * std::sin(15 * M_PI * x_) * (1.0 - 10.f * x_); } else { float x_ = (x - piecewiseX2) / 0.07; return yScaleFactor * ((0.4022 - 0.065) * std::exp(-std::pow(x_, 2)) + 0.065); } } } // functional form for waveshapers uses a lot of transcendental functions, so we cache // the response in a LUT void cacheWaveshaperResponses() { for (int i = 0; i < WAVESHAPE_CACHE_SIZE; ++i) { float x = rescale(i, 0, WAVESHAPE_CACHE_SIZE - 1, 0.0, 10.f); waveshapeA[i] = wavefolderAResponse(x); waveshapeBPositive[i] = wavefolderBResponse(+x); waveshapeBNegative[i] = wavefolderBResponse(-x); } } json_t* dataToJson() override { json_t* rootJ = json_object(); json_object_set_new(rootJ, "filterDC", json_boolean(blockDC)); json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex())); return rootJ; } void dataFromJson(json_t* rootJ) override { json_t* filterDCJ = json_object_get(rootJ, "filterDC"); if (filterDCJ) { blockDC = json_boolean_value(filterDCJ); } json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); if (oversamplingIndexJ) { oversamplingIndex = json_integer_value(oversamplingIndexJ); onSampleRateChange(); } } }; struct ChoppingKinkyWidget : ModuleWidget { ChoppingKinkyWidget(ChoppingKinky* module) { setModule(module); setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/panels/ChoppingKinky.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParamCentered(mm2px(Vec(26.051, 21.999)), module, ChoppingKinky::FOLD_A_PARAM)); addParam(createParamCentered(mm2px(Vec(26.051, 62.768)), module, ChoppingKinky::FOLD_B_PARAM)); addParam(createParamCentered(mm2px(Vec(10.266, 83.297)), module, ChoppingKinky::CV_A_PARAM)); addParam(createParamCentered(mm2px(Vec(30.277, 83.297)), module, ChoppingKinky::CV_B_PARAM)); addInput(createInputCentered(mm2px(Vec(6.127, 27.843)), module, ChoppingKinky::IN_A_INPUT)); addInput(createInputCentered(mm2px(Vec(26.057, 42.228)), module, ChoppingKinky::IN_GATE_INPUT)); addInput(createInputCentered(mm2px(Vec(6.104, 56.382)), module, ChoppingKinky::IN_B_INPUT)); addInput(createInputCentered(mm2px(Vec(5.209, 98.499)), module, ChoppingKinky::CV_A_INPUT)); addInput(createInputCentered(mm2px(Vec(15.259, 98.499)), module, ChoppingKinky::VCA_CV_A_INPUT)); addInput(createInputCentered(mm2px(Vec(25.308, 98.499)), module, ChoppingKinky::CV_B_INPUT)); addInput(createInputCentered(mm2px(Vec(35.358, 98.499)), module, ChoppingKinky::VCA_CV_B_INPUT)); addOutput(createOutputCentered(mm2px(Vec(20.23, 109.669)), module, ChoppingKinky::OUT_CHOPP_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(31.091, 110.747)), module, ChoppingKinky::OUT_B_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(9.589, 110.777)), module, ChoppingKinky::OUT_A_OUTPUT)); addChild(createLightCentered>(mm2px(Vec(26.057, 33.307)), module, ChoppingKinky::LED_A_LIGHT)); addChild(createLightCentered>(mm2px(Vec(26.057, 51.53)), module, ChoppingKinky::LED_B_LIGHT)); } void appendContextMenu(Menu* menu) override { ChoppingKinky* module = dynamic_cast(this->module); assert(module); menu->addChild(new MenuSeparator()); menu->addChild(createBoolPtrMenuItem("Block DC on Chopp", "", &module->blockDC)); menu->addChild(createMenuLabel("Oversampling mode")); menu->addChild(createIndexSubmenuItem("Oversampling", {"Off", "x2", "x4", "x8", "x16"}, [ = ]() { return module->oversamplingIndex; }, [ = ](int mode) { module->oversamplingIndex = mode; module->onSampleRateChange(); } )); } }; Model* modelChoppingKinky = createModel("ChoppingKinky");