MusicXML: import multi-child dynamics as a single marking - #34492
MusicXML: import multi-child dynamics as a single marking#34492rpatters1 wants to merge 2 commits into
Conversation
A dynamics element containing several children is one marking, whose children spell out the glyphs it is drawn with. The MusicXML specification gives "<sf/><mp/>" as an example of dynamics elements combined to create marks not covered by a single element. The importer created one Dynamic per child instead, so such a marking rendered as overlapping dynamics. Collect the children of each dynamics element into a single Dynamic. Each child is converted to the SMuFL markup MuseScore spells it with before being concatenated, as their names cannot be: "sf" + "mp" gives "sfmp", which names no dynamic and would end up as plain text. A combination that names a type still resolves to it, and one that does not keeps the correct glyphs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughMusicXML dynamic parsing now converts recognized names to MuseScore SMuFL markup and preserves unrecognized 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsLinked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were 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.
🧹 Nitpick comments (1)
src/importexport/musicxml/tests/musicxml_tests.cpp (1)
1027-1029: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd notation-level dynamics coverage.
testMultipleDynamics.xmlcontains only direction-level<dynamics>elements. The test does not executeMusicXmlParserNotations::dynamics(). Add a compound<note><notations><dynamics>case and its expectedDynamicentry intestMultipleDynamics_ref.mscx.🤖 Prompt for 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. In `@src/importexport/musicxml/tests/musicxml_tests.cpp` around lines 1027 - 1029, Extend the testMultipleDynamics fixture to include a compound note containing notations-level dynamics, ensuring MusicXmlParserNotations::dynamics() is exercised. Update testMultipleDynamics_ref.mscx with the corresponding expected Dynamic entry while preserving the existing direction-level dynamics coverage.
🤖 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.
Nitpick comments:
In `@src/importexport/musicxml/tests/musicxml_tests.cpp`:
- Around line 1027-1029: Extend the testMultipleDynamics fixture to include a
compound note containing notations-level dynamics, ensuring
MusicXmlParserNotations::dynamics() is exercised. Update
testMultipleDynamics_ref.mscx with the corresponding expected Dynamic entry
while preserving the existing direction-level dynamics coverage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4c202e9d-6a47-43f3-8d4d-f3c3142ee16f
📒 Files selected for processing (4)
src/importexport/musicxml/internal/import/importmusicxmlpass2.cppsrc/importexport/musicxml/tests/data/testMultipleDynamics.xmlsrc/importexport/musicxml/tests/data/testMultipleDynamics_ref.mscxsrc/importexport/musicxml/tests/musicxml_tests.cpp
|
CI here is apparently red from a GitHub Actions outage rather than from this change. All three actual failures are at the
Could someone with write access re-run the failed jobs? |
Resolves: #34439
MusicXML allows a single
<dynamics>element to hold several children, and the specification calls this out explicitly: "Dynamics elements may also be combined to create marks not covered by a single element, such as<sf/><mp/>." The element is one marking; its children spell out the glyph sequence it is drawn with.The importer created one
Dynamicper child instead, so such a marking rendered as several dynamics stacked on top of each other at the same tick. The file attached to the issue has three of them, and none of them read correctly.The fix
MusicXmlParserDirection::dynamics()andMusicXmlParserNotations::dynamics()now collect the children of each<dynamics>element into a singleDynamicinstead of pushing one entry tom_dynamicsListper child.Each child is converted to the SMuFL markup MuseScore spells it with before being concatenated. Their names cannot be concatenated directly:
"sf"+"mp"gives"sfmp", which names no dynamic, soDynamic::setDynamicType()would leave it as plain text with no glyphs. Concatenating markup instead letsDynamic::parseDynamicText()do its normal job. A combination that names a type still resolves to it, and one that does not staysDynamicType::OTHERwhile keeping the correct glyphs.This also makes
m_dynamicsListuniformly markup. It was already mixed, sincetextToDynamic()pushesDynamic::dynamicText()into the same list.<sf/><mp/>OTHER, glyphss f m p<other-dynamics>s</other-dynamics><f/><other-dynamics>m</other-dynamics><p/><ff/><other-dynamics>z</other-dynamics>OTHER, glyphsf f z<other-dynamics>s</other-dynamics><f/>DynamicType::SFDynamics that name a MuseScore type are unaffected, which is the overwhelming majority of real-world files: a single-child
<dynamics>produces exactly what it did before.One deliberate consequence: a combination MuseScore has no type for is
DynamicType::OTHER, whose velocity is -1, so it has no playback effect unless the direction also carries<sound dynamics="_"/>. That matches how MuseScore treats any dynamic whose text it cannot classify, including ones typed by hand.Tests
testMultipleDynamicscovers all four cases in the table, including one combination that resolves to a real type so the classification path is exercised too.inferredDynamicRangeis the existing test most exposed to this change, sinceisLikelyDynamicRange()testsm_dynamicsList.size() == 2. That still holds: a dynamic range is three sibling<direction-type>elements, each holding a single-child<dynamics>, not one element with two children.Full suite passes on macOS apart from
inferredCredits1andinferredCredits2, which fail onmainas well. Both differ only in a vbox<height>(13.9828 vs 13.9746), the platform font-metric divergence the suite'sDISABLED_EXCEPT_ON_LINUXmacro exists for.Proposal: multi-glyph dynamics on the export side
I kept this PR to the reported bug, but the same idea has more to give, and I would rather show you the whole picture than open a surprise follow-up. This section is a proposal, not part of the diff. I am happy to fold it into this PR, do it as a follow-up, or drop it entirely — whichever you prefer.
The observation behind #34439 is that a MusicXML
<dynamics>element is a sequence, so it can describe markings that no single element names. The importer now reads that. The exporter still does not write it.1. Decompose glyph runs on export.
ExportMusicXml::dynamic()writes any dynamic it cannot match to a single MusicXML element as one<other-dynamics>blob, so ansfmpmarking exports as<other-dynamics>sfmp</other-dynamics>— text that other applications will render as literal letters. Emitting the longest valid MusicXML elements found in the glyph run, with<other-dynamics>only for fragments that have no element, would give:sfmp→<sf/><mp/>ffz→<ff/><other-dynamics>z</other-dynamics>sff,sfff,sfffz→<sf/><f/>,<sf/><f/><f/>,<sf/><f/><f/><other-dynamics>z</other-dynamics>(MuseScore types with no MusicXML element)m,r,s,z→ unchanged; single glyphs with no MusicXML element and nothing to decomposeCombined with this PR,
sfmpandffzwould then round-trip exactly. Note this changestestDynamics1.xml, which contains<other-dynamics>sff</other-dynamics>and is amusicXmlIoTest; converting it tomusicXmlIoTestRefwith a_ref.xmlkeeps the input file untouched and makes the intended output change visible in review.2. Read the glyphs rather than a codepoint table. The export path maps
plainText()characters against a hardcoded table of the seven single-letter SMuFL dynamics (U+E520–U+E526). Ligature glyphs such asdynamicFF,dynamicMF,dynamicSforzatoanddynamicPPPPare not in it, so a dynamic containing one currently exports as a raw private-use character inside<other-dynamics>. WalkingDynamic::fragmentList()and resolving SymIds throughIEngravingFontsProviderwould handle those, and would also separate leading and trailing plain text from the glyph run properly.3. The matching import gap. A single element MuseScore cannot classify still becomes plain text with no glyphs —
<sfzp/>, which is valid MusicXML but has no MuseScoreDynamicType, and hand-authored text such as<other-dynamics>sfmp</other-dynamics>. Decomposing the letters into glyphs would fix both. This becomes required rather than merely nice if (1) is done, becausesfzpis a valid MusicXML dynamic: a glyph run spellingsfzpwould export as<sfzp/>and would not survive the round trip back.The MNX module already does all of this and could be drawn on:
toMnxDynamicFromSymIds()anddynamicGlyphLetters()ininternal/shared/mnxtypesconv.cppderive glyph-to-letter spellings from MuseScore's ownDynamicTypetable rather than a maintained second table, andsplitDynamicText()ininternal/export/mnxexportparts.cppdoes the fragment walk in (2). Whether that is worth extracting somewhere both modules can use, or is better duplicated in the MusicXML module, is a question I would want your steer on.