From 3e210a08146ae2318f895a372ffa97d86fd6bb91 Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Mon, 16 Mar 2026 21:49:45 -0400 Subject: [PATCH] semi working clk in --- include/Gate.h | 1 + include/globals.h | 5 +- src/Gate.cpp | 232 +++++++++++++++++++++++----------------------- src/main.cpp | 97 +++++++++++-------- 4 files changed, 177 insertions(+), 158 deletions(-) diff --git a/include/Gate.h b/include/Gate.h index 3bbbebd..71e56cb 100644 --- a/include/Gate.h +++ b/include/Gate.h @@ -60,6 +60,7 @@ public: WaveShape shape = SQUARE; uint32_t startTick = 0; + uint32_t stopTick = 0; uint32_t pulseWidthTicks = 0; bool sticky = false; int8_t modifierSelectionIndex; diff --git a/include/globals.h b/include/globals.h index dd9fe71..0d85057 100644 --- a/include/globals.h +++ b/include/globals.h @@ -41,10 +41,13 @@ void gpio_callback(uint gpio, uint32_t events); // TIME BASED extern volatile bool PLAY; -extern volatile uint8_t BPM; +extern volatile float BPM; static constexpr uint32_t MINUTE_US = 60000000; static constexpr uint8_t PPQN = 96; extern volatile uint32_t MASTER_TICK; +extern volatile float filteredBPM; +extern volatile uint64_t last_external_pulse_us; +extern const uint64_t CLOCK_TIMEOUT_US; extern volatile bool RUN; diff --git a/src/Gate.cpp b/src/Gate.cpp index 9088c7f..1f8c19c 100644 --- a/src/Gate.cpp +++ b/src/Gate.cpp @@ -4,9 +4,16 @@ #include "Settings.h" #include "globals.h" #include "hardware/pwm.h" +#include #include #include #include +#include +#include + +#ifndef max +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif Gate::Gate(uint8_t pin, uint8_t idx, uint8_t slotIdx1, uint8_t slotIdx2) : Output(pin, idx, slotIdx1, slotIdx2) { @@ -231,157 +238,148 @@ void Gate::calculatePulseWidth() { pulseWidthTicks = 0; return; } - this->pulseWidthTicks = - (uint32_t)((float)this->tickInterval * (this->width / 100.0f)); + // Cap the width at 99% so it doesn't bleed into the next trigger + float effectiveWidth = (float)this->width; + if (effectiveWidth > 99.0f) effectiveWidth = 99.0f; + + this->pulseWidthTicks = (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); + + // Minimum 1 tick so we actually see a pulse if (this->width > 0 && this->pulseWidthTicks == 0) { this->pulseWidthTicks = 1; } } void Gate::turnOn() { - if (!isEnabled || tickInterval == 0) - return; + if (!isEnabled || tickInterval == 0) return; - if (MASTER_TICK % tickInterval == 0) { - if (MASTER_TICK != lastTriggerTick) { - lastTriggerTick = MASTER_TICK; + // Trigger on the interval, ensuring we don't double-trigger on the same tick + if (MASTER_TICK % tickInterval == 0 && MASTER_TICK != lastTriggerTick) { + lastTriggerTick = MASTER_TICK; - float baseP = (float)this->p; + // Probability + float effectiveP = (float)this->p + (this->pMod * 100.0f); + if ((rand() % 100) + 1 > (uint8_t)effectiveP) { + scheduledTick = 0xFFFFFFFF; + return; + } - float effectiveP = baseP + (this->pMod * 100.0f); - - if (effectiveP > 100.0f) effectiveP = 100.0f; - if (effectiveP < 0.0f) effectiveP = 0.0f; - - if ((rand() % 100) + 1 > (uint8_t)effectiveP) { - scheduledTick = 0xFFFFFFFF; - return; - } - - // swing - triggerCount++; - - uint32_t swingDelayTicks = - (uint32_t)((float)tickInterval * ((float)swing - 50.0f) / 100.0f); - - if (triggerCount % 2 == 0) { - scheduledTick = MASTER_TICK + swingDelayTicks; - } else { - scheduledTick = MASTER_TICK; - } + // Swing + triggerCount++; + int32_t swingOffset = (int32_t)((float)tickInterval * ((float)swing - 50.0f) / 100.0f); + + if (triggerCount % 2 == 0) { + // Use max(0, offset) to prevent early triggers from breaking the state machine + scheduledTick = MASTER_TICK + (uint32_t)max(0, swingOffset); + } else { + scheduledTick = MASTER_TICK; } } + // Execution if (MASTER_TICK >= scheduledTick && !state) { state = 1; startTick = MASTER_TICK; - startTimeUs = time_us_64(); - scheduledTick = 0xFFFFFFFF; + + calculatePulseWidth(); + stopTick = startTick + pulseWidthTicks; + + scheduledTick = 0xFFFFFFFF; currentRandomVal = (float)rand() / (float)RAND_MAX; } } void Gate::update() { + // 1. EXIT EARLY IF OFF if (!state && !sticky) { - lastOutVal = 0.0f; - return; - } - - uint64_t now = time_us_64(); - uint32_t elapsedUs = (uint32_t)(now - startTimeUs); - - if (elapsedUs >= pulseDurationUs) { - state = 0; - if (width < 100) { - scheduledTick = 0xFFFFFFFF; - lastTriggerTick = 0xFFFFFFFF; - } - if (!sticky) - writeAnalog(0); + lastOutVal = 0.0f; + // Make sure we actually write 0 if we aren't sticky + writeAnalog(0); return; } - float phase = (float)elapsedUs / (float)pulseDurationUs; - float outVal = 0; + // 2. LIVE WIDTH MODULATION + // We calculate the 'stopTick' every frame. + // IMPORTANT: Cap at 98% to ensure the gate has a "low" period before next beat. + float effectiveWidth = (float)width + (widthMod * 100.0f); + if (effectiveWidth > 98.0f) effectiveWidth = 98.0f; + if (effectiveWidth < 1.0f) effectiveWidth = 1.0f; - switch (shape) { - case SINE: - outVal = (sinf(phase * 2.0f * 3.14159f) * 0.5f) + 0.5f; - break; - case HALFSINE: // AKA HUMP - outVal = sinf(phase * 3.14159f); - break; - case TRIANGLE: - outVal = (phase < 0.5f) ? (phase * 2.0f) : (2.0f - (phase * 2.0f)); - break; - case SAW: - outVal = 1.0f - phase; - break; - case RAMP: - outVal = phase; - break; - case EXP: - outVal = expf(-5.0f * phase); - break; - case REXP: - outVal = expf(5.0f * (phase - 1.0f)); - break; - case LOG: - outVal = 1.0f - expf(-5.0f * phase); - break; - case SQUARE: - outVal = 1.0f; - break; - case BOUNCE: - outVal = fabsf(sinf(phase * 3.14159f * 2.0f)); - break; - case SIGMO: - outVal = phase * phase * (3.0f - 2.0f * phase); - break; - case WOBBLE: - outVal = expf(-3.0f * phase) * cosf(phase * 3.14159f * 4.0f); - if (outVal < 0) - outVal = 0; - break; - case STEPDW: - outVal = 1.0f - (floorf(phase * 4.0f) / 3.0f); - break; - case STEPUP: - outVal = floorf(phase * 4.0f) / 3.0f; - break; - case SH: - outVal = currentRandomVal; - break; - } - - this->lastOutVal = outVal; - - // handle width mod - float effectiveWidth = (float)width + (widthMod * 100.0f); - - if (effectiveWidth > 100.0f) effectiveWidth = 100.0f; - if (effectiveWidth < 1.0f) effectiveWidth = 1.0f; - - double us_per_tick = 625000.0 / (double)BPM; uint32_t modulatedTicks = (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); if (modulatedTicks < 1) modulatedTicks = 1; - this->pulseDurationUs = (uint32_t)(us_per_tick * (double)modulatedTicks); + // This is our "Target" end point + this->stopTick = startTick + modulatedTicks; - float baseLevel = (float)this->level / 100.0f; + // 3. THE HARD SYNC (THE FIX) + // If the Master Tick reached stopTick, kill the gate. + if (MASTER_TICK >= stopTick) { + state = 0; + // Don't reset lastTriggerTick here, otherwise turnOn() might re-fire + // on the same tick that we just finished. + if (!sticky) { + lastOutVal = 0.0f; + writeAnalog(0); + } + return; + } - float normalizedMod = this->levelMod; - if (normalizedMod > 1.0f || normalizedMod < -1.0f) { - normalizedMod /= 100.0f; - } + // 4. HYBRID SMOOTHNESS MATH + uint64_t now = time_us_64(); + // Ensure we don't underflow if now < last_clk_us (jitter) + uint64_t usSinceLastTick = (now > last_clk_us) ? (now - last_clk_us) : 0; - float finalLevel = baseLevel + normalizedMod; + double current_BPM_for_math = (double)filteredBPM; + if (current_BPM_for_math < 1.0) current_BPM_for_math = 1.0; - if (finalLevel > 1.0f) finalLevel = 1.0f; - if (finalLevel < 0.0f) finalLevel = 0.0f; + double us_per_tick = 60000000.0 / (current_BPM_for_math * (double)PPQN); + + float subTick = (float)usSinceLastTick / (float)us_per_tick; + if (subTick > 0.98f) subTick = 0.98f; - writeAnalog((uint16_t)(outVal * 1023.0f * finalLevel)); + // Calculate phase (0.0 to 1.0) + float elapsedTicks = (float)(MASTER_TICK - startTick) + subTick; + float totalDurationTicks = (float)(stopTick - startTick); + + // Safety check for division by zero + if (totalDurationTicks < 1.0f) totalDurationTicks = 1.0f; + + float phase = elapsedTicks / totalDurationTicks; + if (phase > 1.0f) phase = 1.0f; + if (phase < 0.0f) phase = 0.0f; + + // 5. WAVEFORM GENERATION + float outVal = 0; + switch (shape) { + case SINE: outVal = (sinf(phase * 2.0f * 3.14159265f) * 0.5f) + 0.5f; break; + case HALFSINE: outVal = sinf(phase * 3.14159265f); break; + case TRIANGLE: outVal = (phase < 0.5f) ? (phase * 2.0f) : (2.0f - (phase * 2.0f)); break; + case SAW: outVal = 1.0f - phase; break; + case RAMP: outVal = phase; break; + case EXP: outVal = expf(-5.0f * phase); break; + case REXP: outVal = expf(5.0f * (phase - 1.0f)); break; + case LOG: outVal = 1.0f - expf(-5.0f * phase); break; + case SQUARE: outVal = 1.0f; break; // Square is simple ON + case BOUNCE: outVal = fabsf(sinf(phase * 3.14159265f * 2.0f)); break; + case SIGMO: outVal = phase * phase * (3.0f - 2.0f * phase); break; + case WOBBLE: outVal = expf(-3.0f * phase) * cosf(phase * 3.14159265f * 4.0f); + if (outVal < 0) outVal = 0; break; + case STEPDW: outVal = 1.0f - (floorf(phase * 4.0f) / 3.0f); break; + case STEPUP: outVal = floorf(phase * 4.0f) / 3.0f; break; + case SH: outVal = currentRandomVal; break; + default: outVal = 1.0f; break; + } + + this->lastOutVal = outVal; + + // 6. FINAL LEVEL & MODULATION + float finalLevel = ((float)this->level / 100.0f) + (this->levelMod); + if (finalLevel > 1.0f) finalLevel = 1.0f; + if (finalLevel < 0.0f) finalLevel = 0.0f; + + // Use (uint16_t) cast to ensure the PWM driver gets a clean integer + writeAnalog((uint16_t)(outVal * 1023.0f * finalLevel)); } void Gate::writeAnalog(uint16_t val) { pwm_set_gpio_level(pin, val); } diff --git a/src/main.cpp b/src/main.cpp index e24429c..863f103 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,7 +23,7 @@ // Time based operations struct repeating_timer bpm_timer = {0}; -volatile uint8_t BPM = 60; +volatile float BPM = 60; volatile bool PLAY = true; volatile uint32_t period_us = 0; volatile uint32_t MASTER_TICK; @@ -33,7 +33,11 @@ volatile uint8_t EXTPPQNIdx = 0; volatile uint64_t last_clk_us = 0; volatile uint64_t last_valid_clk_us; volatile bool EXTERNAL_CLOCK = false; - +volatile float filteredBPM = 60.0f; // High-precision BPM for smooth LFOs +volatile uint64_t last_external_pulse_us = + 0; // Tracks the last Arturia pulse for the watchdog +const uint64_t CLOCK_TIMEOUT_US = 2000000; +bool external_pulse_received = false; ModMatrix matrix; // Initialize Outputs @@ -58,6 +62,7 @@ static EncoderHandler encoder_handler(&display_handler); bool timer_callback(struct repeating_timer *t) { if (PLAY == 1) { + last_clk_us = to_us_since_boot(get_absolute_time()); MASTER_TICK += 1; } return true; @@ -69,7 +74,7 @@ void init_timer(uint32_t period_us) { } void update_period() { - period_us = (uint32_t)(MINUTE_US / (uint32_t)BPM / PPQN); + period_us = (uint32_t)(MINUTE_US / (float)BPM / PPQN); init_timer(period_us); } @@ -82,9 +87,9 @@ void update_BPM(bool up) { update_period(); - for (auto g : outputs) { - g->setWidth(g->width); - } + // for (auto g : outputs) { + // g->setWidth(g->width); + // } if (!EXTERNAL_CLOCK) { init_timer(period_us); @@ -146,46 +151,34 @@ void handle_outs() { } void gpio_callback(uint gpio, uint32_t events) { - // CLK LOGIC - if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { + if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { uint64_t now = to_us_since_boot(get_absolute_time()); + if (now - last_valid_clk_us < 1000) return; - if (now - last_valid_clk_us < 5000) { - return; - } - last_valid_clk_us = now; + uint16_t incomingPPQN = PPQNOPTS[EXTPPQNIdx]; + uint32_t ticks_per_pulse = 96 / incomingPPQN; - uint16_t incomingPPQN; + // Instead of jumping to the NEXT beat, we snap to the CLOSEST beat. + // This prevents the sequencer from "racing" ahead. + MASTER_TICK = ((MASTER_TICK + (ticks_per_pulse / 2)) / ticks_per_pulse) * ticks_per_pulse; + + // BPM Calc if (last_clk_us > 0) { uint64_t diff = now - last_clk_us; - incomingPPQN = PPQNOPTS[EXTPPQNIdx]; - float calculatedBPM = 60000000.0f / (float)(diff * incomingPPQN); - - if (calculatedBPM >= 30 && calculatedBPM <= 255) { - if (fabsf((float)BPM - calculatedBPM) > 0.5f) { - BPM = (uint8_t)(calculatedBPM + 0.5f); - - update_period(); - for (auto g : outputs) { - g->setWidth(g->width); - } - } - } + float calculatedBPM = 60000000.0f / (float)(diff * (float)incomingPPQN); + filteredBPM = filteredBPM + (0.15f * (calculatedBPM - filteredBPM)); + BPM = (uint8_t)(filteredBPM + 0.5f); } - MASTER_TICK += (PPQN / incomingPPQN); + last_clk_us = now; + last_valid_clk_us = now; + last_external_pulse_us = now; + EXTERNAL_CLOCK = true; + external_pulse_received = true; } - if (gpio == IN_RUN_PIN) { - if (RUN) { - if (events & GPIO_IRQ_EDGE_RISE) { - PLAY = true; - } else if (events & GPIO_IRQ_EDGE_FALL) { - PLAY = false; - } - } - } + // SWITCH if (gpio == ENCODER_SW_PIN) { uint64_t now = to_us_since_boot(get_absolute_time()); static uint64_t last_sw_time = 0; @@ -308,19 +301,43 @@ int main() { if (RUN) { PLAY = false; } - while (true) { + uint64_t now = to_us_since_boot(get_absolute_time()); + + // 1. WATCHDOG: Return to internal clock if pulses stop + if (EXTERNAL_CLOCK && (now - last_external_pulse_us > CLOCK_TIMEOUT_US)) { + EXTERNAL_CLOCK = false; + BPM = globalSettings.bpm; + filteredBPM = (float)BPM; + update_period(); // Re-engages internal bpm_timer + printf("Clock Lost. Internal BPM Resumed.\n"); + } + + // 2. EXTERNAL PULSE UI UPDATES + if (external_pulse_received) { + external_pulse_received = false; + + // This is purely for the screen/UI. + // The actual timing is being adjusted inside gpio_callback. + BPM = (uint8_t)(filteredBPM + 0.5f); + + if (PLAY) { + for (Gate *g : outputs) { + g->update(); + } + } + } + + // 3. REGULAR TASKS update_cv(); encoder_handler.update(); if (PLAY) { - handle_outs(); + handle_outs(); // This runs at the frequency set by our PLL } else { for (Gate *g : outputs) { g->turnOff(); } } - - lastPlayState = PLAY; } }