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 {