diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 97ae26c03..996002638 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -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 ``. + */ + public isUnpitched: boolean = false; } /** @@ -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; + } } /** @@ -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)); @@ -2788,15 +2798,24 @@ export class MusicXmlImporter extends ScoreImporter { } const trackInfo = this._indexToTrackInfo.get(track.index)!; - if (instrumentId !== null) { + + if (!isPitched) { + // 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 + // 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); } } diff --git a/packages/alphatab/test-data/musicxml-samples/Telemann.png b/packages/alphatab/test-data/musicxml-samples/Telemann.png index b3c44d944..7495e73a4 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Telemann.png and b/packages/alphatab/test-data/musicxml-samples/Telemann.png differ diff --git a/packages/alphatab/test-data/musicxml4/percussion-instrument-vs-pitched.xml b/packages/alphatab/test-data/musicxml4/percussion-instrument-vs-pitched.xml new file mode 100644 index 000000000..81d4ecb08 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/percussion-instrument-vs-pitched.xml @@ -0,0 +1,60 @@ + + + + + Mixed + + Piano + + + 1 + 1 + + + Snare Drum + + + 10 + 38 + + + + + + + 1 + + 0 + + + + G + 2 + + + + + C + 4 + + 1 + + 1 + quarter + + + + D + 4 + + 1 + + 1 + quarter + + + + diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts index 2ad1577c6..c677909ae 100644 --- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts +++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts @@ -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 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); @@ -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); + + // note referencing a normal pitched instrument via 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 + + // note referencing a score-instrument declared unpitched via + // -> 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}`);