From eec285a8a7750e0bcbfd2c1c9cd699629e138ef3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 03:52:38 +0000 Subject: [PATCH] Verify pickup measure support, document MIDI import gap Traced every path a manually-entered short first measure touches: adding a measure (MeasureNavigationViewModel.ApplyAddMeasure) never checks the previous one is "full", and both playback scheduling (ScoreMidiSchedule.GetMeasureDurationUnits) and MIDI export derive a measure's length purely from its own notes' actual durations, with no fixed-beat-count assumption anywhere. So a deliberately short first measure already plays/exports/renders correctly today -- no code change needed for the editor itself. Added a regression test proving the second measure starts right after the short one instead of being padded out, so this stays true going forward. While verifying this, found a real, separate gap: MidiImportService feeds imported notes through MeasureNormalizationService.NormalizeMeasures, which flattens the whole file into one stream and rechunks it into fixed-size measures from scratch, silently absorbing any real anacrusis in the source file. Documented in ROADMAP.md as its own scoped follow-up rather than fixed here, since reliably detecting an intended pickup from raw MIDI (versus a recording that just started off-beat) is real design work. Verified via the same sandbox pipeline as every prior change this session: dotnet build succeeds against the real project/package graph, dotnet format --verify-no-changes passes. dotnet test itself cannot execute in this sandbox; real confirmation of the new test comes from GitHub Actions CI. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Pguj4XSScE141p1ScWoqEr --- .../Services/ScoreMidiScheduleTests.cs | 27 +++++++++++++++ ROADMAP.md | 34 +++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/JianpuEditor.Tests/Services/ScoreMidiScheduleTests.cs b/JianpuEditor.Tests/Services/ScoreMidiScheduleTests.cs index 81ff79a..c43d860 100644 --- a/JianpuEditor.Tests/Services/ScoreMidiScheduleTests.cs +++ b/JianpuEditor.Tests/Services/ScoreMidiScheduleTests.cs @@ -47,6 +47,33 @@ public void Build_SchedulesMelodyAndChordNotes() Assert.Contains(schedule.Notes, note => note.Channel == ScoreMidiSchedule.ChordChannel); } + [Fact] + public void Build_PickupMeasure_SecondMeasureStartsRightAfterShortFirstMeasure() + { + // A pickup/anacrusis measure (fewer beats than the nominal time signature) isn't + // padded or reflowed to a fixed beat count anywhere in the schedule-building path -- + // each measure's contribution to quarterTime comes purely from its own notes' actual + // durations, so a short first measure just makes the next one start earlier. + var score = ScoreTestHelper.CreateScore( + ScoreTestHelper.Measure(ScoreTestHelper.Note(1, dashes: 1)), + ScoreTestHelper.Measure( + ScoreTestHelper.Note(2), + ScoreTestHelper.Note(3), + ScoreTestHelper.Note(4), + ScoreTestHelper.Note(5))); + + var schedule = ScoreMidiSchedule.Build(score); + var melodyNotes = schedule.Notes + .Where(note => note.Channel == ScoreMidiSchedule.MelodyChannel) + .OrderBy(note => note.StartQuarter) + .ToList(); + + Assert.Equal(0, melodyNotes[0].StartQuarter, 3); + Assert.Equal(2, melodyNotes[0].DurationQuarter, 3); + Assert.Equal(2, melodyNotes[1].StartQuarter, 3); + Assert.Equal(6, schedule.TotalQuarterLength, 3); + } + [Fact] public void Build_SuppressesTieEndNotes() { diff --git a/ROADMAP.md b/ROADMAP.md index 95ad41a..06a0efd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,10 +29,12 @@ Until now, playback always used whatever General MIDI patch 0 (Acoustic Grand Pi ## Indonesian notasi angka completeness (gap analysis + plan) -**Status: phase 3's bug-fix half, the Mordent half of phase 4, and phase 6 (breath marks) are -done. Everything else below is still not started**, including phases 1-2 (`NotationStyle` + the -accidental slash convention) -- see the note under phase 2 for why that one turned out to be more -involved than it looked, and the new note there about the natural sign specifically. +**Status: phase 3's bug-fix half, the Mordent half of phase 4, phase 6 (breath marks), and phase +10 (pickup measure verification) are done. Everything else below is still not started**, including +phases 1-2 (`NotationStyle` + the accidental slash convention) -- see the note under phase 2 for +why that one turned out to be more involved than it looked, and the new note there about the +natural sign specifically. Phase 10 also surfaced a separate, real gap in MIDI import (pickup +measures aren't preserved) -- see the note under phase 10. Researched Indonesian *notasi angka* (Indonesian numbered/jianpu notation — rules, symbols, conventions) against what `JianpuEditor` actually implements. Full gap list and phased plan below. @@ -165,9 +167,27 @@ here and intentionally excluded.* wave as items 1-8, and prototype with 2 voices before committing to 4 (SATB) — 2 voices proves out the whole architecture (each voice's own note list, ties, and undo integration, all sharing one chord-marker row/lyric block/measure grid) at half the risk. -10. **Pickup measure.** Verify first whether a short first measure already plays/exports/renders - correctly (it may — nothing found in `ScoreMidiSchedule` enforcing an exact per-measure beat - count). If it does, this is a documentation note, not code. If not, scope it then. +10. **Pickup measure — verified, documentation only, done.** Traced every path a manually-entered + short first measure touches: editing (`MeasureNavigationViewModel.ApplyAddMeasure` appends a + new measure unconditionally, no check that the previous one is "full"), rendering/beam + grouping and `ScoreMidiSchedule`/`MidiExportService` (`GetMeasureDurationUnits` sums the + measure's own notes, with no fixed-beat-count assumption anywhere in either file). All of it + derives a measure's length purely from its actual note content, so a deliberately short first + measure already plays, exports, and renders correctly today — confirmed with a new regression + test (`ScoreMidiScheduleTests.Build_PickupMeasure_SecondMeasureStartsRightAfterShortFirstMeasure`) + proving the second measure starts right after the short one instead of being padded out. + No code change needed for the editor itself. + + **Found a real, separate gap while verifying this: MIDI import does not preserve a pickup + measure.** `MidiImportService` feeds its raw note stream through + `MeasureNormalizationService.NormalizeMeasures`, which flattens every note across the *entire* + imported file into one continuous stream and rechunks it into fixed `DefaultMeasureBeats`-size + measures from scratch — so a real anacrusis in the source MIDI file gets silently absorbed + into the reflow instead of preserved as a short first measure. Fixing this needs a way to + detect an intended pickup from the MIDI file itself (e.g. a shorter first bar implied by the + time-signature meta-event's position, which isn't reliably distinguishable from "the recording + just started off-beat") — real design work, scoped separately rather than folded into this + already-done verification. ## CLAP support (spike plan only, not started)