Commit graph

9 commits

Author SHA1 Message Date
Me Here
da7c94e67f Implement per-track playback flow (rep / end / relative goto)
Adds the per-track end-action model designed in docs/track-format.md §3, end to
end across both engines, both firmwares, and the editors.

Grammar (parsed + serialized by engine.js and both app.py):
  rep=<n>     cycles before the end-action fires (default 1)
  end=stop    stop after rep cycles
  end=next    advance one track (sugar for end=+1)
  end=<±N>    relative goto after rep cycles (e.g. end=-2 = D.S.)
  (absent)    loop forever — the metronome default

Firmware runtime (pico-cp + pico-explorer): _on_new_bar now consults a per-track
_end_plan() and fires stop / gapless-advance / relative-goto at the right bar.
A cycle = b<bars>, else one master bar; fire bar = rep * cycle. Explicit end=
governs; with no end, the global Continue toggle stays a default (=end=next, still
needs b<bars>) so existing set-lists and the CONT UI are unchanged. _prepare_next
takes a target index; the seam machinery, _do_advance and live-sync all carry rep/end.

Editors (editor.html + editor-beta.html): state.rep/state.end thread through
applySetup / currentSetup / currentPatch so load -> edit -> save preserves the
flow; authoring is via the program-string field (no graphical control yet).

Tests: the 3 playback-flow vectors now pass on both engines (39 pass / 3 known).
Runtime decision logic (_end_plan / _goto_target) unit-tested for stop, rep,
relative goto clamp/wrap, and legacy-Continue precedence. Codec round-trip
verified idempotent. Both firmwares compile + mpy-cross clean.

