Skip to content

Fix/arp clock sync - #266

Open
kartun83 wants to merge 3 commits into
dsp56300:mainfrom
kartun83:fix/arp-clock-sync
Open

Fix/arp clock sync#266
kartun83 wants to merge 3 commits into
dsp56300:mainfrom
kartun83:fix/arp-clock-sync

Conversation

@kartun83

@kartun83 kartun83 commented Jun 1, 2026

Copy link
Copy Markdown

Fix MIDI clock phase and loop re-sync for ARP sync issues

Fixes #205, #241

Two bugs in synthLib/MidiClock caused the arpeggiator to play out of phase with the host tempo in all DAWs.


Bug 1 — wrong initial clock phase in start()

The formula used to initialise m_clockTickPos on playback start:

    quarterPos     = (ppqPos - floor(ppqPos + 1.0))
    m_clockTickPos = quarterPos * 24

produces values in [-24, 0). The tick-generation loop only fires a tick when m_clockTickPos crosses 0, so starting at -24 suppresses all ticks for 24 full tick periods — one entire beat at any tempo.

This is exactly the symptom reported: "ARP tempo is wrong at the beginning, but catches up after one or two bars." The "catch up" was the clock finally escaping its one-beat head start.

Fix — keep m_clockTickPos in [-1, 0) by computing the fractional position within the current tick period:

    absoluteTicks  = ppqPos × 24
    fracTick       = absoluteTicks − floor(absoluteTicks)
    m_clockTickPos = fracTick − 1.0
ppqPos fracTick m_clockTickPos first tick fires after
0.0 0.0 −1.0 1 full tick period
0.5 0.0 −1.0 1 full tick period (on a tick boundary)
0.52 0.48 −0.52 0.52 of a tick period

This also fixes the issue reported in #205 where starting playback mid-beat caused immediate de-sync: the old formula ignored the sub-beat position entirely, the new one accounts for it precisely.


Bug 2 — loop and backward-seek not detected

process() only called start() on the false → true transition of _isPlaying. When the DAW looped back to an earlier bar, ppqPos jumped backward while m_isPlaying remained true, so start() was never called and the clock continued accumulating at the wrong phase indefinitely.

Fix — track m_lastPpqPos and restart the clock whenever ppqPos drops by more than 0.5 beats during playback. The 0.5-beat threshold is safely above any normal per-buffer advance (e.g. 300 BPM with a 4096-sample buffer ≈ 0.28 beats) while reliably detecting all practical loop-back distances.


Both fixes are in source/synthLib/midiClock.cpp and affect all synths that use the shared MIDI clock (Osirus, OsTIrus, Vavra, Xenia, etc.), but the ARP sync issue was primarily reported against OsTIrus.

kartun83 added 3 commits May 31, 2026 23:10
build_mac.sh targeted the wrong CMake output directory (temp/cmake
instead of temp/cmake_macos) and called cpack as a bare command which
is not on PATH when CMake is installed as CMake.app on macOS. Fix both.

The SSE flag pairs in juceRmlUi were broken for macOS universal builds:
CMake was deduplicating the second -Xarch_x86_64, leaving a bare
-msse4.1 that clang rejects when compiling the arm64 slice. Wrap each
pair with SHELL: so CMake preserves them as single arguments.
Adds LunaSVG 3.5.0 as a submodule (source/3rdparty/lunasvg, including
its plutovg dependency) and enables the RmlUi SVG plugin via
RMLUI_SVG_PLUGIN=ON in source/3rdparty/CMakeLists.txt.

The fork's RmlUi (dsp56300/RmlUi, rmlui_multiinstance branch) had not
been updated for the multi-instance CoreInstance API introduced in
EMU-70, causing build failures when the SVG plugin is enabled. The
submodule is updated to a commit that brings the SVG plugin in line:

- ElementSVG constructor accepts CoreInstance& and forwards it to Element
- LoadSource() uses GetSystemInterface(GetCoreInstance()) and
  GetFileInterface(GetCoreInstance()), which are no longer static
- SVGPlugin holds a CoreInstance& reference and registers through
  core_instance.factory rather than the removed static Factory API
- SVG::Initialise() accepts and forwards CoreInstance&; Core.cpp updated

The submodule commit also adds two capabilities not present upstream:

Inline SVG: when src is absent, LoadSource() reconstructs the full SVG
document from the element's attributes and GetInnerRML(), so SVG markup
can be embedded directly in RML without an external file.

Dynamic updates via inner_rml: OnChildAdd/OnChildRemove set source_dirty
without calling DirtyLayout (which caused hangs during document teardown).
A subsequent attribute write on the element, e.g. a data-ts cache-buster,
triggers DirtyLayout via OnAttributeChange, scheduling a re-rasterize.
This lets Lua scripts update SVG content at runtime.
Two bugs in MidiClock::start() and process() caused the arpeggiator
to run out of phase with the host tempo (issues dsp56300#205, dsp56300#241).

Bug 1: wrong initial clock phase in start()

The formula:
  quarterPos = (ppqPos - floor(ppqPos + 1.0))
  m_clockTickPos = quarterPos * 24

produces values in [-24, 0). The tick-generation loop only fires when
m_clockTickPos crosses 0, so starting at -24 suppresses all ticks for
24 full tick periods — one entire beat. This is why the ARP was heard
as "wrong at the beginning" and appeared to catch up after one or two
bars.

The fix computes the fractional position within the current clock tick
period and keeps m_clockTickPos in [-1, 0):

  absoluteTicks  = ppqPos * 24
  fracTick       = absoluteTicks - floor(absoluteTicks)
  m_clockTickPos = fracTick - 1.0

Examples:
  ppqPos = 0.0  → fracTick = 0.0 → m_clockTickPos = -1.0 (fires after 1 tick period)
  ppqPos = 0.5  → fracTick = 0.0 → m_clockTickPos = -1.0 (at a tick boundary)
  ppqPos = 0.52 → fracTick = 0.48 → m_clockTickPos = -0.52 (fires after 0.52 tick period)

Bug 2: no loop or backward-seek detection

process() only called start() on the false→true transition of
_isPlaying. When the DAW looped back to an earlier position, ppqPos
jumped backward while m_isPlaying stayed true, so start() was never
called and the clock continued at the wrong phase indefinitely.

The fix tracks m_lastPpqPos and restarts the clock whenever ppqPos
drops by more than 0.5 beats during playback. 0.5 beats is well above
any normal per-buffer advance (e.g. 300 BPM / 4096 samples ≈ 0.28
beats max) while reliably catching all loop-back distances.
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.

OsTIrus (CLAPi) and Reaper: Lots of stucked notes and problems Syncing Arpeggiator

1 participant