6 Commits

Author SHA1 Message Date
ab80642afb bump version 2026-02-21 10:48:45 -08:00
62a74fe3ee Improve live clock performance when loading pattern data. If the clock is playing, only load pattern and tempo, do not load global settings which impact performance.
Refactor the save / load state use of disabling interrupts by wrapping all private methods from inside the public method. This ensures we will not have a race condition if an interrupt is called in between the private method calls.
2026-02-21 10:45:13 -08:00
624d453b9d add rotate display 2026-02-21 10:33:48 -08:00
bd08ac4352 Add clock run/reset 2026-02-21 10:19:09 -08:00
763d58f411 add additional external ppqn 2026-02-21 09:44:50 -08:00
6d38c6b36b Refactor: remove Euclidean steps into its own firmware 2026-02-21 09:39:57 -08:00
10 changed files with 8 additions and 1455 deletions

View File

@ -2,12 +2,6 @@
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.
## Quick Start
You can flash the firmware to your module using the [Web Installer](https://awonak.github.io/alt-gravity/). This website also provides demo videos and documentation for each firmware version.
https://awonak.github.io/alt-gravity/
## Installation
Download or git clone this repository into your Arduino > libraries folder.

View File

@ -1,435 +0,0 @@
#include <EEPROM.h>
#include <libGravity.h>
// EEPROM addrs
const int EEPROM_INIT_ADDR = 0;
const int EEPROM_CV1_LOW = 1;
const int EEPROM_CV1_HIGH = 3;
const int EEPROM_CV1_OFFSET = 5;
const int EEPROM_CV2_LOW = 7;
const int EEPROM_CV2_HIGH = 9;
const int EEPROM_CV2_OFFSET = 11;
const int EEPROM_COMP1_SHIFT = 13;
const int EEPROM_COMP1_SIZE = 15;
const int EEPROM_COMP2_SHIFT = 17;
const int EEPROM_COMP2_SIZE = 19;
const byte EEPROM_INIT_FLAG = 0xAB; // Update flag to re-init
// EEPROM Delay Save
const unsigned long SAVE_DELAY_MS = 5000;
bool eeprom_needs_save = false;
unsigned long last_param_change = 0;
enum AppMode { MODE_COMPARATOR, MODE_CALIBRATION };
AppMode current_mode = MODE_COMPARATOR;
byte cal_selected_param = 0; // 0=CV1 Low, 1=CV1 Offset, 2=CV1 High, 3=CV2 Low,
// 4=CV2 Offset, 5=CV2 High
// UI Parameters
enum Parameter { COMP1_SHIFT, COMP1_SIZE, COMP2_SHIFT, COMP2_SIZE };
Parameter selected_param = COMP1_SHIFT;
int comp1_shift = 0; // Range: -512 to 512
int comp1_size = 512; // Range: 0 to 1024
int comp2_shift = 0; // Range: -512 to 512
int comp2_size = 512; // Range: 0 to 1024
bool prev_gate1 = false;
bool prev_gate2 = false;
bool ff_state = false;
bool needs_redraw = true;
int last_cv1_draw = -1000;
int last_cv2_draw = -1000;
unsigned long last_redraw = 0;
bool prev_both_buttons = false;
// Calibration Methods
void LoadCalibration() {
if (EEPROM.read(EEPROM_INIT_ADDR) == EEPROM_INIT_FLAG) {
int val = 0;
EEPROM.get(EEPROM_CV1_LOW, val);
gravity.cv1.SetCalibrationLow(val);
EEPROM.get(EEPROM_CV1_HIGH, val);
gravity.cv1.SetCalibrationHigh(val);
EEPROM.get(EEPROM_CV1_OFFSET, val);
gravity.cv1.AdjustOffset(val - gravity.cv1.GetOffset());
EEPROM.get(EEPROM_CV2_LOW, val);
gravity.cv2.SetCalibrationLow(val);
EEPROM.get(EEPROM_CV2_HIGH, val);
gravity.cv2.SetCalibrationHigh(val);
EEPROM.get(EEPROM_CV2_OFFSET, val);
gravity.cv2.AdjustOffset(val - gravity.cv2.GetOffset());
EEPROM.get(EEPROM_COMP1_SHIFT, comp1_shift);
EEPROM.get(EEPROM_COMP1_SIZE, comp1_size);
EEPROM.get(EEPROM_COMP2_SHIFT, comp2_shift);
EEPROM.get(EEPROM_COMP2_SIZE, comp2_size);
}
}
void SaveCalibration() {
EEPROM.update(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG);
int val;
val = gravity.cv1.GetCalibrationLow();
EEPROM.put(EEPROM_CV1_LOW, val);
val = gravity.cv1.GetCalibrationHigh();
EEPROM.put(EEPROM_CV1_HIGH, val);
val = gravity.cv1.GetOffset();
EEPROM.put(EEPROM_CV1_OFFSET, val);
val = gravity.cv2.GetCalibrationLow();
EEPROM.put(EEPROM_CV2_LOW, val);
val = gravity.cv2.GetCalibrationHigh();
EEPROM.put(EEPROM_CV2_HIGH, val);
val = gravity.cv2.GetOffset();
EEPROM.put(EEPROM_CV2_OFFSET, val);
EEPROM.put(EEPROM_COMP1_SHIFT, comp1_shift);
EEPROM.put(EEPROM_COMP1_SIZE, comp1_size);
EEPROM.put(EEPROM_COMP2_SHIFT, comp2_shift);
EEPROM.put(EEPROM_COMP2_SIZE, comp2_size);
}
// Handlers
void OnPlayPress() {
if (gravity.shift_button.On())
return; // ignore if holding both
if (current_mode == MODE_CALIBRATION) {
cal_selected_param = (cal_selected_param < 3) ? cal_selected_param + 3
: cal_selected_param - 3;
needs_redraw = true;
return;
}
if (selected_param == COMP1_SHIFT)
selected_param = COMP2_SHIFT;
else if (selected_param == COMP1_SIZE)
selected_param = COMP2_SIZE;
else if (selected_param == COMP2_SHIFT)
selected_param = COMP1_SHIFT;
else if (selected_param == COMP2_SIZE)
selected_param = COMP1_SIZE;
needs_redraw = true;
}
void OnShiftPress() {
if (gravity.play_button.On())
return; // ignore if holding both
if (current_mode == MODE_CALIBRATION) {
cal_selected_param =
(cal_selected_param / 3) * 3 + ((cal_selected_param + 1) % 3);
needs_redraw = true;
return;
}
if (selected_param == COMP1_SHIFT)
selected_param = COMP1_SIZE;
else if (selected_param == COMP1_SIZE)
selected_param = COMP1_SHIFT;
else if (selected_param == COMP2_SHIFT)
selected_param = COMP2_SIZE;
else if (selected_param == COMP2_SIZE)
selected_param = COMP2_SHIFT;
needs_redraw = true;
}
void OnEncoderRotate(int val) {
if (current_mode == MODE_CALIBRATION) {
AnalogInput *cv = (cal_selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
// Scale val up so tuning is practical without excessive encoder interrupts
int cal_adj = val * 8;
switch (cal_selected_param % 3) {
case 0:
cv->AdjustCalibrationLow(cal_adj);
break;
case 1:
cv->AdjustOffset(cal_adj);
break;
case 2:
cv->AdjustCalibrationHigh(cal_adj);
break;
}
needs_redraw = true;
return;
}
int amount = val * 16;
switch (selected_param) {
case COMP1_SHIFT:
comp1_shift = constrain(comp1_shift + amount, -512, 512);
break;
case COMP1_SIZE:
comp1_size = constrain(comp1_size + amount, 0, 1024);
break;
case COMP2_SHIFT:
comp2_shift = constrain(comp2_shift + amount, -512, 512);
break;
case COMP2_SIZE:
comp2_size = constrain(comp2_size + amount, 0, 1024);
break;
}
eeprom_needs_save = true;
last_param_change = millis();
needs_redraw = true;
}
void DisplayCalibrationPoint(AnalogInput *cv, const char *title, int index) {
int barWidth = 100, barHeight = 10, textHeight = 10;
int half = barWidth / 2;
int offsetX = 16, offsetY = (32 * index);
gravity.display.setDrawColor(1);
int value = cv->Read();
gravity.display.setCursor(0, offsetY + textHeight);
gravity.display.print(title);
if (value >= 0)
gravity.display.print(" ");
gravity.display.print(value);
gravity.display.setCursor(92, offsetY + textHeight);
if (cv->Voltage() >= 0)
gravity.display.print(" ");
gravity.display.print(cv->Voltage(), 1);
gravity.display.print(F("V"));
gravity.display.drawFrame(offsetX, textHeight + offsetY + 2, barWidth,
barHeight);
if (value > 0) {
int x = constrain(map(value, 0, 512, 0, half), 0, half);
gravity.display.drawBox(half + offsetX, textHeight + offsetY + 2, x,
barHeight);
} else {
int x = constrain(map(abs(value), 0, 512, 0, half), 0, half);
gravity.display.drawBox((half + offsetX) - x, textHeight + offsetY + 2, x,
barHeight);
}
if (cal_selected_param / 3 == index) {
int left = offsetX + (half * (cal_selected_param % 3) - 2);
int top = barHeight + textHeight + offsetY + 12;
gravity.display.drawStr(left, top, "^");
}
}
void UpdateCalibrationDisplay() {
gravity.display.setFontMode(0);
gravity.display.setDrawColor(1);
gravity.display.setFont(u8g2_font_profont11_tf);
DisplayCalibrationPoint(&gravity.cv1, "CV1: ", 0);
DisplayCalibrationPoint(&gravity.cv2, "CV2: ", 1);
}
// UpdateDisplay
void UpdateDisplay(int cv1_val, int cv2_val) {
// Comp 1 graphics (Left)
int c1_h = max((comp1_size * 3) / 64, 1);
int c1_center = 26 - ((comp1_shift * 3) / 64);
int c1_y = c1_center - (c1_h / 2);
gravity.display.drawFrame(20, c1_y, 44, c1_h);
// CV 1 Indicator (Filled Box, 50% width, from 0V center)
int cv1_y = constrain(26 - ((cv1_val * 3) / 64), 2, 50);
if (cv1_val >= 0) {
gravity.display.drawBox(31, cv1_y, 22, 26 - cv1_y);
} else {
gravity.display.drawBox(31, 26, 22, cv1_y - 26);
}
// Comp 2 graphics (Right)
int c2_h = max((comp2_size * 3) / 64, 1);
int c2_center = 26 - ((comp2_shift * 3) / 64);
int c2_y = c2_center - (c2_h / 2);
gravity.display.drawFrame(74, c2_y, 44, c2_h);
// CV 2 Indicator (Filled Box, 50% width, from 0V center)
int cv2_y = constrain(26 - ((cv2_val * 3) / 64), 2, 50);
if (cv2_val >= 0) {
gravity.display.drawBox(85, cv2_y, 22, 26 - cv2_y);
} else {
gravity.display.drawBox(85, 26, 22, cv2_y - 26);
}
// Restore solid drawing for labels
gravity.display.setDrawColor(1);
gravity.display.setFont(u8g2_font_5x7_tf);
gravity.display.setCursor(0, 7);
gravity.display.print("+5V");
gravity.display.setCursor(6, 29);
gravity.display.print("0V");
gravity.display.setCursor(0, 51);
gravity.display.print("-5V");
gravity.display.setDrawColor(2); // XOR mode
// Draw center divider and dotted lines in XOR
for (int x = 20; x < 128; x += 4) {
gravity.display.drawPixel(x, 2); // +5V
gravity.display.drawPixel(x, 26); // 0V
gravity.display.drawPixel(x, 50); // -5V
}
for (int y = 0; y <= 50; y += 4) {
gravity.display.drawPixel(69, y); // Center divider
}
// Restore draw color to default (solid)
gravity.display.setDrawColor(1);
// Bottom text area
gravity.display.setDrawColor(0);
gravity.display.drawBox(0, 52, 128, 12);
gravity.display.setDrawColor(1);
gravity.display.drawHLine(0, 52, 128);
gravity.display.setFont(u8g2_font_6x10_tf);
gravity.display.setCursor(2, 62);
char text[32];
switch (selected_param) {
case COMP1_SHIFT:
snprintf(text, sizeof(text), "> Comp 1 Shift: %d", comp1_shift);
break;
case COMP1_SIZE:
snprintf(text, sizeof(text), "> Comp 1 Size: %d", comp1_size);
break;
case COMP2_SHIFT:
snprintf(text, sizeof(text), "> Comp 2 Shift: %d", comp2_shift);
break;
case COMP2_SIZE:
snprintf(text, sizeof(text), "> Comp 2 Size: %d", comp2_size);
break;
}
gravity.display.print(text);
}
void setup() {
gravity.Init();
LoadCalibration();
// Speed up ADC conversions
ADCSRA &= ~(bit(ADPS2) | bit(ADPS1) | bit(ADPS0));
ADCSRA |= bit(ADPS2);
gravity.play_button.AttachPressHandler(OnPlayPress);
gravity.shift_button.AttachPressHandler(OnShiftPress);
gravity.encoder.AttachRotateHandler(OnEncoderRotate);
}
void loop() {
gravity.Process();
bool both_pressed = gravity.play_button.On() && gravity.shift_button.On();
if (both_pressed && !prev_both_buttons) {
if (current_mode == MODE_COMPARATOR) {
current_mode = MODE_CALIBRATION;
cal_selected_param = 0;
// Turn off all outputs to prevent phantom gates while tuning
for (int i = 0; i < 6; i++) {
gravity.outputs[i].Update(LOW);
}
} else {
SaveCalibration();
current_mode = MODE_COMPARATOR;
}
needs_redraw = true;
}
prev_both_buttons = both_pressed;
int cv1_val = gravity.cv1.Read();
int cv2_val = gravity.cv2.Read();
if (current_mode == MODE_COMPARATOR) {
int c1_lower = comp1_shift - (comp1_size / 2);
int c1_upper = comp1_shift + (comp1_size / 2);
int c2_lower = comp2_shift - (comp2_size / 2);
int c2_upper = comp2_shift + (comp2_size / 2);
const int HYSTERESIS = 4; // Margin to prevent noise bouncing at threshold
bool gate1 = prev_gate1;
if (gate1) {
if (cv1_val < c1_lower - HYSTERESIS || cv1_val > c1_upper + HYSTERESIS) {
gate1 = false;
}
} else {
if (cv1_val >= c1_lower + HYSTERESIS &&
cv1_val <= c1_upper - HYSTERESIS) {
gate1 = true;
}
}
bool gate2 = prev_gate2;
if (gate2) {
if (cv2_val < c2_lower - HYSTERESIS || cv2_val > c2_upper + HYSTERESIS) {
gate2 = false;
}
} else {
if (cv2_val >= c2_lower + HYSTERESIS &&
cv2_val <= c2_upper - HYSTERESIS) {
gate2 = true;
}
}
bool logic_and = gate1 && gate2;
bool logic_or = gate1 || gate2;
bool logic_xor = gate1 ^ gate2;
static bool prev_logic_xor = false;
if (logic_xor && !prev_logic_xor) {
ff_state = !ff_state;
}
prev_logic_xor = logic_xor;
gravity.outputs[0].Update(gate1 ? HIGH : LOW);
gravity.outputs[1].Update(gate2 ? HIGH : LOW);
gravity.outputs[2].Update(logic_and ? HIGH : LOW);
gravity.outputs[3].Update(logic_or ? HIGH : LOW);
gravity.outputs[4].Update(logic_xor ? HIGH : LOW);
gravity.outputs[5].Update(ff_state ? HIGH : LOW);
prev_gate1 = gate1;
prev_gate2 = gate2;
}
if (eeprom_needs_save && (millis() - last_param_change > SAVE_DELAY_MS)) {
SaveCalibration();
eeprom_needs_save = false;
}
if (current_mode == MODE_COMPARATOR) {
if (abs(cv1_val - last_cv1_draw) > 12 ||
abs(cv2_val - last_cv2_draw) > 12) {
needs_redraw = true;
}
} else if (current_mode == MODE_CALIBRATION) {
// Need frequent redraws in calibration to see the live target input
if (abs(cv1_val - last_cv1_draw) >= 2 ||
abs(cv2_val - last_cv2_draw) >= 2) {
needs_redraw = true;
}
}
// Unroll the display loop so it doesn't block the logic loop
static bool is_drawing = false;
if (needs_redraw && !is_drawing && (millis() - last_redraw >= 30)) {
needs_redraw = false;
is_drawing = true;
last_redraw = millis();
gravity.display.firstPage();
}
if (is_drawing) {
if (current_mode == MODE_COMPARATOR) {
last_cv1_draw = cv1_val;
last_cv2_draw = cv2_val;
UpdateDisplay(cv1_val, cv2_val);
} else {
UpdateCalibrationDisplay();
}
is_drawing = gravity.display.nextPage();
}
}

View File

@ -505,7 +505,8 @@ void Bootsplash() {
gravity.display.setFont(TEXT_FONT);
textWidth = gravity.display.getStrWidth(StateManager::SKETCH_NAME);
gravity.display.drawStr(4 + (textWidth / 2), 22, StateManager::SKETCH_NAME);
gravity.display.drawStr(16 + (textWidth / 2), 20,
StateManager::SKETCH_NAME);
textWidth = gravity.display.getStrWidth(StateManager::SEMANTIC_VERSION);
gravity.display.drawStr(16 + (textWidth / 2), 32,

View File

@ -18,8 +18,8 @@
// Define the constants for the current firmware.
const char StateManager::SKETCH_NAME[] = "ALT EUCLIDEAN";
const char StateManager::SEMANTIC_VERSION[] =
"V2.0.1"; // NOTE: This should match the version in the
// library.properties file.
"V2.0.1BETA1"; // NOTE: This should match the version in the
// library.properties file.
// Number of available save slots.
const byte StateManager::MAX_SAVE_SLOTS = 10;

View File

@ -18,8 +18,8 @@
// Define the constants for the current firmware.
const char StateManager::SKETCH_NAME[] = "ALT GRAVITY";
const char StateManager::SEMANTIC_VERSION[] =
"V2.0.1"; // NOTE: This should match the version in the
// library.properties file.
"V2.0.1BETA1"; // NOTE: This should match the version in the
// library.properties file.
// Number of available save slots.
const byte StateManager::MAX_SAVE_SLOTS = 10;

View File

@ -1,768 +0,0 @@
#include "maps.h"
#include <EEPROM.h>
#include <Wire.h>
#include <libGravity.h>
// Configuration
const int MAX_DENSITY = 255;
const int MAP_RESOLUTION = 255;
const int MAX_CHAOS = 255;
// EEPROM addrs
const int EEPROM_INIT_ADDR = 0;
const int EEPROM_CV1 = EEPROM_INIT_ADDR + sizeof(byte);
const int EEPROM_CV2 = EEPROM_CV1 + sizeof(int);
const int EEPROM_PATTERNS = EEPROM_CV2 + sizeof(int);
const byte EEPROM_INIT_FLAG = 0xAF; // Update flag to re-init properly allocated memory
// EEPROM Delay Save
const unsigned long SAVE_DELAY_MS = 5000;
bool eeprom_needs_save = false;
unsigned long last_param_change = 0;
// Menus and State
struct PatternState {
int inst_density[3] = {128, 128, 128};
int map_x = 0;
int map_y = 127;
int chaos_amount = 0;
};
PatternState patterns[5];
int selected_slot = 1; // 0 = Global, 1-5 = Pattern A-E
int active_pattern = 0; // Tracks playback pattern index (0-4)
enum GlobalParam {
PARAM_GLOBAL_CLK_SRC = 0,
PARAM_GLOBAL_BPM = 1,
PARAM_GLOBAL_CV1_DEST = 2,
PARAM_GLOBAL_CV2_DEST = 3,
PARAM_GLOBAL_PULSE_OUT = 4,
PARAM_GLOBAL_LAST = 5
};
enum CvDest {
CV_DEST_NONE = 0,
CV_DEST_KICK_DENS = 1,
CV_DEST_SNARE_DENS = 2,
CV_DEST_HHAT_DENS = 3,
CV_DEST_CHAOS = 4,
CV_DEST_MAP_X = 5,
CV_DEST_MAP_Y = 6,
CV_DEST_PRESET = 7,
CV_DEST_LAST = 8
};
GlobalParam current_global_param = PARAM_GLOBAL_CLK_SRC;
CvDest cv1_dest = CV_DEST_MAP_X;
CvDest cv2_dest = CV_DEST_CHAOS;
Clock::Source selected_source = Clock::SOURCE_INTERNAL;
Clock::Pulse selected_pulse = Clock::PULSE_PPQN_4;
// UI & Navigation
enum SelectedParam {
PARAM_KICK_DENS = 0,
PARAM_SNARE_DENS = 1,
PARAM_HIHAT_DENS = 2,
PARAM_CHAOS = 3,
PARAM_MAP_X = 4,
PARAM_MAP_Y = 5,
PARAM_LAST = 6
};
SelectedParam current_param = PARAM_KICK_DENS;
bool editing_param = false;
bool needs_redraw = true;
unsigned long last_redraw = 0;
const unsigned long REDRAW_DELAY_MS = 30; // ~33fps limit
// Sequencer State
int current_step = 0;
volatile int cv1_val = 0;
volatile int cv2_val = 0;
int last_mapped_slot = -1;
// PRNG State for Chaos
uint16_t xorshift_seed = 0xACE1;
// Fast 16-bit xorshift PRNG
uint16_t xorshift16() {
xorshift_seed ^= xorshift_seed << 7;
xorshift_seed ^= xorshift_seed >> 9;
xorshift_seed ^= xorshift_seed << 8;
return xorshift_seed;
}
// Math Helper: 1D Linear Interpolation between two bytes
uint8_t lerp(uint8_t a, uint8_t b, uint8_t t) {
// t is 0-255. returns a if t=0, b if t=255
return a + (((b - a) * t) >> 8);
}
// Math Helper: Get threshold from 2D map via interpolation
uint8_t GetThreshold(int inst, int step, int x_pos, int y_pos) {
// x_pos is 0-255 mapped across 4 nodes (0, 1, 2, 3). Distance is 85 (255 / 3)
// y_pos is 0-255 mapped across 4 nodes (0, 1, 2, 3). Distance is 85 (255 / 3)
int x_idx = 0;
uint8_t x_frac = 0;
if (x_pos < 85) { x_idx = 0; x_frac = x_pos * 3; }
else if (x_pos < 170) { x_idx = 1; x_frac = (x_pos - 85) * 3; }
else { x_idx = 2; x_frac = x_pos == 255 ? 255 : (x_pos - 170) * 3; }
int y_idx = 0;
uint8_t y_frac = 0;
if (y_pos < 85) { y_idx = 0; y_frac = y_pos * 3; }
else if (y_pos < 170) { y_idx = 1; y_frac = (y_pos - 85) * 3; }
else { y_idx = 2; y_frac = y_pos == 255 ? 255 : (y_pos - 170) * 3; }
// Read 4 corners from PROGMEM
uint8_t p00 = pgm_read_byte(&PATTERN_MAPS[x_idx][y_idx][inst][step]);
uint8_t p10 = pgm_read_byte(&PATTERN_MAPS[x_idx + 1][y_idx][inst][step]);
uint8_t p01 = pgm_read_byte(&PATTERN_MAPS[x_idx][y_idx + 1][inst][step]);
uint8_t p11 = pgm_read_byte(&PATTERN_MAPS[x_idx + 1][y_idx + 1][inst][step]);
// Bilinear interpolation
uint8_t lerp_top = lerp(p00, p10, x_frac);
uint8_t lerp_bottom = lerp(p01, p11, x_frac);
return lerp(lerp_top, lerp_bottom, y_frac);
}
void LoadState() {
if (EEPROM.read(EEPROM_INIT_ADDR) == EEPROM_INIT_FLAG) {
EEPROM.get(EEPROM_CV1, cv1_dest);
EEPROM.get(EEPROM_CV2, cv2_dest);
EEPROM.get(EEPROM_PATTERNS, patterns);
}
}
void SaveState() {
EEPROM.update(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG);
EEPROM.put(EEPROM_CV1, cv1_dest);
EEPROM.put(EEPROM_CV2, cv2_dest);
EEPROM.put(EEPROM_PATTERNS, patterns);
}
uint8_t GetRandomBit() {
return xorshift16() & 1;
}
// Get 8-bit pseudo-random number
uint8_t GetRandomByte() {
return xorshift16() & 0xFF;
}
void ProcessSequencerTick(uint32_t tick) {
// Assuming 96 PPQN clock. We want 16th notes.
// 96 pulses per quarter note / 4 = 24 pulses per 16th note.
const int PULSES_PER_16TH = 24;
// Pulse logic outputs low halfway through the 16th note (12 pulses)
if (tick % PULSES_PER_16TH == 12) {
for (int i = 0; i < 6; i++) {
gravity.outputs[i].Low();
}
}
// Expansion Pulse Out gate
if (selected_pulse != Clock::PULSE_NONE) {
int pulse_high_ticks = 96; // 1 PPQN
if (selected_pulse == Clock::PULSE_PPQN_4) pulse_high_ticks = 24;
else if (selected_pulse == Clock::PULSE_PPQN_24) pulse_high_ticks = 4;
int pulse_low_ticks = tick + max(pulse_high_ticks / 2, 1);
if (tick % pulse_high_ticks == 0) {
gravity.pulse.High();
} else if (pulse_low_ticks % pulse_high_ticks == 0) {
gravity.pulse.Low();
}
}
// Handle new 16th note step
if (tick % PULSES_PER_16TH == 0) {
// Evaluate CV Preset changes FIRST so params pull from the right struct
int preset_cv = 0;
if (cv1_dest == CV_DEST_PRESET) preset_cv += cv1_val;
if (cv2_dest == CV_DEST_PRESET) preset_cv += cv2_val;
if (cv1_dest == CV_DEST_PRESET || cv2_dest == CV_DEST_PRESET) {
int mapped_slot = constrain(map(preset_cv, 0, 512, 0, 4), 0, 4);
if (mapped_slot != last_mapped_slot) {
last_mapped_slot = mapped_slot;
if (active_pattern != mapped_slot) {
active_pattern = mapped_slot;
selected_slot = mapped_slot + 1;
needs_redraw = true;
}
}
} else {
last_mapped_slot = -1;
}
PatternState &p = patterns[active_pattern];
int active_map_x = p.map_x;
int active_map_y = p.map_y;
int active_chaos = p.chaos_amount;
int active_dens[3] = { p.inst_density[0], p.inst_density[1], p.inst_density[2] };
// Apply CV1
switch (cv1_dest) {
case CV_DEST_KICK_DENS: active_dens[0] = constrain(active_dens[0] + (cv1_val / 4), 0, 255); break;
case CV_DEST_SNARE_DENS: active_dens[1] = constrain(active_dens[1] + (cv1_val / 4), 0, 255); break;
case CV_DEST_HHAT_DENS: active_dens[2] = constrain(active_dens[2] + (cv1_val / 4), 0, 255); break;
case CV_DEST_CHAOS: active_chaos = constrain(active_chaos + (cv1_val / 4), 0, 255); break;
case CV_DEST_MAP_X: active_map_x = constrain(active_map_x + (cv1_val / 4), 0, 255); break;
case CV_DEST_MAP_Y: active_map_y = constrain(active_map_y + (cv1_val / 4), 0, 255); break;
default: break;
}
// Apply CV2
switch (cv2_dest) {
case CV_DEST_KICK_DENS: active_dens[0] = constrain(active_dens[0] + (cv2_val / 4), 0, 255); break;
case CV_DEST_SNARE_DENS: active_dens[1] = constrain(active_dens[1] + (cv2_val / 4), 0, 255); break;
case CV_DEST_HHAT_DENS: active_dens[2] = constrain(active_dens[2] + (cv2_val / 4), 0, 255); break;
case CV_DEST_CHAOS: active_chaos = constrain(active_chaos + (cv2_val / 4), 0, 255); break;
case CV_DEST_MAP_X: active_map_x = constrain(active_map_x + (cv2_val / 4), 0, 255); break;
case CV_DEST_MAP_Y: active_map_y = constrain(active_map_y + (cv2_val / 4), 0, 255); break;
default: break;
}
// Evaluate hits for Kick, Snare, HiHats
for (int inst = 0; inst < 3; inst++) {
uint8_t threshold = GetThreshold(inst, current_step, active_map_x, active_map_y);
int active_density = active_dens[inst];
// Inject chaos
if (active_chaos > 0) {
// Chaos randomly adds or subtracts from density.
int r = GetRandomByte();
int chaos_variance = map(active_chaos, 0, 255, 0, 128);
if (GetRandomBit()) {
active_density += map(r, 0, 255, 0, chaos_variance);
} else {
active_density -= map(r, 0, 255, 0, chaos_variance);
}
active_density = constrain(active_density, 0, 255);
}
// Fire Trigger?
if (active_density > threshold) {
// Output 1-3
gravity.outputs[inst].High();
// Fire Accent Trigger? (If density greatly exceeds threshold)
if (active_density > threshold + 60) {
// Output 4-6
gravity.outputs[inst + 3].High();
}
}
}
current_step = (current_step + 1) % 16;
}
}
void HandleExtClockTick() {
switch (selected_source) {
case Clock::SOURCE_INTERNAL:
case Clock::SOURCE_EXTERNAL_MIDI:
// Use EXT as Reset when not used for clock source.
for (int i = 0; i < 6; i++) {
gravity.outputs[i].Low();
}
gravity.pulse.Low();
gravity.clock.Reset();
current_step = 0;
break;
default:
// Register EXT clock tick.
gravity.clock.Tick();
}
}
void OnPlayPress() {
if (!gravity.clock.IsPaused()) {
gravity.clock.Stop();
for (int i = 0; i < 6; i++)
gravity.outputs[i].Low();
gravity.pulse.Low();
} else {
gravity.clock.Start();
}
needs_redraw = true;
}
void OnShiftPress() {
for (int i = 0; i < 6; i++) {
gravity.outputs[i].Low();
}
gravity.pulse.Low();
gravity.clock.Reset();
current_step = 0;
needs_redraw = true;
}
void OnEncoderPress() {
editing_param = !editing_param;
needs_redraw = true;
}
void OnEncoderPressRotate(int val) {
selected_slot = constrain(selected_slot + val, 0, 5);
if (selected_slot > 0) {
active_pattern = selected_slot - 1;
}
// Reset editing state when swapping pages
editing_param = false;
needs_redraw = true;
}
// Map CV Destination enum string labels
const char* GetCvDestName(CvDest dest) {
switch(dest) {
case CV_DEST_NONE: return "NONE";
case CV_DEST_KICK_DENS: return "KICK DENS";
case CV_DEST_SNARE_DENS: return "SNAR DENS";
case CV_DEST_HHAT_DENS: return "HHAT DENS";
case CV_DEST_CHAOS: return "CHAOS";
case CV_DEST_MAP_X: return "MAP X";
case CV_DEST_MAP_Y: return "MAP Y";
case CV_DEST_PRESET: return "PRESET";
default: return "";
}
}
void OnEncoderRotate(int val) {
if (!editing_param) {
// Navigate menu (clamp to edges, do not wrap)
if (selected_slot > 0) {
int next_param = (int)current_param + val;
next_param = constrain(next_param, 0, PARAM_LAST - 1);
current_param = (SelectedParam)next_param;
} else {
int next_param = (int)current_global_param + val;
next_param = constrain(next_param, 0, PARAM_GLOBAL_LAST - 1);
current_global_param = (GlobalParam)next_param;
}
} else {
// Edit parameter
if (selected_slot > 0) {
int amt = val * 8; // Adjust by 8 values at a time for speed mapping
PatternState &p = patterns[active_pattern];
switch (current_param) {
case PARAM_KICK_DENS:
p.inst_density[0] = constrain(p.inst_density[0] + amt, 0, 255);
break;
case PARAM_SNARE_DENS:
p.inst_density[1] = constrain(p.inst_density[1] + amt, 0, 255);
break;
case PARAM_HIHAT_DENS:
p.inst_density[2] = constrain(p.inst_density[2] + amt, 0, 255);
break;
case PARAM_CHAOS:
p.chaos_amount = constrain(p.chaos_amount + amt, 0, 255);
break;
case PARAM_MAP_X:
p.map_x = constrain(p.map_x + amt, 0, 255);
break;
case PARAM_MAP_Y:
p.map_y = constrain(p.map_y + amt, 0, 255);
break;
default:
break;
}
} else {
// Edit Global Parameter
switch (current_global_param) {
case PARAM_GLOBAL_CLK_SRC: {
int src = (int)selected_source + val;
src = constrain(src, 0, Clock::SOURCE_LAST - 1);
selected_source = (Clock::Source)src;
gravity.clock.SetSource(selected_source);
break;
}
case PARAM_GLOBAL_BPM: {
if (!gravity.clock.ExternalSource()) {
gravity.clock.SetTempo(gravity.clock.Tempo() + val);
}
break;
}
case PARAM_GLOBAL_CV1_DEST: {
int dest = (int)cv1_dest + val;
dest = constrain(dest, 0, CV_DEST_LAST - 1);
cv1_dest = (CvDest)dest;
break;
}
case PARAM_GLOBAL_CV2_DEST: {
int dest = (int)cv2_dest + val;
dest = constrain(dest, 0, CV_DEST_LAST - 1);
cv2_dest = (CvDest)dest;
break;
}
case PARAM_GLOBAL_PULSE_OUT: {
int pulse = (int)selected_pulse + val;
pulse = constrain(pulse, 0, Clock::PULSE_LAST - 1);
selected_pulse = (Clock::Pulse)pulse;
if (selected_pulse == Clock::PULSE_NONE) {
gravity.pulse.Low();
}
break;
}
default:
break;
}
}
eeprom_needs_save = true;
last_param_change = millis();
}
needs_redraw = true;
}
#include "display_assets.h"
constexpr uint8_t SCREEN_CENTER_X = 32;
constexpr uint8_t MAIN_TEXT_Y = 26;
constexpr uint8_t SUB_TEXT_Y = 40;
constexpr uint8_t VISIBLE_MENU_ITEMS = 3;
constexpr uint8_t MENU_ITEM_HEIGHT = 14;
constexpr uint8_t MENU_BOX_PADDING = 4;
constexpr uint8_t MENU_BOX_WIDTH = 64;
void drawCenteredText(const char *text, int y, const uint8_t *font) {
gravity.display.setFont(font);
int textWidth = gravity.display.getStrWidth(text);
gravity.display.drawStr(SCREEN_CENTER_X - (textWidth / 2), y, text);
}
void drawRightAlignedText(const char *text, int y) {
int textWidth = gravity.display.getStrWidth(text);
int drawX = (SCREEN_WIDTH - textWidth) - MENU_BOX_PADDING;
gravity.display.drawStr(drawX, y, text);
}
void drawMainSelection() {
gravity.display.setDrawColor(1);
const int tickSize = 3;
const int mainWidth = SCREEN_WIDTH / 2;
const int mainHeight = 49;
gravity.display.drawLine(0, 0, tickSize, 0);
gravity.display.drawLine(0, 0, 0, tickSize);
gravity.display.drawLine(mainWidth, 0, mainWidth - tickSize, 0);
gravity.display.drawLine(mainWidth, 0, mainWidth, tickSize);
gravity.display.drawLine(mainWidth, mainHeight, mainWidth,
mainHeight - tickSize);
gravity.display.drawLine(mainWidth, mainHeight, mainWidth - tickSize,
mainHeight);
gravity.display.drawLine(0, mainHeight, tickSize, mainHeight);
gravity.display.drawLine(0, mainHeight, 0, mainHeight - tickSize);
}
const char str_kick[] PROGMEM = "KICK";
const char str_snare[] PROGMEM = "SNARE";
const char str_hhat[] PROGMEM = "HHAT";
const char str_chaos[] PROGMEM = "CHAOS";
const char str_mapx[] PROGMEM = "MAP X";
const char str_mapy[] PROGMEM = "MAP Y";
const char* const param_menu_items[] PROGMEM = {str_kick, str_snare, str_hhat, str_chaos, str_mapx, str_mapy};
const char str_source[] PROGMEM = "SOURCE";
const char str_tempo[] PROGMEM = "TEMPO";
const char str_cv1dest[] PROGMEM = "CV1 DEST";
const char str_cv2dest[] PROGMEM = "CV2 DEST";
const char str_pulse[] PROGMEM = "PULSE OUT";
const char* const global_menu_items[] PROGMEM = {str_source, str_tempo, str_cv1dest, str_cv2dest, str_pulse};
void drawMenuItems(const char* const menu_items[], int menu_size, int current_item) {
gravity.display.setFont(TEXT_FONT);
int selectedBoxY = 0;
if (menu_size >= VISIBLE_MENU_ITEMS && current_item == menu_size - 1) {
selectedBoxY = MENU_ITEM_HEIGHT * min(2, current_item);
} else if (current_item > 0) {
selectedBoxY = MENU_ITEM_HEIGHT;
}
int boxX = MENU_BOX_WIDTH + 1;
int boxY = selectedBoxY + 2;
int boxWidth = MENU_BOX_WIDTH - 1;
int boxHeight = MENU_ITEM_HEIGHT + 1;
gravity.display.setDrawColor(1);
if (editing_param) {
gravity.display.drawBox(boxX, boxY, boxWidth, boxHeight);
drawMainSelection();
} else {
gravity.display.drawFrame(boxX, boxY, boxWidth, boxHeight);
}
gravity.display.setDrawColor(2);
int start_index = 0;
if (menu_size >= VISIBLE_MENU_ITEMS && current_item == menu_size - 1) {
start_index = menu_size - VISIBLE_MENU_ITEMS;
} else if (current_item > 0) {
start_index = current_item - 1;
}
for (int i = 0; i < min(menu_size, (int)VISIBLE_MENU_ITEMS); ++i) {
int idx = start_index + i;
char buffer[16];
strcpy_P(buffer, (char*)pgm_read_ptr(&(menu_items[idx])));
drawRightAlignedText(buffer, MENU_ITEM_HEIGHT * (i + 1) - 1);
}
}
void DisplayMainArea() {
gravity.display.setFontMode(1);
gravity.display.setDrawColor(2);
char mainText[16] = "";
char subText[16] = "";
if (selected_slot > 0) {
PatternState &p = patterns[active_pattern];
int act_val = 0;
switch (current_param) {
case PARAM_KICK_DENS: act_val = p.inst_density[0]; break;
case PARAM_SNARE_DENS: act_val = p.inst_density[1]; break;
case PARAM_HIHAT_DENS: act_val = p.inst_density[2]; break;
case PARAM_CHAOS: act_val = p.chaos_amount; break;
case PARAM_MAP_X: act_val = p.map_x; break;
case PARAM_MAP_Y: act_val = p.map_y; break;
default: break;
}
if (!editing_param) {
if ((int)cv1_dest == (int)current_param + 1) act_val += cv1_val / 4;
if ((int)cv2_dest == (int)current_param + 1) act_val += cv2_val / 4;
act_val = constrain(act_val, 0, 255);
}
switch (current_param) {
case PARAM_KICK_DENS:
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "KICK DENS");
break;
case PARAM_SNARE_DENS:
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "SNAR DENS");
break;
case PARAM_HIHAT_DENS:
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "HHAT DENS");
break;
case PARAM_CHAOS:
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
strcat(mainText, "%");
strcpy(subText, "CHAOS");
break;
case PARAM_MAP_X:
itoa(act_val, mainText, 10);
strcpy(subText, "MAP X");
break;
case PARAM_MAP_Y:
itoa(act_val, mainText, 10);
strcpy(subText, "MAP Y");
break;
default:
break;
}
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
drawMenuItems(param_menu_items, PARAM_LAST, (int)current_param);
} else {
switch (current_global_param) {
case PARAM_GLOBAL_CLK_SRC:
switch (selected_source) {
case Clock::SOURCE_INTERNAL:
strcpy_P(mainText, PSTR("INT"));
strcpy_P(subText, PSTR("CLOCK"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_24:
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("24 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_4:
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("4 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_2:
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("2 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_PPQN_1:
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("1 PPQN"));
break;
case Clock::SOURCE_EXTERNAL_MIDI:
strcpy_P(mainText, PSTR("EXT"));
strcpy_P(subText, PSTR("MIDI"));
break;
}
break;
case PARAM_GLOBAL_BPM:
if (gravity.clock.ExternalSource()) {
strcpy_P(mainText, PSTR("EXT"));
} else {
itoa(gravity.clock.Tempo(), mainText, 10);
}
strcpy_P(subText, PSTR("BPM"));
break;
case PARAM_GLOBAL_CV1_DEST:
strcpy_P(mainText, PSTR("CV1"));
strcpy(subText, GetCvDestName(cv1_dest));
break;
case PARAM_GLOBAL_CV2_DEST:
strcpy_P(mainText, PSTR("CV2"));
strcpy(subText, GetCvDestName(cv2_dest));
break;
case PARAM_GLOBAL_PULSE_OUT:
switch (selected_pulse) {
case Clock::PULSE_NONE:
strcpy_P(mainText, PSTR("OFF"));
break;
case Clock::PULSE_PPQN_24:
strcpy_P(mainText, PSTR("24"));
break;
case Clock::PULSE_PPQN_4:
strcpy_P(mainText, PSTR("4"));
break;
case Clock::PULSE_PPQN_1:
strcpy_P(mainText, PSTR("1"));
break;
default:
break;
}
strcpy_P(subText, PSTR("PPQN"));
break;
default:
break;
}
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
drawMenuItems(global_menu_items, PARAM_GLOBAL_LAST, (int)current_global_param);
}
}
void DisplayBottomBar() {
int boxY = 50;
int boxHeight = 14;
gravity.display.setDrawColor(1);
gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2);
gravity.display.drawVLine(SCREEN_WIDTH - 1, boxY, boxHeight);
for (int i = 0; i <= 5; i++) {
int x = i * 21;
int bw = (i == 5) ? (SCREEN_WIDTH - x) : 21;
gravity.display.setDrawColor(1);
if (selected_slot == i) {
gravity.display.drawBox(x, boxY, bw, boxHeight);
} else {
gravity.display.drawVLine(x, boxY, boxHeight);
}
gravity.display.setDrawColor(2);
if (i == 0) {
gravity.display.setBitmapMode(1);
auto icon = gravity.clock.IsPaused() ? pause_icon : play_icon;
int iconX = x + (bw / 2) - (play_icon_width / 2);
gravity.display.drawXBMP(iconX, boxY, play_icon_width, play_icon_height, icon);
} else {
char label[2] = {(char)('A' + i - 1), '\0'};
gravity.display.setFont(TEXT_FONT);
int patW = gravity.display.getStrWidth(label);
int patX = x + (bw / 2) - (patW / 2);
gravity.display.drawStr(patX, 64 - 3, label);
}
}
}
void UpdateDisplay() {
gravity.display.setFontMode(1);
DisplayMainArea();
DisplayBottomBar();
}
void setup() {
gravity.Init();
LoadState();
gravity.play_button.AttachPressHandler(OnPlayPress);
gravity.shift_button.AttachPressHandler(OnShiftPress);
gravity.encoder.AttachPressHandler(OnEncoderPress);
gravity.encoder.AttachRotateHandler(OnEncoderRotate);
gravity.encoder.AttachPressRotateHandler(OnEncoderPressRotate);
gravity.clock.AttachIntHandler(ProcessSequencerTick);
gravity.clock.AttachExtHandler(HandleExtClockTick);
// Default to 120 BPM internal
gravity.clock.SetTempo(120);
gravity.clock.SetSource(Clock::SOURCE_INTERNAL);
// Speed up I2C for faster OLED refreshing
Wire.setClock(400000);
}
void loop() {
gravity.Process();
static int last_cv1_val = 0;
static int last_cv2_val = 0;
int cv1_temp = gravity.cv1.Read(); // -512 to 512
int cv2_temp = gravity.cv2.Read();
noInterrupts();
cv1_val = cv1_temp;
cv2_val = cv2_temp;
interrupts();
// Trigger redraw if we're not editing and the CV modulating the selected param changes
if (!editing_param && selected_slot > 0) {
if ((int)cv1_dest == (int)current_param + 1) {
if (abs(cv1_val - last_cv1_val) >= 4) {
needs_redraw = true;
last_cv1_val = cv1_val;
}
} else {
last_cv1_val = cv1_val;
}
if ((int)cv2_dest == (int)current_param + 1) {
if (abs(cv2_val - last_cv2_val) >= 4) {
needs_redraw = true;
last_cv2_val = cv2_val;
}
} else {
last_cv2_val = cv2_val;
}
}
if (eeprom_needs_save && (millis() - last_param_change > SAVE_DELAY_MS)) {
SaveState();
eeprom_needs_save = false;
}
if (needs_redraw && (millis() - last_redraw > REDRAW_DELAY_MS)) {
needs_redraw = false;
last_redraw = millis();
gravity.display.firstPage();
do {
UpdateDisplay();
} while (gravity.display.nextPage());
}
}

View File

@ -1,90 +0,0 @@
/*
* 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") =
"\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};

View File

@ -1,136 +0,0 @@
#ifndef MAPS_H
#define MAPS_H
#include <stdint.h>
#include <avr/pgmspace.h>
// 4x4 Grid of Patterns (X = Genre, Y = Variation)
// X=0: House/Techno (4 on floor)
// X=1: Breakbeat/Drum&Bass (Syncopated)
// X=2: HipHop/Trap (Half-time feel)
// X=3: Dem Bow / Reggaeton (Tresillo rhythm)
// Y=0: Sparse/Basic
// Y=1: Standard
// Y=2: Syncopated/Groove
// Y=3: Busy/Fills
// 3 Instruments (Kick=0, Snare=1, HiHat=2)
// 16 Steps per pattern.
// Values are "Thresholds" (0-255). A step fires if the channel's Density setting > Threshold.
// A Threshold of 255 means it NEVER fires. 0 means it ALWAYS fires (even at minimum density).
const uint8_t PATTERN_MAPS[4][4][3][16] PROGMEM = {
// X = 0 (House)
{
// Y = 0 (Sparse)
{
{ 20, 255, 255, 255, 20, 255, 255, 255, 20, 255, 255, 255, 20, 255, 255, 255 }, // Kick
{ 255, 255, 255, 255, 50, 255, 255, 255, 255, 255, 255, 255, 50, 255, 255, 255 }, // Snare
{ 255, 255, 60, 255, 255, 255, 60, 255, 255, 255, 60, 255, 255, 255, 60, 255 } // HiHat (Offbeats)
},
// Y = 1 (Standard)
{
{ 10, 255, 200, 255, 10, 255, 200, 255, 10, 255, 200, 255, 10, 255, 200, 255 }, // Kick
{ 255, 255, 255, 255, 30, 255, 255, 200, 255, 255, 255, 255, 30, 255, 255, 200 }, // Snare
{ 150, 80, 40, 80, 150, 80, 40, 80, 150, 80, 40, 80, 150, 80, 40, 80 } // HiHat
},
// Y = 2 (Syncopated Break/Groove)
{
{ 10, 255, 180, 255, 255, 180, 150, 255, 10, 255, 180, 200, 255, 180, 150, 255 }, // Kick (pushing beats)
{ 255, 200, 255, 200, 20, 200, 255, 150, 255, 200, 255, 200, 20, 200, 255, 150 }, // Snare (ghost notes)
{ 60, 150, 20, 150, 80, 150, 20, 150, 60, 150, 20, 150, 80, 150, 20, 150 } // HiHat (dynamic 16ths)
},
// Y = 3 (Busy / Driving)
{
{ 5, 180, 150, 200, 5, 180, 150, 200, 5, 180, 150, 200, 5, 180, 150, 200 }, // Kick
{ 255, 200, 200, 255, 20, 200, 200, 180, 255, 200, 200, 255, 20, 200, 200, 180 }, // Snare
{ 20, 60, 10, 60, 20, 60, 10, 60, 20, 60, 10, 60, 20, 60, 10, 60 } // HiHat (accented 16ths)
}
},
// X = 1 (Breakbeat)
{
// Y = 0 (Sparse)
{
{ 20, 255, 255, 255, 255, 255, 100, 255, 255, 255, 255, 255, 255, 255, 255, 255 }, // Kick (1, 3& half)
{ 255, 255, 255, 255, 50, 255, 255, 255, 255, 255, 255, 255, 50, 255, 255, 255 }, // Snare (2, 4)
{ 60, 255, 150, 255, 60, 255, 150, 255, 60, 255, 150, 255, 60, 255, 150, 255 } // HiHat (8ths)
},
// Y = 1 (Standard)
{
{ 10, 255, 150, 255, 255, 180, 80, 200, 255, 150, 255, 200, 255, 255, 150, 255 }, // Kick
{ 255, 255, 200, 255, 30, 255, 255, 150, 255, 255, 180, 255, 30, 255, 200, 180 }, // Snare (syncopated ghosts)
{ 40, 120, 80, 120, 40, 120, 80, 120, 40, 120, 80, 120, 40, 120, 80, 120 } // HiHat
},
// Y = 2 (Jungle / Fast Syncopation)
{
{ 10, 200, 255, 150, 255, 200, 80, 255, 10, 200, 255, 150, 255, 200, 80, 200 }, // Kick
{ 255, 150, 200, 180, 20, 200, 150, 100, 255, 150, 200, 180, 20, 200, 150, 100 }, // Snare (heavy ghosts & rolls)
{ 50, 150, 150, 100, 50, 150, 150, 100, 50, 150, 150, 100, 50, 150, 150, 100 } // HiHat (dynamic 16ths)
},
// Y = 3 (Busy Breakcore)
{
{ 5, 150, 120, 150, 180, 100, 50, 120, 150, 80, 150, 100, 200, 80, 100, 150 }, // Kick
{ 200, 180, 150, 200, 20, 150, 180, 100, 180, 120, 100, 150, 20, 100, 120, 80 }, // Snare
{ 20, 50, 40, 50, 20, 50, 40, 50, 20, 50, 40, 50, 20, 50, 40, 50 } // HiHat (16ths + dynamic)
}
},
// X = 2 (HipHop)
{
// Y = 0 (Sparse)
{
{ 20, 255, 255, 255, 255, 255, 255, 255, 100, 255, 255, 255, 255, 255, 255, 255 }, // Kick (1, 3)
{ 255, 255, 255, 255, 60, 255, 255, 255, 255, 255, 255, 255, 60, 255, 255, 255 }, // Snare (2, 4)
{ 80, 255, 255, 255, 80, 255, 255, 255, 80, 255, 255, 255, 80, 255, 255, 255 } // HiHat (Quarters)
},
// Y = 1 (Standard)
{
{ 10, 255, 255, 150, 255, 255, 255, 200, 80, 255, 150, 255, 255, 255, 255, 200 }, // Kick
{ 255, 255, 255, 255, 40, 255, 255, 255, 255, 255, 255, 255, 40, 255, 200, 255 }, // Snare
{ 50, 150, 50, 150, 50, 150, 50, 150, 50, 150, 50, 150, 50, 150, 50, 150 } // HiHat (8ths)
},
// Y = 2 (Boom Bap / Swing)
{
{ 10, 255, 150, 200, 255, 255, 150, 200, 50, 255, 100, 255, 255, 200, 150, 255 }, // Kick (Swung 16ths)
{ 255, 255, 200, 255, 30, 255, 200, 150, 255, 255, 200, 255, 30, 200, 255, 150 }, // Snare (Ghost notes)
{ 30, 200, 80, 150, 30, 200, 80, 150, 30, 200, 80, 150, 30, 200, 80, 150 } // HiHat (Heavy swing feel)
},
// Y = 3 (Busy / Trap)
{
{ 5, 200, 150, 80, 200, 180, 150, 120, 40, 150, 80, 200, 150, 180, 120, 100 }, // Kick
{ 255, 255, 200, 255, 20, 200, 255, 150, 255, 255, 180, 255, 20, 150, 180, 100 }, // Snare (with trap rolls)
{ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20 } // HiHat (rapid 16ths)
}
},
// X = 3 (Dem Bow / Reggaeton - Tresillo Rhythm)
{
// Y = 0 (Sparse)
{
{ 20, 255, 255, 255, 20, 255, 255, 255, 20, 255, 255, 255, 20, 255, 255, 255 }, // Kick (4 on floor)
{ 255, 255, 255, 60, 255, 255, 60, 255, 255, 255, 255, 60, 255, 255, 60, 255 }, // Snare (Tresillo off-beats)
{ 255, 255, 150, 255, 255, 255, 150, 255, 255, 255, 150, 255, 255, 255, 150, 255 } // HiHat (basic offbeats)
},
// Y = 1 (Standard)
{
{ 10, 255, 200, 255, 10, 255, 200, 255, 10, 255, 200, 255, 10, 255, 200, 255 }, // Kick
{ 255, 255, 255, 30, 255, 255, 30, 200, 255, 255, 255, 30, 255, 255, 30, 200 }, // Snare (Strong Tresillo + syncopation)
{ 100, 200, 50, 200, 100, 200, 50, 200, 100, 200, 50, 200, 100, 200, 50, 200 } // HiHat (8th note swing)
},
// Y = 2 (Syncopated Tresillo)
{
{ 10, 255, 150, 200, 10, 255, 150, 200, 10, 255, 150, 200, 10, 255, 150, 200 }, // Kick (Driving but syncopated)
{ 255, 200, 255, 20, 200, 255, 20, 150, 255, 200, 255, 20, 200, 255, 20, 150 }, // Snare (Pushing the backbeat)
{ 50, 150, 100, 150, 50, 150, 100, 150, 50, 150, 100, 150, 50, 150, 100, 150 } // HiHat (Syncopated ride)
},
// Y = 3 (Busy / Rolls)
{
{ 5, 200, 150, 120, 5, 200, 150, 120, 5, 200, 150, 120, 5, 200, 150, 120 }, // Kick (driving)
{ 255, 180, 200, 20, 180, 200, 20, 120, 255, 180, 200, 20, 180, 200, 20, 80 }, // Snare (Dem Bow rolls)
{ 30, 80, 40, 80, 30, 80, 40, 80, 30, 80, 40, 80, 30, 80, 40, 80 } // HiHat (16ths riding on the tresillo)
}
}
};
#endif

View File

@ -1,4 +1,4 @@
version=2.0.1
version=2.0.1beta1
author=Adam Wonak
maintainer=awonak <github.com/awonak>
sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module

View File

@ -42,8 +42,7 @@ public:
old_read_ = read_;
int raw = analogRead(pin_);
read_ = map(raw, 0, MAX_INPUT, low_, high_);
// Cast to long to avoid AVR 16-bit integer overflow prior to constraining
read_ = constrain((long)read_ - (long)offset_, -512, 512);
read_ = constrain(read_ - offset_, -512, 512);
if (inverted_)
read_ = -read_;
}
@ -54,20 +53,8 @@ public:
void AdjustCalibrationHigh(int amount) { high_ += amount; }
void SetCalibrationLow(int low) { low_ = low; }
void SetCalibrationHigh(int high) { high_ = high; }
int GetCalibrationLow() const { return low_; }
int GetCalibrationHigh() const { return high_; }
void SetOffset(float percent) { offset_ = -(percent) * 512; }
void AdjustOffset(int amount) { offset_ += amount; }
int GetOffset() const { return offset_; }
void SetAttenuation(float percent) {
low_ = abs(percent) * CALIBRATED_LOW;
high_ = abs(percent) * CALIBRATED_HIGH;