Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Support for Percussion Tabs in alphaTex #1493

Merged
merged 3 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public open class ObjectDoubleMapEntry<TKey> {
_value = value
}

public operator fun component1(): TKey {
return _key
}

public operator fun component2(): Double {
return _value
}

@Suppress("UNCHECKED_CAST")
public constructor() {
_key = null as TKey
Expand Down
135 changes: 112 additions & 23 deletions src/importer/AlphaTexImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { BeatCloner } from '@src/generated/model/BeatCloner';
import { IOHelper } from '@src/io/IOHelper';
import { Settings } from '@src/Settings';
import { ByteBuffer } from '@src/io/ByteBuffer';
import { PercussionMapper } from '@src/model/PercussionMapper';

/**
* A list of terminals recognized by the alphaTex-parser
Expand All @@ -56,7 +57,7 @@ export enum AlphaTexSymbols {
Pipe,
MetaCommand,
Multiply,
LowerThan,
LowerThan
}

export class AlphaTexError extends AlphaTabError {
Expand All @@ -76,7 +77,7 @@ export class AlphaTexError extends AlphaTabError {
nonTerm: string | null,
expected: AlphaTexSymbols | null,
symbol: AlphaTexSymbols | null,
symbolData: unknown = null,
symbolData: unknown = null
) {
super(AlphaTabErrorType.AlphaTex, message);
this.position = position;
Expand All @@ -96,7 +97,7 @@ export class AlphaTexError extends AlphaTabError {
nonTerm: string,
expected: AlphaTexSymbols,
symbol: AlphaTexSymbols,
symbolData: unknown = null,
symbolData: unknown = null
): AlphaTexError {
let message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): Error on block ${nonTerm}`;
if (expected !== symbol) {
Expand Down Expand Up @@ -125,7 +126,7 @@ export class AlphaTexImporter extends ScoreImporter {
private _score!: Score;
private _currentTrack!: Track;
private _currentStaff!: Staff;
private _input: string = "";
private _input: string = '';
private _ch: number = AlphaTexImporter.Eof;
// Keeps track of where in input string we are
private _curChPos: number = 0;
Expand All @@ -134,7 +135,7 @@ export class AlphaTexImporter extends ScoreImporter {
// Last known position that had valid syntax/symbols
private _lastValidSpot: number[] = [0, 1, 0];
private _sy: AlphaTexSymbols = AlphaTexSymbols.No;
private _syData: unknown = "";
private _syData: unknown = '';
private _allowNegatives: boolean = false;
private _allowFloat: boolean = false;
private _allowTuning: boolean = false;
Expand All @@ -145,6 +146,7 @@ export class AlphaTexImporter extends ScoreImporter {

private _staffHasExplicitTuning: boolean = false;
private _staffTuningApplied: boolean = false;
private _percussionArticulationNames = new Map<string, number>();

public logErrors: boolean = false;

Expand Down Expand Up @@ -613,11 +615,8 @@ export class AlphaTexImporter extends ScoreImporter {
*/
private static isNameLetter(ch: number): boolean {
return (
!AlphaTexImporter.isTerminal(ch) && ( // no control characters, whitespaces, numbers or dots
(0x21 <= ch && ch <= 0x2f) ||
(0x3a <= ch && ch <= 0x7e) ||
0x80 <= ch // Unicode Symbols
)
!AlphaTexImporter.isTerminal(ch) && // no control characters, whitespaces, numbers or dots
((0x21 <= ch && ch <= 0x2f) || (0x3a <= ch && ch <= 0x7e) || 0x80 <= ch) // Unicode Symbols
);
}

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

Expand Down Expand Up @@ -821,7 +820,15 @@ export class AlphaTexImporter extends ScoreImporter {
}
} else if (this._sy === AlphaTexSymbols.String) {
let instrumentName: string = (this._syData as string).toLowerCase();
this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName);
if (instrumentName === 'percussion') {
for (const staff of this._currentTrack.staves) {
this.applyPercussionStaff(staff);
}
this._currentTrack.playbackInfo.primaryChannel = 9;
this._currentTrack.playbackInfo.secondaryChannel = 9;
} else {
this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName);
}
} else {
this.error('instrument', AlphaTexSymbols.Number, true);
}
Expand Down Expand Up @@ -852,7 +859,7 @@ export class AlphaTexImporter extends ScoreImporter {
chord.name = this._syData as string;
this._sy = this.newSy();
} else {
this.error('chord-name', AlphaTexSymbols.Number, true);
this.error('chord-name', AlphaTexSymbols.String, true);
}
for (let i: number = 0; i < this._currentStaff.tuning.length; i++) {
if (this._sy === AlphaTexSymbols.Number) {
Expand All @@ -864,10 +871,57 @@ export class AlphaTexImporter extends ScoreImporter {
}
this._currentStaff.addChord(this.getChordId(this._currentStaff, chord.name), chord);
return true;
case 'articulation':
this._sy = this.newSy();

let name = '';
if (this._sy === AlphaTexSymbols.String) {
name = this._syData as string;
this._sy = this.newSy();
} else {
this.error('articulation-name', AlphaTexSymbols.String, true);
}

if (name === 'defaults') {
for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) {
this._percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue);
this._percussionArticulationNames.set(AlphaTexImporter.toArticulationId(defaultName), defaultValue);
}
return true;
}

