//*********************************************************************************************** //Gate sequencer module for VCV Rack by Marc Boulé // //Based on code from the Fundamental and AudibleInstruments plugins by Andrew Belt //and graphics from the Component Library by Wes Milholen //See ./LICENSE.txt for all licenses //See ./res/fonts/ for font licenses // //Module concept by Nigel Sixsmith and Marc Boulé // //Acknowledgements: please see README.md //*********************************************************************************************** #include "GateSeq64Util.hpp" namespace rack_plugin_ImpromptuModular { struct GateSeq64 : Module { enum ParamIds { ENUMS(STEP_PARAMS, 64), MODES_PARAM, RUN_PARAM, CONFIG_PARAM, COPY_PARAM, PASTE_PARAM, RESET_PARAM, PROB_PARAM, EDIT_PARAM, SEQUENCE_PARAM, CPMODE_PARAM, // -- 0.6.9 ^^ GMODELEFT_PARAM,// no longer used GMODERIGHT_PARAM,// no longer used // -- 0.6.11 ^^ ENUMS(GMODE_PARAMS, 8), NUM_PARAMS }; enum InputIds { CLOCK_INPUT, RESET_INPUT, RUNCV_INPUT, SEQCV_INPUT, // -- 0.6.7 ^^ WRITE_INPUT, GATE_INPUT, PROB_INPUT, WRITE1_INPUT, WRITE0_INPUT, // -- 0.6.10 ^^ STEPL_INPUT, NUM_INPUTS }; enum OutputIds { ENUMS(GATE_OUTPUTS, 4), NUM_OUTPUTS }; enum LightIds { ENUMS(STEP_LIGHTS, 64 * 3),// room for GreenRedWhite P_LIGHT, RUN_LIGHT, RESET_LIGHT, ENUMS(GMODE_LIGHTS, 8 * 2),// room for GreenRed NUM_LIGHTS }; // Constants enum DisplayStateIds {DISP_GATE, DISP_LENGTH, DISP_MODES}; static const int MAX_SEQS = 32; // 1/4 DUO D2 TR1 TR2 TR3 TR23 TRI const uint32_t advGateHitMaskGS[8] = {0x00003F, 0x03F03F, 0x03F000, 0x00000F, 0x000F00, 0x0F0000, 0x0F0F00, 0x0F0F0F}; static const int blinkNumInit = 15;// init number of blink cycles for cursor static constexpr float CONFIG_PARAM_INIT_VALUE = 0.0f;// so that module constructor is coherent with widget initialization, since module created before widget // Need to save int panelTheme = 0; int expansion = 0; bool autoseq; int seqCVmethod;// 0 is 0-10V, 1 is C4-D5#, 2 is TrigIncr int pulsesPerStep;// 1 means normal gate mode, alt choices are 4, 6, 12, 24 PPS (Pulses per step) bool running; SeqAttributesGS sequences[MAX_SEQS]; int runModeSong; int sequence; int phrase[64];// This is the song (series of phases; a phrase is a patten number) int phrases;// 1 to 64 StepAttributesGS attributes[MAX_SEQS][64]; bool resetOnRun; // No need to save int displayState; int stepIndexEdit; int stepIndexRun[4]; int phraseIndexEdit; int phraseIndexRun; unsigned long stepIndexRunHistory; unsigned long phraseIndexRunHistory; StepAttributesGS attribCPbuffer[64]; SeqAttributesGS seqAttribCPbuffer; bool seqCopied; int phraseCPbuffer[64]; int countCP;// number of steps to paste (in case CPMODE_PARAM changes between copy and paste) int startCP; long infoCopyPaste;// 0 when no info, positive downward step counter timer when copy, negative upward when paste long clockIgnoreOnReset; long displayProbInfo;// downward step counter for displayProb feedback int gateCode[4]; long revertDisplay; long editingPpqn;// 0 when no info, positive downward step counter timer when editing ppqn int ppqnCount; long blinkCount;// positive upward counter, reset to 0 when max reached int blinkNum;// number of blink cycles to do, downward counter int stepConfig; long editingPhraseSongRunning;// downward step counter int stepConfigSync = 0;// 0 means no sync requested, 1 means soft sync (no reset lengths), 2 means hard (reset lengths) unsigned int lightRefreshCounter = 0; float resetLight = 0.0f; int sequenceKnob = 0; Trigger modesTrigger; Trigger stepTriggers[64]; Trigger copyTrigger; Trigger pasteTrigger; Trigger runningTrigger; Trigger clockTrigger; Trigger resetTrigger; Trigger writeTrigger; Trigger write0Trigger; Trigger write1Trigger; Trigger stepLTrigger; Trigger gModeTriggers[8]; Trigger probTrigger; Trigger seqCVTrigger; BooleanTrigger editingSequenceTrigger; HoldDetect modeHoldDetect; SeqAttributesGS seqAttribBuffer[MAX_SEQS];// buffer from Json for thread safety inline bool isEditingSequence(void) {return params[EDIT_PARAM].value > 0.5f;} inline int getStepConfig(float paramValue) {// 1 = 4x16 = 0.0f, 2 = 2x32 = 1.0f, 4 = 1x64 = 2.0f if (paramValue < 0.5f) return 1; else if (paramValue < 1.5f) return 2; return 4; } inline int getAdvGateGS(int ppqnCount, int pulsesPerStep, int gateMode) { uint32_t shiftAmt = ppqnCount * (24 / pulsesPerStep); return (int)((advGateHitMaskGS[gateMode] >> shiftAmt) & (uint32_t)0x1); } inline int calcGateCode(StepAttributesGS attribute, int ppqnCount, int pulsesPerStep) { // -1 = gate off for whole step, 0 = gate off for current ppqn, 1 = gate on, 2 = clock high if (ppqnCount == 0 && attribute.getGateP() && !(randomUniform() < ((float)(attribute.getGatePVal())/100.0f)))// randomUniform is [0.0, 1.0), see include/util/common.hpp return -1; if (!attribute.getGate()) return 0; if (pulsesPerStep == 1) return 2;// clock high return getAdvGateGS(ppqnCount, pulsesPerStep, attribute.getGateMode()); } inline void fillStepIndexRunVector(int runMode, int len) { if (runMode != MODE_RN2) { stepIndexRun[1] = stepIndexRun[0]; stepIndexRun[2] = stepIndexRun[0]; stepIndexRun[3] = stepIndexRun[0]; } else { stepIndexRun[1] = randomu32() % len; stepIndexRun[2] = randomu32() % len; stepIndexRun[3] = randomu32() % len; } } inline bool ppsRequirementMet(int gateButtonIndex) { return !( (pulsesPerStep < 2) || (pulsesPerStep == 4 && gateButtonIndex > 2) || (pulsesPerStep == 6 && gateButtonIndex <= 2) ); } GateSeq64() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { for (int i = 0; i < MAX_SEQS; i++) seqAttribBuffer[i].init(16, MODE_FWD); onReset(); } void onReset() override { stepConfig = getStepConfig(CONFIG_PARAM_INIT_VALUE); autoseq = false; seqCVmethod = 0; pulsesPerStep = 1; running = true; runModeSong = MODE_FWD; stepIndexEdit = 0; phraseIndexEdit = 0; sequence = 0; phrases = 4; for (int i = 0; i < MAX_SEQS; i++) { for (int s = 0; s < 64; s++) { attributes[i][s].init(); } sequences[i].init(16 * stepConfig, MODE_FWD); } for (int i = 0; i < 64; i++) { phrase[i] = 0; attribCPbuffer[i].init(); phraseCPbuffer[i] = 0; } initRun(); seqAttribCPbuffer.init(16, MODE_FWD); seqCopied = true; countCP = 64; startCP = 0; displayState = DISP_GATE; displayProbInfo = 0l; infoCopyPaste = 0l; revertDisplay = 0l; resetOnRun = false; editingPpqn = 0l; blinkCount = 0l; blinkNum = blinkNumInit; editingPhraseSongRunning = 0l; clockIgnoreOnReset = (long) (clockIgnoreOnResetDuration * engineGetSampleRate()); } void onRandomize() override { // stepConfig = getStepConfig(params[CONFIG_PARAM].value); // runModeSong = randomu32() % 5; // stepIndexEdit = 0; // phraseIndexEdit = 0; // sequence = randomu32() % MAX_SEQS; // phrases = 1 + (randomu32() % 64); // for (int i = 0; i < MAX_SEQS; i++) { // for (int s = 0; s < 64; s++) { // attributes[i][s].randomize(); // } // sequences[i].randomize(16 * stepConfig, NUM_MODES); // } // for (int i = 0; i < 64; i++) // phrase[i] = randomu32() % MAX_SEQS; // initRun(); if (isEditingSequence()) { for (int s = 0; s < 64; s++) { attributes[sequence][s].randomize(); } sequences[sequence].randomize(16 * stepConfig, NUM_MODES);// ok to use stepConfig since CONFIG_PARAM is not randomizable } } void initRun() {// run button activated or run edge in run input jack phraseIndexRun = (runModeSong == MODE_REV ? phrases - 1 : 0); phraseIndexRunHistory = 0; int seq = (isEditingSequence() ? sequence : phrase[phraseIndexRun]); stepIndexRun[0] = (sequences[seq].getRunMode() == MODE_REV ? sequences[seq].getLength() - 1 : 0); fillStepIndexRunVector(sequences[seq].getRunMode(), sequences[seq].getLength()); stepIndexRunHistory = 0; ppqnCount = 0; for (int i = 0; i < 4; i += stepConfig) gateCode[i] = calcGateCode(attributes[seq][(i * 16) + stepIndexRun[i]], 0, pulsesPerStep); } json_t *toJson() override { json_t *rootJ = json_object(); // panelTheme json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme)); // expansion json_object_set_new(rootJ, "expansion", json_integer(expansion)); // autoseq json_object_set_new(rootJ, "autoseq", json_boolean(autoseq)); // seqCVmethod json_object_set_new(rootJ, "seqCVmethod", json_integer(seqCVmethod)); // pulsesPerStep json_object_set_new(rootJ, "pulsesPerStep", json_integer(pulsesPerStep)); // running json_object_set_new(rootJ, "running", json_boolean(running)); // runModeSong json_object_set_new(rootJ, "runModeSong3", json_integer(runModeSong)); // sequence json_object_set_new(rootJ, "sequence", json_integer(sequence)); // phrase json_t *phraseJ = json_array(); for (int i = 0; i < 64; i++) json_array_insert_new(phraseJ, i, json_integer(phrase[i])); json_object_set_new(rootJ, "phrase2", phraseJ);// "2" appended so no break patches // phrases json_object_set_new(rootJ, "phrases", json_integer(phrases)); // attributes json_t *attributesJ = json_array(); for (int i = 0; i < MAX_SEQS; i++) for (int s = 0; s < 64; s++) { json_array_insert_new(attributesJ, s + (i * 64), json_integer(attributes[i][s].getAttribute())); } json_object_set_new(rootJ, "attributes2", attributesJ);// "2" appended so no break patches // resetOnRun json_object_set_new(rootJ, "resetOnRun", json_boolean(resetOnRun)); // stepIndexEdit json_object_set_new(rootJ, "stepIndexEdit", json_integer(stepIndexEdit)); // phraseIndexEdit json_object_set_new(rootJ, "phraseIndexEdit", json_integer(phraseIndexEdit)); // sequences json_t *sequencesJ = json_array(); for (int i = 0; i < MAX_SEQS; i++) json_array_insert_new(sequencesJ, i, json_integer(sequences[i].getSeqAttrib())); json_object_set_new(rootJ, "sequences", sequencesJ); return rootJ; } void fromJson(json_t *rootJ) override { // panelTheme json_t *panelThemeJ = json_object_get(rootJ, "panelTheme"); if (panelThemeJ) panelTheme = json_integer_value(panelThemeJ); // expansion json_t *expansionJ = json_object_get(rootJ, "expansion"); if (expansionJ) expansion = json_integer_value(expansionJ); // autoseq json_t *autoseqJ = json_object_get(rootJ, "autoseq"); if (autoseqJ) autoseq = json_is_true(autoseqJ); // seqCVmethod json_t *seqCVmethodJ = json_object_get(rootJ, "seqCVmethod"); if (seqCVmethodJ) seqCVmethod = json_integer_value(seqCVmethodJ); // pulsesPerStep json_t *pulsesPerStepJ = json_object_get(rootJ, "pulsesPerStep"); if (pulsesPerStepJ) pulsesPerStep = json_integer_value(pulsesPerStepJ); // running json_t *runningJ = json_object_get(rootJ, "running"); if (runningJ) running = json_is_true(runningJ); // sequences json_t *sequencesJ = json_object_get(rootJ, "sequences"); if (sequencesJ) { for (int i = 0; i < MAX_SEQS; i++) { json_t *sequencesArrayJ = json_array_get(sequencesJ, i); if (sequencesArrayJ) seqAttribBuffer[i].setSeqAttrib(json_integer_value(sequencesArrayJ)); } } else {// legacy int lengths[16];// 1 to 16 int runModeSeq[16]; // runModeSeq json_t *runModeSeqJ = json_object_get(rootJ, "runModeSeq3"); if (runModeSeqJ) { for (int i = 0; i < 16; i++) { json_t *runModeSeqArrayJ = json_array_get(runModeSeqJ, i); if (runModeSeqArrayJ) runModeSeq[i] = json_integer_value(runModeSeqArrayJ); } } else {// legacy runModeSeqJ = json_object_get(rootJ, "runModeSeq2"); if (runModeSeqJ) { for (int i = 0; i < 16; i++) { json_t *runModeSeqArrayJ = json_array_get(runModeSeqJ, i); if (runModeSeqArrayJ) { runModeSeq[i] = json_integer_value(runModeSeqArrayJ); if (runModeSeq[i] >= MODE_PEN)// this mode was not present in version runModeSeq2 runModeSeq[i]++; } } } } // lengths json_t *lengthsJ = json_object_get(rootJ, "lengths"); if (lengthsJ) { for (int i = 0; i < 16; i++) { json_t *lengthsArrayJ = json_array_get(lengthsJ, i); if (lengthsArrayJ) lengths[i] = json_integer_value(lengthsArrayJ); } } // now write into new object for (int i = 0; i < 16; i++) seqAttribBuffer[i].init(lengths[i], runModeSeq[i]); for (int i = 16; i < MAX_SEQS; i++) seqAttribBuffer[i].init(16, MODE_FWD); } // runModeSong json_t *runModeSongJ = json_object_get(rootJ, "runModeSong3"); if (runModeSongJ) runModeSong = json_integer_value(runModeSongJ); else {// legacy runModeSongJ = json_object_get(rootJ, "runModeSong"); if (runModeSongJ) { runModeSong = json_integer_value(runModeSongJ); if (runModeSong >= MODE_PEN)// this mode was not present in original version runModeSong++; } } // sequence json_t *sequenceJ = json_object_get(rootJ, "sequence"); if (sequenceJ) sequence = json_integer_value(sequenceJ); // phrase json_t *phraseJ = json_object_get(rootJ, "phrase2");// "2" appended so no break patches if (phraseJ) { for (int i = 0; i < 64; i++) { json_t *phraseArrayJ = json_array_get(phraseJ, i); if (phraseArrayJ) phrase[i] = json_integer_value(phraseArrayJ); } } else {// legacy phraseJ = json_object_get(rootJ, "phrase"); if (phraseJ) { for (int i = 0; i < 16; i++) { json_t *phraseArrayJ = json_array_get(phraseJ, i); if (phraseArrayJ) phrase[i] = json_integer_value(phraseArrayJ); } for (int i = 16; i < 64; i++) phrase[i] = 0; } } // phrases json_t *phrasesJ = json_object_get(rootJ, "phrases"); if (phrasesJ) phrases = json_integer_value(phrasesJ); // attributes json_t *attributesJ = json_object_get(rootJ, "attributes2"); if (attributesJ) { for (int i = 0; i < MAX_SEQS; i++) for (int s = 0; s < 64; s++) { json_t *attributesArrayJ = json_array_get(attributesJ, s + (i * 64)); if (attributesArrayJ) attributes[i][s].setAttribute((unsigned short)json_integer_value(attributesArrayJ)); } } else { attributesJ = json_object_get(rootJ, "attributes"); if (attributesJ) { for (int i = 0; i < 16; i++) { for (int s = 0; s < 64; s++) { json_t *attributesArrayJ = json_array_get(attributesJ, s + (i * 64)); if (attributesArrayJ) attributes[i][s].setAttribute((unsigned short)json_integer_value(attributesArrayJ)); } } for (int i = 16; i < MAX_SEQS; i++) { for (int s = 0; s < 64; s++) attributes[i][s].init(); } } } // resetOnRun json_t *resetOnRunJ = json_object_get(rootJ, "resetOnRun"); if (resetOnRunJ) resetOnRun = json_is_true(resetOnRunJ); // stepIndexEdit json_t *stepIndexEditJ = json_object_get(rootJ, "stepIndexEdit"); if (stepIndexEditJ) stepIndexEdit = json_integer_value(stepIndexEditJ); // phraseIndexEdit json_t *phraseIndexEditJ = json_object_get(rootJ, "phraseIndexEdit"); if (phraseIndexEditJ) phraseIndexEdit = json_integer_value(phraseIndexEditJ); stepConfigSync = 1;// signal a sync from fromJson so that step will get lengths from seqAttribBuffer } void step() override { static const float displayProbInfoTime = 3.0f;// seconds static const float revertDisplayTime = 0.7f;// seconds static const float holdDetectTime = 2.0f;// seconds static const float editingPhraseSongRunningTime = 4.0f;// seconds static const float editingPpqnTime = 3.5f;// seconds float sampleRate = engineGetSampleRate(); //********** Buttons, knobs, switches and inputs ********** // Edit mode bool editingSequence = isEditingSequence();// true = editing sequence, false = editing song // Run state button if (runningTrigger.process(params[RUN_PARAM].value + inputs[RUNCV_INPUT].value)) {// no input refresh here, don't want to introduce startup skew running = !running; if (running) { if (resetOnRun) initRun(); if (resetOnRun || clockIgnoreOnRun) clockIgnoreOnReset = (long) (clockIgnoreOnResetDuration * sampleRate); } else blinkNum = blinkNumInit; displayState = DISP_GATE; } if ((lightRefreshCounter & userInputsStepSkipMask) == 0) { // Edit mode blink when change if (editingSequenceTrigger.process(editingSequence)) blinkNum = blinkNumInit; // Config switch if (stepConfigSync != 0) { stepConfig = getStepConfig(params[CONFIG_PARAM].value); if (stepConfigSync == 1) {// sync from fromJson, so read lengths from seqAttribBuffer for (int i = 0; i < MAX_SEQS; i++) sequences[i].setSeqAttrib(seqAttribBuffer[i].getSeqAttrib()); } else if (stepConfigSync == 2) {// sync from a real mouse drag event on the switch itself, so init lengths for (int i = 0; i < MAX_SEQS; i++) sequences[i].setLength(16 * stepConfig); } initRun(); stepConfigSync = 0; } // Seq CV input if (inputs[SEQCV_INPUT].active) { if (seqCVmethod == 0) {// 0-10 V int newSeq = (int)( inputs[SEQCV_INPUT].value * (((float)MAX_SEQS) - 1.0f) / 10.0f + 0.5f ); sequence = clamp(newSeq, 0, MAX_SEQS - 1); } else if (seqCVmethod == 1) {// C4-G6 int newSeq = (int)( (inputs[SEQCV_INPUT].value) * 12.0f + 0.5f ); sequence = clamp(newSeq, 0, MAX_SEQS - 1); } else {// TrigIncr if (seqCVTrigger.process(inputs[SEQCV_INPUT].value)) sequence = clamp(sequence + 1, 0, MAX_SEQS - 1); } } // Copy button if (copyTrigger.process(params[COPY_PARAM].value)) { startCP = editingSequence ? stepIndexEdit : phraseIndexEdit; countCP = 64; if (params[CPMODE_PARAM].value > 1.5f)// ALL startCP = 0; else if (params[CPMODE_PARAM].value < 0.5f)// 4 countCP = min(4, 64 - startCP); else// 8 countCP = min(8, 64 - startCP); if (editingSequence) { for (int i = 0, s = startCP; i < countCP; i++, s++) attribCPbuffer[i] = attributes[sequence][s]; seqAttribCPbuffer.setSeqAttrib(sequences[sequence].getSeqAttrib()); seqCopied = true; } else { for (int i = 0, p = startCP; i < countCP; i++, p++) phraseCPbuffer[i] = phrase[p]; seqCopied = false;// so that a cross paste can be detected } infoCopyPaste = (long) (revertDisplayTime * sampleRate / displayRefreshStepSkips); displayState = DISP_GATE; blinkNum = blinkNumInit; } // Paste button if (pasteTrigger.process(params[PASTE_PARAM].value)) { infoCopyPaste = (long) (-1 * revertDisplayTime * sampleRate / displayRefreshStepSkips); startCP = 0; if (countCP <= 8) { startCP = editingSequence ? stepIndexEdit : phraseIndexEdit; countCP = min(countCP, 64 - startCP); } // else nothing to do for ALL if (editingSequence) { if (seqCopied) {// non-crossed paste (seq vs song) for (int i = 0, s = startCP; i < countCP; i++, s++) attributes[sequence][s] = attribCPbuffer[i]; if (params[CPMODE_PARAM].value > 1.5f) {// all sequences[sequence].setSeqAttrib(seqAttribCPbuffer.getSeqAttrib()); if (sequences[sequence].getLength() > 16 * stepConfig) sequences[sequence].setLength(16 * stepConfig); } } else {// crossed paste to seq (seq vs song) if (params[CPMODE_PARAM].value > 1.5f) { // ALL (init steps) for (int s = 0; s < 64; s++) attributes[sequence][s].init(); } else if (params[CPMODE_PARAM].value < 0.5f) {// 4 (randomize gates) for (int s = 0; s < 64; s++) if ( (randomu32() & 0x1) != 0) attributes[sequence][s].toggleGate(); } else {// 8 (randomize probs) for (int s = 0; s < 64; s++) { attributes[sequence][s].setGateP((randomu32() & 0x1) != 0); attributes[sequence][s].setGatePVal(randomu32() % 101); } } startCP = 0; countCP = 64; infoCopyPaste *= 2l; } } else {// song if (!seqCopied) {// non-crossed paste (seq vs song) for (int i = 0, p = startCP; i < countCP; i++, p++) phrase[p] = phraseCPbuffer[i] & 0xF; } else {// crossed paste to song (seq vs song) if (params[CPMODE_PARAM].value > 1.5f) { // ALL (init phrases) for (int p = 0; p < 64; p++) phrase[p] = 0; } else if (params[CPMODE_PARAM].value < 0.5f) {// 4 (phrases increase from 1 to 64) for (int p = 0; p < 64; p++) phrase[p] = p; } else {// 8 (randomize phrases) for (int p = 0; p < 64; p++) phrase[p] = randomu32() % 64; } startCP = 0; countCP = 64; infoCopyPaste *= 2l; } } displayState = DISP_GATE; blinkNum = blinkNumInit; } // Write CV inputs bool writeTrig = writeTrigger.process(inputs[WRITE_INPUT].value); bool write0Trig = write0Trigger.process(inputs[WRITE0_INPUT].value); bool write1Trig = write1Trigger.process(inputs[WRITE1_INPUT].value); if (writeTrig || write0Trig || write1Trig) { if (editingSequence) { blinkNum = blinkNumInit; if (writeTrig) {// higher priority than write0 and write1 if (inputs[PROB_INPUT].active) { attributes[sequence][stepIndexEdit].setGatePVal(clamp( (int)round(inputs[PROB_INPUT].value * 10.0f), 0, 100) ); attributes[sequence][stepIndexEdit].setGateP(true); } else{ attributes[sequence][stepIndexEdit].setGateP(false); } if (inputs[GATE_INPUT].active) attributes[sequence][stepIndexEdit].setGate(inputs[GATE_INPUT].value >= 1.0f); } else {// write1 or write0 attributes[sequence][stepIndexEdit].setGate(write1Trig); } // Autostep (after grab all active inputs) stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + 1, 64); if (stepIndexEdit == 0 && autoseq && !inputs[SEQCV_INPUT].active) sequence = moveIndex(sequence, sequence + 1, MAX_SEQS); } } // Step left CV input if (stepLTrigger.process(inputs[STEPL_INPUT].value)) { if (editingSequence) { blinkNum = blinkNumInit; stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit - 1, 64); } } // Step LED button presses int stepPressed = -1; for (int i = 0; i < 64; i++) { if (stepTriggers[i].process(params[STEP_PARAMS + i].value)) stepPressed = i; } if (stepPressed != -1) { if (editingSequence) { if (displayState == DISP_LENGTH) { sequences[sequence].setLength(stepPressed % (16 * stepConfig) + 1); revertDisplay = (long) (revertDisplayTime * sampleRate / displayRefreshStepSkips); } else if (displayState == DISP_MODES) { } else { if (params[STEP_PARAMS + stepPressed].value > 1.5f) {// right button click attributes[sequence][stepPressed].setGate(false); displayProbInfo = 0l; } else if (!attributes[sequence][stepPressed].getGate()) {// clicked inactive, so turn gate on attributes[sequence][stepPressed].setGate(true); if (attributes[sequence][stepPressed].getGateP()) displayProbInfo = (long) (displayProbInfoTime * sampleRate / displayRefreshStepSkips); else displayProbInfo = 0l; } else {// clicked active if (stepIndexEdit == stepPressed && blinkNum != 0) {// only if coming from current step, turn off attributes[sequence][stepPressed].setGate(false); displayProbInfo = 0l; } else { if (attributes[sequence][stepPressed].getGateP()) displayProbInfo = (long) (displayProbInfoTime * sampleRate / displayRefreshStepSkips); else displayProbInfo = 0l; } } stepIndexEdit = stepPressed; } blinkNum = blinkNumInit; } else {// editing song if (displayState == DISP_LENGTH) { phrases = stepPressed + 1; if (phrases > 64) phrases = 64; if (phrases < 1 ) phrases = 1; revertDisplay = (long) (revertDisplayTime * sampleRate / displayRefreshStepSkips); } else if (displayState == DISP_MODES) { } else { phraseIndexEdit = stepPressed; if (running) editingPhraseSongRunning = (long) (editingPhraseSongRunningTime * sampleRate / displayRefreshStepSkips); else phraseIndexRun = stepPressed; } } } // Mode/Length button if (modesTrigger.process(params[MODES_PARAM].value)) { blinkNum = blinkNumInit; if (editingPpqn != 0l) editingPpqn = 0l; if (displayState == DISP_GATE) displayState = DISP_LENGTH; else if (displayState == DISP_LENGTH) displayState = DISP_MODES; else displayState = DISP_GATE; modeHoldDetect.start((long) (holdDetectTime * sampleRate / displayRefreshStepSkips)); } // Prob button if (probTrigger.process(params[PROB_PARAM].value)) { blinkNum = blinkNumInit; if (editingSequence && attributes[sequence][stepIndexEdit].getGate()) { if (attributes[sequence][stepIndexEdit].getGateP()) { displayProbInfo = 0l; attributes[sequence][stepIndexEdit].setGateP(false); } else { displayProbInfo = (long) (displayProbInfoTime * sampleRate / displayRefreshStepSkips); attributes[sequence][stepIndexEdit].setGateP(true); } } } // GateMode buttons for (int i = 0; i < 8; i++) { if (gModeTriggers[i].process(params[GMODE_PARAMS + i].value)) { blinkNum = blinkNumInit; if (editingSequence && attributes[sequence][stepIndexEdit].getGate()) { if (ppsRequirementMet(i)) { editingPpqn = 0l; attributes[sequence][stepIndexEdit].setGateMode(i); } else { editingPpqn = (long) (editingPpqnTime * sampleRate / displayRefreshStepSkips); } } } } // Sequence knob (Main knob) float seqParamValue = params[SEQUENCE_PARAM].value; int newSequenceKnob = (int)roundf(seqParamValue * 7.0f); if (seqParamValue == 0.0f)// true when constructor or fromJson() occured sequenceKnob = newSequenceKnob; int deltaKnob = newSequenceKnob - sequenceKnob; if (deltaKnob != 0) { if (abs(deltaKnob) <= 3) {// avoid discontinuous step (initialize for example) if (displayProbInfo != 0l && editingSequence) { blinkNum = blinkNumInit; int pval = attributes[sequence][stepIndexEdit].getGatePVal(); pval += deltaKnob * 2; if (pval > 100) pval = 100; if (pval < 0) pval = 0; attributes[sequence][stepIndexEdit].setGatePVal(pval); displayProbInfo = (long) (displayProbInfoTime * sampleRate / displayRefreshStepSkips); } else if (editingPpqn != 0) { pulsesPerStep = indexToPpsGS(ppsToIndexGS(pulsesPerStep) + deltaKnob);// indexToPps() does clamping editingPpqn = (long) (editingPpqnTime * sampleRate / displayRefreshStepSkips); } else if (displayState == DISP_MODES) { if (editingSequence) { sequences[sequence].setRunMode(clamp(sequences[sequence].getRunMode() + deltaKnob, 0, NUM_MODES - 1)); } else { runModeSong = clamp(runModeSong + deltaKnob, 0, 6 - 1); } } else if (displayState == DISP_LENGTH) { if (editingSequence) { sequences[sequence].setLength(clamp(sequences[sequence].getLength() + deltaKnob, 1, (16 * stepConfig))); } else { phrases = clamp(phrases + deltaKnob, 1, 64); } } else { if (editingSequence) { blinkNum = blinkNumInit; if (!inputs[SEQCV_INPUT].active) { sequence = clamp(sequence + deltaKnob, 0, MAX_SEQS - 1); } } else { if (editingPhraseSongRunning > 0l || !running) { int newPhrase = phrase[phraseIndexEdit] + deltaKnob; if (newPhrase < 0) newPhrase += (1 - newPhrase / MAX_SEQS) * MAX_SEQS;// newPhrase now positive newPhrase = newPhrase % MAX_SEQS; phrase[phraseIndexEdit] = newPhrase; if (running) editingPhraseSongRunning = (long) (editingPhraseSongRunningTime * sampleRate / displayRefreshStepSkips); } } } } sequenceKnob = newSequenceKnob; } }// userInputs refresh //********** Clock and reset ********** // Clock if (running && clockIgnoreOnReset == 0l) { if (clockTrigger.process(inputs[CLOCK_INPUT].value)) { ppqnCount++; if (ppqnCount >= pulsesPerStep) ppqnCount = 0; int newSeq = sequence;// good value when editingSequence, overwrite if not editingSequence if (ppqnCount == 0) { if (editingSequence) { moveIndexRunMode(&stepIndexRun[0], sequences[sequence].getLength(), sequences[sequence].getRunMode(), &stepIndexRunHistory); } else { if (moveIndexRunMode(&stepIndexRun[0], sequences[phrase[phraseIndexRun]].getLength(), sequences[phrase[phraseIndexRun]].getRunMode(), &stepIndexRunHistory)) { moveIndexRunMode(&phraseIndexRun, phrases, runModeSong, &phraseIndexRunHistory); stepIndexRun[0] = (sequences[phrase[phraseIndexRun]].getRunMode() == MODE_REV ? sequences[phrase[phraseIndexRun]].getLength() - 1 : 0);// must always refresh after phraseIndexRun has changed } newSeq = phrase[phraseIndexRun]; } fillStepIndexRunVector(sequences[newSeq].getRunMode(), sequences[newSeq].getLength()); } else { if (!editingSequence) newSeq = phrase[phraseIndexRun]; } for (int i = 0; i < 4; i += stepConfig) { if (gateCode[i] != -1 || ppqnCount == 0) gateCode[i] = calcGateCode(attributes[newSeq][(i * 16) + stepIndexRun[i]], ppqnCount, pulsesPerStep); } } } // Reset if (resetTrigger.process(inputs[RESET_INPUT].value + params[RESET_PARAM].value)) { initRun();// must be before SEQCV_INPUT below resetLight = 1.0f; displayState = DISP_GATE; clockIgnoreOnReset = (long) (clockIgnoreOnResetDuration * sampleRate); clockTrigger.reset(); if (inputs[SEQCV_INPUT].active && seqCVmethod == 2) sequence = 0; } //********** Outputs and lights ********** // Gate outputs if (running) { bool retriggingOnReset = (clockIgnoreOnReset != 0l && retrigGatesOnReset); for (int i = 0; i < 4; i++) outputs[GATE_OUTPUTS + i].value = (calcGate(gateCode[i], clockTrigger) && !retriggingOnReset) ? 10.0f : 0.0f; } else {// not running (no gates, no need to hear anything) for (int i = 0; i < 4; i++) outputs[GATE_OUTPUTS + i].value = 0.0f; } lightRefreshCounter++; if (lightRefreshCounter >= displayRefreshStepSkips) { lightRefreshCounter = 0; // Step LED button lights if (infoCopyPaste != 0l) { for (int i = 0; i < 64; i++) { if (i >= startCP && i < (startCP + countCP)) setGreenRed3(STEP_LIGHTS + i * 3, 0.5f, 0.0f); else setGreenRed3(STEP_LIGHTS + i * 3, 0.0f, 0.0f); } } else { int row = -1; int col = -1; for (int i = 0; i < 64; i++) { row = i >> (3 + stepConfig);//i / (16 * stepConfig);// optimized (not equivalent code, but in this case has same effect) if (stepConfig == 2 && row == 1) row++; col = (((stepConfig - 1) << 4) | 0xF) & i;//i % (16 * stepConfig);// optimized if (editingSequence) { if (displayState == DISP_LENGTH) { if (col < (sequences[sequence].getLength() - 1)) setGreenRed3(STEP_LIGHTS + i * 3, 0.1f, 0.0f); else if (col == (sequences[sequence].getLength() - 1)) setGreenRed3(STEP_LIGHTS + i * 3, 1.0f, 0.0f); else setGreenRed3(STEP_LIGHTS + i * 3, 0.0f, 0.0f); } else { float stepHereOffset = ((stepIndexRun[row] == col) && running) ? 0.5f : 1.0f; long blinkCountMarker = (long) (0.67f * sampleRate / displayRefreshStepSkips); if (attributes[sequence][i].getGate()) { bool blinkEnableOn = (displayState != DISP_MODES) && (blinkCount < blinkCountMarker); if (attributes[sequence][i].getGateP()) { if (i == stepIndexEdit)// more orange than yellow setGreenRed3(STEP_LIGHTS + i * 3, blinkEnableOn ? 1.0f : 0.0f, blinkEnableOn ? 1.0f : 0.0f); else// more yellow setGreenRed3(STEP_LIGHTS + i * 3, stepHereOffset, stepHereOffset); } else { if (i == stepIndexEdit) setGreenRed3(STEP_LIGHTS + i * 3, blinkEnableOn ? 1.0f : 0.0f, 0.0f); else setGreenRed3(STEP_LIGHTS + i * 3, stepHereOffset, 0.0f); } } else { if (i == stepIndexEdit && blinkCount > blinkCountMarker && displayState != DISP_MODES) setGreenRed3(STEP_LIGHTS + i * 3, 0.05f, 0.0f); else setGreenRed3(STEP_LIGHTS + i * 3, ((stepIndexRun[row] == col) && running) ? 0.1f : 0.0f, 0.0f); } } } else {// editing Song if (displayState == DISP_LENGTH) { col = i & 0xF;//i % 16;// optimized if (i < (phrases - 1)) setGreenRed3(STEP_LIGHTS + i * 3, 0.1f, 0.0f); else if (i == (phrases - 1)) setGreenRed3(STEP_LIGHTS + i * 3, 1.0f, 0.0f); else setGreenRed3(STEP_LIGHTS + i * 3, 0.0f, 0.0f); } else { float green = (i == (phraseIndexRun) && running) ? 1.0f : 0.0f; float red = (i == (phraseIndexEdit) && ((editingPhraseSongRunning > 0l) || !running)) ? 1.0f : 0.0f; green += ((running && (col == stepIndexRun[row]) && i != (phraseIndexEdit)) ? 0.1f : 0.0f); setGreenRed3(STEP_LIGHTS + i * 3, clamp(green, 0.0f, 1.0f), red); if (green == 0.0f && red == 0.0f && displayState != DISP_MODES){ lights[STEP_LIGHTS + i * 3 + 2].value = (attributes[phrase[phraseIndexRun]][i].getGate() ? 0.2f : 0.0f); lights[STEP_LIGHTS + i * 3 + 2].value -= (attributes[phrase[phraseIndexRun]][i].getGateP() ? 0.18f : 0.0f); } } } } } // GateType lights if (pulsesPerStep != 1 && editingSequence && attributes[sequence][stepIndexEdit].getGate()) { if (editingPpqn != 0) { for (int i = 0; i < 8; i++) { if (ppsRequirementMet(i)) setGreenRed(GMODE_LIGHTS + i * 2, 1.0f, 0.0f); else setGreenRed(GMODE_LIGHTS + i * 2, 0.0f, 0.0f); } } else { int gmode = attributes[sequence][stepIndexEdit].getGateMode(); for (int i = 0; i < 8; i++) { if (i == gmode) { if ( (pulsesPerStep == 4 && i > 2) || (pulsesPerStep == 6 && i <= 2) ) // pps requirement not met setGreenRed(GMODE_LIGHTS + i * 2, 0.0f, 1.0f); else setGreenRed(GMODE_LIGHTS + i * 2, 1.0f, 0.0f); } else setGreenRed(GMODE_LIGHTS + i * 2, 0.0f, 0.0f); } } } else { for (int i = 0; i < 8; i++) setGreenRed(GMODE_LIGHTS + i * 2, 0.0f, 0.0f); } // Reset light lights[RESET_LIGHT].value = resetLight; resetLight -= (resetLight / lightLambda) * engineGetSampleTime() * displayRefreshStepSkips; // Run lights lights[RUN_LIGHT].value = running ? 1.0f : 0.0f; if (infoCopyPaste != 0l) { if (infoCopyPaste > 0l) infoCopyPaste --; if (infoCopyPaste < 0l) infoCopyPaste ++; } if (displayProbInfo > 0l) displayProbInfo--; if (modeHoldDetect.process(params[MODES_PARAM].value)) { displayState = DISP_GATE; editingPpqn = (long) (editingPpqnTime * sampleRate / displayRefreshStepSkips); } if (editingPpqn > 0l) editingPpqn--; if (editingPhraseSongRunning > 0l) editingPhraseSongRunning--; if (revertDisplay > 0l) { if (revertDisplay == 1) displayState = DISP_GATE; revertDisplay--; } if (blinkNum > 0) { blinkCount++; if (blinkCount >= (long) (1.0f * sampleRate / displayRefreshStepSkips)) { blinkCount = 0l; blinkNum--; } } }// lightRefreshCounter if (clockIgnoreOnReset > 0l) clockIgnoreOnReset--; }// step() inline void setGreenRed(int id, float green, float red) { lights[id + 0].value = green; lights[id + 1].value = red; } inline void setGreenRed3(int id, float green, float red) { setGreenRed(id, green, red); lights[id + 2].value = 0.0f; } };// GateSeq64 : module struct GateSeq64Widget : ModuleWidget { GateSeq64 *module; DynamicSVGPanel *panel; int oldExpansion; int expWidth = 60; IMPort* expPorts[6]; struct SequenceDisplayWidget : TransparentWidget { GateSeq64 *module; std::shared_ptr font; char displayStr[4]; SequenceDisplayWidget() { font = Font::load(assetPlugin(plugin, "res/fonts/Segment14.ttf")); } void runModeToStr(int num) { if (num >= 0 && num < NUM_MODES) snprintf(displayStr, 4, "%s", modeLabels[num].c_str()); } void draw(NVGcontext *vg) override { NVGcolor textColor = prepareDisplay(vg, &box, 18); nvgFontFaceId(vg, font->handle); bool editingSequence = module->isEditingSequence(); Vec textPos = Vec(6, 24); nvgFillColor(vg, nvgTransRGBA(textColor, displayAlpha)); nvgText(vg, textPos.x, textPos.y, "~~~", NULL); nvgFillColor(vg, textColor); if (module->infoCopyPaste != 0l) { if (module->infoCopyPaste > 0l)// if copy display "CPY" snprintf(displayStr, 4, "CPY"); else { float cpMode = module->params[GateSeq64::CPMODE_PARAM].value; if (editingSequence && !module->seqCopied) {// cross paste to seq if (cpMode > 1.5f)// All = init snprintf(displayStr, 4, "CLR"); else if (cpMode < 0.5f)// 4 = random gate snprintf(displayStr, 4, "RGT"); else// 8 = random probs snprintf(displayStr, 4, "RPR"); } else if (!editingSequence && module->seqCopied) {// cross paste to song if (cpMode > 1.5f)// All = init snprintf(displayStr, 4, "CLR"); else if (cpMode < 0.5f)// 4 = increase by 1 snprintf(displayStr, 4, "INC"); else// 8 = random phrases snprintf(displayStr, 4, "RPH"); } else snprintf(displayStr, 4, "PST"); } } else if (module->displayProbInfo != 0l) { int prob = module->attributes[module->sequence][module->stepIndexEdit].getGatePVal(); if ( prob>= 100) snprintf(displayStr, 4, "1,0"); else if (prob >= 1) snprintf(displayStr, 4, ",%02u", (unsigned) prob); else snprintf(displayStr, 4, " 0"); } else if (module->editingPpqn != 0ul) { snprintf(displayStr, 4, "x%2u", (unsigned) module->pulsesPerStep); } else if (module->displayState == GateSeq64::DISP_LENGTH) { if (editingSequence) snprintf(displayStr, 4, "L%2u", (unsigned) module->sequences[module->sequence].getLength()); else snprintf(displayStr, 4, "L%2u", (unsigned) module->phrases); } else if (module->displayState == GateSeq64::DISP_MODES) { if (editingSequence) runModeToStr(module->sequences[module->sequence].getRunMode()); else runModeToStr(module->runModeSong); } else { int dispVal = 0; char specialCode = ' '; if (editingSequence) dispVal = module->sequence; else { if (module->editingPhraseSongRunning > 0l || !module->running) { dispVal = module->phrase[module->phraseIndexEdit]; if (module->editingPhraseSongRunning > 0l) specialCode = '*'; } else dispVal = module->phrase[module->phraseIndexRun]; } snprintf(displayStr, 4, "%c%2u", specialCode, (unsigned)(dispVal) + 1 ); } nvgText(vg, textPos.x, textPos.y, displayStr, NULL); } }; struct PanelThemeItem : MenuItem { GateSeq64 *module; int theme; void onAction(EventAction &e) override { module->panelTheme = theme; } void step() override { rightText = (module->panelTheme == theme) ? "✔" : ""; } }; struct ExpansionItem : MenuItem { GateSeq64 *module; void onAction(EventAction &e) override { module->expansion = module->expansion == 1 ? 0 : 1; } }; struct ResetOnRunItem : MenuItem { GateSeq64 *module; void onAction(EventAction &e) override { module->resetOnRun = !module->resetOnRun; } }; struct AutoseqItem : MenuItem { GateSeq64 *module; void onAction(EventAction &e) override { module->autoseq = !module->autoseq; } }; struct SeqCVmethodItem : MenuItem { GateSeq64 *module; void onAction(EventAction &e) override { module->seqCVmethod++; if (module->seqCVmethod > 2) module->seqCVmethod = 0; } void step() override { if (module->seqCVmethod == 0) text = "Seq CV in: <0-10V>, C4-G6, Trig-Incr"; else if (module->seqCVmethod == 1) text = "Seq CV in: 0-10V, , Trig-Incr"; else text = "Seq CV in: 0-10V, C4-G6, "; } }; Menu *createContextMenu() override { Menu *menu = ModuleWidget::createContextMenu(); MenuLabel *spacerLabel = new MenuLabel(); menu->addChild(spacerLabel); GateSeq64 *module = dynamic_cast(this->module); assert(module); MenuLabel *themeLabel = new MenuLabel(); themeLabel->text = "Panel Theme"; menu->addChild(themeLabel); PanelThemeItem *lightItem = new PanelThemeItem(); lightItem->text = lightPanelID;// ImpromptuModular.hpp lightItem->module = module; lightItem->theme = 0; menu->addChild(lightItem); PanelThemeItem *darkItem = new PanelThemeItem(); darkItem->text = darkPanelID;// ImpromptuModular.hpp darkItem->module = module; darkItem->theme = 1; menu->addChild(darkItem); menu->addChild(new MenuLabel());// empty line MenuLabel *settingsLabel = new MenuLabel(); settingsLabel->text = "Settings"; menu->addChild(settingsLabel); ResetOnRunItem *rorItem = MenuItem::create("Reset on run", CHECKMARK(module->resetOnRun)); rorItem->module = module; menu->addChild(rorItem); AutoseqItem *aseqItem = MenuItem::create("AutoSeq when writing via CV inputs", CHECKMARK(module->autoseq)); aseqItem->module = module; menu->addChild(aseqItem); SeqCVmethodItem *seqcvItem = MenuItem::create("Seq CV in: ", ""); seqcvItem->module = module; menu->addChild(seqcvItem); menu->addChild(new MenuLabel());// empty line MenuLabel *expansionLabel = new MenuLabel(); expansionLabel->text = "Expansion module"; menu->addChild(expansionLabel); ExpansionItem *expItem = MenuItem::create(expansionMenuLabel, CHECKMARK(module->expansion != 0)); expItem->module = module; menu->addChild(expItem); return menu; } void step() override { if(module->expansion != oldExpansion) { if (oldExpansion!= -1 && module->expansion == 0) {// if just removed expansion panel, disconnect wires to those jacks for (int i = 0; i < 6; i++) RACK_PLUGIN_UI_RACKWIDGET->wireContainer->removeAllWires(expPorts[i]); } oldExpansion = module->expansion; } box.size.x = panel->box.size.x - (1 - module->expansion) * expWidth; Widget::step(); } struct CKSSThreeInvNotify : CKSSThreeInvNoRandom {// Not randomizable CKSSThreeInvNotify() {} void onDragStart(EventDragStart &e) override { ToggleSwitch::onDragStart(e); ((GateSeq64*)(module))->stepConfigSync = 2;// signal a sync from switch so that steps get initialized } }; struct SequenceKnob : IMBigKnobInf { SequenceKnob() {}; void onMouseDown(EventMouseDown &e) override {// from ParamWidget.cpp GateSeq64* module = dynamic_cast(this->module); if (e.button == 1) { // same code structure below as in sequence knob in main step() bool editingSequence = module->isEditingSequence(); if (module->displayProbInfo != 0l && editingSequence) { //blinkNum = blinkNumInit; module->attributes[module->sequence][module->stepIndexEdit].setGatePVal(50); //displayProbInfo = (long) (displayProbInfoTime * sampleRate / displayRefreshStepSkips); } else if (module->editingPpqn != 0) { module->pulsesPerStep = 1; //editingPpqn = (long) (editingPpqnTime * sampleRate / displayRefreshStepSkips); } else if (module->displayState == GateSeq64::DISP_MODES) { if (editingSequence) { module->sequences[module->sequence].setRunMode(MODE_FWD); } else { module->runModeSong = MODE_FWD; } } else if (module->displayState == GateSeq64::DISP_LENGTH) { if (editingSequence) { module->sequences[module->sequence].setLength(16 * module->stepConfig); } else { module->phrases = 4; } } else { if (editingSequence) { //blinkNum = blinkNumInit; if (!module->inputs[GateSeq64::SEQCV_INPUT].active) { module->sequence = 0; } } else { if (module->editingPhraseSongRunning > 0l || !module->running) { module->phrase[module->phraseIndexEdit] = 0; // if (running) // editingPhraseSongRunning = (long) (editingPhraseSongRunningTime * sampleRate / displayRefreshStepSkips); } } } } ParamWidget::onMouseDown(e); } }; GateSeq64Widget(GateSeq64 *module) : ModuleWidget(module) { this->module = module; oldExpansion = -1; // Main panel from Inkscape panel = new DynamicSVGPanel(); panel->mode = &module->panelTheme; panel->expWidth = &expWidth; panel->addPanel(SVG::load(assetPlugin(plugin, "res/light/GateSeq64.svg"))); panel->addPanel(SVG::load(assetPlugin(plugin, "res/dark/GateSeq64_dark.svg"))); box.size = panel->box.size; box.size.x = box.size.x - (1 - module->expansion) * expWidth; addChild(panel); // Screws addChild(createDynamicScrew(Vec(15, 0), &module->panelTheme)); addChild(createDynamicScrew(Vec(15, 365), &module->panelTheme)); addChild(createDynamicScrew(Vec(panel->box.size.x-30, 0), &module->panelTheme)); addChild(createDynamicScrew(Vec(panel->box.size.x-30, 365), &module->panelTheme)); addChild(createDynamicScrew(Vec(panel->box.size.x-30-expWidth, 0), &module->panelTheme)); addChild(createDynamicScrew(Vec(panel->box.size.x-30-expWidth, 365), &module->panelTheme)); // ****** Top portion (LED button array and gate type LED buttons) ****** static const int rowRuler0 = 32; static const int spacingRows = 32; static const int colRulerSteps = 15; static const int spacingSteps = 20; static const int spacingSteps4 = 4; // Step LED buttons and GateMode lights for (int y = 0; y < 4; y++) { int posX = colRulerSteps; for (int x = 0; x < 16; x++) { addParam(createParam(Vec(posX, rowRuler0 + 8 + y * spacingRows - 4.4f), module, GateSeq64::STEP_PARAMS + y * 16 + x, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(posX + 4.4f, rowRuler0 + 8 + y * spacingRows), module, GateSeq64::STEP_LIGHTS + (y * 16 + x) * 3)); posX += spacingSteps; if ((x + 1) % 4 == 0) posX += spacingSteps4; } } // Gate type LED buttons (bottom left to top left to top right) static const int rowRulerG0 = 166; static const int rowSpacingG = 26; static const int colSpacingG = 56; static const int colRulerG0 = 15 + 28; addParam(createParam(Vec(colRulerG0, rowRulerG0 + rowSpacingG * 2 - 4.4f), module, GateSeq64::GMODE_PARAMS + 2, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerG0 + 4.4f, rowRulerG0 + rowSpacingG * 2), module, GateSeq64::GMODE_LIGHTS + 2 * 2)); addParam(createParam(Vec(colRulerG0, rowRulerG0 + rowSpacingG - 4.4f), module, GateSeq64::GMODE_PARAMS + 1, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerG0 + 4.4f, rowRulerG0 + rowSpacingG), module, GateSeq64::GMODE_LIGHTS + 1 * 2)); addParam(createParam(Vec(colRulerG0, rowRulerG0 - 4.4f), module, GateSeq64::GMODE_PARAMS + 0, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerG0 + 4.4f, rowRulerG0), module, GateSeq64::GMODE_LIGHTS + 0 * 2)); for (int x = 1; x < 6; x++) { addParam(createParam(Vec(colRulerG0 + colSpacingG * x, rowRulerG0 - 4.4f), module, GateSeq64::GMODE_PARAMS + 2 + x, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerG0 + colSpacingG * x + 4.4f, rowRulerG0), module, GateSeq64::GMODE_LIGHTS + (2 + x) * 2)); } // ****** 5x3 Main bottom half Control section ****** static const int colRulerC0 = 25; static const int colRulerC1 = 78; static const int colRulerC2 = 126; static const int colRulerC3 = 189; static const int colRulerC4 = 241; static const int rowRulerC0 = 206; static const int rowRulerSpacing = 58; static const int rowRulerC1 = rowRulerC0 + rowRulerSpacing; static const int rowRulerC2 = rowRulerC1 + rowRulerSpacing; // Clock input addInput(createDynamicPort(Vec(colRulerC0, rowRulerC1), Port::INPUT, module, GateSeq64::CLOCK_INPUT, &module->panelTheme)); // Reset CV addInput(createDynamicPort(Vec(colRulerC0, rowRulerC2), Port::INPUT, module, GateSeq64::RESET_INPUT, &module->panelTheme)); // Prob button addParam(createDynamicParam(Vec(colRulerC1 + offsetCKD6b, rowRulerC0 + offsetCKD6b), module, GateSeq64::PROB_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Reset LED bezel and light addParam(createParam(Vec(colRulerC1 + offsetLEDbezel, rowRulerC1 + offsetLEDbezel), module, GateSeq64::RESET_PARAM, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerC1 + offsetLEDbezel + offsetLEDbezelLight, rowRulerC1 + offsetLEDbezel + offsetLEDbezelLight), module, GateSeq64::RESET_LIGHT)); // Seq CV addInput(createDynamicPort(Vec(colRulerC1, rowRulerC2), Port::INPUT, module, GateSeq64::SEQCV_INPUT, &module->panelTheme)); // Sequence knob addParam(createDynamicParam(Vec(colRulerC2 + 1 + offsetIMBigKnob, rowRulerC0 + offsetIMBigKnob), module, GateSeq64::SEQUENCE_PARAM, -INFINITY, INFINITY, 0.0f, &module->panelTheme)); // Run LED bezel and light addParam(createParam(Vec(colRulerC2 + offsetLEDbezel, rowRulerC1 + offsetLEDbezel), module, GateSeq64::RUN_PARAM, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerC2 + offsetLEDbezel + offsetLEDbezelLight, rowRulerC1 + offsetLEDbezel + offsetLEDbezelLight), module, GateSeq64::RUN_LIGHT)); // Run CV addInput(createDynamicPort(Vec(colRulerC2, rowRulerC2), Port::INPUT, module, GateSeq64::RUNCV_INPUT, &module->panelTheme)); // Sequence display SequenceDisplayWidget *displaySequence = new SequenceDisplayWidget(); displaySequence->box.pos = Vec(colRulerC3 - 15, rowRulerC0 + vOffsetDisplay); displaySequence->box.size = Vec(55, 30);// 3 characters displaySequence->module = module; addChild(displaySequence); // Modes button addParam(createDynamicParam(Vec(colRulerC3 + offsetCKD6b, rowRulerC1 + offsetCKD6b), module, GateSeq64::MODES_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Copy/paste buttons addParam(createDynamicParam(Vec(colRulerC3 - 10, rowRulerC2 + offsetTL1105), module, GateSeq64::COPY_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(colRulerC3 + 20, rowRulerC2 + offsetTL1105), module, GateSeq64::PASTE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Seq/Song selector addParam(createParam(Vec(colRulerC4 + 2 + hOffsetCKSS, rowRulerC0 + vOffsetCKSS), module, GateSeq64::EDIT_PARAM, 0.0f, 1.0f, 1.0f)); // Config switch (3 position) addParam(createParam(Vec(colRulerC4 + 2 + hOffsetCKSS, rowRulerC1 - 2 + vOffsetCKSSThree), module, GateSeq64::CONFIG_PARAM, 0.0f, 2.0f, GateSeq64::CONFIG_PARAM_INIT_VALUE));// 0.0f is top position // Copy paste mode addParam(createParam(Vec(colRulerC4 + 2 + hOffsetCKSS, rowRulerC2 + vOffsetCKSSThree), module, GateSeq64::CPMODE_PARAM, 0.0f, 2.0f, 2.0f)); // Outputs for (int iSides = 0; iSides < 4; iSides++) addOutput(createDynamicPort(Vec(311, rowRulerC0 + iSides * 40), Port::OUTPUT, module, GateSeq64::GATE_OUTPUTS + iSides, &module->panelTheme)); // Expansion module static const int rowRulerExpTop = 60; static const int rowSpacingExp = 50; static const int colRulerExp = 497 - 30 - 90;// GS64 is (2+6)HP less than PS32 addInput(expPorts[0] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 0), Port::INPUT, module, GateSeq64::WRITE_INPUT, &module->panelTheme)); addInput(expPorts[1] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 1), Port::INPUT, module, GateSeq64::GATE_INPUT, &module->panelTheme)); addInput(expPorts[2] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 2), Port::INPUT, module, GateSeq64::PROB_INPUT, &module->panelTheme)); addInput(expPorts[3] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 3), Port::INPUT, module, GateSeq64::WRITE0_INPUT, &module->panelTheme)); addInput(expPorts[4] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 4), Port::INPUT, module, GateSeq64::WRITE1_INPUT, &module->panelTheme)); addInput(expPorts[5] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 5), Port::INPUT, module, GateSeq64::STEPL_INPUT, &module->panelTheme)); } }; } // namespace rack_plugin_ImpromptuModular using namespace rack_plugin_ImpromptuModular; RACK_PLUGIN_MODEL_INIT(ImpromptuModular, GateSeq64) { Model *modelGateSeq64 = Model::create("Impromptu Modular", "Gate-Seq-64", "SEQ - Gate-Seq-64", SEQUENCER_TAG); return modelGateSeq64; } /*CHANGE LOG 0.6.16: support for 32 sequences instead of 16 add step indication in song mode (white lights), and add right-click initialization on main knob 0.6.14: allow right click to turn steps off 0.6.13: fix run mode bug (history not reset when hard reset) fix initRun() timing bug when turn off-and-then-on running button (it was resetting ppqnCount) add two extra modes for Seq CV input (right-click menu): note-voltage-levels and trigger-increment 0.6.12: input refresh optimization add separate buttons for each advanced-gate (remove left right buttons) change behavior of write CV input in exp pannel (prob not reset when ProbIn unconnected, and gate not written when GateIn unconnected) 0.6.11: step optimization of lights refresh add RN2 run mode add step-left CV input in expansion panel implement copy-paste in song mode and change 4/ROW/ALL to 4/8/ALL implement cross paste trick for init and randomize seq/song add AutoSeq option when writing via CV inputs make song mode 64 sequences long 0.6.10: add advanced gate mode 0.6.9: add FW2, FW3 and FW4 run modes for sequences (but not for song) 0.6.7: add expansion panel with extra CVs for writing steps into the module allow full edit capabilities in song mode no reset on run by default, with switch added in context menu reset does not revert seq or song number to 1 0.6.6: config and knob bug fixes when loading patch 0.6.5: swap MODE/LEN so that length happens first (update manual) 0.6.4: initial release of GS64 */