From b32065a109a93fbc539810f4e7b3965734623340 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:34:16 +0100 Subject: [PATCH 1/2] Keep score export notes when bars are shorter than a sixteenth Found while testing crates/engine/src/score.rs:~150 (measure_grid, `if ub <= ua { break; }`), used by layout() Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/score.rs | 48 +++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/score.rs b/crates/engine/src/score.rs index 0188f923..884e8d80 100644 --- a/crates/engine/src/score.rs +++ b/crates/engine/src/score.rs @@ -146,12 +146,20 @@ fn measure_grid(s: &Session, end_unit: i64) -> Vec<(i64, i64, u32, u32)> { let a = s.tempo.bar_start_tick(bar); let b = s.tempo.bar_start_tick(bar + 1); let m = s.tempo.meter_at_tick(a); - let (ua, ub) = (a / UNIT, b / UNIT); - if ub <= ua { + if b <= a { break; } - v.push((ua, ub - ua, m.numerator, m.denominator)); + let (ua, ub) = (a / UNIT, b / UNIT); bar += 1; + if ub <= ua { + // A bar shorter than a sixteenth that does not reach the next unit: the following + // bar carries this span, so the grid stays contiguous. + continue; + } + // Bars shorter than a sixteenth are written as one measure of the units they cover, so + // the time signature matches the measure's content. + let (num, den) = if m.ticks_per_bar() < UNIT { (u32::try_from(ub - ua).unwrap_or(1), 16) } else { (m.numerator, m.denominator) }; + v.push((ua, ub - ua, num, den)); if ub >= end_unit.max(1) || v.len() >= MAX_MEASURES { break; } @@ -644,6 +652,40 @@ mod tests { assert!(s.starts_with(" usize { + parts.iter().flat_map(|p| &p.measures).flat_map(|m| &m.items).filter(|i| !i.tie_stop).map(|i| i.pitches.len()).sum() + } + + #[test] + fn bars_shorter_than_a_sixteenth_keep_every_note() { + let base = crate::demo::demo_engine(); + let baseline = note_starts(&layout(base.session(), &[])); + assert!(baseline > 0, "demo has notes"); + for (n, d) in [(1, 32), (1, 64), (2, 64), (3, 64)] { + for mid in [false, true] { + let mut e = crate::demo::demo_engine(); + let tick = if mid { e.session().tempo.bar_start_tick(5) } else { 0 }; + e.session_mut().tempo.set_meter(tick, n, d).expect("valid meter"); + let parts = layout(e.session(), &[]); + assert!(parts.iter().all(|p| !p.measures.is_empty()), "{n}/{d} mid={mid}: empty part"); + assert_eq!(note_starts(&parts), baseline, "{n}/{d} mid={mid}: notes lost"); + for p in &parts { + for m in &p.measures { + let units: u32 = m.items.iter().map(|i| i.units).sum(); + // The written meter matches the measure's content (divisions are sixteenths). + assert_eq!(units * m.denominator, 16 * m.numerator, "{n}/{d} mid={mid}: meter/content mismatch"); + } + } + let setup = ScoreSetup::from_session(e.session()); + let x = musicxml(&parts, &setup).expect("musicxml fits"); + assert!(x.contains(""), "{n}/{d} mid={mid}: musicxml has no notes"); + let s = svg(&parts, &setup).expect("svg fits"); + assert!(s.contains(" Date: Sun, 1 Nov 2026 23:06:02 -0500 Subject: [PATCH 2/2] Test a grid of 1/16, 1/32 and 1/64 bars in a row Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/score.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/engine/src/score.rs b/crates/engine/src/score.rs index 884e8d80..70849156 100644 --- a/crates/engine/src/score.rs +++ b/crates/engine/src/score.rs @@ -686,6 +686,24 @@ mod tests { } } + #[test] + fn bars_of_a_sixteenth_a_32nd_and_a_64th_in_a_row_keep_a_contiguous_grid() { + let base = crate::demo::demo_engine(); + let baseline = note_starts(&layout(base.session(), &[])); + let mut e = crate::demo::demo_engine(); + // Bars 5 to 8 are 1/16, 1/32, 1/64 and 3/64 long, then 4/4 resumes. + for (bar, (n, d)) in (5..).zip([(1, 16), (1, 32), (1, 64), (3, 64), (4, 4)]) { + let tick = e.session().tempo.bar_start_tick(bar); + e.session_mut().tempo.set_meter(tick, n, d).expect("valid meter"); + } + let grid = measure_grid(e.session(), i64::MAX); + assert_eq!(grid.first().map(|m| m.0), Some(0)); + for w in grid.windows(2) { + assert_eq!(w[0].0 + w[0].1, w[1].0, "measures leave no gap or overlap: {w:?}"); + } + assert_eq!(note_starts(&layout(e.session(), &[])), baseline, "notes lost"); + } + #[test] fn output_is_bounded_by_byte_budget() { // A part with many notes must not produce an unbounded document.