From 9f0f79fedb4b700863b45fe971eb2f447b6e9c32 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Fri, 1 Aug 2025 20:37:48 +0100 Subject: [PATCH 01/16] believe to be working prototype of 'strict external clock' mode --- src/uClock.cpp | 13 ++++++++++++- src/uClock.h | 6 ++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 244f986..03a91cf 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -95,7 +95,18 @@ void uClockHandler() { _millis = millis(); - if (uClock.clock_state == uClock.STARTED || uClock.clock_state == uClock.STARTING) + bool readyToTick = false; + if (uClock.getClockMode()==uClock.ClockMode::INTERNAL_CLOCK) { + // internal clock tick + readyToTick = true; + } else if (uClock.getClockMode()==uClock.ClockMode::EXTERNAL_CLOCK) { + // external clock tick + if (uClock.ext_clock_tick > uClock.int_clock_tick) { + readyToTick = true; + } + } + + if (readyToTick && (uClock.clock_state == uClock.STARTED || uClock.clock_state == uClock.STARTING)) uClock.handleInternalClock(); } diff --git a/src/uClock.h b/src/uClock.h index 38456a0..d414044 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -249,7 +249,7 @@ class uClockClass { // output and internal counters, ticks and references volatile uint32_t tick; - volatile uint32_t int_clock_tick; + //volatile uint32_t int_clock_tick; uint16_t mod_clock_ref; uint8_t mod_step_ref; uint16_t mod_sync1_ref; @@ -269,7 +269,6 @@ class uClockClass { // external clock control volatile uint32_t ext_clock_us = 0; - volatile uint32_t ext_clock_tick = 0; volatile uint32_t ext_interval = 0; // helpers float hlp_external_bpm = 120.0; @@ -287,6 +286,9 @@ class uClockClass { volatile uint32_t * ext_interval_buffer = nullptr; size_t ext_interval_buffer_size = 0; uint16_t ext_interval_idx = 0; + public: + volatile uint32_t ext_clock_tick = 0; + volatile uint32_t int_clock_tick = 0; }; } } // end namespace umodular::clock From 1dde239438511810a808107feaa13a76a47b8d35 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Fri, 1 Aug 2025 20:56:46 +0100 Subject: [PATCH 02/16] moved 'allowTick' check into a class method, setStrictExternalMode(true), to make this behaviour optional --- src/uClock.cpp | 30 ++++++++++++++++++------------ src/uClock.h | 8 +++++++- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 03a91cf..c7513ae 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -95,18 +95,7 @@ void uClockHandler() { _millis = millis(); - bool readyToTick = false; - if (uClock.getClockMode()==uClock.ClockMode::INTERNAL_CLOCK) { - // internal clock tick - readyToTick = true; - } else if (uClock.getClockMode()==uClock.ClockMode::EXTERNAL_CLOCK) { - // external clock tick - if (uClock.ext_clock_tick > uClock.int_clock_tick) { - readyToTick = true; - } - } - - if (readyToTick && (uClock.clock_state == uClock.STARTED || uClock.clock_state == uClock.STARTING)) + if (uClock.allowTick() && (uClock.clock_state == uClock.STARTED || uClock.clock_state == uClock.STARTING)) uClock.handleInternalClock(); } @@ -302,6 +291,23 @@ void uClockClass::clockMe() ATOMIC(handleExternalClock()) } +void uClockClass::setStrictExternalMode(bool strict) +{ + strict_external_mode = strict; +} +bool uClockClass::isStrictExternalMode() +{ + return strict_external_mode; +} +bool uClockClass::allowTick() +{ + if (getClockMode()==ClockMode::EXTERNAL_CLOCK && isStrictExternalMode()) + // in strict mode and external, so only allow internal clock to tick if external clock has already been received + return ext_clock_tick > int_clock_tick; + // in internal clock mode or non-strict external clock mode, always allow internal clock to tick + return true; +} + void uClockClass::start() { resetCounters(); diff --git a/src/uClock.h b/src/uClock.h index d414044..a46d8dc 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -194,10 +194,15 @@ class uClockClass { // for software timer implementation(fallback for no board support) void run(); - // external timming control + // external timing control void setClockMode(ClockMode tempo_mode); ClockMode getClockMode(); void clockMe(); + + // strict external clock mode functions + bool allowTick(); + void setStrictExternalMode(bool strict); + bool isStrictExternalMode(); // for smooth slave tempo calculate display you should raise the // buffer_size of ext_interval_buffer in between 64 to 128. 254 max size. // note: this doesn't impact on sync time, only display time getTempo() @@ -246,6 +251,7 @@ class uClockClass { volatile float tempo = 120; volatile ClockMode clock_mode = INTERNAL_CLOCK; uint32_t start_timer = 0; + bool strict_external_mode = false; // output and internal counters, ticks and references volatile uint32_t tick; From 405f5fa2ee06ac1c2d3352eff5a96b3aff092e97 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Fri, 1 Aug 2025 21:03:38 +0100 Subject: [PATCH 03/16] move int_clock_tick+ext_clock_tick back into private --- src/uClock.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/uClock.h b/src/uClock.h index a46d8dc..f1f3213 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -255,7 +255,7 @@ class uClockClass { // output and internal counters, ticks and references volatile uint32_t tick; - //volatile uint32_t int_clock_tick; + volatile uint32_t int_clock_tick; uint16_t mod_clock_ref; uint8_t mod_step_ref; uint16_t mod_sync1_ref; @@ -275,6 +275,7 @@ class uClockClass { // external clock control volatile uint32_t ext_clock_us = 0; + volatile uint32_t ext_clock_tick = 0; volatile uint32_t ext_interval = 0; // helpers float hlp_external_bpm = 120.0; @@ -292,9 +293,6 @@ class uClockClass { volatile uint32_t * ext_interval_buffer = nullptr; size_t ext_interval_buffer_size = 0; uint16_t ext_interval_idx = 0; - public: - volatile uint32_t ext_clock_tick = 0; - volatile uint32_t int_clock_tick = 0; }; } } // end namespace umodular::clock From e1786232c095c8065b2f4ea14c3688f488f4e167 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Mon, 4 Aug 2025 23:17:26 +0100 Subject: [PATCH 04/16] think fixed 'external midi devices slip out of time' problem? --- src/uClock.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 73dee79..e3f9384 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -95,8 +95,7 @@ void uClockHandler() { _millis = millis(); - if (uClock.allowTick()) - uClock.handleInternalClock(); + uClock.handleInternalClock(); } // initTimer(uint32_t us_interval) and setTimer(uint32_t us_interval) @@ -171,8 +170,14 @@ void uClockClass::handleInternalClock() if (clock_mode == EXTERNAL_CLOCK) { if (mod_clock_counter == 0) { + // Tick Phase-lock if (abs(int_clock_tick - ext_clock_tick) > 1) { + + // check for strict external mode + if (!uClock.allowTick()) + return; + // only update tick at a full quarter or phase_lock_quarters * a quarter // how many quarters to count until we phase-lock? if ((ext_clock_tick * mod_clock_ref) % (output_ppqn*phase_lock_quarters) == 0) { @@ -221,6 +226,7 @@ void uClockClass::handleInternalClock() } } } + } // process clock signal From 47bd015e8a4e451d6a5be70578667b962e91d29c Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Mon, 4 Aug 2025 23:20:46 +0100 Subject: [PATCH 05/16] spacing --- src/uClock.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index e3f9384..8598ec2 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -94,7 +94,6 @@ volatile uint32_t _millis = 0; void uClockHandler() { _millis = millis(); - uClock.handleInternalClock(); } @@ -170,11 +169,10 @@ void uClockClass::handleInternalClock() if (clock_mode == EXTERNAL_CLOCK) { if (mod_clock_counter == 0) { - // Tick Phase-lock if (abs(int_clock_tick - ext_clock_tick) > 1) { - // check for strict external mode + // check for strict external mode -- don't progress if external clock hasn't caught up with internal clock if (!uClock.allowTick()) return; @@ -226,7 +224,6 @@ void uClockClass::handleInternalClock() } } } - } // process clock signal From 917c103b5b8c11cb6c5e22d1e60580574982883e Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Mon, 4 Aug 2025 23:26:02 +0100 Subject: [PATCH 06/16] abs->labs for RP2040 compilability --- src/uClock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 8598ec2..89a7b61 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -170,7 +170,7 @@ void uClockClass::handleInternalClock() if (mod_clock_counter == 0) { // Tick Phase-lock - if (abs(int_clock_tick - ext_clock_tick) > 1) { + if (labs(int_clock_tick - ext_clock_tick) > 1) { // check for strict external mode -- don't progress if external clock hasn't caught up with internal clock if (!uClock.allowTick()) From 32d2753593ef50a48362d64f2a962ee6cb9a47c8 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Mon, 1 Sep 2025 18:25:30 +0100 Subject: [PATCH 07/16] add UCLOCK_HAS_STRICT_EXTERNAL_MODE define so projects don't need changes to recompile with other branches, to ease A/B testing. --- src/uClock.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uClock.h b/src/uClock.h index 28ee981..d4eda8d 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -32,6 +32,8 @@ #include #include +#define UCLOCK_HAS_STRICT_EXTERNAL_MODE + namespace umodular { namespace clock { // Shuffle templates are specific for each PPQN output resolution From 84f0f6486ecbabf6d8d2a1d6eb05b8783d9d4004 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Mon, 1 Sep 2025 18:28:00 +0100 Subject: [PATCH 08/16] fix missing ; in get***OverflowCounter() functions --- src/uClock.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index c318545..317658e 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -686,14 +686,14 @@ uint16_t uClockClass::getIntOverflowCounter() { uint16_t counter = 0; ATOMIC(counter = int_overflow_counter) - return counter + return counter; } uint16_t uClockClass::getExtOverflowCounter() { uint16_t counter = 0; ATOMIC(counter = ext_overflow_counter) - return counter + return counter; } } } // end namespace umodular::clock From 2376848a88873819e1a48b71b6ac7648206d5930 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Sat, 8 Nov 2025 18:28:35 +0000 Subject: [PATCH 09/16] make MINIMUM_SYNC_COUNTER more easily configurable --- src/uClock.cpp | 2 +- src/uClock.h | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 317658e..3fc4d35 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -270,7 +270,7 @@ void uClockClass::handleExternalClock() switch (clock_state) { case STARTING: clock_state = SYNCING; - start_sync_counter = 4; + start_sync_counter = MINIMUM_SYNC_COUNTER; break; case SYNCING: if (--start_sync_counter == 0) diff --git a/src/uClock.h b/src/uClock.h index 5c75626..6743917 100755 --- a/src/uClock.h +++ b/src/uClock.h @@ -91,6 +91,10 @@ struct SyncCallback { #define SECS_PER_HOUR (3600UL) #define SECS_PER_DAY (SECS_PER_HOUR * 24L) +#ifndef MINIMUM_SYNC_COUNTER + #define MINIMUM_SYNC_COUNTER 4 +#endif + class uClockClass { public: From 6cf39cc242e14d8d9a78df07fecc87d57e781c0a Mon Sep 17 00:00:00 2001 From: midilab Date: Mon, 10 Nov 2025 15:19:47 -0300 Subject: [PATCH 10/16] add breakchanges for v2.3+ --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6a7fce..f01346f 100755 --- a/README.md +++ b/README.md @@ -534,16 +534,19 @@ void loop() { ⚠️ **Note**: Software timer mode provides less accurate timing than hardware interrupts. -## Migration Guide (v1.x → v2.0) +## Migration Guide (v1.x → v2.3) ### Breaking Changes -| Old API (v1.x) | New API (v2.0+) | +| Old API (v1.x) | New API (v2.3+) | |----------------|-----------------| | `setClock96PPQNOutput()` | `setOnOutputPPQN()` | | `setClock16PPQNOutput()` | `setOnStep()` | | `setOnClockStartOutput()` | `setOnClockStart()` | | `setOnClockStopOutput()` | `setOnClockStop()` | +| `setOnSync24()` | `setOnSync(uClock.PPQN_24, onSync24)` | +| `setOnSync48()` | `setOnSync(uClock.PPQN_48, onSync48)` | +| `setOnSyncXX()` | `setOnSync(uClock.PPQN_XX, onSyncXX)` | ### Resolution Changes From 17bb8ec732374876c5d5e98c3b4843742e7771c1 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Wed, 11 Feb 2026 21:15:50 +0000 Subject: [PATCH 11/16] RP2350 (Pico 2) support --- README.md | 2 +- .../RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino | 2 +- library.json | 2 +- library.properties | 4 ++-- src/uClock.cpp | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f01346f..99e9ab5 100755 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The absence of real-time features necessary for creating professional-level embe - **AVR**: ATmega168/328, ATmega16u4/32u4, ATmega2560 - **ARM**: Teensy (all versions), STM32XX, Seeed Studio XIAO M0 - **ESP32**: All ESP32 family boards -- **RP2040**: Raspberry Pi Pico and compatible boards +- **RP2040/RP2350**: Raspberry Pico, Pico 2, and compatible boards ## Why uClock? diff --git a/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino b/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino index cf5b6eb..7faf971 100644 --- a/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino +++ b/examples/RP2040UsbUartMasterClock/RP2040UsbUartMasterClock.ino @@ -54,7 +54,7 @@ void onClockStop() { } void setup() { -#if defined(ARDUINO_ARCH_MBED) && defined(ARDUINO_ARCH_RP2040) +#if defined(ARDUINO_ARCH_MBED) && (defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350)) // Manual begin() is required on core without built-in support for TinyUSB such as mbed rp2040 TinyUSB_Device_Init(0); #endif diff --git a/library.json b/library.json index ffe6d19..a0ec300 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "uClock", "version": "2.3.0", - "description": "A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040)", + "description": "A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040/RP2350)", "keywords": "bpm, clock, timing, tick, music, generator", "repository": { "type": "git", diff --git a/library.properties b/library.properties index 86a95fc..32aa442 100755 --- a/library.properties +++ b/library.properties @@ -3,8 +3,8 @@ version=2.3.0 author=Romulo Silva maintainer=Romulo Silva sentence=BPM clock generator for Arduino platform. -paragraph=A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040) +paragraph=A Library to implement BPM clock tick calls using hardware interruption. Supported and tested on AVR boards(ATmega168/328, ATmega16u4/32u4 and ATmega2560) and ARM boards(Teensy, STM32XX, ESP32, Raspberry Pico, Seedstudio XIAO M0 and RP2040/RP2350) category=Timing url=https://github.com/midilab/uClock -architectures=avr,arm,samd,stm32,esp32,rp2040 +architectures=avr,arm,samd,stm32,esp32,rp2040,rp2350 includes=uClock.h diff --git a/src/uClock.cpp b/src/uClock.cpp index 73a3cf3..c3ff6c2 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -67,9 +67,9 @@ #define UCLOCK_PLATFORM_FOUND #endif // - // RP2040 (Raspberry Pico) family + // RP2040 and RP2350 (Raspberry Pico and Pico 2) family // - #if defined(ARDUINO_ARCH_RP2040) + #if defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350) #include "platforms/rp2040.h" #define UCLOCK_PLATFORM_FOUND #endif From d06ff1a4257750f1c60285bbf0528317a82ce0c4 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Thu, 16 Apr 2026 00:02:09 +0100 Subject: [PATCH 12/16] FIX: timer initialization on RP2040/RP2350 -- BPM will now be accurate --- src/platforms/rp2040.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/platforms/rp2040.h b/src/platforms/rp2040.h index eb918c2..896dbf2 100644 --- a/src/platforms/rp2040.h +++ b/src/platforms/rp2040.h @@ -19,12 +19,10 @@ bool handlerISR(repeating_timer *timer) void initTimer(uint32_t init_clock) { // set up RPi interrupt timer - // todo: actually should be -init_clock so that timer is set to start init_clock us after last tick, instead of init_clock us after finished processing last tick! - add_repeating_timer_us(init_clock, &handlerISR, NULL, &timer); + add_repeating_timer_us(-(int32_t)init_clock), &handlerISR, NULL, &timer); } void setTimer(uint32_t us_interval) { cancel_repeating_timer(&timer); - // todo: actually should be -us_interval so that timer is set to start init_clock us after last tick, instead of init_clock us after finished processing last tick! - add_repeating_timer_us(us_interval, &handlerISR, NULL, &timer); -} \ No newline at end of file + add_repeating_timer_us(-(int32_t)us_interval), &handlerISR, NULL, &timer); +} From c7de253c71b37fc01000eca4db15da884ef7bfdb Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Thu, 16 Apr 2026 00:15:00 +0100 Subject: [PATCH 13/16] (fix typo in previous commit) --- src/platforms/rp2040.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platforms/rp2040.h b/src/platforms/rp2040.h index 896dbf2..7cfe51a 100644 --- a/src/platforms/rp2040.h +++ b/src/platforms/rp2040.h @@ -19,10 +19,10 @@ bool handlerISR(repeating_timer *timer) void initTimer(uint32_t init_clock) { // set up RPi interrupt timer - add_repeating_timer_us(-(int32_t)init_clock), &handlerISR, NULL, &timer); + add_repeating_timer_us(-(int32_t)init_clock, &handlerISR, NULL, &timer); } void setTimer(uint32_t us_interval) { cancel_repeating_timer(&timer); - add_repeating_timer_us(-(int32_t)us_interval), &handlerISR, NULL, &timer); + add_repeating_timer_us(-(int32_t)us_interval, &handlerISR, NULL, &timer); } From 63f7234229d4065577c3a0ed992e88469d58f73b Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Thu, 16 Apr 2026 18:48:41 +0100 Subject: [PATCH 14/16] use static_cast --- src/platforms/rp2040.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platforms/rp2040.h b/src/platforms/rp2040.h index 7cfe51a..e804f33 100644 --- a/src/platforms/rp2040.h +++ b/src/platforms/rp2040.h @@ -19,10 +19,10 @@ bool handlerISR(repeating_timer *timer) void initTimer(uint32_t init_clock) { // set up RPi interrupt timer - add_repeating_timer_us(-(int32_t)init_clock, &handlerISR, NULL, &timer); + add_repeating_timer_us(-static_cast(init_clock), &handlerISR, NULL, &timer); } void setTimer(uint32_t us_interval) { cancel_repeating_timer(&timer); - add_repeating_timer_us(-(int32_t)us_interval, &handlerISR, NULL, &timer); + add_repeating_timer_us(-static_cast(us_interval), &handlerISR, NULL, &timer); } From d99f175b3ba2e20de2579e82bad878fdb09fd473 Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Fri, 17 Apr 2026 19:22:43 +0100 Subject: [PATCH 15/16] perhaps better at syncing --- src/uClock.cpp | 57 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 91a9dd4..881b50b 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -165,6 +165,14 @@ void uClockClass::handleInternalClock() if (clock_state <= STARTING) // STOPED=0, PAUSED=1, STARTING=2, SYNCING=3, STARTED=4 return; + // Watchdog: stop musical clock if no external pulse received for 1 second + if (clock_mode == EXTERNAL_CLOCK && clock_state == STARTED && ext_clock_us > 0) { + if (clock_diff(ext_clock_us, micros()) > 1000000UL) { + clock_state = PAUSED; + return; + } + } + // tick phase lock and external tempo match for EXTERNAL_CLOCK mode if (clock_mode == EXTERNAL_CLOCK) { // Tick Phase-lock @@ -194,24 +202,34 @@ void uClockClass::handleInternalClock() } } - // any external interval avaliable to start sync timer? - if (ext_interval > 0) { - counter = ext_interval; - sync_interval = clock_diff(ext_clock_us, micros()); - - // phase-multiplier interval - if (int_clock_tick <= ext_clock_tick) { - counter -= (sync_interval * PHASE_FACTOR) >> 8; - } else { - if (counter > sync_interval) { - counter += ((counter - sync_interval) * PHASE_FACTOR) >> 8; + // use buffer average for stable tempo estimation; raw ext_interval can be corrupted by USB bursts + { + uint32_t avg_interval = 0; + uint8_t valid = 0; + for (uint8_t i = 0; i < ext_interval_buffer_size; i++) { + if (ext_interval_buffer[i] > 0) { + avg_interval += ext_interval_buffer[i]; + valid++; } } + if (valid > 0) { + counter = avg_interval / valid; + sync_interval = clock_diff(ext_clock_us, micros()); + + // phase-multiplier interval + if (int_clock_tick <= ext_clock_tick) { + counter -= (sync_interval * PHASE_FACTOR) >> 8; + } else { + if (counter > sync_interval) { + counter += ((counter - sync_interval) * PHASE_FACTOR) >> 8; + } + } - external_tempo = constrainBpm(freqToBpm(counter)); - if (external_tempo != tempo) { - tempo = external_tempo; - uClockSetTimerTempo(tempo); + external_tempo = constrainBpm(freqToBpm(counter)); + if (external_tempo != tempo) { + tempo = external_tempo; + uClockSetTimerTempo(tempo); + } } } } @@ -280,8 +298,9 @@ void uClockClass::handleExternalClock() clock_state = STARTED; break; default: - // accumulate interval incomming ticks data for getTempo() smooth reads on slave clock_mode - if (ext_interval > 0) { + // accumulate interval incoming ticks data for getTempo() smooth reads on slave clock_mode + // reject intervals shorter than the minimum valid period at MAX_BPM (filters USB burst packets) + if (ext_interval >= (60000000UL / input_ppqn / MAX_BPM)) { ext_interval_buffer[ext_interval_idx] = ext_interval; if(++ext_interval_idx >= ext_interval_buffer_size) ext_interval_idx = 0; @@ -651,8 +670,8 @@ void uClockClass::resetCounters() } // external bpm read buffer - //for (uint8_t i=0; i < ext_interval_buffer_size; i++) - // ext_interval_buffer[i] = 0; + for (uint8_t i=0; i < ext_interval_buffer_size; i++) + ext_interval_buffer[i] = 0; } void uClockClass::tap() From 44ed9de5abb69772663393c72a89c909d4b12abe Mon Sep 17 00:00:00 2001 From: Tristan Rowley Date: Fri, 17 Apr 2026 20:33:03 +0100 Subject: [PATCH 16/16] attempt to stick rigidly to external clock while still syncing; does seem better now --- src/uClock.cpp | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/uClock.cpp b/src/uClock.cpp index 881b50b..564797b 100755 --- a/src/uClock.cpp +++ b/src/uClock.cpp @@ -294,8 +294,38 @@ void uClockClass::handleExternalClock() start_sync_counter = MINIMUM_SYNC_COUNTER; break; case SYNCING: - if (--start_sync_counter == 0) + // Accumulate valid intervals during SYNCING so the PLL buffer has real + // data by the time we reach STARTED. + if (ext_interval >= (60000000UL / input_ppqn / MAX_BPM)) { + ext_interval_buffer[ext_interval_idx] = ext_interval; + if (++ext_interval_idx >= ext_interval_buffer_size) + ext_interval_idx = 0; + } + if (--start_sync_counter == 0) { + // Force-align all internal counters to ext_clock_tick, which is + // always the canonical song position. The existing phase-lock only + // snaps on beat boundaries, which means without this we can start + // up to a full beat out of alignment. + tick = ext_clock_tick * mod_clock_ref; + int_clock_tick = ext_clock_tick; + mod_clock_counter = 0; + for (uint8_t track = 0; track < track_slots_size; track++) { + tracks[track].step_counter = tick / mod_step_ref; + tracks[track].mod_step_counter = 0; + } + for (uint8_t i = 0; i < sync_callback_size; i++) { + if (sync_callbacks[i].callback) { + sync_callbacks[i].tick = tick / sync_callbacks[i].sync_ref; + sync_callbacks[i].mod_counter = 0; + } + } + // Prime the timer to the correct BPM immediately. + if (ext_interval >= (60000000UL / input_ppqn / MAX_BPM)) { + tempo = constrainBpm(freqToBpm(ext_interval)); + uClockSetTimerTempo(tempo); + } clock_state = STARTED; + } break; default: // accumulate interval incoming ticks data for getTempo() smooth reads on slave clock_mode