//*********************************************************************************************** //Multi-phrase 32 step 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 inspired by the SA-100 Stepper Acid sequencer by Transistor Sounds Labs // //Acknowledgements: please see README.md //*********************************************************************************************** #include "PhraseSeqUtil.hpp" namespace rack_plugin_ImpromptuModular { struct PhraseSeq32 : Module { enum ParamIds { LEFT_PARAM, RIGHT_PARAM, RIGHT8_PARAM,// not used EDIT_PARAM, SEQUENCE_PARAM, RUN_PARAM, COPY_PARAM, PASTE_PARAM, RESET_PARAM, ENUMS(OCTAVE_PARAM, 7), GATE1_PARAM, GATE2_PARAM, SLIDE_BTN_PARAM, SLIDE_KNOB_PARAM, ATTACH_PARAM, AUTOSTEP_PARAM, ENUMS(KEY_PARAMS, 12), RUNMODE_PARAM, TRAN_ROT_PARAM, GATE1_KNOB_PARAM, GATE1_PROB_PARAM, TIE_PARAM,// Legato CPMODE_PARAM, ENUMS(STEP_PHRASE_PARAMS, 32), CONFIG_PARAM, // -- 0.6.11 ^^ KEYNOTE_PARAM, KEYGATE_PARAM, NUM_PARAMS }; enum InputIds { WRITE_INPUT, CV_INPUT, RESET_INPUT, CLOCK_INPUT, LEFTCV_INPUT, RIGHTCV_INPUT, RUNCV_INPUT, SEQCV_INPUT, // -- 0.6.4 ^^ MODECV_INPUT, GATE1CV_INPUT, GATE2CV_INPUT, TIEDCV_INPUT, SLIDECV_INPUT, NUM_INPUTS }; enum OutputIds { CVA_OUTPUT, GATE1A_OUTPUT, GATE2A_OUTPUT, CVB_OUTPUT, GATE1B_OUTPUT, GATE2B_OUTPUT, NUM_OUTPUTS }; enum LightIds { ENUMS(STEP_PHRASE_LIGHTS, 32 * 3),// room for GreenRedWhite ENUMS(OCTAVE_LIGHTS, 7),// octaves 1 to 7 ENUMS(KEY_LIGHTS, 12 * 2),// room for GreenRed RUN_LIGHT, RESET_LIGHT, ENUMS(GATE1_LIGHT, 2),// room for GreenRed ENUMS(GATE2_LIGHT, 2),// room for GreenRed SLIDE_LIGHT, ATTACH_LIGHT, ENUMS(GATE1_PROB_LIGHT, 2),// room for GreenRed TIE_LIGHT, KEYNOTE_LIGHT, ENUMS(KEYGATE_LIGHT, 2),// room for GreenRed NUM_LIGHTS }; // Constants enum DisplayStateIds {DISP_NORMAL, DISP_MODE, DISP_LENGTH, DISP_TRANSPOSE, DISP_ROTATE}; 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; bool autostepLen; bool holdTiedNotes; int seqCVmethod;// 0 is 0-10V, 1 is C4-G6, 2 is TrigIncr int pulsesPerStep;// 1 means normal gate mode, alt choices are 4, 6, 12, 24 PPS (Pulses per step) bool running; SeqAttributes sequences[32]; int runModeSong; int sequence; int phrase[32];// This is the song (series of phases; a phrase is a patten number) int phrases;//1 to 32 float cv[32][32];// [-3.0 : 3.917]. First index is patten number, 2nd index is step StepAttributes attributes[32][32];// First index is patten number, 2nd index is step (see enum AttributeBitMasks for details) bool resetOnRun; bool attached; // No need to save int stepIndexEdit; int stepIndexRun[2]; int phraseIndexEdit; int phraseIndexRun; long infoCopyPaste;// 0 when no info, positive downward step counter timer when copy, negative upward when paste unsigned long editingGate;// 0 when no edit gate, downward step counter timer when edit gate float editingGateCV;// no need to initialize, this is a companion to editingGate (output this only when editingGate > 0) int editingGateKeyLight;// no need to initialize, this is a companion to editingGate (use this only when editingGate > 0) int editingChannel;// 0 means channel A, 1 means channel B. no need to initialize, this is a companion to editingGate unsigned long editingType;// similar to editingGate, but just for showing remanent gate type (nothing played); uses editingGateKeyLight unsigned long stepIndexRunHistory; unsigned long phraseIndexRunHistory; int displayState; unsigned long slideStepsRemain[2];// 0 when no slide under way, downward step counter when sliding float slideCVdelta[2];// no need to initialize, this is a companion to slideStepsRemain float cvCPbuffer[32];// copy paste buffer for CVs StepAttributes attribCPbuffer[32]; SeqAttributes seqAttribCPbuffer; bool seqCopied; int phraseCPbuffer[32]; int countCP;// number of steps to paste (in case CPMODE_PARAM changes between copy and paste) int startCP; long clockIgnoreOnReset; unsigned long clockPeriod;// counts number of step() calls upward from last clock (reset after clock processed) long tiedWarning;// 0 when no warning, positive downward step counter timer when warning long attachedWarning;// 0 when no warning, positive downward step counter timer when warning int gate1Code[2]; int gate2Code[2]; bool attachedChanB; long revertDisplay; long editingGateLength;// 0 when no info, positive when gate1, negative when gate2 long lastGateEdit; long editingPpqn;// 0 when no info, positive downward step counter timer when editing ppqn int ppqnCount; int stepConfig; 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 resetTrigger; Trigger leftTrigger; Trigger rightTrigger; Trigger runningTrigger; Trigger clockTrigger; Trigger octTriggers[7]; Trigger octmTrigger; Trigger gate1Trigger; Trigger gate1ProbTrigger; Trigger gate2Trigger; Trigger slideTrigger; Trigger keyTriggers[12]; Trigger writeTrigger; Trigger attachedTrigger; Trigger copyTrigger; Trigger pasteTrigger; Trigger modeTrigger; Trigger rotateTrigger; Trigger transposeTrigger; Trigger tiedTrigger; Trigger stepTriggers[32]; Trigger keyNoteTrigger; Trigger keyGateTrigger; Trigger seqCVTrigger; HoldDetect modeHoldDetect; SeqAttributes seqAttribBuffer[32];// buffer from Json for thread safety inline bool isEditingSequence(void) {return params[EDIT_PARAM].value > 0.5f;} inline int getStepConfig(float paramValue) {// 1 = 2x16 = 1.0f, 2 = 1x32 = 0.0f return (paramValue > 0.5f) ? 1 : 2; } inline void fillStepIndexRunVector(int runMode, int len) { if (runMode != MODE_RN2) stepIndexRun[1] = stepIndexRun[0]; else stepIndexRun[1] = randomu32() % len; } inline void moveStepIndexEdit(int delta, bool _autostepLen) {// 2nd param is for rotate that uses this method also if (stepConfig == 2 || !_autostepLen) // 32 stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + delta, _autostepLen ? sequences[sequence].getLength() : 32); else {// here 1x16 and _autostepLen limit wanted if (stepIndexEdit < 16) { stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + delta, sequences[sequence].getLength()); if (stepIndexEdit == 0) stepIndexEdit = 16; } else stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + delta, sequences[sequence].getLength() + 16); } } PhraseSeq32() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { for (int i = 0; i < 32; i++) seqAttribBuffer[i].init(16, MODE_FWD); onReset(); } // widgets are not yet created when module is created (and when onReset() is called by constructor) // onReset() is also called when right-click initialization of module void onReset() override { stepConfig = getStepConfig(CONFIG_PARAM_INIT_VALUE); autoseq = false; autostepLen = false; holdTiedNotes = true; seqCVmethod = 0;// 0 is 0-10V, 1 is C4-G6, 2 is TrigIncr pulsesPerStep = 1; running = true; runModeSong = MODE_FWD; stepIndexEdit = 0; phraseIndexEdit = 0; sequence = 0; phrases = 4; for (int i = 0; i < 32; i++) { for (int s = 0; s < 32; s++) { cv[i][s] = 0.0f; attributes[i][s].init(); } sequences[i].init(16 * stepConfig, MODE_FWD); phrase[i] = 0; cvCPbuffer[i] = 0.0f; attribCPbuffer[i].init(); phraseCPbuffer[i] = 0; } initRun(); seqAttribCPbuffer.init(32, MODE_FWD); seqCopied = true; countCP = 32; startCP = 0; editingGate = 0ul; editingType = 0ul; infoCopyPaste = 0l; displayState = DISP_NORMAL; slideStepsRemain[0] = 0ul; slideStepsRemain[1] = 0ul; attached = false; clockPeriod = 0ul; tiedWarning = 0ul; attachedWarning = 0l; attachedChanB = false; revertDisplay = 0l; resetOnRun = false; editingGateLength = 0l; lastGateEdit = 1l; editingPpqn = 0l; clockIgnoreOnReset = (long) (clockIgnoreOnResetDuration * engineGetSampleRate()); } void onRandomize() override { if (isEditingSequence()) { for (int s = 0; s < 32; s++) { cv[sequence][s] = ((float)(randomu32() % 7)) + ((float)(randomu32() % 12)) / 12.0f - 3.0f; attributes[sequence][s].randomize(); // if (attributes[sequence][s].getTied()) { // activateTiedStep(sequence, s); // } } 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 < 2; i += stepConfig) { gate1Code[i] = calcGate1Code(attributes[seq][(i * 16) + stepIndexRun[i]], 0, pulsesPerStep, params[GATE1_KNOB_PARAM].value); gate2Code[i] = calcGate2Code(attributes[seq][(i * 16) + stepIndexRun[i]], 0, pulsesPerStep); } slideStepsRemain[0] = 0ul; slideStepsRemain[1] = 0ul; } 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)); // autostepLen json_object_set_new(rootJ, "autostepLen", json_boolean(autostepLen)); // autoseq json_object_set_new(rootJ, "autoseq", json_boolean(autoseq)); // holdTiedNotes json_object_set_new(rootJ, "holdTiedNotes", json_boolean(holdTiedNotes)); // 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 < 32; i++) json_array_insert_new(phraseJ, i, json_integer(phrase[i])); json_object_set_new(rootJ, "phrase", phraseJ); // phrases json_object_set_new(rootJ, "phrases", json_integer(phrases)); // CV json_t *cvJ = json_array(); for (int i = 0; i < 32; i++) for (int s = 0; s < 32; s++) { json_array_insert_new(cvJ, s + (i * 32), json_real(cv[i][s])); } json_object_set_new(rootJ, "cv", cvJ); // attributes json_t *attributesJ = json_array(); for (int i = 0; i < 32; i++) for (int s = 0; s < 32; s++) { json_array_insert_new(attributesJ, s + (i * 32), json_integer(attributes[i][s].getAttribute())); } json_object_set_new(rootJ, "attributes", attributesJ); // attached json_object_set_new(rootJ, "attached", json_boolean(attached)); // 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 < 32; 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); // autostepLen json_t *autostepLenJ = json_object_get(rootJ, "autostepLen"); if (autostepLenJ) autostepLen = json_is_true(autostepLenJ); // autoseq json_t *autoseqJ = json_object_get(rootJ, "autoseq"); if (autoseqJ) autoseq = json_is_true(autoseqJ); // holdTiedNotes json_t *holdTiedNotesJ = json_object_get(rootJ, "holdTiedNotes"); if (holdTiedNotesJ) holdTiedNotes = json_is_true(holdTiedNotesJ); else holdTiedNotes = false;// legacy // 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 < 32; i++) { json_t *sequencesArrayJ = json_array_get(sequencesJ, i); if (sequencesArrayJ) seqAttribBuffer[i].setSeqAttrib(json_integer_value(sequencesArrayJ)); } } else {// legacy int lengths[32];//1 to 32 int runModeSeq[32]; int transposeOffsets[32]; // runModeSeq json_t *runModeSeqJ = json_object_get(rootJ, "runModeSeq3"); if (runModeSeqJ) { for (int i = 0; i < 32; 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 < 32; i++)// bug, should be 32 but keep since legacy patches were written with 16 { 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 < 32; i++) { json_t *lengthsArrayJ = json_array_get(lengthsJ, i); if (lengthsArrayJ) lengths[i] = json_integer_value(lengthsArrayJ); } // transposeOffsets json_t *transposeOffsetsJ = json_object_get(rootJ, "transposeOffsets"); if (transposeOffsetsJ) { for (int i = 0; i < 32; i++) { json_t *transposeOffsetsArrayJ = json_array_get(transposeOffsetsJ, i); if (transposeOffsetsArrayJ) transposeOffsets[i] = json_integer_value(transposeOffsetsArrayJ); } } // now write into new object for (int i = 0; i < 32; i++) { seqAttribBuffer[i].init(lengths[i], runModeSeq[i]); seqAttribBuffer[i].setTranspose(transposeOffsets[i]); } } // runModeSong json_t *runModeSongJ = json_object_get(rootJ, "runModeSong3"); if (runModeSongJ) runModeSong = json_integer_value(runModeSongJ); else {// legacy json_t *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, "phrase"); if (phraseJ) for (int i = 0; i < 32; i++) { json_t *phraseArrayJ = json_array_get(phraseJ, i); if (phraseArrayJ) phrase[i] = json_integer_value(phraseArrayJ); } // phrases json_t *phrasesJ = json_object_get(rootJ, "phrases"); if (phrasesJ) phrases = json_integer_value(phrasesJ); // CV json_t *cvJ = json_object_get(rootJ, "cv"); if (cvJ) { for (int i = 0; i < 32; i++) for (int s = 0; s < 32; s++) { json_t *cvArrayJ = json_array_get(cvJ, s + (i * 32)); if (cvArrayJ) cv[i][s] = json_number_value(cvArrayJ); } } // attributes json_t *attributesJ = json_object_get(rootJ, "attributes"); if (attributesJ) { for (int i = 0; i < 32; i++) for (int s = 0; s < 32; s++) { json_t *attributesArrayJ = json_array_get(attributesJ, s + (i * 32)); if (attributesArrayJ) attributes[i][s].setAttribute((unsigned short)json_integer_value(attributesArrayJ)); } } // attached json_t *attachedJ = json_object_get(rootJ, "attached"); if (attachedJ) attached = json_is_true(attachedJ); // 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 rotateSeq(int seqNum, bool directionRight, int seqLength, bool chanB_16) { // set chanB_16 to false to rotate chan A in 2x16 config (length will be <= 16) or single chan in 1x32 config (length will be <= 32) // set chanB_16 to true to rotate chan B in 2x16 config (length must be <= 16) float rotCV; StepAttributes rotAttributes; int iStart = chanB_16 ? 16 : 0; int iEnd = iStart + seqLength - 1; int iRot = iStart; int iDelta = 1; if (directionRight) { iRot = iEnd; iDelta = -1; } rotCV = cv[seqNum][iRot]; rotAttributes = attributes[seqNum][iRot]; for ( ; ; iRot += iDelta) { if (iDelta == 1 && iRot >= iEnd) break; if (iDelta == -1 && iRot <= iStart) break; cv[seqNum][iRot] = cv[seqNum][iRot + iDelta]; attributes[seqNum][iRot] = attributes[seqNum][iRot + iDelta]; } cv[seqNum][iRot] = rotCV; attributes[seqNum][iRot] = rotAttributes; } void step() override { float sampleRate = engineGetSampleRate(); static const float gateTime = 0.4f;// seconds static const float revertDisplayTime = 0.7f;// seconds static const float warningTime = 0.7f;// seconds static const float holdDetectTime = 2.0f;// seconds static const float editGateLengthTime = 3.5f;// seconds //********** Buttons, knobs, switches and inputs ********** // Edit mode bool editingSequence = isEditingSequence();// true = editing sequence, false = editing song // Run 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); attachedChanB = stepIndexEdit >= 16; } displayState = DISP_NORMAL; } if ((lightRefreshCounter & userInputsStepSkipMask) == 0) { // 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 < 32; 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 < 32; i++) sequences[i].setLength(16 * stepConfig); } initRun(); attachedChanB = false; stepConfigSync = 0; } // Seq CV input if (inputs[SEQCV_INPUT].active) { if (seqCVmethod == 0) {// 0-10 V int newSeq = (int)( inputs[SEQCV_INPUT].value * (32.0f - 1.0f) / 10.0f + 0.5f ); sequence = clamp(newSeq, 0, 32 - 1); } else if (seqCVmethod == 1) {// C4-G6 int newSeq = (int)( (inputs[SEQCV_INPUT].value) * 12.0f + 0.5f ); sequence = clamp(newSeq, 0, 32 - 1); } else {// TrigIncr if (seqCVTrigger.process(inputs[SEQCV_INPUT].value)) sequence = clamp(sequence + 1, 0, 32 - 1); } } // Mode CV input if (inputs[MODECV_INPUT].active) { if (editingSequence) sequences[sequence].setRunMode((int) clamp( round(inputs[MODECV_INPUT].value * ((float)NUM_MODES - 1.0f) / 10.0f), 0.0f, (float)NUM_MODES - 1.0f )); } // Attach button if (attachedTrigger.process(params[ATTACH_PARAM].value)) { attached = !attached; if (running && attached && editingSequence && stepConfig == 1 ) attachedChanB = stepIndexEdit >= 16; displayState = DISP_NORMAL; } if (running && attached) { if (editingSequence) { if (attachedChanB && stepConfig == 1) stepIndexEdit = stepIndexRun[1] + 16; else stepIndexEdit = stepIndexRun[0] + 0; } else phraseIndexEdit = phraseIndexRun; } // Copy button if (copyTrigger.process(params[COPY_PARAM].value)) { startCP = editingSequence ? stepIndexEdit : phraseIndexEdit; countCP = 32; if (params[CPMODE_PARAM].value > 1.5f)// all startCP = 0; else if (params[CPMODE_PARAM].value < 0.5f)// 4 countCP = min(4, 32 - startCP); else// 8 countCP = min(8, 32 - startCP); if (editingSequence) { for (int i = 0, s = startCP; i < countCP; i++, s++) { cvCPbuffer[i] = cv[sequence][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_NORMAL; } // 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, 32 - 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++) { cv[sequence][s] = cvCPbuffer[i]; 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 < 32; s++) { //cv[sequence][s] = 0.0f; //attributes[sequence][s].init(); attributes[sequence][s].toggleGate1(); } sequences[sequence].setTranspose(0); sequences[sequence].setRotate(0); } else if (params[CPMODE_PARAM].value < 0.5f) {// 4 (randomize CVs) for (int s = 0; s < 32; s++) cv[sequence][s] = ((float)(randomu32() % 7)) + ((float)(randomu32() % 12)) / 12.0f - 3.0f; sequences[sequence].setTranspose(0); sequences[sequence].setRotate(0); } else {// 8 (randomize gate 1) for (int s = 0; s < 32; s++) if ( (randomu32() & 0x1) != 0) attributes[sequence][s].toggleGate1(); } startCP = 0; countCP = 32; infoCopyPaste *= 2l; } } else { if (!seqCopied) {// non-crossed paste (seq vs song) for (int i = 0, p = startCP; i < countCP; i++, p++) phrase[p] = phraseCPbuffer[i]; } else {// crossed paste to song (seq vs song) if (params[CPMODE_PARAM].value > 1.5f) { // ALL (init phrases) for (int p = 0; p < 32; p++) phrase[p] = 0; } else if (params[CPMODE_PARAM].value < 0.5f) {// 4 (phrases increase from 1 to 32) for (int p = 0; p < 32; p++) phrase[p] = p; } else {// 8 (randomize phrases) for (int p = 0; p < 32; p++) phrase[p] = randomu32() % 32; } startCP = 0; countCP = 32; infoCopyPaste *= 2l; } } displayState = DISP_NORMAL; } // Write input (must be before Left and Right in case route gate simultaneously to Right and Write for example) // (write must be to correct step) bool writeTrig = writeTrigger.process(inputs[WRITE_INPUT].value); if (writeTrig) { if (editingSequence) { if (!attributes[sequence][stepIndexEdit].getTied()) { cv[sequence][stepIndexEdit] = inputs[CV_INPUT].value; propagateCVtoTied(sequence, stepIndexEdit); } editingGate = (unsigned long) (gateTime * sampleRate / displayRefreshStepSkips); editingGateCV = inputs[CV_INPUT].value;// cv[sequence][stepIndexEdit]; editingGateKeyLight = -1; editingChannel = (stepIndexEdit >= 16 * stepConfig) ? 1 : 0; // Autostep (after grab all active inputs) if (params[AUTOSTEP_PARAM].value > 0.5f) { moveStepIndexEdit(1, autostepLen); if (stepIndexEdit == 0 && autoseq && !inputs[SEQCV_INPUT].active) sequence = moveIndex(sequence, sequence + 1, 32); } } displayState = DISP_NORMAL; } // Left and right CV inputs int delta = 0; if (leftTrigger.process(inputs[LEFTCV_INPUT].value)) { delta = -1; if (displayState != DISP_LENGTH) displayState = DISP_NORMAL; } if (rightTrigger.process(inputs[RIGHTCV_INPUT].value)) { delta = +1; if (displayState != DISP_LENGTH) displayState = DISP_NORMAL; } if (delta != 0) { if (displayState == DISP_LENGTH) { if (editingSequence) { sequences[sequence].setLength(clamp(sequences[sequence].getLength() + delta, 1, (16 * stepConfig))); sequences[sequence].setLength(((sequences[sequence].getLength() - 1) % (16 * stepConfig)) + 1); } else { phrases = clamp(phrases + delta, 1, 32); } } else { if (!running || !attached) {// don't move heads when attach and running if (editingSequence) { stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + delta, 32); if (!attributes[sequence][stepIndexEdit].getTied()) {// play if non-tied step if (!writeTrig) {// in case autostep when simultaneous writeCV and stepCV (keep what was done in Write Input block above) editingGate = (unsigned long) (gateTime * sampleRate / displayRefreshStepSkips); editingGateCV = cv[sequence][stepIndexEdit]; editingGateKeyLight = -1; editingChannel = (stepIndexEdit >= 16 * stepConfig) ? 1 : 0; } } } else { phraseIndexEdit = moveIndex(phraseIndexEdit, phraseIndexEdit + delta, 32); if (!running) phraseIndexRun = phraseIndexEdit; } } } } // Step button presses int stepPressed = -1; for (int i = 0; i < 32; i++) { if (stepTriggers[i].process(params[STEP_PHRASE_PARAMS + i].value)) stepPressed = i; } if (stepPressed != -1) { if (displayState == DISP_LENGTH) { if (editingSequence) sequences[sequence].setLength((stepPressed % (16 * stepConfig)) + 1); else phrases = stepPressed + 1; revertDisplay = (long) (revertDisplayTime * sampleRate / displayRefreshStepSkips); } else { if (!running || !attached) {// not running or detached if (editingSequence) { stepIndexEdit = stepPressed; if (!attributes[sequence][stepIndexEdit].getTied()) {// play if non-tied step editingGate = (unsigned long) (gateTime * sampleRate / displayRefreshStepSkips); editingGateCV = cv[sequence][stepIndexEdit]; editingGateKeyLight = -1; editingChannel = (stepIndexEdit >= 16 * stepConfig) ? 1 : 0; } } else { phraseIndexEdit = stepPressed; if (!running) phraseIndexRun = stepPressed; } } else {// attached and running if (attached) attachedWarning = (long) (warningTime * sampleRate / displayRefreshStepSkips); if (editingSequence) { if ((stepPressed < 16) && attachedChanB) attachedChanB = false; if ((stepPressed >= 16) && !attachedChanB) attachedChanB = true; } } displayState = DISP_NORMAL; } } // Mode/Length button if (modeTrigger.process(params[RUNMODE_PARAM].value)) { if (editingPpqn != 0l) editingPpqn = 0l; if (displayState == DISP_NORMAL || displayState == DISP_TRANSPOSE || displayState == DISP_ROTATE) displayState = DISP_LENGTH; else if (displayState == DISP_LENGTH) displayState = DISP_MODE; else displayState = DISP_NORMAL; modeHoldDetect.start((long) (holdDetectTime * sampleRate / displayRefreshStepSkips)); } // Transpose/Rotate button if (transposeTrigger.process(params[TRAN_ROT_PARAM].value)) { if (editingSequence) { if (displayState == DISP_NORMAL || displayState == DISP_MODE || displayState == DISP_LENGTH) { displayState = DISP_TRANSPOSE; } else if (displayState == DISP_TRANSPOSE) { displayState = DISP_ROTATE; } else displayState = DISP_NORMAL; } } // Sequence 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) // any changes in here should may also require right click behavior to be updated in the knob's onMouseDown() if (editingPpqn != 0) { pulsesPerStep = indexToPps(ppsToIndex(pulsesPerStep) + deltaKnob);// indexToPps() does clamping editingPpqn = (long) (editGateLengthTime * sampleRate / displayRefreshStepSkips); } else if (displayState == DISP_MODE) { if (editingSequence) { if (!inputs[MODECV_INPUT].active) { 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, 32); } } else if (displayState == DISP_TRANSPOSE) { if (editingSequence) { sequences[sequence].setTranspose(clamp(sequences[sequence].getTranspose() + deltaKnob, -99, 99)); float transposeOffsetCV = ((float)(deltaKnob))/12.0f;// Tranpose by deltaKnob number of semi-tones if (stepConfig == 1){ // 2x16 (transpose only the 16 steps corresponding to row where stepIndexEdit is located) int offset = stepIndexEdit < 16 ? 0 : 16; for (int s = offset; s < offset + 16; s++) cv[sequence][s] += transposeOffsetCV; } else { // 1x32 (transpose all 32 steps) for (int s = 0; s < 32; s++) cv[sequence][s] += transposeOffsetCV; } } } else if (displayState == DISP_ROTATE) { if (editingSequence) { int slength = sequences[sequence].getLength(); bool rotChanB = (stepConfig == 1 && stepIndexEdit >= 16); sequences[sequence].setRotate(clamp(sequences[sequence].getRotate() + deltaKnob, -99, 99)); if (deltaKnob > 0 && deltaKnob < 201) {// Rotate right, 201 is safety for (int i = deltaKnob; i > 0; i--) { rotateSeq(sequence, true, slength, rotChanB); if ((stepConfig == 2 || !rotChanB ) && (stepIndexEdit < slength)) stepIndexEdit = (stepIndexEdit + 1) % slength; if (rotChanB && (stepIndexEdit < (slength + 16)) && (stepIndexEdit >= 16)) stepIndexEdit = ((stepIndexEdit - 16 + 1) % slength) + 16; } } if (deltaKnob < 0 && deltaKnob > -201) {// Rotate left, 201 is safety for (int i = deltaKnob; i < 0; i++) { rotateSeq(sequence, false, slength, rotChanB); if ((stepConfig == 2 || !rotChanB ) && (stepIndexEdit < slength)) stepIndexEdit = (stepIndexEdit + (stepConfig * 16 - 1) ) % slength; if (rotChanB && (stepIndexEdit < (slength + 16)) && (stepIndexEdit >= 16)) stepIndexEdit = ((stepIndexEdit - 1 ) % slength) + 16; } } } } else {// DISP_NORMAL if (editingSequence) { if (!inputs[SEQCV_INPUT].active) { sequence = clamp(sequence + deltaKnob, 0, 32 - 1); } } else { if (!attached || (attached && !running)) { int newPhrase = phrase[phraseIndexEdit] + deltaKnob; if (newPhrase < 0) newPhrase += (1 - newPhrase / 32) * 32;// newPhrase now positive newPhrase = newPhrase % 32; phrase[phraseIndexEdit] = newPhrase; } else attachedWarning = (long) (warningTime * sampleRate / displayRefreshStepSkips); } } } sequenceKnob = newSequenceKnob; } // Octave buttons int newOct = -1; for (int i = 0; i < 7; i++) { if (octTriggers[i].process(params[OCTAVE_PARAM + i].value)) { newOct = 6 - i; displayState = DISP_NORMAL; } } if (newOct >= 0 && newOct <= 6) { if (editingSequence) { if (attributes[sequence][stepIndexEdit].getTied()) tiedWarning = (long) (warningTime * sampleRate / displayRefreshStepSkips); else { cv[sequence][stepIndexEdit] = applyNewOct(cv[sequence][stepIndexEdit], newOct); propagateCVtoTied(sequence, stepIndexEdit); editingGate = (unsigned long) (gateTime * sampleRate / displayRefreshStepSkips); editingGateCV = cv[sequence][stepIndexEdit]; editingGateKeyLight = -1; editingChannel = (stepIndexEdit >= 16 * stepConfig) ? 1 : 0; } } } // Keyboard buttons for (int i = 0; i < 12; i++) { if (keyTriggers[i].process(params[KEY_PARAMS + i].value)) { if (editingSequence) { if (editingGateLength != 0l) { int newMode = keyIndexToGateMode(i, pulsesPerStep); if (newMode != -1) { editingPpqn = 0l; attributes[sequence][stepIndexEdit].setGateMode(newMode, editingGateLength > 0l); if (params[KEY_PARAMS + i].value > 1.5f) {// if right-click stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + 1, 32); editingType = (unsigned long) (gateTime * sampleRate / displayRefreshStepSkips); editingGateKeyLight = i; if (windowIsModPressed()) attributes[sequence][stepIndexEdit].setGateMode(newMode, editingGateLength > 0l); } } else editingPpqn = (long) (editGateLengthTime * sampleRate / displayRefreshStepSkips); } else if (attributes[sequence][stepIndexEdit].getTied()) { if (params[KEY_PARAMS + i].value > 1.5f)// if right-click stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + 1, 32); else tiedWarning = (long) (warningTime * sampleRate / displayRefreshStepSkips); } else { float newCV = floor(cv[sequence][stepIndexEdit]) + ((float) i) / 12.0f; cv[sequence][stepIndexEdit] = newCV; propagateCVtoTied(sequence, stepIndexEdit); editingGate = (unsigned long) (gateTime * sampleRate / displayRefreshStepSkips); editingGateCV = cv[sequence][stepIndexEdit]; editingGateKeyLight = -1; editingChannel = (stepIndexEdit >= 16 * stepConfig) ? 1 : 0; if (params[KEY_PARAMS + i].value > 1.5f) {// if right-click stepIndexEdit = moveIndex(stepIndexEdit, stepIndexEdit + 1, 32); editingGateKeyLight = i; if (windowIsModPressed()) cv[sequence][stepIndexEdit] = newCV; } } } displayState = DISP_NORMAL; } } // Keyboard mode (note or gate type) if (keyNoteTrigger.process(params[KEYNOTE_PARAM].value)) { editingGateLength = 0l; } if (keyGateTrigger.process(params[KEYGATE_PARAM].value)) { if (editingGateLength == 0l) { editingGateLength = lastGateEdit; } else { editingGateLength *= -1l; lastGateEdit = editingGateLength; } } // Gate1, Gate1Prob, Gate2, Slide and Tied buttons if (gate1Trigger.process(params[GATE1_PARAM].value + inputs[GATE1CV_INPUT].value)) { if (editingSequence) { attributes[sequence][stepIndexEdit].toggleGate1(); } displayState = DISP_NORMAL; } if (gate1ProbTrigger.process(params[GATE1_PROB_PARAM].value)) { if (editingSequence) { if (attributes[sequence][stepIndexEdit].getTied()) tiedWarning = (long) (warningTime * sampleRate / displayRefreshStepSkips); else attributes[sequence][stepIndexEdit].toggleGate1P(); } displayState = DISP_NORMAL; } if (gate2Trigger.process(params[GATE2_PARAM].value + inputs[GATE2CV_INPUT].value)) { if (editingSequence) { attributes[sequence][stepIndexEdit].toggleGate2(); } displayState = DISP_NORMAL; } if (slideTrigger.process(params[SLIDE_BTN_PARAM].value + inputs[SLIDECV_INPUT].value)) { if (editingSequence) { if (attributes[sequence][stepIndexEdit].getTied()) tiedWarning = (long) (warningTime * sampleRate / displayRefreshStepSkips); else attributes[sequence][stepIndexEdit].toggleSlide(); } displayState = DISP_NORMAL; } if (tiedTrigger.process(params[TIE_PARAM].value + inputs[TIEDCV_INPUT].value)) { if (editingSequence) { if (attributes[sequence][stepIndexEdit].getTied()) { deactivateTiedStep(sequence, stepIndexEdit); } else { activateTiedStep(sequence, stepIndexEdit); } } displayState = DISP_NORMAL; } }// 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) { float slideFromCV[2] = {0.0f, 0.0f}; if (editingSequence) { for (int i = 0; i < 2; i += stepConfig) slideFromCV[i] = cv[sequence][(i * 16) + stepIndexRun[i]]; moveIndexRunMode(&stepIndexRun[0], sequences[sequence].getLength(), sequences[sequence].getRunMode(), &stepIndexRunHistory); } else { for (int i = 0; i < 2; i += stepConfig) slideFromCV[i] = cv[phrase[phraseIndexRun]][(i * 16) + stepIndexRun[i]]; 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()); // Slide for (int i = 0; i < 2; i += stepConfig) { if (attributes[newSeq][(i * 16) + stepIndexRun[i]].getSlide()) { slideStepsRemain[i] = (unsigned long) (((float)clockPeriod * pulsesPerStep) * params[SLIDE_KNOB_PARAM].value / 2.0f); if (slideStepsRemain[i] != 0ul) { float slideToCV = cv[newSeq][(i * 16) + stepIndexRun[i]]; slideCVdelta[i] = (slideToCV - slideFromCV[i])/(float)slideStepsRemain[i]; } } else slideStepsRemain[i] = 0ul; } } else { if (!editingSequence) newSeq = phrase[phraseIndexRun]; } for (int i = 0; i < 2; i += stepConfig) { if (gate1Code[i] != -1 || ppqnCount == 0) gate1Code[i] = calcGate1Code(attributes[newSeq][(i * 16) + stepIndexRun[i]], ppqnCount, pulsesPerStep, params[GATE1_KNOB_PARAM].value); gate2Code[i] = calcGate2Code(attributes[newSeq][(i * 16) + stepIndexRun[i]], ppqnCount, pulsesPerStep); } clockPeriod = 0ul; } clockPeriod++; } // Reset if (resetTrigger.process(inputs[RESET_INPUT].value + params[RESET_PARAM].value)) { initRun();// must be before SEQCV_INPUT below resetLight = 1.0f; displayState = DISP_NORMAL; clockIgnoreOnReset = (long) (clockIgnoreOnResetDuration * sampleRate); clockTrigger.reset(); if (inputs[SEQCV_INPUT].active && seqCVmethod == 2) sequence = 0; } //********** Outputs and lights ********** // CV and gates outputs int seq = editingSequence ? (sequence) : (running ? phrase[phraseIndexRun] : phrase[phraseIndexEdit]); int step0 = editingSequence ? (running ? stepIndexRun[0] : stepIndexEdit) : (stepIndexRun[0]); if (running) { bool muteGate1A = !editingSequence && ((params[GATE1_PARAM].value + inputs[GATE1CV_INPUT].value) > 0.5f);// live mute bool muteGate1B = muteGate1A; bool muteGate2A = !editingSequence && ((params[GATE2_PARAM].value + inputs[GATE2CV_INPUT].value) > 0.5f);// live mute bool muteGate2B = muteGate2A; if (!attached && (muteGate1B || muteGate2B) && stepConfig == 1) { // if not attached in 2x16, mute only the channel where phraseIndexEdit is located (hack since phraseIndexEdit's row has no relation to channels) if (phraseIndexEdit < 16) { muteGate1B = false; muteGate2B = false; } else { muteGate1A = false; muteGate2A = false; } } float slideOffset[2]; for (int i = 0; i < 2; i += stepConfig) slideOffset[i] = (slideStepsRemain[i] > 0ul ? (slideCVdelta[i] * (float)slideStepsRemain[i]) : 0.0f); outputs[CVA_OUTPUT].value = cv[seq][step0] - slideOffset[0]; bool retriggingOnReset = (clockIgnoreOnReset != 0l && retrigGatesOnReset); outputs[GATE1A_OUTPUT].value = (calcGate(gate1Code[0], clockTrigger, clockPeriod, sampleRate) && !muteGate1A && !retriggingOnReset) ? 10.0f : 0.0f; outputs[GATE2A_OUTPUT].value = (calcGate(gate2Code[0], clockTrigger, clockPeriod, sampleRate) && !muteGate2A && !retriggingOnReset) ? 10.0f : 0.0f; if (stepConfig == 1) { int step1 = editingSequence ? (running ? stepIndexRun[1] : stepIndexEdit) : (stepIndexRun[1]); outputs[CVB_OUTPUT].value = cv[seq][16 + step1] - slideOffset[1]; outputs[GATE1B_OUTPUT].value = (calcGate(gate1Code[1], clockTrigger, clockPeriod, sampleRate) && !muteGate1B && !retriggingOnReset) ? 10.0f : 0.0f; outputs[GATE2B_OUTPUT].value = (calcGate(gate2Code[1], clockTrigger, clockPeriod, sampleRate) && !muteGate2B && !retriggingOnReset) ? 10.0f : 0.0f; } else { outputs[CVB_OUTPUT].value = 0.0f; outputs[GATE1B_OUTPUT].value = 0.0f; outputs[GATE2B_OUTPUT].value = 0.0f; } } else {// not running if (stepConfig > 1) {// 1x32 outputs[CVA_OUTPUT].value = (editingGate > 0ul) ? editingGateCV : cv[seq][step0]; outputs[GATE1A_OUTPUT].value = (editingGate > 0ul) ? 10.0f : 0.0f; outputs[GATE2A_OUTPUT].value = (editingGate > 0ul) ? 10.0f : 0.0f; outputs[CVB_OUTPUT].value = 0.0f; outputs[GATE1B_OUTPUT].value = 0.0f; outputs[GATE2B_OUTPUT].value = 0.0f; } else {// 2x16 float cvA = (step0 >= 16 ? cv[seq][step0 - 16] : cv[seq][step0]); float cvB = (step0 >= 16 ? cv[seq][step0] : cv[seq][step0 + 16]); if (editingChannel == 0) { outputs[CVA_OUTPUT].value = (editingGate > 0ul) ? editingGateCV : cvA; outputs[GATE1A_OUTPUT].value = (editingGate > 0ul) ? 10.0f : 0.0f; outputs[GATE2A_OUTPUT].value = (editingGate > 0ul) ? 10.0f : 0.0f; outputs[CVB_OUTPUT].value = cvB; outputs[GATE1B_OUTPUT].value = 0.0f; outputs[GATE2B_OUTPUT].value = 0.0f; } else { outputs[CVA_OUTPUT].value = cvA; outputs[GATE1A_OUTPUT].value = 0.0f; outputs[GATE2A_OUTPUT].value = 0.0f; outputs[CVB_OUTPUT].value = (editingGate > 0ul) ? editingGateCV : cvB; outputs[GATE1B_OUTPUT].value = (editingGate > 0ul) ? 10.0f : 0.0f; outputs[GATE2B_OUTPUT].value = (editingGate > 0ul) ? 10.0f : 0.0f; } } } for (int i = 0; i < 2; i++) if (slideStepsRemain[i] > 0ul) slideStepsRemain[i]--; lightRefreshCounter++; if (lightRefreshCounter >= displayRefreshStepSkips) { lightRefreshCounter = 0; // Step/phrase lights for (int i = 0; i < 32; i++) { int col = (stepConfig == 1 ? (i & 0xF) : i);//i % (16 * stepConfig);// optimized float red = 0.0f; float green = 0.0f; float white = 0.0f; if (infoCopyPaste != 0l) { if (i >= startCP && i < (startCP + countCP)) green = 0.5f; } else if (displayState == DISP_LENGTH) { if (editingSequence) { if (col < (sequences[sequence].getLength() - 1)) green = 0.1f; else if (col == (sequences[sequence].getLength() - 1)) green = 1.0f; } else { if (i < phrases - 1) green = 0.1f; else green = (i == phrases - 1) ? 1.0f : 0.0f; } } else if (displayState == DISP_TRANSPOSE) { red = 0.5f; } else if (displayState == DISP_ROTATE) { red = (i == stepIndexEdit ? 1.0f : (col < sequences[sequence].getLength() ? 0.2f : 0.0f)); } else {// normal led display (i.e. not length) int row = i >> (3 + stepConfig);//i / (16 * stepConfig);// optimized (not equivalent code, but in this case has same effect) // Run cursor (green) if (editingSequence) green = ((running && (col == stepIndexRun[row])) ? 1.0f : 0.0f); else { green = ((running && (i == phraseIndexRun)) ? 1.0f : 0.0f); green += ((running && (col == stepIndexRun[row]) && i != phraseIndexEdit) ? 0.1f : 0.0f); green = clamp(green, 0.0f, 1.0f); } // Edit cursor (red) if (editingSequence) red = (i == stepIndexEdit ? 1.0f : 0.0f); else red = (i == phraseIndexEdit ? 1.0f : 0.0f); bool gate = false; if (editingSequence) gate = attributes[sequence][i].getGate1(); else if (!editingSequence && attached) gate = attributes[phrase[phraseIndexRun]][i].getGate1(); white = ((green == 0.0f && red == 0.0f && gate && displayState != DISP_MODE) ? 0.04f : 0.0f); if (editingSequence && white != 0.0f) { green = 0.02f; white = 0.0f; } //if (white != 0.0f && attributes[sequence][i].getGate1P()) white = 0.01f; } setGreenRed(STEP_PHRASE_LIGHTS + i * 3, green, red); lights[STEP_PHRASE_LIGHTS + i * 3 + 2].value = white; } // Octave lights float octCV = 0.0f; if (editingSequence) octCV = cv[sequence][stepIndexEdit]; else octCV = cv[phrase[phraseIndexEdit]][stepIndexRun[0]]; int octLightIndex = (int) floor(octCV + 3.0f); for (int i = 0; i < 7; i++) { if (!editingSequence && (!attached || !running || (stepConfig == 1)))// no oct lights when song mode and either (detached [1] or stopped [2] or 2x16config [3]) // [1] makes no sense, can't mod steps and stepping though seq that may not be playing // [2] CV is set to 0V when not running and in song mode, so cv[][] makes no sense to display // [3] makes no sense, which sequence would be displayed, top or bottom row! lights[OCTAVE_LIGHTS + i].value = 0.0f; else { if (tiedWarning > 0l) { bool warningFlashState = calcWarningFlash(tiedWarning, (long) (warningTime * sampleRate / displayRefreshStepSkips)); lights[OCTAVE_LIGHTS + i].value = (warningFlashState && (i == (6 - octLightIndex))) ? 1.0f : 0.0f; } else lights[OCTAVE_LIGHTS + i].value = (i == (6 - octLightIndex) ? 1.0f : 0.0f); } } // Keyboard lights (can only show channel A when running attached in 1x16 mode, does not pose problem for all other situations) float cvValOffset; if (editingSequence) cvValOffset = cv[sequence][stepIndexEdit] + 10.0f;//to properly handle negative note voltages else cvValOffset = cv[phrase[phraseIndexEdit]][stepIndexRun[0]] + 10.0f;//to properly handle negative note voltages int keyLightIndex = clamp( (int)((cvValOffset-floor(cvValOffset)) * 12.0f + 0.5f), 0, 11); if (editingPpqn != 0) { for (int i = 0; i < 12; i++) { if (keyIndexToGateMode(i, pulsesPerStep) != -1) { setGreenRed(KEY_LIGHTS + i * 2, 1.0f, 1.0f); } else { setGreenRed(KEY_LIGHTS + i * 2, 0.0f, 0.0f); } } } else if (editingGateLength != 0l && editingSequence) { int modeLightIndex = gateModeToKeyLightIndex(attributes[sequence][stepIndexEdit], editingGateLength > 0l); for (int i = 0; i < 12; i++) { float green = editingGateLength > 0l ? 1.0f : 0.2f; float red = editingGateLength > 0l ? 0.2f : 1.0f; if (editingType > 0ul) { if (i == editingGateKeyLight) { float dimMult = ((float) editingType / (float)(gateTime * sampleRate / displayRefreshStepSkips)); setGreenRed(KEY_LIGHTS + i * 2, green * dimMult, red * dimMult); } else setGreenRed(KEY_LIGHTS + i * 2, 0.0f, 0.0f); } else { if (i == modeLightIndex) { setGreenRed(KEY_LIGHTS + i * 2, green, red); } else { // show dim note if gatetype is different than note setGreenRed(KEY_LIGHTS + i * 2, 0.0f, (i == keyLightIndex ? 0.1f : 0.0f)); } } } } else { for (int i = 0; i < 12; i++) { lights[KEY_LIGHTS + i * 2 + 0].value = 0.0f; if (!editingSequence && (!attached || !running || (stepConfig == 1)))// no oct lights when song mode and either (detached [1] or stopped [2] or 2x16config [3]) // [1] makes no sense, can't mod steps and stepping though seq that may not be playing // [2] CV is set to 0V when not running and in song mode, so cv[][] makes no sense to display // [3] makes no sense, which sequence would be displayed, top or bottom row! lights[KEY_LIGHTS + i * 2 + 1].value = 0.0f; else { if (tiedWarning > 0l) { bool warningFlashState = calcWarningFlash(tiedWarning, (long) (warningTime * sampleRate / displayRefreshStepSkips)); lights[KEY_LIGHTS + i * 2 + 1].value = (warningFlashState && i == keyLightIndex) ? 1.0f : 0.0f; } else { if (editingGate > 0ul && editingGateKeyLight != -1) lights[KEY_LIGHTS + i * 2 + 1].value = (i == editingGateKeyLight ? ((float) editingGate / (float)(gateTime * sampleRate / displayRefreshStepSkips)) : 0.0f); else lights[KEY_LIGHTS + i * 2 + 1].value = (i == keyLightIndex ? 1.0f : 0.0f); } } } } // Key mode light (note or gate type) lights[KEYNOTE_LIGHT].value = editingGateLength == 0l ? 10.0f : 0.0f; if (editingGateLength == 0l) setGreenRed(KEYGATE_LIGHT, 0.0f, 0.0f); else if (editingGateLength > 0l) setGreenRed(KEYGATE_LIGHT, 1.0f, 0.2f); else setGreenRed(KEYGATE_LIGHT, 0.2f, 1.0f); // Gate1, Gate1Prob, Gate2, Slide and Tied lights (can only show channel A when running attached in 1x32 mode, does not pose problem for all other situations) if (!editingSequence && (!attached || !running || (stepConfig == 1))) {// no oct lights when song mode and either (detached [1] or stopped [2] or 2x16config [3]) // [1] makes no sense, can't mod steps and stepping though seq that may not be playing // [2] CV is set to 0V when not running and in song mode, so cv[][] makes no sense to display // [3] makes no sense, which sequence would be displayed, top or bottom row! setGateLight(false, GATE1_LIGHT); setGateLight(false, GATE2_LIGHT); setGreenRed(GATE1_PROB_LIGHT, 0.0f, 0.0f); lights[SLIDE_LIGHT].value = 0.0f; lights[TIE_LIGHT].value = 0.0f; } else { StepAttributes attributesVal = attributes[sequence][stepIndexEdit]; if (!editingSequence) attributesVal = attributes[phrase[phraseIndexEdit]][stepIndexRun[0]]; // setGateLight(attributesVal.getGate1(), GATE1_LIGHT); setGateLight(attributesVal.getGate2(), GATE2_LIGHT); setGreenRed(GATE1_PROB_LIGHT, attributesVal.getGate1P() ? 1.0f : 0.0f, attributesVal.getGate1P() ? 1.0f : 0.0f); lights[SLIDE_LIGHT].value = attributesVal.getSlide() ? 1.0f : 0.0f; if (tiedWarning > 0l) { bool warningFlashState = calcWarningFlash(tiedWarning, (long) (warningTime * sampleRate / displayRefreshStepSkips)); lights[TIE_LIGHT].value = (warningFlashState) ? 1.0f : 0.0f; } else lights[TIE_LIGHT].value = attributesVal.getTied() ? 1.0f : 0.0f; } // Attach light if (attachedWarning > 0l) { bool warningFlashState = calcWarningFlash(attachedWarning, (long) (warningTime * sampleRate / displayRefreshStepSkips)); lights[ATTACH_LIGHT].value = (warningFlashState) ? 1.0f : 0.0f; } else lights[ATTACH_LIGHT].value = (attached ? 1.0f : 0.0f); // Reset light lights[RESET_LIGHT].value = resetLight; resetLight -= (resetLight / lightLambda) * engineGetSampleTime() * displayRefreshStepSkips; // Run light lights[RUN_LIGHT].value = running ? 1.0f : 0.0f; if (editingGate > 0ul) editingGate--; if (editingType > 0ul) editingType--; if (infoCopyPaste != 0l) { if (infoCopyPaste > 0l) infoCopyPaste --; if (infoCopyPaste < 0l) infoCopyPaste ++; } if (editingPpqn > 0l) editingPpqn--; if (tiedWarning > 0l) tiedWarning--; if (attachedWarning > 0l) attachedWarning--; if (modeHoldDetect.process(params[RUNMODE_PARAM].value)) { displayState = DISP_NORMAL; editingPpqn = (long) (editGateLengthTime * sampleRate / displayRefreshStepSkips); } if (revertDisplay > 0l) { if (revertDisplay == 1) displayState = DISP_NORMAL; revertDisplay--; } }// 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 propagateCVtoTied(int seqn, int stepn) { for (int i = stepn + 1; i < 32; i++) { if (!attributes[seqn][i].getTied()) break; cv[seqn][i] = cv[seqn][i - 1]; } } void activateTiedStep(int seqn, int stepn) { attributes[seqn][stepn].setTied(true); if (stepn > 0) propagateCVtoTied(seqn, stepn - 1); if (holdTiedNotes) {// new method attributes[seqn][stepn].setGate1(true); for (int i = max(stepn, 1); i < 32 && attributes[seqn][i].getTied(); i++) { attributes[seqn][i].setGate1Mode(attributes[seqn][i - 1].getGate1Mode()); attributes[seqn][i - 1].setGate1Mode(5); attributes[seqn][i - 1].setGate1(true); } } else {// old method if (stepn > 0) { attributes[seqn][stepn] = attributes[seqn][stepn - 1]; attributes[seqn][stepn].setTied(true); } } } void deactivateTiedStep(int seqn, int stepn) { attributes[seqn][stepn].setTied(false); if (holdTiedNotes) {// new method int lastGateType = attributes[seqn][stepn].getGate1Mode(); for (int i = stepn + 1; i < 32 && attributes[seqn][i].getTied(); i++) lastGateType = attributes[seqn][i].getGate1Mode(); if (stepn > 0) attributes[seqn][stepn - 1].setGate1Mode(lastGateType); } //else old method, nothing to do } inline void setGateLight(bool gateOn, int lightIndex) { if (!gateOn) { lights[lightIndex + 0].value = 0.0f; lights[lightIndex + 1].value = 0.0f; } else if (editingGateLength == 0l) { lights[lightIndex + 0].value = 0.0f; lights[lightIndex + 1].value = 1.0f; } else { lights[lightIndex + 0].value = lightIndex == GATE1_LIGHT ? 1.0f : 0.2f; lights[lightIndex + 1].value = lightIndex == GATE1_LIGHT ? 0.2f : 1.0f; } } }; struct PhraseSeq32Widget : ModuleWidget { PhraseSeq32 *module; DynamicSVGPanel *panel; int oldExpansion; int expWidth = 60; IMPort* expPorts[5]; struct SequenceDisplayWidget : TransparentWidget { PhraseSeq32 *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) snprintf(displayStr, 4, "CPY"); else { float cpMode = module->params[PhraseSeq32::CPMODE_PARAM].value; if (editingSequence && !module->seqCopied) {// cross paste to seq if (cpMode > 1.5f)// All = toggle gate 1 snprintf(displayStr, 4, "TG1"); else if (cpMode < 0.5f)// 4 = random CV snprintf(displayStr, 4, "RCV"); else// 8 = random gate 1 snprintf(displayStr, 4, "RG1"); } 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->editingPpqn != 0ul) { snprintf(displayStr, 4, "x%2u", (unsigned) module->pulsesPerStep); } else if (module->displayState == PhraseSeq32::DISP_MODE) { if (editingSequence) runModeToStr(module->sequences[module->sequence].getRunMode()); else runModeToStr(module->runModeSong); } else if (module->displayState == PhraseSeq32::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 == PhraseSeq32::DISP_TRANSPOSE) { snprintf(displayStr, 4, "+%2u", (unsigned) abs(module->sequences[module->sequence].getTranspose())); if (module->sequences[module->sequence].getTranspose() < 0) displayStr[0] = '-'; } else if (module->displayState == PhraseSeq32::DISP_ROTATE) { snprintf(displayStr, 4, ")%2u", (unsigned) abs(module->sequences[module->sequence].getRotate())); if (module->sequences[module->sequence].getRotate() < 0) displayStr[0] = '('; } else {// DISP_NORMAL snprintf(displayStr, 4, " %2u", (unsigned) (editingSequence ? module->sequence : module->phrase[module->phraseIndexEdit]) + 1 ); } nvgText(vg, textPos.x, textPos.y, displayStr, NULL); } }; struct PanelThemeItem : MenuItem { PhraseSeq32 *module; int theme; void onAction(EventAction &e) override { module->panelTheme = theme; } void step() override { rightText = (module->panelTheme == theme) ? "✔" : ""; } }; struct ExpansionItem : MenuItem { PhraseSeq32 *module; void onAction(EventAction &e) override { module->expansion = module->expansion == 1 ? 0 : 1; } }; struct ResetOnRunItem : MenuItem { PhraseSeq32 *module; void onAction(EventAction &e) override { module->resetOnRun = !module->resetOnRun; } }; struct AutoStepLenItem : MenuItem { PhraseSeq32 *module; void onAction(EventAction &e) override { module->autostepLen = !module->autostepLen; } }; struct AutoseqItem : MenuItem { PhraseSeq32 *module; void onAction(EventAction &e) override { module->autoseq = !module->autoseq; } }; struct HoldTiedItem : MenuItem { PhraseSeq32 *module; void onAction(EventAction &e) override { module->holdTiedNotes = !module->holdTiedNotes; } }; struct SeqCVmethodItem : MenuItem { PhraseSeq32 *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); PhraseSeq32 *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); AutoStepLenItem *astlItem = MenuItem::create("AutoStep write bounded by seq length", CHECKMARK(module->autostepLen)); astlItem->module = module; menu->addChild(astlItem); AutoseqItem *aseqItem = MenuItem::create("AutoSeq when writing via CV inputs", CHECKMARK(module->autoseq)); aseqItem->module = module; menu->addChild(aseqItem); HoldTiedItem *holdItem = MenuItem::create("Hold tied notes", CHECKMARK(module->holdTiedNotes)); holdItem->module = module; menu->addChild(holdItem); 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 < 5; 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 CKSSNotify : CKSS {// Not randomizable CKSSNotify() {} void randomize() override {} void onDragStart(EventDragStart &e) override { ToggleSwitch::onDragStart(e); ((PhraseSeq32*)(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 PhraseSeq32* module = dynamic_cast(this->module); if (e.button == 1) { // same code structure below as in sequence knob in main step() if (module->editingPpqn != 0) { module->pulsesPerStep = 1; //editingPpqn = (long) (editGateLengthTime * sampleRate / displayRefreshStepSkips); } else if (module->displayState == PhraseSeq32::DISP_MODE) { if (module->isEditingSequence()) { if (!module->inputs[PhraseSeq32::MODECV_INPUT].active) { module->sequences[module->sequence].setRunMode(MODE_FWD); } } else { module->runModeSong = MODE_FWD; } } else if (module->displayState == PhraseSeq32::DISP_LENGTH) { if (module->isEditingSequence()) { module->sequences[module->sequence].setLength(16 * module->stepConfig); } else { module->phrases = 4; } } else if (module->displayState == PhraseSeq32::DISP_TRANSPOSE) { // nothing } else if (module->displayState == PhraseSeq32::DISP_ROTATE) { // nothing } else {// DISP_NORMAL if (module->isEditingSequence()) { if (!module->inputs[PhraseSeq32::SEQCV_INPUT].active) { module->sequence = 0; } } else { module->phrase[module->phraseIndexEdit] = 0; } } } ParamWidget::onMouseDown(e); } }; // void onHoverKey(EventHoverKey &e) override {// https://www.glfw.org/docs/latest/group__keys.html // PhraseSeq32* module = dynamic_cast(this->module); // if (e.key == GLFW_KEY_SPACE) { // if (module->isEditingSequence()) { // module->attributes[module->sequence][module->stepIndexEdit].toggleGate1(); // } // e.consumed = true; // } // else // ModuleWidget::onHoverKey(e); // } PhraseSeq32Widget(PhraseSeq32 *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/PhraseSeq32.svg"))); panel->addPanel(SVG::load(assetPlugin(plugin, "res/dark/PhraseSeq32_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 row ****** static const int rowRulerT0 = 48; static const int columnRulerT0 = 18;//30;// Step/Phase LED buttons static const int columnRulerT3 = 377;// Attach static const int columnRulerT4 = 430;// Config // Step/Phrase LED buttons int posX = columnRulerT0; static int spacingSteps = 20; static int spacingSteps4 = 4; for (int x = 0; x < 16; x++) { // First row addParam(createParam(Vec(posX, rowRulerT0 - 10 + 3 - 4.4f), module, PhraseSeq32::STEP_PHRASE_PARAMS + x, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(posX + 4.4f, rowRulerT0 - 10 + 3), module, PhraseSeq32::STEP_PHRASE_LIGHTS + (x * 3))); // Second row addParam(createParam(Vec(posX, rowRulerT0 + 10 + 3 - 4.4f), module, PhraseSeq32::STEP_PHRASE_PARAMS + x + 16, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(posX + 4.4f, rowRulerT0 + 10 + 3), module, PhraseSeq32::STEP_PHRASE_LIGHTS + ((x + 16) * 3))); // step position to next location and handle groups of four posX += spacingSteps; if ((x + 1) % 4 == 0) posX += spacingSteps4; } // Attach button and light addParam(createDynamicParam(Vec(columnRulerT3 - 4, rowRulerT0 - 6 + 2 + offsetTL1105), module, PhraseSeq32::ATTACH_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addChild(createLight>(Vec(columnRulerT3 + 12 + offsetMediumLight, rowRulerT0 - 6 + offsetMediumLight), module, PhraseSeq32::ATTACH_LIGHT)); // Config switch addParam(createParam(Vec(columnRulerT4 + hOffsetCKSS + 1, rowRulerT0 - 6 + vOffsetCKSS), module, PhraseSeq32::CONFIG_PARAM, 0.0f, 1.0f, PhraseSeq32::CONFIG_PARAM_INIT_VALUE)); // ****** Octave and keyboard area ****** // Octave LED buttons static const float octLightsIntY = 20.0f; for (int i = 0; i < 7; i++) { addParam(createParam(Vec(15 + 3, 82 + 24 + i * octLightsIntY- 4.4f), module, PhraseSeq32::OCTAVE_PARAM + i, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(15 + 3 + 4.4f, 82 + 24 + i * octLightsIntY), module, PhraseSeq32::OCTAVE_LIGHTS + i)); } // Keys and Key lights static const int keyNudgeX = 7; static const int KeyBlackY = 103; static const int KeyWhiteY = 141; static const int offsetKeyLEDx = 6; static const int offsetKeyLEDy = 16; // Black keys and lights addParam(createParam( Vec(65+keyNudgeX, KeyBlackY), module, PhraseSeq32::KEY_PARAMS + 1, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(65+keyNudgeX+offsetKeyLEDx, KeyBlackY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 1 * 2)); addParam(createParam( Vec(93+keyNudgeX, KeyBlackY), module, PhraseSeq32::KEY_PARAMS + 3, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(93+keyNudgeX+offsetKeyLEDx, KeyBlackY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 3 * 2)); addParam(createParam( Vec(150+keyNudgeX, KeyBlackY), module, PhraseSeq32::KEY_PARAMS + 6, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(150+keyNudgeX+offsetKeyLEDx, KeyBlackY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 6 * 2)); addParam(createParam( Vec(178+keyNudgeX, KeyBlackY), module, PhraseSeq32::KEY_PARAMS + 8, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(178+keyNudgeX+offsetKeyLEDx, KeyBlackY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 8 * 2)); addParam(createParam( Vec(206+keyNudgeX, KeyBlackY), module, PhraseSeq32::KEY_PARAMS + 10, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(206+keyNudgeX+offsetKeyLEDx, KeyBlackY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 10 * 2)); // White keys and lights addParam(createParam( Vec(51+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 0, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(51+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 0 * 2)); addParam(createParam( Vec(79+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 2, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(79+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 2 * 2)); addParam(createParam( Vec(107+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 4, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(107+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 4 * 2)); addParam(createParam( Vec(136+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 5, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(136+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 5 * 2)); addParam(createParam( Vec(164+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 7, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(164+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 7 * 2)); addParam(createParam( Vec(192+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 9, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(192+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 9 * 2)); addParam(createParam( Vec(220+keyNudgeX, KeyWhiteY), module, PhraseSeq32::KEY_PARAMS + 11, 0.0, 1.0, 0.0)); addChild(createLight>(Vec(220+keyNudgeX+offsetKeyLEDx, KeyWhiteY+offsetKeyLEDy), module, PhraseSeq32::KEY_LIGHTS + 11 * 2)); // Key mode LED buttons static const int colRulerKM = 267; addParam(createParam(Vec(colRulerKM, KeyBlackY + offsetKeyLEDy - 4.4f), module, PhraseSeq32::KEYNOTE_PARAM, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerKM + 4.4f, KeyBlackY + offsetKeyLEDy), module, PhraseSeq32::KEYNOTE_LIGHT)); addParam(createParam(Vec(colRulerKM, KeyWhiteY + offsetKeyLEDy - 4.4f), module, PhraseSeq32::KEYGATE_PARAM, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(colRulerKM + 4.4f, KeyWhiteY + offsetKeyLEDy), module, PhraseSeq32::KEYGATE_LIGHT)); // ****** Right side control area ****** static const int rowRulerMK0 = 101;// Edit mode row static const int rowRulerMK1 = rowRulerMK0 + 56; // Run row static const int rowRulerMK2 = rowRulerMK1 + 54; // Copy-paste Tran/rot row static const int columnRulerMK0 = 307;// Edit mode column static const int columnRulerMK2 = columnRulerT4;// Mode/Len column static const int columnRulerMK1 = 366;// Display column // Edit mode switch addParam(createParam(Vec(columnRulerMK0 + 2 + hOffsetCKSS, rowRulerMK0 + vOffsetCKSS), module, PhraseSeq32::EDIT_PARAM, 0.0f, 1.0f, 1.0f)); // Sequence display SequenceDisplayWidget *displaySequence = new SequenceDisplayWidget(); displaySequence->box.pos = Vec(columnRulerMK1-15, rowRulerMK0 + 3 + vOffsetDisplay); displaySequence->box.size = Vec(55, 30);// 3 characters displaySequence->module = module; addChild(displaySequence); // Len/mode button addParam(createDynamicParam(Vec(columnRulerMK2 + offsetCKD6b, rowRulerMK0 + 0 + offsetCKD6b), module, PhraseSeq32::RUNMODE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Autostep addParam(createParam(Vec(columnRulerMK0 + 2 + hOffsetCKSS, rowRulerMK1 + 7 + vOffsetCKSS), module, PhraseSeq32::AUTOSTEP_PARAM, 0.0f, 1.0f, 1.0f)); // Sequence knob addParam(createDynamicParam(Vec(columnRulerMK1 + 1 + offsetIMBigKnob, rowRulerMK0 + 55 + offsetIMBigKnob), module, PhraseSeq32::SEQUENCE_PARAM, -INFINITY, INFINITY, 0.0f, &module->panelTheme)); // Transpose/rotate button addParam(createDynamicParam(Vec(columnRulerMK2 + offsetCKD6b, rowRulerMK1 + 4 + offsetCKD6b), module, PhraseSeq32::TRAN_ROT_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Reset LED bezel and light addParam(createParam(Vec(columnRulerMK0 - 43 + offsetLEDbezel, rowRulerMK2 + 5 + offsetLEDbezel), module, PhraseSeq32::RESET_PARAM, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(columnRulerMK0 - 43 + offsetLEDbezel + offsetLEDbezelLight, rowRulerMK2 + 5 + offsetLEDbezel + offsetLEDbezelLight), module, PhraseSeq32::RESET_LIGHT)); // Run LED bezel and light addParam(createParam(Vec(columnRulerMK0 + 3 + offsetLEDbezel, rowRulerMK2 + 5 + offsetLEDbezel), module, PhraseSeq32::RUN_PARAM, 0.0f, 1.0f, 0.0f)); addChild(createLight>(Vec(columnRulerMK0 + 3 + offsetLEDbezel + offsetLEDbezelLight, rowRulerMK2 + 5 + offsetLEDbezel + offsetLEDbezelLight), module, PhraseSeq32::RUN_LIGHT)); // Copy/paste buttons addParam(createDynamicParam(Vec(columnRulerMK1 - 10, rowRulerMK2 + 5 + offsetTL1105), module, PhraseSeq32::COPY_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); addParam(createDynamicParam(Vec(columnRulerMK1 + 20, rowRulerMK2 + 5 + offsetTL1105), module, PhraseSeq32::PASTE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Copy-paste mode switch (3 position) addParam(createParam(Vec(columnRulerMK2 + hOffsetCKSS + 1, rowRulerMK2 - 3 + vOffsetCKSSThree), module, PhraseSeq32::CPMODE_PARAM, 0.0f, 2.0f, 2.0f)); // 0.0f is top position // ****** Gate and slide section ****** static const int rowRulerMB0 = 214; static const int columnRulerMBspacing = 70; static const int columnRulerMB2 = 130;// Gate2 static const int columnRulerMB1 = columnRulerMB2 - columnRulerMBspacing;// Gate1 static const int columnRulerMB3 = columnRulerMB2 + columnRulerMBspacing;// Tie static const int posLEDvsButton = + 25; // Gate 1 light and button addChild(createLight>(Vec(columnRulerMB1 + posLEDvsButton + offsetMediumLight, rowRulerMB0 + 4 + offsetMediumLight), module, PhraseSeq32::GATE1_LIGHT)); addParam(createDynamicParam(Vec(columnRulerMB1 + offsetCKD6b, rowRulerMB0 + 4 + offsetCKD6b), module, PhraseSeq32::GATE1_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Gate 2 light and button addChild(createLight>(Vec(columnRulerMB2 + posLEDvsButton + offsetMediumLight, rowRulerMB0 + 4 + offsetMediumLight), module, PhraseSeq32::GATE2_LIGHT)); addParam(createDynamicParam(Vec(columnRulerMB2 + offsetCKD6b, rowRulerMB0 + 4 + offsetCKD6b), module, PhraseSeq32::GATE2_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Tie light and button addChild(createLight>(Vec(columnRulerMB3 + posLEDvsButton + offsetMediumLight, rowRulerMB0 + 4 + offsetMediumLight), module, PhraseSeq32::TIE_LIGHT)); addParam(createDynamicParam(Vec(columnRulerMB3 + offsetCKD6b, rowRulerMB0 + 4 + offsetCKD6b), module, PhraseSeq32::TIE_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // ****** Bottom two rows ****** static const int inputJackSpacingX = 54; static const int outputJackSpacingX = 45; static const int rowRulerB0 = 323; static const int rowRulerB1 = 269; static const int columnRulerB0 = 22; static const int columnRulerB1 = columnRulerB0 + inputJackSpacingX; static const int columnRulerB2 = columnRulerB1 + inputJackSpacingX; static const int columnRulerB3 = columnRulerB2 + inputJackSpacingX; static const int columnRulerB4 = columnRulerB3 + inputJackSpacingX; static const int columnRulerB8 = columnRulerMK2 + 1; static const int columnRulerB7 = columnRulerB8 - outputJackSpacingX; static const int columnRulerB6 = columnRulerB7 - outputJackSpacingX; static const int columnRulerB5 = columnRulerB6 - outputJackSpacingX - 4;// clock and reset // Gate 1 probability light and button addChild(createLight>(Vec(columnRulerB0 + posLEDvsButton + offsetMediumLight, rowRulerB1 + offsetMediumLight), module, PhraseSeq32::GATE1_PROB_LIGHT)); addParam(createDynamicParam(Vec(columnRulerB0 + offsetCKD6b, rowRulerB1 + offsetCKD6b), module, PhraseSeq32::GATE1_PROB_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Gate 1 probability knob addParam(createDynamicParam(Vec(columnRulerB1 + offsetIMSmallKnob, rowRulerB1 + offsetIMSmallKnob), module, PhraseSeq32::GATE1_KNOB_PARAM, 0.0f, 1.0f, 1.0f, &module->panelTheme)); // Slide light and button addChild(createLight>(Vec(columnRulerB2 + posLEDvsButton + offsetMediumLight, rowRulerB1 + offsetMediumLight), module, PhraseSeq32::SLIDE_LIGHT)); addParam(createDynamicParam(Vec(columnRulerB2 + offsetCKD6b, rowRulerB1 + offsetCKD6b), module, PhraseSeq32::SLIDE_BTN_PARAM, 0.0f, 1.0f, 0.0f, &module->panelTheme)); // Slide knob addParam(createDynamicParam(Vec(columnRulerB3 + offsetIMSmallKnob, rowRulerB1 + offsetIMSmallKnob), module, PhraseSeq32::SLIDE_KNOB_PARAM, 0.0f, 2.0f, 0.2f, &module->panelTheme)); // CV in addInput(createDynamicPort(Vec(columnRulerB4, rowRulerB1), Port::INPUT, module, PhraseSeq32::CV_INPUT, &module->panelTheme)); // Clock input addInput(createDynamicPort(Vec(columnRulerB5, rowRulerB1), Port::INPUT, module, PhraseSeq32::CLOCK_INPUT, &module->panelTheme)); // Channel A outputs addOutput(createDynamicPort(Vec(columnRulerB6, rowRulerB1), Port::OUTPUT, module, PhraseSeq32::CVA_OUTPUT, &module->panelTheme)); addOutput(createDynamicPort(Vec(columnRulerB7, rowRulerB1), Port::OUTPUT, module, PhraseSeq32::GATE1A_OUTPUT, &module->panelTheme)); addOutput(createDynamicPort(Vec(columnRulerB8, rowRulerB1), Port::OUTPUT, module, PhraseSeq32::GATE2A_OUTPUT, &module->panelTheme)); // CV control Inputs addInput(createDynamicPort(Vec(columnRulerB0, rowRulerB0), Port::INPUT, module, PhraseSeq32::LEFTCV_INPUT, &module->panelTheme)); addInput(createDynamicPort(Vec(columnRulerB1, rowRulerB0), Port::INPUT, module, PhraseSeq32::RIGHTCV_INPUT, &module->panelTheme)); addInput(createDynamicPort(Vec(columnRulerB2, rowRulerB0), Port::INPUT, module, PhraseSeq32::SEQCV_INPUT, &module->panelTheme)); addInput(createDynamicPort(Vec(columnRulerB3, rowRulerB0), Port::INPUT, module, PhraseSeq32::RUNCV_INPUT, &module->panelTheme)); addInput(createDynamicPort(Vec(columnRulerB4, rowRulerB0), Port::INPUT, module, PhraseSeq32::WRITE_INPUT, &module->panelTheme)); // Reset input addInput(createDynamicPort(Vec(columnRulerB5, rowRulerB0), Port::INPUT, module, PhraseSeq32::RESET_INPUT, &module->panelTheme)); // Channel B outputs addOutput(createDynamicPort(Vec(columnRulerB6, rowRulerB0), Port::OUTPUT, module, PhraseSeq32::CVB_OUTPUT, &module->panelTheme)); addOutput(createDynamicPort(Vec(columnRulerB7, rowRulerB0), Port::OUTPUT, module, PhraseSeq32::GATE1B_OUTPUT, &module->panelTheme)); addOutput(createDynamicPort(Vec(columnRulerB8, rowRulerB0), Port::OUTPUT, module, PhraseSeq32::GATE2B_OUTPUT, &module->panelTheme)); // Expansion module static const int rowRulerExpTop = 65; static const int rowSpacingExp = 60; static const int colRulerExp = 497; addInput(expPorts[0] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 0), Port::INPUT, module, PhraseSeq32::GATE1CV_INPUT, &module->panelTheme)); addInput(expPorts[1] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 1), Port::INPUT, module, PhraseSeq32::GATE2CV_INPUT, &module->panelTheme)); addInput(expPorts[2] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 2), Port::INPUT, module, PhraseSeq32::TIEDCV_INPUT, &module->panelTheme)); addInput(expPorts[3] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 3), Port::INPUT, module, PhraseSeq32::SLIDECV_INPUT, &module->panelTheme)); addInput(expPorts[4] = createDynamicPort(Vec(colRulerExp, rowRulerExpTop + rowSpacingExp * 4), Port::INPUT, module, PhraseSeq32::MODECV_INPUT, &module->panelTheme)); } }; } // namespace rack_plugin_ImpromptuModular using namespace rack_plugin_ImpromptuModular; RACK_PLUGIN_MODEL_INIT(ImpromptuModular, PhraseSeq32) { Model *modelPhraseSeq32 = Model::create("Impromptu Modular", "Phrase-Seq-32", "SEQ - Phrase-Seq-32", SEQUENCER_TAG); return modelPhraseSeq32; } /*CHANGE LOG 0.6.16: add gate status feedback in steps (white lights) 0.6.15: add right-click menu option to bound AutoStep writes by sequence lengths 0.6.14: rotate offsets are now persistent and stored in the sequencer allow ctrl-right-click of notes to copy note/gate-type over to next step (not just move to next step) 0.6.13: fix run mode bug (history not reset when hard reset) fix slide bug when reset happens during a slide and run stays on fix transposeOffset not initialized bug add live mute on Gate1 and Gate2 buttons in song mode fix initRun() timing bug when turn off-and-then-on running button (it was resetting ppqnCount) allow pulsesPerStep setting of 1 and all even values from 2 to 24, and allow all gate types that work in these add two extra modes for Seq CV input (right-click menu): note-voltage-levels and trigger-increment fix tied bug that prevented correct tied propagation when editing beyond sequence length less than 16 implement held tied notes option clear all attributes (gates, gatep, tied, slide) when cross-paste to seq ALL (CVs not affected) implement right-click initialization on main knob 0.6.12: input refresh optimization add buttons for note vs advanced-gate selection (remove timeout method) transposition amount stays persistent and is saved (reset to 0 on module init or paste ALL) 0.6.11: step optimization of lights refresh change behavior of extra CV inputs (Gate1, Gate2, Tied, Slide), such that they act when triggered and not when write add RN2 run mode implement copy-paste in song mode implement cross paste trick for init and randomize seq/song add AutoSeq option when writing via CV inputs 0.6.10: add advanced gate mode unlock gates when tied (turn off when press tied, but allow to be turned back on) 0.6.9: add FW2, FW3 and FW4 run modes for sequences (but not for song) right click on notes now does same as left click but with autostep 0.6.8: allow rollover when selecting sequences in a song phrase (easier access to higher numbered seqs) 0.6.7: allow full edit capabilities in Seq and song mode no reset on run by default, with switch added in context menu reset does not revert seq or song number to 1 gate 2 is off by default fix emitted monitoring gates to depend on gate states instead of always triggering 0.6.6: config and knob bug fixes when loading patch 0.6.5: paste 4, 8 doesn't loop over to overwrite first steps small adjustements to gates and CVs used in monitoring write operations add GATE1, GATE2, TIED, SLIDE CV inputs add MODE CV input (affects only selected sequence and in Seq mode) add expansion panel option swap MODE/LEN so that length happens first (update manual) 0.6.4: initial release of PS32 */