makepad/libs/drumkit_phys/examples/demo_pattern.rs
Admin f2b02531b7 drumkit: a sample-based kit for the score preview; the physical model parked as drumkit_phys
The kit that plays drum scores is now a sample player over the Salamander Drumkit (velocity layers with round-robins, derived toms, a synthesised clap), loaded off the audio thread and swapped in atomically. The physically modelled kit stays as makepad-drumkit-phys for a later pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 02:52:12 +02:00

174 lines
6.5 KiB
Rust

// Renders the kit to WAV files (48 kHz, stereo, 24-bit) for listening and
// for the measurement harness:
// cargo run -p makepad-drumkit-phys --release --example demo_pattern -- <out-dir> [sample-rate]
// Writes model_<voice>_<vel>.wav for every voice at velocities 0.3/0.6/1.0
// (single hits, trimmed when the voice ends) and pattern_model.wav, a
// four-bar groove at 120 bpm that uses the whole kit.
use makepad_drumkit_phys::{DrumKit, DrumVoice};
use std::io::Write;
fn write_wav24(path: &std::path::Path, fs: u32, frames: &[[f32; 2]]) -> std::io::Result<()> {
let mut bytes = Vec::with_capacity(44 + frames.len() * 6);
let data_len = (frames.len() * 6) as u32;
bytes.extend_from_slice(b"RIFF");
bytes.extend_from_slice(&(36 + data_len).to_le_bytes());
bytes.extend_from_slice(b"WAVEfmt ");
bytes.extend_from_slice(&16u32.to_le_bytes());
bytes.extend_from_slice(&1u16.to_le_bytes()); // PCM
bytes.extend_from_slice(&2u16.to_le_bytes());
bytes.extend_from_slice(&fs.to_le_bytes());
bytes.extend_from_slice(&(fs * 6).to_le_bytes());
bytes.extend_from_slice(&6u16.to_le_bytes());
bytes.extend_from_slice(&24u16.to_le_bytes());
bytes.extend_from_slice(b"data");
bytes.extend_from_slice(&data_len.to_le_bytes());
for f in frames {
for &s in f {
let v = (s.clamp(-1.0, 1.0) * 8_388_607.0).round() as i32;
let b = v.to_le_bytes();
bytes.extend_from_slice(&b[..3]);
}
}
std::fs::File::create(path)?.write_all(&bytes)
}
fn voice_name(v: DrumVoice) -> &'static str {
match v {
DrumVoice::Kick => "kick",
DrumVoice::Snare => "snare",
DrumVoice::SideStick => "sidestick",
DrumVoice::HiHatClosed => "hihatclosed",
DrumVoice::HiHatOpen => "hihatopen",
DrumVoice::HiHatPedal => "hihatpedal",
DrumVoice::TomHigh => "tomhigh",
DrumVoice::TomMid => "tommid",
DrumVoice::TomLow => "tomlow",
DrumVoice::TomFloor => "tomfloor",
DrumVoice::Ride => "ride",
DrumVoice::RideBell => "ridebell",
DrumVoice::Crash => "crash",
DrumVoice::Clap => "clap",
}
}
/// Renders one hit until the voice goes quiet (or `max_s`), in 256-frame
/// blocks, and pads 50 ms of silence.
fn render_hit(fs: f32, voice: DrumVoice, vel: f32, max_s: f32) -> Vec<[f32; 2]> {
let mut kit = DrumKit::new(fs);
kit.trigger(voice, vel);
let mut out = Vec::new();
let mut block = [[0.0f32; 2]; 256];
let max = (max_s * fs) as usize;
while kit.active() && out.len() < max {
block.iter_mut().for_each(|f| *f = [0.0; 2]);
kit.process(&mut block);
out.extend_from_slice(&block);
}
out.extend(std::iter::repeat([0.0; 2]).take((0.05 * fs) as usize));
out
}
struct Hit {
beat: f32,
voice: DrumVoice,
vel: f32,
}
fn groove() -> Vec<Hit> {
use DrumVoice::*;
let mut h = Vec::new();
let mut add = |bar: usize, beat: f32, voice: DrumVoice, vel: f32| h.push(Hit { beat: bar as f32 * 4.0 + beat, voice, vel });
for bar in 0..4 {
// hats: 8ths, accents on the beat, open on the "and" of 4 in bars 2/4
for i in 0..8 {
let b = i as f32 * 0.5;
let vel = if i % 2 == 0 { 0.75 } else { 0.45 };
if bar % 2 == 1 && i == 7 {
add(bar, b, HiHatOpen, 0.7);
} else if !(bar == 3 && i >= 4) {
add(bar, b, HiHatClosed, vel);
}
}
if bar % 2 == 1 {
add(bar, 4.0 - 0.02, HiHatPedal, 0.6); // close the open hat on the downbeat
}
// kick
add(bar, 0.0, Kick, 1.0);
add(bar, 1.75, Kick, 0.8);
add(bar, 2.5, Kick, 0.9);
// snare 2 and 4 (+ ghosts)
add(bar, 1.0, Snare, 0.95);
add(bar, 3.0, Snare, 1.0);
add(bar, 2.25, Snare, 0.25);
add(bar, 3.75, Snare, 0.3);
if bar == 1 {
add(bar, 2.0, SideStick, 0.7);
add(bar, 3.5, Clap, 0.9);
}
if bar == 2 {
add(bar, 1.0, Clap, 1.0);
add(bar, 3.0, Clap, 0.8);
}
}
// bar 4: ride + fill
for i in 0..4 {
add(3, i as f32 * 0.5, Ride, if i % 2 == 0 { 0.8 } else { 0.55 });
}
add(3, 1.5, RideBell, 0.9);
add(3, 2.0, TomHigh, 0.9);
add(3, 2.25, TomHigh, 0.7);
add(3, 2.5, TomMid, 0.9);
add(3, 2.75, TomMid, 0.7);
add(3, 3.0, TomLow, 0.95);
add(3, 3.25, TomLow, 0.7);
add(3, 3.5, TomFloor, 1.0);
add(3, 3.75, TomFloor, 0.8);
add(0, 0.0, Crash, 1.0);
add(2, 0.0, Crash, 0.8);
h.push(Hit { beat: 16.0, voice: Crash, vel: 1.0 });
h.push(Hit { beat: 16.0, voice: Kick, vel: 1.0 });
h
}
pub fn render_pattern(fs: f32, bpm: f32, tail_s: f32) -> Vec<[f32; 2]> {
let mut hits = groove();
hits.sort_by(|a, b| a.beat.partial_cmp(&b.beat).unwrap());
let spb = 60.0 / bpm;
let total = ((16.0 * spb + tail_s) * fs) as usize;
let mut kit = DrumKit::new(fs);
let mut out = vec![[0.0f32; 2]; total];
let block = 128usize;
let mut next = 0usize;
let mut pos = 0usize;
while pos < total {
let n = block.min(total - pos);
while next < hits.len() && ((hits[next].beat * spb * fs) as usize) < pos + n {
kit.trigger(hits[next].voice, hits[next].vel);
next += 1;
}
kit.process(&mut out[pos..pos + n]);
pos += n;
}
out
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let dir = std::path::PathBuf::from(args.get(1).map(String::as_str).unwrap_or("."));
let fs: f32 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(48000.0);
std::fs::create_dir_all(&dir).expect("output dir");
for voice in DrumVoice::ALL {
for vel in [0.3f32, 0.6, 1.0] {
let frames = render_hit(fs, voice, vel, 12.0);
let name = format!("model_{}_{:.1}.wav", voice_name(voice), vel);
write_wav24(&dir.join(&name), fs as u32, &frames).expect("write");
let peak = frames.iter().flat_map(|f| f.iter()).fold(0.0f32, |a, v| a.max(v.abs()));
println!("{name}: {:.2} s, peak {:.1} dBFS", frames.len() as f32 / fs, 20.0 * peak.max(1e-9).log10());
}
}
let pattern = render_pattern(fs, 120.0, 3.0);
let peak = pattern.iter().flat_map(|f| f.iter()).fold(0.0f32, |a, v| a.max(v.abs()));
write_wav24(&dir.join("pattern_model.wav"), fs as u32, &pattern).expect("write");
println!("pattern_model.wav: {:.2} s, peak {:.2} dBFS", pattern.len() as f32 / fs, 20.0 * peak.max(1e-9).log10());
}