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
13 changes: 5 additions & 8 deletions packages/alphatab/src/importer/GpifParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,9 +979,7 @@ export class GpifParser {
}
}

if (!staff.isPercussion) {
staff.showTablature = true;
}
staff.showTablature = true;

break;
case 'DiagramCollection':
Expand Down Expand Up @@ -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)) {
Expand Down
4 changes: 1 addition & 3 deletions packages/alphatab/src/importer/PartConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/alphatab/src/model/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
2 changes: 0 additions & 2 deletions packages/alphatab/src/model/Staff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ export class Staff {

public finish(settings: Settings, sharedDataBag: Map<string, unknown> | null = null): void {
if (this.isPercussion) {
this.stringTuning.tunings = [];
this.showTablature = false;
this.displayTranspositionPitch = 0;
}
this.stringTuning.finish();
Expand Down
1 change: 0 additions & 1 deletion packages/alphatab/src/rendering/TabBarRendererFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Binary file not shown.
Binary file not shown.
46 changes: 46 additions & 0 deletions packages/alphatab/test/importer/Gp7Importer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
122 changes: 122 additions & 0 deletions packages/alphatab/test/importer/GpifParser.test.ts
Original file line number Diff line number Diff line change
@@ -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 = `<?xml version="1.0" encoding="utf-8"?>
<GPIF>
<GPRevision>1</GPRevision>
<Score><Title></Title><SubTitle></SubTitle><Artist></Artist><Album></Album><Words></Words><Music></Music><Copyright></Copyright><Tabber></Tabber><Instructions></Instructions><Notices></Notices></Score>
<MasterTrack><Tracks>0</Tracks></MasterTrack>
<Tracks>
<Track id="0">
<Name>Drums</Name>
<ShortName>Dr</ShortName>
<Color>0 0 0</Color>
<Instrument ref="drumkit"/>
<GeneralMidi table="Percussion"><Program>0</Program><Port>0</Port><PrimaryChannel>9</PrimaryChannel><SecondaryChannel>9</SecondaryChannel></GeneralMidi>
<Properties>
<Property name="Tuning"><Pitches>0 0 0 0 0 0</Pitches></Property>
</Properties>
</Track>
</Tracks>
<MasterBars>
<MasterBar><Bars>0</Bars><Key><AccidentalCount>0</AccidentalCount><Mode>Major</Mode></Key></MasterBar>
</MasterBars>
<Bars>
<Bar id="0"><Voices>0</Voices></Bar>
</Bars>
<Voices>
<Voice id="0"><Beats>0</Beats></Voice>
</Voices>
<Beats>
<Beat id="0"><Rhythm ref="0"/><Notes>0</Notes><Properties></Properties></Beat>
</Beats>
<Rhythms>
<Rhythm id="0"><NoteValue>Quarter</NoteValue></Rhythm>
</Rhythms>
<Notes>
<Note id="0">
<InstrumentArticulation>36</InstrumentArticulation>
<Properties>
<Property name="String"><String>5</String></Property>
<Property name="Fret"><Fret>36</Fret></Property>
</Properties>
</Note>
</Notes>
</GPIF>`;

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 = `<?xml version="1.0" encoding="utf-8"?>
<GPIF>
<GPRevision>1</GPRevision>
<Score><Title></Title><SubTitle></SubTitle><Artist></Artist><Album></Album><Words></Words><Music></Music><Copyright></Copyright><Tabber></Tabber><Instructions></Instructions><Notices></Notices></Score>
<MasterTrack><Tracks>0</Tracks></MasterTrack>
<Tracks>
<Track id="0">
<Name>Drums</Name>
<ShortName>Dr</ShortName>
<Color>0 0 0</Color>
<Instrument ref="drumkit"/>
<GeneralMidi table="Percussion"><Program>0</Program><Port>0</Port><PrimaryChannel>9</PrimaryChannel><SecondaryChannel>9</SecondaryChannel></GeneralMidi>
<Properties>
<Property name="Tuning"><Pitches>0 0 0 0 0 0</Pitches></Property>
</Properties>
</Track>
</Tracks>
<MasterBars>
<MasterBar><Bars>0</Bars><Key><AccidentalCount>0</AccidentalCount><Mode>Major</Mode></Key></MasterBar>
</MasterBars>
<Bars>
<Bar id="0"><Voices>0</Voices></Bar>
</Bars>
<Voices>
<Voice id="0"><Beats>0 1 2 3 4</Beats></Voice>
</Voices>
<Beats>
<Beat id="0"><Rhythm ref="0"/><Notes>0</Notes><Properties></Properties></Beat>
<Beat id="1"><Rhythm ref="0"/><Notes>1</Notes><Properties></Properties></Beat>
<Beat id="2"><Rhythm ref="0"/><Notes>2</Notes><Properties></Properties></Beat>
<Beat id="3"><Rhythm ref="0"/><Notes>3</Notes><Properties></Properties></Beat>
<Beat id="4"><Rhythm ref="0"/><Notes>4</Notes><Properties></Properties></Beat>
</Beats>
<Rhythms>
<Rhythm id="0"><NoteValue>Quarter</NoteValue></Rhythm>
</Rhythms>
<Notes>
<Note id="0"><InstrumentArticulation>36</InstrumentArticulation><Properties><Property name="String"><String>0</String></Property><Property name="Fret"><Fret>36</Fret></Property></Properties></Note>
<Note id="1"><InstrumentArticulation>36</InstrumentArticulation><Properties><Property name="String"><String>1</String></Property><Property name="Fret"><Fret>36</Fret></Property></Properties></Note>
<Note id="2"><InstrumentArticulation>36</InstrumentArticulation><Properties><Property name="String"><String>2</String></Property><Property name="Fret"><Fret>36</Fret></Property></Properties></Note>
<Note id="3"><InstrumentArticulation>36</InstrumentArticulation><Properties><Property name="String"><String>3</String></Property><Property name="Fret"><Fret>36</Fret></Property></Properties></Note>
<Note id="4"><InstrumentArticulation>36</InstrumentArticulation><Properties><Property name="String"><String>4</String></Property><Property name="Fret"><Fret>36</Fret></Property></Properties></Note>
</Notes>
</GPIF>`;

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);
});
});
});
129 changes: 129 additions & 0 deletions packages/alphatab/test/model/PercussionTablature.test.ts
Original file line number Diff line number Diff line change
@@ -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;
});
});
});