Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions packages/alphatab/src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2582,16 +2582,6 @@ export class MusicXmlImporter extends ScoreImporter {

this._insertBeatToVoice(newBeat, voice);

if (note !== null) {
note!.isVisible = noteIsVisible;
const trackInfo = this._indexToTrackInfo.get(track.index)!;
if (instrumentId !== null) {
note!.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId!, note!);
} else if (!isPitched) {
note!.percussionArticulation = trackInfo.getOrCreateArticulation('', note!);
}
}

// duration only after we added it into the tree
if (graceType !== GraceType.None) {
newBeat.graceType = graceType;
Expand Down Expand Up @@ -2759,6 +2749,52 @@ export class MusicXmlImporter extends ScoreImporter {

// if not yet created do it befor we exit to ensure we created the beat/note
ensureBeat();

if (note !== null) {
// Final note post-processing depends on the note already being attached to the
// beat/voice/bar/staff tree (e.g. percussion clef context on the resolved staff).
// Therefore this must run after ensureBeat() and after transposition has been applied.
this._finalizeImportedNote(note, track, instrumentId, isPitched, noteIsVisible);
}
}

/**
* Applies note-level post-processing that requires the fully resolved parse context.
*
* Purpose:
* - Set final visibility.
* - Resolve percussion articulation consistently in one place.
*
* Why this is called at the end of _parseNote:
* - The logic relies on final note context (attached beat/voice/bar/staff), especially
* staff percussion state, and on the final display value after transposition.
* - Running this earlier could use incomplete or wrong context and produce wrong
* articulation mapping.
*/
private _finalizeImportedNote(
note: Note,
track: Track,
instrumentId: string | null,
isPitched: boolean,
noteIsVisible: boolean
) {
note.isVisible = noteIsVisible;

if (note.percussionArticulation >= 0) {
return;
}

const trackInfo = this._indexToTrackInfo.get(track.index)!;
if (instrumentId !== null) {
note.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId, note);
} else if (note.beat.voice.bar.staff.isPercussion && isPitched) {
const knownArticulation = PercussionMapper.getArticulationById(note.displayValue);
if (knownArticulation) {
note.percussionArticulation = knownArticulation.id;
}
} else if (!isPitched) {
note.percussionArticulation = trackInfo.getOrCreateArticulation('', note);
}
}

private _parsePlay(element: XmlNode, note: Note | null) {
Expand Down
51 changes: 51 additions & 0 deletions packages/alphatab/test-data/musicxml4/percussion-articulation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<score-partwise version="4.0">
<part-list>
<score-part id="P1">
<part-name>Drums</part-name>
<score-instrument id="P1-I1">
<instrument-name>Drumset</instrument-name>
</score-instrument>
<midi-instrument id="P1-I1">
<midi-channel>10</midi-channel>
<midi-program>1</midi-program>
</midi-instrument>
</score-part>
</part-list>
<part id="P1">
<measure number="1">
<attributes>
<divisions>1</divisions>
<key>
<fifths>0</fifths>
</key>
<time>
<beats>4</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>percussion</sign>
</clef>
</attributes>
<note>
<pitch>
<step>D</step>
<octave>2</octave>
</pitch>
<duration>1</duration>
<voice>1</voice>
<type>quarter</type>
</note>
<note>
<pitch>
<step>C</step>
<alter>1</alter>
<octave>3</octave>
</pitch>
<duration>1</duration>
<voice>1</voice>
<type>quarter</type>
</note>
</measure>
</part>
</score-partwise>
14 changes: 14 additions & 0 deletions packages/alphatab/test/importer/MusicXmlImporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,20 @@ describe('MusicXmlImporterTests', () => {
expect(score).toMatchSnapshot();
});

it('percussion-articulation', async () => {
const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/percussion-articulation.xml');
const notes = score.tracks[0].staves[0].bars[0].voices[0].beats.flatMap(b => b.notes);

expect(notes).toHaveLength(2);
expect(notes[0].displayValue).toBe(38);
expect(notes[0].isPercussion).toBe(true);
expect(notes[0].percussionArticulation).toBe(38);

expect(notes[1].displayValue).toBe(49);
expect(notes[1].isPercussion).toBe(true);
expect(notes[1].percussionArticulation).toBe(49);
});

describe('barnumberdisplay', async () => {
async function testPartwise(filename: string, display: BarNumberDisplay) {
const score = await MusicXmlImporterTestHelper.loadFile(`test-data/musicxml4/${filename}`);
Expand Down
Loading