Skip to content

Keep playhead strictly in sync with external clock/reset when clock-in mult/div is used - #12

Open
othmar52 wants to merge 7 commits into
Befaco:masterfrom
othmar52:improve-divmult-sync
Open

othmar52 wants to merge 7 commits into
Befaco:masterfrom
othmar52:improve-divmult-sync

Conversation

@othmar52

Copy link
Copy Markdown

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

  1. Ignore clock periods measured across a stopped clock
  2. Strictly re-sync playhead to reset while running

engine and others added 7 commits July 22, 2026 09:35
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
othmar52 force-pushed the improve-divmult-sync branch from 78bf4b2 to 2713639 Compare August 5, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant