Conversation
The external clock input (PD4) was polled once per loop() pass, so edge detection jittered by the loop period (a few hundred microseconds, dominated by blocking analogRead calls) and pulses shorter than a blocking call (e.g. an EEPROM write of up to ~26 ms) were missed entirely, leaving the step counter lagging behind the external clock. A PCINT20 ISR now stores a micros() timestamp and a flag; all processing stays in read_clock(). Timing references for clock out, gate windows and step advance use the captured edge timestamp instead of the loop's current time, so the measured clock period and all derived timings are free of polling jitter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EEPROM.write blocks ~3.3 ms per byte; persisting tempo plus clock-in mult (up to 8 bytes) freezes the loop for up to ~26 ms. These writes were triggered 3 s after the last encoder move, on tap-tempo button release and when the external clock was unplugged - i.e. typically while the sequencer was playing, causing steps, gates and clock out to stall audibly (and, before external clock edges were captured by interrupt, even losing pulses entirely). All EEPROM writes now go through service_pending_EEPROM_writes(), which performs them only while the sequencer is stopped. Pending writes are kept flagged until then, so nothing is lost as long as the module is stopped once before power-down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
analogRead blocks for ~112 us and read_address() ran once per loop() pass, making it the dominant contributor to the loop period and thus to input polling latency. Reading the address CV at 500 Hz is still far faster than any musically relevant CV change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Like the external clock, the reset input (PC3 / A3) was polled once per loop() pass, so short reset pulses could be missed during blocking calls and reset timing jittered by the loop period. A PCINT11 ISR now latches the falling edge; the pulse is processed in read_one_shot_reset_input() as before, keeping the processing order (reset before clock) within the loop unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The clock input is non-inverting (series resistor plus clamp diodes straight into PD4), so the pin follows the jack and the trigger fires on the falling edge of the incoming clock - as documented in the manual and identical to the polled detection before. Only the reset input is inverted by a transistor stage, making reset act on the rising edge. Comments only, no functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The external clock period is measured as the distance between two captured edges. When the clock source is stopped and restarted (e.g. sequencer stop/play), the first new edge measures the whole pause as the period. All timings derived from it (clock-in mult subdivisions, gate windows, clock out) were then based on that bogus huge value for one full clock interval: with a positive clock-in mult the playhead stayed on step 1 for the entire first interval and ran offset by the missed subdivided steps from then on. An interval is now accepted as the new period only if it is plausible (below 3x the current period) or confirmed by a second similar interval, which keeps real tempo changes working; a lone pause-length interval is discarded and the previous period is kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A reset received while the sequencer is running only rewound the address counter; the clock divider kept counting from wherever it was, so with a negative clock-in mult the jump to step 1 happened up to a full division late while the display lingered on the old step (e.g. after a sequencer stop/play, whose reset gate rises together with the first clock). Re-arm the divider on reset - as already done when reset is received while stopped - so the next clock edge advances to step 1 immediately. Also close the subdivision window on reset so that with a positive clock-in mult the playhead holds after a reset until the next real clock edge instead of advancing on a leftover subdivision timer slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
othmar52
force-pushed
the
improve-divmult-sync
branch
from
August 5, 2026 12:31
78bf4b2 to
2713639
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: this branch builds on top of
improve-external-clock-sync(PR 1)and includes its commits. Best opened after PR 1 is merged (it will then
show only the two commits below), or open both and mention the dependency.
Description
Follow-up to the external clock sync PR. With a clock-in mult, the
playhead often stayed on step 1 for a whole incoming clock interval after
a sequencer stop/play and ran offset by the missed subdivided steps from
then on. With a clock-in div, the jump to step 1 after stop/play came
up to a full division late while the display lingered on the old step.
Both issues are also present in v1.5 and are unrelated to the interrupt
changes:
Ignore clock periods measured across a stopped clock. The external
clock period is measured between two edges, so the first edge after
stop/play measures the whole pause as the "period". Everything derived
from it (mult subdivisions, gate windows, clock out) then used that
bogus huge value for one full interval. An interval is now accepted as
the new period only if it is plausible (below 3x the current period) or
confirmed by a second similar interval — so real tempo changes,
including drastic slowdowns, still work; a lone pause-length interval is
discarded and the previous period is kept.
Strictly re-sync the playhead on reset while running. A reset
received while running only rewound the address counter; the clock
divider kept counting from wherever it was, delaying the jump to step 1
by up to a full division. The divider is now re-armed on reset (as
already done when reset arrives while stopped — this reinstates a line
that exists commented-out in the current source), and the subdivision
window is closed so that with a mult the playhead holds after a reset
until the next real clock edge instead of advancing on a leftover
subdivision timer slot.
Relevant detail: reset outputs that act as a run gate (e.g. Arturia
BeatStep Pro: high on play, low on stop) deliver exactly one reset edge
together with the first clock of a run, which made both issues show up on
every stop/play.
Tested on a Muxlicer (PCB v1.3) with an Arturia BeatStep Pro (clock +
reset), clock-in mult and div at various settings, repeated stop/play
mid-pattern, plus live tempo changes in both directions: playhead now
follows incoming clock and reset strictly; the mult/div = 0 base case is
unchanged.
Commits