#include "dBiz.hpp" #include "dsp/decimator.hpp" #include "dsp/filter.hpp" namespace rack_plugin_dBiz { extern float sawTable[2048]; extern float triTable[2048]; template struct VoltageControlledOscillator { bool analog = false; bool soft = false; float lastSyncValue = 0.0f; float phase = 0.0f; float freq; float pw = 0.5f; float pitch; bool syncEnabled = false; bool syncDirection = false; Decimator sinDecimator; Decimator triDecimator; Decimator sawDecimator; Decimator sqrDecimator; RCFilter sqrFilter; // For analog detuning effect float pitchSlew = 0.0f; int pitchSlewIndex = 0; float sinBuffer[OVERSAMPLE] = {}; float triBuffer[OVERSAMPLE] = {}; float sawBuffer[OVERSAMPLE] = {}; float sqrBuffer[OVERSAMPLE] = {}; 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 C4 freq = 261.626f * powf(2.0f, pitch / 12.0f); } void setPulseWidth(float pulseWidth) { const float pwMin = 0.01f; pw = clamp(pulseWidth, pwMin, 1.0f - pwMin); } 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) * engineGetSampleTime(); pitchSlewIndex = 0; } } // Advance phase float deltaPhase = clamp(freq * deltaTime, 1e-6, 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.0f, 1.0f) if (syncEnabled) { syncValue -= 0.01f; if (syncValue > 0.0f && lastSyncValue <= 0.0f) { float deltaSync = syncValue - lastSyncValue; syncCrossing = 1.0f - syncValue / deltaSync; syncCrossing *= OVERSAMPLE; syncIndex = (int)syncCrossing; syncCrossing -= syncIndex; } lastSyncValue = syncValue; } if (syncDirection) deltaPhase *= -1.0f; sqrFilter.setCutoff(40.0f * deltaTime); for (int i = 0; i < OVERSAMPLE; i++) { if (syncIndex == i) { if (soft) { syncDirection = !syncDirection; deltaPhase *= -1.0f; } else { // phase = syncCrossing * deltaPhase / OVERSAMPLE; phase = 0.0f; } } if (analog) { // Quadratic approximation of sine, slightly richer harmonics if (phase < 0.5f) sinBuffer[i] = 1.f - 16.f * powf(phase - 0.25f, 2); else sinBuffer[i] = -1.f + 16.f * powf(phase - 0.75f, 2); sinBuffer[i] *= 1.08f; } else { sinBuffer[i] = sinf(2.f * M_PI * phase); } if (analog) { triBuffer[i] = 1.25f * interpolateLinear(triTable, phase * 2047.f); } else { if (phase < 0.25f) triBuffer[i] = 4.f * phase; else if (phase < 0.75f) triBuffer[i] = 2.f - 4.f * phase; else triBuffer[i] = -4.f + 4.f * phase; } if (analog) { sawBuffer[i] = 1.66f * interpolateLinear(sawTable, phase * 2047.f); } else { if (phase < 0.5f) sawBuffer[i] = 2.f * phase; else sawBuffer[i] = -2.f + 2.f * phase; } sqrBuffer[i] = (phase < pw) ? 1.f : -1.f; if (analog) { // Simply filter here sqrFilter.process(sqrBuffer[i]); sqrBuffer[i] = 0.71f * sqrFilter.highpass(); } // Advance phase phase += deltaPhase / OVERSAMPLE; phase = eucmod(phase, 1.0f); } } float sin() { return sinDecimator.process(sinBuffer); } float tri() { return triDecimator.process(triBuffer); } float saw() { return sawDecimator.process(sawBuffer); } float sqr() { return sqrDecimator.process(sqrBuffer); } float light() { return sinf(2 * M_PI * phase); } }; struct TROSC : Module { enum ParamIds { LINK_A_PARAM, LINK_B_PARAM, MODE_A_PARAM, SYNC_A_PARAM, MODE_B_PARAM, SYNC_B_PARAM, MODE_C_PARAM, SYNC_C_PARAM, WAVE_A_SEL_PARAM, WAVE_B_SEL_PARAM, WAVE_C_SEL_PARAM, FREQ_A_PARAM, FINE_A_PARAM, FREQ_B_PARAM, FINE_B_PARAM, FREQ_C_PARAM, FINE_C_PARAM, FM_A_PARAM, FM_B_PARAM, FM_C_PARAM, LEVEL_A_PARAM, LEVEL_B_PARAM, LEVEL_C_PARAM, WAVE_A_MIX, WAVE2_A_MIX, WAVE_B_MIX, WAVE2_B_MIX, WAVE_C_MIX, C_WIDTH_PARAM, NUM_PARAMS }; enum InputIds { PITCH_A_INPUT, PITCH_B_INPUT, PITCH_C_INPUT, SYNC_A_INPUT, SYNC_B_INPUT, SYNC_C_INPUT, FM_A_INPUT, FM_B_INPUT, FM_C_INPUT, A_WAVE_MIX_INPUT, B_WAVE_MIX_INPUT, C_WAVE_MIX_INPUT, A_VOL_IN, B_VOL_IN, C_VOL_IN, C_WIDTH_INPUT, NUM_INPUTS }; enum OutputIds { A_OUTPUT, B_OUTPUT, C_OUTPUT, MIX_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; VoltageControlledOscillator<8, 8> a_osc; VoltageControlledOscillator<8, 8> b_osc; VoltageControlledOscillator<8, 8> c_osc; TROSC() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} void step() override; // For more advanced Module features, read Rack's engine.hpp header file // - toJson, fromJson: serialization of internal data // - onSampleRateChange: event triggered by a change of sample rate // - onReset, onRandomize, onCreate, onDelete: implements special behavior when user clicks these from the context menu }; void TROSC::step() { float a_pitchCv = 0.0; float b_pitchCv = 0.0; float c_pitchCv = 0.0; a_osc.analog = params[MODE_A_PARAM].value > 0.0f; a_osc.soft = params[SYNC_A_PARAM].value <= 0.0f; b_osc.analog = params[MODE_B_PARAM].value > 0.0f; b_osc.soft = params[SYNC_B_PARAM].value <= 0.0f; c_osc.analog = params[MODE_C_PARAM].value > 0.0f; c_osc.soft = params[SYNC_C_PARAM].value <= 0.0f; float a_pitchFine = 3.0f * quadraticBipolar(params[FINE_A_PARAM].value); a_pitchCv = 12.0f * inputs[PITCH_A_INPUT].value; float b_pitchFine = 3.0f * quadraticBipolar(params[FINE_B_PARAM].value); if(params[LINK_A_PARAM].value==1) b_pitchCv = 12.0f * inputs[PITCH_B_INPUT].value; else b_pitchCv = a_pitchCv ; float c_pitchFine = 3.0f * quadraticBipolar(params[FINE_C_PARAM].value); if (params[LINK_B_PARAM].value == 1) c_pitchCv = 12.0f * inputs[PITCH_C_INPUT].value; else c_pitchCv = b_pitchCv; if (inputs[FM_A_INPUT].active) { a_pitchCv += quadraticBipolar(params[FM_A_PARAM].value) * 12.0f * inputs[FM_A_INPUT].value; } a_osc.setPitch(params[FREQ_A_PARAM].value, a_pitchFine + a_pitchCv); a_osc.syncEnabled = inputs[SYNC_A_INPUT].active; if (inputs[FM_B_INPUT].active) { b_pitchCv += quadraticBipolar(params[FM_B_PARAM].value) * 12.0f * inputs[FM_B_INPUT].value; } b_osc.setPitch(params[FREQ_B_PARAM].value, b_pitchFine + b_pitchCv); b_osc.syncEnabled = inputs[SYNC_B_INPUT].active; if (inputs[FM_C_INPUT].active) { c_pitchCv += quadraticBipolar(params[FM_C_PARAM].value) * 12.0f * inputs[FM_C_INPUT].value; } c_osc.setPitch(params[FREQ_C_PARAM].value, c_pitchFine + c_pitchCv); c_osc.setPulseWidth(0.5+params[C_WIDTH_PARAM].value * inputs[C_WIDTH_INPUT].value / 10.0f); c_osc.syncEnabled = inputs[SYNC_C_INPUT].active; a_osc.process(engineGetSampleTime(), inputs[SYNC_A_INPUT].value); b_osc.process(engineGetSampleTime(), inputs[SYNC_A_INPUT].value); c_osc.process(engineGetSampleTime(), inputs[SYNC_A_INPUT].value); // Set output float wave_a = clamp(params[WAVE_A_MIX].value, 0.0f, 1.0f); float wave2_a = clamp(params[WAVE2_A_MIX].value, 0.0f, 1.0f); float mix_a = clamp(params[WAVE_A_SEL_PARAM].value, 0.0f, 1.0f)*clamp(inputs[A_WAVE_MIX_INPUT].normalize(10.0f) / 10.0f, 0.0f, 1.0f); float wave_b = clamp(params[WAVE_B_MIX].value, 0.0f, 1.0f); float wave2_b = clamp(params[WAVE2_B_MIX].value, 0.0f, 1.0f); float mix_b = clamp(params[WAVE_B_SEL_PARAM].value, 0.0f, 1.0f)*clamp(inputs[B_WAVE_MIX_INPUT].normalize(10.0f) / 10.0f, 0.0f, 1.0f); float wave_c = clamp(params[WAVE_C_MIX].value, 0.0f, 1.0f); float mix_c = clamp(params[WAVE_C_SEL_PARAM].value, 0.0f, 1.0f)*clamp(inputs[C_WAVE_MIX_INPUT].normalize(10.0f) / 10.0f, 0.0f, 1.0f); float out_a; float out2_a; float a_out; float out_b; float out2_b; float b_out; float out_c; float out2_c; float c_out; float mixa,mixb,mixc; out_a = crossfade(a_osc.sin(), a_osc.tri(), wave_a); out2_a = crossfade(a_osc.saw(), a_osc.sqr(), wave2_a); a_out = crossfade(out_a, out2_a, mix_a); out_b = crossfade(b_osc.sin(), b_osc.tri(), wave_b); out2_b = crossfade(b_osc.saw(), b_osc.sqr(), wave2_b); b_out = crossfade(out_b, out2_b, mix_b); out_c = crossfade(c_osc.sin(), c_osc.tri(), wave_c); out2_c =c_osc.sqr(); c_out = crossfade(out_c, out2_c, mix_c); mixa = 2.0f * (a_out)*params[LEVEL_A_PARAM].value*clamp(inputs[A_VOL_IN].normalize(10.0f) / 10.0f, 0.0f, 1.0f); outputs[A_OUTPUT].value= mixa; mixb = 2.0f * (b_out)*params[LEVEL_B_PARAM].value*clamp(inputs[B_VOL_IN].normalize(10.0f) / 10.0f, 0.0f, 1.0f); outputs[B_OUTPUT].value = mixb; mixc = 2.0f * (c_out)*params[LEVEL_C_PARAM].value*clamp(inputs[C_VOL_IN].normalize(10.0f) / 10.0f, 0.0f, 1.0f); outputs[C_OUTPUT].value = mixc; outputs[MIX_OUTPUT].value = mixa+mixb+mixc; } struct TROSCWidget : ModuleWidget { TROSCWidget(TROSC *module) : ModuleWidget(module) { setPanel(SVG::load(assetPlugin(plugin, "res/TROSC.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))); int space = 170; int vspace = 50; addParam(ParamWidget::create(Vec(30,20), module, TROSC::FREQ_A_PARAM,-54.0f, 54.0f, 0.0f)); addParam(ParamWidget::create(Vec(30, 150), module, TROSC::FREQ_B_PARAM, -54.0f, 54.0f, 0.0f)); addParam(ParamWidget::create(Vec(30, 280), module, TROSC::FREQ_C_PARAM, -54.0f, 54.0f, 0.0f)); addParam(ParamWidget::create(Vec(5, 5 + 20), module, TROSC::MODE_A_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(5, 5 + 150), module, TROSC::MODE_B_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(5, 5 + 280), module, TROSC::MODE_C_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(143, 75 + 20), module, TROSC::SYNC_A_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(143, 75 + 150), module, TROSC::SYNC_B_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(143, 75 + 280), module, TROSC::SYNC_C_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(110, 20), module, TROSC::FINE_A_PARAM, -1.0f, 1.0f, 0.0f)); addParam(ParamWidget::create(Vec(110, 150), module, TROSC::FINE_B_PARAM, -1.0f, 1.0f, 0.0f)); addParam(ParamWidget::create(Vec(110, 280), module, TROSC::FINE_C_PARAM, -1.0f, 1.0f, 0.0f)); addParam(ParamWidget::create(Vec(150, 20 -10), module, TROSC::FM_A_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(150, 150-10), module, TROSC::FM_B_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(150, 280-10), module, TROSC::FM_C_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(250, vspace+20), module, TROSC::LEVEL_A_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(250, vspace+150), module, TROSC::LEVEL_B_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(250, vspace+280), module, TROSC::LEVEL_C_PARAM , 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(20+space, 20), module, TROSC::WAVE_A_MIX, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(50 + space, 20), module, TROSC::WAVE2_A_MIX, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(20 + space, 150), module, TROSC::WAVE_B_MIX, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(50 + space, 150), module, TROSC::WAVE2_B_MIX, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(20 + space, 280), module, TROSC::WAVE_C_MIX, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(40 + space, 290), module, TROSC::C_WIDTH_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(73 + space, 20 -10), module, TROSC::WAVE_A_SEL_PARAM, 0.0, 1.0, 0.5)); addParam(ParamWidget::create(Vec(73 + space, 150-10), module, TROSC::WAVE_B_SEL_PARAM, 0.0, 1.0, 0.5)); addParam(ParamWidget::create(Vec(73 + space, 280-10), module, TROSC::WAVE_C_SEL_PARAM, 0.0, 1.0, 0.5)); addInput(Port::create(Vec(100 + space,20-13), Port::INPUT, module, TROSC::A_WAVE_MIX_INPUT)); addInput(Port::create(Vec(100 + space,150-13), Port::INPUT, module, TROSC::B_WAVE_MIX_INPUT)); addInput(Port::create(Vec(100 + space,280-13), Port::INPUT, module, TROSC::C_WAVE_MIX_INPUT)); addInput(Port::create(Vec(2, 30 + 20), Port::INPUT, module, TROSC::PITCH_A_INPUT)); addInput(Port::create(Vec(2, 30 + 150), Port::INPUT, module, TROSC::PITCH_B_INPUT)); addInput(Port::create(Vec(2, 30 + 280), Port::INPUT, module, TROSC::PITCH_C_INPUT)); addParam(ParamWidget::create(Vec(60, 90 + 20), module, TROSC::LINK_A_PARAM,0.0,1.0,0.0)); addParam(ParamWidget::create(Vec(60, 90 + 150),module, TROSC::LINK_B_PARAM,0.0,1.0,0.0)); addInput(Port::create(Vec(115, 55 + 20), Port::INPUT, module, TROSC::SYNC_A_INPUT)); addInput(Port::create(Vec(115, 55 + 150), Port::INPUT, module, TROSC::SYNC_B_INPUT)); addInput(Port::create(Vec(115, 55 + 280), Port::INPUT, module, TROSC::SYNC_C_INPUT)); addInput(Port::create(Vec(155, 45 + 20), Port::INPUT, module, TROSC::FM_A_INPUT)); addInput(Port::create(Vec(155, 45 + 150), Port::INPUT, module, TROSC::FM_B_INPUT)); addInput(Port::create(Vec(155, 45 + 280), Port::INPUT, module, TROSC::FM_C_INPUT)); addInput(Port::create(Vec(290,vspace+10+20), Port::INPUT, module, TROSC::A_VOL_IN)); addInput(Port::create(Vec(290,vspace+10+150), Port::INPUT, module, TROSC::B_VOL_IN)); addInput(Port::create(Vec(290,vspace+10+280), Port::INPUT, module, TROSC::C_VOL_IN)); addInput(Port::create(Vec(215, 50 + 280), Port::INPUT, module, TROSC::C_WIDTH_INPUT)); addOutput(Port::create(Vec(290, 30), Port::OUTPUT, module, TROSC::MIX_OUTPUT)); addOutput(Port::create(Vec(255, 20 + 20), Port::OUTPUT, module, TROSC::A_OUTPUT)); addOutput(Port::create(Vec(255, 20 + 150), Port::OUTPUT, module, TROSC::B_OUTPUT)); addOutput(Port::create(Vec(255, 20 + 280), Port::OUTPUT, module, TROSC::C_OUTPUT)); } }; } // namespace rack_plugin_dBiz using namespace rack_plugin_dBiz; RACK_PLUGIN_MODEL_INIT(dBiz, TROSC) { // Specify the Module and ModuleWidget subclass, human-readable // author name for categorization per plugin, module slug (should never // change), human-readable module name, and any number of tags // (found in `include/tags.hpp`) separated by commas. Model *modelTROSC = Model::create("dBiz", "TROSC", "Triple Oscillator", OSCILLATOR_TAG); return modelTROSC; }