metronome/docs/daisy-spike.md
Me Here 802e46f5bb pm-daisy: wire Pod controls (encoder/buttons/knobs/RGB LEDs)
The Daisy Pod hardware arrived (Seed 1.2 / PCM3060). Build the controls
phase on top of the audio spike: encoder turn = tempo, push = play/pause,
buttons step the program list, knob 1 = volume, RGB LED 1 = beat colored
by dynamic, RGB LED 2 = transport. Boot still plays the spike groove so
the spike's decision criteria stay observable.

- pm-synth: add Player::{position,seek,set_volume,last_level} + Synth::
  set_master, with host tests (tests/player.rs). Live tempo/program
  changes rebuild the Player in thread context and swap it in under a
  short critical section, preserving loop phase (seek) on tempo changes.
- pm-daisy: SysTick 1 kHz millis tick, non-blocking beat flash, and
  PLAYING/VOLUME/LAST_LEVEL atomics the audio IRQ honors/publishes.
- New modules controls.rs (buttons/encoder/pots, libDaisy debounce +
  quadrature decode, 1 kHz poll), leds.rs (active-low RGB + boot
  self-test), programs.rs (spike groove at index 0 + pm-grid grooves).
- Pin map verified against libDaisy daisy_pod.cpp + the daisy crate's
  pins.rs. Builds clean for all three Seed revisions; 91 KB/128 KB flash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:04:59 -05:00

9.3 KiB

Daisy Pod spike — scope + status

Status: code complete, host-verified, awaiting hardware. The firmware crate rust/pm-daisy builds for the target and the shared engine is proven on the host (see "What's built" below). Hardware has arrived (a Daisy Seed 1.2 / Seed2 DFM, PCM3060 codec — build with ./build.sh seed_1_2) — flash + listen, then append the verdict below.

Target hardware: Daisy Pod (a Daisy Seed on a dev board with audio jack, buttons, knobs, encoder, RGB LEDs). The Seed is the brain; the spike itself uses its audio out + the Seed USER LED.

Follow-on (code-complete, awaiting the same hardware bring-up): the Pod's extra controls — 2 buttons, encoder + push, 2 knobs, 2 RGB LEDs — are now wired (rust/pm-daisy/src/controls.rs + leds.rs, mapped in programs.rs): encoder = tempo / play-pause, buttons = program step, knob 1 = volume, RGB LEDs = beat-by-dynamic + transport. Boot still plays the spike groove, so the criteria below are unaffected. Pin map + control table in rust/pm-daisy/README.md.

Decision it informs: is the Electrosmith Daisy platform (STM32H750, Cortex-M7 @ 480 MHz + onboard 24-bit audio codec) the right home for the audio engine if PolyMeter ever grows past "metronome" into a real-time audio workstation (live input FX, heavy polyphony, recording)? Today the answer is "RP2350 is the pick" (see rust-port.md) — this spike exists to get a first-hand, low-stakes read on the alternative, not to commit to it.

What's built (this is done)

Piece Where Verified
Shared self-running sequencer Player rust/pm-synth/src/lib.rs host build + render
pm-synth made #![no_std] (libm float math) same builds host and thumbv7em
Host preview / "simulator" → pm-daisy-preview.wav rust/synthrender renders; 48 kHz, on-grid onsets
Daisy firmware (heap, board init, SAI-DMA callback, beat LED) rust/pm-daisy/ cross-compiles + links, ~87 KB (fits 128 KB flash), all 3 codec revisions build
Flashing + revision docs rust/pm-daisy/README.md

The host preview IS the simulator. Full STM32H7 emulation (Renode) exists but won't faithfully model the audio codec — useless for "does it sound right." Instead, because the engine is shared, synthrender drives the exact Player the firmware runs and writes pm-daisy-preview.wav — a bit-identical preview of the hardware output (before its codec). Run cargo run in rust/synthrender and listen. When the Pod arrives, flashing should reproduce that WAV out the jack.

This is a digital/DSP-home experiment only. It deliberately does not touch the heirloom pro-analog chain (THAT receivers/drivers, low-jitter clock, mute relay) from hardware/DESIGN.md — the Daisy's onboard codec is a consumer part. The spike answers "does the engine feel at home on this chip," not "is this the heirloom audio path."

The bet, in one sentence

Almost the entire engine already transfers: track-format is no_std and RP2350-proven, pm-synth is pure f32 with no real dependencies, the STM32H750 has a hardware FPU so those f32 voices run natively (no fixed-point rewrite — the opposite of the RP2350's Cortex-M0+ situation flagged in pm-synth/Cargo.toml), and synthrender/src/main.rs's render loop is the audio callback. So the spike is mostly transport bring-up, not an engine rewrite.

What transfers for free vs. what's new

Piece Source Spike work
Track parse + schedule rust/track-format (#![no_std], builds for RP2350) none — use verbatim
Drum voices / click engine rust/pm-synth (Synth::new / trigger(name,level) / next_sample()->f32) small — see "no_std-ify" below
Render→audio loop shape rust/synthrender/src/main.rs render() adapt — pre-rendered buffer → streaming callback
Audio transport (codec + SAI) new — the actual spike
Toolchain / flash / logs reuse the probe-rs + defmt workflow from pm-grid/pm-kit (probe-flash.md) setup

no_std-ifying pm-synth (mechanical, ~½ day)

pm-synth already uses alloc::vec and core::f32::consts — it's half-way there. To build for the target it needs:

  1. #![no_std] + extern crate alloc; at the crate root.
  2. libm for the float methods it calls as inherent fns (.sin(), .cos(), .powf(), .tanh(), .floor() — see pm-synth/src/lib.rs:19,21,49,56,85,221). In no_std these aren't inherent on f32; route them through libm::{sinf,cosf,powf,tanhf,floorf} (or the num-traits Float shim). Gate with #[cfg(feature = "std")] so the host synthrender keeps building.
  3. A global allocator on-device (embedded-alloc), because trigger() does alloc::vec![…] per voice. The STM32H750 has ample RAM (≥1 MB SRAM, +64 MB SDRAM on the Seed), so a small heap is trivial — but allocating in the audio path is a real-time smell. Whether it glitches is itself a useful spike finding (see Decision criteria); production would pre-allocate fixed voices.

Streaming the render loop (~½ day)

synthrender pre-renders an entire Vec<i16>. On-device, keep a running sample counter n and a click index ci, and inside the SAI block callback do per-frame what the host loop does per-sample: advance t_ns, trigger() any clicks whose time_ns <= t_ns, then next_sample(). The engine is 48 kHz mono; the codec is stereo — duplicate the sample to L/R. Loop the pattern by resetting n/ci at master_bar_ns * bars.

Phases (time-boxed ~2 days)

  1. Toolchain + flash hello (½ day). Add rust/pm-daisy (thin BSP binary). Target thumbv7em-none-eabihf. Pick the Rust BSP: daisy (zlosynth) or libdaisy for a blocking SAI-DMA callback, or daisy-embassy if we want async. Blink + a defmt "hello" over RTT to confirm flashing. Flash via USB DFU (no extra hardware — Daisy boots to DFU) or probe-rs RTT with the Pi Debug Probe we already use.
  2. Codec test tone (½ day). Stand up the SAI audio callback at 48 kHz and emit a sine — confirms the codec init (match the Seed revision: AK4556 rev4 / WM8731 Seed 1.1 / PCM3060 Seed 1.2/2 — the BSP crate selects this; verify the rev at purchase) and that audio comes out the line jack.
  3. Drop in the engine (½ day). Depend on track-format + pm-synth, no_std-ify pm-synth, port the streaming loop. Play a hardcoded program (t124;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X, the same vector synthrender already auditions) on a loop.
  4. Measure + write up (½ day). Capture findings against the criteria below; append a verdict to this file and a one-line pointer in rust-port.md.

Deliverables

  • rust/pm-daisy/ — a thin BSP crate playing a hardcoded 909 pattern out the Daisy line jack.
  • pm-synth building no_std (host synthrender still builds — guarded by a std feature).
  • A findings verdict appended here: go/no-go, with the numbers below.

Decision criteria (what the verdict answers)

  • Sound: clean line-out, no audible glitches/zipper noise on the looping pattern.
  • Timing: clicks land tight (callback keeps up; no underruns logged).
  • Alloc-in-callback: does trigger()'s per-voice alloc::vec! cause dropouts under polyphony (the poly demo vector)? If yes → production needs fixed pre-alloc voices (true on any target — useful regardless of chip choice).
  • Porting friction: how much of the ~2-day estimate was real? Low friction + the codec "just working" = Daisy is a credible workstation home. High friction / ecosystem fights = confirms RP2350.

Non-goals (explicitly out of scope for the spike)

USB-MIDI, live-sync/SysEx, display/LEDs, set lists, buttons, the pro-analog chain, fixed-point/ production voice allocation, A/B firmware update. The spike proves one thing: the click engine sings on this chip with acceptable effort.

Risks / unknowns

  • Codec revision mismatch — confirm which Seed rev (AK4556/WM8731/PCM3060) ships; the BSP crate must match. Verify at purchase ([verify-datasheets memory]).
  • pm-synth float port — mechanical but touches every voice; the std-feature gate keeps the host renderer green so the conformance story is unaffected.
  • Heap in the audio path — flagged above; the spike is exactly how we learn if it matters.
  • Sunk-cost honesty — even a great result doesn't move the metronome off RP2350 today; it only de-risks a future workstation pivot. Keep that framing in the verdict.

References