#include "global_pre.hpp" #include "Bidoo.hpp" #include "BidooComponents.hpp" #include "dsp/samplerate.hpp" #include "dsp/decimator.hpp" #include "dsp/filter.hpp" #include "global_ui.hpp" using namespace std; namespace rack_plugin_Bidoo { extern float sawTable[2048]; extern float triTable[2048]; struct CLACOS : Module { enum ParamIds { PITCH_PARAM, FINE_PARAM, DIST_X_PARAM, DIST_Y_PARAM = DIST_X_PARAM + 4, WAVEFORM_PARAM = DIST_Y_PARAM + 4, MODE_PARAM = WAVEFORM_PARAM + 4, SYNC_PARAM, FM_PARAM, NUM_PARAMS }; enum InputIds { PITCH_INPUT, SYNC_INPUT, DIST_X_INPUT, DIST_Y_INPUT = DIST_X_INPUT + 4, WAVEFORM_INPUT = DIST_Y_PARAM + 4, FM_INPUT = WAVEFORM_INPUT + 4, NUM_INPUTS }; enum OutputIds { MAIN_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; bool analog = false; bool soft = false; float lastSyncValue = 0.0f; float phase = 0.0f; float phaseDist = 0.0f; float phaseDistX[4] = {0.5f}; float phaseDistY[4] = {0.5f}; int waveFormIndex[4] = {0}; float freq; float pitch; bool syncEnabled = false; bool syncDirection = false; int index = 0, prevIndex = 3; Decimator<16, 16> mainDecimator; RCFilter sqrFilter; RCFilter mainFilter; // For analog detuning effect float pitchSlew = 0.0f; int pitchSlewIndex = 0; float sinBuffer[16] = {0.0f}; float triBuffer[16] = {0.0f}; float sawBuffer[16] = {0.0f}; float sqrBuffer[16] = {0.0f}; float mainBuffer[16] = {0.0f}; void setPitch(float pitchKnob, float pitchCv) { // Compute frequency pitch = pitchKnob; if (analog) { // Apply pitch slew const float pitchSlewAmount = 3.0f; pitch += pitchSlew * pitchSlewAmount; } else { // Quantize coarse knob if digital mode pitch = roundf(pitch); } pitch += pitchCv; // Note C3 freq = 261.626f * powf(2.0f, pitch / 12.0f); } void process(float deltaTime, float syncValue) { if (analog) { // Adjust pitch slew if (++pitchSlewIndex > 32) { const float pitchSlewTau = 100.0f; // Time constant for leaky integrator in seconds pitchSlew += (randomNormal() - pitchSlew / pitchSlewTau) / engineGetSampleRate(); pitchSlewIndex = 0.0f; } } // Advance phase float deltaPhase = clamp(freq * deltaTime, 1e-6f, 0.5f); // Detect sync int syncIndex = -1; // Index in the oversample loop where sync occurs [0, OVERSAMPLE) float syncCrossing = 0.0f; // Offset that sync occurs [0.0, 1.0) if (syncEnabled) { syncValue -= 0.01f; if (syncValue > 0.0f && lastSyncValue <= 0.0f) { float deltaSync = syncValue - lastSyncValue; syncCrossing = 1.0f - syncValue / deltaSync; syncCrossing *= 16; syncIndex = (int)syncCrossing; syncCrossing -= syncIndex; } lastSyncValue = syncValue; } if (syncDirection) deltaPhase *= -1.0f; sqrFilter.setCutoff(40.0f * deltaTime); mainFilter.setCutoff(22000.0f * deltaTime); for (int i = 0; i < 16; i++) { if (syncIndex == i) { if (soft) { syncDirection = !syncDirection; deltaPhase *= -1.0f; } else { phase = 0.0f; phaseDist = 0.0f; } } if (phase<0.25f) index = 0; else if ((phase>=0.25f) && (phase<0.50f)) index = 1; else if ((phase>=0.50f) && (phase<0.75f)) index = 2; else index = 3; if (analog) { // Quadratic approximation of sine, slightly richer harmonics if (phaseDist < 0.5f) sinBuffer[i] = 1.0f - 16.0f * powf(phaseDist - 0.25f, 2.0f); else sinBuffer[i] = -1.0f + 16.0f * powf(phaseDist - 0.75f, 2.0f); sinBuffer[i] *= 1.08f; } else { sinBuffer[i] = sinf(2.0f * M_PI * phaseDist); } if (analog) { triBuffer[i] = 1.25f * interpolateLinear(triTable, phaseDist * 2047.f); } else { if (phaseDist < 0.25f) triBuffer[i] = 4.0f * phaseDist; else if (phaseDist < 0.75f) triBuffer[i] = 2.0f - 4.0f * phaseDist; else triBuffer[i] = -4.0f + 4.0f * phaseDist; } if (analog) { sawBuffer[i] = 1.66f * interpolateLinear(sawTable, phaseDist * 2047.f); } else { if (phaseDist < 0.5f) sawBuffer[i] = 2.0f * phaseDist; else sawBuffer[i] = -2.0f + 2.0f * phaseDist; } sqrBuffer[i] = (phaseDist < 0.5f) ? 1.0f : -1.0f; if (analog) { // Simply filter here sqrFilter.process(sqrBuffer[i]); sqrBuffer[i] = 0.71f * sqrFilter.highpass(); } waveFormIndex[index] = inputs[WAVEFORM_INPUT+index].active ? clamp((int)(rescale(inputs[WAVEFORM_INPUT+index].value,0.0f,10.0f,0.0f,3.0f)),0,3) : clamp((int)(params[WAVEFORM_PARAM+index].value),0,3); if (waveFormIndex[index] == 0) mainBuffer[i]=sinBuffer[i]; else if (waveFormIndex[index] == 1) mainBuffer[i]=triBuffer[i]; else if (waveFormIndex[index] == 2) mainBuffer[i]=sawBuffer[i]; else if (waveFormIndex[index] == 3) mainBuffer[i]=sqrBuffer[i]; mainFilter.process(mainBuffer[i]); mainBuffer[i]=mainFilter.lowpass(); // Advance phase phase += deltaPhase / 16.0f; phase = eucmod(phase, 1.0f); if (phase<=0.25f) index = 0; else if ((phase>0.25f) && (phase<=0.5f)) index = 1; else if ((phase>0.5f) && (phase<=0.75f)) index = 2; else index = 3; if (prevIndex!=index){ phaseDist = phase; prevIndex = index; } else { if (rescale(phase,index*0.25f,(index+1)*0.25f,0.0f,1.0f)<=(phaseDistX[index])) phaseDist = min(phaseDist + (deltaPhase / 16.0f) * phaseDistY[index]/phaseDistX[index], (index+1)*0.25f); else phaseDist = min(phaseDist + (deltaPhase / 16.0f) * (1-phaseDistY[index])/(1-phaseDistX[index]), (index+1)*0.25f); phaseDist = eucmod(phaseDist, 1.0f); } } } float main() { return mainDecimator.process(mainBuffer); } float light() { return sinf(2.0f*M_PI * phase); } CLACOS() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { } void step() override; json_t *toJson() override { json_t *rootJ = json_object(); for (int i=0; i<4; i++) { json_object_set_new(rootJ, ("phaseDistX" + to_string(i)).c_str(), json_real(phaseDistX[i])); json_object_set_new(rootJ, ("phaseDistY" + to_string(i)).c_str(), json_real(phaseDistY[i])); } return rootJ; } void fromJson(json_t *rootJ) override { for (int i=0; i<4; i++) { json_t *phaseDistXJ = json_object_get(rootJ, ("phaseDistX" + to_string(i)).c_str()); if (phaseDistXJ) { phaseDistX[i] = json_real_value(phaseDistXJ); } json_t *phaseDistYJ = json_object_get(rootJ, ("phaseDistY" + to_string(i)).c_str()); if (phaseDistYJ) { phaseDistY[i] = json_real_value(phaseDistYJ); } } } void randomize() override { for (int i=0; i<4; i++) { if ((!inputs[CLACOS::DIST_X_INPUT+i].active) && (!inputs[CLACOS::DIST_X_INPUT+i].active)) { phaseDistX[i] = randomUniform(); phaseDistY[i] = randomUniform(); } } } void reset() override { for (int i=0; i<4; i++) { if ((!inputs[CLACOS::DIST_X_INPUT+i].active) && (!inputs[CLACOS::DIST_X_INPUT+i].active)) { phaseDistX[i] = 0.5; phaseDistY[i] = 0.5; } } } }; void CLACOS::step() { analog = params[MODE_PARAM].value > 0.0f; soft = params[SYNC_PARAM].value <= 0.0f; for (int i=0; i<4; i++) { if (inputs[DIST_X_INPUT+i].active) phaseDistX[i] = rescale(clamp(inputs[DIST_X_INPUT+i].value,0.0f,10.0f),0.0f,10.0f,0.01f,0.99f); if (inputs[DIST_Y_INPUT+i].active) phaseDistY[i] = rescale(clamp(inputs[DIST_Y_INPUT+i].value,0.0f,10.0f),0.0f,10.0f,0.01f,0.99f); } float pitchFine = 3.0f * quadraticBipolar(params[FINE_PARAM].value); float pitchCv = 12.0f * inputs[PITCH_INPUT].value; if (inputs[FM_INPUT].active) { pitchCv += quadraticBipolar(params[FM_PARAM].value) * 12.0f * inputs[FM_INPUT].value; } setPitch(params[PITCH_PARAM].value, pitchFine + pitchCv); syncEnabled = inputs[SYNC_INPUT].active; process(1.0f / engineGetSampleRate(), inputs[SYNC_INPUT].value); // Set output outputs[MAIN_OUTPUT].value = 5.0f * main(); } struct CLACOSDisplay : TransparentWidget { CLACOS *module; int frame = 0; string waveForm; int segmentNumber = 0; float initX = 0.0f; float initY = 0.0f; float dragX = 0.0f; float dragY = 0.0f; CLACOSDisplay() {} void onDragStart(EventDragStart &e) override { dragX = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.x; dragY = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.y; } void onDragMove(EventDragMove &e) override { if ((!module->inputs[CLACOS::DIST_X_INPUT + segmentNumber].active) && (!module->inputs[CLACOS::DIST_X_INPUT + segmentNumber].active)) { float newDragX = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.x; float newDragY = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.y; module->phaseDistX[segmentNumber] = rescale(clamp(initX+(newDragX-dragX),0.0f,70.0f), 0.0f, 70.0f, 0.01f,0.99f); module->phaseDistY[segmentNumber] = rescale(clamp(initY-(newDragY-dragY),0.0f,70.0f), 0.0f, 70.0f, 0.01f,0.99f); } } void onMouseDown(EventMouseDown &e) override { if (e.button == 0) { e.consumed = true; e.target = this; initX = e.pos.x; initY = 70.0f - e.pos.y; } } void draw(NVGcontext *vg) override { if (++frame >= 4) { frame = 0; if (module->waveFormIndex[segmentNumber] == 0) waveForm="SIN"; else if (module->waveFormIndex[segmentNumber] == 1) waveForm="TRI"; else if (module->waveFormIndex[segmentNumber] == 2) waveForm="SAW"; else if (module->waveFormIndex[segmentNumber] == 3) waveForm="SQR"; } nvgFontSize(vg, 10.0f); nvgFillColor(vg, nvgRGBA(42, 87, 117, 255)); nvgText(vg, 12.0f, 79.0f, waveForm.c_str(), NULL); // Draw ref lines nvgStrokeColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x80)); { nvgBeginPath(vg); nvgMoveTo(vg, 0.0f, 35.0f); nvgLineTo(vg, 70.0f, 35.0f); nvgMoveTo(vg, 35.0f, 0.0f); nvgLineTo(vg, 35.0f, 70.0f); nvgClosePath(vg); } nvgStroke(vg); // Draw phase distortion nvgStrokeColor(vg, nvgRGBA(42, 87, 117, 255)); { nvgBeginPath(vg); nvgMoveTo(vg, 0.0f, 70.0f); nvgLineTo(vg, (int)(rescale(module->phaseDistX[segmentNumber], 0.0f,1.0f,0.0f,70.0f)) , 70.0f - (int)(rescale(module->phaseDistY[segmentNumber], 0.0f,1.0f,0.01f,70.0f))); nvgMoveTo(vg, (int)(rescale(module->phaseDistX[segmentNumber], 0.0f,1.0f,0.0f,70.0f)) , 70.0f - (int)(rescale(module->phaseDistY[segmentNumber], 0.0f,1.0f,0.01f,70.0f))); nvgLineTo(vg, 70.0f, 0.0f); nvgClosePath(vg); } nvgStroke(vg); } }; struct CLACOSWidget : ModuleWidget { CLACOSWidget(CLACOS *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/CLACOS.svg"))); addChild(Widget::create(Vec(RACK_GRID_WIDTH, 0))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(Widget::create(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); for (int i = 0; i < 4; i++) { { CLACOSDisplay *display = new CLACOSDisplay(); display->module = module; display->segmentNumber = i; display->box.pos = Vec(3.0f + 74.0f * (i%2), 113.0f + 102.0f * round(i/2)); display->box.size = Vec(70.0f, 70.0f); addChild(display); addParam(ParamWidget::create(Vec(2.0f + 74.0f * (i%2), 194.0f + 102.0f * round(i/2)), module, CLACOS::WAVEFORM_PARAM + i, 0.0f, 3.0f, 0.0f)); addInput(Port::create(Vec(22.0f + 74.0f * (i%2), 196.0f + 102.0f * round(i/2)), Port::INPUT, module, CLACOS::WAVEFORM_INPUT + i)); addInput(Port::create(Vec(40.0f + 74.0f * (i%2), 196.0f + 102.0f * round(i/2)), Port::INPUT, module, CLACOS::DIST_X_INPUT + i)); addInput(Port::create(Vec(57.0f + 74.0f * (i%2), 196.0f + 102.0f * round(i/2)), Port::INPUT, module, CLACOS::DIST_Y_INPUT + i)); } } addParam(ParamWidget::create(Vec(15.0f, 80.0f), module, CLACOS::MODE_PARAM, 0.0f, 1.0f, 1.0f)); addParam(ParamWidget::create(Vec(119.0f, 80.0f), module, CLACOS::SYNC_PARAM, 0.0f, 1.0f, 1.0f)); addParam(ParamWidget::create(Vec(57.0f, 45.0f), module, CLACOS::PITCH_PARAM, -54.0f, 54.0f, 0.0f)); addParam(ParamWidget::create(Vec(114.0f,45.0f), module, CLACOS::FINE_PARAM, -1.0f, 1.0f, 0.0f)); addParam(ParamWidget::create(Vec(18.0f,45.0f), module, CLACOS::FM_PARAM, 0.0f, 1.0f, 0.0f)); addInput(Port::create(Vec(38.0f, 83.0f), Port::INPUT, module, CLACOS::FM_INPUT)); addInput(Port::create(Vec(11.0f, 330.0f), Port::INPUT, module, CLACOS::PITCH_INPUT)); addInput(Port::create(Vec(45.0f, 330.0f), Port::INPUT, module, CLACOS::SYNC_INPUT)); addOutput(Port::create(Vec(114.0f, 330.0f), Port::OUTPUT, module, CLACOS::MAIN_OUTPUT)); } }; } // namespace rack_plugin_Bidoo using namespace rack_plugin_Bidoo; RACK_PLUGIN_MODEL_INIT(Bidoo, CLACOS) { Model *modelCLACOS = Model::create("Bidoo", "clACos", "clACos oscillator", OSCILLATOR_TAG); return modelCLACOS; }