diff --git a/packages/alphatab/scripts/smufl-metadata.ts b/packages/alphatab/scripts/smufl-metadata.ts index 6ed233418..ce6029e56 100644 --- a/packages/alphatab/scripts/smufl-metadata.ts +++ b/packages/alphatab/scripts/smufl-metadata.ts @@ -12,8 +12,8 @@ const metadata: SmuflMetadata = JSON.parse(await fs.promises.readFile(input, 'ut const outputMetadata:SmuflMetadata = { engravingDefaults: metadata.engravingDefaults, - glyphBBoxes: {}, - glyphsWithAnchors: {} + glyphBBoxes: {} as SmuflMetadata['glyphBBoxes'], + glyphsWithAnchors: {} as SmuflMetadata['glyphsWithAnchors'] }; const alphaTabUsedGlyphs = new Set(); diff --git a/packages/alphatab/src/EngravingSettings.ts b/packages/alphatab/src/EngravingSettings.ts index 07dee635e..5517fb094 100644 --- a/packages/alphatab/src/EngravingSettings.ts +++ b/packages/alphatab/src/EngravingSettings.ts @@ -928,6 +928,10 @@ export class EngravingSettings { bBoxNE: [1.876, 1.18], bBoxSW: [0, 0] }, + buzzRoll: { + bBoxNE: [0.624, 0.464], + bBoxSW: [-0.62, -0.464] + }, cClef: { bBoxNE: [2.796, 2.024], bBoxSW: [0, -2.024] @@ -1884,6 +1888,14 @@ export class EngravingSettings { bBoxNE: [0.6, 1.112], bBoxSW: [-0.6, -1.12] }, + tremolo4: { + bBoxNE: [0.6, 1.496], + bBoxSW: [-0.6, -1.48] + }, + tremolo5: { + bBoxNE: [0.6, 1.88], + bBoxSW: [-0.604, -1.84] + }, tuplet0: { bBoxNE: [1.2731041262817027, 1.5], bBoxSW: [-0.001204330173715796, -0.032] diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index a01ed2701..ba1bf65ae 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -792,16 +792,17 @@ export class GpifWriter { beatNode.addElement('Fadding').innerText = FadeType[beat.fade]; } if (beat.isTremolo) { - switch (beat.tremoloSpeed) { - case Duration.Eighth: + switch (beat.tremoloPicking!.marks) { + case 1: beatNode.addElement('Tremolo').innerText = '1/2'; break; - case Duration.Sixteenth: + case 2: beatNode.addElement('Tremolo').innerText = '1/4'; break; - case Duration.ThirtySecond: + case 3: beatNode.addElement('Tremolo').innerText = '1/8'; break; + // NOTE: guitar pro does not support other tremolos } } if (beat.hasChord) { diff --git a/packages/alphatab/src/generated/model/BeatCloner.ts b/packages/alphatab/src/generated/model/BeatCloner.ts index 9b72626ef..ec16d076c 100644 --- a/packages/alphatab/src/generated/model/BeatCloner.ts +++ b/packages/alphatab/src/generated/model/BeatCloner.ts @@ -7,6 +7,7 @@ import { Beat } from "@coderline/alphatab/model/Beat"; import { NoteCloner } from "@coderline/alphatab/generated/model/NoteCloner"; import { AutomationCloner } from "@coderline/alphatab/generated/model/AutomationCloner"; import { BendPointCloner } from "@coderline/alphatab/generated/model/BendPointCloner"; +import { TremoloPickingEffectCloner } from "@coderline/alphatab/generated/model/TremoloPickingEffectCloner"; /** * @internal */ @@ -54,7 +55,7 @@ export class BeatCloner { clone.chordId = original.chordId; clone.graceType = original.graceType; clone.pickStroke = original.pickStroke; - clone.tremoloSpeed = original.tremoloSpeed; + clone.tremoloPicking = original.tremoloPicking ? TremoloPickingEffectCloner.clone(original.tremoloPicking) : undefined; clone.crescendo = original.crescendo; clone.displayStart = original.displayStart; clone.playbackStart = original.playbackStart; diff --git a/packages/alphatab/src/generated/model/BeatSerializer.ts b/packages/alphatab/src/generated/model/BeatSerializer.ts index ad318d32a..bcc414c25 100644 --- a/packages/alphatab/src/generated/model/BeatSerializer.ts +++ b/packages/alphatab/src/generated/model/BeatSerializer.ts @@ -8,6 +8,7 @@ import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { NoteSerializer } from "@coderline/alphatab/generated/model/NoteSerializer"; import { AutomationSerializer } from "@coderline/alphatab/generated/model/AutomationSerializer"; import { BendPointSerializer } from "@coderline/alphatab/generated/model/BendPointSerializer"; +import { TremoloPickingEffectSerializer } from "@coderline/alphatab/generated/model/TremoloPickingEffectSerializer"; import { BeatStyleSerializer } from "@coderline/alphatab/generated/model/BeatStyleSerializer"; import { Note } from "@coderline/alphatab/model/Note"; import { BendStyle } from "@coderline/alphatab/model/BendStyle"; @@ -21,6 +22,7 @@ import { BendPoint } from "@coderline/alphatab/model/BendPoint"; import { VibratoType } from "@coderline/alphatab/model/VibratoType"; import { GraceType } from "@coderline/alphatab/model/GraceType"; import { PickStroke } from "@coderline/alphatab/model/PickStroke"; +import { TremoloPickingEffect } from "@coderline/alphatab/model/TremoloPickingEffect"; import { CrescendoType } from "@coderline/alphatab/model/CrescendoType"; import { GolpeType } from "@coderline/alphatab/model/GolpeType"; import { DynamicValue } from "@coderline/alphatab/model/DynamicValue"; @@ -75,7 +77,9 @@ export class BeatSerializer { o.set("chordid", obj.chordId); o.set("gracetype", obj.graceType as number); o.set("pickstroke", obj.pickStroke as number); - o.set("tremolospeed", obj.tremoloSpeed as number | null); + if (obj.tremoloPicking) { + o.set("tremolopicking", TremoloPickingEffectSerializer.toJson(obj.tremoloPicking)); + } o.set("crescendo", obj.crescendo as number); o.set("displaystart", obj.displayStart); o.set("playbackstart", obj.playbackStart); @@ -201,8 +205,14 @@ export class BeatSerializer { case "pickstroke": obj.pickStroke = JsonHelper.parseEnum(v, PickStroke)!; return true; - case "tremolospeed": - obj.tremoloSpeed = JsonHelper.parseEnum(v, Duration) ?? null; + case "tremolopicking": + if (v) { + obj.tremoloPicking = new TremoloPickingEffect(); + TremoloPickingEffectSerializer.fromJson(obj.tremoloPicking, v); + } + else { + obj.tremoloPicking = undefined; + } return true; case "crescendo": obj.crescendo = JsonHelper.parseEnum(v, CrescendoType)!; diff --git a/packages/alphatab/src/generated/model/TremoloPickingEffectCloner.ts b/packages/alphatab/src/generated/model/TremoloPickingEffectCloner.ts new file mode 100644 index 000000000..29b9e0101 --- /dev/null +++ b/packages/alphatab/src/generated/model/TremoloPickingEffectCloner.ts @@ -0,0 +1,17 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TremoloPickingEffect } from "@coderline/alphatab/model/TremoloPickingEffect"; +/** + * @internal + */ +export class TremoloPickingEffectCloner { + public static clone(original: TremoloPickingEffect): TremoloPickingEffect { + const clone = new TremoloPickingEffect(); + clone.marks = original.marks; + clone.style = original.style; + return clone; + } +} diff --git a/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts b/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts new file mode 100644 index 000000000..7150e3f7c --- /dev/null +++ b/packages/alphatab/src/generated/model/TremoloPickingEffectSerializer.ts @@ -0,0 +1,39 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { TremoloPickingEffect } from "@coderline/alphatab/model/TremoloPickingEffect"; +import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { TremoloPickingStyle } from "@coderline/alphatab/model/TremoloPickingEffect"; +/** + * @internal + */ +export class TremoloPickingEffectSerializer { + public static fromJson(obj: TremoloPickingEffect, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => TremoloPickingEffectSerializer.setProperty(obj, k, v)); + } + public static toJson(obj: TremoloPickingEffect | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("marks", obj.marks); + o.set("style", obj.style as number); + return o; + } + public static setProperty(obj: TremoloPickingEffect, property: string, v: unknown): boolean { + switch (property) { + case "marks": + obj.marks = v! as number; + return true; + case "style": + obj.style = JsonHelper.parseEnum(v, TremoloPickingStyle)!; + return true; + } + return false; + } +} diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 4f4423c08..a42714081 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -47,6 +47,7 @@ import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; +import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; /** * @internal @@ -63,7 +64,10 @@ export class Gp3To5Importer extends ScoreImporter { private _playbackInfos: PlaybackInformation[] = []; private _doubleBars: Set = new Set(); private _clefsPerTrack: Map = new Map(); - private _keySignatures: Map = new Map(); + private _keySignatures: Map = new Map< + number, + [KeySignature, KeySignatureType] + >(); private _beatTextChunksByTrack: Map = new Map(); private _directionLookup: Map = new Map(); @@ -1356,18 +1360,9 @@ export class Gp3To5Importer extends ScoreImporter { } public readTremoloPicking(beat: Beat): void { - const speed: number = this.data.readByte(); - switch (speed) { - case 1: - beat.tremoloSpeed = Duration.Eighth; - break; - case 2: - beat.tremoloSpeed = Duration.Sixteenth; - break; - case 3: - beat.tremoloSpeed = Duration.ThirtySecond; - break; - } + const effect = new TremoloPickingEffect(); + beat.tremoloPicking = effect; + effect.marks = this.data.readByte(); } public readSlide(note: Note): void { diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index 5621d9efa..3303fefbb 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -55,6 +55,7 @@ import { Direction } from '@coderline/alphatab/model/Direction'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { BackingTrack } from '@coderline/alphatab/model/BackingTrack'; import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; /** * This structure represents a duration within a gpif @@ -1693,15 +1694,17 @@ export class GpifParser { } break; case 'Tremolo': + const tremolo = new TremoloPickingEffect(); + beat.tremoloPicking = tremolo; switch (c.innerText) { case '1/2': - beat.tremoloSpeed = Duration.Eighth; + tremolo.marks = 1; break; case '1/4': - beat.tremoloSpeed = Duration.Sixteenth; + tremolo.marks = 2; break; case '1/8': - beat.tremoloSpeed = Duration.ThirtySecond; + tremolo.marks = 3; break; } break; diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 51c1ef281..97dbee42a 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -36,6 +36,7 @@ import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; +import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { Voice } from '@coderline/alphatab/model/Voice'; @@ -3579,17 +3580,17 @@ export class MusicXmlImporter extends ScoreImporter { break; // case 'schleifer': Not supported case 'tremolo': - switch (c.innerText) { - case '1': - note.beat.tremoloSpeed = Duration.Eighth; - break; - case '2': - note.beat.tremoloSpeed = Duration.Sixteenth; - break; - case '3': - note.beat.tremoloSpeed = Duration.ThirtySecond; - break; + const tremolo = new TremoloPickingEffect(); + note.beat.tremoloPicking = tremolo; + tremolo.marks = Number.parseInt(c.innerText, 10); + + if ( + (c.getAttribute('type', '') === 'unmeasured' && tremolo.marks === 0) || + c.getAttribute('smufl', '') === 'buzzRoll' + ) { + tremolo.style = TremoloPickingStyle.BuzzRoll; } + break; // case 'haydn': Not supported // case 'other-element': Not supported diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts index d23426ed5..e9913a616 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -20,6 +20,7 @@ import type { TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; import type { SimileMark } from '@coderline/alphatab/model/SimileMark'; +import type { TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import type { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import type { WhammyType } from '@coderline/alphatab/model/WhammyType'; import type { TextAlign } from '@coderline/alphatab/platform/ICanvas'; @@ -397,6 +398,13 @@ export class AlphaTex1EnumMappings { ['dadoublecoda', 18] ]); public static readonly directionReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.direction); + public static readonly tremoloPickingStyle = new Map([ + ['default', 0], + ['buzzroll', 1] + ]); + public static readonly tremoloPickingStyleReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.tremoloPickingStyle + ); public static readonly keySignaturesMinorReversed = new Map([ [-7, 'abminor'], [-6, 'ebminor'], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index a10dd1dde..4224a75fc 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -759,7 +759,15 @@ export class AlphaTex1LanguageDefinitions { ], ['volume', [[[[16], 0]]]], ['balance', [[[[16], 0]]]], - ['tp', [[[[16], 0, ['8', '16', '32']]]]], + [ + 'tp', + [ + [ + [[16], 0], + [[10, 17], 1, ['default', 'buzzroll']] + ] + ] + ], [ 'barre', [ diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 19f4eaf51..a48d3c434 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -78,6 +78,7 @@ import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; +import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; @@ -1734,28 +1735,52 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler beat.automations.push(balanceAutomation); return ApplyNodeResult.Applied; case 'tp': - beat.tremoloSpeed = Duration.Eighth; + const tremolo = new TremoloPickingEffect(); + beat.tremoloPicking = tremolo; if (p.arguments && p.arguments.arguments.length > 0) { - const tremoloSpeedValue = (p.arguments!.arguments[0] as AlphaTexNumberLiteral).value; - switch (tremoloSpeedValue) { - case 8: - beat.tremoloSpeed = Duration.Eighth; - break; - case 16: - beat.tremoloSpeed = Duration.Sixteenth; - break; - case 32: - beat.tremoloSpeed = Duration.ThirtySecond; - break; - default: - importer.addSemanticDiagnostic({ - code: AlphaTexDiagnosticCode.AT209, - message: `Unexpected tremolo speed value '${tremoloSpeedValue}, expected: 8, 16 or 32`, - severity: AlphaTexDiagnosticsSeverity.Error, - start: p.arguments!.arguments[0].start, - end: p.arguments!.arguments[0].end - }); + if (p.arguments.arguments.length > 0) { + const tremoloMarks = (p.arguments!.arguments[0] as AlphaTexNumberLiteral).value; + if ( + tremoloMarks >= TremoloPickingEffect.minMarks && + tremoloMarks <= TremoloPickingEffect.maxMarks + ) { + tremolo.marks = tremoloMarks; + } else { + switch (tremoloMarks) { + // backwards compatibility + case 8: + tremolo.marks = 1; + break; + case 16: + tremolo.marks = 2; + break; + case 32: + tremolo.marks = 3; + break; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected tremolo marks value '${tremoloMarks}, expected: ${TremoloPickingEffect.minMarks}-${TremoloPickingEffect.maxMarks}, or legacy: 8, 16 or 32`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.arguments!.arguments[0].start, + end: p.arguments!.arguments[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + } + } + if (p.arguments.arguments.length > 1) { + const tremoloStyle = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.arguments!, + 'tremolo picking style', + AlphaTex1EnumMappings.tremoloPickingStyle, + 1 + ); + if (tremoloStyle === undefined) { return ApplyNodeResult.NotAppliedSemanticError; + } + tremolo.style = tremoloStyle; } } return ApplyNodeResult.Applied; @@ -2506,15 +2531,15 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler if (stylesheet.globalDisplayChordDiagramsInScore) { nodes.push(Atnf.meta('chordDiagramsInScore')); } - + if (stylesheet.hideEmptyStaves) { nodes.push(Atnf.meta('hideEmptyStaves')); } - + if (stylesheet.hideEmptyStavesInFirstSystem) { nodes.push(Atnf.meta('hideEmptyStavesInFirstSystem')); } - + if (stylesheet.showSingleStaffBrackets) { nodes.push(Atnf.meta('showSingleStaffBrackets')); } @@ -3323,7 +3348,12 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } if (beat.isTremolo) { - Atnf.prop(properties, 'tp', Atnf.numberValue(beat.tremoloSpeed! as number)); + const values: IAlphaTexArgumentValue[] = [Atnf.number(beat.tremoloPicking!.marks)]; + if (beat.tremoloPicking!.style !== TremoloPickingStyle.Default) { + values.push(Atnf.ident(TremoloPickingStyle[beat.tremoloPicking!.style])); + } + + Atnf.prop(properties, 'tp', Atnf.args(values)); } switch (beat.crescendo) { diff --git a/packages/alphatab/src/midi/MidiFileGenerator.ts b/packages/alphatab/src/midi/MidiFileGenerator.ts index 2601d1d7f..ba85eeb98 100644 --- a/packages/alphatab/src/midi/MidiFileGenerator.ts +++ b/packages/alphatab/src/midi/MidiFileGenerator.ts @@ -137,7 +137,6 @@ export class MidiFileGenerator { this._calculatedBeatTimers.clear(); this._currentTime = 0; - // initialize tracks for (const track of this._score.tracks) { this._generateTrack(track); @@ -146,7 +145,6 @@ export class MidiFileGenerator { // tickshift is added after initial track channel details this._detectTickShift(); - Logger.debug('Midi', 'Begin midi generation'); this.syncPoints = []; @@ -1992,9 +1990,16 @@ export class MidiFileGenerator { channel: number ): void { const track: Track = note.beat.voice.bar.staff.track; - let tpLength: number = MidiUtils.toTicks(note.beat.tremoloSpeed!); + const marks = note.beat.tremoloPicking!.marks; + if (marks === 0) { + return; + } + + // the marks represent the duration + let tpLength = MidiUtils.toTicks(note.beat.tremoloPicking!.duration); let tick: number = noteStart; const end: number = noteStart + noteDuration.untilTieOrSlideEnd; + while (tick + 10 < end) { // only the rest on last trill play if (tick + tpLength >= end) { diff --git a/packages/alphatab/src/model/Beat.ts b/packages/alphatab/src/model/Beat.ts index 097fb5a8e..dbb3ad81c 100644 --- a/packages/alphatab/src/model/Beat.ts +++ b/packages/alphatab/src/model/Beat.ts @@ -28,6 +28,7 @@ import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import { ElementStyle } from '@coderline/alphatab/model/ElementStyle'; +import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; /** * Lists the different modes on how beaming for a beat should be done. @@ -534,14 +535,64 @@ export class Beat { */ public pickStroke: PickStroke = PickStroke.None; + /** + * Whether this beat has a tremolo picking effect. + */ public get isTremolo(): boolean { - return !!this.tremoloSpeed; + return this.tremoloPicking !== undefined; } /** - * Gets or sets the speed of the tremolo effect. + * The tremolo picking effect. */ - public tremoloSpeed: Duration | null = null; + public tremoloPicking?: TremoloPickingEffect; + + /** + * The speed of the tremolo. + * @deprecated Set {@link tremoloPicking} instead. + */ + public get tremoloSpeed(): Duration | null { + const tremolo = this.tremoloPicking; + if (tremolo) { + return tremolo.duration; + } + return null; + } + + /** + * The speed of the tremolo. + * @deprecated Set {@link tremoloPicking} instead. + */ + public set tremoloSpeed(value: Duration | null) { + if (value === null) { + this.tremoloPicking = undefined; + return; + } + + let effect = this.tremoloPicking; + if (effect === undefined) { + effect = new TremoloPickingEffect(); + this.tremoloPicking = effect; + } + + switch (value) { + case Duration.Eighth: + effect.marks = 1; + break; + case Duration.Sixteenth: + effect.marks = 2; + break; + case Duration.ThirtySecond: + effect.marks = 3; + break; + case Duration.SixtyFourth: + effect.marks = 4; + break; + case Duration.OneHundredTwentyEighth: + effect.marks = 5; + break; + } + } /** * Gets or sets whether a crescendo/decrescendo is applied on this beat. @@ -882,6 +933,13 @@ export class Beat { this.brushDuration = 0; } + const tremolo = this.tremoloPicking; + if (tremolo !== undefined) { + if (tremolo.marks < TremoloPickingEffect.minMarks || tremolo.marks > TremoloPickingEffect.maxMarks) { + this.tremoloPicking = undefined; + } + } + const displayMode: NotationMode = !settings ? NotationMode.GuitarPro : settings.notation.notationMode; let isGradual: boolean = this.text === 'grad' || this.text === 'grad.'; if (isGradual && displayMode === NotationMode.SongBook) { diff --git a/packages/alphatab/src/model/MusicFontSymbol.ts b/packages/alphatab/src/model/MusicFontSymbol.ts index 8e52b560e..b836c651d 100644 --- a/packages/alphatab/src/model/MusicFontSymbol.ts +++ b/packages/alphatab/src/model/MusicFontSymbol.ts @@ -171,9 +171,13 @@ export enum MusicFontSymbol { TextTuplet3LongStem = 0xe202, TextTupletBracketEndLongStem = 0xe203, - Tremolo3 = 0xe222, - Tremolo2 = 0xe221, Tremolo1 = 0xe220, + Tremolo2 = 0xe221, + Tremolo3 = 0xe222, + Tremolo4 = 0xe223, + Tremolo5 = 0xe224, + + BuzzRoll = 0xe22A, Flag8thUp = 0xe240, Flag8thDown = 0xe241, diff --git a/packages/alphatab/src/model/TremoloPickingEffect.ts b/packages/alphatab/src/model/TremoloPickingEffect.ts new file mode 100644 index 000000000..88c8a8390 --- /dev/null +++ b/packages/alphatab/src/model/TremoloPickingEffect.ts @@ -0,0 +1,60 @@ +import { Duration } from '@coderline/alphatab/model/Duration'; + +/** + * The style of tremolo affecting mainly the display of the effect. + * @public + */ +export enum TremoloPickingStyle { + /** + * A classic tremolo expressed by diagonal bars on the stem. + */ + Default = 0, + /** + * A buzz roll tremolo expressed by a 'z' shaped symbol. + */ + BuzzRoll = 1 +} + +/** + * Describes a tremolo picking effect. + * @json + * @json_strict + * @cloneable + * @public + */ +export class TremoloPickingEffect { + /** + * The minimum number of marks for the tremolo picking effect to be valid. + */ + public static readonly minMarks = 0; + + /** + * The max number of marks for the tremolo picking effect to be valid. + */ + public static readonly maxMarks = 5; + + /** + * The number of marks for the tremolo. + * A mark is equal to a single bar shown for a default tremolos. + */ + public marks: number = 0; + + /** + * The style of the tremolo picking. + */ + public style: TremoloPickingStyle = TremoloPickingStyle.Default; + + /** + * The number of marks define the note value of the note repetition. + * e.g. a single mark is an 8th note. + */ + public get duration(): Duration { + let marks = this.marks; + if (marks < 1) { + marks = 1; + } + const baseDuration = Duration.Eighth as number; + const actualDuration = baseDuration * Math.pow(2, marks); + return actualDuration as Duration; + } +} diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index 2cb44098d..f6c224c2f 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -4,6 +4,7 @@ export { AutomationType, Automation, SyncPointData, type FlatSyncPoint } from '@ export { Bar, SustainPedalMarkerType, SustainPedalMarker, BarSubElement, BarStyle, BarLineStyle } from '@coderline/alphatab/model/Bar'; export { BarreShape } from '@coderline/alphatab/model/BarreShape'; export { Beat, BeatBeamingMode, BeatSubElement, BeatStyle } from '@coderline/alphatab/model/Beat'; +export { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; export { BendPoint } from '@coderline/alphatab/model/BendPoint'; export { BendStyle } from '@coderline/alphatab/model/BendStyle'; export { BendType } from '@coderline/alphatab/model/BendType'; diff --git a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts index 7c57fdee9..90de9f894 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreNoteChordGlyph.ts @@ -244,7 +244,7 @@ export class ScoreNoteChordGlyph extends ScoreNoteChordGlyphBase { } if (this.beat.isTremolo && !this.beat.deadSlapped) { - this._tremoloPicking = new TremoloPickingGlyph(0, 0, this.beat.tremoloSpeed!); + this._tremoloPicking = new TremoloPickingGlyph(0, 0, this.beat.tremoloPicking!); this._tremoloPicking.renderer = this.renderer; this._tremoloPicking.doLayout(); diff --git a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts index 77160dd1c..05aaf9182 100644 --- a/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SlashBeatGlyph.ts @@ -198,7 +198,7 @@ export class SlashBeatGlyph extends BeatOnNoteGlyphBase { this.addNormal(noteHeadGlyph); if (this.container.beat.isTremolo) { - this._tremoloPicking = new TremoloPickingGlyph(0, 0, this.container.beat.tremoloSpeed!); + this._tremoloPicking = new TremoloPickingGlyph(0, 0, this.container.beat.tremoloPicking!); this._tremoloPicking.renderer = this.renderer; this._tremoloPicking.doLayout(); diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts index c0feb640e..da19582b6 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatGlyph.ts @@ -122,8 +122,7 @@ export class TabBeatGlyph extends BeatOnNoteGlyphBase { // // Tremolo Picking if (this.container.beat.isTremolo && !beatEffects.has('tremolo')) { - const speed = this.container.beat.tremoloSpeed!; - const glyph = new TremoloPickingGlyph(0, 0, speed); + const glyph = new TremoloPickingGlyph(0, 0, this.container.beat.tremoloPicking!); glyph.offsetY = this.renderer.smuflMetrics.glyphTop.get(glyph.symbol)!; beatEffects.set('tremolo', glyph); centeredEffectGlyphs.push(glyph); diff --git a/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts index 7322dfcc9..1e9b03d90 100644 --- a/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabNoteChordGlyph.ts @@ -109,7 +109,7 @@ export class TabNoteChordGlyph extends Glyph { if (beat.duration <= Duration.Quarter) { return 0; } - const symbol = TremoloPickingGlyph._getSymbol(beat.tremoloSpeed!); + const symbol = TremoloPickingGlyph._getSymbol(beat.tremoloPicking!); const smufl = this.renderer.smuflMetrics; return smufl.glyphHeights.has(symbol) ? smufl.glyphHeights.get(symbol)! : 0; } diff --git a/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts b/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts index 74f23618c..a02cd6fee 100644 --- a/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TremoloPickingGlyph.ts @@ -1,5 +1,6 @@ -import { Duration } from '@coderline/alphatab/model/Duration'; +import type { Duration } from '@coderline/alphatab/model/Duration'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; +import { type TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; import { MusicFontGlyph } from '@coderline/alphatab/rendering/glyphs/MusicFontGlyph'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; @@ -8,20 +9,28 @@ import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRende * @internal */ export class TremoloPickingGlyph extends MusicFontGlyph { - public constructor(x: number, y: number, duration: Duration) { - super(x, y, 1, TremoloPickingGlyph._getSymbol(duration)); + public constructor(x: number, y: number, effect: TremoloPickingEffect) { + super(x, y, 1, TremoloPickingGlyph._getSymbol(effect)); } - public static _getSymbol(duration: Duration): MusicFontSymbol { - switch (duration) { - case Duration.ThirtySecond: - return MusicFontSymbol.Tremolo3; - case Duration.Sixteenth: - return MusicFontSymbol.Tremolo2; - case Duration.Eighth: - return MusicFontSymbol.Tremolo1; - default: - return MusicFontSymbol.None; + public static _getSymbol(effect: TremoloPickingEffect): MusicFontSymbol { + if (effect.style === TremoloPickingStyle.BuzzRoll) { + return MusicFontSymbol.BuzzRoll; + } else { + switch (effect.marks) { + case 1: + return MusicFontSymbol.Tremolo1; + case 2: + return MusicFontSymbol.Tremolo2; + case 3: + return MusicFontSymbol.Tremolo3; + case 4: + return MusicFontSymbol.Tremolo4; + case 5: + return MusicFontSymbol.Tremolo5; + default: + return MusicFontSymbol.None; + } } } diff --git a/packages/alphatab/src/rendering/utils/BeamingHelper.ts b/packages/alphatab/src/rendering/utils/BeamingHelper.ts index c34c663df..8373dc3d0 100644 --- a/packages/alphatab/src/rendering/utils/BeamingHelper.ts +++ b/packages/alphatab/src/rendering/utils/BeamingHelper.ts @@ -53,7 +53,6 @@ export class BeamingHelper { public voice: Voice | null = null; public beats: Beat[] = []; public shortestDuration: Duration = Duration.QuadrupleWhole; - public tremoloDuration?: Duration; /** * an indicator whether any beat has a tuplet on it. @@ -184,12 +183,6 @@ export class BeamingHelper { this.hasTuplet = true; } - if (beat.isTremolo) { - if (!this.tremoloDuration || this.tremoloDuration < beat.tremoloSpeed!) { - this.tremoloDuration = beat.tremoloSpeed!; - } - } - if (beat.graceType !== GraceType.None) { this.graceType = beat.graceType; } diff --git a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex index 4e60b2d09..860044a6d 100644 --- a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex +++ b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex @@ -370,9 +370,9 @@ 7.5{pm}.8{beam Up} | // Bar 58 / Voice 1 contents - 7.5.4{tp 32 beam Up} - 8.5.4{tp 32 beam Up} - 9.5{acc #}.2{tp 32 beam Up} + 7.5.4{tp 3 beam Up} + 8.5.4{tp 3 beam Up} + 9.5{acc #}.2{tp 3 beam Up} | // Bar 59 / Voice 1 contents 7.3{tr (9 16)}.1{beam Down} diff --git a/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png b/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png index 35ea4306d..0dd679c73 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png and b/packages/alphatab/test-data/musicxml-testsuite/21g-Chords-Tremolos.png differ diff --git a/packages/alphatab/test-data/musicxml4/buzzroll.xml b/packages/alphatab/test-data/musicxml4/buzzroll.xml new file mode 100644 index 000000000..bacae7d3f --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/buzzroll.xml @@ -0,0 +1,106 @@ + + + + Title + + + + + Words + Music + Artist + Copyright + + Tab + Notices + + + + + Track 1 + T1 + + + I1 + + + 1 + 1 + 1 + 0.5 + 0.25 + + + + + + + 960 + + + G + 2 + + + + + C + 4 + + 960 + 1 + quarter + + + 0 + + + + + + C + 4 + + 960 + 1 + quarter + + + 1 + + + + + + C + 4 + + 960 + 1 + quarter + + + 0 + + + + + + C + 4 + + 960 + 1 + quarter + + + 1 + + + + + + \ No newline at end of file diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-beams.png new file mode 100644 index 000000000..6838002e3 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-flags.png new file mode 100644 index 000000000..971aab93c Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-buzzroll-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-beams.png new file mode 100644 index 000000000..83409f8ee Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-flags.png new file mode 100644 index 000000000..86f8e7746 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-standard-default-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-beams.png new file mode 100644 index 000000000..f5d4a1ad0 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-flags.png new file mode 100644 index 000000000..9f20f3cc9 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-buzzroll-flags.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-beams.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-beams.png new file mode 100644 index 000000000..7fe0bf499 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-beams.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-flags.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-flags.png new file mode 100644 index 000000000..2d478d018 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/tremolo-tabs-default-flags.png differ diff --git a/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts b/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts deleted file mode 100644 index 665af2335..000000000 --- a/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; -import type { Score } from '@coderline/alphatab/model/Score'; -import { Settings } from '@coderline/alphatab/Settings'; -import { AlphaTexExporterOld } from 'test/exporter/AlphaTexExporterOld'; -import { AlphaTexError, AlphaTexImporterOld } from 'test/importer/AlphaTexImporterOld'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; -import { TestPlatform } from 'test/TestPlatform'; -import { assert } from 'chai'; - -describe('AlphaTexExporterOldTest', () => { - async function loadScore(name: string): Promise { - const data = await TestPlatform.loadFile(`test-data/${name}`); - try { - return ScoreLoader.loadScoreFromBytes(data); - } catch { - return null; - } - } - - function parseAlphaTex(tex: string): Score { - const readerBase = new AlphaTexImporterOld(); - readerBase.initFromString(tex, new Settings()); - return readerBase.readScore(); - } - - function exportAlphaTex(score: Score, settings: Settings | null = null): string { - return new AlphaTexExporterOld().exportToString(score, settings); - } - - async function testRoundTripEqual(name: string, ignoreKeys: string[] | null = null): Promise { - const fileName = name.substring(name.lastIndexOf('/') + 1); - - let exported: string = ''; - try { - const expected = await loadScore(name); - if (!expected) { - return; - } - - ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); - - exported = exportAlphaTex(expected); - const actual = parseAlphaTex(exported); - - ComparisonHelpers.alphaTexExportRoundtripEqual(fileName, actual, expected, ignoreKeys); - } catch (e) { - let errorLine = ''; - - const error = e as Error; - if (error.cause instanceof AlphaTexError) { - const alphaTexError = error.cause as AlphaTexError; - - const lines = exported.split('\n'); - errorLine = `Error Line: ${lines[alphaTexError.line - 1]}\n`; - } - - assert.fail(`<${fileName}>${e}\n${errorLine}${error.stack}\n Tex:\n${exported}`); - } - } - - async function testRoundTripFolderEqual(name: string): Promise { - const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); - for (const file of files.filter(f => !f.endsWith('.png'))) { - await testRoundTripEqual(`${name}/${file}`, null); - } - } - - // Note: we just test all our importer and visual tests to cover all features - - it('importer', async () => { - await testRoundTripFolderEqual('guitarpro7'); - }); - - it('visual-effects-and-annotations', async () => { - await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); - }); - - it('visual-general', async () => { - await testRoundTripFolderEqual('visual-tests/general'); - }); - - it('visual-guitar-tabs', async () => { - await testRoundTripFolderEqual('visual-tests/guitar-tabs'); - }); - - it('visual-layout', async () => { - await testRoundTripFolderEqual('visual-tests/layout'); - }); - - it('visual-music-notation', async () => { - await testRoundTripFolderEqual('visual-tests/music-notation'); - }); - - it('visual-notation-legend', async () => { - await testRoundTripFolderEqual('visual-tests/notation-legend'); - }); - - it('visual-special-notes', async () => { - await testRoundTripFolderEqual('visual-tests/special-notes'); - }); - - it('visual-special-tracks', async () => { - await testRoundTripFolderEqual('visual-tests/special-tracks'); - }); - - it('gp5-to-alphaTex', async () => { - await testRoundTripEqual(`conversion/full-song.gp5`); - }); - - it('gp6-to-alphaTex', async () => { - await testRoundTripEqual(`conversion/full-song.gpx`); - }); - - it('gp7-to-alphaTex', async () => { - await testRoundTripEqual(`conversion/full-song.gp`); - }); -}); diff --git a/packages/alphatab/test/exporter/AlphaTexExporterOld.ts b/packages/alphatab/test/exporter/AlphaTexExporterOld.ts deleted file mode 100644 index c8babb884..000000000 --- a/packages/alphatab/test/exporter/AlphaTexExporterOld.ts +++ /dev/null @@ -1,1366 +0,0 @@ -/* - * This file contains a copy of the "old" alphaTex importer - * it was never released but battle tested during implementation - */ -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import { Environment } from '@coderline/alphatab/Environment'; -import { ScoreExporter } from '@coderline/alphatab/exporter/ScoreExporter'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; -import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; -import { AutomationType } from '@coderline/alphatab/model/Automation'; -import { type Bar, BarLineStyle, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; -import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; -import { BendStyle } from '@coderline/alphatab/model/BendStyle'; -import { BendType } from '@coderline/alphatab/model/BendType'; -import { BrushType } from '@coderline/alphatab/model/BrushType'; -import type { Chord } from '@coderline/alphatab/model/Chord'; -import { Clef } from '@coderline/alphatab/model/Clef'; -import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; -import { Direction } from '@coderline/alphatab/model/Direction'; -import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { FermataType } from '@coderline/alphatab/model/Fermata'; -import { Fingers } from '@coderline/alphatab/model/Fingers'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import type { MasterBar } from '@coderline/alphatab/model/MasterBar'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import type { Note } from '@coderline/alphatab/model/Note'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; -import { - BracketExtendMode, - type RenderStylesheet, - TrackNameMode, - TrackNameOrientation, - TrackNamePolicy -} from '@coderline/alphatab/model/RenderStylesheet'; -import { Score } from '@coderline/alphatab/model/Score'; -import { SimileMark } from '@coderline/alphatab/model/SimileMark'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; -import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; -import { Staff } from '@coderline/alphatab/model/Staff'; -import { Track } from '@coderline/alphatab/model/Track'; -import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import type { Voice } from '@coderline/alphatab/model/Voice'; -import { WahPedal } from '@coderline/alphatab/model/WahPedal'; -import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { BeamDirection } from '@coderline/alphatab/rendering/_barrel'; -import { Settings } from '@coderline/alphatab/Settings'; - -/** - * @internal - */ -class WriterGroup { - start: string = ''; - end: string = ''; - comment: string = ''; - hasContent: boolean = false; -} - -/** - * A small helper to write formatted alphaTex code to a string buffer. - * @internal - */ -class AlphaTexWriterOld { - public tex: string = ''; - public isStartOfLine: boolean = true; - public indentString: string = ''; - public currentIndent: number = 0; - - public comments: boolean = false; - - private _groups: WriterGroup[] = []; - private _singleLineComment: string = ''; - - public beginGroup(groupStart: string, groupEnd: string, comment: string = '') { - const group = new WriterGroup(); - group.start = groupStart; - group.end = groupEnd; - group.comment = comment; - this._groups.push(group); - } - - public writeSingleLineComment(text: string, onlyIfContent: boolean = false) { - if (this.comments && text) { - if (onlyIfContent) { - this._singleLineComment = `// ${text}`; - } else { - this.writeLine(`// ${text}`); - } - } - } - - public dropSingleLineComment() { - this._singleLineComment = ''; - } - - public writeInlineComment(text: string) { - if (this.comments && text) { - this.write(`/* ${text} */`); - } - } - - public endGroup() { - const topGroup = this._groups.pop()!; - if (topGroup.hasContent) { - this.write(topGroup.end); - } - } - - public indent() { - if (this.indentString.length > 0) { - this.currentIndent++; - } - } - - public outdent() { - if (this.indentString.length > 0) { - this.currentIndent--; - } - } - - private _preWrite() { - if (this._singleLineComment) { - const comment = this._singleLineComment; - this._singleLineComment = ''; - this.writeLine(comment); - } - - // indent if needed - if (this.isStartOfLine && this.indentString.length > 0) { - for (let i = 0; i < this.currentIndent; i++) { - this.tex += this.indentString; - } - } - this.isStartOfLine = false; - - if (this._singleLineComment) { - this.tex += this._singleLineComment; - } - - // start group - if (this._groups.length > 0) { - const groups = this._groups[this._groups.length - 1]; - if (!groups.hasContent) { - groups.hasContent = true; - this.tex += groups.start; - if (this.comments) { - this.writeInlineComment(groups.comment); - } - } - } - } - - public write(text: string) { - this._preWrite(); - this.tex += text; - this.isStartOfLine = false; - } - - public writeGroupItem(text: any) { - if (this._groups.length === 0) { - throw new AlphaTabError( - AlphaTabErrorType.General, - 'Wrong usage of writeGroupItem, this is an internal error.' - ); - } - - const hasContent = this._groups[this._groups.length - 1].hasContent; - this._preWrite(); - - if (hasContent) { - this.tex += ' '; - } - this.tex += text; - this.isStartOfLine = false; - } - - public writeString(text: string) { - this._preWrite(); - this.tex += Environment.quoteJsonString(text); - this.tex += ' '; - } - - public writeStringMeta(tag: string, value: string, writeIfEmpty: boolean = false) { - if (value.length === 0 && !writeIfEmpty) { - return; - } - - this._preWrite(); - this.tex += `\\${tag} `; - this.tex += Environment.quoteJsonString(value); - this.writeLine(); - } - - public writeMeta(tag: string, value?: string) { - this._preWrite(); - this.tex += `\\${tag} `; - if (value) { - this.tex += value; - } - this.writeLine(); - } - - public writeLine(text?: string) { - this._preWrite(); - if (text !== undefined) { - this.tex += text; - } - - // if not formatted, only add a space at the end - if (this.indentString.length > 0) { - this.tex += '\n'; - } else if (!this.tex.endsWith(' ')) { - this.tex += ' '; - } - this.isStartOfLine = true; - } -} - -/** - * This ScoreExporter can write alphaTex strings. - * @internal - */ -export class AlphaTexExporterOld extends ScoreExporter { - // used to lookup some default values. - private static readonly _defaultScore = new Score(); - private static readonly _defaultTrack = new Track(); - - public get name(): string { - return 'alphaTex (old)'; - } - - public exportToString(score: Score, settings: Settings | null = null) { - this.settings = settings ?? new Settings(); - return this.scoreToAlphaTexString(score); - } - - public writeScore(score: Score) { - const raw = IOHelper.stringToBytes(this.scoreToAlphaTexString(score)); - this.data.write(raw, 0, raw.length); - } - - public scoreToAlphaTexString(score: Score): string { - const writer = new AlphaTexWriterOld(); - writer.comments = this.settings.exporter.comments; - writer.indentString = this.settings.exporter.indent > 0 ? ' '.repeat(this.settings.exporter.indent) : ''; - this._writeScoreTo(writer, score); - return writer.tex; - } - - private _writeScoreTo(writer: AlphaTexWriterOld, score: Score) { - writer.writeSingleLineComment('Score Metadata'); - writer.writeStringMeta('album', score.album); - writer.writeStringMeta('artist', score.artist); - writer.writeStringMeta('copyright', score.copyright); - writer.writeStringMeta('instructions', score.instructions); - writer.writeStringMeta('music', score.music); - writer.writeStringMeta('notices', score.notices); - writer.writeStringMeta('subtitle', score.subTitle); - writer.writeStringMeta('title', score.title); - writer.writeStringMeta('words', score.words); - writer.writeStringMeta('tab', score.tab); - writer.write(`\\tempo ${score.tempo} `); - if (score.tempoLabel) { - writer.writeString(score.tempoLabel); - } - writer.writeLine(); - - if (score.defaultSystemsLayout !== AlphaTexExporterOld._defaultScore.defaultSystemsLayout) { - writer.writeMeta('defaultSystemsLayout', `${score.defaultSystemsLayout}`); - } - if (score.systemsLayout.length > 0) { - writer.writeMeta('systemsLayout', score.systemsLayout.join(' ')); - } - - this._writeStyleSheetTo(writer, score.stylesheet); - writer.writeLine('.'); - - // Unsupported: - // - style - - for (const track of score.tracks) { - writer.writeLine(); - this._writeTrackTo(writer, track); - } - - const flatSyncPoints = score.exportFlatSyncPoints(); - if (flatSyncPoints.length > 0) { - writer.writeLine('.'); - for (const p of flatSyncPoints) { - if (p.barPosition > 0) { - writer.writeMeta('sync', `${p.barIndex} ${p.barOccurence} ${p.millisecondOffset}`); - } else { - writer.writeMeta('sync', `${p.barIndex} ${p.barOccurence} ${p.millisecondOffset} ${p.barPosition}`); - } - } - } - } - - private _writeStyleSheetTo(writer: AlphaTexWriterOld, stylesheet: RenderStylesheet) { - writer.writeSingleLineComment('Score Stylesheet'); - if (stylesheet.hideDynamics) { - writer.writeMeta('hideDynamics'); - } - if (stylesheet.bracketExtendMode !== AlphaTexExporterOld._defaultScore.stylesheet.bracketExtendMode) { - writer.writeMeta('bracketExtendMode', BracketExtendMode[stylesheet.bracketExtendMode]); - } - if (stylesheet.useSystemSignSeparator) { - writer.writeMeta('useSystemSignSeparator'); - } - if (stylesheet.multiTrackMultiBarRest) { - writer.writeMeta('multiBarRest'); - } - if ( - stylesheet.singleTrackTrackNamePolicy !== - AlphaTexExporterOld._defaultScore.stylesheet.singleTrackTrackNamePolicy - ) { - writer.writeMeta('singleTrackTrackNamePolicy', TrackNamePolicy[stylesheet.singleTrackTrackNamePolicy]); - } - if ( - stylesheet.multiTrackTrackNamePolicy !== AlphaTexExporterOld._defaultScore.stylesheet.multiTrackTrackNamePolicy - ) { - writer.writeMeta('multiTrackTrackNamePolicy', TrackNamePolicy[stylesheet.multiTrackTrackNamePolicy]); - } - if (stylesheet.firstSystemTrackNameMode !== AlphaTexExporterOld._defaultScore.stylesheet.firstSystemTrackNameMode) { - writer.writeMeta('firstSystemTrackNameMode', TrackNameMode[stylesheet.firstSystemTrackNameMode]); - } - if ( - stylesheet.otherSystemsTrackNameMode !== AlphaTexExporterOld._defaultScore.stylesheet.otherSystemsTrackNameMode - ) { - writer.writeMeta('otherSystemsTrackNameMode', TrackNameMode[stylesheet.otherSystemsTrackNameMode]); - } - if ( - stylesheet.firstSystemTrackNameOrientation !== - AlphaTexExporterOld._defaultScore.stylesheet.firstSystemTrackNameOrientation - ) { - writer.writeMeta( - 'firstSystemTrackNameOrientation', - TrackNameOrientation[stylesheet.firstSystemTrackNameOrientation] - ); - } - if ( - stylesheet.otherSystemsTrackNameOrientation !== - AlphaTexExporterOld._defaultScore.stylesheet.otherSystemsTrackNameOrientation - ) { - writer.writeMeta( - 'otherSystemsTrackNameOrientation', - TrackNameOrientation[stylesheet.otherSystemsTrackNameOrientation] - ); - } - - // Unsupported: - // 'globaldisplaychorddiagramsontop', - // 'pertrackchorddiagramsontop', - // 'globaldisplaytuning', - // 'globaldisplaytuning', - // 'pertrackdisplaytuning', - // 'pertrackchorddiagramsontop', - // 'pertrackmultibarrest', - } - - private _writeTrackTo(writer: AlphaTexWriterOld, track: Track) { - writer.write('\\track '); - writer.writeString(track.name); - if (track.shortName.length > 0) { - writer.writeString(track.shortName); - } - - writer.writeLine(' {'); - writer.indent(); - - writer.writeSingleLineComment('Track Properties'); - - if (track.color.rgba !== AlphaTexExporterOld._defaultTrack.color.rgba) { - writer.write(` color `); - writer.writeString(track.color.rgba); - writer.writeLine(); - } - if (track.defaultSystemsLayout !== AlphaTexExporterOld._defaultTrack.defaultSystemsLayout) { - writer.write(` defaultSystemsLayout ${track.defaultSystemsLayout}`); - writer.writeLine(); - } - if (track.systemsLayout.length > 0) { - writer.write(` systemsLayout ${track.systemsLayout.join(' ')}`); - writer.writeLine(); - } - - writer.writeLine(` volume ${track.playbackInfo.volume}`); - writer.writeLine(` balance ${track.playbackInfo.balance}`); - - if (track.playbackInfo.isMute) { - writer.writeLine(` mute`); - } - if (track.playbackInfo.isSolo) { - writer.writeLine(` solo`); - } - - if ( - track.score.stylesheet.perTrackMultiBarRest && - track.score.stylesheet.perTrackMultiBarRest!.has(track.index) - ) { - writer.writeLine(` multibarrest`); - } - - writer.writeLine( - ` instrument ${track.isPercussion ? 'percussion' : GeneralMidi.getName(track.playbackInfo.program)}` - ); - if (track.playbackInfo.bank > 0) { - writer.writeLine(` bank ${track.playbackInfo.bank}`); - } - - writer.outdent(); - writer.writeLine('}'); - - writer.indent(); - - for (const staff of track.staves) { - this._writeStaffTo(writer, staff); - } - - // Unsupported: - // - custom percussionArticulations - // - style - - writer.outdent(); - } - - private _writeStaffTo(writer: AlphaTexWriterOld, staff: Staff) { - writer.write('\\staff '); - - writer.beginGroup('{', '}', 'Staff Properties'); - if (staff.showStandardNotation) { - if (staff.standardNotationLineCount !== Staff.DefaultStandardNotationLineCount) { - writer.writeGroupItem(`score ${staff.standardNotationLineCount}`); - } else { - writer.writeGroupItem('score'); - } - } - if (staff.showTablature) { - writer.writeGroupItem('tabs'); - } - if (staff.showSlash) { - writer.writeGroupItem('slash'); - } - if (staff.showNumbered) { - writer.writeGroupItem('numbered'); - } - writer.endGroup(); - writer.writeLine(); - - writer.indent(); - - const voiceCount = Math.max(...staff.filledVoices) + 1; - for (let v = 0; v < voiceCount; v++) { - if (voiceCount > 1) { - writer.write('\\voice '); - writer.writeInlineComment(`Voice ${v + 1}`); - writer.writeLine(); - - writer.indent(); - } - - for (const bar of staff.bars) { - this._writeBarTo(writer, bar, v); - } - - if (voiceCount > 1) { - writer.outdent(); - } - } - - // Unsupported: - // - style - - writer.outdent(); - } - - private _writeBarTo(writer: AlphaTexWriterOld, bar: Bar, voiceIndex: number) { - if (bar.index > 0) { - writer.writeLine('|'); - } - - if (voiceIndex === 0) { - let anyWritten = false; - - // Staff meta on first bar - if (bar.index === 0) { - const l = writer.tex.length; - this._writeStaffMetaTo(writer, bar.staff); - anyWritten = writer.tex.length > l; - } - - // Master Bar meta on first track - if (bar.staff.index === 0 && bar.staff.track.index === 0) { - const l = writer.tex.length; - this._writeMasterBarMetaTo(writer, bar.masterBar); - anyWritten = writer.tex.length > l; - } - - if (anyWritten) { - writer.writeLine(); - } - } - - writer.writeSingleLineComment(`Bar ${bar.index + 1}`); - writer.indent(); - this._writeBarMetaTo(writer, bar); - - // Unsupported: - // - style - - if (!bar.isEmpty) { - this._writeVoiceTo(writer, bar.voices[voiceIndex]); - } else { - writer.writeSingleLineComment(`empty bar`); - } - - writer.outdent(); - } - - private _writeStaffMetaTo(writer: AlphaTexWriterOld, staff: Staff) { - writer.writeSingleLineComment(`Staff ${staff.index + 1} Metadata`); - - if (staff.capo !== 0) { - writer.writeMeta('capo', `${staff.capo}`); - } - if (staff.isPercussion) { - writer.writeMeta('articulation', 'defaults'); - } else if (staff.isStringed) { - writer.write('\\tuning'); - for (const t of staff.stringTuning.tunings) { - writer.write(` ${Tuning.getTextForTuning(t, true)}`); - } - - if ( - staff.track.score.stylesheet.perTrackDisplayTuning && - staff.track.score.stylesheet.perTrackDisplayTuning!.has(staff.track.index) - ) { - writer.write(' hide'); - } - - if (staff.stringTuning.name.length > 0) { - writer.write(' '); - writer.writeString(staff.stringTuning.name); - } - - writer.writeLine(); - } - - if (staff.transpositionPitch !== 0) { - writer.writeMeta('transpose', `${-staff.transpositionPitch}`); - } - - const defaultTransposition = ModelUtils.displayTranspositionPitches.has(staff.track.playbackInfo.program) - ? ModelUtils.displayTranspositionPitches.get(staff.track.playbackInfo.program)! - : 0; - if (staff.displayTranspositionPitch !== defaultTransposition) { - writer.writeMeta('displaytranspose', `${-staff.displayTranspositionPitch}`); - } - - writer.writeMeta('accidentals', 'auto'); - - if (staff.chords != null) { - for (const [_, chord] of staff.chords!) { - this._writeChordTo(writer, chord); - } - } - } - - private _writeChordTo(writer: AlphaTexWriterOld, c: Chord) { - writer.write('\\chord {'); - if (c.firstFret > 0) { - writer.write(`firstfret ${c.firstFret} `); - } - writer.write(`showdiagram ${c.showDiagram ? 'true' : 'false'} `); - writer.write(`showfingering ${c.showFingering ? 'true' : 'false'} `); - writer.write(`showname ${c.showName ? 'true' : 'false'} `); - if (c.barreFrets.length > 0) { - const barre = c.barreFrets.map(f => `${f}`).join(' '); - writer.write(`barre ${barre} `); - } - writer.write('} '); - - writer.writeString(c.name); - - for (let i = 0; i < c.staff.tuning.length; i++) { - const fret = i < c.strings.length ? `${c.strings[i]} ` : `x `; - writer.write(fret); - } - writer.writeLine(); - } - - private _writeMasterBarMetaTo(writer: AlphaTexWriterOld, masterBar: MasterBar) { - writer.writeSingleLineComment(`Masterbar ${masterBar.index + 1} Metadata`, true); - - if (masterBar.alternateEndings !== 0) { - writer.write('\\ae ('); - writer.write( - ModelUtils.getAlternateEndingsList(masterBar.alternateEndings) - .map(i => i + 1) - .join(' ') - ); - writer.writeLine(')'); - } - - if (masterBar.isRepeatStart) { - writer.writeMeta('ro'); - } - - if (masterBar.isRepeatEnd) { - writer.writeMeta('rc', `${masterBar.repeatCount}`); - } - - if ( - masterBar.index === 0 || - masterBar.timeSignatureCommon !== masterBar.previousMasterBar?.timeSignatureCommon || - masterBar.timeSignatureNumerator !== masterBar.previousMasterBar.timeSignatureNumerator || - masterBar.timeSignatureDenominator !== masterBar.previousMasterBar.timeSignatureDenominator - ) { - if (masterBar.timeSignatureCommon) { - writer.writeStringMeta('ts ', 'common'); - } else { - writer.writeLine(`\\ts ${masterBar.timeSignatureNumerator} ${masterBar.timeSignatureDenominator}`); - } - } - - if ( - (masterBar.index > 0 && masterBar.tripletFeel !== masterBar.previousMasterBar?.tripletFeel) || - (masterBar.index === 0 && masterBar.tripletFeel !== TripletFeel.NoTripletFeel) - ) { - writer.writeMeta('tf', TripletFeel[masterBar.tripletFeel]); - } - - if (masterBar.isFreeTime) { - writer.writeMeta('ft'); - } - - if (masterBar.section != null) { - writer.write('\\section '); - writer.writeString(masterBar.section.marker); - writer.writeString(masterBar.section.text); - writer.writeLine(); - } - - if (masterBar.isAnacrusis) { - writer.writeMeta('ac'); - } - - if (masterBar.displayScale !== 1) { - writer.writeMeta('scale', masterBar.displayScale.toFixed(3)); - } - - if (masterBar.displayWidth > 0) { - writer.writeMeta('width', `${masterBar.displayWidth}`); - } - - if (masterBar.directions) { - for (const d of masterBar.directions!) { - let jumpValue: string = Direction[d]; - if (jumpValue.startsWith('Target')) { - jumpValue = jumpValue.substring('Target'.length); - } else if (jumpValue.startsWith('Jump')) { - jumpValue = jumpValue.substring('Jump'.length); - } - writer.writeMeta('jump', jumpValue); - } - } - - for (const a of masterBar.tempoAutomations) { - writer.write(`\\tempo ( ${a.value} `); - if (a.text) { - writer.writeString(a.text); - } - writer.write(`${a.ratioPosition} `); - if (!a.isVisible) { - writer.write('hide '); - } - writer.writeLine(`)`); - } - - writer.dropSingleLineComment(); - } - - private _writeBarMetaTo(writer: AlphaTexWriterOld, bar: Bar) { - writer.writeSingleLineComment(`Bar ${bar.index + 1} Metadata`, true); - const l = writer.tex.length; - - if (bar.index === 0 || bar.clef !== bar.previousBar?.clef) { - writer.writeMeta('clef', Clef[bar.clef]); - } - - if ((bar.index === 0 && bar.clefOttava !== Ottavia.Regular) || bar.clefOttava !== bar.previousBar?.clefOttava) { - let ottava = Ottavia[bar.clefOttava]; - if (ottava.startsWith('_')) { - ottava = ottava.substring(1); - } - writer.writeMeta('ottava', ottava); - } - - if ((bar.index === 0 && bar.simileMark !== SimileMark.None) || bar.simileMark !== bar.previousBar?.simileMark) { - writer.writeMeta('simile', SimileMark[bar.simileMark]); - } - - if (bar.displayScale !== 1) { - writer.writeMeta('scale', bar.displayScale.toFixed(3)); - } - - if (bar.displayWidth > 0) { - writer.writeMeta('width', `${bar.displayWidth}`); - } - - // sustainPedals are on beat level - for (const sp of bar.sustainPedals) { - switch (sp.pedalType) { - case SustainPedalMarkerType.Down: - writer.writeMeta('spd', `${sp.ratioPosition}`); - break; - case SustainPedalMarkerType.Hold: - writer.writeMeta('sph', `${sp.ratioPosition}`); - break; - case SustainPedalMarkerType.Up: - writer.writeMeta('spu', `${sp.ratioPosition}`); - break; - } - } - - if (bar.barLineLeft !== BarLineStyle.Automatic) { - writer.writeMeta('barlineleft', BarLineStyle[bar.barLineLeft]); - } - - if (bar.barLineRight !== BarLineStyle.Automatic) { - writer.writeMeta('barlineright', BarLineStyle[bar.barLineRight]); - } - - if ( - bar.index === 0 || - bar.keySignature !== bar.previousBar!.keySignature || - bar.keySignatureType !== bar.previousBar!.keySignatureType - ) { - let ks = ''; - if (bar.keySignatureType === KeySignatureType.Minor) { - switch (bar.keySignature) { - case KeySignature.Cb: - ks = 'abminor'; - break; - case KeySignature.Gb: - ks = 'ebminor'; - break; - case KeySignature.Db: - ks = 'bbminor'; - break; - case KeySignature.Ab: - ks = 'fminor'; - break; - case KeySignature.Eb: - ks = 'cminor'; - break; - case KeySignature.Bb: - ks = 'gminor'; - break; - case KeySignature.F: - ks = 'dminor'; - break; - case KeySignature.C: - ks = 'aminor'; - break; - case KeySignature.G: - ks = 'eminor'; - break; - case KeySignature.D: - ks = 'bminor'; - break; - case KeySignature.A: - ks = 'f#minor'; - break; - case KeySignature.E: - ks = 'c#minor'; - break; - case KeySignature.B: - ks = 'g#minor'; - break; - case KeySignature.FSharp: - ks = 'd#minor'; - break; - case KeySignature.CSharp: - ks = 'a#minor'; - break; - default: - // fallback to major - ks = KeySignature[bar.keySignature]; - break; - } - } else { - switch (bar.keySignature) { - case KeySignature.FSharp: - ks = 'f#'; - break; - case KeySignature.CSharp: - ks = 'c#'; - break; - default: - ks = KeySignature[bar.keySignature]; - break; - } - } - writer.writeStringMeta('ks', ks); - } - - if (writer.tex.length > l) { - writer.writeLine(); - } - - writer.dropSingleLineComment(); - } - - private _writeVoiceTo(writer: AlphaTexWriterOld, voice: Voice) { - if (voice.isEmpty) { - writer.writeSingleLineComment(`empty voice`); - return; - } - - writer.writeSingleLineComment(`Bar ${voice.bar.index + 1} / Voice ${voice.index + 1} contents`); - - // Unsupported: - // - style - - for (const beat of voice.beats) { - this._writeBeatTo(writer, beat); - } - } - - private _writeBeatTo(writer: AlphaTexWriterOld, beat: Beat) { - // Notes - if (beat.isRest) { - writer.write('r'); - } else if (beat.notes.length === 0) { - writer.write('()'); - } else { - if (beat.notes.length > 1) { - writer.write('('); - } - - for (const note of beat.notes) { - if (note.index > 0) { - writer.write(' '); - } - this._writeNoteTo(writer, note); - } - - if (beat.notes.length > 1) { - writer.write(')'); - } - } - - writer.write(`.${beat.duration as number}`); - - // Unsupported: - // - style - - this._writeBeatEffectsTo(writer, beat); - - writer.writeLine(); - } - - private _writeBeatEffectsTo(writer: AlphaTexWriterOld, beat: Beat) { - writer.beginGroup('{', '}'); - - switch (beat.fade) { - case FadeType.FadeIn: - writer.writeGroupItem('f'); - break; - case FadeType.FadeOut: - writer.writeGroupItem('fo'); - break; - case FadeType.VolumeSwell: - writer.writeGroupItem('vs'); - break; - } - - if (beat.vibrato === VibratoType.Slight) { - writer.writeGroupItem('v'); - } else if (beat.vibrato === VibratoType.Wide) { - writer.writeGroupItem('vw'); - } - - if (beat.slap) { - writer.writeGroupItem('s'); - } - - if (beat.pop) { - writer.writeGroupItem('p'); - } - - if (beat.tap) { - writer.writeGroupItem('tt'); - } - - if (beat.dots >= 2) { - writer.writeGroupItem('dd'); - } else if (beat.dots > 0) { - writer.writeGroupItem('d'); - } - - if (beat.pickStroke === PickStroke.Up) { - writer.writeGroupItem('su'); - } else if (beat.pickStroke === PickStroke.Down) { - writer.writeGroupItem('sd'); - } - - if (beat.hasTuplet) { - writer.writeGroupItem(`tu ${beat.tupletNumerator} ${beat.tupletDenominator}`); - } - - if (beat.hasWhammyBar) { - writer.writeGroupItem('tbe'); - switch (beat.whammyBarType) { - case WhammyType.Custom: - writer.writeGroupItem('custom'); - break; - case WhammyType.Dive: - writer.writeGroupItem('dive'); - break; - case WhammyType.Dip: - writer.writeGroupItem('dip'); - break; - case WhammyType.Hold: - writer.writeGroupItem('hold'); - break; - case WhammyType.Predive: - writer.writeGroupItem('predive'); - break; - case WhammyType.PrediveDive: - writer.writeGroupItem('predivedive'); - break; - } - - switch (beat.whammyStyle) { - case BendStyle.Default: - break; - case BendStyle.Gradual: - writer.writeGroupItem('gradual'); - break; - case BendStyle.Fast: - writer.writeGroupItem('fast'); - break; - } - - writer.beginGroup('(', ')'); - - for (const p of beat.whammyBarPoints!) { - writer.writeGroupItem(` ${p.offset} ${p.value}`); - } - - writer.endGroup(); - } - - switch (beat.brushType) { - case BrushType.BrushUp: - writer.writeGroupItem(`bu ${beat.brushDuration}`); - break; - case BrushType.BrushDown: - writer.writeGroupItem(`bd ${beat.brushDuration}`); - break; - case BrushType.ArpeggioUp: - writer.writeGroupItem(`au ${beat.brushDuration}`); - break; - case BrushType.ArpeggioDown: - writer.writeGroupItem(`ad ${beat.brushDuration}`); - break; - } - - if (beat.chord != null) { - writer.writeGroupItem('ch '); - writer.writeString(beat.chord.name); - } - - if (beat.ottava !== Ottavia.Regular) { - let ottava = Ottavia[beat.ottava]; - if (ottava.startsWith('_')) { - ottava = ottava.substring(1); - } - - writer.writeGroupItem(`ot ${ottava}`); - } - - if (beat.hasRasgueado) { - writer.writeGroupItem(`rasg ${Rasgueado[beat.rasgueado]}`); - } - - if (beat.text != null) { - writer.writeGroupItem('txt '); - writer.writeString(beat.text); - } - - if (beat.lyrics != null && beat.lyrics!.length > 0) { - if (beat.lyrics.length > 1) { - for (let i = 0; i < beat.lyrics.length; i++) { - writer.writeGroupItem(`lyrics ${i} `); - writer.writeString(beat.lyrics[i]); - } - } else { - writer.writeGroupItem('lyrics '); - writer.writeString(beat.lyrics[0]); - } - } - - switch (beat.graceType) { - case GraceType.OnBeat: - writer.writeGroupItem('gr ob'); - break; - case GraceType.BeforeBeat: - writer.writeGroupItem('gr'); - break; - case GraceType.BendGrace: - writer.writeGroupItem('gr b'); - break; - } - - if (beat.isTremolo) { - writer.writeGroupItem(`tp ${beat.tremoloSpeed as number}`); - } - - switch (beat.crescendo) { - case CrescendoType.Crescendo: - writer.writeGroupItem('cre'); - break; - case CrescendoType.Decrescendo: - writer.writeGroupItem('dec'); - break; - } - - if ((beat.voice.bar.index === 0 && beat.index === 0) || beat.dynamics !== beat.previousBeat?.dynamics) { - writer.writeGroupItem(`dy ${DynamicValue[beat.dynamics].toLowerCase()}`); - } - - const fermata = beat.fermata; - if (fermata != null) { - writer.writeGroupItem(`fermata ${FermataType[fermata.type]} ${fermata.length}`); - } - - if (beat.isLegatoOrigin) { - writer.writeGroupItem('legatoorigin'); - } - - for (const automation of beat.automations) { - switch (automation.type) { - case AutomationType.Tempo: - writer.writeGroupItem(`tempo ${automation.value}`); - if (automation.text.length > 0) { - writer.write(' '); - writer.writeString(automation.text); - } - break; - case AutomationType.Volume: - writer.writeGroupItem(`volume ${automation.value}`); - break; - case AutomationType.Instrument: - if (!beat.voice.bar.staff.isPercussion) { - writer.writeGroupItem(`instrument ${GeneralMidi.getName(automation.value)}`); - } - break; - case AutomationType.Balance: - writer.writeGroupItem(`balance ${automation.value}`); - break; - } - } - - switch (beat.wahPedal) { - case WahPedal.Open: - writer.writeGroupItem(`waho`); - break; - case WahPedal.Closed: - writer.writeGroupItem(`wahc`); - break; - } - - if (beat.isBarre) { - writer.writeGroupItem(`barre ${beat.barreFret} ${BarreShape[beat.barreShape]}`); - } - - if (beat.slashed) { - writer.writeGroupItem(`slashed`); - } - - if (beat.deadSlapped) { - writer.writeGroupItem(`ds`); - } - - switch (beat.golpe) { - case GolpeType.Thumb: - writer.writeGroupItem(`glpt`); - break; - case GolpeType.Finger: - writer.writeGroupItem(`glpf`); - break; - } - - if (beat.invertBeamDirection) { - writer.writeGroupItem('beam invert'); - } else if (beat.preferredBeamDirection !== null) { - writer.writeGroupItem(`beam ${BeamDirection[beat.preferredBeamDirection!]}`); - } - - if (beat.beamingMode !== BeatBeamingMode.Auto) { - switch (beat.beamingMode) { - case BeatBeamingMode.ForceSplitToNext: - writer.writeGroupItem(`beam split`); - break; - case BeatBeamingMode.ForceMergeWithNext: - writer.writeGroupItem(`beam merge`); - break; - case BeatBeamingMode.ForceSplitOnSecondaryToNext: - writer.writeGroupItem(`beam splitsecondary`); - break; - } - } - - if (beat.showTimer) { - writer.writeGroupItem(`timer`); - } - - writer.endGroup(); - } - - private _writeNoteTo(writer: AlphaTexWriterOld, note: Note) { - if (note.index > 0) { - writer.write(' '); - } - - if (note.isPercussion) { - writer.writeString(PercussionMapper.getArticulationName(note)); - } else if (note.isPiano) { - writer.write(Tuning.getTextForTuning(note.realValueWithoutHarmonic, true)); - } else if (note.isStringed) { - writer.write(`${note.fret}`); - const stringNumber = note.beat.voice.bar.staff.tuning.length - note.string + 1; - writer.write(`.${stringNumber}`); - } else { - throw new Error('What kind of note'); - } - - // Unsupported: - // - style - - this._writeNoteEffectsTo(writer, note); - } - - private _writeNoteEffectsTo(writer: AlphaTexWriterOld, note: Note) { - writer.beginGroup('{', '}'); - - if (note.hasBend) { - writer.writeGroupItem(`be ${BendType[note.bendType]}`); - - if (note.bendStyle !== BendStyle.Default) { - writer.writeGroupItem(`${BendStyle[note.bendStyle]} `); - } - - writer.beginGroup('(', ')'); - - for (const p of note.bendPoints!) { - writer.writeGroupItem(`${p.offset} ${p.value}`); - } - - writer.endGroup(); - } - - switch (note.harmonicType) { - case HarmonicType.Natural: - writer.writeGroupItem('nh'); - break; - case HarmonicType.Artificial: - writer.writeGroupItem(`ah ${note.harmonicValue}`); - break; - case HarmonicType.Pinch: - writer.writeGroupItem(`ph ${note.harmonicValue}`); - break; - case HarmonicType.Tap: - writer.writeGroupItem(`th ${note.harmonicValue}`); - break; - case HarmonicType.Semi: - writer.writeGroupItem(`sh ${note.harmonicValue}`); - break; - case HarmonicType.Feedback: - writer.writeGroupItem(`fh ${note.harmonicValue}`); - break; - } - - if (note.showStringNumber) { - writer.writeGroupItem(`string`); - } - - if (note.isTrill) { - writer.writeGroupItem(`tr ${note.trillFret} ${note.trillSpeed as number}`); - } - - switch (note.vibrato) { - case VibratoType.Slight: - writer.writeGroupItem('v'); - break; - case VibratoType.Wide: - writer.writeGroupItem('vw'); - break; - } - - switch (note.slideInType) { - case SlideInType.IntoFromBelow: - writer.writeGroupItem('sib'); - break; - case SlideInType.IntoFromAbove: - writer.writeGroupItem('sia'); - break; - } - - switch (note.slideOutType) { - case SlideOutType.Shift: - writer.writeGroupItem('ss'); - break; - case SlideOutType.Legato: - writer.writeGroupItem('sl'); - break; - case SlideOutType.OutUp: - writer.writeGroupItem('sou'); - break; - case SlideOutType.OutDown: - writer.writeGroupItem('sod'); - break; - case SlideOutType.PickSlideDown: - writer.writeGroupItem('psd'); - break; - case SlideOutType.PickSlideUp: - writer.writeGroupItem('psu'); - break; - } - - if (note.isHammerPullOrigin) { - writer.writeGroupItem('h'); - } - - if (note.isLeftHandTapped) { - writer.writeGroupItem('lht'); - } - - if (note.isGhost) { - writer.writeGroupItem('g'); - } - - switch (note.accentuated) { - case AccentuationType.Normal: - writer.writeGroupItem('ac'); - break; - case AccentuationType.Heavy: - writer.writeGroupItem('hac'); - break; - case AccentuationType.Tenuto: - writer.writeGroupItem('ten'); - break; - } - - if (note.isPalmMute) { - writer.writeGroupItem('pm'); - } - - if (note.isStaccato) { - writer.writeGroupItem('st'); - } - - if (note.isLetRing) { - writer.writeGroupItem('lr'); - } - - if (note.isDead) { - writer.writeGroupItem('x'); - } - - if (note.isTieDestination) { - writer.writeGroupItem('t'); - } - - switch (note.leftHandFinger) { - case Fingers.Thumb: - writer.writeGroupItem('lf 1'); - break; - case Fingers.IndexFinger: - writer.writeGroupItem('lf 2'); - break; - case Fingers.MiddleFinger: - writer.writeGroupItem('lf 3'); - break; - case Fingers.AnnularFinger: - writer.writeGroupItem('lf 4'); - break; - case Fingers.LittleFinger: - writer.writeGroupItem('lf 5'); - break; - } - - switch (note.rightHandFinger) { - case Fingers.Thumb: - writer.writeGroupItem('rf 1'); - break; - case Fingers.IndexFinger: - writer.writeGroupItem('rf 2'); - break; - case Fingers.MiddleFinger: - writer.writeGroupItem('rf 3'); - break; - case Fingers.AnnularFinger: - writer.writeGroupItem('rf 4'); - break; - case Fingers.LittleFinger: - writer.writeGroupItem('rf 5'); - break; - } - - if (!note.isVisible) { - writer.writeGroupItem('hide'); - } - - if (note.isSlurOrigin) { - const slurId = `s${note.id}`; - writer.writeGroupItem(`slur ${slurId}`); - } - - if (note.isSlurDestination) { - const slurId = `s${note.slurOrigin!.id}`; - writer.writeGroupItem(`slur ${slurId}`); - } - - if (note.isTrill) { - writer.writeGroupItem(`tr ${note.trillFret} ${note.trillSpeed as number}`); - } - - if (note.accidentalMode !== NoteAccidentalMode.Default) { - writer.writeGroupItem(`acc ${NoteAccidentalMode[note.accidentalMode]}`); - } - - switch (note.ornament) { - case NoteOrnament.InvertedTurn: - writer.writeGroupItem(`iturn`); - break; - case NoteOrnament.Turn: - writer.writeGroupItem(`turn`); - break; - case NoteOrnament.UpperMordent: - writer.writeGroupItem(`umordent`); - break; - case NoteOrnament.LowerMordent: - writer.writeGroupItem(`lmordent`); - break; - } - - writer.endGroup(); - } -} diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 8c389a2a9..d46f1a638 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -52,6 +52,7 @@ import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; import { assert, expect } from 'chai'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { TremoloPickingEffectSerializer } from '@coderline/alphatab/generated/model/TremoloPickingEffectSerializer'; describe('AlphaTexImporterTest', () => { /** @@ -236,7 +237,7 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isTremolo).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].tremoloSpeed).to.equal(Duration.Sixteenth); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].tremoloPicking!.marks).to.equal(2); }); it('brushes-arpeggio', () => { @@ -2350,7 +2351,7 @@ describe('AlphaTexImporterTest', () => { it('articulation', () => importErrorTest('\\articulation "Test" 0')); it('duration tuplet', () => importErrorTest('. :4 {tu 0}')); it('beat tuplet', () => importErrorTest('. C4 {tu 0}')); - it('tremolo speed', () => importErrorTest('. C4 {tp 0}')); + it('tremolo speed', () => importErrorTest('. C4 {tp 10}')); it('trill', () => importErrorTest('. 3.3 {tr 4 0}')); it('textalign', () => importErrorTest('\\title "Test" "" invalid')); }); @@ -2581,4 +2582,40 @@ describe('AlphaTexImporterTest', () => { `); expect(score.stylesheet.showSingleStaffBrackets).to.be.true; }); + + describe('tremolos', () => { + function test(tex: string) { + const score = parseTex(tex); + const beat = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; + const serialized = TremoloPickingEffectSerializer.toJson(beat.tremoloPicking!); + expect(serialized).toMatchSnapshot(); + testExportRoundtrip(score); + } + + // simple + it('tremolo1', () => test(`C4 {tp 1}`)); + it('tremolo2', () => test(`C4 {tp 2}`)); + it('tremolo3', () => test(`C4 {tp 3}`)); + it('tremolo4', () => test(`C4 {tp 4}`)); + it('tremolo5', () => test(`C4 {tp 5}`)); + + // backwards compatibility + it('tremolo8', () => test(`C4 {tp 8}`)); + it('tremolo16', () => test(`C4 {tp 16}`)); + it('tremolo32', () => test(`C4 {tp 32}`)); + + // with default style + it('tremolo-default1', () => test(`C4 {tp (1 default)}`)); + it('tremolo-default2', () => test(`C4 {tp (2 default)}`)); + it('tremolo-default3', () => test(`C4 {tp (3 default)}`)); + it('tremolo-default4', () => test(`C4 {tp (4 default)}`)); + it('tremolo-default5', () => test(`C4 {tp (5 default)}`)); + + // buzzroll + it('buzzroll-default1', () => test(`C4 {tp (1 buzzRoll)}`)); + it('buzzroll-default2', () => test(`C4 {tp (2 buzzRoll)}`)); + it('buzzroll-default3', () => test(`C4 {tp (3 buzzRoll)}`)); + it('buzzroll-default4', () => test(`C4 {tp (4 buzzRoll)}`)); + it('buzzroll-default5', () => test(`C4 {tp (5 buzzRoll)}`)); + }); }); diff --git a/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts b/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts deleted file mode 100644 index 92455e4b0..000000000 --- a/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts +++ /dev/null @@ -1,2401 +0,0 @@ -import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; -import { AutomationType } from '@coderline/alphatab/model/Automation'; -import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; -import { BendStyle } from '@coderline/alphatab/model/BendStyle'; -import { BendType } from '@coderline/alphatab/model/BendType'; -import { BrushType } from '@coderline/alphatab/model/BrushType'; -import { Clef } from '@coderline/alphatab/model/Clef'; -import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; -import { Direction } from '@coderline/alphatab/model/Direction'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { FermataType } from '@coderline/alphatab/model/Fermata'; -import { Fingers } from '@coderline/alphatab/model/Fingers'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; -import { type Score, ScoreSubElement } from '@coderline/alphatab/model/Score'; -import { SimileMark } from '@coderline/alphatab/model/SimileMark'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; -import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import type { Track } from '@coderline/alphatab/model/Track'; -import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { HarmonicsEffectInfo } from '@coderline/alphatab/rendering/effects/HarmonicsEffectInfo'; -import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import { Settings } from '@coderline/alphatab/Settings'; -import { StaveProfile } from '@coderline/alphatab/StaveProfile'; -import { AlphaTexExporterOld } from 'test/exporter/AlphaTexExporterOld'; -import { - AlphaTexError, - AlphaTexImporterOld, - AlphaTexLexerOld, - AlphaTexSymbols -} from 'test/importer/AlphaTexImporterOld'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; -import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; -import { assert, expect } from 'chai'; - -describe('AlphaTexImporterOldTest', () => { - function parseTex(tex: string): Score { - const importer: AlphaTexImporterOld = new AlphaTexImporterOld(); - importer.initFromString(tex, new Settings()); - return importer.readScore(); - } - - // as we often add tests here for new alphaTex features, this helper - // directly allows testing the exporter via a roundtrip comparison - function testExportRoundtrip(expected: Score) { - ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); - - const exported = new AlphaTexExporterOld().exportToString(expected); - const actual = parseTex(exported); - - ComparisonHelpers.alphaTexExportRoundtripEqual('export-roundtrip', actual, expected); - } - - it('ensure-metadata-parsing-issue73', () => { - const tex = `\\title Test - \\words test - \\music alphaTab - \\copyright test - \\tempo 200 - \\instrument 30 - \\capo 2 - \\tuning G3 D2 G2 B2 D3 A4 - . - 0.5.2 1.5.4 3.4.4 | 5.3.8 5.3.8 5.3.8 5.3.8 r.2`; - - const score: Score = parseTex(tex); - expect(score.title).to.equal('Test'); - expect(score.words).to.equal('test'); - expect(score.music).to.equal('alphaTab'); - expect(score.copyright).to.equal('test'); - expect(score.tempo).to.equal(200); - expect(score.tracks.length).to.equal(1); - expect(score.tracks[0].playbackInfo.program).to.equal(30); - expect(score.tracks[0].staves[0].capo).to.equal(2); - expect(score.tracks[0].staves[0].tuning.join(',')).to.equal('55,38,43,47,50,69'); - expect(score.masterBars.length).to.equal(2); - - // bars[0] - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].fret).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].string).to.equal(3); - - // bars[1] - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].notes.length).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).to.equal(true); - }); - - it('tuning', () => { - const tex = `\\tuning E4 B3 G3 D3 A2 E2 - . - 0.5.1`; - - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].tuning.join(',')).to.equal(Tuning.getDefaultTuningFor(6)!.tunings.join(',')); - }); - - it('dead-notes1-issue79', () => { - const tex: string = ':4 x.3'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isDead).to.equal(true); - }); - - it('dead-notes2-issue79', () => { - const tex: string = ':4 3.3{x}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isDead).to.equal(true); - }); - - it('trill-issue79', () => { - const tex: string = ':4 3.3{tr 5 16}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isTrill).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillSpeed).to.equal(Duration.Sixteenth); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillFret).to.equal(5); - }); - - it('tremolo-issue79', () => { - const tex: string = ':4 3.3{tr 5 16}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isTrill).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillSpeed).to.equal(Duration.Sixteenth); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillFret).to.equal(5); - }); - - it('tremolo-picking-issue79', () => { - const tex: string = ':4 3.3{tp 16}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isTremolo).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].tremoloSpeed).to.equal(Duration.Sixteenth); - }); - - it('brushes-arpeggio', () => { - const tex: string = ` - (1.1 2.2 3.3 4.4).4{bd 60} (1.1 2.2 3.3 4.4).8{bu 60} (1.1 2.2 3.3 4.4).2{ad 60} (1.1 2.2 3.3 4.4).16{au 60} r | - (1.1 2.2 3.3 4.4).4{bd 120} (1.1 2.2 3.3 4.4).8{bu 120} (1.1 2.2 3.3 4.4).2{ad 120} (1.1 2.2 3.3 4.4).16{au 120} r | - (1.1 2.2 3.3 4.4).4{bd} (1.1 2.2 3.3 4.4).8{bu} (1.1 2.2 3.3 4.4).2{ad} (1.1 2.2 3.3 4.4).16{au} r - `; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].brushType).to.equal(BrushType.BrushDown); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].playbackDuration).to.equal(960); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].brushType).to.equal(BrushType.BrushUp); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].playbackDuration).to.equal(480); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].brushType).to.equal(BrushType.ArpeggioDown); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].playbackDuration).to.equal(1920); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].brushType).to.equal(BrushType.ArpeggioUp); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].playbackDuration).to.equal(240); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].isRest).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].brushType).to.equal(BrushType.None); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].playbackDuration).to.equal(240); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].brushDuration).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].brushType).to.equal(BrushType.BrushDown); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].brushType).to.equal(BrushType.BrushUp); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].brushType).to.equal(BrushType.ArpeggioDown); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].brushType).to.equal(BrushType.ArpeggioUp); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].brushDuration).to.equal(120); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].brushType).to.equal(BrushType.None); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].brushDuration).to.equal(0); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].brushType).to.equal(BrushType.BrushDown); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].brushDuration).to.equal(60); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].brushType).to.equal(BrushType.BrushUp); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].brushDuration).to.equal(30); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].brushType).to.equal(BrushType.ArpeggioDown); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].brushDuration).to.equal(480); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].brushType).to.equal(BrushType.ArpeggioUp); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].brushDuration).to.equal(60); - 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); - testExportRoundtrip(score); - }); - - it('hamonics-issue79', () => { - const tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicType).to.equal( - HarmonicType.Natural - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicType).to.equal( - HarmonicType.Artificial - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicType).to.equal(HarmonicType.Tap); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicType).to.equal(HarmonicType.Pinch); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicType).to.equal(HarmonicType.Semi); - }); - - it('hamonics-rendering-text-issue79', async () => { - const tex: string = ':8 3.3{nh} 3.3{ah} 3.3{th} 3.3{ph} 3.3{sh}'; - const score: Score = parseTex(tex); - - await VisualTestHelper.prepareAlphaSkia(); - const settings: Settings = new Settings(); - settings.core.engine = 'svg'; - settings.core.enableLazyLoading = false; - settings.display.staveProfile = StaveProfile.ScoreTab; - const renderer: ScoreRenderer = new ScoreRenderer(settings); - renderer.width = 970; - let svg: string = ''; - renderer.partialRenderFinished.on(r => { - svg += r.renderResult; - }); - renderer.renderScore(score, [0]); - const regexTemplate: string = ']+>\\s*{0}\\s*'; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Natural))).exec( - svg - ) - ).to.be.ok; - expect( - new RegExp( - regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Artificial)) - ).exec(svg) - ).to.be.ok; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Tap))).exec(svg) - ).to.be.ok; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Pinch))).exec(svg) - ).to.be.ok; - expect( - new RegExp(regexTemplate.replace('{0}', HarmonicsEffectInfo.harmonicToString(HarmonicType.Semi))).exec(svg) - ).to.be.ok; - }); - - it('grace-issue79', () => { - const tex: string = ':8 3.3{gr} 3.3{gr ob}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].graceType).to.equal(GraceType.BeforeBeat); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].graceType).to.equal(GraceType.OnBeat); - testExportRoundtrip(score); - }); - - it('left-hand-finger-single-note', () => { - const tex: string = ':8 3.3{lf 1} 3.3{lf 2} 3.3{lf 3} 3.3{lf 4} 3.3{lf 5}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].leftHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].leftHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].leftHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].leftHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].leftHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('right-hand-finger-single-note', () => { - const tex: string = ':8 3.3{rf 1} 3.3{rf 2} 3.3{rf 3} 3.3{rf 4} 3.3{rf 5}'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].rightHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].rightHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].rightHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].rightHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].rightHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('left-hand-finger-chord', () => { - const tex: string = ':8 (3.1{lf 1} 3.2{lf 2} 3.3{lf 3} 3.4{lf 4} 3.5{lf 5})'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].leftHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1].leftHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[2].leftHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[3].leftHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[4].leftHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('right-hand-finger-chord', () => { - const tex: string = ':8 (3.1{rf 1} 3.2{rf 2} 3.3{rf 3} 3.4{rf 4} 3.5{rf 5})'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].rightHandFinger).to.equal(Fingers.Thumb); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1].rightHandFinger).to.equal( - Fingers.IndexFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[2].rightHandFinger).to.equal( - Fingers.MiddleFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[3].rightHandFinger).to.equal( - Fingers.AnnularFinger - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[4].rightHandFinger).to.equal( - Fingers.LittleFinger - ); - testExportRoundtrip(score); - }); - - it('unstringed', () => { - const tex: string = '\\tuning piano . c4 c#4 d4 d#4 | c4 db4 d4 eb4'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(4); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].realValue).to.equal(60); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].realValue).to.equal(61); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].realValue).to.equal(62); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].realValue).to.equal(63); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].realValue).to.equal(60); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].realValue).to.equal(61); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].realValue).to.equal(62); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].isPiano).to.equal(true); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].realValue).to.equal(63); - testExportRoundtrip(score); - }); - - it('multi-staff-default-settings', () => { - const tex: string = '1.1 | 1.1 | \\staff 2.1 | 2.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(2); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].showTablature).to.be.equal(true); // default settings used - - expect(score.tracks[0].staves[1].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[1].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-staff-default-settings-braces', () => { - const tex: string = '1.1 | 1.1 | \\staff{} 2.1 | 2.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(2); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].showTablature).to.be.equal(true); // default settings used - - expect(score.tracks[0].staves[1].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[1].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('single-staff-with-setting', () => { - const tex: string = '\\staff{score} 1.1 | 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('single-staff-with-slash', () => { - const tex: string = '\\staff{slash} 1.1 | 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].staves[0].showSlash).to.be.equal(true); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(false); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('single-staff-with-score-and-slash', () => { - const tex: string = '\\staff{score slash} 1.1 | 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].staves[0].showSlash).to.be.equal(true); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-staff-with-settings', () => { - const tex = `\\staff{score} 1.1 | 1.1 | - \\staff{tabs} \\capo 2 2.1 | 2.1 | - \\staff{score tabs} \\tuning A1 D2 A2 D3 G3 B3 E4 3.1 | 3.1`; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(3); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(false); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[1].showStandardNotation).to.be.equal(false); - expect(score.tracks[0].staves[1].bars.length).to.equal(2); - expect(score.tracks[0].staves[1].capo).to.equal(2); - expect(score.tracks[0].staves[2].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[2].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[2].bars.length).to.equal(2); - expect(score.tracks[0].staves[2].tuning.length).to.equal(7); - testExportRoundtrip(score); - }); - - it('multi-track', () => { - const tex: string = '\\track "First" 1.1 | 1.1 | \\track "Second" 2.2 | 2.2'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(2); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].name).to.equal('First'); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(0); - expect(score.tracks[0].playbackInfo.secondaryChannel).to.equal(1); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[1].staves.length).to.equal(1); - expect(score.tracks[1].name).to.equal('Second'); - expect(score.tracks[1].playbackInfo.primaryChannel).to.equal(2); - expect(score.tracks[1].playbackInfo.secondaryChannel).to.equal(3); - expect(score.tracks[1].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[1].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[1].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-track-names', () => { - const tex: string = - '\\track 1.1 | 1.1 | \\track "Only Long Name" 2.2 | 2.2 | \\track "Very Long Name" "shrt" 3.3 | 3.3 '; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(3); - expect(score.masterBars.length).to.equal(2); - expect(score.tracks[0].staves.length).to.equal(1); - expect(score.tracks[0].name).to.equal(''); - expect(score.tracks[0].shortName).to.equal(''); - expect(score.tracks[0].playbackInfo.primaryChannel).to.equal(0); - expect(score.tracks[0].playbackInfo.secondaryChannel).to.equal(1); - expect(score.tracks[0].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[0].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[1].staves.length).to.equal(1); - expect(score.tracks[1].name).to.equal('Only Long Name'); - expect(score.tracks[1].shortName).to.equal('Only Long '); - expect(score.tracks[1].playbackInfo.primaryChannel).to.equal(2); - expect(score.tracks[1].playbackInfo.secondaryChannel).to.equal(3); - expect(score.tracks[1].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[1].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[1].staves[0].bars.length).to.equal(2); - expect(score.tracks[2].staves.length).to.equal(1); - expect(score.tracks[2].name).to.equal('Very Long Name'); - expect(score.tracks[2].shortName).to.equal('shrt'); - expect(score.tracks[2].playbackInfo.primaryChannel).to.equal(4); - expect(score.tracks[2].playbackInfo.secondaryChannel).to.equal(5); - expect(score.tracks[2].staves[0].showTablature).to.be.equal(true); - expect(score.tracks[2].staves[0].showStandardNotation).to.be.equal(true); - expect(score.tracks[2].staves[0].bars.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-track-multi-staff', () => { - const tex = `\\track "Piano" - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - c4 d4 e4 f4 | - - \\staff{score} \\tuning piano \\clef F4 - c2 c2 c2 c2 | - - \\track "Guitar" - \\staff{tabs} - 1.2 3.2 0.1 1.1 | - - \\track "Second Guitar" - 1.2 3.2 0.1 1.1 - `; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(3); - expect(score.masterBars.length).to.equal(1); - { - const track1: Track = score.tracks[0]; - expect(track1.name).to.equal('Piano'); - expect(track1.staves.length).to.equal(2); - expect(track1.playbackInfo.program).to.equal(0); - expect(track1.playbackInfo.primaryChannel).to.equal(0); - expect(track1.playbackInfo.secondaryChannel).to.equal(1); - { - const staff1: Staff = track1.staves[0]; - expect(staff1.showTablature).to.be.equal(false); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(0); - expect(staff1.bars.length).to.equal(1); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - { - const staff2: Staff = track1.staves[1]; - expect(staff2.showTablature).to.be.equal(false); - expect(staff2.showStandardNotation).to.be.equal(true); - expect(staff2.tuning.length).to.equal(0); - expect(staff2.bars.length).to.equal(1); - expect(staff2.bars[0].clef).to.equal(Clef.F4); - } - } - { - const track2: Track = score.tracks[1]; - expect(track2.name).to.equal('Guitar'); - expect(track2.staves.length).to.equal(1); - expect(track2.playbackInfo.program).to.equal(25); - expect(track2.playbackInfo.primaryChannel).to.equal(2); - expect(track2.playbackInfo.secondaryChannel).to.equal(3); - { - const staff1: Staff = track2.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(false); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(1); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - { - const track3: Track = score.tracks[2]; - expect(track3.name).to.equal('Second Guitar'); - expect(track3.staves.length).to.equal(1); - expect(track3.playbackInfo.program).to.equal(25); - expect(track3.playbackInfo.primaryChannel).to.equal(4); - expect(track3.playbackInfo.secondaryChannel).to.equal(5); - { - const staff1: Staff = track3.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(1); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - testExportRoundtrip(score); - }); - - it('multi-track-multi-staff-inconsistent-bars', () => { - const tex: string = ` - \\track "Piano" - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - c4 d4 e4 f4 | - - \\staff{score} \\tuning piano \\clef F4 - c2 c2 c2 c2 | c2 c2 c2 c2 | c2 c2 c2 c2 | - - \\track "Guitar" - \\staff{tabs} - 1.2 3.2 0.1 1.1 | 1.2 3.2 0.1 1.1 | - - \\track "Second Guitar" - 1.2 3.2 0.1 1.1 - `; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(3); - expect(score.masterBars.length).to.equal(3); - { - const track1: Track = score.tracks[0]; - expect(track1.name).to.equal('Piano'); - expect(track1.staves.length).to.equal(2); - expect(track1.playbackInfo.program).to.equal(0); - expect(track1.playbackInfo.primaryChannel).to.equal(0); - expect(track1.playbackInfo.secondaryChannel).to.equal(1); - { - const staff1: Staff = track1.staves[0]; - expect(staff1.showTablature).to.be.equal(false); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(0); - expect(staff1.bars.length).to.equal(3); - expect(staff1.bars[0].isEmpty).to.be.equal(false); - expect(staff1.bars[1].isEmpty).to.be.equal(true); - expect(staff1.bars[2].isEmpty).to.be.equal(true); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - { - const staff2: Staff = track1.staves[1]; - expect(staff2.showTablature).to.be.equal(false); - expect(staff2.showStandardNotation).to.be.equal(true); - expect(staff2.tuning.length).to.equal(0); - expect(staff2.bars.length).to.equal(3); - expect(staff2.bars[0].isEmpty).to.be.equal(false); - expect(staff2.bars[1].isEmpty).to.be.equal(false); - expect(staff2.bars[2].isEmpty).to.be.equal(false); - expect(staff2.bars[0].clef).to.equal(Clef.F4); - } - } - { - const track2: Track = score.tracks[1]; - expect(track2.name).to.equal('Guitar'); - expect(track2.staves.length).to.equal(1); - expect(track2.playbackInfo.program).to.equal(25); - expect(track2.playbackInfo.primaryChannel).to.equal(2); - expect(track2.playbackInfo.secondaryChannel).to.equal(3); - { - const staff1: Staff = track2.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(false); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(3); - expect(staff1.bars[0].isEmpty).to.be.equal(false); - expect(staff1.bars[1].isEmpty).to.be.equal(false); - expect(staff1.bars[2].isEmpty).to.be.equal(true); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - { - const track3: Track = score.tracks[2]; - expect(track3.name).to.equal('Second Guitar'); - expect(track3.staves.length).to.equal(1); - expect(track3.playbackInfo.program).to.equal(25); - expect(track3.playbackInfo.primaryChannel).to.equal(4); - expect(track3.playbackInfo.secondaryChannel).to.equal(5); - { - const staff1: Staff = track3.staves[0]; - expect(staff1.showTablature).to.be.equal(true); - expect(staff1.showStandardNotation).to.be.equal(true); - expect(staff1.tuning.length).to.equal(6); - expect(staff1.bars.length).to.equal(3); - expect(staff1.bars[0].isEmpty).to.be.equal(false); - expect(staff1.bars[1].isEmpty).to.be.equal(true); - expect(staff1.bars[2].isEmpty).to.be.equal(true); - expect(staff1.bars[0].clef).to.equal(Clef.G2); - } - } - testExportRoundtrip(score); - }); - - it('slides', () => { - const tex: string = '3.3{sl} 4.3 | 3.3{ss} 4.3 | 3.3{sib} 3.3{sia} 3.3{sou} 3.3{sod} | 3.3{psd} 3.3{psu}'; - const 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].slideTarget!.id).to.equal( - score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].id - ); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].slideOutType).to.equal(SlideOutType.Shift); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].slideTarget!.id).to.equal( - score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].id - ); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].notes[0].slideInType).to.equal( - SlideInType.IntoFromBelow - ); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].notes[0].slideInType).to.equal( - SlideInType.IntoFromAbove - ); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].notes[0].slideOutType).to.equal(SlideOutType.OutUp); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[3].notes[0].slideOutType).to.equal( - SlideOutType.OutDown - ); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].notes[0].slideOutType).to.equal( - SlideOutType.PickSlideDown - ); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[1].notes[0].slideOutType).to.equal( - SlideOutType.PickSlideUp - ); - testExportRoundtrip(score); - }); - - it('section', () => { - const tex: string = '\\section Intro 1.1 | 1.1 | \\section "Chorus 01" 1.1 | \\section S Solo'; - const score: Score = parseTex(tex); - expect(score.tracks.length).to.equal(1); - expect(score.masterBars.length).to.equal(4); - expect(score.masterBars[0].isSectionStart).to.be.equal(true); - expect(score.masterBars[0].section!.text).to.equal('Intro'); - expect(score.masterBars[0].section!.marker).to.equal(''); - expect(score.masterBars[1].isSectionStart).to.be.equal(false); - expect(score.masterBars[2].isSectionStart).to.be.equal(true); - expect(score.masterBars[2].section!.text).to.equal('Chorus 01'); - expect(score.masterBars[2].section!.marker).to.equal(''); - expect(score.masterBars[3].isSectionStart).to.be.equal(true); - expect(score.masterBars[3].section!.text).to.equal('Solo'); - expect(score.masterBars[3].section!.marker).to.equal('S'); - testExportRoundtrip(score); - }); - - it('key-signature', () => { - const tex: string = `:1 3.3 | \\ks C 3.3 | \\ks Cmajor 3.3 | \\ks Aminor 3.3 | - \\ks F 3.3 | \\ks bbmajor 3.3 | \\ks CMINOR 3.3 | \\ks aB 3.3 | \\ks db 3.3 | \\ks Ebminor 3.3 | - \\ks g 3.3 | \\ks Dmajor 3.3 | \\ks f#minor 3.3 | \\ks E 3.3 | \\ks Bmajor 3.3 | \\ks d#minor 3.3`; - const score: Score = parseTex(tex); - - const bars = score.tracks[0].staves[0].bars; - const expected: [KeySignature, KeySignatureType][] = [ - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Minor], - [KeySignature.F, KeySignatureType.Major], - [KeySignature.Bb, KeySignatureType.Major], - [KeySignature.Eb, KeySignatureType.Minor], - [KeySignature.Ab, KeySignatureType.Major], - [KeySignature.Db, KeySignatureType.Major], - [KeySignature.Gb, KeySignatureType.Minor], - [KeySignature.G, KeySignatureType.Major], - [KeySignature.D, KeySignatureType.Major], - [KeySignature.A, KeySignatureType.Minor], - [KeySignature.E, KeySignatureType.Major], - [KeySignature.B, KeySignatureType.Major], - [KeySignature.FSharp, KeySignatureType.Minor] - ]; - - for (let i = 0; i < expected.length; i++) { - expect(bars[i].keySignature).to.equal(expected[i][0]); - expect(bars[i].keySignatureType).to.equal(expected[i][1]); - } - testExportRoundtrip(score); - }); - - it('key-signature-multi-staff', () => { - const tex: string = ` - \\track T1 - \\staff - :1 3.3 | \\ks C 3.3 | \\ks Cmajor 3.3 | \\ks Aminor 3.3 | - \\ks F 3.3 | \\ks bbmajor 3.3 | \\ks CMINOR 3.3 | \\ks aB 3.3 | \\ks db 3.3 | \\ks Ebminor 3.3 | - \\ks g 3.3 | \\ks Dmajor 3.3 | \\ks f#minor 3.3 | \\ks E 3.3 | \\ks Bmajor 3.3 | \\ks d#minor 3.3 - \\staff - \\ks d#minor :1 3.3 | \\ks Bmajor 3.3 | \\ks E 3.3 | - \\ks f#minor 3.3 | \\ks Dmajor 3.3 | \\ks g 3.3 | \\ks Ebminor 3.3 | \\ks db 3.3 | \\ks aB 3.3 | - \\ks CMINOR 3.3 | \\ks bbmajor 3.3 | \\ks F 3.3 | \\ks Aminor 3.3 | \\ks Cmajor 3.3 | \\ks C 3.3 | \\ks C 3.3 - `; - const score: Score = parseTex(tex); - - let bars = score.tracks[0].staves[0].bars; - const expected: [KeySignature, KeySignatureType][] = [ - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Major], - [KeySignature.C, KeySignatureType.Minor], - [KeySignature.F, KeySignatureType.Major], - [KeySignature.Bb, KeySignatureType.Major], - [KeySignature.Eb, KeySignatureType.Minor], - [KeySignature.Ab, KeySignatureType.Major], - [KeySignature.Db, KeySignatureType.Major], - [KeySignature.Gb, KeySignatureType.Minor], - [KeySignature.G, KeySignatureType.Major], - [KeySignature.D, KeySignatureType.Major], - [KeySignature.A, KeySignatureType.Minor], - [KeySignature.E, KeySignatureType.Major], - [KeySignature.B, KeySignatureType.Major], - [KeySignature.FSharp, KeySignatureType.Minor] - ]; - - for (let i = 0; i < expected.length; i++) { - expect(bars[i].keySignature).to.equal(expected[i][0], `Wrong keySignature at index ${i}`); - expect(bars[i].keySignatureType).to.equal(expected[i][1], `Wrong keySignature type at index ${i}`); - } - - bars = score.tracks[0].staves[1].bars; - expected.reverse(); - for (let i = 0; i < expected.length; i++) { - expect(bars[i].keySignature).to.equal(expected[i][0], `at ${i}`); - expect(bars[i].keySignatureType).to.equal(expected[i][1], `at ${i}`); - } - testExportRoundtrip(score); - }); - - it('pop-slap-tap', () => { - const tex: string = '3.3{p} 3.3{s} 3.3{tt} r'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].pop).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].slap).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tap).to.be.equal(true); - testExportRoundtrip(score); - }); - - it('triplet-feel-numeric', () => { - const tex: string = '\\tf 0 | \\tf 1 | \\tf 2 | \\tf 3 | \\tf 4 | \\tf 5 | \\tf 6'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Dotted16th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Dotted8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Scottish16th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.Scottish8th); - testExportRoundtrip(score); - }); - - it('triplet-feel-long-names', () => { - const tex: string = - '\\tf none | \\tf triplet-16th | \\tf triplet-8th | \\tf dotted-16th | \\tf dotted-8th | \\tf scottish-16th | \\tf scottish-8th'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Dotted16th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Dotted8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Scottish16th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.Scottish8th); - testExportRoundtrip(score); - }); - - it('triplet-feel-short-names', () => { - const tex: string = '\\tf no | \\tf t16 | \\tf t8 | \\tf d16 | \\tf d8 | \\tf s16 | \\tf s8'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Dotted16th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Dotted8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Scottish16th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.Scottish8th); - testExportRoundtrip(score); - }); - - it('triplet-feel-multi-bar', () => { - const tex: string = '\\tf t16 C4 | C4 | C4 | \\tf t8 C4 | C4 | C4 | \\tf no | C4 | C4 '; - const score: Score = parseTex(tex); - expect(score.masterBars[0].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[1].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[2].tripletFeel).to.equal(TripletFeel.Triplet16th); - expect(score.masterBars[3].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[4].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[5].tripletFeel).to.equal(TripletFeel.Triplet8th); - expect(score.masterBars[6].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[7].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - expect(score.masterBars[8].tripletFeel).to.equal(TripletFeel.NoTripletFeel); - testExportRoundtrip(score); - }); - - it('tuplet-repeat', () => { - const tex: string = ':8 5.3{tu 3}*3'; - const score: Score = parseTex(tex); - const durations: Duration[] = [Duration.Eighth, Duration.Eighth, Duration.Eighth]; - const tuplets = [3, 3, 3]; - let i: number = 0; - let b: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; - while (b) { - expect(b.duration).to.equal(durations[i], `Duration on beat ${i} was wrong`); - if (tuplets[i] === 1) { - expect(b.hasTuplet).to.be.equal(false); - } else { - expect(b.tupletNumerator).to.equal(tuplets[i], `Tuplet on beat ${i} was wrong`); - } - b = b.nextBeat; - i++; - } - expect(i).to.equal(durations.length); - testExportRoundtrip(score); - }); - - it('tuplet-custom', () => { - const tex: string = ':8 5.3{tu 5 2}*5'; - const score: Score = parseTex(tex); - const tupletNumerators = [5, 5, 5, 5, 5]; - const tupletDenominators = [2, 2, 2, 2, 2]; - - let i: number = 0; - let b: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; - while (b) { - expect(b.tupletNumerator).to.equal(tupletNumerators[i], `Tuplet on beat ${i} was wrong`); - expect(b.tupletDenominator).to.equal(tupletDenominators[i], `Tuplet on beat ${i} was wrong`); - b = b.nextBeat; - i++; - } - testExportRoundtrip(score); - }); - - it('simple-anacrusis', () => { - const tex: string = '\\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isAnacrusis).to.be.equal(true); - expect(score.masterBars[0].calculateDuration()).to.equal(1920); - expect(score.masterBars[1].calculateDuration()).to.equal(3840); - testExportRoundtrip(score); - }); - - it('multi-bar-anacrusis', () => { - const tex: string = '\\ac 3.3 3.3 | \\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isAnacrusis).to.be.equal(true); - expect(score.masterBars[1].isAnacrusis).to.be.equal(true); - expect(score.masterBars[0].calculateDuration()).to.equal(1920); - expect(score.masterBars[1].calculateDuration()).to.equal(1920); - expect(score.masterBars[2].calculateDuration()).to.equal(3840); - testExportRoundtrip(score); - }); - - it('random-anacrusis', () => { - const tex: string = '\\ac 3.3 3.3 | 1.1 2.1 3.1 4.1 | \\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isAnacrusis).to.be.equal(true); - expect(score.masterBars[1].isAnacrusis).to.be.equal(false); - expect(score.masterBars[2].isAnacrusis).to.be.equal(true); - expect(score.masterBars[3].isAnacrusis).to.be.equal(false); - expect(score.masterBars[0].calculateDuration()).to.equal(1920); - expect(score.masterBars[1].calculateDuration()).to.equal(3840); - expect(score.masterBars[2].calculateDuration()).to.equal(1920); - expect(score.masterBars[3].calculateDuration()).to.equal(3840); - testExportRoundtrip(score); - }); - - it('repeat', () => { - const tex: string = - '\\ro 1.3 2.3 3.3 4.3 | 5.3 6.3 7.3 8.3 | \\rc 2 1.3 2.3 3.3 4.3 | \\ro \\rc 3 1.3 2.3 3.3 4.3 |'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isRepeatStart).to.be.equal(true); - expect(score.masterBars[1].isRepeatStart).to.be.equal(false); - expect(score.masterBars[2].isRepeatStart).to.be.equal(false); - expect(score.masterBars[3].isRepeatStart).to.be.equal(true); - expect(score.masterBars[0].repeatCount).to.equal(0); - expect(score.masterBars[1].repeatCount).to.equal(0); - expect(score.masterBars[2].repeatCount).to.equal(2); - expect(score.masterBars[3].repeatCount).to.equal(3); - testExportRoundtrip(score); - }); - - it('alternate-endings', () => { - const tex: string = '\\ro 4.3*4 | \\ae (1 2 3) 6.3*4 | \\ae 4 \\rc 4 6.3 6.3 6.3 5.3 |'; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isRepeatStart).to.be.equal(true); - expect(score.masterBars[1].isRepeatStart).to.be.equal(false); - expect(score.masterBars[2].isRepeatStart).to.be.equal(false); - expect(score.masterBars[0].repeatCount).to.equal(0); - expect(score.masterBars[1].repeatCount).to.equal(0); - expect(score.masterBars[2].repeatCount).to.equal(4); - expect(score.masterBars[0].alternateEndings).to.equal(0b0000); - expect(score.masterBars[1].alternateEndings).to.equal(0b0111); - expect(score.masterBars[2].alternateEndings).to.equal(0b1000); - testExportRoundtrip(score); - }); - - it('random-alternate-endings', () => { - const tex: string = ` - \\ro \\ae 1 1.1.1 | \\ae 2 2.1 | \\ae 3 3.1 | - 4.3.4*4 | - \\ae 1 1.1.1 | \\ae 2 2.1 | \\ae 3 3.1 | - 4.3.4*4 | - \\ae (1 3) 1.1.1 | \\ae 2 \\rc 3 2.1 | - `; - const score: Score = parseTex(tex); - expect(score.masterBars[0].isRepeatStart).to.be.equal(true); - for (let i = 1; i <= 9; i++) { - expect(score.masterBars[i].isRepeatStart).to.be.equal(false); - } - for (let i = 0; i <= 8; i++) { - expect(score.masterBars[i].repeatCount).to.equal(0); - } - expect(score.masterBars[9].repeatCount).to.equal(3); - expect(score.masterBars[0].alternateEndings).to.equal(0b001); - expect(score.masterBars[1].alternateEndings).to.equal(0b010); - expect(score.masterBars[2].alternateEndings).to.equal(0b100); - expect(score.masterBars[3].alternateEndings).to.equal(0b000); - expect(score.masterBars[4].alternateEndings).to.equal(0b001); - expect(score.masterBars[5].alternateEndings).to.equal(0b010); - expect(score.masterBars[6].alternateEndings).to.equal(0b100); - expect(score.masterBars[7].alternateEndings).to.equal(0b000); - expect(score.masterBars[8].alternateEndings).to.equal(0b101); - expect(score.masterBars[9].alternateEndings).to.equal(0b010); - testExportRoundtrip(score); - }); - - it('default-transposition-on-instruments', () => { - const tex: string = ` - \\track "Piano with Grand Staff" "pno." - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - c4 d4 e4 f4 | - \\staff{score} \\tuning piano \\clef F4 - c2 c2 c2 c2 | - \\track Guitar - \\staff{tabs} \\instrument acousticguitarsteel \\capo 5 - 1.2 3.2 0.1 1.1 - `; - const score: Score = parseTex(tex); - - expect(score.tracks[0].staves[0].transpositionPitch).to.equal(0); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); - expect(score.tracks[0].staves[1].transpositionPitch).to.equal(0); - expect(score.tracks[0].staves[1].displayTranspositionPitch).to.equal(0); - expect(score.tracks[1].staves[0].transpositionPitch).to.equal(0); - expect(score.tracks[1].staves[0].displayTranspositionPitch).to.equal(-12); - testExportRoundtrip(score); - }); - - it('dynamics', () => { - const tex: string = '1.1.8{dy ppp} 1.1{dy pp} 1.1{dy p} 1.1{dy mp} 1.1{dy mf} 1.1{dy f} 1.1{dy ff} 1.1{dy fff}'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].dynamics).to.equal(DynamicValue.P); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].dynamics).to.equal(DynamicValue.MP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].dynamics).to.equal(DynamicValue.MF); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[5].dynamics).to.equal(DynamicValue.F); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[6].dynamics).to.equal(DynamicValue.FF); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[7].dynamics).to.equal(DynamicValue.FFF); - testExportRoundtrip(score); - }); - - it('dynamics-auto', () => { - const tex: string = '1.1.4{dy ppp} 1.1 1.1{dy mp} 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].dynamics).to.equal(DynamicValue.MP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].dynamics).to.equal(DynamicValue.MP); - testExportRoundtrip(score); - }); - - it('dynamics-auto-reset-on-track', () => { - const tex: string = '1.1.4{dy ppp} 1.1 \\track "Second" 1.1.4'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[1].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.F); - testExportRoundtrip(score); - }); - - it('dynamics-auto-reset-on-staff', () => { - const tex: string = '1.1.4{dy ppp} 1.1 \\staff 1.1.4'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].dynamics).to.equal(DynamicValue.PPP); - expect(score.tracks[0].staves[1].bars[0].voices[0].beats[0].dynamics).to.equal(DynamicValue.F); - testExportRoundtrip(score); - }); - - it('crescendo', () => { - const tex: string = '1.1.4{dec} 1.1{dec} 1.1{cre} 1.1{cre}'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].crescendo).to.equal(CrescendoType.Decrescendo); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].crescendo).to.equal(CrescendoType.Decrescendo); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].crescendo).to.equal(CrescendoType.Crescendo); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].crescendo).to.equal(CrescendoType.Crescendo); - testExportRoundtrip(score); - }); - - it('left-hand-tapping', () => { - const tex: string = ':4 1.1{lht} 1.1 1.1{lht} 1.1'; - const score: Score = parseTex(tex); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isLeftHandTapped).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isLeftHandTapped).to.equal(false); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].isLeftHandTapped).to.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].isLeftHandTapped).to.equal(false); - testExportRoundtrip(score); - }); - - it('expect-invalid-format-xml', () => { - expect(() => parseTex('')).to.throw(UnsupportedFormatError); - }); - - it('expect-invalid-format-other-text', () => { - expect(() => parseTex('This is not an alphaTex file')).to.throw(UnsupportedFormatError); - }); - - it('auto-detect-tuning-from-instrument', () => { - let score = parseTex('\\instrument acousticguitarsteel . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(6); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(-12); - - score = parseTex('\\instrument acousticbass . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(4); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(-12); - - score = parseTex('\\instrument violin . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(4); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); - - score = parseTex('\\instrument acousticpiano . 3.3'); - expect(score.tracks[0].staves[0].tuning.length).to.equal(0); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); - }); - - it('multibyte-encoding', () => { - const multiByteChars = 'ēˆ±ä½ Ć–Ć„ĆœšŸŽøšŸŽµšŸŽ¶'; - const score = parseTex(`\\title "${multiByteChars}" - . - \\track "šŸŽø" - \\lyrics "Test Lyrics 🤘" - (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('🤘'); - testExportRoundtrip(score); - }); - - it('does-not-hang-on-backslash', () => { - expect(() => parseTex('\\title Test . 3.3 \\')).to.throw(UnsupportedFormatError); - }); - - it('disallows-unclosed-string', () => { - expect(() => parseTex('\\title "Test . 3.3')).to.throw(UnsupportedFormatError); - }); - - function runSectionNoteSymbolTest(noteSymbol: string) { - const score = parseTex(`1.3.4 * 4 | \\section Verse ${noteSymbol}.1 | 2.3.4*4`); - - expect(score.masterBars.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(4); - expect(score.masterBars[1].section!.text).to.equal('Verse'); - expect(score.masterBars[1].section!.marker).to.equal(''); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(1); - } - - it('does-not-interpret-note-symbols-on-section', () => { - runSectionNoteSymbolTest('r'); - runSectionNoteSymbolTest('-'); - runSectionNoteSymbolTest('x'); - }); - - it('loads-score-twice-without-hickups', () => { - const tex = `\\title Test - \\words test - \\music alphaTab - \\copyright test - \\tempo 200 - \\instrument 30 - \\capo 2 - \\tuning G3 D2 G2 B2 D3 A4 - . - 0.5.2 1.5.4 3.4.4 | 5.3.8 5.3.8 5.3.8 5.3.8 r.2`; - const importer: AlphaTexImporterOld = new AlphaTexImporterOld(); - for (const _i of [1, 2]) { - importer.initFromString(tex, new Settings()); - const score = importer.readScore(); - expect(score.title).to.equal('Test'); - expect(score.words).to.equal('test'); - expect(score.music).to.equal('alphaTab'); - expect(score.copyright).to.equal('test'); - expect(score.tempo).to.equal(200); - expect(score.tracks.length).to.equal(1); - expect(score.tracks[0].playbackInfo.program).to.equal(30); - expect(score.tracks[0].staves[0].capo).to.equal(2); - expect(score.tracks[0].staves[0].tuning.join(',')).to.equal('55,38,43,47,50,69'); - expect(score.masterBars.length).to.equal(2); - - // bars[0] - expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].fret).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].fret).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].string).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].duration).to.equal(Duration.Quarter); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].fret).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].string).to.equal(3); - - // bars[1] - expect(score.tracks[0].staves[0].bars[1].voices[0].beats.length).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[1].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[2].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].duration).to.equal(Duration.Eighth); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].fret).to.equal(5); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].notes[0].string).to.equal(4); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].notes.length).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].duration).to.equal(Duration.Half); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[4].isRest).to.equal(true); - } - }); - - it('error-shows-symbol-data', () => { - const tex = '3.3.ABC'; - expect(() => parseTex(tex)).to.throw(UnsupportedFormatError); - try { - parseTex(tex); - } catch (e) { - if (!(e instanceof UnsupportedFormatError)) { - assert.fail('Did not throw correct error'); - return; - } - if (!(e.cause instanceof AlphaTexError)) { - assert.fail('Did not contain an AlphaTexError'); - return; - } - const i = e.cause as AlphaTexError; - expect(i.expected).to.equal(AlphaTexSymbols.Number); - expect(i.message?.includes('Number')).to.be.true; - expect(i.symbol).to.equal(AlphaTexSymbols.String); - expect(i.message?.includes('String')).to.be.true; - expect(i.symbolData).to.equal('ABC'); - expect(i.message?.includes('ABC')).to.be.true; - } - }); - - it('tempo-as-float', () => { - const score = parseTex('\\tempo 112.5 .'); - expect(score.tempo).to.equal(112.5); - testExportRoundtrip(score); - }); - - it('tempo-as-float-in-bar', () => { - const score = parseTex('\\tempo 112 . 3.3.1 | \\tempo 333.3 3.3'); - expect(score.tempo).to.equal(112); - expect(score.tracks[0].staves[0].bars[1].masterBar.tempoAutomations[0]?.value).to.equal(333.3); - testExportRoundtrip(score); - }); - - 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(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - 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(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - 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(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - 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(0); - expect(score.tracks[0].percussionArticulations[0].outputMidiNumber).to.equal(49); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].percussionArticulation).to.equal(1); - expect(score.tracks[0].percussionArticulations[1].outputMidiNumber).to.equal(40); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].percussionArticulation).to.equal(2); - expect(score.tracks[0].percussionArticulations[2].outputMidiNumber).to.equal(37); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].percussionArticulation).to.equal(3); - expect(score.tracks[0].percussionArticulations[3].outputMidiNumber).to.equal(38); - testExportRoundtrip(score); - }); - - it('beat-tempo-change', () => { - const score = parseTex(` - . \\tempo 120 1.1.4 1.1 1.1{tempo 60} 1.1 | 1.1.4{tempo 100} 1.1 1.1{tempo 120} 1.1 - `); - expect(score.masterBars[0].tempoAutomations.length).to.equal(2); - expect(score.masterBars[0].tempoAutomations[0].value).to.equal(120); - expect(score.masterBars[0].tempoAutomations[0].ratioPosition).to.equal(0); - expect(score.masterBars[0].tempoAutomations[1].value).to.equal(60); - expect(score.masterBars[0].tempoAutomations[1].ratioPosition).to.equal(0.5); - testExportRoundtrip(score); - }); - - it('note-accidentals', () => { - let tex = '. \n'; - const expectedAccidentalModes: NoteAccidentalMode[] = []; - for (const [k, v] of ModelUtils.accidentalModeMapping) { - if (k) { - tex += `3.3 { acc ${k} } \n`; - expectedAccidentalModes.push(v); - } - } - - const score = parseTex(tex); - - const actualAccidentalModes: NoteAccidentalMode[] = []; - - let b: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0]; - while (b != null) { - actualAccidentalModes.push(b.notes[0].accidentalMode); - b = b.nextBeat; - } - - expect(actualAccidentalModes.join(',')).to.equal(expectedAccidentalModes.join(',')); - testExportRoundtrip(score); - }); - - it('accidental-mode', () => { - // song level - let score = parseTex('\\accidentals auto . F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - - // track level - score = parseTex('\\track "T1" F##4 | \\track "T2" \\accidentals auto F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.ForceDoubleSharp - ); - expect(score.tracks[1].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - - // staff level - score = parseTex('\\track "T1" \\staff F##4 \\staff \\accidentals auto F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.ForceDoubleSharp - ); - expect(score.tracks[0].staves[1].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - - // bar level - score = parseTex('F##4 | \\accidentals auto F##4'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.ForceDoubleSharp - ); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].notes[0].accidentalMode).to.equal( - NoteAccidentalMode.Default - ); - testExportRoundtrip(score); - }); - - it('dead-slap', () => { - const score = parseTex('r { ds }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isRest).to.be.false; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].deadSlapped).to.be.true; - testExportRoundtrip(score); - }); - - it('golpe', () => { - const score = parseTex('3.3 { glpf } 3.3 { glpt }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].golpe).to.equal(GolpeType.Finger); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].golpe).to.equal(GolpeType.Thumb); - testExportRoundtrip(score); - }); - - it('fade', () => { - const score = parseTex('3.3 { f } 3.3 { fo } 3.3 { vs } '); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].fade).to.equal(FadeType.FadeIn); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].fade).to.equal(FadeType.FadeOut); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fade).to.equal(FadeType.VolumeSwell); - testExportRoundtrip(score); - }); - - it('barre', () => { - const score = parseTex('3.3 { barre 5 } 3.3 { barre 14 half }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreFret).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreShape).to.equal(BarreShape.Full); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].barreFret).to.equal(14); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].barreShape).to.equal(BarreShape.Half); - testExportRoundtrip(score); - testExportRoundtrip(score); - }); - - it('ornaments', () => { - const score = parseTex('3.3 { turn } 3.3 { iturn } 3.3 { umordent } 3.3 { lmordent }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].ornament).to.equal(NoteOrnament.Turn); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].ornament).to.equal( - NoteOrnament.InvertedTurn - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].ornament).to.equal( - NoteOrnament.UpperMordent - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].ornament).to.equal( - NoteOrnament.LowerMordent - ); - testExportRoundtrip(score); - }); - - it('rasgueado', () => { - const score = parseTex('3.3 { rasg mi } 3.3 { rasg pmptriplet } 3.3 { rasg amianapaest }'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].rasgueado).to.equal(Rasgueado.Mi); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].hasRasgueado).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].rasgueado).to.equal(Rasgueado.PmpTriplet); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].hasRasgueado).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].rasgueado).to.equal(Rasgueado.AmiAnapaest); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].hasRasgueado).to.be.true; - testExportRoundtrip(score); - }); - - it('directions', () => { - const score = parseTex('. \\jump Segno | | \\jump DaCapoAlCoda \\jump Coda \\jump SegnoSegno '); - expect(score.masterBars[0].directions).to.be.ok; - expect(score.masterBars[0].directions).to.contain(Direction.TargetSegno); - - expect(score.masterBars[1].directions).to.not.be.ok; - - expect(score.masterBars[2].directions).to.be.ok; - expect(score.masterBars[2].directions).to.contain(Direction.JumpDaCapoAlCoda); - expect(score.masterBars[2].directions).to.contain(Direction.TargetCoda); - expect(score.masterBars[2].directions).to.contain(Direction.TargetSegnoSegno); - testExportRoundtrip(score); - }); - - it('multi-voice-full', () => { - const score = parseTex(` - \\track "Piano" - \\staff{score} \\tuning piano \\instrument acousticgrandpiano - \\voice - c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice - c3 d3 e3 f3 | c3 d3 e3 f3 - `); - - expect(score.masterBars.length).to.equal(2); - - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-voice-simple-all-voices', () => { - const score = parseTex(` - \\voice - c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice - c3 d3 e3 f3 | c3 d3 e3 f3 - `); - - expect(score.masterBars.length).to.equal(2); - - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('multi-voice-simple-skip-initial', () => { - const score = parseTex(` - c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice - c3 d3 e3 f3 | c3 d3 e3 f3 - `); - - expect(score.masterBars.length).to.equal(2); - - expect(score.tracks[0].staves[0].bars.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[1].voices.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('standard-notation-line-count', () => { - const score = parseTex(` - \\staff { score 3 } - `); - expect(score.tracks[0].staves[0].standardNotationLineCount).to.equal(3); - testExportRoundtrip(score); - }); - - it('song-metadata', () => { - const score = parseTex(` - \\title "Title\\tTitle" - \\instructions "Line1\nLine2" - . - `); - expect(score.title).to.equal('Title\tTitle'); - expect(score.instructions).to.equal('Line1\nLine2'); - testExportRoundtrip(score); - }); - - it('tempo-label', () => { - const score = parseTex(` - \\tempo 80 "Label" - . - `); - expect(score.tempo).to.equal(80); - expect(score.tempoLabel).to.equal('Label'); - testExportRoundtrip(score); - }); - - it('transpose', () => { - const score = parseTex(` - \\staff - \\displaytranspose 12 - \\transpose 6 - . - `); - expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(-12); - expect(score.tracks[0].staves[0].transpositionPitch).to.equal(-6); - testExportRoundtrip(score); - }); - - it('beat-vibrato', () => { - const score = parseTex(` - 3.3.4{v} 3.3.4{vw} - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].vibrato).to.equal(VibratoType.Slight); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].vibrato).to.equal(VibratoType.Wide); - testExportRoundtrip(score); - }); - - it('note-vibrato', () => { - const score = parseTex(` - 3.3{v}.4 3.3{vw}.4 - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].vibrato).to.equal(VibratoType.Slight); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].vibrato).to.equal(VibratoType.Wide); - testExportRoundtrip(score); - }); - - it('whammy', () => { - const score = parseTex(` - 3.3.4{ tb dive (0 -12.5) } | - 3.3.4{ tb dive gradual (0 -12.5) } | - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarType).to.equal(WhammyType.Dive); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints!.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12.5); - - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarType).to.equal(WhammyType.Dive); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyStyle).to.equal(BendStyle.Gradual); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints!.length).to.equal(2); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![0].value).to.equal(0); - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].whammyBarPoints![1].value).to.equal(-12.5); - testExportRoundtrip(score); - }); - - it('beat-ottava', () => { - const score = parseTex(` - 3.3.4{ ot 15ma } 3.3.4{ ot 8vb } - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].ottava).to.equal(Ottavia._15ma); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].ottava).to.equal(Ottavia._8vb); - testExportRoundtrip(score); - }); - - it('beat-text', () => { - const score = parseTex(` - 3.3.4{ txt "Hello World" } 3.3.4{ txt Hello } - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].text).to.equal('Hello World'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].text).to.equal('Hello'); - testExportRoundtrip(score); - }); - - it('legato-origin', () => { - const score = parseTex(` - 3.3.4{ legatoOrigin } 4.3.4 - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isLegatoOrigin).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].isLegatoDestination).to.be.true; - testExportRoundtrip(score); - }); - - it('instrument-change', () => { - const score = parseTex(` - \\instrument acousticgrandpiano - G4 G4 G4 { instrument brightacousticpiano } - `); - expect(score.tracks[0].playbackInfo.program).to.equal(0); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( - AutomationType.Instrument - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].value).to.equal(1); - testExportRoundtrip(score); - }); - - it('beat-fermata', () => { - const score = parseTex(` - G4 G4 G4 { fermata medium 4 } - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fermata).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fermata!.type).to.equal(FermataType.Medium); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fermata!.length).to.equal(4); - testExportRoundtrip(score); - }); - - it('bend-type', () => { - const score = parseTex(` - 3.3{ b bend gradual (0 4)} - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendType).to.equal(BendType.Bend); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendStyle).to.equal(BendStyle.Gradual); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].bendPoints!.length).to.equal(2); - testExportRoundtrip(score); - }); - - it('harmonic-values', () => { - const score = parseTex(` - 2.3{nh} 2.3{ah} 2.3{ah 7} 2.3{th} 2.3{th 7} 2.3{ph} 2.3{ph 7} 2.3{sh} 2.3{sh 7} 2.3{fh} 2.3{fh 7} - `); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicType).to.equal( - HarmonicType.Natural - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].harmonicValue).to.equal(2.4); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicType).to.equal( - HarmonicType.Artificial - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicType).to.equal( - HarmonicType.Artificial - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicType).to.equal(HarmonicType.Tap); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicType).to.equal(HarmonicType.Tap); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[4].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[5].notes[0].harmonicType).to.equal(HarmonicType.Pinch); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[5].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[6].notes[0].harmonicType).to.equal(HarmonicType.Pinch); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[6].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[7].notes[0].harmonicType).to.equal(HarmonicType.Semi); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[7].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[8].notes[0].harmonicType).to.equal(HarmonicType.Semi); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[8].notes[0].harmonicValue).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[9].notes[0].harmonicType).to.equal( - HarmonicType.Feedback - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[9].notes[0].harmonicValue).to.equal(0); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[10].notes[0].harmonicType).to.equal( - HarmonicType.Feedback - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[10].notes[0].harmonicValue).to.equal(7); - testExportRoundtrip(score); - }); - - it('time-signature-commons', () => { - const score = parseTex(` - \\ts common - `); - expect(score.masterBars[0].timeSignatureNumerator).to.equal(4); - expect(score.masterBars[0].timeSignatureDenominator).to.equal(4); - expect(score.masterBars[0].timeSignatureCommon).to.be.true; - testExportRoundtrip(score); - }); - - it('clef-ottava', () => { - const score = parseTex(` - \\ottava 15ma - `); - expect(score.tracks[0].staves[0].bars[0].clefOttava).to.equal(Ottavia._15ma); - testExportRoundtrip(score); - }); - - it('simile-mark', () => { - const score = parseTex(` - \\simile simple - `); - expect(score.tracks[0].staves[0].bars[0].simileMark).to.equal(SimileMark.Simple); - testExportRoundtrip(score); - }); - - it('tempo-automation-text', () => { - const score = parseTex(` - \\tempo 100 T1 - . - 3.3.4 * 4 | \\tempo 80 T2 4.3.4*4 - `); - expect(score.tempo).to.equal(100); - expect(score.tempoLabel).to.equal('T1'); - - expect(score.masterBars[1].tempoAutomations.length).to.equal(1); - expect(score.masterBars[1].tempoAutomations[0].value).to.equal(80); - expect(score.masterBars[1].tempoAutomations[0].text).to.equal('T2'); - testExportRoundtrip(score); - }); - - it('double-bar', () => { - const tex: string = '3.3 3.3 3.3 3.3 | \\db 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); - expect(score.masterBars[1].isDoubleBar).to.be.equal(true); - testExportRoundtrip(score); - }); - - it('score-options', () => { - const score = parseTex(` - \\defaultSystemsLayout 5 - \\systemsLayout 3 2 3 - \\hideDynamics - \\bracketExtendMode nobrackets - \\useSystemSignSeparator - \\singleTrackTrackNamePolicy allsystems - \\multiTrackTrackNamePolicy Hidden - \\firstSystemTrackNameMode fullname - \\otherSystemsTrackNameMode fullname - \\firstSystemTrackNameOrientation horizontal - \\otherSystemsTrackNameOrientation horizontal - . - `); - - expect(score.defaultSystemsLayout).to.equal(5); - expect(score.systemsLayout.length).to.equal(3); - expect(score.systemsLayout[0]).to.equal(3); - expect(score.systemsLayout[1]).to.equal(2); - expect(score.systemsLayout[2]).to.equal(3); - expect(score.stylesheet.hideDynamics).to.be.true; - expect(score.stylesheet.bracketExtendMode).to.equal(BracketExtendMode.NoBrackets); - expect(score.stylesheet.useSystemSignSeparator).to.be.true; - expect(score.stylesheet.singleTrackTrackNamePolicy).to.equal(TrackNamePolicy.AllSystems); - expect(score.stylesheet.multiTrackTrackNamePolicy).to.equal(TrackNamePolicy.Hidden); - expect(score.stylesheet.firstSystemTrackNameMode).to.equal(TrackNameMode.FullName); - expect(score.stylesheet.otherSystemsTrackNameMode).to.equal(TrackNameMode.FullName); - expect(score.stylesheet.firstSystemTrackNameOrientation).to.equal(TrackNameOrientation.Horizontal); - expect(score.stylesheet.otherSystemsTrackNameOrientation).to.equal(TrackNameOrientation.Horizontal); - testExportRoundtrip(score); - }); - - it('bar-sizing', () => { - const score = parseTex(` - 3.3.4 | \\scale 0.5 3.3.4 | \\width 300 3.3.4 - `); - - expect(score.masterBars[1].displayScale).to.equal(0.5); - expect(score.masterBars[2].displayWidth).to.equal(300); - testExportRoundtrip(score); - }); - - it('track-properties', () => { - const score = parseTex(` - \\track "First" { - color "#FF0000" - defaultSystemsLayout 6 - systemsLayout 3 2 3 - volume 7 - balance 3 - mute - solo - } - `); - - expect(score.tracks[0].color.rgba).to.equal('#FF0000'); - expect(score.tracks[0].defaultSystemsLayout).to.equal(6); - expect(score.tracks[0].systemsLayout.length).to.equal(3); - expect(score.tracks[0].systemsLayout[0]).to.equal(3); - expect(score.tracks[0].systemsLayout[1]).to.equal(2); - expect(score.tracks[0].systemsLayout[0]).to.equal(3); - expect(score.tracks[0].playbackInfo.volume).to.equal(7); - expect(score.tracks[0].playbackInfo.balance).to.equal(3); - expect(score.tracks[0].playbackInfo.isMute).to.be.true; - expect(score.tracks[0].playbackInfo.isSolo).to.be.true; - testExportRoundtrip(score); - }); - - it('beat-beam', () => { - const score = parseTex(` - :8 3.3{ beam invert } 3.3 | - 3.3{ beam up } 3.3 | - 3.3{ beam down } 3.3 | - 3.3{ beam auto } 3.3 | - 3.3{ beam split } 3.3 | - 3.3{ beam merge } 3.3 | - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].invertBeamDirection).to.be.true; - expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].preferredBeamDirection).to.equal(BeamDirection.Up); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].preferredBeamDirection).to.equal( - BeamDirection.Down - ); - expect(score.tracks[0].staves[0].bars[3].voices[0].beats[0].beamingMode).to.equal(BeatBeamingMode.Auto); - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].beamingMode).to.equal( - BeatBeamingMode.ForceSplitToNext - ); - expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].beamingMode).to.equal( - BeatBeamingMode.ForceMergeWithNext - ); - testExportRoundtrip(score); - }); - - it('note-show-string', () => { - const score = parseTex(` - :8 3.3{ string } - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].showStringNumber).to.be.true; - testExportRoundtrip(score); - }); - - it('note-hide', () => { - const score = parseTex(` - :8 3.3{ hide } - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isVisible).to.be.false; - }); - - it('note-slur', () => { - const score = parseTex(` - :8 (3.3{ slur s1 } 3.4 3.5) (10.3 {slur s1} 17.4 15.5) - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isSlurOrigin).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].isSlurDestination).to.be.true; - testExportRoundtrip(score); - }); - - it('hide-tuning', () => { - const score = parseTex(` - \\track "Track 1" - \\track "Track 2" - \\staff {tabs} - \\tuning A1 D2 A2 D3 G3 B3 E4 hide - 4.1 3.1 2.1 1.1`); - - expect(score.tracks[1].staves[0].stringTuning.tunings[0]).to.equal(33); - expect(score.stylesheet.perTrackDisplayTuning).to.be.ok; - expect(score.stylesheet.perTrackDisplayTuning!.has(1)).to.be.true; - expect(score.stylesheet.perTrackDisplayTuning!.get(1)).to.be.false; - testExportRoundtrip(score); - }); - - it('clefs', () => { - const score = parseTex(` - \\clef C4 \\ottava 15ma C4 | C4 | - \\clef 0 | \\clef 48 | \\clef 60 | \\clef 65 | \\clef 43 | - \\clef Neutral | \\clef C3 | \\clef C4 | \\clef F4 | \\clef G2 | - \\clef "Neutral" | \\clef "C3" | \\clef "C4" | \\clef "F4" | \\clef "G2" | - \\clef n | \\clef alto | \\clef tenor | \\clef bass | \\clef treble | - \\clef "n" | \\clef "alto" | \\clef "tenor" | \\clef "bass" | \\clef "treble" - `); - let barIndex = 0; - expect(score.tracks[0].staves[0].bars[barIndex].clef).to.equal(Clef.C4); - expect(score.tracks[0].staves[0].bars[barIndex++].clefOttava).to.equal(Ottavia._15ma); - expect(score.tracks[0].staves[0].bars[barIndex].clef).to.equal(Clef.C4); - expect(score.tracks[0].staves[0].bars[barIndex++].clefOttava).to.equal(Ottavia._15ma); - - for (let i = 0; i < 5; i++) { - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.Neutral, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.C3, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.C4, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.F4, - `Invalid clef at index ${barIndex - 1}` - ); - expect(score.tracks[0].staves[0].bars[barIndex++].clef).to.equal( - Clef.G2, - `Invalid clef at index ${barIndex - 1}` - ); - } - - testExportRoundtrip(score); - }); - - it('multibar-rest', () => { - const score = parseTex(` - \\multiBarRest - . - \\track A { multiBarRest } - 3.3 - \\track B - 3.3 - - `); - expect(score.stylesheet.multiTrackMultiBarRest).to.be.true; - expect(score.stylesheet.perTrackMultiBarRest).to.be.ok; - expect(score.stylesheet.perTrackMultiBarRest!.has(0)).to.be.true; - expect(score.stylesheet.perTrackMultiBarRest!.has(1)).to.be.false; - testExportRoundtrip(score); - }); - - it('header-footer', async () => { - const score = parseTex(` - \\title "Title" "Title: %TITLE%" left - \\subtitle "Subtitle" "Subtitle: %SUBTITLE%" center - \\artist "Artist" "Artist: %ARTIST%" right - \\album "Album" "Album: %ALBUM%" left - \\words "Words" "Words: %WORDS%" center - \\music "Music" "Music: %MUSIC%" right - \\wordsAndMusic "Words & Music: %MUSIC%" left - \\tab "Tab" "Transcriber: %TABBER%" center - \\copyright "Copyright" "Copyright: %COPYRIGHT%" right - \\copyright2 "Copyright2" right - . - `); - - expect(score.style).to.be.ok; - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Title)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.template).to.equal('Title: %TITLE%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Title)!.textAlign).to.equal(TextAlign.Left); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.SubTitle)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.SubTitle)!.template).to.equal('Subtitle: %SUBTITLE%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.SubTitle)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.SubTitle)!.textAlign).to.equal(TextAlign.Center); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Artist)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.template).to.equal('Artist: %ARTIST%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Artist)!.textAlign).to.equal(TextAlign.Right); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Album)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Album)!.template).to.equal('Album: %ALBUM%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Album)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Album)!.textAlign).to.equal(TextAlign.Left); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Words)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.template).to.equal('Words: %WORDS%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Words)!.textAlign).to.equal(TextAlign.Center); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Music)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Music)!.template).to.equal('Music: %MUSIC%'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Music)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Music)!.textAlign).to.equal(TextAlign.Right); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.WordsAndMusic)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.template).to.equal( - 'Words & Music: %MUSIC%' - ); - expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.WordsAndMusic)!.textAlign).to.equal(TextAlign.Left); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Transcriber)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Transcriber)!.template).to.equal( - 'Transcriber: %TABBER%' - ); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Transcriber)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Transcriber)!.textAlign).to.equal(TextAlign.Center); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.Copyright)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.template).to.equal( - 'Copyright: %COPYRIGHT%' - ); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.textAlign).to.equal(TextAlign.Right); - - expect(score.style!.headerAndFooter.has(ScoreSubElement.CopyrightSecondLine)).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.template).to.equal('Copyright2'); - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.isVisible).to.be.true; - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.textAlign).to.equal( - TextAlign.Right - ); - testExportRoundtrip(score); - }); - - it('barlines', () => { - const score = parseTex(` - \\instrument piano - . - \\track "T1" - \\staff - \\barlineleft dashed - \\barlineright dotted - | - \\barlineleft heavyheavy - \\barlineright heavyheavy - - \\staff - \\barlineleft lightlight - \\barlineright lightheavy - | - \\barlineleft heavylight - \\barlineright dashed - `); - expect(score).toMatchSnapshot(); - }); - - it('sync', () => { - const score = parseTex(` - \\tempo 90 - . - 3.4.4*4 | 3.4.4*4 | - \\ro 3.4.4*4 | 3.4.4*4 | \\rc 2 3.4.4*4 | - 3.4.4*4 | 3.4.4*4 - . - \\sync 0 0 0 - \\sync 0 0 1000 0.5 - \\sync 1 0 2000 - \\sync 3 0 3000 - \\sync 3 1 4000 - \\sync 6 1 5000 - `); - - // simplify snapshot - const tracks = score.tracks; - score.tracks = []; - - expect(score).toMatchSnapshot(); - - score.tracks = tracks; - testExportRoundtrip(score); - }); - - it('sync-expect-dot', () => { - const score = parseTex(` - \\title "Prelude in D Minor" - \\artist "J.S. Bach (1685-1750)" - \\copyright "Public Domain" - \\tempo 80 - . - \\ts 3 4 - 0.4.16 (3.2 -.4) (1.1 -.4) (5.1 -.4) 1.1 3.2 1.1 3.2 2.3.8 (3.2 3.4) | - (3.2 0.4).16 (3.2 -.4) (1.1 -.4) (5.1 -.4) 1.1 3.2 1.1 3.2 2.3.8 (3.2 3.4) | - (3.2 0.4).16 (3.2 -.4) (3.1 -.4) (6.1 -.4) 3.1 3.2 3.1 3.2 3.3.8 (3.2 0.3) | - (3.2 0.4).16 (3.2 -.4) (3.1 -.4) (6.1 -.4) 3.1 3.2 3.1 3.2 3.3.8 (3.2 0.3) | - . - \\sync 0 0 0 - \\sync 0 0 1500 0.666 - \\sync 1 0 4075 0.666 - \\sync 2 0 6475 0.333 - \\sync 3 0 10223 1 - `); - - // simplify snapshot - const tracks = score.tracks; - score.tracks = []; - - expect(score).toMatchSnapshot(); - - score.tracks = tracks; - testExportRoundtrip(score); - }); - - it('tuning-name', () => { - const score = parseTex(` - \\tuning E4 B3 G3 D3 A2 E2 "Default" - `); - - expect(score.tracks[0].staves[0].stringTuning.tunings.join(',')).to.equal( - Tuning.getDefaultTuningFor(6)!.tunings.join(',') - ); - expect(score.tracks[0].staves[0].stringTuning.name).to.equal('Default'); - testExportRoundtrip(score); - }); - - it('volume-change', () => { - const score = parseTex(` - \\track "T1" { - volume 7 - } - G4 G4 { volume 8 } G4 { volume 9 } - `); - - expect(score.tracks[0].playbackInfo.volume).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].type).to.equal( - AutomationType.Volume - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].value).to.equal(8); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( - AutomationType.Volume - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].value).to.equal(9); - testExportRoundtrip(score); - }); - - it('balance-change', () => { - const score = parseTex(` - \\track "T1" { - balance 7 - } - G4 G4 { balance 8 } G4 { balance 9 } - `); - - expect(score.tracks[0].playbackInfo.balance).to.equal(7); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].type).to.equal( - AutomationType.Balance - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].automations[0].value).to.equal(8); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].type).to.equal( - AutomationType.Balance - ); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].automations[0].value).to.equal(9); - testExportRoundtrip(score); - }); - - it('beat-barre', () => { - const score = parseTex(` - 3.3.4 { barre 5 half } - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreFret).to.equal(5); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreShape).to.equal(BarreShape.Half); - testExportRoundtrip(score); - }); - - it('beat-dead-slapped', () => { - const score = parseTex(` - ().16 {ds} - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].deadSlapped).to.be.true; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).to.equal(0); - testExportRoundtrip(score); - }); - - function parseNumberOrNameTest(tex: string, allowFloats: boolean, expectedSymbols: string[]) { - const lexer = new AlphaTexLexerOld(tex); - lexer.init(allowFloats); - - const actualSymbols: string[] = []; - do { - actualSymbols.push(`${AlphaTexSymbols[lexer.sy]}(${lexer.syData})`); - lexer.sy = lexer.newSy(allowFloats); - } while (lexer.sy !== AlphaTexSymbols.Eof); - - expect(actualSymbols.join(',')).to.equal(expectedSymbols.join(',')); - } - - it('parses-numbers-and-names', () => { - parseNumberOrNameTest('1', false, ['Number(1)']); - parseNumberOrNameTest('1', true, ['Number(1)']); - - parseNumberOrNameTest('1.1', false, ['Number(1)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('1.1', true, ['Number(1.1)']); - - parseNumberOrNameTest('1.1.4', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Dot(.)', 'Number(4)']); - parseNumberOrNameTest('1.1.4', true, ['Number(1.1)', 'Dot(.)', 'Number(4)']); - - parseNumberOrNameTest('1.1 .4', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Dot(.)', 'Number(4)']); - parseNumberOrNameTest('1.1 .4', true, ['Number(1.1)', 'Dot(.)', 'Number(4)']); - - parseNumberOrNameTest('1 .1.4', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Dot(.)', 'Number(4)']); - parseNumberOrNameTest('1 .1.4', true, ['Number(1)', 'Dot(.)', 'Number(1.4)']); - - parseNumberOrNameTest('-1', false, ['Number(-1)']); - parseNumberOrNameTest('-1', true, ['Number(-1)']); - - parseNumberOrNameTest('-1.1', false, ['Number(-1)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('-1.1', true, ['Number(-1.1)']); - - parseNumberOrNameTest('-1.-1', false, ['Number(-1)', 'Dot(.)', 'Number(-1)']); - parseNumberOrNameTest('-1.-1', true, ['Number(-1)', 'Dot(.)', 'Number(-1)']); - - parseNumberOrNameTest('-.1', false, ['String(-)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('-.1', true, ['String(-)', 'Dot(.)', 'Number(1)']); - - parseNumberOrNameTest('1.1(', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'LParensis(()']); - parseNumberOrNameTest('1.1(', true, ['Number(1.1)', 'LParensis(()']); - - parseNumberOrNameTest('1.1{', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'LBrace({)']); - parseNumberOrNameTest('1.1{', true, ['Number(1.1)', 'LBrace({)']); - - parseNumberOrNameTest('1.1|', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'Pipe(|)']); - parseNumberOrNameTest('1.1|', true, ['Number(1.1)', 'Pipe(|)']); - - parseNumberOrNameTest('1.1a', false, ['Number(1)', 'Dot(.)', 'String(1a)']); - parseNumberOrNameTest('1.1a', true, ['String(1.1a)']); // ['Number(1.1a)', 'Dot(.)', 'String(1a)'] would be better but its good enough what we have - - parseNumberOrNameTest('1a.1', false, ['String(1a)', 'Dot(.)', 'Number(1)']); - parseNumberOrNameTest('1a.1', true, ['String(1a)', 'Dot(.)', 'Number(1)']); - - parseNumberOrNameTest('1.1\\test', false, ['Number(1)', 'Dot(.)', 'Number(1)', 'MetaCommand(test)']); - parseNumberOrNameTest('1.1\\test', true, ['Number(1.1)', 'MetaCommand(test)']); - }); - - it('unicode-escape', () => { - const score = parseTex(` - \\title "\\uD83D\\uDE38" - . - `); - - expect(score.title).to.equal('😸'); - }); - - it('utf16', () => { - const score = parseTex(`\\title "šŸ¤˜šŸ»" .`); - - expect(score.title).to.equal('šŸ¤˜šŸ»'); - }); - - it('beat-lyrics', () => { - const score = parseTex(` - . - 3.3.3 - 3.3.3 {lyrics "A"} - 3.3.3 {lyrics 0 "B C D"} - 3.3.3 {lyrics 0 "E" lyrics 1 "F" lyrics 2 "G"} - `); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].lyrics).to.not.be.ok; - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics!.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].lyrics![0]).to.equal('A'); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics!.length).to.equal(1); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].lyrics![0]).to.equal('B C D'); - - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics).to.be.ok; - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics!.length).to.equal(3); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics![0]).to.equal('E'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics![1]).to.equal('F'); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].lyrics![2]).to.equal('G'); - - testExportRoundtrip(score); - }); - - it('bank', () => { - const score = parseTex(` - \\track "Piano" { instrument electricpiano1} - c4 d4 e4 f4 - - \\track "Piano" { instrument electricpiano1 bank 2 } - c4 d4 e4 f4 - `); - - expect(score.tracks[0].playbackInfo.program).to.equal(4); - expect(score.tracks[0].playbackInfo.bank).to.equal(0); - - expect(score.tracks[1].playbackInfo.program).to.equal(4); - expect(score.tracks[1].playbackInfo.bank).to.equal(2); - - testExportRoundtrip(score); - }); - - it('hide-tempo', () => { - const score = parseTex(` - . - \\tempo (120 "Moderate" 0) - c4 d4 e4 f4 | - \\tempo (120 "Moderate" 0 hide) - c4 d4 e4 f4 - `); - - expect(score.masterBars[0].tempoAutomations[0].isVisible).to.be.true; - expect(score.masterBars[1].tempoAutomations[0].isVisible).to.be.false; - - testExportRoundtrip(score); - }); -}); diff --git a/packages/alphatab/test/importer/AlphaTexImporterOld.ts b/packages/alphatab/test/importer/AlphaTexImporterOld.ts deleted file mode 100644 index 5ae07a607..000000000 --- a/packages/alphatab/test/importer/AlphaTexImporterOld.ts +++ /dev/null @@ -1,3733 +0,0 @@ -import { AlphaTabError, AlphaTabErrorType } from '@coderline/alphatab/AlphaTabError'; -import { BeatCloner } from '@coderline/alphatab/generated/model/BeatCloner'; -import { AlphaTexAccidentalMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; -import { ScoreImporter } from '@coderline/alphatab/importer/ScoreImporter'; -import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; -import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; -import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import { Logger } from '@coderline/alphatab/Logger'; -import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; -import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; -import { Automation, AutomationType, type FlatSyncPoint } from '@coderline/alphatab/model/Automation'; -import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; -import { BarreShape } from '@coderline/alphatab/model/BarreShape'; -import { Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; -import { BendPoint } from '@coderline/alphatab/model/BendPoint'; -import { BendStyle } from '@coderline/alphatab/model/BendStyle'; -import { BendType } from '@coderline/alphatab/model/BendType'; -import { BrushType } from '@coderline/alphatab/model/BrushType'; -import { Chord } from '@coderline/alphatab/model/Chord'; -import { Clef } from '@coderline/alphatab/model/Clef'; -import { Color } from '@coderline/alphatab/model/Color'; -import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; -import { Direction } from '@coderline/alphatab/model/Direction'; -import { Duration } from '@coderline/alphatab/model/Duration'; -import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; -import { FadeType } from '@coderline/alphatab/model/FadeType'; -import { Fermata, FermataType } from '@coderline/alphatab/model/Fermata'; -import { Fingers } from '@coderline/alphatab/model/Fingers'; -import { GolpeType } from '@coderline/alphatab/model/GolpeType'; -import { GraceType } from '@coderline/alphatab/model/GraceType'; -import { HarmonicType } from '@coderline/alphatab/model/HarmonicType'; -import { KeySignature } from '@coderline/alphatab/model/KeySignature'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; -import { Lyrics } from '@coderline/alphatab/model/Lyrics'; -import { MasterBar } from '@coderline/alphatab/model/MasterBar'; -import { ModelUtils, type TuningParseResult } from '@coderline/alphatab/model/ModelUtils'; -import { Note } from '@coderline/alphatab/model/Note'; -import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode'; -import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; -import { Ottavia } from '@coderline/alphatab/model/Ottavia'; -import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; -import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@coderline/alphatab/model/RenderStylesheet'; -import { Score, ScoreSubElement } from '@coderline/alphatab/model/Score'; -import { Section } from '@coderline/alphatab/model/Section'; -import { SimileMark } from '@coderline/alphatab/model/SimileMark'; -import { SlideInType } from '@coderline/alphatab/model/SlideInType'; -import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; -import type { Staff } from '@coderline/alphatab/model/Staff'; -import { Track } from '@coderline/alphatab/model/Track'; -import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; -import { VibratoType } from '@coderline/alphatab/model/VibratoType'; -import { Voice } from '@coderline/alphatab/model/Voice'; -import { WahPedal } from '@coderline/alphatab/model/WahPedal'; -import { WhammyType } from '@coderline/alphatab/model/WhammyType'; -import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; -import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; -import type { Settings } from '@coderline/alphatab/Settings'; -import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; - -/** - * A list of terminals recognized by the alphaTex-parser - * @public - */ -export enum AlphaTexSymbols { - No = 0, - Eof = 1, - Number = 2, - DoubleDot = 3, - Dot = 4, - String = 5, - Tuning = 6, - LParensis = 7, - RParensis = 8, - LBrace = 9, - RBrace = 10, - Pipe = 11, - MetaCommand = 12, - Multiply = 13, - LowerThan = 14 -} - -/** - * @internal - */ -enum StaffMetaResult { - KnownStaffMeta = 0, - UnknownStaffMeta = 1, - EndOfMetaDetected = 2 -} - -/** - * @internal - */ -export class AlphaTexError extends AlphaTabError { - public position: number; - public line: number; - public col: number; - public nonTerm: string; - public expected: AlphaTexSymbols; - public symbol: AlphaTexSymbols; - public symbolData: unknown; - - public constructor( - message: string | null, - position: number, - line: number, - col: number, - nonTerm: string | null, - expected: AlphaTexSymbols | null, - symbol: AlphaTexSymbols | null, - symbolData: unknown = null - ) { - super(AlphaTabErrorType.AlphaTex, message); - this.position = position; - this.line = line; - this.col = col; - this.nonTerm = nonTerm ?? ''; - this.expected = expected ?? AlphaTexSymbols.No; - this.symbol = symbol ?? AlphaTexSymbols.No; - this.symbolData = symbolData; - Object.setPrototypeOf(this, AlphaTexError.prototype); - } - - public static symbolError( - position: number, - line: number, - col: number, - nonTerm: string, - expected: AlphaTexSymbols, - symbol: AlphaTexSymbols, - symbolData: unknown = null - ): AlphaTexError { - let message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): Error on block ${nonTerm}`; - if (expected !== symbol) { - message += `, expected a ${AlphaTexSymbols[expected]} found a ${AlphaTexSymbols[symbol]}`; - if (symbolData !== null) { - message += `: '${symbolData}'`; - } - } else { - message += `, invalid value: '${symbolData}'`; - } - return new AlphaTexError(message, position, line, col, nonTerm, expected, symbol, symbolData); - } - - public static errorMessage(message: string, position: number, line: number, col: number): AlphaTexError { - message = `MalFormed AlphaTex: @${position} (line ${line}, col ${col}): ${message}`; - return new AlphaTexError(message, position, line, col, null, null, null, null); - } -} - -/** - * @internal - */ -export class AlphaTexLexerOld { - private static readonly _eof: number = 0; - - private _position: number = 0; - private _line: number = 1; - private _col: number = 0; - - private _codepoints: number[]; - private _codepoint: number = AlphaTexLexerOld._eof; - - public sy: AlphaTexSymbols = AlphaTexSymbols.No; - public syData: unknown = ''; - - public lastValidSpot: number[] = [0, 1, 0]; - - public allowTuning: boolean = false; - public logErrors: boolean = true; - - public constructor(input: string) { - this._codepoints = [...IOHelper.iterateCodepoints(input)]; - } - - public init(allowFloats: boolean = false) { - this._position = 0; - this._line = 1; - this._col = 0; - this._saveValidSpot(); - - this._codepoint = this._nextCodepoint(); - this.sy = this.newSy(allowFloats); - } - - /** - * Saves the current position, line, and column. - * All parsed data until this point is assumed to be valid. - */ - private _saveValidSpot(): void { - this.lastValidSpot = [this._position, this._line, this._col]; - } - - /** - * Reads, saves, and returns the next character of the source stream. - */ - private _nextCodepoint(): number { - if (this._position < this._codepoints.length) { - this._codepoint = this._codepoints[this._position++]; - // line/col countingF - if (this._codepoint === 0x0a /* \n */) { - this._line++; - this._col = 0; - } else { - this._col++; - } - } else { - this._codepoint = AlphaTexLexerOld._eof; - } - return this._codepoint; - } - - /** - * Reads, saves, and returns the next terminal symbol. - */ - public newSy(allowFloats: boolean = false): AlphaTexSymbols { - // When a new symbol is read, the previous one is assumed to be valid. - // The valid spot is also moved forward when reading past whitespace or comments. - this._saveValidSpot(); - this.sy = AlphaTexSymbols.No; - while (this.sy === AlphaTexSymbols.No) { - this.syData = null; - - if (this._codepoint === AlphaTexLexerOld._eof) { - this.sy = AlphaTexSymbols.Eof; - } else if (AlphaTexLexerOld._isWhiteSpace(this._codepoint)) { - // skip whitespaces - this._codepoint = this._nextCodepoint(); - this._saveValidSpot(); - } else if (this._codepoint === 0x2f /* / */) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === 0x2f /* / */) { - // single line comment - while ( - this._codepoint !== 0x0d /* \r */ && - this._codepoint !== 0x0a /* \n */ && - this._codepoint !== AlphaTexLexerOld._eof - ) { - this._codepoint = this._nextCodepoint(); - } - } else if (this._codepoint === 0x2a /* * */) { - // multiline comment - while (this._codepoint !== AlphaTexLexerOld._eof) { - if (this._codepoint === 0x2a /* * */) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === 0x2f /* / */) { - this._codepoint = this._nextCodepoint(); - break; - } - } else { - this._codepoint = this._nextCodepoint(); - } - } - } else { - this._errorMessage(`Unexpected character ${String.fromCodePoint(this._codepoint)}`); - } - this._saveValidSpot(); - } else if (this._codepoint === 0x22 /* " */ || this._codepoint === 0x27 /* ' */) { - const startChar: number = this._codepoint; - this._codepoint = this._nextCodepoint(); - let s: string = ''; - this.sy = AlphaTexSymbols.String; - - let previousCodepoint: number = -1; - - while (this._codepoint !== startChar && this._codepoint !== AlphaTexLexerOld._eof) { - // escape sequences - let codepoint = -1; - - if (this._codepoint === 0x5c /* \ */) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === 0x5c /* \\ */) { - codepoint = 0x5c; - } else if (this._codepoint === startChar /* \ */) { - codepoint = startChar; - } else if (this._codepoint === 0x52 /* \R */ || this._codepoint === 0x72 /* \r */) { - codepoint = 0x0d; - } else if (this._codepoint === 0x4e /* \N */ || this._codepoint === 0x6e /* \n */) { - codepoint = 0x0a; - } else if (this._codepoint === 0x54 /* \T */ || this._codepoint === 0x74 /* \t */) { - codepoint = 0x09; - } else if (this._codepoint === 0x75 /* \u */) { - // \uXXXX - let hex = ''; - - for (let i = 0; i < 4; i++) { - this._codepoint = this._nextCodepoint(); - if (this._codepoint === AlphaTexLexerOld._eof) { - this._errorMessage('Unexpected end of escape sequence'); - } - hex += String.fromCodePoint(this._codepoint); - } - - codepoint = Number.parseInt(hex, 16); - if (Number.isNaN(codepoint)) { - this._errorMessage(`Invalid unicode value ${hex}`); - } - } else { - this._errorMessage('Unsupported escape sequence'); - } - } else { - codepoint = this._codepoint; - } - - // unicode handling - - // https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type - if (IOHelper.isLeadingSurrogate(previousCodepoint) && IOHelper.isTrailingSurrogate(codepoint)) { - codepoint = (previousCodepoint - 0xd800) * 0x400 + (codepoint - 0xdc00) + 0x10000; - s += String.fromCodePoint(codepoint); - } else if (IOHelper.isLeadingSurrogate(codepoint)) { - // only remember for next character to form a surrogate pair - } else { - // standalone leading surrogate from previous char - if (IOHelper.isLeadingSurrogate(previousCodepoint)) { - s += String.fromCodePoint(previousCodepoint); - } - - if (codepoint > 0) { - s += String.fromCodePoint(codepoint); - } - } - - previousCodepoint = codepoint; - this._codepoint = this._nextCodepoint(); - } - if (this._codepoint === AlphaTexLexerOld._eof) { - this._errorMessage('String opened but never closed'); - } - this.syData = s; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x2d /* - */) { - this._readNumberOrName(allowFloats); - } else if (this._codepoint === 0x2e /* . */) { - this.sy = AlphaTexSymbols.Dot; - this.syData = '.'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x3a /* : */) { - this.sy = AlphaTexSymbols.DoubleDot; - this.syData = ':'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x28 /* ( */) { - this.sy = AlphaTexSymbols.LParensis; - this._codepoint = this._nextCodepoint(); - this.syData = '('; - } else if (this._codepoint === 0x5c /* \ */) { - this._codepoint = this._nextCodepoint(); - this.sy = AlphaTexSymbols.MetaCommand; - // allow double backslash (easier to test when copying from escaped Strings) - if (this._codepoint === 0x5c /* \ */) { - this._codepoint = this._nextCodepoint(); - } - - this.syData = this._readName(); - } else if (this._codepoint === 0x29 /* ) */) { - this.sy = AlphaTexSymbols.RParensis; - this.syData = ')'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x7b /* { */) { - this.sy = AlphaTexSymbols.LBrace; - this.syData = '{'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x7d /* } */) { - this.sy = AlphaTexSymbols.RBrace; - this.syData = '}'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x7c /* | */) { - this.sy = AlphaTexSymbols.Pipe; - this.syData = '|'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x2a /* * */) { - this.sy = AlphaTexSymbols.Multiply; - this.syData = '*'; - this._codepoint = this._nextCodepoint(); - } else if (this._codepoint === 0x3c /* < */) { - this.sy = AlphaTexSymbols.LowerThan; - this.syData = '<'; - this._codepoint = this._nextCodepoint(); - } else if (AlphaTexLexerOld._isDigit(this._codepoint)) { - this._readNumberOrName(allowFloats); - } else if (AlphaTexLexerOld._isNameLetter(this._codepoint)) { - const name: string = this._readName(); - const tuning: TuningParseResult | null = this.allowTuning ? ModelUtils.parseTuning(name) : null; - if (tuning) { - this.sy = AlphaTexSymbols.Tuning; - this.syData = tuning; - } else { - this.sy = AlphaTexSymbols.String; - this.syData = name; - } - } else { - this._errorMessage(`Unexpected character ${String.fromCodePoint(this._codepoint)}`); - } - } - return this.sy; - } - - private _errorMessage(message: string): void { - const e: AlphaTexError = AlphaTexError.errorMessage( - message, - this.lastValidSpot[0], - this.lastValidSpot[1], - this.lastValidSpot[2] - ); - if (this.logErrors) { - Logger.error('AlphaTex', e.message!); - } - throw e; - } - - private _readNumberOrName(allowFloat: boolean) { - let str: string = ''; - - // assume number at start - this.sy = AlphaTexSymbols.Number; - - // negative start or dash - if (this._codepoint === 0x2d) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - - // need a number afterwards otherwise we have a string(-) - if (!AlphaTexLexerOld._isDigit(this._codepoint)) { - this.sy = AlphaTexSymbols.String; - } - } - - let keepReading = true; - - let hasDot = false; - do { - switch (this.sy) { - case AlphaTexSymbols.Number: - // adding digits to the number - if (AlphaTexLexerOld._isDigit(this._codepoint)) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - } - // adding a dot to the number (expecting digit after dot) - else if ( - allowFloat && - !hasDot && - this._codepoint === 0x2e /* . */ && - AlphaTexLexerOld._isDigit(this._codepoints[this._position]) - ) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - hasDot = true; - } - // letter in number -> fallback to name reading - else if (AlphaTexLexerOld._isNameLetter(this._codepoint)) { - this.sy = AlphaTexSymbols.String; - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - } - // general unknown character -> end reading - else { - keepReading = false; - } - break; - case AlphaTexSymbols.String: - if (AlphaTexLexerOld._isNameLetter(this._codepoint)) { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - keepReading = true; - } else { - keepReading = false; - } - break; - default: - keepReading = false; // should never happen - break; - } - } while (keepReading); - - if (str.length === 0) { - this._errorMessage('number was empty'); - } - - if (this.sy === AlphaTexSymbols.String) { - this.syData = str; - } else { - this.syData = allowFloat ? Number.parseFloat(str) : Number.parseInt(str, 10); - } - return; - } - - /** - * Reads a string from the stream. - * @returns the read string. - */ - private _readName(): string { - let str: string = ''; - do { - str += String.fromCodePoint(this._codepoint); - this._codepoint = this._nextCodepoint(); - } while ( - AlphaTexLexerOld._isNameLetter(this._codepoint) || - AlphaTexLexerOld._isDigit(this._codepoint) || - this._codepoint === 0x2d /*-*/ - ); - return str; - } - - /** - * Checks if the given character is a valid letter for a name. - * (no control characters, whitespaces, numbers or dots) - */ - private static _isNameLetter(ch: number): boolean { - return ( - !AlphaTexLexerOld._isTerminal(ch) && // no control characters, whitespaces, numbers or dots - ((0x21 <= ch && ch <= 0x2f) || (0x3a <= ch && ch <= 0x7e) || 0x80 <= ch) // Unicode Symbols - ); - } - - private static _isTerminal(ch: number): boolean { - return ( - ch === 0x2e /* . */ || - ch === 0x7b /* { */ || - ch === 0x7d /* } */ || - ch === 0x5b /* [ */ || - ch === 0x5d /* ] */ || - ch === 0x28 /* ( */ || - ch === 0x29 /* ) */ || - ch === 0x7c /* | */ || - ch === 0x27 /* ' */ || - ch === 0x22 /* " */ || - ch === 0x2a /* * */ || - ch === 0x5c /* \ */ - ); - } - - private static _isWhiteSpace(ch: number): boolean { - return ( - ch === 0x09 /* \t */ || - ch === 0x0a /* \n */ || - ch === 0x0b /* \v */ || - ch === 0x0d /* \r */ || - ch === 0x20 /* space */ - ); - } - - private static _isDigit(ch: number): boolean { - return ch >= 0x30 && ch <= 0x39 /* 0-9 */; - } -} - -/** - * This importer can parse alphaTex markup into a score structure. - * @internal - */ -export class AlphaTexImporterOld extends ScoreImporter { - private _trackChannel: number = 0; - private _score!: Score; - private _currentTrack!: Track; - - private _currentStaff!: Staff; - private _barIndex: number = 0; - private _voiceIndex: number = 0; - private _initialTempo = Automation.buildTempoAutomation(false, 0, 120, 0); - - // Last known position that had valid syntax/symbols - private _currentDuration: Duration = Duration.QuadrupleWhole; - private _currentDynamics: DynamicValue = DynamicValue.PPP; - private _currentTuplet: number = 0; - private _lyrics!: Map; - private _ignoredInitialVoice = false; - - private _staffHasExplicitDisplayTransposition: boolean = false; - private _staffDisplayTranspositionApplied: boolean = false; - private _staffHasExplicitTuning: boolean = false; - private _staffTuningApplied: boolean = false; - private _percussionArticulationNames = new Map(); - private _sustainPedalToBeat = new Map(); - - private _slurs: Map = new Map(); - - private _articulationValueToIndex = new Map(); - - private _lexer!: AlphaTexLexerOld; - - private _accidentalMode: AlphaTexAccidentalMode = AlphaTexAccidentalMode.Explicit; - private _flatSyncPoints: FlatSyncPoint[] = []; - - public logErrors: boolean = true; - - public get name(): string { - return 'AlphaTex'; - } - - public initFromString(tex: string, settings: Settings) { - this.data = ByteBuffer.empty(); - this._lexer = new AlphaTexLexerOld(tex); - this.settings = settings; - // when beginning reading a new score we reset the IDs. - Score.resetIds(); - } - - private get _sy() { - return this._lexer.sy; - } - - private get _syData() { - return this._lexer.syData; - } - - private set _sy(value: AlphaTexSymbols) { - this._lexer.sy = value; - } - - private _newSy(allowFloat: boolean = false) { - return this._lexer.newSy(allowFloat); - } - - public readScore(): Score { - try { - if (this.data.length > 0) { - this._lexer = new AlphaTexLexerOld( - IOHelper.toString(this.data.readAll(), this.settings.importer.encoding) - ); - } - this._lexer.logErrors = this.logErrors; - - this._lexer.allowTuning = true; - this._lyrics = new Map(); - this._sustainPedalToBeat = new Map(); - - this._lexer.init(); - - this._createDefaultScore(); - this._currentDuration = Duration.Quarter; - this._currentDynamics = DynamicValue.F; - this._currentTuplet = 1; - if (this._sy === AlphaTexSymbols.LowerThan) { - // potential XML, stop parsing (alphaTex never starts with <) - throw new UnsupportedFormatError("Unknown start sign '<' (meant to import as XML?)"); - } - - if (this._sy !== AlphaTexSymbols.Eof) { - const anyMetaRead = this._metaData(); - const anyBarsRead = this._bars(); - if (!anyMetaRead && !anyBarsRead) { - throw new UnsupportedFormatError('No alphaTex data found'); - } - - if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - this._syncPoints(); - } - } - - ModelUtils.consolidate(this._score); - this._score.finish(this.settings); - ModelUtils.trimEmptyBarsAtEnd(this._score); - this._score.rebuildRepeatGroups(); - this._score.applyFlatSyncPoints(this._flatSyncPoints); - for (const [track, lyrics] of this._lyrics) { - this._score.tracks[track].applyLyrics(lyrics); - } - for (const [sustainPedal, beat] of this._sustainPedalToBeat) { - const duration = beat.voice.bar.masterBar.calculateDuration(); - sustainPedal.ratioPosition = beat.playbackStart / duration; - } - return this._score; - } catch (e) { - if (e instanceof AlphaTexError) { - throw new UnsupportedFormatError(e.message, e); - } - throw e; - } - } - - private _syncPoints() { - while (this._sy !== AlphaTexSymbols.Eof) { - this._syncPoint(); - } - } - - private _syncPoint() { - // \sync BarIndex Occurence MillisecondOffset - // \sync BarIndex Occurence MillisecondOffset RatioPosition - - if (this._sy !== AlphaTexSymbols.MetaCommand || (this._syData as string) !== 'sync') { - this._error('syncPoint', AlphaTexSymbols.MetaCommand, true); - } - - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarIndex', AlphaTexSymbols.Number, true); - } - const barIndex = this._syData as number; - - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarOccurence', AlphaTexSymbols.Number, true); - } - const barOccurence = this._syData as number; - - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarMillis', AlphaTexSymbols.Number, true); - } - const millisecondOffset = this._syData as number; - - this._sy = this._newSy(true); - let barPosition = 0; - if (this._sy === AlphaTexSymbols.Number) { - barPosition = this._syData as number; - this._sy = this._newSy(); - } - - this._flatSyncPoints.push({ - barIndex, - barOccurence, - barPosition, - millisecondOffset - }); - } - - private _error(nonterm: string, expected: AlphaTexSymbols, wrongSymbol: boolean = true): void { - let receivedSymbol: AlphaTexSymbols; - let showSyData = false; - if (wrongSymbol) { - receivedSymbol = this._sy; - if ( - // These are the only symbols that can have associated _syData set - receivedSymbol === AlphaTexSymbols.String || - receivedSymbol === AlphaTexSymbols.Number || - receivedSymbol === AlphaTexSymbols.MetaCommand // || - // Tuning does not have a toString() yet, therefore excluded. - // receivedSymbol === AlphaTexSymbols.Tuning - ) { - showSyData = true; - } - } else { - receivedSymbol = expected; - showSyData = true; - } - const e = AlphaTexError.symbolError( - this._lexer.lastValidSpot[0], - this._lexer.lastValidSpot[1], - this._lexer.lastValidSpot[2], - nonterm, - expected, - receivedSymbol, - showSyData ? this._syData : null - ); - if (this.logErrors) { - Logger.error(this.name, e.message!); - } - throw e; - } - - private _errorMessage(message: string): void { - const e: AlphaTexError = AlphaTexError.errorMessage( - message, - this._lexer.lastValidSpot[0], - this._lexer.lastValidSpot[1], - this._lexer.lastValidSpot[2] - ); - if (this.logErrors) { - Logger.error(this.name, e.message!); - } - throw e; - } - - /** - * Initializes the song with some required default values. - * @returns - */ - private _createDefaultScore(): void { - this._score = new Score(); - this._newTrack(); - } - - private _newTrack(): void { - this._currentTrack = new Track(); - this._currentTrack.ensureStaveCount(1); - this._currentTrack.playbackInfo.program = 25; - this._currentTrack.playbackInfo.primaryChannel = this._trackChannel++; - this._currentTrack.playbackInfo.secondaryChannel = this._trackChannel++; - const staff = this._currentTrack.staves[0]; - staff.displayTranspositionPitch = 0; - staff.stringTuning = Tuning.getDefaultTuningFor(6)!; - this._articulationValueToIndex.clear(); - - this._beginStaff(staff); - - this._score.addTrack(this._currentTrack); - this._lyrics.set(this._currentTrack.index, []); - this._currentDynamics = DynamicValue.F; - } - - /** - * Converts a clef string into the clef value. - * @param str the string to convert - * @returns the clef value - */ - private _parseClefFromString(str: string): Clef { - switch (str.toLowerCase()) { - case 'g2': - case 'treble': - return Clef.G2; - case 'f4': - case 'bass': - return Clef.F4; - case 'c3': - case 'alto': - return Clef.C3; - case 'c4': - case 'tenor': - return Clef.C4; - case 'n': - case 'neutral': - return Clef.Neutral; - default: - return Clef.G2; - // error("clef-value", AlphaTexSymbols.String, false); - } - } - - /** - * Converts a clef tuning into the clef value. - * @param i the tuning value to convert - * @returns the clef value - */ - private _parseClefFromInt(i: number): Clef { - switch (i) { - case 0: - return Clef.Neutral; - case 43: - return Clef.G2; - case 65: - return Clef.F4; - case 48: - return Clef.C3; - case 60: - return Clef.C4; - default: - return Clef.G2; - } - } - - private _parseTripletFeelFromString(str: string): TripletFeel { - switch (str.toLowerCase()) { - case 'no': - case 'none': - case 'notripletfeel': - return TripletFeel.NoTripletFeel; - case 't16': - case 'triplet-16th': - case 'triplet16th': - return TripletFeel.Triplet16th; - case 't8': - case 'triplet-8th': - case 'triplet8th': - return TripletFeel.Triplet8th; - case 'd16': - case 'dotted-16th': - case 'dotted16th': - return TripletFeel.Dotted16th; - case 'd8': - case 'dotted-8th': - case 'dotted8th': - return TripletFeel.Dotted8th; - case 's16': - case 'scottish-16th': - case 'scottish16th': - return TripletFeel.Scottish16th; - case 's8': - case 'scottish-8th': - case 'scottish8th': - return TripletFeel.Scottish8th; - default: - return TripletFeel.NoTripletFeel; - } - } - - private _parseTripletFeelFromInt(i: number): TripletFeel { - switch (i) { - case 0: - return TripletFeel.NoTripletFeel; - case 1: - return TripletFeel.Triplet16th; - case 2: - return TripletFeel.Triplet8th; - case 3: - return TripletFeel.Dotted16th; - case 4: - return TripletFeel.Dotted8th; - case 5: - return TripletFeel.Scottish16th; - case 6: - return TripletFeel.Scottish8th; - default: - return TripletFeel.NoTripletFeel; - } - } - - /** - * Converts a keysignature string into the assocciated value. - * @param str the string to convert - * @returns the assocciated keysignature value - */ - private _parseKeySignature(str: string): KeySignature { - switch (str.toLowerCase()) { - case 'cb': - case 'cbmajor': - case 'abminor': - return KeySignature.Cb; - case 'gb': - case 'gbmajor': - case 'ebminor': - return KeySignature.Gb; - case 'db': - case 'dbmajor': - case 'bbminor': - return KeySignature.Db; - case 'ab': - case 'abmajor': - case 'fminor': - return KeySignature.Ab; - case 'eb': - case 'ebmajor': - case 'cminor': - return KeySignature.Eb; - case 'bb': - case 'bbmajor': - case 'gminor': - return KeySignature.Bb; - case 'f': - case 'fmajor': - case 'dminor': - return KeySignature.F; - case 'c': - case 'cmajor': - case 'aminor': - return KeySignature.C; - case 'g': - case 'gmajor': - case 'eminor': - return KeySignature.G; - case 'd': - case 'dmajor': - case 'bminor': - return KeySignature.D; - case 'a': - case 'amajor': - case 'f#minor': - return KeySignature.A; - case 'e': - case 'emajor': - case 'c#minor': - return KeySignature.E; - case 'b': - case 'bmajor': - case 'g#minor': - return KeySignature.B; - case 'f#': - case 'f#major': - case 'd#minor': - return KeySignature.FSharp; - case 'c#': - case 'c#major': - case 'a#minor': - return KeySignature.CSharp; - default: - return KeySignature.C; - // error("keysignature-value", AlphaTexSymbols.String, false); return 0 - } - } - - private _parseKeySignatureType(str: string): KeySignatureType { - if (str.toLowerCase().endsWith('minor')) { - return KeySignatureType.Minor; - } - return KeySignatureType.Major; - } - - private _metaData(): boolean { - let anyTopLevelMeta = false; - let anyOtherMeta = false; - let continueReading: boolean = true; - while (this._sy === AlphaTexSymbols.MetaCommand && continueReading) { - const metadataTag: string = (this._syData as string).toLowerCase(); - switch (metadataTag) { - case 'title': - case 'subtitle': - case 'artist': - case 'album': - case 'words': - case 'music': - case 'copyright': - case 'instructions': - case 'notices': - case 'tab': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - // Known issue: Strings that happen to be parsed as valid Tunings or positive Numbers will not pass this. - // Need to use quotes in that case, or rewrite parsing logic. - this._error(metadataTag, AlphaTexSymbols.String, true); - } - - const metadataValue: string = this._syData as string; - this._sy = this._newSy(); - anyTopLevelMeta = true; - - let element: ScoreSubElement = ScoreSubElement.ChordDiagramList; - switch (metadataTag) { - case 'title': - this._score.title = metadataValue; - element = ScoreSubElement.Title; - break; - case 'subtitle': - this._score.subTitle = metadataValue; - element = ScoreSubElement.SubTitle; - break; - case 'artist': - this._score.artist = metadataValue; - element = ScoreSubElement.Artist; - break; - case 'album': - this._score.album = metadataValue; - element = ScoreSubElement.Album; - break; - case 'words': - this._score.words = metadataValue; - element = ScoreSubElement.Words; - break; - case 'music': - this._score.music = metadataValue; - element = ScoreSubElement.Music; - break; - case 'copyright': - this._score.copyright = metadataValue; - element = ScoreSubElement.Copyright; - break; - case 'instructions': - this._score.instructions = metadataValue; - break; - case 'notices': - this._score.notices = metadataValue; - break; - case 'tab': - this._score.tab = metadataValue; - element = ScoreSubElement.Transcriber; - break; - } - - if (element !== ScoreSubElement.ChordDiagramList) { - this.headerFooterStyle(element); - } - - break; - case 'copyright2': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error(metadataTag, AlphaTexSymbols.String, true); - } - - this.headerFooterStyle(ScoreSubElement.CopyrightSecondLine); - anyTopLevelMeta = true; - break; - case 'wordsandmusic': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error(metadataTag, AlphaTexSymbols.String, true); - } - - this.headerFooterStyle(ScoreSubElement.WordsAndMusic); - anyTopLevelMeta = true; - break; - case 'tempo': - this._sy = this._newSy(true); - if (this._sy === AlphaTexSymbols.Number) { - this._initialTempo.value = this._syData as number; - } else { - this._error('tempo', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.String) { - this._initialTempo.text = this._syData as string; - this._sy = this._newSy(); - } - anyTopLevelMeta = true; - break; - case 'defaultsystemslayout': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._score.defaultSystemsLayout = this._syData as number; - this._sy = this._newSy(); - anyTopLevelMeta = true; - } else { - this._error('default-systems-layout', AlphaTexSymbols.Number, true); - } - break; - case 'systemslayout': - this._sy = this._newSy(); - anyTopLevelMeta = true; - while (this._sy === AlphaTexSymbols.Number) { - this._score.systemsLayout.push(this._syData as number); - this._sy = this._newSy(); - } - break; - case 'hidedynamics': - this._score.stylesheet.hideDynamics = true; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'showdynamics': - this._score.stylesheet.hideDynamics = false; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'bracketextendmode': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('bracketExtendMode', AlphaTexSymbols.String, true); - } - this._score.stylesheet.bracketExtendMode = this._parseBracketExtendMode(this._syData as string); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'usesystemsignseparator': - this._score.stylesheet.useSystemSignSeparator = true; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'multibarrest': - this._score.stylesheet.multiTrackMultiBarRest = true; - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'singletracktracknamepolicy': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('singleTrackTrackNamePolicy', AlphaTexSymbols.String, true); - } - this._score.stylesheet.singleTrackTrackNamePolicy = this._parseTrackNamePolicy( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'multitracktracknamepolicy': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('multiTrackTrackNamePolicy', AlphaTexSymbols.String, true); - } - this._score.stylesheet.multiTrackTrackNamePolicy = this._parseTrackNamePolicy( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'firstsystemtracknamemode': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('firstSystemTrackNameMode', AlphaTexSymbols.String, true); - } - this._score.stylesheet.firstSystemTrackNameMode = this._parseTrackNameMode(this._syData as string); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'othersystemstracknamemode': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('otherSystemsTrackNameMode', AlphaTexSymbols.String, true); - } - this._score.stylesheet.otherSystemsTrackNameMode = this._parseTrackNameMode(this._syData as string); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'firstsystemtracknameorientation': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('firstSystemTrackNameOrientation', AlphaTexSymbols.String, true); - } - this._score.stylesheet.firstSystemTrackNameOrientation = this._parseTrackNameOrientation( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - case 'othersystemstracknameorientation': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('otherSystemsTrackNameOrientation', AlphaTexSymbols.String, true); - } - this._score.stylesheet.otherSystemsTrackNameOrientation = this._parseTrackNameOrientation( - this._syData as string - ); - this._sy = this._newSy(); - anyTopLevelMeta = true; - break; - default: - switch (this._handleStaffMeta()) { - case StaffMetaResult.KnownStaffMeta: - anyOtherMeta = true; - break; - case StaffMetaResult.UnknownStaffMeta: - if (anyTopLevelMeta || anyOtherMeta) { - // invalid meta encountered - this._error('metaDataTags', AlphaTexSymbols.String, false); - } else { - // fall forward to bar meta if unknown score meta was found - continueReading = false; - } - break; - case StaffMetaResult.EndOfMetaDetected: - continueReading = false; - break; - } - break; - } - } - if (anyTopLevelMeta) { - if (this._sy !== AlphaTexSymbols.Dot) { - this._error('song', AlphaTexSymbols.Dot, true); - } - this._sy = this._newSy(); - } else if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - anyTopLevelMeta = true; // just to indicate that there is an indication of proper alphaTex - } - - return anyTopLevelMeta || anyOtherMeta; - } - headerFooterStyle(element: ScoreSubElement) { - const style = ModelUtils.getOrCreateHeaderFooterStyle(this._score, element); - if (style.isVisible === undefined) { - style.isVisible = true; - } - - if (this._sy === AlphaTexSymbols.String) { - const value = this._syData as string; - if (value) { - style.template = value; - } else { - style.isVisible = false; - } - this._sy = this._newSy(); - } - - if (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'left': - style.textAlign = TextAlign.Left; - break; - case 'center': - style.textAlign = TextAlign.Center; - break; - case 'right': - style.textAlign = TextAlign.Right; - break; - } - this._sy = this._newSy(); - } - } - - private _parseTrackNamePolicy(v: string): TrackNamePolicy { - switch (v.toLowerCase()) { - case 'hidden': - return TrackNamePolicy.Hidden; - case 'allsystems': - return TrackNamePolicy.AllSystems; - // case 'firstsystem': - default: - return TrackNamePolicy.FirstSystem; - } - } - - private _parseTrackNameMode(v: string): TrackNameMode { - switch (v.toLowerCase()) { - case 'fullname': - return TrackNameMode.FullName; - // case 'shortname': - default: - return TrackNameMode.ShortName; - } - } - - private _parseTrackNameOrientation(v: string): TrackNameOrientation { - switch (v.toLowerCase()) { - case 'horizontal': - return TrackNameOrientation.Horizontal; - //case 'vertical': - default: - return TrackNameOrientation.Vertical; - } - } - - private _handleStaffMeta(): StaffMetaResult { - switch ((this._syData as string).toLowerCase()) { - case 'capo': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.capo = this._syData as number; - } else { - this._error('capo', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'tuning': - this._sy = this._newSy(); - const strings: number = this._currentStaff.tuning.length; - this._staffHasExplicitTuning = true; - this._staffTuningApplied = false; - this._currentStaff.stringTuning.reset(); - switch (this._sy) { - case AlphaTexSymbols.String: - const text: string = (this._syData as string).toLowerCase(); - if (text === 'piano' || text === 'none' || text === 'voice') { - this._makeCurrentStaffPitched(); - } else { - this._error('tuning', AlphaTexSymbols.Tuning, true); - } - this._sy = this._newSy(); - break; - case AlphaTexSymbols.Tuning: - const tuning: number[] = []; - do { - const t: TuningParseResult = this._syData as TuningParseResult; - tuning.push(t.realValue); - this._sy = this._newSy(); - } while (this._sy === AlphaTexSymbols.Tuning); - this._currentStaff.stringTuning.tunings = tuning; - break; - default: - this._error('tuning', AlphaTexSymbols.Tuning, true); - break; - } - - if (this._sy === AlphaTexSymbols.String) { - if ((this._syData as string).toLowerCase() === 'hide') { - if (!this._score.stylesheet.perTrackDisplayTuning) { - this._score.stylesheet.perTrackDisplayTuning = new Map(); - } - this._score.stylesheet.perTrackDisplayTuning!.set(this._currentTrack.index, false); - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String) { - this._currentStaff.stringTuning.name = this._syData as string; - this._sy = this._newSy(); - } - } else { - this._currentStaff.stringTuning.name = this._syData as string; - this._sy = this._newSy(); - } - } - - if (strings !== this._currentStaff.tuning.length && (this._currentStaff.chords?.size ?? 0) > 0) { - this._errorMessage('Tuning must be defined before any chord'); - } - return StaffMetaResult.KnownStaffMeta; - case 'instrument': - this._staffTuningApplied = false; - this._readTrackInstrument(); - - return StaffMetaResult.KnownStaffMeta; - case 'bank': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bank', AlphaTexSymbols.Number, true); - } - - this._currentTrack.playbackInfo.bank = this._syData as number; - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'lyrics': - this._sy = this._newSy(); - const lyrics: Lyrics = new Lyrics(); - lyrics.startBar = 0; - lyrics.text = ''; - if (this._sy === AlphaTexSymbols.Number) { - lyrics.startBar = this._syData as number; - this._sy = this._newSy(); - } - if (this._sy === AlphaTexSymbols.String) { - lyrics.text = this._syData as string; - this._sy = this._newSy(); - } else { - this._error('lyrics', AlphaTexSymbols.String, true); - } - this._lyrics.get(this._currentTrack.index)!.push(lyrics); - return StaffMetaResult.KnownStaffMeta; - case 'chord': - this._sy = this._newSy(); - const chord: Chord = new Chord(); - this._chordProperties(chord); - if (this._sy === AlphaTexSymbols.String) { - chord.name = this._syData as string; - this._sy = this._newSy(); - } else { - this._error('chord-name', AlphaTexSymbols.String, true); - } - for (let i: number = 0; i < this._currentStaff.tuning.length; i++) { - if (this._sy === AlphaTexSymbols.Number) { - chord.strings.push(this._syData as number); - } else if (this._sy === AlphaTexSymbols.String && (this._syData as string).toLowerCase() === 'x') { - chord.strings.push(-1); - } - this._sy = this._newSy(); - } - this._currentStaff.addChord(this._getChordId(this._currentStaff, chord.name), chord); - return StaffMetaResult.KnownStaffMeta; - 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( - AlphaTexImporterOld._toArticulationId(defaultName), - defaultValue - ); - } - return StaffMetaResult.KnownStaffMeta; - } - - 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 StaffMetaResult.KnownStaffMeta; - case 'accidentals': - this._handleAccidentalMode(); - return StaffMetaResult.KnownStaffMeta; - case 'displaytranspose': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.displayTranspositionPitch = (this._syData as number) * -1; - this._staffHasExplicitDisplayTransposition = true; - } else { - this._error('displaytranspose', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'transpose': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.transpositionPitch = (this._syData as number) * -1; - } else { - this._error('transpose', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - return StaffMetaResult.KnownStaffMeta; - case 'track': - case 'staff': - // on empty staves we need to proceeed when starting directly a new track or staff - return StaffMetaResult.EndOfMetaDetected; - case 'voice': - this._sy = this._newSy(); - if (this._handleNewVoice()) { - return StaffMetaResult.EndOfMetaDetected; - } - return StaffMetaResult.KnownStaffMeta; - default: - return StaffMetaResult.UnknownStaffMeta; - } - } - private _readTrackInstrument() { - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.Number) { - const instrument: number = this._syData as number; - if (instrument >= 0 && instrument <= 127) { - this._currentTrack.playbackInfo.program = this._syData as number; - } else { - this._error('instrument', AlphaTexSymbols.Number, false); - } - } else if (this._sy === AlphaTexSymbols.String) { - const instrumentName: string = (this._syData as string).toLowerCase(); - if (instrumentName === 'percussion') { - for (const staff of this._currentTrack.staves) { - this._applyPercussionStaff(staff); - } - this._currentTrack.playbackInfo.program = 0; - this._currentTrack.playbackInfo.primaryChannel = SynthConstants.PercussionChannel; - this._currentTrack.playbackInfo.secondaryChannel = SynthConstants.PercussionChannel; - } else { - this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName); - } - } else { - this._error('instrument', AlphaTexSymbols.Number, true); - } - this._sy = this._newSy(); - } - - private _handleAccidentalMode() { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('accidental-mode', AlphaTexSymbols.String, true); - } - - switch (this._syData as string) { - case 'auto': - this._accidentalMode = AlphaTexAccidentalMode.Auto; - break; - case 'explicit': - this._accidentalMode = AlphaTexAccidentalMode.Explicit; - break; - } - - this._sy = this._newSy(); - } - - private _makeCurrentStaffPitched() { - // clear tuning - this._currentStaff.stringTuning.reset(); - if (!this._staffHasExplicitDisplayTransposition) { - this._currentStaff.displayTranspositionPitch = 0; - } - } - - /** - * Encodes a given string to a shorthand text form without spaces or special characters - */ - private static _toArticulationId(plain: string): string { - return plain.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); - } - - private _applyPercussionStaff(staff: Staff) { - staff.isPercussion = true; - staff.showTablature = false; - staff.track.playbackInfo.program = 0; - } - - private _chordProperties(chord: Chord): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'firstfret': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.Number: - chord.firstFret = this._syData as number; - break; - default: - this._error('chord-firstfret', AlphaTexSymbols.Number, true); - break; - } - this._sy = this._newSy(); - break; - case 'showdiagram': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - chord.showDiagram = (this._syData as string).toLowerCase() !== 'false'; - break; - case AlphaTexSymbols.Number: - chord.showDiagram = (this._syData as number) !== 0; - break; - default: - this._error('chord-showdiagram', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - break; - case 'showfingering': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - chord.showDiagram = (this._syData as string).toLowerCase() !== 'false'; - break; - case AlphaTexSymbols.Number: - chord.showFingering = (this._syData as number) !== 0; - break; - default: - this._error('chord-showfingering', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - break; - case 'showname': - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - chord.showName = (this._syData as string).toLowerCase() !== 'false'; - break; - case AlphaTexSymbols.Number: - chord.showName = (this._syData as number) !== 0; - break; - default: - this._error('chord-showname', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - break; - case 'barre': - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.Number) { - chord.barreFrets.push(this._syData as number); - this._sy = this._newSy(); - } - break; - default: - this._error('chord-properties', AlphaTexSymbols.String, false); - break; - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('chord-properties', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _bars(): boolean { - const anyData = this._bar(); - while (this._sy !== AlphaTexSymbols.Eof) { - // read pipe from last bar - if (this._sy === AlphaTexSymbols.Pipe) { - this._sy = this._newSy(); - this._bar(); - } else if (this._sy === AlphaTexSymbols.MetaCommand) { - this._bar(); - } else { - break; - } - } - return anyData; - } - - private _trackStaffMeta(): boolean { - if (this._sy !== AlphaTexSymbols.MetaCommand) { - return false; - } - if ((this._syData as string).toLowerCase() === 'track') { - this._staffHasExplicitDisplayTransposition = false; - this._staffHasExplicitTuning = false; - this._staffTuningApplied = false; - this._staffDisplayTranspositionApplied = false; - this._ignoredInitialVoice = false; - - this._sy = this._newSy(); - // new track starting? - if no masterbars it's the \track of the initial track. - if (this._score.masterBars.length > 0) { - this._newTrack(); - } - // name - if (this._sy === AlphaTexSymbols.String) { - this._currentTrack.name = this._syData as string; - this._sy = this._newSy(); - } - // short name - if (this._sy === AlphaTexSymbols.String) { - this._currentTrack.shortName = this._syData as string; - this._sy = this._newSy(); - } - - this._trackProperties(); - } - if (this._sy === AlphaTexSymbols.MetaCommand && (this._syData as string).toLowerCase() === 'staff') { - this._staffHasExplicitDisplayTransposition = false; - this._staffHasExplicitTuning = false; - this._staffTuningApplied = false; - this._staffDisplayTranspositionApplied = false; - this._ignoredInitialVoice = false; - - this._sy = this._newSy(); - if (this._currentTrack.staves[0].bars.length > 0) { - const previousWasPercussion = this._currentStaff.isPercussion; - - this._currentTrack.ensureStaveCount(this._currentTrack.staves.length + 1); - this._beginStaff(this._currentTrack.staves[this._currentTrack.staves.length - 1]); - - if (previousWasPercussion) { - this._applyPercussionStaff(this._currentStaff); - } - - this._currentDynamics = DynamicValue.F; - } - this._staffProperties(); - } - - if (this._sy === AlphaTexSymbols.MetaCommand && (this._syData as string).toLowerCase() === 'voice') { - this._sy = this._newSy(); - - this._handleNewVoice(); - } - - return true; - } - - private _handleNewVoice(): boolean { - if ( - this._voiceIndex === 0 && - (this._currentStaff.bars.length === 0 || - (this._currentStaff.bars.length === 1 && - this._currentStaff.bars[0].isEmpty && - !this._ignoredInitialVoice)) - ) { - // voice marker on the begining of the first voice without any bar yet? - // -> ignore - this._ignoredInitialVoice = true; - return false; - } - // create directly a new empty voice for all bars - for (const b of this._currentStaff.bars) { - const v = new Voice(); - b.addVoice(v); - } - // start using the new voice (see newBar for details on matching) - this._voiceIndex++; - this._barIndex = 0; - return true; - } - - private _beginStaff(staff: Staff) { - this._currentStaff = staff; - this._slurs.clear(); - this._barIndex = 0; - this._voiceIndex = 0; - } - - private _trackProperties(): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'color': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('track-color', AlphaTexSymbols.String, true); - } - this._currentTrack.color = Color.fromJson(this._syData as string)!; - this._sy = this._newSy(); - - break; - case 'defaultsystemslayout': - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - this._currentTrack.defaultSystemsLayout = this._syData as number; - this._sy = this._newSy(); - } else { - this._error('default-systems-layout', AlphaTexSymbols.Number, true); - } - break; - case 'systemslayout': - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.Number) { - this._currentTrack.systemsLayout.push(this._syData as number); - this._sy = this._newSy(); - } - break; - case 'volume': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('track-volume', AlphaTexSymbols.Number, true); - } - this._currentTrack.playbackInfo.volume = ModelUtils.clamp(this._syData as number, 0, 16); - this._sy = this._newSy(); - break; - case 'balance': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('track-balance', AlphaTexSymbols.Number, true); - } - this._currentTrack.playbackInfo.balance = ModelUtils.clamp(this._syData as number, 0, 16); - this._sy = this._newSy(); - break; - case 'mute': - this._sy = this._newSy(); - this._currentTrack.playbackInfo.isMute = true; - break; - case 'solo': - this._sy = this._newSy(); - this._currentTrack.playbackInfo.isSolo = true; - break; - case 'multibarrest': - this._sy = this._newSy(); - if (!this._score.stylesheet.perTrackMultiBarRest) { - this._score.stylesheet.perTrackMultiBarRest = new Set(); - } - this._score.stylesheet.perTrackMultiBarRest!.add(this._currentTrack.index); - break; - case 'instrument': - this._readTrackInstrument(); - break; - case 'bank': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bank', AlphaTexSymbols.Number, true); - } - - this._currentTrack.playbackInfo.bank = this._syData as number; - this._sy = this._newSy(); - break; - default: - this._error('track-properties', AlphaTexSymbols.String, false); - break; - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('track-properties', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _staffProperties(): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - let showStandardNotation: boolean = false; - let showTabs: boolean = false; - let showSlash: boolean = false; - let showNumbered: boolean = false; - while (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'score': - showStandardNotation = true; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.Number) { - this._currentStaff.standardNotationLineCount = this._syData as number; - this._sy = this._newSy(); - } - - break; - case 'tabs': - showTabs = true; - this._sy = this._newSy(); - break; - case 'slash': - showSlash = true; - this._sy = this._newSy(); - break; - case 'numbered': - showNumbered = true; - this._sy = this._newSy(); - break; - default: - this._error('staff-properties', AlphaTexSymbols.String, false); - break; - } - } - if (showStandardNotation || showTabs || showSlash || showNumbered) { - this._currentStaff.showStandardNotation = showStandardNotation; - this._currentStaff.showTablature = showTabs; - this._currentStaff.showSlash = showSlash; - this._currentStaff.showNumbered = showNumbered; - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('staff-properties', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _bar(): boolean { - const anyStaffMeta = this._trackStaffMeta(); - - const bar: Bar = this._newBar(this._currentStaff); - if (this._currentStaff.bars.length > this._score.masterBars.length) { - const master: MasterBar = new MasterBar(); - this._score.addMasterBar(master); - if (master.index > 0) { - master.timeSignatureDenominator = master.previousMasterBar!.timeSignatureDenominator; - master.timeSignatureNumerator = master.previousMasterBar!.timeSignatureNumerator; - master.tripletFeel = master.previousMasterBar!.tripletFeel; - } else { - master.tempoAutomations.push(this._initialTempo); - } - } - const anyBarMeta = this._barMeta(bar); - - // detect tuning for staff - const program = this._currentTrack.playbackInfo.program; - if (!this._staffTuningApplied && !this._staffHasExplicitTuning) { - // reset to defaults - this._currentStaff.stringTuning.reset() - - if (program === 15) { - // dulcimer E4 B3 G3 D3 A2 E2 - this._currentStaff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; - } else if (program >= 24 && program <= 31) { - // guitar E4 B3 G3 D3 A2 E2 - this._currentStaff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; - } else if (program >= 32 && program <= 39) { - // bass G2 D2 A1 E1 - this._currentStaff.stringTuning.tunings = [43, 38, 33, 28]; - } 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) { - // viola A3 D3 G2 C2 - this._currentStaff.stringTuning.tunings = [57, 50, 43, 36]; - } else if (program === 42) { - // cello A2 D2 G1 C1 - this._currentStaff.stringTuning.tunings = [45, 38, 31, 24]; - } else if (program === 43) { - // contrabass - // G2 D2 A1 E1 - this._currentStaff.stringTuning.tunings = [43, 38, 33, 28]; - } else if (program === 105) { - // banjo - // D3 B2 G2 D2 G3 - this._currentStaff.stringTuning.tunings = [50, 47, 43, 38, 55]; - } else if (program === 106) { - // shamisen - // A3 E3 A2 - this._currentStaff.stringTuning.tunings = [57, 52, 45]; - } else if (program === 107) { - // koto - // E3 A2 D2 G1 - this._currentStaff.stringTuning.tunings = [52, 45, 38, 31]; - } else if (program === 110) { - // Fiddle - // E4 A3 D3 G2 - this._currentStaff.stringTuning.tunings = [64, 57, 50, 43]; - } - - this._staffTuningApplied = true; - } - - // display transposition - if (!this._staffDisplayTranspositionApplied && !this._staffHasExplicitDisplayTransposition) { - if (ModelUtils.displayTranspositionPitches.has(program)) { - // guitar E4 B3 G3 D3 A2 E2 - this._currentStaff.displayTranspositionPitch = ModelUtils.displayTranspositionPitches.get(program)!; - } else { - this._currentStaff.displayTranspositionPitch = 0; - } - this._staffDisplayTranspositionApplied = true; - } - - let anyBeatData = false; - const voice: Voice = bar.voices[this._voiceIndex]; - - // if we have a setup like \track \staff \track \staff (without any notes/beats defined) - // we are at a track meta at this point and we don't read any beats - const emptyStaffWithNewStart = - this._sy === AlphaTexSymbols.MetaCommand && - ((this._syData as string).toLowerCase() === 'track' || (this._syData as string).toLowerCase() === 'staff'); - - if (!emptyStaffWithNewStart) { - while (this._sy !== AlphaTexSymbols.Pipe && this._sy !== AlphaTexSymbols.Eof) { - if (!this._beat(voice)) { - break; - } - anyBeatData = true; - } - } - - if (voice.beats.length === 0) { - const emptyBeat: Beat = new Beat(); - emptyBeat.isEmpty = true; - voice.addBeat(emptyBeat); - } - - return anyStaffMeta || anyBarMeta || anyBeatData; - } - - private _newBar(staff: Staff): Bar { - // existing bar? -> e.g. in multi-voice setups where we fill empty voices later - if (this._barIndex < staff.bars.length) { - const bar = staff.bars[this._barIndex]; - this._barIndex++; - return bar; - } - - const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; - - // need new bar - const newBar: Bar = new Bar(); - staff.addBar(newBar); - if (newBar.previousBar) { - newBar.clef = newBar.previousBar.clef; - newBar.clefOttava = newBar.previousBar.clefOttava; - newBar.keySignature = newBar.previousBar!.keySignature; - newBar.keySignatureType = newBar.previousBar!.keySignatureType; - } - this._barIndex++; - - if (newBar.index > 0) { - newBar.clef = newBar.previousBar!.clef; - } - - for (let i = 0; i < voiceCount; i++) { - const voice: Voice = new Voice(); - newBar.addVoice(voice); - } - - return newBar; - } - - private _beat(voice: Voice): boolean { - // duration specifier? - this._beatDuration(); - - const beat: Beat = new Beat(); - voice.addBeat(beat); - - this._lexer.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._lexer.allowTuning = !this._currentStaff.isPercussion; - if (!this._note(beat)) { - break; - } - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('note-list', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else if (this._sy === AlphaTexSymbols.String && (this._syData as string).toLowerCase() === 'r') { - // rest voice -> no notes - this._sy = this._newSy(); - } else { - if (!this._note(beat)) { - voice.beats.splice(voice.beats.length - 1, 1); - return false; - } - } - // new duration - if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('duration', AlphaTexSymbols.Number, true); - } - this._currentDuration = this._parseDuration(this._syData as number); - this._sy = this._newSy(); - } - beat.duration = this._currentDuration; - beat.dynamics = this._currentDynamics; - if (this._currentTuplet !== 1 && !beat.hasTuplet) { - AlphaTexImporterOld._applyTuplet(beat, this._currentTuplet); - } - // beat multiplier (repeat beat n times) - let beatRepeat: number = 1; - if (this._sy === AlphaTexSymbols.Multiply) { - this._sy = this._newSy(); - // multiplier count - if (this._sy !== AlphaTexSymbols.Number) { - this._error('multiplier', AlphaTexSymbols.Number, true); - } else { - beatRepeat = this._syData as number; - } - this._sy = this._newSy(); - } - this._beatEffects(beat); - for (let i: number = 0; i < beatRepeat - 1; i++) { - voice.addBeat(BeatCloner.clone(beat)); - } - return true; - } - - private _beatDuration(): void { - if (this._sy !== AlphaTexSymbols.DoubleDot) { - return; - } - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('duration', AlphaTexSymbols.Number, true); - } - this._currentDuration = this._parseDuration(this._syData as number); - this._currentTuplet = 1; - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - const effect: string = (this._syData as string).toLowerCase(); - switch (effect) { - case 'tu': - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('duration-tuplet', AlphaTexSymbols.Number, true); - } - this._currentTuplet = this._syData as number; - this._sy = this._newSy(); - break; - default: - this._error('beat-duration', AlphaTexSymbols.String, false); - break; - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('beat-duration', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - private _beatEffects(beat: Beat): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - if (!this._applyBeatEffect(beat)) { - this._error('beat-effects', AlphaTexSymbols.String, false); - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('beat-effects', AlphaTexSymbols.RBrace, true); - } - this._sy = this._newSy(); - } - - /** - * Tries to apply a beat effect to the given beat. - * @returns true if a effect could be applied, otherwise false - */ - private _applyBeatEffect(beat: Beat): boolean { - const syData: string = (this._syData as string).toLowerCase(); - if (syData === 'f') { - beat.fade = FadeType.FadeIn; - } else if (syData === 'fo') { - beat.fade = FadeType.FadeOut; - } else if (syData === 'vs') { - beat.fade = FadeType.VolumeSwell; - } else if (syData === 'v') { - beat.vibrato = VibratoType.Slight; - } else if (syData === 'vw') { - beat.vibrato = VibratoType.Wide; - } else if (syData === 's') { - beat.slap = true; - } else if (syData === 'p') { - beat.pop = true; - } else if (syData === 'tt') { - beat.tap = true; - } else if (syData === 'txt') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('beat-text', AlphaTexSymbols.String, true); - return false; - } - beat.text = this._syData as string; - } else if (syData === 'lyrics') { - this._sy = this._newSy(); - - let lyricsLine = 0; - if (this._sy === AlphaTexSymbols.Number) { - lyricsLine = this._syData as number; - this._sy = this._newSy(); - } - - if (this._sy !== AlphaTexSymbols.String) { - this._error('lyrics', AlphaTexSymbols.String, true); - return false; - } - - if (!beat.lyrics) { - beat.lyrics = []; - } - - while (beat.lyrics!.length <= lyricsLine) { - beat.lyrics.push(''); - } - - beat.lyrics[lyricsLine] = this._syData as string; - } else if (syData === 'dd') { - beat.dots = 2; - } else if (syData === 'd') { - beat.dots = 1; - } else if (syData === 'su') { - beat.pickStroke = PickStroke.Up; - } else if (syData === 'sd') { - beat.pickStroke = PickStroke.Down; - } else if (syData === 'tu') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tuplet', AlphaTexSymbols.Number, true); - return false; - } - - const numerator = this._syData as number; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.Number) { - const denominator = this._syData as number; - this._sy = this._newSy(); - beat.tupletNumerator = numerator; - beat.tupletDenominator = denominator; - } else { - AlphaTexImporterOld._applyTuplet(beat, numerator); - } - - return true; - } else if (syData === 'tb' || syData === 'tbe') { - this._sy = this._newSy(); - - const exact: boolean = syData === 'tbe'; - - // Type - if (this._sy === AlphaTexSymbols.String) { - beat.whammyBarType = this._parseWhammyType(this._syData as string); - this._sy = this._newSy(); - } - - // Style - if (this._sy === AlphaTexSymbols.String) { - beat.whammyStyle = this._parseBendStyle(this._syData as string); - this._sy = this._newSy(); - } - - // read points - if (this._sy !== AlphaTexSymbols.LParensis) { - this._error('tremolobar-effect', AlphaTexSymbols.LParensis, true); - } - this._sy = this._newSy(true); - while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) { - let offset: number = 0; - let value: number = 0; - if (exact) { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tremolobar-effect', AlphaTexSymbols.Number, true); - } - offset = this._syData as number; - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tremolobar-effect', AlphaTexSymbols.Number, true); - } - value = this._syData as number; - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tremolobar-effect', AlphaTexSymbols.Number, true); - } - offset = 0; - value = this._syData as number; - } - beat.addWhammyBarPoint(new BendPoint(offset, value)); - this._sy = this._newSy(true); - } - if (beat.whammyBarPoints != null) { - while (beat.whammyBarPoints.length > 60) { - beat.removeWhammyBarPoint(beat.whammyBarPoints.length - 1); - } - // set positions - if (!exact) { - const count: number = beat.whammyBarPoints.length; - const step: number = (BendPoint.MaxPosition / (count - 1)) | 0; - let i: number = 0; - while (i < count) { - beat.whammyBarPoints[i].offset = Math.min(BendPoint.MaxPosition, i * step); - i++; - } - } else { - beat.whammyBarPoints.sort((a, b) => a.offset - b.offset); - } - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('tremolobar-effect', AlphaTexSymbols.RParensis, true); - } - } else if (syData === 'bu' || syData === 'bd' || syData === 'au' || syData === 'ad') { - switch (syData) { - case 'bu': - beat.brushType = BrushType.BrushUp; - break; - case 'bd': - beat.brushType = BrushType.BrushDown; - break; - case 'au': - beat.brushType = BrushType.ArpeggioUp; - break; - case 'ad': - beat.brushType = BrushType.ArpeggioDown; - break; - } - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.Number) { - // explicit duration - beat.brushDuration = this._syData as number; - this._sy = this._newSy(); - return true; - } - // default to calculated duration - beat.updateDurations(); - if (syData === 'bu' || syData === 'bd') { - beat.brushDuration = beat.playbackDuration / 4 / beat.notes.length; - } else if (syData === 'au' || syData === 'ad') { - beat.brushDuration = beat.playbackDuration / beat.notes.length; - } - return true; - } else if (syData === 'ch') { - this._sy = this._newSy(); - const chordName: string = this._syData as string; - const chordId: string = this._getChordId(this._currentStaff, chordName); - if (!this._currentStaff.hasChord(chordId)) { - const chord: Chord = new Chord(); - chord.showDiagram = false; - chord.name = chordName; - this._currentStaff.addChord(chordId, chord); - } - beat.chordId = chordId; - } else if (syData === 'gr') { - this._sy = this._newSy(); - if ((this._syData as string).toLowerCase() === 'ob') { - beat.graceType = GraceType.OnBeat; - this._sy = this._newSy(); - } else if ((this._syData as string).toLowerCase() === 'b') { - beat.graceType = GraceType.BendGrace; - this._sy = this._newSy(); - } else { - beat.graceType = GraceType.BeforeBeat; - } - return true; - } else if (syData === 'dy') { - this._sy = this._newSy(); - const dynamicString = (this._syData as string).toUpperCase() as keyof typeof DynamicValue; - switch (dynamicString) { - case 'PPP': - case 'PP': - case 'P': - case 'MP': - case 'MF': - case 'F': - case 'FF': - case 'FFF': - case 'PPPP': - case 'PPPPP': - case 'PPPPPP': - case 'FFFF': - case 'FFFFF': - case 'FFFFFF': - case 'SF': - case 'SFP': - case 'SFPP': - case 'FP': - case 'RF': - case 'RFZ': - case 'SFZ': - case 'SFFZ': - case 'FZ': - case 'N': - case 'PF': - case 'SFZP': - beat.dynamics = DynamicValue[dynamicString]; - break; - } - this._currentDynamics = beat.dynamics; - } else if (syData === 'cre') { - beat.crescendo = CrescendoType.Crescendo; - } else if (syData === 'dec') { - beat.crescendo = CrescendoType.Decrescendo; - } else if (syData === 'tempo') { - // NOTE: playbackRatio is calculated on score finish when playback positions are known - const tempoAutomation = this._readTempoAutomation(false); - - if (beat.index === 0) { - const existing = beat.voice.bar.masterBar.tempoAutomations.find(a => a.ratioPosition === 0); - if (existing) { - existing.value = tempoAutomation.value; - existing.text = tempoAutomation.text; - beat.automations.push(existing); - return true; - } - } - beat.automations.push(tempoAutomation); - beat.voice.bar.masterBar.tempoAutomations.push(tempoAutomation); - - return true; - } else if (syData === 'volume') { - // NOTE: playbackRatio is calculated on score finish when playback positions are known - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('volume', AlphaTexSymbols.Number, true); - } - const volumeAutomation: Automation = new Automation(); - volumeAutomation.isLinear = true; - volumeAutomation.type = AutomationType.Volume; - volumeAutomation.value = this._syData as number; - this._sy = this._newSy(); - - beat.automations.push(volumeAutomation); - return true; - } else if (syData === 'balance') { - // NOTE: playbackRatio is calculated on score finish when playback positions are known - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('balance', AlphaTexSymbols.Number, true); - } - const balanceAutomation: Automation = new Automation(); - balanceAutomation.isLinear = true; - balanceAutomation.type = AutomationType.Balance; - balanceAutomation.value = ModelUtils.clamp(this._syData as number, 0, 16); - this._sy = this._newSy(); - - beat.automations.push(balanceAutomation); - return true; - } else if (syData === 'tp') { - this._sy = this._newSy(); - beat.tremoloSpeed = Duration.Eighth; - if (this._sy === AlphaTexSymbols.Number) { - switch (this._syData as number) { - case 8: - beat.tremoloSpeed = Duration.Eighth; - break; - case 16: - beat.tremoloSpeed = Duration.Sixteenth; - break; - case 32: - beat.tremoloSpeed = Duration.ThirtySecond; - break; - default: - beat.tremoloSpeed = Duration.Eighth; - break; - } - this._sy = this._newSy(); - } - return true; - } else if (syData === 'spd') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Down; - // exact ratio position will be applied after .finish() when times are known - sustainPedal.ratioPosition = beat.voice.bar.sustainPedals.length; - this._sustainPedalToBeat.set(sustainPedal, beat); - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'sph') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Hold; - // exact ratio position will be applied after .finish() when times are known - sustainPedal.ratioPosition = beat.voice.bar.sustainPedals.length; - this._sustainPedalToBeat.set(sustainPedal, beat); - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'spu') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Up; - // exact ratio position will be applied after .finish() when times are known - sustainPedal.ratioPosition = beat.voice.bar.sustainPedals.length; - this._sustainPedalToBeat.set(sustainPedal, beat); - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'spe') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Up; - sustainPedal.ratioPosition = 1; - beat.voice.bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - return true; - } else if (syData === 'slashed') { - beat.slashed = true; - this._sy = this._newSy(); - return true; - } else if (syData === 'ds') { - beat.deadSlapped = true; - this._sy = this._newSy(); - if (beat.notes.length === 1 && beat.notes[0].isDead) { - beat.removeNote(beat.notes[0]); - } - return true; - } else if (syData === 'glpf') { - this._sy = this._newSy(); - beat.golpe = GolpeType.Finger; - return true; - } else if (syData === 'glpt') { - this._sy = this._newSy(); - beat.golpe = GolpeType.Thumb; - return true; - } else if (syData === 'waho') { - this._sy = this._newSy(); - beat.wahPedal = WahPedal.Open; - return true; - } else if (syData === 'wahc') { - this._sy = this._newSy(); - beat.wahPedal = WahPedal.Closed; - return true; - } else if (syData === 'barre') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('beat-barre', AlphaTexSymbols.Number, true); - } - beat.barreFret = this._syData as number; - beat.barreShape = BarreShape.Full; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String) { - switch ((this._syData as string).toLowerCase()) { - case 'full': - beat.barreShape = BarreShape.Full; - this._sy = this._newSy(); - break; - case 'half': - beat.barreShape = BarreShape.Half; - this._sy = this._newSy(); - break; - } - } - - return true; - } else if (syData === 'rasg') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('rasgueado', AlphaTexSymbols.String, true); - } - - switch ((this._syData as string).toLowerCase()) { - case 'ii': - beat.rasgueado = Rasgueado.Ii; - break; - case 'mi': - beat.rasgueado = Rasgueado.Mi; - break; - case 'miitriplet': - beat.rasgueado = Rasgueado.MiiTriplet; - break; - case 'miianapaest': - beat.rasgueado = Rasgueado.MiiAnapaest; - break; - case 'pmptriplet': - beat.rasgueado = Rasgueado.PmpTriplet; - break; - case 'pmpanapaest': - beat.rasgueado = Rasgueado.PmpAnapaest; - break; - case 'peitriplet': - beat.rasgueado = Rasgueado.PeiTriplet; - break; - case 'peianapaest': - beat.rasgueado = Rasgueado.PeiAnapaest; - break; - case 'paitriplet': - beat.rasgueado = Rasgueado.PaiTriplet; - break; - case 'paianapaest': - beat.rasgueado = Rasgueado.PaiAnapaest; - break; - case 'amitriplet': - beat.rasgueado = Rasgueado.AmiTriplet; - break; - case 'amianapaest': - beat.rasgueado = Rasgueado.AmiAnapaest; - break; - case 'ppp': - beat.rasgueado = Rasgueado.Ppp; - break; - case 'amii': - beat.rasgueado = Rasgueado.Amii; - break; - case 'amip': - beat.rasgueado = Rasgueado.Amip; - break; - case 'eami': - beat.rasgueado = Rasgueado.Eami; - break; - case 'eamii': - beat.rasgueado = Rasgueado.Eamii; - break; - case 'peami': - beat.rasgueado = Rasgueado.Peami; - break; - } - this._sy = this._newSy(); - - return true; - } else if (syData === 'ot') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('beat-ottava', AlphaTexSymbols.String, true); - } - - beat.ottava = this._parseClefOttavaFromString(this._syData as string); - } else if (syData === 'legatoorigin') { - beat.isLegatoOrigin = true; - } else if (syData === 'instrument') { - this._sy = this._newSy(); - - let program = 0; - - if (this._sy === AlphaTexSymbols.Number) { - program = this._syData as number; - } else if (this._sy === AlphaTexSymbols.String) { - program = GeneralMidi.getValue(this._syData as string); - } else { - this._error('instrument-change', AlphaTexSymbols.Number, true); - } - - const automation = new Automation(); - automation.isLinear = false; - automation.type = AutomationType.Instrument; - automation.value = program; - beat.automations.push(automation); - } else if (syData === 'bank') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bank-change', AlphaTexSymbols.Number, true); - } - - const automation = new Automation(); - automation.isLinear = false; - automation.type = AutomationType.Bank; - automation.value = this._syData as number; - beat.automations.push(automation); - } else if (syData === 'fermata') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('fermata', AlphaTexSymbols.Number, true); - } - - const fermata = new Fermata(); - fermata.type = this._parseFermataFromString(this._syData as string); - - this._sy = this._newSy(true); - if (this._sy === AlphaTexSymbols.Number) { - fermata.length = this._syData as number; - this._sy = this._newSy(); - } - - beat.fermata = fermata; - - return true; - } else if (syData === 'beam') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('beam', AlphaTexSymbols.Number, true); - } - - switch ((this._syData as string).toLowerCase()) { - case 'invert': - beat.invertBeamDirection = true; - break; - case 'up': - beat.preferredBeamDirection = BeamDirection.Up; - break; - case 'down': - beat.preferredBeamDirection = BeamDirection.Down; - break; - case 'auto': - beat.beamingMode = BeatBeamingMode.Auto; - break; - case 'split': - beat.beamingMode = BeatBeamingMode.ForceSplitToNext; - break; - case 'merge': - beat.beamingMode = BeatBeamingMode.ForceMergeWithNext; - break; - case 'splitsecondary': - beat.beamingMode = BeatBeamingMode.ForceSplitOnSecondaryToNext; - break; - } - } else if (syData === 'timer') { - beat.showTimer = true; - } else { - // string didn't match any beat effect syntax - return false; - } - // default behaviour when a beat effect above - // does not handle new symbol + return on its own - this._sy = this._newSy(); - return true; - } - - private _parseBracketExtendMode(str: string): BracketExtendMode { - switch (str.toLowerCase()) { - case 'nobrackets': - return BracketExtendMode.NoBrackets; - case 'groupstaves': - return BracketExtendMode.GroupStaves; - case 'groupsimilarinstruments': - return BracketExtendMode.GroupSimilarInstruments; - default: - return BracketExtendMode.GroupStaves; - } - } - - private _parseFermataFromString(str: string): FermataType { - switch (str.toLowerCase()) { - case 'short': - return FermataType.Short; - case 'medium': - return FermataType.Medium; - case 'long': - return FermataType.Long; - default: - return FermataType.Medium; - } - } - - private _parseClefOttavaFromString(str: string): Ottavia { - switch (str.toLowerCase()) { - case '15ma': - return Ottavia._15ma; - case '8va': - return Ottavia._8va; - case 'regular': - return Ottavia.Regular; - case '8vb': - return Ottavia._8vb; - case '15mb': - return Ottavia._15mb; - default: - return Ottavia.Regular; - } - } - - private _getChordId(currentStaff: Staff, chordName: string): string { - return chordName.toLowerCase() + currentStaff.index + currentStaff.track.index; - } - - private static _applyTuplet(beat: Beat, tuplet: number): void { - switch (tuplet) { - case 3: - beat.tupletNumerator = 3; - beat.tupletDenominator = 2; - break; - case 5: - beat.tupletNumerator = 5; - beat.tupletDenominator = 4; - break; - case 6: - beat.tupletNumerator = 6; - beat.tupletDenominator = 4; - break; - case 7: - beat.tupletNumerator = 7; - beat.tupletDenominator = 4; - break; - case 9: - beat.tupletNumerator = 9; - beat.tupletDenominator = 8; - break; - case 10: - beat.tupletNumerator = 10; - beat.tupletDenominator = 8; - break; - case 11: - beat.tupletNumerator = 11; - beat.tupletDenominator = 8; - break; - case 12: - beat.tupletNumerator = 12; - beat.tupletDenominator = 8; - break; - default: - beat.tupletNumerator = 1; - beat.tupletDenominator = 1; - break; - } - } - - private _isNoteText(txt: string): boolean { - return txt === 'x' || txt === '-' || txt === 'r'; - } - - private _note(beat: Beat): boolean { - // fret.string or TuningWithAccidentals - let isDead: boolean = false; - let isTie: boolean = false; - let fret: number = -1; - let octave: number = -1; - let tone: number = -1; - let accidentalMode: NoteAccidentalMode = NoteAccidentalMode.Default; - 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: - 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 { - 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: - // auto convert staff - if (beat.index === 0 && beat.voice.index === 0 && beat.voice.bar.index === 0) { - this._makeCurrentStaffPitched(); - } - - const tuning: TuningParseResult = this._syData as TuningParseResult; - octave = tuning.octave; - tone = tuning.tone.noteValue; - if (this._accidentalMode === AlphaTexAccidentalMode.Explicit) { - accidentalMode = tuning.tone.accidentalMode; - } - break; - default: - return false; - } - this._sy = this._newSy(); // Fret done - - const isFretted: boolean = - octave === -1 && this._currentStaff.tuning.length > 0 && !this._currentStaff.isPercussion; - let noteString: number = -1; - if (isFretted) { - // Fret [Dot] String - if (this._sy !== AlphaTexSymbols.Dot) { - this._error('note', AlphaTexSymbols.Dot, true); - } - this._sy = this._newSy(); // dot done - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('note-string', AlphaTexSymbols.Number, true); - } - noteString = this._syData as number; - if (noteString < 1 || noteString > this._currentStaff.tuning.length) { - this._error('note-string', AlphaTexSymbols.Number, false); - } - this._sy = this._newSy(); // string done - } - // read effects - const note: Note = new Note(); - if (isFretted) { - note.string = this._currentStaff.tuning.length - (noteString - 1); - note.isDead = isDead; - note.isTieDestination = isTie; - if (!isTie) { - note.fret = fret; - } - } else if (this._currentStaff.isPercussion) { - const articulationValue = fret; - let articulationIndex: number = 0; - if (this._articulationValueToIndex.has(articulationValue)) { - articulationIndex = this._articulationValueToIndex.get(articulationValue)!; - } else { - articulationIndex = this._currentTrack.percussionArticulations.length; - const articulation = PercussionMapper.getArticulationByInputMidiNumber(articulationValue); - if (articulation === null) { - this._errorMessage(`Unknown articulation value ${articulationValue}`); - } - - this._currentTrack.percussionArticulations.push(articulation!); - this._articulationValueToIndex.set(articulationValue, articulationIndex); - } - - note.percussionArticulation = articulationIndex; - } else { - note.octave = octave; - note.tone = tone; - note.accidentalMode = accidentalMode; - note.isTieDestination = isTie; - } - beat.addNote(note); - this._noteEffects(note); - return true; - } - - private _noteEffects(note: Note): void { - if (this._sy !== AlphaTexSymbols.LBrace) { - return; - } - this._sy = this._newSy(); - while (this._sy === AlphaTexSymbols.String) { - const syData = (this._syData as string).toLowerCase(); - if (syData === 'b' || syData === 'be') { - this._sy = this._newSy(); - const exact: boolean = syData === 'be'; - - // Type - if (this._sy === AlphaTexSymbols.String) { - note.bendType = this._parseBendType(this._syData as string); - this._sy = this._newSy(); - } - - // Style - if (this._sy === AlphaTexSymbols.String) { - note.bendStyle = this._parseBendStyle(this._syData as string); - this._sy = this._newSy(); - } - - // read points - if (this._sy !== AlphaTexSymbols.LParensis) { - this._error('bend-effect', AlphaTexSymbols.LParensis, true); - } - - if (exact) { - // float on position - this._sy = this._newSy(true); - } else { - this._sy = this._newSy(); - } - - while (this._sy !== AlphaTexSymbols.RParensis && this._sy !== AlphaTexSymbols.Eof) { - let offset: number = 0; - let value: number = 0; - if (exact) { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bend-effect-value', AlphaTexSymbols.Number, true); - } - offset = this._syData as number; - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bend-effect-value', AlphaTexSymbols.Number, true); - } - value = this._syData as number; - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('bend-effect-value', AlphaTexSymbols.Number, true); - } - value = this._syData as number; - } - note.addBendPoint(new BendPoint(offset, value)); - - if (exact) { - // float on position - this._sy = this._newSy(true); - } else { - this._sy = this._newSy(); - } - } - const points = note.bendPoints; - if (points != null) { - while (points.length > 60) { - points.splice(points.length - 1, 1); - } - // set positions - if (exact) { - points.sort((a, b) => { - return a.offset - b.offset; - }); - } else { - const count: number = points.length; - const step: number = (60 / (count - 1)) | 0; - let i: number = 0; - while (i < count) { - points[i].offset = Math.min(60, i * step); - i++; - } - } - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('bend-effect', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else if (syData === 'nh') { - note.harmonicType = HarmonicType.Natural; - note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret); - this._sy = this._newSy(); - } else if (syData === 'ah') { - // todo: Artificial Key - note.harmonicType = HarmonicType.Artificial; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'th') { - // todo: store tapped fret in data - note.harmonicType = HarmonicType.Tap; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'ph') { - note.harmonicType = HarmonicType.Pinch; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'sh') { - note.harmonicType = HarmonicType.Semi; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'fh') { - note.harmonicType = HarmonicType.Feedback; - note.harmonicValue = this._harmonicValue(note.harmonicValue); - } else if (syData === 'tr') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('trill-effect', AlphaTexSymbols.Number, true); - } - const fret: number = this._syData as number; - this._sy = this._newSy(); - let duration: Duration = Duration.Sixteenth; - if (this._sy === AlphaTexSymbols.Number) { - switch (this._syData as number) { - case 16: - duration = Duration.Sixteenth; - break; - case 32: - duration = Duration.ThirtySecond; - break; - case 64: - duration = Duration.SixtyFourth; - break; - default: - duration = Duration.Sixteenth; - break; - } - this._sy = this._newSy(); - } - note.trillValue = fret + note.stringTuning; - note.trillSpeed = duration; - } else if (syData === 'v') { - this._sy = this._newSy(); - note.vibrato = VibratoType.Slight; - } else if (syData === 'vw') { - this._sy = this._newSy(); - note.vibrato = VibratoType.Wide; - } else if (syData === 'sl') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.Legato; - } else if (syData === 'ss') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.Shift; - } else if (syData === 'sib') { - this._sy = this._newSy(); - note.slideInType = SlideInType.IntoFromBelow; - } else if (syData === 'sia') { - this._sy = this._newSy(); - note.slideInType = SlideInType.IntoFromAbove; - } else if (syData === 'sou') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.OutUp; - } else if (syData === 'sod') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.OutDown; - } else if (syData === 'psd') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.PickSlideDown; - } else if (syData === 'psu') { - this._sy = this._newSy(); - note.slideOutType = SlideOutType.PickSlideUp; - } else if (syData === 'h') { - this._sy = this._newSy(); - note.isHammerPullOrigin = true; - } else if (syData === 'lht') { - this._sy = this._newSy(); - note.isLeftHandTapped = true; - } else if (syData === 'g') { - this._sy = this._newSy(); - note.isGhost = true; - } else if (syData === 'ac') { - this._sy = this._newSy(); - note.accentuated = AccentuationType.Normal; - } else if (syData === 'hac') { - this._sy = this._newSy(); - note.accentuated = AccentuationType.Heavy; - } else if (syData === 'ten') { - this._sy = this._newSy(); - note.accentuated = AccentuationType.Tenuto; - } else if (syData === 'pm') { - this._sy = this._newSy(); - note.isPalmMute = true; - } else if (syData === 'st') { - this._sy = this._newSy(); - note.isStaccato = true; - } else if (syData === 'lr') { - this._sy = this._newSy(); - note.isLetRing = true; - } else if (syData === 'x') { - this._sy = this._newSy(); - note.isDead = true; - } else if (syData === '-' || syData === 't') { - this._sy = this._newSy(); - note.isTieDestination = true; - } else if (syData === 'lf') { - this._sy = this._newSy(); - let finger: Fingers = Fingers.Thumb; - if (this._sy === AlphaTexSymbols.Number) { - finger = this._toFinger(this._syData as number); - this._sy = this._newSy(); - } - note.leftHandFinger = finger; - } else if (syData === 'rf') { - this._sy = this._newSy(); - let finger: Fingers = Fingers.Thumb; - if (this._sy === AlphaTexSymbols.Number) { - finger = this._toFinger(this._syData as number); - this._sy = this._newSy(); - } - note.rightHandFinger = finger; - } else if (syData === 'acc') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('note-accidental', AlphaTexSymbols.String, true); - } - - note.accidentalMode = ModelUtils.parseAccidentalMode(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'turn') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.Turn; - } else if (syData === 'iturn') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.InvertedTurn; - } else if (syData === 'umordent') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.UpperMordent; - } else if (syData === 'lmordent') { - this._sy = this._newSy(); - note.ornament = NoteOrnament.LowerMordent; - } else if (syData === 'string') { - this._sy = this._newSy(); - note.showStringNumber = true; - } else if (syData === 'hide') { - this._sy = this._newSy(); - note.isVisible = false; - } else if (syData === 'slur') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('slur', AlphaTexSymbols.String, true); - } - - const slurId = this._syData as string; - if (this._slurs.has(slurId)) { - const slurOrigin = this._slurs.get(slurId)!; - slurOrigin.slurDestination = note; - - note.slurOrigin = slurOrigin; - note.isSlurDestination = true; - } else { - this._slurs.set(slurId, note); - } - - this._sy = this._newSy(); - } else if (this._applyBeatEffect(note.beat)) { - // Success - } else { - this._error(syData, AlphaTexSymbols.String, false); - } - } - if (this._sy !== AlphaTexSymbols.RBrace) { - this._error('note-effect', AlphaTexSymbols.RBrace, false); - } - this._sy = this._newSy(); - } - - private _harmonicValue(harmonicValue: number): number { - this._sy = this._newSy(true); - if (this._sy === AlphaTexSymbols.Number) { - harmonicValue = this._syData as number; - this._sy = this._newSy(true); - } - return harmonicValue; - } - - private _toFinger(num: number): Fingers { - switch (num) { - case 1: - return Fingers.Thumb; - case 2: - return Fingers.IndexFinger; - case 3: - return Fingers.MiddleFinger; - case 4: - return Fingers.AnnularFinger; - case 5: - return Fingers.LittleFinger; - } - return Fingers.Thumb; - } - - private _parseDuration(duration: number): Duration { - switch (duration) { - case -4: - return Duration.QuadrupleWhole; - case -2: - return Duration.DoubleWhole; - case 1: - return Duration.Whole; - case 2: - return Duration.Half; - case 4: - return Duration.Quarter; - case 8: - return Duration.Eighth; - case 16: - return Duration.Sixteenth; - case 32: - return Duration.ThirtySecond; - case 64: - return Duration.SixtyFourth; - case 128: - return Duration.OneHundredTwentyEighth; - case 256: - return Duration.TwoHundredFiftySixth; - default: - return Duration.Quarter; - } - } - - private _parseBendStyle(str: string): BendStyle { - switch (str.toLowerCase()) { - case 'gradual': - return BendStyle.Gradual; - case 'fast': - return BendStyle.Fast; - default: - return BendStyle.Default; - } - } - - private _parseBendType(str: string): BendType { - switch (str.toLowerCase()) { - case 'none': - return BendType.None; - case 'custom': - return BendType.Custom; - case 'bend': - return BendType.Bend; - case 'release': - return BendType.Release; - case 'bendrelease': - return BendType.BendRelease; - case 'hold': - return BendType.Hold; - case 'prebend': - return BendType.Prebend; - case 'prebendbend': - return BendType.PrebendBend; - case 'prebendrelease': - return BendType.PrebendRelease; - default: - return BendType.Custom; - } - } - - private _barMeta(bar: Bar): boolean { - let anyMeta = false; - const master: MasterBar = bar.masterBar; - let endOfMeta = false; - while (!endOfMeta && this._sy === AlphaTexSymbols.MetaCommand) { - anyMeta = true; - const syData: string = (this._syData as string).toLowerCase(); - if (syData === 'ts') { - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.String) { - if ((this._syData as string).toLowerCase() === 'common') { - master.timeSignatureCommon = true; - master.timeSignatureNumerator = 4; - master.timeSignatureDenominator = 4; - this._sy = this._newSy(); - } else { - this._error('timesignature-numerator', AlphaTexSymbols.String, true); - } - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('timesignature-numerator', AlphaTexSymbols.Number, true); - } - master.timeSignatureNumerator = this._syData as number; - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('timesignature-denominator', AlphaTexSymbols.Number, true); - } - master.timeSignatureDenominator = this._syData as number; - this._sy = this._newSy(); - } - } else if (syData === 'ft') { - master.isFreeTime = true; - this._sy = this._newSy(); - } else if (syData === 'ro') { - master.isRepeatStart = true; - this._sy = this._newSy(); - } else if (syData === 'rc') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('repeatclose', AlphaTexSymbols.Number, true); - } - if ((this._syData as number) > 2048) { - this._error('repeatclose', AlphaTexSymbols.Number, false); - } - master.repeatCount = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'ae') { - this._sy = this._newSy(); - if (this._sy === AlphaTexSymbols.LParensis) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('alternateending', AlphaTexSymbols.Number, true); - } - this._applyAlternateEnding(master); - while (this._sy === AlphaTexSymbols.Number) { - this._applyAlternateEnding(master); - } - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('alternateending-list', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else { - if (this._sy !== AlphaTexSymbols.Number) { - this._error('alternateending', AlphaTexSymbols.Number, true); - } - this._applyAlternateEnding(master); - } - } else if (syData === 'ks') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('keysignature', AlphaTexSymbols.String, true); - } - bar.keySignature = this._parseKeySignature(this._syData as string); - bar.keySignatureType = this._parseKeySignatureType(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'clef') { - this._sy = this._newSy(); - switch (this._sy) { - case AlphaTexSymbols.String: - bar.clef = this._parseClefFromString(this._syData as string); - break; - case AlphaTexSymbols.Number: - bar.clef = this._parseClefFromInt(this._syData as number); - break; - case AlphaTexSymbols.Tuning: - const parseResult: TuningParseResult = this._syData as TuningParseResult; - bar.clef = this._parseClefFromInt(parseResult.realValue); - break; - default: - this._error('clef', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - } else if (syData === 'tempo') { - const tempoAutomation = this._readTempoAutomation(true); - - const existing = master.tempoAutomations.find(a => a.ratioPosition === tempoAutomation.ratioPosition); - if (existing) { - existing.value = tempoAutomation.value; - existing.text = tempoAutomation.text; - existing.isVisible = tempoAutomation.isVisible; - } else { - master.tempoAutomations.push(tempoAutomation); - } - } else if (syData === 'section') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('section', AlphaTexSymbols.String, true); - } - let text: string = this._syData as string; - this._sy = this._newSy(); - let marker: string = ''; - if (this._sy === AlphaTexSymbols.String && !this._isNoteText((this._syData as string).toLowerCase())) { - marker = text; - text = this._syData as string; - this._sy = this._newSy(); - } - const section: Section = new Section(); - section.marker = marker; - section.text = text; - master.section = section; - } else if (syData === 'tf') { - this._lexer.allowTuning = false; - this._sy = this._newSy(); - this._lexer.allowTuning = true; - switch (this._sy) { - case AlphaTexSymbols.String: - master.tripletFeel = this._parseTripletFeelFromString(this._syData as string); - break; - case AlphaTexSymbols.Number: - master.tripletFeel = this._parseTripletFeelFromInt(this._syData as number); - break; - default: - this._error('triplet-feel', AlphaTexSymbols.String, true); - break; - } - this._sy = this._newSy(); - } else if (syData === 'ac') { - master.isAnacrusis = true; - this._sy = this._newSy(); - } else if (syData === 'db') { - master.isDoubleBar = true; - bar.barLineRight = BarLineStyle.LightLight; - this._sy = this._newSy(); - } else if (syData === 'barlineleft') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('barlineleft', AlphaTexSymbols.String, true); - } - - bar.barLineLeft = this._parseBarLineStyle(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'barlineright') { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('barlineright', AlphaTexSymbols.String, true); - } - - bar.barLineRight = this._parseBarLineStyle(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'accidentals') { - this._handleAccidentalMode(); - } else if (syData === 'jump') { - this._handleDirections(master); - } else if (syData === 'ottava') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('ottava', AlphaTexSymbols.String, true); - } - - bar.clefOttava = this._parseClefOttavaFromString(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'simile') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.String) { - this._error('simile', AlphaTexSymbols.String, true); - } - - bar.simileMark = this._parseSimileMarkFromString(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'scale') { - this._sy = this._newSy(true); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('scale', AlphaTexSymbols.Number, true); - } - - master.displayScale = this._syData as number; - bar.displayScale = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'width') { - this._sy = this._newSy(); - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('width', AlphaTexSymbols.Number, true); - } - - master.displayWidth = this._syData as number; - bar.displayWidth = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'spd') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Down; - - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('spd', AlphaTexSymbols.Number, true); - } - sustainPedal.ratioPosition = this._syData as number; - bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - } else if (syData === 'spu') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Up; - - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('spu', AlphaTexSymbols.Number, true); - } - sustainPedal.ratioPosition = this._syData as number; - bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - } else if (syData === 'sph') { - const sustainPedal = new SustainPedalMarker(); - sustainPedal.pedalType = SustainPedalMarkerType.Hold; - - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('sph', AlphaTexSymbols.Number, true); - } - sustainPedal.ratioPosition = this._syData as number; - bar.sustainPedals.push(sustainPedal); - this._sy = this._newSy(); - } else { - if (bar.index === 0) { - switch (this._handleStaffMeta()) { - case StaffMetaResult.KnownStaffMeta: - // ok -> Continue - break; - case StaffMetaResult.UnknownStaffMeta: - this._error('measure-effects', AlphaTexSymbols.String, false); - break; - case StaffMetaResult.EndOfMetaDetected: - endOfMeta = true; - break; - } - } else { - switch (this._handleStaffMeta()) { - case StaffMetaResult.EndOfMetaDetected: - endOfMeta = true; - break; - default: - this._error('measure-effects', AlphaTexSymbols.String, false); - break; - } - } - } - } - - if (master.index === 0 && master.tempoAutomations.length === 0) { - const tempoAutomation: Automation = new Automation(); - tempoAutomation.isLinear = false; - tempoAutomation.type = AutomationType.Tempo; - tempoAutomation.value = this._score.tempo; - tempoAutomation.text = this._score.tempoLabel; - master.tempoAutomations.push(tempoAutomation); - } - return anyMeta; - } - - private _parseBarLineStyle(v: string): BarLineStyle { - switch (v.toLowerCase()) { - case 'automatic': - return BarLineStyle.Automatic; - case 'dashed': - return BarLineStyle.Dashed; - case 'dotted': - return BarLineStyle.Dotted; - case 'heavy': - return BarLineStyle.Heavy; - case 'heavyheavy': - return BarLineStyle.HeavyHeavy; - case 'heavylight': - return BarLineStyle.HeavyLight; - case 'lightheavy': - return BarLineStyle.LightHeavy; - case 'lightlight': - return BarLineStyle.LightLight; - case 'none': - return BarLineStyle.None; - case 'regular': - return BarLineStyle.Regular; - case 'short': - return BarLineStyle.Short; - case 'tick': - return BarLineStyle.Tick; - } - - return BarLineStyle.Automatic; - } - - private _parseSimileMarkFromString(str: string): SimileMark { - switch (str.toLowerCase()) { - case 'none': - return SimileMark.None; - case 'simple': - return SimileMark.Simple; - case 'firstofdouble': - return SimileMark.FirstOfDouble; - case 'secondofdouble': - return SimileMark.SecondOfDouble; - default: - return SimileMark.None; - } - } - - private _handleDirections(master: MasterBar) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('direction', AlphaTexSymbols.String, true); - } - - switch ((this._syData as string).toLowerCase()) { - case 'fine': - master.addDirection(Direction.TargetFine); - break; - case 'segno': - master.addDirection(Direction.TargetSegno); - break; - case 'segnosegno': - master.addDirection(Direction.TargetSegnoSegno); - break; - case 'coda': - master.addDirection(Direction.TargetCoda); - break; - case 'doublecoda': - master.addDirection(Direction.TargetDoubleCoda); - break; - - case 'dacapo': - master.addDirection(Direction.JumpDaCapo); - break; - case 'dacapoalcoda': - master.addDirection(Direction.JumpDaCapoAlCoda); - break; - case 'dacapoaldoublecoda': - master.addDirection(Direction.JumpDaCapoAlDoubleCoda); - break; - case 'dacapoalfine': - master.addDirection(Direction.JumpDaCapoAlFine); - break; - - case 'dalsegno': - master.addDirection(Direction.JumpDalSegno); - break; - case 'dalsegnoalcoda': - master.addDirection(Direction.JumpDalSegnoAlCoda); - break; - case 'dalsegnoaldoublecoda': - master.addDirection(Direction.JumpDalSegnoAlDoubleCoda); - break; - case 'dalsegnoalfine': - master.addDirection(Direction.JumpDalSegnoAlFine); - break; - - case 'dalsegnosegno': - master.addDirection(Direction.JumpDalSegnoSegno); - break; - case 'dalsegnosegnoalcoda': - master.addDirection(Direction.JumpDalSegnoSegnoAlCoda); - break; - case 'dalsegnosegnoaldoublecoda': - master.addDirection(Direction.JumpDalSegnoSegnoAlDoubleCoda); - break; - case 'dalsegnosegnoalfine': - master.addDirection(Direction.JumpDalSegnoSegnoAlFine); - break; - - case 'dacoda': - master.addDirection(Direction.JumpDaCoda); - break; - case 'dadoublecoda': - master.addDirection(Direction.JumpDaDoubleCoda); - break; - default: - this._errorMessage(`Unexpected direction value: '${this._syData}'`); - return; - } - - this._sy = this._newSy(); - } - - private _readTempoAutomation(withPosition: boolean) { - this._sy = this._newSy(true); - - const tempoAutomation: Automation = new Automation(); - tempoAutomation.isLinear = false; - tempoAutomation.type = AutomationType.Tempo; - - if (this._sy === AlphaTexSymbols.LParensis && withPosition) { - this._sy = this._newSy(true); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tempo', AlphaTexSymbols.Number, true); - } - - tempoAutomation.value = this._syData as number; - this._sy = this._newSy(true); - - if (this._sy === AlphaTexSymbols.String) { - tempoAutomation.text = this._syData as string; - this._sy = this._newSy(true); - } - - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tempo', AlphaTexSymbols.Number, true); - } - tempoAutomation.ratioPosition = this._syData as number; - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String && (this._syData as string) === 'hide') { - tempoAutomation.isVisible = false; - this._sy = this._newSy(); - } - - if (this._sy !== AlphaTexSymbols.RParensis) { - this._error('tempo', AlphaTexSymbols.RParensis, true); - } - this._sy = this._newSy(); - } else if (this._sy === AlphaTexSymbols.Number) { - tempoAutomation.value = this._syData as number; - - this._sy = this._newSy(); - - if (this._sy === AlphaTexSymbols.String && (this._syData as string) !== 'r') { - tempoAutomation.text = this._syData as string; - this._sy = this._newSy(); - } - } else { - this._error('tempo', AlphaTexSymbols.Number, true); - } - - return tempoAutomation; - } - - private _applyAlternateEnding(master: MasterBar): void { - const num = this._syData as number; - if (num < 1) { - // Repeat numberings start from 1 - this._error('alternateending', AlphaTexSymbols.Number, true); - } - // Alternate endings bitflag starts from 0 - master.alternateEndings |= 1 << (num - 1); - this._sy = this._newSy(); - } - - private _parseWhammyType(str: string): WhammyType { - switch (str.toLowerCase()) { - case 'none': - return WhammyType.None; - case 'custom': - return WhammyType.Custom; - case 'dive': - return WhammyType.Dive; - case 'dip': - return WhammyType.Dip; - case 'hold': - return WhammyType.Hold; - case 'predive': - return WhammyType.Predive; - case 'predivedive': - return WhammyType.PrediveDive; - default: - return WhammyType.Custom; - } - } -} diff --git a/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts b/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts deleted file mode 100644 index a5359ff89..000000000 --- a/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { AlphaTexErrorWithDiagnostics, AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter'; -import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; -import { Logger } from '@coderline/alphatab/Logger'; -import type { Score } from '@coderline/alphatab/model/Score'; -import { Settings } from '@coderline/alphatab/Settings'; -import { AlphaTexExporterOld } from 'test/exporter/AlphaTexExporterOld'; -import { AlphaTexError, AlphaTexImporterOld } from 'test/importer/AlphaTexImporterOld'; -import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; -import { TestPlatform } from 'test/TestPlatform'; -import { assert, expect } from 'chai'; - -describe('AlphaTexImporterOldNewCompat', () => { - async function loadScore(name: string): Promise { - const data = await TestPlatform.loadFile(`test-data/${name}`); - try { - return ScoreLoader.loadScoreFromBytes(data); - } catch { - return null; - } - } - - async function readAndCompare( - name: string, - ignoreKeys: string[] | null, - tex: string, - settings: Settings - ): Promise { - const fileName = name.substring(name.lastIndexOf('/') + 1); - const lines = tex.split('\n'); - - let oldScore: Score; - let newScore: Score; - - try { - const oldImporter = new AlphaTexImporterOld(); - oldImporter.initFromString(tex, settings); - oldScore = oldImporter.readScore(); - ComparisonHelpers.alphaTexExportRoundtripPrepare(oldScore); - } catch (e) { - let errorLine = ''; - const error = e as Error; - if (error.cause instanceof AlphaTexError) { - const alphaTexError = error.cause as AlphaTexError; - errorLine = `Error Line: ${lines[alphaTexError.line - 1]}\n`; - } - - assert.fail(`<${fileName}>${e}\n${errorLine}${error.stack}\n Tex:\n${tex}`); - return; - } - - try { - const newImporter = new AlphaTexImporter(); - newImporter.initFromString(tex, settings); - newScore = newImporter.readScore(); - ComparisonHelpers.alphaTexExportRoundtripPrepare(newScore); - } catch (e) { - let errorDetails = ''; - const error = e as Error; - if (error instanceof AlphaTexErrorWithDiagnostics) { - errorDetails = (error as AlphaTexErrorWithDiagnostics).toString(); - } else if (error.cause instanceof AlphaTexErrorWithDiagnostics) { - errorDetails = (error.cause as AlphaTexErrorWithDiagnostics).toString(); - } - assert.fail(`<${fileName}>${e}\n${errorDetails}${error.stack}\n Tex:\n${tex}`); - return; - } - - ComparisonHelpers.alphaTexExportRoundtripEqual(fileName, newScore, oldScore, ignoreKeys); - } - - async function testRoundTripEqual(name: string, ignoreKeys: string[] | null = null): Promise { - const settings = new Settings(); - const expected = await loadScore(name); - if (!expected) { - return; - } - - ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); - - // use exporters to create alphaTex code for comparison test - - settings.exporter.comments = true; - const exportedOld = new AlphaTexExporterOld().exportToString(expected, settings); - await readAndCompare(name, ignoreKeys, exportedOld, settings); - - // old importer cannot load new alphaTex - // const exportedNew = new AlphaTexExporter().exportToString(expected, settings); - // await readAndCompare(name, ignoreKeys, exportedNew, settings); - } - - async function testRoundTripFolderEqual(name: string): Promise { - const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); - for (const file of files.filter(f => !f.endsWith('.png'))) { - await testRoundTripEqual(`${name}/${file}`, null); - } - } - - it('importer', async () => { - await testRoundTripFolderEqual('guitarpro7'); - }); - - it('visual-effects-and-annotations', async () => { - await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); - }); - - it('visual-general', async () => { - await testRoundTripFolderEqual('visual-tests/general'); - }); - - it('visual-guitar-tabs', async () => { - await testRoundTripFolderEqual('visual-tests/guitar-tabs'); - }); - - it('visual-layout', async () => { - await testRoundTripFolderEqual('visual-tests/layout'); - }); - - it('visual-music-notation', async () => { - await testRoundTripFolderEqual('visual-tests/music-notation'); - }); - - it('visual-notation-legend', async () => { - await testRoundTripFolderEqual('visual-tests/notation-legend'); - }); - - it('visual-special-notes', async () => { - await testRoundTripFolderEqual('visual-tests/special-notes'); - }); - - it('visual-special-tracks', async () => { - await testRoundTripFolderEqual('visual-tests/special-tracks'); - }); - - it('performance', async () => { - const newTex = await TestPlatform.loadFileAsString('test-data/exporter/notation-legend-formatted.atex'); - const settings = new Settings(); - const oldTex = new AlphaTexExporterOld().exportToString(ScoreLoader.loadAlphaTex(newTex, settings)); - - const newTimes: number[] = [] ; - const oldTimes: number[] = []; - - function run(i: number, check: boolean, log: boolean) { - const oldImporter = new AlphaTexImporterOld(); - oldImporter.initFromString(oldTex, settings); - - const oldStart = performance.now(); - oldImporter.readScore(); - const oldEnd = performance.now(); - - const newImporter = new AlphaTexImporter(); - newImporter.initFromString(oldTex, settings); - - /*@target web*/ - if (gc) { - gc(); - } - const newStart = performance.now(); - newImporter.readScore(); - const newEnd = performance.now(); - - if (check) { - const oldTime = oldEnd - oldStart; - const newTime = newEnd - newStart; - if (log) { - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'Old', i, oldTime); - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'New', i, newTime); - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'Diff', i, newTime - oldTime); - } - newTimes.push(newTime); - oldTimes.push(oldTime); - } - } - - // warmup - for (let i = 0; i < 10; i++) { - run(i, false, false); - } - - const testCount = 100; - for (let i = 0; i < testCount; i++) { - run(i, true, false); - } - - const meanNew = newTimes[(newTimes.length / 2) | 0]; - expect(meanNew).to.be.lessThan(25); - const meanOld = oldTimes[(oldTimes.length / 2) | 0]; - Logger.info('Test-AlphaTexImporterOldNewCompat-performance', 'Mean Ratio', meanNew / meanOld); - }); - - // it('profile', async () => { - // const session = new inspector.Session(); - // session.connect(); - - // const newTex = await TestPlatform.loadFileAsString('test-data/exporter/notation-legend-formatted.atex'); - // const settings = new Settings(); - // const oldTex = new AlphaTexExporterOld().exportToString(ScoreLoader.loadAlphaTex(newTex, settings)); - - // await new Promise(resolve => { - // session.post('Profiler.enable', () => - // session.post('Profiler.start', () => { - // resolve(); - // }) - // ); - // }); - - // for (let i = 0; i < 10; i++) { - // const newImporter = new AlphaTexImporter(); - // newImporter.initFromString(oldTex, settings); - // newImporter.readScore(); - // } - - // await new Promise((resolve, reject) => { - // session.post('Profiler.stop', async (sessionErr, data) => { - // if (sessionErr) { - // reject(sessionErr); - // return; - // } - - // try { - // await TestPlatform.saveFileAsString( - // `${new Date().toISOString().replaceAll(/[^0-9]/g, '')}.cpuprofile`, - // JSON.stringify(data.profile) - // ); - // resolve(); - // } catch (e) { - // reject(e); - // } - // }); - // }); - // }).timeout(60000); -}); diff --git a/packages/alphatab/test/importer/GpImporterTestHelper.ts b/packages/alphatab/test/importer/GpImporterTestHelper.ts index 605fcdc92..269796297 100644 --- a/packages/alphatab/test/importer/GpImporterTestHelper.ts +++ b/packages/alphatab/test/importer/GpImporterTestHelper.ts @@ -279,13 +279,13 @@ export class GpImporterTestHelper { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].trillSpeed).to.equal(Duration.Sixteenth); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].isTremolo).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].tremoloSpeed).to.equal(Duration.ThirtySecond); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].tremoloPicking!.marks).to.equal(3); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].isTremolo).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tremoloSpeed).to.equal(Duration.Sixteenth); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].tremoloPicking!.marks).to.equal(2); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].isTremolo).to.be.equal(true); - expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].tremoloSpeed).to.equal(Duration.Eighth); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].tremoloPicking!.marks).to.equal(1); } public static checkOtherEffects(score: Score, skipInstrumentCheck: boolean = false): void { diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts index 92e1012b2..159d20799 100644 --- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts +++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts @@ -268,4 +268,9 @@ describe('MusicXmlImporterTests', () => { expect(score.tracks[1].playbackInfo.program).to.equal(1); expect(score.tracks[1].playbackInfo.bank).to.equal(77); }); + + it('buzzroll', async () => { + const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/buzzroll.xml'); + expect(score).toMatchSnapshot(); + }); }); diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap index 1bde3aee4..6fb498388 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap @@ -5625,11 +5625,11 @@ Array [ Map { "code" => 209, "severity" => 2, - "message" => "Unexpected tremolo speed value '0, expected: 8, 16 or 32", + "message" => "Unexpected tremolo marks value '10, expected: 0-5, or legacy: 8, 16 or 32", "start" => Map { - "col" => 11, + "col" => 12, "line" => 1, - "offset" => 10, + "offset" => 11, }, }, ] @@ -6492,6 +6492,132 @@ Map { } `; +exports[`AlphaTexImporterTest tremolos buzzroll-default1 1`] = ` +Map { + "marks" => 1, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default2 1`] = ` +Map { + "marks" => 2, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default3 1`] = ` +Map { + "marks" => 3, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default4 1`] = ` +Map { + "marks" => 4, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos buzzroll-default5 1`] = ` +Map { + "marks" => 5, + "style" => 1, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default1 1`] = ` +Map { + "marks" => 1, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default2 1`] = ` +Map { + "marks" => 2, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default3 1`] = ` +Map { + "marks" => 3, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default4 1`] = ` +Map { + "marks" => 4, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo-default5 1`] = ` +Map { + "marks" => 5, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo1 1`] = ` +Map { + "marks" => 1, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo2 1`] = ` +Map { + "marks" => 2, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo3 1`] = ` +Map { + "marks" => 3, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo4 1`] = ` +Map { + "marks" => 4, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo5 1`] = ` +Map { + "marks" => 5, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo8 1`] = ` +Map { + "marks" => 1, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo16 1`] = ` +Map { + "marks" => 2, + "style" => 0, +} +`; + +exports[`AlphaTexImporterTest tremolos tremolo32 1`] = ` +Map { + "marks" => 3, + "style" => 0, +} +`; + exports[`AlphaTexImporterTest voice-mode barWise 1`] = ` Map { "__kind" => "Score", diff --git a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap index ce04358b9..5e17d7f36 100644 --- a/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/MusicXmlImporter.test.ts.snap @@ -562,6 +562,160 @@ Map { } `; +exports[`MusicXmlImporterTests buzzroll 1`] = ` +Map { + "__kind" => "Score", + "artist" => "Artist", + "copyright" => "Copyright", + "music" => "Music", + "notices" => "Notices", + "title" => "Title", + "words" => "Words", + "tab" => "Tab", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + ], + "tracks" => Array [ + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 0, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 0, + "octave" => 5, + "tone" => 0, + }, + ], + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "tremolopicking" => Map { + "marks" => 0, + "style" => 1, + }, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 5, + "tone" => 0, + }, + ], + "tremolopicking" => Map { + "marks" => 1, + "style" => 0, + }, + "displaystart" => 960, + "playbackstart" => 960, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 2, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 2, + "octave" => 5, + "tone" => 0, + }, + ], + "tremolopicking" => Map { + "marks" => 0, + "style" => 1, + }, + "displaystart" => 1920, + "playbackstart" => 1920, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + Map { + "__kind" => "Beat", + "id" => 3, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 3, + "octave" => 5, + "tone" => 0, + }, + ], + "tremolopicking" => Map { + "marks" => 1, + "style" => 1, + }, + "displaystart" => 2880, + "playbackstart" => 2880, + "displayduration" => 960, + "playbackduration" => 960, + "overridedisplayduration" => 960, + }, + ], + }, + ], + }, + ], + "showtablature" => false, + }, + ], + "playbackinfo" => Map { + "volume" => 0, + "secondarychannel" => 1, + }, + "name" => "TrackĀ 1", + "shortname" => "T1", + }, + ], + "stylesheet" => Map { + "hidedynamics" => true, + }, +} +`; + exports[`MusicXmlImporterTests partwise-anacrusis 1`] = ` Map { "__kind" => "Score", diff --git a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts index 907c000ea..396bec7ca 100644 --- a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -407,5 +407,121 @@ describe('EffectsAndAnnotationsTests', () => { } ); }); + + async function test(tex: string, referenceFileName: string, configure?: (o: VisualTestOptions) => void) { + await VisualTestHelper.runVisualTestTex( + tex, + `test-data/visual-tests/effects-and-annotations/${referenceFileName}.png`, + undefined, + configure + ); + } + + describe('standard', () => { + it('default-flags', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp 1} | C4.32 {tp 1} | + C4.8 {tp 2} | C4.32 {tp 2} | + C4.8 {tp 3} | C4.32 {tp 3} | + C4.8 {tp 4} | C4.32 {tp 4} | + C4.8 {tp 5} | C4.32 {tp 5} + `, + 'tremolo-standard-default-flags' + )); + + it('default-beams', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp 1} C4.8 {tp 1} | C4.32 {tp 1} C4.32 {tp 1} | + C4.8 {tp 2} C4.8 {tp 2} | C4.32 {tp 2} C4.32 {tp 2} | + C4.8 {tp 3} C4.8 {tp 3} | C4.32 {tp 3} C4.32 {tp 3} | + C4.8 {tp 4} C4.8 {tp 4} | C4.32 {tp 4} C4.32 {tp 4} | + C4.8 {tp 5} C4.8 {tp 5} | C4.32 {tp 5} C4.32 {tp 5} + `, + 'tremolo-standard-default-beams' + )); + + it('buzzroll-flags', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp (1 buzzRoll)} | C4.32 {tp (1 buzzRoll)} | + C4.8 {tp (2 buzzRoll)} | C4.32 {tp (2 buzzRoll)} | + C4.8 {tp (3 buzzRoll)} | C4.32 {tp (3 buzzRoll)} | + C4.8 {tp (4 buzzRoll)} | C4.32 {tp (4 buzzRoll)} | + C4.8 {tp (5 buzzRoll)} | C4.32 {tp (5 buzzRoll)} + `, + 'tremolo-standard-buzzroll-flags' + )); + + it('buzzroll-beams', async () => + await test( + ` + \\staff {score slash} + C4.8 {tp (1 buzzRoll)} C4.8 {tp (1 buzzRoll)} | C4.32 {tp (1 buzzRoll)} C4.32 {tp (1 buzzRoll)} | + C4.8 {tp (2 buzzRoll)} C4.8 {tp (2 buzzRoll)} | C4.32 {tp (2 buzzRoll)} C4.32 {tp (2 buzzRoll)} | + C4.8 {tp (3 buzzRoll)} C4.8 {tp (3 buzzRoll)} | C4.32 {tp (3 buzzRoll)} C4.32 {tp (3 buzzRoll)} | + C4.8 {tp (4 buzzRoll)} C4.8 {tp (4 buzzRoll)} | C4.32 {tp (4 buzzRoll)} C4.32 {tp (4 buzzRoll)} | + C4.8 {tp (5 buzzRoll)} C4.8 {tp (5 buzzRoll)} | C4.32 {tp (5 buzzRoll)} C4.32 {tp (5 buzzRoll)} + `, + 'tremolo-standard-buzzroll-beams' + )); + }); + describe('tabs', () => { + it('default-flags', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp 1} | 3.6.32 {tp 1} | + 3.6.8 {tp 2} | 3.6.32 {tp 2} | + 3.6.8 {tp 3} | 3.6.32 {tp 3} | + 3.6.8 {tp 4} | 3.6.32 {tp 4} | + 3.6.8 {tp 5} | 3.6.32 {tp 5} + `, + 'tremolo-tabs-default-flags' + )); + + it('default-beams', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp 1} 3.6.8 {tp 1} | 3.6.32 {tp 1} 3.6.32 {tp 1} | + 3.6.8 {tp 2} 3.6.8 {tp 2} | 3.6.32 {tp 2} 3.6.32 {tp 2} | + 3.6.8 {tp 3} 3.6.8 {tp 3} | 3.6.32 {tp 3} 3.6.32 {tp 3} | + 3.6.8 {tp 4} 3.6.8 {tp 4} | 3.6.32 {tp 4} 3.6.32 {tp 4} | + 3.6.8 {tp 5} 3.6.8 {tp 5} | 3.6.32 {tp 5} 3.6.32 {tp 5} + `, + 'tremolo-tabs-default-beams' + )); + + it('buzzroll-flags', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp (1 buzzRoll)} | 3.6.32 {tp (1 buzzRoll)} | + 3.6.8 {tp (2 buzzRoll)} | 3.6.32 {tp (2 buzzRoll)} | + 3.6.8 {tp (3 buzzRoll)} | 3.6.32 {tp (3 buzzRoll)} | + 3.6.8 {tp (4 buzzRoll)} | 3.6.32 {tp (4 buzzRoll)} | + 3.6.8 {tp (5 buzzRoll)} | 3.6.32 {tp (5 buzzRoll)} + `, + 'tremolo-tabs-buzzroll-flags' + )); + + it('buzzroll-beams', async () => + await test( + ` + \\staff {tabs} + 3.6.8 {tp (1 buzzRoll)} 3.6.8 {tp (1 buzzRoll)} | 3.6.32 {tp (1 buzzRoll)} 3.6.32 {tp (1 buzzRoll)} | + 3.6.8 {tp (2 buzzRoll)} 3.6.8 {tp (2 buzzRoll)} | 3.6.32 {tp (2 buzzRoll)} 3.6.32 {tp (2 buzzRoll)} | + 3.6.8 {tp (3 buzzRoll)} 3.6.8 {tp (3 buzzRoll)} | 3.6.32 {tp (3 buzzRoll)} 3.6.32 {tp (3 buzzRoll)} | + 3.6.8 {tp (4 buzzRoll)} 3.6.8 {tp (4 buzzRoll)} | 3.6.32 {tp (4 buzzRoll)} 3.6.32 {tp (4 buzzRoll)} | + 3.6.8 {tp (5 buzzRoll)} 3.6.8 {tp (5 buzzRoll)} | 3.6.32 {tp (5 buzzRoll)} 3.6.32 {tp (5 buzzRoll)} + `, + 'tremolo-tabs-buzzroll-beams' + )); + }); }); }); diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 162dfd86b..71786b005 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -27,7 +27,8 @@ export const alphaTexMappedEnumLookup = { TripletFeel: alphaTab.model.TripletFeel, BarLineStyle: alphaTab.model.BarLineStyle, SimileMark: alphaTab.model.SimileMark, - Direction: alphaTab.model.Direction + Direction: alphaTab.model.Direction, + TremoloPickingStyle: alphaTab.model.TremoloPickingStyle }; export type AlphaTexMappedEnumName = keyof typeof alphaTexMappedEnumLookup; @@ -448,6 +449,10 @@ export const alphaTexMappedEnumMapping: { JumpDalSegnoSegnoAlFine: { snippet: 'dalSegnoSegnoAlFine', shortDescription: 'DalSegnoSegnoAlFine (Jump)' }, JumpDaCoda: { snippet: 'daCoda', shortDescription: 'DaCoda (Jump)' }, JumpDaDoubleCoda: { snippet: 'daDoubleCoda', shortDescription: 'DaDoubleCoda (Jump)' } + }, + TremoloPickingStyle: { + Default: { snippet: 'default', shortDescription: 'Default tremolo' }, + BuzzRoll: { snippet: 'buzzRoll', shortDescription: 'Buzz roll tremolo' } } }; diff --git a/packages/alphatex/src/properties/beat/tp.ts b/packages/alphatex/src/properties/beat/tp.ts index a9084ccbe..cf06c2c02 100644 --- a/packages/alphatex/src/properties/beat/tp.ts +++ b/packages/alphatex/src/properties/beat/tp.ts @@ -1,4 +1,5 @@ import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; import type { PropertyDefinition } from '@coderline/alphatab-alphatex/types'; export const tp: PropertyDefinition = { @@ -10,20 +11,36 @@ export const tp: PropertyDefinition = { { parameters: [ { - name: 'speed', - shortDescription: 'The tremolo picking speed', + name: 'marks', + shortDescription: 'The number of tremolo marks', type: alphaTab.importer.alphaTex.AlphaTexNodeType.Number, parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + valuesOnlyForCompletion: true, values: [ - { name: '8', snippet: '8', shortDescription: '8th Notes' }, - { name: '16', snippet: '16', shortDescription: '16th Notes' }, - { name: '32', snippet: '32', shortDescription: '32nd Notes' } + { name: '1', snippet: '1', shortDescription: '1 tremolo mark (8th notes)' }, + { name: '2', snippet: '2', shortDescription: '2 tremolo mark (16th notes)' }, + { name: '3', snippet: '3', shortDescription: '3 tremolo mark (32nd notes)' }, + { name: '4', snippet: '4', shortDescription: '4 tremolo mark (64th notes)' }, + { name: '5', snippet: '5', shortDescription: '5 tremolo mark (128th notes)' } ] + }, + { + name: 'style', + shortDescription: 'The tremolo style', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Optional, + ...enumParameter('TremoloPickingStyle') } ] } ], - examples: ` - 3.3{tp 8} 3.3{tp 16} 3.3{tp 32} + examples: [ + ` + 3.3{tp 1} 3.3{tp 2} 3.3{tp 3} + `, + ` + \\title "Buzz Rolls" + 3.3{tp (0 buzzRoll)} // no audio + 3.3{tp (1 buzzRoll)} // 8th notes tremolo shown as buzzroll ` + ] };