diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index ceffcb228..9a43f1623 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -979,9 +979,7 @@ export class GpifParser { } } - if (!staff.isPercussion) { - staff.showTablature = true; - } + staff.showTablature = true; break; case 'DiagramCollection': @@ -2779,12 +2777,11 @@ export class GpifParser { for (const noteId of this._notesOfBeat.get(beatId)!) { if (noteId !== GpifParser._invalidId) { const note = NoteCloner.clone(this._noteById.get(noteId)!); - // reset midi value for non-percussion staves - if (staff.isPercussion) { - note.fret = -1; - note.string = -1; - } else { + if (!staff.isPercussion) { note.percussionArticulation = -1; + } else if (note.string > 5) { + // Drum notation uses 5 lines; string 6+ won't render + note.string = 5; } beat.addNote(note); if (this._tappedNotes.has(noteId)) { diff --git a/packages/alphatab/src/importer/PartConfiguration.ts b/packages/alphatab/src/importer/PartConfiguration.ts index 3b8e08b14..eae655136 100644 --- a/packages/alphatab/src/importer/PartConfiguration.ts +++ b/packages/alphatab/src/importer/PartConfiguration.ts @@ -70,9 +70,7 @@ export class PartConfiguration { if (trackIndex < score.tracks.length) { const track: Track = score.tracks[trackIndex]; for (const staff of track.staves) { - if(!staff.isPercussion){ - staff.showTablature = trackConfig.showTablature; - } + staff.showTablature = trackConfig.showTablature; staff.showStandardNotation = trackConfig.showStandardNotation; staff.showSlash = trackConfig.showSlash; staff.showNumbered = trackConfig.showNumbered; diff --git a/packages/alphatab/src/model/Note.ts b/packages/alphatab/src/model/Note.ts index 1a55a49a6..6d9f3bf7d 100644 --- a/packages/alphatab/src/model/Note.ts +++ b/packages/alphatab/src/model/Note.ts @@ -238,7 +238,7 @@ export class Note { public tone: number = -1; public get isPercussion(): boolean { - return !this.isStringed && this.percussionArticulation >= 0; + return this.percussionArticulation >= 0; } /** diff --git a/packages/alphatab/src/model/Staff.ts b/packages/alphatab/src/model/Staff.ts index 82cb2e2f9..e6a8eddd5 100644 --- a/packages/alphatab/src/model/Staff.ts +++ b/packages/alphatab/src/model/Staff.ts @@ -123,8 +123,6 @@ export class Staff { public finish(settings: Settings, sharedDataBag: Map | null = null): void { if (this.isPercussion) { - this.stringTuning.tunings = []; - this.showTablature = false; this.displayTranspositionPitch = 0; } this.stringTuning.finish(); diff --git a/packages/alphatab/src/rendering/TabBarRendererFactory.ts b/packages/alphatab/src/rendering/TabBarRendererFactory.ts index df2521c38..bfdcfc2e1 100644 --- a/packages/alphatab/src/rendering/TabBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/TabBarRendererFactory.ts @@ -17,7 +17,6 @@ export class TabBarRendererFactory extends BarRendererFactory { public constructor(effectBands: EffectBandInfo[]) { super(effectBands); - this.hideOnPercussionTrack = true; } public override canCreate(track: Track, staff: Staff): boolean { diff --git a/packages/alphatab/test-data/guitarpro7/drum-custom-lines.gp b/packages/alphatab/test-data/guitarpro7/drum-custom-lines.gp new file mode 100644 index 000000000..9f0c25070 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro7/drum-custom-lines.gp differ diff --git a/packages/alphatab/test-data/guitarpro7/drum-tabs.gp b/packages/alphatab/test-data/guitarpro7/drum-tabs.gp new file mode 100644 index 000000000..b055181dd Binary files /dev/null and b/packages/alphatab/test-data/guitarpro7/drum-tabs.gp differ diff --git a/packages/alphatab/test/importer/Gp7Importer.test.ts b/packages/alphatab/test/importer/Gp7Importer.test.ts index beac718d4..d7d87fd43 100644 --- a/packages/alphatab/test/importer/Gp7Importer.test.ts +++ b/packages/alphatab/test/importer/Gp7Importer.test.ts @@ -1050,4 +1050,50 @@ describe('Gp7ImporterTest', () => { expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].invertBeamDirection).to.be.false; expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); }); + + it('drum-tabs-preserves-percussion-tab-data', async () => { + const reader = await prepareImporterWithFile('guitarpro7/drum-tabs.gp'); + const score: Score = reader.readScore(); + + const staff = score.tracks[0].staves[0]; + expect(staff.isPercussion).to.be.true; + expect(staff.tuning.length).to.equal(6); + expect(staff.tuning.every((t: number) => t === 0)).to.be.true; + + const beats = staff.bars[0].voices[0].beats; + expect(beats.length).to.equal(4); + + for (const beat of beats) { + for (const note of beat.notes) { + expect(note.isPercussion).to.be.true; + expect(note.isStringed).to.be.true; + expect(note.string).to.be.greaterThanOrEqual(1); + expect(note.string).to.be.lessThanOrEqual(6); + expect(note.fret).to.be.greaterThan(0); + expect(note.percussionArticulation).to.be.greaterThanOrEqual(0); + } + } + }); + + it('drum-custom-lines-preserves-string-assignments', async () => { + const reader = await prepareImporterWithFile('guitarpro7/drum-custom-lines.gp'); + const score: Score = reader.readScore(); + + const staff = score.tracks[0].staves[0]; + expect(staff.isPercussion).to.be.true; + expect(staff.showTablature).to.be.true; + expect(staff.tuning.length).to.equal(6); + + const beats = staff.bars[0].voices[0].beats; + expect(beats.length).to.equal(4); + + expect(beats[0].notes[0].string).to.equal(5); + expect(beats[0].notes[0].fret).to.equal(36); + expect(beats[1].notes[0].string).to.equal(4); + expect(beats[1].notes[0].fret).to.equal(36); + expect(beats[2].notes[0].string).to.equal(3); + expect(beats[2].notes[0].fret).to.equal(36); + expect(beats[3].notes[0].string).to.equal(2); + expect(beats[3].notes[0].fret).to.equal(36); + }); }); diff --git a/packages/alphatab/test/importer/GpifParser.test.ts b/packages/alphatab/test/importer/GpifParser.test.ts new file mode 100644 index 000000000..5814d7242 --- /dev/null +++ b/packages/alphatab/test/importer/GpifParser.test.ts @@ -0,0 +1,122 @@ +import { GpifParser } from '@coderline/alphatab/importer/GpifParser'; +import { Settings } from '@coderline/alphatab/Settings'; +import { expect } from 'chai'; + +describe('GpifParser', () => { + describe('drum string clamping', () => { + it('clamps drum note string 6 to 5', () => { + // Minimal GPIF with drum track and note with String 6 (0-based: 5 -> our string 6) + const gpif = ` + +1 + +0 + + +Drums +Dr +0 0 0 + +0099 + +0 0 0 0 0 0 + + + + +00Major + + +0 + + +0 + + +0 + + +Quarter + + + +36 + +5 +36 + + + +`; + + const parser = new GpifParser(); + parser.parseXml(gpif, new Settings()); + + const staff = parser.score.tracks[0].staves[0]; + expect(staff.isPercussion).to.be.true; + + const note = parser.score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + // GPIF String 5 = 0-based index 5 -> our string 6. Should be clamped to 5. + expect(note.string).to.equal(5); + expect(note.percussionArticulation).to.equal(36); + }); + + it('preserves drum note string 1-5', () => { + const gpif = ` + +1 + +0 + + +Drums +Dr +0 0 0 + +0099 + +0 0 0 0 0 0 + + + + +00Major + + +0 + + +0 1 2 3 4 + + +0 +1 +2 +3 +4 + + +Quarter + + +36036 +36136 +36236 +36336 +36436 + +`; + + const parser = new GpifParser(); + parser.parseXml(gpif, new Settings()); + + const beats = parser.score.tracks[0].staves[0].bars[0].voices[0].beats; + // GPIF String 0->4 = our strings 1->5 + expect(beats[0].notes[0].string).to.equal(1); + expect(beats[1].notes[0].string).to.equal(2); + expect(beats[2].notes[0].string).to.equal(3); + expect(beats[3].notes[0].string).to.equal(4); + expect(beats[4].notes[0].string).to.equal(5); + }); + }); +}); diff --git a/packages/alphatab/test/model/PercussionTablature.test.ts b/packages/alphatab/test/model/PercussionTablature.test.ts new file mode 100644 index 000000000..6fdf96f18 --- /dev/null +++ b/packages/alphatab/test/model/PercussionTablature.test.ts @@ -0,0 +1,129 @@ +import { Note } from '@coderline/alphatab/model/Note'; +import { Staff } from '@coderline/alphatab/model/Staff'; +import { Track } from '@coderline/alphatab/model/Track'; +import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { TabBarRendererFactory } from '@coderline/alphatab/rendering/TabBarRendererFactory'; +import { Settings } from '@coderline/alphatab/Settings'; +import { expect } from 'chai'; + +describe('PercussionTablature', () => { + describe('Note.isPercussion', () => { + it('returns true when percussionArticulation is set regardless of string', () => { + const note = new Note(); + note.percussionArticulation = 36; + note.string = 6; + note.fret = 36; + + expect(note.isPercussion).to.be.true; + expect(note.isStringed).to.be.true; + }); + + it('returns true when percussionArticulation is set without string', () => { + const note = new Note(); + note.percussionArticulation = 38; + + expect(note.isPercussion).to.be.true; + expect(note.isStringed).to.be.false; + }); + + it('returns false when percussionArticulation is not set', () => { + const note = new Note(); + note.string = 1; + note.fret = 5; + + expect(note.isPercussion).to.be.false; + expect(note.isStringed).to.be.true; + }); + }); + + describe('Staff.finish', () => { + it('preserves showTablature and tuning for percussion with virtual tuning', () => { + const staff = new Staff(); + staff.isPercussion = true; + staff.showTablature = true; + staff.stringTuning = new Tuning('', [0, 0, 0, 0, 0, 0], false); + + staff.finish(new Settings()); + + expect(staff.showTablature).to.be.true; + expect(staff.tuning.length).to.equal(6); + expect(staff.displayTranspositionPitch).to.equal(0); + }); + + it('disables showTablature for percussion without tuning', () => { + const staff = new Staff(); + staff.isPercussion = true; + staff.showTablature = true; + staff.stringTuning = new Tuning('', [], false); + + staff.finish(new Settings()); + + expect(staff.showTablature).to.be.false; + expect(staff.tuning.length).to.equal(0); + }); + + it('resets displayTranspositionPitch for percussion', () => { + const staff = new Staff(); + staff.isPercussion = true; + staff.displayTranspositionPitch = 12; + staff.stringTuning = new Tuning('', [0, 0, 0, 0, 0, 0], false); + + staff.finish(new Settings()); + + expect(staff.displayTranspositionPitch).to.equal(0); + }); + + it('preserves showTablature for non-percussion with tuning', () => { + const staff = new Staff(); + staff.isPercussion = false; + staff.showTablature = true; + staff.stringTuning = new Tuning('', [64, 59, 55, 50, 45, 40], false); + + staff.finish(new Settings()); + + expect(staff.showTablature).to.be.true; + expect(staff.tuning.length).to.equal(6); + }); + }); + + describe('TabBarRendererFactory.canCreate', () => { + function createStaff(isPercussion: boolean, showTablature: boolean, tuning: number[]): [Track, Staff] { + const track = new Track(); + const staff = new Staff(); + staff.isPercussion = isPercussion; + staff.showTablature = showTablature; + staff.stringTuning = new Tuning('', tuning, false); + staff.track = track; + track.staves.push(staff); + return [track, staff]; + } + + it('allows creation for percussion staff with virtual tuning and showTablature', () => { + const factory = new TabBarRendererFactory([]); + const [track, staff] = createStaff(true, true, [0, 0, 0, 0, 0, 0]); + + expect(factory.canCreate(track, staff)).to.be.true; + }); + + it('rejects percussion staff when showTablature is false', () => { + const factory = new TabBarRendererFactory([]); + const [track, staff] = createStaff(true, false, [0, 0, 0, 0, 0, 0]); + + expect(factory.canCreate(track, staff)).to.be.false; + }); + + it('rejects percussion staff without tuning', () => { + const factory = new TabBarRendererFactory([]); + const [track, staff] = createStaff(true, true, []); + + expect(factory.canCreate(track, staff)).to.be.false; + }); + + it('allows creation for regular guitar staff', () => { + const factory = new TabBarRendererFactory([]); + const [track, staff] = createStaff(false, true, [64, 59, 55, 50, 45, 40]); + + expect(factory.canCreate(track, staff)).to.be.true; + }); + }); +});