Skip to content

Commit db773c0

Browse files
authored
feat: Add Support for Percussion Tabs in alphaTex (#1493)
1 parent 4c5e1f0 commit db773c0

File tree

4 files changed

+305
-45
lines changed

4 files changed

+305
-45
lines changed

src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ public open class ObjectDoubleMapEntry<TKey> {
1515
_value = value
1616
}
1717

18+
public operator fun component1(): TKey {
19+
return _key
20+
}
21+
22+
public operator fun component2(): Double {
23+
return _value
24+
}
25+
1826
@Suppress("UNCHECKED_CAST")
1927
public constructor() {
2028
_key = null as TKey

src/importer/AlphaTexImporter.ts

+112-23
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { BeatCloner } from '@src/generated/model/BeatCloner';
3737
import { IOHelper } from '@src/io/IOHelper';
3838
import { Settings } from '@src/Settings';
3939
import { ByteBuffer } from '@src/io/ByteBuffer';
40+
import { PercussionMapper } from '@src/model/PercussionMapper';
4041

4142
/**
4243
* A list of terminals recognized by the alphaTex-parser
@@ -56,7 +57,7 @@ export enum AlphaTexSymbols {
5657
Pipe,
5758
MetaCommand,
5859
Multiply,
59-
LowerThan,
60+
LowerThan
6061
}
6162

6263
export class AlphaTexError extends AlphaTabError {
@@ -76,7 +77,7 @@ export class AlphaTexError extends AlphaTabError {
7677
nonTerm: string | null,
7778
expected: AlphaTexSymbols | null,
7879
symbol: AlphaTexSymbols | null,
79-
symbolData: unknown = null,
80+
symbolData: unknown = null
8081
) {
8182
super(AlphaTabErrorType.AlphaTex, message);
8283
this.position = position;
@@ -96,7 +97,7 @@ export class AlphaTexError extends AlphaTabError {
9697
nonTerm: string,
9798
expected: AlphaTexSymbols,
9899
symbol: AlphaTexSymbols,
99-
symbolData: unknown = null,
100+
symbolData: unknown = null
100101
): AlphaTexError {
101102
let message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): Error on block ${nonTerm}`;
102103
if (expected !== symbol) {
@@ -125,7 +126,7 @@ export class AlphaTexImporter extends ScoreImporter {
125126
private _score!: Score;
126127
private _currentTrack!: Track;
127128
private _currentStaff!: Staff;
128-
private _input: string = "";
129+
private _input: string = '';
129130
private _ch: number = AlphaTexImporter.Eof;
130131
// Keeps track of where in input string we are
131132
private _curChPos: number = 0;
@@ -134,7 +135,7 @@ export class AlphaTexImporter extends ScoreImporter {
134135
// Last known position that had valid syntax/symbols
135136
private _lastValidSpot: number[] = [0, 1, 0];
136137
private _sy: AlphaTexSymbols = AlphaTexSymbols.No;
137-
private _syData: unknown = "";
138+
private _syData: unknown = '';
138139
private _allowNegatives: boolean = false;
139140
private _allowFloat: boolean = false;
140141
private _allowTuning: boolean = false;
@@ -145,6 +146,7 @@ export class AlphaTexImporter extends ScoreImporter {
145146

146147
private _staffHasExplicitTuning: boolean = false;
147148
private _staffTuningApplied: boolean = false;
149+
private _percussionArticulationNames = new Map<string, number>();
148150

149151
public logErrors: boolean = false;
150152

@@ -613,11 +615,8 @@ export class AlphaTexImporter extends ScoreImporter {
613615
*/
614616
private static isNameLetter(ch: number): boolean {
615617
return (
616-
!AlphaTexImporter.isTerminal(ch) && ( // no control characters, whitespaces, numbers or dots
617-
(0x21 <= ch && ch <= 0x2f) ||
618-
(0x3a <= ch && ch <= 0x7e) ||
619-
0x80 <= ch // Unicode Symbols
620-
)
618+
!AlphaTexImporter.isTerminal(ch) && // no control characters, whitespaces, numbers or dots
619+
((0x21 <= ch && ch <= 0x2f) || (0x3a <= ch && ch <= 0x7e) || 0x80 <= ch) // Unicode Symbols
621620
);
622621
}
623622

@@ -650,8 +649,8 @@ export class AlphaTexImporter extends ScoreImporter {
650649
private isDigit(ch: number): boolean {
651650
return (
652651
(ch >= 0x30 && ch <= 0x39) /* 0-9 */ ||
653-
(this._allowNegatives && ch === 0x2d /* - */) || // allow minus sign if negatives
654-
(this._allowFloat && ch === 0x2e /* . */) // allow dot if float
652+
(this._allowNegatives && ch === 0x2d) /* - */ || // allow minus sign if negatives
653+
(this._allowFloat && ch === 0x2e) /* . */ // allow dot if float
655654
);
656655
}
657656

@@ -821,7 +820,15 @@ export class AlphaTexImporter extends ScoreImporter {
821820
}
822821
} else if (this._sy === AlphaTexSymbols.String) {
823822
let instrumentName: string = (this._syData as string).toLowerCase();
824-
this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName);
823+
if (instrumentName === 'percussion') {
824+
for (const staff of this._currentTrack.staves) {
825+
this.applyPercussionStaff(staff);
826+
}
827+
this._currentTrack.playbackInfo.primaryChannel = 9;
828+
this._currentTrack.playbackInfo.secondaryChannel = 9;
829+
} else {
830+
this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName);
831+
}
825832
} else {
826833
this.error('instrument', AlphaTexSymbols.Number, true);
827834
}
@@ -852,7 +859,7 @@ export class AlphaTexImporter extends ScoreImporter {
852859
chord.name = this._syData as string;
853860
this._sy = this.newSy();
854861
} else {
855-
this.error('chord-name', AlphaTexSymbols.Number, true);
862+
this.error('chord-name', AlphaTexSymbols.String, true);
856863
}
857864
for (let i: number = 0; i < this._currentStaff.tuning.length; i++) {
858865
if (this._sy === AlphaTexSymbols.Number) {
@@ -864,10 +871,57 @@ export class AlphaTexImporter extends ScoreImporter {
864871
}
865872
this._currentStaff.addChord(this.getChordId(this._currentStaff, chord.name), chord);
866873
return true;
874+
case 'articulation':
875+
this._sy = this.newSy();
876+
877+
let name = '';
878+
if (this._sy === AlphaTexSymbols.String) {
879+
name = this._syData as string;
880+
this._sy = this.newSy();
881+
} else {
882+
this.error('articulation-name', AlphaTexSymbols.String, true);
883+
}
884+
885+
if (name === 'defaults') {
886+
for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) {
887+
this._percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue);
888+
this._percussionArticulationNames.set(AlphaTexImporter.toArticulationId(defaultName), defaultValue);
889+
}
890+
return true;
891+
}
892+
893+
let number = 0;
894+
if (this._sy === AlphaTexSymbols.Number) {
895+
number = this._syData as number;
896+
this._sy = this.newSy();
897+
} else {
898+
this.error('articulation-number', AlphaTexSymbols.Number, true);
899+
}
900+
901+
if (!PercussionMapper.instrumentArticulations.has(number)) {
902+
this.errorMessage(
903+
`Unknown articulation ${number}. Refer to https://www.alphatab.net/docs/alphatex/percussion for available ids`
904+
);
905+
}
906+
907+
this._percussionArticulationNames.set(name.toLowerCase(), number);
908+
return true;
867909
default:
868910
return false;
869911
}
870912
}
913+
914+
/**
915+
* Encodes a given string to a shorthand text form without spaces or special characters
916+
*/
917+
private static toArticulationId(plain: string): string {
918+
return plain.replace(new RegExp("[^a-zA-Z0-9]", "g"), "").toLowerCase()
919+
}
920+
921+
private applyPercussionStaff(staff: Staff) {
922+
staff.isPercussion = true;
923+
staff.showTablature = false;
924+
}
871925

