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 4e0f7dd..e1211bc 100644 --- a/examples/Gravity/display.h +++ b/examples/Gravity/display.h @@ -147,6 +147,24 @@ void drawMenuItems(String 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() { mainText = String(ch.getOffset(withCvMod)) + F("%"); subText = F("SHIFT HIT"); break; + case PARAM_CH_SWING: + ch.getSwing() == 50 + ? 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"); @@ -277,6 +302,9 @@ void DisplayChannelPage() { case CV_DEST_OFFSET: subText = F("OFFSET"); break; + case CV_DEST_SWING: + subText = F("SWING"); + break; } break; } @@ -287,7 +315,7 @@ void DisplayChannelPage() { // Draw Channel Page menu items String menu_items[PARAM_CH_LAST] = { - F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("CV SOURCE"), F("CV DEST")}; + F("MOD"), F("PROBABILITY"), F("DUTY"), F("OFFSET"), F("SWING"), 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 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 };