////////////////////////////////////////////////////////////////////////////////////////////////////////////// //// KlokSpid is a 8 HP module, initially designed to divide/multiply an external clock frequency (clock //// //// modulator), but it can work as standalone (BPM-based) clock generator, too! //// //// - 2 input jacks: //// //// - external clock (CLK), to work as divider/multiplier (standalone clock generator if not patched). //// //// - multipurpose CV-RATIO/TRIG. jack: //// //// - when running as clock multiplier/divider: CV-controllable ratio (full range /64 to x64). //// //// - when running as BPM-clock generator: trigger input (BPM start/stop, or BPM reset). //// //// - 4 output jacks: gates (default +5V/0V Square waveform). Other gates (%) and 1ms/2ms/5ms triggers //// //// (fixed duration pulses) are possible, via SETUP. //// //// //// //// As standalone (BPM-based) clock generator only: //// //// - any jack may have its custom ratio (via SETUP) - default is x1, for all jacks. //// //// - when set as "Custom" for first time, proposed default ratios are, per jack: /4, x1, x2, and x4. //// //// - jack #4 can be set (via SETUP) to send LFO-based waveform (instead of square/pulse) @ x1 only. //// ////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include "Ohmer.hpp" #include #include namespace rack_plugin_Ohmer { // Dedicated LFO (based on LFO-1 stuff from Fundamental, but simplified as required). // It will be used - if enabled via SETUP - to output specific waveform to jack #4. Disabled by default. // LFO can be enabled to jack #4 but only if this jack is set at default ratio x1. struct LFO { float phase = 0.0f; float pw = 0.5f; float freq = 1.0f; bool offset = false; bool invert = false; LFO() {} void step(float dt) { float deltaPhase = fminf(freq * dt, 0.5f); phase += deltaPhase; if (phase >= 1.0f) phase -= 1.0f; } float sin() { if (offset) return 1.0f - cosf(2*M_PI * phase) * (invert ? -1.0f : 1.0f); else return sinf(2.0f*M_PI * phase) * (invert ? -1.0f : 1.0f); } float tri(float x) { return 4.0f * fabsf(x - roundf(x)); } float tri() { if (offset) return tri(invert ? phase - 0.5f : phase); else return -1.0f + tri(invert ? phase - 0.25f : phase - 0.75f); } float saw(float x) { return 2.0f * (x - roundf(x)); } float saw() { if (offset) return invert ? 2.0f * (1.0f - phase) : 2.0f * phase; else return saw(phase) * (invert ? -1.0f : 1.0f); } }; // KlokSpid module architecture. struct KlokSpidModule : Module { enum ParamIds { PARAM_ENCODER, PARAM_BUTTON, NUM_PARAMS }; enum InputIds { INPUT_CLOCK, INPUT_CV_TRIG, NUM_INPUTS }; enum OutputIds { OUTPUT_1, OUTPUT_2, OUTPUT_3, OUTPUT_4, NUM_OUTPUTS }; enum LightIds { LED_CLK, LED_CV_TRIG, LED_CVMODE, LED_TRIGMODE, LED_SYNC_GREEN, LED_SYNC_RED, NUM_LIGHTS }; // Pointer to encoder (handled as knob). Knob *klokspdEncoder; // Optional LFO for jack #4. LFO LFOjack4; // Module interface definitions, such parameters (encoder, button), input ports, output ports and LEDs. KlokSpidModule() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { onReset(); onRandomize(); } void onReset() override { if (!avoidOnResetReentry) { encoderPrevious = 0; onFirstInitCounter = (long)(engineGetSampleRate() / 4.0f); currentStep = 0; avoidOnResetReentry = true; } } // Inhibit "Randomize" from context-menu / Ctrl+R / Command+R keyboard shortcut over module. void onRandomize() override { } //// GENERAL PURPOSE VARIABLES/FLAGS/TABLES. int onFirstInitCounter = 0; bool avoidOnResetReentry = false; // When true, this avoid OnReset() reentry. //// CLOCK MODULATOR RATIOS. // Real clock ratios (global) list/array. Preset ratios while KlokSpid module runs as clock modulator (can be selected via encoder exclusively). float list_fRatio[31] = {64.0f, 32.0f, 24.0f, 16.0f, 15.0f, 12.0f, 10.0f, 9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f, 0.5f, 1.0f/3.0f, 0.25f, 0.2f, 1.0f/6.0f, 1.0f/7.0f, 0.125f, 1.0f/9.0f, 0.1f, 1.0f/12.0f, 1.0f/15.0f, 0.0625f, 1.0f/24.0f, 0.03125f, 0.015625f}; //// MODEL (GUI THEME). // Current selected KlokSpid model (GUI theme). int Theme = 0; // DMD-font color (default is "Classic" beige model). NVGcolor DMDtextColor = nvgRGB(0x08, 0x08, 0x08); //// Main DMD and small displays (near output jacks). char dmdMessage1[24] = ""; char dmdMessage2[24] = ""; int xOffsetValue = 0; // Horizontal offset on DMD to display for second line. char dispOut1[4] = ""; int xdispOutOffset1 = 0; char dispOut2[4] = ""; int xdispOutOffset2 = 0; char dispOut3[4] = ""; int xdispOutOffset3 = 0; char dispOut4[4] = ""; int xdispOutOffset4 = 0; // Strings for running modes. const std::string runningMode[3] = {"Clk Generator", "Clk Modulator", "Clk CV-Ratio"}; //// STEP-RELATED (REALTIME) COUNTERS/GAPS. // Step related variables: used to determine the frequency of source signal, and when KlokSpid must sends relevant pulses to output(s). long long int currentStep = 0; long long int previousStep = 0; long long int expectedStep = 0; long stepGap = 0; long stepGapPrevious = 0; long long int nextPulseStep[NUM_OUTPUTS] = {0, 0, 0, 0}; // Current jacks states, voltages on input jacks, and button state. bool activeCLK = false; bool activeCLKPrevious = true; bool activeCV = false; bool activeCVPrevious = true; float voltageOnCV = 0.0f; bool buttonPressed = false; // Encoder (registered position to be used on next step for relative move). int encoderCurrent = 0; int encoderPrevious = 0; // Encoder "absolute" (saved to jSon)... int encoderDelta = 0; // 0 if not moved, -1 if counter-clockwise (decrement), 1 if clockwise (increment). // Ratio (clock modulator). int svRatio = 15; // saved value. int rateRatioByEncoder = 15; // Assuming encoder is, by default "centered" to "x1" (= 15). // Clock modulator modes. enum ClkModModeIds { X1, // work at x1. DIV, // divider mode. MULT // muliplier mode. }; // Clock modulator mode, assuming default is X1. int clkModulatorMode = X1; //// SCHMITT TRIGGERS. // Schmitt trigger to check thresholds on CLK input connector. SchmittTrigger CLKInputPort; // Schmitt trigger to handle BPM start/stop state (only when KlokSpid is acting as clock generator) via button. SchmittTrigger runButton; // Schmitt trigger to handle the start/stop toggle button (also used for SETUP to confirm menu/parameter) - via CV/TRIG input port (if configured as "Start/Stop"). SchmittTrigger runTriggerPort; //// RATIO-BY-CV VARIABLES/FLAGS. // Incoming CV may be bipolar (true) or unipolar (false). bool bipolarCV = true; // Is CV used to modulate ratio? bool isRatioCVmod = false; // Real ratio, given by current CV voltage. float rateRatioCV = 0.0f; // Real ratio, given by current CV voltage, integer is required only for display into DMD (to avoid "decimals" cosmetic issues, at the right side of DMD!). int rateRatioCVi = 0; //// BPM-RELATED VARIABLES (STANDALONE CLOCK GENERATOR). // Default BPM (when KlokSpid is acting as clock generator). Default is 120 BPM (centered knob). int svBPM = 120; // saved value. int BPM = 120; // Previous registed BPM (when KlokSpid is acting as clock generator), from previous step. int previousBPM = 120; // Custom jacks ratios (per output jack). By default false, all are X1 (original setting for KlokSpid). True means each jack can receive an optional ratio. bool defOutRatios = true; int outputRatio[4] = {9, 12, 13, 15}; int outputRatioInUse[4] = {12, 12, 12, 12}; float list_outRatiof[25] = {64.0f, 32.0f, 24.0f, 16.0f, 12.0f, 9.0f, 8.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f, 0.5f, 1.0f/3.0f, 0.25f, 0.2f, 1.0f/6.0f, 0.125f, 1.0f/9.0f, 1.0f/12.0f, 0.0625f, 1.0f/24.0f, 0.03125f, 0.015625f}; // Indicates if "CV-RATIO/TRIG." input port (used as trigger, standalone BPM-clock mode only) is a transport trigger (true = toggle start/stop) or reset (false, default) useful for "re-sync" between modules. bool transportTrig = false; // Standalone clock generator mode only: indicates if BPM is running or stopped. bool isBPMRunning = true; bool runBPMOnInit = true; //// SETUP-RELATED VARIABLES/TABLES. // Enumeration of SETUP menu entries. enum setupMenuEntries { SETUP_WELCOME_MESSAGE, // SETUP menu entry for #0 is always dedicated to welcome message ("*- SETUP -*") displayed on DMD. SETUP_CVPOLARITY, // SETUP menu entry for CV polarity (bipolar, or unipolar). SETUP_DURATION, // SETUP menu entry for pulse duration (fixed and gate-based parameters). SETUP_OUTVOLTAGE, // SETUP menu entry for output voltage. SETUP_OUTSRATIOS, // SETUP menu entry for custom ratio concerning all jacks (all at x1, or custom). SETUP_OUT1RATIO, // SETUP menu entry for custom ratio concerning output jack #1. SETUP_OUT2RATIO, // SETUP menu entry for custom ratio concerning output jack #2. SETUP_OUT3RATIO, // SETUP menu entry for custom ratio concerning output jack #3. SETUP_OUT4RATIO, // SETUP menu entry for custom ratio concerning output jack #4. SETUP_OUT4LFO, // SETUP menu entry for LFO to output jack #4. SETUP_OUT4LFOPOLARITY, // SETUP menu entry for LFO polarity to output jack #4 (bipolar, or unipolar). SETUP_CVTRIG, // SETUP menu entry describing how CV/TRIG input port is working (as start/stop toggle, or as "RST" input). SETUP_EXIT, // Lastest menu entry is always used to exit SETUP (options are "Save/Exit", "Canc/Exit", "Review" or "Factory"). NUM_SETUP_ENTRIES // This position indicates how many entries the KlokSpid's SETUP menu have. }; // Strings for SETUP entries. const std::string setupMenuName[NUM_SETUP_ENTRIES] = {"*-SETUP-*", "CV Polarity", "Pulse Durat.", "Out. Voltage", "Outp. Ratios", "Out. 1 Ratio", "Out. 2 Ratio", "Out. 3 Ratio", "Out. 4 Ratio", "Out. 4 LFO", "LFO Polarity", "TRIG. Jack", "Exit SETUP"}; // Strings for SETUP possible parameters. std::string setupParamName[NUM_SETUP_ENTRIES][25]; // Related horizontal offsets (second line of DMD). int setupParamXOffset[NUM_SETUP_ENTRIES][25]; // This flag indicates if KlokSpid module is currently running SETUP, or not. bool isSetupRunning = false; // This flag indicates if KlokSpid module is entering SETUP (2 seconds delay), or not. bool isEnteringSetup = false; // This flag indicates if KlokSpid module is exiting SETUP (2 seconds delay), or not. bool isExitingSetup = false; // This flag is designed to avoid continuous SETUP entries/exits while button is continously held. bool allowedButtonHeld = false; // Item index (edited parameter number). int setup_ParamIdx = 0; // Current edited value for selected parameter. int setup_CurrentValue = 0; // Table containing number of possible values for each parameter. int setup_NumValue[NUM_SETUP_ENTRIES] = {0, 2, 9, 4, 2, 25, 25, 25, 25, 7, 2, 2, 4}; // Default factory values for each parameter. int setup_Factory[NUM_SETUP_ENTRIES] = {0, 0, 5, 0, 0, 9, 12, 13, 15, 0, 0, 1, 1}; // Table containing current values for all parameters. int setup_Current[NUM_SETUP_ENTRIES] = {0, 0, 5, 0, 0, 9, 12, 13, 15, 0, 0, 1, 1}; // Table containing edited parameters during SETUP (will be filled when entering SETUP). int setup_Edited[NUM_SETUP_ENTRIES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; // Table containing backup edited parameters during SETUP (will be filled when entering SETUP). int setup_Backup[NUM_SETUP_ENTRIES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; // Counter (as "delay") used to enter and (optionally) to saved/exit SETUP quickly on long press. long setupCounter = 0; //// PULSE TO OUTPUT RELATED VARIABLES AND PULSE GENERATORS. // Enumeration of possible pulse durations: fixed 1 ms, fixed 2 ms, fixed 5 ms, Gate 1/4, Gate 1/3, Square, Gate 2/3, Gate 3/4, Gate 95%. enum PulseDurations { FIXED1MS, // Fixed 1 ms. FIXED2MS, // Fixed 2 ms. FIXED5MS, // Fixed 5 ms. GATE25, // Gate 1/4 (25%). GATE33, // Gate 1/3 (33%). SQUARE, // Square waveform. GATE66, // Gate 2/3 (66%). GATE75, // Gate 3/4 (75%). GATE95, // Gate 95%. }; // Pulse counter for divider mode (set at max divider value, minus 1). int pulseDivCounter[NUM_OUTPUTS] = {63, 63, 63, 63}; // Pulse counter for multiplier mode, to avoid continuous pulse when no more receiving (set at max divider value, minus 1). Kind of "timeout". int pulseMultCounter[NUM_OUTPUTS] = {0, 0, 0, 0}; // Pulse generators, to send "pulses" to output jacks (one pulse generator per output jack). PulseGenerator sendPulse[NUM_OUTPUTS]; // These flags are related to pulse generators (current pulse state). bool sendingOutput[NUM_OUTPUTS] = {false, false, false, false}; // This flag indicates if sending pulse (one per output jack) is allowed (true) or not (false). bool canPulse[NUM_OUTPUTS] = {false, false, false, false}; // Current pulse duration (time in second). Default is fixed 1 ms at start. Operational can be changed via SETUP. float pulseDuration[NUM_OUTPUTS] = {0.001f, 0.001f, 0.001f, 0.001f}; // Extension of "pulseDuration" value (for square and gate modes), set as square wave (50 %) by default, can be changed via SETUP. int pulseDurationExt = SQUARE; // Voltage for outputs (pulses/gates), default is +5V, can be changed to +10V, +12V (+11.7V) or +2V instead, via SETUP. float outVoltage = 5.0f; // Special LFO output on jack #4. int jack4LFO = 0; bool jack4LFObipolar = true; bool resetPhase = true; ////////////////////////////////////// //// CLK LED AFTERGLOW. //// ////////////////////////////////////// // Counter used for red CLK LED afterglow (used together with "ledClkAfterglow" boolean flag). long ledClkDelay = 0; // long is required for highest engine samplerates! // This flag controls CLK (red) LED afterglow (active or not). bool ledClkAfterglow = false; ////////////////////////////////////// //// SYNC STATUS. //// ////////////////////////////////////// // Assuming clock generator isn't synchronized (sync'd) with source clock on initialization. bool isSync = false; ////////////////////////////////////// //// FUNCTIONS & METHODS (VOIDS). //// ////////////////////////////////////// void step() override; // Convert a string to char pointer (char*). char* stringToPchar(std::string str) { char *cstr = new char[str.length() + 1]; strcpy(cstr, str.c_str()); return cstr; delete [] cstr; } void updateDisplayJack(int jackID) { if (activeCLK) { // Clock modulator mode. For now, all ports are at x1. xdispOutOffset1 = 5; strcpy(dispOut1, stringToPchar("x1")); xdispOutOffset2 = 5; strcpy(dispOut2, stringToPchar("x1")); xdispOutOffset3 = 5; strcpy(dispOut3, stringToPchar("x1")); xdispOutOffset4 = 5; strcpy(dispOut4, stringToPchar("x1")); } else { // Clock generator mode. switch (jackID) { case 0: xdispOutOffset1 = 0; if ((outputRatioInUse[0] > 4) && (outputRatioInUse[0] < 12)) xdispOutOffset1 = 4; else if ((outputRatioInUse[0] > 11) && (outputRatioInUse[0] < 20)) xdispOutOffset1 = 5; else if (outputRatioInUse[0] > 19) xdispOutOffset2 = 1; strcpy(dispOut1, stringToPchar(setupParamName[SETUP_OUT1RATIO][outputRatioInUse[0]])); break; case 1: xdispOutOffset2 = 0; if ((outputRatioInUse[1] > 4) && (outputRatioInUse[1] < 12)) xdispOutOffset2 = 4; else if ((outputRatioInUse[1] > 11) && (outputRatioInUse[1] < 20)) xdispOutOffset2 = 5; else if (outputRatioInUse[1] > 19) xdispOutOffset2 = 1; strcpy(dispOut2, stringToPchar(setupParamName[SETUP_OUT2RATIO][outputRatioInUse[1]])); break; case 2: xdispOutOffset3 = 0; if ((outputRatioInUse[2] > 4) && (outputRatioInUse[2] < 12)) xdispOutOffset3 = 4; else if ((outputRatioInUse[2] > 11) && (outputRatioInUse[2] < 20)) xdispOutOffset3 = 5; else if (outputRatioInUse[2] > 19) xdispOutOffset3 = 1; strcpy(dispOut3, stringToPchar(setupParamName[SETUP_OUT3RATIO][outputRatioInUse[2]])); break; case 3: if (outputRatioInUse[3] == 12) { if (jack4LFO != 0) { xdispOutOffset4 = 0; switch (jack4LFO) { case 1: case 2: strcpy(dispOut4, stringToPchar("SIN")); break; case 3: case 4: strcpy(dispOut4, stringToPchar("TRI")); break; case 5: case 6: strcpy(dispOut4, stringToPchar("SAW")); } } else { xdispOutOffset4 = 5; strcpy(dispOut4, stringToPchar("x1")); } } else { xdispOutOffset4 = 0; if ((outputRatioInUse[3] > 4) && (outputRatioInUse[3] < 12)) xdispOutOffset4 = 4; else if ((outputRatioInUse[3] > 11) && (outputRatioInUse[3] < 20)) xdispOutOffset4 = 5; else if (outputRatioInUse[3] > 19) xdispOutOffset4 = 1; strcpy(dispOut4, stringToPchar(setupParamName[SETUP_OUT4RATIO][outputRatioInUse[3]])); } break; } } } // Set the DMD, regarding current mode (0 = BPM generator, 1 = clock modulator by encoder). void updateDMDtoRunningMode(int currMode) { // Update small displays for each output jacks. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) updateDisplayJack(i); switch (currMode) { case 0: // BPM clock generator. // Update main DMD. if (!isSetupRunning) { strcpy(dmdMessage1, stringToPchar(runningMode[0])); if (BPM < 10) xOffsetValue = 19; else if (BPM < 100) xOffsetValue = 13; else xOffsetValue = 7; strcpy(dmdMessage2, stringToPchar(std::to_string(BPM) + " BPM")); } break; case 1: // Clock modulator. if (inputs[INPUT_CV_TRIG].active) { // Ratio is modulated by CV. voltageOnCV = inputs[INPUT_CV_TRIG].value; if (bipolarCV) rateRatioCV = round(clamp(static_cast(voltageOnCV), -5.0f, 5.0f) * 12.6f); // By bipolar voltage (-5V/+5V). else rateRatioCV = round((clamp(static_cast(voltageOnCV), 0.0f, 10.0f) - 5.0f) * 12.6f); // By unipolar voltage (0V/+10V). // Required to display ratio without artifacts! rateRatioCVi = static_cast(rateRatioCV); if (round(rateRatioCV) == 0.0f) { clkModulatorMode = X1; rateRatioCV = 1.0f; // Real ratio becomes... 1.0f because it's multiplied by 1. } else if (round(rateRatioCV) > 0.0f) { clkModulatorMode = MULT; rateRatioCV = round(rateRatioCV + 1.0f); } else { clkModulatorMode = DIV; rateRatioCV = 1.0f / round(1.0f - rateRatioCV); } if (!isSetupRunning) { // Clock modulator (free ratio by CV). strcpy(dmdMessage1, stringToPchar(runningMode[2])); xOffsetValue = 2; std::string sSign = "x"; if (rateRatioCVi >= 0) { rateRatioCVi = rateRatioCVi + 1; } else { rateRatioCVi = 1 - rateRatioCVi; sSign = "/"; } strcpy(dmdMessage2, stringToPchar("Rate: " + sSign + std::to_string(rateRatioCVi))); } } else { // Clock modulator (preset ratio by encoder). // Ratio is selected from encoder. if (!isSetupRunning) { // Related multiplier/divider mode. clkModulatorMode = DIV; if (rateRatioByEncoder == 15) clkModulatorMode = X1; else if (rateRatioByEncoder > 15) clkModulatorMode = MULT; static const int list_iRatio[31] = {64, 32, 24, 16, 15, 12, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 16, 24, 32, 64}; strcpy(dmdMessage1, stringToPchar(runningMode[1])); xOffsetValue = 2; std::string sSign = "x"; if (rateRatioByEncoder < 15) sSign = "/"; strcpy(dmdMessage2, stringToPchar("Rate: " + sSign + std::to_string(list_iRatio[rateRatioByEncoder]))); } } } } // This custom function applies current settings (useful after SETUP operation, also "on-the-fly" altered parameter during Setup - useful to experiment). void UpdateKlokSpidSettings(bool allowJsonUpdate) { // SETUP parameter SETUP_CVPOLARITY: CV polarity (bipolar or unipolar CV-Ratio). bipolarCV = (setup_Current[SETUP_CVPOLARITY] == 0); // json persistence (only if SETUP isn't running). if (allowJsonUpdate) this->bipolarCV = (setup_Current[SETUP_CVPOLARITY] == 0); // json persistence (only if SETUP isn't running). // SETUP parameter SETUP_DURATION: possible pulse durations (1 ms, 2 ms, 5 ms, Gate 1/4, Gate 1/3, Square, Gate 2/3, Gate 3/4, Gate 95%). Keept for compatibility with v0.5.2 .vcv patches! switch (setup_Current[SETUP_DURATION]) { case FIXED1MS: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.001f; break; case FIXED2MS: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.002f; break; case FIXED5MS: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.005f; } // Extension for pulse duration parameter (it's a kind of "descriptor" for non-fixed durations). pulseDurationExt = setup_Current[SETUP_DURATION]; if (allowJsonUpdate) this->pulseDurationExt = setup_Current[SETUP_DURATION]; // json persistence (only if SETUP isn't running). // SETUP parameter SETUP_OUTVOLTAGE: output voltage: +2V, +5V, +10V or +12V (+11.7V). switch (setup_Current[SETUP_OUTVOLTAGE]) { case 0: outVoltage = 5.0f; if (allowJsonUpdate) this->outVoltage = 5.0f; // First setting is +5V, also factory (default) setting. json persistence (only if SETUP isn't running). break; case 1: outVoltage = 10.0f; if (allowJsonUpdate) this->outVoltage = 10.0f; // Second setting is +10V. json persistence (only if SETUP isn't running). break; case 2: outVoltage = 11.7f; if (allowJsonUpdate) this->outVoltage = 11.7f; // Third setting is +12V (real +11.7 V). json persistence (only if SETUP isn't running). break; case 3: outVoltage = 2.0f; if (allowJsonUpdate) this->outVoltage = 2.0f; // Last setting (introduced from v0.5.5/v0.6.0.4-beta): +2V. json persistence (only if SETUP isn't running). } // SETUP parameter SETUP_OUTSRATIOS: all output jacks at default x1, or custom (useful to bypass all 4 jack rations during SETUP, if let at default). defOutRatios = (setup_Current[SETUP_OUTSRATIOS] == 0); // json persistence (only if SETUP isn't running). if (allowJsonUpdate) this->defOutRatios = (setup_Current[SETUP_OUTSRATIOS] == 0); // json persistence (only if SETUP isn't running). // SETUP parameter SETUP_OUT1RATIO: optional BPM rate applied on output jack #1. outputRatio[0] = setup_Current[SETUP_OUT1RATIO]; if (allowJsonUpdate) this->outputRatio[0] = setup_Current[SETUP_OUT1RATIO]; // json persistence (only if SETUP isn't running). if (defOutRatios) outputRatioInUse[0] = 12; else outputRatioInUse[0] = outputRatio[0]; // SETUP parameter SETUP_OUT2RATIO: optional BPM rate applied on output jack #2. outputRatio[1] = setup_Current[SETUP_OUT2RATIO]; if (allowJsonUpdate) this->outputRatio[1] = setup_Current[SETUP_OUT2RATIO]; // json persistence (only if SETUP isn't running). if (defOutRatios) outputRatioInUse[1] = 12; else outputRatioInUse[1] = outputRatio[1]; // SETUP parameter SETUP_OUT3RATIO: optional BPM rate applied on output jack #3. outputRatio[2] = setup_Current[SETUP_OUT3RATIO]; if (allowJsonUpdate) this->outputRatio[2] = setup_Current[SETUP_OUT3RATIO]; // json persistence (only if SETUP isn't running). if (defOutRatios) outputRatioInUse[2] = 12; else outputRatioInUse[2] = outputRatio[2]; // SETUP parameter SETUP_OUT4RATIO: optional BPM rate applied on output jack #4. outputRatio[3] = setup_Current[SETUP_OUT4RATIO]; if (allowJsonUpdate) this->outputRatio[3] = setup_Current[SETUP_OUT4RATIO]; // json persistence (only if SETUP isn't running). if (defOutRatios) outputRatioInUse[3] = 12; else outputRatioInUse[3] = outputRatio[3]; // SETUP parameter SETUP_OUT4LFO: optional LFO on output jack #4: Disabled, Sine, Triangle, Saw, Inverse Sine, Inverse Triangle, Inverse Saw. // Introduced from v0.6.1, but remaining to do. jack4LFO = setup_Current[SETUP_OUT4LFO]; if (allowJsonUpdate) this->jack4LFO = setup_Current[SETUP_OUT4LFO]; // json persistence (only if SETUP isn't running). // SETUP parameter SETUP_OUT4LFOPOLARITY: LFO polarity (bipolar or unipolar). jack4LFObipolar = (setup_Current[SETUP_OUT4LFOPOLARITY] == 0); // json persistence (only if SETUP isn't running). if (allowJsonUpdate) this->jack4LFObipolar = (setup_Current[SETUP_OUT4LFOPOLARITY] == 0); // json persistence (only if SETUP isn't running). // SETUP parameter SETUP_CVTRIG: CV-RATIO/TRIG. input port behavior (standalone clock generator only, this port is TRIG.). // - "true" is meaning the TRIG. input port acts as "start/stop toggle". // - "false" is meaning the TRIG. input port acts as "BPM reset" (useful to "re-sync" BPM from an external/reference source clock, for example). transportTrig = (setup_Current[SETUP_CVTRIG] == 0); if (allowJsonUpdate) this->transportTrig = (setup_Current[SETUP_CVTRIG] == 0); // json persistence (only if SETUP isn't running). // Update small displays for each output jacks. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) updateDisplayJack(i); } // This custom function returns pulse duration (ms), regardling number of samples (long int) and pulsation duration parameter (SETUP). float GetPulsingTime(long int stepGap, float rate) { float pTime = 0.001; // As default pulse duration is set to 1ms (also can be set to "fixed 1ms" via SETUP). if (stepGap == 0) { // No reference duration (number of samples is zero). switch (setup_Current[SETUP_DURATION]) { case FIXED2MS: pTime = 0.002f; // Fixed 2 ms pulse. break; case FIXED5MS: pTime = 0.005f; // Fixed 5 ms pulse. } } else { // Reference duration in number of samples (when known stepGap). Variable-length pulse duration can be defined. switch (setup_Current[SETUP_DURATION]) { case FIXED2MS: pTime = 0.002f; // Fixed 2 ms pulse. break; case FIXED5MS: pTime = 0.005f; // Fixed 5 ms pulse. break; case GATE25: pTime = rate * 0.25f * (stepGap / engineGetSampleRate()); // Gate 1/4 (25%) break; case GATE33: pTime = rate * (1.0f / 3.0f) * (stepGap / engineGetSampleRate()); // Gate 1/3 (33%) break; case SQUARE: pTime = rate * 0.5f * (stepGap / engineGetSampleRate()); // Square wave (50%) break; case GATE66: pTime = rate * (2.0f / 3.0f) * (stepGap / engineGetSampleRate()); // Gate 2/3 (66%) break; case GATE75: pTime = rate * 0.75f * (stepGap / engineGetSampleRate()); // Gate 3/4 (75%) break; case GATE95: pTime = rate * 0.95f * (stepGap / engineGetSampleRate()); // Gate 95% } } return pTime; } // Persistence for extra datas via json functions (in particular parameters defined via KlokSpid's SETUP, also BPM state). // These extra datas are saved to .vcv file (including "autosave.vcv") also are "transfered" when you duplicate the module. json_t *toJson() override { json_t *rootJ = json_object(); json_object_set_new(rootJ, "Theme", json_integer(Theme)); json_object_set_new(rootJ, "bipolarCV", json_boolean(bipolarCV)); json_object_set_new(rootJ, "pulseDurationExt", json_integer(pulseDurationExt)); json_object_set_new(rootJ, "outVoltage", json_real(outVoltage)); json_object_set_new(rootJ, "defOutRatios", json_boolean(defOutRatios)); // When true, all jacks are x1. Otherwise (false) any jack may have its specific ratio. json_object_set_new(rootJ, "out1Ratio", json_integer(outputRatio[0])); json_object_set_new(rootJ, "out2Ratio", json_integer(outputRatio[1])); json_object_set_new(rootJ, "out3Ratio", json_integer(outputRatio[2])); json_object_set_new(rootJ, "out4Ratio", json_integer(outputRatio[3])); json_object_set_new(rootJ, "jack4LFO", json_integer(jack4LFO)); json_object_set_new(rootJ, "jack4LFObipolar", json_boolean(jack4LFObipolar)); json_object_set_new(rootJ, "transportTrig", json_boolean(transportTrig)); // CV-RATIO/TRIG. port may be used as BPM "start/stop" toggle or as BPM-reset. BPM-reset is default factory (false). json_object_set_new(rootJ, "Ratio", json_integer(rateRatioByEncoder)); // Ratio set by encoder. json_object_set_new(rootJ, "BPM", json_integer(BPM)); // BPM set by encoder. json_object_set_new(rootJ, "runBPMOnInit", json_boolean(runBPMOnInit)); // State of BPM pulsing or stopped. return rootJ; } void fromJson(json_t *rootJ) override { // Retrieving module theme/variation (when loading .vcv and cloning module). json_t *ThemeJ = json_object_get(rootJ, "Theme"); if (ThemeJ) Theme = json_integer_value(ThemeJ); // Retrieving bipolar or unipolar mode (for CV when running as clock multiplier/divider). json_t *bipolarCVJ = json_object_get(rootJ, "bipolarCV"); if (bipolarCVJ) bipolarCV = json_is_true(bipolarCVJ); // Retrieving pulse duration "mode" data. Introducted since v0.5.3. Thanks Yoann for this idea! json_t *pulseDurationExtJ = json_object_get(rootJ, "pulseDurationExt"); if (pulseDurationExtJ) pulseDurationExt = json_integer_value(pulseDurationExtJ); // Retrieving output voltage data (real/float value). json_t *outVoltageJ = json_object_get(rootJ, "outVoltage"); if (outVoltageJ) outVoltage = json_real_value(outVoltageJ); // Retrieving if ratio par jack is disabled (all at x1), or enabled (each having its ratio). json_t *defOutRatiosJ = json_object_get(rootJ, "defOutRatios"); if (defOutRatiosJ) defOutRatios = json_is_true(defOutRatiosJ); // Retrieving ratio for output jack #1 (when loading .vcv and cloning module). json_t *jack1BPMRateJ = json_object_get(rootJ, "out1Ratio"); if (jack1BPMRateJ) outputRatio[0] = json_integer_value(jack1BPMRateJ); // Retrieving ratio for output jack #2 (when loading .vcv and cloning module). json_t *jack2BPMRateJ = json_object_get(rootJ, "out2Ratio"); if (jack2BPMRateJ) outputRatio[1] = json_integer_value(jack2BPMRateJ); // Retrieving ratio for output jack #3 (when loading .vcv and cloning module). json_t *jack3BPMRateJ = json_object_get(rootJ, "out3Ratio"); if (jack3BPMRateJ) outputRatio[2] = json_integer_value(jack3BPMRateJ); // Retrieving ratio for output jack #4 (when loading .vcv and cloning module). json_t *jack4BPMRateJ = json_object_get(rootJ, "out4Ratio"); if (jack4BPMRateJ) outputRatio[3] = json_integer_value(jack4BPMRateJ); // Retrieving output jack #4 LFO mode (when loading .vcv and cloning module). json_t *jack4LFOJ = json_object_get(rootJ, "jack4LFO"); if (jack4LFOJ) jack4LFO = json_integer_value(jack4LFOJ); // Retrieving bipolar or unipolar for jack #4 LFO. json_t *jack4LFObipolarJ = json_object_get(rootJ, "jack4LFObipolar"); if (jack4LFObipolarJ) jack4LFObipolar = json_is_true(jack4LFObipolarJ); // Retrieving usage of TRIG. input port: start/stop toggle (true) or BPM-reset (false). json_t *transportTrigJ = json_object_get(rootJ, "transportTrig"); if (transportTrigJ) transportTrig = json_is_true(transportTrigJ); // Retrieving ratio (clock modulator) set by encoder (when loading .vcv and cloning module). json_t *svRatioJ = json_object_get(rootJ, "Ratio"); if (svRatioJ) svRatio = json_integer_value(svRatioJ); // Retrieving BPM (when loading .vcv and cloning module). json_t *svBPMJ = json_object_get(rootJ, "BPM"); if (svBPMJ) svBPM = json_integer_value(svBPMJ); // Retrieving last saved BPM-clocking state (it was running or stopped). json_t *runBPMOnInitJ = json_object_get(rootJ, "runBPMOnInit"); if (runBPMOnInitJ) runBPMOnInit = json_is_true(runBPMOnInitJ); } }; void KlokSpidModule::step() { // step() function is the right place for DSP processing! // Depending current KlokSpid model (theme), set the relevant DMD-text color. DMDtextColor = tblDMDtextColor[Theme]; // Bypass early steps, due to encoder/knob behaviors on init (from 0.0f... to json saved value!). if (onFirstInitCounter > 0) { currentStep++; // Small displays near output jacks. xdispOutOffset1 = 0; strcpy(dispOut1, ""); xdispOutOffset2 = 0; strcpy(dispOut2, ""); xdispOutOffset3 = 0; strcpy(dispOut3, ""); xdispOutOffset4 = 0; strcpy(dispOut4, ""); // Flashing calibration message on DMD. if ((currentStep % 8000) > 4000) strcpy(dmdMessage1, "Calibrating..."); else strcpy(dmdMessage1, ""); xOffsetValue = 2; strcpy(dmdMessage2, ""); // Encoder calibration. encoderCurrent = (int)roundf(10.0f * params[PARAM_ENCODER].value); if (encoderPrevious != encoderCurrent) { onFirstInitCounter = onFirstInitCounter + (int)(engineGetSampleRate() / 512.0f); encoderPrevious = encoderCurrent; } else onFirstInitCounter--; // Last step for initialization. if (onFirstInitCounter == 1) { // This is the lastest step of initialization. // Filling table containing current SETUP parameters. // SETUP parameter SETUP_CVPOLARITY: bipolar or unipolar CV. setup_Current[SETUP_CVPOLARITY] = bipolarCV ? 0 : 1; // SETUP parameter SETUP_DURATION: Pulse duration (extended, to keep compatibility with previous v0.5.2). // Parameter #2: possible pulse durations (fixed 1 ms, 2 ms or 5 ms durations, Gate 1/4, Gate 1/3, Square, Gate 2/3, Gate 3/4, Gate 95%). setup_Current[SETUP_DURATION] = pulseDurationExt; switch (pulseDurationExt) { case FIXED1MS: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.001f; break; case FIXED2MS: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.002f; break; case FIXED5MS: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.005f; break; default: for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) pulseDuration[i] = 0.001f; // It's a default value, but gates are defined in realtime (later). } // If output voltage is above +11V, assuming +11.7V. if (round(outVoltage * 10) > 110) outVoltage = 11.7f; // Assuming +5V is default output voltage. setup_Current[SETUP_OUTVOLTAGE] = 0; // +5V. // SETUP parameter SETUP_OUTVOLTAGE: Output voltage. if (round(outVoltage * 10) == 20) setup_Current[SETUP_OUTVOLTAGE] = 3; // +2V. Lastest value (instead of "inserted" at first, to preserve compatibility!). else if (round(outVoltage * 10) == 100) setup_Current[SETUP_OUTVOLTAGE] = 1; // +10V. else if (round(outVoltage * 10) == 117) setup_Current[SETUP_OUTVOLTAGE] = 2; // +11.7V (indicated +12V in module's SETUP). // SETUP parameter SETUP_OUTSRATIOS: enabled or disabled custom ratios (for all output jacks). setup_Current[SETUP_OUTSRATIOS] = defOutRatios ? 0 : 1; // SETUP parameter SETUP_OUT1RATIO to SETUP_OUT4RATIO: optional BPM rate applied on output jacks #1~#4. for (int i = 0; i < NUM_OUTPUTS; i++) { setup_Current[SETUP_OUT1RATIO + i] = outputRatio[i]; if (defOutRatios) outputRatioInUse[i] = 12; else outputRatioInUse[i] = outputRatio[i]; } // SETUP parameter SETUP_OUT4LFO: optional LFO on output jack #4. setup_Current[SETUP_OUT4LFO] = jack4LFO; // SETUP parameter SETUP_OUT4LFOPOLARITY: bipolar or unipolar LFO. setup_Current[SETUP_OUT4LFOPOLARITY] = jack4LFObipolar ? 0 : 1; // SETUP parameter SETUP_CVTRIG: CV/TRIG port, as trigger input when running as standalone clock generator (only). setup_Current[SETUP_CVTRIG] = transportTrig ? 0 : 1; // Parameter's value is, by default 1 for default "Save/Exit". setup_Current[SETUP_EXIT] = 1; // Is standalone clock is running at init, or not (previous state). isBPMRunning = this->runBPMOnInit; // Strings construction for SETUP. // SETUP_WELCOME_MESSAGE (unique option). setupParamName[SETUP_WELCOME_MESSAGE][0] = "Press Btn!"; setupParamXOffset[SETUP_WELCOME_MESSAGE][0] = -1; for (int i = 1; i < 22; i++) { setupParamName[SETUP_WELCOME_MESSAGE][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_WELCOME_MESSAGE][i] = 0; // Useless x-offsets to 0. } // SETUP_CVPOLARITY: having 2 possible parameters. setupParamName[SETUP_CVPOLARITY][0] = "Bipolar"; setupParamXOffset[SETUP_CVPOLARITY][0] = 2; setupParamName[SETUP_CVPOLARITY][1] = "Unipolar"; setupParamXOffset[SETUP_CVPOLARITY][1] = 2; for (int i = 2; i < 22; i++) { setupParamName[SETUP_CVPOLARITY][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_CVPOLARITY][i] = 0; // Useless x-offsets to 0. } // SETUP_DURATION: having 9 possible parameters. setupParamName[SETUP_DURATION][FIXED1MS] = "Fixed 1ms"; setupParamXOffset[SETUP_DURATION][FIXED1MS] = 1; setupParamName[SETUP_DURATION][FIXED2MS] = "Fixed 2ms"; setupParamXOffset[SETUP_DURATION][FIXED2MS] = 1; setupParamName[SETUP_DURATION][FIXED5MS] = "Fixed 5ms"; setupParamXOffset[SETUP_DURATION][FIXED5MS] = 1; setupParamName[SETUP_DURATION][GATE25] = "Gate 25%"; setupParamXOffset[SETUP_DURATION][GATE25] = 2; setupParamName[SETUP_DURATION][GATE33] = "Gate 33%"; setupParamXOffset[SETUP_DURATION][GATE33] = 2; setupParamName[SETUP_DURATION][SQUARE] = "Square W."; setupParamXOffset[SETUP_DURATION][SQUARE] = 2; setupParamName[SETUP_DURATION][GATE66] = "Gate 66%"; setupParamXOffset[SETUP_DURATION][GATE66] = 2; setupParamName[SETUP_DURATION][GATE75] = "Gate 75%"; setupParamXOffset[SETUP_DURATION][GATE75] = 2; setupParamName[SETUP_DURATION][GATE95] = "Gate 95%"; setupParamXOffset[SETUP_DURATION][GATE95] = 2; // SETUP_OUTVOLTAGE: having 4 possible parameters. setupParamName[SETUP_OUTVOLTAGE][0] = "+5V"; setupParamXOffset[SETUP_OUTVOLTAGE][0] = 2; setupParamName[SETUP_OUTVOLTAGE][1] = "+10V"; setupParamXOffset[SETUP_OUTVOLTAGE][1] = 2; setupParamName[SETUP_OUTVOLTAGE][2] = "+11.7V"; setupParamXOffset[SETUP_OUTVOLTAGE][2] = 2; setupParamName[SETUP_OUTVOLTAGE][3] = "+2V"; setupParamXOffset[SETUP_OUTVOLTAGE][3] = 2; for (int i = 4; i < 22; i++) { setupParamName[SETUP_OUTVOLTAGE][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_OUTVOLTAGE][i] = 0; // Useless x-offsets to 0. } // SETUP_OUTSRATIOS: having 2 possible parameters. setupParamName[SETUP_OUTSRATIOS][0] = "All @ x1"; setupParamXOffset[SETUP_OUTSRATIOS][0] = 2; setupParamName[SETUP_OUTSRATIOS][1] = "Custom"; setupParamXOffset[SETUP_OUTSRATIOS][1] = 2; for (int i = 2; i < 22; i++) { setupParamName[SETUP_OUTSRATIOS][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_OUTSRATIOS][i] = 0; // Useless x-offsets to 0. } // SETUP_OUT1RATIO to SETUP_OUT4RATIO: each ratio for any output jack have 25 possible parameters. for (int i = 0; i < 4; i++) { setupParamName[SETUP_OUT1RATIO + i][0] = "/64"; setupParamXOffset[SETUP_OUT1RATIO + i][0] = 32; setupParamName[SETUP_OUT1RATIO + i][1] = "/32"; setupParamXOffset[SETUP_OUT1RATIO + i][1] = 32; setupParamName[SETUP_OUT1RATIO + i][2] = "/24"; setupParamXOffset[SETUP_OUT1RATIO + i][2] = 32; setupParamName[SETUP_OUT1RATIO + i][3] = "/16"; setupParamXOffset[SETUP_OUT1RATIO + i][3] = 32; setupParamName[SETUP_OUT1RATIO + i][4] = "/12"; setupParamXOffset[SETUP_OUT1RATIO + i][4] = 32; setupParamName[SETUP_OUT1RATIO + i][5] = "/9"; setupParamXOffset[SETUP_OUT1RATIO + i][5] = 38; setupParamName[SETUP_OUT1RATIO + i][6] = "/8"; setupParamXOffset[SETUP_OUT1RATIO + i][6] = 38; setupParamName[SETUP_OUT1RATIO + i][7] = "/6"; setupParamXOffset[SETUP_OUT1RATIO + i][7] = 38; setupParamName[SETUP_OUT1RATIO + i][8] = "/5"; setupParamXOffset[SETUP_OUT1RATIO + i][8] = 38; setupParamName[SETUP_OUT1RATIO + i][9] = "/4"; setupParamXOffset[SETUP_OUT1RATIO + i][9] = 38; setupParamName[SETUP_OUT1RATIO + i][10] = "/3"; setupParamXOffset[SETUP_OUT1RATIO + i][10] = 38; setupParamName[SETUP_OUT1RATIO + i][11] = "/2"; setupParamXOffset[SETUP_OUT1RATIO + i][11] = 38; setupParamName[SETUP_OUT1RATIO + i][12] = "x1"; setupParamXOffset[SETUP_OUT1RATIO + i][12] = 38; setupParamName[SETUP_OUT1RATIO + i][13] = "x2"; setupParamXOffset[SETUP_OUT1RATIO + i][13] = 38; setupParamName[SETUP_OUT1RATIO + i][14] = "x3"; setupParamXOffset[SETUP_OUT1RATIO + i][14] = 38; setupParamName[SETUP_OUT1RATIO + i][15] = "x4"; setupParamXOffset[SETUP_OUT1RATIO + i][15] = 38; setupParamName[SETUP_OUT1RATIO + i][16] = "x5"; setupParamXOffset[SETUP_OUT1RATIO + i][16] = 38; setupParamName[SETUP_OUT1RATIO + i][17] = "x6"; setupParamXOffset[SETUP_OUT1RATIO + i][17] = 38; setupParamName[SETUP_OUT1RATIO + i][18] = "x8"; setupParamXOffset[SETUP_OUT1RATIO + i][18] = 38; setupParamName[SETUP_OUT1RATIO + i][19] = "x9"; setupParamXOffset[SETUP_OUT1RATIO + i][19] = 38; setupParamName[SETUP_OUT1RATIO + i][20] = "x12"; setupParamXOffset[SETUP_OUT1RATIO + i][20] = 32; setupParamName[SETUP_OUT1RATIO + i][21] = "x16"; setupParamXOffset[SETUP_OUT1RATIO + i][21] = 32; setupParamName[SETUP_OUT1RATIO + i][22] = "x24"; setupParamXOffset[SETUP_OUT1RATIO + i][22] = 32; setupParamName[SETUP_OUT1RATIO + i][23] = "x32"; setupParamXOffset[SETUP_OUT1RATIO + i][23] = 32; setupParamName[SETUP_OUT1RATIO + i][24] = "x64"; setupParamXOffset[SETUP_OUT1RATIO + i][24] = 32; } // SETUP_OUT4LFO: having 7 possible parameters. setupParamName[SETUP_OUT4LFO][0] = "Disabled"; setupParamXOffset[SETUP_OUT4LFO][0] = 2; setupParamName[SETUP_OUT4LFO][1] = "Sine"; setupParamXOffset[SETUP_OUT4LFO][1] = 2; setupParamName[SETUP_OUT4LFO][2] = "Inv. Sine"; setupParamXOffset[SETUP_OUT4LFO][2] = 2; setupParamName[SETUP_OUT4LFO][3] = "Triangle"; setupParamXOffset[SETUP_OUT4LFO][3] = 2; setupParamName[SETUP_OUT4LFO][4] = "Inv. Tri."; setupParamXOffset[SETUP_OUT4LFO][4] = 2; setupParamName[SETUP_OUT4LFO][5] = "Sawtooth"; setupParamXOffset[SETUP_OUT4LFO][5] = 2; setupParamName[SETUP_OUT4LFO][6] = "Inv. Saw."; setupParamXOffset[SETUP_OUT4LFO][6] = 2; for (int i = 7; i < 22; i++) { setupParamName[SETUP_OUT4LFO][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_OUT4LFO][i] = 0; // Useless x-offsets to 0. } // SETUP_OUT4LFOPOLARITY: having 2 possible parameters. setupParamName[SETUP_OUT4LFOPOLARITY][0] = "Bipolar"; setupParamXOffset[SETUP_OUT4LFOPOLARITY][0] = 2; setupParamName[SETUP_OUT4LFOPOLARITY][1] = "Unipolar"; setupParamXOffset[SETUP_OUT4LFOPOLARITY][1] = 2; for (int i = 2; i < 22; i++) { setupParamName[SETUP_OUT4LFOPOLARITY][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_OUT4LFOPOLARITY][i] = 0; // Useless x-offsets to 0. } // SETUP_CVTRIG: having 2 possible parameters. setupParamName[SETUP_CVTRIG][0] = "Play/Stop"; setupParamXOffset[SETUP_CVTRIG][0] = 2; setupParamName[SETUP_CVTRIG][1] = "Reset In."; setupParamXOffset[SETUP_CVTRIG][1] = 2; for (int i = 2; i < 22; i++) { setupParamName[SETUP_CVTRIG][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_CVTRIG][i] = 0; // Useless x-offsets to 0. } // SETUP_EXIT: having 4 possible parameters. setupParamName[SETUP_EXIT][0] = "Canc./Exit"; setupParamXOffset[SETUP_EXIT][0] = -1; setupParamName[SETUP_EXIT][1] = "Save/Exit"; setupParamXOffset[SETUP_EXIT][1] = 1; setupParamName[SETUP_EXIT][2] = "Review..."; setupParamXOffset[SETUP_EXIT][2] = 2; setupParamName[SETUP_EXIT][3] = "Factory"; setupParamXOffset[SETUP_EXIT][3] = 2; for (int i = 4; i < 22; i++) { setupParamName[SETUP_EXIT][i] = ""; // Unused (useless) strings are set to empty. setupParamXOffset[SETUP_EXIT][i] = 0; // Useless x-offsets to 0. } activeCLK = inputs[INPUT_CLOCK].active; activeCLKPrevious = activeCLK; activeCV = inputs[INPUT_CV_TRIG].active; activeCVPrevious = activeCV; // Reinit encoder reading. encoderCurrent = (int)roundf(10.0f * params[PARAM_ENCODER].value); encoderPrevious = encoderCurrent; encoderDelta = 0; // Default assuming encoder isn't moved. // currentStep = 0; rateRatioByEncoder = this->svRatio; BPM = this->svBPM; if (activeCLK) updateDMDtoRunningMode(1); else updateDMDtoRunningMode(0); onFirstInitCounter = 0; } return; } // Current state of CLK port. Active means connected/wired. activeCLK = inputs[INPUT_CLOCK].active; // Current state and voltage (CV/TRIG port). Active means connected/wired. activeCV = inputs[INPUT_CV_TRIG].active; // Encoder behavior (moved or not). encoderCurrent = (int)roundf(10.0f * params[PARAM_ENCODER].value); encoderDelta = 0; // Default assuming encoder isn't moved. if (abs(encoderCurrent - encoderPrevious) <= 2) { if (encoderCurrent < encoderPrevious) encoderDelta = -1; // Counter-clockwise ==> decrement. else if (encoderCurrent > encoderPrevious) encoderDelta = 1; // Clockwise => increment. } // Save current encoder position to become previous (for next check). encoderPrevious = encoderCurrent; if (activeCLK != activeCLKPrevious) { // Is state was changed (added or removed a patch cable to/away CLK port)? // New state will become "previous" state. activeCLKPrevious = activeCLK; // Reset all steps counter and "gaps", not synchronized. currentStep = 0; previousStep = 0; expectedStep = 0; stepGap = 0; stepGapPrevious = 0; isSync = false; for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { canPulse[i] = false; nextPulseStep[i] = 0; } if (!activeCLK) updateDMDtoRunningMode(0); else { activeCV = inputs[INPUT_CV_TRIG].active; updateDMDtoRunningMode(1); } } if (activeCV != activeCVPrevious) { // Is state was changed (added or removed a patch cable to/away CLK port)? // New state will become "previous" state. activeCVPrevious = activeCV; if (activeCLK) updateDMDtoRunningMode(1); } // Considering CV (if applicable e.g. wired!). voltageOnCV = 0.0f; isRatioCVmod = false; rateRatioCV = 0.0f; if (activeCV) { voltageOnCV = inputs[INPUT_CV_TRIG].value; if (activeCLK) { // Considering CV-RATIO signal to modulate ratio (doesn't matter if SETUP is running, or not). isRatioCVmod = true; if (bipolarCV) rateRatioCV = round(clamp(static_cast(voltageOnCV), -5.0f, 5.0f) * 12.6f); // By bipolar voltage (-5V/+5V). else rateRatioCV = round((clamp(static_cast(voltageOnCV), 0.0f, 10.0f) - 5.0f) * 12.6f); // By unipolar voltage (0V/+10V). // Update DMD. updateDMDtoRunningMode(1); } else { // BPM is set by encoded (except while SETUP is running). if (!isSetupRunning) { if (encoderDelta != 0) { BPM = BPM + encoderDelta; // May be increased or decreased. if (BPM < 1) BPM = 1; // Minimum 1 BPM. else if (BPM > 960) BPM = 960; // Maximum 960 BPM. this->svBPM = BPM; // Reset encoder move detection. encoderDelta = 0; // Update DMD. updateDMDtoRunningMode(0); } } } } else { if (!isSetupRunning) { if (activeCLK) { // Preset ratios are controlled by encoder. if (encoderDelta != 0) { rateRatioByEncoder = rateRatioByEncoder + encoderDelta; if (rateRatioByEncoder < 0) rateRatioByEncoder = 0; // Limiting to 0 (/64). else if (rateRatioByEncoder > 30) rateRatioByEncoder = 30; // Limiting to 30 (X64). this->svRatio = rateRatioByEncoder; // Related multiplier/divider mode. clkModulatorMode = DIV; if (rateRatioByEncoder == 15) clkModulatorMode = X1; else if (rateRatioByEncoder > 15) clkModulatorMode = MULT; // Reset encoder move detection. encoderDelta = 0; // Update DMD. updateDMDtoRunningMode(1); } } else { // BPM is set by encoded (except while SETUP is running). if (!isSetupRunning) { if (encoderDelta != 0) { BPM = BPM + encoderDelta; // May be increased or decreased. if (BPM < 1) BPM = 1; // Minimum BPM is 1. else if (BPM > 960) BPM = 960; // Maximum 960 BPM. this->svBPM = BPM; // Reset encoder move detection. encoderDelta = 0; // Update DMD. updateDMDtoRunningMode(0); } } } } } // Button state. buttonPressed = runButton.process(params[PARAM_BUTTON].value); // KlokSpid is working as multiplier/divider module (when CLK input port is connected - aka "active"). if (activeCLK) { // Increment step number. currentStep++; // Using Schmitt trigger (SchmittTrigger is provided by dsp/digital.hpp) to detect thresholds from CLK input connector. Calibration: +1.7V (rising edge), low +0.2V (falling edge). if (CLKInputPort.process(rescale(inputs[INPUT_CLOCK].value, 0.2f, 1.7f, 0.0f, 1.0f))) { // CLK input is receiving a compliant trigger voltage (rising edge): lit and "afterglow" CLK (red) LED. ledClkDelay = 0; ledClkAfterglow = true; if (previousStep == 0) { // No "history", it's the first pulse received on CLK input after a frequency change. Not synchronized. expectedStep = 0; stepGap = 0; stepGapPrevious = 0; // stepGap at 0: the pulse duration will be 1 ms (default), or 2 ms or 5 ms (depending SETUP). Variable pulses can't be used as long as frequency remains unknown. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) if (isRatioCVmod) pulseDuration[i] = GetPulsingTime(0, 1.0f / rateRatioCV); // Ratio is CV-controlled. else pulseDuration[i] = GetPulsingTime(0, list_fRatio[rateRatioByEncoder]); // Ratio is controlled by encoder. // Not synchronized. isSync = false; for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { canPulse[i] = (clkModulatorMode != MULT); // MULT needs second pulse to establish source frequency. pulseDivCounter[i] = 0; // Used for DIV mode exclusively! pulseMultCounter[i] = 0; // Used for MULT mode exclusively! } previousStep = currentStep; } else { // It's the second pulse received on CLK input after a frequency change. stepGapPrevious = stepGap; stepGap = currentStep - previousStep; expectedStep = currentStep + stepGap; // The frequency is known, we can determine the pulse duration (defined by SETUP). // The pulse duration also depends of clocking ratio, such "X1", multiplied or divided, and its ratio. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) if (isRatioCVmod) pulseDuration[i] = GetPulsingTime(stepGap, 1.0f / rateRatioCV); // Ratio is CV-controlled. else pulseDuration[i] = GetPulsingTime(stepGap, list_fRatio[rateRatioByEncoder]); // Ratio is controlled by encoder. isSync = true; if (stepGap > stepGapPrevious) isSync = ((stepGap - stepGapPrevious) < 2); else if (stepGap < stepGapPrevious) isSync = ((stepGapPrevious - stepGap) < 2); if (isSync) { for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) canPulse[i] = (clkModulatorMode != DIV); } else { for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) canPulse[i] = (clkModulatorMode == X1); } previousStep = currentStep; } switch (clkModulatorMode) { case X1: // Ratio is x1, following source clock, the easiest scenario! (always sync'd). for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) canPulse[i] = true; break; case DIV: // Divider mode scenario. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { if (pulseDivCounter[i] == 0) { if (isRatioCVmod) pulseDivCounter[i] = int(1.0f / rateRatioCV) - 1; // Ratio is CV-controlled. else pulseDivCounter[i] = int(list_fRatio[rateRatioByEncoder] - 1); // Ratio is controlled by knob. canPulse[i] = true; } else { pulseDivCounter[i]--; canPulse[i] = false; } } break; case MULT: // Multiplier mode scenario: pulsing only when source frequency is established. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { if (isSync) { // Next step for pulsing in multiplier mode. if (isRatioCVmod) { // Ratio is CV-controlled. nextPulseStep[i] = currentStep + round(stepGap / rateRatioCV); pulseMultCounter[i] = int(rateRatioCV) - 1; } else { // Ratio is controlled by knob. nextPulseStep[i] = currentStep + round(stepGap * list_fRatio[rateRatioByEncoder]); pulseMultCounter[i] = round(1.0f / list_fRatio[rateRatioByEncoder]) - 1; } canPulse[i] = true; } } } } else { // At this point, it's not a rising edge! // When running as multiplier, may pulse here too during low voltages on CLK input! for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { if (isSync && (nextPulseStep[i] == currentStep) && (clkModulatorMode == MULT)) { if (isRatioCVmod) nextPulseStep[i] = currentStep + round(stepGap / rateRatioCV); // Ratio is CV-controlled. else nextPulseStep[i] = currentStep + round(stepGap * list_fRatio[rateRatioByEncoder]); // Ratio is controlled by knob. // This block is to avoid continuous pulsing if no more receiving incoming signal. if (pulseMultCounter[i] > 0) { pulseMultCounter[i]--; canPulse[i] = true; } else { canPulse[i] = false; isSync = false; } } } } } else { // CLK input port isn't connected (not active): KlokSpid is working as clock generator. ledClkAfterglow = false; if (previousBPM == BPM) { // CV-RATIO/TRIG. input port is used as TRIG. to reset clock generator or to toggle BPM-clocking, while voltage is +1.7 V (or above) - rising edge. if (activeCV) { if (runTriggerPort.process(rescale(voltageOnCV, 0.2f, 1.7f, 0.0f, 1.0f))) { // On +1.7 V trigger (rising edge), the clock generator state if toggled (started or stopped). if (transportTrig) { // CV-RATIO/TRIG. input port (TRIG.) is configured as "play/stop toggle". isBPMRunning = !isBPMRunning; // BPM state persistence (json). this->runBPMOnInit = isBPMRunning; for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) nextPulseStep[i] = 0; currentStep = 0; // Reset phase for LFO jack #4. resetPhase = true; } else { // CV-RATIO/TRIG. input port (TRIG.) is configured as RESET input (default factory): assuming it's an incoming reset signal! currentStep = 0; for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) nextPulseStep[i] = 0; // Reset phase for LFO jack #4. resetPhase = true; } } } // Incrementing step counter... currentStep++; for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { if (isBPMRunning) { if (currentStep >= nextPulseStep[i]) canPulse[i] = true; if (canPulse[i]) { // Setting pulse... // Define the step for next pulse. Time reference is given by (current) engine samplerate setting. nextPulseStep[i] = currentStep + round(60.0f * engineGetSampleRate() * list_outRatiof[outputRatioInUse[i]] / BPM); // Define the pulse duration (fixed or variable-length). pulseDuration[i] = GetPulsingTime(engineGetSampleRate(), 60.0f / BPM * list_outRatiof[outputRatioInUse[i]]); if (i == OUTPUT_4) resetPhase = true; } } else { // BPM clock is stopped. canPulse[i] = false; nextPulseStep[i] = 0; currentStep = 0; // Reset phase for LFO jack #4. resetPhase = true; } } } else { // Update DMD (number of BPM). updateDMDtoRunningMode(0); // Altered BPM: reset phase for LFO jack #4. resetPhase = true; } previousBPM = BPM; } // Using pulse generator to output to all ports. for (int i = OUTPUT_1; i < NUM_OUTPUTS; i++) { if (canPulse[i]) { if (i == OUTPUT_4) { if (resetPhase) { LFOjack4.phase = 0.0f; resetPhase = false; } } // Sending pulse, using pulse generator. sendPulse[i].triggerDuration = pulseDuration[i]; sendPulse[i].trigger(pulseDuration[i]); canPulse[i] = false; } sendingOutput[i] = sendPulse[i].process(engineGetSampleTime()); if (i < OUTPUT_4) outputs[i].value = sendingOutput[i] ? outVoltage : 0.0f; else { // Jack #4 specific (LFO feature to output jack #4, but: clock generator mode only, and if jack ratio is set at "x1" only). if ((!activeCLK) && (jack4LFO != 0) && (outputRatioInUse[OUTPUT_4] == 12)) { LFOjack4.invert = ((jack4LFO % 2) == 0); LFOjack4.freq = (float)BPM / 60.0f; LFOjack4.step(engineGetSampleTime()); if (jack4LFObipolar) LFOjack4.offset = 0.0f; else LFOjack4.offset = outVoltage / 2.0f; switch (jack4LFO) { case 1: case 2: // LFO for jack #4 is a sine-based waveform. outputs[OUTPUT_4].value = isBPMRunning ? outVoltage / 2.0f * LFOjack4.sin() : 0.0f; break; case 3: case 4: // LFO for jack #4 is a triangle-based waveform. outputs[OUTPUT_4].value = isBPMRunning ? outVoltage / 2.0f * LFOjack4.tri() : 0.0f; break; case 5: case 6: // LFO for jack #4 is a sawtooth-based waveform. outputs[OUTPUT_4].value = isBPMRunning ? outVoltage / 2.0f * LFOjack4.saw() : 0.0f; } } else outputs[OUTPUT_4].value = sendingOutput[OUTPUT_4] ? outVoltage : 0.0f; } } // Afterglow for CLK (red) LED. if (ledClkAfterglow) { if (inputs[INPUT_CLOCK].value < 1.7f) { ledClkDelay++; if (ledClkDelay > round(engineGetSampleRate() / 16)) { ledClkAfterglow = false; ledClkDelay = 0; } } } // Handling the button (it's a momentary button, handled by a dedicated Schmitt trigger). // - Short presses toggles BPM clock start/stops (when released). // - Long press to enter SETUP. // - When SETUP is running, press to advance to next parameter. if (buttonPressed) { if (!isSetupRunning && !isEnteringSetup) { // Try to enter SETUP... starting delay counter for 2 seconds. isEnteringSetup = true; setupCounter = 0; allowedButtonHeld = true; // Allow to keep button held. } else if (isSetupRunning && !isExitingSetup) { // Try to quick save/exit SETUP... starting delay counter for 2 seconds. isExitingSetup = true; setupCounter = 0; // Necessary to avoid continuous entry/exit SETUP while button is held. allowedButtonHeld = true; // Allow to keep button held. } else allowedButtonHeld = false; // Button must be released. } // Don't add "else" clause from here, otherwise be sure it doesn't work! if (buttonPressed && isSetupRunning) { // SETUP is running: when (shortly) pressed, advance to next parameter. // Storing previous edited parameter into "edited" table prior to advance to next SETUP parameter. setup_Edited[setup_ParamIdx] = setup_CurrentValue; // Advance to next SETUP parameter (conditional). if ((setup_ParamIdx == SETUP_OUTSRATIOS) && (setup_Edited[SETUP_OUTSRATIOS] == 0)) setup_ParamIdx = SETUP_OUT4LFO; // Bypass all four jack ratios, and go directly to jack #4 LFO. else if ((setup_ParamIdx == SETUP_OUT4RATIO) && (setup_Edited[SETUP_OUTSRATIOS] == 1) && (setup_Edited[SETUP_OUT4RATIO] != 12)) setup_ParamIdx = SETUP_CVTRIG; // In case custom output jack ratios, jack #4 ratio isn't x1, bypass jack #4 LFO & polarity, and go directly to CV/TRIG. else if ((setup_ParamIdx == SETUP_OUT4LFO) && (setup_Edited[SETUP_OUT4LFO] == 0)) setup_ParamIdx = SETUP_CVTRIG; // Bypass LFO polarity entry is LFO mode is disabled, go directly to CV/TRIG. else setup_ParamIdx++; // Advance to next, in all other cases. // These variables are used to cycle display (parameter name, then its value). DEPRECATED! setupCounter = 0; if (setup_ParamIdx > SETUP_EXIT) { // Exiting SETUP. From here, all required actions on exit SETUP (such save, cancel changes, reset to default factory etc), except "Review" option! switch (setup_Edited[SETUP_EXIT]) { case 0: // Cancel/Exit: all changes from SETUP are ignored (changes are cancelled). // all previous (backuped) will be restored (any change is ignored). for (int i = 1; i < SETUP_EXIT; i++) setup_Current[i] = setup_Backup[i]; // Restored pre-SETUP settings, so it's useless to save them as "json" persistent. UpdateKlokSpidSettings(false); break; case 1: // Save/Exit: all parameters from SETUP will be saved. for (int i = 1; i < SETUP_EXIT; i++) setup_Current[i] = setup_Edited[i]; // Using new settings (because edited are saved), so it's mandatory to save them as "json" persistent datas. UpdateKlokSpidSettings(true); break; case 2: // Review: return to first parameter (don't exit "SETUP" in this choice is selected). setup_ParamIdx = 1; setup_CurrentValue = setup_Edited[1]; // Bypass the welcome message and edit first parameter. strcpy(dmdMessage1, stringToPchar(setupMenuName[1])); xOffsetValue = setupParamXOffset[1][setup_CurrentValue]; strcpy(dmdMessage2, stringToPchar(setupParamName[1][setup_CurrentValue])); break; case 3: // Factory: restore all factory default parameters. for (int i = 1; i < SETUP_EXIT; i++) setup_Current[i] = setup_Factory[i]; // Using new settings (because restored as default factory), like "Save/Exit", it's mandatory to save them as "json" persistent datas. UpdateKlokSpidSettings(true); break; } // Exit SETUP, except if "Review" was selected. if (setup_Edited[SETUP_EXIT] != 2) { // Exit SETUP (except if "Review" was selected). setupCounter = 0; // Clearing flag because now exit SETUP. isSetupRunning = false; // Update DMD to current running mode. if (activeCLK) updateDMDtoRunningMode(1); // Ratio (clock modulator mode). else updateDMDtoRunningMode(0); // Number of BPM. } } else if (setup_ParamIdx == SETUP_EXIT) { // Last default proposed parameter will be "Save and Exit" (SETUP). setup_CurrentValue = 1; // Update DMD. strcpy(dmdMessage1, stringToPchar(setupMenuName[setup_ParamIdx])); xOffsetValue = setupParamXOffset[setup_ParamIdx][setup_CurrentValue]; strcpy(dmdMessage2, stringToPchar(setupParamName[setup_ParamIdx][setup_CurrentValue])); } else { // Set currently displayed (on DMD) value as current (edited) parameter. setup_CurrentValue = setup_Edited[setup_ParamIdx]; // Update DMD. strcpy(dmdMessage1, stringToPchar(setupMenuName[setup_ParamIdx])); xOffsetValue = setupParamXOffset[setup_ParamIdx][setup_CurrentValue]; strcpy(dmdMessage2, stringToPchar(setupParamName[setup_ParamIdx][setup_CurrentValue])); } } if (runButton.isHigh()) { // Button is held, don't matter if SETUP is running or not. // Always increment the counter. setupCounter++; if (isSetupRunning && isExitingSetup) { if (setupCounter >= 2 * engineGetSampleRate()) { // Button was held during 2 seconds (while SETUP is running): now KlokSpid module exit SETUP (doing auto "Save/Exit"). // isExitingSetup = false; setupCounter = 0; allowedButtonHeld = false; // Button must be released (retrigger is required). for (int i=0; i= 2 * engineGetSampleRate())) { // Button was finally held during 2 seconds: now KlokSpid module runs its SETUP. Initializing some variables/arrays/flags first. // isEnteringSetup = false; setupCounter = 0; // Button must be released (retrigger is required). allowedButtonHeld = false; // Menu entry #0 is used to display "- SETUP -" as welcome message (don't have parameters, so be sure "parameter" is set to 0). setup_Current[SETUP_WELCOME_MESSAGE] = 0; // Copy current parameters (since initialization or previous SETUP) to "edited" parameters before entering SETUP. // Also use a "backup" table in case of "Cancel/Exit" choice. for (int i = 0; i < SETUP_EXIT; i++) { setup_Backup[i] = setup_Current[i]; setup_Edited[i] = setup_Current[i]; } // Lastest menu entry is used to exit SETUP menu, by default with save. setup_Edited[SETUP_EXIT] = 1; // Select first parameter will ne displayed. In fact, the welcome message "- SETUP -" on DMD when entered SETUP. setup_ParamIdx = 0; // Update DMD. strcpy(dmdMessage1, stringToPchar(setupMenuName[SETUP_WELCOME_MESSAGE])); xOffsetValue = setupParamXOffset[SETUP_WELCOME_MESSAGE][0]; strcpy(dmdMessage2, stringToPchar(setupParamName[SETUP_WELCOME_MESSAGE][0])); // This flag indicates SETUP is running. isSetupRunning = true; } } } else { if (isEnteringSetup) { // Abort entering SETUP. isEnteringSetup = false; // Button works as BPM start/stop toggle: inverting state. isBPMRunning = !isBPMRunning; // Persistence for current BPM-state (toJson). this->runBPMOnInit = isBPMRunning; } else if (isSetupRunning && isExitingSetup) { // Abort quick exit SETUP. isExitingSetup = false; } setupCounter = 0; allowedButtonHeld = false; // button must be "retriggered", to avoid continuous entry/exit SETUP while held. } // KlokSpid module's SETUP. if (isSetupRunning) { // SETUP is running. if (encoderDelta > 0) { // Incremented encoder (rotated clockwise). setup_CurrentValue++; if (setup_CurrentValue >= setup_NumValue[setup_ParamIdx]) setup_CurrentValue = 0; // End of values list: return to first value. // Update DMD. strcpy(dmdMessage1, stringToPchar(setupMenuName[setup_ParamIdx])); xOffsetValue = setupParamXOffset[setup_ParamIdx][setup_CurrentValue]; strcpy(dmdMessage2, stringToPchar(setupParamName[setup_ParamIdx][setup_CurrentValue])); // Update current parameter "in realtime". setup_Current[setup_ParamIdx] = setup_CurrentValue; // Update parameters, but without "jSon" persistence while SETUP is running! UpdateKlokSpidSettings(false); // Reset encoder move detection. encoderDelta = 0; } else if (encoderDelta < 0) { // Decremented encoder (rotated counter-clockwise). if (setup_NumValue[setup_ParamIdx] != 0) { if (setup_CurrentValue == 0) setup_CurrentValue = setup_NumValue[setup_ParamIdx] - 1; // Return to last possible value. else setup_CurrentValue--; //Previous value. } // Update DMD. strcpy(dmdMessage1, stringToPchar(setupMenuName[setup_ParamIdx])); xOffsetValue = setupParamXOffset[setup_ParamIdx][setup_CurrentValue]; strcpy(dmdMessage2, stringToPchar(setupParamName[setup_ParamIdx][setup_CurrentValue])); // Update current parameter "in realtime". setup_Current[setup_ParamIdx] = setup_CurrentValue; // Update parameters, but without "jSon" persistence while SETUP is running! UpdateKlokSpidSettings(false); // Reset encoder move detection. encoderDelta = 0; } } // Handling LEDs on KlokSpid module (at the end of step). lights[LED_SYNC_GREEN].value = ((activeCLK && (isSync || (clkModulatorMode == X1))) || (!activeCLK && isBPMRunning)) ? 1.0 : 0.0; // Unique "SYNC" LED: will be lit green color when sync'd / BPM is running. lights[LED_SYNC_RED].value = ((activeCLK && (isSync || (clkModulatorMode == X1))) || (!activeCLK && isBPMRunning)) ? 0.0 : 1.0; // Unique "SYNC" LED: will be lit red color (opposite cases). lights[LED_CLK].value = (isSetupRunning || ledClkAfterglow) ? 1.0 : 0.0; lights[LED_CV_TRIG].value = (isSetupRunning || activeCV) ? 1.0 : 0.0; // TODO -- MUST BE ENHANCED! lights[LED_CVMODE].value = (isSetupRunning || activeCLK) ? 1.0 : 0.0; lights[LED_TRIGMODE].value = (isSetupRunning || !activeCLK) ? 1.0 : 0.0; } // End of KlokSpid module's step() function. // Dot-matrix display (DMD) and small displays (near output jack) handler. struct KlokSpidDMD : TransparentWidget { KlokSpidModule *module; std::shared_ptr font; KlokSpidDMD() { font = Font::load(assetPlugin(plugin, "res/fonts/LEDCounter7.ttf")); } void updateDMD1(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, char* dmdMessage1) { nvgFontSize(vg, 16); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -2); nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff)); nvgText(vg, pos.x, pos.y, dmdMessage1, NULL); } void updateDMD2(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, int xOffsetValue, char* dmdMessage2) { nvgFontSize(vg, 20); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -1); nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff)); nvgText(vg, pos.x + xOffsetValue, pos.y, dmdMessage2, NULL); } void updateDispOut1(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, int xdispOutOffset1, char* dispOut1) { nvgFontSize(vg, 14); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -1); nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff)); nvgText(vg, pos.x + xdispOutOffset1, pos.y, dispOut1, NULL); } void updateDispOut2(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, int xdispOutOffset2, char* dispOut2) { nvgFontSize(vg, 14); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -1); nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff)); nvgText(vg, pos.x + xdispOutOffset2, pos.y, dispOut2, NULL); } void updateDispOut3(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, int xdispOutOffset3, char* dispOut3) { nvgFontSize(vg, 14); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -1); nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff)); nvgText(vg, pos.x + xdispOutOffset3, pos.y, dispOut3, NULL); } void updateDispOut4(NVGcontext *vg, Vec pos, NVGcolor DMDtextColor, int xdispOutOffset4, char* dispOut4) { nvgFontSize(vg, 14); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -1); nvgFillColor(vg, nvgTransRGBA(DMDtextColor, 0xff)); nvgText(vg, pos.x + xdispOutOffset4, pos.y, dispOut4, NULL); } void draw(NVGcontext *vg) override { // Main DMD. updateDMD1(vg, Vec(14, box.size.y - 174), module->DMDtextColor, module->dmdMessage1); updateDMD2(vg, Vec(12, box.size.y - 152), module->DMDtextColor, module->xOffsetValue, module->dmdMessage2); // Display between output jacks. updateDispOut1(vg, Vec(35, box.size.y + 61), module->DMDtextColor, module->xdispOutOffset1, module->dispOut1); updateDispOut2(vg, Vec(62.5, box.size.y + 61), module->DMDtextColor, module->xdispOutOffset2, module->dispOut2); updateDispOut3(vg, Vec(35, box.size.y + 75), module->DMDtextColor, module->xdispOutOffset3, module->dispOut3); updateDispOut4(vg, Vec(62.5, box.size.y + 75), module->DMDtextColor, module->xdispOutOffset4, module->dispOut4); } }; struct KlokSpidWidget : ModuleWidget { // Themed plates. SVGPanel *panelKlokSpidClassic; SVGPanel *panelKlokSpidStageRepro; SVGPanel *panelKlokSpidAbsoluteNight; SVGPanel *panelKlokSpidDarkSignature; SVGPanel *panelKlokSpidDeepBlueSignature; SVGPanel *panelCarbonSignature; // Silver Torx screws. SVGScrew *topLeftScrewSilver; SVGScrew *topRightScrewSilver; SVGScrew *bottomLeftScrewSilver; SVGScrew *bottomRightScrewSilver; // Gold Torx screws. SVGScrew *topLeftScrewGold; SVGScrew *topRightScrewGold; SVGScrew *bottomLeftScrewGold; SVGScrew *bottomRightScrewGold; // Silver button. SVGSwitch *buttonSilver; // Gold button. SVGSwitch *buttonGold; // KlokSpidWidget(KlokSpidModule *module); void step() override; // Action for "Initialize", from context-menu, is (for now) bypassed. void reset() override { return; }; // Action for "Randomize", from context-menu, is (for now) bypassed. void randomize() override { return; }; Menu* createContextMenu() override; }; KlokSpidWidget::KlokSpidWidget(KlokSpidModule *module) : ModuleWidget(module) { box.size = Vec(8 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT); // Model: Classic (beige plate, always default GUI when added from modules menu). panelKlokSpidClassic = new SVGPanel(); panelKlokSpidClassic->box.size = box.size; panelKlokSpidClassic->setBackground(SVG::load(assetPlugin(plugin, "res/KlokSpid_Classic.svg"))); addChild(panelKlokSpidClassic); // Model: Stage Repro. panelKlokSpidStageRepro = new SVGPanel(); panelKlokSpidStageRepro->box.size = box.size; panelKlokSpidStageRepro->setBackground(SVG::load(assetPlugin(plugin, "res/KlokSpid_Stage_Repro.svg"))); addChild(panelKlokSpidStageRepro); // Model: Absolute Night. panelKlokSpidAbsoluteNight = new SVGPanel(); panelKlokSpidAbsoluteNight->box.size = box.size; panelKlokSpidAbsoluteNight->setBackground(SVG::load(assetPlugin(plugin, "res/KlokSpid_Absolute_Night.svg"))); addChild(panelKlokSpidAbsoluteNight); // Model: Dark "Signature". panelKlokSpidDarkSignature = new SVGPanel(); panelKlokSpidDarkSignature->box.size = box.size; panelKlokSpidDarkSignature->setBackground(SVG::load(assetPlugin(plugin, "res/KlokSpid_Dark_Signature.svg"))); addChild(panelKlokSpidDarkSignature); // Model: Deepblue "Signature". panelKlokSpidDeepBlueSignature = new SVGPanel(); panelKlokSpidDeepBlueSignature->box.size = box.size; panelKlokSpidDeepBlueSignature->setBackground(SVG::load(assetPlugin(plugin, "res/KlokSpid_Deepblue_Signature.svg"))); addChild(panelKlokSpidDeepBlueSignature); // Model: Carbon "Signature". panelCarbonSignature = new SVGPanel(); panelCarbonSignature->box.size = box.size; panelCarbonSignature->setBackground(SVG::load(assetPlugin(plugin, "res/KlokSpid_Carbon_Signature.svg"))); addChild(panelCarbonSignature); // Always four screws for 8 HP module, may are silver or gold, depending model. // Top-left silver Torx screw. topLeftScrewSilver = Widget::create(Vec(RACK_GRID_WIDTH, 0)); addChild(topLeftScrewSilver); // Top-right silver Torx screw. topRightScrewSilver = Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)); addChild(topRightScrewSilver); // Bottom-left silver Torx screw. bottomLeftScrewSilver = Widget::create(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)); addChild(bottomLeftScrewSilver); // Bottom-right silver Torx screw. bottomRightScrewSilver = Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)); addChild(bottomRightScrewSilver); // Top-left gold Torx screw. topLeftScrewGold = Widget::create(Vec(RACK_GRID_WIDTH, 0)); addChild(topLeftScrewGold); // Top-right gold Torx screw. topRightScrewGold = Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)); addChild(topRightScrewGold); // Bottom-left gold Torx screw. bottomLeftScrewGold = Widget::create(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)); addChild(bottomLeftScrewGold); // Bottom-right gold Torx screw. bottomRightScrewGold = Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)); addChild(bottomRightScrewGold); // DMD display. { KlokSpidDMD *display = new KlokSpidDMD(); display->module = module; display->box.pos = Vec(0, 0); display->box.size = Vec(box.size.x, 234); addChild(display); } // Ratio/BPM/SETUP encoder. //addParam(ParamWidget::create(Vec(20, 106), module, KlokSpidModule::PARAM_ENCODER, -INFINITY, +INFINITY, 0.0)); module->klokspdEncoder = dynamic_cast(Knob::create(Vec(20, 106), module, KlokSpidModule::PARAM_ENCODER, -INFINITY, +INFINITY, 0.0)); addParam(module->klokspdEncoder); // Push button (silver), used to toggle START/STOP, also used to enter SETUP, and to advance to next parameter in SETUP. buttonSilver = ParamWidget::create(Vec(94, 178), module, KlokSpidModule::PARAM_BUTTON , 0.0, 1.0, 0.0); addParam(buttonSilver); // Push button (gold), used to toggle START/STOP, also used to enter SETUP, and to advance to next parameter in SETUP. buttonGold = ParamWidget::create(Vec(94, 178), module, KlokSpidModule::PARAM_BUTTON , 0.0, 1.0, 0.0); addParam(buttonGold); // Input ports (golden jacks). addInput(Port::create(Vec(24, 215), Port::INPUT, module, KlokSpidModule::INPUT_CLOCK)); addInput(Port::create(Vec(72, 215), Port::INPUT, module, KlokSpidModule::INPUT_CV_TRIG)); // Output ports (golden jacks). addOutput(Port::create(Vec(10, 261), Port::OUTPUT, module, KlokSpidModule::OUTPUT_1)); addOutput(Port::create(Vec(86, 261), Port::OUTPUT, module, KlokSpidModule::OUTPUT_2)); addOutput(Port::create(Vec(10, 310), Port::OUTPUT, module, KlokSpidModule::OUTPUT_3)); addOutput(Port::create(Vec(86, 310), Port::OUTPUT, module, KlokSpidModule::OUTPUT_4)); // LEDs (red for CLK input, yellow for CV-RATIO/TRIG input). addChild(ModuleLightWidget::create>(Vec(31.5, 242), module, KlokSpidModule::LED_CLK)); addChild(ModuleLightWidget::create>(Vec(79.5, 242), module, KlokSpidModule::LED_CV_TRIG)); //addChild(ModuleLightWidget::create>(Vec(56, 242), module, KlokSpidModule::LED_OUTPUT)); addChild(ModuleLightWidget::create>(Vec(7, 96), module, KlokSpidModule::LED_SYNC_GREEN)); // Unified SYNC LED (green/red). // Small-sized orange LEDs near CV-RATIO/TRIG input port (when lit, each LED indicates the port role). addChild(ModuleLightWidget::create>(Vec(67.5, 206), module, KlokSpidModule::LED_CVMODE)); addChild(ModuleLightWidget::create>(Vec(95, 206), module, KlokSpidModule::LED_TRIGMODE)); } //// Make one visible theme at once! void KlokSpidWidget::step() { KlokSpidModule *klokspidmodule = dynamic_cast(module); assert(klokspidmodule); // "Signature"-line modules are using gold parts (instead of silver). bool isSignatureLine = (klokspidmodule->Theme > 2); // Themed module plate, selected via context-menu, is visible (all others are obviously, hidden). panelKlokSpidClassic->visible = (klokspidmodule->Theme == 0); panelKlokSpidStageRepro->visible = (klokspidmodule->Theme == 1); panelKlokSpidAbsoluteNight->visible = (klokspidmodule->Theme == 2); panelKlokSpidDarkSignature->visible = (klokspidmodule->Theme == 3); panelKlokSpidDeepBlueSignature->visible = (klokspidmodule->Theme == 4); panelCarbonSignature->visible = (klokspidmodule->Theme == 5); // Silver Torx screws are visible only for non-"Signature" modules. topLeftScrewSilver->visible = !isSignatureLine; topRightScrewSilver->visible = !isSignatureLine; bottomLeftScrewSilver->visible = !isSignatureLine; bottomRightScrewSilver->visible = !isSignatureLine; // Gold Torx screws are visible only for "Signature" modules. topLeftScrewGold->visible = isSignatureLine; topRightScrewGold->visible = isSignatureLine; bottomLeftScrewGold->visible = isSignatureLine; bottomRightScrewGold->visible = isSignatureLine; // Silver or gold button is visible at once (opposite is, obvisouly, hidden). buttonSilver->visible = !isSignatureLine; buttonGold->visible = isSignatureLine; // Resume original step() method. ModuleWidget::step(); } //// CONTEXT-MENU (RIGHT-CLICK ON MODULE). // Classic (default beige) module. struct klokspidClassicMenu : MenuItem { KlokSpidModule *klokspidmodule; void onAction(EventAction &e) override { klokspidmodule->Theme = 0; } void step() override { rightText = (klokspidmodule->Theme == 0) ? "✔" : ""; MenuItem::step(); } }; // Stage Repro module. struct klokspidStageReproMenu : MenuItem { KlokSpidModule *klokspidmodule; void onAction(EventAction &e) override { klokspidmodule->Theme = 1; } void step() override { rightText = (klokspidmodule->Theme == 1) ? "✔" : ""; MenuItem::step(); } }; // Absolute Night module. struct klokspidAbsoluteNightMenu : MenuItem { KlokSpidModule *klokspidmodule; void onAction(EventAction &e) override { klokspidmodule->Theme = 2; } void step() override { rightText = (klokspidmodule->Theme == 2) ? "✔" : ""; MenuItem::step(); } }; // Dark "Signature" module. struct klokspidDarkSignatureMenu : MenuItem { KlokSpidModule *klokspidmodule; void onAction(EventAction &e) override { klokspidmodule->Theme = 3; } void step() override { rightText = (klokspidmodule->Theme == 3) ? "✔" : ""; MenuItem::step(); } }; // Deepblue "Signature" module. struct klokspidDeepblueSignatureMenu : MenuItem { KlokSpidModule *klokspidmodule; void onAction(EventAction &e) override { klokspidmodule->Theme = 4; } void step() override { rightText = (klokspidmodule->Theme == 4) ? "✔" : ""; MenuItem::step(); } }; // Carbon "Signature" module. struct klokspidCarbonSignatureMenu : MenuItem { KlokSpidModule *klokspidmodule; void onAction(EventAction &e) override { klokspidmodule->Theme = 5; } void step() override { rightText = (klokspidmodule->Theme == 5) ? "✔" : ""; MenuItem::step(); } }; // CONTEXT-MENU CONSTRUCTION. Menu* KlokSpidWidget::createContextMenu() { Menu* menu = ModuleWidget::createContextMenu(); KlokSpidModule *klokspidmodule = dynamic_cast(module); assert(klokspidmodule); menu->addChild(construct()); menu->addChild(construct(&MenuLabel::text, "Model:")); menu->addChild(construct(&klokspidClassicMenu::text, "Classic (default)", &klokspidClassicMenu::klokspidmodule, klokspidmodule)); menu->addChild(construct(&klokspidStageReproMenu::text, "Stage Repro", &klokspidStageReproMenu::klokspidmodule, klokspidmodule)); menu->addChild(construct(&klokspidAbsoluteNightMenu::text, "Absolute Night", &klokspidAbsoluteNightMenu::klokspidmodule, klokspidmodule)); menu->addChild(construct(&klokspidDarkSignatureMenu::text, "Dark \"Signature\"", &klokspidDarkSignatureMenu::klokspidmodule, klokspidmodule)); menu->addChild(construct(&klokspidDeepblueSignatureMenu::text, "Deepblue \"Signature\"", &klokspidDeepblueSignatureMenu::klokspidmodule, klokspidmodule)); menu->addChild(construct(&klokspidCarbonSignatureMenu::text, "Carbon \"Signature\"", &klokspidCarbonSignatureMenu::klokspidmodule, klokspidmodule)); return menu; } } // namespace rack_plugin_Ohmer using namespace rack_plugin_Ohmer; RACK_PLUGIN_MODEL_INIT(Ohmer, KlokSpid) { Model *modelKlokSpid = Model::create("Ohmer Modules", "KlokSpid", "KlokSpid", CLOCK_TAG, CLOCK_MODULATOR_TAG); // CLOCK_MODULATOR_TAG introduced in 0.6 API. return modelKlokSpid; }