musescore / musescore/MuseScore
MusicXML import: `<note dynamics="0">` is silently dropped — a zero-velocity (silent) note plays at full volume
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 15.1k
- Forks
- 3.3k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 91
Description
Describe the bug
MusicXML defines a dynamics attribute on the <note> element:
The dynamics attribute corresponds to MIDI 1.0's Note On velocity. It is expressed in terms of percentages of the default forte value (90 for MIDI 1.0).
So <note dynamics="0"> unambiguously means "this note sounds at zero velocity" — the natural way to notate a visible but silent note (ghost notes, unpitched gestures written with an x notehead, educational scores, etc.).
The importer handles this attribute correctly for positive values: dynamics="50" imports as <velocity>45</velocity> on the Note, and playback/MIDI export honour it (verified). But dynamics="0" is dropped entirely, and the note plays at full default velocity — the one value for which the attribute expresses something that cannot be approximated by anything else in the file.
Root cause analysis
In MusicXmlParserPass2::note() the attribute is read into an int:
int velocity = round(m_e.doubleAttribute("dynamics") * 0.9);
(importmusicxmlpass2.cpp#L6932)
doubleAttribute() returns 0.0 when the attribute is absent, so 0 doubles as the "no attribute" sentinel, and the value is only applied when positive:
if (velocity > 0) {
note->setUserVelocity(velocity);
}
(importmusicxmlpass2.cpp#L7263-L7265)
An explicit dynamics="0" is therefore indistinguishable from "attribute not present" and is discarded.
There is a second wrinkle: the engraving model itself reserves userVelocity == 0 for "unset" — both playback paths skip the override when it is zero (playbackmodel.cpp#L331, compatmidirenderinternal.cpp#L227), and the MusicXML exporter likewise only writes the dynamics attribute for non-zero velocities (exportmusicxml.cpp#L4360-L4363). So even a fixed importer cannot map dynamics="0" to velocity 0; the closest representable value is velocity 1, which is effectively silent (verified: a hand-edited <velocity>1</velocity> in the .mscx exports MIDI Note On velocity 1).
Suggested fix: in note(), distinguish "attribute absent" from "attribute present with value 0" (e.g. test the raw attribute string for emptiness instead of testing the computed value), and clamp the computed velocity into the representable range:
const String dynamicsAttr = m_e.attribute("dynamics");
...
if (!dynamicsAttr.empty()) {
note->setUserVelocity(std::clamp(velocity, 1, 127));
}
The lower bound of 1 is the smallest value representable in the score model (0 means "unset"); the upper bound mirrors the [0, 127] clamping the importer already applies to direction-level <sound dynamics> values (importmusicxmlpass2.cpp#L3729-L3737) — the current note-level code stores e.g. dynamics="200" as velocity 180, above the MIDI maximum. The exporter would then round-trip dynamics="0" as dynamics="1.11" (velocity 1), which is acceptable; exact 0 round-tripping would require the model to distinguish "unset" from "zero", which seems out of scope.
To Reproduce
Minimal test case (one measure, four quarter notes; validates against the MusicXML 4.0 schema):
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<score-partwise version="4.0">
<movement-title>Muted note playback test</movement-title>
<part-list>
<score-part id="P1">
<part-name>Voice</part-name>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>480</divisions>
<key><fifths>0</fifths></key>
<time><beats>4</beats><beat-type>4</beat-type></time>
<clef><sign>G</sign><line>2</line></clef>
</attributes>
<note>
<pitch><step>C</step><octave>4</octave></pitch>
<duration>480</duration>
<voice>1</voice>
<type>quarter</type>
</note>
<note dynamics="0">
<pitch><step>D</step><octave>4</octave></pitch>
<duration>480</duration>
<voice>1</voice>
<type>quarter</type>
<notehead>x</notehead>
</note>
<note dynamics="50">
<pitch><step>E</step><octave>4</octave></pitch>
<duration>480</duration>
<voice>1</voice>
<type>quarter</type>
</note>
<note>
<pitch><step>F</step><octave>4</octave></pitch>
<duration>480</duration>
<voice>1</voice>
<type>quarter</type>
</note>
</measure>
</part>
</score-partwise>
- Convert to MIDI:
MuseScore4 -o out.mid test.musicxml - Inspect the Note On velocities (midicsv):
| Note | MusicXML | Imported velocity | MIDI velocity | |
|---|---|---|---|---|
| C4 | (none) | (unset) | 80 | ✓ control |
| D4 | dynamics="0" |
(unset — dropped) | 80 | ✗ should be silent |
| E4 | dynamics="50" |
45 | 45 | ✓ works |
| F4 | (none) | (unset) | 80 | ✓ control |
Converting to .mscx confirms the model state: the E4 note carries <velocity>45</velocity>, the D4 note carries nothing.
Expected behavior
<note dynamics="0"> should import as a silent note (velocity 1, the lowest value representable in the score model), consistent with how all positive dynamics values are already imported.
Platform information
- OS: Windows 11
- MuseScore version: reproduced on MuseScore Studio 4.7.2 (release); code analysis against current master (d53a8edc98). Unlike #33801 this is not a regression — the
velocity > 0guard has been there since the attribute was first supported. - Reproduced via command-line conversion
Additional context
Found while writing a NoteWorthy Composer → MusicXML converter. NWC has note-level muting (Opts:Muted), commonly used for percussive gestures written with x noteheads. Without an importable silent-note representation those must be converted to rests, losing the notation; dynamics="0" is the spec-backed way to keep both notation and playback faithful.
(Deliberately out of scope: <note><play><mute>on</mute></play> should not silence a note — mute is a playing-technique indication (straight mute, cup mute, …) and a technique-muted instrument still sounds. The dynamics attribute is the unambiguous vehicle for note-level loudness including silence.)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/importexport/musicxml/internal/import/importmusicxmlpass2.cpp, especially MusicXmlParserPass2::note() at the referenced attribute-read and velocity-application locations. Run the supplied MusicXML through MuseScore4 -o out.mid test.musicxml and inspect the resulting velocities with midicsv. Done means an explicit dynamics="0" note is imported at velocity 1 while absent and positive dynamics values retain their current behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100