|
|
@@ -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"); |