From 3339e9cecbc55a1a5eec9d32ac46f4e9181152dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=B6gstr=C3=B6m?= Date: Sat, 13 Jun 2026 01:53:20 +0200 Subject: [PATCH] Honour an explicit dynamics="0" on MusicXML note import The dynamics attribute of the element expresses the note velocity as a percentage of the MIDI 1.0 default forte value of 90. Positive values were imported correctly, but an explicit dynamics="0" (a silent note, e.g. a ghost note or an unpitched gesture with an x notehead) was indistinguishable from an absent attribute and silently dropped, so the note played at full default velocity. Detect the attribute's presence instead of testing the computed value, and clamp the velocity to [1, 127]: the score model reserves velocity 0 for "unset", so 1 is the lowest representable (effectively silent) value, and the upper bound matches the clamping already applied to direction-level values. Resolves: https://github.com/musescore/MuseScore/issues/33803 Co-Authored-By: Claude Fable 5 --- .../internal/import/importmusicxmlpass2.cpp | 8 +- .../musicxml/tests/data/testNoteDynamics.xml | 84 +++++++++ .../tests/data/testNoteDynamics_ref.mscx | 164 ++++++++++++++++++ .../musicxml/tests/musicxml_tests.cpp | 3 + 4 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 src/importexport/musicxml/tests/data/testNoteDynamics.xml create mode 100644 src/importexport/musicxml/tests/data/testNoteDynamics_ref.mscx diff --git a/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp b/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp index b13fbbe51e1d7..2b96f673a22cc 100644 --- a/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp +++ b/src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp @@ -6929,7 +6929,11 @@ Note* MusicXmlParserPass2::note(const String& partId, Color beamColor; bool noteheadParentheses = false; String noteheadFilled; - int velocity = round(m_e.doubleAttribute("dynamics") * 0.9); + // velocity as a percentage of the MIDI 1.0 default forte value of 90; + // an explicit dynamics="0" means a silent note, which the score model can + // only represent as velocity 1 (velocity 0 means "unset") + const bool hasDynamics = m_e.hasAttribute("dynamics"); + const int velocity = std::clamp(int(round(m_e.doubleAttribute("dynamics") * 0.9)), 1, 127); bool graceSlash = false; bool printObject = m_e.asciiAttribute("print-object") != "no"; bool printLyric = (printObject && m_e.asciiAttribute("print-lyric") != "no") || m_e.asciiAttribute("print-lyric") == "yes"; @@ -7260,7 +7264,7 @@ Note* MusicXmlParserPass2::note(const String& partId, } } - if (velocity > 0) { + if (hasDynamics) { note->setUserVelocity(velocity); } diff --git a/src/importexport/musicxml/tests/data/testNoteDynamics.xml b/src/importexport/musicxml/tests/data/testNoteDynamics.xml new file mode 100644 index 0000000000000..c611ae708f3da --- /dev/null +++ b/src/importexport/musicxml/tests/data/testNoteDynamics.xml @@ -0,0 +1,84 @@ + + + + Note dynamics attribute + + + Voice + + + + + + 480 + + 0 + + + + G + 2 + + + + + C + 4 + + 480 + 1 + quarter + + + + D + 4 + + 480 + 1 + quarter + x + + + + E + 4 + + 480 + 1 + quarter + + + + F + 4 + + 480 + 1 + quarter + + + + G + 4 + + 480 + 1 + quarter + + + + diff --git a/src/importexport/musicxml/tests/data/testNoteDynamics_ref.mscx b/src/importexport/musicxml/tests/data/testNoteDynamics_ref.mscx new file mode 100644 index 0000000000000..01e8d86865aae --- /dev/null +++ b/src/importexport/musicxml/tests/data/testNoteDynamics_ref.mscx @@ -0,0 +1,164 @@ + + + + B_B + 480 + + 1 + 1 + 1 + 0 + + + + + + Note dynamics attribute + + + + + + C_C + + D_D + + stdNormal + + + + + Voice + + + voice.vocals + + 100 + 100 + + + 100 + 33 + + + 100 + 50 + + + 100 + 67 + + + 100 + 100 + + + 120 + 67 + + + 150 + 100 + + + 150 + 50 + + + 120 + 50 + + + 120 + 100 + + + + + + + + + + 10 + E_E + + F_F + + Note dynamics attribute + + + + G_G + + + G + G + 1 + H_H + + + I_I + 5 + 4 + + + J_J + quarter + + K_K + 60 + 14 + + + + L_L + quarter + + M_M + 62 + 16 + cross + 1 + + + + N_N + quarter + + O_O + 64 + 18 + 45 + + + + P_P + quarter + + Q_Q + 65 + 13 + 127 + + + + R_R + quarter + + S_S + 67 + 15 + + + + T_T + + + + + + diff --git a/src/importexport/musicxml/tests/musicxml_tests.cpp b/src/importexport/musicxml/tests/musicxml_tests.cpp index cc47ad7cde8db..126127d03b7c2 100644 --- a/src/importexport/musicxml/tests/musicxml_tests.cpp +++ b/src/importexport/musicxml/tests/musicxml_tests.cpp @@ -1024,6 +1024,9 @@ TEST_F(MusicXml_Tests, noteAttributes3) { TEST_F(MusicXml_Tests, noteColor) { musicXmlIoTest("testNoteColor"); } +TEST_F(MusicXml_Tests, noteDynamics) { + musicXmlImportTestRef("testNoteDynamics"); +} TEST_F(MusicXml_Tests, noteheadNames) { musicXmlIoTest("testNoteheadNames"); }