// Copyright (c) 2026, Patrick Middendorf // MIT License // // SeqCtrl v3 – 8/16-Step-Sequencer fuer Phazerville 2.0 (Quadrants), // gesteuert per Akai MIDImix am USB-Host des O.R.N.8. // // MIDImix: // Fader 1..8 = Trigger/Gate pro Step (unten = aus, hoch = langes Gate) // Knob-Reihe 1 = CV A pro Step (Spalte = Step; in 16-Step-Modus geteilt mit Step 9..16) // Knob-Reihe 2 = CV B pro Step (Spalte = Step; in 16-Step-Modus geteilt mit Step 9..16) // Knob-Reihe 3 = Normal: CV C; In 16-Step-Modus: Trigger/Gate fuer Steps 9..16 (0 = aus) // MUTE 1..8 = Step stumm (sofort 0V Trigger, CV laeuft weiter; in 16-Step pro Spalte) // REC ARM 1..8 = Lauflicht (aktiver Step leuchtet); Klick = Step ueberspringen (Skip) // SOLO (gehalten) = + REC ARM n -> Laenge = n (in 16-Step-Modus: 8 + n) // BANK LEFT = Toggle Random-Modus (LED an = Random aktiv) // BANK RIGHT = Toggle 16-Step-Modus (LED an = 16 Steps aktiv) // // Eingaenge: TR1 = Clock, TR2 = Reset // Ausgaenge (4): OUT1 = Trigger/Gate, OUT2 = CV A, OUT3 = CV B, OUT4 = CV C (0V bei 16 Steps) #pragma once #include "../HemisphereApplet.h" class SeqCtrl : public HemisphereApplet { public: static constexpr int MAX_STEPS = 16; static constexpr int COLS = 8; static constexpr int LANES = 3; static SeqCtrl *active_instance; const char *applet_name() { return "SeqCtrl"; } const uint8_t *applet_icon() { return PhzIcons::trigseq; } void Start() { active_instance = this; length = 8; step = 0; skip = mute = 0; for (int s = 0; s < COLS; ++s) { fader[s] = 0; for (int l = 0; l < LANES; ++l) knob[l][s] = 0; } for (int l = 0; l < LANES; ++l) range[l] = 0; gateMode = 0; random_mode = false; sixteen_steps_mode = false; reset_armed = true; solo_held = false; led_dirty = true; } void Reset() { step = 0; reset_armed = true; led_dirty = true; } // ---------------------------------------------------------------- ISR void Controller() { active_instance = this; ++ticks_since_clock; if (Clock(1)) reset_armed = true; if (Clock(0)) { clock_interval = ticks_since_clock; ticks_since_clock = 0; Advance(); FireStep(); } // Gate-Ende if (gate_ticks > 0 && --gate_ticks == 0) OutN(0, 0); } // ---------------------------------------------------------------- UI / main loop void View() { active_instance = this; DrawSteps(); DrawParams(); if (led_dirty && mm_port >= 0) SendLeds(); } void OnButtonPress() { CursorToggle(); } void OnEncoderMove(int d) { if (!EditMode()) { MoveCursor(cursor, d, 6); return; } const int max_len = sixteen_steps_mode ? MAX_STEPS : COLS; switch (cursor) { case 0: length = constrain(length + d, 1, max_len); break; case 1: case 2: case 3: range[cursor - 1] = constrain(range[cursor - 1] + d, 0, 4); break; case 4: gateMode = constrain(gateMode + d, 0, 1); break; case 5: random_mode = !random_mode; led_dirty = true; break; case 6: sixteen_steps_mode = !sixteen_steps_mode; if (!sixteen_steps_mode && length > 8) length = 8; led_dirty = true; break; } } uint64_t OnDataRequest() { uint64_t data = 0; Pack(data, PackLocation{0, 5}, length - 1); Pack(data, PackLocation{5, 8}, skip); Pack(data, PackLocation{13, 8}, mute); Pack(data, PackLocation{21, 3}, range[0]); Pack(data, PackLocation{24, 3}, range[1]); Pack(data, PackLocation{27, 3}, range[2]); Pack(data, PackLocation{30, 2}, gateMode); Pack(data, PackLocation{32, 1}, random_mode ? 1 : 0); Pack(data, PackLocation{33, 1}, sixteen_steps_mode ? 1 : 0); return data; } void OnDataReceive(uint64_t data) { length = constrain((int)Unpack(data, PackLocation{0, 5}) + 1, 1, MAX_STEPS); skip = Unpack(data, PackLocation{5, 8}); mute = Unpack(data, PackLocation{13, 8}); range[0] = constrain((int)Unpack(data, PackLocation{21, 3}), 0, 4); range[1] = constrain((int)Unpack(data, PackLocation{24, 3}), 0, 4); range[2] = constrain((int)Unpack(data, PackLocation{27, 3}), 0, 4); gateMode = Unpack(data, PackLocation{30, 2}); random_mode = (Unpack(data, PackLocation{32, 1}) != 0); sixteen_steps_mode = (Unpack(data, PackLocation{33, 1}) != 0); if (!sixteen_steps_mode && length > 8) length = 8; led_dirty = true; } // ------------------------------------------------ Host MIDI Dispatch static void DispatchHostMIDI(int port, MIDIDevice_BigBuffer &d) { if (active_instance) { active_instance->PollDevice(port, d); } else { while (d.read()) {} } } void PollDevice(int port, MIDIDevice_BigBuffer &d) { const bool isMix = (d.idVendor() == 0x09E8); if (isMix && mm_port != port) { mm_port = port; led_dirty = true; } if (!isMix && mm_port == port) { mm_port = -1; return; } while (d.read()) { const uint8_t t = d.getType(); const uint8_t d1 = d.getData1(); const uint8_t d2 = d.getData2(); HandleMidimix(t, d1, d2); } if (led_dirty && mm_port >= 0) SendLeds(); } protected: void SetHelp() { help[HELP_DIGITAL1] = "Clock"; help[HELP_DIGITAL2] = "Reset"; help[HELP_CV1] = ""; help[HELP_CV2] = ""; help[HELP_OUT1] = "Trig"; help[HELP_OUT2] = "CV A"; help[HELP_EXTRA1] = "Out3/4 = CV B/C"; help[HELP_EXTRA2] = "MIDImix @ USB Host"; } private: uint8_t fader[COLS]; // 0..127 uint8_t knob[LANES][COLS]; // 0..127 uint8_t length; uint8_t step; uint8_t skip; // Bitmaske fuer Spalten 0..7 uint8_t mute; // Bitmaske fuer Spalten 0..7 uint8_t range[LANES]; // 0: 0..5V 1: 0..2V 2: +-5V 3: 0..1V 4: Bass (-2..+2V) uint8_t gateMode; // 0: Gate-Laenge per Fader, 1: fester 10ms Trigger bool random_mode; // BANK LEFT bool sixteen_steps_mode; // BANK RIGHT int cursor = 0; volatile bool reset_armed = true; uint32_t ticks_since_clock = 0; uint32_t clock_interval = 2000; uint32_t gate_ticks = 0; bool solo_held = false; bool led_dirty = true; int8_t mm_port = -1; static constexpr uint8_t FADER_CC[COLS] = {19, 23, 27, 31, 49, 53, 57, 61}; void OutN(int n, int value) { HS::frame.Out((DAC_CHANNEL)(io_offset + n), value); } void Advance() { const int max_len = sixteen_steps_mode ? constrain(length, 1, MAX_STEPS) : constrain(length, 1, COLS); if (reset_armed) { reset_armed = false; step = 0; } else if (random_mode) { // Zufaelligen nicht-uebersprungenen Schritt waehlen int tries = 16; int r = random(0, max_len); while (tries-- > 0 && ((skip >> (r % 8)) & 1)) { r = random(0, max_len); } step = r; } else { step = (step + 1) % max_len; for (int i = 0; i < max_len && ((skip >> (step % 8)) & 1); ++i) { step = (step + 1) % max_len; } } led_dirty = true; // Lauflicht aktualisieren } void FireStep() { const int col = step % 8; if ((skip >> col) & 1) return; // Geskippt // CV A & CV B (Spalte) OutN(1, ScaleCV(0, knob[0][col])); OutN(2, ScaleCV(1, knob[1][col])); // CV C (oder 0V bei 16-Step-Modus) if (sixteen_steps_mode) { OutN(3, 0); // CV C bleibt auf 0V, da Knobs 3 fuer Steps 9..16 Trigger sind } else { OutN(3, ScaleCV(2, knob[2][col])); } // Trigger / Gate Logik if ((mute >> col) & 1) { OutN(0, 0); gate_ticks = 0; return; } if (sixteen_steps_mode && step >= 8) { // Steps 9..16: Trigger per unterer Knopfreihe (Knob Reihe 3) const uint8_t trig_val = knob[2][col]; if (trig_val == 0) { OutN(0, 0); gate_ticks = 0; return; } OutN(0, HEMISPHERE_MAX_CV); gate_ticks = HEMISPHERE_CLOCK_TICKS * 10; // Fester 10ms Trigger } else { // Steps 1..8: Trigger per Fader const uint8_t f = fader[col]; if (f < 8) { OutN(0, 0); gate_ticks = 0; return; } OutN(0, HEMISPHERE_MAX_CV); if (gateMode == 1) { gate_ticks = HEMISPHERE_CLOCK_TICKS * 10; // ~10 ms Trigger } else { const uint32_t pct = 5 + (uint32_t)(f - 8) * 95 / 119; gate_ticks = max(HEMISPHERE_CLOCK_TICKS, clock_interval * pct / 100); if (pct >= 100) gate_ticks = clock_interval + 1; // Legato } } } int ScaleCV(int lane, uint8_t v) { switch (range[lane]) { case 1: return Proportion(v, 127, HEMISPHERE_MAX_CV * 2 / 5); // 0..2V case 2: return Proportion(v, 127, HEMISPHERE_MAX_CV * 2) - HEMISPHERE_MAX_CV; // +-5V case 3: return Proportion(v, 127, HEMISPHERE_MAX_CV / 5); // 0..1V (1 Okt.) case 4: return Proportion(v, 127, HEMISPHERE_MAX_CV * 4 / 5) - (HEMISPHERE_MAX_CV * 2 / 5); // Bass (-2..+2V) default: return Proportion(v, 127, HEMISPHERE_MAX_CV); // 0..5V } } void HandleMidimix(uint8_t type, uint8_t d1, uint8_t d2) { // ---------------- Fader / Knobs CC if (type == usbMIDI.ControlChange) { for (int s = 0; s < COLS; ++s) { if (d1 == FADER_CC[s]) { fader[s] = d2; // Sofort stumm bei Fader ganz unten, wenn aktiver Step in dieser Spalte if (step < 8 && (step % 8) == s && d2 < 8) { OutN(0, 0); gate_ticks = 0; } return; } if (d1 >= FADER_CC[s] - 3 && d1 < FADER_CC[s]) { const int lane = d1 - (FADER_CC[s] - 3); knob[lane][s] = d2; if (sixteen_steps_mode && step >= 8 && (step % 8) == s && lane == 2 && d2 == 0) { OutN(0, 0); gate_ticks = 0; } return; } } return; } const bool on = (type == usbMIDI.NoteOn && d2 > 0); const bool off = (type == usbMIDI.NoteOff || (type == usbMIDI.NoteOn && d2 == 0)); // SOLO-Taste (Note 27) if (d1 == 27) { solo_held = on; return; } // BANK LEFT (Note 25) -> Random Toggle if (d1 == 25) { if (on) { random_mode = !random_mode; led_dirty = true; } return; } // BANK RIGHT (Note 26) -> 16 Steps Toggle if (d1 == 26) { if (on) { sixteen_steps_mode = !sixteen_steps_mode; if (!sixteen_steps_mode && length > 8) length = 8; led_dirty = true; } return; } if (!on || off || d1 < 1 || d1 > 24) return; const int s = (d1 - 1) / 3; const int which = (d1 - 1) % 3; // 0 = MUTE, 2 = REC ARM if (which == 0) { // MUTE Toggle mute ^= (1 << s); if (((mute >> s) & 1) && (step % 8) == s) { OutN(0, 0); gate_ticks = 0; } led_dirty = true; } else if (which == 2) { // REC ARM if (solo_held) { if (sixteen_steps_mode) { length = 8 + s + 1; // 9..16 } else { length = s + 1; // 1..8 } } else { skip ^= (1 << s); } led_dirty = true; } } void SendLeds() { if (mm_port < 0) return; MIDIDevice_BigBuffer &d = usbHostMIDI[mm_port]; const int active_col = step % 8; for (int s = 0; s < COLS; ++s) { // MUTE LEDs d.sendNoteOn(1 + s * 3, ((mute >> s) & 1) ? 127 : 0, 1); // REC ARM LEDs: Lauflicht const bool is_chase = (s == active_col); const bool is_skip = ((skip >> s) & 1); d.sendNoteOn(3 + s * 3, is_chase ? 127 : (is_skip ? 20 : 0), 1); } // BANK LEFT (Random LED) d.sendNoteOn(25, random_mode ? 127 : 0, 1); // BANK RIGHT (16-Step LED) d.sendNoteOn(26, sixteen_steps_mode ? 127 : 0, 1); led_dirty = false; } // ------------------------------------------------ Display (64 x 50) void DrawSteps() { if (sixteen_steps_mode) { // 16 Steps in 2 Reihen (Steps 1..8 oben, Steps 9..16 unten) for (int s = 0; s < 8; ++s) { const int x = 1 + s * 8; const bool inLen1 = s < length; const bool inLen2 = (s + 8) < length; const int h1 = Proportion(fader[s], 127, 8); const int h2 = (knob[2][s] > 0) ? 6 : 0; // Reihe 1 (Steps 1..8) if (!inLen1) gfxDottedLine(x, 24, x + 5, 24); else { gfxFrame(x, 16, 6, 9); if (!((mute >> s) & 1) && h1 > 0) gfxRect(x + 1, 24 - h1, 4, h1); if (step == s) gfxInvert(x - 1, 15, 8, 11); } // Reihe 2 (Steps 9..16) if (!inLen2) gfxDottedLine(x, 34, x + 5, 34); else { gfxFrame(x, 26, 6, 9); if (!((mute >> s) & 1) && h2 > 0) gfxRect(x + 1, 34 - h2, 4, h2); if (step == (s + 8)) gfxInvert(x - 1, 25, 8, 11); } } } else { // 8 Steps normal for (int s = 0; s < COLS; ++s) { const int x = 1 + s * 8; const bool inLen = s < length; const int h = Proportion(fader[s], 127, 18); if (!inLen) { gfxDottedLine(x, 34, x + 5, 34); continue; } if ((skip >> s) & 1) { gfxLine(x, 16, x + 5, 34); continue; } gfxFrame(x, 16, 6, 19); if (!((mute >> s) & 1) && h > 0) gfxRect(x + 1, 34 - h, 4, h); if (s == step) gfxInvert(x - 1, 15, 8, 21); } for (int l = 0; l < LANES; ++l) { for (int s = 0; s < length; ++s) { gfxPixel(2 + s * 8 + Proportion(knob[l][s], 127, 4), 38 + l * 2); } } } } void DrawParams() { static const char *const RNG[] = {"5V", "2V", "+-5", "1V", "BS"}; gfxPrint(1, 52, "L"); gfxPrint(length); for (int l = 0; l < LANES; ++l) { gfxPrint(15 + l * 12, 52, RNG[range[l]]); } gfxPrint(52, 52, gateMode ? "T" : "G"); // Header Indicators if (random_mode) gfxPrint(1, 2, "RND"); if (sixteen_steps_mode) gfxPrint(22, 2, "16S"); gfxPrint(50, 2, mm_port >= 0 ? "MM" : "--"); const int cx[] = {0, 14, 26, 38, 51, 0, 21}; const int cw[] = {13, 11, 11, 11, 8, 18, 18}; const int cy[] = {51, 51, 51, 51, 51, 1, 1}; if (cursor < 5) { gfxCursor(cx[cursor], cy[cursor], cw[cursor]); } else { gfxCursor(cx[cursor], cy[cursor], cw[cursor]); } } }; SeqCtrl *SeqCtrl::active_instance = nullptr; constexpr uint8_t SeqCtrl::FADER_CC[];