//! 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 { 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 { 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), } }