Skip to content

fix: spell MusicXML notes from step/alter, export spellings correctly to Guitar Pro and alphaTex - #2931

Merged
Danielku15 merged 8 commits into
developfrom
fix/2861-musicxml-accidental-spelling
Oct 7, 2026
Merged

Danielku15 merged 8 commits into
developfrom
fix/2861-musicxml-accidental-spelling

Conversation

@Danielku15

Copy link
Copy Markdown
Member

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.

Issues

Fixes #2861

Proposed changes

Root cause: the MusicXML importer turned <step>/<alter> into a plain semitone value. Only <accidental>
set Note.accidentalMode, which holds the note's spelling. Notes without a printed sign (repeated in a bar, or
tied over a bar line) were therefore spelled from the key signature: F# became Gb in flat keys, and B became Cb
in Gb major. A tie then connected two different staff lines.

In MusicXML the spelling is defined by <pitch>, and <accidental> only describes the printed sign. MuseScore
(step2tpc(step, alter)) and Verovio (pname from <step>) both work this way.

Importer: accidentalMode is now set from <alter> (0 → ForceNatural, ±1 → ForceSharp/ForceFlat,
±2 → ForceDoubleSharp/ForceDoubleFlat). This is the same model the Guitar Pro 7 importer already produces.
An <accidental> that contradicts the pitch only logs a warning; the pitch wins.

The Force* modes only choose among spellings of the same pitch; the printed sign is still worked out from the
bar. So a forced mode can never move a note to a wrong pitch. This was checked over every key × step × alter
(−2..+2) × octave × clef combination.

Setting the spelling on every note affects both exporters, so they are fixed here as well:

  • Guitar Pro export: GpifWriter took the Pitch step from a fixed sharp table and the accidental from the
    sign needed in the key. That wrote wrong pitch spellings in many keys, in Default and forced modes alike (e.g.
    Bb in F major became "A" or "Ab"). It now writes the resolved spelling. The octave belongs to the letter, as in
    Guitar Pro's own files (B#4 = MIDI 60, Cb5 = MIDI 59). Exporting GP-authored test files now reproduces their
    TransposedPitch exactly (0 differences, previously 755 of 2085 notes). Opening the exported files in Guitar
    Pro shows the same note placement and accidentals as alphaTab.
  • alphaTex export: {acc …} was written for nearly every note coming from Guitar Pro, and would have been
    for MusicXML too. A new ModelUtils.simplifyAccidentalMode reduces a hint to Default when it gives the same
    spelling as the default. Rendering only reads the mode through resolveSpelling and the ForceNone checks, so
    dropping such hints cannot change the output. The exporter now only writes hints that change the spelling.
    Across the test data this cuts the acc properties from 23,901 to 1,387, with identical rendering after an
    alphaTex round trip.
  • Round-trip tests: these now compare the accidental modes that matter for rendering, instead of ignoring
    accidentalmode.

Reference changes (all reviewed): MusicXML samples and test-suite files now follow the source spelling.
Examples: Dichterliebe bar 27 had this issue's tie bug; Debussy, Schubert and Brahms lose wrong enharmonic
swaps; 13a now renders C♮ in C# major instead of B#. The MusicXML snapshots gain accidentalmode values, and
the alphaTex formatted reference loses redundant acc properties.

Out of scope:

  • printing the file's own courtesy, cautionary or microtonal signs (needs a separate model feature, since
    accidentalMode is spelling only)
  • the same missing-spelling pattern in the Capella importer

Checklist

  • I consent that this change becomes part of alphaTab under its current or any future open source license
  • This PR is linked to an accepted issue (see above)
  • Changes are implemented
  • New tests were added
  • I have read AGENTS.md if an AI helped draft any part of this PR

AI authorship disclosure

  • No AI agent authored any part of this PR (description, code, tests, or commit messages)
  • An AI agent contributed to this PR. The AI-authored disclosure block
    (alphatab-ai-authored-v1) is present at the top of this body, and I have
    personally reviewed every change and can explain each one

Further details

  • This is a breaking change
  • This change will require update of the documentation/website

Adds ModelUtils.simplifyAccidentalMode which reduces a forced accidental
mode to Default when it resolves to the spelling Default would choose
anyhow. Exporters can use it to only write the hints that matter.
The ConcertPitch/TransposedPitch step was taken from a fixed sharp table
and the accidental from the sign needed in the key, which wrote wrong
pitches in many keys (e.g. Bb in F major as A or Ab). Write the resolved
spelling instead, with the octave belonging to the step as Guitar Pro does.

Round trip tests now compare the accidental modes which matter for the
rendering instead of ignoring them.
…spelling

Formats like Guitar Pro store the spelling on every note, which resulted
in an acc property on nearly every exported note. Only write the hints
which result in a different spelling than the default one.
The spelling of a note is defined by <step> and <alter>, <accidental> only
describes the printed sign. Notes without printed sign (repeated in a bar,
tied over a bar line) were spelled from the key signature instead,
e.g. F# as Gb in flat keys. Signs contradicting the pitch are reported.
@Danielku15
Danielku15 merged commit 95fa413 into develop Oct 7, 2026
6 checks passed
@Danielku15
Danielku15 deleted the fix/2861-musicxml-accidental-spelling branch October 7, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MusicXML: a note without an <accidental> element is spelled from the key signature instead of its <step>/<alter> (F# rendered as Gb)

1 participant