diff --git a/examples/Gravity/Gravity.ino b/examples/Gravity/Gravity.ino index 8b2234c..61da3f8 100644 --- a/examples/Gravity/Gravity.ino +++ b/examples/Gravity/Gravity.ino @@ -213,6 +213,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: { 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 bb9f662..efedc2a 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..d0b5d28 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 { @@ -41,6 +42,7 @@ class Channel { base_duty_cycle = 50; base_offset = 0; base_swing = 50; + cv_source = CV_NONE; cv_destination = CV_DEST_NONE; @@ -49,6 +51,8 @@ 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) @@ -75,6 +79,13 @@ class Channel { CvDestination getCvDestination() { return cv_destination; } bool isCvModActive() const { return cv_source != CV_NONE && cv_destination != CV_DEST_NONE; } + // Euclidean + void setSteps(byte val) { pattern.SetSteps(val); } + void setHits(byte val) { pattern.SetHits(val); } + + byte getSteps() { pattern.GetSteps(); } + byte getHits() { 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 +106,26 @@ 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); + if (pattern.IsActive()) { + // Euclidean rhythm check + switch (pattern.NextStep()) { + case Pattern::REST: // Rest when active or fall back to probability + hit = pattern.IsActive() ? false : hit; + break; + case Pattern::HIT: // Hit if probability is true + hit &= true; + break; + case Pattern::PADDING: // Padding returns only when active, always rest) + hit = false; + break; + } + } + if (hit) { output.High(); } } @@ -173,6 +199,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 e1211bc..382d34e 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: { switch (ch.getCvSource()) { mainText = F("SRC"); @@ -315,7 +323,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("EUC STEPS"), + F("EUC 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..647e3eb --- /dev/null +++ b/examples/Gravity/euclidean.h @@ -0,0 +1,111 @@ +#ifndef EUCLIDEAN_H +#define EUCLIDEAN_H + +#define MAX_PATTERN_LEN 16 + +struct PatternState { + uint8_t steps; + uint8_t hits; +}; + +const PatternState DEFAULT_PATTERN = {16, 4}; + +class Pattern { + public: + Pattern() {} + ~Pattern() {} + + enum Step { + HIT, + REST, + PADDING, + }; + + void Init(PatternState state) { + steps_ = constrain(state.steps, 0, MAX_PATTERN_LEN); + hits_ = constrain(state.hits, 1, steps_); + updatePattern(); + } + + PatternState GetState() { return {steps_, hits_}; } + + // Get the current step value and advance the euclidean rhythm step index + // to the next step in the pattern. + Step NextStep() { + byte padding_ = 0; + if (steps_ == 0) return REST; + + Step value = GetCurrentStep(current_step_); + current_step_ = + (current_step_ < steps_ + padding_ - 1) ? current_step_ + 1 : 0; + return value; + } + + Step GetCurrentStep(byte i) { return pattern_[i]; } + + void SetSteps(byte steps) { + steps_ = constrain(steps, 0, MAX_PATTERN_LEN); + hits_ = min(hits_, steps_); + updatePattern(); + } + + void SetHits(byte hits) { + hits_ = constrain(hits, 0, steps_); + } + + // void ChangeOffset(byte val) { + // offset_ = constrain(offset_ + val, 0, (steps_ + padding_)); + // updatePattern(); + // } + + // void ChangePadding(byte val) { + // if (val == 1 && padding_ + steps_ < MAX_PATTERN_LEN) { + // padding_++; + // updatePattern(); + // } else if (val == -1 && padding_ > 0) { + // padding_--; + // offset_ = min(offset_, (padding_ + steps_) - 1); + // updatePattern(); + // } + // } + + void Reset() { current_step_ = 0; } + bool IsActive() { return steps_ != 0 && hits_ != 0; } + + inline uint8_t GetSteps() { return steps_; } + inline uint8_t GetHits() { return hits_; } + inline uint8_t GetStepIndex() { return current_step_; } + + private: + uint8_t steps_ = 0; + uint8_t hits_ = 0; + volatile uint8_t current_step_ = 0; + Step pattern_[MAX_PATTERN_LEN]; + + // Update the euclidean rhythm pattern when attributes change. + void updatePattern() { + // Fill current pattern with "padding" steps, then overwrite with hits + // and rests. + for (int i = 0; i < MAX_PATTERN_LEN; i++) { + pattern_[i] = PADDING; + } + + // Populate the euclidean rhythm pattern according to the current + // instance variables. + byte bucket = 0; + byte offset_ = 0; // temp disable + byte padding_ = 0; // temp disable + pattern_[offset_] = (hits_ > 0) ? HIT : REST; + for (int i = 1; i < steps_; i++) { + bucket += hits_; + if (bucket >= steps_) { + bucket -= steps_; + pattern_[(i + offset_) % (steps_ + padding_)] = HIT; + } else { + pattern_[(i + offset_) % (steps_ + padding_)] = REST; + } + } + } +}; + +#endif \ No newline at end of file diff --git a/examples/Gravity/save_state.h b/examples/Gravity/save_state.h index eb3c7a2..7ca9df5 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;