From 3e210a08146ae2318f895a372ffa97d86fd6bb91 Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Mon, 16 Mar 2026 21:49:45 -0400 Subject: [PATCH 1/4] 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; } } -- 2.45.2 From 787f084b74c670a09fc6431f186b3345e2a64ad2 Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Mon, 16 Mar 2026 22:32:32 -0400 Subject: [PATCH 2/4] working still... --- include/globals.h | 4 +++ src/main.cpp | 71 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/include/globals.h b/include/globals.h index 0d85057..c7b8827 100644 --- a/include/globals.h +++ b/include/globals.h @@ -58,6 +58,10 @@ extern volatile uint64_t last_clk_us; extern volatile uint64_t last_valid_clk_us; extern volatile bool EXTERNAL_CLOCK; +#define AVG_SAMPLES 12 +extern uint64_t pulse_intervals[AVG_SAMPLES]; +extern uint8_t pulse_idx; + enum WaveShape { SQUARE, TRIANGLE, SAW, RAMP, EXP, HALFSINE, REXP, LOG, SINE, BOUNCE, SIGMO, WOBBLE, STEPDW, STEPUP, SH, SHAPE_COUNT}; inline const char* waveShapeToString(WaveShape shape) { diff --git a/src/main.cpp b/src/main.cpp index 863f103..f93b8ca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -37,6 +37,10 @@ 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; +uint64_t pulse_intervals[AVG_SAMPLES] = {0}; +uint8_t pulse_idx = 0; +uint64_t last_external_clk_us = 0; + bool external_pulse_received = false; ModMatrix matrix; @@ -151,34 +155,63 @@ void handle_outs() { } void gpio_callback(uint gpio, uint32_t events) { - 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()); + + // 1. Debounce if (now - last_valid_clk_us < 1000) return; - uint16_t incomingPPQN = PPQNOPTS[EXTPPQNIdx]; - uint32_t ticks_per_pulse = 96 / incomingPPQN; + // 2. BPM Math - Use 'last_external_clk_us' specifically + if (last_external_clk_us > 0) { + uint64_t latest_diff = now - last_external_clk_us; - // Instead of jumping to the NEXT beat, we snap to the CLOSEST beat. - // This prevents the sequencer from "racing" ahead. + pulse_intervals[pulse_idx] = latest_diff; + pulse_idx = (pulse_idx + 1) % AVG_SAMPLES; + + uint64_t sum = 0; + uint8_t count = 0; + for (int i = 0; i < AVG_SAMPLES; i++) { + if (pulse_intervals[i] > 0) { + sum += pulse_intervals[i]; + count++; + } + } + + if (count > 0) { + double avg_diff = (double)sum / (double)count; + uint16_t incomingPPQN = PPQNOPTS[EXTPPQNIdx]; + + // Use 60,000,000.0 (double) to ensure high precision + double calculatedBPM = 60000000.0 / (avg_diff * (double)incomingPPQN); + + if (calculatedBPM > 20.0 && calculatedBPM < 300.0) { + // Slow down the LPF even more (0.05 instead of 0.1) + // This makes the screen feel "heavy" and professional like a real synth + filteredBPM = filteredBPM + (0.05f * ((float)calculatedBPM - filteredBPM)); + } + }} + + // 3. Sync Logic + uint32_t ticks_per_pulse = 96 / PPQNOPTS[EXTPPQNIdx]; 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; - float calculatedBPM = 60000000.0f / (float)(diff * (float)incomingPPQN); - filteredBPM = filteredBPM + (0.15f * (calculatedBPM - filteredBPM)); - BPM = (uint8_t)(filteredBPM + 0.5f); + for (int i = 0; i < 8; i++) { + if (outputs[i]->lastTriggerTick > MASTER_TICK) { + outputs[i]->lastTriggerTick = 0xFFFFFFFF; + } } - last_clk_us = now; + // 4. Update Timestamps + last_external_clk_us = now; // Store for next external pulse + last_clk_us = now; // Update for the Gate's sub-tick interpolation last_valid_clk_us = now; last_external_pulse_us = now; + external_pulse_received = true; EXTERNAL_CLOCK = true; - external_pulse_received = true; } - // SWITCH + // SWITCH if (gpio == ENCODER_SW_PIN) { uint64_t now = to_us_since_boot(get_absolute_time()); static uint64_t last_sw_time = 0; @@ -317,6 +350,16 @@ int main() { if (external_pulse_received) { external_pulse_received = false; + static uint8_t last_ppqn_idx = 0xFF; + if (EXTPPQNIdx != last_ppqn_idx) { + MASTER_TICK = 0; // Reset to start of bar + for (Gate *g : outputs) { + g->lastTriggerTick = 0xFFFFFFFF; + g->state = 0; // Kill any stuck gates + } + last_ppqn_idx = EXTPPQNIdx; + } + // This is purely for the screen/UI. // The actual timing is being adjusted inside gpio_callback. BPM = (uint8_t)(filteredBPM + 0.5f); -- 2.45.2 From ed79c8e3b911cd150ad3083b2aa1d0e4541cf9e3 Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Mon, 16 Mar 2026 22:40:01 -0400 Subject: [PATCH 3/4] fixing width --- src/Gate.cpp | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Gate.cpp b/src/Gate.cpp index 1f8c19c..97f7720 100644 --- a/src/Gate.cpp +++ b/src/Gate.cpp @@ -294,59 +294,58 @@ void Gate::update() { // 1. EXIT EARLY IF OFF if (!state && !sticky) { lastOutVal = 0.0f; - // Make sure we actually write 0 if we aren't sticky writeAnalog(0); return; } // 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; + if (effectiveWidth > 100.0f) effectiveWidth = 100.0f; + if (effectiveWidth < 0.0f) effectiveWidth = 0.0f; uint32_t modulatedTicks = (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); - if (modulatedTicks < 1) modulatedTicks = 1; - - // This is our "Target" end point this->stopTick = startTick + modulatedTicks; - // 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); + // 3. THE HARD SYNC + // Only kill the gate if width is strictly less than 100% + if (effectiveWidth < 100.0f) { + if (MASTER_TICK >= stopTick) { + state = 0; + if (!sticky) { + lastOutVal = 0.0f; + writeAnalog(0); + } + return; } - return; } // 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; double current_BPM_for_math = (double)filteredBPM; if (current_BPM_for_math < 1.0) current_BPM_for_math = 1.0; - 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; + if (subTick > 0.99f) subTick = 0.99f; - // Calculate phase (0.0 to 1.0) + // --- THE SINE WAVE FIX --- + // If width is 100%, we calculate phase based on the WHOLE interval. + // If width < 100%, we calculate phase based on the PULSE duration. float elapsedTicks = (float)(MASTER_TICK - startTick) + subTick; - float totalDurationTicks = (float)(stopTick - startTick); + float totalDurationTicks = (effectiveWidth >= 100.0f) ? (float)tickInterval : (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; + + // Keep phase looping if we are at 100% width (so LFOs/Sines keep moving) + if (effectiveWidth >= 100.0f) { + while (phase >= 1.0f) phase -= 1.0f; + } else { + if (phase > 1.0f) phase = 1.0f; + } if (phase < 0.0f) phase = 0.0f; // 5. WAVEFORM GENERATION @@ -360,7 +359,7 @@ void Gate::update() { 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 SQUARE: outVal = 1.0f; break; 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); @@ -378,7 +377,6 @@ void Gate::update() { 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)); } -- 2.45.2 From 67922bdd56dc2537975dcd7c1f8d44b530bd7c98 Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Tue, 17 Mar 2026 20:23:31 -0400 Subject: [PATCH 4/4] cleanup --- src/DisplayHandler.cpp | 2 +- src/Gate.cpp | 227 +++++++++++++++++++++++------------------ src/main.cpp | 87 ++++++++-------- 3 files changed, 171 insertions(+), 145 deletions(-) diff --git a/src/DisplayHandler.cpp b/src/DisplayHandler.cpp index 2cd0cf9..1486e70 100644 --- a/src/DisplayHandler.cpp +++ b/src/DisplayHandler.cpp @@ -397,7 +397,7 @@ void DisplayHandler::render() { } void DisplayHandler::renderMainPage() { - std::string bpm_string = "BPM: " + std::to_string(BPM); + std::string bpm_string = "BPM: " + std::to_string((uint8_t)BPM); if (cursorPosition == 0) { if (cursorClick == 1) { diff --git a/src/Gate.cpp b/src/Gate.cpp index 97f7720..a280dea 100644 --- a/src/Gate.cpp +++ b/src/Gate.cpp @@ -9,18 +9,17 @@ #include #include #include -#include #ifndef max -#define max(a,b) (((a) > (b)) ? (a) : (b)) +#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) { +Gate::Gate(uint8_t pin, uint8_t idx, uint8_t slotIdx1, uint8_t slotIdx2) + : Output(pin, idx, slotIdx1, slotIdx2) { this->pin = pin; - this->idx = idx; - this->slotIdx1 = slotIdx1; - this->slotIdx2 = slotIdx2; + this->idx = idx; + this->slotIdx1 = slotIdx1; + this->slotIdx2 = slotIdx2; state = 0; @@ -37,48 +36,48 @@ Gate::Gate(uint8_t pin, uint8_t idx, uint8_t slotIdx1, uint8_t slotIdx2) : Outpu p = 100; // probability of a pulse level = 100; - modDest1 = (idx + 1) % 8; - modDest2 = (idx + 2) % 8; + modDest1 = (idx + 1) % 8; + modDest2 = (idx + 2) % 8; } - void Gate::pack(OutputConfig &cfg) { - cfg.type = TYPE_GATE; + cfg.type = TYPE_GATE; - GateSettings* s = (GateSettings*)cfg.data; + GateSettings *s = (GateSettings *)cfg.data; - s->modifierSelectionIndex = this->modifierSelectionIndex; - s->divideMode = this->divideMode; - s->modifier = this->modifier; - s->width = this->width; - s->p = this->p; - s->level = this->level; - s->shape = (uint8_t)this->shape; + s->modifierSelectionIndex = this->modifierSelectionIndex; + s->divideMode = this->divideMode; + s->modifier = this->modifier; + s->width = this->width; + s->p = this->p; + s->level = this->level; + s->shape = (uint8_t)this->shape; } void Gate::unpack(const OutputConfig &cfg) { - if (cfg.type != TYPE_GATE) return; + if (cfg.type != TYPE_GATE) + return; - GateSettings s; - memcpy(&s, cfg.data, sizeof(GateSettings)); + GateSettings s; + memcpy(&s, cfg.data, sizeof(GateSettings)); - this->modifierSelectionIndex = s.modifierSelectionIndex; - this->divideMode = s.divideMode; - this->modifier = s.modifier; - this->width = s.width; - this->p = s.p; - this->level = s.level; - this->shape = (WaveShape)s.shape; + this->modifierSelectionIndex = s.modifierSelectionIndex; + this->divideMode = s.divideMode; + this->modifier = s.modifier; + this->width = s.width; + this->p = s.p; + this->level = s.level; + this->shape = (WaveShape)s.shape; - setDiv(this->modifierSelectionIndex); - setWidth(this->width); + setDiv(this->modifierSelectionIndex); + setWidth(this->width); } void Gate::setupPatches() { - - matrix.patch(this->slotIdx1, this->modDest1, this->idx, DEST_LEVEL, 100, false); - - matrix.patch(this->slotIdx2, this->modDest2, this->idx, DEST_LEVEL, 100, false); + matrix.patch(this->slotIdx1, this->modDest1, this->idx, DEST_LEVEL, 100, + false); + matrix.patch(this->slotIdx2, this->modDest2, this->idx, DEST_LEVEL, 100, + false); } void Gate::setLen(uint32_t currentPeriod) { @@ -199,8 +198,6 @@ void Gate::setDiv(uint8_t modifier_selecton_index) { this->lastTriggerTick = 0xFFFFFFFF; setWidth(this->width); - // this is called in width, check if needed still? - calculatePulseWidth(); }; void Gate::setWidth(uint16_t newWidth) { @@ -239,22 +236,16 @@ void Gate::calculatePulseWidth() { return; } - // 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; + } - // 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; @@ -267,46 +258,46 @@ void Gate::turnOn() { // Swing triggerCount++; - int32_t swingOffset = (int32_t)((float)tickInterval * ((float)swing - 50.0f) / 100.0f); - + 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; - + calculatePulseWidth(); stopTick = startTick + pulseWidthTicks; - - scheduledTick = 0xFFFFFFFF; + + scheduledTick = 0xFFFFFFFF; currentRandomVal = (float)rand() / (float)RAND_MAX; } } void Gate::update() { - // 1. EXIT EARLY IF OFF if (!state && !sticky) { lastOutVal = 0.0f; writeAnalog(0); return; } - // 2. LIVE WIDTH MODULATION - float effectiveWidth = (float)width + (widthMod * 100.0f); - if (effectiveWidth > 100.0f) effectiveWidth = 100.0f; - if (effectiveWidth < 0.0f) effectiveWidth = 0.0f; + // width + float effectiveWidth = (float)width + (widthMod * 100.0f); + if (effectiveWidth > 100.0f) + effectiveWidth = 100.0f; + if (effectiveWidth < 0.0f) + effectiveWidth = 0.0f; - uint32_t modulatedTicks = (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); + uint32_t modulatedTicks = + (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); this->stopTick = startTick + modulatedTicks; - // 3. THE HARD SYNC // Only kill the gate if width is strictly less than 100% if (effectiveWidth < 100.0f) { if (MASTER_TICK >= stopTick) { @@ -319,63 +310,103 @@ void Gate::update() { } } - // 4. HYBRID SMOOTHNESS MATH uint64_t now = time_us_64(); - uint64_t usSinceLastTick = (now > last_clk_us) ? (now - last_clk_us) : 0; + uint64_t usSinceLastTick = (now > last_clk_us) ? (now - last_clk_us) : 0; double current_BPM_for_math = (double)filteredBPM; - if (current_BPM_for_math < 1.0) current_BPM_for_math = 1.0; + if (current_BPM_for_math < 1.0) + current_BPM_for_math = 1.0; double us_per_tick = 60000000.0 / (current_BPM_for_math * (double)PPQN); - - float subTick = (float)usSinceLastTick / (float)us_per_tick; - if (subTick > 0.99f) subTick = 0.99f; - // --- THE SINE WAVE FIX --- - // If width is 100%, we calculate phase based on the WHOLE interval. - // If width < 100%, we calculate phase based on the PULSE duration. + float subTick = (float)usSinceLastTick / (float)us_per_tick; + if (subTick > 0.99f) + subTick = 0.99f; + + // If width is 100%, calculate phase based on the WHOLE interval. + // If width < 100%, calculate phase based on the PULSE duration. float elapsedTicks = (float)(MASTER_TICK - startTick) + subTick; - float totalDurationTicks = (effectiveWidth >= 100.0f) ? (float)tickInterval : (float)(stopTick - startTick); - - if (totalDurationTicks < 1.0f) totalDurationTicks = 1.0f; + float totalDurationTicks = (effectiveWidth >= 100.0f) + ? (float)tickInterval + : (float)(stopTick - startTick); + + if (totalDurationTicks < 1.0f) + totalDurationTicks = 1.0f; float phase = elapsedTicks / totalDurationTicks; - // Keep phase looping if we are at 100% width (so LFOs/Sines keep moving) + // Keep phase looping if at 100% width if (effectiveWidth >= 100.0f) { - while (phase >= 1.0f) phase -= 1.0f; + while (phase >= 1.0f) + phase -= 1.0f; } else { - if (phase > 1.0f) phase = 1.0f; + if (phase > 1.0f) + phase = 1.0f; } - if (phase < 0.0f) phase = 0.0f; - // 5. WAVEFORM GENERATION + if (phase < 0.0f) + phase = 0.0f; + 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; - 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; + 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; + 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; + float finalLevel = ((float)this->level / 100.0f) + (this->levelMod); + if (finalLevel > 1.0f) + finalLevel = 1.0f; + if (finalLevel < 0.0f) + finalLevel = 0.0f; writeAnalog((uint16_t)(outVal * 1023.0f * finalLevel)); } diff --git a/src/main.cpp b/src/main.cpp index f93b8ca..990ff66 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,13 +33,13 @@ 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 +volatile float filteredBPM = 60.0f; +volatile uint64_t last_external_pulse_us = 0; const uint64_t CLOCK_TIMEOUT_US = 2000000; uint64_t pulse_intervals[AVG_SAMPLES] = {0}; uint8_t pulse_idx = 0; uint64_t last_external_clk_us = 0; +uint8_t BPM_UI_REFRESH = 0; bool external_pulse_received = false; ModMatrix matrix; @@ -91,10 +91,6 @@ void update_BPM(bool up) { update_period(); - // for (auto g : outputs) { - // g->setWidth(g->width); - // } - if (!EXTERNAL_CLOCK) { init_timer(period_us); } else { @@ -155,13 +151,12 @@ void handle_outs() { } void gpio_callback(uint gpio, uint32_t events) { -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()); - - // 1. Debounce - if (now - last_valid_clk_us < 1000) return; - // 2. BPM Math - Use 'last_external_clk_us' specifically + if (now - last_valid_clk_us < 1000) + return; + if (last_external_clk_us > 0) { uint64_t latest_diff = now - last_external_clk_us; @@ -177,23 +172,27 @@ if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { } } - if (count > 0) { + if (count > 0) { double avg_diff = (double)sum / (double)count; uint16_t incomingPPQN = PPQNOPTS[EXTPPQNIdx]; - - // Use 60,000,000.0 (double) to ensure high precision + double calculatedBPM = 60000000.0 / (avg_diff * (double)incomingPPQN); if (calculatedBPM > 20.0 && calculatedBPM < 300.0) { - // Slow down the LPF even more (0.05 instead of 0.1) - // This makes the screen feel "heavy" and professional like a real synth - filteredBPM = filteredBPM + (0.05f * ((float)calculatedBPM - filteredBPM)); - } - }} + float diff = (float)calculatedBPM - filteredBPM; + + if (fabsf(diff) > 5.0f || filteredBPM < 1.0f) { + filteredBPM = (float)calculatedBPM; + } else { + filteredBPM += (0.3f * diff); + } + } + } + } - // 3. Sync Logic uint32_t ticks_per_pulse = 96 / PPQNOPTS[EXTPPQNIdx]; - MASTER_TICK = ((MASTER_TICK + (ticks_per_pulse / 2)) / ticks_per_pulse) * ticks_per_pulse; + MASTER_TICK = ((MASTER_TICK + (ticks_per_pulse / 2)) / ticks_per_pulse) * + ticks_per_pulse; for (int i = 0; i < 8; i++) { if (outputs[i]->lastTriggerTick > MASTER_TICK) { @@ -201,17 +200,29 @@ if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { } } - // 4. Update Timestamps - last_external_clk_us = now; // Store for next external pulse - last_clk_us = now; // Update for the Gate's sub-tick interpolation + last_external_clk_us = now; + last_clk_us = now; last_valid_clk_us = now; last_external_pulse_us = now; external_pulse_received = true; EXTERNAL_CLOCK = true; + + BPM_UI_REFRESH += 1; + if (BPM_UI_REFRESH % 4 == 0) { + display_handler.updateScreen = 1; + } } + 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; @@ -244,10 +255,8 @@ void setup_ins() { adc_gpio_init(27); } -// Helper to scale your current range to 0.0 - 1.0 float fmap(float x, float in_min, float in_max) { float result = (x - in_min) / (in_max - in_min); - // Constraints to keep it between 0.0 and 1.0 if (result < 0.0f) return 0.0f; if (result > 1.0f) @@ -259,33 +268,24 @@ void update_cv() { static uint64_t last_adc_read = 0; uint64_t now = to_us_since_boot(get_absolute_time()); if (now - last_adc_read < 2000) - return; // 2ms is plenty fast + return; last_adc_read = now; - // Calibration (Adjust these based on your earlier -0.19 to 0.15 range) const float raw_min = -0.19f; const float raw_max = 0.15f; - const float offset_zero = 0.404f; // Your calibrated offset + const float offset_zero = 0.404f; for (int i = 0; i < 2; i++) { adc_select_input(i); - // CROSSTALK FIX: Dummy read to clear the ADC capacitor adc_read(); - busy_wait_us(10); // Tiny pause to settle + busy_wait_us(10); - // Actual read float raw_val = (float)adc_read() * (1.0f / 4095.0f); float centered = offset_zero - raw_val; - // SCALING & FLIPPING: - // By using (max - centered), we flip the inversion. float scaled = (centered - raw_min) / (raw_max - raw_min); - // Optional: If it's STILL upside down, use this instead: - // float scaled = 1.0f - ((centered - raw_min) / (raw_max - raw_min)); - - // Constrain 0.0 to 1.0 if (scaled < 0.01f) scaled = 0.0f; if (scaled > 1.0f) @@ -337,7 +337,6 @@ int main() { 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; @@ -346,7 +345,6 @@ int main() { printf("Clock Lost. Internal BPM Resumed.\n"); } - // 2. EXTERNAL PULSE UI UPDATES if (external_pulse_received) { external_pulse_received = false; @@ -360,8 +358,6 @@ int main() { last_ppqn_idx = EXTPPQNIdx; } - // This is purely for the screen/UI. - // The actual timing is being adjusted inside gpio_callback. BPM = (uint8_t)(filteredBPM + 0.5f); if (PLAY) { @@ -371,12 +367,11 @@ int main() { } } - // 3. REGULAR TASKS update_cv(); encoder_handler.update(); if (PLAY) { - handle_outs(); // This runs at the frequency set by our PLL + handle_outs(); } else { for (Gate *g : outputs) { g->turnOff(); -- 2.45.2