From fabc9b9b8d8a363790556805561c7270c47b98c0 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 18 Mar 2026 13:11:08 -0700 Subject: [PATCH] feat: Add configurable pulse output feature with UI control and associated logic. --- firmware/Rhythm/Rhythm.ino | 62 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/firmware/Rhythm/Rhythm.ino b/firmware/Rhythm/Rhythm.ino index 7f279aa..55e426e 100644 --- a/firmware/Rhythm/Rhythm.ino +++ b/firmware/Rhythm/Rhythm.ino @@ -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 { @@ -163,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) { @@ -256,6 +272,7 @@ void HandleExtClockTick() { for (int i = 0; i < 6; i++) { gravity.outputs[i].Low(); } + gravity.pulse.Low(); gravity.clock.Reset(); current_step = 0; break; @@ -270,12 +287,23 @@ void OnPlayPress() { 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; @@ -375,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; } @@ -437,7 +474,8 @@ 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}; +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); @@ -588,6 +626,25 @@ void DisplayMainArea() { 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; } @@ -645,6 +702,7 @@ 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);