ztrackerprime

star 26

zTracker Prime (m6502/ztrackerprime) contributor skill — orienting context, key files, and pointers to deeper reference material.

m6502 By m6502 schedule Updated 6/16/2026

name: ztrackerprime description: zTracker Prime (m6502/ztrackerprime) contributor skill — orienting context, key files, and pointers to deeper reference material. domain: repository-specific source_repo: https://github.com/m6502/ztrackerprime source_platform: github tags: [cpp, sdl3, tracker, midi, cross-platform] triggers: keywords: primary: [ztracker, ztrackerprime, zt, zTracker] secondary: [m6502, tracker, MIDI tracker, impulse tracker, SDL3 tracker]


zTracker Prime Development Skill

Last verified: 2026-06-15. If this date is more than ~7 days old when you load this skill, your first move is to check what merged since: gh pr list --repo m6502/ztrackerprime --state merged --json number,title,mergedAt --jq '[.[] | select(.mergedAt > "<this date>")]'. Reconcile any architecture / shortcut / invariant claims below against current master before acting on them. Then bump this date in the same PR that fixes the drift.

What lives elsewhere on purpose: the open-PR list and merged landmarks are NOT in this skill — they go stale fast. For current state run gh pr list --repo m6502/ztrackerprime (open) or gh pr list --repo m6502/ztrackerprime --state merged --limit 30 (recent landings). The skill stays timeless: architecture, invariants, foot-guns, conventions.

What this is

zTracker Prime (m6502/ztrackerprime) is Manuel Montoto's maintained fork of zTracker — an Impulse-Tracker-inspired MIDI tracker originally by Christopher Micali (2000–2001) with contributions from Daniel Kahlin. SDL 3, cross-platform (Windows XP through 11, macOS, Linux). C++17, CMake.

MIDI-only. No sample playback, no audio mixing. Pattern data drives MIDI Out events.

Build

mkdir -p build && cd build
cmake -G "Unix Makefiles" ..   # or Ninja, MSVC, MinGW
make -j8                       # → build/Program/zt(.exe) or build/Program/zt.app
ctest --output-on-failure      # unit-test harness (Linux CI runs this)

extlibs/ (libpng + zlib + lua) must be populated; CI fetches them from upstream tarballs (see .github/workflows/build.yml). SDL3 from Homebrew on macOS (brew install sdl3).

ZT_BUILD_TESTS CMake option (default ON) controls whether tests/ builds. Production binaries are unaffected.

Launching the app on macOS

open <path>/zt.app (or open <path>/zt) — gives the app its own session. Avoid ./zt & from a non-interactive shell; it can exit silently when stdin is detached.

scripts/zt-screenshot.sh (macOS) launches zt.app and captures F1/F2/F3/F11/F12/Ctrl+F12 via screencapture -R + AppleScript. Output at /tmp/zt-shots/. Use it to self-verify layout changes.

Headless framebuffer screenshots

zt has a built-in windowless renderer that dumps its real framebuffer to a PNG — no window, no compositor, no screen-recording permission. This is the most reliable way to verify page layout and the only method that works in headless / remote / CI contexts.

# Script = one command per line. Commands:
#   key <chord>        e.g. key F11, key shift+F3, key ctrl+s, key space
#   mousemove <x> <y>  /  mousedown <x> <y>  /  mouseup <x> <y>
#   click <x> <y>      full left click (DOWN then UP on separate frames)
#   wait <ms>          let update()/draw() run a few frames
#   shot <path>        dump the current framebuffer to PNG
#   quit               tear down cleanly (always end with this)
# Coords are internal-resolution pixels — the col()/row() space, col(N)=N*8.
cat > /tmp/f11.script <<'EOF'
wait 400
key F11
wait 400
shot /tmp/f11.png
quit
EOF
build/Program/zt.app/Contents/MacOS/zt --headless --script /tmp/f11.script
# then Read /tmp/f11.png to eyeball it

