Fix grace note MIDI export: silent grace notes in saved score files#33743
Fix grace note MIDI export: silent grace notes in saved score files#33743jonashogstrom wants to merge 1 commit into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR fixes grace note MIDI export by preventing grace chords from being marked/persisted as user play-events, correcting timing in the render path to use the principal chord tick and properly clamp/adjust on/off times for negative offsets, always regenerating stored grace-note event lists, and adding end-to-end tests (grace-before, grace-after, appoggiatura, and stale zero-length event regression). 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsLinked repositories: Your configuration references 1 linked repositories, but your current plan allows 0. Analyzed ``, skipped Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/importexport/midi/tests/midiexport_tests.cpp`:
- Around line 352-354: The comment in the midiGraceAppoggiatura test is wrong
about the appoggiatura duration: update the explanatory comment next to the
EXPECT_NE checks for findNoteEvent(track, 480, 71, 80) and findNoteEvent(track,
720, 72, 80) to state that the grace starts at 480 and the principal is at 720
(so the grace length is 240 ticks, not 480), and remove the incorrect
“(min(500,500)) = 480” math so the comment correctly reflects the +240 tick
displacement between grace and principal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 84390cd9-843c-4166-b809-4c61049b76b6
📒 Files selected for processing (8)
src/engraving/compat/midi/compatmidirender.cppsrc/engraving/compat/midi/compatmidirenderinternal.cppsrc/engraving/rw/read400/tread.cppsrc/engraving/rw/write/twrite.cppsrc/importexport/midi/tests/midiexport_data/testGraceAfter.mscxsrc/importexport/midi/tests/midiexport_data/testGraceAppoggiatura.mscxsrc/importexport/midi/tests/midiexport_data/testGraceStaleEvents.mscxsrc/importexport/midi/tests/midiexport_tests.cpp
36c12fc to
e51ac69
Compare
Grace notes were silent in MIDI export after a score had been saved at least once. Root causes: (1) the before-beat graceTickSum shift was broken since 4.1; (2) saving wrote <Events> for grace chords, which on reload set PlayEventType::User and caused createGraceNotesPlayEvents() to skip recomputing, leaving stale len=0 events in place; (3) acciaccatura at tick 0 produced a zero-duration MIDI note. Fixed at write, read, and render layers (defence-in-depth). Four new unit tests cover before-beat, after-beat, appoggiatura, and stale-event scenarios. Fixes musescore#22669
e51ac69 to
340cf28
Compare
Fixes #22669
Background
Issue #22669 reports that acciaccatura grace notes stopped exporting correctly to MIDI in MuseScore 4.1 (regression from 4.0.2). In the exported MIDI they play on the beat rather than before it, or are silent entirely. The workaround discovered by users — manually entering play events via the Properties panel — is itself a clue: setting events manually forces
PlayEventType::User, which (as described below) is part of the root cause.Root causes and fixes
Fix 1 — Before-beat timing regression (the core 4.1 regression)
The
graceTickSummechanism that shifts acciaccatura events before the beat was broken, causing grace notes to play on the beat with near-zero duration instead of before it. Restored the correct before-beat offset computation incollectGraceBeforeChordEvents.Fix 2 — Stale
PlayEventType::Usersilences grace notes in saved filesMuseScore writes
<Events>blocks for every note whenever a score is saved, including grace notes. On load, reading<Events>marks the chordPlayEventType::User.createGraceNotesPlayEvents()was guarded byif (gc->playEventType() == PlayEventType::Auto), so it skipped recomputing grace events — leaving stalelen=0values from earlier renders in place and making those grace notes completely silent.This affected any score that had been saved at least once, including files freshly imported from MusicXML and saved with the current version. It also explains why the manual-entry workaround in the issue worked: forcing
PlayEventType::Userwith correct values bypassed the broken auto-computation.Fixed at three levels (defence-in-depth):
twrite.cpp: never write<Events>for grace note chords. MuseScore 4 exposes no UI for editing grace note MIDI timing, so there is nothing worth persisting. Breaks the write-load-skip cycle at its source; existing saved files are cleaned up on next save.tread.cpp: when reading<Events>for a grace chord, do not setPlayEventType::User— the stored data is always a stale render artifact.compatmidirender.cpp: always recompute grace note events regardless ofPlayEventType, as a defensive backstop.Fix 3 — Acciaccatura at tick 0 produces zero-duration MIDI note
Acciaccatura before the very first beat of a piece was silent. The before-beat start is
tick - graceTickSum; attick=0this clamps to0. WithNoteEvent.len=0(correct for acciaccatura),off = max(0, 0 - 1) = 0, giving a zero-duration MIDI note. Fixed by preserving the intended duration when the start is clamped:offTime = graceOffsetOn - 1.Tests added
midiGraceBeforemidiGraceAftermidiGraceAppoggiaturamidiGraceStaleEventslen=0Events (as written by MuseScore on every save) are exported with correct timingFiles changed
src/engraving/compat/midi/compatmidirender.cppPlayEventType::Autoguards — always recompute grace eventssrc/engraving/compat/midi/compatmidirenderinternal.cppgraceTickSummechanismsrc/engraving/rw/write/twrite.cpp<Events>for grace note chordssrc/engraving/rw/read400/tread.cppPlayEventType::Userwhen reading grace chord eventssrc/importexport/midi/tests/midiexport_tests.cppsrc/importexport/midi/tests/midiexport_data/testGraceAfter.mscxsrc/importexport/midi/tests/midiexport_data/testGraceAppoggiatura.mscxsrc/importexport/midi/tests/midiexport_data/testGraceStaleEvents.mscxsrc/importexport/musicxml/tests/data/testBackupRoundingError_ref.mscxsrc/importexport/musicxml/tests/data/testCueGraceNotes_ref.mscxsrc/importexport/musicxml/tests/data/testDuplicateFermataOnGraceNote_ref.mscxsrc/importexport/musicxml/tests/data/testDuplicateFermataOnGraceNoteAndMainNote_ref.mscxsrc/importexport/musicxml/tests/data/testSticking_ref.mscxNote on the MusicXML reference file changes: The five
_ref.mscxfiles are round-trip reference snapshots used byMusicXml_Tests. Becausetwrite.cppno longer writes<Events>for grace note chords, the saved output no longer contains those blocks — so the reference files must match. The removed blocks were all stale<len>0</len>render artifacts that had no effect on the score; removing them is correct behaviour.