From af3cfe961451a7732e769ec89057f6a324bded32 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:06:53 -0700 Subject: [PATCH] initial commit of new GridSeq firmware --- firmware/GridSeq/GridSeq.ino | 167 +++++++++++++++++++++++++++++++++++ firmware/GridSeq/app_state.h | 38 ++++++++ firmware/GridSeq/channel.h | 129 +++++++++++++++++++++++++++ firmware/GridSeq/display.h | 93 +++++++++++++++++++ firmware/GridSeq/euclidean.h | 100 +++++++++++++++++++++ firmware/GridSeq/step.h | 0 6 files changed, 527 insertions(+) create mode 100644 firmware/GridSeq/GridSeq.ino create mode 100644 firmware/GridSeq/app_state.h create mode 100644 firmware/GridSeq/channel.h create mode 100644 firmware/GridSeq/display.h create mode 100644 firmware/GridSeq/euclidean.h create mode 100644 firmware/GridSeq/step.h diff --git a/firmware/GridSeq/GridSeq.ino b/firmware/GridSeq/GridSeq.ino new file mode 100644 index 0000000..2dddb6c --- /dev/null +++ b/firmware/GridSeq/GridSeq.ino @@ -0,0 +1,167 @@ +/** + * @file GridSeq.ino + * @author Adam Wonak (https://github.com/awonak/) + * @brief Grid based step sequencer firmware for Gravity by Sitka Instruments. + * @version v1.0.0 - August 2025 awonak + * @date 2025-08-12 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + * Grid based step sequencer with lots of dynamic features. + * + * Pattern: + * - length + * - clock division + * - probability + * - fill density + * - direction (fwd, rev, pend, rand) + * - mode: + * - step equencer + * - euclidean rhythm + * - pattern (grids like presets) + * + * Step: + * - gate / trigger + * - duty / duration + * - probability + * - ratchet / retrig + * + * Global: + * - internal / external / midi + * - run / reset + * - mute + * - save / load banks + * - 6 channel / 3 channel accent + * + * ENCODER: + * Press: change between selecting a parameter and editing the parameter. + * Hold & Rotate: change current selected output channel. + * + * BTN1: + * Play/pause - start or stop the internal clock. + * + * BTN2: + * Shift - hold and rotate encoder to change current selected output channel. + * + * EXT: + * External clock input. When Gravity is set to INTERNAL or MIDI clock + * source, this input is used to reset clocks. + * + * CV1: + * External analog input used to provide modulation to any channel parameter. + * + * CV2: + * External analog input used to provide modulation to any channel parameter. + * + */ + +#include + +#include "app_state.h" +#include "channel.h" +#include "display.h" + +AppState app; + +// +// Arduino setup and loop. +// + +void setup() { + // Start Gravity. + gravity.Init(); + + // Clock handlers. + gravity.clock.AttachIntHandler(HandleIntClockTick); + gravity.clock.AttachExtHandler(HandleExtClockTick); + + // Encoder rotate and press handlers. + gravity.encoder.AttachPressHandler(HandleEncoderPressed); + gravity.encoder.AttachRotateHandler(HandleRotate); + gravity.encoder.AttachPressRotateHandler(HandlePressedRotate); + + // Button press handlers. + gravity.play_button.AttachPressHandler(HandlePlayPressed); +} + +void loop() { + // Process change in state of inputs and outputs. + gravity.Process(); + + // Check if cv run or reset is active and read cv. + CheckRunReset(gravity.cv1, gravity.cv2); + + if (app.refresh_screen) { + UpdateDisplay(); + } +} + +// +// Firmware handlers for clocks. +// + +void HandleIntClockTick(uint32_t tick) { + bool refresh = false; + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + app.channel[i].processClockTick(tick, gravity.outputs[i]); + } +} + +void HandleExtClockTick() { + switch (app.selected_source) { + case Clock::SOURCE_INTERNAL: + case Clock::SOURCE_EXTERNAL_MIDI: + // Use EXT as Reset when not used for clock source. + ResetOutputs(); + gravity.clock.Reset(); + break; + default: + // Register EXT cv clock tick. + gravity.clock.Tick(); + } + app.refresh_screen = true; +} + +void CheckRunReset(AnalogInput& cv1, AnalogInput& cv2) { + // Clock Run + if (app.cv_run == 1 || app.cv_run == 2) { + const int val = (app.cv_run == 1) ? cv1.Read() : cv2.Read(); + if (val > AnalogInput::GATE_THRESHOLD && gravity.clock.IsPaused()) { + gravity.clock.Start(); + app.refresh_screen = true; + } else if (val < AnalogInput::GATE_THRESHOLD && !gravity.clock.IsPaused()) { + gravity.clock.Stop(); + ResetOutputs(); + app.refresh_screen = true; + } + } + + // Clock Reset + if ((app.cv_reset == 1 && cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) || + (app.cv_reset == 2 && cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) { + gravity.clock.Reset(); + } +} + +// +// UI handlers for encoder and buttons. +// + +void HandlePlayPressed() { +} + +void HandleEncoderPressed() { +} + +void HandleRotate(int val) { +} + +void HandlePressedRotate(int val) { +} + +// TODO: move to libGravity +void ResetOutputs() { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + gravity.outputs[i].Low(); + } +} diff --git a/firmware/GridSeq/app_state.h b/firmware/GridSeq/app_state.h new file mode 100644 index 0000000..ab3efc9 --- /dev/null +++ b/firmware/GridSeq/app_state.h @@ -0,0 +1,38 @@ +/** + * @file app_state.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + +#ifndef APP_STATE_H +#define APP_STATE_H + +#include + +#include "channel.h" + +// Global state for settings and app behavior. +struct AppState { + int tempo = Clock::DEFAULT_TEMPO; + Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Channel channel[Gravity::OUTPUT_COUNT]; + byte selected_param = 0; + byte selected_channel = 0; // 0=tempo, 1-6=output channel + byte cv_run = 0; + byte cv_reset = 0; + bool editing_param = false; + bool refresh_screen = true; +}; + +extern AppState app; + +static Channel& GetSelectedChannel() { + return app.channel[app.selected_channel - 1]; +} + +#endif // APP_STATE_H \ No newline at end of file diff --git a/firmware/GridSeq/channel.h b/firmware/GridSeq/channel.h new file mode 100644 index 0000000..9344cbe --- /dev/null +++ b/firmware/GridSeq/channel.h @@ -0,0 +1,129 @@ +/** + * @file channel.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Grid Sequencer. + * @version 1.0.0 + * @date 2025-08-12 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + +#ifndef CHANNEL_H +#define CHANNEL_H + +#include +#include + +#include "euclidean.h" + +// Enums for CV Mod destination +enum CvDestination : uint8_t { + CV_DEST_NONE, + CV_DEST_MODE, + CV_DEST_LENGTH, + CV_DEST_DIV, + CV_DEST_PROB, + CV_DEST_DENSITY, + CV_DEST_LAST, +}; + +// Enums for GridSeq modes +enum Mode : uint8_t { + MODE_SEQ, + MODE_EUCLIDEAN, + MODE_PATTERN, + MODE_LAST, +}; + + +class Channel { + public: + Channel() { + Init(); + } + + void Init() { + base_probability = 100; + + cv1_dest = CV_DEST_NONE; + cv2_dest = CV_DEST_NONE; + } + + bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } + + // Setters (Set the BASE value) + + void setProbability(int prob) { + base_probability = constrain(prob, 0, 100); + } + + void setCv1Dest(CvDestination dest) { + cv1_dest = dest; + } + void setCv2Dest(CvDestination dest) { + cv2_dest = dest; + } + CvDestination getCv1Dest() const { return cv1_dest; } + CvDestination getCv2Dest() const { return cv2_dest; } + + // Getters (Get the BASE value for editing or cv modded value for display) + int getProbability() const { return base_probability; } + + // Getters that calculate the value with CV modulation applied. + int getProbabilityWithMod(int cv1_val, int cv2_val) { + int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); + return constrain(base_probability + prob_mod, 0, 100); + } + + void toggleMute() { mute = !mute; } + + /** + * @brief Processes a clock tick and determines if the output should be high or low. + * Note: this method is called from an ISR and must be kept as simple as possible. + * @param tick The current clock tick count. + * @param output The output object to be modified. + */ + void processClockTick(uint32_t tick, DigitalOutput& output) { + // Mute check + if (mute) { + output.Low(); + return; + } + + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); + int cvmod_probability = getProbabilityWithMod(cv1, cv2); + + // Duty cycle high check logic + if (!output.On()) { + // Step check + bool hit = cvmod_probability >= random(0, 100); + if (hit) { + output.Trigger(); + } + } + + } + + private: + int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) { + int mod1 = (cv1_dest == dest) ? map(cv1_val, -512, 512, min_range, max_range) : 0; + int mod2 = (cv2_dest == dest) ? map(cv2_val, -512, 512, min_range, max_range) : 0; + return mod1 + mod2; + } + + // User-settable base values. + byte base_probability; + + // CV mod configuration + CvDestination cv1_dest; + CvDestination cv2_dest; + + // Mute channel flag + bool mute; + + uint16_t _duty_pulses; +}; + +#endif // CHANNEL_H \ No newline at end of file diff --git a/firmware/GridSeq/display.h b/firmware/GridSeq/display.h new file mode 100644 index 0000000..cc0d0be --- /dev/null +++ b/firmware/GridSeq/display.h @@ -0,0 +1,93 @@ +/** + * @file display.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 1.0.0 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + +#ifndef DISPLAY_H +#define DISPLAY_H + +#include + +#include "app_state.h" + +// +// UI Display functions for drawing the UI to the OLED display. +// + +/* + * Font: velvetscreen.bdf 9pt + * https://stncrn.github.io/u8g2-unifont-helper/ + * "%/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + */ +const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM = + "\64\0\2\2\3\3\2\3\4\5\5\0\0\5\0\5\0\0\221\0\0\1\230 \4\200\134%\11\255tT" + "R\271RI(\6\252\334T\31)\7\252\134bJ\12+\7\233\345\322J\0,\5\221T\4-\5\213" + "f\6.\5\211T\2/\6\244\354c\33\60\10\254\354T\64\223\2\61\7\353\354\222\254\6\62\11\254l" + "\66J*\217\0\63\11\254l\66J\32\215\4\64\10\254l\242\34\272\0\65\11\254l\206\336h$\0\66" + "\11\254\354T^\61)\0\67\10\254lF\216u\4\70\11\254\354TL*&\5\71\11\254\354TL;" + ")\0:\6\231UR\0A\10\254\354T\34S\6B\11\254lV\34)\216\4C\11\254\354T\324\61" + ")\0D\10\254lV\64G\2E\10\254l\206\36z\4F\10\254l\206^\71\3G\11\254\354TN" + "\63)\0H\10\254l\242\34S\6I\6\251T\206\0J\10\254\354k\231\24\0K\11\254l\242J\62" + "\225\1L\7\254lr{\4M\11\255t\362ZI\353\0N\11\255t\362TI\356\0O\10\254\354T" + "\64\223\2P\11\254lV\34)g\0Q\10\254\354T\264b\12R\10\254lV\34\251\31S\11\254\354" + "FF\32\215\4T\7\253dVl\1U\10\254l\242\63)\0V\11\255t\262Ne\312\21W\12\255" + "t\262J*\251.\0X\11\254l\242L*\312\0Y\12\255tr\252\63\312(\2Z\7\253df*" + "\7p\10\255\364V\266\323\2q\7\255\364\216\257\5r\10\253d\242\32*\2t\6\255t\376#w\11" + "\255\364V\245FN\13x\6\233dR\7\0\0\0\4\377\377\0"; + +/* + * Font: STK-L.bdf 36pt + * https://stncrn.github.io/u8g2-unifont-helper/ + * "%/0123456789ABCDEFILNORSTUVXx" + */ +const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") PROGMEM = + "\35\0\4\4\4\5\3\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\341%'\17;\226\261\245FL" + "\64B\214\30\22\223\220)Bj\10Q\232\214\42R\206\310\210\21d\304\30\32a\254\304\270!\0/\14" + "\272\272\275\311H\321g\343\306\1\60\37|\373\35CJT\20:fW\207\320\210\60\42\304\204\30D\247" + "\214\331\354\20\11%\212\314\0\61\24z\275\245a\244\12\231\71\63b\214\220q\363\377(E\6\62\33|" + "\373\35ShT\20:fl\344\14\211\231\301\306T\71\202#g\371\340\201\1\63\34|\373\35ShT" + "\20:fl\344@r\264\263\222\344,\215\35\42\241\6\225\31\0\64 |\373-!\203\206\214!\62\204" + "\314\220A#\10\215\30\65b\324\210Q\306\354\354\1\213\225\363\1\65\32|\373\15\25[\214\234/\10)" + "Y\61j\350\310Y\32;DB\15*\63\0\66\33}\33\236SiV\14;gt^\230Y\302\202\324" + "\71\273;EbM\252\63\0\67\23|\373\205\25\17R\316\207\344\350p\312\201#\347\35\0\70 |\373" + "\35ShT\20:f\331!\22D\310 :\205\206\10\11B\307\354\354\20\11\65\250\314\0\71\32|\373" + "\35ShT\20:fg\207H,Q\223r\276\30DB\15*\63\0A\26}\33\246r\247\322P\62" + "j\310\250\21\343\354\335\203\357\354w\3B$}\33\206Dj\226\214\42\61l\304\260\21\303F\14\33\61" + "\212\304\222MF\221\30v\316\236=\10\301b\11\0C\27}\33\236Si\226\20Bft\376O\211\215" + " Db\215\42$\0D\33}\33\206Dj\226\214\32\62l\304\260\21\343\354\177vl\304(\22K\324" + "$\2E\22|\373\205\17R\316KD\30\215\234_>x`\0F\20|\373\205\17R\316\227i\262\31" + "\71\377\22\0I\7s\333\204\77HL\15{\333\205\201\363\377\77|\360`\0N$}\33\6\201\346\314" + "\35;\206\12U\242D&\306\230\30cd\210\221!fF\230\31a(+\314\256\63\67\0O\26}\33" + "\236Si\226\214\32\61\316\376\277\33\61j\310\232Tg\0R\61\216;\6Ek\230\14#\61n\304\270" + "\21\343F\214\33\61n\304\60\22\243\210\60Q\224j\310\260\61\243\306\20\232\325\230QD\206\221\30\67b" + "\334\301\1S\42\216;\236c\211\226\220\42\61n\304\270\21c\307R\232,[\262\203\307\216\65h\16\25" + "\21&\253\320\0T\15}\33\206\17R\15\235\377\377\25\0U\21|\373\205a\366\377\237\215\30\64D\15" + "*\63\0V\26\177\371\205\221\366\377\313\21\343\206\220\42C\25\11r'\313\16\3X)~;\206\201\6" + "\217\221\30\66\204\20\31\42\244\206\14Cg\320$Q\222\6\315!\33\62\212\10\31BD\206\215 v\320" + "\302\1x\24\312\272\205A\206\216\220@c\212\224\31$S\14\262h\0\0\0\0\4\377\377\0"; + +#define play_icon_width 14 +#define play_icon_height 14 +static const unsigned char play_icon[28] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x7C, 0x00, 0xFC, 0x00, 0xFC, 0x03, + 0xFC, 0x0F, 0xFC, 0x0F, 0xFC, 0x03, 0xFC, 0x00, 0x7C, 0x00, 0x3C, 0x00, + 0x00, 0x00, 0x00, 0x00}; +static const unsigned char pause_icon[28] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, + 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, + 0x38, 0x0E, 0x00, 0x00}; + +void UpdateDisplay() { + app.refresh_screen = false; + gravity.display.firstPage(); + do { + } while (gravity.display.nextPage()); +} + +#endif // DISPLAY_H diff --git a/firmware/GridSeq/euclidean.h b/firmware/GridSeq/euclidean.h new file mode 100644 index 0000000..ae2bbef --- /dev/null +++ b/firmware/GridSeq/euclidean.h @@ -0,0 +1,100 @@ +/** + * @file euclidean.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + +#ifndef EUCLIDEAN_H +#define EUCLIDEAN_H + +#define MAX_PATTERN_LEN 32 + +struct EuclideanState { + uint8_t steps; + uint8_t hits; + uint8_t offset; + uint8_t padding; +}; + +const EuclideanState DEFAULT_PATTERN = {1, 1}; + +class Euclidean { + public: + Euclidean() {} + ~Euclidean() {} + + enum Step : uint8_t { + REST, + HIT, + }; + + void Init(EuclideanState state) { + steps_ = constrain(state.steps, 1, MAX_PATTERN_LEN); + hits_ = constrain(state.hits, 1, steps_); + updatePattern(); + } + + EuclideanState GetState() const { return {steps_, hits_}; } + + Step GetCurrentStep(byte i) { + if (i >= MAX_PATTERN_LEN) return REST; + return (pattern_bitmap_ & (1UL << i)) ? HIT : REST; + } + + void SetSteps(int steps) { + steps_ = constrain(steps, 1, MAX_PATTERN_LEN); + hits_ = min(hits_, steps_); + updatePattern(); + } + + void SetHits(int hits) { + hits_ = constrain(hits, 1, steps_); + updatePattern(); + } + + void Reset() { step_index_ = 0; } + + uint8_t GetSteps() const { return steps_; } + uint8_t GetHits() const { return hits_; } + uint8_t GetStepIndex() const { return step_index_; } + + Step NextStep() { + if (steps_ == 0) return REST; + + Step value = GetCurrentStep(step_index_); + step_index_ = (step_index_ < steps_ - 1) ? step_index_ + 1 : 0; + return value; + } + + private: + uint8_t steps_ = 0; + uint8_t hits_ = 0; + volatile uint8_t step_index_ = 0; + uint32_t pattern_bitmap_ = 0; + + // Update the euclidean rhythm pattern using bitmap + void updatePattern() { + pattern_bitmap_ = 0; // Clear the bitmap + + if (steps_ == 0) return; + + byte bucket = 0; + // Set the first bit (index 0) if it's a HIT + pattern_bitmap_ |= (1UL << 0); + + for (int i = 1; i < steps_; i++) { + bucket += hits_; + if (bucket >= steps_) { + bucket -= steps_; + pattern_bitmap_ |= (1UL << i); + } + } + } +}; + +#endif \ No newline at end of file diff --git a/firmware/GridSeq/step.h b/firmware/GridSeq/step.h new file mode 100644 index 0000000..e69de29