From 79d88c8fcb08346de4ebdd4c2830d0e897c55b92 Mon Sep 17 00:00:00 2001 From: leocaseiro Date: Wed, 4 Mar 2026 22:32:17 +1100 Subject: [PATCH 01/12] feat(drums/tabs): allow showTablature for percussion (drums) notation --- packages/alphatab/src/importer/GpifParser.ts | 10 +- .../src/importer/PartConfiguration.ts | 4 +- packages/alphatab/src/model/Note.ts | 2 +- packages/alphatab/src/model/Staff.ts | 2 - .../src/rendering/TabBarRendererFactory.ts | 1 - .../test-data/guitarpro7/drum-custom-lines.gp | Bin 0 -> 15748 bytes .../test-data/guitarpro7/drum-tabs.gp | Bin 0 -> 14837 bytes .../test/importer/Gp7Importer.test.ts | 46 +++++++ .../test/model/PercussionTablature.test.ts | 129 ++++++++++++++++++ 9 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 packages/alphatab/test-data/guitarpro7/drum-custom-lines.gp create mode 100644 packages/alphatab/test-data/guitarpro7/drum-tabs.gp create mode 100644 packages/alphatab/test/model/PercussionTablature.test.ts diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index ceffcb228..fd629aac2 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,11 +2777,7 @@ 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; } beat.addNote(note); 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 0000000000000000000000000000000000000000..9f0c2507090bc7834aa54d9c88e13ddc0d89b4f4 GIT binary patch literal 15748 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5ddb629eb288392ab811dcb99d1b3495fd41c5a7 GIT binary patch literal 14837 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 Kfy3qcwEh { 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/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; + }); + }); +}); From c49ce2a41aafea08971746b0700831aaec8f4579 Mon Sep 17 00:00:00 2001 From: leocaseiro Date: Wed, 4 Mar 2026 22:54:45 +1100 Subject: [PATCH 02/12] add drum-tabs.gp file --- .../test-data/guitarpro7/drum-tabs.gp | Bin 14837 -> 17619 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/alphatab/test-data/guitarpro7/drum-tabs.gp b/packages/alphatab/test-data/guitarpro7/drum-tabs.gp index 5ddb629eb288392ab811dcb99d1b3495fd41c5a7..b055181dd4f4a06498ca1bdcf905f130cb8ccfb1 100644 GIT binary patch literal 17619 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 U`dJGA=Hu`6qk+zkivR%s2Sjx+i~s-t literal 14837 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 Kfy3qcwEh Date: Thu, 5 Mar 2026 12:54:11 +1100 Subject: [PATCH 03/12] fix(drums/tab): make sure all tabs are notated forcing string 5 for 5+ strings --- packages/alphatab/src/importer/GpifParser.ts | 3 + .../alphatab/test/importer/GpifParser.test.ts | 122 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 packages/alphatab/test/importer/GpifParser.test.ts diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index fd629aac2..9a43f1623 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -2779,6 +2779,9 @@ export class GpifParser { const note = NoteCloner.clone(this._noteById.get(noteId)!); 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/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); + }); + }); +}); From 2a099e81bbd207bad0c1c7cc58c7c9e01879606d Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Thu, 2 Jul 2026 14:32:14 +0200 Subject: [PATCH 04/12] chore: some cleanup work --- packages/alphatab/src/importer/GpifParser.ts | 13 +++---------- .../alphatab/src/rendering/TabBarRendererFactory.ts | 6 +----- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index 9a43f1623..9e1965a89 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -606,8 +606,6 @@ export class GpifParser { const track: Track = new Track(); track.ensureStaveCount(1); - const staff: Staff = track.staves[0]; - staff.showStandardNotation = true; const trackId: string = node.getAttribute('id'); for (const c of node.childElements()) { @@ -979,8 +977,6 @@ export class GpifParser { } } - staff.showTablature = true; - break; case 'DiagramCollection': case 'ChordCollection': @@ -1159,8 +1155,6 @@ export class GpifParser { } for (const staff of track.staves) { staff.stringTuning.tunings = tuning; - staff.showStandardNotation = true; - staff.showTablature = true; } break; case 'DiagramCollection': @@ -2777,11 +2771,10 @@ export class GpifParser { for (const noteId of this._notesOfBeat.get(beatId)!) { if (noteId !== GpifParser._invalidId) { const note = NoteCloner.clone(this._noteById.get(noteId)!); - if (!staff.isPercussion) { + if (staff.isPercussion) { + note.fret = -1; + } else { 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/rendering/TabBarRendererFactory.ts b/packages/alphatab/src/rendering/TabBarRendererFactory.ts index bfdcfc2e1..d02343bcc 100644 --- a/packages/alphatab/src/rendering/TabBarRendererFactory.ts +++ b/packages/alphatab/src/rendering/TabBarRendererFactory.ts @@ -2,7 +2,7 @@ import type { Bar } from '@coderline/alphatab/model/Bar'; import type { Staff } from '@coderline/alphatab/model/Staff'; import type { Track } from '@coderline/alphatab/model/Track'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; -import { BarRendererFactory, type EffectBandInfo } from '@coderline/alphatab/rendering/BarRendererFactory'; +import { BarRendererFactory } from '@coderline/alphatab/rendering/BarRendererFactory'; import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer'; @@ -15,10 +15,6 @@ export class TabBarRendererFactory extends BarRendererFactory { return TabBarRenderer.StaffId; } - public constructor(effectBands: EffectBandInfo[]) { - super(effectBands); - } - public override canCreate(track: Track, staff: Staff): boolean { return staff.showTablature && staff.tuning.length > 0 && super.canCreate(track, staff); } From 1e060d2c71d0c5930a0396ab99acdc88a6649f03 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 15:03:25 +0200 Subject: [PATCH 05/12] feat(alphatex): allow @ as note string separator --- .../alphatab/src/exporter/AlphaTexExporter.ts | 4 +- .../alphatab/src/importer/AlphaTexImporter.ts | 37 +- .../alphaTex/AlphaTex1LanguageDefinitions.ts | 365 +++++++++--------- .../alphaTex/AlphaTex1LanguageHandler.ts | 2 + .../src/importer/alphaTex/AlphaTexAst.ts | 48 ++- .../src/importer/alphaTex/AlphaTexLexer.ts | 2 + .../src/importer/alphaTex/AlphaTexParser.ts | 62 +-- packages/alphatab/test/PrettyFormat.ts | 4 +- packages/lsp/src/alphatex.tmLanguage.json | 12 +- 9 files changed, 294 insertions(+), 242 deletions(-) diff --git a/packages/alphatab/src/exporter/AlphaTexExporter.ts b/packages/alphatab/src/exporter/AlphaTexExporter.ts index 27dd5c55b..8c68f190b 100644 --- a/packages/alphatab/src/exporter/AlphaTexExporter.ts +++ b/packages/alphatab/src/exporter/AlphaTexExporter.ts @@ -181,7 +181,7 @@ class AlphaTexPrinter { this._writeComments(n.leadingComments); this._writeValue(n.noteValue); - this._writeToken(n.noteStringDot, false); + this._writeToken(n.noteStringSeparator, false); this._writeValue(n.noteString); if (n.noteEffects) { @@ -668,7 +668,7 @@ export class AlphaTexExporter extends ScoreExporter { nodeType: AlphaTexNodeType.Number, value: data.fret } as AlphaTexNumberLiteral; - note.noteStringDot = { + note.noteStringSeparator = { nodeType: AlphaTexNodeType.Dot }; const stringNumber = data.beat.voice.bar.staff.tuning.length - data.string + 1; diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index 7db2ff544..1a05d4dbf 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -570,7 +570,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter isTie = str === '-'; if (isTie || isDead) { numericValue = 0; - if (node.noteStringDot && node.noteString) { + if (node.noteStringSeparator && node.noteString) { detectedNoteKind = AlphaTexStaffNoteKind.Fretted; } else { detectedNoteKind = undefined; // don't know on those notes @@ -674,8 +674,8 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter code: AlphaTexDiagnosticCode.AT208, message: `Note string is out of range. Available range: 1-${this._state.currentStaff!.tuning.length}`, severity: AlphaTexDiagnosticsSeverity.Error, - start: noteValue.end, - end: noteValue.end + start: node.noteString.start, + end: node.noteString.end }); return; } @@ -716,6 +716,34 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter this._state.articulationUniqueIdToIndex.set(articulationValue, articulationIndex); } note.percussionArticulation = articulationIndex; + + if (node.noteString) { + const noteString: number = node.noteString!.value; + if (noteString < 1 || noteString > this._state.currentStaff!.tuning.length) { + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT208, + message: `Note string is out of range. Available range: 1-${this._state.currentStaff!.tuning.length}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: node.noteString.start, + end: node.noteString.end + }); + return; + } + note.string = this._state.currentStaff!.tuning.length - (noteString - 1); + } else { + // find free string + for(let i = 0; i < this._state.currentStaff!.tuning.length; i++) { + const s = this._state.currentStaff!.tuning.length - i; + if(!beat.noteStringLookup.has(s)) { + note.string = s; + break; + } + } + if(note.string === -1) { + note.string = this._state.currentStaff!.tuning.length; + } + } + break; } } @@ -742,7 +770,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter switch (staffNoteKind) { case AlphaTexStaffNoteKind.Pitched: staff.isPercussion = false; - staff.stringTuning.reset(); + staff.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; if (!this._state.staffHasExplicitDisplayTransposition.has(staff)) { staff.displayTranspositionPitch = 0; } @@ -1097,7 +1125,6 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter public applyPercussionStaff(staff: Staff) { staff.isPercussion = true; - staff.showTablature = false; staff.track.playbackInfo.program = 0; } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index cbda1438a..64522954f 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -80,9 +80,9 @@ export class AlphaTex1LanguageDefinitions { 'title', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -90,9 +90,9 @@ export class AlphaTex1LanguageDefinitions { 'subtitle', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -100,9 +100,9 @@ export class AlphaTex1LanguageDefinitions { 'artist', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -110,9 +110,9 @@ export class AlphaTex1LanguageDefinitions { 'album', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -120,9 +120,9 @@ export class AlphaTex1LanguageDefinitions { 'words', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -130,9 +130,9 @@ export class AlphaTex1LanguageDefinitions { 'music', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -140,8 +140,8 @@ export class AlphaTex1LanguageDefinitions { 'wordsandmusic', [ [ - [[17], 0], - [[10, 17], 1, ['left', 'center', 'right']] + [[107], 0], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -149,9 +149,9 @@ export class AlphaTex1LanguageDefinitions { 'copyright', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], @@ -159,87 +159,87 @@ export class AlphaTex1LanguageDefinitions { 'copyright2', [ [ - [[17], 0], - [[10, 17], 1, ['left', 'center', 'right']] + [[107], 0], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], - ['instructions', [[[[17, 10], 0]]]], - ['notices', [[[[17, 10], 0]]]], + ['instructions', [[[[107, 100], 0]]]], + ['notices', [[[[107, 100], 0]]]], [ 'tab', [ [ - [[17, 10], 0], - [[17], 1], - [[10, 17], 1, ['left', 'center', 'right']] + [[107, 100], 0], + [[107], 1], + [[100, 107], 1, ['left', 'center', 'right']] ] ] ], - ['systemslayout', [[[[16], 5]]]], - ['defaultsystemslayout', [[[[16], 0]]]], + ['systemslayout', [[[[106], 5]]]], + ['defaultsystemslayout', [[[[106], 0]]]], ['showdynamics', null], ['hidedynamics', null], ['usesystemsignseparator', null], - ['tuningdisplaymode', [[[[10, 17], 0, ['score', 'staff']]]]], + ['tuningdisplaymode', [[[[100, 107], 0, ['score', 'staff']]]]], ['multibarrest', null], - ['bracketextendmode', [[[[10, 17], 0, ['nobrackets', 'groupstaves', 'groupsimilarinstruments']]]]], - ['singletracktracknamepolicy', [[[[10, 17], 0, ['hidden', 'firstsystem', 'allsystems']]]]], - ['multitracktracknamepolicy', [[[[10, 17], 0, ['hidden', 'firstsystem', 'allsystems']]]]], - ['firstsystemtracknamemode', [[[[10, 17], 0, ['fullname', 'shortname']]]]], - ['othersystemstracknamemode', [[[[10, 17], 0, ['fullname', 'shortname']]]]], - ['firstsystemtracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]], - ['othersystemstracknameorientation', [[[[10, 17], 0, ['horizontal', 'vertical']]]]], + ['bracketextendmode', [[[[100, 107], 0, ['nobrackets', 'groupstaves', 'groupsimilarinstruments']]]]], + ['singletracktracknamepolicy', [[[[100, 107], 0, ['hidden', 'firstsystem', 'allsystems']]]]], + ['multitracktracknamepolicy', [[[[100, 107], 0, ['hidden', 'firstsystem', 'allsystems']]]]], + ['firstsystemtracknamemode', [[[[100, 107], 0, ['fullname', 'shortname']]]]], + ['othersystemstracknamemode', [[[[100, 107], 0, ['fullname', 'shortname']]]]], + ['firstsystemtracknameorientation', [[[[100, 107], 0, ['horizontal', 'vertical']]]]], + ['othersystemstracknameorientation', [[[[100, 107], 0, ['horizontal', 'vertical']]]]], ['extendbarlines', null], - ['chorddiagramsinscore', [[[[10], 1, ['true', 'false']]]]], + ['chorddiagramsinscore', [[[[100], 1, ['true', 'false']]]]], ['hideemptystaves', null], ['hideemptystavesinfirstsystem', null], ['showsinglestaffbrackets', null], - ['defaultbarnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]] + ['defaultbarnumberdisplay', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]] ]); public static readonly staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ - ['tuning', [[[[10, 17], 0, ['piano', 'none', 'voice']]], [[[10, 17], 5]]]], + ['tuning', [[[[100, 107], 0, ['piano', 'none', 'voice']]], [[[100, 107], 5]]]], [ 'chord', [ [ - [[17, 10], 0], - [[10, 17, 16], 5] + [[107, 100], 0], + [[100, 107, 106], 5] ] ] ], - ['capo', [[[[16], 0]]]], + ['capo', [[[[106], 0]]]], [ 'lyrics', [ - [[[17], 0]], + [[[107], 0]], [ - [[16], 0], - [[17], 0] + [[106], 0], + [[107], 0] ] ] ], [ 'articulation', [ - [[[10], 0, ['defaults']]], + [[[100], 0, ['defaults']]], [ - [[17, 10], 0], - [[16], 0] + [[107, 100], 0], + [[106], 0] ] ] ], - ['displaytranspose', [[[[16], 0]]]], - ['transpose', [[[[16], 0]]]], - ['instrument', [[[[16], 0]], [[[17, 10], 0]], [[[10], 0, ['percussion']]]]] + ['displaytranspose', [[[[106], 0]]]], + ['transpose', [[[[106], 0]]]], + ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]] ]); public static readonly structuralMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ [ 'track', [ [ - [[17], 1], - [[17], 1] + [[107], 1], + [[107], 1] ] ] ], @@ -250,22 +250,22 @@ export class AlphaTex1LanguageDefinitions { [ 'ts', [ - [[[10, 17], 0, ['common']]], + [[[100, 107], 0, ['common']]], [ - [[16], 0], - [[16], 0] + [[106], 0], + [[106], 0] ] ] ], ['ro', null], - ['rc', [[[[16], 0]]]], - ['ae', [[[[16, 13], 4]]]], + ['rc', [[[[106], 0]]]], + ['ae', [[[[106, 103], 4]]]], [ 'ks', [ [ [ - [10, 17], + [100, 107], 0, [ 'cb', @@ -320,16 +320,16 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['clef', [[[[10, 16, 17], 0, ['neutral', 'c3', 'c4', 'f4', 'g2', 'n', 'alto', 'tenor', 'bass', 'treble']]]]], - ['ottava', [[[[10, 17], 0, ['15ma', '8va', 'regular', '8vb', '15mb', '15ma', '8va', '8vb', '15mb']]]]], + ['clef', [[[[100, 106, 107], 0, ['neutral', 'c3', 'c4', 'f4', 'g2', 'n', 'alto', 'tenor', 'bass', 'treble']]]]], + ['ottava', [[[[100, 107], 0, ['15ma', '8va', 'regular', '8vb', '15mb', '15ma', '8va', '8vb', '15mb']]]]], [ 'tempo', [ [ - [[16], 2], - [[17], 1] + [[106], 2], + [[107], 1] ], - [null, [[16], 2], [[17], 0], [[16], 1], [[10], 1, ['hide']]] + [null, [[106], 2], [[107], 0], [[106], 1], [[100], 1, ['hide']]] ] ], [ @@ -337,7 +337,7 @@ export class AlphaTex1LanguageDefinitions { [ [ [ - [10, 16, 17], + [100, 106, 107], 0, [ 'none', @@ -371,10 +371,10 @@ export class AlphaTex1LanguageDefinitions { [ 'section', [ - [[[17, 10], 0]], + [[[107, 100], 0]], [ - [[17, 10], 0], - [[17, 10], 0, null, ['x', '-', 'r']] + [[107, 100], 0], + [[107, 100], 0, null, ['x', '-', 'r']] ] ] ], @@ -383,7 +383,7 @@ export class AlphaTex1LanguageDefinitions { [ [ [ - [10, 17], + [100, 107], 0, [ 'fine', @@ -411,13 +411,13 @@ export class AlphaTex1LanguageDefinitions { ] ], ['ft', null], - ['simile', [[[[10, 17], 0, ['none', 'simple', 'firstofdouble', 'secondofdouble']]]]], + ['simile', [[[[100, 107], 0, ['none', 'simple', 'firstofdouble', 'secondofdouble']]]]], [ 'barlineleft', [ [ [ - [10, 17], + [100, 107], 0, [ 'automatic', @@ -442,7 +442,7 @@ export class AlphaTex1LanguageDefinitions { [ [ [ - [10, 17], + [100, 107], 0, [ 'automatic', @@ -462,32 +462,32 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['scale', [[[[16], 2]]]], - ['width', [[[[16], 2]]]], + ['scale', [[[[106], 2]]]], + ['width', [[[[106], 2]]]], [ 'sync', [ [ - [[16], 0], - [[16], 0], - [[16], 0], - [[16], 3] + [[106], 0], + [[106], 0], + [[106], 0], + [[106], 3] ] ] ], - ['accidentals', [[[[10, 17], 0, ['auto', 'explicit']]]]], - ['spd', [[[[16], 2]]]], - ['sph', [[[[16], 2]]]], - ['spu', [[[[16], 2]]]], + ['accidentals', [[[[100, 107], 0, ['auto', 'explicit']]]]], + ['spd', [[[[106], 2]]]], + ['sph', [[[[106], 2]]]], + ['spu', [[[[106], 2]]]], ['db', null], - ['voicemode', [[[[10, 17], 0, ['staffwise', 'barwise']]]]], - ['barnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['voicemode', [[[[100, 107], 0, ['staffwise', 'barwise']]]]], + ['barnumberdisplay', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], [ 'beaming', [ [ - [[16], 0], - [[16], 5] + [[106], 0], + [[106], 5] ] ] ] @@ -496,22 +496,22 @@ export class AlphaTex1LanguageDefinitions { [ 'track', [ - ['color', [[[[17], 0]]]], - ['systemslayout', [[[[16], 5]]]], - ['defaultsystemslayout', [[[[16], 0]]]], + ['color', [[[[107], 0]]]], + ['systemslayout', [[[[106], 5]]]], + ['defaultsystemslayout', [[[[106], 0]]]], ['solo', null], ['mute', null], - ['volume', [[[[16], 0]]]], - ['balance', [[[[16], 0]]]], - ['instrument', [[[[16], 0]], [[[17, 10], 0]], [[[10], 0, ['percussion']]]]], - ['bank', [[[[16], 0]]]], + ['volume', [[[[106], 0]]]], + ['balance', [[[[106], 0]]]], + ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]], + ['bank', [[[[106], 0]]]], ['multibarrest', null] ] ], [ 'staff', [ - ['score', [[[[16], 1]]]], + ['score', [[[[106], 1]]]], ['tabs', null], ['slash', null], ['numbered', null] @@ -554,25 +554,25 @@ export class AlphaTex1LanguageDefinitions { 'tuning', [ ['hide', null], - ['label', [[[[17], 0]]]] + ['label', [[[[107], 0]]]] ] ], [ 'chord', [ - ['firstfret', [[[[16], 0]]]], - ['barre', [[[[16], 5]]]], + ['firstfret', [[[[106], 0]]]], + ['barre', [[[[106], 5]]]], [ 'showdiagram', - [[], [[[17], 0, ['true', 'false']]], [[[10], 0, ['true', 'false']]], [[[16], 0, ['1', '0']]]] + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] ], [ 'showfingering', - [[], [[[17], 0, ['true', 'false']]], [[[10], 0, ['true', 'false']]], [[[16], 0, ['1', '0']]]] + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] ], [ 'showname', - [[], [[[17], 0, ['true', 'false']]], [[[10], 0, ['true', 'false']]], [[[16], 0, ['1', '0']]]] + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] ] ] ], @@ -620,10 +620,10 @@ export class AlphaTex1LanguageDefinitions { [ 'tu', [ - [[[16], 0, ['3', '5', '6', '7', '9', '10', '12']]], + [[[106], 0, ['3', '5', '6', '7', '9', '10', '12']]], [ - [[16], 0], - [[16], 0] + [[106], 0], + [[106], 0] ] ] ] @@ -658,75 +658,74 @@ export class AlphaTex1LanguageDefinitions { [ 'tu', [ - [[[16], 0, ['3', '5', '6', '7', '9', '10', '12']]], + [[[106], 0, ['3', '5', '6', '7', '9', '10', '12']]], [ - [[16], 0], - [[16], 0] + [[106], 0], + [[106], 0] ] ] ], - ['txt', [[[[17, 10], 0]]]], - ['restdisplaypitch', [[[[10, 17], 0]]]], + ['txt', [[[[107, 100], 0]]]], [ 'lyrics', [ - [[[17], 0]], + [[[107], 0]], [ - [[16], 0], - [[17], 0] + [[106], 0], + [[107], 0] ] ] ], [ 'tb', [ - [[[16], 5]], + [[[106], 5]], [ - [[10, 17], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], - [[16], 5] + [[100, 107], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], + [[106], 5] ], [ - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ], [ - [[10, 17], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ] ] ], [ 'tbe', [ - [[[16], 5]], + [[[106], 5]], [ - [[10, 17], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], - [[16], 5] + [[100, 107], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], + [[106], 5] ], [ - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ], [ - [[10, 17], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['custom', 'dive', 'dip', 'hold', 'predive', 'predivedive']], + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ] ] ], - ['bu', [[[[16], 1]]]], - ['bd', [[[[16], 1]]]], - ['au', [[[[16], 1]]]], - ['ad', [[[[16], 1]]]], - ['ch', [[[[17, 10], 0]]]], - ['gr', [[[[10, 17], 1, ['onbeat', 'beforebeat', 'bendgrace', 'ob', 'bb', 'b']]]]], + ['bu', [[[[106], 1]]]], + ['bd', [[[[106], 1]]]], + ['au', [[[[106], 1]]]], + ['ad', [[[[106], 1]]]], + ['ch', [[[[107, 100], 0]]]], + ['gr', [[[[100, 107], 1, ['onbeat', 'beforebeat', 'bendgrace', 'ob', 'bb', 'b']]]]], [ 'dy', [ [ [ - [10, 17], + [100, 107], 0, [ 'ppp', @@ -764,24 +763,24 @@ export class AlphaTex1LanguageDefinitions { 'tempo', [ [ - [[16], 0], - [[10], 1, ['hide']] + [[106], 0], + [[100], 1, ['hide']] ], [ - [[16], 0], - [[17], 0], - [[10], 1, ['hide']] + [[106], 0], + [[107], 0], + [[100], 1, ['hide']] ] ] ], - ['volume', [[[[16], 0]]]], - ['balance', [[[[16], 0]]]], + ['volume', [[[[106], 0]]]], + ['balance', [[[[106], 0]]]], [ 'tp', [ [ - [[16], 0], - [[10, 17], 1, ['default', 'buzzroll']] + [[106], 0], + [[100, 107], 1, ['default', 'buzzroll']] ] ] ], @@ -789,8 +788,8 @@ export class AlphaTex1LanguageDefinitions { 'barre', [ [ - [[16], 0], - [[10, 17], 1, ['full', 'half']] + [[106], 0], + [[100, 107], 1, ['full', 'half']] ] ] ], @@ -799,7 +798,7 @@ export class AlphaTex1LanguageDefinitions { [ [ [ - [10, 17], + [100, 107], 0, [ 'ii', @@ -825,27 +824,27 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['ot', [[[[10, 17], 0, ['15ma', '8va', 'regular', '8vb', '15mb', '15ma', '8va', '8vb', '15mb']]]]], - ['instrument', [[[[16], 0]], [[[17, 10], 0]], [[[10], 0, ['percussion']]]]], - ['bank', [[[[16], 0]]]], + ['ot', [[[[100, 107], 0, ['15ma', '8va', 'regular', '8vb', '15mb', '15ma', '8va', '8vb', '15mb']]]]], + ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]], + ['bank', [[[[106], 0]]]], [ 'fermata', [ [ - [[10, 17], 0, ['short', 'medium', 'long']], - [[16], 3] + [[100, 107], 0, ['short', 'medium', 'long']], + [[106], 3] ] ] ], - ['beam', [[[[10, 17], 0, ['invert', 'up', 'down', 'auto', 'split', 'merge', 'splitsecondary']]]]] + ['beam', [[[[100, 107], 0, ['invert', 'up', 'down', 'auto', 'split', 'merge', 'splitsecondary']]]]] ]); public static readonly noteProperties = AlphaTex1LanguageDefinitions._props([ ['nh', null], - ['ah', [[[[16], 1]]]], - ['th', [[[[16], 1]]]], - ['ph', [[[[16], 1]]]], - ['sh', [[[[16], 1]]]], - ['fh', [[[[16], 1]]]], + ['ah', [[[[106], 1]]]], + ['th', [[[[106], 1]]]], + ['ph', [[[[106], 1]]]], + ['sh', [[[[106], 1]]]], + ['fh', [[[[106], 1]]]], ['v', null], ['vw', null], ['sl', null], @@ -866,8 +865,8 @@ export class AlphaTex1LanguageDefinitions { 'tr', [ [ - [[16], 0], - [[16], 1, ['16', '32', '64']] + [[106], 0], + [[106], 1, ['16', '32', '64']] ] ] ], @@ -885,65 +884,65 @@ export class AlphaTex1LanguageDefinitions { [ 'b', [ - [[[16], 5]], + [[[106], 5]], [ [ - [10, 17], + [100, 107], 0, ['custom', 'bend', 'release', 'bendrelease', 'hold', 'prebend', 'prebendbend', 'prebendrelease'] ], - [[16], 5] + [[106], 5] ], [ - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ], [ [ - [10, 17], + [100, 107], 0, ['custom', 'bend', 'release', 'bendrelease', 'hold', 'prebend', 'prebendbend', 'prebendrelease'] ], - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ] ] ], [ 'be', [ - [[[16], 5]], + [[[106], 5]], [ [ - [10, 17], + [100, 107], 0, ['custom', 'bend', 'release', 'bendrelease', 'hold', 'prebend', 'prebendbend', 'prebendrelease'] ], - [[16], 5] + [[106], 5] ], [ - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ], [ [ - [10, 17], + [100, 107], 0, ['custom', 'bend', 'release', 'bendrelease', 'hold', 'prebend', 'prebendbend', 'prebendrelease'] ], - [[10, 17], 0, ['default', 'gradual', 'fast']], - [[16], 5] + [[100, 107], 0, ['default', 'gradual', 'fast']], + [[106], 5] ] ] ], - ['lf', [[[[16], 0, ['1', '2', '3', '4', '5']]]]], - ['rf', [[[[16], 0, ['1', '2', '3', '4', '5']]]]], + ['lf', [[[[106], 0, ['1', '2', '3', '4', '5']]]]], + ['rf', [[[[106], 0, ['1', '2', '3', '4', '5']]]]], [ 'acc', [ [ [ - [10, 17], + [100, 107], 0, [ 'default', @@ -966,7 +965,7 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['slur', [[[[17], 0]], [[[10], 0]]]], + ['slur', [[[[107], 0]], [[[100], 0]]]], ['-', null] ]); } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 83ff9ad8d..0bd5ab102 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -1181,6 +1181,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler const instrumentName = (args!.arguments[0] as AlphaTexTextNode).text.toLowerCase(); if (instrumentName === 'percussion') { for (const staff of track.staves) { + // hide tablature by default unless explicitly requested in staff + staff.showTablature = false; importer.applyStaffNoteKind(staff, AlphaTexStaffNoteKind.Articulation); } track.playbackInfo.primaryChannel = SynthConstants.PercussionChannel; diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts index 76d7ee857..7d81e078b 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts @@ -14,24 +14,25 @@ export enum AlphaTexNodeType { RParen = 7, Colon = 8, Asterisk = 9, + At = 10, // General Nodes - Ident = 10, - Tag = 11, - Meta = 12, - Arguments = 13, - Props = 14, - Prop = 15, - Number = 16, - String = 17, + Ident = 100, + Tag = 101, + Meta = 102, + Arguments = 103, + Props = 104, + Prop = 105, + Number = 106, + String = 107, // Semantic Nodes - Score = 18, - Bar = 19, - Beat = 20, - Duration = 21, - NoteList = 22, - Note = 23 + Score = 200, + Bar = 201, + Beat = 202, + Duration = 203, + NoteList = 204, + Note = 205 } // @@ -125,11 +126,16 @@ export interface AlphaTexComment { */ export interface AlphaTexTokenNode extends AlphaTexAstNode {} +/** + * @public + */ +export interface IAlphaTexStringSeparatorNode extends IAlphaTexAstNode {} + /** * @record * @public */ -export interface AlphaTexDotTokenNode extends AlphaTexTokenNode { +export interface AlphaTexDotTokenNode extends AlphaTexTokenNode, IAlphaTexStringSeparatorNode { nodeType: AlphaTexNodeType.Dot; } @@ -205,6 +211,14 @@ export interface AlphaTexAsteriskTokenNode extends AlphaTexTokenNode { nodeType: AlphaTexNodeType.Asterisk; } +/** + * @record + * @public + */ +export interface AlphaTexAtTokenNode extends AlphaTexTokenNode, IAlphaTexStringSeparatorNode { + nodeType: AlphaTexNodeType.At; +} + /** * A number literal within alphaTex. Can be a integer or floating point number. * @record @@ -528,9 +542,9 @@ export interface AlphaTexNoteNode extends AlphaTexAstNode { noteValue: IAlphaTexNoteValueNode; /** - * The dot separating the note value and the string for fretted/stringed instruments like guitars. + * The dot or @ separating the note value and the string for fretted/stringed instruments like guitars. */ - noteStringDot?: AlphaTexDotTokenNode; + noteStringSeparator?: IAlphaTexStringSeparatorNode; /** * The string value for fretted/stringed notes like guitars. diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts index c9b81b313..bef5c22f0 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts @@ -2,6 +2,7 @@ type AlphaTexAsteriskTokenNode, type AlphaTexAstNode, type AlphaTexAstNodeLocation, + AlphaTexAtTokenNode, type AlphaTexBackSlashTokenNode, type AlphaTexBraceCloseTokenNode, type AlphaTexBraceOpenTokenNode, @@ -225,6 +226,7 @@ export class AlphaTexLexer { [0x7d /* } */, l => l._token({ nodeType: AlphaTexNodeType.RBrace } as AlphaTexBraceCloseTokenNode)], [0x7c /* | */, l => l._token({ nodeType: AlphaTexNodeType.Pipe } as AlphaTexPipeTokenNode)], [0x2a /* * */, l => l._token({ nodeType: AlphaTexNodeType.Asterisk } as AlphaTexAsteriskTokenNode)], + [0x40 /* @ */, l => l._token({ nodeType: AlphaTexNodeType.At } as AlphaTexAtTokenNode)], [0x5c /* \ */, l => l._metaCommand()], [0x09 /* \t */, l => l._whitespace()], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts index 5d11afbf1..02ff39596 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts @@ -1,5 +1,6 @@ import { AlphaTex1MetaDataReader } from '@coderline/alphatab/importer/alphaTex/AlphaTex1MetaDataReader'; import { + type AlphaTexArgumentList, type AlphaTexAsteriskTokenNode, type AlphaTexAstNode, type AlphaTexBarNode, @@ -23,7 +24,7 @@ import { type AlphaTexPropertyNode, type AlphaTexScoreNode, type AlphaTexStringLiteral, - type AlphaTexArgumentList + type IAlphaTexStringSeparatorNode } from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; import { AlphaTexLexer } from '@coderline/alphatab/importer/alphaTex/AlphaTexLexer'; import { @@ -427,13 +428,13 @@ export class AlphaTexParser { start: noteValue.start }; try { - let canHaveString = false; + let canHaveStringDot = false; switch (noteValue.nodeType) { case AlphaTexNodeType.Number: note.noteValue = noteValue as AlphaTexNumberLiteral; this.lexer.advance(); - canHaveString = true; + canHaveStringDot = true; break; case AlphaTexNodeType.String: @@ -442,7 +443,7 @@ export class AlphaTexParser { switch ((note.noteValue as AlphaTexStringLiteral).text) { case 'x': case '-': - canHaveString = true; + canHaveStringDot = true; break; } break; @@ -452,7 +453,7 @@ export class AlphaTexParser { switch ((note.noteValue as AlphaTexIdentifier).text) { case 'x': case '-': - canHaveString = true; + canHaveStringDot = true; break; } break; @@ -465,35 +466,36 @@ export class AlphaTexParser { return undefined; } - if (canHaveString) { - const dot = this.lexer.peekToken(); - if (dot?.nodeType === AlphaTexNodeType.Dot) { - const noteStringDot = dot as AlphaTexDotTokenNode; - this.lexer.advance(); + const separator = this.lexer.peekToken(); + if ( + (canHaveStringDot && separator?.nodeType === AlphaTexNodeType.Dot) || + separator?.nodeType === AlphaTexNodeType.At + ) { + const noteStringSeparator = separator as IAlphaTexStringSeparatorNode; + this.lexer.advance(); - const noteString = this.lexer.peekToken(); - if (!noteString) { - this.unexpectedToken(noteString, [AlphaTexNodeType.Number], true); - return undefined; - } + const noteString = this.lexer.peekToken(); + if (!noteString) { + this.unexpectedToken(noteString, [AlphaTexNodeType.Number], true); + return undefined; + } - if (noteString.nodeType === AlphaTexNodeType.Tag) { - // backwards compatibility with older alphaTex: there was a dot separator - // between the song content and sync points at the end + if (noteString.nodeType === AlphaTexNodeType.Tag) { + // backwards compatibility with older alphaTex: there was a dot separator + // between the song content and sync points at the end - // handle switch to sync points like: 3 4 5 . \sync 1 1 1 - // in this example the numbers are percussion articulations + // handle switch to sync points like: 3 4 5 . \sync 1 1 1 + // in this example the numbers are percussion articulations - // (we can drop the separation dot as it is not part of the AST) - return note; - } else if (noteString.nodeType === AlphaTexNodeType.Number) { - note.noteStringDot = noteStringDot; - note.noteString = noteString as AlphaTexNumberLiteral; - this.lexer.advance(); - } else { - this.unexpectedToken(noteString, [AlphaTexNodeType.Number], true); - return undefined; - } + // (we can drop the separation dot as it is not part of the AST) + return note; + } else if (noteString.nodeType === AlphaTexNodeType.Number) { + note.noteStringSeparator = noteStringSeparator; + note.noteString = noteString as AlphaTexNumberLiteral; + this.lexer.advance(); + } else { + this.unexpectedToken(noteString, [AlphaTexNodeType.Number], true); + return undefined; } } diff --git a/packages/alphatab/test/PrettyFormat.ts b/packages/alphatab/test/PrettyFormat.ts index f96c711b8..f19e8cd4d 100644 --- a/packages/alphatab/test/PrettyFormat.ts +++ b/packages/alphatab/test/PrettyFormat.ts @@ -633,8 +633,8 @@ export class AlphaTexAstNodePlugin implements PrettyFormatNewPlugin { if (note.noteValue) { children.push(['noteValue', note.noteValue]); } - if (note.noteStringDot) { - children.push(['noteStringDot', note.noteStringDot]); + if (note.noteStringSeparator) { + children.push(['noteStringSeparator', note.noteStringSeparator]); } if (note.noteString) { children.push(['noteString', note.noteString]); diff --git a/packages/lsp/src/alphatex.tmLanguage.json b/packages/lsp/src/alphatex.tmLanguage.json index 90c90ae07..383be75c4 100644 --- a/packages/lsp/src/alphatex.tmLanguage.json +++ b/packages/lsp/src/alphatex.tmLanguage.json @@ -15,7 +15,8 @@ { "include": "#literal" }, { "include": "#expression" }, { "include": "#punctuation-pipe" }, - { "include": "#punctuation-dot" } + { "include": "#punctuation-dot" }, + { "include": "#punctuation-at" } ] }, "string": { @@ -125,14 +126,15 @@ }, "identifier": { "name": "variable.identifier.alphatex", - "match": "[^/\"'-\\.:\\(\\)\\{\\}\\|\\*\\\\\\s][^/\"'\\.:\\(\\)\\{\\}\\|\\*\\\\\\s]+", + "match": "[^@/\"'-\\.:\\(\\)\\{\\}\\|\\*\\\\\\s][^@/\"'\\.:\\(\\)\\{\\}\\|\\*\\\\\\s]+", "captures": { "0": { "name": "variable.identifier.alphatex" } } }, "punctuation": { "patterns": [ { "include": "#punctuation-dot" }, { "include": "#punctuation-pipe" }, - { "include": "#punctuation-asterisk" } + { "include": "#punctuation-asterisk" }, + { "include": "#punctuation-at" } ] }, "punctuation-pipe": { @@ -146,6 +148,10 @@ "punctuation-asterisk": { "name": "punctuation.asterisk.alphatex", "match": "\\*" + }, + "punctuation-at": { + "name": "punctuation.at.alphatex", + "match": "@" } } } \ No newline at end of file From 53329116f848aa9d16a87c9185e01c1ebf7fb612 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 15:04:00 +0200 Subject: [PATCH 06/12] refactor: ensure correct string value handling --- packages/alphatab/src/exporter/GpifWriter.ts | 4 ++++ packages/alphatab/src/importer/Gp3To5Importer.ts | 1 - packages/alphatab/src/model/Beat.ts | 4 ++-- packages/alphatab/src/model/Note.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index b00ed8737..39d33d6ef 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -290,6 +290,10 @@ export class GpifWriter { } } + if (note.isPercussion) { + this._writeSimplePropertyNode(properties, 'String', 'String', (note.string - 1).toString()); + } + if (note.isPiano) { this._writeSimplePropertyNode(properties, 'Octave', 'Number', note.octave.toString()); this._writeSimplePropertyNode(properties, 'Tone', 'Step', note.tone.toString()); diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 949838be2..ea7fb9844 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -1401,7 +1401,6 @@ export class Gp3To5Importer extends ScoreImporter { newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret) ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret)! : newNote.fret; - newNote.string = -1; newNote.fret = -1; } if (swapAccidentals) { diff --git a/packages/alphatab/src/model/Beat.ts b/packages/alphatab/src/model/Beat.ts index cc17b96d2..2da5622fd 100644 --- a/packages/alphatab/src/model/Beat.ts +++ b/packages/alphatab/src/model/Beat.ts @@ -823,7 +823,7 @@ export class Beat { note.beat = this; note.index = this.notes.length; this.notes.push(note); - if (note.isStringed) { + if (note.string >= 0 ) { this.noteStringLookup.set(note.string, note); } } @@ -832,7 +832,7 @@ export class Beat { const index: number = this.notes.indexOf(note); if (index >= 0) { this.notes.splice(index, 1); - if (note.isStringed) { + if (note.string >= 0) { this.noteStringLookup.delete(note.string); } } diff --git a/packages/alphatab/src/model/Note.ts b/packages/alphatab/src/model/Note.ts index 8e02d89c2..697662a49 100644 --- a/packages/alphatab/src/model/Note.ts +++ b/packages/alphatab/src/model/Note.ts @@ -204,7 +204,7 @@ export class Note { } public get isStringed(): boolean { - return this.string >= 0; + return this.string >= 0 && this.fret >= 0; } /** From fb81d24d6bf8ff66c1aa80c829dc0949ad57499a Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 15:04:49 +0200 Subject: [PATCH 07/12] feat: show articulation id as fret in percussion tabs --- .../src/rendering/glyphs/NoteNumberGlyph.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts b/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts index d97617b2b..f17007151 100644 --- a/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts @@ -3,6 +3,7 @@ import type { Font } from '@coderline/alphatab/model/Font'; import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { type Note, NoteSubElement } from '@coderline/alphatab/model/Note'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { NotationElement, NotationMode } from '@coderline/alphatab/NotationSettings'; import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; @@ -43,10 +44,21 @@ export class NoteNumberGlyph extends Glyph { public override doLayout(): void { const n: Note = this._note; - let fret: number = n.fret - n.beat.voice.bar.staff.transpositionPitch; - if (n.harmonicType === HarmonicType.Natural && n.harmonicValue !== 0) { - fret = n.harmonicValue - n.beat.voice.bar.staff.transpositionPitch; + let fret: number; + if (n.isPercussion) { + const articulation = PercussionMapper.getArticulation(n); + if (articulation) { + fret = articulation.id; + } else { + fret = n.percussionArticulation; + } + } else { + fret = n.fret - n.beat.voice.bar.staff.transpositionPitch; + if (n.harmonicType === HarmonicType.Natural && n.harmonicValue !== 0) { + fret = n.harmonicValue - n.beat.voice.bar.staff.transpositionPitch; + } } + if (!n.isTieDestination) { this._noteString = n.isDead ? 'x' : fret.toString(); if (n.isGhost) { From 467c2c8658a023e587989ccd8188bc8ca9489e9a Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 16:30:37 +0200 Subject: [PATCH 08/12] fix: add missing restDisplayPitch language definition --- .../alphaTex/AlphaTex1LanguageDefinitions.ts | 3 ++- packages/alphatex/src/definitions.ts | 4 +++- .../src/properties/beat/restDisplayPitch.ts | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 packages/alphatex/src/properties/beat/restDisplayPitch.ts diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 64522954f..0ab0b5484 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -836,7 +836,8 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['beam', [[[[100, 107], 0, ['invert', 'up', 'down', 'auto', 'split', 'merge', 'splitsecondary']]]]] + ['beam', [[[[100, 107], 0, ['invert', 'up', 'down', 'auto', 'split', 'merge', 'splitsecondary']]]]], + ['restdisplaypitch', [[[[100], 0]]]] ]); public static readonly noteProperties = AlphaTex1LanguageDefinitions._props([ ['nh', null], diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index 14b425f06..9646b46b0 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -159,6 +159,7 @@ import { instrumentMeta } from '@coderline/alphatab-alphatex/metadata/staff/inst import type { AlphaTexExample, WithDescription, WithSignatures } from '@coderline/alphatab-alphatex/types'; import { barNumberDisplay } from '@coderline/alphatab-alphatex/metadata/bar/barnumberdisplay'; import { beaming } from '@coderline/alphatab-alphatex/metadata/bar/beamingRule'; +import { restDisplayPitch } from '@coderline/alphatab-alphatex/properties/beat/restDisplayPitch'; export const structuralMetaData = metadata(track, staff, voice); export const scoreMetaData = metadata( @@ -295,7 +296,8 @@ export const beatProperties = properties( instrument, bank, fermata, - beam + beam, + restDisplayPitch ); export const noteProperties = properties( diff --git a/packages/alphatex/src/properties/beat/restDisplayPitch.ts b/packages/alphatex/src/properties/beat/restDisplayPitch.ts new file mode 100644 index 000000000..95e61f671 --- /dev/null +++ b/packages/alphatex/src/properties/beat/restDisplayPitch.ts @@ -0,0 +1,24 @@ +import * as alphaTab from '@coderline/alphatab'; +import type { PropertyDefinition } from '@coderline/alphatab-alphatex/types'; + +export const restDisplayPitch: PropertyDefinition = { + property: 'restDisplayPitch', + snippet: 'restDisplayPitch $1$0', + shortDescription: 'Rest Display Pitch', + longDescription: `Define the pitch on which the rest symbol should be placed.`, + signatures: [ + { + parameters: [ + { + name: 'pitch', + shortDescription: 'The note pitch defining the position, like C4', + type: alphaTab.importer.alphaTex.AlphaTexNodeType.Ident, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required + } + ] + } + ], + examples: ` + r.4{ restDisplayPitch C4 } C4 + ` +}; From cf42efaa87c2963698abe33fe61839b2c7949a29 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 16:42:50 +0200 Subject: [PATCH 09/12] refactor: switch to NaN as marker for unfilled note values --- .../alphatab/src/importer/AlphaTexImporter.ts | 14 ++++++------- .../alphatab/src/importer/Gp3To5Importer.ts | 2 +- packages/alphatab/src/importer/GpifParser.ts | 6 +++--- packages/alphatab/src/model/Beat.ts | 8 ++++---- packages/alphatab/src/model/Note.ts | 20 +++++++++---------- .../alphatab/src/model/PercussionMapper.ts | 2 +- .../src/rendering/glyphs/NoteNumberGlyph.ts | 12 +++++------ .../src/rendering/glyphs/ScoreBeatGlyph.ts | 2 +- packages/alphatab/test/PrettyFormat.ts | 2 +- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index 1a05d4dbf..dc5c4db1d 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -542,10 +542,10 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter // Note value let isDead: boolean = false; let isTie: boolean = false; - let numericValue: number = -1; + let numericValue: number = Number.NaN; let articulationValue: string = ''; - let octave: number = -1; - let tone: number = -1; + let octave: number = Number.NaN; + let tone: number = Number.NaN; let accidentalMode = NoteAccidentalMode.Default; const noteValue = node.noteValue as AlphaTexAstNode; let detectedNoteKind: AlphaTexStaffNoteKind | undefined = undefined; @@ -732,15 +732,15 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter note.string = this._state.currentStaff!.tuning.length - (noteString - 1); } else { // find free string - for(let i = 0; i < this._state.currentStaff!.tuning.length; i++) { + for (let i = 0; i < this._state.currentStaff!.tuning.length; i++) { const s = this._state.currentStaff!.tuning.length - i; - if(!beat.noteStringLookup.has(s)) { + if (!beat.noteStringLookup.has(s)) { note.string = s; break; } } - if(note.string === -1) { - note.string = this._state.currentStaff!.tuning.length; + if (Number.isNaN(note.string)) { + note.string = this._state.currentStaff!.tuning.length; } } diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index ea7fb9844..415e28516 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -1401,7 +1401,7 @@ export class Gp3To5Importer extends ScoreImporter { newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret) ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret)! : newNote.fret; - newNote.fret = -1; + newNote.fret = Number.NaN; } if (swapAccidentals) { const accidental = ModelUtils.computeAccidental( diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index c575d37bd..816086db9 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -2429,7 +2429,7 @@ export class GpifParser { case 'Octave': note.octave = GpifParser._parseIntSafe(c.findChildElement('Number')?.innerText, 0); // when exporting GP6 from GP7 the tone might be missing - if (note.tone === -1) { + if (Number.isNaN(note.tone)) { note.tone = 0; } break; @@ -2772,9 +2772,9 @@ export class GpifParser { if (noteId !== GpifParser._invalidId) { const note = NoteCloner.clone(this._noteById.get(noteId)!); if (staff.isPercussion) { - note.fret = -1; + note.fret = Number.NaN; } else { - note.percussionArticulation = -1; + note.percussionArticulation = Number.NaN; } beat.addNote(note); if (this._tappedNotes.has(noteId)) { diff --git a/packages/alphatab/src/model/Beat.ts b/packages/alphatab/src/model/Beat.ts index 2da5622fd..1d7b60379 100644 --- a/packages/alphatab/src/model/Beat.ts +++ b/packages/alphatab/src/model/Beat.ts @@ -427,15 +427,15 @@ export class Beat { /** * Gets or sets the chromatic tone value (0–11) of the pitch at which this rest should be displayed. - * A value of -1 means use the default position formula. + * A value of NaN means use the default position formula. */ - public restDisplayTone: number = -1; + public restDisplayTone: number = Number.NaN; /** * Gets or sets the octave at which this rest should be displayed. - * Only relevant when {@link restDisplayTone} is set. -1 means use the default position formula. + * Only relevant when {@link restDisplayTone} is set. NaN means use the default position formula. */ - public restDisplayOctave: number = -1; + public restDisplayOctave: number = Number.NaN; /** * Gets or sets the brush type applied to the notes of this beat. diff --git a/packages/alphatab/src/model/Note.ts b/packages/alphatab/src/model/Note.ts index 697662a49..9eee13391 100644 --- a/packages/alphatab/src/model/Note.ts +++ b/packages/alphatab/src/model/Note.ts @@ -204,21 +204,21 @@ export class Note { } public get isStringed(): boolean { - return this.string >= 0 && this.fret >= 0; + return !Number.isNaN(this.string) && !Number.isNaN(this.fret); } /** * Gets or sets the fret on which this note is played on the instrument. * 0 is the nut. */ - public fret: number = -1; + public fret: number = Number.NaN; /** * Gets or sets the string number where the note is placed. * 1 is the lowest string on the guitar and the bottom line on the tablature. * It then increases the the number of strings on available on the track. */ - public string: number = -1; + public string: number = Number.NaN; /** * Gets or sets whether the string number for this note should be shown. @@ -226,21 +226,21 @@ export class Note { public showStringNumber: boolean = false; public get isPiano(): boolean { - return !this.isStringed && this.octave >= 0 && this.tone >= 0; + return !this.isStringed && !Number.isNaN(this.octave) && !Number.isNaN(this.tone); } /** * Gets or sets the octave on which this note is played. */ - public octave: number = -1; + public octave: number = Number.NaN; /** * Gets or sets the tone of this note within the octave. */ - public tone: number = -1; + public tone: number = Number.NaN; public get isPercussion(): boolean { - return this.percussionArticulation >= 0; + return !Number.isNaN(this.percussionArticulation); } /** @@ -358,7 +358,7 @@ export class Note { * - 126 Ride (middle) * - 127 Ride (bell) */ - public percussionArticulation: number = -1; + public percussionArticulation: number = Number.NaN; /** * Gets or sets whether this note is visible on the music sheet. @@ -633,7 +633,7 @@ export class Note { } public static getStringTuning(staff: Staff, noteString: number): number { - if (staff.tuning.length > 0) { + if (staff.tuning.length > 0 && noteString >= 0) { return staff.tuning[staff.tuning.length - (noteString - 1) - 1]; } return 0; @@ -1171,7 +1171,7 @@ export class Note { return noteOnString; } } else { - if (note.octave === -1 && note.tone === -1) { + if (Number.isNaN(note.octave) && Number.isNaN(note.tone)) { // if the note has no value (e.g. alphaTex dash tie), we try to find a matching // note on the previous beat by index. if (note.index < previousBeat.notes.length) { diff --git a/packages/alphatab/src/model/PercussionMapper.ts b/packages/alphatab/src/model/PercussionMapper.ts index 6932bbcd2..67c63ef22 100644 --- a/packages/alphatab/src/model/PercussionMapper.ts +++ b/packages/alphatab/src/model/PercussionMapper.ts @@ -1180,7 +1180,7 @@ export class PercussionMapper { return articulationsByOutputNumber.has(articulation.outputMidiNumber) ? articulationsByOutputNumber.get(articulation.outputMidiNumber)!.id - : -1; + : Number.NaN; } private static _instrumentArticulationsByUniqueId: Map | undefined; diff --git a/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts b/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts index f17007151..657c2a182 100644 --- a/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NoteNumberGlyph.ts @@ -45,18 +45,18 @@ export class NoteNumberGlyph extends Glyph { public override doLayout(): void { const n: Note = this._note; let fret: number; - if (n.isPercussion) { + if (n.isStringed) { + fret = n.fret - n.beat.voice.bar.staff.transpositionPitch; + if (n.harmonicType === HarmonicType.Natural && n.harmonicValue !== 0) { + fret = n.harmonicValue - n.beat.voice.bar.staff.transpositionPitch; + } + } else { const articulation = PercussionMapper.getArticulation(n); if (articulation) { fret = articulation.id; } else { fret = n.percussionArticulation; } - } else { - fret = n.fret - n.beat.voice.bar.staff.transpositionPitch; - if (n.harmonicType === HarmonicType.Natural && n.harmonicValue !== 0) { - fret = n.harmonicValue - n.beat.voice.bar.staff.transpositionPitch; - } } if (!n.isTieDestination) { diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts index 28d513072..59da5271a 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts @@ -306,7 +306,7 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase { const lineCount = this.renderer.bar.staff.standardNotationLineCount; let steps: number; - if (beat.restDisplayTone !== -1 && beat.restDisplayOctave !== -1) { + if (!Number.isNaN(beat.restDisplayTone) && !Number.isNaN(beat.restDisplayOctave)) { // Per-beat override: same step as a note at that pitch. SMuFL rest glyphs use the same // baseline convention as note heads, so no further adjustment is applied. steps = AccidentalHelper.calculateRestDisplaySteps(sr.bar, beat.restDisplayTone, beat.restDisplayOctave); diff --git a/packages/alphatab/test/PrettyFormat.ts b/packages/alphatab/test/PrettyFormat.ts index f19e8cd4d..36b9a9cca 100644 --- a/packages/alphatab/test/PrettyFormat.ts +++ b/packages/alphatab/test/PrettyFormat.ts @@ -873,7 +873,7 @@ export class ScoreSerializerPlugin implements PrettyFormatNewPlugin { isEqual = (v as string) === (dv as string); break; case 'number': - isEqual = (v as number) === (dv as number); + isEqual = (v as number) === (dv as number) || (Number.isNaN(v) && Number.isNaN(dv)); break; case 'bigint': isEqual = (v as bigint) === (dv as bigint); From 6ffb155304f4f7572669d3e3c5238b88c36494c9 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 16:43:47 +0200 Subject: [PATCH 10/12] feat: handle tablature display flags --- .../alphatab/src/exporter/AlphaTexExporter.ts | 20 ++- .../alphatab/src/importer/AlphaTexImporter.ts | 8 +- .../alphatab/src/importer/MusicXmlImporter.ts | 3 +- packages/alphatab/src/model/Staff.ts | 11 +- .../test/importer/AlphaTexLexer.test.ts | 1 + .../test/importer/Gp7Importer.test.ts | 48 ++++--- .../alphatab/test/importer/GpifParser.test.ts | 122 ------------------ .../__snapshots__/AlphaTexLexer.test.ts.snap | 6 + .../AlphaTexParameter.test.ts.snap | 6 +- .../__snapshots__/AlphaTexParser.test.ts.snap | 104 +++++++-------- .../test/model/PercussionTablature.test.ts | 32 ++--- 11 files changed, 132 insertions(+), 229 deletions(-) delete mode 100644 packages/alphatab/test/importer/GpifParser.test.ts diff --git a/packages/alphatab/src/exporter/AlphaTexExporter.ts b/packages/alphatab/src/exporter/AlphaTexExporter.ts index 8c68f190b..57d290dff 100644 --- a/packages/alphatab/src/exporter/AlphaTexExporter.ts +++ b/packages/alphatab/src/exporter/AlphaTexExporter.ts @@ -17,7 +17,9 @@ import { type AlphaTexScoreNode, type AlphaTexStringLiteral, type AlphaTexArgumentList, - type IAlphaTexAstNode + type IAlphaTexAstNode, + AlphaTexDotTokenNode, + AlphaTexAtTokenNode } from '@coderline/alphatab/importer/alphaTex/AlphaTexAst'; import type { IAlphaTexLanguageImportHandler } from '@coderline/alphatab/importer/alphaTex/IAlphaTexLanguageImportHandler'; import { IOHelper } from '@coderline/alphatab/io/IOHelper'; @@ -378,6 +380,9 @@ class AlphaTexPrinter { case AlphaTexNodeType.Dot: this._writer.write('.'); break; + case AlphaTexNodeType.At: + this._writer.write('@'); + break; case AlphaTexNodeType.Backslash: this._writer.write('\\'); break; @@ -658,6 +663,17 @@ export class AlphaTexExporter extends ScoreExporter { nodeType: AlphaTexNodeType.String, text: PercussionMapper.getArticulationName(data) } as AlphaTexStringLiteral; + + if (!Number.isNaN(data.string)) { + note.noteStringSeparator = { + nodeType: AlphaTexNodeType.At + } as AlphaTexAtTokenNode; + const stringNumber = data.beat.voice.bar.staff.tuning.length - data.string + 1; + note.noteString = { + nodeType: AlphaTexNodeType.Number, + value: stringNumber + }; + } } else if (data.isPiano) { note.noteValue = { nodeType: AlphaTexNodeType.Ident, @@ -670,7 +686,7 @@ export class AlphaTexExporter extends ScoreExporter { } as AlphaTexNumberLiteral; note.noteStringSeparator = { nodeType: AlphaTexNodeType.Dot - }; + } as AlphaTexDotTokenNode; const stringNumber = data.beat.voice.bar.staff.tuning.length - data.string + 1; note.noteString = { nodeType: AlphaTexNodeType.Number, diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index dc5c4db1d..7d0da85e1 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -770,7 +770,8 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter switch (staffNoteKind) { case AlphaTexStaffNoteKind.Pitched: staff.isPercussion = false; - staff.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; + staff.showTablature = false; + staff.stringTuning.reset(); if (!this._state.staffHasExplicitDisplayTransposition.has(staff)) { staff.displayTranspositionPitch = 0; } @@ -783,6 +784,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter case AlphaTexStaffNoteKind.Articulation: staff.isPercussion = true; staff.stringTuning.reset(); + staff.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; if (!this._state.staffHasExplicitDisplayTransposition.has(staff)) { staff.displayTranspositionPitch = 0; } @@ -841,7 +843,9 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter // reset to defaults staff.stringTuning.reset(); - if (program === 15) { + if (staff.isPercussion) { + staff.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; + } else if (program === 15) { // dulcimer E4 B3 G3 D3 A2 E2 staff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; } else if (program >= 24 && program <= 31) { diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 4742ccce4..97ae26c03 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -1903,8 +1903,9 @@ export class MusicXmlImporter extends ScoreImporter { break; case 'percussion': bar.clef = Clef.Neutral; - if(bar.index === 0){ + if (bar.index === 0) { bar.staff.isPercussion = true; + bar.staff.showTablature = false; } break; case 'tab': diff --git a/packages/alphatab/src/model/Staff.ts b/packages/alphatab/src/model/Staff.ts index 9b5bfd43a..f016c5670 100644 --- a/packages/alphatab/src/model/Staff.ts +++ b/packages/alphatab/src/model/Staff.ts @@ -137,28 +137,29 @@ export class Staff { */ public standardNotationLineCount: number = Staff.DefaultStandardNotationLineCount; - private _filledVoices:Set = new Set([0]); + private _filledVoices: Set = new Set([0]); /** * The indexes of the non-empty voices in this staff.. * @json_ignore */ - public get filledVoices():Set { + public get filledVoices(): Set { return this._filledVoices; } public finish(settings: Settings, sharedDataBag: Map | null = null): void { + this.stringTuning.finish(); if (this.isPercussion) { this.displayTranspositionPitch = 0; + this.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; } - this.stringTuning.finish(); - if(this.stringTuning.tunings.length === 0){ + if (this.stringTuning.tunings.length === 0) { this.showTablature = false; } for (let i: number = 0, j: number = this.bars.length; i < j; i++) { this.bars[i].finish(settings, sharedDataBag); - for(const v of this.bars[i].filledVoices) { + for (const v of this.bars[i].filledVoices) { this._filledVoices.add(v); } } diff --git a/packages/alphatab/test/importer/AlphaTexLexer.test.ts b/packages/alphatab/test/importer/AlphaTexLexer.test.ts index ace725e35..2bdd4dde7 100644 --- a/packages/alphatab/test/importer/AlphaTexLexer.test.ts +++ b/packages/alphatab/test/importer/AlphaTexLexer.test.ts @@ -73,6 +73,7 @@ describe('AlphaTexLexerTest', () => { lexerTest(`}`); lexerTest(`|`); lexerTest(`*`); + lexerTest(`@`); }); it('meta-command', () => { diff --git a/packages/alphatab/test/importer/Gp7Importer.test.ts b/packages/alphatab/test/importer/Gp7Importer.test.ts index 8654535f3..c8fbeff80 100644 --- a/packages/alphatab/test/importer/Gp7Importer.test.ts +++ b/packages/alphatab/test/importer/Gp7Importer.test.ts @@ -19,6 +19,7 @@ import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper'; import { TestPlatform } from 'test/TestPlatform'; import { AutomationType } from '@coderline/alphatab/model/Automation'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; describe('Gp7ImporterTest', () => { async function prepareImporterWithFile(name: string): Promise { @@ -974,21 +975,15 @@ describe('Gp7ImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].preferredBeamDirection).toBe(BeamDirection.Up); // break - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].beamingMode).toBe( - BeatBeamingMode.ForceSplitToNext - ); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].beamingMode).toBe(BeatBeamingMode.ForceSplitToNext); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].invertBeamDirection).toBe(false); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].preferredBeamDirection).toBe(BeamDirection.Up); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].beamingMode).toBe( - BeatBeamingMode.ForceSplitToNext - ); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].beamingMode).toBe(BeatBeamingMode.ForceSplitToNext); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].invertBeamDirection).toBe(false); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].preferredBeamDirection).toBe(BeamDirection.Up); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].beamingMode).toBe( - BeatBeamingMode.ForceSplitToNext - ); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].beamingMode).toBe(BeatBeamingMode.ForceSplitToNext); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].invertBeamDirection).toBe(false); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].preferredBeamDirection).toBe(BeamDirection.Up); @@ -1014,9 +1009,7 @@ describe('Gp7ImporterTest', () => { // invert to down expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].beamingMode).toBe(BeatBeamingMode.Auto); expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].invertBeamDirection).toBe(false); - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).toBe( - BeamDirection.Down - ); + expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).toBe(BeamDirection.Down); // invert to up expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].beamingMode).toBe(BeatBeamingMode.Auto); @@ -1029,21 +1022,24 @@ describe('Gp7ImporterTest', () => { const score: Score = reader.readScore(); const staff = score.tracks[0].staves[0]; - expect(staff.isPercussion).to.be.true; + expect(staff.isPercussion).toBe(true); expect(staff.tuning.length).to.equal(6); - expect(staff.tuning.every((t: number) => t === 0)).to.be.true; + expect(staff.tuning.every((t: number) => t === 0)).toBe(true); const beats = staff.bars[0].voices[0].beats; - expect(beats.length).to.equal(4); + expect(beats.length).to.equal(16); + const articulationIds = [29, 30, 31, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]; + const articulationStrings = [1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 2, 5, 2, 5, 2]; + let noteIndex = 0; 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.isPercussion).toBe(true); + expect(note.isStringed).to.be.false; 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); + expect(note.string).toBe(articulationStrings[noteIndex]); + expect(PercussionMapper.getArticulation(note)!.id).toBe(articulationIds[noteIndex]); + noteIndex++; } } }); @@ -1053,20 +1049,20 @@ describe('Gp7ImporterTest', () => { 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.isPercussion).toBe(true); + expect(staff.showTablature).toBe(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(PercussionMapper.getArticulation(beats[0].notes[0])!.id).toBe(36); expect(beats[1].notes[0].string).to.equal(4); - expect(beats[1].notes[0].fret).to.equal(36); + expect(PercussionMapper.getArticulation(beats[1].notes[0])!.id).toBe(36); expect(beats[2].notes[0].string).to.equal(3); - expect(beats[2].notes[0].fret).to.equal(36); + expect(PercussionMapper.getArticulation(beats[2].notes[0])!.id).toBe(36); expect(beats[3].notes[0].string).to.equal(2); - expect(beats[3].notes[0].fret).to.equal(36); + expect(PercussionMapper.getArticulation(beats[3].notes[0])!.id).toBe(36); }); }); diff --git a/packages/alphatab/test/importer/GpifParser.test.ts b/packages/alphatab/test/importer/GpifParser.test.ts deleted file mode 100644 index 5814d7242..000000000 --- a/packages/alphatab/test/importer/GpifParser.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -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/importer/__snapshots__/AlphaTexLexer.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexLexer.test.ts.snap index b4f8b8d62..f08bf1d9d 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexLexer.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexLexer.test.ts.snap @@ -48,6 +48,12 @@ exports[`AlphaTexLexerTest > basic-tokens 8`] = ` ] `; +exports[`AlphaTexLexerTest > basic-tokens 9`] = ` +[ + At (1,1) -> (1,2), +] +`; + exports[`AlphaTexLexerTest > errors > at001 1`] = `[]`; exports[`AlphaTexLexerTest > errors > at001 2`] = ` diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap index 395d10e4a..228823b3d 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParameter.test.ts.snap @@ -2250,7 +2250,7 @@ Score (1,1) -> (1,10) { notes: [ Note (1,7) -> (1,10) { noteValue: Number "1" (1,7) -> (1,8), - noteStringDot: Dot (1,8) -> (1,9), + noteStringSeparator: Dot (1,8) -> (1,9), noteString: Number "1" (1,9) -> (1,10), }, ], @@ -2600,7 +2600,7 @@ Score (1,1) -> (1,10) { notes: [ Note (1,7) -> (1,10) { noteValue: Number "3" (1,7) -> (1,8), - noteStringDot: Dot (1,8) -> (1,9), + noteStringSeparator: Dot (1,8) -> (1,9), noteString: Number "3" (1,9) -> (1,10), }, ], @@ -3382,7 +3382,7 @@ Score (1,1) -> (1,10) { notes: [ Note (1,7) -> (1,10) { noteValue: Number "3" (1,7) -> (1,8), - noteStringDot: Dot (1,8) -> (1,9), + noteStringSeparator: Dot (1,8) -> (1,9), noteString: Number "3" (1,9) -> (1,10), }, ], diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap index cc17a13f8..9fda0aa90 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap @@ -37,7 +37,7 @@ Score (1,1) -> (1,19) { notes: [ Note (1,12) -> (1,15) { noteValue: Number "3" (1,12) -> (1,13), - noteStringDot: Dot (1,13) -> (1,14), + noteStringSeparator: Dot (1,13) -> (1,14), noteString: Number "3" (1,14) -> (1,15), }, ], @@ -48,7 +48,7 @@ Score (1,1) -> (1,19) { notes: [ Note (1,16) -> (1,19) { noteValue: Number "3" (1,16) -> (1,17), - noteStringDot: Dot (1,17) -> (1,18), + noteStringSeparator: Dot (1,17) -> (1,18), noteString: Number "4" (1,18) -> (1,19), }, ], @@ -662,7 +662,7 @@ Score (1,1) -> (1,8) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, ], @@ -704,7 +704,7 @@ Score (1,1) -> (1,10) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, ], @@ -3623,7 +3623,7 @@ Score (1,1) -> (1,8) { notes: [ Note (1,1) -> (1,4) { noteValue: Number "3" (1,1) -> (1,2), - noteStringDot: Dot (1,2) -> (1,3), + noteStringSeparator: Dot (1,2) -> (1,3), noteString: Number "3" (1,3) -> (1,4), }, ], @@ -3634,7 +3634,7 @@ Score (1,1) -> (1,8) { notes: [ Note (1,5) -> (1,8) { noteValue: Number "4" (1,5) -> (1,6), - noteStringDot: Dot (1,6) -> (1,7), + noteStringSeparator: Dot (1,6) -> (1,7), noteString: Number "2" (1,7) -> (1,8), }, ], @@ -3664,7 +3664,7 @@ Score (1,1) -> (1,26) { notes: [ Note (1,4) -> (1,7) { noteValue: Number "3" (1,4) -> (1,5), - noteStringDot: Dot (1,5) -> (1,6), + noteStringSeparator: Dot (1,5) -> (1,6), noteString: Number "3" (1,6) -> (1,7), }, ], @@ -3725,7 +3725,7 @@ Score (1,1) -> (1,12) { notes: [ Note (1,1) -> (1,4) { noteValue: Number "3" (1,1) -> (1,2), - noteStringDot: Dot (1,2) -> (1,3), + noteStringSeparator: Dot (1,2) -> (1,3), noteString: Number "3" (1,3) -> (1,4), }, ], @@ -3738,7 +3738,7 @@ Score (1,1) -> (1,12) { notes: [ Note (1,7) -> (1,10) { noteValue: Number "4" (1,7) -> (1,8), - noteStringDot: Dot (1,8) -> (1,9), + noteStringSeparator: Dot (1,8) -> (1,9), noteString: Number "2" (1,9) -> (1,10), }, ], @@ -3770,7 +3770,7 @@ Score (1,1) -> (1,14) { notes: [ Note (1,4) -> (1,7) { noteValue: Number "3" (1,4) -> (1,5), - noteStringDot: Dot (1,5) -> (1,6), + noteStringSeparator: Dot (1,5) -> (1,6), noteString: Number "3" (1,6) -> (1,7), }, ], @@ -3785,7 +3785,7 @@ Score (1,1) -> (1,14) { notes: [ Note (1,11) -> (1,14) { noteValue: Number "4" (1,11) -> (1,12), - noteStringDot: Dot (1,12) -> (1,13), + noteStringSeparator: Dot (1,12) -> (1,13), noteString: Number "2" (1,13) -> (1,14), }, ], @@ -3811,7 +3811,7 @@ Score (1,1) -> (1,18) { notes: [ Note (1,1) -> (1,4) { noteValue: Number "3" (1,1) -> (1,2), - noteStringDot: Dot (1,2) -> (1,3), + noteStringSeparator: Dot (1,2) -> (1,3), noteString: Number "3" (1,3) -> (1,4), }, ], @@ -3829,7 +3829,7 @@ Score (1,1) -> (1,18) { notes: [ Note (1,10) -> (1,13) { noteValue: Number "4" (1,10) -> (1,11), - noteStringDot: Dot (1,11) -> (1,12), + noteStringSeparator: Dot (1,11) -> (1,12), noteString: Number "2" (1,12) -> (1,13), }, ], @@ -3862,7 +3862,7 @@ Score (1,1) -> (1,31) { notes: [ Note (1,1) -> (1,4) { noteValue: Number "3" (1,1) -> (1,2), - noteStringDot: Dot (1,2) -> (1,3), + noteStringSeparator: Dot (1,2) -> (1,3), noteString: Number "3" (1,3) -> (1,4), }, ], @@ -3887,7 +3887,7 @@ Score (1,1) -> (1,31) { notes: [ Note (1,13) -> (1,16) { noteValue: Number "4" (1,13) -> (1,14), - noteStringDot: Dot (1,14) -> (1,15), + noteStringSeparator: Dot (1,14) -> (1,15), noteString: Number "2" (1,15) -> (1,16), }, ], @@ -3946,7 +3946,7 @@ Score (1,1) -> (1,12) { notes: [ Note (1,1) -> (1,4) { noteValue: Number "3" (1,1) -> (1,2), - noteStringDot: Dot (1,2) -> (1,3), + noteStringSeparator: Dot (1,2) -> (1,3), noteString: Number "3" (1,3) -> (1,4), }, ], @@ -3959,7 +3959,7 @@ Score (1,1) -> (1,12) { notes: [ Note (1,7) -> (1,10) { noteValue: Number "4" (1,7) -> (1,8), - noteStringDot: Dot (1,8) -> (1,9), + noteStringSeparator: Dot (1,8) -> (1,9), noteString: Number "2" (1,9) -> (1,10), }, ], @@ -3987,7 +3987,7 @@ Score (1,1) -> (1,37) { notes: [ Note (1,1) -> (1,4) { noteValue: Number "3" (1,1) -> (1,2), - noteStringDot: Dot (1,2) -> (1,3), + noteStringSeparator: Dot (1,2) -> (1,3), noteString: Number "3" (1,3) -> (1,4), }, ], @@ -4005,7 +4005,7 @@ Score (1,1) -> (1,37) { notes: [ Note (1,9) -> (1,14) { noteValue: Number "3" (1,9) -> (1,10), - noteStringDot: Dot (1,11) -> (1,12), + noteStringSeparator: Dot (1,11) -> (1,12), noteString: Number "3" (1,13) -> (1,14), }, ], @@ -4023,7 +4023,7 @@ Score (1,1) -> (1,37) { notes: [ Note (1,21) -> (1,24) { noteValue: Number "3" (1,21) -> (1,22), - noteStringDot: Dot (1,22) -> (1,23), + noteStringSeparator: Dot (1,22) -> (1,23), noteString: Number "3" (1,23) -> (1,24), }, ], @@ -4041,7 +4041,7 @@ Score (1,1) -> (1,37) { notes: [ Note (1,30) -> (1,35) { noteValue: Number "3" (1,30) -> (1,31), - noteStringDot: Dot (1,32) -> (1,33), + noteStringSeparator: Dot (1,32) -> (1,33), noteString: Number "3" (1,34) -> (1,35), }, ], @@ -4408,12 +4408,12 @@ Score (1,1) -> (1,20) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, Note (1,6) -> (1,9) { noteValue: Number "4" (1,6) -> (1,7), - noteStringDot: Dot (1,7) -> (1,8), + noteStringSeparator: Dot (1,7) -> (1,8), noteString: Number "2" (1,8) -> (1,9), }, ], @@ -4426,12 +4426,12 @@ Score (1,1) -> (1,20) { notes: [ Note (1,12) -> (1,15) { noteValue: Number "1" (1,12) -> (1,13), - noteStringDot: Dot (1,13) -> (1,14), + noteStringSeparator: Dot (1,13) -> (1,14), noteString: Number "2" (1,14) -> (1,15), }, Note (1,16) -> (1,19) { noteValue: Number "6" (1,16) -> (1,17), - noteStringDot: Dot (1,17) -> (1,18), + noteStringSeparator: Dot (1,17) -> (1,18), noteString: Number "1" (1,18) -> (1,19), }, ], @@ -4463,12 +4463,12 @@ Score (1,1) -> (1,32) { notes: [ Note (1,5) -> (1,8) { noteValue: Number "3" (1,5) -> (1,6), - noteStringDot: Dot (1,6) -> (1,7), + noteStringSeparator: Dot (1,6) -> (1,7), noteString: Number "3" (1,7) -> (1,8), }, Note (1,9) -> (1,12) { noteValue: Number "4" (1,9) -> (1,10), - noteStringDot: Dot (1,10) -> (1,11), + noteStringSeparator: Dot (1,10) -> (1,11), noteString: Number "2" (1,11) -> (1,12), }, ], @@ -4531,12 +4531,12 @@ Score (1,1) -> (1,24) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, Note (1,6) -> (1,9) { noteValue: Number "4" (1,6) -> (1,7), - noteStringDot: Dot (1,7) -> (1,8), + noteStringSeparator: Dot (1,7) -> (1,8), noteString: Number "2" (1,8) -> (1,9), }, ], @@ -4551,12 +4551,12 @@ Score (1,1) -> (1,24) { notes: [ Note (1,14) -> (1,17) { noteValue: Number "1" (1,14) -> (1,15), - noteStringDot: Dot (1,15) -> (1,16), + noteStringSeparator: Dot (1,15) -> (1,16), noteString: Number "2" (1,16) -> (1,17), }, Note (1,18) -> (1,21) { noteValue: Number "6" (1,18) -> (1,19), - noteStringDot: Dot (1,19) -> (1,20), + noteStringSeparator: Dot (1,19) -> (1,20), noteString: Number "1" (1,20) -> (1,21), }, ], @@ -4590,12 +4590,12 @@ Score (1,1) -> (1,26) { notes: [ Note (1,5) -> (1,8) { noteValue: Number "3" (1,5) -> (1,6), - noteStringDot: Dot (1,6) -> (1,7), + noteStringSeparator: Dot (1,6) -> (1,7), noteString: Number "3" (1,7) -> (1,8), }, Note (1,9) -> (1,12) { noteValue: Number "4" (1,9) -> (1,10), - noteStringDot: Dot (1,10) -> (1,11), + noteStringSeparator: Dot (1,10) -> (1,11), noteString: Number "2" (1,11) -> (1,12), }, ], @@ -4612,12 +4612,12 @@ Score (1,1) -> (1,26) { notes: [ Note (1,18) -> (1,21) { noteValue: Number "1" (1,18) -> (1,19), - noteStringDot: Dot (1,19) -> (1,20), + noteStringSeparator: Dot (1,19) -> (1,20), noteString: Number "2" (1,20) -> (1,21), }, Note (1,22) -> (1,25) { noteValue: Number "6" (1,22) -> (1,23), - noteStringDot: Dot (1,23) -> (1,24), + noteStringSeparator: Dot (1,23) -> (1,24), noteString: Number "1" (1,24) -> (1,25), }, ], @@ -4645,12 +4645,12 @@ Score (1,1) -> (1,30) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, Note (1,6) -> (1,9) { noteValue: Number "4" (1,6) -> (1,7), - noteStringDot: Dot (1,7) -> (1,8), + noteStringSeparator: Dot (1,7) -> (1,8), noteString: Number "2" (1,8) -> (1,9), }, ], @@ -4670,12 +4670,12 @@ Score (1,1) -> (1,30) { notes: [ Note (1,17) -> (1,20) { noteValue: Number "1" (1,17) -> (1,18), - noteStringDot: Dot (1,18) -> (1,19), + noteStringSeparator: Dot (1,18) -> (1,19), noteString: Number "2" (1,19) -> (1,20), }, Note (1,21) -> (1,24) { noteValue: Number "6" (1,21) -> (1,22), - noteStringDot: Dot (1,22) -> (1,23), + noteStringSeparator: Dot (1,22) -> (1,23), noteString: Number "1" (1,23) -> (1,24), }, ], @@ -4710,12 +4710,12 @@ Score (1,1) -> (1,43) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, Note (1,6) -> (1,9) { noteValue: Number "4" (1,6) -> (1,7), - noteStringDot: Dot (1,7) -> (1,8), + noteStringSeparator: Dot (1,7) -> (1,8), noteString: Number "2" (1,8) -> (1,9), }, ], @@ -4742,12 +4742,12 @@ Score (1,1) -> (1,43) { notes: [ Note (1,20) -> (1,23) { noteValue: Number "1" (1,20) -> (1,21), - noteStringDot: Dot (1,21) -> (1,22), + noteStringSeparator: Dot (1,21) -> (1,22), noteString: Number "2" (1,22) -> (1,23), }, Note (1,24) -> (1,27) { noteValue: Number "6" (1,24) -> (1,25), - noteStringDot: Dot (1,25) -> (1,26), + noteStringSeparator: Dot (1,25) -> (1,26), noteString: Number "1" (1,26) -> (1,27), }, ], @@ -4808,12 +4808,12 @@ Score (1,1) -> (1,24) { notes: [ Note (1,2) -> (1,5) { noteValue: Number "3" (1,2) -> (1,3), - noteStringDot: Dot (1,3) -> (1,4), + noteStringSeparator: Dot (1,3) -> (1,4), noteString: Number "3" (1,4) -> (1,5), }, Note (1,6) -> (1,9) { noteValue: Number "4" (1,6) -> (1,7), - noteStringDot: Dot (1,7) -> (1,8), + noteStringSeparator: Dot (1,7) -> (1,8), noteString: Number "2" (1,8) -> (1,9), }, ], @@ -4828,12 +4828,12 @@ Score (1,1) -> (1,24) { notes: [ Note (1,14) -> (1,17) { noteValue: Number "1" (1,14) -> (1,15), - noteStringDot: Dot (1,15) -> (1,16), + noteStringSeparator: Dot (1,15) -> (1,16), noteString: Number "2" (1,16) -> (1,17), }, Note (1,18) -> (1,21) { noteValue: Number "6" (1,18) -> (1,19), - noteStringDot: Dot (1,19) -> (1,20), + noteStringSeparator: Dot (1,19) -> (1,20), noteString: Number "1" (1,20) -> (1,21), }, ], @@ -4863,12 +4863,12 @@ Score (1,1) -> (5,18) { notes: [ Note (3,17) -> (3,20) { noteValue: Number "3" (3,17) -> (3,18), - noteStringDot: Dot (3,18) -> (3,19), + noteStringSeparator: Dot (3,18) -> (3,19), noteString: Number "3" (3,19) -> (3,20), }, Note (4,17) -> (4,22) { noteValue: Number "3" (4,17) -> (4,18), - noteStringDot: Dot (4,19) -> (4,20), + noteStringSeparator: Dot (4,19) -> (4,20), noteString: Number "3" (4,21) -> (4,22), }, ], @@ -6331,12 +6331,12 @@ Score (1,1) -> (1,28) { notes: [ Note (1,18) -> (1,21) { noteValue: Number "3" (1,18) -> (1,19), - noteStringDot: Dot (1,19) -> (1,20), + noteStringSeparator: Dot (1,19) -> (1,20), noteString: Number "3" (1,20) -> (1,21), }, Note (1,22) -> (1,25) { noteValue: Number "3" (1,22) -> (1,23), - noteStringDot: Dot (1,23) -> (1,24), + noteStringSeparator: Dot (1,23) -> (1,24), noteString: Number "3" (1,24) -> (1,25), }, ], @@ -6513,7 +6513,7 @@ Score (1,1) -> (1,21) { notes: [ Note (1,4) -> (1,7) { noteValue: Number "3" (1,4) -> (1,5), - noteStringDot: Dot (1,5) -> (1,6), + noteStringSeparator: Dot (1,5) -> (1,6), noteString: Number "3" (1,6) -> (1,7), }, ], diff --git a/packages/alphatab/test/model/PercussionTablature.test.ts b/packages/alphatab/test/model/PercussionTablature.test.ts index 6fdf96f18..7445f0f45 100644 --- a/packages/alphatab/test/model/PercussionTablature.test.ts +++ b/packages/alphatab/test/model/PercussionTablature.test.ts @@ -4,7 +4,7 @@ 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'; +import { describe, expect, it } from 'vitest'; describe('PercussionTablature', () => { describe('Note.isPercussion', () => { @@ -14,15 +14,15 @@ describe('PercussionTablature', () => { note.string = 6; note.fret = 36; - expect(note.isPercussion).to.be.true; - expect(note.isStringed).to.be.true; + expect(note.isPercussion).toBe(true); + expect(note.isStringed).toBe(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.isPercussion).toBe(true); expect(note.isStringed).to.be.false; }); @@ -32,7 +32,7 @@ describe('PercussionTablature', () => { note.fret = 5; expect(note.isPercussion).to.be.false; - expect(note.isStringed).to.be.true; + expect(note.isStringed).toBe(true); }); }); @@ -45,12 +45,12 @@ describe('PercussionTablature', () => { staff.finish(new Settings()); - expect(staff.showTablature).to.be.true; - expect(staff.tuning.length).to.equal(6); - expect(staff.displayTranspositionPitch).to.equal(0); + expect(staff.showTablature).toBe(true); + expect(staff.tuning.length).toBe(6); + expect(staff.displayTranspositionPitch).toBe(0); }); - it('disables showTablature for percussion without tuning', () => { + it('preserves showTablature for percussion without tuning', () => { const staff = new Staff(); staff.isPercussion = true; staff.showTablature = true; @@ -58,8 +58,8 @@ describe('PercussionTablature', () => { staff.finish(new Settings()); - expect(staff.showTablature).to.be.false; - expect(staff.tuning.length).to.equal(0); + expect(staff.showTablature).toBe(true); + expect(staff.tuning.length).toBe(6); }); it('resets displayTranspositionPitch for percussion', () => { @@ -70,7 +70,7 @@ describe('PercussionTablature', () => { staff.finish(new Settings()); - expect(staff.displayTranspositionPitch).to.equal(0); + expect(staff.displayTranspositionPitch).toBe(0); }); it('preserves showTablature for non-percussion with tuning', () => { @@ -81,8 +81,8 @@ describe('PercussionTablature', () => { staff.finish(new Settings()); - expect(staff.showTablature).to.be.true; - expect(staff.tuning.length).to.equal(6); + expect(staff.showTablature).toBe(true); + expect(staff.tuning.length).toBe(6); }); }); @@ -102,7 +102,7 @@ describe('PercussionTablature', () => { const factory = new TabBarRendererFactory([]); const [track, staff] = createStaff(true, true, [0, 0, 0, 0, 0, 0]); - expect(factory.canCreate(track, staff)).to.be.true; + expect(factory.canCreate(track, staff)).toBe(true); }); it('rejects percussion staff when showTablature is false', () => { @@ -123,7 +123,7 @@ describe('PercussionTablature', () => { const factory = new TabBarRendererFactory([]); const [track, staff] = createStaff(false, true, [64, 59, 55, 50, 45, 40]); - expect(factory.canCreate(track, staff)).to.be.true; + expect(factory.canCreate(track, staff)).toBe(true); }); }); }); From 0886cd46de429915e48e6743ecffd0171d5c0d6b Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 16:49:04 +0200 Subject: [PATCH 11/12] build: fix transpilation related errors --- .../alphatab/src/importer/AlphaTexImporter.ts | 12 +++++----- packages/alphatab/test/PrettyFormat.ts | 2 +- .../test/importer/Gp7Importer.test.ts | 22 +++++++++---------- .../test/model/PercussionTablature.test.ts | 8 +++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index 7d0da85e1..853b8cce1 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -668,8 +668,8 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter return; } - const noteString: number = node.noteString!.value; - if (noteString < 1 || noteString > this._state.currentStaff!.tuning.length) { + const frettedNoteString: number = node.noteString!.value; + if (frettedNoteString < 1 || frettedNoteString > this._state.currentStaff!.tuning.length) { this.addSemanticDiagnostic({ code: AlphaTexDiagnosticCode.AT208, message: `Note string is out of range. Available range: 1-${this._state.currentStaff!.tuning.length}`, @@ -680,7 +680,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter return; } - note.string = this._state.currentStaff!.tuning.length - (noteString - 1); + note.string = this._state.currentStaff!.tuning.length - (frettedNoteString - 1); if (!isTie) { note.fret = numericValue; } @@ -718,8 +718,8 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter note.percussionArticulation = articulationIndex; if (node.noteString) { - const noteString: number = node.noteString!.value; - if (noteString < 1 || noteString > this._state.currentStaff!.tuning.length) { + const percussionNoteString: number = node.noteString!.value; + if (percussionNoteString < 1 || percussionNoteString > this._state.currentStaff!.tuning.length) { this.addSemanticDiagnostic({ code: AlphaTexDiagnosticCode.AT208, message: `Note string is out of range. Available range: 1-${this._state.currentStaff!.tuning.length}`, @@ -729,7 +729,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter }); return; } - note.string = this._state.currentStaff!.tuning.length - (noteString - 1); + note.string = this._state.currentStaff!.tuning.length - (percussionNoteString - 1); } else { // find free string for (let i = 0; i < this._state.currentStaff!.tuning.length; i++) { diff --git a/packages/alphatab/test/PrettyFormat.ts b/packages/alphatab/test/PrettyFormat.ts index 36b9a9cca..aa84ce69f 100644 --- a/packages/alphatab/test/PrettyFormat.ts +++ b/packages/alphatab/test/PrettyFormat.ts @@ -873,7 +873,7 @@ export class ScoreSerializerPlugin implements PrettyFormatNewPlugin { isEqual = (v as string) === (dv as string); break; case 'number': - isEqual = (v as number) === (dv as number) || (Number.isNaN(v) && Number.isNaN(dv)); + isEqual = (v as number) === (dv as number) || (Number.isNaN(v as number) && Number.isNaN(dv as number)); break; case 'bigint': isEqual = (v as bigint) === (dv as bigint); diff --git a/packages/alphatab/test/importer/Gp7Importer.test.ts b/packages/alphatab/test/importer/Gp7Importer.test.ts index c8fbeff80..e7a551bbd 100644 --- a/packages/alphatab/test/importer/Gp7Importer.test.ts +++ b/packages/alphatab/test/importer/Gp7Importer.test.ts @@ -1023,11 +1023,11 @@ describe('Gp7ImporterTest', () => { const staff = score.tracks[0].staves[0]; expect(staff.isPercussion).toBe(true); - expect(staff.tuning.length).to.equal(6); - expect(staff.tuning.every((t: number) => t === 0)).toBe(true); + expect(staff.tuning.length).toBe(6); + expect(staff.tuning.some((t: number) => t !== 0)).toBe(false); const beats = staff.bars[0].voices[0].beats; - expect(beats.length).to.equal(16); + expect(beats.length).toBe(16); const articulationIds = [29, 30, 31, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]; const articulationStrings = [1, 1, 1, 1, 1, 1, 1, 4, 4, 1, 1, 2, 5, 2, 5, 2]; @@ -1035,8 +1035,8 @@ describe('Gp7ImporterTest', () => { for (const beat of beats) { for (const note of beat.notes) { expect(note.isPercussion).toBe(true); - expect(note.isStringed).to.be.false; - expect(note.string).to.be.greaterThanOrEqual(1); + expect(note.isStringed).toBe(false); + expect(note.string).toBeGreaterThanOrEqual(1); expect(note.string).toBe(articulationStrings[noteIndex]); expect(PercussionMapper.getArticulation(note)!.id).toBe(articulationIds[noteIndex]); noteIndex++; @@ -1051,18 +1051,18 @@ describe('Gp7ImporterTest', () => { const staff = score.tracks[0].staves[0]; expect(staff.isPercussion).toBe(true); expect(staff.showTablature).toBe(true); - expect(staff.tuning.length).to.equal(6); + expect(staff.tuning.length).toBe(6); const beats = staff.bars[0].voices[0].beats; - expect(beats.length).to.equal(4); + expect(beats.length).toBe(4); - expect(beats[0].notes[0].string).to.equal(5); + expect(beats[0].notes[0].string).toBe(5); expect(PercussionMapper.getArticulation(beats[0].notes[0])!.id).toBe(36); - expect(beats[1].notes[0].string).to.equal(4); + expect(beats[1].notes[0].string).toBe(4); expect(PercussionMapper.getArticulation(beats[1].notes[0])!.id).toBe(36); - expect(beats[2].notes[0].string).to.equal(3); + expect(beats[2].notes[0].string).toBe(3); expect(PercussionMapper.getArticulation(beats[2].notes[0])!.id).toBe(36); - expect(beats[3].notes[0].string).to.equal(2); + expect(beats[3].notes[0].string).toBe(2); expect(PercussionMapper.getArticulation(beats[3].notes[0])!.id).toBe(36); }); }); diff --git a/packages/alphatab/test/model/PercussionTablature.test.ts b/packages/alphatab/test/model/PercussionTablature.test.ts index 7445f0f45..98914d49c 100644 --- a/packages/alphatab/test/model/PercussionTablature.test.ts +++ b/packages/alphatab/test/model/PercussionTablature.test.ts @@ -23,7 +23,7 @@ describe('PercussionTablature', () => { note.percussionArticulation = 38; expect(note.isPercussion).toBe(true); - expect(note.isStringed).to.be.false; + expect(note.isStringed).toBe(false); }); it('returns false when percussionArticulation is not set', () => { @@ -31,7 +31,7 @@ describe('PercussionTablature', () => { note.string = 1; note.fret = 5; - expect(note.isPercussion).to.be.false; + expect(note.isPercussion).toBe(false); expect(note.isStringed).toBe(true); }); }); @@ -109,14 +109,14 @@ describe('PercussionTablature', () => { const factory = new TabBarRendererFactory([]); const [track, staff] = createStaff(true, false, [0, 0, 0, 0, 0, 0]); - expect(factory.canCreate(track, staff)).to.be.false; + expect(factory.canCreate(track, staff)).toBe(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; + expect(factory.canCreate(track, staff)).toBe(false); }); it('allows creation for regular guitar staff', () => { From b0e723ad1434c96501de33f4da9dfd334dbdaaf8 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Fri, 3 Jul 2026 16:58:55 +0200 Subject: [PATCH 12/12] build: add missing `some` to DoubleList --- .../src/main/java/alphaTab/collections/DoubleList.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt b/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt index 06466dfbf..7fd70ece2 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/collections/DoubleList.kt @@ -187,4 +187,13 @@ public class DoubleList : IDoubleIterable { for (element in _items) accumulator = operation(accumulator, element) return accumulator } + + public fun some(predicate: (Double) -> Boolean): Boolean { + for (el in this) { + if(predicate(el)) { + return true + } + } + return false + } }