diff --git a/rust/pm-ui/src/lib.rs b/rust/pm-ui/src/lib.rs index 5b3f345..eb2d089 100644 --- a/rust/pm-ui/src/lib.rs +++ b/rust/pm-ui/src/lib.rs @@ -244,6 +244,128 @@ where Ok(()) } +// ---- drum notation ---- +enum Head { + Oval, + Cross, +} +/// Map a voice name to (vertical offset from the staff top line, notehead, stem-up?). +/// Offsets are multiples of 6 (half a 12px line-space) so heads land on lines/spaces. +fn map_voice(name: &str) -> (i32, Head, bool) { + if name.starts_with("kick") { + (42, Head::Oval, false) // bass drum: low, stem DOWN (foot) + } else if name.starts_with("snare") || name.starts_with("clap") || name.starts_with("rim") { + (18, Head::Oval, true) + } else if name.starts_with("hat") || name.starts_with("openHat") { + (-6, Head::Cross, true) // hi-hat: above the staff + } else if name.starts_with("ride") || name.starts_with("crash") { + (0, Head::Cross, true) + } else if name.starts_with("tom") { + (12, Head::Oval, true) + } else if name.starts_with("cowbell") || name.starts_with("woodblock") || name.starts_with("claves") || name.starts_with("tambourine") { + (6, Head::Cross, true) + } else { + (24, Head::Oval, true) + } +} + +/// Render one bar of the groove as drum notation: 5-line staff, time signature, noteheads with +/// stems (hands up / feet down) and beamed eighths/sixteenths. First pass — refine freely. +pub fn draw_notation(d: &mut D, s: &Screen) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let bb = d.bounding_box(); + let w = bb.size.width as i32; + d.clear(BG)?; + + // header + Text::new(s.name, Point::new(12, 22), MonoTextStyle::new(&FONT_10X20, TXT)).draw(d)?; + let mut nb = [0u8; 12]; + Text::with_alignment(fmt_u32(s.bpm.max(0) as u32, &mut nb), Point::new(w - 12, 18), MonoTextStyle::new(&FONT_9X18_BOLD, CYAN), Alignment::Right).draw(d)?; + + let beats = s.lanes.first().map(|l| l.beats.max(1)).unwrap_or(4) as i32; + let staff_top = 80; + let line_gap = 12; + let m = 14; + let clef_w = 34; + let x0 = m + clef_w; + let x1 = w - m; + let bw = (x1 - x0).max(1); + let ink = TXT; + + // staff (5 lines) + for i in 0..5 { + let y = staff_top + i * line_gap; + Line::new(Point::new(m, y), Point::new(x1, y)).into_styled(PrimitiveStyle::with_stroke(ink, 1)).draw(d)?; + } + // bar lines (start + end) + Line::new(Point::new(m, staff_top), Point::new(m, staff_top + 4 * line_gap)).into_styled(PrimitiveStyle::with_stroke(ink, 2)).draw(d)?; + Line::new(Point::new(x1, staff_top), Point::new(x1, staff_top + 4 * line_gap)).into_styled(PrimitiveStyle::with_stroke(ink, 2)).draw(d)?; + // time signature (beats / 4) + let ts = MonoTextStyle::new(&FONT_10X20, ink); + let mut tb = [0u8; 12]; + Text::with_alignment(fmt_u32(beats as u32, &mut tb), Point::new(m + clef_w / 2, staff_top + 16), ts, Alignment::Center).draw(d)?; + Text::with_alignment("4", Point::new(m + clef_w / 2, staff_top + 40), ts, Alignment::Center).draw(d)?; + + let stem_len = 30; + + for lane in s.lanes.iter() { + if lane.muted { + continue; + } + let (off, head, up) = map_voice(lane.name); + let hy = staff_top + off; + let steps = lane.levels.len().max(1) as i32; + let sub = (steps / beats).max(1); // steps per beat → note value + let stem_y = if up { hy - stem_len } else { hy + stem_len }; + + // stem x is on the right for up-stems, left for down-stems + let mut prev_beam: Option<(i32, i32)> = None; // (x, beatgroup) of previous beamable note + + for (i, &lvl) in lane.levels.iter().enumerate() { + if lvl == 0 { + continue; + } + let i = i as i32; + let cx = x0 + i * bw / steps + bw / (2 * steps); + let stem_x = if up { cx + 5 } else { cx - 5 }; + let col = if lvl == 2 { AMBER } else { ink }; // accent tint + + // notehead + match head { + Head::Oval => { + embedded_graphics::primitives::Ellipse::new(Point::new(cx - 6, hy - 4), Size::new(12, 8)) + .into_styled(PrimitiveStyle::with_fill(col)) + .draw(d)?; + } + Head::Cross => { + Line::new(Point::new(cx - 5, hy - 5), Point::new(cx + 5, hy + 5)).into_styled(PrimitiveStyle::with_stroke(col, 2)).draw(d)?; + Line::new(Point::new(cx - 5, hy + 5), Point::new(cx + 5, hy - 5)).into_styled(PrimitiveStyle::with_stroke(col, 2)).draw(d)?; + } + } + // stem + Line::new(Point::new(stem_x, hy), Point::new(stem_x, stem_y)).into_styled(PrimitiveStyle::with_stroke(ink, 2)).draw(d)?; + + // beam to the previous note if both are subdivided and in the same beat + if sub >= 2 { + let beat = i / sub; + if let Some((px, pbeat)) = prev_beam { + if pbeat == beat { + Rectangle::new(Point::new(px.min(stem_x), stem_y - 1), Size::new((px - stem_x).unsigned_abs() + 2, 4)) + .into_styled(PrimitiveStyle::with_fill(ink)) + .draw(d)?; + } + } + prev_beam = Some((stem_x, beat)); + } + } + } + + Text::new("drum notation", Point::new(12, staff_top + 4 * line_gap + 40), MonoTextStyle::new(&FONT_6X10, MUTE)).draw(d)?; + Ok(()) +} + /// Bring-up diagnostic pattern (kept for hardware bring-up / fallback). pub fn draw_ui(d: &mut D) -> Result<(), D::Error> where diff --git a/rust/uisim/src/bin/notesim.rs b/rust/uisim/src/bin/notesim.rs new file mode 100644 index 0000000..f15961d --- /dev/null +++ b/rust/uisim/src/bin/notesim.rs @@ -0,0 +1,49 @@ +//! Render the drum-notation view (pm-ui) for a parsed groove → PNG. `cargo run --bin notesim [prog]`. + +use embedded_graphics::{pixelcolor::Rgb565, prelude::*}; +use pm_ui::{LaneView, Screen}; + +const W: u32 = 320; +const H: u32 = 480; + +struct Fb { + px: Vec, +} +impl DrawTarget for Fb { + type Color = Rgb565; + type Error = core::convert::Infallible; + fn draw_iter>>(&mut self, pixels: I) -> Result<(), Self::Error> { + for Pixel(p, c) in pixels { + if p.x >= 0 && p.y >= 0 && (p.x as u32) < W && (p.y as u32) < H { + self.px[(p.y as u32 * W + p.x as u32) as usize] = c; + } + } + Ok(()) + } +} +impl OriginDimensions for Fb { + fn size(&self) -> Size { + Size::new(W, H) + } +} + +fn main() { + let prog = std::env::args().nth(1).unwrap_or_else(|| "t96;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2=xxxxxxxx".into()); + let track = track_format::parse(&prog); + let lanes: Vec = track + .lanes + .iter() + .map(|l| LaneView { name: &l.sound, levels: &l.levels, beats: l.groups.iter().sum::().min(255) as u8, poly: l.poly, muted: l.mute }) + .collect(); + let screen = Screen { name: "Rock beat", bpm: track.bpm, playing: false, phase: 0.0, lanes: &lanes }; + + let mut fb = Fb { px: vec![Rgb565::BLACK; (W * H) as usize] }; + pm_ui::draw_notation(&mut fb, &screen).unwrap(); + + let img = image::RgbImage::from_fn(W, H, |x, y| { + let c = fb.px[(y * W + x) as usize]; + image::Rgb([(c.r() << 3) | (c.r() >> 2), (c.g() << 2) | (c.g() >> 4), (c.b() << 3) | (c.b() >> 2)]) + }); + img.save("notation.png").unwrap(); + println!("wrote notation.png for: {prog}"); +}