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>
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 inprograms.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 inrust/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