Compare commits
6 Commits
rhythm
...
ab80642afb
| Author | SHA1 | Date | |
|---|---|---|---|
| ab80642afb | |||
| 62a74fe3ee | |||
| 624d453b9d | |||
| bd08ac4352 | |||
| 763d58f411 | |||
| 6d38c6b36b |
@ -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.
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user