Commit graph

347 commits

Author SHA1 Message Date
Me Here
185ed7736b pm-mobile: more vertical breathing room between sections
Bump the #mid inter-section gap and add padding above the first section so
the top bar, track panel, tempo, lanes and transport are less crowded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:51:46 -05:00
Me Here
80cebaad5a pm-mobile: ~75% taller transport buttons (min-height 38->66px), raise transport cap to fit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:42:47 -05:00
Me Here
ee0c6f329a pm-mobile: help tour covers tempo-nudge + prev/next; coachmark can span a group
- showTour now unions the rects of all elements matching a step's selector,
  so a step can highlight a row of buttons.
- New steps: "Nudge the tempo" (the four ±10/±1 buttons) and
  "Previous / next track" (the ⏮/⏭ buttons).
- Trim the now-duplicated ±10/±1 mention from the Tempo step; note the TAP
  button there instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:45:55 -05:00
Me Here
4401be3c5d pm-mobile: Controls help covers only icons+volume, not the pickers; drop logo from copy
Wrap the brand/icon row and volume slider in #topctl and aim the Controls
coachmark there, so the set-list / track pickers are no longer inside the
highlight. Reword the step to stop describing the logo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:44:44 -05:00
Me Here
355405d946 pm-mobile: help tour runs top-to-bottom; Controls highlights whole top bar
Reorder coachmarks to follow the on-screen layout top→down (track-settings
panel now comes before the tempo, matching its new position), and point the
Controls step at #top so the hole covers the logo, icons and volume slider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:41:44 -05:00
Me Here
56d8a4c093 pm-mobile: taller logo + taller transport buttons
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:39:53 -05:00
Me Here
344ff43ceb pm-mobile: shorter transport at bottom, header icons by logo, full-width volume, beat-glow TAP, roller wheel, track-panel above tempo
- Transport buttons reduced in height and pinned to the bottom; a flexible
  gap sits between the lanes and the buttons. Buttons shrink as more lanes
  are added (lanes grow, the spacer collapses, then the button grid shrinks).
- Header: share/help/theme/fullscreen icons moved up onto the logo row
  (right-aligned, smaller); volume slider is now its own full-width row.
