# No more jitter when syncing to external MIDI clock

**URL:** <https://discourse.zynthian.org/t/no-more-jitter-when-syncing-to-external-midi-clock/13812>\
**Category:** Software\
**Tags:** feature-request\
**Created:** [October 30, 2026, 12:45am UTC](https://discourse.zynthian.org/t/no-more-jitter-when-syncing-to-external-midi-clock/13812 "2026-10-30T00:45:05Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![jofemodo](https://discourse.zynthian.org/user_avatar/discourse.zynthian.org/jofemodo/32/2_2.png) [@jofemodo](https://discourse.zynthian.org/u/jofemodo)\
**Post date:** [October 30, 2026, 12:45am UTC](https://discourse.zynthian.org/t/no-more-jitter-when-syncing-to-external-midi-clock/13812/1 "2026-10-30T00:45:05Z")

</div>

Hi @gitnob , @nemoy and others!

Our friend claude code has implemented a nice PLL algorithm that solves the issue:

> <https://github.com/zynthian/zynthian-issue-tracking/issues/1680>
>
> \# Describe the issue
> As already mentioned here https://discourse.zynthian.org/t/…zynbleton-vangelis-read-me/12619/661 there is a high jitter of the bpm value compared to oram's older behaviour.
> \# To Reproduce
> \## Steps to reproduce the behavior:
> 1. Connect an external midi clock source
> 2. set Zynthian to use this as the only clock source
> 3. start the external midi device
> 
> \## Expected behaviour
> A reasonable stable bpm value to be used as bpm value.
> This is used IIRC by the lv2 plugins to get their timing correctly.
> \## Actual behaviour
> A much more higher jitter with the same device in vangelis compared to the latest stable oram.
> \# Screenshots
> 
> \[ui\_sesion-oram.zip\](https://github.com/user-attachments/files/28458928/ui\_sesion-oram.zip)
> 
> \[ui\_session-vangelis.zip\](https://github.com/user-attachments/files/28458329/ui\_session-vangelis.zip)
> 
> \# Additional context
> A small jitter amount is normal, but this higher jitter reduces the usability of all the lv2 plugins which are using this value as a parameter (synched delays, perfomix, ...).
> Wondering where this problem came from I did some investigation in the zynseq.cpp file, but couldn't get the overall idea of the algorithms exactly. But I was stumbling upon the line
> \`// Ensure next clock frame is not in the past
> if (dNextIntClockFrame \< nNow)
> dNextIntClockFrame = nNow;\`
> Is this the culprit?
> 
> \# Configuration
> \## Hardware
> \`\`\`
> Raspberry Pi 4 Model B Rev 1.5
> Audio: GeneralPlus USB
> Display: MIPI DSI 800x480 (inverted)
> Wiring: V5
> I2C: MCP23017@0x20, MCP23017@0x21, ADS1115@0x4A
> \`\`\`
> \## System
> \`\`\`
> Debian GNU/Linux 12 (bookworm)
> Build Date: 2026-01-27
> Memory: 16% (633M/3796M)
> Internal Storage: 58% (17G/29G ext4)
> Temperature: 38.9ºC
> Overclock: Maximum
> USB/INTENSO: 29% (8.5G/30G vfat)
> \`\`\`
> \## MIDI & UI
> \`\`\`
> Tuning: 440 Hz
> Master Channel: Off
> Preload Presets: On
> ZS3 (SubSnapShots): On
> Power Save: 0 minutes
> Audio Levels on Snapshots: On
> \`\`\`
> \## Software
> \`\`\`
> zyncoder: vangelis (a4b603f) 
> zynthian-ui: vangelis (71022b9)
> zynthian-sys: vangelis (5d564c8)
> zynthian-data: vangelis (3f50b18)
> zynthian-webconf: vangelis (3700254)
> \`\`\`

It’s been my first test with Claude Code and i must say i’m deeply impressed. My expectations have been exceeded by far. Congrats to Anthropic guys!

For the curious, here the details by Claude:

* * *

## How it works

There is a new function, `onExtClock()`, which executes with every external clock pulse:

Pulse filtering: A second-order loop smooths out the arrival times of the pulses. This estimates the actual period of the external clock and predicts when the next pulse will arrive. The bandwidth is 2 Hz during the first 2 beats (or following a significant error) to ensure rapid synchronization, and 0.5 Hz once locked, in order to reject jitter.

Phase locking: Each pulse corresponds to a fixed position within the internal ticks (80 ticks per pulse at 24 PPQN). The speed of the internal clock is adjusted so that it reaches exactly that position at the predicted time of the next pulse. Ticks are neither skipped nor duplicated; the phase does not drift, and pulse jitter does not affect this trajectory.

Interleaved ticks: Within each JACK period, internal ticks are generated up to the point of each pulse—before that pulse is processed—rather than generating all of them at the end based on the final tempo.

Stable tempo: The BPM value displayed and published to JACK is derived from the filtered period, rather than from the interval between any two individual pulses.

## Other changes

Beats and measures following MIDI START: The first pulse received after a START command via the clock port is aligned with the next internal tick; this tick then becomes the start of a measure (beat 1, with measure synchronization).

Large tempo jumps: If three consecutive pulses arrive, all showing a significant error in the same direction, the period is re-estimated based on those pulses. The pulse count is maintained, ensuring that the beats do not shift.

Loss of clock signal: If 4 consecutive pulses are missed, the lock is released, and the internal clock continues to run freely at the most recent tempo. Tempo locked: while synchronized (locked), `setTempo()` has no effect; therefore, tap tempo, UI-based changes, and sequenced tempo changes are ignored.

PPQN change: `setExtClockPPQN()` resets the PLL.

I have removed the previous adjustment of `nNextBeatTime` using `nTickDelta`.

## Simulation results

The simulation uses the actual function extracted from the file, at 48 kHz with 256-frame periods:

┌────────────────────────────────────────────┬──────────────  
│ Scenario │ Displayed Tempo │ Phase Error (RMS / Max)  
├────────────────────────────────────────────┼──────────────  
│ 120 BPM, 2 ms jitter │ 119.7 – 120.2 │ 0.45 / 1.3 ms  
├────────────────────────────────────────────┼──────────────  
│ 120 BPM, timestamps quantized to the period │ 119.9 – 120.1 │ 0.35 / 0.7 ms  
├────────────────────────────────────────────┼──────────────  
│ Jump 120 → 90 BPM │ Stabilizes at 90 │ 0.53 / 1.8 ms  
├────────────────────────────────────────────┼──────────────  
│ Jump 174 → 60 and 60 → 174 BPM │ Stabilizes │ \< 0.5 / 1.5 ms  
├────────────────────────────────────────────┼──────────────  
│ Jump 200 → 80, 3 ms jitter + quantization │ Stabilizes │ 1.9 / 3.9 ms  
└────────────────────────────────────────────┴──────────────

For reference, with the previous algorithm, a 2 ms jitter at 120 BPM causes the beat-by-beat calculated tempo to vary by approximately ±14% (roughly 104–136 BPM). This is a manual estimate, not derived from the simulation.

* * *

Amazing!

The fix is available in tomita branch, FYI, the new development branch.

Enjoy!
