Skip to content

MusicXML: dynamics on one staff of a multi-staff part also show up on the other staff #2979

Closed

Description

@weblings

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

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: FFFF for the first three beats of bar 1, then P from beat 4 onward.
  • Staff 2: P on every beat, starting at bar 1 beat 1 (before the p appears 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

  1. Load 43e-Multistaff-ClefDynamics.xml (from the alphaTab repo, packages/alphatab/test-data/musicxml-testsuite/) with the MusicXML importer.
  2. Print beat.dynamics for 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.

Activity

  1. added theissue type on Oct 29, 2026
  2. updated Priority Low, Effort Low, Area File Formats and Platform All / Multiple on Oct 29, 2026
  3. self-assigned this
    on Oct 29, 2026
  4. added this to the 1.9.0 milestone on Oct 29, 2026
  5. added 2 commits that reference this issue on Oct 29, 2026
    7006447
    7c099a3
  6. Danielku15 commented on Oct 29, 2026

    @Danielku15
    Member

    Thanks, confirmed: the importer applied each dynamic to whatever beat came next, on any staff or part.

    The fix (#2996):

    • Display: a dynamic is shown only on the staff (and voice) it is written for. In 43e, staff 2 shows nothing.
    • Playback: a dynamic applies to all staves of its part from its position on. In 43e, staff 2 plays ffff, then p.

    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.dynamics is the dynamic in effect for playback; the new beat.showDynamics says whether a marking is drawn on that beat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Fields

Priority

Low

Effort

Low

Area

File Formats

Platform

All / Multiple

Work State

Accepted

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions