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>
8.7 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 was purchased — flash + listen when it arrives.
Target hardware: Daisy Pod (a Daisy Seed on a dev board with audio jack, buttons, knobs,
encoder, RGB LEDs). The Seed is the brain; this spike uses its audio out + the Seed USER LED. The
Pod's extra controls are a documented next step (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:
#![no_std]+extern crate alloc;at the crate root.libmfor the float methods it calls as inherent fns (.sin(),.cos(),.powf(),.tanh(),.floor()— seepm-synth/src/lib.rs:19,21,49,56,85,221). Inno_stdthese aren't inherent onf32; route them throughlibm::{sinf,cosf,powf,tanhf,floorf}(or thenum-traitsFloatshim). Gate with#[cfg(feature = "std")]so the hostsynthrenderkeeps building.- A global allocator on-device (
embedded-alloc), becausetrigger()doesalloc::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)
- Toolchain + flash hello (½ day). Add
rust/pm-daisy(thin BSP binary). Targetthumbv7em-none-eabihf. Pick the Rust BSP:daisy(zlosynth) orlibdaisyfor a blocking SAI-DMA callback, ordaisy-embassyif 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. - 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.
- Drop in the engine (½ day). Depend on
track-format+pm-synth, no_std-ifypm-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 vectorsynthrenderalready auditions) on a loop. - 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-synthbuildingno_std(hostsynthrenderstill builds — guarded by astdfeature).- 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-voicealloc::vec!cause dropouts under polyphony (thepolydemo 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-synthfloat port — mechanical but touches every voice; thestd-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
- Daisy Seed product page + datasheet — STM32H750, 24-bit/96 kHz codec, FPU
- Rust BSPs:
daisy(zlosynth) ·libdaisy·daisy-embassy - libDaisy audio-callback model (interleaved/non-interleaved; the C reference for the SAI callback shape)
- Reuse targets in-repo:
rust/track-format,rust/pm-synth,rust/synthrender/src/main.rs