1 Commits

Author SHA1 Message Date
af3cfe9614 initial commit of new GridSeq firmware 2025-08-13 07:06:53 -07:00
10 changed files with 446 additions and 58 deletions

View File

@ -1,18 +1,6 @@
# Sitka Instruments Gravity Firmware Abstraction # Sitka Instruments Gravity Firmware Abstraction
This library helps make writing firmware for the [Sitka Instruments Gravity](https://sitkainstruments.com/gravity/) eurorack module easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library. This library helps make writing firmware easier by abstracting away the initialization and peripheral interactions. Now your firmware code can just focus on the logic and behavior of the app, and keep the low level code neatly tucked away in this library.
The latest releases of all Sitka Instruments Gravity firmware builds can be found on the [Updater](https://sitkainstruments.com/gravity/updater/) page. You can use this page to flash the latest build directly to the Arduino Nano on the back of your module.
## Project Code Layout
* [`src/`](src/) - **libGravity**: This is the hardware abstraction library used to simplify the creation of new Gravity module firmware by providing common reusable wrappers around the module peripherials like [DigitalOutput](src/digital_output.h#L18) providing methods like [`Update(uint8_t state)`](src/digital_output.h#L45) which allow you to set that output channel voltage high or low, and common module behavior like [Clock](src/clock.h#L30) which provides handlers like [`AttachExtHandler(callback)`](src/clock.h#L69) which takes a callback function to handle external clock tick behavior when receiving clock trigger.
* [`firmware/Gravity`](firmware/Gravity/) - **Alt Gravity**: This is the implementation of the default 6-channel trigger/gate clock modulation firmware. This is a full rewrite of the original firmware designed to use `libGravity` with a focus on open source friendlines.
* `firmware/GridSeq` - **GridSeq**: Comming Soon.
* [`examples/skeleton`](examples/skeleton/skeleton.ino) - **Skeleton**: This is the bare bones scaffloding for a `libGravity` firmware app.
## Installation ## Installation
@ -29,14 +17,13 @@ Common directory locations:
* [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler. * [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler.
* [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation.
* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. * [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library.
* [NeoHWSerial](https://github.com/SlashDevin/NeoHWSerial) [GPL] - Hardware serial library with attachInterrupt.
## Example ## Example
Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation. Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation.
```cpp ```cpp
#include "libGravity.h" #include "gravity.h"
byte idx = 0; byte idx = 0;
bool reversed = false; bool reversed = false;
@ -88,11 +75,11 @@ void HandlePlayPressed() {
} }
} }
void HandleRotate(int val) { void HandleRotate(Direction dir, int val) {
if (selected_param == 0) { if (selected_param == 0) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val); gravity.clock.SetTempo(gravity.clock.Tempo() + val);
} else if (selected_param == 1) { } else if (selected_param == 1) {
reversed = (val < 0); reversed = (dir == DIRECTION_DECREMENT);
} }
} }
@ -124,16 +111,8 @@ void UpdateDisplay() {
} }
``` ```
**Building New Firmware Using libGravity**
When starting a new firmware sketch you can use the [skeleton](examples/skeleton/skeleton.ino) app as a place to start.
**Building New Firmware from scratch**
If you do not want to use the libGravity hardware abstraction library and want to roll your own vanilla firmware, take a look at the [peripherials.h](src/peripherials.h) file for the pinout definitions used by the module.
### Build for release ### Build for release
``` ```
$ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/ $ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/
``` ```

View File

@ -17,7 +17,7 @@
* TODO: Store the calibration value in EEPROM. * TODO: Store the calibration value in EEPROM.
*/ */
#include "libGravity.h" #include "gravity.h"
#define TEXT_FONT u8g2_font_profont11_tf #define TEXT_FONT u8g2_font_profont11_tf
#define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t #define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t
@ -43,7 +43,7 @@ void NextCalibrationPoint() {
selected_param = (selected_param + 1) % 6; selected_param = (selected_param + 1) % 6;
} }
void CalibrateCV(int val) { void CalibrateCV(Direction dir, int val) {
AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
switch (selected_param % 3) { switch (selected_param % 3) {
case 0: case 0:

View File

@ -14,7 +14,7 @@
* *
*/ */
#include "libGravity.h" #include "gravity.h"
#define TEXT_FONT u8g2_font_profont11_tf #define TEXT_FONT u8g2_font_profont11_tf
@ -39,7 +39,7 @@ void NextCalibrationPoint() {
selected_param = (selected_param + 1) % 2; selected_param = (selected_param + 1) % 2;
} }
void CalibrateCV(int val) { void CalibrateCV(Direction dir, int val) {
// AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; // AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
AnalogInput* cv = &gravity.cv1; AnalogInput* cv = &gravity.cv1;
switch (selected_param % 2) { switch (selected_param % 2) {

View File

@ -1,4 +1,4 @@
#include "libGravity.h" #include "gravity.h"
byte idx = 0; byte idx = 0;
bool reversed = false; bool reversed = false;
@ -33,28 +33,28 @@ void IntClock(uint32_t tick) {
if (tick % 12 == 0 && ! freeze) { if (tick % 12 == 0 && ! freeze) {
gravity.outputs[idx].Low(); gravity.outputs[idx].Low();
if (reversed) { if (reversed) {
idx = (idx == 0) ? Gravity::OUTPUT_COUNT - 1 : idx - 1; idx = (idx == 0) ? OUTPUT_COUNT - 1 : idx - 1;
} else { } else {
idx = (idx + 1) % Gravity::OUTPUT_COUNT; idx = (idx + 1) % OUTPUT_COUNT;
} }
gravity.outputs[idx].High(); gravity.outputs[idx].High();
} }
} }
void HandlePlayPressed() { void HandlePlayPressed() {
gravity.clock.Stop(); gravity.clock.Pause();
if (gravity.clock.IsPaused()) { if (gravity.clock.IsPaused()) {
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { for (int i = 0; i < OUTPUT_COUNT; i++) {
gravity.outputs[i].Low(); gravity.outputs[i].Low();
} }
} }
} }
void HandleRotate(int val) { void HandleRotate(Direction dir, int val) {
if (selected_param == 0) { if (selected_param == 0) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val); gravity.clock.SetTempo(gravity.clock.Tempo() + val);
} else if (selected_param == 1) { } else if (selected_param == 1) {
reversed = (val < 0); reversed = (dir == DIRECTION_DECREMENT);
} }
} }
@ -80,7 +80,7 @@ void UpdateDisplay() {
gravity.display.print("Direction: "); gravity.display.print("Direction: ");
gravity.display.print((reversed) ? "Backward" : "Forward"); gravity.display.print((reversed) ? "Backward" : "Forward");
gravity.display.drawStr(0, selected_param * 10, "x"); gravity.display.drawChar(0, selected_param * 10, 0x10, 1, 0, 1);
gravity.display.display(); gravity.display.display();
} }

View File

@ -1,14 +1,37 @@
/** /**
* @file skeleton.ino * @file GridSeq.ino
* @author YOUR_NAME (<url>) * @author Adam Wonak (https://github.com/awonak/)
* @brief Skeleton app for Sitka Instruments Gravity. * @brief Grid based step sequencer firmware for Gravity by Sitka Instruments.
* @version vX.Y.Z - MONTH YEAR YOUR_NAME * @version v1.0.0 - August 2025 awonak
* @date YYYY-MM-DD * @date 2025-08-12
* *
* @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com
*
* Grid based step sequencer with lots of dynamic features.
* *
* Skeleton app for basic structure of a new firmware for Sitka Instruments * Pattern:
* Gravity using the libGravity library. * - 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: * ENCODER:
* Press: change between selecting a parameter and editing the parameter. * Press: change between selecting a parameter and editing the parameter.
@ -34,14 +57,9 @@
#include <libGravity.h> #include <libGravity.h>
#include "app_state.h"
#include "channel.h"
// Global state for settings and app behavior. #include "display.h"
struct AppState {
int tempo = Clock::DEFAULT_TEMPO;
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
// Add app specific state variables here.
};
AppState app; AppState app;
@ -70,7 +88,12 @@ void loop() {
// Process change in state of inputs and outputs. // Process change in state of inputs and outputs.
gravity.Process(); gravity.Process();
// Non-ISR loop behavior. // Check if cv run or reset is active and read cv.
CheckRunReset(gravity.cv1, gravity.cv2);
if (app.refresh_screen) {
UpdateDisplay();
}
} }
// //
@ -80,7 +103,7 @@ void loop() {
void HandleIntClockTick(uint32_t tick) { void HandleIntClockTick(uint32_t tick) {
bool refresh = false; bool refresh = false;
for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
// Process each output tick handlers. app.channel[i].processClockTick(tick, gravity.outputs[i]);
} }
} }
@ -89,12 +112,35 @@ void HandleExtClockTick() {
case Clock::SOURCE_INTERNAL: case Clock::SOURCE_INTERNAL:
case Clock::SOURCE_EXTERNAL_MIDI: case Clock::SOURCE_EXTERNAL_MIDI:
// Use EXT as Reset when not used for clock source. // Use EXT as Reset when not used for clock source.
ResetOutputs();
gravity.clock.Reset(); gravity.clock.Reset();
break; break;
default: default:
// Register EXT cv clock tick. // Register EXT cv clock tick.
gravity.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();
}
} }
// //
@ -113,6 +159,9 @@ void HandleRotate(int val) {
void HandlePressedRotate(int val) { void HandlePressedRotate(int val) {
} }
// // TODO: move to libGravity
// Application logic goes here. void ResetOutputs() {
// for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) {
gravity.outputs[i].Low();
}
}

View File

@ -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 <libGravity.h>
#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

129
firmware/GridSeq/channel.h Normal file
View File

@ -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 <Arduino.h>
#include <libGravity.h>
#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

View File

@ -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 <Arduino.h>
#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

View File

@ -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

0
firmware/GridSeq/step.h Normal file
View File