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>
126 lines
8.7 KiB
Markdown
126 lines
8.7 KiB
Markdown
# Daisy Pod spike — scope + status
|
|
|
|
**Status:** **code complete, host-verified, awaiting hardware.** The firmware crate
|
|
[`rust/pm-daisy`](../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](https://electro-smith.com/products/pod)
|
|
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](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`](../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](../rust/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
|
|
- [Daisy Seed product page + datasheet](https://electro-smith.com/products/daisy-seed) — STM32H750, 24-bit/96 kHz codec, FPU
|
|
- Rust BSPs: [`daisy` (zlosynth)](https://github.com/zlosynth/daisy) · [`libdaisy`](https://crates.io/crates/libdaisy) · [`daisy-embassy`](https://crates.io/crates/daisy-embassy)
|
|
- [libDaisy audio-callback model](https://electro-smith.github.io/libDaisy/md_doc_2md_2__a3___getting-_started-_audio.html) (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`
|
|
</content>
|
|
</invoke>
|