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>
87 lines
3.7 KiB
Rust
87 lines
3.7 KiB
Rust
//! Render PolyMeter grooves through pm-synth to 16-bit mono 48 kHz WAVs so we can audition the
|
|
//! ported 808/909 voices on a host (no hardware). Output files are written to the current dir.
|
|
use pm_synth::{default_kit, Player, Synth, SPIKE_BARS, SPIKE_PROGRAM, SR};
|
|
use std::fs::File;
|
|
use std::io::{self, Write};
|
|
|
|
/// Render through the shared [`Player`] — the *exact* code path the Daisy firmware runs in its SAI
|
|
/// callback (`pm-daisy`). This is the host-side preview/"simulator": the samples here are what the
|
|
/// hardware will produce (before its codec). `secs` of audio, looping at the pattern boundary.
|
|
fn render_device(prog: &str, bars: i64, secs: f32) -> Vec<i16> {
|
|
let track = track_format::parse(prog);
|
|
let mut player = Player::new(&track, bars);
|
|
let total = (SR * secs) as usize;
|
|
(0..total).map(|_| (player.next_sample() * 30000.0) as i16).collect()
|
|
}
|
|
|
|
fn render(prog: &str, bars: i64) -> Vec<i16> {
|
|
let track = track_format::parse(prog);
|
|
let mbar = track_format::schedule::master_bar_ns(&track);
|
|
let clicks = track_format::schedule::render(&track, bars);
|
|
let total_ns = mbar * bars + 1_000_000_000; // + 1 s tail for decays
|
|
let total_samples = (total_ns as f64 / 1e9 * SR as f64) as usize;
|
|
let mut synth = Synth::new();
|
|
let mut out = Vec::with_capacity(total_samples);
|
|
let mut ci = 0usize;
|
|
for n in 0..total_samples {
|
|
let t_ns = (n as f64 / SR as f64 * 1e9) as i64;
|
|
while ci < clicks.len() && clicks[ci].time_ns <= t_ns {
|
|
let voice = default_kit(&track.lanes[clicks[ci].lane].sound);
|
|
synth.trigger(voice, clicks[ci].level);
|
|
ci += 1;
|
|
}
|
|
out.push((synth.next_sample() * 30000.0) as i16);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn write_wav(path: &str, samples: &[i16]) -> io::Result<()> {
|
|
let data_len = (samples.len() * 2) as u32;
|
|
let sr = 48_000u32;
|
|
let mut f = File::create(path)?;
|
|
f.write_all(b"RIFF")?;
|
|
f.write_all(&(36 + data_len).to_le_bytes())?;
|
|
f.write_all(b"WAVE")?;
|
|
f.write_all(b"fmt ")?;
|
|
f.write_all(&16u32.to_le_bytes())?; // PCM fmt chunk
|
|
f.write_all(&1u16.to_le_bytes())?; // format = PCM
|
|
f.write_all(&1u16.to_le_bytes())?; // channels = mono
|
|
f.write_all(&sr.to_le_bytes())?;
|
|
f.write_all(&(sr * 2).to_le_bytes())?; // byte rate
|
|
f.write_all(&2u16.to_le_bytes())?; // block align
|
|
f.write_all(&16u16.to_le_bytes())?; // bits/sample
|
|
f.write_all(b"data")?;
|
|
f.write_all(&data_len.to_le_bytes())?;
|
|
for s in samples {
|
|
f.write_all(&s.to_le_bytes())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn main() {
|
|
let demos: &[(&str, &str, i64)] = &[
|
|
("pm-synth-909.wav", "t124;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X", 4),
|
|
("pm-synth-808.wav", "t90;kick808:4=X..x;snare808:4=.X.X;hat808:4/2", 4),
|
|
("pm-synth-default.wav", "t120;kick:4;snare:4=.x.x;hatClosed:4/2", 4),
|
|
("pm-synth-poly.wav", "t100;kick909:4;clap909:5~;hat808:4/2", 4),
|
|
];
|
|
for (name, prog, bars) in demos {
|
|
let s = render(prog, *bars);
|
|
match write_wav(name, &s) {
|
|
Ok(_) => println!("wrote {} ({:.1}s, {} samples)", name, s.len() as f32 / SR, s.len()),
|
|
Err(e) => eprintln!("error writing {}: {}", name, e),
|
|
}
|
|
}
|
|
|
|
// Device preview: the SAME Player the Daisy firmware runs, rendered to a WAV. Eight seconds of
|
|
// SPIKE_PROGRAM, looping at the pattern boundary — a faithful preview of the hardware output.
|
|
let preview = render_device(SPIKE_PROGRAM, SPIKE_BARS, 8.0);
|
|
match write_wav("pm-daisy-preview.wav", &preview) {
|
|
Ok(_) => println!(
|
|
"wrote pm-daisy-preview.wav ({:.1}s) — device-identical render of `{}`",
|
|
preview.len() as f32 / SR,
|
|
SPIKE_PROGRAM
|
|
),
|
|
Err(e) => eprintln!("error writing pm-daisy-preview.wav: {}", e),
|
|
}
|
|
}
|