diff --git a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt index 6a606ec98..2f710fc92 100644 --- a/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt +++ b/src.kotlin/alphaTab/alphaTab/src/commonMain/kotlin/alphaTab/collections/ObjectDoubleMap.kt @@ -15,6 +15,14 @@ public open class ObjectDoubleMapEntry { _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 diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index 44b890539..e788128e1 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -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 @@ -56,7 +57,7 @@ export enum AlphaTexSymbols { Pipe, MetaCommand, Multiply, - LowerThan, + LowerThan } export class AlphaTexError extends AlphaTabError { @@ -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; @@ -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) { @@ -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; @@ -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; @@ -145,6 +146,7 @@ export class AlphaTexImporter extends ScoreImporter { private _staffHasExplicitTuning: boolean = false; private _staffTuningApplied: boolean = false; + private _percussionArticulationNames = new Map(); public logErrors: boolean = false; @@ -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 ); } @@ -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 ); } @@ -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); } @@ -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) { @@ -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) { @@ -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(); @@ -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) { @@ -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; } @@ -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: @@ -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 @@ -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; diff --git a/src/model/PercussionMapper.ts b/src/model/PercussionMapper.ts index a8f859f10..aa5c8aa30 100644 --- a/src/model/PercussionMapper.ts +++ b/src/model/PercussionMapper.ts @@ -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([ + ['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; diff --git a/test/importer/AlphaTexImporter.test.ts b/test/importer/AlphaTexImporter.test.ts index bf7d79e1c..e4344394c 100644 --- a/test/importer/AlphaTexImporter.test.ts +++ b/test/importer/AlphaTexImporter.test.ts @@ -206,7 +206,6 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[2].voices[0].beats[4].isRest).to.equal(true); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[4].brushType).to.equal(BrushType.None); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[4].brushDuration).to.equal(0); - }); it('hamonics-issue79', () => { @@ -668,7 +667,9 @@ describe('AlphaTexImporterTest', () => { let score: Score = parseTex(tex); expect(score.tracks.length).to.equal(1); expect(score.masterBars.length).to.equal(4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].slideOutType).to.equal(SlideOutType.Legato); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].slideOutType).to.equal( + SlideOutType.Legato + ); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].slideTarget!.id).to.equal( score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].id ); @@ -716,22 +717,22 @@ describe('AlphaTexImporterTest', () => { \\ks F 3.3 | \\ks bbmajor 3.3 | \\ks CMINOR 3.3 | \\ks aB 3.3 | \\ks db 3.3 | \\ks d#minor 3.3 | \\ks g 3.3 | \\ks Dmajor 3.3 | \\ks f#minor 3.3 | \\ks E 3.3 | \\ks Bmajor 3.3 | \\ks Ebminor 3.3`; let score: Score = parseTex(tex); - expect(score.masterBars[0].keySignature).to.equal(KeySignature.C) - expect(score.masterBars[1].keySignature).to.equal(KeySignature.C) - expect(score.masterBars[2].keySignature).to.equal(KeySignature.C) - expect(score.masterBars[3].keySignature).to.equal(KeySignature.C) - expect(score.masterBars[4].keySignature).to.equal(KeySignature.F) - expect(score.masterBars[5].keySignature).to.equal(KeySignature.Bb) - expect(score.masterBars[6].keySignature).to.equal(KeySignature.Eb) - expect(score.masterBars[7].keySignature).to.equal(KeySignature.Ab) - expect(score.masterBars[8].keySignature).to.equal(KeySignature.Db) - expect(score.masterBars[9].keySignature).to.equal(KeySignature.Gb) - expect(score.masterBars[10].keySignature).to.equal(KeySignature.G) - expect(score.masterBars[11].keySignature).to.equal(KeySignature.D) - expect(score.masterBars[12].keySignature).to.equal(KeySignature.A) - expect(score.masterBars[13].keySignature).to.equal(KeySignature.E) - expect(score.masterBars[14].keySignature).to.equal(KeySignature.B) - expect(score.masterBars[15].keySignature).to.equal(KeySignature.FSharp) + expect(score.masterBars[0].keySignature).to.equal(KeySignature.C); + expect(score.masterBars[1].keySignature).to.equal(KeySignature.C); + expect(score.masterBars[2].keySignature).to.equal(KeySignature.C); + expect(score.masterBars[3].keySignature).to.equal(KeySignature.C); + expect(score.masterBars[4].keySignature).to.equal(KeySignature.F); + expect(score.masterBars[5].keySignature).to.equal(KeySignature.Bb); + expect(score.masterBars[6].keySignature).to.equal(KeySignature.Eb); + expect(score.masterBars[7].keySignature).to.equal(KeySignature.Ab); + expect(score.masterBars[8].keySignature).to.equal(KeySignature.Db); + expect(score.masterBars[9].keySignature).to.equal(KeySignature.Gb); + expect(score.masterBars[10].keySignature).to.equal(KeySignature.G); + expect(score.masterBars[11].keySignature).to.equal(KeySignature.D); + expect(score.masterBars[12].keySignature).to.equal(KeySignature.A); + expect(score.masterBars[13].keySignature).to.equal(KeySignature.E); + expect(score.masterBars[14].keySignature).to.equal(KeySignature.B); + expect(score.masterBars[15].keySignature).to.equal(KeySignature.FSharp); }); it('pop-slap-tap', () => { @@ -870,7 +871,7 @@ describe('AlphaTexImporterTest', () => { expect(score.masterBars[0].alternateEndings).to.equal(0b0000); expect(score.masterBars[1].alternateEndings).to.equal(0b0111); expect(score.masterBars[2].alternateEndings).to.equal(0b1000); - }) + }); it('random-alternate-endings', () => { let tex: string = ` @@ -899,7 +900,7 @@ describe('AlphaTexImporterTest', () => { expect(score.masterBars[7].alternateEndings).to.equal(0b000); expect(score.masterBars[8].alternateEndings).to.equal(0b101); expect(score.masterBars[9].alternateEndings).to.equal(0b010); - }) + }); it('default-transposition-on-instruments', () => { let tex: string = ` @@ -1013,8 +1014,8 @@ describe('AlphaTexImporterTest', () => { (1.2 1.1).4 x.2.8 0.1 1.1 | 1.2 3.2 0.1 1.1`); expect(score.title).to.equal(multiByteChars); - expect(score.tracks[0].name).to.equal("🎸"); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics![0]).to.equal("🤘"); + expect(score.tracks[0].name).to.equal('🎸'); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics![0]).to.equal('🤘'); }); it('does-not-hang-on-backslash', () => { @@ -1144,4 +1145,66 @@ describe('AlphaTexImporterTest', () => { it('tempo-invalid-float', () => { expect(() => parseTex('\\tempo 112.Q .')).to.throw(UnsupportedFormatError); }); + + it('percussion-numbers', () => { + const score = parseTex(` + \\instrument "percussion" + . + 30 31 33 34 + `); + expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); + expect(score.tracks[0].staves[0].isPercussion).to.be.true; + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(30); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + }); + + it('percussion-custom-articulation', () => { + const score = parseTex(` + \\instrument "percussion" + \\articulation A 30 + \\articulation B 31 + \\articulation C 33 + \\articulation D 34 + . + A B C D + `); + expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); + expect(score.tracks[0].staves[0].isPercussion).to.be.true; + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(30); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + }); + + it('percussion-default-articulations', () => { + const score = parseTex(` + \\instrument "percussion" + \\articulation defaults + . + "Cymbal (hit)" "Snare (side stick)" "Snare (side stick) 2" "Snare (hit)" + `); + expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); + expect(score.tracks[0].staves[0].isPercussion).to.be.true; + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(30); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + }); + + it('percussion-default-articulations-short', () => { + const score = parseTex(` + \\instrument "percussion" + \\articulation defaults + . + CymbalHit SnareSideStick SnareSideStick2 SnareHit + `); + expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(9); + expect(score.tracks[0].staves[0].isPercussion).to.be.true; + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].percussionArticulation).to.equal(30); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(31); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(33); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(34); + }); });