|  |  | @@ -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<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | 
		
	
		
			
			|  |  |  | addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); | 
		
	
		
			
			|  |  |  | addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | 
		
	
		
			
			|  |  |  | addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<Davies1900hLargeWhiteKnob>(mm2px(Vec(26.051, 21.999)), module, ChoppingKinky::FOLD_A_PARAM)); | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<Davies1900hLargeWhiteKnob>(mm2px(Vec(26.051, 62.768)), module, ChoppingKinky::FOLD_B_PARAM)); | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(10.266, 83.297)), module, ChoppingKinky::CV_A_PARAM)); | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(30.277, 83.297)), module, ChoppingKinky::CV_B_PARAM)); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.127, 27.843)), module, ChoppingKinky::IN_A_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(26.057, 42.228)), module, ChoppingKinky::IN_GATE_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.104, 56.382)), module, ChoppingKinky::IN_B_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.209, 98.499)), module, ChoppingKinky::CV_A_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.259, 98.499)), module, ChoppingKinky::VCA_CV_A_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.308, 98.499)), module, ChoppingKinky::CV_B_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.358, 98.499)), module, ChoppingKinky::VCA_CV_B_INPUT)); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(20.23, 109.669)), module, ChoppingKinky::OUT_CHOPP_OUTPUT)); | 
		
	
		
			
			|  |  |  | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(31.091, 110.747)), module, ChoppingKinky::OUT_B_OUTPUT)); | 
		
	
		
			
			|  |  |  | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(9.589, 110.777)), module, ChoppingKinky::OUT_A_OUTPUT)); | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(26.057, 33.307)), module, ChoppingKinky::LED_A_LIGHT)); | 
		
	
		
			
			|  |  |  | addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(26.057, 51.53)), module, ChoppingKinky::LED_B_LIGHT)); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  |  | 
		
	
		
			
			|  |  |  | void appendContextMenu(Menu* menu) override { | 
		
	
		
			
			|  |  |  | ChoppingKinky* module = dynamic_cast<ChoppingKinky*>(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<DCMenuItem>("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<ModeItem>(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<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); | 
		
	
		
			
			|  |  |  | addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); | 
		
	
		
			
			|  |  |  | addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | 
		
	
		
			
			|  |  |  | addChild(createWidget<Knurlie>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<Davies1900hLargeWhiteKnob>(mm2px(Vec(26.051, 21.999)), module, ChoppingKinky::FOLD_A_PARAM)); | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<Davies1900hLargeWhiteKnob>(mm2px(Vec(26.051, 62.768)), module, ChoppingKinky::FOLD_B_PARAM)); | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(10.266, 83.297)), module, ChoppingKinky::CV_A_PARAM)); | 
		
	
		
			
			|  |  |  | addParam(createParamCentered<BefacoTinyKnob>(mm2px(Vec(30.277, 83.297)), module, ChoppingKinky::CV_B_PARAM)); | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.127, 27.843)), module, ChoppingKinky::IN_A_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(26.057, 42.228)), module, ChoppingKinky::IN_GATE_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(6.104, 56.382)), module, ChoppingKinky::IN_B_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(5.209, 98.499)), module, ChoppingKinky::CV_A_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(15.259, 98.499)), module, ChoppingKinky::VCA_CV_A_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(25.308, 98.499)), module, ChoppingKinky::CV_B_INPUT)); | 
		
	
		
			
			|  |  |  | addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(35.358, 98.499)), module, ChoppingKinky::VCA_CV_B_INPUT)); | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(20.23, 109.669)), module, ChoppingKinky::OUT_CHOPP_OUTPUT)); | 
		
	
		
			
			|  |  |  | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(31.091, 110.747)), module, ChoppingKinky::OUT_B_OUTPUT)); | 
		
	
		
			
			|  |  |  | addOutput(createOutputCentered<BefacoOutputPort>(mm2px(Vec(9.589, 110.777)), module, ChoppingKinky::OUT_A_OUTPUT)); | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(26.057, 33.307)), module, ChoppingKinky::LED_A_LIGHT)); | 
		
	
		
			
			|  |  |  | addChild(createLightCentered<SmallLight<RedLight>>(mm2px(Vec(26.057, 51.53)), module, ChoppingKinky::LED_B_LIGHT)); | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | void appendContextMenu(Menu* menu) override { | 
		
	
		
			
			|  |  |  | ChoppingKinky* module = dynamic_cast<ChoppingKinky*>(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<ModeItem>(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, ChoppingKinkyWidget>("ChoppingKinky"); |