- TAP button glows in time with the beat (rides the #pulse flash).
- BPM thumbwheel restyled as a horizontal roller: top/bottom end shadows +
  cylinder shading so it reads as a wheel you scrape vertically.
- Repeat / ramp / practice-gap panel moved above the BPM readout.
- #mid spacing opened up; landscape grid + header updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:35:13 -05:00
Me Here
d27bf07069 pm-mobile: top logo, tap/number/wheel tempo, transport reorder, Journal, no staff
- VARASYS logo (tagline-on-the-side lockup) moved to the top, linking the
  Codeberg repo; deleted the bottom session-bar footer.
- Tempo plate is now [TAP] [big BPM] [thumbwheel]: TAP = tap-tempo, tap the
  number to type, drag the ridged wheel on the right to scrub. Removed the ♩
  note glyph next to the number.
- Repeat is a checkbox (with Tempo ramp / Practice gaps); expanding any of them
  now SHRINKS the transport buttons instead of scrolling the page (lanes scroll
  on their own if there are many).
- Transport reordered: row 1 = −10 / − / + / +10, row 2 = prev / play / practice
  / next. Added a Journal button (reaches the practice-sessions log; doubles as
  the live recording timer while practising).
- Removed the staff lines behind the lanes.
- New build markers @BUILD:logo-side-{dark,light}@ (assets added).

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:35:59 -05:00
Me Here
36b7cacd3f pm-mobile: compact tempo plate, Repeat checkbox, controls under lanes, bigger transport
- Replaced the big BPM circle with a compact tempo "plate" (♩ = N · per minute)
  that flashes on the beat — reclaims the wasted vertical space.
- Transport buttons now grow to fill the freed space (2×4 grid stretches).
- Removed the bottom-sheet note-value picker; note value is chosen by graphic
  inside the lane modal only (kept there as you liked).
- Repeat is now a checkbox (like Tempo ramp / Practice gaps); checking it reveals
  "Play N bars, then stop / next / prev". The whole control group moved BELOW the
  lanes.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:15:10 -05:00
Me Here
8402b4f92c pm-mobile: engraved tempo marking (♩ = N) + subtle staff behind the lanes
- The BPM now reads as a sheet-music tempo marking: a quarter-note glyph
  (reusing the lane rhythm-figure SVG, so it matches) + "= N", with a small
  "per minute" beneath. Tap/hold/drag editing unchanged.
- A faint 5-line staff sits behind the lane rows (--staff, theme-aware) for a
  subtle engraved feel; pads/labels render above it.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:14:33 -05:00
Me Here
1a66eb962d pm-mobile: note-value picker, top-bar reorg, share menu, end rework, scaling layout
Lanes / note values:
- Removed the swing toggle + shuffle glyph (swing == triplet in this engine, so
  it's redundant; `swing` stays in the DSL). Pick the note value by GRAPHIC: tap
  a lane's rhythm icon (or use the lane sheet) to choose quarter/eighth/triplet/
  sixteenth/sextuplet — replaces the number dropdown; adds per-lane gain earlier.

Top bar:
- Utilities grouped on one row (volume p→f, share, theme, full screen, help);
  Set list + Track + a disk Save button on the row below.
- Save icon is now a disk; help "?" replays the tour.

Track end:
- Dropped the nonsensical "repeat + loop". Now "Play N bars, then [stop / next
  track / prev track]"; 0 bars = loops forever. Honored at runtime.

Share:
- Removed inline Copy/Apply. New Share sheet (↑): toggle This track / This set
  list → shareable link (+ copy link / copy text), and paste a string/link to
  load. (setlistToCode added; multi-select tree is a follow-up.)

Layout (rock-solid, pure phone↔tablet scaling):
- Content capped to --maxw and centered; fixed (non-wrapping) track panel and
  rows so nothing re-flows as the screen grows — phone and tablet are the same
  layout, just scaled. Landscape now uses one 2-column layout at ALL heights
  (was falling back to portrait on tall tablets). Bigger margins in full screen.

Inconspicuous VARASYS logo in the bottom bar links to the Codeberg repo.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:05:31 -05:00
Me Here
1f5fdeaba9 pm-mobile: swing shuffle glyph, largest-note rhythm icons, polyrhythm indicator
- Lane rhythm icon now reflects what's actually played: it reduces the
  subdivision grid to the largest note that lands on every active hit
  (gcd of stepsPerBeat + active offsets), so a triplet grid that only plays the
  beat shows a quarter, a 16th grid playing eighths shows eighths, etc. Updates
  live as pads are toggled.
- Swung eighths now render the dotted-eighth + sixteenth shuffle figure (full
  8th beam + partial 16th beam + augmentation dot) instead of plain eighths.
- Polyrhythm (poly ~) lanes are clearly marked on the main screen: a violet
  left-stripe, violet pads, and a ratio badge (e.g. ↻5:4 = lane beats : the
  reference lane's beats) — replaces the cryptic "~".

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:29:56 -05:00
Me Here
b98c37ff68 pm-mobile: beat-aligned pads + per-lane subdivision rhythm icons
- Pads now group into per-beat cells (one flex cell per beat), so beats line up
  in columns across lanes regardless of each lane's subdivision; the downbeat
  pad in each cell is full-height and the sub-beat pads are shorter/smaller.
- Each lane label shows a small engraved rhythm figure for its subdivision,
  drawn as SVG (notehead + stem + beams + tuplet number): quarter, beamed
  eighths, triplet (3), sixteenths (double beam), sextuplet (6), etc.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:18:48 -05:00
Me Here
4b53f917f4 seed lists + mobile icons: rich Styles/Practice, drop Song/Notation, subtle music theme
setlists.js (shared by all pages):
- Removed the "Song (continuous)" and "Notation showcase" seed lists.
- "Styles" is now a rich, genre-true collection (16): rock, pop 16ths, funk,
  disco, Motown, blues shuffle, jazz swing, bossa, samba, reggae one-drop,
  afrobeat, hip-hop, metal, 6/8 ballad, 7/8, 5/4 — full grooves to jam over.
- "Practice" is 15 drummer drills to learn those styles: hat subdivisions,
  ghost-note backbeats, 16th hand control, shuffle/jazz ride, bossa & 3-over-4
  independence, dynamics, double bass, hemiola/5-over-4, tempo & gap trainers.
- Dropped the cartoon emoji from the titles. All patches validated: every lane
  parses and pattern lengths match their meters.

Mobile icons — less cartoonish, subtly musical:
- Volume rail now reads p … f (piano/forte dynamics) instead of speaker emoji.
- Save 💾 -> ↧; library +/✕ instead of /🗑.
- Practice-sessions empty state uses a treble clef instead of 🎼.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:14:06 -05:00
Me Here
ca2a695f4f pm-mobile: inline track panel above lanes + fixed landscape layout
- Repeat (bar count) + End, Ramp, Gap and the track share-string now show
  directly on the main screen in a compact panel above the lanes (with live
  Copy/Apply of the string) — replaces the ⚙ Track dialog, which is removed.
- Landscape was overlapping (top row + pulse + lanes collided at short
  heights). Reworked: the work area is a 2-column grid — pulse + transport on
  the left, track panel + lanes (scrollable) on the right — with a single-row
  top bar (dropdowns + volume + icons). Portrait keeps the centered block with
  the transport below. Verified clean in both orientations down to ~300px tall.

Engine untouched; conformance unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:03:18 -05:00
Me Here
582118abf9 pm-mobile: per-lane gain + Save & Library (create/save-as/reorder tracks)
- Per-lane gain: a volume slider (-18..+6 dB) in the lane dialog → m.gainDb,
  encoded in the lane token (e.g. kick:4@-6) and saved with the track.
- Save & Library sheet (💾): writes to the SAME store/format as the editor
  (metronome.setlists), so tracks made on the phone show up in the editor too.
    * Save current track: name + target set list (or "+ New set list"),
      "Save as new track" (always) and "Update <name>" for your own tracks —
      Update confirms the overwrite (the iterate-and-resave path is one tap, but
      a named confirm prevents accidentally clobbering the original when you
      meant to save a new track).
    * Manage library: reorder (up/down), rename and delete your set lists and
      their tracks; built-ins stay read-only (Save-as copies edits out of them).
- Built-in/transient set lists can't be overwritten — saving promotes the live
  working copy into one of your own set lists.

Engine untouched; conformance suite unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:35:57 -05:00
Me Here
8f5635af52 pm-mobile: track settings dialog, balanced layout, clearer help wording
- Help wording: Practice no longer says "record" (which read like audio
  capture) — it now says it times your playing and logs it to the practice log,
  not audio. Play step and the session bar reworded too ("Practising…").
- Track settings dialog (mirrors the lane dialog), opened from a "⚙ Track"
  summary button under the lanes: bar count, end behavior (loop / stop / next),
  tempo ramp, practice gaps, and copy/paste of the track share-string. These
  were previously read-only chips; now editable and persisted.
  end/loop is now honored at runtime: loop repeats the phrase, stop halts at the
  bar count, next advances the set list (the bar readout cycles 1..N when looping).
- Layout: pulse + lanes are centered as one block with the transport pinned at
  the bottom — kills the big empty mid-band. Landscape reflows to pulse-left /
  lanes-right with a full-width transport. Bigger pulse.

Engine untouched; conformance suite unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:03:06 -05:00
Me Here
8b795d4107 pm-mobile: editable lane pads, guided help tour, persisted state
- Editable lanes (no notation/konnakol — just pads): each lane is a row of pads
  that blink on the beat; tap a pad to cycle rest → beat → accent → ghost. A
  lane's label opens a sheet to set sound, grouping (e.g. 2+2+3), subdivision,
  swing, mute and polymeter; plus "+ Add lane" / delete. Edits are live and feed
  straight into the scheduler. (Replaces the read-only lane chips; the global
  feature chips — bars/end/ramp/gaps — stay.)
- Help: a "?" runs a 7-step guided coachmark tour (spotlight + tooltip), shown
  once on first run and re-runnable anytime. Removed the instruction hint under
  the BPM (the tour covers it). Tour also frames tracks as named practice items.
- Persist + restore: the working state (set list / track / tempo / volume / lane
  edits) is saved to metronome.mobile.state and restored on reload.
- Dropped the separate beat-dot row — the pulse flash + per-lane pad playhead
  cover it, freeing room for the editable lanes.

Engine untouched; conformance suite unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:44:31 -05:00
Me Here
812a69942f pm-mobile: sessions page — per-track comparison + collapsible sessions
- Top section aggregates the CURRENT track across all sessions (track picker
  defaults to the metronome's current track, persisted via metronome.curtrack):
  total time / plays / bpm range, plus a per-session comparison table so you can
  watch a single track progress across days.
- Each session is now a collapsible <details>: the summary shows a friendly
  timestamp ("Fri Jun 16 at 2:46 PM") with total/practiced/track-count; the note
  + per-track aggregate table + delete live in the expanded body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:32:28 -05:00
Me Here
5b11363520 pm-mobile: practice sessions, richer readout, BPM-tap, lane/feature detail
Bigger rework of the mobile player around a new "practice session" concept,
plus a second page to review sessions.

Transport / sessions:
- Practice now starts a continuous SESSION clock and begins practicing the
  current track. While practicing, the Play button becomes Stop and Practice
  becomes Pause, so Practice starts/stops individual tracks while the session
  clock keeps running. Stop (the Play button) ends the session and records it.
- Plain Play still runs the metronome with no session/recording.
- Each track-practice is one segment {name, at, sec, bpm}; sub-3s blips are
  skipped. A session = {at, endedAt, clockSec, note, segments[]} stored under
  metronome.sessions (replaces the old per-track metronome.logs sheet).
- Switching track / set list mid-session rolls the current segment over.

Display:
- Removed the Tap Tempo button; the BPM display now does it: tap = tap tempo,
  hold = type an exact value, vertical drag = scrub.
- Detail panel shows every lane (canonical share-token chips, disabled lanes
  struck through) and the active features: bar count, end behavior, ramp, and
  gaps (trainer play/mute).
- Meter line shows live bar count with total (e.g. "bar 4 / 16") and elapsed
  play time; the bottom bar shows live session time + track count while
  recording, and links to the sessions page otherwise.

New page mobile-sessions.html: lists saved sessions, each with an editable note
(autosaved) and an aggregate table of tracks practiced in that session
(track - time - plays - bpm range), with per-session delete. PWA scope widened
to /mobile so both pages stay in the installed app + offline (SW v2).

Engine untouched; conformance suite unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:14:41 -05:00
Me Here
dca2a405f7 pm-mobile: top dropdowns, ±10 tempo, Play/Practice split, collapsible log
Reworks the mobile player's controls per use on a phone:

- Set list + track are now two dropdowns at the top (with the volume slider +
  theme/fullscreen beside them); drops the hamburger/bottom-sheet menu. The
  track dropdown stays in sync with prev/next and set-list auto-advance.
- Tempo grid adds coarse -10/+10 buttons above the fine -/+ buttons, laid out
  as a 4-col grid with prev/next and play/practice in the centre columns.
- Separate Play and Practice transports: Play runs the metronome without
  touching the practice log; Practice runs AND records a session
  (metronome.logs, same format/key as the editor: {at,name,durationSec,bpm,
  lanes}, per-track history, sub-3s blips skipped).
- Tap Tempo restyled as a real button.
- Collapsible practice log: a thin bar at the bottom opens a bottom-sheet
  showing past sessions for the current track (date - duration @ bpm), with
  per-entry delete and clear-this-track.

Landscape phones switch to a two-column layout (pulse left, transport right) so
everything fits without vertical overflow. Engine untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:52:41 -05:00
Me Here
a3a09bc77d pm-mobile: touch-first phone/tablet PWA player (mobile.html)
A new full-screen, touch-first edition of the player aimed at phones through
tablets - no native app, just a web page you can "Add to Home Screen".

Reuses the shared engine + look-ahead scheduler (same player loop as
player.html); new UI is a big pulsing beat display, beat-dot row with accent
grouping, huge BPM (tap to type, vertical drag to scrub), prev/play/next +/-
and tap-tempo, and a bottom sheet for set lists / patch+link loading / volume.

Mobile concerns handled:
- iOS ring/silent switch: navigator.audioSession.type="playback" + a silent
  buffer warmup inside the play gesture, so audio isn't muted by the switch.
- Screen Wake Lock while running (re-acquired on visibilitychange).
- PWA: manifest.webmanifest + apple-touch meta + mobile-sw.js (network-first
  app shell, passthrough for everything else) -> installable + offline.
  Multi-file is fine here since it targets mobile (waives the single-file rule).
- viewport-fit=cover + safe-area insets, no user zoom, touch-action:manipulation,
  overscroll-behavior:none; transport buttons flex-share the row so they never
  overflow a narrow phone; responsive portrait/landscape, phone->tablet.
- Fullscreen toggle where supported (Android/desktop; iOS uses home-screen PWA).

Wired into build.sh + deploy.sh (page + PWA assets) and added to the index
gallery as PM_M-1 Mobile. New metronome app icons generated in assets/.
Conformance suite unaffected (engine untouched): 47 pass, 1 known.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:21:52 -05:00
Me Here
76392ab20f pm-kit: beat-pendulum on PIO too (unified PIO stepper, drop bit-bang driver)
Move the play-mode pendulum onto the same PIO/DMA stepper as jog: at each beat the
CPU just reverses direction and sets the sweep rate (rate = STEPPER_ARC / beat,
capped at STEPPER_MAX_RATE -> auto-shrink); the state machine sweeps continuously
between beats, so the pendulum stays smooth even while the screen redraws its
graphic. _pend_start kicks the first sweep on play; stop de-energizes via off().

self.pend is now a single PioStepper shared by play + jog (jog reuses it instead
of recreating). Removed the superseded bit-bang Pendulum class and the unused
_pend_last. README updated (pendulum motion is PIO-driven).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:38:38 -05:00
Me Here
f2cf3e3ed9 pm-kit: jog stops promptly on release (drop the decel ramp)
Releasing the joystick used to ramp the speed down through zero (~0.3s), so the
motor coasted after release. Decel isn't needed (only fast *starts* stall), so
release now stops immediately; keep the gentle accel ramp on start. Also only
rewrite the PIO clock when the rate actually changes (no 100Hz redundant writes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:29:11 -05:00
Me Here
44193a07c1 pm-kit: PIO/DMA stepper for jog mode (hardware-timed, CPU-free pulses)
Move jog-mode stepping off the CPU loop onto a PIO state machine: one hardcoded
instruction (out pins,4 [31] = 0x7F04, no adafruit_pioasm needed) shifts a 4-bit
coil nibble to GP18..21 every 32 PIO cycles; one 32-bit word packs all 8 half-step
phases; background_write(loop=) DMA-feeds it continuously. half-steps/s = clock/32,
so speed + accel = setting sm.frequency. Pulses now run on dedicated hardware, so a
display refresh or GC pause can't stall them - which was the ~1s "smooth then jump".

Jog loop is now a light 100Hz CPU controller (joystick + accel ramp + frequency);
the live step/rate readout is restored since the motor runs from PIO/DMA. Bit-bang
Pendulum keeps a deinit() so jog can hand GP18..21 to PIO. Beat-pendulum still on
bit-bang (PIO port next).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:24:12 -05:00
Me Here
9651e8bc6a pm-kit: fix jumpy jog (stop drawing mid-spin) - smooth steady spin
The ~1s hitch was the once-per-second readout: show_stats() allocates text
bitmaps (GC pause) and display.refresh() blocks the SPI blit, both stalling the
step loop exactly every second. Now the rate is measured silently while spinning
and the readout (steps + peak) is redrawn only when you release; a gc.collect()
on release + before spinning keeps the heap clean. Steady spin does zero display
work -> smooth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:13:07 -05:00
Me Here
a7e8061a9b pm-kit: stepper/pendulum tuning via settings.json (no recompile)
_load_settings now also reads stepper_max_rate / stepper_accel / stepper_jog_start
/ pend_swing_deg / stepper_steps_per_rev and recomputes the derived PEND_THETA +
STEPPER_ARC, so the motor speed/accel/swing can be dialed in by editing the file
on the drive instead of rebuilding the firmware. README documents the keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:59:35 -05:00
Me Here
19d646a873 pm-kit: jog mode = direction-only, accelerate to a smooth max speed
The joystick has no useful fine speed control, so jog now treats it as direction
only and runs the motor at STEPPER_MAX_RATE, reached via a trapezoidal accel ramp
(STEPPER_ACCEL from STEPPER_JOG_START) so it doesn't stall trying to start at top
speed; reversing decelerates through zero then accelerates the other way. Default
top rate set to a realistic 600 half-steps/s for the 28BYJ-48; tune via jog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:57:46 -05:00
Me Here
5f9e9dfad7 pm-kit: fix jog-mode step-rate cap (loop overhead, not the motor)
The ~330 steps/s ceiling was the CircuitPython loop, not the stepper: an analog
read + time.sleep(0.0005) every iteration made each pass ~3ms (1/0.003 ~ 330).
Tighten the jog loop - poll the joystick at ~250Hz off the step hot-path, drop
the per-iteration sleep, refresh the readout ~1/s instead of 3+/s, and raise the
commanded ceiling to ~1600 steps/s - so the peak reflects the motor's real limit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:51:34 -05:00
Me Here
36c7406d71 pm-kit: jog-mode step counter + rate readout, and document the pendulum/jog feature
- Jog/test screen now shows a live step count and current/peak step rate; jog
  ceiling raised to ~1000 steps/s so you can probe past a motor's max and read
  the peak rate where it stalls -> set STEPPER_MAX_RATE just below that.
- README: new "Pendulum (stepper motion)" section (wiring GP18-21, the config
  knobs, motion behaviour, jog/test mode) + the A-alone / A+B power-on chords;
  noted the GP19/20/21 conflict with the custom PM_K-1 board's ribbon.

Pure ASCII; conformance 47/47; app.mpy precompiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:43:22 -05:00
Me Here
15755f4d0c pm-kit: hidden stepper jog/test mode (hold A+B at boot)
Hold both buttons at power-on to enter a self-contained jog screen: the joystick
spins the stepper CW/CCW (speed by deflection), with an on-screen direction
needle + RGB LED feedback. Runs in its own loop; power-cycle to return to normal.

- app.py: _jog_loop drawn entirely in the overlay group (cover + labels + needle);
  Pendulum.spin() does a free half-step either way; _jog set when A+B held in init;
  run() branches to it before the normal loop.
- boot.py: editor mode is now "A alone" (A pressed, B not). A+B stays in appliance
  mode, so the jog chord doesn't also flip the drive writable.

Pure ASCII; conformance 47/47; build precompiles app.mpy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:37:57 -05:00
Me Here
5f3c518089 pm-kit: pendulum swing as a single degrees knob (PEND_SWING_DEG=120)
Make the swing arc one source of truth in degrees, driving both the screen
graphic (exactly) and the physical arm (mapped through STEPPER_STEPS_PER_REV).
Set to 120 deg end-to-end. PEND_THETA now derives from it; STEPPER_ARC =
steps_per_rev * deg/360. Graphic geometry verified on-screen at 120 deg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:26:17 -05:00
Me Here
0eb38f1c1e pm-kit: on-screen swinging pendulum graphic, synced to the motor's beat phase
Draw a swinging pendulum on the ST7796 that mirrors the physical stepper arm.
Inverted-metronome style (pivot near the bottom, weighted bob swinging up top),
shown over the practice-log area while playing and swapped back to the log when
stopped (the log is for post-session review, not mid-play).

- _build_scene: a hidden g_pend group (stand + pivot + arm Polygon + bob Circle).
- draw_pendulum(now) computes the bob from the SAME swing phase the motor uses
  (_pend_beat0 / _pend_dir / _beat_ns), so screen and arm move identically and it
  follows tempo ramps. Animated ~30fps from run(); the gated refresh renders it.
- _pend_service now advances the swing clock even when no motor is wired, so the
  graphic works standalone (STEPPER_ENABLED=False still animates the screen).
- tick() toggles g_pend/g_log visibility on the play<->stop transition.

Pure ASCII; conformance 47/47; build.sh precompiles app.mpy fine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:07:53 -05:00
Me Here
305d9373d0 pm-kit: beat-synced pendulum stepper on the CircuitPython Kit (display-model motion test)
Add an optional physical pendulum to pico-cp/app.py: a 4-input unipolar stepper
(e.g. ULN2003 on the EP-0172's free GP18-21) swung as a metronome arm in time
with the beat. First motion-feedback test for the display-model Kit.

- New Pendulum driver class (half-step 8-phase, non-blocking step_toward + release).
- _pend_service(now) derives the swing from the live _beat_ns: it reverses
  direction at each beat boundary so the arm hits an extreme exactly on the beat,
  and auto-shrinks the arc when STEPPER_MAX_RATE can't sweep the full travel in a
  beat. Reading _beat_ns live means it follows tempo ramps for free.
- Hooked into tick(): swings while running, de-energizes the coils once on stop
  (covers all stop paths). Swing phase re-aligns to the clock in _reset_clock.
- Config knobs (STEPPER_ENABLED/ARC/MAX_RATE) + P_STEP pins at the top; disabled
  cleanly leaves the pins free.

Stays pure ASCII (USB-MIDI push requirement); conformance suite still 47/47;
build.sh precompiles app.mpy fine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:59:12 -05:00
Me Here
d80c35984e pm-daisy: Daisy Pod spike — play the click engine on STM32H7 (host-verified, awaiting hardware)
Develop the full Daisy Pod spike so it can be flashed the moment the board
arrives. Architecture: one shared engine, two front-ends.

- pm-synth: make it `#![no_std]` (mirroring track-format), routing float math
  through `libm` so the SAME f32 code runs on the host and on the Daisy's
  Cortex-M7F (hardware FPU — no fixed-point port needed). Add `Player`, a
  self-running sequencer that owns the Synth + scheduled clicks and renders
  sample-by-sample, looping at the pattern boundary. Integer-only hot path
  (clicks pre-resolved to sample indices); exposes a `fired()` beat counter.
  Add SPIKE_PROGRAM/SPIKE_BARS as the shared source of truth.

- synthrender: render the SAME Player to pm-daisy-preview.wav — the host-side
  "simulator". Bit-identical preview of the hardware output (before its codec);
  far more useful than chip emulation (Renode can't model the audio codec).

- pm-daisy (new, workspace-excluded firmware): thin BSP binary for the Daisy
  Seed/Pod. embedded-alloc heap + board bring-up + SAI-DMA audio interrupt
  feeding Player::next_sample() into stereo frames, USER LED flashing per click.
  Audio loop follows the `daisy` crate's examples/audio.rs. Board revision
  (codec) is a Cargo feature; README documents matching it + both flash paths
  (probe-rs/RTT and USB DFU) + the QSPI-bootloader fallback.

Verified without hardware: host build + preview render (48 kHz, onsets on the
8th-note grid at 124 BPM); firmware cross-compiles + links for thumbv7em-none-
eabihf at ~87 KB (fits the 128 KB internal flash) across all three codec
revisions; track-format conformance + `node tests/run.mjs` (47 pass) still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:41:10 -05:00
Me Here
0bec13abab midi-out: 24-PPQN clock + Start/Stop; shared src/midiout.js; mirror into editor.html
- Extract MIDI-out into a shared partial src/midiout.js (one copy, no drift); both
  editors @BUILD:include it. The page wires three transport hooks: midiOutStart(t0)
  in start(), midiOutStop() in stop(), midiOutClock(ahead) at end of scheduler();
  engine.js calls onMeterHit() per hit.
- Clock-out: a "clock" checkbox (default on) appears with the port picker. When on:
  MIDI Start (0xFA) at the downbeat, 24-PPQN clock (0xF8) scheduled across the audio
  look-ahead window (timestamped, tracks tempo/ramp, stays tight), Stop (0xFC) on
  stop. Guarded against starve-looping at extreme tempos.
- Mirror the feature into editor.html (PM_E-1): header .devctrl pills, the include,
  _wireMidi port refresh, transport hooks, listener, and the _isDevicePort fix
  (recognizes PM_G-1 Grid etc.).

Conformance suite still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:11:24 -05:00
Me Here
34e0d24aad pm_e-2: MIDI out — drive external gear (drum module / e-kit) from the editor
engine.js: add an opt-in per-hit hook in scheduleMeterTick — onMeterHit(sound,
time, lvl) — called only if a page defines it (no-op everywhere else). Lets a
page emit MIDI per scheduled hit, in lockstep with the audio scheduler.

pm_e-2.html: a "🎛 MIDI out" header toggle + output-port picker. When on, each
groove hit is sent as a GM drum Note-On (channel 10; note from SOUND_GM e.g.
kick*->36, snare*->38, hat*->42; velocity by accent/normal/ghost) to the chosen
port via output.send([..], ts) with a timestamp derived from the hits audio time
(performance.now() + (time - audioCtx.currentTime)*1000) for tight sync; a note-off
follows 60ms later. Port list prefers a non-PM output (the external gear) and
refreshes on MIDI connect/disconnect. Independent of the local synth + Device
audio; goes green (blue) when on. Web MIDI (Chrome/Edge/Firefox).

Conformance suite still green (engine.js change is in the scheduler, not the codec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:41:01 -05:00
Me Here
964dee01d6 pm-synth: port engine.js 808/909/GM voices to Rust + host wav renderer
pm-synth: a polyphonic drum-voice synth, a faithful f32 port of engine.js DRUMS
(tone/ampEnv/v_noise/metalHat/clap recipes; RBJ biquads; exp envelopes). A Synth
mixes active Voices sample-by-sample (transport-agnostic: offline render now,
real-time device buffer fills later). All 808/909 + GM voices ported.

synthrender (host bin): parse a groove -> track-format schedule -> trigger voices
at click times -> 16-bit/48k mono WAV. Applies the editor default kit (kick->
kick909 etc.). Renders four demo grooves to audition off-bench.

This is the reusable half of the audio feature; the device port (no_std +
fixed-point/table osc, since the M0+ has no FPU) comes with the transport.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:34:31 -05:00
Me Here
15392174aa web: hide PM_E-1 from landing (focus PM_E-2); clarify pm_e-2 device controls
Landing (index.html):
- Remove the PM_E-1 (editor.html) viewport; default the live viewport to PM_E-2.
- Repoint the vp-bar + the "design in the editor" link to pm_e-2.html. editor.html
  still exists, just not featured on the landing.

PM_E-2 editor (pm_e-2.html) - the device-connection badge + Device-audio toggle:
- Group both in the header as matching .devctrl pills, side by side.
- Clear tooltips spelling out exactly what each does: the badge only REPORTS the
  USB-MIDI link (green + name when a PM device is plugged in); Device audio is an
  on/off switch that routes a connected device through the computer speakers and
  does not require a device to toggle.
- Device-audio button now shows on/off state via colour (green when on), matching
  the badge, instead of the .primary class (which clashed with the pill style).
- Fix _isDevicePort: it only matched pico/circuitpython/pimoroni/varasys, so the
  native Rust devices ("PM_G-1 Grid" etc.) were never recognized -> the badge
  stayed "no device" even when connected. Now matches pm_g/pm_k/pm_x/grid/polymeter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:23:15 -05:00
Me Here
5f7c85d910 docs(rust-port): record the pm-grid hardening pass (tx_q cap, fonts split) 2026-06-04 11:24:28 -05:00
Me Here
bdf69cfd30 pm-grid: start main.rs modularization (extract fonts module)
Move the 3x5 LED font (DIGITS, glyph, build_name_cols) into src/fonts.rs.
Pure code move, compiler-verified identical behavior; main.rs 1835 -> ~1770 lines.
First step of the recommended main.rs split; further extraction (FAT/MSC storage,
views) to continue incrementally as those areas are touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:24:09 -05:00
Me Here
72dbb2ecd0 pm-grid: hardening - bound the USB-MIDI TX queue + defensive set-list guard
Audit for panic/brick risks (a panic = black screen on this device):
- sx_send (live-sync broadcasts + 5s heartbeat) pushed to tx_q with no cap. If
  the editor disconnects without a BYE while sync_armed and nothing drains
  MIDI-IN, tx_q grows unbounded -> heap exhaustion -> brick. Now drops messages
  when tx_q > 256 (the heartbeat re-syncs when the host returns). Notes/clock
  were already capped.
- build_setlists now drops empty set lists, so load()/next_track() can never
  hit a `% 0`. (parse guarantees >=1 lane; built-ins/parsed lists are non-empty,
  this is belt-and-suspenders.)

Other unwrap()s are boot-time peripheral init; lanes[0]/items[0]/step[0] are all
safe (parse substitutes beep:4 for empty programs; built-ins lead the list).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:21:06 -05:00
Me Here
dd27d553fe pm-grid: live re-read of programs.json (no reboot needed)
When the host writes the drive (SCSI Write sets a dirty flag) and the drive has
been idle ~1.5s AND playback is stopped, the loop re-reads programs.json and
rebuilds the set lists (reload_user) -> a dropped file applies without a reboot.

Read-only path (split read_programs_json out of read_user_setlists; the format
flash-write only happens at boot), so no FAT-corruption risk from dual access.

Note on the recommended write path: the device deliberately does NOT write the
shared FAT while the host has it mounted (that corrupts the host cache - same
reason CircuitPython is one-direction-at-a-time). The practice log should instead
go to the editor via LOGSYNC (0x45); settings.json *read* (device read-only) is a
safe follow-up. Documented in docs/rust-port.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:19:43 -05:00
Me Here
219fb267a0 pm-grid: harden FAT-read boot (fix black screen regression)
The drive-read at boot bricked the display (barely blinked, then black).
Likely the new fatfs + owned set lists exhausted the 24KB heap (alloc panic ->
halt before the splash). Three fixes:
- Heap 24KB -> 96KB (Pico has 264KB).
- format_pmg1 writes one 4KB sector per call (the proven MSC write pattern)
  instead of a single 7-sector erase+program.
- Run read_user_setlists AFTER the splash, so a FAT/flash failure can no longer
  leave the screen black; added defmt logs around it to localize any remaining
  failure over the probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:35:28 -05:00
Me Here
768ec0021f pm-grid: name the drive PM_G-1 + read set lists from it (on-device FAT)
On boot (before USB setup, so the flash write cannot disrupt enumeration) the
device mounts the Mass Storage FAT volume and uses it:
- fatfs 0.4 (git rev c4b88477; 0.3.6 needs core_io for no_std) via a read-only
  FlashIo over the .filesystem region (reads flash through a black_box ptr).
- If the root-dir volume label is not "PM_G-1" (e.g. a leftover CircuitPython
  volume), write an embedded blank PM_G-1 FAT12 template (src/fat_template.bin =
  first 7 sectors of mkfs.fat -F12 -S4096 -n PM_G-1; sets both BPB + root-dir
  VOLUME_ID label) -> the drive now shows as PM_G-1.
- Read programs.json (LFN) and a tolerant scanner (parse_setlists) turns it into
  user set lists appended to the built-ins. Drop programs.json on the drive,
  reboot, your grooves appear (B-hold cycles set lists).

Set lists are now a runtime Vec<SetList>{title,items} (built-ins -> owned +
drive); refactored load/next_track/next_setlist/goto_target/prepare_next/sel.

Validated off-bench: a host probe ran fatfs against a real mkfs 4096-sector image
(label + programs.json read confirmed) before flashing.

WRITE-from-device (practice log / settings) is still deferred (the read path is
in; needs a write-back FlashIo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:29:30 -05:00
Me Here
394ae65eaf pm-grid: USB Mass Storage (composite MIDI + 1MB flash-backed drive)
Adapt the usbd-storage rp2040 example into pm-grid as a composite MIDI+MSC
device:
- Host sees a 1MB removable drive backed by the upper 1MB of flash (a
  .filesystem region, NOLOAD so it stays out of the UF2 and survives reflashes).
- scsi_command handles the SCSI set (Inquiry / ReadCapacity10/16 /
  ReadFormatCapacities / Read / Write / ModeSense / RequestSense / TestUnitReady).
  Reads come from flash via raw pointer; writes accumulate a 4KB block then
  erase+program the sector with rp2040-flash (wrapped in interrupt::free).
- Host owns the FAT format (formats on first use). Unblocks on-device persistence.
- Composite poll: usb_dev.poll([&mut midi, &mut scsi]); scsi.poll services commands.

Build fixes required by adding rp2040-flash:
- rp2040-hal 0.10 -> 0.11 (0.10 + rp2040-flash 0.6 both export the __aeabi_*/
  __addsf3 ROM intrinsics -> duplicate symbols). No HAL API breakage.
- lto = false + codegen-units = 1 (fat-LTO tripped the same duplicate intrinsic).

UF2 stays ~257KB thanks to NOLOAD. defmt logs on block writes + unknown commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:25:51 -05:00
Me Here
dce51866d2 docs(rust-port): mark MIDI clock in done 2026-06-03 16:08:29 -05:00
Me Here
d035ee2a06 pm-grid: MIDI clock in (slave tempo to external 24-PPQN clock)
- feed_midi (was feed_sx) now also handles realtime: 0xF8 tick -> slave_tick
  (EMA of the inter-tick interval -> derived BPM, 5..300 clamp, jitter reject),
  0xFA/0xFB -> start, 0xFC -> stop. RX loop feeds CIN 0xF single-byte packets too.
- While slaved: the tempo ramp and our own clock-OUT are suppressed (no feedback);
  the lock drops after a >1s gap in incoming ticks.
- Default on; only engages when a host actually sends clock (the editor does not).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:07:55 -05:00
Me Here
36989c96de pm-grid: playback-flow auto-advance (rep/end) + MIDI clock out
Playback flow (rep/end), ported from pico-scroll:
- At each master-bar boundary, after bars*rep cycles the end-action fires:
  end=stop stops; end=next / end=+N advances through the set list.
- The next track is preloaded one bar early (parsed + per-lane durs) into a
  pending slot, then swapped at the exact seam (master lane bar boundary; all
  lanes restart there) for a gapless handoff. load()/manual nav clears pending.

MIDI clock out (default on, so a DAW can slave to the Grid):
- 24-PPQN 0xF8 against the wall clock + 0xFA/0xFC Start/Stop on play/stop (button
  or live-sync). Queued on tx_q as CIN 0xF single-byte packets.

Deferred items needing persistent storage (no CIRCUITPY drive in the Rust build,
needs a flash KV layer - separate milestone): practice log, settings.json,
SLSYNC/LOGSYNC. Also deferred: MIDI clock in, optional piezo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:50:20 -05:00
Me Here
7e2a3b181b pm-grid: live-sync over USB-MIDI SysEx (editor <-> device)
Port pico-scroll's live-sync to Rust (docs/livesync-protocol.md):
- Reassemble SysEx from incoming USB-MIDI 4-byte packets (by Code Index Number);
  dispatch manufacturer 0x7D frames.
- Version query 0x02 -> 0x03 'G;0.1.0' (editor now identifies the device).
- HELLO 0x40 -> reply FULL; FULL 0x41 -> parse patch + running and adopt it;
  DELTA 0x42 -> apply play/stop/bpm/sel/beat; BYE 0x43 -> disarm.
- Broadcast a DELTA from each on-device input (play/stop, sel, bpm) + a FULL
  heartbeat ~5s (track-format::serialize). Echo-guarded by a boot-derived origin;
  sync_applying flag suppresses re-broadcast while applying.
- Unify all USB-MIDI TX (notes + SysEx) onto one tx_q drained one-per-poll.
- defmt info! on every received op for probe debugging.

Structural lane= edits aren't applied incrementally (arrive as a fresh FULL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:40:23 -05:00