Also: untrack stale __pycache__/*.pyc build artifacts and gitignore them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:37:06 -05:00
Me Here
9701f49913 Firmware: parse euclid, GM note-numbers, and unknown-sound fallback
Close three real parser divergences the conformance suite flagged on the device
side (pico-cp + pico-explorer) — cases where the firmware produced a different
groove/sound than the web for the same patch:

- Euclidean (k,n,rot) shorthand (e.g. kick:4(3,8)) — was silently dropped to a
  plain bar; now expands to the same hits as engine.js (added _euclid + parsing).
- GM note-number lane sounds (e.g. 36:4) — now resolve to the voice name (GM_NUM).
- Unknown sound names fall back to beep, matching the web.

vol/cd are NOT carried by the firmware by design: they are web-authoring fields
(the device has a hardware volume knob and no count-in). Documented as an
intentional, permanent host difference rather than a bug; the vol-and-countin
vector stays as expectFail[py] to mark the boundary.

tests/adapters/py_adapter.py: extract the new SOUND_GM/GM_NUM/_euclid nodes.
fixtures: euclid/unknown-sound/gm-note-number now pass on both engines.
docs §6 updated. node tests/run.mjs: 33 pass / 9 known, round-trips stable.
pico-explorer parser spot-checked identical to pico-cp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:15:25 -05:00
Me Here
bf74c860e5 Track format: unify default (no-pattern) groove across web + firmware
A lane with no =pattern produced different defaults on web vs device — a real,
shipped divergence the new conformance suite caught (e.g. hatClosed:4/2 in
"Four-on-the-floor" played steady 8ths in the browser but quarter-notes on the
device). Adopt one rule everywhere: every subdivision sounds at normal level,
accents fall ONLY on group starts (the grouping is the accent map).

- pico-cp/app.py, pico-explorer/app.py: off-beat subdivisions sound at normal (1)
  instead of resting (0); group-start accenting was already correct.
- src/engine.js: default beatsOn accents group starts only (was: every beat);
  laneCfgToStr isDefault check updated to match so round-trips stay idempotent.
- docs + fixtures: document the rule; default-pattern vectors now pass on both.

Audible effect (intended): device subdivided hat/ride lanes gain their off-beat
strokes (now match the web); web stops over-accenting every beat. Lanes with an
explicit =pattern are unchanged. Verified green via node tests/run.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:03:34 -05:00
Me Here
8fe9ade210 PM_X-1 0.0.5: clear logo/divider overlap + simpler always-on amp
User reported the y=28 divider crossed the bottom of the V in the VARASYS
logo. The logo is actually 29 px tall (gen_assets.py blob: w=156 h=29), so
positioned at y=8 it ran to y=37 - the divider sat ~halfway through the V.

Fix: logo now at y=4 (top-margin 4 px), divider at y=34 (~1 px clearance
below the logo bottom at y=33). Everything below the divider shifted down
6 px: BPM/time y=44, bar y=56, train y=64, tab y=78, title y=94, GRID_TOP=116,
LOG_TOP=224 with 5 rows (was 6).

Simpler amp control. 0.0.4's per-click _amp(True)/_amp(False) toggling was
both timing-sensitive (the brief 22 ms PWM burst would race with amp settling
time) and polarity-guessed. Replaced with: hold amp_en at the polarity-correct
"enable" value the whole time. One config flag (AMP_EN_ON_VALUE, default True)
controls polarity. If the user still hears no sound after dragging this on,
flip the flag to False and re-flash.

Also still check Settings -> Speaker. If a saved /settings.json from an earlier
build has speaker=auto, Live sync's heartbeat will silence the piezo - cycle
to "Always" with A in the Settings menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 23:28:09 -05:00
Me Here
c625a8aaa2 Firmware push fix on both Kit (0.0.24) + Explorer (0.0.4)
Diagnosed from the user's console output - 25 chunks pushed cleanly at
~124ms each, then stalled. Two coupled causes:

1) Bus contention. tick() and Live sync share self.midi with the chunk
   ACKs. While the device was processing a chunk, a Note On / Clock Out /
   Live-sync FULL heartbeat could land on the same MIDI OUT stream and
   the host's parser dropped the interleaved ACK SysEx.

   Fix: self._fw_pushing flag set on 0x21 BEGIN, cleared on 0x23 COMMIT
   or any error. midi_send / Clock Out / _sync_broadcast / _sync_broadcast_full
   all early-out when _fw_pushing is True. Only ACKs go out during a push.

2) SysEx assembler garbage. self._sx = bytearray() per chunk leaks ~70
   bytes / chunk that only GC'd every 50 chunks. 25 chunks of trash plus a
   slow heap walked the wrong way explains the ramp-up to 174 -> 119 -> 124
   ms ACK times. GC every chunk now (~30ms cost on RP2040/RP2350 with
   small heap) so the assembler buffer is always fresh.

Same patch on both pico-cp/ and pico-explorer/ since the bug is identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:51:46 -05:00
Me Here
3805c5ee00 PM_X-1 0.0.4 + editor push diagnostics
Layout fixes (user reported BPM/time were still bumping the header at 0.0.3):
- All Y coords below the header divider shifted down 6px: BPM 30->38,
  time 32->38, bar 44->50, train 52->58, setlist tab 66->72, title 82->88.
- GRID_TOP 104 -> 110.

Restored the Kit-style footer practice log:
- LOG_TOP=218, LOG_ROWH=14, LOG_ROWS=6.
- MAXLANES dropped from 6 visible to 4 visible (rowh capped at 26 so the grid
  doesn't run into the log). Tracks with more lanes still play silently.
- _build_scene now appends g_log (with a divider above it).
- draw_log() draws the current-track log into the footer; load() + _log_play()
  + the seam apply path all call it. The Practice-log menu entry is kept for
  the full scrollable history.

Editor diagnostics for the firmware push (the user got chunk-1 ACK then the
device's MIDI badge went gray, meaning chunks 2+ never reached it):
- editor.html + editor-beta.html _pushFirmware() now logs every MIDI output
  + input it sees along with which ones _isDevicePort() matched, plus per-
  chunk send/ACK timing for the first 3 chunks and any failed chunk.
- This narrows down whether the failure is (a) wrong-port routing
  (filter doesn't match the Pimoroni Explorer's name), (b) ACK never arriving
  back to the host, or (c) chunks sent fine but the device's RX buffer is
  dropping them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:40:11 -05:00
Me Here
ea7bb9bfee PM_X-1 0.0.3: compact 240x320 layout + piezo polarity flag + Pimoroni device filter
Three fixes the user reported on 0.0.2:

1. Layout still overlapped. 0.0.2 ported the Kit's pixel positions verbatim,
   but those were designed for 480 px of height; on 320 the same Y values
   stack on top of each other. This pass actually scales everything down:
   - BPM big number was FONT_L (~30 px tall) -> FONT_M (~16 px tall).
   - Time + bar meters were FONT_M -> FONT_S, tightly stacked at y=32/44
     instead of y=50/78.
   - Setlist tab + CONT y=66 (was 118); track title y=82 (was 134).
   - GRID_TOP=104 (was 138). Frees ~34 px more vertical room for the
     pad grid.
   - Modal panels: PX/PW shrunk to use less of the 240-wide canvas, RH
     22 (was 26), inter-line spacing 13-14 (was 14-16). Title strings
     trimmed ("Practice log" instead of "Practice log (this track)").

2. No sound from the piezo. Two likely causes:
   - SPEAKER_AUTO_MUTE was True. Live sync sends a FULL heartbeat over
     USB-MIDI every 5s; the firmware sees those bytes and treats it as
     "host listening" -> mutes the piezo. Default now False on Explorer
     (toggle to Auto in Settings if you ARE using "Device audio" in the
     editor).
   - AMP_EN polarity. Added AMP_EN_ACTIVE_HIGH config flag (default True)
     and a _amp(on) helper. If your specific board's amp is active-low,
     flip to False at the top of CONFIG.

3. Firmware push stalled at chunk 1. Editor's _isDevicePort() only matched
   "pico" / "circuitpython" / "usb_midi"; the Pimoroni Explorer reports
   under a different name, so the editor broadcast to all MIDI outputs
   and the ACK got lost in routing. Filter now also matches "pimoroni",
   "explorer", "rp2350", and "varasys" (future-proofing).

The .mpy build dropped from 29.8 KB to 29.3 KB (smaller font footprint plus
fewer modal hint strings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:05:56 -05:00
Me Here
51c81b45e0 PM_X-1 0.0.2: portrait flip + Kit-style layout (no more overlapping bits)
The 320x240 landscape layout was too cramped vertically; multiple elements
sat on the same Y rows, especially the BPM big-number area and the bar/time
meters, plus the run dot collided with the MIDI/USB icons.

Switching to portrait at 240x320 (display.rotation = 270; user holds the
device with A/B/C buttons along the top) gives the same vertical real
estate the PM_K-1 Kit's portrait UI uses, just narrower. Layout now mirrors
the Kit:
- y 0..28: header (logo + version + MIDI/USB icons + run dot)
- y 44:    BPM big (right)
- y 50:    elapsed time (left, FONT_M)
- y 78:    bar counter (left, FONT_M)
- y 100:   ramp / gap-trainer indicators
- y 118:   setlist tab + CONT (single row)
- y 134:   track title (FONT_M)
- y 138+:  pad grid (up to 6 lanes, taller rowh ceiling now 30px)

Plus:
- DISPLAY_ROTATION constant near the top of CONFIG so the user can flip it
  to 90 / 180 if their orientation differs.
- Pad grid uses px0=48 (was 60) since the lane label column has less
  horizontal room at 240 width; max 6-char labels.
- Removed the inline modal hints (e.g. "X/Z move, A select, C close") that
  would have collided with the modal titles at 240 width. The Help screen
  documents the modal nav pattern, which is consistent across modals.
- HELP_PAGES page 1 leads with "Hold portrait with A/B/C on top."
- README documents the rotation flag.

Bumps Explorer to 0.0.2. .mpy can be pushed via the editor's Update firmware
flow (device id reply = X;0.0.1 -> editor fetches the right .mpy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 21:37:12 -05:00
Me Here
3192f3debc PM_X-1 0.0.1: Pimoroni Explorer sibling firmware + Kit 0.0.23 device-id reply
Adds pico-explorer/ as a parallel CircuitPython firmware target alongside the 52Pi
Kit in pico-cp/. Same engine, same program-string grammar, same programs.json, same
live-sync protocol. Read-only on the device (no on-device beat editing); the web
editor's Live sync mirrors all edits in real time and the Explorer emits its own
play/stop/bpm/sel deltas back.

Hardware (Pimoroni Explorer PIM744):
- RP2350B + 2.8" ST7789V 320x240 LCD (8-bit parallel; CircuitPython's official
  board definition pre-builds the BusDisplay so we just use board.DISPLAY).
- 6 user buttons - A/B/C on the left of the screen, X/Y/Z on the right.
- Piezo speaker on GP12 (PWM) with amp enable on GP13.
- I2C QwSTEMMA on GP20/21 - reserved, unused by the firmware.
- No touchscreen, no joystick, no RGB LED. Run state shows on a tiny on-screen dot.

Buttons:
- A = play/stop. B = tap tempo. C = menu.
- X = prev track (hold-repeat). Z = next track (hold-repeat).
- Y = tempo -1 (hold-repeat; -5 after 1.5s).
- X+Z chord = tempo +1 (mirrors Y).
- In a menu: X/Z move the row cursor, Y decrements, A cycles/increments/selects,
  B = back, C = close.

Files added:
- pico-explorer/{boot.py, code.py, app.py, programs.json, README.md}.
  app.py = 1444 lines (~73KB source -> 29.8KB compiled .mpy).
- info-explorer.html.

Files touched:
- pico-cp/app.py: bump to 0.0.23. Version-query (SysEx 0x02 -> 0x03) reply now
  includes the device id as "K;<version>" (backward-compat: editor parses
  "contains ';'?" - old firmware sent bare version, treated as K).
- editor.html + editor-beta.html: _parseDeviceReply() splits id;version, FW_PATHS
  maps id to .py/.mpy URL pair, so Update firmware now pushes the right binary.
- build.sh + deploy.sh: precompile pico-explorer/app.py -> dist/explorer-app.mpy,
  zip pm_x1_circuitpy.zip alongside pm_k1_circuitpy.zip, ship
  pico-explorer-app.{py,mpy} next to pico-cp-app.{py,mpy}.
- docs/livesync-protocol.md: new section 7 - per-device emit/apply matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:43:38 -05:00