let number = 0;
if (this._sy === AlphaTexSymbols.Number) {
number = this._syData as number;
this._sy = this.newSy();
} else {
this.error('articulation-number', AlphaTexSymbols.Number, true);
}

if (!PercussionMapper.instrumentArticulations.has(number)) {
this.errorMessage(
`Unknown articulation ${number}. Refer to https://www.alphatab.net/docs/alphatex/percussion for available ids`
);
}

this._percussionArticulationNames.set(name.toLowerCase(), number);
return true;
default:
return false;
}
}

/**
* Encodes a given string to a shorthand text form without spaces or special characters
*/
private static toArticulationId(plain: string): string {
return plain.replace(new RegExp("[^a-zA-Z0-9]", "g"), "").toLowerCase()
}

private applyPercussionStaff(staff: Staff) {
staff.isPercussion = true;
staff.showTablature = false;
}

private chordProperties(chord: Chord): void {
if (this._sy !== AlphaTexSymbols.LBrace) {
Expand Down Expand Up @@ -998,7 +1052,14 @@ export class AlphaTexImporter extends ScoreImporter {
this._sy = this.newSy();
if (this._currentTrack.staves[0].bars.length > 0) {
this._currentTrack.ensureStaveCount(this._currentTrack.staves.length + 1);

const isPercussion = this._currentStaff.isPercussion;
this._currentStaff = this._currentTrack.staves[this._currentTrack.staves.length - 1];

if (isPercussion) {
this.applyPercussionStaff(this._currentStaff);
}

this._currentDynamics = DynamicValue.F;
}
this.staffProperties();
Expand Down Expand Up @@ -1070,7 +1131,15 @@ export class AlphaTexImporter extends ScoreImporter {
// bass G2 D2 A1 E1
this._currentStaff.displayTranspositionPitch = -12;
this._currentStaff.stringTuning.tunings = [43, 38, 33, 28];
} else if (program == 40 || program == 44 || program == 45 || program == 48 || program == 49 || program == 50 || program == 51) {
} else if (
program == 40 ||
program == 44 ||
program == 45 ||
program == 48 ||
program == 49 ||
program == 50 ||
program == 51
) {
// violin E3 A3 D3 G2
this._currentStaff.stringTuning.tunings = [52, 57, 50, 43];
} else if (program == 41) {
Expand Down Expand Up @@ -1134,15 +1203,20 @@ export class AlphaTexImporter extends ScoreImporter {
}

private beat(voice: Voice): boolean {
// duration specifier?
// duration specifier?
this.beatDuration();

let beat: Beat = new Beat();
voice.addBeat(beat);

this._allowTuning = !this._currentStaff.isPercussion;

// notes
if (this._sy === AlphaTexSymbols.LParensis) {
this._sy = this.newSy();
this.note(beat);
while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) {
this._allowTuning = !this._currentStaff.isPercussion;
if (!this.note(beat)) {
break;
}
Expand Down Expand Up @@ -1510,15 +1584,27 @@ export class AlphaTexImporter extends ScoreImporter {
switch (this._sy) {
case AlphaTexSymbols.Number:
fret = this._syData as number;
if (this._currentStaff.isPercussion && !PercussionMapper.instrumentArticulations.has(fret)) {
this.errorMessage(`Unknown percussion articulation ${fret}`);
}
break;
case AlphaTexSymbols.String:
isDead = (this._syData as string) === 'x';
isTie = (this._syData as string) === '-';

if (isTie || isDead) {
fret = 0;
if (this._currentStaff.isPercussion) {
const articulationName = (this._syData as string).toLowerCase();
if (this._percussionArticulationNames.has(articulationName)) {
fret = this._percussionArticulationNames.get(articulationName)!;
} else {
this.errorMessage(`Unknown percussion articulation '${this._syData}'`);
}
} else {
this.error('note-fret', AlphaTexSymbols.Number, true);
isDead = (this._syData as string) === 'x';
isTie = (this._syData as string) === '-';

if (isTie || isDead) {
fret = 0;
} else {
this.error('note-fret', AlphaTexSymbols.Number, true);
}
}
break;
case AlphaTexSymbols.Tuning:
Expand All @@ -1531,7 +1617,8 @@ export class AlphaTexImporter extends ScoreImporter {
}
this._sy = this.newSy(); // Fret done

let isFretted: boolean = octave === -1 && this._currentStaff.tuning.length > 0;
let isFretted: boolean =
octave === -1 && this._currentStaff.tuning.length > 0 && !this._currentStaff.isPercussion;
let noteString: number = -1;
if (isFretted) {
// Fret [Dot] String
Expand All @@ -1558,6 +1645,8 @@ export class AlphaTexImporter extends ScoreImporter {
if (!isTie) {
note.fret = fret;
}
} else if (this._currentStaff.isPercussion) {
note.percussionArticulation = fret;
} else {
note.octave = octave;
note.tone = tone;
Expand Down
100 changes: 100 additions & 0 deletions src/model/PercussionMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,106 @@ export class PercussionMapper {
[34, new InstrumentArticulation("snare", 3, 38, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack, MusicFontSymbol.NoteheadBlack)]
]);

// these are manually defined names/identifiers for the articulation list above.
// they are currently only used in the AlphaTex importer when using default articulations
// but they are kept here close to the source of the default aritculation list to maintain them together.
public static instrumentArticulationNames = new Map<string, number>([
['Ride (choke)', 29],
['Cymbal (hit)', 30],
['Snare (side stick)', 31],
['Snare (side stick) 2', 33],
['Snare (hit)', 34],
['Kick (hit)', 35],
['Kick (hit) 2', 36],
['Snare (side stick) 3', 37],
['Snare (hit) 2', 38],
['Hand Clap (hit)', 39],
['Snare (hit) 3', 40],
['Low Floor Tom (hit)', 41],
['Hi-Hat (closed)', 42],
['Very Low Tom (hit)', 43],
['Pedal Hi-Hat (hit)', 44],
['Low Tom (hit)', 45],
['Hi-Hat (open)', 46],
['Mid Tom (hit)', 47],
['High Tom (hit)', 48],
['Crash high (hit)', 49],
['High Floor Tom (hit)', 50],
['Ride (middle)', 51],
['China (hit)', 52],
['Ride (bell)', 53],
['Tambourine (hit)', 54],
['Splash (hit)', 55],
['Cowbell medium (hit)', 56],
['Crash medium (hit)', 57],
['Vibraslap (hit)', 58],
['Ride (edge)', 59],
['Hand (hit)', 60],
['Hand (hit)', 61],
['Conga high (mute)', 62],
['Conga high (hit)', 63],
['Conga low (hit)', 64],
['Timbale high (hit)', 65],
['Timbale low (hit)', 66],
['Agogo high (hit)', 67],
['Agogo tow (hit)', 68],
['Cabasa (hit)', 69],
['Left Maraca (hit)', 70],
['Whistle high (hit)', 71],
['Whistle low (hit)', 72],
['Guiro (hit)', 73],
['Guiro (scrap-return)', 74],
['Claves (hit)', 75],
['Woodblock high (hit)', 76],
['Woodblock low (hit)', 77],
['Cuica (mute)', 78],
['Cuica (open)', 79],
['Triangle (rnute)', 80],
['Triangle (hit)', 81],
['Shaker (hit)', 82],
['Tinkle Bell (hat)', 83],
['Jingle Bell (hit)', 83],
['Bell Tree (hit)', 84],
['Castanets (hit)', 85],
['Surdo (hit)', 86],
['Surdo (mute)', 87],
['Snare (rim shot)', 91],
['Hi-Hat (half)', 92],
['Ride (edge) 2', 93],
['Ride (choke) 2', 94],
['Splash (choke)', 95],
['China (choke)', 96],
['Crash high (choke)', 97],
['Crash medium (choke)', 98],
['Cowbell low (hit)', 99],
['Cowbell low (tip)', 100],
['Cowbell medium (tip)', 101],
['Cowbell high (hit)', 102],
['Cowbell high (tip)', 103],
['Hand (mute)', 104],
['Hand (slap)', 105],
['Hand (mute) 2', 106],
['Hand (slap) 2', 107],
['Conga low (slap)', 108],
['Conga low (mute)', 109],
['Conga high (slap)', 110],
['Tambourine (return)', 111],
['Tambourine (roll)', 112],
['Tambourine (hand)', 113],
['Grancassa (hit)', 114],
['Piatti (hat)', 115],
['Piatti (hand)', 116],
['Cabasa (return)', 117],
['Left Maraca (return)', 118],
['Right Maraca (hit)', 119],
['Right Maraca (return)', 120],
['Shaker (return)', 122],
['Bell Tee (return)', 123],
['Golpe (thumb)', 124],
['Golpe (finger)', 125],
['Ride (middle) 2', 126],
['Ride (bell) 2', 127]
]);

public static getArticulation(n: Note): InstrumentArticulation | null {
const articulationIndex = n.percussionArticulation;
Expand Down
Loading
Loading