From df1a499fa0bbbd8a26e61b06d18b5aebdca9ad28 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 19 Jun 2025 09:32:41 -0700 Subject: [PATCH 01/54] Initial non-working commit of shuffle behavior. This change exposed a bug that seems to be calling each "processClockTick" method twice per tick. --- examples/Gravity/Gravity.ino | 3 +++ examples/Gravity/app_state.h | 2 ++ examples/Gravity/channel.h | 47 ++++++++++++++++++++++++++++++--- examples/Gravity/display.h | 10 ++++++- examples/Gravity/save_state.cpp | 2 ++ examples/Gravity/save_state.h | 3 ++- 6 files changed, 62 insertions(+), 5 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 5869a92..9ab9f6d 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -210,6 +210,9 @@ void editChannelParameter(int val) { case PARAM_CH_OFFSET: ch.setOffset(ch.getOffset() + val); break; + case PARAM_CH_SHUFFLE: + ch.setShuffleIndex(ch.getShuffleIndex() + val); + break; case PARAM_CH_CV_SRC: { int source = static_cast(ch.getCvSource()); updateSelection(source, val, CV_LAST); diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 09cd256..f7f2d96 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -14,6 +14,7 @@ struct AppState { int selected_param = 0; int selected_sub_param = 0; byte selected_channel = 0; // 0=tempo, 1-6=output channel + byte selected_shuffle = 0; // index into shuffle template Clock::Source selected_source = Clock::SOURCE_INTERNAL; Channel channel[Gravity::OUTPUT_COUNT]; }; @@ -37,6 +38,7 @@ enum ParamsChannelPage { PARAM_CH_PROB, PARAM_CH_DUTY, PARAM_CH_OFFSET, + PARAM_CH_SHUFFLE, PARAM_CH_CV_SRC, PARAM_CH_CV_DEST, PARAM_CH_LAST, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index b7115a3..c6676b1 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -27,6 +27,30 @@ static const int clock_mod[MOD_CHOICE_SIZE] = {-24, -12, -8, -6, -4, -3, -2, 1, // This represents the number of clock pulses for a 96 PPQN clock source that match the above div/mult mods. static const int clock_mod_pulses[MOD_CHOICE_SIZE] = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; +static const int8_t shuffle_size = 2; + +// MPC60 groove signatures? +static const int8_t shuffle_54[2] = {0, 2}; +static const int8_t shuffle_58[2] = {0, 4}; +static const int8_t shuffle_62[2] = {0, 6}; +static const int8_t shuffle_66[2] = {0, 8}; +static const int8_t shuffle_71[2] = {0, 10}; +static const int8_t shuffle_75[2] = {0, 12}; + +// SWING Groove +static const int8_t swing_54[2] = {0, 1}; +static const int8_t swing_58[2] = {-1, 1}; +static const int8_t swing_62[2] = {-1, 2}; +static const int8_t swing_66[2] = {-2, 2}; +static const int8_t swing_71[2] = {-2, 3}; +static const int8_t swing_75[2] = {-3, 3}; + +// static const String shuffle_name[6] = {"OFF", "54%", "58%", "62%", "66%", "71%"}; +static const uint8_t SHUFFLE_SIZE = 6; +static const byte shuffle_amount[SHUFFLE_SIZE] = {54, 58, 62, 66, 71, 75}; +static const int8_t* shuffle_templates[SHUFFLE_SIZE] = {shuffle_54, shuffle_58, shuffle_62, shuffle_66, shuffle_71, shuffle_75}; +// static const int8_t* shuffle_templates[SHUFFLE_SIZE] = {swing_54, swing_58, swing_62, swing_66, swing_71, swing_75}; + class Channel { public: Channel() { @@ -41,6 +65,8 @@ class Channel { base_offset = 0; cv_source = CV_NONE; cv_destination = CV_DEST_NONE; + shuffle_index = 0; + step_count = 0; cvmod_clock_mod_index = base_clock_mod_index; cvmod_probability = base_probability; @@ -56,6 +82,7 @@ class Channel { void setProbability(int prob) { base_probability = constrain(prob, 0, 100); } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); } void setOffset(int off) { base_offset = constrain(off, 0, 100); } + void setShuffleIndex(int val) { shuffle_index = constrain(val, 0, SHUFFLE_SIZE - 1); } void setCvSource(CvSource source) { cv_source = source; } void setCvDestination(CvDestination dest) { cv_destination = dest; } @@ -64,11 +91,13 @@ class Channel { int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } + int getShuffleIndex() const { return shuffle_index; } int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } CvDestination getCvDestination() { return cv_destination; } bool isCvModActive() const { return cv_source != CV_NONE && cv_destination != CV_DEST_NONE; } + int getStepCount() {return step_count;} /** * @brief Processes a clock tick and determines if the output should be high or low. @@ -81,17 +110,26 @@ class Channel { const uint32_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); const uint32_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); - const uint32_t current_tick_offset = tick + offset_pulses; + uint32_t shuffle_pulses = 0; + if (step_count % 2 == 0) { + // shuffle_pulses = (long)((mod_pulses * (100L - shuffle_amount[shuffle_index])) / 100L); + shuffle_pulses = 4 * shuffle_templates[shuffle_index][1]; + } - // Duty cycle high check + const uint32_t current_tick_offset = tick + offset_pulses + shuffle_pulses; + + // Step check + // TODO: Why is this incrementing twice? if (current_tick_offset % mod_pulses == 0) { + // Duty cycle high check if (cvmod_probability >= random(0, 100)) { + step_count += 1; output.High(); } } // Duty cycle low check - const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses; + const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + shuffle_pulses; if (duty_cycle_end_tick % mod_pulses == 0) { output.Low(); } @@ -131,11 +169,14 @@ class Channel { } private: + uint32_t step_count; + // User-settable base values. byte base_clock_mod_index; byte base_probability; byte base_duty_cycle; byte base_offset; + byte shuffle_index; // Base value with cv mod applied. byte cvmod_clock_mod_index; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 3291238..3fa379c 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -244,6 +244,14 @@ void DisplayChannelPage() { sprintf(mainText, "%d%%", ch.getOffset(withCvMod)); subText = "SHIFT HIT"; break; + case PARAM_CH_SHUFFLE: + ch.getShuffleIndex() == 0 + // ? sprintf(mainText, "OFF") + // TODO: why is this being incremented by 2? + ? sprintf(mainText, "%d", ch.getStepCount()) + : sprintf(mainText, "%d%%", shuffle_amount[ch.getShuffleIndex()]); + subText = "SHUFFLE"; + break; case PARAM_CH_CV_SRC: { switch (ch.getCvSource()) { case CV_NONE: @@ -293,7 +301,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items const char* menu_items[PARAM_CH_LAST] = { - "MOD", "PROBABILITY", "DUTY", "OFFSET", "CV SOURCE", "CV DEST"}; + "MOD", "PROBABILITY", "DUTY", "OFFSET", "SHUFFLE", "CV SOURCE", "CV DEST"}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index 87357cb..76ac7e4 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -27,6 +27,7 @@ bool StateManager::initialize(AppState& app) { ch.setProbability(saved_ch_state.base_probability); ch.setDutyCycle(saved_ch_state.base_duty_cycle); ch.setOffset(saved_ch_state.base_offset); + ch.setShuffleIndex(saved_ch_state.shuffle_index); ch.setCvSource(static_cast(saved_ch_state.cv_source)); ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); } @@ -105,6 +106,7 @@ void StateManager::_saveState(const AppState& app) { save_ch.base_probability = ch.getProbability(false); save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); + save_ch.shuffle_index = ch.getShuffleIndex(); save_ch.cv_source = static_cast(ch.getCvSource()); save_ch.cv_destination = static_cast(ch.getCvDestination()); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index b747189..023bf72 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -9,7 +9,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 3; +const byte SKETCH_VERSION = 4; // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -41,6 +41,7 @@ class StateManager { byte base_probability; byte base_duty_cycle; byte base_offset; + byte shuffle_index; byte cv_source; // Cast the CvSource enum to a byte for storage byte cv_destination; // Cast the CvDestination enum as a byte for storage }; -- 2.39.5 From 0b610f53c053b5a575030f4b490b9188dd1b1ee2 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 20 Jun 2025 09:21:38 -0700 Subject: [PATCH 02/54] Fix shuffle calculation logic. Make shuffle percentge based. --- examples/Gravity/Gravity.ino | 2 +- examples/Gravity/channel.h | 56 +++++++++------------------------ examples/Gravity/display.h | 8 ++--- examples/Gravity/save_state.cpp | 4 +-- examples/Gravity/save_state.h | 2 +- 5 files changed, 21 insertions(+), 51 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 9ab9f6d..d49592e 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -211,7 +211,7 @@ void editChannelParameter(int val) { ch.setOffset(ch.getOffset() + val); break; case PARAM_CH_SHUFFLE: - ch.setShuffleIndex(ch.getShuffleIndex() + val); + ch.setShuffle(ch.getShuffle() + val); break; case PARAM_CH_CV_SRC: { int source = static_cast(ch.getCvSource()); diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index c6676b1..742d77f 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -27,30 +27,6 @@ static const int clock_mod[MOD_CHOICE_SIZE] = {-24, -12, -8, -6, -4, -3, -2, 1, // This represents the number of clock pulses for a 96 PPQN clock source that match the above div/mult mods. static const int clock_mod_pulses[MOD_CHOICE_SIZE] = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; -static const int8_t shuffle_size = 2; - -// MPC60 groove signatures? -static const int8_t shuffle_54[2] = {0, 2}; -static const int8_t shuffle_58[2] = {0, 4}; -static const int8_t shuffle_62[2] = {0, 6}; -static const int8_t shuffle_66[2] = {0, 8}; -static const int8_t shuffle_71[2] = {0, 10}; -static const int8_t shuffle_75[2] = {0, 12}; - -// SWING Groove -static const int8_t swing_54[2] = {0, 1}; -static const int8_t swing_58[2] = {-1, 1}; -static const int8_t swing_62[2] = {-1, 2}; -static const int8_t swing_66[2] = {-2, 2}; -static const int8_t swing_71[2] = {-2, 3}; -static const int8_t swing_75[2] = {-3, 3}; - -// static const String shuffle_name[6] = {"OFF", "54%", "58%", "62%", "66%", "71%"}; -static const uint8_t SHUFFLE_SIZE = 6; -static const byte shuffle_amount[SHUFFLE_SIZE] = {54, 58, 62, 66, 71, 75}; -static const int8_t* shuffle_templates[SHUFFLE_SIZE] = {shuffle_54, shuffle_58, shuffle_62, shuffle_66, shuffle_71, shuffle_75}; -// static const int8_t* shuffle_templates[SHUFFLE_SIZE] = {swing_54, swing_58, swing_62, swing_66, swing_71, swing_75}; - class Channel { public: Channel() { @@ -65,8 +41,7 @@ class Channel { base_offset = 0; cv_source = CV_NONE; cv_destination = CV_DEST_NONE; - shuffle_index = 0; - step_count = 0; + base_shuffle = 0; cvmod_clock_mod_index = base_clock_mod_index; cvmod_probability = base_probability; @@ -82,7 +57,7 @@ class Channel { void setProbability(int prob) { base_probability = constrain(prob, 0, 100); } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); } void setOffset(int off) { base_offset = constrain(off, 0, 100); } - void setShuffleIndex(int val) { shuffle_index = constrain(val, 0, SHUFFLE_SIZE - 1); } + void setShuffle(int val) { base_shuffle = constrain(val, 0, 50); } void setCvSource(CvSource source) { cv_source = source; } void setCvDestination(CvDestination dest) { cv_destination = dest; } @@ -91,13 +66,12 @@ class Channel { int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } - int getShuffleIndex() const { return shuffle_index; } + int getShuffle() const { return base_shuffle; } int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } CvDestination getCvDestination() { return cv_destination; } bool isCvModActive() const { return cv_source != CV_NONE && cv_destination != CV_DEST_NONE; } - int getStepCount() {return step_count;} /** * @brief Processes a clock tick and determines if the output should be high or low. @@ -111,25 +85,25 @@ class Channel { const uint32_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); uint32_t shuffle_pulses = 0; - if (step_count % 2 == 0) { - // shuffle_pulses = (long)((mod_pulses * (100L - shuffle_amount[shuffle_index])) / 100L); - shuffle_pulses = 4 * shuffle_templates[shuffle_index][1]; + // Check step increment for odd beats. + if ((tick / mod_pulses) % 2 == 1) { + shuffle_pulses = (long)((mod_pulses * (100L - base_shuffle)) / 100L); } const uint32_t current_tick_offset = tick + offset_pulses + shuffle_pulses; // Step check - // TODO: Why is this incrementing twice? - if (current_tick_offset % mod_pulses == 0) { - // Duty cycle high check - if (cvmod_probability >= random(0, 100)) { - step_count += 1; - output.High(); + if (!output.On()) { + if (current_tick_offset % mod_pulses == 0) { + // Duty cycle high check + if (cvmod_probability >= random(0, 100)) { + output.High(); + } } } // Duty cycle low check - const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + shuffle_pulses; + const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses; if (duty_cycle_end_tick % mod_pulses == 0) { output.Low(); } @@ -169,14 +143,12 @@ class Channel { } private: - uint32_t step_count; - // User-settable base values. byte base_clock_mod_index; byte base_probability; byte base_duty_cycle; byte base_offset; - byte shuffle_index; + byte base_shuffle; // Base value with cv mod applied. byte cvmod_clock_mod_index; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 3fa379c..199edb2 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -245,11 +245,9 @@ void DisplayChannelPage() { subText = "SHIFT HIT"; break; case PARAM_CH_SHUFFLE: - ch.getShuffleIndex() == 0 - // ? sprintf(mainText, "OFF") - // TODO: why is this being incremented by 2? - ? sprintf(mainText, "%d", ch.getStepCount()) - : sprintf(mainText, "%d%%", shuffle_amount[ch.getShuffleIndex()]); + ch.getShuffle() == 0 + ? sprintf(mainText, "OFF") + : sprintf(mainText, "%d%%", ch.getShuffle() + 50); subText = "SHUFFLE"; break; case PARAM_CH_CV_SRC: { diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index 76ac7e4..ee39dc3 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -27,7 +27,7 @@ bool StateManager::initialize(AppState& app) { ch.setProbability(saved_ch_state.base_probability); ch.setDutyCycle(saved_ch_state.base_duty_cycle); ch.setOffset(saved_ch_state.base_offset); - ch.setShuffleIndex(saved_ch_state.shuffle_index); + ch.setShuffle(saved_ch_state.base_shuffle); ch.setCvSource(static_cast(saved_ch_state.cv_source)); ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); } @@ -106,7 +106,7 @@ void StateManager::_saveState(const AppState& app) { save_ch.base_probability = ch.getProbability(false); save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); - save_ch.shuffle_index = ch.getShuffleIndex(); + save_ch.base_shuffle = ch.getShuffle(); save_ch.cv_source = static_cast(ch.getCvSource()); save_ch.cv_destination = static_cast(ch.getCvDestination()); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index 023bf72..eb3c7a2 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -41,7 +41,7 @@ class StateManager { byte base_probability; byte base_duty_cycle; byte base_offset; - byte shuffle_index; + byte base_shuffle; byte cv_source; // Cast the CvSource enum to a byte for storage byte cv_destination; // Cast the CvDestination enum as a byte for storage }; -- 2.39.5 From 0452006e675345d243f07ff5372e8f915e483489 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 20 Jun 2025 09:33:17 -0700 Subject: [PATCH 03/54] allow cv mod with shuffle --- examples/Gravity/channel.h | 14 +++++++++++--- examples/Gravity/display.h | 6 +++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 742d77f..d737995 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -18,6 +18,7 @@ enum CvDestination { CV_DEST_PROB, CV_DEST_DUTY, CV_DEST_OFFSET, + CV_DEST_SHUFFLE, CV_DEST_LAST, }; @@ -39,14 +40,15 @@ class Channel { base_probability = 100; base_duty_cycle = 50; base_offset = 0; + base_shuffle = 0; cv_source = CV_NONE; cv_destination = CV_DEST_NONE; - base_shuffle = 0; cvmod_clock_mod_index = base_clock_mod_index; cvmod_probability = base_probability; cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; + cvmod_shuffle = base_shuffle; } // Setters (Set the BASE value) @@ -66,7 +68,7 @@ class Channel { int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } - int getShuffle() const { return base_shuffle; } + int getShuffle(bool withCvMod = false) const { return withCvMod ? cvmod_shuffle : base_shuffle; } int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } @@ -87,7 +89,7 @@ class Channel { uint32_t shuffle_pulses = 0; // Check step increment for odd beats. if ((tick / mod_pulses) % 2 == 1) { - shuffle_pulses = (long)((mod_pulses * (100L - base_shuffle)) / 100L); + shuffle_pulses = (long)((mod_pulses * (100L - cvmod_shuffle)) / 100L); } const uint32_t current_tick_offset = tick + offset_pulses + shuffle_pulses; @@ -116,6 +118,7 @@ class Channel { cvmod_probability = base_probability; cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; + cvmod_shuffle = base_shuffle; return; } @@ -140,6 +143,10 @@ class Channel { cvmod_offset = (cv_destination == CV_DEST_OFFSET) ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) : base_offset; + + cvmod_shuffle = (cv_destination == CV_DEST_SHUFFLE) + ? constrain(base_shuffle + map(value, -512, 512, -25, 25), 0, 50) + : base_shuffle; } private: @@ -155,6 +162,7 @@ class Channel { byte cvmod_probability; byte cvmod_duty_cycle; byte cvmod_offset; + byte cvmod_shuffle; // CV configuration CvSource cv_source = CV_NONE; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 199edb2..006915c 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -247,7 +247,7 @@ void DisplayChannelPage() { case PARAM_CH_SHUFFLE: ch.getShuffle() == 0 ? sprintf(mainText, "OFF") - : sprintf(mainText, "%d%%", ch.getShuffle() + 50); + : sprintf(mainText, "%d%%", ch.getShuffle(withCvMod) + 50); subText = "SHUFFLE"; break; case PARAM_CH_CV_SRC: { @@ -289,6 +289,10 @@ void DisplayChannelPage() { sprintf(mainText, "DEST"); subText = "OFFSET"; break; + case CV_DEST_SHUFFLE: + sprintf(mainText, "DEST"); + subText = "SHUFFLE"; + break; } break; } -- 2.39.5 From c8e42c744873c7d27eea7e0f0e99e6d2da8a9793 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 15:45:11 -0700 Subject: [PATCH 04/54] use appropriate string width method. --- examples/Gravity/display.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 3291238..b9994ed 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -82,13 +82,13 @@ constexpr int CHANNEL_BOX_HEIGHT = 14; // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { gravity.display.setFont(font); - int textWidth = gravity.display.getUTF8Width(text); + int textWidth = gravity.display.getStrWidth(text); gravity.display.drawStr(SCREEN_CENTER_X - (textWidth / 2), y, text); } // Helper function to draw right-aligned text void drawRightAlignedText(const char* text, int y) { - int textWidth = gravity.display.getUTF8Width(text); + int textWidth = gravity.display.getStrWidth(text); int drawX = (SCREEN_WIDTH - textWidth) - MENU_BOX_PADDING; gravity.display.drawStr(drawX, y, text); } -- 2.39.5 From d39954d6c532677d36b3abe1a6c03f55f7bd5e9c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 16:44:28 -0700 Subject: [PATCH 05/54] Rename from shuffle to swing. Add indicator for aligning swing hits with notes. --- examples/Gravity/Gravity.ino | 4 +-- examples/Gravity/app_state.h | 2 +- examples/Gravity/channel.h | 8 +++--- examples/Gravity/display.h | 43 ++++++++++++++++++++++++--------- examples/Gravity/save_state.cpp | 4 +-- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index d49592e..adb4b54 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -210,8 +210,8 @@ void editChannelParameter(int val) { case PARAM_CH_OFFSET: ch.setOffset(ch.getOffset() + val); break; - case PARAM_CH_SHUFFLE: - ch.setShuffle(ch.getShuffle() + val); + case PARAM_CH_SWING: + ch.setSwing(ch.getSwing() + val); break; case PARAM_CH_CV_SRC: { int source = static_cast(ch.getCvSource()); diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index f7f2d96..06cec2a 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -38,7 +38,7 @@ enum ParamsChannelPage { PARAM_CH_PROB, PARAM_CH_DUTY, PARAM_CH_OFFSET, - PARAM_CH_SHUFFLE, + PARAM_CH_SWING, PARAM_CH_CV_SRC, PARAM_CH_CV_DEST, PARAM_CH_LAST, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index d737995..bd42a28 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -18,7 +18,7 @@ enum CvDestination { CV_DEST_PROB, CV_DEST_DUTY, CV_DEST_OFFSET, - CV_DEST_SHUFFLE, + CV_DEST_SWING, CV_DEST_LAST, }; @@ -59,7 +59,7 @@ class Channel { void setProbability(int prob) { base_probability = constrain(prob, 0, 100); } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); } void setOffset(int off) { base_offset = constrain(off, 0, 100); } - void setShuffle(int val) { base_shuffle = constrain(val, 0, 50); } + void setSwing(int val) { base_shuffle = constrain(val, 0, 50); } void setCvSource(CvSource source) { cv_source = source; } void setCvDestination(CvDestination dest) { cv_destination = dest; } @@ -68,7 +68,7 @@ class Channel { int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } - int getShuffle(bool withCvMod = false) const { return withCvMod ? cvmod_shuffle : base_shuffle; } + int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_shuffle : base_shuffle; } int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } @@ -144,7 +144,7 @@ class Channel { ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) : base_offset; - cvmod_shuffle = (cv_destination == CV_DEST_SHUFFLE) + cvmod_shuffle = (cv_destination == CV_DEST_SWING) ? constrain(base_shuffle + map(value, -512, 512, -25, 25), 0, 50) : base_shuffle; } diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 006915c..c46ce4a 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -96,16 +96,16 @@ void drawRightAlignedText(const char* text, int y) { void drawSelectHero() { gravity.display.setDrawColor(1); const int tickSize = 3; - const int heroWidth = SCREEN_WIDTH/2; + const int heroWidth = SCREEN_WIDTH / 2; const int heroHeight = 49; gravity.display.drawLine(0, 0, tickSize, 0); gravity.display.drawLine(0, 0, 0, tickSize); - gravity.display.drawLine(heroWidth, 0, heroWidth-tickSize, 0); + gravity.display.drawLine(heroWidth, 0, heroWidth - tickSize, 0); gravity.display.drawLine(heroWidth, 0, heroWidth, tickSize); - gravity.display.drawLine(heroWidth, heroHeight, heroWidth, heroHeight-tickSize); - gravity.display.drawLine(heroWidth, heroHeight, heroWidth-tickSize, heroHeight); + gravity.display.drawLine(heroWidth, heroHeight, heroWidth, heroHeight - tickSize); + gravity.display.drawLine(heroWidth, heroHeight, heroWidth - tickSize, heroHeight); gravity.display.drawLine(0, heroHeight, tickSize, heroHeight); - gravity.display.drawLine(0, heroHeight, 0, heroHeight-tickSize); + gravity.display.drawLine(0, heroHeight, 0, heroHeight - tickSize); gravity.display.setDrawColor(2); } @@ -147,6 +147,24 @@ void drawMenuItems(const char* menu_items[], int menu_size) { } } +void swingDivisionMark() { + auto& ch = GetSelectedChannel(); + switch (ch.getSwing() + 50) { + case 58: // 1/32nd + case 66: // 1/16th + case 75: // 1/8th + gravity.display.drawBox(56, 4, 4, 4); + break; + case 54: // 1/32nd tripplet + case 62: // 1/16th tripplet + case 71: // 1/8th tripplet + gravity.display.drawBox(56, 4, 4, 4); + gravity.display.drawBox(57, 5, 2, 2); + break; + + } +} + // Main display functions void DisplayMainPage() { @@ -244,11 +262,12 @@ void DisplayChannelPage() { sprintf(mainText, "%d%%", ch.getOffset(withCvMod)); subText = "SHIFT HIT"; break; - case PARAM_CH_SHUFFLE: - ch.getShuffle() == 0 + case PARAM_CH_SWING: + ch.getSwing() == 0 ? sprintf(mainText, "OFF") - : sprintf(mainText, "%d%%", ch.getShuffle(withCvMod) + 50); - subText = "SHUFFLE"; + : sprintf(mainText, "%d%%", ch.getSwing(withCvMod) + 50); + subText = "DOWN BEAT"; + swingDivisionMark(); break; case PARAM_CH_CV_SRC: { switch (ch.getCvSource()) { @@ -289,9 +308,9 @@ void DisplayChannelPage() { sprintf(mainText, "DEST"); subText = "OFFSET"; break; - case CV_DEST_SHUFFLE: + case CV_DEST_SWING: sprintf(mainText, "DEST"); - subText = "SHUFFLE"; + subText = "SWING"; break; } break; @@ -303,7 +322,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items const char* menu_items[PARAM_CH_LAST] = { - "MOD", "PROBABILITY", "DUTY", "OFFSET", "SHUFFLE", "CV SOURCE", "CV DEST"}; + "MOD", "PROBABILITY", "DUTY", "OFFSET", "SWING", "CV SOURCE", "CV DEST"}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index ee39dc3..f7529ed 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -27,7 +27,7 @@ bool StateManager::initialize(AppState& app) { ch.setProbability(saved_ch_state.base_probability); ch.setDutyCycle(saved_ch_state.base_duty_cycle); ch.setOffset(saved_ch_state.base_offset); - ch.setShuffle(saved_ch_state.base_shuffle); + ch.setSwing(saved_ch_state.base_shuffle); ch.setCvSource(static_cast(saved_ch_state.cv_source)); ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); } @@ -106,7 +106,7 @@ void StateManager::_saveState(const AppState& app) { save_ch.base_probability = ch.getProbability(false); save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); - save_ch.base_shuffle = ch.getShuffle(); + save_ch.base_shuffle = ch.getSwing(); save_ch.cv_source = static_cast(ch.getCvSource()); save_ch.cv_destination = static_cast(ch.getCvDestination()); } -- 2.39.5 From afcda2d911176cd3d9e2836aa60d75b00834c411 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 21:33:51 -0700 Subject: [PATCH 06/54] Fix swing offset, update additional var names from shuffle to swing. --- examples/Gravity/channel.h | 31 ++++++++++++++++--------------- examples/Gravity/display.h | 6 +++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index bd42a28..fa4b3f6 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -40,7 +40,7 @@ class Channel { base_probability = 100; base_duty_cycle = 50; base_offset = 0; - base_shuffle = 0; + base_swing = 50; cv_source = CV_NONE; cv_destination = CV_DEST_NONE; @@ -48,7 +48,7 @@ class Channel { cvmod_probability = base_probability; cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; - cvmod_shuffle = base_shuffle; + cvmod_swing = base_swing; } // Setters (Set the BASE value) @@ -58,8 +58,8 @@ class Channel { } void setProbability(int prob) { base_probability = constrain(prob, 0, 100); } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); } - void setOffset(int off) { base_offset = constrain(off, 0, 100); } - void setSwing(int val) { base_shuffle = constrain(val, 0, 50); } + void setOffset(int off) { base_offset = constrain(off, 0, 99); } + void setSwing(int val) { base_swing = constrain(val, 50, 95); } void setCvSource(CvSource source) { cv_source = source; } void setCvDestination(CvDestination dest) { cv_destination = dest; } @@ -68,7 +68,7 @@ class Channel { int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } - int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_shuffle : base_shuffle; } + int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } @@ -86,13 +86,14 @@ class Channel { const uint32_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); const uint32_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); - uint32_t shuffle_pulses = 0; + uint32_t swing_pulses = 0; // Check step increment for odd beats. - if ((tick / mod_pulses) % 2 == 1) { - shuffle_pulses = (long)((mod_pulses * (100L - cvmod_shuffle)) / 100L); + if (cvmod_swing > 50 && (tick / mod_pulses) % 2 == 1) { + int shifted_swing = cvmod_swing - 50; + swing_pulses = (long)((mod_pulses * (100L - shifted_swing)) / 100L); } - const uint32_t current_tick_offset = tick + offset_pulses + shuffle_pulses; + const uint32_t current_tick_offset = tick + offset_pulses + swing_pulses; // Step check if (!output.On()) { @@ -118,7 +119,7 @@ class Channel { cvmod_probability = base_probability; cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; - cvmod_shuffle = base_shuffle; + cvmod_swing = base_swing; return; } @@ -144,9 +145,9 @@ class Channel { ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) : base_offset; - cvmod_shuffle = (cv_destination == CV_DEST_SWING) - ? constrain(base_shuffle + map(value, -512, 512, -25, 25), 0, 50) - : base_shuffle; + cvmod_swing = (cv_destination == CV_DEST_SWING) + ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) + : base_swing; } private: @@ -155,14 +156,14 @@ class Channel { byte base_probability; byte base_duty_cycle; byte base_offset; - byte base_shuffle; + byte base_swing; // Base value with cv mod applied. byte cvmod_clock_mod_index; byte cvmod_probability; byte cvmod_duty_cycle; byte cvmod_offset; - byte cvmod_shuffle; + byte cvmod_swing; // CV configuration CvSource cv_source = CV_NONE; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index c46ce4a..d0db752 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -149,7 +149,7 @@ void drawMenuItems(const char* menu_items[], int menu_size) { void swingDivisionMark() { auto& ch = GetSelectedChannel(); - switch (ch.getSwing() + 50) { + switch (ch.getSwing()) { case 58: // 1/32nd case 66: // 1/16th case 75: // 1/8th @@ -263,9 +263,9 @@ void DisplayChannelPage() { subText = "SHIFT HIT"; break; case PARAM_CH_SWING: - ch.getSwing() == 0 + ch.getSwing() == 50 ? sprintf(mainText, "OFF") - : sprintf(mainText, "%d%%", ch.getSwing(withCvMod) + 50); + : sprintf(mainText, "%d%%", ch.getSwing(withCvMod)); subText = "DOWN BEAT"; swingDivisionMark(); break; -- 2.39.5 From 05b0989f91e7ef26e5fffbd7518e6a978a868120 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 22:23:44 -0700 Subject: [PATCH 07/54] minor cleanup --- examples/Gravity/channel.h | 37 +++++++++++++++++++++---------------- examples/Gravity/display.h | 1 - 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index fa4b3f6..0901398 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -129,25 +129,30 @@ class Channel { // Calculate and store cv modded values using bipolar mapping. // Default to base value if not the current CV destination. - cvmod_clock_mod_index = (cv_destination == CV_DEST_MOD) - ? constrain(base_clock_mod_index + map(value, -512, 512, -10, 10), 0, MOD_CHOICE_SIZE - 1) - : base_clock_mod_index; + cvmod_clock_mod_index = + (cv_destination == CV_DEST_MOD) + ? constrain(base_clock_mod_index + map(value, -512, 512, -10, 10), 0, MOD_CHOICE_SIZE - 1) + : base_clock_mod_index; - cvmod_probability = (cv_destination == CV_DEST_PROB) - ? constrain(base_probability + map(value, -512, 512, -50, 50), 0, 100) - : base_probability; + cvmod_probability = + (cv_destination == CV_DEST_PROB) + ? constrain(base_probability + map(value, -512, 512, -50, 50), 0, 100) + : base_probability; - cvmod_duty_cycle = (cv_destination == CV_DEST_DUTY) - ? constrain(base_duty_cycle + map(value, -512, 512, -50, 50), 1, 99) - : base_duty_cycle; + cvmod_duty_cycle = + (cv_destination == CV_DEST_DUTY) + ? constrain(base_duty_cycle + map(value, -512, 512, -50, 50), 1, 99) + : base_duty_cycle; - cvmod_offset = (cv_destination == CV_DEST_OFFSET) - ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) - : base_offset; - - cvmod_swing = (cv_destination == CV_DEST_SWING) - ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) - : base_swing; + cvmod_offset = + (cv_destination == CV_DEST_OFFSET) + ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) + : base_offset; + + cvmod_swing = + (cv_destination == CV_DEST_SWING) + ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) + : base_swing; } private: diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index d0db752..26388f5 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -161,7 +161,6 @@ void swingDivisionMark() { gravity.display.drawBox(56, 4, 4, 4); gravity.display.drawBox(57, 5, 2, 2); break; - } } -- 2.39.5 From ec7df0e517be342dc5f64b40f1adf667c09d41f5 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 22:36:25 -0700 Subject: [PATCH 08/54] reduce memory footprint --- examples/Gravity/channel.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 0901398..12a858e 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -82,18 +82,18 @@ class Channel { */ void processClockTick(uint32_t tick, DigitalOutput& output) { // Calculate output duty cycle state using cv modded values to determine pulse counts. - const uint32_t mod_pulses = clock_mod_pulses[cvmod_clock_mod_index]; - const uint32_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); - const uint32_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); + const uint16_t mod_pulses = clock_mod_pulses[cvmod_clock_mod_index]; + const uint16_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); + const uint16_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); - uint32_t swing_pulses = 0; + uint16_t swing_pulses = 0; // Check step increment for odd beats. if (cvmod_swing > 50 && (tick / mod_pulses) % 2 == 1) { int shifted_swing = cvmod_swing - 50; swing_pulses = (long)((mod_pulses * (100L - shifted_swing)) / 100L); } - const uint32_t current_tick_offset = tick + offset_pulses + swing_pulses; + const uint16_t current_tick_offset = tick + offset_pulses + swing_pulses; // Step check if (!output.On()) { @@ -106,7 +106,7 @@ class Channel { } // Duty cycle low check - const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses; + const uint16_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + swing_pulses; if (duty_cycle_end_tick % mod_pulses == 0) { output.Low(); } -- 2.39.5 From 572b47f9d168a0e17ccd5e82f6145715b33fe6d1 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 22:46:38 -0700 Subject: [PATCH 09/54] more minor cleanup --- examples/Gravity/app_state.h | 2 +- examples/Gravity/channel.h | 4 ++-- examples/Gravity/display.h | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 06cec2a..bb9f662 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -14,7 +14,7 @@ struct AppState { int selected_param = 0; int selected_sub_param = 0; byte selected_channel = 0; // 0=tempo, 1-6=output channel - byte selected_shuffle = 0; // index into shuffle template + byte selected_shuffle = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; Channel channel[Gravity::OUTPUT_COUNT]; }; diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 12a858e..3567643 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -93,7 +93,7 @@ class Channel { swing_pulses = (long)((mod_pulses * (100L - shifted_swing)) / 100L); } - const uint16_t current_tick_offset = tick + offset_pulses + swing_pulses; + const uint32_t current_tick_offset = tick + offset_pulses + swing_pulses; // Step check if (!output.On()) { @@ -106,7 +106,7 @@ class Channel { } // Duty cycle low check - const uint16_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + swing_pulses; + const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + swing_pulses; if (duty_cycle_end_tick % mod_pulses == 0) { output.Low(); } diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 26388f5..4396d05 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -147,6 +147,7 @@ void drawMenuItems(const char* menu_items[], int menu_size) { } } +// Display an indicator when swing percentage matches a musical note. void swingDivisionMark() { auto& ch = GetSelectedChannel(); switch (ch.getSwing()) { -- 2.39.5 From fef02d980f58567b6a632868a5cd90d88759e5aa Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 21 Jun 2025 23:13:32 -0700 Subject: [PATCH 10/54] Do not wrap menu selections. --- examples/Gravity/Gravity.ino | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index adb4b54..8b2234c 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -21,8 +21,8 @@ #include "app_state.h" #include "channel.h" -#include "save_state.h" #include "display.h" +#include "save_state.h" AppState app; StateManager stateManager; @@ -229,8 +229,7 @@ void editChannelParameter(int val) { } void updateSelection(int& param, int change, int maxValue) { - // This formula correctly handles positive and negative wrapping. - param = (param + change % maxValue + maxValue) % maxValue; + param = constrain(param + change, 0, maxValue - 1); } // -- 2.39.5 From 973c13b8efdc3bdd2d4f9feaaad2378f5adf7abe Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 22 Jun 2025 18:38:51 +0000 Subject: [PATCH 11/54] Add per-channel Swing configuration (#7) Select swing amount from a percentage range of the beat starting a 50% (unchanged) to a max swing amount of 95% (about 1/32nd note before end of period). Swing percentage shows an indicator marker when the percentage lines up with a quantized note on the grid. This is probably going to be the last feature because it is pushing up against the limits of available dynamic memory. Out of scope changes: - selecting parameters / values no longer wraps - reduce dynamic memory used in processClockTick - various readability formatting Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/7 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- examples/Gravity/Gravity.ino | 8 ++-- examples/Gravity/app_state.h | 2 + examples/Gravity/channel.h | 71 +++++++++++++++++++++++---------- examples/Gravity/display.h | 41 ++++++++++++++++--- examples/Gravity/save_state.cpp | 2 + examples/Gravity/save_state.h | 3 +- 6 files changed, 95 insertions(+), 32 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 5869a92..8b2234c 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -21,8 +21,8 @@ #include "app_state.h" #include "channel.h" -#include "save_state.h" #include "display.h" +#include "save_state.h" AppState app; StateManager stateManager; @@ -210,6 +210,9 @@ void editChannelParameter(int val) { case PARAM_CH_OFFSET: ch.setOffset(ch.getOffset() + val); break; + case PARAM_CH_SWING: + ch.setSwing(ch.getSwing() + val); + break; case PARAM_CH_CV_SRC: { int source = static_cast(ch.getCvSource()); updateSelection(source, val, CV_LAST); @@ -226,8 +229,7 @@ void editChannelParameter(int val) { } void updateSelection(int& param, int change, int maxValue) { - // This formula correctly handles positive and negative wrapping. - param = (param + change % maxValue + maxValue) % maxValue; + param = constrain(param + change, 0, maxValue - 1); } // diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 09cd256..bb9f662 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -14,6 +14,7 @@ struct AppState { int selected_param = 0; int selected_sub_param = 0; byte selected_channel = 0; // 0=tempo, 1-6=output channel + byte selected_shuffle = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; Channel channel[Gravity::OUTPUT_COUNT]; }; @@ -37,6 +38,7 @@ enum ParamsChannelPage { PARAM_CH_PROB, PARAM_CH_DUTY, PARAM_CH_OFFSET, + PARAM_CH_SWING, PARAM_CH_CV_SRC, PARAM_CH_CV_DEST, PARAM_CH_LAST, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index b7115a3..3567643 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -18,6 +18,7 @@ enum CvDestination { CV_DEST_PROB, CV_DEST_DUTY, CV_DEST_OFFSET, + CV_DEST_SWING, CV_DEST_LAST, }; @@ -39,6 +40,7 @@ class Channel { base_probability = 100; base_duty_cycle = 50; base_offset = 0; + base_swing = 50; cv_source = CV_NONE; cv_destination = CV_DEST_NONE; @@ -46,6 +48,7 @@ class Channel { cvmod_probability = base_probability; cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; + cvmod_swing = base_swing; } // Setters (Set the BASE value) @@ -55,7 +58,8 @@ class Channel { } void setProbability(int prob) { base_probability = constrain(prob, 0, 100); } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); } - void setOffset(int off) { base_offset = constrain(off, 0, 100); } + void setOffset(int off) { base_offset = constrain(off, 0, 99); } + void setSwing(int val) { base_swing = constrain(val, 50, 95); } void setCvSource(CvSource source) { cv_source = source; } void setCvDestination(CvDestination dest) { cv_destination = dest; } @@ -64,6 +68,7 @@ class Channel { int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } + int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } @@ -77,21 +82,31 @@ class Channel { */ void processClockTick(uint32_t tick, DigitalOutput& output) { // Calculate output duty cycle state using cv modded values to determine pulse counts. - const uint32_t mod_pulses = clock_mod_pulses[cvmod_clock_mod_index]; - const uint32_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); - const uint32_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); + const uint16_t mod_pulses = clock_mod_pulses[cvmod_clock_mod_index]; + const uint16_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); + const uint16_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); - const uint32_t current_tick_offset = tick + offset_pulses; + uint16_t swing_pulses = 0; + // Check step increment for odd beats. + if (cvmod_swing > 50 && (tick / mod_pulses) % 2 == 1) { + int shifted_swing = cvmod_swing - 50; + swing_pulses = (long)((mod_pulses * (100L - shifted_swing)) / 100L); + } - // Duty cycle high check - if (current_tick_offset % mod_pulses == 0) { - if (cvmod_probability >= random(0, 100)) { - output.High(); + const uint32_t current_tick_offset = tick + offset_pulses + swing_pulses; + + // Step check + if (!output.On()) { + if (current_tick_offset % mod_pulses == 0) { + // Duty cycle high check + if (cvmod_probability >= random(0, 100)) { + output.High(); + } } } // Duty cycle low check - const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses; + const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + swing_pulses; if (duty_cycle_end_tick % mod_pulses == 0) { output.Low(); } @@ -104,6 +119,7 @@ class Channel { cvmod_probability = base_probability; cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; + cvmod_swing = base_swing; return; } @@ -113,21 +129,30 @@ class Channel { // Calculate and store cv modded values using bipolar mapping. // Default to base value if not the current CV destination. - cvmod_clock_mod_index = (cv_destination == CV_DEST_MOD) - ? constrain(base_clock_mod_index + map(value, -512, 512, -10, 10), 0, MOD_CHOICE_SIZE - 1) - : base_clock_mod_index; + cvmod_clock_mod_index = + (cv_destination == CV_DEST_MOD) + ? constrain(base_clock_mod_index + map(value, -512, 512, -10, 10), 0, MOD_CHOICE_SIZE - 1) + : base_clock_mod_index; - cvmod_probability = (cv_destination == CV_DEST_PROB) - ? constrain(base_probability + map(value, -512, 512, -50, 50), 0, 100) - : base_probability; + cvmod_probability = + (cv_destination == CV_DEST_PROB) + ? constrain(base_probability + map(value, -512, 512, -50, 50), 0, 100) + : base_probability; - cvmod_duty_cycle = (cv_destination == CV_DEST_DUTY) - ? constrain(base_duty_cycle + map(value, -512, 512, -50, 50), 1, 99) - : base_duty_cycle; + cvmod_duty_cycle = + (cv_destination == CV_DEST_DUTY) + ? constrain(base_duty_cycle + map(value, -512, 512, -50, 50), 1, 99) + : base_duty_cycle; - cvmod_offset = (cv_destination == CV_DEST_OFFSET) - ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) - : base_offset; + cvmod_offset = + (cv_destination == CV_DEST_OFFSET) + ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) + : base_offset; + + cvmod_swing = + (cv_destination == CV_DEST_SWING) + ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) + : base_swing; } private: @@ -136,12 +161,14 @@ class Channel { byte base_probability; byte base_duty_cycle; byte base_offset; + byte base_swing; // Base value with cv mod applied. byte cvmod_clock_mod_index; byte cvmod_probability; byte cvmod_duty_cycle; byte cvmod_offset; + byte cvmod_swing; // CV configuration CvSource cv_source = CV_NONE; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index b9994ed..6cce08e 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -96,16 +96,16 @@ void drawRightAlignedText(const char* text, int y) { void drawSelectHero() { gravity.display.setDrawColor(1); const int tickSize = 3; - const int heroWidth = SCREEN_WIDTH/2; + const int heroWidth = SCREEN_WIDTH / 2; const int heroHeight = 49; gravity.display.drawLine(0, 0, tickSize, 0); gravity.display.drawLine(0, 0, 0, tickSize); - gravity.display.drawLine(heroWidth, 0, heroWidth-tickSize, 0); + gravity.display.drawLine(heroWidth, 0, heroWidth - tickSize, 0); gravity.display.drawLine(heroWidth, 0, heroWidth, tickSize); - gravity.display.drawLine(heroWidth, heroHeight, heroWidth, heroHeight-tickSize); - gravity.display.drawLine(heroWidth, heroHeight, heroWidth-tickSize, heroHeight); + gravity.display.drawLine(heroWidth, heroHeight, heroWidth, heroHeight - tickSize); + gravity.display.drawLine(heroWidth, heroHeight, heroWidth - tickSize, heroHeight); gravity.display.drawLine(0, heroHeight, tickSize, heroHeight); - gravity.display.drawLine(0, heroHeight, 0, heroHeight-tickSize); + gravity.display.drawLine(0, heroHeight, 0, heroHeight - tickSize); gravity.display.setDrawColor(2); } @@ -147,6 +147,24 @@ void drawMenuItems(const char* menu_items[], int menu_size) { } } +// Display an indicator when swing percentage matches a musical note. +void swingDivisionMark() { + auto& ch = GetSelectedChannel(); + switch (ch.getSwing()) { + case 58: // 1/32nd + case 66: // 1/16th + case 75: // 1/8th + gravity.display.drawBox(56, 4, 4, 4); + break; + case 54: // 1/32nd tripplet + case 62: // 1/16th tripplet + case 71: // 1/8th tripplet + gravity.display.drawBox(56, 4, 4, 4); + gravity.display.drawBox(57, 5, 2, 2); + break; + } +} + // Main display functions void DisplayMainPage() { @@ -244,6 +262,13 @@ void DisplayChannelPage() { sprintf(mainText, "%d%%", ch.getOffset(withCvMod)); subText = "SHIFT HIT"; break; + case PARAM_CH_SWING: + ch.getSwing() == 50 + ? sprintf(mainText, "OFF") + : sprintf(mainText, "%d%%", ch.getSwing(withCvMod)); + subText = "DOWN BEAT"; + swingDivisionMark(); + break; case PARAM_CH_CV_SRC: { switch (ch.getCvSource()) { case CV_NONE: @@ -283,6 +308,10 @@ void DisplayChannelPage() { sprintf(mainText, "DEST"); subText = "OFFSET"; break; + case CV_DEST_SWING: + sprintf(mainText, "DEST"); + subText = "SWING"; + break; } break; } @@ -293,7 +322,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items const char* menu_items[PARAM_CH_LAST] = { - "MOD", "PROBABILITY", "DUTY", "OFFSET", "CV SOURCE", "CV DEST"}; + "MOD", "PROBABILITY", "DUTY", "OFFSET", "SWING", "CV SOURCE", "CV DEST"}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index 87357cb..f7529ed 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -27,6 +27,7 @@ bool StateManager::initialize(AppState& app) { ch.setProbability(saved_ch_state.base_probability); ch.setDutyCycle(saved_ch_state.base_duty_cycle); ch.setOffset(saved_ch_state.base_offset); + ch.setSwing(saved_ch_state.base_shuffle); ch.setCvSource(static_cast(saved_ch_state.cv_source)); ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); } @@ -105,6 +106,7 @@ void StateManager::_saveState(const AppState& app) { save_ch.base_probability = ch.getProbability(false); save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); + save_ch.base_shuffle = ch.getSwing(); save_ch.cv_source = static_cast(ch.getCvSource()); save_ch.cv_destination = static_cast(ch.getCvDestination()); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index b747189..eb3c7a2 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -9,7 +9,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 3; +const byte SKETCH_VERSION = 4; // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -41,6 +41,7 @@ class StateManager { byte base_probability; byte base_duty_cycle; byte base_offset; + byte base_shuffle; byte cv_source; // Cast the CvSource enum to a byte for storage byte cv_destination; // Cast the CvDestination enum as a byte for storage }; -- 2.39.5 From 6fa5674909e77de325c3698b56c1bd70818ed5c0 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 22 Jun 2025 18:44:01 +0000 Subject: [PATCH 12/54] Display memory usage reduction (#8) Convert all string references from const char* to String/F() to store values in flash instead of ram. Memory usage from `main`: ``` Sketch uses 27878 bytes (90%) of program storage space. Maximum is 30720 bytes. Global variables use 1755 bytes (85%) of dynamic memory, leaving 293 bytes for local variables. Maximum is 2048 bytes. ``` Memory usage after these changes: ``` Sketch uses 28054 bytes (91%) of program storage space. Maximum is 30720 bytes. Global variables use 1445 bytes (70%) of dynamic memory, leaving 603 bytes for local variables. Maximum is 2048 bytes. ``` This provides a dynamic memory savings of 310 bytes! Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/8 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- examples/Gravity/display.h | 135 ++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 6cce08e..e1211bc 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -58,11 +58,11 @@ const PROGMEM uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") = #define play_icon_width 14 #define play_icon_height 14 -static const unsigned char play_icon[] = { +static const unsigned char play_icon[] 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[] = { +static const unsigned char pause_icon[] 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}; @@ -93,23 +93,23 @@ void drawRightAlignedText(const char* text, int y) { gravity.display.drawStr(drawX, y, text); } -void drawSelectHero() { +void drawMainSelection() { gravity.display.setDrawColor(1); const int tickSize = 3; - const int heroWidth = SCREEN_WIDTH / 2; - const int heroHeight = 49; + 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(heroWidth, 0, heroWidth - tickSize, 0); - gravity.display.drawLine(heroWidth, 0, heroWidth, tickSize); - gravity.display.drawLine(heroWidth, heroHeight, heroWidth, heroHeight - tickSize); - gravity.display.drawLine(heroWidth, heroHeight, heroWidth - tickSize, heroHeight); - gravity.display.drawLine(0, heroHeight, tickSize, heroHeight); - gravity.display.drawLine(0, heroHeight, 0, heroHeight - 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); gravity.display.setDrawColor(2); } -void drawMenuItems(const char* menu_items[], int menu_size) { +void drawMenuItems(String menu_items[], int menu_size) { // Draw menu items gravity.display.setFont(TEXT_FONT); @@ -128,7 +128,7 @@ void drawMenuItems(const char* menu_items[], int menu_size) { if (app.editing_param) { gravity.display.drawBox(boxX, boxY, boxWidth, boxHeight); - drawSelectHero(); + drawMainSelection(); } else { gravity.display.drawFrame(boxX, boxY, boxWidth, boxHeight); } @@ -143,7 +143,7 @@ void drawMenuItems(const char* menu_items[], int menu_size) { for (int i = 0; i < min(menu_size, VISIBLE_MENU_ITEMS); ++i) { int idx = start_index + i; - drawRightAlignedText(menu_items[idx], MENU_ITEM_HEIGHT * (i + 1) - 1); + drawRightAlignedText(menu_items[idx].c_str(), MENU_ITEM_HEIGHT * (i + 1) - 1); } } @@ -173,54 +173,52 @@ void DisplayMainPage() { gravity.display.setFont(TEXT_FONT); // Display selected editable value - char mainText[8]; - const char* subText; + String mainText; + String subText; switch (app.selected_param) { case PARAM_MAIN_TEMPO: // Serial MIDI is too unstable to display bpm in real time. if (app.selected_source == Clock::SOURCE_EXTERNAL_MIDI) { - sprintf(mainText, "%s", "EXT"); + mainText = F("EXT"); } else { - sprintf(mainText, "%d", gravity.clock.Tempo()); + mainText = String(gravity.clock.Tempo()); } - subText = "BPM"; + subText = F("BPM"); break; case PARAM_MAIN_SOURCE: + mainText = F("EXT"); switch (app.selected_source) { case Clock::SOURCE_INTERNAL: - sprintf(mainText, "%s", "INT"); - subText = "CLOCK"; + mainText = F("INT"); + subText = F("CLOCK"); break; case Clock::SOURCE_EXTERNAL_PPQN_24: - sprintf(mainText, "%s", "EXT"); - subText = "24 PPQN"; + subText = F("24 PPQN"); break; case Clock::SOURCE_EXTERNAL_PPQN_4: - sprintf(mainText, "%s", "EXT"); - subText = "4 PPQN"; + subText = F("4 PPQN"); break; case Clock::SOURCE_EXTERNAL_MIDI: - sprintf(mainText, "%s", "EXT"); - subText = "MIDI"; + subText = F("MIDI"); break; } break; case PARAM_MAIN_ENCODER_DIR: - sprintf(mainText, "%s", "DIR"); - subText = app.selected_sub_param == 0 ? "DEFAULT" : "REVERSED"; + mainText = F("DIR"); + subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); break; case PARAM_MAIN_RESET_STATE: - sprintf(mainText, "%s", "RST"); - subText = app.selected_sub_param == 0 ? "RESET ALL" : "BACK"; + mainText = F("RST"); + subText = app.selected_sub_param == 0 ? F("RESET ALL") : F("BACK"); break; } - drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT); - drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT); + drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); + drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - const char* menu_items[PARAM_MAIN_LAST] = {"TEMPO", "SOURCE", "ENCODER DIR", "RESET"}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("ENCODER DIR"), F("RESET")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -231,8 +229,8 @@ void DisplayChannelPage() { gravity.display.setDrawColor(2); // Display selected editable value - char mainText[5]; - const char* subText; + String mainText; + String subText; // When editing a param, just show the base value. When not editing show // the value with cv mod. @@ -242,87 +240,82 @@ void DisplayChannelPage() { case PARAM_CH_MOD: { int mod_value = ch.getClockMod(withCvMod); if (mod_value > 1) { - sprintf(mainText, "/%d", mod_value); - subText = "DIVIDE"; + mainText = F("/"); + mainText += String(mod_value); + subText = F("DIVIDE"); } else { - sprintf(mainText, "x%d", abs(mod_value)); - subText = "MULTIPLY"; + mainText = F("x"); + mainText += String(abs(mod_value)); + subText = F("MULTIPLY"); } break; } case PARAM_CH_PROB: - sprintf(mainText, "%d%%", ch.getProbability(withCvMod)); - subText = "HIT CHANCE"; + mainText = String(ch.getProbability(withCvMod)) + F("%"); + subText = F("HIT CHANCE"); break; case PARAM_CH_DUTY: - sprintf(mainText, "%d%%", ch.getDutyCycle(withCvMod)); - subText = "PULSE WIDTH"; + mainText = String(ch.getDutyCycle(withCvMod)) + F("%"); + subText = F("PULSE WIDTH"); break; case PARAM_CH_OFFSET: - sprintf(mainText, "%d%%", ch.getOffset(withCvMod)); - subText = "SHIFT HIT"; + mainText = String(ch.getOffset(withCvMod)) + F("%"); + subText = F("SHIFT HIT"); break; case PARAM_CH_SWING: ch.getSwing() == 50 - ? sprintf(mainText, "OFF") - : sprintf(mainText, "%d%%", ch.getSwing(withCvMod)); + ? mainText = F("OFF") + : mainText = String(ch.getSwing(withCvMod)) + F("%"); subText = "DOWN BEAT"; swingDivisionMark(); break; case PARAM_CH_CV_SRC: { switch (ch.getCvSource()) { + mainText = F("SRC"); case CV_NONE: - sprintf(mainText, "SRC"); - subText = "NONE"; + subText = F("NONE"); break; case CV_1: - sprintf(mainText, "SRC"); - subText = "CV 1"; + subText = F("CV 1"); break; case CV_2: - sprintf(mainText, "SRC"); - subText = "CV 2"; + subText = F("CV 2"); break; } break; } case PARAM_CH_CV_DEST: { switch (ch.getCvDestination()) { + mainText = F("DEST"); case CV_DEST_NONE: - sprintf(mainText, "DEST"); - subText = "NONE"; + subText = F("NONE"); break; case CV_DEST_MOD: - sprintf(mainText, "DEST"); - subText = "CLOCK MOD"; + subText = F("CLOCK MOD"); break; case CV_DEST_PROB: - sprintf(mainText, "DEST"); - subText = "PROBABILITY"; + subText = F("PROBABILITY"); break; case CV_DEST_DUTY: - sprintf(mainText, "DEST"); - subText = "DUTY CYCLE"; + subText = F("DUTY CYCLE"); break; case CV_DEST_OFFSET: - sprintf(mainText, "DEST"); - subText = "OFFSET"; + subText = F("OFFSET"); break; case CV_DEST_SWING: - sprintf(mainText, "DEST"); - subText = "SWING"; + subText = F("SWING"); break; } break; } } - drawCenteredText(mainText, MAIN_TEXT_Y, LARGE_FONT); - drawCenteredText(subText, SUB_TEXT_Y, TEXT_FONT); + drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); + drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Channel Page menu items - const char* menu_items[PARAM_CH_LAST] = { - "MOD", "PROBABILITY", "DUTY", "OFFSET", "SWING", "CV SOURCE", "CV DEST"}; + String menu_items[PARAM_CH_LAST] = { + F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("CV SOURCE"), F("CV DEST")}; drawMenuItems(menu_items, PARAM_CH_LAST); } @@ -349,7 +342,7 @@ void DisplaySelectedChannel() { if (i == 0) { gravity.display.setBitmapMode(1); auto icon = gravity.clock.IsPaused() ? pause_icon : play_icon; - gravity.display.drawXBM(2, boxY, play_icon_width, play_icon_height, icon); + gravity.display.drawXBMP(2, boxY, play_icon_width, play_icon_height, icon); } else { gravity.display.setFont(TEXT_FONT); gravity.display.setCursor((i * boxWidth) + textOffset, SCREEN_HEIGHT - 3); -- 2.39.5 From d56355a94b5addf798aea28ded053c7b4175c6ac Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 28 Jun 2025 09:45:50 -0700 Subject: [PATCH 13/54] refactor encoder. no need for Dir enum. --- encoder_dir.h => encoder.h | 39 +++++++----------------------------- examples/Gravity/app_state.h | 4 ++-- gravity.cpp | 6 +++--- gravity.h | 4 ++-- 4 files changed, 14 insertions(+), 39 deletions(-) rename encoder_dir.h => encoder.h (72%) diff --git a/encoder_dir.h b/encoder.h similarity index 72% rename from encoder_dir.h rename to encoder.h index 5375b57..e9a8635 100644 --- a/encoder_dir.h +++ b/encoder.h @@ -16,28 +16,21 @@ #include "button.h" #include "peripherials.h" -enum Direction { - DIRECTION_UNCHANGED, - DIRECTION_INCREMENT, - DIRECTION_DECREMENT, -}; - -class EncoderDir { +class Encoder { protected: typedef void (*CallbackFunction)(void); - typedef void (*RotateCallbackFunction)(Direction dir, int val); + typedef void (*RotateCallbackFunction)(int val); CallbackFunction on_press; RotateCallbackFunction on_press_rotate; RotateCallbackFunction on_rotate; int change; - Direction dir; public: - EncoderDir() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3), + Encoder() : encoder_(ENCODER_PIN1, ENCODER_PIN2, RotaryEncoder::LatchMode::FOUR3), button_(ENCODER_SW_PIN) { _instance = this; } - ~EncoderDir() {} + ~Encoder() {} // Set to true if the encoder read direction should be reversed. void SetReverseDirection(bool reversed) { @@ -55,12 +48,6 @@ class EncoderDir { on_press_rotate = f; } - // Parse EncoderButton increment direction. - Direction RotateDirection() { - int dir = (int)(encoder_.getDirection()); - return rotate_(dir, reversed_); - } - void Process() { // Get encoder position change amount. int encoder_rotated = _rotate_change() != 0; @@ -70,9 +57,9 @@ class EncoderDir { // Handle encoder position change and button press. if (button_pressed && encoder_rotated) { rotated_while_held_ = true; - if (on_press_rotate != NULL) on_press_rotate(dir, change); + if (on_press_rotate != NULL) on_press_rotate(change); } else if (!button_pressed && encoder_rotated) { - if (on_rotate != NULL) on_rotate(dir, change); + if (on_rotate != NULL) on_rotate(change); } else if (button_.Change() == Button::CHANGE_RELEASED && !rotated_while_held_) { if (on_press != NULL) on_press(); } @@ -91,7 +78,7 @@ class EncoderDir { } private: - static EncoderDir* _instance; + static Encoder* _instance; int previous_pos_; bool rotated_while_held_; @@ -112,7 +99,6 @@ class EncoderDir { // Update state variables. change = position - previous_pos_; previous_pos_ = position; - dir = RotateDirection(); // Encoder rotate acceleration. if (ms < 16) { @@ -126,17 +112,6 @@ class EncoderDir { } return change; } - - inline Direction rotate_(int dir, bool reversed) { - switch (dir) { - case 1: - return (reversed) ? DIRECTION_DECREMENT : DIRECTION_INCREMENT; - case -1: - return (reversed) ? DIRECTION_INCREMENT : DIRECTION_DECREMENT; - default: - return DIRECTION_UNCHANGED; - } - } }; #endif \ No newline at end of file diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index bb9f662..3f78caf 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -11,8 +11,8 @@ struct AppState { bool encoder_reversed = false; bool refresh_screen = true; bool editing_param = false; - int selected_param = 0; - int selected_sub_param = 0; + byte selected_param = 0; + byte selected_sub_param = 0; byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_shuffle = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; diff --git a/gravity.cpp b/gravity.cpp index 159dc6f..658a960 100644 --- a/gravity.cpp +++ b/gravity.cpp @@ -13,7 +13,7 @@ // Initialize the static pointer for the EncoderDir class to null. We want to // have a static pointer to decouple the ISR from the global gravity object. -EncoderDir* EncoderDir::_instance = nullptr; +Encoder* Encoder::_instance = nullptr; void Gravity::Init() { initClock(); @@ -74,11 +74,11 @@ void Gravity::Process() { // Pin Change Interrupt on Port D (D4). ISR(PCINT2_vect) { - EncoderDir::isr(); + Encoder::isr(); }; // Pin Change Interrupt on Port C (D17/A3). ISR(PCINT1_vect) { - EncoderDir::isr(); + Encoder::isr(); }; // Global instance diff --git a/gravity.h b/gravity.h index 2002616..1cc424b 100644 --- a/gravity.h +++ b/gravity.h @@ -8,7 +8,7 @@ #include "button.h" #include "clock.h" #include "digital_output.h" -#include "encoder_dir.h" +#include "encoder.h" #include "peripherials.h" // Hardware abstraction wrapper for the Gravity module. @@ -32,7 +32,7 @@ class Gravity { U8G2_SSD1306_128X64_NONAME_1_HW_I2C display; // OLED display object. Clock clock; // Clock source wrapper. DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object. - EncoderDir encoder; // Rotary encoder with button instance + Encoder encoder; // Rotary encoder with button instance Button shift_button; Button play_button; AnalogInput cv1; -- 2.39.5 From 6d0a9f9f7fc61bcb180a407984e2b1ec3a8f2810 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 28 Jun 2025 09:46:50 -0700 Subject: [PATCH 14/54] additional refactoring, small memory reduction. --- examples/Gravity/Gravity.ino | 25 +++++++++++++------------ examples/Gravity/display.h | 12 ++++++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 8b2234c..7604f94 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -142,7 +142,7 @@ void HandleEncoderPressed() { app.refresh_screen = true; } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (!app.editing_param) { // Navigation Mode const int max_param = (app.selected_channel == 0) ? PARAM_MAIN_LAST : PARAM_CH_LAST; @@ -158,12 +158,8 @@ void HandleRotate(Direction dir, int val) { app.refresh_screen = true; } -void HandlePressedRotate(Direction dir, int val) { - if (dir == DIRECTION_INCREMENT && app.selected_channel < Gravity::OUTPUT_COUNT) { - app.selected_channel++; - } else if (dir == DIRECTION_DECREMENT && app.selected_channel > 0) { - app.selected_channel--; - } +void HandlePressedRotate(int val) { + updateSelection(app.selected_channel, val, Gravity::OUTPUT_COUNT + 1); app.selected_param = 0; stateManager.markDirty(); app.refresh_screen = true; @@ -180,7 +176,7 @@ void editMainParameter(int val) { break; case PARAM_MAIN_SOURCE: { - int source = static_cast(app.selected_source); + byte source = static_cast(app.selected_source); updateSelection(source, val, Clock::SOURCE_LAST); app.selected_source = static_cast(source); gravity.clock.SetSource(app.selected_source); @@ -214,13 +210,13 @@ void editChannelParameter(int val) { ch.setSwing(ch.getSwing() + val); break; case PARAM_CH_CV_SRC: { - int source = static_cast(ch.getCvSource()); + byte source = static_cast(ch.getCvSource()); updateSelection(source, val, CV_LAST); ch.setCvSource(static_cast(source)); break; } case PARAM_CH_CV_DEST: { - int dest = static_cast(ch.getCvDestination()); + byte dest = static_cast(ch.getCvDestination()); updateSelection(dest, val, CV_DEST_LAST); ch.setCvDestination(static_cast(dest)); break; @@ -228,12 +224,17 @@ void editChannelParameter(int val) { } } -void updateSelection(int& param, int change, int maxValue) { +// Changes the param by the value provided. +void updateSelection(byte& param, int change, int maxValue) { + // Do not apply acceleration if max value is less than 25. + if (maxValue < 25) { + change = change > 0 ? 1 : -1; + } param = constrain(param + change, 0, maxValue - 1); } // -// Helper functions. +// App Helper functions. // void InitAppState(AppState& app) { diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index e1211bc..a65289b 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -9,7 +9,7 @@ // UI Display functions for drawing the UI to the OLED display. // -const PROGMEM uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") = +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" @@ -25,7 +25,7 @@ const PROGMEM uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") = "\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"; -const PROGMEM uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") = +const uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") PROGMEM = "#\0\4\4\4\5\2\1\6\17\30\1\0\27\0\0\0\1\77\0\0\3w%'\17\37\313\330R#&" "\32!F\14\211I\310\24!\65\204(MF\21)Cd\304\10\62b\14\215\60Vb\334\20\0/\14" "\272\336\336d\244\350\263q\343\0\60\37|\377\216!%*\10\35\263\253ChD\30\21bB\14\242S" @@ -58,11 +58,11 @@ const PROGMEM uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") = #define play_icon_width 14 #define play_icon_height 14 -static const unsigned char play_icon[] PROGMEM = { +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[] PROGMEM = { +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}; @@ -270,8 +270,8 @@ void DisplayChannelPage() { swingDivisionMark(); break; case PARAM_CH_CV_SRC: { + mainText = F("SRC"); switch (ch.getCvSource()) { - mainText = F("SRC"); case CV_NONE: subText = F("NONE"); break; @@ -285,8 +285,8 @@ void DisplayChannelPage() { break; } case PARAM_CH_CV_DEST: { + mainText = F("DEST"); switch (ch.getCvDestination()) { - mainText = F("DEST"); case CV_DEST_NONE: subText = F("NONE"); break; -- 2.39.5 From ceb01bf03f5e1153889517e1d0bae346ab5339fa Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 30 Jun 2025 17:23:56 +0000 Subject: [PATCH 15/54] Introduce basic Euclidean Rhythm (#9) Each channel can define a euclidean rhythm by setting a number of steps (up to 16) and a number of hits to evenly distribute within those steps. CV Mod is available, however the cv mod acts as an override instead of a sum mix like the other parameters. Refactor `applyCvMod()` so it is only called if cv mod is active for that channel. Now the setter methods will update the final output value if cv mod is not active. Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/9 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- examples/Gravity/Gravity.ino | 23 ++++++++- examples/Gravity/app_state.h | 2 + examples/Gravity/channel.h | 86 ++++++++++++++++++++++++++------- examples/Gravity/display.h | 17 ++++++- examples/Gravity/euclidean.h | 82 +++++++++++++++++++++++++++++++ examples/Gravity/save_state.cpp | 4 ++ examples/Gravity/save_state.h | 4 +- 7 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 examples/Gravity/euclidean.h diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 7604f94..8a46749 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -60,8 +60,23 @@ void loop() { // Read CVs and call the update function for each channel. int cv1 = gravity.cv1.Read(); int cv2 = gravity.cv2.Read(); + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { - app.channel[i].applyCvMod(cv1, cv2); + auto& ch = app.channel[i]; + // Only apply CV to the channel when the current channel has cv + // mod configured. + if (ch.isCvModActive()) { + // hack -- do not apply mod to euclidean rhythm when editing. + bool editing_euc; + editing_euc |= ch.getCvDestination() == CV_DEST_EUC_STEPS; + editing_euc |= ch.getCvDestination() == CV_DEST_EUC_HITS; + editing_euc &= (app.selected_channel - 1) == i; + editing_euc &= app.editing_param; + if (editing_euc) { + continue; + } + ch.applyCvMod(cv1, cv2); + } } // Check for dirty state eligible to be saved. @@ -209,6 +224,12 @@ void editChannelParameter(int val) { case PARAM_CH_SWING: ch.setSwing(ch.getSwing() + val); break; + case PARAM_CH_EUC_STEPS: + ch.setSteps(ch.getSteps() + val); + break; + case PARAM_CH_EUC_HITS: + ch.setHits(ch.getHits() + val); + break; case PARAM_CH_CV_SRC: { byte source = static_cast(ch.getCvSource()); updateSelection(source, val, CV_LAST); diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 3f78caf..201ac72 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -39,6 +39,8 @@ enum ParamsChannelPage { PARAM_CH_DUTY, PARAM_CH_OFFSET, PARAM_CH_SWING, + PARAM_CH_EUC_STEPS, + PARAM_CH_EUC_HITS, PARAM_CH_CV_SRC, PARAM_CH_CV_DEST, PARAM_CH_LAST, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 3567643..5578d42 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -3,6 +3,7 @@ #include #include +#include "euclidean.h" // Enums for CV configuration enum CvSource { @@ -19,6 +20,8 @@ enum CvDestination { CV_DEST_DUTY, CV_DEST_OFFSET, CV_DEST_SWING, + CV_DEST_EUC_STEPS, + CV_DEST_EUC_HITS, CV_DEST_LAST, }; @@ -41,6 +44,7 @@ class Channel { base_duty_cycle = 50; base_offset = 0; base_swing = 50; + cv_source = CV_NONE; cv_destination = CV_DEST_NONE; @@ -49,17 +53,46 @@ class Channel { cvmod_duty_cycle = base_duty_cycle; cvmod_offset = base_offset; cvmod_swing = base_swing; + + pattern.Init(DEFAULT_PATTERN); } // Setters (Set the BASE value) void setClockMod(int index) { - if (index >= 0 && index < MOD_CHOICE_SIZE) base_clock_mod_index = index; + base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); + if (!isCvModActive()) { + cvmod_clock_mod_index = base_clock_mod_index; + } } - void setProbability(int prob) { base_probability = constrain(prob, 0, 100); } - void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); } - void setOffset(int off) { base_offset = constrain(off, 0, 99); } - void setSwing(int val) { base_swing = constrain(val, 50, 95); } + + void setProbability(int prob) { + base_probability = constrain(prob, 0, 100); + if (!isCvModActive()) { + cvmod_probability = base_probability; + } + } + + void setDutyCycle(int duty) { + base_duty_cycle = constrain(duty, 1, 99); + if (!isCvModActive()) { + cvmod_duty_cycle = base_duty_cycle; + } + } + + void setOffset(int off) { + base_offset = constrain(off, 0, 99); + if (!isCvModActive()) { + cvmod_offset = base_offset; + } + } + void setSwing(int val) { + base_swing = constrain(val, 50, 95); + if (!isCvModActive()) { + cvmod_swing = base_swing; + } + } + void setCvSource(CvSource source) { cv_source = source; } void setCvDestination(CvDestination dest) { cv_destination = dest; } @@ -75,6 +108,12 @@ class Channel { CvDestination getCvDestination() { return cv_destination; } bool isCvModActive() const { return cv_source != CV_NONE && cv_destination != CV_DEST_NONE; } + // Euclidean + void setSteps(int val) { pattern.SetSteps(val); } + void setHits(int val) { pattern.SetHits(val); } + byte getSteps() { return pattern.GetSteps(); } + byte getHits() { return pattern.GetHits(); } + /** * @brief Processes a clock tick and determines if the output should be high or low. * @param tick The current clock tick count. @@ -95,11 +134,21 @@ class Channel { const uint32_t current_tick_offset = tick + offset_pulses + swing_pulses; - // Step check + // Duty cycle high check logic if (!output.On()) { + // Step check if (current_tick_offset % mod_pulses == 0) { - // Duty cycle high check - if (cvmod_probability >= random(0, 100)) { + bool hit = cvmod_probability >= random(0, 100); + // Euclidean rhythm check + switch (pattern.NextStep()) { + case Pattern::REST: // Rest when active or fall back to probability + hit = false; + break; + case Pattern::HIT: // Hit if probability is true + hit &= true; + break; + } + if (hit) { output.High(); } } @@ -113,16 +162,6 @@ class Channel { } void applyCvMod(int cv1_value, int cv2_value) { - if (!isCvModActive()) { - // If CV is off, ensure cv modded values match the base values. - cvmod_clock_mod_index = base_clock_mod_index; - cvmod_probability = base_probability; - cvmod_duty_cycle = base_duty_cycle; - cvmod_offset = base_offset; - cvmod_swing = base_swing; - return; - } - // Use the CV value for current selected cv source. int value = (cv_source == CV_1) ? cv1_value : cv2_value; @@ -153,6 +192,14 @@ class Channel { (cv_destination == CV_DEST_SWING) ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) : base_swing; + + if (cv_destination == CV_DEST_EUC_STEPS) { + pattern.SetSteps(map(value, -512, 512, 0, MAX_PATTERN_LEN)); + } + + if (cv_destination == CV_DEST_EUC_HITS) { + pattern.SetHits(map(value, -512, 512, 0, pattern.GetSteps())); + } } private: @@ -173,6 +220,9 @@ class Channel { // CV configuration CvSource cv_source = CV_NONE; CvDestination cv_destination = CV_DEST_NONE; + + // Euclidean pattern + Pattern pattern; }; #endif // CHANNEL_H \ No newline at end of file diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index a65289b..c1dc144 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -269,6 +269,14 @@ void DisplayChannelPage() { subText = "DOWN BEAT"; swingDivisionMark(); break; + case PARAM_CH_EUC_STEPS: + mainText = String(ch.getSteps()); + subText = "EUCLID STEPS"; + break; + case PARAM_CH_EUC_HITS: + mainText = String(ch.getHits()); + subText = "EUCLID HITS"; + break; case PARAM_CH_CV_SRC: { mainText = F("SRC"); switch (ch.getCvSource()) { @@ -305,6 +313,12 @@ void DisplayChannelPage() { case CV_DEST_SWING: subText = F("SWING"); break; + case CV_DEST_EUC_STEPS: + subText = F("EUCLID STEPS"); + break; + case CV_DEST_EUC_HITS: + subText = F("EUCLID HITS"); + break; } break; } @@ -315,7 +329,8 @@ void DisplayChannelPage() { // Draw Channel Page menu items String menu_items[PARAM_CH_LAST] = { - F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("CV SOURCE"), F("CV DEST")}; + F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("EUCLID STEPS"), + F("EUCLID HITS"), F("CV SOURCE"), F("CV DEST")}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/euclidean.h b/examples/Gravity/euclidean.h new file mode 100644 index 0000000..57e5cb6 --- /dev/null +++ b/examples/Gravity/euclidean.h @@ -0,0 +1,82 @@ +#ifndef EUCLIDEAN_H +#define EUCLIDEAN_H + +#define MAX_PATTERN_LEN 16 + +struct PatternState { + uint8_t steps; + uint8_t hits; +}; + +const PatternState DEFAULT_PATTERN = {1, 1}; + +class Pattern { + public: + Pattern() {} + ~Pattern() {} + + enum Step { + REST, + HIT, + }; + + void Init(PatternState state) { + steps_ = constrain(state.steps, 1, MAX_PATTERN_LEN); + hits_ = constrain(state.hits, 1, steps_); + updatePattern(); + } + + PatternState GetState() { return {steps_, hits_}; } + + Step GetCurrentStep(byte i) { return pattern_[i]; } + + void SetSteps(int steps) { + steps_ = constrain(steps, 1, MAX_PATTERN_LEN); + hits_ = min(hits_, steps_); + updatePattern(); + } + + void SetHits(int hits) { + hits_ = constrain(hits, 1, steps_); + updatePattern(); + } + + void Reset() { step_index_ = 0; } + + uint8_t GetSteps() { return steps_; } + uint8_t GetHits() { return hits_; } + uint8_t GetStepIndex() { return step_index_; } + + // Get the current step value and advance the euclidean rhythm step index + // to the next step in the pattern. + Step NextStep() { + if (steps_ == 0) return REST; + + Step value = GetCurrentStep(step_index_); + step_index_ = (step_index_ < steps_ - 1) ? step_index_ + 1 : 0; + return value; + } + + private: + uint8_t steps_ = 0; + uint8_t hits_ = 0; + volatile uint8_t step_index_ = 0; + Step pattern_[MAX_PATTERN_LEN]; + + // Update the euclidean rhythm pattern when attributes change. + void updatePattern() { + byte bucket = 0; + pattern_[0] = HIT; + for (int i = 1; i < steps_; i++) { + bucket += hits_; + if (bucket >= steps_) { + bucket -= steps_; + pattern_[i] = HIT; + } else { + pattern_[i] = REST; + } + } + } +}; + +#endif \ No newline at end of file diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index f7529ed..aca6977 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -30,6 +30,8 @@ bool StateManager::initialize(AppState& app) { ch.setSwing(saved_ch_state.base_shuffle); ch.setCvSource(static_cast(saved_ch_state.cv_source)); ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); + ch.setSteps(saved_ch_state.euc_steps); + ch.setHits(saved_ch_state.euc_hits); } return true; @@ -109,6 +111,8 @@ void StateManager::_saveState(const AppState& app) { save_ch.base_shuffle = ch.getSwing(); save_ch.cv_source = static_cast(ch.getCvSource()); save_ch.cv_destination = static_cast(ch.getCvDestination()); + save_ch.euc_steps = ch.getSteps(); + save_ch.euc_hits = ch.getHits(); } EEPROM.put(sizeof(Metadata), save_data); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index eb3c7a2..d37a6ae 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -9,7 +9,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 4; +const byte SKETCH_VERSION = 5; // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -44,6 +44,8 @@ class StateManager { byte base_shuffle; byte cv_source; // Cast the CvSource enum to a byte for storage byte cv_destination; // Cast the CvDestination enum as a byte for storage + byte euc_steps; + byte euc_hits; }; // This struct holds all the parameters we want to save. struct EepromData { -- 2.39.5 From edddfd5879952ef8c06f51c82917fa0c5b7a1c00 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 1 Jul 2025 12:23:50 -0700 Subject: [PATCH 16/54] Optimize euclidean pattern by changing from an array of ints to a bitmask. --- examples/Gravity/euclidean.h | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/Gravity/euclidean.h b/examples/Gravity/euclidean.h index 57e5cb6..f18e0ac 100644 --- a/examples/Gravity/euclidean.h +++ b/examples/Gravity/euclidean.h @@ -1,7 +1,7 @@ #ifndef EUCLIDEAN_H #define EUCLIDEAN_H -#define MAX_PATTERN_LEN 16 +#define MAX_PATTERN_LEN 32 struct PatternState { uint8_t steps; @@ -26,9 +26,12 @@ class Pattern { updatePattern(); } - PatternState GetState() { return {steps_, hits_}; } + PatternState GetState() const { return {steps_, hits_}; } - Step GetCurrentStep(byte i) { return pattern_[i]; } + Step GetCurrentStep(byte i) { + if (i >= MAX_PATTERN_LEN) return REST; + return (pattern_bitmap_ & (1UL << i)) ? HIT : REST; + } void SetSteps(int steps) { steps_ = constrain(steps, 1, MAX_PATTERN_LEN); @@ -47,8 +50,6 @@ class Pattern { uint8_t GetHits() { return hits_; } uint8_t GetStepIndex() { return step_index_; } - // Get the current step value and advance the euclidean rhythm step index - // to the next step in the pattern. Step NextStep() { if (steps_ == 0) return REST; @@ -61,19 +62,23 @@ class Pattern { uint8_t steps_ = 0; uint8_t hits_ = 0; volatile uint8_t step_index_ = 0; - Step pattern_[MAX_PATTERN_LEN]; + uint32_t pattern_bitmap_ = 0; - // Update the euclidean rhythm pattern when attributes change. + // Update the euclidean rhythm pattern using bitmap void updatePattern() { + pattern_bitmap_ = 0; // Clear the bitmap + + if (steps_ == 0) return; + byte bucket = 0; - pattern_[0] = HIT; + // Set the first bit (index 0) if it's a HIT + pattern_bitmap_ |= (1UL << 0); + for (int i = 1; i < steps_; i++) { bucket += hits_; if (bucket >= steps_) { bucket -= steps_; - pattern_[i] = HIT; - } else { - pattern_[i] = REST; + pattern_bitmap_ |= (1UL << i); } } } -- 2.39.5 From dd1228be00bd739300ffb846aa7c8ce3afdb302f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 2 Jul 2025 02:45:39 +0000 Subject: [PATCH 17/54] Vendorize uClock (#10) Add copy of uClock to the repo including memory optimization changes. Also add user config setting for changing Pulse Out resolution. Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/10 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- README.md | 5 +- clock.h | 17 +- examples/Gravity/Gravity.ino | 33 +++ examples/Gravity/app_state.h | 2 + examples/Gravity/channel.h | 25 +- examples/Gravity/display.h | 41 +++- examples/Gravity/save_state.cpp | 5 +- examples/Gravity/save_state.h | 1 + gravity.cpp | 4 +- gravity.h | 3 +- uClock.cpp | 409 ++++++++++++++++++++++++++++++++ uClock.h | 180 ++++++++++++++ uClock/platforms/avr.h | 98 ++++++++ uClock/uClock.h | 180 ++++++++++++++ 14 files changed, 966 insertions(+), 37 deletions(-) create mode 100755 uClock.cpp create mode 100755 uClock.h create mode 100644 uClock/platforms/avr.h create mode 100755 uClock/uClock.h diff --git a/README.md b/README.md index 58d0e58..f244487 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,9 @@ Common directory locations: ## Required Third-party Libraries -* [uClock](https://github.com/midilab/uClock) [MIT] - Handle clock tempo, external clock input, and internal clock timer handler. +* [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation. -* [Adafruit_GFX](https://github.com/adafruit/Adafruit-GFX-Library) [BSD] - Graphics helper library. -* [Adafruit_SSD1306](https://github.com/adafruit/Adafruit_SSD1306) [BSD] - Library for interacting with the SSD1306 OLED display. +* [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. ## Example diff --git a/clock.h b/clock.h index 19ed250..00bd213 100644 --- a/clock.h +++ b/clock.h @@ -13,9 +13,9 @@ #define CLOCK_H #include -#include #include "peripherials.h" +#include "uClock.h" // MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 @@ -39,6 +39,14 @@ class Clock { SOURCE_LAST, }; + enum Pulse { + PULSE_NONE, + PULSE_PPQN_1, + PULSE_PPQN_4, + PULSE_PPQN_24, + PULSE_LAST, + }; + void Init() { NeoSerial.begin(31250); @@ -55,7 +63,6 @@ class Clock { uClock.setOnClockStart(sendMIDIStart); uClock.setOnClockStop(sendMIDIStop); uClock.setOnSync24(sendMIDIClock); - uClock.setOnSync48(sendPulseOut); uClock.start(); } @@ -75,7 +82,7 @@ class Clock { void SetSource(Source source) { bool was_playing = !IsPaused(); uClock.stop(); - // If source is currently MIDI, disable the serial interrupt handler. + // If we are changing the source from MIDI, disable the serial interrupt handler. if (source_ == SOURCE_EXTERNAL_MIDI) { NeoSerial.attachInterrupt(serialEventNoop); } @@ -175,10 +182,6 @@ class Clock { static void sendMIDIClock(uint32_t tick) { NeoSerial.write(MIDI_CLOCK); } - - static void sendPulseOut(uint32_t tick) { - digitalWrite(PULSE_OUT_PIN, !digitalRead(PULSE_OUT_PIN)); - } }; #endif \ No newline at end of file diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 8a46749..99c33a5 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -101,6 +101,32 @@ void HandleIntClockTick(uint32_t tick) { } } + // Pulse Out gate + if (app.selected_pulse != Clock::PULSE_NONE) { + int clock_index; + switch (app.selected_pulse) { + case Clock::PULSE_PPQN_24: + clock_index = 0; + break; + case Clock::PULSE_PPQN_4: + clock_index = 4; + break; + case Clock::PULSE_PPQN_1: + clock_index = 7; + break; + } + + const uint16_t pulse_high_ticks = clock_mod_pulses[clock_index]; + const uint32_t pulse_low_ticks = tick + max((long)(pulse_high_ticks / 2), 1L); + + if (tick % pulse_high_ticks == 0) { + gravity.pulse.High(); + } + if (pulse_low_ticks % pulse_high_ticks == 0) { + gravity.pulse.Low(); + } + } + if (!app.editing_param) { app.refresh_screen |= refresh; } @@ -197,6 +223,13 @@ void editMainParameter(int val) { gravity.clock.SetSource(app.selected_source); break; } + case PARAM_MAIN_PULSE: + byte pulse = static_cast(app.selected_pulse); + updateSelection(pulse, val, Clock::PULSE_LAST); + app.selected_pulse = static_cast(pulse); + if (app.selected_pulse == Clock::PULSE_NONE) { + gravity.pulse.Low(); + } case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 201ac72..062ac3c 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -16,6 +16,7 @@ struct AppState { byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_shuffle = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; Channel channel[Gravity::OUTPUT_COUNT]; }; @@ -28,6 +29,7 @@ static Channel& GetSelectedChannel() { enum ParamsMainPage { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, + PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_RESET_STATE, PARAM_MAIN_LAST, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 5578d42..9acbb70 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -3,6 +3,7 @@ #include #include + #include "euclidean.h" // Enums for CV configuration @@ -66,7 +67,7 @@ class Channel { } } - void setProbability(int prob) { + void setProbability(int prob) { base_probability = constrain(prob, 0, 100); if (!isCvModActive()) { cvmod_probability = base_probability; @@ -74,20 +75,20 @@ class Channel { } void setDutyCycle(int duty) { - base_duty_cycle = constrain(duty, 1, 99); + base_duty_cycle = constrain(duty, 1, 99); if (!isCvModActive()) { cvmod_duty_cycle = base_duty_cycle; } } - void setOffset(int off) { + void setOffset(int off) { base_offset = constrain(off, 0, 99); if (!isCvModActive()) { cvmod_offset = base_offset; } } void setSwing(int val) { - base_swing = constrain(val, 50, 95); + base_swing = constrain(val, 50, 95); if (!isCvModActive()) { cvmod_swing = base_swing; } @@ -141,12 +142,12 @@ class Channel { bool hit = cvmod_probability >= random(0, 100); // Euclidean rhythm check switch (pattern.NextStep()) { - case Pattern::REST: // Rest when active or fall back to probability - hit = false; - break; - case Pattern::HIT: // Hit if probability is true - hit &= true; - break; + case Pattern::REST: // Rest when active or fall back to probability + hit = false; + break; + case Pattern::HIT: // Hit if probability is true + hit &= true; + break; } if (hit) { output.High(); @@ -192,11 +193,11 @@ class Channel { (cv_destination == CV_DEST_SWING) ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) : base_swing; - + if (cv_destination == CV_DEST_EUC_STEPS) { pattern.SetSteps(map(value, -512, 512, 0, MAX_PATTERN_LEN)); } - + if (cv_destination == CV_DEST_EUC_HITS) { pattern.SetHits(map(value, -512, 512, 0, pattern.GetSteps())); } diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index c1dc144..038d4bf 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -68,16 +68,16 @@ static const unsigned char pause_icon[28] PROGMEM = { 0x38, 0x0E, 0x00, 0x00}; // Constants for screen layout and fonts -constexpr int SCREEN_CENTER_X = 32; -constexpr int MAIN_TEXT_Y = 26; -constexpr int SUB_TEXT_Y = 40; -constexpr int VISIBLE_MENU_ITEMS = 3; -constexpr int MENU_ITEM_HEIGHT = 14; -constexpr int MENU_BOX_PADDING = 4; -constexpr int MENU_BOX_WIDTH = 64; -constexpr int CHANNEL_BOXES_Y = 50; -constexpr int CHANNEL_BOX_WIDTH = 18; -constexpr int CHANNEL_BOX_HEIGHT = 14; +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; +constexpr uint8_t CHANNEL_BOXES_Y = 50; +constexpr uint8_t CHANNEL_BOX_WIDTH = 18; +constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { @@ -204,6 +204,23 @@ void DisplayMainPage() { break; } break; + case PARAM_MAIN_PULSE: + mainText = F("OUT"); + switch (app.selected_pulse) { + case Clock::PULSE_NONE: + subText = F("PULSE OFF"); + break; + case Clock::PULSE_PPQN_24: + subText = F("24 PPQN PULSE"); + break; + case Clock::PULSE_PPQN_4: + subText = F("4 PPQN PULSE"); + break; + case Clock::PULSE_PPQN_1: + subText = F("1 PPQN PULSE"); + break; + } + break; case PARAM_MAIN_ENCODER_DIR: mainText = F("DIR"); subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); @@ -218,7 +235,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("ENCODER DIR"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("RESET")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -329,7 +346,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items String menu_items[PARAM_CH_LAST] = { - F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("EUCLID STEPS"), + F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("EUCLID STEPS"), F("EUCLID HITS"), F("CV SOURCE"), F("CV DEST")}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index aca6977..c435df8 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -17,6 +17,7 @@ bool StateManager::initialize(AppState& app) { app.selected_param = load_data.selected_param; app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); + app.selected_pulse = static_cast(load_data.selected_pulse); // Loop through and restore each channel's state. for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { @@ -54,6 +55,7 @@ void StateManager::reset(AppState& app) { app.selected_param = 0; app.selected_channel = 0; app.selected_source = Clock::SOURCE_INTERNAL; + app.selected_pulse = Clock::PULSE_PPQN_24; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -61,7 +63,7 @@ void StateManager::reset(AppState& app) { noInterrupts(); _saveMetadata(); // Write the new metadata - _saveState(app); // Write the new (default) app state + _saveState(app); // Write the new (default) app state interrupts(); _isDirty = false; @@ -97,6 +99,7 @@ void StateManager::_saveState(const AppState& app) { save_data.selected_param = app.selected_param; save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); + save_data.selected_pulse = static_cast(app.selected_pulse); // Loop through and populate each channel's state for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index d37a6ae..8a2659e 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -54,6 +54,7 @@ class StateManager { byte selected_param; byte selected_channel; byte selected_source; + byte selected_pulse; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; diff --git a/gravity.cpp b/gravity.cpp index 658a960..3ef6d2b 100644 --- a/gravity.cpp +++ b/gravity.cpp @@ -12,7 +12,7 @@ #include "gravity.h" // Initialize the static pointer for the EncoderDir class to null. We want to -// have a static pointer to decouple the ISR from the global gravity object. +// have a static pointer to decouple the ISR from the global gravity object. Encoder* Encoder::_instance = nullptr; void Gravity::Init() { @@ -52,6 +52,8 @@ void Gravity::initOutputs() { outputs[3].Init(OUT_CH4); outputs[4].Init(OUT_CH5); outputs[5].Init(OUT_CH6); + // Expansion Pulse Output + pulse.Init(PULSE_OUT_PIN); } void Gravity::initDisplay() { // OLED Display configuration. diff --git a/gravity.h b/gravity.h index 1cc424b..5701586 100644 --- a/gravity.h +++ b/gravity.h @@ -32,7 +32,8 @@ class Gravity { U8G2_SSD1306_128X64_NONAME_1_HW_I2C display; // OLED display object. Clock clock; // Clock source wrapper. DigitalOutput outputs[OUTPUT_COUNT]; // An array containing each Output object. - Encoder encoder; // Rotary encoder with button instance + DigitalOutput pulse; // MIDI Expander module pulse output. + Encoder encoder; // Rotary encoder with button instance Button shift_button; Button play_button; AnalogInput cv1; diff --git a/uClock.cpp b/uClock.cpp new file mode 100755 index 0000000..1f53084 --- /dev/null +++ b/uClock.cpp @@ -0,0 +1,409 @@ +/*! + * @file uClock.cpp + * Project BPM clock generator for Arduino + * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) + * @version 2.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include "uClock.h" +#include "uClock/platforms/avr.h" + +// +// Platform specific timer setup/control +// +// initTimer(uint32_t us_interval) and setTimer(uint32_t us_interval) +// are called from architecture specific module included at the +// header of this file +void uclockInitTimer() +{ + // begin at 120bpm + initTimer(uClock.bpmToMicroSeconds(120.00)); +} + +void setTimerTempo(float bpm) +{ + setTimer(uClock.bpmToMicroSeconds(bpm)); +} + +namespace umodular { namespace clock { + +static inline uint32_t phase_mult(uint32_t val) +{ + return (val * PHASE_FACTOR) >> 8; +} + +static inline uint32_t clock_diff(uint32_t old_clock, uint32_t new_clock) +{ + if (new_clock >= old_clock) { + return new_clock - old_clock; + } else { + return new_clock + (4294967295 - old_clock); + } +} + +uClockClass::uClockClass() +{ + tempo = 120; + start_timer = 0; + last_interval = 0; + sync_interval = 0; + clock_state = PAUSED; + clock_mode = INTERNAL_CLOCK; + resetCounters(); + + onOutputPPQNCallback = nullptr; + onSync24Callback = nullptr; + onClockStartCallback = nullptr; + onClockStopCallback = nullptr; + // initialize reference data + calculateReferencedata(); +} + +void uClockClass::init() +{ + if (ext_interval_buffer == nullptr) + setExtIntervalBuffer(1); + + uclockInitTimer(); + // first interval calculus + setTempo(tempo); +} + +uint32_t uClockClass::bpmToMicroSeconds(float bpm) +{ + return (60000000.0f / (float)output_ppqn / bpm); +} + +void uClockClass::calculateReferencedata() +{ + mod_clock_ref = output_ppqn / input_ppqn; + mod_sync24_ref = output_ppqn / PPQN_24; +} + +void uClockClass::setOutputPPQN(PPQNResolution resolution) +{ + // dont allow PPQN lower than PPQN_4 for output clock (to avoid problems with mod_step_ref) + if (resolution < PPQN_4) + return; + + ATOMIC( + output_ppqn = resolution; + calculateReferencedata(); + ) +} + +void uClockClass::setInputPPQN(PPQNResolution resolution) +{ + ATOMIC( + input_ppqn = resolution; + calculateReferencedata(); + ) +} + +void uClockClass::start() +{ + resetCounters(); + start_timer = millis(); + + if (onClockStartCallback) { + onClockStartCallback(); + } + + if (clock_mode == INTERNAL_CLOCK) { + clock_state = STARTED; + } else { + clock_state = STARTING; + } +} + +void uClockClass::stop() +{ + clock_state = PAUSED; + start_timer = 0; + resetCounters(); + if (onClockStopCallback) { + onClockStopCallback(); + } +} + +void uClockClass::pause() +{ + if (clock_mode == INTERNAL_CLOCK) { + if (clock_state == PAUSED) { + start(); + } else { + stop(); + } + } +} + +void uClockClass::setTempo(float bpm) +{ + if (clock_mode == EXTERNAL_CLOCK) { + return; + } + + if (bpm < MIN_BPM || bpm > MAX_BPM) { + return; + } + + ATOMIC( + tempo = bpm + ) + + setTimerTempo(bpm); +} + +float uClockClass::getTempo() +{ + if (clock_mode == EXTERNAL_CLOCK) { + uint32_t acc = 0; + // wait the buffer to get full + if (ext_interval_buffer[ext_interval_buffer_size-1] == 0) { + return tempo; + } + for (uint8_t i=0; i < ext_interval_buffer_size; i++) { + acc += ext_interval_buffer[i]; + } + if (acc != 0) { + return constrainBpm(freqToBpm(acc / ext_interval_buffer_size)); + } + } + return tempo; +} + +// for software timer implementation(fallback for no board support) +void uClockClass::run() {} + +float inline uClockClass::freqToBpm(uint32_t freq) +{ + return 60000000.0f / (float)(freq * input_ppqn); +} + +float inline uClockClass::constrainBpm(float bpm) +{ + return (bpm < MIN_BPM) ? MIN_BPM : ( bpm > MAX_BPM ? MAX_BPM : bpm ); +} + +void uClockClass::setClockMode(ClockMode tempo_mode) +{ + clock_mode = tempo_mode; +} + +uClockClass::ClockMode uClockClass::getClockMode() +{ + return clock_mode; +} + +void uClockClass::clockMe() +{ + if (clock_mode == EXTERNAL_CLOCK) { + ATOMIC( + handleExternalClock() + ) + } +} + +void uClockClass::setExtIntervalBuffer(uint8_t buffer_size) +{ + if (ext_interval_buffer != nullptr) + return; + + // alloc once and forever policy + ext_interval_buffer_size = buffer_size; + ext_interval_buffer = (uint32_t*) malloc( sizeof(uint32_t) * ext_interval_buffer_size ); +} + +void uClockClass::resetCounters() +{ + tick = 0; + int_clock_tick = 0; + mod_clock_counter = 0; + + mod_sync24_counter = 0; + sync24_tick = 0; + + ext_clock_tick = 0; + ext_clock_us = 0; + ext_interval_idx = 0; + + for (uint8_t i=0; i < ext_interval_buffer_size; i++) { + ext_interval_buffer[i] = 0; + } +} + +void uClockClass::handleExternalClock() +{ + switch (clock_state) { + case PAUSED: + break; + + case STARTING: + clock_state = STARTED; + ext_clock_us = micros(); + break; + + case STARTED: + uint32_t now_clock_us = micros(); + last_interval = clock_diff(ext_clock_us, now_clock_us); + ext_clock_us = now_clock_us; + + // external clock tick me! + ext_clock_tick++; + + // accumulate interval incomming ticks data for getTempo() smooth reads on slave clock_mode + if(++ext_interval_idx >= ext_interval_buffer_size) { + ext_interval_idx = 0; + } + ext_interval_buffer[ext_interval_idx] = last_interval; + + if (ext_clock_tick == 1) { + ext_interval = last_interval; + } else { + ext_interval = (((uint32_t)ext_interval * (uint32_t)PLL_X) + (uint32_t)(256 - PLL_X) * (uint32_t)last_interval) >> 8; + } + break; + } +} + +void uClockClass::handleTimerInt() +{ + // track main input clock counter + if (mod_clock_counter == mod_clock_ref) + mod_clock_counter = 0; + + // process sync signals first please... + if (mod_clock_counter == 0) { + + if (clock_mode == EXTERNAL_CLOCK) { + // sync tick position with external tick clock + if ((int_clock_tick < ext_clock_tick) || (int_clock_tick > (ext_clock_tick + 1))) { + int_clock_tick = ext_clock_tick; + tick = int_clock_tick * mod_clock_ref; + mod_clock_counter = tick % mod_clock_ref; + } + + uint32_t counter = ext_interval; + uint32_t now_clock_us = micros(); + sync_interval = clock_diff(ext_clock_us, now_clock_us); + + if (int_clock_tick <= ext_clock_tick) { + counter -= phase_mult(sync_interval); + } else { + if (counter > sync_interval) { + counter += phase_mult(counter - sync_interval); + } + } + + // update internal clock timer frequency + float bpm = constrainBpm(freqToBpm(counter)); + if (bpm != tempo) { + tempo = bpm; + setTimerTempo(bpm); + } + } + + // internal clock tick me! + ++int_clock_tick; + } + ++mod_clock_counter; + + // Sync24 callback + if (onSync24Callback) { + if (mod_sync24_counter == mod_sync24_ref) + mod_sync24_counter = 0; + if (mod_sync24_counter == 0) { + onSync24Callback(sync24_tick); + ++sync24_tick; + } + ++mod_sync24_counter; + } + + // main PPQNCallback + if (onOutputPPQNCallback) { + onOutputPPQNCallback(tick); + ++tick; + } +} + +// elapsed time support +uint8_t uClockClass::getNumberOfSeconds(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return ((_millis - time) / 1000) % SECS_PER_MIN; +} + +uint8_t uClockClass::getNumberOfMinutes(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return (((_millis - time) / 1000) / SECS_PER_MIN) % SECS_PER_MIN; +} + +uint8_t uClockClass::getNumberOfHours(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return (((_millis - time) / 1000) % SECS_PER_DAY) / SECS_PER_HOUR; +} + +uint8_t uClockClass::getNumberOfDays(uint32_t time) +{ + if ( time == 0 ) { + return time; + } + return ((_millis - time) / 1000) / SECS_PER_DAY; +} + +uint32_t uClockClass::getNowTimer() +{ + return _millis; +} + +uint32_t uClockClass::getPlayTime() +{ + return start_timer; +} + +} } // end namespace umodular::clock + +umodular::clock::uClockClass uClock; + +volatile uint32_t _millis = 0; + +// +// TIMER HANDLER +// +void uClockHandler() +{ + // global timer counter + _millis = millis(); + + if (uClock.clock_state == uClock.STARTED) { + uClock.handleTimerInt(); + } +} diff --git a/uClock.h b/uClock.h new file mode 100755 index 0000000..d8670b0 --- /dev/null +++ b/uClock.h @@ -0,0 +1,180 @@ +/*! + * @file uClock.h + * Project BPM clock generator for Arduino + * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) + * @version 2.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef __U_CLOCK_H__ +#define __U_CLOCK_H__ + +#include +#include + +namespace umodular { namespace clock { + +#define MIN_BPM 1 +#define MAX_BPM 400 + +#define PHASE_FACTOR 16 +#define PLL_X 220 + +#define SECS_PER_MIN (60UL) +#define SECS_PER_HOUR (3600UL) +#define SECS_PER_DAY (SECS_PER_HOUR * 24L) + +class uClockClass { + + public: + enum ClockMode { + INTERNAL_CLOCK = 0, + EXTERNAL_CLOCK + }; + + enum ClockState { + PAUSED = 0, + STARTING, + STARTED + }; + + enum PPQNResolution { + PPQN_1 = 1, + PPQN_2 = 2, + PPQN_4 = 4, + PPQN_8 = 8, + PPQN_12 = 12, + PPQN_24 = 24, + PPQN_48 = 48, + PPQN_96 = 96, + PPQN_384 = 384, + PPQN_480 = 480, + PPQN_960 = 960 + }; + + ClockState clock_state; + + uClockClass(); + + void setOnOutputPPQN(void (*callback)(uint32_t tick)) { + onOutputPPQNCallback = callback; + } + + void setOnSync24(void (*callback)(uint32_t tick)) { + onSync24Callback = callback; + } + + void setOnClockStart(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStop(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void setOutputPPQN(PPQNResolution resolution); + void setInputPPQN(PPQNResolution resolution); + + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // for software timer implementation(fallback for no board support) + void run(); + + // external timming control + void setClockMode(ClockMode tempo_mode); + ClockMode getClockMode(); + void clockMe(); + // for smooth slave tempo calculate display you should raise the + // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. + // note: this doesn't impact on sync time, only display time getTempo() + // if you dont want to use it, it is default set it to 1 for memory save + void setExtIntervalBuffer(uint8_t buffer_size); + + // elapsed time support + uint8_t getNumberOfSeconds(uint32_t time); + uint8_t getNumberOfMinutes(uint32_t time); + uint8_t getNumberOfHours(uint32_t time); + uint8_t getNumberOfDays(uint32_t time); + uint32_t getNowTimer(); + uint32_t getPlayTime(); + + uint32_t bpmToMicroSeconds(float bpm); + + private: + float inline freqToBpm(uint32_t freq); + float inline constrainBpm(float bpm); + void calculateReferencedata(); + + void (*onOutputPPQNCallback)(uint32_t tick); + void (*onSync24Callback)(uint32_t tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // clock input/output control + PPQNResolution output_ppqn = PPQN_96; + PPQNResolution input_ppqn = PPQN_24; + // output and internal counters, ticks and references + uint32_t tick; + uint32_t int_clock_tick; + uint8_t mod_clock_counter; + uint16_t mod_clock_ref; + + uint8_t mod_sync24_counter; + uint16_t mod_sync24_ref; + uint32_t sync24_tick; + + // external clock control + volatile uint32_t ext_clock_us; + volatile uint32_t ext_clock_tick; + volatile uint32_t ext_interval; + uint32_t last_interval; + uint32_t sync_interval; + + float tempo; + uint32_t start_timer; + ClockMode clock_mode; + + volatile uint32_t * ext_interval_buffer = nullptr; + uint8_t ext_interval_buffer_size; + uint16_t ext_interval_idx; +}; + +} } // end namespace umodular::clock + +extern umodular::clock::uClockClass uClock; + +extern "C" { + extern volatile uint32_t _millis; +} + +#endif /* __U_CLOCK_H__ */ diff --git a/uClock/platforms/avr.h b/uClock/platforms/avr.h new file mode 100644 index 0000000..c6a25d7 --- /dev/null +++ b/uClock/platforms/avr.h @@ -0,0 +1,98 @@ +/*! + * @file avr.h + * Project BPM clock generator for Arduino + * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) + * @version 2.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include + +#define ATOMIC(X) noInterrupts(); X; interrupts(); + +// want a different avr clock support? +// TODO: we should do this using macro guards for avrs different clocks freqeuncy setup at compile time +#define AVR_CLOCK_FREQ 16000000 + +// forward declaration of uClockHandler +void uClockHandler(); + +// AVR ISR Entrypoint +ISR(TIMER1_COMPA_vect) +{ + uClockHandler(); +} + +void initTimer(uint32_t init_clock) +{ + ATOMIC( + // 16bits Timer1 init + // begin at 120bpm (48.0007680122882 Hz) + TCCR1A = 0; // set entire TCCR1A register to 0 + TCCR1B = 0; // same for TCCR1B + TCNT1 = 0; // initialize counter value to 0 + // set compare match register for 48.0007680122882 Hz increments + OCR1A = 41665; // = 16000000 / (8 * 48.0007680122882) - 1 (must be <65536) + // turn on CTC mode + TCCR1B |= (1 << WGM12); + // Set CS12, CS11 and CS10 bits for 8 prescaler + TCCR1B |= (0 << CS12) | (1 << CS11) | (0 << CS10); + // enable timer compare interrupt + TIMSK1 |= (1 << OCIE1A); + ) +} + +void setTimer(uint32_t us_interval) +{ + float tick_hertz_interval = 1/((float)us_interval/1000000); + + uint32_t ocr; + uint8_t tccr = 0; + + // 16bits avr timer setup + if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 1 prescaler + tccr |= (0 << CS12) | (0 << CS11) | (1 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 8 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 8 prescaler + tccr |= (0 << CS12) | (1 << CS11) | (0 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 64 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 64 prescaler + tccr |= (0 << CS12) | (1 << CS11) | (1 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 256 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 256 prescaler + tccr |= (1 << CS12) | (0 << CS11) | (0 << CS10); + } else if ((ocr = AVR_CLOCK_FREQ / ( tick_hertz_interval * 1024 )) < 65535) { + // Set CS12, CS11 and CS10 bits for 1024 prescaler + tccr |= (1 << CS12) | (0 << CS11) | (1 << CS10); + } else { + // tempo not achiavable + return; + } + + ATOMIC( + TCCR1B = 0; + OCR1A = ocr-1; + TCCR1B |= (1 << WGM12); + TCCR1B |= tccr; + ) +} \ No newline at end of file diff --git a/uClock/uClock.h b/uClock/uClock.h new file mode 100755 index 0000000..d8670b0 --- /dev/null +++ b/uClock/uClock.h @@ -0,0 +1,180 @@ +/*! + * @file uClock.h + * Project BPM clock generator for Arduino + * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) + * @version 2.2.1 + * @author Romulo Silva + * @date 10/06/2017 + * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef __U_CLOCK_H__ +#define __U_CLOCK_H__ + +#include +#include + +namespace umodular { namespace clock { + +#define MIN_BPM 1 +#define MAX_BPM 400 + +#define PHASE_FACTOR 16 +#define PLL_X 220 + +#define SECS_PER_MIN (60UL) +#define SECS_PER_HOUR (3600UL) +#define SECS_PER_DAY (SECS_PER_HOUR * 24L) + +class uClockClass { + + public: + enum ClockMode { + INTERNAL_CLOCK = 0, + EXTERNAL_CLOCK + }; + + enum ClockState { + PAUSED = 0, + STARTING, + STARTED + }; + + enum PPQNResolution { + PPQN_1 = 1, + PPQN_2 = 2, + PPQN_4 = 4, + PPQN_8 = 8, + PPQN_12 = 12, + PPQN_24 = 24, + PPQN_48 = 48, + PPQN_96 = 96, + PPQN_384 = 384, + PPQN_480 = 480, + PPQN_960 = 960 + }; + + ClockState clock_state; + + uClockClass(); + + void setOnOutputPPQN(void (*callback)(uint32_t tick)) { + onOutputPPQNCallback = callback; + } + + void setOnSync24(void (*callback)(uint32_t tick)) { + onSync24Callback = callback; + } + + void setOnClockStart(void (*callback)()) { + onClockStartCallback = callback; + } + + void setOnClockStop(void (*callback)()) { + onClockStopCallback = callback; + } + + void init(); + void setOutputPPQN(PPQNResolution resolution); + void setInputPPQN(PPQNResolution resolution); + + void handleTimerInt(); + void handleExternalClock(); + void resetCounters(); + + // external class control + void start(); + void stop(); + void pause(); + void setTempo(float bpm); + float getTempo(); + + // for software timer implementation(fallback for no board support) + void run(); + + // external timming control + void setClockMode(ClockMode tempo_mode); + ClockMode getClockMode(); + void clockMe(); + // for smooth slave tempo calculate display you should raise the + // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. + // note: this doesn't impact on sync time, only display time getTempo() + // if you dont want to use it, it is default set it to 1 for memory save + void setExtIntervalBuffer(uint8_t buffer_size); + + // elapsed time support + uint8_t getNumberOfSeconds(uint32_t time); + uint8_t getNumberOfMinutes(uint32_t time); + uint8_t getNumberOfHours(uint32_t time); + uint8_t getNumberOfDays(uint32_t time); + uint32_t getNowTimer(); + uint32_t getPlayTime(); + + uint32_t bpmToMicroSeconds(float bpm); + + private: + float inline freqToBpm(uint32_t freq); + float inline constrainBpm(float bpm); + void calculateReferencedata(); + + void (*onOutputPPQNCallback)(uint32_t tick); + void (*onSync24Callback)(uint32_t tick); + void (*onClockStartCallback)(); + void (*onClockStopCallback)(); + + // clock input/output control + PPQNResolution output_ppqn = PPQN_96; + PPQNResolution input_ppqn = PPQN_24; + // output and internal counters, ticks and references + uint32_t tick; + uint32_t int_clock_tick; + uint8_t mod_clock_counter; + uint16_t mod_clock_ref; + + uint8_t mod_sync24_counter; + uint16_t mod_sync24_ref; + uint32_t sync24_tick; + + // external clock control + volatile uint32_t ext_clock_us; + volatile uint32_t ext_clock_tick; + volatile uint32_t ext_interval; + uint32_t last_interval; + uint32_t sync_interval; + + float tempo; + uint32_t start_timer; + ClockMode clock_mode; + + volatile uint32_t * ext_interval_buffer = nullptr; + uint8_t ext_interval_buffer_size; + uint16_t ext_interval_idx; +}; + +} } // end namespace umodular::clock + +extern umodular::clock::uClockClass uClock; + +extern "C" { + extern volatile uint32_t _millis; +} + +#endif /* __U_CLOCK_H__ */ -- 2.39.5 From a640723be82c7f9256a857347c186d5da6b808b4 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 1 Jul 2025 21:31:20 -0700 Subject: [PATCH 18/54] minor memory improvements --- examples/Gravity/app_state.h | 4 ++-- examples/Gravity/channel.h | 13 +++++++------ examples/Gravity/euclidean.h | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 062ac3c..75ce86f 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -26,7 +26,7 @@ static Channel& GetSelectedChannel() { return app.channel[app.selected_channel - 1]; } -enum ParamsMainPage { +enum ParamsMainPage : uint8_t { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, PARAM_MAIN_PULSE, @@ -35,7 +35,7 @@ enum ParamsMainPage { PARAM_MAIN_LAST, }; -enum ParamsChannelPage { +enum ParamsChannelPage : uint8_t { PARAM_CH_MOD, PARAM_CH_PROB, PARAM_CH_DUTY, diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 9acbb70..4110039 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -7,14 +7,14 @@ #include "euclidean.h" // Enums for CV configuration -enum CvSource { +enum CvSource : uint8_t { CV_NONE, CV_1, CV_2, CV_LAST, }; -enum CvDestination { +enum CvDestination : uint8_t { CV_DEST_NONE, CV_DEST_MOD, CV_DEST_PROB, @@ -28,9 +28,9 @@ enum CvDestination { static const int MOD_CHOICE_SIZE = 21; // Negative for multiply, positive for divide. -static const int clock_mod[MOD_CHOICE_SIZE] = {-24, -12, -8, -6, -4, -3, -2, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 24, 32, 64, 128}; +static const int clock_mod[MOD_CHOICE_SIZE] PROGMEM = {-24, -12, -8, -6, -4, -3, -2, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 24, 32, 64, 128}; // This represents the number of clock pulses for a 96 PPQN clock source that match the above div/mult mods. -static const int clock_mod_pulses[MOD_CHOICE_SIZE] = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; +static const int clock_mod_pulses[MOD_CHOICE_SIZE] PROGMEM = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; class Channel { public: @@ -103,7 +103,7 @@ class Channel { int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } - int getClockMod(bool withCvMod = false) const { return clock_mod[getClockModIndex(withCvMod)]; } + int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&clock_mod[getClockModIndex(withCvMod)]); } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } CvSource getCvSource() { return cv_source; } CvDestination getCvDestination() { return cv_destination; } @@ -117,12 +117,13 @@ class Channel { /** * @brief Processes a clock tick and determines if the output should be high or low. + * Note: this method is called from an ISR and must be kept as simple as possible. * @param tick The current clock tick count. * @param output The output object to be modified. */ void processClockTick(uint32_t tick, DigitalOutput& output) { // Calculate output duty cycle state using cv modded values to determine pulse counts. - const uint16_t mod_pulses = clock_mod_pulses[cvmod_clock_mod_index]; + const uint16_t mod_pulses = pgm_read_word_near(&clock_mod_pulses[cvmod_clock_mod_index]); const uint16_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); const uint16_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); diff --git a/examples/Gravity/euclidean.h b/examples/Gravity/euclidean.h index f18e0ac..3eb75dc 100644 --- a/examples/Gravity/euclidean.h +++ b/examples/Gravity/euclidean.h @@ -15,7 +15,7 @@ class Pattern { Pattern() {} ~Pattern() {} - enum Step { + enum Step : uint8_t { REST, HIT, }; -- 2.39.5 From 7ce8bb661d720cc0704cd696c1fcd34aeebd8c55 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 2 Jul 2025 14:16:15 -0700 Subject: [PATCH 19/54] refactor cv mod to allow both cv mods configurable per channel. Fix euclidean sum mod. update large font. --- examples/Gravity/Gravity.ino | 23 ++---- examples/Gravity/app_state.h | 4 +- examples/Gravity/channel.h | 122 +++++++++++++++++--------------- examples/Gravity/display.h | 70 ++++++++---------- examples/Gravity/euclidean.h | 6 +- examples/Gravity/save_state.cpp | 16 ++--- examples/Gravity/save_state.h | 10 +-- 7 files changed, 118 insertions(+), 133 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 99c33a5..58069c0 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -66,15 +66,6 @@ void loop() { // Only apply CV to the channel when the current channel has cv // mod configured. if (ch.isCvModActive()) { - // hack -- do not apply mod to euclidean rhythm when editing. - bool editing_euc; - editing_euc |= ch.getCvDestination() == CV_DEST_EUC_STEPS; - editing_euc |= ch.getCvDestination() == CV_DEST_EUC_HITS; - editing_euc &= (app.selected_channel - 1) == i; - editing_euc &= app.editing_param; - if (editing_euc) { - continue; - } ch.applyCvMod(cv1, cv2); } } @@ -263,16 +254,16 @@ void editChannelParameter(int val) { case PARAM_CH_EUC_HITS: ch.setHits(ch.getHits() + val); break; - case PARAM_CH_CV_SRC: { - byte source = static_cast(ch.getCvSource()); - updateSelection(source, val, CV_LAST); - ch.setCvSource(static_cast(source)); + case PARAM_CH_CV1_DEST: { + byte dest = static_cast(ch.getCv1Dest()); + updateSelection(dest, val, CV_DEST_LAST); + ch.setCv1Dest(static_cast(dest)); break; } - case PARAM_CH_CV_DEST: { - byte dest = static_cast(ch.getCvDestination()); + case PARAM_CH_CV2_DEST: { + byte dest = static_cast(ch.getCv2Dest()); updateSelection(dest, val, CV_DEST_LAST); - ch.setCvDestination(static_cast(dest)); + ch.setCv2Dest(static_cast(dest)); break; } } diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index 75ce86f..a52be4b 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -43,8 +43,8 @@ enum ParamsChannelPage : uint8_t { PARAM_CH_SWING, PARAM_CH_EUC_STEPS, PARAM_CH_EUC_HITS, - PARAM_CH_CV_SRC, - PARAM_CH_CV_DEST, + PARAM_CH_CV1_DEST, + PARAM_CH_CV2_DEST, PARAM_CH_LAST, }; diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 4110039..d9a0028 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -6,14 +6,7 @@ #include "euclidean.h" -// Enums for CV configuration -enum CvSource : uint8_t { - CV_NONE, - CV_1, - CV_2, - CV_LAST, -}; - +// Enums for CV Mod destination enum CvDestination : uint8_t { CV_DEST_NONE, CV_DEST_MOD, @@ -45,9 +38,8 @@ class Channel { base_duty_cycle = 50; base_offset = 0; base_swing = 50; - - cv_source = CV_NONE; - cv_destination = CV_DEST_NONE; + base_euc_steps = 1; + base_euc_hits = 1; cvmod_clock_mod_index = base_clock_mod_index; cvmod_probability = base_probability; @@ -62,40 +54,56 @@ class Channel { void setClockMod(int index) { base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); - if (!isCvModActive()) { + if (cv1_dest != CV_DEST_MOD && cv2_dest != CV_DEST_MOD) { cvmod_clock_mod_index = base_clock_mod_index; } } void setProbability(int prob) { base_probability = constrain(prob, 0, 100); - if (!isCvModActive()) { + if (cv1_dest != CV_DEST_PROB && cv2_dest != CV_DEST_PROB) { cvmod_probability = base_probability; } } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); - if (!isCvModActive()) { + if (cv1_dest != CV_DEST_DUTY && cv2_dest != CV_DEST_DUTY) { cvmod_duty_cycle = base_duty_cycle; } } void setOffset(int off) { base_offset = constrain(off, 0, 99); - if (!isCvModActive()) { + if (cv1_dest != CV_DEST_OFFSET && cv2_dest != CV_DEST_OFFSET) { cvmod_offset = base_offset; } } void setSwing(int val) { base_swing = constrain(val, 50, 95); - if (!isCvModActive()) { + if (cv1_dest != CV_DEST_SWING && cv2_dest != CV_DEST_SWING) { cvmod_swing = base_swing; } } - void setCvSource(CvSource source) { cv_source = source; } - void setCvDestination(CvDestination dest) { cv_destination = dest; } + // Euclidean + void setSteps(int val) { + base_euc_steps = constrain(val, 1, MAX_PATTERN_LEN); + if (cv1_dest != CV_DEST_EUC_STEPS && cv2_dest != CV_DEST_EUC_STEPS) { + pattern.SetSteps(val); + } + } + void setHits(int val) { + base_euc_hits = constrain(val, 1, base_euc_steps); + if (cv1_dest != CV_DEST_EUC_HITS && cv2_dest != CV_DEST_EUC_HITS) { + pattern.SetHits(val); + } + } + + void setCv1Dest(CvDestination dest) { cv1_dest = dest; } + void setCv2Dest(CvDestination dest) { cv2_dest = dest; } + CvDestination getCv1Dest() const { return cv1_dest; } + CvDestination getCv2Dest() const { return cv2_dest; } // Getters (Get the BASE value for editing or cv modded value for display) @@ -105,15 +113,10 @@ class Channel { int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&clock_mod[getClockModIndex(withCvMod)]); } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } - CvSource getCvSource() { return cv_source; } - CvDestination getCvDestination() { return cv_destination; } - bool isCvModActive() const { return cv_source != CV_NONE && cv_destination != CV_DEST_NONE; } + bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } - // Euclidean - void setSteps(int val) { pattern.SetSteps(val); } - void setHits(int val) { pattern.SetHits(val); } - byte getSteps() { return pattern.GetSteps(); } - byte getHits() { return pattern.GetHits(); } + byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; } + byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; } /** * @brief Processes a clock tick and determines if the output should be high or low. @@ -163,54 +166,55 @@ class Channel { } } - void applyCvMod(int cv1_value, int cv2_value) { - // Use the CV value for current selected cv source. - int value = (cv_source == CV_1) ? cv1_value : cv2_value; - + void applyCvMod(int cv1_val, int cv2_val) { // Calculate and store cv modded values using bipolar mapping. // Default to base value if not the current CV destination. - cvmod_clock_mod_index = - (cv_destination == CV_DEST_MOD) - ? constrain(base_clock_mod_index + map(value, -512, 512, -10, 10), 0, MOD_CHOICE_SIZE - 1) - : base_clock_mod_index; + // Note: This is optimized for cpu performance. This method is called + // from the main loop and stores the cv mod values. This reduces CPU + // cycles inside the internal clock interrupt, which is preferrable. + // However, if RAM usage grows too much, we have an opportunity to + // refactor this to store just the CV read values, and calculate the + // cv mod value per channel inside the getter methods by passing cv + // values. This would reduce RAM usage, but would introduce a + // significant CPU cost, which may have undesirable performance issues. - cvmod_probability = - (cv_destination == CV_DEST_PROB) - ? constrain(base_probability + map(value, -512, 512, -50, 50), 0, 100) - : base_probability; + int dest_mod = calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -10, 10); + cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100); - cvmod_duty_cycle = - (cv_destination == CV_DEST_DUTY) - ? constrain(base_duty_cycle + map(value, -512, 512, -50, 50), 1, 99) - : base_duty_cycle; + int prob_mod = calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); + cvmod_probability = constrain(base_probability + prob_mod, 0, 100); - cvmod_offset = - (cv_destination == CV_DEST_OFFSET) - ? constrain(base_offset + map(value, -512, 512, -50, 50), 0, 99) - : base_offset; + int duty_mod = calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); + cvmod_duty_cycle = constrain(base_duty_cycle + duty_mod, 1, 99); - cvmod_swing = - (cv_destination == CV_DEST_SWING) - ? constrain(base_swing + map(value, -512, 512, -25, 25), 50, 95) - : base_swing; + int offset_mod = calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); + cvmod_offset = constrain(base_offset + offset_mod, 0, 99); - if (cv_destination == CV_DEST_EUC_STEPS) { - pattern.SetSteps(map(value, -512, 512, 0, MAX_PATTERN_LEN)); - } + int swing_mod = calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); + cvmod_swing = constrain(base_swing + swing_mod, 50, 95); - if (cv_destination == CV_DEST_EUC_HITS) { - pattern.SetHits(map(value, -512, 512, 0, pattern.GetSteps())); - } + int step_mod = calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + pattern.SetSteps(base_euc_steps + step_mod); + + int hit_mod = calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + pattern.SetHits(base_euc_hits + hit_mod); } private: + int calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) { + int mod1 = (cv1_dest == dest) ? map(cv1_val, -512, 512, min_range, max_range) : 0; + int mod2 = (cv2_dest == dest) ? map(cv2_val, -512, 512, min_range, max_range) : 0; + return mod1 + mod2; + } // User-settable base values. byte base_clock_mod_index; byte base_probability; byte base_duty_cycle; byte base_offset; byte base_swing; + byte base_euc_steps; + byte base_euc_hits; // Base value with cv mod applied. byte cvmod_clock_mod_index; @@ -219,9 +223,9 @@ class Channel { byte cvmod_offset; byte cvmod_swing; - // CV configuration - CvSource cv_source = CV_NONE; - CvDestination cv_destination = CV_DEST_NONE; + // CV mod configuration + CvDestination cv1_dest; + CvDestination cv2_dest; // Euclidean pattern Pattern pattern; diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 038d4bf..9349dd0 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -9,6 +9,11 @@ // UI Display functions for drawing the UI to the OLED display. // +/* + * 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" @@ -25,8 +30,13 @@ const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM = "\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"; -const uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") PROGMEM = - "#\0\4\4\4\5\2\1\6\17\30\1\0\27\0\0\0\1\77\0\0\3w%'\17\37\313\330R#&" +/* + * Font: STK-L.bdf 36pt + * https://stncrn.github.io/u8g2-unifont-helper/ + * "%/0123456789ACDEFINORSTUVXx" + */ +const uint8_t LARGE_FONT[715] U8G2_FONT_SECTION("stk-l") = + "\33\0\4\4\4\5\2\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\256%'\17\37\313\330R#&" "\32!F\14\211I\310\24!\65\204(MF\21)Cd\304\10\62b\14\215\60Vb\334\20\0/\14" "\272\336\336d\244\350\263q\343\0\60\37|\377\216!%*\10\35\263\253ChD\30\21bB\14\242S" "\306lv\210\204\22Ef\0\61\24z\337\322\60R\205\314\234\31\61F\310\270\371\177\224\42\3\62\33|" @@ -37,24 +47,18 @@ const uint8_t LARGE_FONT[916] U8G2_FONT_SECTION("stk-l") PROGMEM = "\234\335\235\42\261&\325\31\0\67\23|\377\302\212\7)\347Crt\70\345\300\221\363\16\0\70 |\377" "\216)\64*\10\35\263\354\20\11\42d\20\235BC\204\4\241cvv\210\204\32Tf\0\71\32|\377" "\216)\64*\10\35\263\263C$\226\250I\71_\14\42\241\6\225\31\0A\26}\17S\271Si(\31" - "\65d\324\210q\366\356\301w\366\273\1B$}\17C\42\65KF\221\30\66b\330\210a#\206\215\30" - "Eb\311&\243H\14;g\317\36\204`\261\4\0C\27}\17\317\251\64K\10!\63:\377\247\304F" - "\20\42\261F\21\22\0D\33}\17C\42\65KF\15\31\66b\330\210q\366\77;\66b\24\211%j" - "\22\1E\21|\377\302\7)\347%\42\214F\316/\37<\60F\20|\377\302\7)\347\313\64\331\214\234" - "\177\11\0G\31\216\37\17*\65L\206\35\264v>\322\241\15\217\221 \65\204\215\262\63\0H\17|\377" - "\302\60\373g\17\36\60\263\177\66\0I\7so\302\37$J\22|\377\346\374\377\322\230\261C\210H\250" - "Ae\6\0K\42|\377\302\60S\247F\14\42\61h\310\30\42c&!\63\202\320\251\64JV\14\42" - "\61\352\230\375l\0L\15{\357\302\300\371\377\37>x\60\0M$}\17\203\310r\346N\245Q\263\202" - "E\12)L\224\60Q\302\310\20#C\214\14\61\23\306L\30s\366\335\0N#}\17\203@s\346\216" - "\35C\205*Q\42\23cL\214\61\62\304\310\20\63#\314\214\60\224\25f\327\231\33O\26}\17\317\251" - "\64KF\215\30g\377\337\215\30\65dM\252\63\0P\26|\377B\32%+F\35\263W\207H\254H" - "\203h\344\374%\0Q\31}\17S\261\64KF\215\30g\377oF\230\31q\246\210\42E%F\0R" + "\65d\324\210q\366\356\301w\366\273\1C\27}\17\317\251\64K\10!\63:\377\247\304F\20\42\261F" + "\21\22\0D\33}\17C\42\65KF\15\31\66b\330\210q\366\77;\66b\24\211%j\22\1E\21" + "|\377\302\7)\347%\42\214F\316/\37<\60F\20|\377\302\7)\347\313\64\331\214\234\177\11\0I" + "\7so\302\37$N#}\17\203@s\346\216\35C\205*Q\42\23cL\214\61\62\304\310\20\63#" + "\314\214\60\224\25f\327\231\33O\26}\17\317\251\64KF\215\30g\377\337\215\30\65dM\252\63\0R" "\61\216\37\203\242\65L\206\221\30\67b\334\210q#\306\215\30\67b\30\211QD\230(J\65d\330\230" - "Qc\10\315j\314(\42\303H\214\33\61\356\340\0S!\216\37\317\261DKH\221\30\67b\334\210\261" - "c)M\246Ji\331\331\32\64\207\212D\223Uh\0T\15}\17\303\7\251\206\316\377\377\12\0U\21" - "|\377\302\60\373\377\317F\14\32\242\6\225\31\0X)~\37\303@\203\307H\14\33B\210\14\21RC" - "\206\241\63h\222(I\203\346\220\15\31E\204\14!\42\303F\20;h\341\0x\24\312\336\302 CG" - "H\240\61E\312\14\222)\6Y\64\0\0\0\0\4\377\377\0"; + "Qc\10\315j\314(\42\303H\214\33\61\356\340\0S\42\216\37\317\261DKH\221\30\67b\334\210\261" + "c)M\226-\331\301c\307\32\64\207\212D\223Uh\0T\15}\17\303\7\251\206\316\377\377\12\0U" + "\21|\377\302\60\373\377\317F\14\32\242\6\225\31\0V\26\177\375\302H\373\377\345\210qCH\221\241\212" + "\4\271\223e\207\1X)~\37\303@\203\307H\14\33B\210\14\21RC\206\241\63h\222(I\203\346" + "\220\15\31E\204\14!\42\303F\20;h\341\0x\24\312\336\302 CGH\240\61E\312\14\222)\6" + "Y\64\0\0\0\0\4\377\377\0"; #define play_icon_width 14 #define play_icon_height 14 @@ -287,31 +291,17 @@ void DisplayChannelPage() { swingDivisionMark(); break; case PARAM_CH_EUC_STEPS: - mainText = String(ch.getSteps()); + mainText = String(ch.getSteps(withCvMod)); subText = "EUCLID STEPS"; break; case PARAM_CH_EUC_HITS: - mainText = String(ch.getHits()); + mainText = String(ch.getHits(withCvMod)); subText = "EUCLID HITS"; break; - case PARAM_CH_CV_SRC: { - mainText = F("SRC"); - switch (ch.getCvSource()) { - case CV_NONE: - subText = F("NONE"); - break; - case CV_1: - subText = F("CV 1"); - break; - case CV_2: - subText = F("CV 2"); - break; - } - break; - } - case PARAM_CH_CV_DEST: { - mainText = F("DEST"); - switch (ch.getCvDestination()) { + case PARAM_CH_CV1_DEST: + case PARAM_CH_CV2_DEST: { + mainText = (app.selected_param == PARAM_CH_CV1_DEST) ? F("CV1") : F("CV2"); + switch ((app.selected_param == PARAM_CH_CV1_DEST) ? ch.getCv1Dest() : ch.getCv2Dest()) { case CV_DEST_NONE: subText = F("NONE"); break; @@ -347,7 +337,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items String menu_items[PARAM_CH_LAST] = { F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), F("EUCLID STEPS"), - F("EUCLID HITS"), F("CV SOURCE"), F("CV DEST")}; + F("EUCLID HITS"), F("CV1 MOD"), F("CV2 MOD")}; drawMenuItems(menu_items, PARAM_CH_LAST); } diff --git a/examples/Gravity/euclidean.h b/examples/Gravity/euclidean.h index 3eb75dc..9c744c3 100644 --- a/examples/Gravity/euclidean.h +++ b/examples/Gravity/euclidean.h @@ -46,9 +46,9 @@ class Pattern { void Reset() { step_index_ = 0; } - uint8_t GetSteps() { return steps_; } - uint8_t GetHits() { return hits_; } - uint8_t GetStepIndex() { return step_index_; } + uint8_t GetSteps() const { return steps_; } + uint8_t GetHits() const { return hits_; } + uint8_t GetStepIndex() const { return step_index_; } Step NextStep() { if (steps_ == 0) return REST; diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index c435df8..62895a7 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -29,10 +29,10 @@ bool StateManager::initialize(AppState& app) { ch.setDutyCycle(saved_ch_state.base_duty_cycle); ch.setOffset(saved_ch_state.base_offset); ch.setSwing(saved_ch_state.base_shuffle); - ch.setCvSource(static_cast(saved_ch_state.cv_source)); - ch.setCvDestination(static_cast(saved_ch_state.cv_destination)); - ch.setSteps(saved_ch_state.euc_steps); - ch.setHits(saved_ch_state.euc_hits); + ch.setSteps(saved_ch_state.base_euc_steps); + ch.setHits(saved_ch_state.base_euc_hits); + ch.setCv1Dest(static_cast(saved_ch_state.cv1_dest)); + ch.setCv1Dest(static_cast(saved_ch_state.cv2_dest)); } return true; @@ -112,10 +112,10 @@ void StateManager::_saveState(const AppState& app) { save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); save_ch.base_shuffle = ch.getSwing(); - save_ch.cv_source = static_cast(ch.getCvSource()); - save_ch.cv_destination = static_cast(ch.getCvDestination()); - save_ch.euc_steps = ch.getSteps(); - save_ch.euc_hits = ch.getHits(); + save_ch.base_euc_steps = ch.getSteps(); + save_ch.base_euc_hits = ch.getHits(); + save_ch.cv1_dest = static_cast(ch.getCv1Dest()); + save_ch.cv2_dest = static_cast(ch.getCv2Dest()); } EEPROM.put(sizeof(Metadata), save_data); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index 8a2659e..826fc93 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -9,7 +9,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 5; +const byte SKETCH_VERSION = 6; // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -42,10 +42,10 @@ class StateManager { byte base_duty_cycle; byte base_offset; byte base_shuffle; - byte cv_source; // Cast the CvSource enum to a byte for storage - byte cv_destination; // Cast the CvDestination enum as a byte for storage - byte euc_steps; - byte euc_hits; + byte base_euc_steps; + byte base_euc_hits; + byte cv1_dest; // Cast the CvDestination enum as a byte for storage + byte cv2_dest; // Cast the CvDestination enum as a byte for storage }; // This struct holds all the parameters we want to save. struct EepromData { -- 2.39.5 From db50132c2855222edaa3402bd1fcd9404594bed1 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 3 Jul 2025 07:57:18 -0700 Subject: [PATCH 20/54] update comments and minor fixes. --- examples/Gravity/Gravity.ino | 34 ++++++++++++++++++++++++++-------- examples/Gravity/channel.h | 12 ++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 58069c0..755d8ea 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -1,15 +1,33 @@ /** * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) - * @brief Demo firmware for Sitka Instruments Gravity. + * @brief Alt firmware version of Gravity by Sitka Instruments. * @version 0.1 * @date 2025-05-04 * * @copyright Copyright (c) 2025 + * + * This version of Gravity firmware is a full rewrite that leverages the + * libGravity hardware abstraction library. The goal of this project was to + * create an open source friendly version of the firmware that makes it easy + * for users/developers to modify and create their own original alt firmware + * implementations. + * + * The libGravity library represents wrappers around the + * hardware peripherials to make it easy to interact with and add behavior + * to them. The library tries not to make any assumptions about what the + * firmware can or should do. + * + * The Gravity firmware is a slightly different implementation of the original + * firmware. There are a few notable changes; the internal clock operates at + * 96 PPQN instead of the original 24 PPQN, which allows for more granular + * quantization of features like duty cycle (pulse width) or offset. + * Additionally, this firmware replaces the sequencer with a Euclidean Rhythm + * generator. * * ENCODER: - * Press to change between selecting a parameter and editing the parameter. - * Hold & Rotate to change current output channel pattern. + * Press: change between selecting a parameter and editing the parameter. + * Hold & Rotate: change current selected output channel. * * BTN1: Play/pause the internal clock. * @@ -37,7 +55,7 @@ void setup() { // Initialize the state manager. This will load settings from EEPROM stateManager.initialize(app); - InitAppState(app); + InitGravity(app); // Clock handlers. gravity.clock.AttachIntHandler(HandleIntClockTick); @@ -107,8 +125,8 @@ void HandleIntClockTick(uint32_t tick) { break; } - const uint16_t pulse_high_ticks = clock_mod_pulses[clock_index]; - const uint32_t pulse_low_ticks = tick + max((long)(pulse_high_ticks / 2), 1L); + const uint32_t pulse_high_ticks = CLOCK_MOD_PULSES[clock_index]; + const uint32_t pulse_low_ticks = tick + max((pulse_high_ticks / 2), 1L); if (tick % pulse_high_ticks == 0) { gravity.pulse.High(); @@ -162,7 +180,7 @@ void HandleEncoderPressed() { if (app.selected_param == PARAM_MAIN_RESET_STATE) { if (app.selected_sub_param == 0) { // Reset stateManager.reset(app); - InitAppState(app); + InitGravity(app); } } } @@ -282,7 +300,7 @@ void updateSelection(byte& param, int change, int maxValue) { // App Helper functions. // -void InitAppState(AppState& app) { +void InitGravity(AppState& app) { gravity.clock.SetTempo(app.tempo); gravity.clock.SetSource(app.selected_source); gravity.encoder.SetReverseDirection(app.encoder_reversed); diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index d9a0028..55a87ac 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -19,11 +19,11 @@ enum CvDestination : uint8_t { CV_DEST_LAST, }; -static const int MOD_CHOICE_SIZE = 21; +static const byte MOD_CHOICE_SIZE = 21; // Negative for multiply, positive for divide. -static const int clock_mod[MOD_CHOICE_SIZE] PROGMEM = {-24, -12, -8, -6, -4, -3, -2, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 24, 32, 64, 128}; +static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = {-24, -12, -8, -6, -4, -3, -2, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 24, 32, 64, 128}; // This represents the number of clock pulses for a 96 PPQN clock source that match the above div/mult mods. -static const int clock_mod_pulses[MOD_CHOICE_SIZE] PROGMEM = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; +static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; class Channel { public: @@ -111,7 +111,7 @@ class Channel { int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } - int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&clock_mod[getClockModIndex(withCvMod)]); } + int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex(withCvMod)]); } int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } @@ -126,7 +126,7 @@ class Channel { */ void processClockTick(uint32_t tick, DigitalOutput& output) { // Calculate output duty cycle state using cv modded values to determine pulse counts. - const uint16_t mod_pulses = pgm_read_word_near(&clock_mod_pulses[cvmod_clock_mod_index]); + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); const uint16_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); const uint16_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); @@ -144,7 +144,7 @@ class Channel { // Step check if (current_tick_offset % mod_pulses == 0) { bool hit = cvmod_probability >= random(0, 100); - // Euclidean rhythm check + // Euclidean rhythm hit check switch (pattern.NextStep()) { case Pattern::REST: // Rest when active or fall back to probability hit = false; -- 2.39.5 From 17a9212fc440ba7025242cd1387e1ba036ba93d2 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 3 Jul 2025 08:45:53 -0700 Subject: [PATCH 21/54] pre-calculate clock pulse mods to improve ISR performance --- examples/Gravity/channel.h | 92 +++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index 55a87ac..ca2648d 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -48,41 +48,49 @@ class Channel { cvmod_swing = base_swing; pattern.Init(DEFAULT_PATTERN); + + // Calcule the clock mod pulses on init. + _recalculatePulses(); } // Setters (Set the BASE value) void setClockMod(int index) { base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); - if (cv1_dest != CV_DEST_MOD && cv2_dest != CV_DEST_MOD) { + if (!isCvModActive()) { cvmod_clock_mod_index = base_clock_mod_index; + _recalculatePulses(); } } void setProbability(int prob) { base_probability = constrain(prob, 0, 100); - if (cv1_dest != CV_DEST_PROB && cv2_dest != CV_DEST_PROB) { + if (!isCvModActive()) { cvmod_probability = base_probability; + _recalculatePulses(); } } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); - if (cv1_dest != CV_DEST_DUTY && cv2_dest != CV_DEST_DUTY) { + if (!isCvModActive()) { cvmod_duty_cycle = base_duty_cycle; + _recalculatePulses(); } } void setOffset(int off) { base_offset = constrain(off, 0, 99); - if (cv1_dest != CV_DEST_OFFSET && cv2_dest != CV_DEST_OFFSET) { + if (!isCvModActive()) { cvmod_offset = base_offset; + _recalculatePulses(); } } void setSwing(int val) { base_swing = constrain(val, 50, 95); - if (cv1_dest != CV_DEST_SWING && cv2_dest != CV_DEST_SWING) { + if (!isCvModActive()) { cvmod_swing = base_swing; + _recalculatePulses(); } } @@ -125,21 +133,16 @@ class Channel { * @param output The output object to be modified. */ void processClockTick(uint32_t tick, DigitalOutput& output) { - // Calculate output duty cycle state using cv modded values to determine pulse counts. const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); - const uint16_t duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); - const uint16_t offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); + // Conditionally apply swing on down beats. uint16_t swing_pulses = 0; - // Check step increment for odd beats. - if (cvmod_swing > 50 && (tick / mod_pulses) % 2 == 1) { - int shifted_swing = cvmod_swing - 50; - swing_pulses = (long)((mod_pulses * (100L - shifted_swing)) / 100L); + if (_swing_pulse_amount > 0 && (tick / mod_pulses) % 2 == 1) { + swing_pulses = _swing_pulse_amount; } - const uint32_t current_tick_offset = tick + offset_pulses + swing_pulses; - // Duty cycle high check logic + const uint32_t current_tick_offset = tick + _offset_pulses + swing_pulses; if (!output.On()) { // Step check if (current_tick_offset % mod_pulses == 0) { @@ -160,16 +163,20 @@ class Channel { } // Duty cycle low check - const uint32_t duty_cycle_end_tick = tick + duty_pulses + offset_pulses + swing_pulses; + const uint32_t duty_cycle_end_tick = tick + _duty_pulses + _offset_pulses + swing_pulses; if (duty_cycle_end_tick % mod_pulses == 0) { output.Low(); } } - + /** + * @brief Calculate and store cv modded values using bipolar mapping. + * Default to base value if not the current CV destination. + * + * @param cv1_val analog input reading for cv1 + * @param cv2_val analog input reading for cv2 + * + */ void applyCvMod(int cv1_val, int cv2_val) { - // Calculate and store cv modded values using bipolar mapping. - // Default to base value if not the current CV destination. - // Note: This is optimized for cpu performance. This method is called // from the main loop and stores the cv mod values. This reduces CPU // cycles inside the internal clock interrupt, which is preferrable. @@ -178,35 +185,61 @@ class Channel { // cv mod value per channel inside the getter methods by passing cv // values. This would reduce RAM usage, but would introduce a // significant CPU cost, which may have undesirable performance issues. + if (!isCvModActive()) { + cvmod_clock_mod_index = base_clock_mod_index; + cvmod_probability = base_clock_mod_index; + cvmod_duty_cycle = base_clock_mod_index; + cvmod_offset = base_clock_mod_index; + cvmod_swing = base_clock_mod_index; + return; + } - int dest_mod = calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -10, 10); + int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -10, 10); cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100); - int prob_mod = calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); + int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); cvmod_probability = constrain(base_probability + prob_mod, 0, 100); - int duty_mod = calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); + int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); cvmod_duty_cycle = constrain(base_duty_cycle + duty_mod, 1, 99); - int offset_mod = calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); + int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); cvmod_offset = constrain(base_offset + offset_mod, 0, 99); - int swing_mod = calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); + int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); cvmod_swing = constrain(base_swing + swing_mod, 50, 95); - int step_mod = calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); pattern.SetSteps(base_euc_steps + step_mod); - int hit_mod = calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); pattern.SetHits(base_euc_hits + hit_mod); + + // After all cvmod values are updated, recalculate clock pulse modifiers. + _recalculatePulses(); } private: - int calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) { + int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) { int mod1 = (cv1_dest == dest) ? map(cv1_val, -512, 512, min_range, max_range) : 0; int mod2 = (cv2_dest == dest) ? map(cv2_val, -512, 512, min_range, max_range) : 0; return mod1 + mod2; } + + void _recalculatePulses() { + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); + _duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); + _offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); + + // Calculate the down beat swing amount. + if (cvmod_swing > 50) { + int shifted_swing = cvmod_swing - 50; + _swing_pulse_amount = (long)((mod_pulses * (100L - shifted_swing)) / 100L); + } else { + _swing_pulse_amount = 0; + } + } + // User-settable base values. byte base_clock_mod_index; byte base_probability; @@ -229,6 +262,11 @@ class Channel { // Euclidean pattern Pattern pattern; + + // Pre-calculated pulse values for ISR performance + uint16_t _duty_pulses; + uint16_t _offset_pulses; + uint16_t _swing_pulse_amount; }; #endif // CHANNEL_H \ No newline at end of file -- 2.39.5 From 74d98fed139dd6fe92396854dec5ee96ded4fac1 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 3 Jul 2025 09:08:38 -0700 Subject: [PATCH 22/54] add missing midi echo --- clock.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clock.h b/clock.h index 00bd213..5401d1a 100644 --- a/clock.h +++ b/clock.h @@ -163,10 +163,12 @@ class Clock { break; case MIDI_STOP: uClock.stop(); + sendMIDIStop(); break; case MIDI_START: case MIDI_CONTINUE: uClock.start(); + sendMIDIStart(); break; } } -- 2.39.5 From d21c0a810fea87ddc8bf9b58bff227adf5b69216 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 3 Jul 2025 09:12:15 -0700 Subject: [PATCH 23/54] Add more clock mult/div options and improve documentation to make it easier to modify the list. --- examples/Gravity/channel.h | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index ca2648d..f7d5f46 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -19,11 +19,28 @@ enum CvDestination : uint8_t { CV_DEST_LAST, }; -static const byte MOD_CHOICE_SIZE = 21; -// Negative for multiply, positive for divide. -static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = {-24, -12, -8, -6, -4, -3, -2, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 24, 32, 64, 128}; -// This represents the number of clock pulses for a 96 PPQN clock source that match the above div/mult mods. -static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = {4, 8, 12, 16, 24, 32, 48, 96, 192, 288, 384, 480, 576, 1152, 672, 768, 1536, 2304, 3072, 6144, 12288}; +static const byte MOD_CHOICE_SIZE = 25; + +// Negative numbers are multipliers, positive are divisors. +static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = { + // Multipliers + -24, -16, -12, -8, -6, -4, -3, -2, + // Internal Clock Unity + 1, + // Divisors + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 24, 32, 64, 128 +}; + +// This represents the number of clock pulses for a 96 PPQN clock source +// that match the above div/mult mods. +static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = { + // Multiplier Pulses (96 / X) + 4, 6, 8, 12, 16, 24, 32, 48, + // Internal Clock Pulses + 96, + // Divisor Pulses (96 * X) + 192, 288, 384, 480, 576, 672, 768, 864, 960, 1056, 1152, 1536, 2304, 3072, 6144, 12288 +}; class Channel { public: @@ -194,7 +211,7 @@ class Channel { return; } - int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -10, 10); + int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE/2), MOD_CHOICE_SIZE/2); cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100); int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); -- 2.39.5 From d2228af55fd22064021e63741fade3965deccac2 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 08:22:25 -0700 Subject: [PATCH 24/54] bug fix: must have curly braces when declaring new variables inside CASE statement. --- examples/Gravity/Gravity.ino | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 755d8ea..d153253 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -232,13 +232,14 @@ void editMainParameter(int val) { gravity.clock.SetSource(app.selected_source); break; } - case PARAM_MAIN_PULSE: + case PARAM_MAIN_PULSE: { byte pulse = static_cast(app.selected_pulse); updateSelection(pulse, val, Clock::PULSE_LAST); app.selected_pulse = static_cast(pulse); if (app.selected_pulse == Clock::PULSE_NONE) { gravity.pulse.Low(); } + } case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; -- 2.39.5 From 14aad8285d37e6ddbece5bc83608d5c41758eacc Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 17:33:57 +0000 Subject: [PATCH 25/54] Introduce Save/Load banks for storing different preset settings. (#11) This also includes a lot of minor fixes. Reviewed-on: https://git.pinkduck.xyz/adam/libGravity/pulls/11 Co-authored-by: Adam Wonak Co-committed-by: Adam Wonak --- examples/Gravity/Gravity.ino | 40 +++++++--- examples/Gravity/app_state.h | 9 ++- examples/Gravity/channel.h | 25 +++--- examples/Gravity/display.h | 98 +++++++++++++++-------- examples/Gravity/euclidean.h | 2 +- examples/Gravity/save_state.cpp | 136 +++++++++++++++++++------------- examples/Gravity/save_state.h | 25 ++++-- 7 files changed, 216 insertions(+), 119 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index d153253..d0d21d8 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -6,20 +6,20 @@ * @date 2025-05-04 * * @copyright Copyright (c) 2025 - * + * * This version of Gravity firmware is a full rewrite that leverages the * libGravity hardware abstraction library. The goal of this project was to * create an open source friendly version of the firmware that makes it easy * for users/developers to modify and create their own original alt firmware - * implementations. - * - * The libGravity library represents wrappers around the + * implementations. + * + * The libGravity library represents wrappers around the * hardware peripherials to make it easy to interact with and add behavior - * to them. The library tries not to make any assumptions about what the + * to them. The library tries not to make any assumptions about what the * firmware can or should do. - * + * * The Gravity firmware is a slightly different implementation of the original - * firmware. There are a few notable changes; the internal clock operates at + * firmware. There are a few notable changes; the internal clock operates at * 96 PPQN instead of the original 24 PPQN, which allows for more granular * quantization of features like duty cycle (pulse width) or offset. * Additionally, this firmware replaces the sequencer with a Euclidean Rhythm @@ -172,11 +172,24 @@ void HandleEncoderPressed() { // Check if leaving editing mode should apply a selection. if (app.editing_param) { if (app.selected_channel == 0) { // main page + // TODO: rewrite as switch if (app.selected_param == PARAM_MAIN_ENCODER_DIR) { bool reversed = app.selected_sub_param == 1; gravity.encoder.SetReverseDirection(reversed); } - // Reset state + if (app.selected_param == PARAM_MAIN_SAVE_DATA) { + if (app.selected_sub_param < MAX_SAVE_SLOTS) { + app.selected_save_slot = app.selected_sub_param; + stateManager.saveData(app); + } + } + if (app.selected_param == PARAM_MAIN_LOAD_DATA) { + if (app.selected_sub_param < MAX_SAVE_SLOTS) { + app.selected_save_slot = app.selected_sub_param; + stateManager.loadData(app, app.selected_save_slot); + InitGravity(app); + } + } if (app.selected_param == PARAM_MAIN_RESET_STATE) { if (app.selected_sub_param == 0) { // Reset stateManager.reset(app); @@ -184,10 +197,11 @@ void HandleEncoderPressed() { } } } - // Only mark dirty when leaving editing mode. + // Only mark dirty and reset selected_sub_param when leaving editing mode. stateManager.markDirty(); + app.selected_sub_param = 0; } - app.selected_sub_param = 0; + app.editing_param = !app.editing_param; app.refresh_screen = true; } @@ -224,7 +238,6 @@ void editMainParameter(int val) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); app.tempo = gravity.clock.Tempo(); break; - case PARAM_MAIN_SOURCE: { byte source = static_cast(app.selected_source); updateSelection(source, val, Clock::SOURCE_LAST); @@ -239,10 +252,15 @@ void editMainParameter(int val) { if (app.selected_pulse == Clock::PULSE_NONE) { gravity.pulse.Low(); } + break; } case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; + case PARAM_MAIN_SAVE_DATA: + case PARAM_MAIN_LOAD_DATA: + updateSelection(app.selected_sub_param, val, MAX_SAVE_SLOTS + 1); + break; case PARAM_MAIN_RESET_STATE: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/examples/Gravity/app_state.h b/examples/Gravity/app_state.h index a52be4b..c52fc26 100644 --- a/examples/Gravity/app_state.h +++ b/examples/Gravity/app_state.h @@ -12,9 +12,10 @@ struct AppState { bool refresh_screen = true; bool editing_param = false; byte selected_param = 0; - byte selected_sub_param = 0; - byte selected_channel = 0; // 0=tempo, 1-6=output channel - byte selected_shuffle = 0; + byte selected_sub_param = 0; // Temporary value for editing params. + byte selected_channel = 0; // 0=tempo, 1-6=output channel + byte selected_swing = 0; + byte selected_save_slot = 0; // The currently active save slot. Clock::Source selected_source = Clock::SOURCE_INTERNAL; Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; Channel channel[Gravity::OUTPUT_COUNT]; @@ -31,6 +32,8 @@ enum ParamsMainPage : uint8_t { PARAM_MAIN_SOURCE, PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_SAVE_DATA, + PARAM_MAIN_LOAD_DATA, PARAM_MAIN_RESET_STATE, PARAM_MAIN_LAST, }; diff --git a/examples/Gravity/channel.h b/examples/Gravity/channel.h index f7d5f46..309ce98 100644 --- a/examples/Gravity/channel.h +++ b/examples/Gravity/channel.h @@ -24,23 +24,23 @@ static const byte MOD_CHOICE_SIZE = 25; // Negative numbers are multipliers, positive are divisors. static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = { // Multipliers - -24, -16, -12, -8, -6, -4, -3, -2, + -24, -16, -12, -8, -6, -4, -3, -2, // Internal Clock Unity - 1, + 1, // Divisors - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 24, 32, 64, 128 -}; + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 24, 32, 64, 128}; -// This represents the number of clock pulses for a 96 PPQN clock source +// This represents the number of clock pulses for a 96 PPQN clock source // that match the above div/mult mods. static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = { // Multiplier Pulses (96 / X) - 4, 6, 8, 12, 16, 24, 32, 48, + 4, 6, 8, 12, 16, 24, 32, 48, // Internal Clock Pulses - 96, + 96, // Divisor Pulses (96 * X) - 192, 288, 384, 480, 576, 672, 768, 864, 960, 1056, 1152, 1536, 2304, 3072, 6144, 12288 -}; + 192, 288, 384, 480, 576, 672, 768, 864, 960, 1056, 1152, 1536, 2304, 3072, 6144, 12288}; + +static const byte DEFAULT_CLOCK_MOD_INDEX = 8; // x1 or 96 PPQN. class Channel { public: @@ -50,7 +50,7 @@ class Channel { void Init() { // Reset base values to their defaults - base_clock_mod_index = 7; + base_clock_mod_index = DEFAULT_CLOCK_MOD_INDEX; base_probability = 100; base_duty_cycle = 50; base_offset = 0; @@ -64,6 +64,9 @@ class Channel { cvmod_offset = base_offset; cvmod_swing = base_swing; + cv1_dest = CV_DEST_NONE; + cv2_dest = CV_DEST_NONE; + pattern.Init(DEFAULT_PATTERN); // Calcule the clock mod pulses on init. @@ -211,7 +214,7 @@ class Channel { return; } - int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE/2), MOD_CHOICE_SIZE/2); + int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100); int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); diff --git a/examples/Gravity/display.h b/examples/Gravity/display.h index 9349dd0..c000dde 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -4,6 +4,7 @@ #include #include "app_state.h" +#include "save_state.h" // // UI Display functions for drawing the UI to the OLED display. @@ -33,32 +34,33 @@ const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM = /* * Font: STK-L.bdf 36pt * https://stncrn.github.io/u8g2-unifont-helper/ - * "%/0123456789ACDEFINORSTUVXx" + * "%/0123456789ABCDEFILNORSTUVXx" */ -const uint8_t LARGE_FONT[715] U8G2_FONT_SECTION("stk-l") = - "\33\0\4\4\4\5\2\1\6\20\30\0\0\27\0\0\0\1\77\0\0\2\256%'\17\37\313\330R#&" - "\32!F\14\211I\310\24!\65\204(MF\21)Cd\304\10\62b\14\215\60Vb\334\20\0/\14" - "\272\336\336d\244\350\263q\343\0\60\37|\377\216!%*\10\35\263\253ChD\30\21bB\14\242S" - "\306lv\210\204\22Ef\0\61\24z\337\322\60R\205\314\234\31\61F\310\270\371\177\224\42\3\62\33|" - "\377\216)\64*\10\35\63\66r\206\304\314`c\252\34\301\221\263|\360\300\0\63\34|\377\216)\64*" - "\10\35\63\66r \71\332YIr\226\306\16\221P\203\312\14\0\64 |\377\226\220AC\306\20\31B" - "f\310\240\21\204F\214\32\61j\304(cv\366\200\305\312\371\0\65\32|\377\206\212-F\316\27\204\224" - "\254\30\65t\344,\215\35\42\241\6\225\31\0\66\33}\17\317\251\64+\206\235\63:/\314,aA\352" - "\234\335\235\42\261&\325\31\0\67\23|\377\302\212\7)\347Crt\70\345\300\221\363\16\0\70 |\377" - "\216)\64*\10\35\263\354\20\11\42d\20\235BC\204\4\241cvv\210\204\32Tf\0\71\32|\377" - "\216)\64*\10\35\263\263C$\226\250I\71_\14\42\241\6\225\31\0A\26}\17S\271Si(\31" - "\65d\324\210q\366\356\301w\366\273\1C\27}\17\317\251\64K\10!\63:\377\247\304F\20\42\261F" - "\21\22\0D\33}\17C\42\65KF\15\31\66b\330\210q\366\77;\66b\24\211%j\22\1E\21" - "|\377\302\7)\347%\42\214F\316/\37<\60F\20|\377\302\7)\347\313\64\331\214\234\177\11\0I" - "\7so\302\37$N#}\17\203@s\346\216\35C\205*Q\42\23cL\214\61\62\304\310\20\63#" - "\314\214\60\224\25f\327\231\33O\26}\17\317\251\64KF\215\30g\377\337\215\30\65dM\252\63\0R" - "\61\216\37\203\242\65L\206\221\30\67b\334\210q#\306\215\30\67b\30\211QD\230(J\65d\330\230" - "Qc\10\315j\314(\42\303H\214\33\61\356\340\0S\42\216\37\317\261DKH\221\30\67b\334\210\261" - "c)M\226-\331\301c\307\32\64\207\212D\223Uh\0T\15}\17\303\7\251\206\316\377\377\12\0U" - "\21|\377\302\60\373\377\317F\14\32\242\6\225\31\0V\26\177\375\302H\373\377\345\210qCH\221\241\212" - "\4\271\223e\207\1X)~\37\303@\203\307H\14\33B\210\14\21RC\206\241\63h\222(I\203\346" - "\220\15\31E\204\14!\42\303F\20;h\341\0x\24\312\336\302 CGH\240\61E\312\14\222)\6" - "Y\64\0\0\0\0\4\377\377\0"; +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 @@ -151,6 +153,10 @@ void drawMenuItems(String menu_items[], int menu_size) { } } +// Visual indicators for main section of screen. +inline void solidTick() { gravity.display.drawBox(56, 4, 4, 4); } +inline void hollowTick() { gravity.display.drawBox(56, 4, 4, 4); } + // Display an indicator when swing percentage matches a musical note. void swingDivisionMark() { auto& ch = GetSelectedChannel(); @@ -158,17 +164,25 @@ void swingDivisionMark() { case 58: // 1/32nd case 66: // 1/16th case 75: // 1/8th - gravity.display.drawBox(56, 4, 4, 4); + solidTick(); break; case 54: // 1/32nd tripplet case 62: // 1/16th tripplet case 71: // 1/8th tripplet - gravity.display.drawBox(56, 4, 4, 4); - gravity.display.drawBox(57, 5, 2, 2); + hollowTick(); break; } } +// Human friendly display value for save slot. +String displaySaveSlot(int slot) { + if (slot >= 0 && slot < MAX_SAVE_SLOTS / 2) { + return String("A") + String(slot + 1); + } else if (slot >= MAX_SAVE_SLOTS / 2 && slot <= MAX_SAVE_SLOTS) { + return String("B") + String(slot - (MAX_SAVE_SLOTS / 2) + 1); + } +} + // Main display functions void DisplayMainPage() { @@ -229,17 +243,37 @@ void DisplayMainPage() { mainText = F("DIR"); subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); break; - case PARAM_MAIN_RESET_STATE: - mainText = F("RST"); - subText = app.selected_sub_param == 0 ? F("RESET ALL") : F("BACK"); + case PARAM_MAIN_SAVE_DATA: + case PARAM_MAIN_LOAD_DATA: + if (app.selected_sub_param == MAX_SAVE_SLOTS) { + mainText = F("x"); + subText = F("BACK TO MAIN"); + } else { + // Indicate currently active slot. + if (app.selected_sub_param == app.selected_save_slot) { + solidTick(); + } + mainText = displaySaveSlot(app.selected_sub_param); + subText = (app.selected_param == PARAM_MAIN_SAVE_DATA) + ? F("SAVE TO SLOT") + : F("LOAD FROM SLOT"); + } break; + case PARAM_MAIN_RESET_STATE: + if (app.selected_sub_param == 0) { + mainText = F("RST"); + subText = F("RESET ALL"); + } else { + mainText = F("x"); + subText = F("BACK TO MAIN"); + } } drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } diff --git a/examples/Gravity/euclidean.h b/examples/Gravity/euclidean.h index 9c744c3..33dfe56 100644 --- a/examples/Gravity/euclidean.h +++ b/examples/Gravity/euclidean.h @@ -66,7 +66,7 @@ class Pattern { // Update the euclidean rhythm pattern using bitmap void updatePattern() { - pattern_bitmap_ = 0; // Clear the bitmap + pattern_bitmap_ = 0; // Clear the bitmap if (steps_ == 0) return; diff --git a/examples/Gravity/save_state.cpp b/examples/Gravity/save_state.cpp index 62895a7..3c1e9d9 100644 --- a/examples/Gravity/save_state.cpp +++ b/examples/Gravity/save_state.cpp @@ -4,49 +4,50 @@ #include "app_state.h" +// Calculate the starting address for EepromData, leaving space for metadata. +static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); + StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} bool StateManager::initialize(AppState& app) { if (_isDataValid()) { - static EepromData load_data; - EEPROM.get(sizeof(Metadata), load_data); - - // Restore main app state - app.tempo = load_data.tempo; - app.encoder_reversed = load_data.encoder_reversed; - app.selected_param = load_data.selected_param; - app.selected_channel = load_data.selected_channel; - app.selected_source = static_cast(load_data.selected_source); - app.selected_pulse = static_cast(load_data.selected_pulse); - - // Loop through and restore each channel's state. - for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { - auto& ch = app.channel[i]; - const auto& saved_ch_state = load_data.channel_data[i]; - - ch.setClockMod(saved_ch_state.base_clock_mod_index); - ch.setProbability(saved_ch_state.base_probability); - ch.setDutyCycle(saved_ch_state.base_duty_cycle); - ch.setOffset(saved_ch_state.base_offset); - ch.setSwing(saved_ch_state.base_shuffle); - ch.setSteps(saved_ch_state.base_euc_steps); - ch.setHits(saved_ch_state.base_euc_hits); - ch.setCv1Dest(static_cast(saved_ch_state.cv1_dest)); - ch.setCv1Dest(static_cast(saved_ch_state.cv2_dest)); - } - - return true; + // Load data from the transient slot. + return loadData(app, MAX_SAVE_SLOTS); } else { + // EEPROM does not contain save data for this firmware & version. + // Initialize eeprom and save default patter to all save slots. reset(app); + _saveMetadata(); + // MAX_SAVE_SLOTS slot is reserved for transient state. + for (int i = 0; i <= MAX_SAVE_SLOTS; i++) { + app.selected_save_slot = i; + _saveState(app, i); + } return false; } } -void StateManager::_save(const AppState& app) { - // Ensure interrupts do not cause corrupt data writes. - noInterrupts(); - _saveState(app); - interrupts(); +bool StateManager::loadData(AppState& app, byte slot_index) { + if (slot_index >= MAX_SAVE_SLOTS) return false; + + _loadState(app, slot_index); + + return true; +} + +void StateManager::saveData(const AppState& app) { + if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; + + _saveState(app, app.selected_save_slot); + _isDirty = false; +} + +void StateManager::update(const AppState& app) { + if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { + // MAX_SAVE_SLOTS slot is reserved for transient state. + _saveState(app, MAX_SAVE_SLOTS); + _isDirty = false; + } } void StateManager::reset(AppState& app) { @@ -56,27 +57,15 @@ void StateManager::reset(AppState& app) { app.selected_channel = 0; app.selected_source = Clock::SOURCE_INTERNAL; app.selected_pulse = Clock::PULSE_PPQN_24; + app.selected_save_slot = 0; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); } - noInterrupts(); - _saveMetadata(); // Write the new metadata - _saveState(app); // Write the new (default) app state - interrupts(); - _isDirty = false; } -void StateManager::update(const AppState& app) { - // Check if a save is pending and if enough time has passed. - if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { - _save(app); - _isDirty = false; // Clear the flag, we are now "clean". - } -} - void StateManager::markDirty() { _isDirty = true; _lastChangeTime = millis(); @@ -90,39 +79,76 @@ bool StateManager::_isDataValid() { return name_match && version_match; } -void StateManager::_saveState(const AppState& app) { +void StateManager::_saveState(const AppState& app, byte slot_index) { + if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; + + noInterrupts(); static EepromData save_data; - // Populate main app state save_data.tempo = app.tempo; save_data.encoder_reversed = app.encoder_reversed; save_data.selected_param = app.selected_param; save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); save_data.selected_pulse = static_cast(app.selected_pulse); + save_data.selected_save_slot = app.selected_save_slot; - // Loop through and populate each channel's state for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { const auto& ch = app.channel[i]; auto& save_ch = save_data.channel_data[i]; - - // Use the getters with 'withCvMod = false' to get the base values save_ch.base_clock_mod_index = ch.getClockModIndex(false); save_ch.base_probability = ch.getProbability(false); save_ch.base_duty_cycle = ch.getDutyCycle(false); save_ch.base_offset = ch.getOffset(false); - save_ch.base_shuffle = ch.getSwing(); - save_ch.base_euc_steps = ch.getSteps(); - save_ch.base_euc_hits = ch.getHits(); + save_ch.base_swing = ch.getSwing(false); + save_ch.base_euc_steps = ch.getSteps(false); + save_ch.base_euc_hits = ch.getHits(false); save_ch.cv1_dest = static_cast(ch.getCv1Dest()); save_ch.cv2_dest = static_cast(ch.getCv2Dest()); } - EEPROM.put(sizeof(Metadata), save_data); + + int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); + EEPROM.put(address, save_data); + interrupts(); +} + +void StateManager::_loadState(AppState& app, byte slot_index) { + noInterrupts(); + static EepromData load_data; + int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); + EEPROM.get(address, load_data); + + // Restore app state from loaded data. + app.tempo = load_data.tempo; + app.encoder_reversed = load_data.encoder_reversed; + app.selected_param = load_data.selected_param; + app.selected_channel = load_data.selected_channel; + app.selected_source = static_cast(load_data.selected_source); + app.selected_pulse = static_cast(load_data.selected_pulse); + app.selected_save_slot = slot_index; + + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + auto& ch = app.channel[i]; + const auto& saved_ch_state = load_data.channel_data[i]; + + ch.setClockMod(saved_ch_state.base_clock_mod_index); + ch.setProbability(saved_ch_state.base_probability); + ch.setDutyCycle(saved_ch_state.base_duty_cycle); + ch.setOffset(saved_ch_state.base_offset); + ch.setSwing(saved_ch_state.base_swing); + ch.setSteps(saved_ch_state.base_euc_steps); + ch.setHits(saved_ch_state.base_euc_hits); + ch.setCv1Dest(static_cast(saved_ch_state.cv1_dest)); + ch.setCv2Dest(static_cast(saved_ch_state.cv2_dest)); + } + interrupts(); } void StateManager::_saveMetadata() { + noInterrupts(); Metadata current_meta; strcpy(current_meta.sketch_name, SKETCH_NAME); current_meta.version = SKETCH_VERSION; EEPROM.put(0, current_meta); + interrupts(); } diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index 826fc93..a8d5a7e 100644 --- a/examples/Gravity/save_state.h +++ b/examples/Gravity/save_state.h @@ -9,13 +9,21 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 6; +const byte SKETCH_VERSION = 7; + +// Number of available save slots. +const byte MAX_SAVE_SLOTS = 10; // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; /** * @brief Manages saving and loading of the application state to and from EEPROM. + * The number of user slots is defined by MAX_SAVE_SLOTS, and one additional slot + * is reseved for transient state to persist state between power cycles before + * state is explicitly saved to a user slot. Metadata is stored in the beginning + * of the memory space which stores firmware version information to validate that + * the data can be loaded into the current version of AppState. */ class StateManager { public: @@ -23,6 +31,10 @@ class StateManager { // Populate the AppState instance with values from EEPROM if they exist. bool initialize(AppState& app); + // Load data from specified slot. + bool loadData(AppState& app, byte slot_index); + // Save data to specified slot. + void saveData(const AppState& app); // Reset AppState instance back to default values. void reset(AppState& app); // Call from main loop, check if state has changed and needs to be saved. @@ -30,18 +42,17 @@ class StateManager { // Indicate that state has changed and we should save. void markDirty(); - private: // This struct holds the data that identifies the firmware version. struct Metadata { - char sketch_name[16]; byte version; + char sketch_name[16]; }; struct ChannelState { byte base_clock_mod_index; byte base_probability; byte base_duty_cycle; byte base_offset; - byte base_shuffle; + byte base_swing; byte base_euc_steps; byte base_euc_hits; byte cv1_dest; // Cast the CvDestination enum as a byte for storage @@ -55,13 +66,15 @@ class StateManager { byte selected_channel; byte selected_source; byte selected_pulse; + byte selected_save_slot; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; - void _save(const AppState& app); + private: bool _isDataValid(); - void _saveState(const AppState& app); void _saveMetadata(); + void _saveState(const AppState& app, byte slot_index); + void _loadState(AppState& app, byte slot_index); bool _isDirty; unsigned long _lastChangeTime; -- 2.39.5 From ae726313a04c32a023bea32e19f6eec7f52e85af Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 10:44:03 -0700 Subject: [PATCH 26/54] use shift button to change channel when held + rotate. --- examples/Gravity/Gravity.ino | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index d153253..d6f00f4 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -24,14 +24,16 @@ * quantization of features like duty cycle (pulse width) or offset. * Additionally, this firmware replaces the sequencer with a Euclidean Rhythm * generator. - * + * * ENCODER: * Press: change between selecting a parameter and editing the parameter. * Hold & Rotate: change current selected output channel. * - * BTN1: Play/pause the internal clock. + * BTN1: + * Play/pause - start or stop the internal clock. * - * BTN2: Stop all clocks. + * BTN2: + * Shift - hold and rotate encoder to change current selected output channel. * */ @@ -68,7 +70,6 @@ void setup() { // Button press handlers. gravity.play_button.AttachPressHandler(HandlePlayPressed); - gravity.shift_button.AttachPressHandler(HandleShiftPressed); } void loop() { @@ -162,12 +163,6 @@ void HandlePlayPressed() { app.refresh_screen = true; } -void HandleShiftPressed() { - gravity.clock.Stop(); - ResetOutputs(); - app.refresh_screen = true; -} - void HandleEncoderPressed() { // Check if leaving editing mode should apply a selection. if (app.editing_param) { @@ -193,6 +188,12 @@ void HandleEncoderPressed() { } void HandleRotate(int val) { + // Shift & Rotate check + if (gravity.shift_button.On()) { + HandlePressedRotate(val); + return; + } + if (!app.editing_param) { // Navigation Mode const int max_param = (app.selected_channel == 0) ? PARAM_MAIN_LAST : PARAM_CH_LAST; -- 2.39.5 From 14d1c497b32ac91bac7461415c65e5a83737365d Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 10:54:16 -0700 Subject: [PATCH 27/54] Add clock reset behavior to EXT when internally clocked. --- clock.h | 5 +++++ examples/Gravity/Gravity.ino | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/clock.h b/clock.h index 5401d1a..8fa084e 100644 --- a/clock.h +++ b/clock.h @@ -145,6 +145,11 @@ class Clock { uClock.stop(); } + // Reset all clock counters to 0. + void Reset() { + uClock.resetCounters(); + } + // Returns true if the clock is not running. bool IsPaused() { return uClock.clock_state == uClock.PAUSED; diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 7cc17d7..a7842b2 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -35,6 +35,14 @@ * BTN2: * Shift - hold and rotate encoder to change current selected output channel. * + * EXT: + * External clock input. When Gravity is set to INTERNAL clock mode, this + * input is used to reset clocks. + * + * CV1: + * CV2: + * External analog input used to provide modulation to any channel parameter. + * */ #include @@ -143,11 +151,14 @@ void HandleIntClockTick(uint32_t tick) { } void HandleExtClockTick() { - // Ignore tick if not using external source. - if (!gravity.clock.ExternalSource()) { - return; + if (gravity.clock.InternalSource()) { + // Use EXT as Reset when internally clocked. + ResetOutputs(); + gravity.clock.Reset(); + } else { + // Register clock tick. + gravity.clock.Tick(); } - gravity.clock.Tick(); app.refresh_screen = true; } -- 2.39.5 From f6b4b8a2ad63c75dfc51f81318a757747d62094b Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 10:57:22 -0700 Subject: [PATCH 28/54] migrate Gravity firmware into a new dedicated firmware directory. --- {examples => firmware}/Gravity/Gravity.ino | 0 {examples => firmware}/Gravity/app_state.h | 0 {examples => firmware}/Gravity/channel.h | 0 {examples => firmware}/Gravity/display.h | 0 {examples => firmware}/Gravity/euclidean.h | 0 {examples => firmware}/Gravity/save_state.cpp | 0 {examples => firmware}/Gravity/save_state.h | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {examples => firmware}/Gravity/Gravity.ino (100%) rename {examples => firmware}/Gravity/app_state.h (100%) rename {examples => firmware}/Gravity/channel.h (100%) rename {examples => firmware}/Gravity/display.h (100%) rename {examples => firmware}/Gravity/euclidean.h (100%) rename {examples => firmware}/Gravity/save_state.cpp (100%) rename {examples => firmware}/Gravity/save_state.h (100%) diff --git a/examples/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino similarity index 100% rename from examples/Gravity/Gravity.ino rename to firmware/Gravity/Gravity.ino diff --git a/examples/Gravity/app_state.h b/firmware/Gravity/app_state.h similarity index 100% rename from examples/Gravity/app_state.h rename to firmware/Gravity/app_state.h diff --git a/examples/Gravity/channel.h b/firmware/Gravity/channel.h similarity index 100% rename from examples/Gravity/channel.h rename to firmware/Gravity/channel.h diff --git a/examples/Gravity/display.h b/firmware/Gravity/display.h similarity index 100% rename from examples/Gravity/display.h rename to firmware/Gravity/display.h diff --git a/examples/Gravity/euclidean.h b/firmware/Gravity/euclidean.h similarity index 100% rename from examples/Gravity/euclidean.h rename to firmware/Gravity/euclidean.h diff --git a/examples/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp similarity index 100% rename from examples/Gravity/save_state.cpp rename to firmware/Gravity/save_state.cpp diff --git a/examples/Gravity/save_state.h b/firmware/Gravity/save_state.h similarity index 100% rename from examples/Gravity/save_state.h rename to firmware/Gravity/save_state.h -- 2.39.5 From ab71ac9c3701d6c54bfa4580a41610d4d508067f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 14:05:58 -0700 Subject: [PATCH 29/54] Add copyright license information --- LICENSE | 21 +++++++++++++++++++++ analog_input.h | 2 +- button.h | 8 +++----- clock.h | 4 ++-- digital_output.h | 4 ++-- encoder.h | 4 ++-- firmware/Gravity/Gravity.ino | 7 ++++--- firmware/Gravity/app_state.h | 11 +++++++++++ firmware/Gravity/channel.h | 11 +++++++++++ firmware/Gravity/display.h | 11 +++++++++++ firmware/Gravity/euclidean.h | 11 +++++++++++ firmware/Gravity/save_state.cpp | 11 +++++++++++ firmware/Gravity/save_state.h | 11 +++++++++++ gravity.cpp | 4 ++-- gravity.h | 11 +++++++++++ peripherials.h | 5 +++-- uClock.cpp | 6 ++++++ uClock.h | 6 ++++++ 18 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..257a4bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Adam Wonak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/analog_input.h b/analog_input.h index 7838653..496899b 100644 --- a/analog_input.h +++ b/analog_input.h @@ -5,7 +5,7 @@ * @version 0.1 * @date 2025-05-23 * - * @copyright Copyright (c) 2025 + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ #ifndef ANALOG_INPUT_H diff --git a/button.h b/button.h index 16bd9a9..c66ba7c 100644 --- a/button.h +++ b/button.h @@ -1,13 +1,11 @@ /** * @file button.h * @author Adam Wonak (https://github.com/awonak) - * @brief for interacting with trigger / gate inputs. + * @brief Wrapper class for interacting with trigger / gate inputs. * @version 0.1 * @date 2025-04-20 - * - * Provide methods to convey curent state (HIGH / LOW) and change in state (disengaged, engageing, engaged, disengaging). - * - * @copyright Copyright (c) 2025 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ #ifndef BUTTON_H diff --git a/clock.h b/clock.h index 8fa084e..9cb2ad8 100644 --- a/clock.h +++ b/clock.h @@ -4,8 +4,8 @@ * @brief Wrapper Class for clock timing functions. * @version 0.1 * @date 2025-05-04 - * - * @copyright Copyright (c) 2025 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ diff --git a/digital_output.h b/digital_output.h index 38be3d3..9c4cfc8 100644 --- a/digital_output.h +++ b/digital_output.h @@ -4,8 +4,8 @@ * @brief Class for interacting with trigger / gate outputs. * @version 0.1 * @date 2025-04-17 - * - * @copyright Copyright (c) 2023 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ #ifndef DIGITAL_OUTPUT_H diff --git a/encoder.h b/encoder.h index e9a8635..85d52f1 100644 --- a/encoder.h +++ b/encoder.h @@ -4,8 +4,8 @@ * @brief Class for interacting with encoders. * @version 0.1 * @date 2025-04-19 - * - * @copyright Copyright (c) 2025 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ #ifndef ENCODER_DIR_H diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index a7842b2..34de653 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -2,10 +2,11 @@ * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) * @brief Alt firmware version of Gravity by Sitka Instruments. - * @version 0.1 - * @date 2025-05-04 + * @version v2.0.1 - June 2025 awonak - Full rewrite + * @version v1.0 - August 2023 Oleksiy H - Initial release + * @date 2025-07-04 * - * @copyright Copyright (c) 2025 + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * * This version of Gravity firmware is a full rewrite that leverages the * libGravity hardware abstraction library. The goal of this project was to diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index c52fc26..e7d9ab5 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -1,3 +1,14 @@ +/** + * @file app_state.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #ifndef APP_STATE_H #define APP_STATE_H diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 309ce98..6f46b28 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -1,3 +1,14 @@ +/** + * @file channel.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #ifndef CHANNEL_H #define CHANNEL_H diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index c000dde..cf37631 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -1,3 +1,14 @@ +/** + * @file display.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #ifndef DISPLAY_H #define DISPLAY_H diff --git a/firmware/Gravity/euclidean.h b/firmware/Gravity/euclidean.h index 33dfe56..8956ffd 100644 --- a/firmware/Gravity/euclidean.h +++ b/firmware/Gravity/euclidean.h @@ -1,3 +1,14 @@ +/** + * @file euclidean.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #ifndef EUCLIDEAN_H #define EUCLIDEAN_H diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 3c1e9d9..052779b 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -1,3 +1,14 @@ +/** + * @file save_state.cpp + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #include "save_state.h" #include diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index a8d5a7e..354253a 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -1,3 +1,14 @@ +/** + * @file save_state.h + * @author Adam Wonak (https://github.com/awonak/) + * @brief Alt firmware version of Gravity by Sitka Instruments. + * @version 2.0.1 + * @date 2025-07-04 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #ifndef SAVE_STATE_H #define SAVE_STATE_H diff --git a/gravity.cpp b/gravity.cpp index 3ef6d2b..23d79f8 100644 --- a/gravity.cpp +++ b/gravity.cpp @@ -4,8 +4,8 @@ * @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @version 0.1 * @date 2025-04-19 - * - * @copyright Copyright (c) 2025 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ diff --git a/gravity.h b/gravity.h index 5701586..00539fb 100644 --- a/gravity.h +++ b/gravity.h @@ -1,3 +1,14 @@ +/** + * @file gravity.h + * @author Adam Wonak (https://github.com/awonak) + * @brief Library for building custom scripts for the Sitka Instruments Gravity module. + * @version 0.1 + * @date 2025-04-19 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + */ + #ifndef GRAVITY_H #define GRAVITY_H diff --git a/peripherials.h b/peripherials.h index 5c4ec91..9eea4b6 100644 --- a/peripherials.h +++ b/peripherials.h @@ -4,10 +4,11 @@ * @brief Arduino pin definitions for the Sitka Instruments Gravity module. * @version 0.1 * @date 2025-04-19 - * - * @copyright Copyright (c) 2025 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * */ + #ifndef PERIPHERIALS_H #define PERIPHERIALS_H diff --git a/uClock.cpp b/uClock.cpp index 1f53084..1003930 100755 --- a/uClock.cpp +++ b/uClock.cpp @@ -6,6 +6,12 @@ * @author Romulo Silva * @date 10/06/2017 * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * 2025-06-30 - https://github.com/awonak/uClock/tree/picoClock + * Modified by awonak to remove all unused sync callback + * methods and associated variables to dramatically reduce + * memory usage. + * See: https://github.com/midilab/uClock/issues/58 * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), diff --git a/uClock.h b/uClock.h index d8670b0..ad45334 100755 --- a/uClock.h +++ b/uClock.h @@ -6,6 +6,12 @@ * @author Romulo Silva * @date 10/06/2017 * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co + * + * 2025-06-30 - https://github.com/awonak/uClock/tree/picoClock + * Modified by awonak to remove all unused sync callback + * methods and associated variables to dramatically reduce + * memory usage. + * See: https://github.com/midilab/uClock/issues/58 * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), -- 2.39.5 From 60a7a7a349a90d339788e4ed0a261ec62a42ed5c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Fri, 4 Jul 2025 15:21:03 -0700 Subject: [PATCH 30/54] fix example code --- examples/clock_mod/clock_mod.ino | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/clock_mod/clock_mod.ino b/examples/clock_mod/clock_mod.ino index e9458ea..8033c0d 100644 --- a/examples/clock_mod/clock_mod.ino +++ b/examples/clock_mod/clock_mod.ino @@ -33,8 +33,8 @@ struct AppState { bool editing_param = false; int selected_param = 0; byte selected_channel = 0; // 0=tempo, 1-6=output channel - Source selected_source = SOURCE_INTERNAL; - Channel channel[OUTPUT_COUNT]; + Clock::Source selected_source = Clock::SOURCE_INTERNAL; + Channel channel[Gravity::OUTPUT_COUNT]; }; AppState app; @@ -123,7 +123,7 @@ void loop() { // void HandleIntClockTick(uint32_t tick) { - for (int i = 0; i < OUTPUT_COUNT; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { auto& channel = app.channel[i]; auto& output = gravity.outputs[i]; @@ -178,7 +178,7 @@ void HandleEncoderPressed() { app.refresh_screen = true; } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (!app.editing_param) { // Navigation Mode const int max_param = (app.selected_channel == 0) ? PARAM_MAIN_LAST : PARAM_CH_LAST; @@ -188,17 +188,17 @@ void HandleRotate(Direction dir, int val) { if (app.selected_channel == 0) { editMainParameter(val); } else { - editChannelParameter(dir, val); + editChannelParameter(val); } } app.refresh_screen = true; } -void HandlePressedRotate(Direction dir, int val) { - if (dir == DIRECTION_INCREMENT && app.selected_channel < OUTPUT_COUNT) { +void HandlePressedRotate(int val) { + if (val > 0 && app.selected_channel < Gravity::OUTPUT_COUNT) { app.selected_channel++; - } else if (dir == DIRECTION_DECREMENT && app.selected_channel > 0) { + } else if (val < 0 && app.selected_channel > 0) { app.selected_channel--; } app.selected_param = 0; @@ -216,21 +216,21 @@ void editMainParameter(int val) { case PARAM_MAIN_SOURCE: { int source = static_cast(app.selected_source); - updateSelection(source, val, SOURCE_LAST); - app.selected_source = static_cast(source); + updateSelection(source, val, Clock::SOURCE_LAST); + app.selected_source = static_cast(source); gravity.clock.SetSource(app.selected_source); break; } } } -void editChannelParameter(Direction dir, int val) { +void editChannelParameter(int val) { auto& ch = GetSelectedChannel(); switch (static_cast(app.selected_param)) { case PARAM_CH_MOD: - if (dir == DIRECTION_INCREMENT && ch.clock_mod_index < MOD_CHOICE_SIZE - 1) { + if (val > 0 && ch.clock_mod_index < MOD_CHOICE_SIZE - 1) { ch.clock_mod_index++; - } else if (dir == DIRECTION_DECREMENT && ch.clock_mod_index > 0) { + } else if (val < 0 && ch.clock_mod_index > 0) { ch.clock_mod_index--; } break; @@ -265,7 +265,7 @@ Channel& GetSelectedChannel() { } void ResetOutputs() { - for (int i = 0; i < OUTPUT_COUNT; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { gravity.outputs[i].Low(); } } @@ -311,7 +311,7 @@ void DisplayMainPage() { if (app.selected_param == 0) { // Serial MIID is too unstable to display bpm in real time. - if (app.selected_source == SOURCE_EXTERNAL_MIDI) { + if (app.selected_source == Clock::SOURCE_EXTERNAL_MIDI) { sprintf(mainText, "%s", "EXT"); } else { sprintf(mainText, "%d", gravity.clock.Tempo()); @@ -319,19 +319,19 @@ void DisplayMainPage() { subText = "BPM"; } else if (app.selected_param == 1) { switch (app.selected_source) { - case SOURCE_INTERNAL: + case Clock::SOURCE_INTERNAL: sprintf(mainText, "%s", "INT"); subText = "Clock"; break; - case SOURCE_EXTERNAL_PPQN_24: + case Clock::SOURCE_EXTERNAL_PPQN_24: sprintf(mainText, "%s", "EXT"); subText = "24 PPQN"; break; - case SOURCE_EXTERNAL_PPQN_4: + case Clock::SOURCE_EXTERNAL_PPQN_4: sprintf(mainText, "%s", "EXT"); subText = "4 PPQN"; break; - case SOURCE_EXTERNAL_MIDI: + case Clock::SOURCE_EXTERNAL_MIDI: sprintf(mainText, "%s", "EXT"); subText = "MIDI"; break; @@ -399,7 +399,7 @@ void DisplaySelectedChannel() { gravity.display.drawHLine(1, boxY, SCREEN_WIDTH - 2); gravity.display.drawVLine(SCREEN_WIDTH - 2, boxY, boxHeight); - for (int i = 0; i < OUTPUT_COUNT + 1; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT + 1; i++) { // Draw box frame or filled selected box. gravity.display.setDrawColor(1); (app.selected_channel == i) -- 2.39.5 From 385ce85da3b37db8f9db23f0c62902fe69484d21 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 13 Jul 2025 12:00:37 -0700 Subject: [PATCH 31/54] add const for pulse clock mod choices. update git ignore and readme for build command. --- .gitignore | 3 ++- README.md | 5 +++++ firmware/Gravity/Gravity.ino | 6 +++--- firmware/Gravity/channel.h | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 41cc7c8..af53804 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ docs .vscode -.DS_Store \ No newline at end of file +.DS_Store +build/* \ No newline at end of file diff --git a/README.md b/README.md index f244487..569aeed 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,8 @@ void UpdateDisplay() { } ``` +### Build for release + +``` +$ arduino-cli compile -v -b arduino:avr:nano ./firmware/Gravity/Gravity.ino -e --output-dir=./build/ +``` \ No newline at end of file diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 34de653..374ee03 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -125,13 +125,13 @@ void HandleIntClockTick(uint32_t tick) { int clock_index; switch (app.selected_pulse) { case Clock::PULSE_PPQN_24: - clock_index = 0; + clock_index = PULSE_PPQN_24_CLOCK_MOD_INDEX; break; case Clock::PULSE_PPQN_4: - clock_index = 4; + clock_index = PULSE_PPQN_4_CLOCK_MOD_INDEX; break; case Clock::PULSE_PPQN_1: - clock_index = 7; + clock_index = PULSE_PPQN_1_CLOCK_MOD_INDEX; break; } diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 6f46b28..8036781 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -53,6 +53,10 @@ static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = { static const byte DEFAULT_CLOCK_MOD_INDEX = 8; // x1 or 96 PPQN. +static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = 0; +static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = 4; +static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = 8; + class Channel { public: Channel() { -- 2.39.5 From 5729eef037d31981e1829a2d436899670540594a Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:00:47 +0000 Subject: [PATCH 32/54] Factory Reset (#13) Fixes https://github.com/awonak/alt-gravity/issues/1 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/13 --- firmware/Gravity/Gravity.ino | 9 ++++++++ firmware/Gravity/app_state.h | 24 -------------------- firmware/Gravity/display.h | 39 ++++++++++++++++++++++++++++++++- firmware/Gravity/save_state.cpp | 12 +++++++++- firmware/Gravity/save_state.h | 6 +++-- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 374ee03..06934f9 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -203,6 +203,12 @@ void HandleEncoderPressed() { InitGravity(app); } } + if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { + if (app.selected_sub_param == 0) { // Reset + stateManager.factoryReset(); + InitGravity(app); + } + } } // Only mark dirty and reset selected_sub_param when leaving editing mode. stateManager.markDirty(); @@ -277,6 +283,9 @@ void editMainParameter(int val) { case PARAM_MAIN_RESET_STATE: updateSelection(app.selected_sub_param, val, 2); break; + case PARAM_MAIN_FACTORY_RESET: + updateSelection(app.selected_sub_param, val, 2); + break; } } diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index e7d9ab5..0ddd014 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -38,28 +38,4 @@ static Channel& GetSelectedChannel() { return app.channel[app.selected_channel - 1]; } -enum ParamsMainPage : uint8_t { - PARAM_MAIN_TEMPO, - PARAM_MAIN_SOURCE, - PARAM_MAIN_PULSE, - PARAM_MAIN_ENCODER_DIR, - PARAM_MAIN_SAVE_DATA, - PARAM_MAIN_LOAD_DATA, - PARAM_MAIN_RESET_STATE, - PARAM_MAIN_LAST, -}; - -enum ParamsChannelPage : uint8_t { - PARAM_CH_MOD, - PARAM_CH_PROB, - PARAM_CH_DUTY, - PARAM_CH_OFFSET, - PARAM_CH_SWING, - PARAM_CH_EUC_STEPS, - PARAM_CH_EUC_HITS, - PARAM_CH_CV1_DEST, - PARAM_CH_CV2_DEST, - PARAM_CH_LAST, -}; - #endif // APP_STATE_H \ No newline at end of file diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index cf37631..1823b75 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -96,6 +96,33 @@ constexpr uint8_t CHANNEL_BOXES_Y = 50; constexpr uint8_t CHANNEL_BOX_WIDTH = 18; constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; +// Menu items for editing global parameters. +enum ParamsMainPage : uint8_t { + PARAM_MAIN_TEMPO, + PARAM_MAIN_SOURCE, + PARAM_MAIN_PULSE, + PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_SAVE_DATA, + PARAM_MAIN_LOAD_DATA, + PARAM_MAIN_RESET_STATE, + PARAM_MAIN_FACTORY_RESET, + PARAM_MAIN_LAST, +}; + +// Menu items for editing channel parameters. +enum ParamsChannelPage : uint8_t { + PARAM_CH_MOD, + PARAM_CH_PROB, + PARAM_CH_DUTY, + PARAM_CH_OFFSET, + PARAM_CH_SWING, + PARAM_CH_EUC_STEPS, + PARAM_CH_EUC_HITS, + PARAM_CH_CV1_DEST, + PARAM_CH_CV2_DEST, + PARAM_CH_LAST, +}; + // Helper function to draw centered text void drawCenteredText(const char* text, int y, const uint8_t* font) { gravity.display.setFont(font); @@ -278,13 +305,23 @@ void DisplayMainPage() { mainText = F("x"); subText = F("BACK TO MAIN"); } + break; + case PARAM_MAIN_FACTORY_RESET: + if (app.selected_sub_param == 0) { + mainText = F("DEL"); + subText = F("FACTORY RESET"); + } else { + mainText = F("x"); + subText = F("BACK TO MAIN"); + } + break; } drawCenteredText(mainText.c_str(), MAIN_TEXT_Y, LARGE_FONT); drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 052779b..16ef342 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -26,7 +26,8 @@ bool StateManager::initialize(AppState& app) { return loadData(app, MAX_SAVE_SLOTS); } else { // EEPROM does not contain save data for this firmware & version. - // Initialize eeprom and save default patter to all save slots. + // Erase EEPROM and initialize state. Save default pattern to all save slots. + factoryReset(); reset(app); _saveMetadata(); // MAX_SAVE_SLOTS slot is reserved for transient state. @@ -82,6 +83,15 @@ void StateManager::markDirty() { _lastChangeTime = millis(); } +// Erases all data in the EEPROM by writing 0 to every address. +void StateManager::factoryReset() { + noInterrupts(); + for (unsigned int i = 0 ; i < EEPROM.length() ; i++) { + EEPROM.write(i, 0); + } + interrupts(); +} + bool StateManager::_isDataValid() { Metadata load_meta; EEPROM.get(0, load_meta); diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 354253a..29ae845 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -19,8 +19,8 @@ struct AppState; // Define the constants for the current firmware. -const char SKETCH_NAME[] = "Gravity"; -const byte SKETCH_VERSION = 7; +const char SKETCH_NAME[] = "AltGravity"; +const byte SKETCH_VERSION = 1; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; @@ -52,6 +52,8 @@ class StateManager { void update(const AppState& app); // Indicate that state has changed and we should save. void markDirty(); + // Erase all data stored in the EEPROM. + void factoryReset(); // This struct holds the data that identifies the firmware version. struct Metadata { -- 2.39.5 From 1bf90e16742d6e789534bf6e5dc12e0e7d5e76e4 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:01:18 +0000 Subject: [PATCH 33/54] Mute channel when shift + play pressed (#14) Fixes https://github.com/awonak/alt-gravity/issues/2 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/14 --- firmware/Gravity/Gravity.ino | 25 ++++++++++++++++++++----- firmware/Gravity/channel.h | 11 +++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 06934f9..b76a61b 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -2,7 +2,7 @@ * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) * @brief Alt firmware version of Gravity by Sitka Instruments. - * @version v2.0.1 - June 2025 awonak - Full rewrite + * @version v2.0.1 - June 2025 awonak - Full rewrite * @version v1.0 - August 2023 Oleksiy H - Initial release * @date 2025-07-04 * @@ -25,7 +25,7 @@ * quantization of features like duty cycle (pulse width) or offset. * Additionally, this firmware replaces the sequencer with a Euclidean Rhythm * generator. - * + * * ENCODER: * Press: change between selecting a parameter and editing the parameter. * Hold & Rotate: change current selected output channel. @@ -33,17 +33,17 @@ * BTN1: * Play/pause - start or stop the internal clock. * - * BTN2: + * BTN2: * Shift - hold and rotate encoder to change current selected output channel. * * EXT: * External clock input. When Gravity is set to INTERNAL clock mode, this * input is used to reset clocks. - * + * * CV1: * CV2: * External analog input used to provide modulation to any channel parameter. - * + * */ #include @@ -168,6 +168,21 @@ void HandleExtClockTick() { // void HandlePlayPressed() { + // Check if SHIFT is pressed to mute all/current channel. + if (gravity.shift_button.On()) { + if (app.selected_channel == 0) { + // Mute all channels + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + app.channel[i].toggleMute(); + } + } else { + // Mute selected channel + auto& ch = GetSelectedChannel(); + ch.toggleMute(); + } + return; + } + gravity.clock.IsPaused() ? gravity.clock.Start() : gravity.clock.Stop(); diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 8036781..8bcf999 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -161,6 +161,8 @@ class Channel { byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; } byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; } + void toggleMute() { mute = !mute; } + /** * @brief Processes a clock tick and determines if the output should be high or low. * Note: this method is called from an ISR and must be kept as simple as possible. @@ -168,6 +170,12 @@ class Channel { * @param output The output object to be modified. */ void processClockTick(uint32_t tick, DigitalOutput& output) { + // Mute check + if (mute) { + output.Low(); + return; + } + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); // Conditionally apply swing on down beats. @@ -298,6 +306,9 @@ class Channel { // Euclidean pattern Pattern pattern; + // Mute channel flag + bool mute; + // Pre-calculated pulse values for ISR performance uint16_t _duty_pulses; uint16_t _offset_pulses; -- 2.39.5 From 4f04137f67059f4b2f58f4bbb4dfb5150a7768c8 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Mon, 21 Jul 2025 00:27:32 +0000 Subject: [PATCH 34/54] Add global/hardware settings to metadata EEPROM (#15) Settings like Encoder Direction and Display Orientation should persist when resetting channel state. Fixes https://github.com/awonak/alt-gravity/issues/7 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/15 --- firmware/Gravity/Gravity.ino | 4 ++-- firmware/Gravity/save_state.cpp | 42 ++++++++++++++++++++++++++------- firmware/Gravity/save_state.h | 5 +++- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index b76a61b..3b388d4 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -196,8 +196,8 @@ void HandleEncoderPressed() { if (app.selected_channel == 0) { // main page // TODO: rewrite as switch if (app.selected_param == PARAM_MAIN_ENCODER_DIR) { - bool reversed = app.selected_sub_param == 1; - gravity.encoder.SetReverseDirection(reversed); + app.encoder_reversed = app.selected_sub_param == 1; + gravity.encoder.SetReverseDirection(app.encoder_reversed); } if (app.selected_param == PARAM_MAIN_SAVE_DATA) { if (app.selected_sub_param < MAX_SAVE_SLOTS) { diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 16ef342..8aa1f50 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -16,6 +16,7 @@ #include "app_state.h" // Calculate the starting address for EepromData, leaving space for metadata. +static const int METADATA_START_ADDR = 0; static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} @@ -24,12 +25,14 @@ bool StateManager::initialize(AppState& app) { if (_isDataValid()) { // Load data from the transient slot. return loadData(app, MAX_SAVE_SLOTS); - } else { - // EEPROM does not contain save data for this firmware & version. + } + // EEPROM does not contain save data for this firmware & version. + else { // Erase EEPROM and initialize state. Save default pattern to all save slots. factoryReset(); + // Initialize eeprom and save default patter to all save slots. + _saveMetadata(app); reset(app); - _saveMetadata(); // MAX_SAVE_SLOTS slot is reserved for transient state. for (int i = 0; i <= MAX_SAVE_SLOTS; i++) { app.selected_save_slot = i; @@ -43,10 +46,12 @@ bool StateManager::loadData(AppState& app, byte slot_index) { if (slot_index >= MAX_SAVE_SLOTS) return false; _loadState(app, slot_index); + _loadMetadata(app); return true; } +// Save app state to user specified save slot. void StateManager::saveData(const AppState& app) { if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; @@ -54,17 +59,18 @@ void StateManager::saveData(const AppState& app) { _isDirty = false; } +// Save transient state if it has changed and enough time has passed since last save. void StateManager::update(const AppState& app) { if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { // MAX_SAVE_SLOTS slot is reserved for transient state. _saveState(app, MAX_SAVE_SLOTS); + _saveMetadata(app); _isDirty = false; } } void StateManager::reset(AppState& app) { app.tempo = Clock::DEFAULT_TEMPO; - app.encoder_reversed = false; app.selected_param = 0; app.selected_channel = 0; app.selected_source = Clock::SOURCE_INTERNAL; @@ -75,6 +81,9 @@ void StateManager::reset(AppState& app) { app.channel[i].Init(); } + // Load global settings from Metadata + _loadMetadata(app); + _isDirty = false; } @@ -86,7 +95,7 @@ void StateManager::markDirty() { // Erases all data in the EEPROM by writing 0 to every address. void StateManager::factoryReset() { noInterrupts(); - for (unsigned int i = 0 ; i < EEPROM.length() ; i++) { + for (unsigned int i = 0; i < EEPROM.length(); i++) { EEPROM.write(i, 0); } interrupts(); @@ -94,7 +103,7 @@ void StateManager::factoryReset() { bool StateManager::_isDataValid() { Metadata load_meta; - EEPROM.get(0, load_meta); + EEPROM.get(METADATA_START_ADDR, load_meta); bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0); bool version_match = (load_meta.version == SKETCH_VERSION); return name_match && version_match; @@ -114,6 +123,10 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_data.selected_pulse = static_cast(app.selected_pulse); save_data.selected_save_slot = app.selected_save_slot; + // TODO: break this out into a separate function. Save State should be + // broken out into global / per-channel save methods. When saving via + // "update" only save state for the current channel since other channels + // will not have changed when saving user edits. for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { const auto& ch = app.channel[i]; auto& save_ch = save_data.channel_data[i]; @@ -141,7 +154,6 @@ void StateManager::_loadState(AppState& app, byte slot_index) { // Restore app state from loaded data. app.tempo = load_data.tempo; - app.encoder_reversed = load_data.encoder_reversed; app.selected_param = load_data.selected_param; app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); @@ -165,11 +177,23 @@ void StateManager::_loadState(AppState& app, byte slot_index) { interrupts(); } -void StateManager::_saveMetadata() { +void StateManager::_saveMetadata(const AppState& app) { noInterrupts(); Metadata current_meta; strcpy(current_meta.sketch_name, SKETCH_NAME); current_meta.version = SKETCH_VERSION; - EEPROM.put(0, current_meta); + + // Global user settings + current_meta.encoder_reversed = app.encoder_reversed; + + EEPROM.put(METADATA_START_ADDR, current_meta); interrupts(); } + +void StateManager::_loadMetadata(AppState& app) { + noInterrupts(); + Metadata metadata; + EEPROM.get(METADATA_START_ADDR, metadata); + app.encoder_reversed = metadata.encoder_reversed; + interrupts(); +} \ No newline at end of file diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 29ae845..bda7c4f 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -59,6 +59,8 @@ class StateManager { struct Metadata { byte version; char sketch_name[16]; + // Additional global/hardware settings + bool encoder_reversed; }; struct ChannelState { byte base_clock_mod_index; @@ -85,7 +87,8 @@ class StateManager { private: bool _isDataValid(); - void _saveMetadata(); + void _saveMetadata(const AppState& app); + void _loadMetadata(AppState& app); void _saveState(const AppState& app, byte slot_index); void _loadState(AppState& app, byte slot_index); -- 2.39.5 From 01f32407f6594405a6c3539194bad655be179b30 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 20 Jul 2025 17:53:03 -0700 Subject: [PATCH 35/54] bump version --- firmware/Gravity/save_state.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 91e1e8f..659a6e6 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -20,7 +20,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "ALT GRAVITY"; -const char SEMANTIC_VERSION[] = "V2.0.0BETA1"; +const char SEMANTIC_VERSION[] = "V2.0.0BETA2"; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. -- 2.39.5 From 1c0fb86bc102c303907de39642d5ca2bfaf1e88d Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 00:00:49 +0000 Subject: [PATCH 36/54] Reverse the order of clock mod options. (#16) This now matches original Gravity behavior. Also, now when applying CV mod positive voltages increase clock mod instead of reducing it. Also fix pulse out, which wasn't previously updated when CLOCK_MOD was moved to program mem. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/16 --- clock.h | 3 --- firmware/Gravity/Gravity.ino | 5 ++--- firmware/Gravity/channel.h | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/clock.h b/clock.h index 9cb2ad8..4008e0d 100644 --- a/clock.h +++ b/clock.h @@ -50,9 +50,6 @@ class Clock { void Init() { NeoSerial.begin(31250); - // Static pin definition for pulse out. - pinMode(PULSE_OUT_PIN, OUTPUT); - // Initialize the clock library uClock.init(); uClock.setClockMode(uClock.INTERNAL_CLOCK); diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 3b388d4..c0f9481 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -135,13 +135,12 @@ void HandleIntClockTick(uint32_t tick) { break; } - const uint32_t pulse_high_ticks = CLOCK_MOD_PULSES[clock_index]; + const uint16_t pulse_high_ticks = pgm_read_word_near(&CLOCK_MOD_PULSES[clock_index]); const uint32_t pulse_low_ticks = tick + max((pulse_high_ticks / 2), 1L); if (tick % pulse_high_ticks == 0) { gravity.pulse.High(); - } - if (pulse_low_ticks % pulse_high_ticks == 0) { + } else if (pulse_low_ticks % pulse_high_ticks == 0) { gravity.pulse.Low(); } } diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 8bcf999..f30b4c8 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -34,28 +34,28 @@ static const byte MOD_CHOICE_SIZE = 25; // Negative numbers are multipliers, positive are divisors. static const int CLOCK_MOD[MOD_CHOICE_SIZE] PROGMEM = { - // Multipliers - -24, -16, -12, -8, -6, -4, -3, -2, - // Internal Clock Unity - 1, // Divisors - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 24, 32, 64, 128}; + 128, 64, 32, 24, 16, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, + // Internal Clock Unity (quarter note) + 1, + // Multipliers + -2, -3, -4, -6, -8, -12, -16, -24}; // This represents the number of clock pulses for a 96 PPQN clock source // that match the above div/mult mods. static const int CLOCK_MOD_PULSES[MOD_CHOICE_SIZE] PROGMEM = { - // Multiplier Pulses (96 / X) - 4, 6, 8, 12, 16, 24, 32, 48, + // Divisor Pulses (96 * X) + 12288, 6144, 3072, 2304, 1536, 1152, 1056, 960, 864, 768, 672, 576, 480, 384, 288, 192, // Internal Clock Pulses 96, - // Divisor Pulses (96 * X) - 192, 288, 384, 480, 576, 672, 768, 864, 960, 1056, 1152, 1536, 2304, 3072, 6144, 12288}; + // Multiplier Pulses (96 / X) + 48, 32, 24, 16, 12, 8, 6, 4}; -static const byte DEFAULT_CLOCK_MOD_INDEX = 8; // x1 or 96 PPQN. +static const byte DEFAULT_CLOCK_MOD_INDEX = 16; // x1 or 96 PPQN. -static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = 0; -static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = 4; -static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = 8; +static const byte PULSE_PPQN_24_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 1; +static const byte PULSE_PPQN_4_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 6; +static const byte PULSE_PPQN_1_CLOCK_MOD_INDEX = MOD_CHOICE_SIZE - 9; class Channel { public: -- 2.39.5 From b0accdc83a6625b7a8c9333c9e174b98d3653559 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 05:12:45 +0000 Subject: [PATCH 37/54] Fix Initial Transient State (#17) There was an off-by-one error that was not properly loading transient state from the designated memory slot. Also fixes setting the last saved/loaded slot indicator with metadata. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/17 --- firmware/Gravity/save_state.cpp | 25 +++++++++++++++---------- firmware/Gravity/save_state.h | 5 +++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 8aa1f50..08c811b 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -24,7 +24,7 @@ StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} bool StateManager::initialize(AppState& app) { if (_isDataValid()) { // Load data from the transient slot. - return loadData(app, MAX_SAVE_SLOTS); + return loadData(app, TRANSIENT_SLOT); } // EEPROM does not contain save data for this firmware & version. else { @@ -33,17 +33,17 @@ bool StateManager::initialize(AppState& app) { // Initialize eeprom and save default patter to all save slots. _saveMetadata(app); reset(app); - // MAX_SAVE_SLOTS slot is reserved for transient state. - for (int i = 0; i <= MAX_SAVE_SLOTS; i++) { - app.selected_save_slot = i; + for (int i = 0; i < MAX_SAVE_SLOTS; i++) { _saveState(app, i); } + _saveState(app, TRANSIENT_SLOT); return false; } } bool StateManager::loadData(AppState& app, byte slot_index) { - if (slot_index >= MAX_SAVE_SLOTS) return false; + // Check if slot_index is within max range + 1 for transient. + if (slot_index >= MAX_SAVE_SLOTS + 1) return false; _loadState(app, slot_index); _loadMetadata(app); @@ -53,7 +53,8 @@ bool StateManager::loadData(AppState& app, byte slot_index) { // Save app state to user specified save slot. void StateManager::saveData(const AppState& app) { - if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; + // Check if slot_index is within max range + 1 for transient. + if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; _saveState(app, app.selected_save_slot); _isDirty = false; @@ -62,8 +63,7 @@ void StateManager::saveData(const AppState& app) { // Save transient state if it has changed and enough time has passed since last save. void StateManager::update(const AppState& app) { if (_isDirty && (millis() - _lastChangeTime > SAVE_DELAY_MS)) { - // MAX_SAVE_SLOTS slot is reserved for transient state. - _saveState(app, MAX_SAVE_SLOTS); + _saveState(app, TRANSIENT_SLOT); _saveMetadata(app); _isDirty = false; } @@ -110,7 +110,8 @@ bool StateManager::_isDataValid() { } void StateManager::_saveState(const AppState& app, byte slot_index) { - if (app.selected_save_slot >= MAX_SAVE_SLOTS) return; + // Check if slot_index is within max range + 1 for transient. + if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; noInterrupts(); static EepromData save_data; @@ -121,7 +122,6 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); save_data.selected_pulse = static_cast(app.selected_pulse); - save_data.selected_save_slot = app.selected_save_slot; // TODO: break this out into a separate function. Save State should be // broken out into global / per-channel save methods. When saving via @@ -147,6 +147,9 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { } void StateManager::_loadState(AppState& app, byte slot_index) { + // Check if slot_index is within max range + 1 for transient. + if (slot_index >= MAX_SAVE_SLOTS + 1) return; + noInterrupts(); static EepromData load_data; int address = EEPROM_DATA_START_ADDR + (slot_index * sizeof(EepromData)); @@ -184,6 +187,7 @@ void StateManager::_saveMetadata(const AppState& app) { current_meta.version = SKETCH_VERSION; // Global user settings + current_meta.selected_save_slot = app.selected_save_slot; current_meta.encoder_reversed = app.encoder_reversed; EEPROM.put(METADATA_START_ADDR, current_meta); @@ -194,6 +198,7 @@ void StateManager::_loadMetadata(AppState& app) { noInterrupts(); Metadata metadata; EEPROM.get(METADATA_START_ADDR, metadata); + app.selected_save_slot = metadata.selected_save_slot; app.encoder_reversed = metadata.encoder_reversed; interrupts(); } \ No newline at end of file diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index bda7c4f..ec75f03 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -23,7 +23,8 @@ const char SKETCH_NAME[] = "AltGravity"; const byte SKETCH_VERSION = 1; // Number of available save slots. -const byte MAX_SAVE_SLOTS = 10; +const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. +const byte TRANSIENT_SLOT = 10; // Transient slot index to persist state when powered off. // Define the minimum amount of time between EEPROM writes. static const unsigned long SAVE_DELAY_MS = 2000; @@ -60,6 +61,7 @@ class StateManager { byte version; char sketch_name[16]; // Additional global/hardware settings + byte selected_save_slot; bool encoder_reversed; }; struct ChannelState { @@ -81,7 +83,6 @@ class StateManager { byte selected_channel; byte selected_source; byte selected_pulse; - byte selected_save_slot; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; -- 2.39.5 From c5bddef66d0ddd4017339ddd26862e79e25c9d1c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Tue, 22 Jul 2025 05:16:32 +0000 Subject: [PATCH 38/54] Show loading bootsplash with firmware name and version (#18) Bootsplash is displayed before EEPROM erase, which is a slow operation. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/18 --- firmware/Gravity/Gravity.ino | 6 ++++++ firmware/Gravity/display.h | 17 +++++++++++++++++ firmware/Gravity/save_state.cpp | 4 ++-- firmware/Gravity/save_state.h | 6 +++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index c0f9481..e58e8c5 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -64,6 +64,10 @@ void setup() { // Start Gravity. gravity.Init(); + // Show bootsplash when initializing firmware. + Bootsplash(); + delay(2000); + // Initialize the state manager. This will load settings from EEPROM stateManager.initialize(app); InitGravity(app); @@ -219,7 +223,9 @@ void HandleEncoderPressed() { } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { if (app.selected_sub_param == 0) { // Reset + Bootsplash(); stateManager.factoryReset(); + stateManager.reset(app); InitGravity(app); } } diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index 1823b75..39ad8fd 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -469,4 +469,21 @@ void UpdateDisplay() { } while (gravity.display.nextPage()); } +void Bootsplash() { + gravity.display.firstPage(); + do { + int textWidth; + gravity.display.setFont(TEXT_FONT); + + textWidth = gravity.display.getStrWidth(SKETCH_NAME); + gravity.display.drawStr(24 + (textWidth / 2), 24, SKETCH_NAME); + + textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION); + gravity.display.drawStr(24 + (textWidth / 2), 36, SEMANTIC_VERSION); + + textWidth = gravity.display.getStrWidth("LOADING...."); + gravity.display.drawStr(34 + (textWidth / 2), 48, "LOADING...."); + } while (gravity.display.nextPage()); +} + #endif // DISPLAY_H diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 08c811b..3b29239 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -105,7 +105,7 @@ bool StateManager::_isDataValid() { Metadata load_meta; EEPROM.get(METADATA_START_ADDR, load_meta); bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0); - bool version_match = (load_meta.version == SKETCH_VERSION); + bool version_match = (strcmp(load_meta.version, SEMANTIC_VERSION) == 0); return name_match && version_match; } @@ -184,7 +184,7 @@ void StateManager::_saveMetadata(const AppState& app) { noInterrupts(); Metadata current_meta; strcpy(current_meta.sketch_name, SKETCH_NAME); - current_meta.version = SKETCH_VERSION; + strcpy(current_meta.version, SEMANTIC_VERSION); // Global user settings current_meta.selected_save_slot = app.selected_save_slot; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index ec75f03..91e1e8f 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -19,8 +19,8 @@ struct AppState; // Define the constants for the current firmware. -const char SKETCH_NAME[] = "AltGravity"; -const byte SKETCH_VERSION = 1; +const char SKETCH_NAME[] = "ALT GRAVITY"; +const char SEMANTIC_VERSION[] = "V2.0.0BETA1"; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. @@ -58,8 +58,8 @@ class StateManager { // This struct holds the data that identifies the firmware version. struct Metadata { - byte version; char sketch_name[16]; + char version[16]; // Additional global/hardware settings byte selected_save_slot; bool encoder_reversed; -- 2.39.5 From ec34bc3a7bba67d9da002c1b6228e778a3213114 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 23 Jul 2025 03:32:16 +0000 Subject: [PATCH 39/54] Fix metadata loading issues with Initialization and refactor Factory Reset. (#19) Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/19 --- firmware/Gravity/Gravity.ino | 5 ++- firmware/Gravity/display.h | 6 ++-- firmware/Gravity/save_state.cpp | 54 ++++++++++++++++++--------------- firmware/Gravity/save_state.h | 5 ++- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index e58e8c5..122cbf2 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -222,10 +222,9 @@ void HandleEncoderPressed() { } } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { - if (app.selected_sub_param == 0) { // Reset + if (app.selected_sub_param == 0) { // Erase Bootsplash(); - stateManager.factoryReset(); - stateManager.reset(app); + stateManager.factoryReset(app); InitGravity(app); } } diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index 39ad8fd..dc67447 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -476,13 +476,13 @@ void Bootsplash() { gravity.display.setFont(TEXT_FONT); textWidth = gravity.display.getStrWidth(SKETCH_NAME); - gravity.display.drawStr(24 + (textWidth / 2), 24, SKETCH_NAME); + gravity.display.drawStr(16 + (textWidth / 2), 20, SKETCH_NAME); textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION); - gravity.display.drawStr(24 + (textWidth / 2), 36, SEMANTIC_VERSION); + gravity.display.drawStr(16 + (textWidth / 2), 32, SEMANTIC_VERSION); textWidth = gravity.display.getStrWidth("LOADING...."); - gravity.display.drawStr(34 + (textWidth / 2), 48, "LOADING...."); + gravity.display.drawStr(26 + (textWidth / 2), 44, "LOADING...."); } while (gravity.display.nextPage()); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 3b29239..674115f 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -23,20 +23,16 @@ StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} bool StateManager::initialize(AppState& app) { if (_isDataValid()) { - // Load data from the transient slot. - return loadData(app, TRANSIENT_SLOT); + // Load global settings. + _loadMetadata(app); + // Load app data from the transient slot. + _loadState(app, TRANSIENT_SLOT); + return true; } // EEPROM does not contain save data for this firmware & version. else { // Erase EEPROM and initialize state. Save default pattern to all save slots. - factoryReset(); - // Initialize eeprom and save default patter to all save slots. - _saveMetadata(app); - reset(app); - for (int i = 0; i < MAX_SAVE_SLOTS; i++) { - _saveState(app, i); - } - _saveState(app, TRANSIENT_SLOT); + factoryReset(app); return false; } } @@ -45,8 +41,11 @@ bool StateManager::loadData(AppState& app, byte slot_index) { // Check if slot_index is within max range + 1 for transient. if (slot_index >= MAX_SAVE_SLOTS + 1) return false; + // Load the state data from the specified EEPROM slot and update the app state save slot. _loadState(app, slot_index); - _loadMetadata(app); + app.selected_save_slot = slot_index; + // Persist this change in the global metadata. + _saveMetadata(app); return true; } @@ -57,6 +56,7 @@ void StateManager::saveData(const AppState& app) { if (app.selected_save_slot >= MAX_SAVE_SLOTS + 1) return; _saveState(app, app.selected_save_slot); + _saveMetadata(app); _isDirty = false; } @@ -70,12 +70,12 @@ void StateManager::update(const AppState& app) { } void StateManager::reset(AppState& app) { - app.tempo = Clock::DEFAULT_TEMPO; - app.selected_param = 0; - app.selected_channel = 0; - app.selected_source = Clock::SOURCE_INTERNAL; - app.selected_pulse = Clock::PULSE_PPQN_24; - app.selected_save_slot = 0; + AppState default_app; + app.tempo = default_app.tempo; + app.selected_param = default_app.selected_param; + app.selected_channel = default_app.selected_channel; + app.selected_source = default_app.selected_source; + app.selected_pulse = default_app.selected_pulse; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -93,19 +93,27 @@ void StateManager::markDirty() { } // Erases all data in the EEPROM by writing 0 to every address. -void StateManager::factoryReset() { +void StateManager::factoryReset(AppState& app) { noInterrupts(); for (unsigned int i = 0; i < EEPROM.length(); i++) { EEPROM.write(i, 0); } + // Initialize eeprom and save default patter to all save slots. + _saveMetadata(app); + reset(app); + for (int i = 0; i < MAX_SAVE_SLOTS; i++) { + app.selected_save_slot = i; + _saveState(app, i); + } + _saveState(app, TRANSIENT_SLOT); interrupts(); } bool StateManager::_isDataValid() { - Metadata load_meta; - EEPROM.get(METADATA_START_ADDR, load_meta); - bool name_match = (strcmp(load_meta.sketch_name, SKETCH_NAME) == 0); - bool version_match = (strcmp(load_meta.version, SEMANTIC_VERSION) == 0); + Metadata metadata; + EEPROM.get(METADATA_START_ADDR, metadata); + bool name_match = (strcmp(metadata.sketch_name, SKETCH_NAME) == 0); + bool version_match = (strcmp(metadata.version, SEMANTIC_VERSION) == 0); return name_match && version_match; } @@ -117,7 +125,6 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { static EepromData save_data; save_data.tempo = app.tempo; - save_data.encoder_reversed = app.encoder_reversed; save_data.selected_param = app.selected_param; save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); @@ -161,7 +168,6 @@ void StateManager::_loadState(AppState& app, byte slot_index) { app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); app.selected_pulse = static_cast(load_data.selected_pulse); - app.selected_save_slot = slot_index; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { auto& ch = app.channel[i]; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 91e1e8f..cd7fcba 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -20,7 +20,7 @@ struct AppState; // Define the constants for the current firmware. const char SKETCH_NAME[] = "ALT GRAVITY"; -const char SEMANTIC_VERSION[] = "V2.0.0BETA1"; +const char SEMANTIC_VERSION[] = "V2.0.0BETA2"; // Number of available save slots. const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. @@ -54,7 +54,7 @@ class StateManager { // Indicate that state has changed and we should save. void markDirty(); // Erase all data stored in the EEPROM. - void factoryReset(); + void factoryReset(AppState& app); // This struct holds the data that identifies the firmware version. struct Metadata { @@ -78,7 +78,6 @@ class StateManager { // This struct holds all the parameters we want to save. struct EepromData { int tempo; - bool encoder_reversed; byte selected_param; byte selected_channel; byte selected_source; -- 2.39.5 From c7a3277b5fc9e2ae10987f3f972acb498cc9256f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 07:53:41 -0700 Subject: [PATCH 40/54] Memory improvements in bootsplash and StateManager --- firmware/Gravity/Gravity.ino | 7 ++++--- firmware/Gravity/app_state.h | 8 ++++---- firmware/Gravity/display.h | 23 ++++++++++++----------- firmware/Gravity/save_state.cpp | 15 +++++++++++++-- firmware/Gravity/save_state.h | 20 +++++++++----------- 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 122cbf2..18c784e 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -203,13 +203,13 @@ void HandleEncoderPressed() { gravity.encoder.SetReverseDirection(app.encoder_reversed); } if (app.selected_param == PARAM_MAIN_SAVE_DATA) { - if (app.selected_sub_param < MAX_SAVE_SLOTS) { + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { app.selected_save_slot = app.selected_sub_param; stateManager.saveData(app); } } if (app.selected_param == PARAM_MAIN_LOAD_DATA) { - if (app.selected_sub_param < MAX_SAVE_SLOTS) { + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { app.selected_save_slot = app.selected_sub_param; stateManager.loadData(app, app.selected_save_slot); InitGravity(app); @@ -223,6 +223,7 @@ void HandleEncoderPressed() { } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { if (app.selected_sub_param == 0) { // Erase + // Show bootsplash during slow erase operation. Bootsplash(); stateManager.factoryReset(app); InitGravity(app); @@ -297,7 +298,7 @@ void editMainParameter(int val) { break; case PARAM_MAIN_SAVE_DATA: case PARAM_MAIN_LOAD_DATA: - updateSelection(app.selected_sub_param, val, MAX_SAVE_SLOTS + 1); + updateSelection(app.selected_sub_param, val, StateManager::MAX_SAVE_SLOTS + 1); break; case PARAM_MAIN_RESET_STATE: updateSelection(app.selected_sub_param, val, 2); diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index 0ddd014..b53d314 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -19,9 +19,7 @@ // Global state for settings and app behavior. struct AppState { int tempo = Clock::DEFAULT_TEMPO; - bool encoder_reversed = false; - bool refresh_screen = true; - bool editing_param = false; + Channel channel[Gravity::OUTPUT_COUNT]; byte selected_param = 0; byte selected_sub_param = 0; // Temporary value for editing params. byte selected_channel = 0; // 0=tempo, 1-6=output channel @@ -29,7 +27,9 @@ struct AppState { byte selected_save_slot = 0; // The currently active save slot. Clock::Source selected_source = Clock::SOURCE_INTERNAL; Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; - Channel channel[Gravity::OUTPUT_COUNT]; + bool editing_param = false; + bool encoder_reversed = false; + bool refresh_screen = true; }; extern AppState app; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index dc67447..37b900b 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -214,10 +214,10 @@ void swingDivisionMark() { // Human friendly display value for save slot. String displaySaveSlot(int slot) { - if (slot >= 0 && slot < MAX_SAVE_SLOTS / 2) { + if (slot >= 0 && slot < StateManager::MAX_SAVE_SLOTS / 2) { return String("A") + String(slot + 1); - } else if (slot >= MAX_SAVE_SLOTS / 2 && slot <= MAX_SAVE_SLOTS) { - return String("B") + String(slot - (MAX_SAVE_SLOTS / 2) + 1); + } else if (slot >= StateManager::MAX_SAVE_SLOTS / 2 && slot <= StateManager::MAX_SAVE_SLOTS) { + return String("B") + String(slot - (StateManager::MAX_SAVE_SLOTS / 2) + 1); } } @@ -283,7 +283,7 @@ void DisplayMainPage() { break; case PARAM_MAIN_SAVE_DATA: case PARAM_MAIN_LOAD_DATA: - if (app.selected_sub_param == MAX_SAVE_SLOTS) { + if (app.selected_sub_param == StateManager::MAX_SAVE_SLOTS) { mainText = F("x"); subText = F("BACK TO MAIN"); } else { @@ -465,7 +465,7 @@ void UpdateDisplay() { DisplayChannelPage(); } // Global channel select UI. - DisplaySelectedChannel(); + DisplaySelectedChannel(); } while (gravity.display.nextPage()); } @@ -473,16 +473,17 @@ void Bootsplash() { gravity.display.firstPage(); do { int textWidth; + String loadingText = F("LOADING...."); gravity.display.setFont(TEXT_FONT); - textWidth = gravity.display.getStrWidth(SKETCH_NAME); - gravity.display.drawStr(16 + (textWidth / 2), 20, SKETCH_NAME); + textWidth = gravity.display.getStrWidth(StateManager::SKETCH_NAME); + gravity.display.drawStr(16 + (textWidth / 2), 20, StateManager::SKETCH_NAME); - textWidth = gravity.display.getStrWidth(SEMANTIC_VERSION); - gravity.display.drawStr(16 + (textWidth / 2), 32, SEMANTIC_VERSION); + textWidth = gravity.display.getStrWidth(StateManager::SEMANTIC_VERSION); + gravity.display.drawStr(16 + (textWidth / 2), 32, StateManager::SEMANTIC_VERSION); - textWidth = gravity.display.getStrWidth("LOADING...."); - gravity.display.drawStr(26 + (textWidth / 2), 44, "LOADING...."); + textWidth = gravity.display.getStrWidth(loadingText.c_str()); + gravity.display.drawStr(26 + (textWidth / 2), 44, loadingText.c_str()); } while (gravity.display.nextPage()); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 674115f..be4db95 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -15,9 +15,20 @@ #include "app_state.h" +// Define the constants for the current firmware. +const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; + +// Number of available save slots. +const byte StateManager::MAX_SAVE_SLOTS = 10; +const byte StateManager::TRANSIENT_SLOT = 10; + +// Define the minimum amount of time between EEPROM writes. +const unsigned long StateManager::SAVE_DELAY_MS = 2000; + // Calculate the starting address for EepromData, leaving space for metadata. -static const int METADATA_START_ADDR = 0; -static const int EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); +const int StateManager::METADATA_START_ADDR = 0; +const int StateManager::EEPROM_DATA_START_ADDR = sizeof(StateManager::Metadata); StateManager::StateManager() : _isDirty(false), _lastChangeTime(0) {} diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index cd7fcba..b1a94d6 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -18,17 +18,6 @@ // Forward-declare AppState to avoid circular dependencies. struct AppState; -// Define the constants for the current firmware. -const char SKETCH_NAME[] = "ALT GRAVITY"; -const char SEMANTIC_VERSION[] = "V2.0.0BETA2"; - -// Number of available save slots. -const byte MAX_SAVE_SLOTS = 10; // Count of save slots 0 - 9 to save/load presets. -const byte TRANSIENT_SLOT = 10; // Transient slot index to persist state when powered off. - -// Define the minimum amount of time between EEPROM writes. -static const unsigned long SAVE_DELAY_MS = 2000; - /** * @brief Manages saving and loading of the application state to and from EEPROM. * The number of user slots is defined by MAX_SAVE_SLOTS, and one additional slot @@ -39,6 +28,11 @@ static const unsigned long SAVE_DELAY_MS = 2000; */ class StateManager { public: + static const char SKETCH_NAME[]; + static const char SEMANTIC_VERSION[]; + static const byte MAX_SAVE_SLOTS; + static const byte TRANSIENT_SLOT; + StateManager(); // Populate the AppState instance with values from EEPROM if they exist. @@ -92,6 +86,10 @@ class StateManager { void _saveState(const AppState& app, byte slot_index); void _loadState(AppState& app, byte slot_index); + static const unsigned long SAVE_DELAY_MS; + static const int METADATA_START_ADDR; + static const int EEPROM_DATA_START_ADDR; + bool _isDirty; unsigned long _lastChangeTime; }; -- 2.39.5 From 65dde4d62e8ff3ddc063d2181d03e013daf0b917 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 15:07:15 +0000 Subject: [PATCH 41/54] Reorganization of library structure to better match Arduino spec (#20) Note, this will also require to you "uninstall and reinstall" the Arduino library due to the library file location changes. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/20 --- examples/clock_mod/clock_mod.ino | 2 +- firmware/Gravity/Gravity.ino | 4 +- firmware/Gravity/app_state.h | 2 +- firmware/Gravity/channel.h | 2 +- firmware/Gravity/save_state.cpp | 2 +- firmware/Gravity/save_state.h | 2 +- library.properties | 10 ++ analog_input.h => src/analog_input.h | 0 button.h => src/button.h | 0 clock.h => src/clock.h | 2 +- digital_output.h => src/digital_output.h | 0 encoder.h => src/encoder.h | 0 gravity.cpp => src/libGravity.cpp | 4 +- gravity.h => src/libGravity.h | 2 +- peripherials.h => src/peripherials.h | 0 {uClock => src/uClock}/platforms/avr.h | 0 uClock.cpp => src/uClock/uClock.cpp | 2 +- uClock.h => src/uClock/uClock.h | 0 uClock/uClock.h | 180 ----------------------- 19 files changed, 22 insertions(+), 192 deletions(-) create mode 100644 library.properties rename analog_input.h => src/analog_input.h (100%) rename button.h => src/button.h (100%) rename clock.h => src/clock.h (99%) rename digital_output.h => src/digital_output.h (100%) rename encoder.h => src/encoder.h (100%) rename gravity.cpp => src/libGravity.cpp (97%) rename gravity.h => src/libGravity.h (98%) rename peripherials.h => src/peripherials.h (100%) rename {uClock => src/uClock}/platforms/avr.h (100%) rename uClock.cpp => src/uClock/uClock.cpp (99%) rename uClock.h => src/uClock/uClock.h (100%) delete mode 100755 uClock/uClock.h diff --git a/examples/clock_mod/clock_mod.ino b/examples/clock_mod/clock_mod.ino index 8033c0d..cba7091 100644 --- a/examples/clock_mod/clock_mod.ino +++ b/examples/clock_mod/clock_mod.ino @@ -17,7 +17,7 @@ * */ -#include "gravity.h" +#include // Firmware state variables. struct Channel { diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 18c784e..0a033aa 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -2,7 +2,7 @@ * @file Gravity.ino * @author Adam Wonak (https://github.com/awonak/) * @brief Alt firmware version of Gravity by Sitka Instruments. - * @version v2.0.1 - June 2025 awonak - Full rewrite + * @version v2.0.0 - June 2025 awonak - Full rewrite * @version v1.0 - August 2023 Oleksiy H - Initial release * @date 2025-07-04 * @@ -46,7 +46,7 @@ * */ -#include +#include #include "app_state.h" #include "channel.h" diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index b53d314..90712df 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -12,7 +12,7 @@ #ifndef APP_STATE_H #define APP_STATE_H -#include +#include #include "channel.h" diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index f30b4c8..b668f2f 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -13,7 +13,7 @@ #define CHANNEL_H #include -#include +#include #include "euclidean.h" diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index be4db95..96a1f48 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index b1a94d6..8f25dd1 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -13,7 +13,7 @@ #define SAVE_STATE_H #include -#include +#include // Forward-declare AppState to avoid circular dependencies. struct AppState; diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..77fe46f --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=libGravity +version=2.0.0beta2 +author=Adam Wonak +maintainer=awonak +sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module +category=Other +license=MIT +url=https://github.com/awonak/libGravity +architectures=avr +depends=uClock,RotaryEncoder,U8g2 \ No newline at end of file diff --git a/analog_input.h b/src/analog_input.h similarity index 100% rename from analog_input.h rename to src/analog_input.h diff --git a/button.h b/src/button.h similarity index 100% rename from button.h rename to src/button.h diff --git a/clock.h b/src/clock.h similarity index 99% rename from clock.h rename to src/clock.h index 4008e0d..613667a 100644 --- a/clock.h +++ b/src/clock.h @@ -15,7 +15,7 @@ #include #include "peripherials.h" -#include "uClock.h" +#include "uClock/uClock.h" // MIDI clock, start, stop, and continue byte definitions - based on MIDI 1.0 Standards. #define MIDI_CLOCK 0xF8 diff --git a/digital_output.h b/src/digital_output.h similarity index 100% rename from digital_output.h rename to src/digital_output.h diff --git a/encoder.h b/src/encoder.h similarity index 100% rename from encoder.h rename to src/encoder.h diff --git a/gravity.cpp b/src/libGravity.cpp similarity index 97% rename from gravity.cpp rename to src/libGravity.cpp index 23d79f8..c4ae4bc 100644 --- a/gravity.cpp +++ b/src/libGravity.cpp @@ -1,5 +1,5 @@ /** - * @file gravity.cpp + * @file libGravity.cpp * @author Adam Wonak (https://github.com/awonak) * @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @version 0.1 @@ -9,7 +9,7 @@ * */ -#include "gravity.h" +#include "libGravity.h" // Initialize the static pointer for the EncoderDir class to null. We want to // have a static pointer to decouple the ISR from the global gravity object. diff --git a/gravity.h b/src/libGravity.h similarity index 98% rename from gravity.h rename to src/libGravity.h index 00539fb..daf5192 100644 --- a/gravity.h +++ b/src/libGravity.h @@ -1,5 +1,5 @@ /** - * @file gravity.h + * @file libGravity.h * @author Adam Wonak (https://github.com/awonak) * @brief Library for building custom scripts for the Sitka Instruments Gravity module. * @version 0.1 diff --git a/peripherials.h b/src/peripherials.h similarity index 100% rename from peripherials.h rename to src/peripherials.h diff --git a/uClock/platforms/avr.h b/src/uClock/platforms/avr.h similarity index 100% rename from uClock/platforms/avr.h rename to src/uClock/platforms/avr.h diff --git a/uClock.cpp b/src/uClock/uClock.cpp similarity index 99% rename from uClock.cpp rename to src/uClock/uClock.cpp index 1003930..94c432e 100755 --- a/uClock.cpp +++ b/src/uClock/uClock.cpp @@ -32,7 +32,7 @@ * DEALINGS IN THE SOFTWARE. */ #include "uClock.h" -#include "uClock/platforms/avr.h" +#include "platforms/avr.h" // // Platform specific timer setup/control diff --git a/uClock.h b/src/uClock/uClock.h similarity index 100% rename from uClock.h rename to src/uClock/uClock.h diff --git a/uClock/uClock.h b/uClock/uClock.h deleted file mode 100755 index d8670b0..0000000 --- a/uClock/uClock.h +++ /dev/null @@ -1,180 +0,0 @@ -/*! - * @file uClock.h - * Project BPM clock generator for Arduino - * @brief A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(RPI2040, Teensy, Seedstudio XIAO M0 and ESP32) - * @version 2.2.1 - * @author Romulo Silva - * @date 10/06/2017 - * @license MIT - (c) 2024 - Romulo Silva - contact@midilab.co - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#ifndef __U_CLOCK_H__ -#define __U_CLOCK_H__ - -#include -#include - -namespace umodular { namespace clock { - -#define MIN_BPM 1 -#define MAX_BPM 400 - -#define PHASE_FACTOR 16 -#define PLL_X 220 - -#define SECS_PER_MIN (60UL) -#define SECS_PER_HOUR (3600UL) -#define SECS_PER_DAY (SECS_PER_HOUR * 24L) - -class uClockClass { - - public: - enum ClockMode { - INTERNAL_CLOCK = 0, - EXTERNAL_CLOCK - }; - - enum ClockState { - PAUSED = 0, - STARTING, - STARTED - }; - - enum PPQNResolution { - PPQN_1 = 1, - PPQN_2 = 2, - PPQN_4 = 4, - PPQN_8 = 8, - PPQN_12 = 12, - PPQN_24 = 24, - PPQN_48 = 48, - PPQN_96 = 96, - PPQN_384 = 384, - PPQN_480 = 480, - PPQN_960 = 960 - }; - - ClockState clock_state; - - uClockClass(); - - void setOnOutputPPQN(void (*callback)(uint32_t tick)) { - onOutputPPQNCallback = callback; - } - - void setOnSync24(void (*callback)(uint32_t tick)) { - onSync24Callback = callback; - } - - void setOnClockStart(void (*callback)()) { - onClockStartCallback = callback; - } - - void setOnClockStop(void (*callback)()) { - onClockStopCallback = callback; - } - - void init(); - void setOutputPPQN(PPQNResolution resolution); - void setInputPPQN(PPQNResolution resolution); - - void handleTimerInt(); - void handleExternalClock(); - void resetCounters(); - - // external class control - void start(); - void stop(); - void pause(); - void setTempo(float bpm); - float getTempo(); - - // for software timer implementation(fallback for no board support) - void run(); - - // external timming control - void setClockMode(ClockMode tempo_mode); - ClockMode getClockMode(); - void clockMe(); - // for smooth slave tempo calculate display you should raise the - // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. - // note: this doesn't impact on sync time, only display time getTempo() - // if you dont want to use it, it is default set it to 1 for memory save - void setExtIntervalBuffer(uint8_t buffer_size); - - // elapsed time support - uint8_t getNumberOfSeconds(uint32_t time); - uint8_t getNumberOfMinutes(uint32_t time); - uint8_t getNumberOfHours(uint32_t time); - uint8_t getNumberOfDays(uint32_t time); - uint32_t getNowTimer(); - uint32_t getPlayTime(); - - uint32_t bpmToMicroSeconds(float bpm); - - private: - float inline freqToBpm(uint32_t freq); - float inline constrainBpm(float bpm); - void calculateReferencedata(); - - void (*onOutputPPQNCallback)(uint32_t tick); - void (*onSync24Callback)(uint32_t tick); - void (*onClockStartCallback)(); - void (*onClockStopCallback)(); - - // clock input/output control - PPQNResolution output_ppqn = PPQN_96; - PPQNResolution input_ppqn = PPQN_24; - // output and internal counters, ticks and references - uint32_t tick; - uint32_t int_clock_tick; - uint8_t mod_clock_counter; - uint16_t mod_clock_ref; - - uint8_t mod_sync24_counter; - uint16_t mod_sync24_ref; - uint32_t sync24_tick; - - // external clock control - volatile uint32_t ext_clock_us; - volatile uint32_t ext_clock_tick; - volatile uint32_t ext_interval; - uint32_t last_interval; - uint32_t sync_interval; - - float tempo; - uint32_t start_timer; - ClockMode clock_mode; - - volatile uint32_t * ext_interval_buffer = nullptr; - uint8_t ext_interval_buffer_size; - uint16_t ext_interval_idx; -}; - -} } // end namespace umodular::clock - -extern umodular::clock::uClockClass uClock; - -extern "C" { - extern volatile uint32_t _millis; -} - -#endif /* __U_CLOCK_H__ */ -- 2.39.5 From d1c8ee16a497566824d579606097e3f2c68c10eb Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 08:35:05 -0700 Subject: [PATCH 42/54] EXT will reset clocks in MIDI clock mode. Add reset behavior for EXT clock input when MIDI clock source is selected. Fixes: https://git.pinkduck.xyz/awonak/libGravity/issues/22 --- firmware/Gravity/Gravity.ino | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 0a033aa..ebc0fe3 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -37,10 +37,12 @@ * Shift - hold and rotate encoder to change current selected output channel. * * EXT: - * External clock input. When Gravity is set to INTERNAL clock mode, this - * input is used to reset clocks. + * External clock input. When Gravity is set to INTERNAL or MIDI clock + * source, this input is used to reset clocks. * * CV1: + * External analog input used to provide modulation to any channel parameter. + * * CV2: * External analog input used to provide modulation to any channel parameter. * @@ -155,13 +157,16 @@ void HandleIntClockTick(uint32_t tick) { } void HandleExtClockTick() { - if (gravity.clock.InternalSource()) { - // Use EXT as Reset when internally clocked. - ResetOutputs(); - gravity.clock.Reset(); - } else { - // Register clock tick. - gravity.clock.Tick(); + switch (app.selected_source) { + case Clock::SOURCE_INTERNAL: + case Clock::SOURCE_EXTERNAL_MIDI: + // Use EXT as Reset when not used for clock source. + ResetOutputs(); + gravity.clock.Reset(); + break; + default: + // Register EXT cv clock tick. + gravity.clock.Tick(); } app.refresh_screen = true; } -- 2.39.5 From dd7217d04ef1a95a0bec565fcded10bd0667b76f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 18:27:24 -0700 Subject: [PATCH 43/54] Fix euclidean hit mod --- firmware/Gravity/channel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index b668f2f..df88785 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -255,7 +255,7 @@ class Channel { int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); pattern.SetSteps(base_euc_steps + step_mod); - int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, pattern.GetSteps()); pattern.SetHits(base_euc_hits + hit_mod); // After all cvmod values are updated, recalculate clock pulse modifiers. -- 2.39.5 From 19473db67e761c7f96a3ff0d4e33e87dfb568ca1 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Thu, 24 Jul 2025 18:38:34 -0700 Subject: [PATCH 44/54] bump version in code --- firmware/Gravity/save_state.cpp | 2 +- library.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 96a1f48..e701c4f 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA2"; // NOTE: This should match the version in the library.properties file. +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA3"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; diff --git a/library.properties b/library.properties index 77fe46f..1390c78 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=libGravity -version=2.0.0beta2 +version=2.0.0beta3 author=Adam Wonak maintainer=awonak sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module -- 2.39.5 From b6402380c0866051623be5c790e2fb74b2a5ffac Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 26 Jul 2025 18:51:18 -0700 Subject: [PATCH 45/54] fixed bug in cv mod of clock multiplication upper range. --- firmware/Gravity/channel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index df88785..114e7fd 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -238,7 +238,7 @@ class Channel { } int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); - cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, 100); + cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, MOD_CHOICE_SIZE - 1); int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); cvmod_probability = constrain(base_probability + prob_mod, 0, 100); -- 2.39.5 From fc17afc9a1dbe5427e587a271716d3e14c56aee6 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 9 Aug 2025 23:57:10 +0000 Subject: [PATCH 46/54] Remove Reset State (#26) This feature is essentially overlapping with loading default save slots. I need the few bytes it affords me. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/26 --- firmware/Gravity/Gravity.ino | 9 --------- firmware/Gravity/display.h | 14 ++------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index ebc0fe3..1944947 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -220,12 +220,6 @@ void HandleEncoderPressed() { InitGravity(app); } } - if (app.selected_param == PARAM_MAIN_RESET_STATE) { - if (app.selected_sub_param == 0) { // Reset - stateManager.reset(app); - InitGravity(app); - } - } if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { if (app.selected_sub_param == 0) { // Erase // Show bootsplash during slow erase operation. @@ -305,9 +299,6 @@ void editMainParameter(int val) { case PARAM_MAIN_LOAD_DATA: updateSelection(app.selected_sub_param, val, StateManager::MAX_SAVE_SLOTS + 1); break; - case PARAM_MAIN_RESET_STATE: - updateSelection(app.selected_sub_param, val, 2); - break; case PARAM_MAIN_FACTORY_RESET: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index 37b900b..d71c801 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -47,7 +47,7 @@ const uint8_t TEXT_FONT[437] U8G2_FONT_SECTION("velvetscreen") PROGMEM = * https://stncrn.github.io/u8g2-unifont-helper/ * "%/0123456789ABCDEFILNORSTUVXx" */ -const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") = +const uint8_t LARGE_FONT[766] U8G2_FONT_SECTION("stk-l") PROGMEM = "\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" @@ -104,7 +104,6 @@ enum ParamsMainPage : uint8_t { PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_SAVE_DATA, PARAM_MAIN_LOAD_DATA, - PARAM_MAIN_RESET_STATE, PARAM_MAIN_FACTORY_RESET, PARAM_MAIN_LAST, }; @@ -297,15 +296,6 @@ void DisplayMainPage() { : F("LOAD FROM SLOT"); } break; - case PARAM_MAIN_RESET_STATE: - if (app.selected_sub_param == 0) { - mainText = F("RST"); - subText = F("RESET ALL"); - } else { - mainText = F("x"); - subText = F("BACK TO MAIN"); - } - break; case PARAM_MAIN_FACTORY_RESET: if (app.selected_sub_param == 0) { mainText = F("DEL"); @@ -321,7 +311,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("RESET"), F("ERASE")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } -- 2.39.5 From 872af30fbcbe8c89d9872b5f6efd254ca963ef5c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 9 Aug 2025 23:59:24 +0000 Subject: [PATCH 47/54] Refactor CV Mod (#24) Move cv mod calculation to processClockTick. This is less ideas because it is an ISR, but it saves a significant amount of memory. Performance doesn't seem to take much of a hit. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/24 --- firmware/Gravity/Gravity.ino | 13 --- firmware/Gravity/channel.h | 190 +++++++++++++------------------- firmware/Gravity/display.h | 16 +-- firmware/Gravity/save_state.cpp | 14 +-- 4 files changed, 95 insertions(+), 138 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 1944947..4b19f2c 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -91,19 +91,6 @@ void loop() { // Process change in state of inputs and outputs. gravity.Process(); - // Read CVs and call the update function for each channel. - int cv1 = gravity.cv1.Read(); - int cv2 = gravity.cv2.Read(); - - for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { - auto& ch = app.channel[i]; - // Only apply CV to the channel when the current channel has cv - // mod configured. - if (ch.isCvModActive()) { - ch.applyCvMod(cv1, cv2); - } - } - // Check for dirty state eligible to be saved. stateManager.update(app); diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 114e7fd..29a9ff3 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -70,14 +70,6 @@ class Channel { base_duty_cycle = 50; base_offset = 0; base_swing = 50; - base_euc_steps = 1; - base_euc_hits = 1; - - cvmod_clock_mod_index = base_clock_mod_index; - cvmod_probability = base_probability; - cvmod_duty_cycle = base_duty_cycle; - cvmod_offset = base_offset; - cvmod_swing = base_swing; cv1_dest = CV_DEST_NONE; cv2_dest = CV_DEST_NONE; @@ -88,78 +80,100 @@ class Channel { _recalculatePulses(); } + bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } + // Setters (Set the BASE value) void setClockMod(int index) { base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); - if (!isCvModActive()) { - cvmod_clock_mod_index = base_clock_mod_index; - _recalculatePulses(); - } } void setProbability(int prob) { base_probability = constrain(prob, 0, 100); - if (!isCvModActive()) { - cvmod_probability = base_probability; - _recalculatePulses(); - } } void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); - if (!isCvModActive()) { - cvmod_duty_cycle = base_duty_cycle; - _recalculatePulses(); - } } void setOffset(int off) { base_offset = constrain(off, 0, 99); - if (!isCvModActive()) { - cvmod_offset = base_offset; - _recalculatePulses(); - } } void setSwing(int val) { base_swing = constrain(val, 50, 95); - if (!isCvModActive()) { - cvmod_swing = base_swing; - _recalculatePulses(); - } } // Euclidean void setSteps(int val) { - base_euc_steps = constrain(val, 1, MAX_PATTERN_LEN); - if (cv1_dest != CV_DEST_EUC_STEPS && cv2_dest != CV_DEST_EUC_STEPS) { - pattern.SetSteps(val); - } + pattern.SetSteps(val); } void setHits(int val) { - base_euc_hits = constrain(val, 1, base_euc_steps); - if (cv1_dest != CV_DEST_EUC_HITS && cv2_dest != CV_DEST_EUC_HITS) { - pattern.SetHits(val); - } + pattern.SetHits(val); } - void setCv1Dest(CvDestination dest) { cv1_dest = dest; } - void setCv2Dest(CvDestination dest) { cv2_dest = dest; } + void setCv1Dest(CvDestination dest) { + cv1_dest = dest; + _recalculatePulses(); + } + void setCv2Dest(CvDestination dest) { + cv2_dest = dest; + _recalculatePulses(); + } CvDestination getCv1Dest() const { return cv1_dest; } CvDestination getCv2Dest() const { return cv2_dest; } // Getters (Get the BASE value for editing or cv modded value for display) + int getProbability() const { return base_probability; } + int getDutyCycle() const { return base_duty_cycle; } + int getOffset() const { return base_offset; } + int getSwing() const { return base_swing; } + int getClockMod() const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex()]); } + int getClockModIndex() const { return base_clock_mod_index; } + byte getSteps() const { return pattern.GetSteps(); } + byte getHits() const { return pattern.GetHits(); } - int getProbability(bool withCvMod = false) const { return withCvMod ? cvmod_probability : base_probability; } - int getDutyCycle(bool withCvMod = false) const { return withCvMod ? cvmod_duty_cycle : base_duty_cycle; } - int getOffset(bool withCvMod = false) const { return withCvMod ? cvmod_offset : base_offset; } - int getSwing(bool withCvMod = false) const { return withCvMod ? cvmod_swing : base_swing; } - int getClockMod(bool withCvMod = false) const { return pgm_read_word_near(&CLOCK_MOD[getClockModIndex(withCvMod)]); } - int getClockModIndex(bool withCvMod = false) const { return withCvMod ? cvmod_clock_mod_index : base_clock_mod_index; } - bool isCvModActive() const { return cv1_dest != CV_DEST_NONE || cv2_dest != CV_DEST_NONE; } + // Getters that calculate the value with CV modulation applied. + int getClockModIndexWithMod(int cv1_val, int cv2_val) { + int clock_mod_index = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); + return constrain(base_clock_mod_index + clock_mod_index, 0, MOD_CHOICE_SIZE - 1); + } - byte getSteps(bool withCvMod = false) const { return withCvMod ? pattern.GetSteps() : base_euc_steps; } - byte getHits(bool withCvMod = false) const { return withCvMod ? pattern.GetHits() : base_euc_hits; } + int getClockModWithMod(int cv1_val, int cv2_val) { + int clock_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); + return pgm_read_word_near(&CLOCK_MOD[getClockModIndexWithMod(cv1_val, cv2_val)]); + } + + int getProbabilityWithMod(int cv1_val, int cv2_val) { + int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); + return constrain(base_probability + prob_mod, 0, 100); + } + + int getDutyCycleWithMod(int cv1_val, int cv2_val) { + int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); + return constrain(base_duty_cycle + duty_mod, 1, 99); + } + + int getOffsetWithMod(int cv1_val, int cv2_val) { + int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); + return constrain(base_offset + offset_mod, 0, 99); + } + + int getSwingWithMod(int cv1_val, int cv2_val) { + int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); + return constrain(base_swing + swing_mod, 50, 95); + } + + byte getStepsWithMod(int cv1_val, int cv2_val) { + int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); + return constrain(pattern.GetSteps() + step_mod, 1, MAX_PATTERN_LEN); + } + + byte getHitsWithMod(int cv1_val, int cv2_val) { + // The number of hits is dependent on the modulated number of steps. + byte modulated_steps = getStepsWithMod(cv1_val, cv2_val); + int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, modulated_steps); + return constrain(pattern.GetHits() + hit_mod, 1, modulated_steps); + } void toggleMute() { mute = !mute; } @@ -176,6 +190,13 @@ class Channel { return; } + if (isCvModActive()) _recalculatePulses(); + + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); + int cvmod_clock_mod_index = getClockModIndexWithMod(cv1, cv2); + int cvmod_probability = getProbabilityWithMod(cv1, cv2); + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); // Conditionally apply swing on down beats. @@ -211,56 +232,6 @@ class Channel { output.Low(); } } - /** - * @brief Calculate and store cv modded values using bipolar mapping. - * Default to base value if not the current CV destination. - * - * @param cv1_val analog input reading for cv1 - * @param cv2_val analog input reading for cv2 - * - */ - void applyCvMod(int cv1_val, int cv2_val) { - // Note: This is optimized for cpu performance. This method is called - // from the main loop and stores the cv mod values. This reduces CPU - // cycles inside the internal clock interrupt, which is preferrable. - // However, if RAM usage grows too much, we have an opportunity to - // refactor this to store just the CV read values, and calculate the - // cv mod value per channel inside the getter methods by passing cv - // values. This would reduce RAM usage, but would introduce a - // significant CPU cost, which may have undesirable performance issues. - if (!isCvModActive()) { - cvmod_clock_mod_index = base_clock_mod_index; - cvmod_probability = base_clock_mod_index; - cvmod_duty_cycle = base_clock_mod_index; - cvmod_offset = base_clock_mod_index; - cvmod_swing = base_clock_mod_index; - return; - } - - int dest_mod = _calculateMod(CV_DEST_MOD, cv1_val, cv2_val, -(MOD_CHOICE_SIZE / 2), MOD_CHOICE_SIZE / 2); - cvmod_clock_mod_index = constrain(base_clock_mod_index + dest_mod, 0, MOD_CHOICE_SIZE - 1); - - int prob_mod = _calculateMod(CV_DEST_PROB, cv1_val, cv2_val, -50, 50); - cvmod_probability = constrain(base_probability + prob_mod, 0, 100); - - int duty_mod = _calculateMod(CV_DEST_DUTY, cv1_val, cv2_val, -50, 50); - cvmod_duty_cycle = constrain(base_duty_cycle + duty_mod, 1, 99); - - int offset_mod = _calculateMod(CV_DEST_OFFSET, cv1_val, cv2_val, -50, 50); - cvmod_offset = constrain(base_offset + offset_mod, 0, 99); - - int swing_mod = _calculateMod(CV_DEST_SWING, cv1_val, cv2_val, -25, 25); - cvmod_swing = constrain(base_swing + swing_mod, 50, 95); - - int step_mod = _calculateMod(CV_DEST_EUC_STEPS, cv1_val, cv2_val, 0, MAX_PATTERN_LEN); - pattern.SetSteps(base_euc_steps + step_mod); - - int hit_mod = _calculateMod(CV_DEST_EUC_HITS, cv1_val, cv2_val, 0, pattern.GetSteps()); - pattern.SetHits(base_euc_hits + hit_mod); - - // After all cvmod values are updated, recalculate clock pulse modifiers. - _recalculatePulses(); - } private: int _calculateMod(CvDestination dest, int cv1_val, int cv2_val, int min_range, int max_range) { @@ -270,13 +241,19 @@ class Channel { } void _recalculatePulses() { - const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[cvmod_clock_mod_index]); - _duty_pulses = max((long)((mod_pulses * (100L - cvmod_duty_cycle)) / 100L), 1L); - _offset_pulses = (long)((mod_pulses * (100L - cvmod_offset)) / 100L); + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); + int clock_mod_index = getClockModIndexWithMod(cv1, cv2); + int duty_cycle = getDutyCycleWithMod(cv1, cv2); + int offset = getOffsetWithMod(cv1, cv2); + int swing = getSwingWithMod(cv1, cv2); + const uint16_t mod_pulses = pgm_read_word_near(&CLOCK_MOD_PULSES[clock_mod_index]); + _duty_pulses = max((long)((mod_pulses * (100L - duty_cycle)) / 100L), 1L); + _offset_pulses = (long)((mod_pulses * (100L - offset)) / 100L); // Calculate the down beat swing amount. - if (cvmod_swing > 50) { - int shifted_swing = cvmod_swing - 50; + if (swing > 50) { + int shifted_swing = swing - 50; _swing_pulse_amount = (long)((mod_pulses * (100L - shifted_swing)) / 100L); } else { _swing_pulse_amount = 0; @@ -289,15 +266,6 @@ class Channel { byte base_duty_cycle; byte base_offset; byte base_swing; - byte base_euc_steps; - byte base_euc_hits; - - // Base value with cv mod applied. - byte cvmod_clock_mod_index; - byte cvmod_probability; - byte cvmod_duty_cycle; - byte cvmod_offset; - byte cvmod_swing; // CV mod configuration CvDestination cv1_dest; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index d71c801..d83de64 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -328,10 +328,12 @@ void DisplayChannelPage() { // When editing a param, just show the base value. When not editing show // the value with cv mod. bool withCvMod = !app.editing_param; + int cv1 = gravity.cv1.Read(); + int cv2 = gravity.cv2.Read(); switch (app.selected_param) { case PARAM_CH_MOD: { - int mod_value = ch.getClockMod(withCvMod); + int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2): ch.getClockMod(); if (mod_value > 1) { mainText = F("/"); mainText += String(mod_value); @@ -344,30 +346,30 @@ void DisplayChannelPage() { break; } case PARAM_CH_PROB: - mainText = String(ch.getProbability(withCvMod)) + F("%"); + mainText = String(withCvMod ? ch.getProbabilityWithMod(cv1, cv2) : ch.getProbability()) + F("%"); subText = F("HIT CHANCE"); break; case PARAM_CH_DUTY: - mainText = String(ch.getDutyCycle(withCvMod)) + F("%"); + mainText = String(withCvMod ? ch.getDutyCycleWithMod(cv1, cv2) : ch.getDutyCycle()) + F("%"); subText = F("PULSE WIDTH"); break; case PARAM_CH_OFFSET: - mainText = String(ch.getOffset(withCvMod)) + F("%"); + mainText = String(withCvMod ? ch.getOffsetWithMod(cv1, cv2) : ch.getOffset()) + F("%"); subText = F("SHIFT HIT"); break; case PARAM_CH_SWING: ch.getSwing() == 50 ? mainText = F("OFF") - : mainText = String(ch.getSwing(withCvMod)) + F("%"); + : mainText = String(withCvMod ? ch.getSwingWithMod(cv1, cv2) : ch.getSwing()) + F("%"); subText = "DOWN BEAT"; swingDivisionMark(); break; case PARAM_CH_EUC_STEPS: - mainText = String(ch.getSteps(withCvMod)); + mainText = String(withCvMod ? ch.getStepsWithMod(cv1, cv2) : ch.getSteps()); subText = "EUCLID STEPS"; break; case PARAM_CH_EUC_HITS: - mainText = String(ch.getHits(withCvMod)); + mainText = String(withCvMod ? ch.getHitsWithMod(cv1, cv2) : ch.getHits()); subText = "EUCLID HITS"; break; case PARAM_CH_CV1_DEST: diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index e701c4f..f3f77c1 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -148,13 +148,13 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { const auto& ch = app.channel[i]; auto& save_ch = save_data.channel_data[i]; - save_ch.base_clock_mod_index = ch.getClockModIndex(false); - save_ch.base_probability = ch.getProbability(false); - save_ch.base_duty_cycle = ch.getDutyCycle(false); - save_ch.base_offset = ch.getOffset(false); - save_ch.base_swing = ch.getSwing(false); - save_ch.base_euc_steps = ch.getSteps(false); - save_ch.base_euc_hits = ch.getHits(false); + save_ch.base_clock_mod_index = ch.getClockModIndex(); + save_ch.base_probability = ch.getProbability(); + save_ch.base_duty_cycle = ch.getDutyCycle(); + save_ch.base_offset = ch.getOffset(); + save_ch.base_swing = ch.getSwing(); + save_ch.base_euc_steps = ch.getSteps(); + save_ch.base_euc_hits = ch.getHits(); save_ch.cv1_dest = static_cast(ch.getCv1Dest()); save_ch.cv2_dest = static_cast(ch.getCv2Dest()); } -- 2.39.5 From 1161da38c1e5e9d09352b52e137f04ecfa4b6453 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 10 Aug 2025 00:25:06 +0000 Subject: [PATCH 48/54] Add menu options for using cv input as Clock Run/Reset (#25) Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/25 --- firmware/Gravity/Gravity.ino | 35 ++++++++++++++++++++++++++++++++- firmware/Gravity/app_state.h | 2 ++ firmware/Gravity/display.h | 34 ++++++++++++++++++++++++++++++-- firmware/Gravity/save_state.cpp | 8 +++++++- firmware/Gravity/save_state.h | 2 ++ src/analog_input.h | 14 +++++++++++++ src/digital_output.h | 1 - 7 files changed, 91 insertions(+), 5 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 4b19f2c..8618f46 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -42,7 +42,7 @@ * * CV1: * External analog input used to provide modulation to any channel parameter. - * + * * CV2: * External analog input used to provide modulation to any channel parameter. * @@ -91,6 +91,9 @@ void loop() { // Process change in state of inputs and outputs. gravity.Process(); + // Check if cv run or reset is active and read cv. + CheckRunReset(gravity.cv1, gravity.cv2); + // Check for dirty state eligible to be saved. stateManager.update(app); @@ -158,6 +161,27 @@ void HandleExtClockTick() { app.refresh_screen = true; } +void CheckRunReset(AnalogInput& cv1, AnalogInput& cv2) { + // Clock Run + if (app.cv_run == 1 || app.cv_run == 2) { + const int val = (app.cv_run == 1) ? cv1.Read() : cv2.Read(); + if (val > AnalogInput::GATE_THRESHOLD && gravity.clock.IsPaused()) { + gravity.clock.Start(); + app.refresh_screen = true; + } else if (val < AnalogInput::GATE_THRESHOLD && !gravity.clock.IsPaused()) { + gravity.clock.Stop(); + ResetOutputs(); + app.refresh_screen = true; + } + } + + // Clock Reset + if ((app.cv_reset == 1 && cv1.IsRisingEdge(AnalogInput::GATE_THRESHOLD)) || + (app.cv_reset == 2 && cv2.IsRisingEdge(AnalogInput::GATE_THRESHOLD))) { + gravity.clock.Reset(); + } +} + // // UI handlers for encoder and buttons. // @@ -263,6 +287,14 @@ void editMainParameter(int val) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); app.tempo = gravity.clock.Tempo(); break; + case PARAM_MAIN_RUN: + updateSelection(app.selected_sub_param, val, 3); + app.cv_run = app.selected_sub_param; + break; + case PARAM_MAIN_RESET: + updateSelection(app.selected_sub_param, val, 3); + app.cv_reset = app.selected_sub_param; + break; case PARAM_MAIN_SOURCE: { byte source = static_cast(app.selected_source); updateSelection(source, val, Clock::SOURCE_LAST); @@ -279,6 +311,7 @@ void editMainParameter(int val) { } break; } + // These changes are applied upon encoder button press. case PARAM_MAIN_ENCODER_DIR: updateSelection(app.selected_sub_param, val, 2); break; diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index 90712df..0f06f02 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -25,6 +25,8 @@ struct AppState { byte selected_channel = 0; // 0=tempo, 1-6=output channel byte selected_swing = 0; byte selected_save_slot = 0; // The currently active save slot. + byte cv_run = 0; + byte cv_reset = 0; Clock::Source selected_source = Clock::SOURCE_INTERNAL; Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; bool editing_param = false; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index d83de64..d5a5f6e 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -100,6 +100,8 @@ constexpr uint8_t CHANNEL_BOX_HEIGHT = 14; enum ParamsMainPage : uint8_t { PARAM_MAIN_TEMPO, PARAM_MAIN_SOURCE, + PARAM_MAIN_RUN, + PARAM_MAIN_RESET, PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, PARAM_MAIN_SAVE_DATA, @@ -259,6 +261,34 @@ void DisplayMainPage() { break; } break; + case PARAM_MAIN_RUN: + mainText = F("RUN"); + switch (app.cv_run) { + case 0: + subText = F("NONE"); + break; + case 1: + subText = F("CV 1"); + break; + case 2: + subText = F("CV 2"); + break; + } + break; + case PARAM_MAIN_RESET: + mainText = F("RST"); + switch (app.cv_reset) { + case 0: + subText = F("NONE"); + break; + case 1: + subText = F("CV 1"); + break; + case 2: + subText = F("CV 2"); + break; + } + break; case PARAM_MAIN_PULSE: mainText = F("OUT"); switch (app.selected_pulse) { @@ -311,7 +341,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -457,7 +487,7 @@ void UpdateDisplay() { DisplayChannelPage(); } // Global channel select UI. - DisplaySelectedChannel(); + DisplaySelectedChannel(); } while (gravity.display.nextPage()); } diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index f3f77c1..4950d7e 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA3"; // NOTE: This should match the version in the library.properties file. +const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA4"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; @@ -87,6 +87,8 @@ void StateManager::reset(AppState& app) { app.selected_channel = default_app.selected_channel; app.selected_source = default_app.selected_source; app.selected_pulse = default_app.selected_pulse; + app.cv_run = default_app.cv_run; + app.cv_reset = default_app.cv_reset; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { app.channel[i].Init(); @@ -140,6 +142,8 @@ void StateManager::_saveState(const AppState& app, byte slot_index) { save_data.selected_channel = app.selected_channel; save_data.selected_source = static_cast(app.selected_source); save_data.selected_pulse = static_cast(app.selected_pulse); + save_data.cv_run = app.cv_run; + save_data.cv_reset = app.cv_reset; // TODO: break this out into a separate function. Save State should be // broken out into global / per-channel save methods. When saving via @@ -179,6 +183,8 @@ void StateManager::_loadState(AppState& app, byte slot_index) { app.selected_channel = load_data.selected_channel; app.selected_source = static_cast(load_data.selected_source); app.selected_pulse = static_cast(load_data.selected_pulse); + app.cv_run = load_data.cv_run; + app.cv_reset = load_data.cv_reset; for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { auto& ch = app.channel[i]; diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 8f25dd1..34bfffe 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -76,6 +76,8 @@ class StateManager { byte selected_channel; byte selected_source; byte selected_pulse; + byte cv_run; + byte cv_reset; ChannelState channel_data[Gravity::OUTPUT_COUNT]; }; diff --git a/src/analog_input.h b/src/analog_input.h index 496899b..2be670a 100644 --- a/src/analog_input.h +++ b/src/analog_input.h @@ -19,6 +19,8 @@ const int CALIBRATED_HIGH = 512; class AnalogInput { public: + static const int GATE_THRESHOLD = 0; + AnalogInput() {} ~AnalogInput() {} @@ -74,6 +76,18 @@ class AnalogInput { */ inline float Voltage() { return ((read_ / 512.0) * 5.0); } + /** + * Checks for a rising edge transition across a threshold. + * + * @param threshold The value that the input must cross. + * @return True if the value just crossed the threshold from below, false otherwise. + */ + inline bool IsRisingEdge(int16_t threshold) const { + bool was_high = old_read_ > threshold; + bool is_high = read_ > threshold; + return is_high && !was_high; + } + private: uint8_t pin_; int16_t read_; diff --git a/src/digital_output.h b/src/digital_output.h index 9c4cfc8..ccb6f04 100644 --- a/src/digital_output.h +++ b/src/digital_output.h @@ -82,7 +82,6 @@ class DigitalOutput { unsigned long last_triggered_; uint8_t trigger_duration_; uint8_t cv_pin_; - uint8_t led_pin_; bool on_; void update(uint8_t state) { -- 2.39.5 From 7c0262840385bb517a37b57fa117245e74b29c00 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 10 Aug 2025 00:26:20 +0000 Subject: [PATCH 49/54] Add more EXT clock source options (#23) Fixes https://github.com/awonak/alt-gravity/issues/12 Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/23 --- firmware/Gravity/display.h | 6 ++++++ src/clock.h | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index d5a5f6e..faeb4d1 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -256,6 +256,12 @@ void DisplayMainPage() { case Clock::SOURCE_EXTERNAL_PPQN_4: subText = F("4 PPQN"); break; + case Clock::SOURCE_EXTERNAL_PPQN_2: + subText = F("2 PPQN"); + break; + case Clock::SOURCE_EXTERNAL_PPQN_1: + subText = F("1 PPQN"); + break; case Clock::SOURCE_EXTERNAL_MIDI: subText = F("MIDI"); break; diff --git a/src/clock.h b/src/clock.h index 613667a..ea39214 100644 --- a/src/clock.h +++ b/src/clock.h @@ -35,6 +35,8 @@ class Clock { SOURCE_INTERNAL, SOURCE_EXTERNAL_PPQN_24, SOURCE_EXTERNAL_PPQN_4, + SOURCE_EXTERNAL_PPQN_2, + SOURCE_EXTERNAL_PPQN_1, SOURCE_EXTERNAL_MIDI, SOURCE_LAST, }; @@ -96,6 +98,14 @@ class Clock { uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_4); break; + case SOURCE_EXTERNAL_PPQN_2: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_2); + break; + case SOURCE_EXTERNAL_PPQN_1: + uClock.setClockMode(uClock.EXTERNAL_CLOCK); + uClock.setInputPPQN(uClock.PPQN_1); + break; case SOURCE_EXTERNAL_MIDI: uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_24); -- 2.39.5 From c5965aa1f7aeaa830f55c3f824f4f43d10b8f36c Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sat, 9 Aug 2025 18:45:21 -0700 Subject: [PATCH 50/54] bug fix - need to recalculate pulses when mod duty and swing are changed. --- firmware/Gravity/channel.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firmware/Gravity/channel.h b/firmware/Gravity/channel.h index 29a9ff3..fd97537 100644 --- a/firmware/Gravity/channel.h +++ b/firmware/Gravity/channel.h @@ -86,6 +86,7 @@ class Channel { void setClockMod(int index) { base_clock_mod_index = constrain(index, 0, MOD_CHOICE_SIZE - 1); + _recalculatePulses(); } void setProbability(int prob) { @@ -94,13 +95,16 @@ class Channel { void setDutyCycle(int duty) { base_duty_cycle = constrain(duty, 1, 99); + _recalculatePulses(); } void setOffset(int off) { base_offset = constrain(off, 0, 99); + _recalculatePulses(); } void setSwing(int val) { base_swing = constrain(val, 50, 95); + _recalculatePulses(); } // Euclidean -- 2.39.5 From 6ada2aba30fef81a4b182737280208e8be2ad742 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Sun, 10 Aug 2025 02:47:59 +0000 Subject: [PATCH 51/54] Add option to rotate the display (#27) I needed to cut the bootsplash to make room for adding this features. Reviewed-on: https://git.pinkduck.xyz/awonak/libGravity/pulls/27 --- firmware/Gravity/Gravity.ino | 59 +++++++++++++++++---------------- firmware/Gravity/app_state.h | 1 + firmware/Gravity/display.h | 30 ++++------------- firmware/Gravity/save_state.cpp | 3 +- firmware/Gravity/save_state.h | 5 +-- library.properties | 2 +- src/clock.h | 9 ++--- 7 files changed, 46 insertions(+), 63 deletions(-) diff --git a/firmware/Gravity/Gravity.ino b/firmware/Gravity/Gravity.ino index 8618f46..aef4ab8 100644 --- a/firmware/Gravity/Gravity.ino +++ b/firmware/Gravity/Gravity.ino @@ -66,10 +66,6 @@ void setup() { // Start Gravity. gravity.Init(); - // Show bootsplash when initializing firmware. - Bootsplash(); - delay(2000); - // Initialize the state manager. This will load settings from EEPROM stateManager.initialize(app); InitGravity(app); @@ -213,31 +209,34 @@ void HandleEncoderPressed() { // Check if leaving editing mode should apply a selection. if (app.editing_param) { if (app.selected_channel == 0) { // main page - // TODO: rewrite as switch - if (app.selected_param == PARAM_MAIN_ENCODER_DIR) { - app.encoder_reversed = app.selected_sub_param == 1; - gravity.encoder.SetReverseDirection(app.encoder_reversed); - } - if (app.selected_param == PARAM_MAIN_SAVE_DATA) { - if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { - app.selected_save_slot = app.selected_sub_param; - stateManager.saveData(app); - } - } - if (app.selected_param == PARAM_MAIN_LOAD_DATA) { - if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { - app.selected_save_slot = app.selected_sub_param; - stateManager.loadData(app, app.selected_save_slot); - InitGravity(app); - } - } - if (app.selected_param == PARAM_MAIN_FACTORY_RESET) { - if (app.selected_sub_param == 0) { // Erase - // Show bootsplash during slow erase operation. - Bootsplash(); - stateManager.factoryReset(app); - InitGravity(app); - } + switch (app.selected_param) { + case PARAM_MAIN_ENCODER_DIR: + app.encoder_reversed = app.selected_sub_param == 1; + gravity.encoder.SetReverseDirection(app.encoder_reversed); + break; + case PARAM_MAIN_ROTATE_DISP: + app.rotate_display = app.selected_sub_param == 1; + gravity.display.setFlipMode(app.rotate_display ? 1 : 0); + break; + case PARAM_MAIN_SAVE_DATA: + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { + app.selected_save_slot = app.selected_sub_param; + stateManager.saveData(app); + } + break; + case PARAM_MAIN_LOAD_DATA: + if (app.selected_sub_param < StateManager::MAX_SAVE_SLOTS) { + app.selected_save_slot = app.selected_sub_param; + stateManager.loadData(app, app.selected_save_slot); + InitGravity(app); + } + break; + case PARAM_MAIN_FACTORY_RESET: + if (app.selected_sub_param == 0) { // Erase + stateManager.factoryReset(app); + InitGravity(app); + } + break; } } // Only mark dirty and reset selected_sub_param when leaving editing mode. @@ -313,6 +312,7 @@ void editMainParameter(int val) { } // These changes are applied upon encoder button press. case PARAM_MAIN_ENCODER_DIR: + case PARAM_MAIN_ROTATE_DISP: updateSelection(app.selected_sub_param, val, 2); break; case PARAM_MAIN_SAVE_DATA: @@ -381,6 +381,7 @@ void InitGravity(AppState& app) { gravity.clock.SetTempo(app.tempo); gravity.clock.SetSource(app.selected_source); gravity.encoder.SetReverseDirection(app.encoder_reversed); + gravity.display.setFlipMode(app.rotate_display ? 1 : 0); } void ResetOutputs() { diff --git a/firmware/Gravity/app_state.h b/firmware/Gravity/app_state.h index 0f06f02..c02f8a3 100644 --- a/firmware/Gravity/app_state.h +++ b/firmware/Gravity/app_state.h @@ -31,6 +31,7 @@ struct AppState { Clock::Pulse selected_pulse = Clock::PULSE_PPQN_24; bool editing_param = false; bool encoder_reversed = false; + bool rotate_display = false; bool refresh_screen = true; }; diff --git a/firmware/Gravity/display.h b/firmware/Gravity/display.h index faeb4d1..7c4abfa 100644 --- a/firmware/Gravity/display.h +++ b/firmware/Gravity/display.h @@ -104,6 +104,7 @@ enum ParamsMainPage : uint8_t { PARAM_MAIN_RESET, PARAM_MAIN_PULSE, PARAM_MAIN_ENCODER_DIR, + PARAM_MAIN_ROTATE_DISP, PARAM_MAIN_SAVE_DATA, PARAM_MAIN_LOAD_DATA, PARAM_MAIN_FACTORY_RESET, @@ -256,9 +257,6 @@ void DisplayMainPage() { case Clock::SOURCE_EXTERNAL_PPQN_4: subText = F("4 PPQN"); break; - case Clock::SOURCE_EXTERNAL_PPQN_2: - subText = F("2 PPQN"); - break; case Clock::SOURCE_EXTERNAL_PPQN_1: subText = F("1 PPQN"); break; @@ -316,6 +314,10 @@ void DisplayMainPage() { mainText = F("DIR"); subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("REVERSED"); break; + case PARAM_MAIN_ROTATE_DISP: + mainText = F("ROT"); + subText = app.selected_sub_param == 0 ? F("DEFAULT") : F("FLIPPED"); + break; case PARAM_MAIN_SAVE_DATA: case PARAM_MAIN_LOAD_DATA: if (app.selected_sub_param == StateManager::MAX_SAVE_SLOTS) { @@ -347,7 +349,7 @@ void DisplayMainPage() { drawCenteredText(subText.c_str(), SUB_TEXT_Y, TEXT_FONT); // Draw Main Page menu items - String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("SAVE"), F("LOAD"), F("ERASE")}; + String menu_items[PARAM_MAIN_LAST] = {F("TEMPO"), F("SOURCE"), F("CLK RUN"), F("CLK RESET"), F("PULSE OUT"), F("ENCODER DIR"), F("ROTATE DISP"), F("SAVE"), F("LOAD"), F("ERASE")}; drawMenuItems(menu_items, PARAM_MAIN_LAST); } @@ -369,7 +371,7 @@ void DisplayChannelPage() { switch (app.selected_param) { case PARAM_CH_MOD: { - int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2): ch.getClockMod(); + int mod_value = withCvMod ? ch.getClockModWithMod(cv1, cv2) : ch.getClockMod(); if (mod_value > 1) { mainText = F("/"); mainText += String(mod_value); @@ -497,22 +499,4 @@ void UpdateDisplay() { } while (gravity.display.nextPage()); } -void Bootsplash() { - gravity.display.firstPage(); - do { - int textWidth; - String loadingText = F("LOADING...."); - gravity.display.setFont(TEXT_FONT); - - textWidth = gravity.display.getStrWidth(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, StateManager::SEMANTIC_VERSION); - - textWidth = gravity.display.getStrWidth(loadingText.c_str()); - gravity.display.drawStr(26 + (textWidth / 2), 44, loadingText.c_str()); - } while (gravity.display.nextPage()); -} - #endif // DISPLAY_H diff --git a/firmware/Gravity/save_state.cpp b/firmware/Gravity/save_state.cpp index 4950d7e..ff00005 100644 --- a/firmware/Gravity/save_state.cpp +++ b/firmware/Gravity/save_state.cpp @@ -17,7 +17,7 @@ // Define the constants for the current firmware. const char StateManager::SKETCH_NAME[] = "ALT GRAVITY"; -const char StateManager::SEMANTIC_VERSION[] = "V2.0.0BETA4"; // NOTE: This should match the version in the library.properties file. +const char StateManager::SEMANTIC_VERSION[] = "2.0.0"; // NOTE: This should match the version in the library.properties file. // Number of available save slots. const byte StateManager::MAX_SAVE_SLOTS = 10; @@ -212,6 +212,7 @@ void StateManager::_saveMetadata(const AppState& app) { // Global user settings current_meta.selected_save_slot = app.selected_save_slot; current_meta.encoder_reversed = app.encoder_reversed; + current_meta.rotate_display = app.rotate_display; EEPROM.put(METADATA_START_ADDR, current_meta); interrupts(); diff --git a/firmware/Gravity/save_state.h b/firmware/Gravity/save_state.h index 34bfffe..29226e7 100644 --- a/firmware/Gravity/save_state.h +++ b/firmware/Gravity/save_state.h @@ -52,11 +52,12 @@ class StateManager { // This struct holds the data that identifies the firmware version. struct Metadata { - char sketch_name[16]; - char version[16]; + char sketch_name[12]; + char version[5]; // Additional global/hardware settings byte selected_save_slot; bool encoder_reversed; + bool rotate_display; }; struct ChannelState { byte base_clock_mod_index; diff --git a/library.properties b/library.properties index 1390c78..bce41dd 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=libGravity -version=2.0.0beta3 +version=2.0.0 author=Adam Wonak maintainer=awonak sentence=Hardware abstraction library for Sitka Instruments Gravity eurorack module diff --git a/src/clock.h b/src/clock.h index ea39214..9bc695c 100644 --- a/src/clock.h +++ b/src/clock.h @@ -35,7 +35,6 @@ class Clock { SOURCE_INTERNAL, SOURCE_EXTERNAL_PPQN_24, SOURCE_EXTERNAL_PPQN_4, - SOURCE_EXTERNAL_PPQN_2, SOURCE_EXTERNAL_PPQN_1, SOURCE_EXTERNAL_MIDI, SOURCE_LAST, @@ -43,9 +42,9 @@ class Clock { enum Pulse { PULSE_NONE, - PULSE_PPQN_1, - PULSE_PPQN_4, PULSE_PPQN_24, + PULSE_PPQN_4, + PULSE_PPQN_1, PULSE_LAST, }; @@ -98,10 +97,6 @@ class Clock { uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_4); break; - case SOURCE_EXTERNAL_PPQN_2: - uClock.setClockMode(uClock.EXTERNAL_CLOCK); - uClock.setInputPPQN(uClock.PPQN_2); - break; case SOURCE_EXTERNAL_PPQN_1: uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setInputPPQN(uClock.PPQN_1); -- 2.39.5 From 4bcd6180733a84f4d9a108ec6e76af38710e00a7 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:18:45 -0700 Subject: [PATCH 52/54] Add skeleton app to examples --- examples/skeleton/skeleton.ino | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 examples/skeleton/skeleton.ino diff --git a/examples/skeleton/skeleton.ino b/examples/skeleton/skeleton.ino new file mode 100644 index 0000000..eb07186 --- /dev/null +++ b/examples/skeleton/skeleton.ino @@ -0,0 +1,118 @@ +/** + * @file skeleton.ino + * @author YOUR_NAME () + * @brief Skeleton app for Sitka Instruments Gravity. + * @version v1.0.0 - August 2025 + * @date 2025-08-12 + * + * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com + * + * Skeleton app for basic structure of a new firmware for Sitka Instruments + * Gravity using the libGravity library. + * + * ENCODER: + * Press: change between selecting a parameter and editing the parameter. + * Hold & Rotate: change current selected output channel. + * + * BTN1: + * Play/pause - start or stop the internal clock. + * + * BTN2: + * Shift - hold and rotate encoder to change current selected output channel. + * + * EXT: + * External clock input. When Gravity is set to INTERNAL or MIDI clock + * source, this input is used to reset clocks. + * + * CV1: + * External analog input used to provide modulation to any channel parameter. + * + * CV2: + * External analog input used to provide modulation to any channel parameter. + * + */ + +#include + + + +// Global state for settings and app behavior. +struct AppState { + int tempo = Clock::DEFAULT_TEMPO; + Clock::Source selected_source = Clock::SOURCE_INTERNAL; + // Add app specific state variables here. +}; + +AppState app; + +// +// Arduino setup and loop. +// + +void setup() { + // Start Gravity. + gravity.Init(); + + // Clock handlers. + gravity.clock.AttachIntHandler(HandleIntClockTick); + gravity.clock.AttachExtHandler(HandleExtClockTick); + + // Encoder rotate and press handlers. + gravity.encoder.AttachPressHandler(HandleEncoderPressed); + gravity.encoder.AttachRotateHandler(HandleRotate); + gravity.encoder.AttachPressRotateHandler(HandlePressedRotate); + + // Button press handlers. + gravity.play_button.AttachPressHandler(HandlePlayPressed); +} + +void loop() { + // Process change in state of inputs and outputs. + gravity.Process(); + + // Non-ISR loop behavior. +} + +// +// Firmware handlers for clocks. +// + +void HandleIntClockTick(uint32_t tick) { + bool refresh = false; + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { + // Process each output tick handlers. + } +} + +void HandleExtClockTick() { + switch (app.selected_source) { + case Clock::SOURCE_INTERNAL: + case Clock::SOURCE_EXTERNAL_MIDI: + // Use EXT as Reset when not used for clock source. + gravity.clock.Reset(); + break; + default: + // Register EXT cv clock tick. + gravity.clock.Tick(); + } +} + +// +// UI handlers for encoder and buttons. +// + +void HandlePlayPressed() { +} + +void HandleEncoderPressed() { +} + +void HandleRotate(int val) { +} + +void HandlePressedRotate(int val) { +} + +// +// Application logic goes here. +// -- 2.39.5 From b5029bde888ef0b5d31a81bfbe85785b262bdb2f Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:19:06 -0700 Subject: [PATCH 53/54] add skeleton app to examples --- examples/skeleton/skeleton.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/skeleton/skeleton.ino b/examples/skeleton/skeleton.ino index eb07186..a71b878 100644 --- a/examples/skeleton/skeleton.ino +++ b/examples/skeleton/skeleton.ino @@ -2,8 +2,8 @@ * @file skeleton.ino * @author YOUR_NAME () * @brief Skeleton app for Sitka Instruments Gravity. - * @version v1.0.0 - August 2025 - * @date 2025-08-12 + * @version vX.Y.Z - MONTH YEAR YOUR_NAME + * @date YYYY-MM-DD * * @copyright MIT - (c) 2025 - Adam Wonak - adam.wonak@gmail.com * -- 2.39.5 From 3f670fa9f78b69327ce6b37abf4c8d2ae76267d7 Mon Sep 17 00:00:00 2001 From: Adam Wonak Date: Wed, 13 Aug 2025 07:42:02 -0700 Subject: [PATCH 54/54] Update docs and example firmware --- README.md | 19 +++++++++++++++---- .../calibrate_analog/calibrate_analog.ino | 4 ++-- .../calibrate_analog2/calibrate_analog2.ino | 4 ++-- examples/hardware_test/hardware_test.ino | 16 ++++++++-------- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 569aeed..7afaabf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Sitka Instruments Gravity Firmware Abstraction -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. +This library helps make writing firmware for the [Sitka Instruments Gravity](https://sitkainstruments.com/gravity/) eurorack module 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. + +The latest releases of all Sitka Instruments Gravity firmware builds can be found on the [Updater](https://sitkainstruments.com/gravity/updater/) page. You can use this page to flash the latest build directly to the Arduino Nano on the back of your module. ## Installation @@ -17,13 +19,14 @@ Common directory locations: * [uClock](https://github.com/midilab/uClock) [MIT] - (Included with this repo) Handle clock tempo, external clock input, and internal clock timer handler. * [RotateEncoder](https://github.com/mathertel/RotaryEncoder) [BSD] - Library for reading and interpreting encoder rotation. * [U8g2](https://github.com/olikraus/u8g2/) [MIT] - Graphics helper library. +* [NeoHWSerial](https://github.com/SlashDevin/NeoHWSerial) [GPL] - Hardware serial library with attachInterrupt. ## Example Here's a trivial example showing some of the ways to interact with the library. This script rotates the active clock channel according to the set tempo. The encoder can change the temo or rotation direction. The play/pause button will toggle the clock activity on or off. The shift button will freeze the clock from advancing the channel rotation. ```cpp -#include "gravity.h" +#include "libGravity.h" byte idx = 0; bool reversed = false; @@ -75,11 +78,11 @@ void HandlePlayPressed() { } } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (selected_param == 0) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); } else if (selected_param == 1) { - reversed = (dir == DIRECTION_DECREMENT); + reversed = (val < 0); } } @@ -111,6 +114,14 @@ void UpdateDisplay() { } ``` +**Builing New Firmware Using libGravity** + +When starting a new firmware sketch you can use the [skeleton](examples/skeleton/skeleton.ino) app as a place to start. + +**Building New Firmware from scratch** + +If you do not want to use the libGravity hardware abstraction library and want to roll your own vanilla firmware, take a look at the [peripherials.h](src/peripherials.h) file for the pinout definitions used by the module. + ### Build for release ``` diff --git a/examples/calibrate_analog/calibrate_analog.ino b/examples/calibrate_analog/calibrate_analog.ino index c3e6a5f..388494f 100644 --- a/examples/calibrate_analog/calibrate_analog.ino +++ b/examples/calibrate_analog/calibrate_analog.ino @@ -17,7 +17,7 @@ * TODO: Store the calibration value in EEPROM. */ -#include "gravity.h" +#include "libGravity.h" #define TEXT_FONT u8g2_font_profont11_tf #define INDICATOR_FONT u8g2_font_open_iconic_arrow_1x_t @@ -43,7 +43,7 @@ void NextCalibrationPoint() { selected_param = (selected_param + 1) % 6; } -void CalibrateCV(Direction dir, int val) { +void CalibrateCV(int val) { AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; switch (selected_param % 3) { case 0: diff --git a/examples/calibrate_analog2/calibrate_analog2.ino b/examples/calibrate_analog2/calibrate_analog2.ino index ac5d772..48958d2 100644 --- a/examples/calibrate_analog2/calibrate_analog2.ino +++ b/examples/calibrate_analog2/calibrate_analog2.ino @@ -14,7 +14,7 @@ * */ -#include "gravity.h" +#include "libGravity.h" #define TEXT_FONT u8g2_font_profont11_tf @@ -39,7 +39,7 @@ void NextCalibrationPoint() { selected_param = (selected_param + 1) % 2; } -void CalibrateCV(Direction dir, int val) { +void CalibrateCV(int val) { // AnalogInput* cv = (selected_param > 2) ? &gravity.cv2 : &gravity.cv1; AnalogInput* cv = &gravity.cv1; switch (selected_param % 2) { diff --git a/examples/hardware_test/hardware_test.ino b/examples/hardware_test/hardware_test.ino index 5f3d345..9ba6b90 100644 --- a/examples/hardware_test/hardware_test.ino +++ b/examples/hardware_test/hardware_test.ino @@ -1,4 +1,4 @@ -#include "gravity.h" +#include "libGravity.h" byte idx = 0; bool reversed = false; @@ -33,28 +33,28 @@ void IntClock(uint32_t tick) { if (tick % 12 == 0 && ! freeze) { gravity.outputs[idx].Low(); if (reversed) { - idx = (idx == 0) ? OUTPUT_COUNT - 1 : idx - 1; + idx = (idx == 0) ? Gravity::OUTPUT_COUNT - 1 : idx - 1; } else { - idx = (idx + 1) % OUTPUT_COUNT; + idx = (idx + 1) % Gravity::OUTPUT_COUNT; } gravity.outputs[idx].High(); } } void HandlePlayPressed() { - gravity.clock.Pause(); + gravity.clock.Stop(); if (gravity.clock.IsPaused()) { - for (int i = 0; i < OUTPUT_COUNT; i++) { + for (int i = 0; i < Gravity::OUTPUT_COUNT; i++) { gravity.outputs[i].Low(); } } } -void HandleRotate(Direction dir, int val) { +void HandleRotate(int val) { if (selected_param == 0) { gravity.clock.SetTempo(gravity.clock.Tempo() + val); } else if (selected_param == 1) { - reversed = (dir == DIRECTION_DECREMENT); + reversed = (val < 0); } } @@ -80,7 +80,7 @@ void UpdateDisplay() { gravity.display.print("Direction: "); gravity.display.print((reversed) ? "Backward" : "Forward"); - gravity.display.drawChar(0, selected_param * 10, 0x10, 1, 0, 1); + gravity.display.drawStr(0, selected_param * 10, "x"); gravity.display.display(); } \ No newline at end of file -- 2.39.5