9 Commits

7 changed files with 961 additions and 6 deletions

View File

@ -0,0 +1,435 @@
#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

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

View File

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

371
firmware/Rhythm/Rhythm.ino Normal file
View File

@ -0,0 +1,371 @@
#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_DENS_K = 1;
const int EEPROM_DENS_S = 3;
const int EEPROM_DENS_H = 5;
const int EEPROM_MAP_X = 7;
const int EEPROM_MAP_Y = 9;
const int EEPROM_CHAOS = 11;
const byte EEPROM_INIT_FLAG = 0xAC; // 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;
// 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;
bool is_playing = false;
// Engine Parameters (0-255)
int inst_density[3] = {128, 128,
128}; // Default 50% density for kick, snare, hihat
int map_x = 0; // 0 to 255 (0 = House, 127 = Breakbeat, 255 = Hiphop)
int map_y = 127; // 0 to 255 (0 = Sparse, 127 = Standard, 255 = Busy)
int chaos_amount = 0; // 0 to 255
volatile int cv1_val = 0;
volatile int cv2_val = 0;
// LFSR State for Chaos
uint16_t lfsr = 0xACE1;
// 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 = x_pos / 85;
int y_idx = y_pos / 85;
uint8_t x_frac = (x_pos % 85) * 3; // scale remainder 0-84 up to 0-255
uint8_t y_frac = (y_pos % 85) * 3;
// Guard against out of bounds if exactly 255
if (x_idx >= 3) {
x_idx = 2;
x_frac = 255;
}
if (y_idx >= 3) {
y_idx = 2;
y_frac = 255;
}
// 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_DENS_K, inst_density[0]);
EEPROM.get(EEPROM_DENS_S, inst_density[1]);
EEPROM.get(EEPROM_DENS_H, inst_density[2]);
EEPROM.get(EEPROM_MAP_X, map_x);
EEPROM.get(EEPROM_MAP_Y, map_y);
EEPROM.get(EEPROM_CHAOS, chaos_amount);
}
}
void SaveState() {
EEPROM.update(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG);
EEPROM.put(EEPROM_DENS_K, inst_density[0]);
EEPROM.put(EEPROM_DENS_S, inst_density[1]);
EEPROM.put(EEPROM_DENS_H, inst_density[2]);
EEPROM.put(EEPROM_MAP_X, map_x);
EEPROM.put(EEPROM_MAP_Y, map_y);
EEPROM.put(EEPROM_CHAOS, chaos_amount);
}
// LFSR random bit generator (returns 0 or 1, fast)
uint8_t GetRandomBit() {
uint8_t bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1;
lfsr = (lfsr >> 1) | (bit << 15);
return bit;
}
// Get 8-bit pseudo-random number
uint8_t GetRandomByte() {
uint8_t r = 0;
for (int i = 0; i < 8; i++) {
r = (r << 1) | GetRandomBit();
}
return r;
}
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();
}
}
// Handle new 16th note step
if (tick % PULSES_PER_16TH == 0) {
int mod_map_x = constrain(map_x + (cv1_val / 2), 0, 255);
int active_chaos = constrain(chaos_amount + (cv2_val / 2), 0, 255);
// Evaluate hits for Kick, Snare, HiHats
for (int inst = 0; inst < 3; inst++) {
uint8_t threshold = GetThreshold(inst, current_step, mod_map_x, map_y);
int active_density = inst_density[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 OnPlayPress() {
if (is_playing) {
gravity.clock.Stop();
for (int i = 0; i < 6; i++)
gravity.outputs[i].Low();
} else {
gravity.clock.Start();
}
is_playing = !is_playing;
needs_redraw = true;
}
void OnEncoderPress() {
editing_param = !editing_param;
needs_redraw = true;
}
void OnEncoderRotate(int val) {
if (!editing_param) {
// Navigate menu (clamp to edges, do not wrap)
int next_param = (int)current_param + val;
next_param = constrain(next_param, 0, PARAM_LAST - 1);
current_param = (SelectedParam)next_param;
} else {
// Edit parameter
int amt = val * 8; // Adjust by 8 values at a time for speed mapping
switch (current_param) {
case PARAM_KICK_DENS:
inst_density[0] = constrain(inst_density[0] + amt, 0, 255);
break;
case PARAM_SNARE_DENS:
inst_density[1] = constrain(inst_density[1] + amt, 0, 255);
break;
case PARAM_HIHAT_DENS:
inst_density[2] = constrain(inst_density[2] + amt, 0, 255);
break;
case PARAM_CHAOS:
chaos_amount = constrain(chaos_amount + amt, 0, 255);
break;
case PARAM_MAP_X:
map_x = constrain(map_x + amt, 0, 255);
break;
case PARAM_MAP_Y:
map_y = constrain(map_y + amt, 0, 255);
break;
default:
break;
}
eeprom_needs_save = true;
last_param_change = millis();
}
needs_redraw = true;
}
void DrawBarGraph(int y, const char *label, int value, bool is_selected) {
// Reset draw color to default foreground
gravity.display.setDrawColor(1);
gravity.display.setCursor(0, y);
if (is_selected) {
gravity.display.print(">");
if (editing_param) {
// Draw solid white box behind the label
gravity.display.drawBox(6, y - 8, 26, 10);
// Switch to black text to 'cut out' the label from the box
gravity.display.setDrawColor(0);
}
} else {
gravity.display.print(" ");
}
gravity.display.setCursor(6, y);
gravity.display.print(label);
// Restore draw color to white for the bar and text
gravity.display.setDrawColor(1);
// Draw Bar
int barLen = map(value, 0, 255, 0, 60);
gravity.display.drawFrame(34, y - 8, 60, 8);
gravity.display.drawBox(34, y - 8, barLen, 8);
// Draw value percentage
gravity.display.setCursor(98, y);
int pct = map(value, 0, 255, 0, 100);
gravity.display.print(pct);
gravity.display.print("%");
}
void UpdateDisplay() {
gravity.display.setFontMode(1);
gravity.display.setDrawColor(1);
gravity.display.setFont(u8g2_font_5x7_tf);
// Header
gravity.display.setCursor(0, 7);
if (is_playing)
gravity.display.print("[>] PLAY");
else
gravity.display.print("[||] PAUS");
gravity.display.setCursor(55, 7);
gravity.display.print("BPM:");
gravity.display.print(gravity.clock.Tempo());
gravity.display.drawHLine(0, 10, 128);
// Parameters List (Scrollable window of 5 items)
int y_start = 20;
int y_spacing = 9;
// Calculate window start index
int window_start = max(0, min((int)current_param - 2, PARAM_LAST - 5));
for (int i = 0; i < 5; i++) {
int param_idx = window_start + i;
int y_pos = y_start + (y_spacing * i);
bool is_sel = (current_param == param_idx);
switch (param_idx) {
case PARAM_KICK_DENS:
DrawBarGraph(y_pos, "KICK", inst_density[0], is_sel);
break;
case PARAM_SNARE_DENS:
DrawBarGraph(y_pos, "SNAR", inst_density[1], is_sel);
break;
case PARAM_HIHAT_DENS:
DrawBarGraph(y_pos, "HHAT", inst_density[2], is_sel);
break;
case PARAM_CHAOS:
DrawBarGraph(y_pos, "CHAO", chaos_amount, is_sel);
break;
case PARAM_MAP_X:
DrawBarGraph(y_pos, "MAPX", map_x, is_sel);
break;
case PARAM_MAP_Y:
DrawBarGraph(y_pos, "MAPY", map_y, is_sel);
break;
}
}
}
void setup() {
gravity.Init();
LoadState();
gravity.play_button.AttachPressHandler(OnPlayPress);
gravity.encoder.AttachPressHandler(OnEncoderPress);
gravity.encoder.AttachRotateHandler(OnEncoderRotate);
gravity.clock.AttachIntHandler(ProcessSequencerTick);
// 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();
// Apply CV modulation
// CV1 modulates Map X, CV2 modulates Chaos
cv1_val = gravity.cv1.Read(); // -512 to 512
cv2_val = gravity.cv2.Read();
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());
}
}

136
firmware/Rhythm/maps.h Normal file
View File

@ -0,0 +1,136 @@
#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.1beta1 version=2.0.1
author=Adam Wonak author=Adam Wonak
maintainer=awonak <github.com/awonak> maintainer=awonak <github.com/awonak>
sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module

View File

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