--headless sets SDL_HINT_VIDEO_DRIVER=dummy, so it runs anywhere (no display needed). The binary reads zt.conf relative to its location (Contents/Resources/zt.conf for a .app) — edit that file to set up preconditions (enabled flags, loaded song, etc.) before driving the UI.

Mouse injection (commands above) makes mouse-only controls testable. Many F11 checkboxes/sliders are NOT in the keyboard Tab cycle (F11's Tab only toggles title↔order-list), so drive them with click <x> <y>. Coordinates are internal-resolution pixels — the same col(N)=N*8 / row(M)=M*8 space the widgets use — so a checkbox the page positions at cb->x/cb->y is clicked at click <cb->x * 8> <cb->y * 8> (aim a pixel or two inside it). Capture a shot before and after to diff the resulting state change. Because click defers its button-up to the next pump, a checkbox latches on DOWN and toggles on UP exactly as it does under a real click. click spans two frames (widgets latch on DOWN, act on UP), so a wait after it before shot is good practice.

# 1. Relaunch a fresh binary (kill the old instance first, or you screenshot stale pixels)
pkill -f "zt.app/Contents/MacOS/zt"; sleep 1
open build/Program/zt.app && sleep 2

# 2. Bring zt to the front — screencapture grabs whatever is frontmost, and
#    `open` does NOT guarantee focus over the terminal you launched it from
osascript -e 'tell application "zt" to activate' \
  || osascript -e 'tell application "System Events" to set frontmost of (first process whose name is "zt") to true'
sleep 2

# 3. Capture. Full-screen is the robust fallback; -o omits the window shadow, -x silences the shutter
screencapture -o -x /tmp/zt_shot.png

Then Read the PNG to eyeball it. Foot-guns learned the hard way:

  • cp is interactive. A bare cp src dst prompts overwrite? (y/n) and, with no tty answer, silently does NOT overwrite — your swap never happened. Use /bin/cp -f for any scripted overwrite.
  • open then capture grabs the terminal, not zt, unless you activate zt first (step 2). The first capture attempt usually shows iTerm — bring zt forward and recapture.
  • A backgrounded python3 - <<'PY' ... Quartz ... window-id lookup will fail on stock macOS; don't depend on it. Full-screen screencapture needs no window id.

Screenshotting a specific palette / skin without driving the UI

To verify how a palette .conf (e.g. palettes/hotdog_stand.conf) or a skin renders, you don't have to click through the Palette Editor. The startup colours come from the skin named in zt.conf (skin: default), loaded from <bundle>/skins/<name>/colors.confsame slot format as palettes/*.conf. So:

RES=build/Program/zt.app/Contents/Resources
/bin/cp -f "$RES/skins/default/colors.conf" /tmp/colors_backup.conf   # back up
/bin/cp -f "$RES/palettes/hotdog_stand.conf" "$RES/skins/default/colors.conf"  # swap in
# ... relaunch + capture per the recipe above ...
/bin/cp -f /tmp/colors_backup.conf "$RES/skins/default/colors.conf"   # ALWAYS restore

COLORS.Background is the full-screen fill (main.cpp fillRect(0,0,…,COLORS.Background)), so a wrong Background value shows instantly. Note that palettes/skins are copied into the bundle by a CMake POST_BUILD step — edit the source palettes/*.conf (repo root) and rebuild to propagate; editing only the bundle copy is for throwaway verification and must be restored.

Palette gotcha (PR: Hotdog Stand): scripts/import-schism-palettes.py maps Schism colour index 1 to Background, but some Schism palettes (Hotdog Stand) put the desktop fill on index 2 — the importer left Background black and Lowlight yellow, so the desktop was black and, worse, black Text on a black desktop was invisible. Frames draw with both Lowlight (top/left) and Highlight (bottom/right), so a Lowlight that equals Background makes half of every frame vanish. Hand-tune imported palettes against an actual screenshot; don't trust the 16→18 slot mapping.

Key files

File Purpose
src/main.cpp Entry, main loop, key dispatch, switch_page, CLI parser
src/CUI_Patterneditor.cpp Pattern editor (F2) + 10+ pattern operations + Ctrl+Shift+§ CC drawmode
src/CUI_Midimacroeditor.cpp / CUI_Arpeggioeditor.cpp F4 / Shift+F4 editors
src/CUI_KeyBindings.cpp Unified Shortcuts & MIDI Mappings page (Shift+F2 — moved from Shift+F3 to free that slot)
src/CUI_CcConsole.cpp CC Console (Shift+F3) — Paketti CCizer file load + sliders/knobs send-out + MIDI Learn
src/CUI_SysExLibrarian.cpp SysEx Librarian (Shift+F5) — .syx file send + auto-capture of incoming SysEx
src/CUI_LuaConsole.cpp Lua Console (Ctrl+Alt+L) — Renoise-style interactive console over the embedded Lua engine. Tab cycling, rprint/oprint/help, scrollback wrapping.
src/lua_engine.{cpp,h} Embedded Lua API: object model (zt, song, transport, pattern, track, cells, order list) + notifiers (zt.on/off/fire for play/stop/row/idle). lua/selftest.lua exercises the surface via --lua-test (a ctest target).
src/ableton_link.{cpp,h} Ableton Link tempo + transport sync (PR #159). C API: zt_ableton_link_startup/teardown/pump/defer_play/available/get_tempo. Driven from playback.cpp; configured from F11 Song Config; conf keys in conf.cpp. Rebuilt on the shared phase_chase.h controller.
src/phase_chase.h Shared closed-loop position controller (pure logic, SDL-free, unit-tested), used by both Ableton Link and MIDI clock sync. phase_chase_step() returns a bounded skew that cancels rate error (int-BPM rounding, crystal drift); deadband-tiered feedback.
src/midi_clock_sync.{cpp,h} MIDI clock slave (midi_in_sync / midi_in_sync_chase_tempo). MIDI-in thread only records into atomic g_mclk_* counters; zt_midi_clock_pump() (main thread, once/frame) consumes transport, estimates tempo, phase-locks via phase_chase.h. Six-state zt_sync_state; zt_midi_clock_get_status(). Honors sync_offset_ms.
src/midi_timestamp.h Pure helpers (SDL-free, unit-tested) to use a MIDI hardware timestamp as clock-arrival time. macOS mach_absolute_timeSDL_GetTicks() ms via zt_mach_stamp_to_sdl_ms, sanity-gated (garbage stamp falls back to software clock).
src/CUI_*.cpp Other pages (Sysconfig, Songconfig, Help, InstEditor, MainMenu, Playsong, etc.)
src/CUI_Page.{h,cpp} Base class for pages
src/UserInterface.{h,cpp} Widget classes — CheckBox, ValueSlider, TextInput, Frame, Button, TextBox, ListBox, MidiOutDeviceOpener, SkinSelector, VUPlay. Fix rendering bugs HERE, not in callers.
src/keybuffer.{h,cpp} Key state (KS_ALT/KS_CTRL/KS_META/KS_SHIFT) + KS_HAS_ALT macro. ZT_TEST_NO_SDL guard for SDL-free unit tests.
src/font.cpp print() / printtitle() / printBG() drawing primitives
src/zt.h STATE_/CMD_ enums, page globals (UIP_CcConsole, UIP_SysExLibrarian, ...), layout macros, g_cc_drawmode
src/conf.{h,cpp} zt.conf parsing, zt_config_globals. Keys include ccizer_folder, syx_folder
src/module.{h,cpp} Song data (patterns, tracks, events, instruments, arpeggios, midimacros). instrument carries ccizer_bank (per-instrument Paketti CCizer file).
src/playback.{h,cpp} MIDI output timing, R-effect arpeggio + Z-effect macro dispatch. Z on a *.syx-named midimacro dispatches the file as SysEx (see sysex_macro.h).
src/winmm_compat.h Cross-platform MIDI shim. Exposes midiOut* (WinMM-style API) on Win/macOS/Linux. Adds zt_midi_out_sysex(handle, bytes, len) for SysEx send. macOS zt_coremidi_read_proc parses incoming F0..F7 into zt_sysex_inq_push.
src/sysex_inq.{h,cpp} Process-wide receive queue for incoming SysEx (16 slots × 8 KB; overflow drops oldest). std::mutex protected.
src/sysex_macro.{h,cpp} Helpers for the *.syx-named-midimacro convention: predicate, path resolution against syx_folder, file read with F0..F7 framing check.
src/ccizer.{h,cpp} CCizer parser, .cc-view slider/knob sidecar, .cc-midi MIDI Learn sidecar, folder scan, MIDI byte builder, zt_ccizer_current_file() (read by Pattern Editor for friendly slot names in CC drawmode status).
src/preset_data.h F4/Shift+F4 preset arrays + pure apply functions
src/preset_selector.h Pure listbox decision logic (click / arrow / Space / P-cycle)
src/page_sync.h F4/Shift+F4 widget↔data sync helpers
src/save_key_dispatch.h Global Ctrl-S dispatch decision
src/editor_layout.h Shared character-grid constants for F4/Shift+F4
assets/ccizer/ Bundled CCizer banks (sc88st, microfreak, minilogue, monologue, Prophet6, deepmind6, waldorf_blofeld, tb03, se02, pro800, polyend_*, midi_control_example) — copied from Paketti. README.md documents the format.
assets/syx/ Bundled SysEx files. request_universal_inquiry.syx = F0 7E 7F 06 01 F7 for first-test handshakes. README.md documents the librarian workflow.
tests/ CTest unit-test executables — 14 suites: presets, selector, page_sync, save_key, keybuffer, ccizer, sysex_inq, sysex_stress, sysex_macro, ccbn_roundtrip, ccenv, keyjazz_map, ableton_link, lua_api. The lua_api suite drives the real zt binary headless (--headless --lua-test) against a scratch song and runs on macOS CI (PRs #154, #156); the rest are SDL-free and run on Linux CI. The MIDI clock phase-lock feature (integration PR #169) adds 4 more (phase_chase, midi_timestamp, midi_clock_sync, midi_clock_estimator → 18). (Count drifts as suites are added — ctest -N is the source of truth.)
doc/help.txt In-app F1 help — update when adding keybinds or CLI flags
doc/CHANGELOG.txt Release notes, chronological
.github/workflows/build.yml 5-platform CI matrix; Linux job runs ctest

For the live PR queue: gh pr list --repo m6502/ztrackerprime.

Page / shortcut map (current)

Key Page Notes
F1 Help
F2 Pattern Editor F2 again from PE → PEParms. F2 F2 toggles "PianoKey" — the Ableton/Logic piano keyjazz layout (PR #135, keyjazz_piano_layout).
F3 Instrument Editor F3 row 11 shows CCizer Bank: <basename> (per-instrument bank). F3 again → "Create 16 Channels" popup: fills the next empty instrument slots with the current device on ch 1..16 (PR #136).
F4 MIDI Macro Editor A macro whose name ends in .syx is dispatched as a SysEx file send by the playback engine; data grid is ignored (hint shown).
Shift+F2 Shortcuts & MIDI Mappings Was Shift+F3 historically; moved 2026-04-30 to free the slot for CC Console.
Shift+F3 CC Console Loads CCizer .txt, sliders/knobs send CC out, L to MIDI Learn, B to assign as current instrument's bank.
Shift+F4 Arpeggio Editor
F5 Play Song
Shift+F5 SysEx Librarian Lists .syx files. Enter sends. Incoming SysEx auto-saved as recv_<timestamp>.syx.
Shift+F6 CC Envelope Editor Per-instrument, CCizer-aware CC envelopes (Schism-style). New page, PR #132. Persisted in the optional INSE chunk; editor loads/saves .env presets from a configurable folder.
F6 / F7 / F8 / F9 Play Pattern / From Cursor / Stop / Panic
F10 Song Message Editor
F11 Song Configuration / Order Hosts the Ableton Link controls (enable checkbox, start/stop-sync checkbox, quantize-offset slider). See "Ableton Link" below.
F12 System Configuration Includes CCizer Folder text input (binds to ccizer_folder zt.conf key).
Ctrl+Alt+L Lua Console Interactive Lua over the embedded engine. Sibling MainMenu chords: Ctrl+Alt+K (Keybindings), Ctrl+Alt+N (New Song).
Ctrl+Shift+§ CC drawmode toggle While ON, incoming CC writes Sxxyy and PB writes Wxxxx at the cursor row. Per-session UNDO_SAVE. [CC DRAW] badge shown top-right of Pattern Editor while active. Extended by PRs #123–#129: mouse drag-to-draw drawbars (MD_CC_DRAW), CCizer-slot cycling, one-key arm-and-draw, double-click reset, Ctrl+F2 slot cycle, keyjazz audition while mouse-drawing. Windows fires the toggle via Ctrl+Shift+`` ``.

Coordinate system

  • row(N) = N * 8 pixels (Y offset). Named for character rows but returns pixels.
  • col(M) = M * 8 pixels (X offset).
  • print(x, y, str, color, drawable) — x first, y second.
  • Some legacy code calls print(row(2), col(13), ...) — this works only because FONT_SIZE_X == FONT_SIZE_Y == 8. Confusing; don't propagate.

Layout macros: INITIAL_ROW=1, PAGE_TITLE_ROW_Y=9, TRACKS_ROW_Y=11, SPACE_AT_BOTTOM=8.

CCizer + SysEx architecture (2026-04-29 → 2026-04-30, PRs #78–#84)

Seven-PR feature stack landed end of April 2026 wiring Paketti-style CCizer banks and a complete SysEx librarian into zTracker. The architecture has three layers worth keeping in mind:

1. Per-page (UI)

  • Shift+F3 CC Console (CUI_CcConsole.cpp) — file list of .txt CCizer banks, slot grid with sliders/knobs, V toggles slider↔knob, L to MIDI Learn ((status, data1) → slot), U to unbind, B to assign loaded file as current instrument's bank. [/] adjusts MIDI channel.
  • Shift+F5 SysEx Librarian (CUI_SysExLibrarian.cpp) — file list of .syx files in syx_folder. Enter/S sends. Receive log auto-pops from zt_sysex_inq and writes new files as recv_<TS>.syx. R rescans, C clears log.
  • Pattern Editor CC drawmode (CUI_Patterneditor.cpp) — Ctrl+Shift+§ flips g_cc_drawmode. While on, the existing MIDI-in pump in update() routes 0xB0..0xBF / 0xE0..0xEF to update_event(...) writing S<cc><val> / W<14bit>. Cursor advances by EditStep. Single UNDO_SAVE per session (file-static g_cc_draw_session_snapped).

2. Process-wide

  • g_cc_drawmode (in zt.h) — toggle.
  • zt_ccizer_current_file() (in ccizer.h) — last-loaded file pointer, so the Pattern Editor's drawmode status can show friendly slot names ("Cutoff = 92") instead of raw CC numbers.
  • zt_sysex_inq_* (in sysex_inq.h) — ring buffer of incoming complete SysEx messages. Filled by the macOS CoreMIDI read_proc; drained by CUI_SysExLibrarian::drain_recv every update().
  • MidiOut->sendSysEx(dev, bytes, len) — virtual on OutputDevice, default no-op. MidiOutputDevice impl asserts F0/F7 framing and calls zt_midi_out_sysex in winmm_compat.h. Backed by CoreMIDI MIDIPacketListAdd (256-byte chunks), Win midiOutLongMsg, ALSA snd_seq_ev_set_sysex.

3. Persistence

  • ccizer_folder zt.conf key — folder of .txt files. Resolution: override → <exe-dir>/ccizer → macOS Resources/ccizer → Linux share/zt/ccizer~/.config/zt/ccizer. F12 page exposes the text input.
  • syx_folder zt.conf key — folder of .syx files. Resolution: override → ./syx (next to binary or in .app Resources) → ~/.config/zt/syx. Auto-created.
  • <file>.cc-view sidecar — per-CCizer slot slider/knob choice. Written next to the .txt.
  • <file>.cc-midi sidecar — per-CCizer slot MIDI Learn binding. Format: <slot> <status hex> <data1 hex>.
  • instrument.ccizer_bank[256] — per-instrument CCizer file. Persisted via new optional CCBN chunk in .zt. Format: short int count then per entry unsigned char inst_idx, short int length, bytes path. Fully backward+forward compatible: old zTracker reading new .zt skips the unrecognized chunk via readblock's built-in advance-past behavior.

SysEx-by-filename convention (sysex_macro.h): a midimacro whose name ends in .syx (case insensitive, requires len > 4) is dispatched by the playback engine's Z effect handler as a file send. The macro's data array is ignored. No .zt format change — the existing MMAC chunk already round-trips the name. Old zTracker sees an empty data array and silently does nothing — safe forward-compat.

Cross-platform status (verified 2026-05-02):

  • SysEx send: works on macOS (CoreMIDI MIDIPacketListAdd), Windows (WinMM midiOutLongMsg + MHDR_DONE poll, PR #87), Linux (ALSA snd_seq_ev_set_sysex).
  • SysEx receive: works on macOS (CoreMIDI read_proc parsing F0..F7) and Windows (MIM_LONGDATA accumulator, PR #90). Initial Linux ALSA MIDI input landed in PR #113 (marked "needs hardware verification") — so the Linux-in platform layer is no longer a bare stub, but confirm SysEx-receive-on-Linux against that path on real hardware before relying on it.
  • Audit cluster (PRs #87–#93) hardened the foundation: Windows MHDR_DONE polling, macOS heap-alloc on receive, recv_seq survives restart, recv rotation, *.syx macro caching at load, lifetime docs, CCBN roundtrip test.

Test suites added:

  • tests/test_ccizer.cpp — 51 checks: parser, comments/blanks, out-of-range, view sidecar round-trip, MIDI byte builder, folder resolution, dir scan.
  • tests/test_sysex_inq.cpp — ~25 checks: empty / push-pop / FIFO / overflow / buffer-too-small / invalid input.
  • tests/test_sysex_macro.cpp — ~20 checks: predicate, path resolution, valid/malformed/oversized/missing file read.

Lua scripting (PRs #148–#157, 2026-05-31)

An embedded Lua engine + interactive console landed across ten PRs at the end of May 2026. Treat it as a first-class subsystem.

  • Engine (src/lua_engine.{cpp,h}) — exposes a Renoise-flavoured object model: zt (top-level), song, transport, pattern, track, plus full cell access, the order list, note names, and constants. Notifier API: zt.on(event, fn) / zt.off(...) / zt.fire(...) for play / stop / row / idle events, pumped from the main loop. MIDI macros expose :send() (PR #157).
  • Console (src/CUI_LuaConsole.cpp, Ctrl+Alt+L) — Tab cycling, rprint / oprint / help helpers, Renoise-style object printing, and scrollback that wraps (PR #153) rather than clips (PR #152) long lines.
  • Self-testlua/selftest.lua exercises the whole API surface; runs via --lua-test and is wired into ctest, and the macOS CI jobs run it (PRs #154, #156). When you add or rename a Lua binding, update lua/selftest.lua in the same PR — CI will catch a regression there.

Ableton Link (PR #159, merged 2026-06-10)

Tempo + transport sync with Ableton Live and other Link-aware apps over the local network.

  • Core (src/ableton_link.{cpp,h}) — C API: zt_ableton_link_startup / _teardown / _pump / _defer_play(row, pattern, pm) / _available / _get_tempo. Pumped from playback.cpp; the pump can move song->bpm underneath the UI, so F11 re-reads it each frame.
  • Config (conf.cpp, keys persisted in zt.conf): ableton_link_enable (OFF by default — no surprise LAN traffic), ableton_link_start_stop_sync (ON by default; only effective once Link is enabled), ableton_link_quantum (default 4 = one bar of 4/4), ableton_link_offset_ms (fire quantized starts early by this much).
  • UI — F11 Song Configuration (CUI_Songconfig.cpp) hosts the enable + start/stop-sync checkboxes and the offset slider.
  • Precedence: if both Ableton Link and MIDI Sync are enabled, Link wins (conf.cpp).

MIDI clock sync & phase-lock (integration PR #169, documented ahead of merge)

External MIDI-clock slave sync built on the same phase_chase.h controller as Ableton Link, so a fractional-tempo master cannot drift the engine. midi_in_sync follows start/stop; midi_in_sync_chase_tempo derives tempo and phase-locks; the --midi-clock CLI flag turns both on. The engine is the merge of two independent rewrites that converged on an identical phase_chase.h: Chris Micali's tempo estimator (cmicali/midi-sync-improvements) under the hardening branch's observability + robustness.

  • Observer architecture (midi_clock_sync.{cpp,h}) — the MIDI-in callback thread only records into std::atomic counters (g_mclk_*, same advisory contract as player::stop_count); all decisions run in zt_midi_clock_pump() on the main thread once/frame, next to zt_ableton_link_pump(). ThreadSanitizer-clean (TSan CI job). Relies only on per-counter atomicity, tolerating a bump seen a frame late.
  • Tempo estimate (Chris's) — least-squares regression over a ~1 s sliding window of clock-arrival samples (a jitter-delayed edge carries only 1/n weight), plus retune anti-flap hysteresis (>0.6 BPM held, >3.0 BPM immediate) so a master on a rounding boundary like 124.5 doesn't flap the nominal.
  • Position, not just rate — 24 clocks/beat = 4 subticks/clock; clocks-since-START is the exact expected position (MIDI-clock analogue of Link's beatAtTime), fed through phase_chase_step() into player::chase_skew_us.
  • Hardware timestamps (midi_timestamp.h, macOS) — CoreMIDI packet stamp → SDL_GetTicks() epoch removes callback scheduling jitter and feeds the estimator's arrival edge; sanity-gated so a bad stamp falls back to the software clock.
  • Hardening — warm re-lock + snap-on-discontinuity recover cleanly from a dropout or transport jump.
  • Observability — six-state lock model (zt_sync_state: off/waiting/dropout/transport/chasing/locked) via zt_midi_clock_get_status() → F11 lock meter + Lua zt.transport.sync_state/sync_bpm/sync_offset_ms + the sync notifier. sync_offset_ms conf key (legacy alias ableton_link_offset_ms).
  • Testsphase_chase, midi_timestamp, midi_clock_sync, and Chris's midi_clock_estimator (the 4 suites that take the harness from 14 → 18 when this lands).

INVARIANTS for any new or modified page

These rules are non-negotiable. Every one of them was learned the hard way during the CCizer + SysEx + CC-Console work (PRs #78–#84, #95, #96). Don't re-learn them.

  1. Every key handler that mutates visible state ends with need_refresh++; — including enter() and any background pump that changes what should be on screen. Forget this and arrow keys silently mutate state without a redraw; the page looks frozen until some other path bumps the flag. main.cpp gates ActivePage->draw() on need_refresh != 0.

  2. Interactive elements are real widgets, not ASCII art. A slider is a ValueSlider registered with UI->add_element, drawn by UI->draw(S), mutated by ValueSlider::mouseupdate, and absorbed by the page's update() reading the changed flag. Never ship printBG text bars as a stand-in for "we'll iterate later" — visually identical in one screenshot, completely unclickable to the user. The pool-of-N pattern (pre-allocate N widgets in the ctor, position the visible window per-frame, hide off-screen ones with xsize=0) is the idiom for variable-count grids; CUI_CcConsole.cpp::position_sliders is the canonical worked example.

  3. Any list pane uses a real ListBox subclass. Never hand-draw > filename with a custom highlight. F11 SkinSelector / F4 / Shift+F4 / Shift+F3 all share one pattern, documented in references/list-pane-idiom.md. New list-driven pages copy that pattern verbatim — anonymous-namespace : ListBox subclass, is_sorted=true, use_checks=true, use_key_select=true, Space-applies override on update(), never call ListBox::OnSelect from the subclass.

  4. Per-frame need_redraw = 1 on always-visible widgets. UserInterface::draw skips elements with need_redraw == 0. If any sibling fires needaclear++ the whole element area is cleared and stale-need_redraw widgets vanish. The page's update() (or position_sliders()) stamps need_redraw = 1 on the listbox, the grid-focus stub, and every visible pool widget, every frame. Worked example: PR #96.

  5. Each visual column is its own print / printBG anchored to its own col(CC_*_COL) constant. Never pack adjacent columns into one packed snprintf — adjacent fields will smash into each other (PBPitchbend, LeUani) the first time a string is longer than expected. Headers and data rows reference the same constants so they stay aligned.

  6. Tab cycles UI->cur_element between IDs. Page-level "focus" is derived from cur_element each frame, not stored. Use a 1x1 UserInterfaceElement stub (e.g. MmGridFocus, CcSlotGridFocus) for non-list panes that need Tab focus.

  7. Pages whose own ESC handler matters add their STATE_* to the global ESC exclusion list in main.cpp::global_keys (the list with STATE_HELP / ABOUT / LUA_CONSOLE / SONG_MESSAGE / KEYBINDINGS). Otherwise the global handler eats ESC first and the page's "ESC cancels capture" hint is a lie. PR #95 was a one-line fix for STATE_KEYBINDINGS.

  8. Visual verification before claiming done. Build green + ctest green ≠ "the page works." Use the headless framebuffer screenshots (--headless --script ... shot, see "Launching the app") when you need to visually verify features. Drive each user-visible control: key for F-keys / arrows / Tab / Space / Enter / Esc, and click <x> <y> for mouse-only widgets, then shot and Read the PNG. If you genuinely cannot do it, say so explicitly — don't ship hope.

The full list-pane construction pattern (with code) lives in references/list-pane-idiom.md. Read it before adding any new page that has a list, a grid, or both.

Deeper references (load on demand)

PR discipline

  • Push topic branches to origin, open PRs against master, don't merge without maintainer approval.
  • Keep PRs small and focused: 1 feature, ~30–300 LOC.
  • doc/CHANGELOG.txt gets a section per PR.
  • Tag AI-assisted commits: Co-Authored-By: Claude <noreply@anthropic.com> (or current model identifier).
  • Don't duplicate content across PRs.

Bug reports = branch + PR + push, no asking

When Esa reports a bug, the default response is: investigate → fix → branch → commit → push → open PR. Do not ask "want me to fix?" or "should I open a PR?" — that's the implicit ask. Just do it.

The only acceptable preamble is one or two sentences naming the root cause and the fix, then go.

  • Branch: esa/<short-descriptive> (e.g. esa/cc-console-file-switch-leak).
  • Build clean + run ctest before pushing.
  • PR body: summary, root cause, fix, test plan checklist. Quote the user's own description of the symptom verbatim if they gave one — that anchors the PR to the report.
  • Push to origin; Esa has write access.

Ask for clarification only if the bug report itself is ambiguous about the symptom — never about whether to fix it. Reference: PR #106 (CC Console file-switch slider value leak, 2026-05-02).

Paketti as a feature reference

Paketti (a Renoise tool) is a reference for tracker quality-of-life features ported / portable to zTracker. Already landed: Replicate at Cursor, Clone Pattern, Humanize Velocities, MIDI Macro + Arpeggio editors. Future candidates: Fill-with-note-at-cursor, Find/Replace note, Transpose selection, Groove templates, Chord memory. zTracker is MIDI-only, so sample-editor features are N/A.


Update this skill (and the relevant reference file) when the project's context shifts materially: new architecture area, new feature region, new recurring foot-gun.

Install via CLI
npx skills add https://github.com/m6502/ztrackerprime --skill ztrackerprime
Repository Details
star Stars 26
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator