Skip to content

fix(importer): pair and apply MusicXML spans per part, staff and number - #2938

Merged
Danielku15 merged 5 commits into
developfrom
fix/2867-musicxml-letring-span
Oct 8, 2026
Merged

Danielku15 merged 5 commits into
developfrom
fix/2867-musicxml-letring-span

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 #2867

Proposed changes

#2867: a let ring written as <words>LetRing</words> + <dashes> was drawn but not heard. There were two causes:

  • The flag went to the wrong place. The importer set only the derived Beat.isLetRing, but playback and the palm-mute rendering read Note.isLetRing/Note.isPalmMute. The beat flag is not serialized either, so the line was also missing in the web worker.
  • The span state itself was unreliable. It was one global flag, applied in document order:
    • <dashes type="stop"/> without repeated words never ended the span;
    • spans leaked into other staves and parts;
    • number and <offset> were ignored.

Wedges, octave shifts, slash notation and endings had the same global, document-order handling. So the fix gives all MusicXML spans one shared foundation instead of special-casing let ring.

Design

  • MusicXmlSpans pairs starts and stops per part:
    • by element and MusicXML number-level;
    • never across different staves (exporters reuse numbers per staff);
    • stops before starts within one direction;
    • stops that appear in the document before their start (other voice after <backup>) are parked until the start appears.
  • MusicXmlSpanApplier applies the paired time ranges to beats, pedal markers and simile bars.
    • Beats next to the position the start or stop is written at count as written. MuseScore writes stops after the last covered note; Finale nudges them with a small <offset> into the neighbouring note.
    • Beats in other voices count if their middle lies within the span.
    • Grace notes at a boundary are ordered as in the document.
  • MusicXmlNoteLinks pairs ties, slurs, slides, glissandos and trill/vibrato lines within a staff.
  • Endings are tracked per part in the importer.

Behaviour changes

  • Let ring / palm mute:
    • applied to the notes, so they are heard, drawn for palm mute, and kept after JSON serialization;
    • ringing ends when the same string is struck again;
    • also read from <bracket> and MuseScore's spellings ("let ring", "P.M.", "palm mute"), and from a label in a separate preceding direction (music21);
    • <tied type="let-ring"/> is supported, including TuxGuitar's placement outside <notations>.
  • Lines with other labels ("cresc.", "dim.", "ritard.") keep their label as beat text instead of dropping it.
  • Wedges: stay on their staff and on the voice they are written at. No spill onto other staves or parts, and no duplicated hairpins.
  • Octave shifts:
    • stay on their staff (an 8va on the right hand no longer shifts the left hand);
    • an unsupported 22ma no longer ends a running 8va;
    • Finale/Sibelius stops follow their convention (detected via <software>, as in test suite 33da/33db).
  • Pedals: a release in a later bar is no longer dropped, and change, resume and discontinue are supported. A change is drawn as "✱ Ped." side by side.
  • Trills:
    • a trill line covers all notes of its voice;
    • trill-step half/unison is supported;
    • trills on tablature use the played fret.
  • Measure styles: slash notation no longer leaks into other parts, and measure repeats cover the whole range up to their stop.
  • Endings: combined across parts, so a later part without <ending> no longer erases them.
  • Note links:
    • unended ties are tied to the next note with the same pitch;
    • glissandos and slides no longer share number slots;
    • a slur written along a hammer-on is not drawn twice.
  • Offsets are converted from divisions for tempo, metronome and pedal positions. Tempo honours <offset sound="yes">.
  • Hammer-ons / pull-offs: MusicXML notes are now registered by string once their <string> is read. Before, the string lookup of MusicXML beats was always empty, so hammer-ons and pull-offs were never imported. Only the start of a hammer-on/pull-off marks the origin.
  • Model (Bar.finish): a pedal pressed again after a lift, in a bar after a held bar, was turned into a hold.

Reference renders

The changed renders were checked one by one:

Change Files
Pedal changes now drawn 31a, 33a, 34b, Dichterliebe01
Line labels ("cresc.", "dim.", "ritard.") kept 31d, 34b, 34c, DebuMandSample, Dichterliebe01
Correct octave shifts 33da
Continuous trill lines 33a, 33f, 34a, 34c
Unended ties 33i
Spill onto other staves and duplicated hairpins removed DebuMandSample, MahlFaGe4Sample
Offset units (tempo mark, pedal marks) 31b, BeetAnGeSample
Hammer-ons / pull-offs now imported chord-diagram, BrookeWestSample

