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()); }