Compare commits
4 Commits
50c551f198
...
rhythm
| Author | SHA1 | Date | |
|---|---|---|---|
| fabc9b9b8d | |||
| e363c05823 | |||
| 59fbd37524 | |||
| 37711bc6ef |
@ -37,7 +37,8 @@ enum GlobalParam {
|
||||
PARAM_GLOBAL_BPM = 1,
|
||||
PARAM_GLOBAL_CV1_DEST = 2,
|
||||
PARAM_GLOBAL_CV2_DEST = 3,
|
||||
PARAM_GLOBAL_LAST = 4
|
||||
PARAM_GLOBAL_PULSE_OUT = 4,
|
||||
PARAM_GLOBAL_LAST = 5
|
||||
};
|
||||
|
||||
enum CvDest {
|
||||
@ -56,6 +57,7 @@ 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 {
|
||||
@ -81,8 +83,16 @@ volatile int cv1_val = 0;
|
||||
volatile int cv2_val = 0;
|
||||
int last_mapped_slot = -1;
|
||||
|
||||
// LFSR State for Chaos
|
||||
uint16_t lfsr = 0xACE1;
|
||||
// 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) {
|
||||
@ -95,21 +105,17 @@ uint8_t GetThreshold(int inst, int step, int x_pos, int y_pos) {
|
||||
// x_pos is 0-255 mapped across 4 nodes (0, 1, 2, 3). Distance is 85 (255 / 3)
|
||||
// y_pos is 0-255 mapped across 4 nodes (0, 1, 2, 3). Distance is 85 (255 / 3)
|
||||
|
||||
int x_idx = x_pos / 85;
|
||||
int y_idx = y_pos / 85;
|
||||
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; }
|
||||
|
||||
uint8_t x_frac = (x_pos % 85) * 3; // scale remainder 0-84 up to 0-255
|
||||
uint8_t y_frac = (y_pos % 85) * 3;
|
||||
|
||||
// Guard against out of bounds if exactly 255
|
||||
if (x_idx >= 3) {
|
||||
x_idx = 2;
|
||||
x_frac = 255;
|
||||
}
|
||||
if (y_idx >= 3) {
|
||||
y_idx = 2;
|
||||
y_frac = 255;
|
||||
}
|
||||
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]);
|
||||
@ -138,20 +144,13 @@ void SaveState() {
|
||||
EEPROM.put(EEPROM_PATTERNS, patterns);
|
||||
}
|
||||
|
||||
// LFSR random bit generator (returns 0 or 1, fast)
|
||||
uint8_t GetRandomBit() {
|
||||
uint8_t bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1;
|
||||
lfsr = (lfsr >> 1) | (bit << 15);
|
||||
return bit;
|
||||
return xorshift16() & 1;
|
||||
}
|
||||
|
||||
// Get 8-bit pseudo-random number
|
||||
uint8_t GetRandomByte() {
|
||||
uint8_t r = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
r = (r << 1) | GetRandomBit();
|
||||
}
|
||||
return r;
|
||||
return xorshift16() & 0xFF;
|
||||
}
|
||||
|
||||
void ProcessSequencerTick(uint32_t tick) {
|
||||
@ -166,6 +165,20 @@ void ProcessSequencerTick(uint32_t tick) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@ -251,17 +264,46 @@ void ProcessSequencerTick(uint32_t tick) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -361,6 +403,15 @@ void OnEncoderRotate(int val) {
|
||||
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;
|
||||
}
|
||||
@ -411,7 +462,22 @@ void drawMainSelection() {
|
||||
gravity.display.drawLine(0, mainHeight, 0, mainHeight - tickSize);
|
||||
}
|
||||
|
||||
void drawMenuItems(String menu_items[], int menu_size, int current_item) {
|
||||
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;
|
||||
@ -444,8 +510,9 @@ void drawMenuItems(String menu_items[], int menu_size, int current_item) {
|
||||
|
||||
for (int i = 0; i < min(menu_size, (int)VISIBLE_MENU_ITEMS); ++i) {
|
||||
int idx = start_index + i;
|
||||
drawRightAlignedText(menu_items[idx].c_str(),
|
||||
MENU_ITEM_HEIGHT * (i + 1) - 1);
|
||||
char buffer[16];
|
||||
strcpy_P(buffer, (char*)pgm_read_ptr(&(menu_items[idx])));
|
||||
drawRightAlignedText(buffer, MENU_ITEM_HEIGHT * (i + 1) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,103 +520,139 @@ void DisplayMainArea() {
|
||||
gravity.display.setFontMode(1);
|
||||
gravity.display.setDrawColor(2);
|
||||
|
||||
String mainText;
|
||||
String subText;
|
||||
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:
|
||||
mainText = String(map(p.inst_density[0], 0, 255, 0, 100)) + "%";
|
||||
subText = "KICK DENS";
|
||||
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
|
||||
strcat(mainText, "%");
|
||||
strcpy(subText, "KICK DENS");
|
||||
break;
|
||||
case PARAM_SNARE_DENS:
|
||||
mainText = String(map(p.inst_density[1], 0, 255, 0, 100)) + "%";
|
||||
subText = "SNAR DENS";
|
||||
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
|
||||
strcat(mainText, "%");
|
||||
strcpy(subText, "SNAR DENS");
|
||||
break;
|
||||
case PARAM_HIHAT_DENS:
|
||||
mainText = String(map(p.inst_density[2], 0, 255, 0, 100)) + "%";
|
||||
subText = "HHAT DENS";
|
||||
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
|
||||
strcat(mainText, "%");
|
||||
strcpy(subText, "HHAT DENS");
|
||||
break;
|
||||
case PARAM_CHAOS:
|
||||
mainText = String(map(p.chaos_amount, 0, 255, 0, 100)) + "%";
|
||||
subText = "CHAOS";
|
||||
itoa(map(act_val, 0, 255, 0, 100), mainText, 10);
|
||||
strcat(mainText, "%");
|
||||
strcpy(subText, "CHAOS");
|
||||
break;
|
||||
case PARAM_MAP_X:
|
||||
mainText = String(p.map_x);
|
||||
subText = "MAP X";
|
||||
itoa(act_val, mainText, 10);
|
||||
strcpy(subText, "MAP X");
|
||||
break;
|
||||
case PARAM_MAP_Y:
|
||||
mainText = String(p.map_y);
|
||||
subText = "MAP Y";
|
||||
itoa(act_val, mainText, 10);
|
||||
strcpy(subText, "MAP Y");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT);
|
||||
drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT);
|
||||
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
|
||||
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
|
||||
|
||||
String menu_items[PARAM_LAST] = {
|
||||
"KICK", "SNARE", "HHAT", "CHAOS", "MAP X", "MAP Y"};
|
||||
drawMenuItems(menu_items, PARAM_LAST, (int)current_param);
|
||||
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:
|
||||
mainText = F("INT");
|
||||
subText = F("CLOCK");
|
||||
strcpy_P(mainText, PSTR("INT"));
|
||||
strcpy_P(subText, PSTR("CLOCK"));
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_24:
|
||||
mainText = F("EXT");
|
||||
subText = F("24 PPQN");
|
||||
strcpy_P(mainText, PSTR("EXT"));
|
||||
strcpy_P(subText, PSTR("24 PPQN"));
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_4:
|
||||
mainText = F("EXT");
|
||||
subText = F("4 PPQN");
|
||||
strcpy_P(mainText, PSTR("EXT"));
|
||||
strcpy_P(subText, PSTR("4 PPQN"));
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_2:
|
||||
mainText = F("EXT");
|
||||
subText = F("2 PPQN");
|
||||
strcpy_P(mainText, PSTR("EXT"));
|
||||
strcpy_P(subText, PSTR("2 PPQN"));
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_PPQN_1:
|
||||
mainText = F("EXT");
|
||||
subText = F("1 PPQN");
|
||||
strcpy_P(mainText, PSTR("EXT"));
|
||||
strcpy_P(subText, PSTR("1 PPQN"));
|
||||
break;
|
||||
case Clock::SOURCE_EXTERNAL_MIDI:
|
||||
mainText = F("EXT");
|
||||
subText = F("MIDI");
|
||||
strcpy_P(mainText, PSTR("EXT"));
|
||||
strcpy_P(subText, PSTR("MIDI"));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case PARAM_GLOBAL_BPM:
|
||||
if (gravity.clock.ExternalSource()) {
|
||||
mainText = F("EXT");
|
||||
strcpy_P(mainText, PSTR("EXT"));
|
||||
} else {
|
||||
mainText = String(gravity.clock.Tempo());
|
||||
itoa(gravity.clock.Tempo(), mainText, 10);
|
||||
}
|
||||
subText = F("BPM");
|
||||
strcpy_P(subText, PSTR("BPM"));
|
||||
break;
|
||||
case PARAM_GLOBAL_CV1_DEST:
|
||||
mainText = F("CV1");
|
||||
subText = GetCvDestName(cv1_dest);
|
||||
strcpy_P(mainText, PSTR("CV1"));
|
||||
strcpy(subText, GetCvDestName(cv1_dest));
|
||||
break;
|
||||
case PARAM_GLOBAL_CV2_DEST:
|
||||
mainText = F("CV2");
|
||||
subText = GetCvDestName(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.c_str(), MAIN_TEXT_Y, LARGE_FONT);
|
||||
drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT);
|
||||
drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT);
|
||||
drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT);
|
||||
|
||||
String menu_items[PARAM_GLOBAL_LAST] = {
|
||||
"SOURCE", "TEMPO", "CV1 DEST", "CV2 DEST"};
|
||||
drawMenuItems(menu_items, PARAM_GLOBAL_LAST, (int)current_global_param);
|
||||
drawMenuItems(global_menu_items, PARAM_GLOBAL_LAST, (int)current_global_param);
|
||||
}
|
||||
}
|
||||
|
||||
@ -599,11 +702,13 @@ void setup() {
|
||||
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);
|
||||
@ -615,10 +720,37 @@ void setup() {
|
||||
void loop() {
|
||||
gravity.Process();
|
||||
|
||||
// Apply CV modulation
|
||||
// CV1 modulates Map X, CV2 modulates Chaos
|
||||
cv1_val = gravity.cv1.Read(); // -512 to 512
|
||||
cv2_val = gravity.cv2.Read();
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user