Not covered

The model has nothing to hold these:

  • two-note tremolo
  • the trill accidental-mark
  • tuplet bracket bounds
  • other-notation, principal-voice, string-mute
  • links across staves

Pedals are not played back (MIDI does not read the pedal markers). This is unchanged.

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

New tests:

  • MusicXmlImporter.test.ts: hammer-pull and describe('spans'), covering lines, wedges, octave shifts, pedals, wavy lines, measure styles, endings, note links and tempo offsets
  • Bar.test.ts: the pedal retake

TS, C# and Kotlin test suites pass.

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

…nown

MusicXML notes are attached to their beat before <technical><string> is
read, so the beat's string lookup stayed empty. Lookups by string failed for
every MusicXML note: hammer-on/pull-off destinations were never found (the
effect was dropped) and let ring could not end on a note struck on the same
string.

Only the start of a hammer-on/pull-off is the origin now. The stop note was
marked as origin too, which was hidden while the lookup was empty.
Let ring and palm mute written as <words> + <dashes> were only set as
the derived beat flags. Playback and the palm mute rendering read the note
flags, and the beat flags are lost when the score is serialized (worker
rendering). The span state was a global flag applied in document order:
a <dashes type="stop"/> without repeated words never ended the span, and
spans leaked into other staves and parts.

Direction spans are now tracked per part (MusicXmlSpans): starts and stops
are paired by element family and number, preferring the same staff and
kind, stops before starts within a direction, and stops appearing in the
document before their start are parked. Positions are musical positions
including <offset>, the stop is the exclusive end. After parsing, the spans
set the note flags on all covered notes of their staff.

Fixes #2867
Snapshot before restructuring the code design.

Direction spans (dashes, bracket, wedge, octave-shift, pedal), wavy lines,
measure styles (slash, measure-repeat), endings and note links (tied, slur,
glissando, slide) are paired and applied through MusicXmlSpans instead of
importer-global state in document order:

- spans stay on their part, staff and voice (wedges and octave shifts no
  longer spill onto other staves and parts)
- numbers pair starts and stops, stops before starts are parked
- MuseScore let ring / palm mute spellings and brackets are supported,
  unknown line labels are kept as text
- octave shift stops follow the exporter convention (Finale, Sibelius)
- pedal releases in later bars, pedal changes, resume and discontinue
- trill lines cover all notes in their voice, trill steps and frets fixed
- measure repeats cover their range, endings are combined across parts
- unended ties are tied to the next note, glissando and slide numbers no
  longer collide, slurs written along hammer-ons are not duplicated
- offsets are converted from divisions (tempo, metronome, pedal)

Pedal markers pressed after a lift in a held bar are no longer turned into
holds, and pedal changes are drawn side by side.
The span tracker handled time ranges, note links and bar ranges in one
place and branched on specific elements throughout. Split it up:

- MusicXmlSpans: span records, the pairing of starts and stops per part
  and the readers of the span elements.
- MusicXmlSpanApplier: applies the time ranges of a part to beats, pedal
  markers and simile bars (coverage, offsets, exporter conventions).
- MusicXmlNoteLinks: pairs and applies note links (ties, slurs, slides,
  glissandos, trill and vibrato lines).
- Endings are tracked per part in the importer.

The placement of a span element is passed to the tracker instead of
patched into the events, and the voice a wedge is written at comes from
the following beat instead of a queue of pending events. This fixes:

- directions separated by a <forward> lost their order
- a direction with a voice changed the voice of earlier directions
- a stop read before its start closed a span of another staff with the
  same number
- pedal markers in overfull bars were placed after the bar end
@Danielku15 Danielku15 self-assigned this Oct 8, 2026
@Danielku15
Danielku15 merged commit 2ca8908 into develop Oct 8, 2026
6 checks passed
@Danielku15
Danielku15 deleted the fix/2867-musicxml-letring-span branch October 8, 2026 10:53
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 LetRing <words> + <dashes> span is drawn but produces no audible let ring

1 participant