Skip to content

AMYboard: phase-lock sketch loop() to the sequencer downbeat#1123

Merged
bwhitman merged 4 commits into
mainfrom
sketch-loop-downbeat-sync
Jul 9, 2026
Merged

AMYboard: phase-lock sketch loop() to the sequencer downbeat#1123
bwhitman merged 4 commits into
mainfrom
sketch-loop-downbeat-sync

Conversation

@bwhitman

@bwhitman bwhitman commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes the "loop() sounds out of phase with AMYSequence" problem (e.g. dx7_groove_128_bpm on AMYboard World: the AMY-sequenced drums are locked to the absolute tick grid, but the sketch's local step counter starts at 0 whenever the sketch loads — some arbitrary 32nd note inside the bar — so the bass/piano/arp play at a constant phase offset from the drums).

loop() callbacks already fire on the AMY sequencer's absolute tick grid (tick % 6 == 0 at 48 PPQ), and tulip_amy_sequencer_hook already hands the tick to the callback — _guarded_loop was just discarding it. Two changes in _start_sketch_loop:

  1. Downbeat-aligned start: the first loop() call is held until the next bar boundary (tick % 192 == 0, 4/4 at 48 PPQ). Existing sketches' internal counters now start on the downbeat, in phase with AMYSequence bars, with no sketch changes. (Costs up to one bar of startup delay — ~1.9s at 128 bpm — which for music sketches means "starts on the one".)
  2. loop(step) signature: sketches can declare def loop(step): and get the global 32nd-note index on the bar-locked grid (step % 32 == 0 is always a downbeat). Deriving bar position from step instead of a local counter also can't drift if a callback is ever dropped (mp_sched_schedule queue full under heavy UI). Detection is a one-time TypeError fallback, so plain loop() sketches are unchanged.

The default sketch template (canonical copy in amyboard.py, both fallback copies in spss.js) now uses loop(step) and documents the grid. Verified the three template copies are byte-identical and simulated the dispatch logic for both signatures (step-aware first call lands on step % 32 == 0; plain first call lands on tick % 192 == 0).

Note: the server-side sketch-generator prompt (railway) still teaches the old def loop(): convention — worth updating it to loop(step) so generated sketches are phase-locked by construction.

🤖 Generated with Claude Code

@bwhitman

bwhitman commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Added a commit bumping the amy submodule to latest amy main (3a7c176, 1.2.43), so merging this PR brings both the loop() downbeat sync and current AMY in one go. Note this is amy main — it does not include shorepine/amy#854 (sequencer-in-render-loop), which is still open; #1120 remains the test pin for that.

bwhitman and others added 2 commits July 9, 2026 14:46
Sketch loop() callbacks fire on the AMY sequencer's absolute tick grid
(every 6 ticks = a 32nd note), the same clock AMYSequence events use --
but a sketch's own step counter starts at 0 whenever the sketch loads,
which is some arbitrary 32nd note inside the bar. Any pattern built on
that counter plays at a constant phase offset from AMY-sequenced
patterns (e.g. dx7_groove_128_bpm's bass/piano/arp vs. its drums).

Two changes in _start_sketch_loop:
- The first loop() call is held until the next bar boundary
  (tick % 192 == 0 at 48 PPQ, 4/4), so existing sketches' counters
  start on the downbeat, in phase with AMYSequence bars.
- If the sketch declares loop(step), it gets the global 32nd-note
  index on the bar-locked grid (step % 32 == 0 is always a downbeat).
  Deriving position from step instead of a local counter also can't
  drift if a callback is ever dropped (mp_sched_schedule queue full).
  Plain loop() still works via a one-time TypeError fallback.

The default sketch template (amyboard.py and both spss.js copies)
now uses loop(step) and documents the grid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bwhitman bwhitman force-pushed the sketch-loop-downbeat-sync branch from 2b0fa13 to ca987a6 Compare July 9, 2026 21:47
bwhitman and others added 2 commits July 9, 2026 14:53
All sketch guidance now documents the loop(step) convention: step is the
global 32nd-note index on the sequencer's bar-locked grid (step % 32 == 0
is always a downbeat, the same grid AMYSequence events fire on), and the
first call lands on a downbeat. Every example converts from a hand-rolled
call counter (or, in the generator's arpeggio example, wall-clock
BPM-to-milliseconds math) to the step grid. The zero-argument loop() is
documented as legacy and still works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
python3 tulip/server/sync_amy_docs.py against amy@3a7c176; only the
source marker changed (doc contents are identical to the previous pin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bwhitman

bwhitman commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Two more commits, per Brian:

  • loop(step) is now taught everywhere: AMY_AGENTS.md (read at request time by the sketch generator), the generator's base system prompt in tulip/server/amyboardworld_db_api.py (OUTPUT CONTRACT + all four example sketches — the arpeggio example also drops its wall-clock BPM math for the step grid), and docs/amyboard/online.md. Zero-arg loop() is documented as legacy.
  • Refreshed tulip/server/refdocs/amy/ for the 1.2.43 pin per AGENTS.md (content identical, marker SHA updated).

Also: the amy 1.2.43 pin in this PR includes shorepine/amy#854 (merged as 1.2.42), so merging this brings the render-loop sequencer, the downbeat sync, and the guidance in one go. Closed the #1120 test pin as obsolete.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🔌 AMYboard PR preview

Editor + flasher: https://amyboard-pr-1123.vercel.app/editor/

This preview bundles this PR's firmware — its flasher only flashes this build (not the release). Rebuilt on every push; removed when the PR closes.

The hardware CI has been kicked off and should return within a few minutes, stand by!

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🌷 Tulip Web PR preview

Tulip Web: https://tulip-pr-1123.vercel.app/run/

Flash a Tulip to this build: on-device run tulip.upgrade(pr=1123).

Rebuilt on every push; removed when the PR closes.

@bwhitman bwhitman merged commit 7a158f2 into main Jul 9, 2026
3 checks passed
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🎛️ HW CI (physical bench)

AMYboard (USB-MIDI + AMY zP → audio; built-in tones + AMYboard World sketches acid/house/woodpiano over the SysEx control API): ❌ FAIL — a check did not match, or the run errored. See the log/artifacts.

Tulip (TULIP4_R11; serial-REPL audio + WiFi screenshot): ✅ PASS — flashed this PR’s firmware; all checks matched the references.

⬇️ Artifacts: recordings · screenshot · serial logs · run logs

Self-hosted bench. Audio spectral-compared to ref/hwci_basic.wav, the AMYboard World sketch refs (ref/{acid_generator,house_generator,woodpiano}.wav) + ref/tulip_basic.wav; Tulip screenshot pixel-compared to ref/tulip_screenshot.png. Both analog outs share one capture card, so the tests run sequentially.

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