Skip to content

Commit d04d3be

Browse files
authored
feat: new guitar pro instrument set writing (#2477)
1 parent 52bfad1 commit d04d3be

31 files changed

+2286
-1209
lines changed

packages/alphatab/src/ScrollHandlers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { AlphaTabApiBase } from '@coderline/alphatab/AlphaTabApiBase';
22
import type { MidiTickLookupFindBeatResultCursorMode } from '@coderline/alphatab/midi/MidiTickLookup';
33
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
44
import { ScrollMode } from '@coderline/alphatab/PlayerSettings';
5-
import type { BeatBounds, MasterBarBounds } from '@coderline/alphatab/rendering/_barrel';
5+
import type { BeatBounds } from '@coderline/alphatab/rendering/utils/BeatBounds';
6+
import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds';
67

78
/**
89
* Classes implementing this interface can handle the scroll logic
@@ -90,7 +91,7 @@ export abstract class BasicScrollHandler<TSettings> implements IScrollHandler {
9091

9192
public onBeatCursorUpdating(
9293
startBeat: BeatBounds,
93-
_endBeat: BeatBounds|undefined,
94+
_endBeat: BeatBounds | undefined,
9495
_cursorMode: MidiTickLookupFindBeatResultCursorMode,
9596
_actualBeatCursorStartX: number,
9697
_actualBeatCursorEndX: number,

packages/alphatab/src/exporter/GpifSoundMapper.ts

Lines changed: 630 additions & 0 deletions
Large diffs are not rendered by default.

packages/alphatab/src/exporter/GpifWriter.ts

Lines changed: 25 additions & 273 deletions
Large diffs are not rendered by default.

packages/alphatab/src/generated/model/InstrumentArticulationSerializer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class InstrumentArticulationSerializer {
2222
return null;
2323
}
2424
const o = new Map<string, unknown>();
25+
o.set("id", obj.id);
2526
o.set("elementtype", obj.elementType);
2627
o.set("staffline", obj.staffLine);
2728
o.set("noteheaddefault", obj.noteHeadDefault as number);
@@ -34,6 +35,9 @@ export class InstrumentArticulationSerializer {
3435
}
3536
public static setProperty(obj: InstrumentArticulation, property: string, v: unknown): boolean {
3637
switch (property) {
38+
case "id":
39+
obj.id = v! as number;
40+
return true;
3741
case "elementtype":
3842
obj.elementType = v! as string;
3943
return true;

packages/alphatab/src/importer/AlphaTexImporter.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,11 @@ class AlphaTexImportState implements IAlphaTexImporterState {
148148
public ignoredInitialStaff = false;
149149
public ignoredInitialTrack = false;
150150
public currentDuration = Duration.Quarter;
151-
public articulationValueToIndex = new Map<number, number>();
151+
public articulationUniqueIdToIndex = new Map<string, number>();
152152

153153
public hasAnyProperData = false;
154154

155-
public readonly percussionArticulationNames = new Map<string, number>();
155+
public readonly percussionArticulationNames = new Map<string, string>();
156156

157157
public readonly slurs = new Map<string, Note>();
158158
public readonly lyrics = new Map<number, Lyrics[]>();
@@ -321,7 +321,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter
321321
const staff = this._state.currentTrack.staves[0];
322322
staff.displayTranspositionPitch = 0;
323323
staff.stringTuning = Tuning.getDefaultTuningFor(6)!;
324-
this._state.articulationValueToIndex.clear();
324+
this._state.articulationUniqueIdToIndex.clear();
325325

326326
this._beginStaff(staff);
327327

@@ -543,6 +543,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter
543543
let isDead: boolean = false;
544544
let isTie: boolean = false;
545545
let numericValue: number = -1;
546+
let articulationValue: string = '';
546547
let octave: number = -1;
547548
let tone: number = -1;
548549
let accidentalMode = NoteAccidentalMode.Default;
@@ -590,13 +591,19 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter
590591
const percussionArticulationNames = this._state.percussionArticulationNames;
591592
if (staffNoteKind === undefined && percussionArticulationNames.size === 0) {
592593
for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) {
593-
percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue);
594-
percussionArticulationNames.set(ModelUtils.toArticulationId(defaultName), defaultValue);
594+
const articulation = PercussionMapper.getInstrumentArticulationByUniqueId(defaultValue);
595+
if (articulation) {
596+
percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue);
597+
percussionArticulationNames.set(
598+
ModelUtils.toArticulationId(defaultName),
599+
defaultValue
600+
);
601+
}
595602
}
596603
}
597604

598605
if (percussionArticulationNames.has(articulationName)) {
599-
numericValue = percussionArticulationNames.get(articulationName)!;
606+
articulationValue = percussionArticulationNames.get(articulationName)!;
600607
} else {
601608
this.addSemanticDiagnostic({
602609
code: AlphaTexDiagnosticCode.AT209,
@@ -606,7 +613,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter
606613
end: noteValue.end
607614
});
608615
// avoid double error
609-
numericValue = Array.from(PercussionMapper.instrumentArticulationNames.values())[0];
616+
articulationValue = Array.from(PercussionMapper.instrumentArticulationNames.values())[0];
610617
return;
611618
}
612619
}
@@ -681,11 +688,19 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter
681688
break;
682689
case AlphaTexStaffNoteKind.Articulation:
683690
let articulationIndex: number = 0;
684-
if (this._state.articulationValueToIndex.has(numericValue)) {
685-
articulationIndex = this._state.articulationValueToIndex.get(numericValue)!;
691+
692+
if (articulationValue.length === 0 && numericValue > 0) {
693+
const byId = PercussionMapper.getArticulationById(numericValue);
694+
if (byId) {
695+
articulationValue = byId.uniqueId;
696+
}
697+
}
698+
699+
if (this._state.articulationUniqueIdToIndex.has(articulationValue)) {
700+
articulationIndex = this._state.articulationUniqueIdToIndex.get(articulationValue)!;
686701
} else {
687702
articulationIndex = this._state.currentTrack!.percussionArticulations.length;
688-
const articulation = PercussionMapper.getArticulationByInputMidiNumber(numericValue);
703+
const articulation = PercussionMapper.getInstrumentArticulationByUniqueId(articulationValue);
689704
if (articulation === null) {
690705
this.addSemanticDiagnostic({
691706
code: AlphaTexDiagnosticCode.AT209,
@@ -698,7 +713,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter
698713
}
699714

700715
this._state.currentTrack!.percussionArticulations.push(articulation!);
701-
this._state.articulationValueToIndex.set(numericValue, articulationIndex);
716+
this._state.articulationUniqueIdToIndex.set(articulationValue, articulationIndex);
702717
}
703718
note.percussionArticulation = articulationIndex;
704719
break;

packages/alphatab/src/importer/GpifParser.ts

Lines changed: 68 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,27 @@ import { Voice } from '@coderline/alphatab/model/Voice';
3535
import type { Settings } from '@coderline/alphatab/Settings';
3636
import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument';
3737

38-
import { type XmlNode, XmlNodeType } from '@coderline/alphatab/xml/XmlNode';
39-
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
40-
import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection';
41-
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
42-
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
43-
import { InstrumentArticulation, TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation';
44-
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
4538
import { BeatCloner } from '@coderline/alphatab/generated/model/BeatCloner';
4639
import { NoteCloner } from '@coderline/alphatab/generated/model/NoteCloner';
4740
import { Logger } from '@coderline/alphatab/Logger';
48-
import { GolpeType } from '@coderline/alphatab/model/GolpeType';
49-
import { FadeType } from '@coderline/alphatab/model/FadeType';
50-
import { WahPedal } from '@coderline/alphatab/model/WahPedal';
41+
import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils';
42+
import { BackingTrack } from '@coderline/alphatab/model/BackingTrack';
5143
import { BarreShape } from '@coderline/alphatab/model/BarreShape';
52-
import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament';
53-
import { Rasgueado } from '@coderline/alphatab/model/Rasgueado';
5444
import { Direction } from '@coderline/alphatab/model/Direction';
45+
import { FadeType } from '@coderline/alphatab/model/FadeType';
46+
import { GolpeType } from '@coderline/alphatab/model/GolpeType';
47+
import { InstrumentArticulation, TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentArticulation';
5548
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
56-
import { BackingTrack } from '@coderline/alphatab/model/BackingTrack';
49+
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
50+
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
51+
import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament';
52+
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
53+
import { Rasgueado } from '@coderline/alphatab/model/Rasgueado';
5754
import { Tuning } from '@coderline/alphatab/model/Tuning';
5855
import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect';
56+
import { WahPedal } from '@coderline/alphatab/model/WahPedal';
57+
import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection';
58+
import { type XmlNode, XmlNodeType } from '@coderline/alphatab/xml/XmlNode';
5959

6060
/**
6161
* This structure represents a duration within a gpif
@@ -705,7 +705,7 @@ export class GpifParser {
705705
}
706706
break;
707707
case 'Elements':
708-
this._parseElements(track, c);
708+
this._parseElements(track, c, false);
709709
break;
710710
}
711711
}
@@ -722,7 +722,7 @@ export class GpifParser {
722722
}
723723
break;
724724
case 'Elements':
725-
this._parseElements(track, c);
725+
this._parseElements(track, c, true);
726726
break;
727727
case 'LineCount':
728728
const lineCount = GpifParser._parseIntSafe(c.innerText, 5);
@@ -733,80 +733,74 @@ export class GpifParser {
733733
}
734734
}
735735
}
736-
private _parseElements(track: Track, node: XmlNode) {
736+
private _parseElements(track: Track, node: XmlNode, isInstrumentSet: boolean) {
737737
for (const c of node.childElements()) {
738738
switch (c.localName) {
739739
case 'Element':
740-
this._parseElement(track, c);
740+
this._parseElement(track, c, isInstrumentSet);
741741
break;
742742
}
743743
}
744744
}
745745

746-
private _parseElement(track: Track, node: XmlNode) {
747-
const type = node.findChildElement('Type')?.innerText ?? '';
746+
private _parseElement(track: Track, node: XmlNode, isInstrumentSet: boolean) {
747+
const name = node.findChildElement('Name')?.innerText ?? '';
748+
748749
for (const c of node.childElements()) {
749750
switch (c.localName) {
750751
case 'Name':
751752
case 'Articulations':
752-
this._parseArticulations(track, c, type);
753+
this._parseArticulations(track, c, isInstrumentSet, name);
753754
break;
754755
}
755756
}
756757
}
757-
private _parseArticulations(track: Track, node: XmlNode, elementType: string) {
758+
private _parseArticulations(track: Track, node: XmlNode, isInstrumentSet: boolean, elementName: string) {
758759
for (const c of node.childElements()) {
759760
switch (c.localName) {
760761
case 'Articulation':
761-
this._parseArticulation(track, c, elementType);
762+
this._parseArticulation(track, c, isInstrumentSet, elementName);
762763
break;
763764
}
764765
}
765766
}
766767

767-
private _parseArticulation(track: Track, node: XmlNode, elementType: string) {
768+
private _parseArticulation(track: Track, node: XmlNode, isInstrumentSet: boolean, elementName: string) {
768769
const articulation = new InstrumentArticulation();
769770
articulation.outputMidiNumber = -1;
770-
articulation.elementType = elementType;
771+
// NOTE: in the past we used the type here, but it is not unique enough. e.g. there are multiple kinds of "ride" ('Ride' vs 'Ride Cymbal 2')
772+
// we have to use the name as element identifier
773+
// using a wrong type leads to wrong "NotationPatch" updates
774+
articulation.elementType = elementName;
771775
let name = '';
772776
for (const c of node.childElements()) {
773777
const txt = c.innerText;
774778
switch (c.localName) {
775779
case 'Name':
776780
name = c.innerText;
777781
break;
782+
case 'InputMidiNumbers':
783+
articulation.id = GpifParser._parseIntSafe(txt.split(' ')[0], 0);
784+
break;
778785
case 'OutputMidiNumber':
779786
articulation.outputMidiNumber = GpifParser._parseIntSafe(txt, 0);
780787
break;
781788
case 'TechniqueSymbol':
782-
articulation.techniqueSymbol = this._parseTechniqueSymbol(txt);
789+
articulation.techniqueSymbol = GpifParser.parseTechniqueSymbol(txt);
783790
break;
784791
case 'TechniquePlacement':
785-
switch (txt) {
786-
case 'outside':
787-
articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Outside;
788-
break;
789-
case 'inside':
790-
articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Inside;
791-
break;
792-
case 'above':
793-
articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Above;
794-
break;
795-
case 'below':
796-
articulation.techniqueSymbolPlacement = TechniqueSymbolPlacement.Below;
797-
break;
798-
}
792+
articulation.techniqueSymbolPlacement = GpifParser.parseTechniqueSymbolPlacement(txt);
799793
break;
800794
case 'Noteheads':
801795
const noteHeadsTxt = GpifParser._splitSafe(txt);
802796
if (noteHeadsTxt.length >= 1) {
803-
articulation.noteHeadDefault = this._parseNoteHead(noteHeadsTxt[0]);
797+
articulation.noteHeadDefault = GpifParser.parseNoteHead(noteHeadsTxt[0]);
804798
}
805799
if (noteHeadsTxt.length >= 2) {
806-
articulation.noteHeadHalf = this._parseNoteHead(noteHeadsTxt[1]);
800+
articulation.noteHeadHalf = GpifParser.parseNoteHead(noteHeadsTxt[1]);
807801
}
808802
if (noteHeadsTxt.length >= 3) {
809-
articulation.noteHeadWhole = this._parseNoteHead(noteHeadsTxt[2]);
803+
articulation.noteHeadWhole = GpifParser.parseNoteHead(noteHeadsTxt[2]);
810804
}
811805

812806
if (articulation.noteHeadHalf === MusicFontSymbol.None) {
@@ -824,17 +818,20 @@ export class GpifParser {
824818
}
825819
}
826820

827-
if (articulation.outputMidiNumber !== -1) {
821+
const fullName = `${elementName}.${name}`;
822+
if (isInstrumentSet) {
828823
track.percussionArticulations.push(articulation);
829-
if (name.length > 0) {
830-
this._articulationByName.set(name, articulation);
831-
}
832-
} else if (name.length > 0 && this._articulationByName.has(name)) {
833-
this._articulationByName.get(name)!.staffLine = articulation.staffLine;
824+
this._articulationByName.set(fullName, articulation);
825+
} else if (this._articulationByName.has(fullName)) {
826+
// notation patch
827+
this._articulationByName.get(fullName)!.staffLine = articulation.staffLine;
834828
}
835829
}
836830

837-
private _parseTechniqueSymbol(txt: string): MusicFontSymbol {
831+
/**
832+
* @internal
833+
*/
834+
public static parseTechniqueSymbol(txt: string): MusicFontSymbol {
838835
switch (txt) {
839836
case 'pictEdgeOfCymbal':
840837
return MusicFontSymbol.PictEdgeOfCymbal;
@@ -853,7 +850,28 @@ export class GpifParser {
853850
}
854851
}
855852

856-
private _parseNoteHead(txt: string): MusicFontSymbol {
853+
/**
854+
* @internal
855+
*/
856+
public static parseTechniqueSymbolPlacement(txt: string): TechniqueSymbolPlacement {
857+
switch (txt) {
858+
case 'outside':
859+
return TechniqueSymbolPlacement.Outside;
860+
case 'inside':
861+
return TechniqueSymbolPlacement.Inside;
862+
case 'above':
863+
return TechniqueSymbolPlacement.Above;
864+
case 'below':
865+
return TechniqueSymbolPlacement.Below;
866+
default:
867+
return TechniqueSymbolPlacement.Outside;
868+
}
869+
}
870+
871+
/**
872+
* @internal
873+
*/
874+
public static parseNoteHead(txt: string): MusicFontSymbol {
857875
switch (txt) {
858876
case 'noteheadDoubleWholeSquare':
859877
return MusicFontSymbol.NoteheadDoubleWholeSquare;

packages/alphatab/src/importer/MusicXmlImporter.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { Note, NoteStyle } from '@coderline/alphatab/model/Note';
2929
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
3030
import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament';
3131
import { Ottavia } from '@coderline/alphatab/model/Ottavia';
32+
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
3233
import { PickStroke } from '@coderline/alphatab/model/PickStroke';
3334
import { Score } from '@coderline/alphatab/model/Score';
3435
import { Section } from '@coderline/alphatab/model/Section';
@@ -127,7 +128,8 @@ class TrackInfo {
127128
return line;
128129
}
129130

130-
private static _defaultNoteArticulation: InstrumentArticulation = new InstrumentArticulation(
131+
private static _defaultNoteArticulation: InstrumentArticulation = InstrumentArticulation.create(
132+
0,
131133
'Default',
132134
0,
133135
0,
@@ -169,7 +171,8 @@ class TrackInfo {
169171

170172
const staffLine = musicXmlStaffSteps - stepDifference;
171173

172-
const newArticulation = new InstrumentArticulation(
174+
const newArticulation = InstrumentArticulation.create(
175+
articulation.id,
173176
articulation.elementType,
174177
staffLine,
175178
articulation.outputMidiNumber,
@@ -694,6 +697,11 @@ export class MusicXmlImporter extends ScoreImporter {
694697
// case 'elevation': Ignored
695698
}
696699
}
700+
701+
articulation.id = PercussionMapper.tryMatchKnownArticulation(articulation);
702+
if (articulation.id < 0) {
703+
articulation.id = 0;
704+
}
697705
}
698706

699707
private static _interpolatePercent(value: number) {

0 commit comments

Comments
 (0)