pm-ui: drum notation view (first pass)
draw_notation() renders a bar as standard drum notation: 5-line staff + time signature, voices mapped to staff positions and notehead types (oval drums, cross hi-hat/cymbals), hands stem-up / feet stem-down, beamed eighths/sixteenths grouped per beat, accents tinted. Developed entirely in the simulator (uisim --bin notesim → PNG). Firmware build unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
04350f9d09
commit
93617e1a91
2 changed files with 171 additions and 0 deletions
|
|
@ -244,6 +244,128 @@ where
|
||||||
Ok(())
|
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>(d: &mut D, s: &Screen) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Rgb565>,
|
||||||
|
{
|
||||||
|
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).
|
/// Bring-up diagnostic pattern (kept for hardware bring-up / fallback).
|
||||||
pub fn draw_ui<D>(d: &mut D) -> Result<(), D::Error>
|
pub fn draw_ui<D>(d: &mut D) -> Result<(), D::Error>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
49
rust/uisim/src/bin/notesim.rs
Normal file
49
rust/uisim/src/bin/notesim.rs
Normal file
|
|
@ -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<Rgb565>,
|
||||||
|
}
|
||||||
|
impl DrawTarget for Fb {
|
||||||
|
type Color = Rgb565;
|
||||||
|
type Error = core::convert::Infallible;
|
||||||
|
fn draw_iter<I: IntoIterator<Item = Pixel<Rgb565>>>(&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<LaneView> = track
|
||||||
|
.lanes
|
||||||
|
.iter()
|
||||||
|
.map(|l| LaneView { name: &l.sound, levels: &l.levels, beats: l.groups.iter().sum::<u32>().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}");
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue