diff --git a/esp32/AGENTS.md b/esp32/AGENTS.md index 554286f..87b80d5 100644 --- a/esp32/AGENTS.md +++ b/esp32/AGENTS.md @@ -61,7 +61,8 @@ Notes: | `link_listener.*` + `link_protocol.*` | Link adapter; `link_protocol.c` is our own ~100-line gossip parser (the vendored `lib/link/` SDK is **not** used at runtime) | | `link_measurement.{h,c}` + `link_measurement_io.cpp` | Link measurement (ping/pong) client; `link_measurement.c` is pure TLV build/parse + median/offset math (host-tested), `link_measurement_io.cpp` is the thin WiFiUDP unicast glue — pinger-only, no PingResponder | | `midi_clock.*` · `midi_bpm.*` · `midi_bpm_calc.*` | USB-MIDI adapter; `midi_bpm_calc` is the pure, host-tested BPM math (symlinked from `X32MidiClock/`). `midi_clock` also owns the shared USBMIDI endpoint + `midi_clock_send_f8()` for clock OUT | -| `midi_clock_out.{h,c}` + `midi_clock_out_io.cpp` | LNK-027 Link→USB-MIDI clock OUT; `midi_clock_out.c` is the pure, host-tested 24-PPQN tick scheduler (quantizes `tempo_source_beats_now()` to `0xF8` ticks), `midi_clock_out_io.cpp` is the 1ms FreeRTOS task + TinyUSB writes | +| `clock_ticker.{h,c}` | LNK-027/028 shared pure tick engine: quantizes `tempo_source_beats_now()` to N PPQN pulses (phase-locked, re-primes on re-origin) + a `BarReset` tracker that fires once per bar boundary (analog reset pulse). Host-tested | +| `midi_clock_out.{h,c}` + `midi_clock_out_io.cpp` | LNK-027 Link→USB-MIDI clock OUT; `midi_clock_out.{h,c}` is a thin 24-PPQN adapter over `clock_ticker`, `midi_clock_out_io.cpp` is the 1ms FreeRTOS task + TinyUSB writes | | `bpm_tracker.*` | change/threshold detection | | `bar.{h,c}` | ARC-003: a bar is `quantum` beats — `bar_beats()` / `bar_ms()`; one place owns beats-per-bar so non-4/4 `quantum` scales refresh/resend (was a hardcoded `4 *`) | | `osc_out.*` / `osc_sender.*` | OSC packet build / UDP send to the mixer | diff --git a/esp32/X32Link/clock_ticker.c b/esp32/X32Link/clock_ticker.c new file mode 100644 index 0000000..548e3b4 --- /dev/null +++ b/esp32/X32Link/clock_ticker.c @@ -0,0 +1,60 @@ +// Shared pure clock-tick scheduler — see clock_ticker.h. +#include "clock_ticker.h" +#include + +void clock_ticker_reset(ClockTicker* s) { + s->last_tick = 0; + s->primed = false; +} + +int clock_ticker_ticks_due(ClockTicker* s, double beats_now, int ppqn, int max_burst) { + if (ppqn <= 0) return 0; + if (beats_now < 0.0) beats_now = 0.0; + int64_t tick = (int64_t)floor(beats_now * (double)ppqn); + + if (!s->primed) { // first observation: align the grid, no backlog + s->primed = true; + s->last_tick = tick; + return 0; + } + + int64_t due = tick - s->last_tick; + if (due <= 0) return 0; // backward or same 1/ppqn-beat slot + + if (max_burst > 0 && due > (int64_t)max_burst) { + // Big forward jump (tempo re-origin / long stall): realign to the new + // position instead of flooding the output with a catch-up burst. + s->last_tick = tick; + return 0; + } + + s->last_tick = tick; + return (int)due; +} + +void bar_reset_reset(BarReset* s) { + s->last_bar = 0; + s->primed = false; +} + +bool bar_reset_due(BarReset* s, double beats_now, double quantum) { + if (quantum <= 0.0) return false; + if (beats_now < 0.0) beats_now = 0.0; + int64_t bar = (int64_t)floor(beats_now / quantum); + + if (!s->primed) { + s->primed = true; + s->last_bar = bar; + return false; + } + + int64_t delta = bar - s->last_bar; + if (delta == 1) { // clean crossing into the next bar + s->last_bar = bar; + return true; + } + if (delta != 0) { // backward or multi-bar jump (re-origin): realign, + s->last_bar = bar; // don't fire a false downbeat + } + return false; +} diff --git a/esp32/X32Link/clock_ticker.h b/esp32/X32Link/clock_ticker.h new file mode 100644 index 0000000..c9406a2 --- /dev/null +++ b/esp32/X32Link/clock_ticker.h @@ -0,0 +1,53 @@ +#pragma once +// Shared, pure clock-tick scheduler for tempo fan-out (LNK-027 USB-MIDI clock +// out at 24 PPQN; LNK-028 analog Eurorack sync at a configurable PPQN). It +// quantizes a *monotonic beat position* (from the Link phase pipeline) to N +// pulses per beat and reports how many pulses are due since the last call — +// phase-locked (first pulse on phase-zero), re-priming on a large jump +// (re-origin / stall) instead of flooding. A companion BarReset tracker fires +// once per bar boundary, for the analog "reset" trigger. No Arduino/hardware +// dependency — host-tested in test/test_clock_ticker.c. Glue owns the timer/RMT +// and the actual edge emission. +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + int64_t last_tick; // last emitted 1/ppqn-beat grid index + bool primed; // false until the first beats_now aligns the grid +} ClockTicker; + +// Clear state: the next ticks_due() call re-primes the grid (emits nothing). +void clock_ticker_reset(ClockTicker* s); + +// Advance to beats_now and return how many pulses to emit now, at `ppqn` pulses +// per beat: +// - first call after a reset primes the grid and returns 0 (no backlog dump); +// - a backward or same-slot move returns 0; +// - a forward move of N pulses returns N, UNLESS N exceeds max_burst (a tempo +// re-origin or a long stall), in which case it re-primes and returns 0 — the +// caller keeps phase alignment rather than flooding the output. +// beats_now < 0 is treated as 0 (a "not valid yet" sentinel from the glue). +// ppqn <= 0 is a no-op that returns 0 without touching state. +int clock_ticker_ticks_due(ClockTicker* s, double beats_now, int ppqn, int max_burst); + +typedef struct { + int64_t last_bar; // last observed bar index = floor(beats / quantum) + bool primed; +} BarReset; + +void bar_reset_reset(BarReset* s); + +// True exactly once when beats_now crosses forward into the next bar (bar = +// floor(beats / quantum)). First call after a reset primes and returns false. A +// same-bar read returns false. A backward move or a forward jump of more than one +// bar (a re-origin) re-primes and returns false — better to miss a reset than to +// fire a false downbeat. quantum <= 0 returns false. +bool bar_reset_due(BarReset* s, double beats_now, double quantum); + +#ifdef __cplusplus +} +#endif diff --git a/esp32/X32Link/midi_clock_out.c b/esp32/X32Link/midi_clock_out.c index 81765a3..b1fe09a 100644 --- a/esp32/X32Link/midi_clock_out.c +++ b/esp32/X32Link/midi_clock_out.c @@ -1,32 +1,11 @@ -// Pure 24-PPQN MIDI-clock-out scheduler — see midi_clock_out.h. +// 24-PPQN MIDI clock OUT — a thin adapter over the shared clock_ticker engine. +// See midi_clock_out.h. #include "midi_clock_out.h" -#include void midi_clock_out_reset(MidiClockOut* s) { - s->last_tick = 0; - s->primed = false; + clock_ticker_reset(s); } int midi_clock_out_ticks_due(MidiClockOut* s, double beats_now, int max_burst) { - if (beats_now < 0.0) beats_now = 0.0; - int64_t tick = (int64_t)floor(beats_now * (double)MIDI_CLOCK_OUT_PPQN); - - if (!s->primed) { // first observation: align the grid, no backlog - s->primed = true; - s->last_tick = tick; - return 0; - } - - int64_t due = tick - s->last_tick; - if (due <= 0) return 0; // backward or same 1/24-beat slot - - if (max_burst > 0 && due > (int64_t)max_burst) { - // Big forward jump (tempo re-origin / long stall): realign to the new - // position instead of flooding the bus with a catch-up burst. - s->last_tick = tick; - return 0; - } - - s->last_tick = tick; - return (int)due; + return clock_ticker_ticks_due(s, beats_now, MIDI_CLOCK_OUT_PPQN, max_burst); } diff --git a/esp32/X32Link/midi_clock_out.h b/esp32/X32Link/midi_clock_out.h index c27d287..33bcc20 100644 --- a/esp32/X32Link/midi_clock_out.h +++ b/esp32/X32Link/midi_clock_out.h @@ -1,16 +1,11 @@ #pragma once -// Pure 24-PPQN clock-tick scheduler for MIDI clock OUT (LNK-027), and the -// shared tick engine a future analog sync out (LNK-028) can reuse. No -// Arduino/USB dependency — host-tested in test/test_midi_clock_out.c. The thin -// glue (midi_clock_out_io.cpp) owns the timer task and the TinyUSB writes. -// -// The scheduler quantizes a *monotonic beat position* (as produced by the Link -// phase pipeline, LNK-017/019) to 24 ticks per beat and reports how many 0xF8 -// ticks are now due since the last call. Because the grid is derived from the -// session timeline, ticks are phase-locked: the first tick lands on phase-zero -// and the clock stays bar-aligned, not just tempo-matched. -#include -#include +// 24-PPQN MIDI clock OUT scheduler (LNK-027) — a thin adapter over the shared, +// pure clock_ticker engine (LNK-028 uses the same engine at a configurable +// PPQN). No Arduino/USB dependency — host-tested in test/test_midi_clock_out.c. +// The thin glue (midi_clock_out_io.cpp) owns the timer task and the TinyUSB +// writes. Ticks are phase-locked: the first tick lands on phase-zero and the +// clock stays bar-aligned, not just tempo-matched. +#include "clock_ticker.h" #ifdef __cplusplus extern "C" { @@ -18,24 +13,16 @@ extern "C" { #define MIDI_CLOCK_OUT_PPQN 24 -typedef struct { - int64_t last_tick; // last emitted 1/24-beat grid index - bool primed; // false until the first beats_now aligns the grid -} MidiClockOut; +// MIDI clock out is a ClockTicker fixed at 24 PPQN. +typedef ClockTicker MidiClockOut; // Clear state: the next ticks_due() call re-primes the grid (emits nothing). void midi_clock_out_reset(MidiClockOut* s); // Advance the scheduler to the current monotonic beat position and return how -// many 0xF8 ticks to emit now. -// - first call after a reset primes the grid to beats_now and returns 0 (no -// backlog dump); -// - a backward or same-slot move returns 0; -// - a forward move of N 1/24-beat slots returns N, UNLESS N exceeds -// max_burst (a tempo re-origin or a long stall), in which case it re-primes -// and returns 0 — the caller keeps phase alignment rather than flooding the -// bus with a catch-up burst. -// beats_now < 0 is treated as 0 (a "not valid yet" sentinel from the glue). +// many 0xF8 ticks to emit now (24 PPQN). Same semantics as +// clock_ticker_ticks_due() with ppqn=24 — prime-on-first, no backward/backlog, +// re-prime (return 0) on a forward jump larger than max_burst. int midi_clock_out_ticks_due(MidiClockOut* s, double beats_now, int max_burst); #ifdef __cplusplus diff --git a/esp32/tasks/LNK-028.md b/esp32/tasks/LNK-028.md index 0c3f3b4..cc77b35 100644 --- a/esp32/tasks/LNK-028.md +++ b/esp32/tasks/LNK-028.md @@ -78,15 +78,52 @@ drive a modular clock input directly — no computer in the patch. decision with LNK-027)? 5. **DIN-sync (Sync24)** — worth a 5-pin DIN variant (24 PPQN + run/stop on one connector) for Roland-style gear, or keep it to bare 3.5 mm clock/reset? + *(Owner input: deferred follow-on — see "Target gear" below. Reach feature; no + Roland gear owned; BSP takes 24 PPQN on 3.5 mm anyway.)* 6. **Which board** — the Waveshare touch board has free GPIOs but no jacks; this implies a small add-on/protoboard or a board revision. Scope the hardware deliverable, not just firmware. +## Target gear & format priorities (owner input, 2026-07-04) + +Concrete gear the owner runs, which anchors the format priorities: +- **Moog DFAM** — sync is the **ADV/CLOCK** input, *step-advance*: one rising edge + = advance one sequencer step. **No reset input**, no tempo abstraction. Drive + with the clock pulse at the PPQN matching the desired step rate (2 PPQN = 8th + steps, 4 = 16th). Because there's no reset, a dropped/added pulse slips the + pattern permanently → **pulse accuracy is critical** (reinforces RMT, not + loop-polled edges). Triggers Eurorack-ish; **~5 V target** (3.3 V marginal). +- **Arturia BeatStep Pro** — **CLOCK IN** (3.5 mm) with a *selectable* rate + (1 step / 1 / 2 / 24 / 48 PPQN); our `{1,2,4,8,24}` covers 1/2/24 directly. Acts + as a **clock hub**: redistributes to MIDI-DIN / USB / 2× CV-gate / 8 drum gates, + so one clock line from our box → BSP → clocks the DFAM + the rest. **A single + clock jack may be enough for the whole rig** when a BSP fronts it. +- **Vintage Roland / DIN-sync (Sync24)** — owner owns none; **attractive for + product reach**, not owner-critical → keep as a lower-priority follow-on, not + MVP. Note the BSP already accepts **24 PPQN on its 3.5 mm clock in** (DIN-sync + *rate* without the 5-pin connector), so 3.5 mm 24 PPQN covers BSP-fronted + DIN-rate gear; the physical 5-pin DIN is only needed for direct-to-Roland. + +**Resulting priorities:** +1. **MVP:** one rock-solid **configurable-PPQN clock pulse on a 3.5 mm jack @ ~5 V** + (covers DFAM, BSP, Eurorack, Volca). Jitter/accuracy is the top spec. +2. **Secondary:** a **reset** jack (bar downbeat) for sequencers that have one + (not the DFAM; useful for Eurorack/others). +3. **Follow-on:** 5-pin **DIN-sync** for direct vintage-Roland support — reach + feature, defer. + +This argues open Q6 (jack count) toward **1–2 jacks (clock, + optional reset)**, +and open Q5 (DIN) toward a **deferred follow-on**. + ## Acceptance Criteria -- [ ] Shared clock-tick scheduler (pure, host-tested; reused by LNK-027) emits - ticks at a configurable PPQN, first tick aligned to `tempo_source_phase()` - phase-zero, gated on `tempo_source_phase_valid()`. +- [x] Shared clock-tick scheduler (pure, host-tested; reused by LNK-027) emits + ticks at a configurable PPQN, first tick aligned to phase-zero. **DONE:** + `clock_ticker.{h,c}` (`clock_ticker_ticks_due(state, beats, ppqn, max_burst)` + + a `BarReset` tracker for the reset pulse); LNK-027's `midi_clock_out` is + now a thin 24-PPQN adapter over it (its 8 tests still green + 11 new + `test_clock_ticker` tests). The phase-valid gate + re-origin realign live in + the glue (reused from LNK-027). *(One engine now, per the note below.)* - [ ] Analog clock + reset pulses appear on GPIO, level-shifted to ~5 V on 3.5 mm jacks, with the protection/level-shift circuit documented (schematic or BOM). - [ ] New NVS config (`analog_sync_*`) exposed in the web config UI and the touch @@ -97,7 +134,9 @@ drive a modular clock input directly — no computer in the patch. clock locked to the Link bar with no visible jitter/flam, and reset firing on the downbeat, across a transport stop/start and a tempo change. - [ ] No regression to Link → OSC, USB-MIDI-in, or (if landed) LNK-027 MIDI-out. -- [ ] `esp32/AGENTS.md` module table updated for the new module. + *(Scheduler slice: full host suite green, LNK-027 MIDI-out behavior unchanged.)* +- [x] `esp32/AGENTS.md` module table updated (`clock_ticker`; `midi_clock_out` + re-noted as its adapter). ## Notes @@ -109,3 +148,19 @@ drive a modular clock input directly — no computer in the patch. - Filed from a feature ask (2026-07-03); hardware design (level-shift + jacks) is the gating unknown, hence `priority: low` / `needs-triage` until the board question (add-on vs revision) is decided. + +## Progress + +**2026-07-04 — shared pure scheduler landed (AC #1).** Extracted `clock_ticker.c` +— the configurable-PPQN tick engine both LNK-027 and LNK-028 want — with a +`BarReset` tracker that fires once per bar boundary for the analog reset pulse. +TDD'd (11 host tests: PPQN 1/4/24 cadence, jump-reprime, backward/negative guards, +bar-reset crossing/backward/multi-bar-jump/bad-quantum). Refactored LNK-027's +`midi_clock_out` into a thin 24-PPQN adapter over it — behavior-preserving, its 8 +tests unchanged and green. Firmware compiles (86% flash); AGENTS.md updated. + +**Still gated (needs the hardware/board decision — open Q6):** the GPIO/RMT edge +emission, 3.3→5V level-shift + protection + 3.5mm jacks, `analog_sync_*` config + +web/touch UI, and on-scope hardware verification. Those wait on whether this is an +add-on board vs a revision, and the level-shifter approach (open Q1). The pure +timing core is decision-independent and is now in place for whenever that lands. diff --git a/esp32/test/Makefile b/esp32/test/Makefile index 1f3b9da..90b63cb 100644 --- a/esp32/test/Makefile +++ b/esp32/test/Makefile @@ -23,10 +23,11 @@ TOUCHUI = ../X32Link/touch_ui.c SNAP = ../X32Link/tempo_snapshot.c BAR = ../X32Link/bar.c MIDIOUT = ../X32Link/midi_clock_out.c +CLOCKTICK = ../X32Link/clock_ticker.c .PHONY: all clean -all: test_bpm_tracker test_link_protocol test_link_measurement test_link_phase test_led_phase test_wifi_down_blink test_app_config test_osc_out test_midi_bpm_calc test_bpm_publisher test_osc_in test_fader_db test_web_status_json test_touch_ui test_tempo_snapshot test_bar test_midi_clock_out +all: test_bpm_tracker test_link_protocol test_link_measurement test_link_phase test_led_phase test_wifi_down_blink test_app_config test_osc_out test_midi_bpm_calc test_bpm_publisher test_osc_in test_fader_db test_web_status_json test_touch_ui test_tempo_snapshot test_bar test_midi_clock_out test_clock_ticker ./test_bpm_tracker ./test_link_protocol ./test_link_measurement @@ -44,6 +45,7 @@ all: test_bpm_tracker test_link_protocol test_link_measurement test_link_phase t ./test_tempo_snapshot ./test_bar ./test_midi_clock_out + ./test_clock_ticker ./check_docs.sh test_bpm_tracker: test_bpm_tracker.c $(TRACKER) $(UNITY) @@ -97,7 +99,10 @@ test_tempo_snapshot: test_tempo_snapshot.c $(SNAP) $(UNITY) test_bar: test_bar.c $(BAR) $(UNITY) $(CC) $(CFLAGS) $^ -o $@ -lm -test_midi_clock_out: test_midi_clock_out.c $(MIDIOUT) $(UNITY) +test_midi_clock_out: test_midi_clock_out.c $(MIDIOUT) $(CLOCKTICK) $(UNITY) + $(CC) $(CFLAGS) $^ -o $@ -lm + +test_clock_ticker: test_clock_ticker.c $(CLOCKTICK) $(UNITY) $(CC) $(CFLAGS) $^ -o $@ -lm clean: diff --git a/esp32/test/test_clock_ticker.c b/esp32/test/test_clock_ticker.c new file mode 100644 index 0000000..65b81d5 --- /dev/null +++ b/esp32/test/test_clock_ticker.c @@ -0,0 +1,98 @@ +// Host tests for the shared pure clock-tick scheduler (LNK-027/LNK-028). +#include "unity.h" +#include "clock_ticker.h" + +static ClockTicker ct; +static BarReset br; +void setUp(void) { clock_ticker_reset(&ct); bar_reset_reset(&br); } +void tearDown(void) {} + +/* ---- ClockTicker ------------------------------------------------------- */ + +void test_first_call_primes_and_emits_nothing(void) { + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, 10.0, 4, 4)); +} + +void test_ppqn4_four_ticks_per_beat(void) { + double slot = 1.0 / 4.0; + clock_ticker_ticks_due(&ct, 0.0, 4, 4); // prime at beat 0 + int total = 0; + for (int i = 1; i <= 4; i++) + total += clock_ticker_ticks_due(&ct, i * slot, 4, 4); + TEST_ASSERT_EQUAL_INT(4, total); // 4 PPQN over one beat +} + +void test_ppqn1_one_tick_per_beat(void) { + clock_ticker_ticks_due(&ct, 0.0, 1, 4); // prime + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, 0.5, 1, 4)); // same beat-slot + TEST_ASSERT_EQUAL_INT(1, clock_ticker_ticks_due(&ct, 1.0, 1, 4)); // next beat +} + +void test_ppqn24_matches_midi_cadence(void) { + double slot = 1.0 / 24.0; + clock_ticker_ticks_due(&ct, 0.0, 24, 4); // prime + int total = 0; + for (int i = 1; i <= 24; i++) + total += clock_ticker_ticks_due(&ct, i * slot, 24, 4); + TEST_ASSERT_EQUAL_INT(24, total); +} + +void test_large_jump_reprimes_no_flood(void) { + clock_ticker_ticks_due(&ct, 0.0, 4, 4); // prime + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, 100.0, 4, 4)); // realign + TEST_ASSERT_EQUAL_INT(1, clock_ticker_ticks_due(&ct, 100.0 + 0.25, 4, 4)); // resumes +} + +void test_backward_move_emits_nothing(void) { + clock_ticker_ticks_due(&ct, 5.0, 4, 4); + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, 4.0, 4, 4)); +} + +void test_negative_beats_and_bad_ppqn(void) { + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, -1.0, 4, 4)); // primes at 0 + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, 1.0, 0, 4)); // ppqn<=0 no-op + TEST_ASSERT_EQUAL_INT(0, clock_ticker_ticks_due(&ct, 1.0, -4, 4)); +} + +/* ---- BarReset ---------------------------------------------------------- */ + +void test_bar_reset_primes_then_fires_on_crossing(void) { + TEST_ASSERT_FALSE(bar_reset_due(&br, 0.0, 4.0)); // prime at bar 0 + TEST_ASSERT_FALSE(bar_reset_due(&br, 3.9, 4.0)); // still bar 0 + TEST_ASSERT_TRUE (bar_reset_due(&br, 4.1, 4.0)); // crossed into bar 1 + TEST_ASSERT_FALSE(bar_reset_due(&br, 5.0, 4.0)); // still bar 1 + TEST_ASSERT_TRUE (bar_reset_due(&br, 8.0, 4.0)); // bar 2 +} + +void test_bar_reset_backward_reprimes_no_fire(void) { + bar_reset_due(&br, 8.0, 4.0); // prime at bar 2 + TEST_ASSERT_FALSE(bar_reset_due(&br, 1.0, 4.0)); // jumped back -> reprime, no false downbeat + TEST_ASSERT_FALSE(bar_reset_due(&br, 1.5, 4.0)); // same bar 0 + TEST_ASSERT_TRUE (bar_reset_due(&br, 4.0, 4.0)); // clean crossing again +} + +void test_bar_reset_multibar_jump_reprimes_no_fire(void) { + bar_reset_due(&br, 0.0, 4.0); // prime bar 0 + TEST_ASSERT_FALSE(bar_reset_due(&br, 40.0, 4.0)); // +10 bars (re-origin) -> reprime, no fire + TEST_ASSERT_TRUE (bar_reset_due(&br, 44.0, 4.0)); // next clean crossing fires +} + +void test_bar_reset_bad_quantum(void) { + TEST_ASSERT_FALSE(bar_reset_due(&br, 4.0, 0.0)); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_first_call_primes_and_emits_nothing); + RUN_TEST(test_ppqn4_four_ticks_per_beat); + RUN_TEST(test_ppqn1_one_tick_per_beat); + RUN_TEST(test_ppqn24_matches_midi_cadence); + RUN_TEST(test_large_jump_reprimes_no_flood); + RUN_TEST(test_backward_move_emits_nothing); + RUN_TEST(test_negative_beats_and_bad_ppqn); + RUN_TEST(test_bar_reset_primes_then_fires_on_crossing); + RUN_TEST(test_bar_reset_backward_reprimes_no_fire); + RUN_TEST(test_bar_reset_multibar_jump_reprimes_no_fire); + RUN_TEST(test_bar_reset_bad_quantum); + return UNITY_END(); +}