diff --git a/plugin.json b/plugin.json index 923da00..66bdcd8 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "slug": "Befaco", - "version": "1.1.1", + "version": "2.0.0", "license": "GPL-3.0-or-later", "name": "Befaco", "author": "VCV, Ewan Hemingway", diff --git a/src/ADSR.cpp b/src/ADSR.cpp index c4824e7..c7e09e5 100644 --- a/src/ADSR.cpp +++ b/src/ADSR.cpp @@ -225,20 +225,10 @@ struct ADSR : Module { return minStageTime * std::pow(maxStageTime / minStageTime, cv); } - struct TriggerGateParamQuantity : ParamQuantity { - std::string getDisplayValueString() override { - switch ((EnvelopeMode) getValue()) { - case ADSR::GATE_MODE: return "Gate"; - case ADSR::TRIGGER_MODE: return "Trigger"; - default: assert(false); - } - } - }; - ADSR() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configParam(TRIGG_GATE_TOGGLE_PARAM, GATE_MODE, TRIGGER_MODE, GATE_MODE, "Mode"); - configParam(MANUAL_TRIGGER_PARAM, 0.f, 1.f, 0.f, "Trigger envelope"); + configSwitch(TRIGG_GATE_TOGGLE_PARAM, GATE_MODE, TRIGGER_MODE, GATE_MODE, "Mode", {"Gate", "Trigger"}); + configButton(MANUAL_TRIGGER_PARAM, "Trigger envelope"); configParam(SHAPE_PARAM, 0.f, 1.f, 0.f, "Envelope shape"); configParam(ATTACK_PARAM, 0.f, 1.f, 0.f, "Attack time", "s", maxStageTime / minStageTime, minStageTime); @@ -246,6 +236,18 @@ struct ADSR : Module { configParam(SUSTAIN_PARAM, 0.f, 1.f, 0.f, "Sustain level", "%", 0.f, 100.f); configParam(RELEASE_PARAM, 0.f, 1.f, 0.f, "Release time", "s", maxStageTime / minStageTime, minStageTime); + configInput(TRIGGER_INPUT, "Trigger"); + configInput(CV_ATTACK_INPUT, "Attack CV"); + configInput(CV_DECAY_INPUT, "Decay CV"); + configInput(CV_SUSTAIN_INPUT, "Sustain CV"); + configInput(CV_RELEASE_INPUT, "Release CV"); + + configOutput(OUT_OUTPUT, "Envelope"); + configOutput(STAGE_ATTACK_OUTPUT, "Attack stage"); + configOutput(STAGE_DECAY_OUTPUT, "Decay stage"); + configOutput(STAGE_SUSTAIN_OUTPUT, "Sustain stage"); + configOutput(STAGE_RELEASE_OUTPUT, "Release stage"); + cvDivider.setDivision(16); } diff --git a/src/ChoppingKinky.cpp b/src/ChoppingKinky.cpp index 512a8c2..9652066 100644 --- a/src/ChoppingKinky.cpp +++ b/src/ChoppingKinky.cpp @@ -1,362 +1,365 @@ -#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<> oversampler[NUM_CHANNELS]; - int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling - - dsp::BiquadFilter 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"); - - cacheWaveshaperResponses(); - - // calculate up/downsampling rates - onSampleRateChange(); - } - - void onSampleRateChange() override { - float sampleRate = APP->engine->getSampleRate(); - - blockDCFilter.setParameters(dsp::BiquadFilter::HIGHPASS, 10.3f / sampleRate, M_SQRT1_2, 1.0f); - - 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/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()); - - struct DCMenuItem : MenuItem { - ChoppingKinky* module; - void onAction(const event::Action& e) override { - module->blockDC ^= true; - } - }; - DCMenuItem* dcItem = createMenuItem("Block DC on Chopp", CHECKMARK(module->blockDC)); - dcItem->module = module; - menu->addChild(dcItem); - - menu->addChild(createMenuLabel("Oversampling mode")); - - struct ModeItem : MenuItem { - ChoppingKinky* module; - int oversamplingIndex; - void onAction(const event::Action& e) override { - module->oversamplingIndex = oversamplingIndex; - module->onSampleRateChange(); - } - }; - for (int i = 0; i < 5; i++) { - ModeItem* modeItem = createMenuItem(string::f("%dx", int (1 << i))); - modeItem->rightText = CHECKMARK(module->oversamplingIndex == i); - modeItem->module = module; - modeItem->oversamplingIndex = i; - menu->addChild(modeItem); - } - } -}; - - +#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<> oversampler[NUM_CHANNELS]; + int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling + + dsp::BiquadFilter 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"); + + 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.setParameters(dsp::BiquadFilter::HIGHPASS, 10.3f / sampleRate, M_SQRT1_2, 1.0f); + + 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/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")); + + struct ModeItem : MenuItem { + ChoppingKinky* module; + int oversamplingIndex; + void onAction(const event::Action& e) override { + module->oversamplingIndex = oversamplingIndex; + module->onSampleRateChange(); + } + }; + for (int i = 0; i < 5; i++) { + ModeItem* modeItem = createMenuItem(string::f("%dx", int (1 << i))); + modeItem->rightText = CHECKMARK(module->oversamplingIndex == i); + modeItem->module = module; + modeItem->oversamplingIndex = i; + menu->addChild(modeItem); + } + } +}; + + Model* modelChoppingKinky = createModel("ChoppingKinky"); \ No newline at end of file diff --git a/src/Muxlicer.hpp b/src/Muxlicer.hpp index ef38195..71476cd 100644 --- a/src/Muxlicer.hpp +++ b/src/Muxlicer.hpp @@ -15,13 +15,13 @@ struct BefacoSwitchMomentary : SVGSwitch { void onDragStart(const event::DragStart& e) override { latched = false; - startMouseY = APP->scene->rack->mousePos.y; + startMouseY = APP->scene->rack->getMousePos().y; ParamWidget::onDragStart(e); } void onDragMove(const event::DragMove& e) override { - - float diff = APP->scene->rack->mousePos.y - startMouseY; + ParamQuantity* paramQuantity = getParamQuantity(); + float diff = APP->scene->rack->getMousePos().y - startMouseY; // Once the user has dragged the mouse a "threshold" distance, latch // to disallow further changes of state until the mouse is released. @@ -41,14 +41,11 @@ struct BefacoSwitchMomentary : SVGSwitch { void onDragEnd(const event::DragEnd& e) override { // on release, the switch resets to default/neutral/middle position - paramQuantity->setValue(1); + getParamQuantity()->setValue(1); latched = false; ParamWidget::onDragEnd(e); } - // do nothing - void randomize() override {} - float startMouseY = 0.f; bool latched = false; }; @@ -361,14 +358,33 @@ struct Muxlicer : Module { Muxlicer() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configParam(Muxlicer::PLAY_PARAM, STATE_PLAY_ONCE, STATE_PLAY, STATE_STOPPED, "Play switch"); + configSwitch(Muxlicer::PLAY_PARAM, STATE_PLAY_ONCE, STATE_PLAY, STATE_STOPPED, "Play switch", {"Play Once/Reset", "", "Play/Stop"}); + getParamQuantity(Muxlicer::PLAY_PARAM)->randomizeEnabled = false; configParam(Muxlicer::ADDRESS_PARAM, -1.f, 7.f, -1.f, "Address"); + getParamQuantity(Muxlicer::ADDRESS_PARAM)->randomizeEnabled = false; + configParam(Muxlicer::GATE_MODE_PARAM, -1.f, 8.f, 1.f, "Gate mode"); configParam(Muxlicer::DIV_MULT_PARAM, 0, 1, 0.5, "Main clock mult/div"); for (int i = 0; i < SEQUENCE_LENGTH; ++i) { - configParam(Muxlicer::LEVEL_PARAMS + i, 0.0, 1.0, 1.0, string::f("Gain %d", i + 1)); - } + configParam(Muxlicer::LEVEL_PARAMS + i, 0.0, 1.0, 1.0, string::f("Gain step %d", i + 1)); + configInput(Muxlicer::MUX_INPUTS + i, string::f("Step %d", i + 1)); + configOutput(Muxlicer::GATE_OUTPUTS + i, string::f("Gate step %d", i + 1)); + configOutput(Muxlicer::MUX_OUTPUTS + i, string::f("Step %d", i + 1)); + configLight(Muxlicer::GATE_LIGHTS + i, string::f("Step %d gates", i + 1)); + } + configOutput(Muxlicer::EOC_OUTPUT, "End of cycle trigger"); + configOutput(Muxlicer::CLOCK_OUTPUT, "Clock"); + configOutput(Muxlicer::ALL_GATES_OUTPUT, "All gates"); + configOutput(Muxlicer::ALL_OUTPUT, "All"); + configOutput(Muxlicer::COM_OUTPUT, "COM I/O"); + + configInput(Muxlicer::GATE_MODE_INPUT, "Gate mode CV"); + configInput(Muxlicer::ADDRESS_INPUT, "Address CV"); + configInput(Muxlicer::CLOCK_INPUT, "Clock"); + configInput(Muxlicer::RESET_INPUT, "One shot/reset"); + configInput(Muxlicer::COM_INPUT, "COM I/O"); + configInput(Muxlicer::ALL_INPUT, "All"); onReset(); } @@ -941,7 +957,7 @@ struct MuxlicerWidget : ModuleWidget { OutputRangeItem* outputRangeItem = createMenuItem("All In Normalled Value", "▸"); outputRangeItem->module = module; menu->addChild(outputRangeItem); - } + } else { menu->addChild(createMenuLabel("All In Normalled Value (disabled)")); } @@ -969,16 +985,16 @@ struct MuxlicerWidget : ModuleWidget { void clearCables() { for (int i = Muxlicer::MUX_OUTPUTS; i <= Muxlicer::MUX_OUTPUTS_LAST; ++i) { - APP->scene->rack->clearCablesOnPort(outputs[i]); + APP->scene->rack->clearCablesOnPort(this->getOutput(i)); } - APP->scene->rack->clearCablesOnPort(inputs[Muxlicer::COM_INPUT]); - APP->scene->rack->clearCablesOnPort(inputs[Muxlicer::ALL_INPUT]); + APP->scene->rack->clearCablesOnPort(this->getInput(Muxlicer::COM_INPUT)); + APP->scene->rack->clearCablesOnPort(this->getInput(Muxlicer::ALL_INPUT)); for (int i = Muxlicer::MUX_INPUTS; i <= Muxlicer::MUX_INPUTS_LAST; ++i) { - APP->scene->rack->clearCablesOnPort(inputs[i]); + APP->scene->rack->clearCablesOnPort(this->getInput(i)); } - APP->scene->rack->clearCablesOnPort(outputs[Muxlicer::COM_OUTPUT]); - APP->scene->rack->clearCablesOnPort(outputs[Muxlicer::ALL_OUTPUT]); + APP->scene->rack->clearCablesOnPort(this->getOutput(Muxlicer::COM_OUTPUT)); + APP->scene->rack->clearCablesOnPort(this->getOutput(Muxlicer::ALL_OUTPUT)); } // set ports visibility, either for 1 input -> 8 outputs or 8 inputs -> 1 output @@ -987,19 +1003,18 @@ struct MuxlicerWidget : ModuleWidget { bool visibleToggle = (mode == Muxlicer::COM_1_IN_8_OUT); for (int i = Muxlicer::MUX_OUTPUTS; i <= Muxlicer::MUX_OUTPUTS_LAST; ++i) { - outputs[i]->visible = visibleToggle; + this->getOutput(i)->visible = visibleToggle; } - inputs[Muxlicer::COM_INPUT]->visible = visibleToggle; - outputs[Muxlicer::ALL_OUTPUT]->visible = visibleToggle; + this->getInput(Muxlicer::COM_INPUT)->visible = visibleToggle; + this->getOutput(Muxlicer::ALL_OUTPUT)->visible = visibleToggle; for (int i = Muxlicer::MUX_INPUTS; i <= Muxlicer::MUX_INPUTS_LAST; ++i) { - inputs[i]->visible = !visibleToggle; + this->getInput(i)->visible = !visibleToggle; } - outputs[Muxlicer::COM_OUTPUT]->visible = !visibleToggle; - inputs[Muxlicer::ALL_INPUT]->visible = !visibleToggle; + this->getOutput(Muxlicer::COM_OUTPUT)->visible = !visibleToggle; + this->getInput(Muxlicer::ALL_INPUT)->visible = !visibleToggle; } - };