Repository navigation
MusicXML: dynamics on one staff of a multi-staff part also show up on the other staff #2979
Description
Note
AI-authored disclosure (alphatab-ai-authored-v1)
Portions of this content were authored by an AI agent. The agent has read
AGENTS.md and the human submitter accepts responsibility for
compliance with the rules in that document.
Is there an existing issue for this?
- I have searched the existing issues
I have read the contribution rules
- I have read CONTRIBUTING.md and, if any AI agent helped draft this issue, AGENTS.md.
AI authorship
AI-assisted (disclosure block included as described in AGENTS.md)
Current Behavior
When a MusicXML part has two staves (piano), a dynamic marking that is addressed to one staff via <staff> is applied to the other staff as well.
Using alphaTab's own test file 43e-Multistaff-ClefDynamics.xml from test-data/musicxml-testsuite: its description says the ffff and the wedge apply to only one voice or staff. Both <dynamics> directions (ffff, then p at an offset) are addressed to staff 1. Staff 2 has none.
After importing the file, reading beat.dynamics for every beat gives:
- Staff 1:
FFFFfor the first three beats of bar 1, thenPfrom beat 4 onward. - Staff 2:
Pon every beat, starting at bar 1 beat 1 (before thepappears on staff 1 at all).
So staff 2 carries the p that was written for staff 1, including on beats that come before it.
Expected Behavior
A dynamic addressed to staff 1 affects only staff 1. Staff 2 keeps the default dynamic until a mark addressed to staff 2 appears. This matches the test file's own description.
Steps To Reproduce
- Load
43e-Multistaff-ClefDynamics.xml(from the alphaTab repo,packages/alphatab/test-data/musicxml-testsuite/) with the MusicXML importer. - Print
beat.dynamicsfor each beat of both staves (staff 2 should stay at the default):
import * as alphaTab from '@coderline/alphatab';
import { readFileSync } from 'node:fs';
const score = alphaTab.importer.ScoreLoader.loadScoreFromBytes(
new Uint8Array(readFileSync('43e-Multistaff-ClefDynamics.xml')),
new alphaTab.Settings()
);
const names = Object.fromEntries(
Object.entries(alphaTab.model.DynamicValue).filter(([, v]) => typeof v === 'number').map(([k, v]) => [v, k])
);
for (const staff of score.tracks[0].staves) {
console.log('staff');
staff.bars.forEach((bar, bi) =>
bar.voices[0].beats.forEach((b, i) => console.log(` bar ${bi + 1} beat ${i + 1}: ${names[b.dynamics]}`))
);
}Link to jsFiddle, CodePen, Project
No hosted project. The minimal script is inline under Steps To Reproduce and runs against the unmodified test file from this repo: https://github.com/CoderLine/alphaTab/blob/develop/packages/alphatab/test-data/musicxml-testsuite/43e-Multistaff-ClefDynamics.xml
Version and Environment
[AlphaTab][VersionInfo] alphaTab 1.9.0
[AlphaTab][VersionInfo] commit: c53d5adfcefd49af6c748fbadd5e054de8ce1b3c
[AlphaTab][VersionInfo] build date: 2026-10-29T06:11:56.958Z
[AlphaTab][VersionInfo] High DPI: 1
[AlphaTab][VersionInfo] Platform: NodeJs
[AlphaTab][VersionInfo] WebPack: false
[AlphaTab][VersionInfo] Vite: false
node v22.23.3 darwin arm64
The same result occurs with the published 1.9.0-alpha.1963.
Platform
Node.js
Anything else?
Fixing this would change how unmarked staves render in a number of existing visual reference tests (an unmarked staff would show the default f rather than a carried-over value), so I'm filing an issue first to ask which rendering the maintainers want before anyone writes a patch. Happy to prepare one once the direction is agreed.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Area
Platform
Work State
Projects
- StatusShow more project fieldsDone
Activity
fix(importer): apply MusicXML dynamics per part and show them only wh…
fix(importer): apply MusicXML dynamics per part and show them only wh…
Danielku15 commentedon Oct 29, 2026
Thanks, confirmed: the importer applied each dynamic to whatever beat came next, on any staff or part.
The fix (#2996):
Why playback is part-wide: MusicXML can't tell "this hand only" from "both hands". 43e uses the same encoding as Finale's piano exports, where the dynamic is meant for both hands, and MuseScore and Dorico also apply dynamics to the whole instrument by default.
beat.dynamicsis the dynamic in effect for playback; the newbeat.showDynamicssays whether a marking is drawn on that beat.