- 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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
_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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
The bulk MIDI endpoint holds one 4-byte packet until the host reads it (~once
per USB frame), so calling send_bytes twice for simultaneous lane hits dropped
the 2nd note (WouldBlock, silently ignored). Queue note-ons in a VecDeque and
drain one-per-poll, keeping the rest for the next iteration — chords now play in
full (staggered ~1ms, imperceptible) instead of all-but-one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audio (the Scroll Pack has no speaker, so MIDI is the only path):
- usb-device 0.3 + usbd-midi 0.5 on the rp2040-hal UsbBus; enumerates as a
class-compliant MIDI device 'PM_G-1 Grid'.
- tick() emits a GM note-on per lane hit on channel 10 (note from the ported
SOUND_GM map, velocity by level) via send_bytes([0x09,0x99,note,vel]) — raw
4-byte packets, so arbitrary GM drum notes work without the named Note enum.
- USB polled every loop iteration AND during the boot splash (so the host can
enumerate during the ~2.5s animation).
Debug: defmt/defmt-rtt + panic-probe + flip-link; runner probe-rs run --chip
RP2040 (Pi Debug Probe). build.sh emits pm-grid.uf2 + pm-grid.elf; deploy serves
both; key info! log points + 1Hz heartbeat.
Web: drop CircuitPython from the PM_G-1 product. info-grid.html features the
Rust .uf2 download + accurate controls/views (X/Y swap, Ticker); build.sh +
deploy.sh no longer bundle/serve pm_g1_circuitpy.zip or pico-scroll-app.{py,mpy}.
pico-scroll/ stays as the reference port; editor FW_PATHS.G left for graceful
degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Free the top row for a beat indicator: faint ticks at each beat (every sub steps)
across cols 0-10 with a bright playhead at the master lane's current step. The
scrolling name moves down to rows 2-6 (row 1 = separator). BPM block unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Swap X/Y: X now tempo-up, Y tempo-down (match the physical Scroll Pack layout).
- On the master lane's step 0 (the '1'), flash the entire 17x7 matrix at full
brightness for 80ms — a visual downbeat strobe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the pm-kit.uf2 block — copy rust/pm-grid/pm-grid.uf2 to the web root if
built, so it downloads from metronome.varasys.io/pm-grid.uf2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rust sibling of pico-scroll/app.py — the PM_G-1 'Grid' 17x7 LED metronome on a
plain RP2040 Pico (thumbv6m, not the Pico 2). LED-first milestone:
- IS31FL3731 driver: vendored bulk 144-byte framebuffer, one I2C block write per
frame (port of the CircuitPython Matrix; the is31fl3731 crate isn't used).
- Polymeter scheduler driven by track-format::schedule::lane_durs (the cross-impl
contract) + per-lane step clocks + tempo ramp + gap-trainer.
- 4-button input (A play/stop·hold=view, B next-track·hold=next-setlist, X/Y tempo).
- Built-in set lists; 3 views: Ticker (default), Grid, Pendulum.
- Ticker (user-designed): name infinite-scrolls left; BPM pinned right rotated 90
CCW = hundreds dot-bar (1 dot/100) + last 2 digits rotated. 130 -> 1 dot + '30'.
- Build scaffolding: rp2040-hal 0.10 + boot2, memory.x, build.sh + uf2.py (RP2040
family id). thumbv6m-none-eabi added to rust/Containerfile. Excluded from the
host workspace like pm-kit. Compiles clean -> 48 KB pm-grid.uf2.
Audio (USB-MIDI; the board has no speaker), live-sync, firmware push, practice log
and playback-flow auto-advance are deferred to the next milestone (as on pm-kit).
Also: delete COORDINATION.md (solo now); docs/rust-port.md updated with pm-grid
status + corrected Grid driver-matrix row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>