872926
private chordProperties(chord: Chord): void {
873927
if (this._sy !== AlphaTexSymbols.LBrace) {
@@ -998,7 +1052,14 @@ export class AlphaTexImporter extends ScoreImporter {
9981052
this._sy = this.newSy();
9991053
if (this._currentTrack.staves[0].bars.length > 0) {
10001054
this._currentTrack.ensureStaveCount(this._currentTrack.staves.length + 1);
1055+
1056+
const isPercussion = this._currentStaff.isPercussion;
10011057
this._currentStaff = this._currentTrack.staves[this._currentTrack.staves.length - 1];
1058+
1059+
if (isPercussion) {
1060+
this.applyPercussionStaff(this._currentStaff);
1061+
}
1062+
10021063
this._currentDynamics = DynamicValue.F;
10031064
}
10041065
this.staffProperties();
@@ -1070,7 +1131,15 @@ export class AlphaTexImporter extends ScoreImporter {
10701131
// bass G2 D2 A1 E1
10711132
this._currentStaff.displayTranspositionPitch = -12;
10721133
this._currentStaff.stringTuning.tunings = [43, 38, 33, 28];
1073-
} else if (program == 40 || program == 44 || program == 45 || program == 48 || program == 49 || program == 50 || program == 51) {
1134+
} else if (
1135+
program == 40 ||
1136+
program == 44 ||
1137+
program == 45 ||
1138+
program == 48 ||
1139+
program == 49 ||
1140+
program == 50 ||
1141+
program == 51
1142+
) {
10741143
// violin E3 A3 D3 G2
10751144
this._currentStaff.stringTuning.tunings = [52, 57, 50, 43];
10761145
} else if (program == 41) {
@@ -1134,15 +1203,20 @@ export class AlphaTexImporter extends ScoreImporter {
11341203
}
11351204

11361205
private beat(voice: Voice): boolean {
1137-
// duration specifier?
1206+
// duration specifier?
11381207
this.beatDuration();
1208+
11391209
let beat: Beat = new Beat();
11401210
voice.addBeat(beat);
1211+
1212+
this._allowTuning = !this._currentStaff.isPercussion;
1213+
11411214
// notes
11421215
if (this._sy === AlphaTexSymbols.LParensis) {
11431216
this._sy = this.newSy();
11441217
this.note(beat);
11451218
while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) {
1219+
this._allowTuning = !this._currentStaff.isPercussion;
11461220
if (!this.note(beat)) {
11471221
break;
11481222
}
@@ -1510,15 +1584,27 @@ export class AlphaTexImporter extends ScoreImporter {
15101584
switch (this._sy) {
15111585
case AlphaTexSymbols.Number:
15121586
fret = this._syData as number;
1587+
if (this._currentStaff.isPercussion && !PercussionMapper.instrumentArticulations.has(fret)) {
1588+
this.errorMessage(`Unknown percussion articulation ${fret}`);
1589+
}
15131590
break;
15141591
case AlphaTexSymbols.String:
1515-
isDead = (this._syData as string) === 'x';
1516-
isTie = (this._syData as string) === '-';
1517-
1518-
if (isTie || isDead) {
1519-
fret = 0;
1592+
if (this._currentStaff.isPercussion) {
1593+
const articulationName = (this._syData as string).toLowerCase();
1594+
if (this._percussionArticulationNames.has(articulationName)) {
1595+
fret = this._percussionArticulationNames.get(articulationName)!;
1596+
} else {
1597+
this.errorMessage(`Unknown percussion articulation '${this._syData}'`);
1598+
}
15201599
} else {
1521-
this.error('note-fret', AlphaTexSymbols.Number, true);
1600+
isDead = (this._syData as string) === 'x';
1601+
isTie = (this._syData as string) === '-';
1602+
1603+
if (isTie || isDead) {
1604+
fret = 0;
1605+
} else {
1606+
this.error('note-fret', AlphaTexSymbols.Number, true);
1607+
}
15221608
}
15231609
break;
15241610
case AlphaTexSymbols.Tuning:
@@ -1531,7 +1617,8 @@ export class AlphaTexImporter extends ScoreImporter {
15311617
}
15321618
this._sy = this.newSy(); // Fret done
15331619

1534-
let isFretted: boolean = octave === -1 && this._currentStaff.tuning.length > 0;
1620+
let isFretted: boolean =
1621+
octave === -1 && this._currentStaff.tuning.length > 0 && !this._currentStaff.isPercussion;
15351622
let noteString: number = -1;
15361623
if (isFretted) {
15371624
// Fret [Dot] String
@@ -1558,6 +1645,8 @@ export class AlphaTexImporter extends ScoreImporter {
15581645
if (!isTie) {
15591646
note.fret = fret;
15601647
}
1648+
} else if (this._currentStaff.isPercussion) {
1649+
note.percussionArticulation = fret;
15611650
} else {
15621651
note.octave = octave;
15631652
note.tone = tone;

src/model/PercussionMapper.ts

+100
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,106 @@ export class PercussionMapper {
171171
[34, new InstrumentArticulation("snare", 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack)]
172172
]);
173173

174+
// these are manually defined names/identifiers for the articulation list above.
175+
// they are currently only used in the AlphaTex importer when using default articulations
176+
// but they are kept here close to the source of the default aritculation list to maintain them together.
177+
public static instrumentArticulationNames = new Map<string, number>([
178+
['Ride (choke)', 29],
179+
['Cymbal (hit)', 30],
180+
['Snare (side stick)', 31],
181+
['Snare (side stick) 2', 33],
182+
['Snare (hit)', 34],
183+
['Kick (hit)', 35],
184+
['Kick (hit) 2', 36],
185+
['Snare (side stick) 3', 37],
186+
['Snare (hit) 2', 38],
187+
['Hand Clap (hit)', 39],
188+
['Snare (hit) 3', 40],
189+
['Low Floor Tom (hit)', 41],
190+
['Hi-Hat (closed)', 42],
191+
['Very Low Tom (hit)', 43],
192+
['Pedal Hi-Hat (hit)', 44],
193+
['Low Tom (hit)', 45],
194+
['Hi-Hat (open)', 46],
195+
['Mid Tom (hit)', 47],
196+
['High Tom (hit)', 48],
197+
['Crash high (hit)', 49],
198+
['High Floor Tom (hit)', 50],
199+
['Ride (middle)', 51],
200+
['China (hit)', 52],
201+
['Ride (bell)', 53],
202+
['Tambourine (hit)', 54],
203+
['Splash (hit)', 55],
204+
['Cowbell medium (hit)', 56],
205+
['Crash medium (hit)', 57],
206+
['Vibraslap (hit)', 58],
207+
['Ride (edge)', 59],
208+
['Hand (hit)', 60],
209+
['Hand (hit)', 61],
210+
['Conga high (mute)', 62],
211+
['Conga high (hit)', 63],
212+
['Conga low (hit)', 64],
213+
['Timbale high (hit)', 65],
214+
['Timbale low (hit)', 66],
215+
['Agogo high (hit)', 67],
216+
['Agogo tow (hit)', 68],
217+
['Cabasa (hit)', 69],
218+
['Left Maraca (hit)', 70],
219+
['Whistle high (hit)', 71],
220+
['Whistle low (hit)', 72],
221+
['Guiro (hit)', 73],
222+
['Guiro (scrap-return)', 74],
223+
['Claves (hit)', 75],
224+
['Woodblock high (hit)', 76],
225+
['Woodblock low (hit)', 77],
226+
['Cuica (mute)', 78],
227+
['Cuica (open)', 79],
228+
['Triangle (rnute)', 80],
229+
['Triangle (hit)', 81],
230+
['Shaker (hit)', 82],
231+
['Tinkle Bell (hat)', 83],
232+
['Jingle Bell (hit)', 83],
233+
['Bell Tree (hit)', 84],
234+
['Castanets (hit)', 85],
235+
['Surdo (hit)', 86],
236+
['Surdo (mute)', 87],
237+
['Snare (rim shot)', 91],
238+
['Hi-Hat (half)', 92],
239+
['Ride (edge) 2', 93],
240+
['Ride (choke) 2', 94],
241+
['Splash (choke)', 95],
242+
['China (choke)', 96],
243+
['Crash high (choke)', 97],
244+
['Crash medium (choke)', 98],
245+
['Cowbell low (hit)', 99],
246+
['Cowbell low (tip)', 100],
247+
['Cowbell medium (tip)', 101],
248+
['Cowbell high (hit)', 102],
249+
['Cowbell high (tip)', 103],
250+
['Hand (mute)', 104],
251+
['Hand (slap)', 105],
252+
['Hand (mute) 2', 106],
253+
['Hand (slap) 2', 107],
254+
['Conga low (slap)', 108],
255+
['Conga low (mute)', 109],
256+
['Conga high (slap)', 110],
257+
['Tambourine (return)', 111],
258+
['Tambourine (roll)', 112],
259+
['Tambourine (hand)', 113],
260+
['Grancassa (hit)', 114],
261+
['Piatti (hat)', 115],
262+
['Piatti (hand)', 116],
263+
['Cabasa (return)', 117],
264+
['Left Maraca (return)', 118],
265+
['Right Maraca (hit)', 119],
266+
['Right Maraca (return)', 120],
267+
['Shaker (return)', 122],
268+
['Bell Tee (return)', 123],
269+
['Golpe (thumb)', 124],
270+
['Golpe (finger)', 125],
271+
['Ride (middle) 2', 126],
272+
['Ride (bell) 2', 127]
273+
]);
174274

175275
public static getArticulation(n: Note): InstrumentArticulation | null {
176276
const articulationIndex = n.percussionArticulation;

0 commit comments

Comments
 (0)