Compare commits
8 Commits
main
...
e363c05823
| Author | SHA1 | Date | |
|---|---|---|---|
| e363c05823 | |||
| 59fbd37524 | |||
| 37711bc6ef | |||
| 50c551f198 | |||
| ecc6ae01c6 | |||
| a70b567a33 | |||
| 8b4b96e65a | |||
| 7c06da08b4 |
@ -13,9 +13,6 @@ const int EEPROM_COMP1_SHIFT = 13;
|
|||||||
const int EEPROM_COMP1_SIZE = 15;
|
const int EEPROM_COMP1_SIZE = 15;
|
||||||
const int EEPROM_COMP2_SHIFT = 17;
|
const int EEPROM_COMP2_SHIFT = 17;
|
||||||
const int EEPROM_COMP2_SIZE = 19;
|
const int EEPROM_COMP2_SIZE = 19;
|
||||||
const int EEPROM_ENCODER_DIR = 21;
|
|
||||||
const int EEPROM_HYSTERESIS = 23;
|
|
||||||
const int EEPROM_DISPLAY_CV = 25;
|
|
||||||
const byte EEPROM_INIT_FLAG = 0xAB; // Update flag to re-init
|
const byte EEPROM_INIT_FLAG = 0xAB; // Update flag to re-init
|
||||||
|
|
||||||
// EEPROM Delay Save
|
// EEPROM Delay Save
|
||||||
@ -23,20 +20,11 @@ const unsigned long SAVE_DELAY_MS = 5000;
|
|||||||
bool eeprom_needs_save = false;
|
bool eeprom_needs_save = false;
|
||||||
unsigned long last_param_change = 0;
|
unsigned long last_param_change = 0;
|
||||||
|
|
||||||
enum AppMode { MODE_COMPARATOR, MODE_SETTINGS, MODE_CALIBRATION };
|
enum AppMode { MODE_COMPARATOR, MODE_CALIBRATION };
|
||||||
AppMode current_mode = MODE_COMPARATOR;
|
AppMode current_mode = MODE_COMPARATOR;
|
||||||
byte cal_selected_param = 0; // 0=CV1 Low, 1=CV1 Offset, 2=CV1 High, 3=CV2 Low,
|
byte cal_selected_param = 0; // 0=CV1 Low, 1=CV1 Offset, 2=CV1 High, 3=CV2 Low,
|
||||||
// 4=CV2 Offset, 5=CV2 High
|
// 4=CV2 Offset, 5=CV2 High
|
||||||
|
|
||||||
// Settings Menu State
|
|
||||||
bool encoder_reversed = false;
|
|
||||||
int hysteresis = 4;
|
|
||||||
bool display_cv = true;
|
|
||||||
bool settings_editing = false;
|
|
||||||
|
|
||||||
enum SettingsParameter { SETTING_ENCODER_DIR, SETTING_HYSTERESIS, SETTING_DISPLAY_CV };
|
|
||||||
SettingsParameter settings_selected_param = SETTING_ENCODER_DIR;
|
|
||||||
|
|
||||||
// UI Parameters
|
// UI Parameters
|
||||||
enum Parameter { COMP1_SHIFT, COMP1_SIZE, COMP2_SHIFT, COMP2_SIZE };
|
enum Parameter { COMP1_SHIFT, COMP1_SIZE, COMP2_SHIFT, COMP2_SIZE };
|
||||||
|
|
||||||
@ -56,6 +44,7 @@ int last_cv1_draw = -1000;
|
|||||||
int last_cv2_draw = -1000;
|
int last_cv2_draw = -1000;
|
||||||
|
|
||||||
unsigned long last_redraw = 0;
|
unsigned long last_redraw = 0;
|
||||||
|
bool prev_both_buttons = false;
|
||||||
|
|
||||||
// Calibration Methods
|
// Calibration Methods
|
||||||
void LoadCalibration() {
|
void LoadCalibration() {
|
||||||
@ -78,12 +67,6 @@ void LoadCalibration() {
|
|||||||
EEPROM.get(EEPROM_COMP1_SIZE, comp1_size);
|
EEPROM.get(EEPROM_COMP1_SIZE, comp1_size);
|
||||||
EEPROM.get(EEPROM_COMP2_SHIFT, comp2_shift);
|
EEPROM.get(EEPROM_COMP2_SHIFT, comp2_shift);
|
||||||
EEPROM.get(EEPROM_COMP2_SIZE, comp2_size);
|
EEPROM.get(EEPROM_COMP2_SIZE, comp2_size);
|
||||||
|
|
||||||
EEPROM.get(EEPROM_ENCODER_DIR, encoder_reversed);
|
|
||||||
EEPROM.get(EEPROM_HYSTERESIS, hysteresis);
|
|
||||||
EEPROM.get(EEPROM_DISPLAY_CV, display_cv);
|
|
||||||
if (hysteresis < 0 || hysteresis > 16) hysteresis = 4; // Bounds check
|
|
||||||
gravity.encoder.SetReverseDirection(encoder_reversed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,52 +90,12 @@ void SaveCalibration() {
|
|||||||
EEPROM.put(EEPROM_COMP1_SIZE, comp1_size);
|
EEPROM.put(EEPROM_COMP1_SIZE, comp1_size);
|
||||||
EEPROM.put(EEPROM_COMP2_SHIFT, comp2_shift);
|
EEPROM.put(EEPROM_COMP2_SHIFT, comp2_shift);
|
||||||
EEPROM.put(EEPROM_COMP2_SIZE, comp2_size);
|
EEPROM.put(EEPROM_COMP2_SIZE, comp2_size);
|
||||||
|
|
||||||
EEPROM.put(EEPROM_ENCODER_DIR, encoder_reversed);
|
|
||||||
EEPROM.put(EEPROM_HYSTERESIS, hysteresis);
|
|
||||||
EEPROM.put(EEPROM_DISPLAY_CV, display_cv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
void OnEncoderPress() {
|
|
||||||
if (current_mode == MODE_SETTINGS) {
|
|
||||||
settings_editing = !settings_editing;
|
|
||||||
needs_redraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HandlePressedRotate(int val) {
|
|
||||||
int change = val > 0 ? 1 : -1;
|
|
||||||
int next_mode = ((int)current_mode + change) % 3;
|
|
||||||
if (next_mode < 0) next_mode += 3;
|
|
||||||
|
|
||||||
// Cleanup current mode
|
|
||||||
if (current_mode == MODE_SETTINGS) {
|
|
||||||
settings_editing = false;
|
|
||||||
SaveCalibration();
|
|
||||||
gravity.encoder.SetReverseDirection(encoder_reversed);
|
|
||||||
} else if (current_mode == MODE_CALIBRATION) {
|
|
||||||
SaveCalibration();
|
|
||||||
}
|
|
||||||
|
|
||||||
current_mode = (AppMode)next_mode;
|
|
||||||
|
|
||||||
// Setup new mode
|
|
||||||
if (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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
needs_redraw = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnPlayPress() {
|
void OnPlayPress() {
|
||||||
if (gravity.shift_button.On())
|
if (gravity.shift_button.On())
|
||||||
return; // ignore if holding both
|
return; // ignore if holding both
|
||||||
if (current_mode == MODE_SETTINGS) return; // Ignore in settings
|
|
||||||
if (current_mode == MODE_CALIBRATION) {
|
if (current_mode == MODE_CALIBRATION) {
|
||||||
cal_selected_param = (cal_selected_param < 3) ? cal_selected_param + 3
|
cal_selected_param = (cal_selected_param < 3) ? cal_selected_param + 3
|
||||||
: cal_selected_param - 3;
|
: cal_selected_param - 3;
|
||||||
@ -173,7 +116,6 @@ void OnPlayPress() {
|
|||||||
void OnShiftPress() {
|
void OnShiftPress() {
|
||||||
if (gravity.play_button.On())
|
if (gravity.play_button.On())
|
||||||
return; // ignore if holding both
|
return; // ignore if holding both
|
||||||
if (current_mode == MODE_SETTINGS) return; // Ignore in settings
|
|
||||||
if (current_mode == MODE_CALIBRATION) {
|
if (current_mode == MODE_CALIBRATION) {
|
||||||
cal_selected_param =
|
cal_selected_param =
|
||||||
(cal_selected_param / 3) * 3 + ((cal_selected_param + 1) % 3);
|
(cal_selected_param / 3) * 3 + ((cal_selected_param + 1) % 3);
|
||||||
@ -192,38 +134,6 @@ void OnShiftPress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OnEncoderRotate(int val) {
|
void OnEncoderRotate(int val) {
|
||||||
if (current_mode == MODE_SETTINGS) {
|
|
||||||
if (!settings_editing) {
|
|
||||||
// Navigate Mode
|
|
||||||
int change = val > 0 ? 1 : -1;
|
|
||||||
settings_selected_param = (SettingsParameter)(((int)settings_selected_param + change + 3) % 3);
|
|
||||||
} else {
|
|
||||||
// Edit Mode
|
|
||||||
switch (settings_selected_param) {
|
|
||||||
case SETTING_ENCODER_DIR: {
|
|
||||||
int setting = encoder_reversed ? 1 : 0;
|
|
||||||
setting = constrain(setting + (val > 0 ? 1 : -1), 0, 1);
|
|
||||||
encoder_reversed = (setting == 1);
|
|
||||||
gravity.encoder.SetReverseDirection(encoder_reversed);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SETTING_HYSTERESIS:
|
|
||||||
hysteresis = constrain(hysteresis + val, 0, 16);
|
|
||||||
break;
|
|
||||||
case SETTING_DISPLAY_CV: {
|
|
||||||
int setting = display_cv ? 1 : 0;
|
|
||||||
setting = constrain(setting + (val > 0 ? 1 : -1), 0, 1);
|
|
||||||
display_cv = (setting == 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eeprom_needs_save = true;
|
|
||||||
last_param_change = millis();
|
|
||||||
}
|
|
||||||
needs_redraw = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_mode == MODE_CALIBRATION) {
|
if (current_mode == MODE_CALIBRATION) {
|
||||||
AnalogInput *cv = (cal_selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
AnalogInput *cv = (cal_selected_param > 2) ? &gravity.cv2 : &gravity.cv1;
|
||||||
// Scale val up so tuning is practical without excessive encoder interrupts
|
// Scale val up so tuning is practical without excessive encoder interrupts
|
||||||
@ -311,38 +221,6 @@ void UpdateCalibrationDisplay() {
|
|||||||
DisplayCalibrationPoint(&gravity.cv2, "CV2: ", 1);
|
DisplayCalibrationPoint(&gravity.cv2, "CV2: ", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateSettingsDisplay() {
|
|
||||||
gravity.display.setFontMode(0);
|
|
||||||
gravity.display.setDrawColor(1);
|
|
||||||
gravity.display.setFont(u8g2_font_profont11_tf);
|
|
||||||
|
|
||||||
gravity.display.setCursor(0, 10);
|
|
||||||
gravity.display.print("Settings:");
|
|
||||||
|
|
||||||
int y = 25;
|
|
||||||
gravity.display.setCursor(10, y);
|
|
||||||
gravity.display.print("Encoder:");
|
|
||||||
gravity.display.setCursor(80, y);
|
|
||||||
gravity.display.print(encoder_reversed ? "Rev" : "Fwd");
|
|
||||||
|
|
||||||
y += 12;
|
|
||||||
gravity.display.setCursor(10, y);
|
|
||||||
gravity.display.print("Hysteresis:");
|
|
||||||
gravity.display.setCursor(80, y);
|
|
||||||
gravity.display.print(hysteresis);
|
|
||||||
|
|
||||||
y += 12;
|
|
||||||
gravity.display.setCursor(10, y);
|
|
||||||
gravity.display.print("Display CV:");
|
|
||||||
gravity.display.setCursor(80, y);
|
|
||||||
gravity.display.print(display_cv ? "On" : "Off");
|
|
||||||
|
|
||||||
// Selection indicator cursor `>` or `*`
|
|
||||||
y = 25 + (12 * (int)settings_selected_param);
|
|
||||||
gravity.display.setCursor(0, y);
|
|
||||||
gravity.display.print(settings_editing ? "*" : ">");
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDisplay
|
// UpdateDisplay
|
||||||
void UpdateDisplay(int cv1_val, int cv2_val) {
|
void UpdateDisplay(int cv1_val, int cv2_val) {
|
||||||
// Comp 1 graphics (Left)
|
// Comp 1 graphics (Left)
|
||||||
@ -352,14 +230,12 @@ void UpdateDisplay(int cv1_val, int cv2_val) {
|
|||||||
gravity.display.drawFrame(20, c1_y, 44, c1_h);
|
gravity.display.drawFrame(20, c1_y, 44, c1_h);
|
||||||
|
|
||||||
// CV 1 Indicator (Filled Box, 50% width, from 0V center)
|
// CV 1 Indicator (Filled Box, 50% width, from 0V center)
|
||||||
if (display_cv) {
|
|
||||||
int cv1_y = constrain(26 - ((cv1_val * 3) / 64), 2, 50);
|
int cv1_y = constrain(26 - ((cv1_val * 3) / 64), 2, 50);
|
||||||
if (cv1_val >= 0) {
|
if (cv1_val >= 0) {
|
||||||
gravity.display.drawBox(31, cv1_y, 22, 26 - cv1_y);
|
gravity.display.drawBox(31, cv1_y, 22, 26 - cv1_y);
|
||||||
} else {
|
} else {
|
||||||
gravity.display.drawBox(31, 26, 22, cv1_y - 26);
|
gravity.display.drawBox(31, 26, 22, cv1_y - 26);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Comp 2 graphics (Right)
|
// Comp 2 graphics (Right)
|
||||||
int c2_h = max((comp2_size * 3) / 64, 1);
|
int c2_h = max((comp2_size * 3) / 64, 1);
|
||||||
@ -368,14 +244,12 @@ void UpdateDisplay(int cv1_val, int cv2_val) {
|
|||||||
gravity.display.drawFrame(74, c2_y, 44, c2_h);
|
gravity.display.drawFrame(74, c2_y, 44, c2_h);
|
||||||
|
|
||||||
// CV 2 Indicator (Filled Box, 50% width, from 0V center)
|
// CV 2 Indicator (Filled Box, 50% width, from 0V center)
|
||||||
if (display_cv) {
|
|
||||||
int cv2_y = constrain(26 - ((cv2_val * 3) / 64), 2, 50);
|
int cv2_y = constrain(26 - ((cv2_val * 3) / 64), 2, 50);
|
||||||
if (cv2_val >= 0) {
|
if (cv2_val >= 0) {
|
||||||
gravity.display.drawBox(85, cv2_y, 22, 26 - cv2_y);
|
gravity.display.drawBox(85, cv2_y, 22, 26 - cv2_y);
|
||||||
} else {
|
} else {
|
||||||
gravity.display.drawBox(85, 26, 22, cv2_y - 26);
|
gravity.display.drawBox(85, 26, 22, cv2_y - 26);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Restore solid drawing for labels
|
// Restore solid drawing for labels
|
||||||
gravity.display.setDrawColor(1);
|
gravity.display.setDrawColor(1);
|
||||||
@ -439,14 +313,30 @@ void setup() {
|
|||||||
|
|
||||||
gravity.play_button.AttachPressHandler(OnPlayPress);
|
gravity.play_button.AttachPressHandler(OnPlayPress);
|
||||||
gravity.shift_button.AttachPressHandler(OnShiftPress);
|
gravity.shift_button.AttachPressHandler(OnShiftPress);
|
||||||
gravity.encoder.AttachPressHandler(OnEncoderPress);
|
|
||||||
gravity.encoder.AttachRotateHandler(OnEncoderRotate);
|
gravity.encoder.AttachRotateHandler(OnEncoderRotate);
|
||||||
gravity.encoder.AttachPressRotateHandler(HandlePressedRotate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
gravity.Process();
|
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 cv1_val = gravity.cv1.Read();
|
||||||
int cv2_val = gravity.cv2.Read();
|
int cv2_val = gravity.cv2.Read();
|
||||||
|
|
||||||
@ -457,26 +347,28 @@ void loop() {
|
|||||||
int c2_lower = comp2_shift - (comp2_size / 2);
|
int c2_lower = comp2_shift - (comp2_size / 2);
|
||||||
int c2_upper = 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;
|
bool gate1 = prev_gate1;
|
||||||
if (gate1) {
|
if (gate1) {
|
||||||
if (cv1_val < c1_lower - hysteresis || cv1_val > c1_upper + hysteresis) {
|
if (cv1_val < c1_lower - HYSTERESIS || cv1_val > c1_upper + HYSTERESIS) {
|
||||||
gate1 = false;
|
gate1 = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cv1_val >= c1_lower + hysteresis &&
|
if (cv1_val >= c1_lower + HYSTERESIS &&
|
||||||
cv1_val <= c1_upper - hysteresis) {
|
cv1_val <= c1_upper - HYSTERESIS) {
|
||||||
gate1 = true;
|
gate1 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool gate2 = prev_gate2;
|
bool gate2 = prev_gate2;
|
||||||
if (gate2) {
|
if (gate2) {
|
||||||
if (cv2_val < c2_lower - hysteresis || cv2_val > c2_upper + hysteresis) {
|
if (cv2_val < c2_lower - HYSTERESIS || cv2_val > c2_upper + HYSTERESIS) {
|
||||||
gate2 = false;
|
gate2 = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cv2_val >= c2_lower + hysteresis &&
|
if (cv2_val >= c2_lower + HYSTERESIS &&
|
||||||
cv2_val <= c2_upper - hysteresis) {
|
cv2_val <= c2_upper - HYSTERESIS) {
|
||||||
gate2 = true;
|
gate2 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -508,15 +400,10 @@ void loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (current_mode == MODE_COMPARATOR) {
|
if (current_mode == MODE_COMPARATOR) {
|
||||||
if (display_cv) {
|
|
||||||
if (abs(cv1_val - last_cv1_draw) > 12 ||
|
if (abs(cv1_val - last_cv1_draw) > 12 ||
|
||||||
abs(cv2_val - last_cv2_draw) > 12) {
|
abs(cv2_val - last_cv2_draw) > 12) {
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
} else if (abs(comp1_shift - last_cv1_draw /* hack to force redraw on comp change if CV disabled? */) ) {
|
|
||||||
// We only trigger redraws if comp1/comp2 settings change. But those set needs_redraw=true in OnEncoderRotate.
|
|
||||||
// So if display_cv is false, we don't redraw from CV fluctuations at all loop.
|
|
||||||
}
|
|
||||||
} else if (current_mode == MODE_CALIBRATION) {
|
} else if (current_mode == MODE_CALIBRATION) {
|
||||||
// Need frequent redraws in calibration to see the live target input
|
// Need frequent redraws in calibration to see the live target input
|
||||||
if (abs(cv1_val - last_cv1_draw) >= 2 ||
|
if (abs(cv1_val - last_cv1_draw) >= 2 ||
|
||||||
@ -540,10 +427,8 @@ void loop() {
|
|||||||
last_cv1_draw = cv1_val;
|
last_cv1_draw = cv1_val;
|
||||||
last_cv2_draw = cv2_val;
|
last_cv2_draw = cv2_val;
|
||||||
UpdateDisplay(cv1_val, cv2_val);
|
UpdateDisplay(cv1_val, cv2_val);
|
||||||
} else if (current_mode == MODE_CALIBRATION) {
|
} else {
|
||||||
UpdateCalibrationDisplay();
|
UpdateCalibrationDisplay();
|
||||||
} else if (current_mode == MODE_SETTINGS) {
|
|
||||||
UpdateSettingsDisplay();
|
|
||||||
}
|
}
|
||||||
is_drawing = gravity.display.nextPage();
|
is_drawing = gravity.display.nextPage();
|
||||||
}
|
}
|
||||||
|
|||||||
710
firmware/Rhythm/Rhythm.ino
Normal file
710
firmware/Rhythm/Rhythm.ino
Normal file
@ -0,0 +1,710 @@
|
|||||||
|
#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_LAST = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.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();
|
||||||
|
} else {
|
||||||
|
gravity.clock.Start();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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* const global_menu_items[] PROGMEM = {str_source, str_tempo, str_cv1dest, str_cv2dest};
|
||||||
|
|
||||||
|
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;
|
||||||
|
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.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
90
firmware/Rhythm/display_assets.h
Normal file
90
firmware/Rhythm/display_assets.h
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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};
|
||||||
136
firmware/Rhythm/maps.h
Normal file
136
firmware/Rhythm/maps.h
Normal 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
|
||||||
@ -21,7 +21,6 @@ class Encoder {
|
|||||||
typedef void (*CallbackFunction)(void);
|
typedef void (*CallbackFunction)(void);
|
||||||
typedef void (*RotateCallbackFunction)(int val);
|
typedef void (*RotateCallbackFunction)(int val);
|
||||||
CallbackFunction on_press;
|
CallbackFunction on_press;
|
||||||
CallbackFunction on_long_press;
|
|
||||||
RotateCallbackFunction on_press_rotate;
|
RotateCallbackFunction on_press_rotate;
|
||||||
RotateCallbackFunction on_rotate;
|
RotateCallbackFunction on_rotate;
|
||||||
int change;
|
int change;
|
||||||
@ -41,10 +40,6 @@ class Encoder {
|
|||||||
on_press = f;
|
on_press = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AttachLongPressHandler(CallbackFunction f) {
|
|
||||||
on_long_press = f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AttachRotateHandler(RotateCallbackFunction f) {
|
void AttachRotateHandler(RotateCallbackFunction f) {
|
||||||
on_rotate = f;
|
on_rotate = f;
|
||||||
}
|
}
|
||||||
@ -67,8 +62,6 @@ class Encoder {
|
|||||||
if (on_rotate != NULL) on_rotate(change);
|
if (on_rotate != NULL) on_rotate(change);
|
||||||
} else if (button_.Change() == Button::CHANGE_RELEASED && !rotated_while_held_) {
|
} else if (button_.Change() == Button::CHANGE_RELEASED && !rotated_while_held_) {
|
||||||
if (on_press != NULL) on_press();
|
if (on_press != NULL) on_press();
|
||||||
} else if (button_.Change() == Button::CHANGE_RELEASED_LONG && !rotated_while_held_) {
|
|
||||||
if (on_long_press != NULL) on_long_press();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset rotate while held state.
|
// Reset rotate while held state.
|
||||||
|
|||||||
Reference in New Issue
Block a user