Skip to content
Merged
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
27 changes: 23 additions & 4 deletions packages/alphatab/src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ class InstrumentArticulationWithPlaybackInfo extends InstrumentArticulation {
* The balance to use when playing the note (-1 if using the default track balance).
*/
public outputBalance: number = -1;

/**
* Whether this instrument was declared as an unpitched/percussion sound via `<midi-unpitched>`.
*/
public isUnpitched: boolean = false;
}

/**
Expand Down Expand Up @@ -189,6 +194,10 @@ class TrackInfo {
this.track.percussionArticulations.push(newArticulation);
return index;
}

public isUnpitchedInstrument(instrumentId: string): boolean {
return this.instruments.has(instrumentId) && this.instruments.get(instrumentId)!.isUnpitched;
}
}

/**
Expand Down Expand Up @@ -692,6 +701,7 @@ export class MusicXmlImporter extends ScoreImporter {
break;
case 'midi-unpitched':
articulation.outputMidiNumber = Number.parseInt(c.innerText, 10) - 1;
articulation.isUnpitched = true;
break;
case 'volume':
articulation.outputVolume = MusicXmlImporter._interpolatePercent(Number.parseFloat(c.innerText));
Expand Down Expand Up @@ -2788,15 +2798,24 @@ export class MusicXmlImporter extends ScoreImporter {
}

const trackInfo = this._indexToTrackInfo.get(track.index)!;
if (instrumentId !== null) {

if (!isPitched) {
// <unpitched> note -> always a percussion/unpitched sound
note.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId ?? '', note);
return;
}

// isPitched === true from here on: only treat as percussion if we have
// explicit evidence this is really an unpitched/percussion sound. A plain
// <instrument> reference on a pitched note can just be disambiguating between
// multiple pitched score-instruments in the same part and must not imply percussion.
if (instrumentId !== null && trackInfo.isUnpitchedInstrument(instrumentId)) {
note.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId, note);
} else if (note.beat.voice.bar.staff.isPercussion && isPitched) {
} else if (note.beat.voice.bar.staff.isPercussion) {
const knownArticulation = PercussionMapper.getArticulationById(note.displayValue);
if (knownArticulation) {
note.percussionArticulation = knownArticulation.id;
}
} else if (!isPitched) {
note.percussionArticulation = trackInfo.getOrCreateArticulation('', note);
}
}

Expand Down
Binary file modified packages/alphatab/test-data/musicxml-samples/Telemann.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<score-partwise version="4.0">
<part-list>
<score-part id="P1">
<part-name>Mixed</part-name>
<score-instrument id="P1-I1">
<instrument-name>Piano</instrument-name>
</score-instrument>
<midi-instrument id="P1-I1">
<midi-channel>1</midi-channel>
<midi-program>1</midi-program>
</midi-instrument>
<score-instrument id="P1-I2">
<instrument-name>Snare Drum</instrument-name>
</score-instrument>
<midi-instrument id="P1-I2">
<midi-channel>10</midi-channel>
<midi-unpitched>38</midi-unpitched>
</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>G</sign>
<line>2</line>
</clef>
</attributes>
<note>
<pitch>
<step>C</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<instrument id="P1-I1" />
<voice>1</voice>
<type>quarter</type>
</note>
<note>
<pitch>
<step>D</step>
<octave>4</octave>
</pitch>
<duration>1</duration>
<instrument id="P1-I2" />
<voice>1</voice>
<type>quarter</type>
</note>
</measure>
</part>
</score-partwise>
28 changes: 28 additions & 0 deletions packages/alphatab/test/importer/MusicXmlImporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ describe('MusicXmlImporterTests', () => {
expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].isTieDestination).toBe(true);
expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].tieOrigin).toBeTruthy();

// notes carry a <instrument id="..."/> reference purely to disambiguate the
// score-instrument (a pitched acoustic guitar) - this must not mark them as percussion.
for (const beat of score.tracks[0].staves[0].bars[0].voices[0].beats) {
for (const note of beat.notes) {
expect(note.isPercussion).toBe(false);
}
}

score = JsonConverter.jsObjectToScore(JsonConverter.scoreToJsObject(score));

expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isTieOrigin).toBe(true);
Expand Down Expand Up @@ -289,6 +297,26 @@ describe('MusicXmlImporterTests', () => {
expect(notes[1].percussionArticulation).toBe(49);
});

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

expect(notes).toHaveLength(2);

// <pitch> note referencing a normal pitched instrument via <instrument id> just to
// disambiguate the score-instrument -> must NOT be treated as percussion.
expect(notes[0].isPercussion).toBe(false);
expect(Number.isNaN(notes[0].percussionArticulation)).toBe(true);
expect(notes[0].realValue).toBe(60); // C4

// <pitch> note referencing a score-instrument declared unpitched via
// <midi-instrument><midi-unpitched> -> IS a genuine percussion sound.
expect(notes[1].isPercussion).toBe(true);
expect(notes[1].percussionArticulation).toBeGreaterThanOrEqual(0);
});

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