diff --git a/biome.jsonc b/biome.jsonc index c2d239aa3..10cd06bb8 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", "files": { "maxSize": 5242880 }, @@ -12,6 +12,13 @@ "lineWidth": 120, "lineEnding": "lf" }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, "linter": { "enabled": true, "rules": { @@ -28,7 +35,8 @@ "noDescendingSpecificity": "off" }, "suspicious": { - "noExplicitAny": "off" // used in areas where we work with dynamic JSON data + "noExplicitAny": "off", // used in areas where we work with dynamic JSON data + "noEmptyInterface": "off" // we use these for marker interfaces }, "correctness": { "noUnusedImports": { @@ -62,4 +70,4 @@ "trailingCommas": "none" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 100adedb3..0c98e7abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2545,6 +2545,12 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "license": "Apache-2.0" }, + "node_modules/ace-builds": { + "version": "1.43.3", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.3.tgz", + "integrity": "sha512-MCl9rALmXwIty/4Qboijo/yNysx1r6hBTzG+6n/TiOm5LFhZpEvEIcIITPFiEOEFDfgBOEmxu+a4f54LEFM6Sg==", + "license": "BSD-3-Clause" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7859,6 +7865,7 @@ "@fortawesome/fontawesome-free": "^7.1.0", "@popperjs/core": "^2.11.8", "@types/serve-static": "^1.15.9", + "ace-builds": "^1.43.3", "bootstrap": "^5.3.8", "handlebars": "^4.7.8", "serve-static": "^2.2.0" diff --git a/package.json b/package.json index 7e146bcbd..38b99c64d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "generate-typescript": "npm run generate-typescript --workspace=packages/alphatab", "build": "npm run build --workspace=packages/alphatab", - "test": "npm run build --workspace=packages/alphatab", + "test": "npm run test --workspace=packages/alphatab", "build-web": "npm run build && npm run build-webpack && npm run build-vite", "test-web": "npm run test && npm run test-webpack && npm run test-vite", diff --git a/packages/alphatab/.env b/packages/alphatab/.env index ce5c0bc60..72295dc04 100644 --- a/packages/alphatab/.env +++ b/packages/alphatab/.env @@ -1 +1,2 @@ -FORCE_COLOR=1 \ No newline at end of file +FORCE_COLOR=1 +NODE_OPTIONS=--expose-gc diff --git a/packages/alphatab/biome.jsonc b/packages/alphatab/biome.jsonc index c2170eb90..dc36797f3 100644 --- a/packages/alphatab/biome.jsonc +++ b/packages/alphatab/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", "root": false, "extends": "//", "files": { diff --git a/packages/alphatab/src/AlphaTabApiBase.ts b/packages/alphatab/src/AlphaTabApiBase.ts index 8ea913b86..3a35588fb 100644 --- a/packages/alphatab/src/AlphaTabApiBase.ts +++ b/packages/alphatab/src/AlphaTabApiBase.ts @@ -19,14 +19,15 @@ import type { AlphaTabRestEvent, ControlChangeEvent, EndOfTrackEvent, - MidiEvent,MidiEventType, + MidiEvent, + MidiEventType, NoteBendEvent, NoteOffEvent, NoteOnEvent, PitchBendEvent, ProgramChangeEvent, TempoChangeEvent, - TimeSignatureEvent + TimeSignatureEvent } from '@src/midi/MidiEvent'; import { MidiFile } from '@src/midi/MidiFile'; import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; @@ -772,7 +773,7 @@ export class AlphaTabApiBase { */ public tex(tex: string, tracks?: number[]): void { try { - const parser: AlphaTexImporter = new AlphaTexImporter(); + const parser = new AlphaTexImporter(); parser.logErrors = true; parser.initFromString(tex, this.settings); const score: Score = parser.readScore(); diff --git a/packages/alphatab/src/AlphaTabError.ts b/packages/alphatab/src/AlphaTabError.ts index 0c5df6c42..3a6168703 100644 --- a/packages/alphatab/src/AlphaTabError.ts +++ b/packages/alphatab/src/AlphaTabError.ts @@ -16,6 +16,5 @@ export class AlphaTabError extends Error { public constructor(type: AlphaTabErrorType, message: string | null = '', inner?: Error) { super(message ?? '', { cause: inner }); this.type = type; - Object.setPrototypeOf(this, AlphaTabError.prototype); } } diff --git a/packages/alphatab/src/FileLoadError.ts b/packages/alphatab/src/FileLoadError.ts index fea73ca4a..c74bce795 100644 --- a/packages/alphatab/src/FileLoadError.ts +++ b/packages/alphatab/src/FileLoadError.ts @@ -10,6 +10,5 @@ export class FileLoadError extends AlphaTabError { public constructor(message: string, xhr: XMLHttpRequest) { super(AlphaTabErrorType.General, message); this.xhr = xhr; - Object.setPrototypeOf(this, FileLoadError.prototype); } } diff --git a/packages/alphatab/src/FormatError.ts b/packages/alphatab/src/FormatError.ts index c6238a480..53dd4ca27 100644 --- a/packages/alphatab/src/FormatError.ts +++ b/packages/alphatab/src/FormatError.ts @@ -7,6 +7,5 @@ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; export class FormatError extends AlphaTabError { public constructor(message: string) { super(AlphaTabErrorType.Format, message); - Object.setPrototypeOf(this, FormatError.prototype); } } diff --git a/packages/alphatab/src/exporter/AlphaTexExporter.ts b/packages/alphatab/src/exporter/AlphaTexExporter.ts index 3f134000a..70a13e147 100644 --- a/packages/alphatab/src/exporter/AlphaTexExporter.ts +++ b/packages/alphatab/src/exporter/AlphaTexExporter.ts @@ -1,69 +1,35 @@ -import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { Environment } from '@src/Environment'; +import { ScoreExporter } from '@src/exporter/ScoreExporter'; +import { AlphaTex1LanguageHandler } from '@src/importer/alphaTex/AlphaTex1LanguageHandler'; +import { + type AlphaTexBarNode, + type AlphaTexBeatDurationChangeNode, + type AlphaTexBeatNode, + type AlphaTexComment, + type AlphaTexIdentifier, + type AlphaTexMetaDataNode, + AlphaTexNodeType, + type AlphaTexNoteListNode, + type AlphaTexNoteNode, + type AlphaTexNumberLiteral, + type AlphaTexPropertiesNode, + type AlphaTexPropertyNode, + type AlphaTexScoreNode, + type AlphaTexStringLiteral, + type AlphaTexValueList, + type IAlphaTexAstNode +} from '@src/importer/alphaTex/AlphaTexAst'; +import type { IAlphaTexLanguageImportHandler } from '@src/importer/alphaTex/IAlphaTexLanguageImportHandler'; import { IOHelper } from '@src/io/IOHelper'; -import { GeneralMidi } from '@src/midi/GeneralMidi'; -import { AccentuationType } from '@src/model/AccentuationType'; -import { AutomationType } from '@src/model/Automation'; -import { type Bar, BarLineStyle, SustainPedalMarkerType } from '@src/model/Bar'; -import { BarreShape } from '@src/model/BarreShape'; -import { type Beat, BeatBeamingMode } from '@src/model/Beat'; -import { BendStyle } from '@src/model/BendStyle'; -import { BendType } from '@src/model/BendType'; -import { BrushType } from '@src/model/BrushType'; -import type { Chord } from '@src/model/Chord'; -import { Clef } from '@src/model/Clef'; -import { CrescendoType } from '@src/model/CrescendoType'; -import { Direction } from '@src/model/Direction'; -import { DynamicValue } from '@src/model/DynamicValue'; -import { FadeType } from '@src/model/FadeType'; -import { FermataType } from '@src/model/Fermata'; -import { Fingers } from '@src/model/Fingers'; -import { GolpeType } from '@src/model/GolpeType'; -import { GraceType } from '@src/model/GraceType'; -import { HarmonicType } from '@src/model/HarmonicType'; -import { KeySignature } from '@src/model/KeySignature'; -import { KeySignatureType } from '@src/model/KeySignatureType'; -import type { MasterBar } from '@src/model/MasterBar'; -import { ModelUtils } from '@src/model/ModelUtils'; +import type { Bar } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; import type { Note } from '@src/model/Note'; -import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; -import { NoteOrnament } from '@src/model/NoteOrnament'; -import { Ottavia } from '@src/model/Ottavia'; import { PercussionMapper } from '@src/model/PercussionMapper'; -import { PickStroke } from '@src/model/PickStroke'; -import { Rasgueado } from '@src/model/Rasgueado'; -import { - BracketExtendMode, - type RenderStylesheet, - TrackNameMode, - TrackNameOrientation, - TrackNamePolicy -} from '@src/model/RenderStylesheet'; -import { Score } from '@src/model/Score'; -import { SimileMark } from '@src/model/SimileMark'; -import { SlideInType } from '@src/model/SlideInType'; -import { SlideOutType } from '@src/model/SlideOutType'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; -import { TripletFeel } from '@src/model/TripletFeel'; +import type { Score } from '@src/model/Score'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; import { Tuning } from '@src/model/Tuning'; -import { VibratoType } from '@src/model/VibratoType'; -import type { Voice } from '@src/model/Voice'; -import { WahPedal } from '@src/model/WahPedal'; -import { WhammyType } from '@src/model/WhammyType'; -import { BeamDirection } from '@src/rendering/_barrel'; import { Settings } from '@src/Settings'; -import { ScoreExporter } from './ScoreExporter'; - -/** - * @internal - */ -class WriterGroup { - start: string = ''; - end: string = ''; - comment: string = ''; - hasContent: boolean = false; -} /** * A small helper to write formatted alphaTex code to a string buffer. @@ -75,46 +41,6 @@ class AlphaTexWriter { 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++; @@ -128,12 +54,6 @@ class AlphaTexWriter { } 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++) { @@ -141,22 +61,6 @@ class AlphaTexWriter { } } 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) { @@ -165,48 +69,9 @@ class AlphaTexWriter { 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) { @@ -226,1137 +91,610 @@ class AlphaTexWriter { } /** - * This ScoreExporter can write alphaTex strings. - * @public + * @internal */ -export class AlphaTexExporter extends ScoreExporter { - // used to lookup some default values. - private static readonly _defaultScore = new Score(); - private static readonly _defaultTrack = new Track(); +class AlphaTexPrinter { + private _writer: AlphaTexWriter; + private _comments = false; - public get name(): string { - return 'alphaTex'; - } - - public exportToString(score: Score, settings: Settings | null = null) { - this.settings = settings ?? new Settings(); - return this.scoreToAlphaTexString(score); + public get tex() { + return this._writer.tex; } - public writeScore(score: Score) { - const raw = IOHelper.stringToBytes(this.scoreToAlphaTexString(score)); - this.data.write(raw, 0, raw.length); - } - - public scoreToAlphaTexString(score: Score): string { + constructor(settings: Settings) { const writer = new AlphaTexWriter(); - 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; + this._comments = settings.exporter.comments; + writer.indentString = settings.exporter.indent > 0 ? ' '.repeat(settings.exporter.indent) : ''; + this._writer = writer; } - private _writeScoreTo(writer: AlphaTexWriter, 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 !== AlphaTexExporter._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 + public writeScoreNode(node: AlphaTexScoreNode) { + this._writeComments(node.leadingComments); - for (const track of score.tracks) { - writer.writeLine(); - this._writeTrackTo(writer, track); + for (const b of node.bars) { + this._writeBar(b); } - 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: AlphaTexWriter, stylesheet: RenderStylesheet) { - writer.writeSingleLineComment('Score Stylesheet'); - if (stylesheet.hideDynamics) { - writer.writeMeta('hideDynamics'); - } - if (stylesheet.bracketExtendMode !== AlphaTexExporter._defaultScore.stylesheet.bracketExtendMode) { - writer.writeMeta('bracketExtendMode', BracketExtendMode[stylesheet.bracketExtendMode]); - } - if (stylesheet.useSystemSignSeparator) { - writer.writeMeta('useSystemSignSeparator'); - } - if (stylesheet.multiTrackMultiBarRest) { - writer.writeMeta('multiBarRest'); - } - if ( - stylesheet.singleTrackTrackNamePolicy !== - AlphaTexExporter._defaultScore.stylesheet.singleTrackTrackNamePolicy - ) { - writer.writeMeta('singleTrackTrackNamePolicy', TrackNamePolicy[stylesheet.singleTrackTrackNamePolicy]); - } - if ( - stylesheet.multiTrackTrackNamePolicy !== AlphaTexExporter._defaultScore.stylesheet.multiTrackTrackNamePolicy - ) { - writer.writeMeta('multiTrackTrackNamePolicy', TrackNamePolicy[stylesheet.multiTrackTrackNamePolicy]); - } - if (stylesheet.firstSystemTrackNameMode !== AlphaTexExporter._defaultScore.stylesheet.firstSystemTrackNameMode) { - writer.writeMeta('firstSystemTrackNameMode', TrackNameMode[stylesheet.firstSystemTrackNameMode]); - } - if ( - stylesheet.otherSystemsTrackNameMode !== AlphaTexExporter._defaultScore.stylesheet.otherSystemsTrackNameMode - ) { - writer.writeMeta('otherSystemsTrackNameMode', TrackNameMode[stylesheet.otherSystemsTrackNameMode]); - } - if ( - stylesheet.firstSystemTrackNameOrientation !== - AlphaTexExporter._defaultScore.stylesheet.firstSystemTrackNameOrientation - ) { - writer.writeMeta( - 'firstSystemTrackNameOrientation', - TrackNameOrientation[stylesheet.firstSystemTrackNameOrientation] - ); - } - if ( - stylesheet.otherSystemsTrackNameOrientation !== - AlphaTexExporter._defaultScore.stylesheet.otherSystemsTrackNameOrientation - ) { - writer.writeMeta( - 'otherSystemsTrackNameOrientation', - TrackNameOrientation[stylesheet.otherSystemsTrackNameOrientation] - ); - } - - // Unsupported: - // 'globaldisplaychorddiagramsontop', - // 'pertrackchorddiagramsontop', - // 'globaldisplaytuning', - // 'globaldisplaytuning', - // 'pertrackdisplaytuning', - // 'pertrackchorddiagramsontop', - // 'pertrackmultibarrest', + this._writeComments(node.trailingComments); } - private _writeTrackTo(writer: AlphaTexWriter, track: Track) { - writer.write('\\track '); - writer.writeString(track.name); - if (track.shortName.length > 0) { - writer.writeString(track.shortName); - } - - writer.writeLine(' {'); - writer.indent(); + private _writeBar(bar: AlphaTexBarNode) { + this._writeComments(bar.leadingComments); + this._writeMetaDataList(bar.metaData); - writer.writeSingleLineComment('Track Properties'); + this._writer.indent(); - if (track.color.rgba !== AlphaTexExporter._defaultTrack.color.rgba) { - writer.write(` color `); - writer.writeString(track.color.rgba); - writer.writeLine(); - } - if (track.defaultSystemsLayout !== AlphaTexExporter._defaultTrack.defaultSystemsLayout) { - writer.write(` defaultSystemsLayout ${track.defaultSystemsLayout}`); - writer.writeLine(); - } - if (track.systemsLayout.length > 0) { - writer.write(` systemsLayout ${track.systemsLayout.join(' ')}`); - writer.writeLine(); + for (const beat of bar.beats) { + this._writeBeat(beat); } - writer.writeLine(` volume ${track.playbackInfo.volume}`); - writer.writeLine(` balance ${track.playbackInfo.balance}`); + this._writer.outdent(); - if (track.playbackInfo.isMute) { - writer.writeLine(` mute`); - } - if (track.playbackInfo.isSolo) { - writer.writeLine(` solo`); - } + this._writeComments(bar.trailingComments); + this._writeToken(bar.pipe, true); + } - if ( - track.score.stylesheet.perTrackMultiBarRest && - track.score.stylesheet.perTrackMultiBarRest!.has(track.index) - ) { - writer.writeLine(` multibarrest`); + private _writeBeat(beat: AlphaTexBeatNode) { + this._writeComments(beat.leadingComments); + if (beat.durationChange) { + this._writeDurationChange(beat.durationChange); } - writer.writeLine( - ` instrument ${track.isPercussion ? 'percussion' : GeneralMidi.getName(track.playbackInfo.program)}` - ); - if (track.playbackInfo.bank > 0) { - writer.writeLine(` bank ${track.playbackInfo.bank}`); + if (beat.rest) { + this._writeValue(beat.rest); + } else if (beat.notes) { + this._writeNotes(beat.notes); } - writer.outdent(); - writer.writeLine('}'); + this._writeToken(beat.durationDot, false); + this._writeValue(beat.durationValue); - writer.indent(); + this._writeToken(beat.beatMultiplier, false); + this._writeValue(beat.beatMultiplierValue); - for (const staff of track.staves) { - this._writeStaffTo(writer, staff); + if (beat.beatEffects) { + this._writeProperties(beat.beatEffects, false); } - // Unsupported: - // - custom percussionArticulations - // - style - - writer.outdent(); + this._writeComments(beat.trailingComments); + this._writer.writeLine(); } - private _writeStaffTo(writer: AlphaTexWriter, staff: Staff) { - writer.write('\\staff '); + private _writeNotes(notes: AlphaTexNoteListNode) { + this._writeComments(notes.leadingComments); + this._writeToken(notes.openParenthesis, false); - 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(); + let first = true; + for (const n of notes.notes) { + if (!first) { + this._writer.write(' '); } + this._writeNote(n); + first = false; } - // Unsupported: - // - style - - writer.outdent(); + this._writeToken(notes.closeParenthesis, false); + this._writeComments(notes.trailingComments); } - private _writeBarTo(writer: AlphaTexWriter, 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); + private _writeNote(n: AlphaTexNoteNode) { + this._writeComments(n.leadingComments); - // Unsupported: - // - style + this._writeValue(n.noteValue); + this._writeToken(n.noteStringDot, false); + this._writeValue(n.noteString); - if (!bar.isEmpty) { - this._writeVoiceTo(writer, bar.voices[voiceIndex]); - } else { - writer.writeSingleLineComment(`empty bar`); + if (n.noteEffects) { + this._writeProperties(n.noteEffects, false); } - writer.outdent(); + this._writeComments(n.trailingComments); } - private _writeStaffMetaTo(writer: AlphaTexWriter, staff: Staff) { - writer.writeSingleLineComment(`Staff ${staff.index + 1} Metadata`); + private _writeDurationChange(durationChange: AlphaTexBeatDurationChangeNode) { + this._writeComments(durationChange.leadingComments); + this._writeToken(durationChange.colon, false); + this._writeValue(durationChange.value); - if (staff.capo !== 0) { - writer.writeMeta('capo', `${staff.capo}`); + if (durationChange.properties) { + this._writer.write(' '); + this._writeProperties(durationChange.properties, false); } - 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); - } + this._writeComments(durationChange.trailingComments); - 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); - } - } + this._writer.write(' '); } - private _writeChordTo(writer: AlphaTexWriter, c: Chord) { - writer.write('\\chord {'); - if (c.firstFret > 0) { - writer.write(`firstfret ${c.firstFret} `); + private _writeMetaDataList(metaData: AlphaTexMetaDataNode[]) { + for (const m of metaData) { + this._writeMetaData(m); } - 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: AlphaTexWriter, 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}`); - } + private _trackIndex = 0; + private _staffIndex = 0; + private _voiceIndex = 0; - 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)); + private _writeMetaData(m: AlphaTexMetaDataNode) { + // outdent from previous items if we had indents + switch (m.tag.tag.text) { + case 'track': + if (this._trackIndex > 0) { + this._writer.outdent(); + } + break; + case 'staff': + if (this._staffIndex > 0) { + this._writer.outdent(); + } + break; + case 'voice': + if (this._voiceIndex > 0) { + this._writer.outdent(); + } + break; } - if (masterBar.displayWidth > 0) { - writer.writeMeta('width', `${masterBar.displayWidth}`); - } + this._writeComments(m.leadingComments); + this._writeToken(m.tag.prefix, false); + this._writeValue(m.tag.tag); - 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); + let newLineAfterMeta = true; + if (m.propertiesBeforeValues) { + if (m.properties) { + this._writer.write(' '); + this._writeProperties(m.properties, false); } - } - - for (const a of masterBar.tempoAutomations) { - writer.write(`\\tempo ( ${a.value} `); - if (a.text) { - writer.writeString(a.text); + if (m.values) { + this._writer.write(' '); + this._writeValues(m.values); } - writer.write(`${a.ratioPosition} `); - if (!a.isVisible) { - writer.writeLine('hide '); + } else { + if (m.values) { + this._writer.write(' '); + this._writeValues(m.values); } - writer.writeLine(`)`); - } - - writer.dropSingleLineComment(); - } - - private _writeBarMetaTo(writer: AlphaTexWriter, 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); + if (m.properties) { + this._writer.write(' '); + this._writeProperties(m.properties, true); + newLineAfterMeta = false; } - 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 (m.trailingComments) { + this._writer.write(' '); + this._writeComments(m.trailingComments); } - 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; - } + // outdent from previous items if we had indents + switch (m.tag.tag.text) { + case 'track': + this._trackIndex++; + this._staffIndex = 0; + this._voiceIndex = 0; + this._writer.indent(); + break; + case 'staff': + this._staffIndex++; + this._voiceIndex = 0; + this._writer.indent(); + break; + case 'voice': + this._voiceIndex++; + this._writer.indent(); + break; } - if (bar.barLineLeft !== BarLineStyle.Automatic) { - writer.writeMeta('barlineleft', BarLineStyle[bar.barLineLeft]); + if (newLineAfterMeta) { + this._writer.writeLine(); } + } - if (bar.barLineRight !== BarLineStyle.Automatic) { - writer.writeMeta('barlineright', BarLineStyle[bar.barLineRight]); + private _writeProperties(properties: AlphaTexPropertiesNode, indent: boolean) { + this._writeComments(properties.leadingComments); + this._writeToken(properties.openBrace, indent); + if (indent) { + this._writer.indent(); } - 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; - } + let first = true; + for (const p of properties.properties) { + if (!first && !indent) { + this._writer.write(' '); + } + this._writeProperty(p); + first = false; + if (indent) { + this._writer.writeLine(); } - writer.writeStringMeta('ks', ks); } - if (writer.tex.length > l) { - writer.writeLine(); + if (indent) { + this._writer.outdent(); } - - writer.dropSingleLineComment(); + this._writeToken(properties.closeBrace, indent); + this._writeComments(properties.trailingComments); } - private _writeVoiceTo(writer: AlphaTexWriter, voice: Voice) { - if (voice.isEmpty) { - writer.writeSingleLineComment(`empty voice`); - return; - } - - writer.writeSingleLineComment(`Bar ${voice.bar.index + 1} / Voice ${voice.index + 1} contents`); + private _writeProperty(p: AlphaTexPropertyNode) { + this._writeComments(p.leadingComments); - // Unsupported: - // - style - - for (const beat of voice.beats) { - this._writeBeatTo(writer, beat); + this._writeValue(p.property); + if (p.values) { + this._writer.write(' '); + this._writeValues(p.values); } - } - - private _writeBeatTo(writer: AlphaTexWriter, 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); - } + this._writeComments(p.trailingComments); + } - if (beat.notes.length > 1) { - writer.write(')'); + private _writeValues(values: AlphaTexValueList) { + this._writeComments(values.leadingComments); + this._writeToken(values.openParenthesis, false); + let first = true; + for (const v of values.values) { + if (!first) { + this._writer.write(' '); } + this._writeValue(v); + first = false; } - - writer.write(`.${beat.duration as number}`); - - // Unsupported: - // - style - - this._writeBeatEffectsTo(writer, beat); - - writer.writeLine(); + this._writeToken(values.closeParenthesis, false); + this._writeComments(values.trailingComments); } - private _writeBeatEffectsTo(writer: AlphaTexWriter, beat: Beat) { - writer.beginGroup('{', '}'); + private _writeValue(v: IAlphaTexAstNode | undefined) { + if (!v) { + return; + } + this._writeComments(v.leadingComments); + switch (v.nodeType) { + case AlphaTexNodeType.Ident: + this._writer.write((v as AlphaTexIdentifier).text); + break; - switch (beat.fade) { - case FadeType.FadeIn: - writer.writeGroupItem('f'); + case AlphaTexNodeType.Values: + this._writeValues(v as AlphaTexValueList); break; - case FadeType.FadeOut: - writer.writeGroupItem('fo'); + case AlphaTexNodeType.Number: + this._writer.write((v as AlphaTexNumberLiteral).value.toString()); break; - case FadeType.VolumeSwell: - writer.writeGroupItem('vs'); + case AlphaTexNodeType.String: + this._writer.writeString((v as AlphaTexStringLiteral).text); break; } + this._writeComments(v.trailingComments); + } - 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'); + private _writeToken(tokenNode: IAlphaTexAstNode | undefined, newLine: boolean) { + if (tokenNode) { + this._writeComments(tokenNode.leadingComments); + switch (tokenNode.nodeType) { + case AlphaTexNodeType.Dot: + this._writer.write('.'); break; - case WhammyType.Dive: - writer.writeGroupItem('dive'); + case AlphaTexNodeType.Backslash: + this._writer.write('\\'); break; - case WhammyType.Dip: - writer.writeGroupItem('dip'); + case AlphaTexNodeType.DoubleBackslash: + this._writer.write('\\\\'); break; - case WhammyType.Hold: - writer.writeGroupItem('hold'); + case AlphaTexNodeType.Pipe: + this._writer.write('|'); break; - case WhammyType.Predive: - writer.writeGroupItem('predive'); + case AlphaTexNodeType.LBrace: + this._writer.write('{'); break; - case WhammyType.PrediveDive: - writer.writeGroupItem('predivedive'); + case AlphaTexNodeType.RBrace: + this._writer.write('}'); break; - } - - switch (beat.whammyStyle) { - case BendStyle.Default: + case AlphaTexNodeType.LParen: + this._writer.write('('); break; - case BendStyle.Gradual: - writer.writeGroupItem('gradual'); + case AlphaTexNodeType.RParen: + this._writer.write(')'); break; - case BendStyle.Fast: - writer.writeGroupItem('fast'); + case AlphaTexNodeType.Colon: + this._writer.write(':'); + break; + case AlphaTexNodeType.Asterisk: + this._writer.write('*'); break; } - - writer.beginGroup('(', ')'); - - for (const p of beat.whammyBarPoints!) { - writer.writeGroupItem(` ${p.offset} ${p.value}`); + this._writeComments(tokenNode.trailingComments); + if (newLine) { + this._writer.writeLine(); } - - 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); + private _writeComments(comments: AlphaTexComment[] | undefined) { + if (!this._comments || !comments) { + return; } - if (beat.ottava !== Ottavia.Regular) { - let ottava = Ottavia[beat.ottava]; - if (ottava.startsWith('_')) { - ottava = ottava.substring(1); + for (const c of comments) { + let txt = c.text; + if (!txt.startsWith(' ')) { + txt = ` ${txt}`; } - 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]); + if (c.multiLine) { + if (!txt.endsWith(' ')) { + txt += ' '; } + + this._writer.write(`/*${txt}*/`); } else { - writer.writeGroupItem('lyrics '); - writer.writeString(beat.lyrics[0]); + this._writer.writeLine(`//${txt}`); } } + } +} - 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}`); - } +/** + * This ScoreExporter can write alphaTex strings. + * @public + */ +export class AlphaTexExporter extends ScoreExporter { + private _handler: IAlphaTexLanguageImportHandler = AlphaTex1LanguageHandler.instance; - switch (beat.crescendo) { - case CrescendoType.Crescendo: - writer.writeGroupItem('cre'); - break; - case CrescendoType.Decrescendo: - writer.writeGroupItem('dec'); - break; - } + public get name(): string { + return 'alphaTex'; + } - if ((beat.voice.bar.index === 0 && beat.index === 0) || beat.dynamics !== beat.previousBeat?.dynamics) { - writer.writeGroupItem(`dy ${DynamicValue[beat.dynamics].toLowerCase()}`); - } + public exportToString(score: Score, settings: Settings | null = null) { + this.settings = settings ?? new Settings(); + return this.scoreToAlphaTexString(score); + } - const fermata = beat.fermata; - if (fermata != null) { - writer.writeGroupItem(`fermata ${FermataType[fermata.type]} ${fermata.length}`); - } + public writeScore(score: Score) { + const raw = IOHelper.stringToBytes(this.scoreToAlphaTexString(score)); + this.data.write(raw, 0, raw.length); + } - if (beat.isLegatoOrigin) { - writer.writeGroupItem('legatoorigin'); - } + public scoreToAlphaTexString(score: Score): string { + const printer = new AlphaTexPrinter(this.settings); + printer.writeScoreNode(this._score(score)); + return printer.tex; + } - 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; - } - } + private _score(data: Score): AlphaTexScoreNode { + const score: AlphaTexScoreNode = { + nodeType: AlphaTexNodeType.Score, + bars: [] + }; - switch (beat.wahPedal) { - case WahPedal.Open: - writer.writeGroupItem(`waho`); - break; - case WahPedal.Closed: - writer.writeGroupItem(`wahc`); - break; + for (const t of data.tracks) { + this._track(score, t); } - if (beat.isBarre) { - writer.writeGroupItem(`barre ${beat.barreFret} ${BarreShape[beat.barreShape]}`); + if (score.bars.length === 0) { + score.bars.push({ + nodeType: AlphaTexNodeType.Bar, + metaData: this._handler.buildScoreMetaDataNodes(data), + beats: [] + }); + } else { + score.bars[0].metaData = this._handler + .buildScoreMetaDataNodes(data) + .concat(score.bars[0].metaData as AlphaTexMetaDataNode[]); } - if (beat.slashed) { - writer.writeGroupItem(`slashed`); - } + score.bars.push({ + nodeType: AlphaTexNodeType.Bar, + metaData: this._handler.buildSyncPointNodes(data), + beats: [] + }); - if (beat.deadSlapped) { - writer.writeGroupItem(`ds`); - } + return score; + } - switch (beat.golpe) { - case GolpeType.Thumb: - writer.writeGroupItem(`glpt`); - break; - case GolpeType.Finger: - writer.writeGroupItem(`glpf`); - break; + private _track(score: AlphaTexScoreNode, data: Track) { + for (const s of data.staves) { + this._staff(score, s); } + } - if (beat.invertBeamDirection) { - writer.writeGroupItem('beam invert'); - } else if (beat.preferredBeamDirection !== null) { - writer.writeGroupItem(`beam ${BeamDirection[beat.preferredBeamDirection!]}`); - } + private _staff(score: AlphaTexScoreNode, data: Staff) { + const voiceCount = Math.max(...data.filledVoices) + 1; - 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 (data.bars.length === 0) { + const bar: AlphaTexBarNode = { + nodeType: AlphaTexNodeType.Bar, + metaData: this._handler.buildBarMetaDataNodes(data, undefined, 0, false), + beats: [], + pipe: undefined + }; + score.bars.push(bar); + } else { + for (let v = 0; v < voiceCount; v++) { + this._voice(score, v, data, voiceCount > 1); } } - - if (beat.showTimer) { - writer.writeGroupItem(`timer`); - } - - writer.endGroup(); } - private _writeNoteTo(writer: AlphaTexWriter, note: Note) { - if (note.index > 0) { - writer.write(' '); + private _voice(score: AlphaTexScoreNode, v: number, data: Staff, isMultiVoice: boolean) { + for (const bar of data.bars) { + this._bar(score, bar, v, isMultiVoice); } - - 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: AlphaTexWriter, note: Note) { - writer.beginGroup('{', '}'); - - if (note.hasBend) { - writer.writeGroupItem(`be ${BendType[note.bendType]}`); + private _bar(score: AlphaTexScoreNode, data: Bar, voiceIndex: number, isMultiVoice: boolean) { + const bar: AlphaTexBarNode = { + nodeType: AlphaTexNodeType.Bar, + metaData: this._handler.buildBarMetaDataNodes(data.staff, data, voiceIndex, isMultiVoice), + beats: [], + pipe: undefined + }; - if (note.bendStyle !== BendStyle.Default) { - writer.writeGroupItem(`${BendStyle[note.bendStyle]} `); - } - - writer.beginGroup('(', ')'); - - for (const p of note.bendPoints!) { - writer.writeGroupItem(`${p.offset} ${p.value}`); + if (!data.isEmpty) { + const voice = data.voices[voiceIndex]; + if (voice.isEmpty) { + bar.trailingComments = [ + { + multiLine: false, + text: `Bar ${data.index + 1} / Voice ${voiceIndex + 1} no contents` + } + ]; + } else { + for (const b of voice.beats) { + bar.beats.push(this._beat(b)); + } + if (bar.beats.length > 0) { + bar.beats[0].leadingComments ??= []; + bar.beats[0].leadingComments.unshift({ + multiLine: false, + text: `Bar ${data.index + 1} / Voice ${voiceIndex + 1} contents` + }); + } } - - 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; + } else { + bar.trailingComments = [ + { + multiLine: false, + text: `Bar ${data.index + 1} / Voice ${voiceIndex + 1} no contents` + } + ]; } - if (!note.isVisible) { - writer.writeGroupItem('hide'); + if (data.index < data.staff.bars.length - 1) { + bar.pipe = { + nodeType: AlphaTexNodeType.Pipe + }; } - if (note.isSlurOrigin) { - const slurId = `s${note.id}`; - writer.writeGroupItem(`slur ${slurId}`); - } + score.bars.push(bar); + } - if (note.isSlurDestination) { - const slurId = `s${note.slurOrigin!.id}`; - writer.writeGroupItem(`slur ${slurId}`); - } + private _beat(data: Beat): AlphaTexBeatNode { + const beat: AlphaTexBeatNode = { + nodeType: AlphaTexNodeType.Beat, + durationChange: undefined, + notes: undefined, + rest: undefined, + beatEffects: undefined + }; - if (note.isTrill) { - writer.writeGroupItem(`tr ${note.trillFret} ${note.trillSpeed as number}`); + if (data.isRest) { + beat.rest = { + nodeType: AlphaTexNodeType.Ident, + text: 'r' + }; + } else { + beat.notes = this._notes(data.notes); + } + + beat.durationDot = { + nodeType: AlphaTexNodeType.Dot + }; + beat.durationValue = { + nodeType: AlphaTexNodeType.Number, + value: data.duration as number + }; + + beat.beatEffects = this._beatEffects(data); + + return beat; + } + + private _beatEffects(data: Beat): AlphaTexPropertiesNode | undefined { + const properties = this._handler.buildBeatEffects(data); + return properties.length > 0 + ? { + nodeType: AlphaTexNodeType.Props, + openBrace: { + nodeType: AlphaTexNodeType.LBrace + }, + properties, + closeBrace: { + nodeType: AlphaTexNodeType.RBrace + } + } + : undefined; + } + + private _notes(data: Note[]): AlphaTexNoteListNode { + const notes: AlphaTexNoteListNode = { + nodeType: AlphaTexNodeType.NoteList, + openParenthesis: undefined, + notes: [], + closeParenthesis: undefined + }; + + if (data.length === 0 || data.length > 1) { + notes.openParenthesis = { + nodeType: AlphaTexNodeType.LParen + }; + notes.closeParenthesis = { + nodeType: AlphaTexNodeType.RParen + }; + } + + for (const n of data) { + notes.notes.push(this._note(n)); + } + + return notes; + } + + private _note(data: Note): AlphaTexNoteNode { + const note: AlphaTexNoteNode = { + nodeType: AlphaTexNodeType.Note, + noteValue: { + // placeholder value + nodeType: AlphaTexNodeType.Ident, + text: '' + } as AlphaTexIdentifier + }; + + if (data.isPercussion) { + note.noteValue = { + nodeType: AlphaTexNodeType.String, + text: PercussionMapper.getArticulationName(data) + } as AlphaTexStringLiteral; + } else if (data.isPiano) { + note.noteValue = { + nodeType: AlphaTexNodeType.Ident, + text: Tuning.getTextForTuning(data.realValueWithoutHarmonic, true) + } as AlphaTexIdentifier; + } else if (data.isStringed) { + note.noteValue = { + nodeType: AlphaTexNodeType.Number, + value: data.fret + } as AlphaTexNumberLiteral; + note.noteStringDot = { + nodeType: AlphaTexNodeType.Dot + }; + const stringNumber = data.beat.voice.bar.staff.tuning.length - data.string + 1; + note.noteString = { + nodeType: AlphaTexNodeType.Number, + value: stringNumber + }; + } else { + throw new Error('What kind of note'); } - if (note.accidentalMode !== NoteAccidentalMode.Default) { - writer.writeGroupItem(`acc ${NoteAccidentalMode[note.accidentalMode]}`); - } + note.noteEffects = this._noteEffects(data); - 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; - } + return note; + } - writer.endGroup(); + private _noteEffects(data: Note): AlphaTexPropertiesNode | undefined { + const properties = this._handler.buildNoteEffects(data); + return properties.length > 0 + ? { + nodeType: AlphaTexNodeType.Props, + openBrace: { + nodeType: AlphaTexNodeType.LBrace + }, + properties, + closeBrace: { + nodeType: AlphaTexNodeType.RBrace + } + } + : undefined; } } diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index bd628c388..2f4b11875 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -1219,7 +1219,7 @@ export class GpifWriter { syncPointAutomation.addElement('Linear').innerText = 'false'; syncPointAutomation.addElement('Bar').innerText = mb.index.toString(); syncPointAutomation.addElement('Position').innerText = syncPoint.ratioPosition.toString(); - syncPointAutomation.addElement('Visible').innerText = 'true'; + syncPointAutomation.addElement('Visible').innerText = syncPoint.isVisible ? 'true' : 'false'; const value = syncPointAutomation.addElement('Value'); value.addElement('BarIndex').innerText = mb.index.toString(); diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index d7827274b..618409d62 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -1,3736 +1,1076 @@ -import { GeneralMidi } from '@src/midi/GeneralMidi'; +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { BeatCloner } from '@src/generated/model/BeatCloner'; +import { AlphaTex1LanguageHandler } from '@src/importer/alphaTex/AlphaTex1LanguageHandler'; +import { + type AlphaTexAstNode, + type AlphaTexBarNode, + type AlphaTexBeatDurationChangeNode, + type AlphaTexBeatNode, + type AlphaTexMetaDataNode, + AlphaTexNodeType, + type AlphaTexNoteNode, + type AlphaTexNumberLiteral, + type AlphaTexPropertiesNode, + type AlphaTexScoreNode, + type AlphaTexTextNode +} from '@src/importer/alphaTex/AlphaTexAst'; +import { AlphaTexParser } from '@src/importer/alphaTex/AlphaTexParser'; +import { + AlphaTexAccidentalMode, + type AlphaTexDiagnostic, + AlphaTexDiagnosticBag, + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity, + type IAlphaTexImporter, + type IAlphaTexImporterState, + StaffNoteKind +} from '@src/importer/alphaTex/AlphaTexShared'; +import { + ApplyNodeResult, + ApplyStructuralMetaDataResult, + type IAlphaTexLanguageImportHandler +} from '@src/importer/alphaTex/IAlphaTexLanguageImportHandler'; import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; -import { AccentuationType } from '@src/model/AccentuationType'; -import { Automation, AutomationType, type FlatSyncPoint } from '@src/model/Automation'; -import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; -import { Beat, BeatBeamingMode } from '@src/model/Beat'; -import { BendPoint } from '@src/model/BendPoint'; -import { BrushType } from '@src/model/BrushType'; -import { Chord } from '@src/model/Chord'; +import { ByteBuffer } from '@src/io/ByteBuffer'; +import { IOHelper } from '@src/io/IOHelper'; +import { Logger } from '@src/Logger'; +import type { FlatSyncPoint } from '@src/model/Automation'; +import { Bar, type SustainPedalMarker } from '@src/model/Bar'; +import { Beat } from '@src/model/Beat'; import { Clef } from '@src/model/Clef'; -import { CrescendoType } from '@src/model/CrescendoType'; import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; -import { Fingers } from '@src/model/Fingers'; -import { GraceType } from '@src/model/GraceType'; -import { HarmonicType } from '@src/model/HarmonicType'; -import { KeySignature } from '@src/model/KeySignature'; -import { Lyrics } from '@src/model/Lyrics'; +import type { Lyrics } from '@src/model/Lyrics'; import { MasterBar } from '@src/model/MasterBar'; +import { ModelUtils } from '@src/model/ModelUtils'; import { Note } from '@src/model/Note'; -import { PickStroke } from '@src/model/PickStroke'; -import { Score, ScoreSubElement } from '@src/model/Score'; -import { Section } from '@src/model/Section'; -import { SlideInType } from '@src/model/SlideInType'; -import { SlideOutType } from '@src/model/SlideOutType'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { PercussionMapper } from '@src/model/PercussionMapper'; +import { Score } from '@src/model/Score'; import type { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; -import { TripletFeel } from '@src/model/TripletFeel'; import { Tuning } from '@src/model/Tuning'; -import { VibratoType } from '@src/model/VibratoType'; import { Voice } from '@src/model/Voice'; -import { Logger } from '@src/Logger'; -import { ModelUtils, type TuningParseResult } from '@src/model/ModelUtils'; -import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; -import { BeatCloner } from '@src/generated/model/BeatCloner'; -import { IOHelper } from '@src/io/IOHelper'; import type { Settings } from '@src/Settings'; -import { ByteBuffer } from '@src/io/ByteBuffer'; -import { PercussionMapper } from '@src/model/PercussionMapper'; -import { KeySignatureType } from '@src/model/KeySignatureType'; -import { GolpeType } from '@src/model/GolpeType'; -import { FadeType } from '@src/model/FadeType'; -import { WahPedal } from '@src/model/WahPedal'; -import { BarreShape } from '@src/model/BarreShape'; -import { NoteOrnament } from '@src/model/NoteOrnament'; -import { Rasgueado } from '@src/model/Rasgueado'; -import { SynthConstants } from '@src/synth/SynthConstants'; -import { Direction } from '@src/model/Direction'; -import { Fermata, FermataType } from '@src/model/Fermata'; -import { Ottavia } from '@src/model/Ottavia'; -import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; -import { BendType } from '@src/model/BendType'; -import { SimileMark } from '@src/model/SimileMark'; -import { WhammyType } from '@src/model/WhammyType'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; -import { Color } from '@src/model/Color'; -import { BendStyle } from '@src/model/BendStyle'; -import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { TextAlign } from '@src/platform/ICanvas'; +import { Lazy } from '@src/util/Lazy'; /** - * 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 -} +export class AlphaTexErrorWithDiagnostics extends AlphaTabError { + public lexerDiagnostics?: AlphaTexDiagnosticBag; + public parserDiagnostics?: AlphaTexDiagnosticBag; + public semanticDiagnostics?: AlphaTexDiagnosticBag; -/** - * @public - */ -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 *iterateDiagnostics() { + if (this.lexerDiagnostics) { + for (const d of this.lexerDiagnostics.items) { + yield d; + } + } + if (this.parserDiagnostics) { + for (const d of this.parserDiagnostics.items) { + yield d; + } + } + if (this.semanticDiagnostics) { + for (const d of this.semanticDiagnostics.items) { + yield d; + } + } + } public constructor( - message: string | null, - position: number, - line: number, - col: number, - nonTerm: string | null, - expected: AlphaTexSymbols | null, - symbol: AlphaTexSymbols | null, - symbolData: unknown = null + message: string, + lexerDiagnostics?: AlphaTexDiagnosticBag, + parserDiagnostics?: AlphaTexDiagnosticBag, + semanticDiagnostics?: AlphaTexDiagnosticBag ) { 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); + this.lexerDiagnostics = lexerDiagnostics; + this.parserDiagnostics = parserDiagnostics; + this.semanticDiagnostics = semanticDiagnostics; } - 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}'`; + public override toString(): string { + return [ + this.message!, + 'lexer diagnostics:', + AlphaTexErrorWithDiagnostics._diagnosticsToString(this.lexerDiagnostics, ' '), + 'parser diagnostics:', + AlphaTexErrorWithDiagnostics._diagnosticsToString(this.parserDiagnostics, ' '), + 'semantic diagnostics:', + AlphaTexErrorWithDiagnostics._diagnosticsToString(this.semanticDiagnostics, ' ') + ].join('\n'); + } + + private static _diagnosticsToString(semanticDiagnostics: AlphaTexDiagnosticBag | undefined, indent: string) { + if (!semanticDiagnostics) { + return `${indent}none`; } - return new AlphaTexError(message, position, line, col, nonTerm, expected, symbol, symbolData); + + return semanticDiagnostics.items + .map( + d => + `${indent}${AlphaTexDiagnosticsSeverity[d.severity]} AT${(d.code as number).toString().padStart(3, '0')}${AlphaTexErrorWithDiagnostics._locationToString(d)}: ${d.message}` + ) + .join('\n'); } - 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); + private static _locationToString(d: AlphaTexDiagnostic): string { + let s = ''; + if (d.start) { + s += `(${d.start.line},${d.start.col})`; + } + if (d.end) { + if (s.length > 0) { + s += '->'; + } + s += `(${d.end.line},${d.end.col})`; + } + return s; } } /** * @internal */ -enum AlphaTexAccidentalMode { - Auto = 0, - Explicit = 1 +class AlphaTexImportState implements IAlphaTexImporterState { + public trackChannel: number = 0; + public score!: Score; + public currentTrack?: Track; + public currentStaff?: Staff; + public barIndex: number = 0; + public voiceIndex: number = 0; + public ignoredInitialVoice = false; + public ignoredInitialStaff = false; + public ignoredInitialTrack = false; + public currentDuration = Duration.Quarter; + public articulationValueToIndex = new Map(); + + public hasAnyProperData = false; + + public readonly percussionArticulationNames = new Map(); + + public readonly slurs = new Map(); + public readonly lyrics = new Map(); + public readonly sustainPedalToBeat = new Map(); + public readonly staffTuningApplied = new Set(); + public readonly staffNoteKind = new Map(); + public readonly staffHasExplicitTuning = new Set(); + public readonly staffHasExplicitDisplayTransposition = new Set(); + public readonly staffDisplayTranspositionApplied = new Set(); + public readonly staffInitialClef = new Map(); + public readonly syncPoints: FlatSyncPoint[] = []; + + public currentDynamics = DynamicValue.F; + public accidentalMode = AlphaTexAccidentalMode.Explicit; + public currentTupletNumerator = -1; + public currentTupletDenominator = -1; } /** - * @internal + * @public */ -export class AlphaTexLexer { - private static readonly _eof: number = 0; +export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter { + private _parser!: AlphaTexParser; + private _handler: IAlphaTexLanguageImportHandler = AlphaTex1LanguageHandler.instance; - private _position: number = 0; - private _line: number = 1; - private _col: number = 0; + private _state = new AlphaTexImportState(); - private _codepoints: number[]; - private _codepoint: number = AlphaTexLexer._eof; - - public sy: AlphaTexSymbols = AlphaTexSymbols.No; - public syData: unknown = ''; + public get state(): IAlphaTexImporterState { + return this._state; + } - public lastValidSpot: number[] = [0, 1, 0]; + public get name(): string { + return 'AlphaTex'; + } - public allowTuning: boolean = false; - public logErrors: boolean = true; + public get lexerDiagnostics() { + return this._parser.lexerDiagnostics; + } - public constructor(input: string) { - this._codepoints = [...IOHelper.iterateCodepoints(input)]; + public get parserDiagnostics() { + return this._parser.parserDiagnostics; } - public init(allowFloats: boolean = false) { - this._position = 0; - this._line = 1; - this._col = 0; - this._saveValidSpot(); + public logErrors: boolean = false; - this._codepoint = this._nextCodepoint(); - this.sy = this.newSy(allowFloats); + public readonly semanticDiagnostics = new AlphaTexDiagnosticBag(); + + public addSemanticDiagnostic(diagnostic: AlphaTexDiagnostic) { + this.semanticDiagnostics.push(diagnostic); } - /** - * 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]; + public initFromString(tex: string, settings: Settings) { + this.data = ByteBuffer.empty(); + this._parser = new AlphaTexParser(tex); + this.settings = settings; + // when beginning reading a new score we reset the IDs. + Score.resetIds(); } - /** - * 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++; + public readScore(): Score { + this._state = new AlphaTexImportState(); + this._createDefaultScore(); + + if (this.data.length > 0) { + this._parser = new AlphaTexParser(IOHelper.toString(this.data.readAll(), this.settings.importer.encoding)); + } + + let scoreNode: AlphaTexScoreNode; + try { + scoreNode = this._parser.read(); + } catch (e) { + if (this.logErrors) { + Logger.error('AlphaTex', `Error while parsing alphaTex: ${(e as Error).toString()}`); } - } else { - this._codepoint = AlphaTexLexer._eof; + throw new UnsupportedFormatError('Error parsing alphaTex, check inner error for details', e as Error); } - 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 === AlphaTexLexer._eof) { - this.sy = AlphaTexSymbols.Eof; - } else if (AlphaTexLexer._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 !== AlphaTexLexer._eof - ) { - this._codepoint = this._nextCodepoint(); - } - } else if (this._codepoint === 0x2a /* * */) { - // multiline comment - while (this._codepoint !== AlphaTexLexer._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 !== AlphaTexLexer._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 === AlphaTexLexer._eof) { - this._errorMessage('Unexpected end of escape sequence'); - } - hex += String.fromCodePoint(this._codepoint); - } + if (this._parser.parserDiagnostics.hasErrors || this._parser.lexer.lexerDiagnostics.hasErrors) { + const error = new AlphaTexErrorWithDiagnostics( + 'There are errors in the parsed alphaTex, check the diagnostics for details', + this.lexerDiagnostics, + this.parserDiagnostics, + this.semanticDiagnostics + ); - 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; - } + if (this.logErrors) { + Logger.error('AlphaTex', `Error while parsing alphaTex: ${error.toString()}`); + } + + throw new UnsupportedFormatError( + 'Error parsing alphaTex, check diagnostics on inner error for details', + error + ); + } - // unicode handling + // even start translating when we have parser errors + // as long we have some nodes, we can already start semantically + // validating and using them - // 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 (scoreNode.bars.length === 0) { + throw new UnsupportedFormatError('No alphaTex data found'); + } - if (codepoint > 0) { - s += String.fromCodePoint(codepoint); - } - } + this._bars(scoreNode); - previousCodepoint = codepoint; - this._codepoint = this._nextCodepoint(); - } - if (this._codepoint === AlphaTexLexer._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(); + if (this.semanticDiagnostics.hasErrors) { + if (this._state.hasAnyProperData) { + const error = new AlphaTexErrorWithDiagnostics( + 'There are errors in the parsed alphaTex, check the diagnostics for details', + this.lexerDiagnostics, + this.parserDiagnostics, + this.semanticDiagnostics + ); + if (this.logErrors) { + Logger.error('AlphaTex', `Error while parsing alphaTex: ${error.toString()}`); } - 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 (AlphaTexLexer._isDigit(this._codepoint)) { - this._readNumberOrName(allowFloats); - } else if (AlphaTexLexer._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; - } + throw error; } else { - this._errorMessage(`Unexpected character ${String.fromCodePoint(this._codepoint)}`); + throw new UnsupportedFormatError('No alphaTex data found'); } } - 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!); + ModelUtils.consolidate(this._state.score); + this._state.score.finish(this.settings); + ModelUtils.trimEmptyBarsAtEnd(this._state.score); + this._state.score.rebuildRepeatGroups(); + this._state.score.applyFlatSyncPoints(this._state.syncPoints); + for (const [track, lyrics] of this._state.lyrics) { + this._state.score.tracks[track].applyLyrics(lyrics); } - 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 (!AlphaTexLexer._isDigit(this._codepoint)) { - this.sy = AlphaTexSymbols.String; + for (const [sustainPedal, beat] of this._state.sustainPedalToBeat) { + if (sustainPedal.ratioPosition < 1) { + const duration = beat.voice.bar.masterBar.calculateDuration(); + sustainPedal.ratioPosition = beat.playbackStart / duration; } } + return this._state.score; + } - let keepReading = true; - - let hasDot = false; - do { - switch (this.sy) { - case AlphaTexSymbols.Number: - // adding digits to the number - if (AlphaTexLexer._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 /* . */ && - AlphaTexLexer._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 (AlphaTexLexer._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 (AlphaTexLexer._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); + private _createDefaultScore(): void { + this._state.score = new Score(); + this._newTrack(); + } - if (str.length === 0) { - this._errorMessage('number was empty'); - } + private _newTrack(): void { + this._state.currentTrack = new Track(); + this._state.currentTrack.ensureStaveCount(1); + this._state.currentTrack.playbackInfo.program = 25; + this._state.currentTrack.playbackInfo.primaryChannel = this._state.trackChannel++; + this._state.currentTrack.playbackInfo.secondaryChannel = this._state.trackChannel++; + const staff = this._state.currentTrack.staves[0]; + staff.displayTranspositionPitch = 0; + staff.stringTuning = Tuning.getDefaultTuningFor(6)!; + this._state.articulationValueToIndex.clear(); - if (this.sy === AlphaTexSymbols.String) { - this.syData = str; - } else { - this.syData = allowFloat ? Number.parseFloat(str) : Number.parseInt(str, 10); - } - return; - } + this._beginStaff(staff); - /** - * 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 ( - AlphaTexLexer._isNameLetter(this._codepoint) || - AlphaTexLexer._isDigit(this._codepoint) || - this._codepoint === 0x2d /*-*/ - ); - return str; + this._state.score.addTrack(this._state.currentTrack); + this._state.lyrics.set(this._state.currentTrack.index, []); + this._state.currentDynamics = DynamicValue.F; + this._state.currentTupletDenominator = -1; + this._state.currentTupletNumerator = -1; } - /** - * 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 ( - !AlphaTexLexer._isTerminal(ch) && // no control characters, whitespaces, numbers or dots - ((0x21 <= ch && ch <= 0x2f) || (0x3a <= ch && ch <= 0x7e) || 0x80 <= ch) // Unicode Symbols - ); - } + private _beginStaff(staff: Staff) { + // ensure previous staff is properly initialized + if (this._state.currentStaff) { + this._detectTuningForStaff(this._state.currentStaff!); + this._handleTransposition(this._state.currentStaff!); + } - 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 /* \ */ - ); + this._state.currentStaff = staff; + this._state.slurs.clear(); + this._state.barIndex = 0; + this._state.voiceIndex = 0; } - private static _isWhiteSpace(ch: number): boolean { - return ( - ch === 0x09 /* \t */ || - ch === 0x0a /* \n */ || - ch === 0x0b /* \v */ || - ch === 0x0d /* \r */ || - ch === 0x20 /* space */ - ); + private _bars(node: AlphaTexScoreNode) { + if (node.bars.length > 0) { + for (const b of node.bars) { + this._bar(b); + } + } else { + this._newBar(this._state.currentStaff!); + this._detectTuningForStaff(this._state.currentStaff!); + this._handleTransposition(this._state.currentStaff!); + } } - private static _isDigit(ch: number): boolean { - return ch >= 0x30 && ch <= 0x39 /* 0-9 */; - } -} + private _bar(node: AlphaTexBarNode) { + const bar = this._barMeta(node); -/** - * This importer can parse alphaTex markup into a score structure. - * @internal - */ -export class AlphaTexImporter extends ScoreImporter { - private _trackChannel: number = 0; - private _score!: Score; - private _currentTrack!: Track; + this._detectTuningForStaff(this._state.currentStaff!); + this._handleTransposition(this._state.currentStaff!); - private _currentStaff!: Staff; - private _barIndex: number = 0; - private _voiceIndex: number = 0; - private _initialTempo = Automation.buildTempoAutomation(false, 0, 120, 0); + if (bar.index === 0 && this._state.staffInitialClef.has(this._state.currentStaff!)) { + bar.clef = this._state.staffInitialClef.get(this._state.currentStaff!)!; + } - // 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; + const voice: Voice = bar.voices[this._state.voiceIndex]; - private _staffHasExplicitDisplayTransposition: boolean = false; - private _staffDisplayTranspositionApplied: boolean = false; - private _staffHasExplicitTuning: boolean = false; - private _staffTuningApplied: boolean = false; - private _percussionArticulationNames = new Map(); - private _sustainPedalToBeat = new Map(); + for (const b of node.beats) { + this._beat(voice, b); + } - private _slurs: Map = new Map(); + if (voice.beats.length === 0) { + const emptyBeat = new Beat(); + emptyBeat.isEmpty = true; + voice.addBeat(emptyBeat); + } + } - private _articulationValueToIndex = new Map(); + private _beat(voice: Voice, node: AlphaTexBeatNode) { + if (node.durationChange) { + this._beatDuration(node.durationChange); + } - private _lexer!: AlphaTexLexer; + if (!node.notes && !node.rest) { + return; + } - private _accidentalMode: AlphaTexAccidentalMode = AlphaTexAccidentalMode.Explicit; - private _flatSyncPoints: FlatSyncPoint[] = []; + const beat = new Beat(); + voice.addBeat(beat); - public logErrors: boolean = true; + if (node.notes) { + for (const n of node.notes!.notes) { + this._note(beat, n); + } + } else if (node.rest) { + // beat is already a rest at start + } - public get name(): string { - return 'AlphaTex'; - } + if (node.durationValue) { + this._state.currentDuration = this._parseDuration(node.durationValue!); + } - public initFromString(tex: string, settings: Settings) { - this.data = ByteBuffer.empty(); - this._lexer = new AlphaTexLexer(tex); - this.settings = settings; - // when beginning reading a new score we reset the IDs. - Score.resetIds(); - } + beat.duration = this._state.currentDuration; + beat.dynamics = this._state.currentDynamics; + if (this._state.currentTupletNumerator !== -1 && !beat.hasTuplet) { + beat.tupletNumerator = this._state.currentTupletNumerator; + beat.tupletDenominator = this._state.currentTupletDenominator; + } - private get _sy() { - return this._lexer.sy; - } + // beat multiplier (repeat beat n times) + let beatRepeat: number = 1; + if (node.beatMultiplierValue !== undefined) { + beatRepeat = node.beatMultiplierValue.value; + } - private get _syData() { - return this._lexer.syData; - } + if (node.beatEffects) { + this._beatEffects(beat, node.beatEffects!); + } - private set _sy(value: AlphaTexSymbols) { - this._lexer.sy = value; + for (let i: number = 0; i < beatRepeat - 1; i++) { + voice.addBeat(BeatCloner.clone(beat)); + } } - private _newSy(allowFloat: boolean = false) { - return this._lexer.newSy(allowFloat); - } + private _beatEffects(beat: Beat, node: AlphaTexPropertiesNode) { + for (const p of node.properties) { + const result = this._handler.applyBeatProperty(this, beat, p); + switch (result) { + case ApplyNodeResult.Applied: + case ApplyNodeResult.NotAppliedSemanticError: + this._state.hasAnyProperData = true; + break; - public readScore(): Score { - try { - if (this.data.length > 0) { - this._lexer = new AlphaTexLexer( - IOHelper.toString(this.data.readAll(), this.settings.importer.encoding) - ); + case ApplyNodeResult.NotAppliedUnrecognizedMarker: + const knownProps = Array.from(this._handler.knownBeatProperties).join(','); + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT212, + message: `Unrecogized property '${p.property.text}', expected one of ${knownProps}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.start, + end: p.end + }); + break; } - this._lexer.logErrors = this.logErrors; - - this._lexer.allowTuning = true; - this._lyrics = new Map(); - this._sustainPedalToBeat = new Map(); + } + } - this._lexer.init(); + private _beatDuration(node: AlphaTexBeatDurationChangeNode) { + if (node.value) { + this._state.currentDuration = this._parseDuration(node.value!); + } - 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?)"); - } + this._state.currentTupletNumerator = -1; + this._state.currentTupletDenominator = -1; - if (this._sy !== AlphaTexSymbols.Eof) { - const anyMetaRead = this._metaData(); - const anyBarsRead = this._bars(); - if (!anyMetaRead && !anyBarsRead) { - throw new UnsupportedFormatError('No alphaTex data found'); - } + if (node.properties) { + for (const p of node.properties.properties) { + const result = this._handler.applyBeatDurationProperty(this, p); + switch (result) { + case ApplyNodeResult.Applied: + case ApplyNodeResult.NotAppliedSemanticError: + this._state.hasAnyProperData = true; + break; - if (this._sy === AlphaTexSymbols.Dot) { - this._sy = this._newSy(); - this._syncPoints(); + case ApplyNodeResult.NotAppliedUnrecognizedMarker: + const knownProps = Array.from(this._handler.knownBeatDurationProperties).join(','); + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT212, + message: `Unrecogized property '${p.property.text}', expected one of ${knownProps}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.start, + end: p.end + }); + break; } } - - 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 _parseDuration(duration: AlphaTexNumberLiteral): Duration { + switch (duration.value) { + 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: + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected duration value '${duration.value}', expected: -4, -2, 1, 2, 4, 8, 16, 32, 64, 128 or 256`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: duration.start, + end: duration.end + }); + return this._state.currentDuration; } } - 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); - } + private _note(beat: Beat, node: AlphaTexNoteNode) { + // + // Note value + let isDead: boolean = false; + let isTie: boolean = false; + let numericValue: number = -1; + let octave: number = -1; + let tone: number = -1; + let accidentalMode = NoteAccidentalMode.Default; + const noteValue = node.noteValue as AlphaTexAstNode; + let detectedNoteKind: StaffNoteKind | undefined = undefined; + let staffNoteKind = this._state.staffNoteKind.has(this._state.currentStaff!) + ? this._state.staffNoteKind.get(this._state.currentStaff!)! + : undefined; + + switch (noteValue.nodeType) { + case AlphaTexNodeType.Number: + numericValue = (noteValue as AlphaTexNumberLiteral).value; + if (node.noteString !== undefined) { + detectedNoteKind = StaffNoteKind.Fretted; + } else { + detectedNoteKind = StaffNoteKind.Articulation; + } + break; + case AlphaTexNodeType.String: + case AlphaTexNodeType.Ident: + const str = (noteValue as AlphaTexTextNode).text; + + isDead = str === 'x'; + isTie = str === '-'; + if (isTie || isDead) { + numericValue = 0; + detectedNoteKind = undefined; // don't know on those notes + } else { + const tuning = ModelUtils.parseTuning(str); + if (tuning) { + detectedNoteKind = StaffNoteKind.Pitched; + octave = tuning.octave; + tone = tuning.tone.noteValue; + if (this._state.accidentalMode === AlphaTexAccidentalMode.Explicit) { + accidentalMode = tuning.tone.accidentalMode; + } + } else { + detectedNoteKind = StaffNoteKind.Articulation; + const articulationName = str.toLowerCase(); + // apply defaults + const percussionArticulationNames = this._state.percussionArticulationNames; + if (staffNoteKind === undefined && percussionArticulationNames.size === 0) { + for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) { + percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue); + percussionArticulationNames.set(ModelUtils.toArticulationId(defaultName), defaultValue); + } + } - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarIndex', AlphaTexSymbols.Number, true); + if (percussionArticulationNames.has(articulationName)) { + numericValue = percussionArticulationNames.get(articulationName)!; + } else { + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected percussion articulation value '${articulationName}', expected: oneOf(${Array.from(this._state.percussionArticulationNames.keys()).join(',')}).`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: noteValue.start, + end: noteValue.end + }); + // avoid double error + numericValue = Array.from(PercussionMapper.instrumentArticulationNames.values())[0]; + return; + } + } + } + break; } - 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; + if (detectedNoteKind !== undefined) { + if (staffNoteKind === undefined) { + staffNoteKind = detectedNoteKind; + this.applyStaffNoteKind(this._state.currentStaff!, staffNoteKind); + } else if (staffNoteKind !== detectedNoteKind) { + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT218, + message: `Wrong note kind '${StaffNoteKind[detectedNoteKind]}' for staff with note kind ''${StaffNoteKind[staffNoteKind]}'. Do not mix incompatible staves and notes.`, + severity: AlphaTexDiagnosticsSeverity.Error + }); + } + } else if (staffNoteKind !== undefined) { + detectedNoteKind = staffNoteKind; + } + + // + // Construct Note + const note = new Note(); + note.isDead = isDead; + if (isDead || isTie) { + note.fret = numericValue; + } + note.isTieDestination = isTie; + + // valid note kind detected, apply values, tied/dead notes at start might be rare, but can happen. + if (detectedNoteKind !== undefined && detectedNoteKind === staffNoteKind) { + switch (detectedNoteKind) { + case StaffNoteKind.Pitched: + note.octave = octave; + note.tone = tone; + note.accidentalMode = accidentalMode; + break; + case StaffNoteKind.Fretted: + // Fret [Dot] String + if (!node.noteString) { + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT207, + message: `Missing string for fretted note.`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: noteValue.end, + end: noteValue.end + }); + return; + } - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('syncPointBarMillis', AlphaTexSymbols.Number, true); - } - const millisecondOffset = this._syData as number; + const noteString: number = node.noteString!.value; + if (noteString < 1 || noteString > this._state.currentStaff!.tuning.length) { + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT208, + message: `Note string is out of range. Available range: 1-${this._state.currentStaff!.tuning.length}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: noteValue.end, + end: noteValue.end + }); + return; + } - this._sy = this._newSy(true); - let barPosition = 0; - if (this._sy === AlphaTexSymbols.Number) { - barPosition = this._syData as number; - this._sy = this._newSy(); - } + note.string = this._state.currentStaff!.tuning.length - (noteString - 1); + if (!isTie) { + note.fret = numericValue; + } - this._flatSyncPoints.push({ - barIndex, - barOccurence, - barPosition, - millisecondOffset - }); - } + break; + case StaffNoteKind.Articulation: + let articulationIndex: number = 0; + if (this._state.articulationValueToIndex.has(numericValue)) { + articulationIndex = this._state.articulationValueToIndex.get(numericValue)!; + } else { + articulationIndex = this._state.currentTrack!.percussionArticulations.length; + const articulation = PercussionMapper.getArticulationByInputMidiNumber(numericValue); + if (articulation === null) { + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected articulation value '${numericValue}', expected: oneOf(${Array.from(PercussionMapper.instrumentArticulations.keys()).join(',')}).`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: noteValue.end, + end: noteValue.end + }); + return; + } - 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; + this._state.currentTrack!.percussionArticulations.push(articulation!); + this._state.articulationValueToIndex.set(numericValue, articulationIndex); + } + note.percussionArticulation = articulationIndex; + break; } - } 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!); + beat.addNote(note); + this._state.hasAnyProperData = true; + + // + // Note Effects + if (node.noteEffects) { + this._noteEffects(note, node.noteEffects!); } - throw e; } /** - * Initializes the song with some required default values. - * @returns + * @internal */ - private _createDefaultScore(): void { - this._score = new Score(); - this._newTrack(); + public getStaffNoteKind(staff: Staff): StaffNoteKind | undefined { + return this._state.staffNoteKind.has(staff) ? this._state.staffNoteKind.get(staff) : undefined; } - 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.tunings = Tuning.getDefaultTuningFor(6)!.tunings; - 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; - 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( - AlphaTexImporter._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.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.tunings = []; - 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.tunings = []; - - 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) { - AlphaTexImporter._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 { - AlphaTexImporter._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); + public applyStaffNoteKind(staff: Staff, staffNoteKind: StaffNoteKind) { + this._state.staffNoteKind.set(staff, staffNoteKind); + switch (staffNoteKind) { + case StaffNoteKind.Pitched: + staff.isPercussion = false; + staff.stringTuning.reset(); + if (!this._state.staffHasExplicitDisplayTransposition.has(staff)) { + staff.displayTranspositionPitch = 0; } - - 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); + break; + case StaffNoteKind.Fretted: + staff.isPercussion = false; + this._detectTuningForStaff(staff); + this._handleTransposition(staff); + break; + case StaffNoteKind.Articulation: + staff.isPercussion = true; + staff.stringTuning.reset(); + if (!this._state.staffHasExplicitDisplayTransposition.has(staff)) { + staff.displayTranspositionPitch = 0; } + break; + } + } - const slurId = this._syData as string; - if (this._slurs.has(slurId)) { - const slurOrigin = this._slurs.get(slurId)!; - slurOrigin.slurDestination = note; + private _noteEffects(note: Note, node: AlphaTexPropertiesNode) { + for (const p of node.properties) { + let result = this._handler.applyNoteProperty(this, note, p); + if (result === ApplyNodeResult.NotAppliedUnrecognizedMarker) { + result = this._handler.applyBeatProperty(this, note.beat, p); + } - note.slurOrigin = slurOrigin; - note.isSlurDestination = true; - } else { - this._slurs.set(slurId, note); - } + switch (result) { + case ApplyNodeResult.Applied: + case ApplyNodeResult.NotAppliedSemanticError: + break; - this._sy = this._newSy(); - } else if (this._applyBeatEffect(note.beat)) { - // Success - } else { - this._error(syData, AlphaTexSymbols.String, false); + case ApplyNodeResult.NotAppliedUnrecognizedMarker: + const knownProps = Array.from(this._handler.knownNoteProperties) + .concat(Array.from(this._handler.knownBeatProperties)) + .join(','); + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT212, + message: `Unrecogized property '${p.property.text}', expected one of ${knownProps}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.start, + end: p.end + }); + break; } } - 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); + private _handleTransposition(staff: Staff) { + if ( + !this._state.staffDisplayTranspositionApplied.has(staff) && + !this._state.staffHasExplicitDisplayTransposition.has(staff) + ) { + const program = staff.track.playbackInfo.program; + if (ModelUtils.displayTranspositionPitches.has(program)) { + // guitar E4 B3 G3 D3 A2 E2 + staff.displayTranspositionPitch = ModelUtils.displayTranspositionPitches.get(program)!; + } else { + this._state.currentStaff!.displayTranspositionPitch = 0; + } + this._state.staffDisplayTranspositionApplied.add(staff); } - 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 _detectTuningForStaff(staff: Staff) { + // detect tuning for staff + const program = staff.track.playbackInfo.program; + if (!this._state.staffTuningApplied.has(staff) && !this._state.staffHasExplicitTuning.has(staff)) { + // reset to defaults + staff.stringTuning.reset(); - 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; - } - } + if (program === 15) { + // dulcimer E4 B3 G3 D3 A2 E2 + staff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; + } else if (program >= 24 && program <= 31) { + // guitar E4 B3 G3 D3 A2 E2 + staff.stringTuning.tunings = Tuning.getDefaultTuningFor(6)!.tunings; + } else if (program >= 32 && program <= 39) { + // bass G2 D2 A1 E1 + staff.stringTuning.tunings = [43, 38, 33, 28]; + this._state.staffInitialClef.set(staff, Clef.F4); + } else if ( + program === 40 || + program === 44 || + program === 45 || + program === 48 || + program === 49 || + program === 50 || + program === 51 + ) { + // violin E3 A3 D3 G2 + staff.stringTuning.tunings = [52, 57, 50, 43]; + } else if (program === 41) { + // viola A3 D3 G2 C2 + staff.stringTuning.tunings = [57, 50, 43, 36]; + } else if (program === 42) { + // cello A2 D2 G1 C1 + staff.stringTuning.tunings = [45, 38, 31, 24]; + } else if (program === 43) { + // contrabass + // G2 D2 A1 E1 + staff.stringTuning.tunings = [43, 38, 33, 28]; + } else if (program === 105) { + // banjo + // D3 B2 G2 D2 G3 + staff.stringTuning.tunings = [50, 47, 43, 38, 55]; + } else if (program === 106) { + // shamisen + // A3 E3 A2 + staff.stringTuning.tunings = [57, 52, 45]; + } else if (program === 107) { + // koto + // E3 A2 D2 G1 + staff.stringTuning.tunings = [52, 45, 38, 31]; + } else if (program === 110) { + // Fiddle + // E4 A3 D3 G2 + staff.stringTuning.tunings = [64, 57, 50, 43]; + } else { + // any non-guitar instrument -> use guitar 6 string tuning + if ( + this._state.staffNoteKind.has(staff) && + this._state.staffNoteKind.get(staff)! === StaffNoteKind.Fretted + ) { + staff.stringTuning = Tuning.getDefaultTuningFor(6)!; + } + } - private _parseBendStyle(str: string): BendStyle { - switch (str.toLowerCase()) { - case 'gradual': - return BendStyle.Gradual; - case 'fast': - return BendStyle.Fast; - default: - return BendStyle.Default; + this._state.staffTuningApplied.add(staff); } } - 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(node: AlphaTexBarNode): Bar { + // it might be a bit an edge case but a valid one: + // one might repeat multiple structural metadata + // in one bar starting multiple tracks/staves/voices which are + // empty. + // for this reason we first remember the bar metadata + // and do not create bars directly + // the tricky thing is: \track, \staff, \voice might not create + // new items but reuse the initial ones. + // here we need to detect such scenarios and ensure we apply + // any preceeding metadata to empty bars - 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); - } + let initialBarMeta: AlphaTexMetaDataNode[] | undefined = + this._state.score.masterBars.length > 0 ? undefined : []; - 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); - } + let previousStaff = this._state.currentStaff!; + let hadNewTrack = false; + let hadNewStaff = false; + let applyInitialBarMetaToPreviousStaff = false; - 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); - } + const resetInitialBarMeta = () => { + // reset state + if (!initialBarMeta) { + return; + } - bar.clefOttava = this._parseClefOttavaFromString(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'simile') { - this._sy = this._newSy(); + initialBarMeta = undefined; + previousStaff = this._state.currentStaff!; + hadNewTrack = false; + hadNewStaff = false; + applyInitialBarMetaToPreviousStaff = false; + }; - if (this._sy !== AlphaTexSymbols.String) { - this._error('simile', AlphaTexSymbols.String, true); + const bar: Lazy = new Lazy(() => { + const b = this._newBar(this._state.currentStaff!); + if (initialBarMeta) { + for (const initial of initialBarMeta) { + this._handler.applyBarMetaData(this, b, initial); } + resetInitialBarMeta(); + } + return b; + }); - bar.simileMark = this._parseSimileMarkFromString(this._syData as string); - this._sy = this._newSy(); - } else if (syData === 'scale') { - this._sy = this._newSy(true); + // bar meta + for (const m of node.metaData) { + const tag = m.tag.tag.text.toLowerCase(); + if (this._handler.knownStructuralMetaDataTags.has(tag)) { + this._state.hasAnyProperData = true; + + const result = this._handler.applyStructuralMetaData(this, m); + switch (result) { + case ApplyStructuralMetaDataResult.AppliedNewTrack: + if (hadNewStaff) { + // new track after new staff -> apply to previous staff + applyInitialBarMetaToPreviousStaff = true; + } else if (hadNewTrack) { + // multiple new tracks -> apply to previous staff + applyInitialBarMetaToPreviousStaff = true; + } else { + hadNewTrack = true; + previousStaff = this._state.currentStaff!; + } - if (this._sy !== AlphaTexSymbols.Number) { - this._error('scale', AlphaTexSymbols.Number, true); - } + bar.reset(); - master.displayScale = this._syData as number; - bar.displayScale = this._syData as number; - this._sy = this._newSy(); - } else if (syData === 'width') { - this._sy = this._newSy(); + break; + case ApplyStructuralMetaDataResult.AppliedNewStaff: + if (hadNewStaff) { + applyInitialBarMetaToPreviousStaff = true; + } else { + hadNewStaff = true; + previousStaff = this._state.currentStaff!; + } - if (this._sy !== AlphaTexSymbols.Number) { - this._error('width', AlphaTexSymbols.Number, true); + // new bar needed on new structural level + bar.reset(); + break; } - 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; + if (initialBarMeta) { + if (applyInitialBarMetaToPreviousStaff) { + if (previousStaff.bars.length === 0) { + // need initial bar + const initialBar: Bar = new Bar(); + previousStaff.addBar(initialBar); - 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; + const initialVoice: Voice = new Voice(); + initialBar.addVoice(initialVoice); + + if (previousStaff.bars.length > this._state.score.masterBars.length) { + const master = new MasterBar(); + this._state.score.addMasterBar(master); + } + + // apply all data + for (const initial of initialBarMeta) { + this._handler.applyBarMetaData(this, initialBar, initial); + } + + resetInitialBarMeta(); + } else { + // this should never occur. as far I can judge, + // we only run into this case when we have multiple \ and \staff tags + // without content inbetween, hence they should be empty + throw new AlphaTabError( + AlphaTabErrorType.AlphaTex, + `Unexpected internal error, didn't expect a filled staff after multiple \\track and/or \\staff tags. Please report this problem providing the input alphaTex.` + ); + } } + } + } else if (this._handler.knownScoreMetaDataTags.has(m.tag.tag.text.toLowerCase())) { + this._state.hasAnyProperData = true; + this._handler.applyScoreMetaData(this, this._state.score, m); + } else if (this._handler.knownStaffMetaDataTags.has(m.tag.tag.text.toLowerCase())) { + // backwards compatiblity (staff metadata tags) + this._state.hasAnyProperData = true; + this._handler.applyStaffMetaData(this, this._state.currentStaff!, m); + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT306, + message: 'This staff metadata tag should be specified as staff property.', + severity: AlphaTexDiagnosticsSeverity.Warning, + start: m.start, + end: m.end + }); + } else if (this._handler.knownBarMetaDataTags.has(m.tag.tag.text.toLowerCase())) { + this._state.hasAnyProperData = true; + if (initialBarMeta) { + initialBarMeta.push(m); } else { - switch (this._handleStaffMeta()) { - case StaffMetaResult.EndOfMetaDetected: - endOfMeta = true; - break; - default: - this._error('measure-effects', AlphaTexSymbols.String, false); - break; - } + this._handler.applyBarMetaData(this, bar.value, m); } + } else { + const knownMeta = Array.from(this._handler.allKnownMetaDataTags).join(','); + this.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT204, + message: `Unrecognized metadata '${m.tag.tag.text}', expected one of: ${knownMeta}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: m.tag.start, + end: m.tag.end + }); } } - 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; + return bar.value; } - 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; + private _newBar(staff: Staff): Bar { + // existing bar? -> e.g. in multi-voice setups where we fill empty voices later + if (this._state.barIndex < staff.bars.length) { + const bar = staff.bars[this._state.barIndex]; + this._state.barIndex++; + return bar; } - return BarLineStyle.Automatic; - } + const voiceCount = staff.bars.length === 0 ? 1 : staff.bars[0].voices.length; - 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; + // 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._state.barIndex++; - private _handleDirections(master: MasterBar) { - this._sy = this._newSy(); - if (this._sy !== AlphaTexSymbols.String) { - this._error('direction', AlphaTexSymbols.String, true); + if (newBar.index > 0) { + newBar.clef = newBar.previousBar!.clef; } - 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; + for (let i = 0; i < voiceCount; i++) { + const voice: Voice = new Voice(); + newBar.addVoice(voice); + } - case 'dacoda': - master.addDirection(Direction.JumpDaCoda); - break; - case 'dadoublecoda': - master.addDirection(Direction.JumpDaDoubleCoda); - break; - default: - this._errorMessage(`Unexpected direction value: '${this._syData}'`); - return; + if (this._state.currentStaff!.bars.length > this._state.score.masterBars.length) { + const master = new MasterBar(); + this._state.score.addMasterBar(master); + if (master.index > 0) { + master.timeSignatureDenominator = master.previousMasterBar!.timeSignatureDenominator; + master.timeSignatureNumerator = master.previousMasterBar!.timeSignatureNumerator; + master.tripletFeel = master.previousMasterBar!.tripletFeel; + } } - this._sy = this._newSy(); + return newBar; } - 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); + public startNewStaff(): Staff { + this._state.ignoredInitialVoice = false; - if (this._sy === AlphaTexSymbols.String) { - tempoAutomation.text = this._syData as string; - this._sy = this._newSy(true); - } + if (this._state.ignoredInitialStaff || this._state.currentTrack!.staves[0].bars.length > 0) { + const previousWasPercussion = this._state.currentStaff!.isPercussion; + this._state.currentTrack!.ensureStaveCount(this._state.currentTrack!.staves.length + 1); + const staff = this._state.currentTrack!.staves[this._state.currentTrack!.staves.length - 1]; + this._beginStaff(staff); - if (this._sy !== AlphaTexSymbols.Number) { - this._error('tempo', AlphaTexSymbols.Number, true); + if (previousWasPercussion) { + this.applyPercussionStaff(this._state.currentStaff!); } - 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(); - } + this._state.currentDynamics = DynamicValue.F; + } else { + this._state.ignoredInitialStaff = true; + } + return this._state.currentStaff!; + } - 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; + public applyPercussionStaff(staff: Staff) { + staff.isPercussion = true; + staff.showTablature = false; + staff.track.playbackInfo.program = 0; + } - this._sy = this._newSy(); + public startNewTrack(): Track { + this._state.ignoredInitialVoice = false; + this._state.ignoredInitialStaff = false; - if (this._sy === AlphaTexSymbols.String && (this._syData as string) !== 'r') { - tempoAutomation.text = this._syData as string; - this._sy = this._newSy(); - } + // new track starting? - if no masterbars it's the \track of the initial track. + if (this._state.ignoredInitialTrack || this._state.score.masterBars.length > 0) { + this._newTrack(); } else { - this._error('tempo', AlphaTexSymbols.Number, true); + this._state.ignoredInitialTrack = true; } - return tempoAutomation; + return this._state.currentTrack!; } - 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); + public startNewVoice() { + if ( + this._state.voiceIndex === 0 && + (this._state.currentStaff!.bars.length === 0 || + (this._state.currentStaff!.bars.length === 1 && + this._state.currentStaff!.bars[0].isEmpty && + !this._state.ignoredInitialVoice)) + ) { + // voice marker on the begining of the first voice without any bar yet? + // -> ignore + this._state.ignoredInitialVoice = true; + return; } - // 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; + // create directly a new empty voice for all bars + for (const b of this._state.currentStaff!.bars) { + const v = new Voice(); + b.addVoice(v); } + // start using the new voice (see newBar for details on matching) + this._state.voiceIndex++; + this._state.barIndex = 0; + this._state.currentTupletDenominator = -1; + this._state.currentTupletNumerator = -1; } } diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 2b6d29dd0..baae80344 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -3760,7 +3760,7 @@ export class MusicXmlImporter extends ScoreImporter { note.octave = 0; note.tone = 0; } else { - const value: number = octave * 12 + ModelUtils.getToneForText(step).noteValue; + const value: number = octave * 12 + (ModelUtils.getToneForText(step)?.noteValue ?? 0); note.octave = (value / 12) | 0; note.tone = value - note.octave * 12; } @@ -3791,7 +3791,7 @@ export class MusicXmlImporter extends ScoreImporter { } semitones = semitones | 0; // no microtones supported - const value: number = octave * 12 + ModelUtils.getToneForText(step).noteValue + semitones; + const value: number = octave * 12 + (ModelUtils.getToneForText(step)?.noteValue ?? 0) + semitones; const note = new Note(); note.octave = (value / 12) | 0; diff --git a/packages/alphatab/src/importer/ScoreLoader.ts b/packages/alphatab/src/importer/ScoreLoader.ts index 36e99257c..95faf3c2a 100644 --- a/packages/alphatab/src/importer/ScoreLoader.ts +++ b/packages/alphatab/src/importer/ScoreLoader.ts @@ -1,16 +1,13 @@ import { Environment } from '@src/Environment'; import { FileLoadError } from '@src/FileLoadError'; - +import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import type { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { ByteBuffer } from '@src/io/ByteBuffer'; - +import { Logger } from '@src/Logger'; import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; -import { Logger } from '@src/Logger'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; - /** * The ScoreLoader enables you easy loading of Scores using all * available importers diff --git a/packages/alphatab/src/importer/UnsupportedFormatError.ts b/packages/alphatab/src/importer/UnsupportedFormatError.ts index 46a7874f5..a678bf21e 100644 --- a/packages/alphatab/src/importer/UnsupportedFormatError.ts +++ b/packages/alphatab/src/importer/UnsupportedFormatError.ts @@ -8,6 +8,5 @@ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; export class UnsupportedFormatError extends AlphaTabError { public constructor(message: string | null = null, inner?: Error) { super(AlphaTabErrorType.Format, message ?? 'Unsupported format', inner); - Object.setPrototypeOf(this, UnsupportedFormatError.prototype); } } diff --git a/packages/alphatab/src/importer/_barrel.ts b/packages/alphatab/src/importer/_barrel.ts index 8214fd854..0385c7105 100644 --- a/packages/alphatab/src/importer/_barrel.ts +++ b/packages/alphatab/src/importer/_barrel.ts @@ -1,4 +1,5 @@ +export { AlphaTexErrorWithDiagnostics, AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +export * as alphaTex from '@src/importer/alphaTex/_barrel'; export { ScoreImporter } from '@src/importer/ScoreImporter'; export { ScoreLoader } from '@src/importer/ScoreLoader'; export { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; -export { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; diff --git a/packages/alphatab/src/importer/alphaTex/ATNF.ts b/packages/alphatab/src/importer/alphaTex/ATNF.ts new file mode 100644 index 000000000..d1e68589c --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/ATNF.ts @@ -0,0 +1,144 @@ +import { + type AlphaTexBackSlashTokenNode, + type AlphaTexBraceCloseTokenNode, + type AlphaTexBraceOpenTokenNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataNode, + type AlphaTexMetaDataTagNode, + AlphaTexNodeType, + type AlphaTexNumberLiteral, + type AlphaTexParenthesisCloseTokenNode, + type AlphaTexParenthesisOpenTokenNode, + type AlphaTexPropertiesNode, + type AlphaTexPropertyNode, + type AlphaTexStringLiteral, + type AlphaTexValueList, + type IAlphaTexValueListItem +} from '@src/importer/alphaTex/AlphaTexAst'; + +/** + * AlphaTexNodeFactory (short name for less code) + * @internal + */ +export class Atnf { + static ident(text: string): AlphaTexIdentifier { + return { + nodeType: AlphaTexNodeType.Ident, + text + } as AlphaTexIdentifier; + } + static string(text: string): AlphaTexStringLiteral { + return { nodeType: AlphaTexNodeType.String, text } as AlphaTexStringLiteral; + } + static number(value: number): AlphaTexNumberLiteral { + return { + nodeType: AlphaTexNodeType.Number, + value + } as AlphaTexNumberLiteral; + } + + public static meta( + tag: string, + values?: AlphaTexValueList, + properties?: AlphaTexPropertiesNode + ): AlphaTexMetaDataNode { + return { + nodeType: AlphaTexNodeType.Meta, + tag: { + nodeType: AlphaTexNodeType.Tag, + prefix: { + nodeType: AlphaTexNodeType.Backslash + } as AlphaTexBackSlashTokenNode, + tag: { + nodeType: AlphaTexNodeType.Ident, + text: tag + } as AlphaTexIdentifier + } as AlphaTexMetaDataTagNode, + values, + properties, + propertiesBeforeValues: false + } as AlphaTexMetaDataNode; + } + + public static identMeta(tag: string, value: string): AlphaTexMetaDataNode { + return Atnf.meta(tag, Atnf.identValue(value)); + } + + public static numberMeta(tag: string, value: number): AlphaTexMetaDataNode { + return Atnf.meta(tag, Atnf.numberValue(value)); + } + + public static values( + values: (IAlphaTexValueListItem | undefined)[], + parentheses: boolean | undefined = undefined + ): AlphaTexValueList | undefined { + const valueList = { + nodeType: AlphaTexNodeType.Values, + values: values.filter(v => v !== undefined) + } as AlphaTexValueList; + + const addParenthesis: boolean = parentheses === undefined ? valueList.values.length > 1 : parentheses; + + if (addParenthesis) { + valueList.openParenthesis = { + nodeType: AlphaTexNodeType.LParen + } as AlphaTexParenthesisOpenTokenNode; + valueList.closeParenthesis = { + nodeType: AlphaTexNodeType.RParen + } as AlphaTexParenthesisCloseTokenNode; + } + + if (valueList.values.length === 0) { + return undefined; + } + + return valueList; + } + + public static stringValue(text: string): AlphaTexValueList { + return Atnf.values([Atnf.string(text)])!; + } + + public static identValue(text: string): AlphaTexValueList { + return Atnf.values([Atnf.ident(text)])!; + } + + public static numberValue(value: number): AlphaTexValueList { + return Atnf.values([Atnf.number(value)])!; + } + + public static props( + properties: ([string, AlphaTexValueList | undefined] | undefined)[] + ): AlphaTexPropertiesNode { + const node = { + nodeType: AlphaTexNodeType.Props, + properties: [], + openBrace: { + nodeType: AlphaTexNodeType.LBrace + } as AlphaTexBraceOpenTokenNode, + closeBrace: { + nodeType: AlphaTexNodeType.RBrace + } as AlphaTexBraceCloseTokenNode + } as AlphaTexPropertiesNode; + + for (const p of properties) { + if (p) { + node.properties.push({ + nodeType: AlphaTexNodeType.Prop, + property: Atnf.ident(p[0]), + values: p[1] + } as AlphaTexPropertyNode); + } + } + + return node; + } + + public static prop(properties: AlphaTexPropertyNode[], identifier: string, values?: AlphaTexValueList) { + properties.push({ + nodeType: AlphaTexNodeType.Prop, + property: Atnf.ident(identifier), + values + }); + } +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts new file mode 100644 index 000000000..d1356b472 --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -0,0 +1,341 @@ +import { AlphaTexAccidentalMode } from '@src/importer/alphaTex/AlphaTexShared'; +import { BarLineStyle } from '@src/model/Bar'; +import { BarreShape } from '@src/model/BarreShape'; +import { BendStyle } from '@src/model/BendStyle'; +import { BendType } from '@src/model/BendType'; +import { Clef } from '@src/model/Clef'; +import { Direction } from '@src/model/Direction'; +import { DynamicValue } from '@src/model/DynamicValue'; +import { FermataType } from '@src/model/Fermata'; +import { GraceType } from '@src/model/GraceType'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { Ottavia } from '@src/model/Ottavia'; +import { Rasgueado } from '@src/model/Rasgueado'; +import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; +import { SimileMark } from '@src/model/SimileMark'; +import { TripletFeel } from '@src/model/TripletFeel'; +import { WhammyType } from '@src/model/WhammyType'; +import { TextAlign } from '@src/platform/ICanvas'; + +/** + * @internal + * @partial + */ +export class AlphaTex1EnumMappings { + /** + * @target web + * @partial + */ + private static _toEnum(_type: object, value:number,) { + return value as T; + } + + private static *_basicMappingIterator(type: object) { + for (const [name, value] of Object.entries(type).filter(e => typeof e[1] === 'number')) { + const txt = name.startsWith('_') ? name.substring(1) : name; + yield [txt.toLowerCase(), AlphaTex1EnumMappings._toEnum(type, value as number)] as [string, T]; + } + } + + private static _basic(type: object, additionals?: [string, T][]): Map { + const mapping = new Map(AlphaTex1EnumMappings._basicMappingIterator(type)); + if (additionals) { + for (const i of additionals) { + mapping.set(i[0], i[1]); + } + } + return mapping; + } + + private static _reverse(map: Map): Map { + const reversed = new Map(); + for (const [k, v] of map) { + if (!reversed.has(v)) { + reversed.set(v, k); + } + } + return reversed; + } + + public static readonly whammyTypes = AlphaTex1EnumMappings._basic(WhammyType); + public static readonly whammyTypesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.whammyTypes); + + public static readonly bendStyles = AlphaTex1EnumMappings._basic(BendStyle); + public static readonly bendStylesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.bendStyles); + + public static readonly graceTypes = new Map([ + ['ob', GraceType.OnBeat], + ['b', GraceType.BendGrace], + ['bb', GraceType.BeforeBeat] + ]); + public static readonly graceTypesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.graceTypes); + + public static readonly fermataTypes = AlphaTex1EnumMappings._basic(FermataType); + public static readonly fermataTypesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.fermataTypes); + + public static readonly accidentalModes = + AlphaTex1EnumMappings._basic(AlphaTexAccidentalMode); + public static readonly accidentalModesReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.accidentalModes + ); + + public static readonly barreShapes = AlphaTex1EnumMappings._basic(BarreShape); + public static readonly barreShapesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.barreShapes); + + public static readonly ottava = AlphaTex1EnumMappings._basic(Ottavia); + public static readonly ottavaReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.ottava); + + public static readonly rasgueadoPatterns = AlphaTex1EnumMappings._basic(Rasgueado); + public static readonly rasgueadoPatternsReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.rasgueadoPatterns + ); + + public static readonly dynamics = AlphaTex1EnumMappings._basic(DynamicValue); + + public static readonly dynamicsReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.dynamics); + + public static readonly bracketExtendModes = AlphaTex1EnumMappings._basic(BracketExtendMode); + public static readonly bracketExtendModesReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.bracketExtendModes + ); + + public static readonly trackNamePolicies = AlphaTex1EnumMappings._basic(TrackNamePolicy); + + public static readonly trackNamePoliciesReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.trackNamePolicies + ); + + public static readonly trackNameOrientations = + AlphaTex1EnumMappings._basic(TrackNameOrientation); + + public static readonly trackNameOrientationsReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.trackNameOrientations + ); + + public static readonly trackNameMode = AlphaTex1EnumMappings._basic(TrackNameMode); + public static readonly trackNameModeReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.trackNameMode); + + public static readonly textAligns = AlphaTex1EnumMappings._basic(TextAlign); + + public static readonly textAlignsReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.textAligns); + + public static readonly bendTypes = AlphaTex1EnumMappings._basic(BendType); + public static readonly bendTypesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.bendTypes); + + public static readonly keySignatures = AlphaTex1EnumMappings._basic(KeySignature, [ + ['cbmajor', KeySignature.Cb], + ['abminor', KeySignature.Cb], + + ['gbmajor', KeySignature.Gb], + ['ebminor', KeySignature.Gb], + + ['dbmajor', KeySignature.Db], + ['bbminor', KeySignature.Db], + + ['abmajor', KeySignature.Ab], + ['fminor', KeySignature.Ab], + + ['ebmajor', KeySignature.Eb], + ['cminor', KeySignature.Eb], + + ['bbmajor', KeySignature.Bb], + ['gminor', KeySignature.Bb], + + ['fmajor', KeySignature.F], + ['dminor', KeySignature.F], + + ['cmajor', KeySignature.C], + ['aminor', KeySignature.C], + + ['gmajor', KeySignature.G], + ['eminor', KeySignature.G], + + ['dmajor', KeySignature.D], + ['bminor', KeySignature.D], + + ['amajor', KeySignature.A], + ['f#minor', KeySignature.A], + + ['emajor', KeySignature.E], + ['c#minor', KeySignature.E], + + ['bmajor', KeySignature.B], + ['g#minor', KeySignature.B], + + ['f#', KeySignature.FSharp], + ['f#major', KeySignature.FSharp], + ['d#minor', KeySignature.FSharp], + + ['c#', KeySignature.CSharp], + ['c#major', KeySignature.CSharp], + ['a#minor', KeySignature.CSharp] + ]); + + public static readonly keySignaturesMajorReversed = new Map([ + [KeySignature.Cb, 'cb'], + [KeySignature.Gb, 'gb'], + [KeySignature.Db, 'db'], + [KeySignature.Ab, 'ab'], + [KeySignature.Eb, 'eb'], + [KeySignature.Bb, 'bb'], + [KeySignature.F, 'f'], + [KeySignature.C, 'c'], + [KeySignature.G, 'g'], + [KeySignature.D, 'd'], + [KeySignature.A, 'a'], + [KeySignature.E, 'e'], + [KeySignature.B, 'b'], + [KeySignature.FSharp, 'f#'], + [KeySignature.CSharp, 'c#'] + ]); + + public static readonly keySignaturesMinorReversed = new Map([ + [KeySignature.Cb, 'abminor'], + [KeySignature.Gb, 'ebminor'], + [KeySignature.Db, 'bbminor'], + [KeySignature.Ab, 'fminor'], + [KeySignature.Eb, 'cminor'], + [KeySignature.Bb, 'gminor'], + [KeySignature.F, 'dminor'], + [KeySignature.C, 'aminor'], + [KeySignature.G, 'eminor'], + [KeySignature.D, 'bminor'], + [KeySignature.A, 'f#minor'], + [KeySignature.E, 'c#minor'], + [KeySignature.B, 'g#minor'], + [KeySignature.FSharp, 'd#minor'], + [KeySignature.CSharp, 'a#minor'] + ]); + + public static readonly keySignatureTypes = new Map([ + ['cb', KeySignatureType.Major], + ['cbmajor', KeySignatureType.Major], + ['abminor', KeySignatureType.Minor], + + ['gb', KeySignatureType.Major], + ['gbmajor', KeySignatureType.Major], + ['ebminor', KeySignatureType.Minor], + + ['db', KeySignatureType.Major], + ['dbmajor', KeySignatureType.Major], + ['bbminor', KeySignatureType.Minor], + + ['ab', KeySignatureType.Major], + ['abmajor', KeySignatureType.Major], + ['fminor', KeySignatureType.Minor], + + ['eb', KeySignatureType.Major], + ['ebmajor', KeySignatureType.Major], + ['cminor', KeySignatureType.Minor], + + ['bb', KeySignatureType.Major], + ['bbmajor', KeySignatureType.Major], + ['gminor', KeySignatureType.Minor], + + ['f', KeySignatureType.Major], + ['fmajor', KeySignatureType.Major], + ['dminor', KeySignatureType.Minor], + + ['c', KeySignatureType.Major], + ['cmajor', KeySignatureType.Major], + ['aminor', KeySignatureType.Minor], + + ['g', KeySignatureType.Major], + ['gmajor', KeySignatureType.Major], + ['eminor', KeySignatureType.Minor], + + ['d', KeySignatureType.Major], + ['dmajor', KeySignatureType.Major], + ['bminor', KeySignatureType.Minor], + + ['a', KeySignatureType.Major], + ['amajor', KeySignatureType.Major], + ['f#minor', KeySignatureType.Minor], + + ['e', KeySignatureType.Major], + ['emajor', KeySignatureType.Major], + ['c#minor', KeySignatureType.Minor], + + ['b', KeySignatureType.Major], + ['bmajor', KeySignatureType.Major], + ['g#minor', KeySignatureType.Minor], + + ['f#', KeySignatureType.Major], + ['f#major', KeySignatureType.Major], + ['d#minor', KeySignatureType.Minor], + + ['c#', KeySignatureType.Major], + ['c#major', KeySignatureType.Major], + ['a#minor', KeySignatureType.Minor] + ]); + + public static readonly clefs = AlphaTex1EnumMappings._basic(Clef, [ + ['treble', Clef.G2], + ['bass', Clef.F4], + ['alto', Clef.C3], + ['tenor', Clef.C4], + ['n', Clef.Neutral] + ]); + public static readonly clefsReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.clefs); + + public static readonly tripletFeels = AlphaTex1EnumMappings._basic(TripletFeel, [ + ['no', TripletFeel.NoTripletFeel], + ['none', TripletFeel.NoTripletFeel], + + ['t16', TripletFeel.Triplet16th], + ['triplet-16th', TripletFeel.Triplet16th], + + ['t8', TripletFeel.Triplet8th], + ['triplet-8th', TripletFeel.Triplet8th], + + ['d16', TripletFeel.Dotted16th], + ['dotted-16th', TripletFeel.Dotted16th], + + ['d8', TripletFeel.Dotted8th], + ['dotted-8th', TripletFeel.Dotted8th], + + ['s16', TripletFeel.Scottish16th], + ['scottish-16th', TripletFeel.Scottish16th], + + ['s8', TripletFeel.Scottish8th], + ['scottish-8th', TripletFeel.Scottish8th] + ]); + public static readonly tripletFeelsReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.tripletFeels); + + public static readonly barLines = AlphaTex1EnumMappings._basic(BarLineStyle); + public static readonly barLinesReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.barLines); + + public static readonly ottavia = AlphaTex1EnumMappings._basic(Ottavia); + public static readonly ottaviaReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.ottavia); + + public static readonly simileMarks = AlphaTex1EnumMappings._basic(SimileMark); + public static readonly simileMarksReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.simileMarks); + + public static readonly directions = new Map([ + ['fine', Direction.TargetFine], + ['segno', Direction.TargetSegno], + ['segnosegno', Direction.TargetSegnoSegno], + ['coda', Direction.TargetCoda], + ['doublecoda', Direction.TargetDoubleCoda], + + ['dacapo', Direction.JumpDaCapo], + ['dacapoalcoda', Direction.JumpDaCapoAlCoda], + ['dacapoaldoublecoda', Direction.JumpDaCapoAlDoubleCoda], + ['dacapoalfine', Direction.JumpDaCapoAlFine], + + ['dalsegno', Direction.JumpDalSegno], + ['dalsegnoalcoda', Direction.JumpDalSegnoAlCoda], + ['dalsegnoaldoublecoda', Direction.JumpDalSegnoAlDoubleCoda], + ['dalsegnoalfine', Direction.JumpDalSegnoAlFine], + + ['dalsegnosegno', Direction.JumpDalSegnoSegno], + ['dalsegnosegnoalcoda', Direction.JumpDalSegnoSegnoAlCoda], + ['dalsegnosegnoaldoublecoda', Direction.JumpDalSegnoSegnoAlDoubleCoda], + ['dalsegnosegnoalfine', Direction.JumpDalSegnoSegnoAlFine], + + ['dacoda', Direction.JumpDaCoda], + ['dadoublecoda', Direction.JumpDaDoubleCoda] + ]); + public static readonly directionsReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.directions); +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts new file mode 100644 index 000000000..9406ad15f --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -0,0 +1,882 @@ +import { AlphaTex1EnumMappings } from '@src/importer/alphaTex/AlphaTex1EnumMappings'; +import { AlphaTexNodeType } from '@src/importer/alphaTex/AlphaTexAst'; + +/** + * Defines how the value of the meta data tag is parsed. + * @internal + */ +export enum ValueListParseTypesMode { + /** + * Indicates that the value of the given types is required. + * If the token matches, it is added to the value list. + * If the token does not match, an error diagnostic is added and parsing is stopped. + */ + Required, + /** + * Indicates that the value of the given types is optional. + * If the token matches, it is added to the value list. + * If the token does not match, the value list completes and parsing continues. + */ + Optional, + + /** + * Same as {@link Required} but the next value is interpreted as a float. + */ + RequiredAsFloat, + + /** + * Same as {@link Optional} but the next value is interpreted as a float. + */ + OptionalAsFloat, + + /** + * Same as {@link Optional} but the next value is interpreted as a float. + * But this value is only handled on value lists with parenthesis. + * @remarks + * This mode primarily serves the need of preventing tempo automations + * to overlap with stringed notes: + * `\tempo 120 "Moderate" 1.0 2.0` - 1.0 should be a fretted note not the ratio position + * but here it is the ratio position: + * `\tempo (120 "Moderate" 1.0) 2.0 + */ + OptionalAsFloatInValueList, + + /** + * Indicates that the value of the given types is optional and if matched the + * only value of this list. + * If the token matches, it is added to the value list and the parsing continues. + * If the token does not match, the value list completes and parsing continues. + */ + OptionalAndStop, + + /** + * Indicates that multiple values of the same types should be parsed as a value list. + * If the token is a open parenthesis, it starts reading the specified types as value list. If an unexpected item is + * encountered an error diagnostic is added. + * If the token is not an open parenthesis, an error diagnostic is added and parsing is stopped. + */ + RequiredAsValueList, + + /** + * Indicates that multiple values of the same types should be parsed. + * If the token matches, it is added to the value list. Parsing stays on the current type. + * If the token does not match, the value list completes and parsing continues. + */ + ValueListWithoutParenthesis +} + +/** + * @record + * @internal + */ +export interface ValueListParseTypesExtended { + expectedTypes: Set; + parseMode: ValueListParseTypesMode; + allowedValues?: Set; + reservedIdentifiers?: Set; +} + +/** + * @internal + */ +export class AlphaTex1LanguageDefinitions { + private static _valueType( + expectedTypes: AlphaTexNodeType[], + parseMode: ValueListParseTypesMode, + allowedValues?: string[], + reservedIdentifiers?: string[] + ): ValueListParseTypesExtended { + return { + expectedTypes: new Set(expectedTypes), + parseMode, + allowedValues: allowedValues ? new Set(allowedValues) : undefined, + reservedIdentifiers: reservedIdentifiers ? new Set(reservedIdentifiers) : undefined + }; + } + private static _basicList( + basic: [AlphaTexNodeType[] /* accepted types */, ValueListParseTypesMode][] + ): ValueListParseTypesExtended[] { + return basic.map(b => AlphaTex1LanguageDefinitions._valueType(b[0], b[1])); + } + + private static readonly _scoreInfoValueListTypes = [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Required + ), + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.String], ValueListParseTypesMode.Optional), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.textAligns.keys()) + ) + ]; + + private static readonly _scoreInfoTemplateValueListTypes = [ + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.String], ValueListParseTypesMode.Required), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.textAligns.keys()) + ) + ]; + private static readonly _numberOnlyValueListTypes = AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required] + ]); + private static readonly _textLikeValueListTypes = AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.String, AlphaTexNodeType.Ident], ValueListParseTypesMode.Required] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in `\chord {}` properties. + */ + public static readonly chordPropertyValueListTypes = new Map([ + // firstfret 3 + ['firstfret', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // showdiagram, showdiagram true, showdiagram false, showdiagram 0, showdiagram 1 + [ + 'showdiagram', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.String, AlphaTexNodeType.Ident, AlphaTexNodeType.Number], + ValueListParseTypesMode.Optional + ] + ]) + ], + + // showfingering, showfingering true, showfingering false, showfingering 0, showfingering 1 + [ + 'showfingering', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.String, AlphaTexNodeType.Ident, AlphaTexNodeType.Number], + ValueListParseTypesMode.Optional + ] + ]) + ], + + // showname, showname true, showname false, showname 0, showname 1 + [ + 'showname', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.String, AlphaTexNodeType.Ident, AlphaTexNodeType.Number], + ValueListParseTypesMode.Optional + ] + ]) + ], + + // barre 1 2 3 + [ + 'barre', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.ValueListWithoutParenthesis] + ]) + ] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in `\tuning {}` properties. + */ + public static readonly tuningPropertyValueListTypes = new Map([ + // hide + ['hide', undefined], + + // label "Label" + [ + 'label', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.String], ValueListParseTypesMode.Required]]) + ] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in `\staff {}` properties. + */ + public static readonly staffPropertyValueListTypes = new Map([ + // score, score 1 + [ + 'score', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + ['tabs', undefined], + ['slash', undefined], + ['numbered', undefined] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in `\track {}` properties. + */ + public static readonly trackPropertyValueListTypes = new Map([ + // color red, color "#FF0000" + ['color', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // defaultsystemslayout 3 + ['defaultsystemslayout', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // volume 16 + ['volume', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // balance 16 + ['balance', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // bank 16 + ['bank', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // systemslayout 1 2 3 4 5 + [ + 'systemslayout', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.ValueListWithoutParenthesis] + ]) + ], + + // instrument 27, instrument percussion, instrument "acoustic guitar nylon" + [ + 'instrument', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + ValueListParseTypesMode.Required + ] + ]) + ], + + ['mute', undefined], + ['solo', undefined], + ['multibarrest', undefined] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in beat level properties (beat effects). + */ + public static readonly beatPropertyValueListTypes = new Map([ + ['f', undefined], + ['fo', undefined], + ['vs', undefined], + ['v', undefined], + ['vw', undefined], + ['s', undefined], + ['p', undefined], + ['tt', undefined], + ['dd', undefined], + ['d', undefined], + ['su', undefined], + ['sd', undefined], + ['cre', undefined], + ['dec', undefined], + ['spd', undefined], + ['sph', undefined], + ['spu', undefined], + ['spe', undefined], + ['slashed', undefined], + ['ds', undefined], + ['glpf', undefined], + ['glpt', undefined], + ['waho', undefined], + ['wahc', undefined], + ['legatoorigin', undefined], + ['timer', undefined], + + // tu 3, tu 3,2 + [ + 'tu', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional] + ]) + ], + + // txt "Text", txt Intro + ['txt', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // lyrics "Lyrics", lyrics 2 "Lyrics Line 2" + [ + 'lyrics', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional], + [[AlphaTexNodeType.String], ValueListParseTypesMode.Required] + ]) + ], + + // tb dip fast (0 -1 0), tb dip (0 -1 0), tb (0 -1 0) + [ + 'tb', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.RequiredAsValueList + ) + ] + ], + + // tbe dip fast (0 0 -1 30 0 60), tbe dip (0 0 -1 30 0 60), tbe (0 0 -1 30 0 60) + [ + 'tbe', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.whammyTypes.keys()) + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.bendStyles.keys()) + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.RequiredAsValueList + ) + ] + ], + + // bu, bu 16 + [ + 'bu', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + // bd, bd 16 + [ + 'bd', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + // au, au 16 + [ + 'au', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + // ad, ad 16 + [ + 'ad', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + // ch C, ch "C" + ['ch', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // gr, gr ob, gr b + [ + 'gr', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.graceTypes.keys()) + ) + ] + ], + + // dy F, dy "F" + ['dy', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // tempo 120, tempo 120 "Label", tempo 120 "Label" hide + [ + 'tempo', + [ + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.Number], ValueListParseTypesMode.Required), + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.String], ValueListParseTypesMode.Optional), + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.Ident], ValueListParseTypesMode.Optional, [ + 'hide' + ]) + ] + ], + + // volume 10 + ['volume', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // balance 0 + ['balance', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // tp 16 + ['tp', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // barre 7, barre 7 full, barre 7 "half" + [ + 'barre', + [ + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.Number], ValueListParseTypesMode.Required), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.barreShapes.keys()) + ) + ] + ], + + // rasg ii, rasg "mi" + ['rasg', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // ot 15ma, ot "regular" + ['ot', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // instrument 27, instrument percussion, instrument "acoustic guitar nylon" + [ + 'instrument', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + ValueListParseTypesMode.Required + ] + ]) + ], + + // bank 127 + ['bank', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // fermata short, fermata short 0.5 + [ + 'fermata', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.fermataTypes.keys()) + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.OptionalAsFloat + ) + ] + ], + + // beam invert + ['beam', AlphaTex1LanguageDefinitions._textLikeValueListTypes] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in beat duration properties. + */ + public static readonly beatDurationPropertyValueListTypes = new Map< + string, + ValueListParseTypesExtended[] | undefined + >([ + // tu 3, tu 3 2 + [ + 'tu', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional] + ]) + ] + ]); + + /** + * Contains the definitions how to read the values for given properties using {@link readTypedValueList} + * in note level properties (note effects). + */ + public static readonly notePropertyValueListTypes = new Map([ + [ + 'nh', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + [ + 'ah', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + [ + 'th', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + [ + 'ph', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + [ + 'sh', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + [ + 'fh', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + ['v', undefined], + ['vw', undefined], + ['sl', undefined], + ['ss', undefined], + ['sib', undefined], + ['sia', undefined], + ['sou', undefined], + ['sod', undefined], + ['psd', undefined], + ['psu', undefined], + ['h', undefined], + ['lht', undefined], + ['g', undefined], + ['ac', undefined], + ['hac', undefined], + ['ten', undefined], + ['pm', undefined], + ['st', undefined], + ['lr', undefined], + ['x', undefined], + ['-', undefined], + ['t', undefined], + ['turn', undefined], + ['iturn', undefined], + ['umordent', undefined], + ['lmordent', undefined], + ['string', undefined], + ['hide', undefined], + + // b bendRelease fast (0 4 0), b bendRelease (0 4 0), b (0 4 0) + [ + 'b', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.RequiredAsValueList + ) + ] + ], + + // be bendRelease fast (0 0 4 30 0 60), be bendRelease (0 0 4 30 0 60), be (0 0 4 30 0 60) + [ + 'be', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.bendTypes.keys()) + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + Array.from(AlphaTex1EnumMappings.bendStyles.keys()) + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.RequiredAsValueList + ) + ] + ], + + // tr 14, tr 14 32 + [ + 'tr', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional] + ]) + ], + + // lf, lf 1 + [ + 'lf', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + // rf, rf 1 + [ + 'rf', + AlphaTex1LanguageDefinitions._basicList([[[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional]]) + ], + + // acc "#" + ['acc', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // slur S1, slur "1" + ['slur', AlphaTex1LanguageDefinitions._textLikeValueListTypes] + ]); + + public static readonly structuralMetaDataValueListTypes = new Map< + string, + ValueListParseTypesExtended[] | undefined + >([ + // track, track Name, track ShortName Name, track "Name", track "ShortName" "Name" + [ + 'track', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.String], ValueListParseTypesMode.Optional], + [[AlphaTexNodeType.String], ValueListParseTypesMode.Optional] + ]) + ], + ['staff', undefined], + ['voice', undefined] + ]); + + public static readonly staffMetaDataValueListTypes = new Map([ + // tuning E4 B3 G3 D3 A2 E2, \tuning "E4" "B3" "G3" "D3", \tuning piano + [ + 'tuning', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Ident, AlphaTexNodeType.String], + ValueListParseTypesMode.OptionalAndStop, + ['piano', 'none', 'voice'] + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Ident, AlphaTexNodeType.String], + ValueListParseTypesMode.ValueListWithoutParenthesis + ) + ] + ], + + // chord "C" 0 1 0 2 3 x + [ + 'chord', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Ident, AlphaTexNodeType.String], ValueListParseTypesMode.Required], + [ + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + ValueListParseTypesMode.ValueListWithoutParenthesis + ] + ]) + ], + // capo 3 + ['capo', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // instrument 27, instrument percussion, instrument "acoustic guitar nylon" + [ + 'instrument', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + ValueListParseTypesMode.Required + ] + ]) + ], + // bank 127 + ['bank', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // lyrics "Text", lyrics 1 "Text" + [ + 'lyrics', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional], + [[AlphaTexNodeType.String], ValueListParseTypesMode.Required] + ]) + ], + + // articulation defaults, articulation "Name" 27 + [ + 'articulation', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.String, AlphaTexNodeType.Ident], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Optional] + ]) + ], + + // displaytranspose -12 + ['displaytranspose', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // transpose -12 + ['transpose', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes] + ]); + + public static readonly scoreMetaDataValueListTypes = new Map([ + ['title', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['subtitle', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['artist', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['album', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['words', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['music', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['wordsandmusic', AlphaTex1LanguageDefinitions._scoreInfoTemplateValueListTypes], + ['copyright', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['copyright2', AlphaTex1LanguageDefinitions._scoreInfoTemplateValueListTypes], + ['instructions', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['notices', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['tab', AlphaTex1LanguageDefinitions._scoreInfoValueListTypes], + ['defaultsystemslayout', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + // systemslayout 1 2 3 4 5 + [ + 'systemslayout', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.ValueListWithoutParenthesis] + ]) + ], + ['hidedynamics', undefined], + ['showdynamics', undefined], + ['usesystemsignseparator', undefined], + ['multibarrest', undefined], + ['bracketextendmode', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + ['singletracktracknamepolicy', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + ['multitracktracknamepolicy', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + ['firstsystemtracknamemode', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + ['othersystemstracknamemode', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + ['firstsystemtracknameorientation', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + ['othersystemstracknameorientation', AlphaTex1LanguageDefinitions._textLikeValueListTypes] + ]); + + /** + * Contains the definitions how to read the metadata values for given metadata tags using + * {@link readTypedValueList} + * @private + */ + public static readonly barMetaDataValueListTypes = new Map([ + // tempo 120, tempo 120 "Moderate", tempo 120 "Moderate" 0.5, tempo 120 hide 0.5, tempo 120 hide + [ + 'tempo', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.RequiredAsFloat + ), + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.String], ValueListParseTypesMode.Optional), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.Number], + ValueListParseTypesMode.OptionalAsFloatInValueList + ), + AlphaTex1LanguageDefinitions._valueType([AlphaTexNodeType.Ident], ValueListParseTypesMode.Optional, [ + 'hide' + ]) + ] + ], + + // rc 2 + ['rc', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // ae (1 2 3), ae 2 + [ + 'ae', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.RequiredAsValueList] + ]) + ], + + // ts common, ts "common", ts 3 4 + [ + 'ts', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.String, AlphaTexNodeType.Ident], ValueListParseTypesMode.OptionalAndStop], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required] + ]) + ], + + // ks fmajor or ks "fmajor" + ['ks', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // clef G2, clef "g2", clef 43, + [ + 'clef', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + ValueListParseTypesMode.Required + ] + ]) + ], + + // section "Text", section "Marker" "Text", section Intro, section I Intro + [ + 'section', + [ + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Required + ), + AlphaTex1LanguageDefinitions._valueType( + [AlphaTexNodeType.String, AlphaTexNodeType.Ident], + ValueListParseTypesMode.Optional, + undefined, + ['x', '-', 'r'] + ) + ] + ], + + // tf triplet16th, tf "Triplet16th", tf 1 + [ + 'tf', + AlphaTex1LanguageDefinitions._basicList([ + [ + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + ValueListParseTypesMode.Required + ] + ]) + ], + + // barlineleft dotted, barlineleft "dotted" + ['barlineleft', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + // barlineright dotted, barlineright "dotted" + ['barlineright', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + // accidentals auto, accidentals "explicit" + ['accidentals', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + // jump fine, jump "segno" + ['jump', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + // ottava 15ma, ottava "regular" + ['ottava', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + // simile none, simile "firstOfDouble" + ['simile', AlphaTex1LanguageDefinitions._textLikeValueListTypes], + + // width 300 + ['width', AlphaTex1LanguageDefinitions._numberOnlyValueListTypes], + + // scale 0.5 + [ + 'scale', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.RequiredAsFloat] + ]) + ], + + [ + 'spd', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.RequiredAsFloat] + ]) + ], + [ + 'spu', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.RequiredAsFloat] + ]) + ], + [ + 'sph', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.RequiredAsFloat] + ]) + ], + + ['ft', undefined], + ['ro', undefined], + ['ac', undefined], + ['db', undefined], + + // \sync BarIndex Occurence MillisecondOffset + // \sync BarIndex Occurence MillisecondOffset RatioPosition + [ + 'sync', + AlphaTex1LanguageDefinitions._basicList([ + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.Required], + [[AlphaTexNodeType.Number], ValueListParseTypesMode.OptionalAsFloat] + ]) + ] + ]); + + public static readonly metaDataValueListTypes = [ + AlphaTex1LanguageDefinitions.scoreMetaDataValueListTypes, + AlphaTex1LanguageDefinitions.staffMetaDataValueListTypes, + AlphaTex1LanguageDefinitions.structuralMetaDataValueListTypes, + AlphaTex1LanguageDefinitions.barMetaDataValueListTypes + ]; +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts new file mode 100644 index 000000000..f008680dd --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -0,0 +1,3464 @@ +// unfortunately the "old" alphaTex syntax had no strict delimiters +// for values and properties. That's why we need to parse the properties exactly +// as needed for the identifiers. In an alphaTex2 we should make this parsing simpler. +// the parser should not need to do that semantic checks, that's the importers job +// but we emit "Hint" diagnostics for now. + +import { AlphaTex1EnumMappings } from '@src/importer/alphaTex/AlphaTex1EnumMappings'; +import { + AlphaTex1LanguageDefinitions, + type ValueListParseTypesExtended, + ValueListParseTypesMode +} from '@src/importer/alphaTex/AlphaTex1LanguageDefinitions'; +import { + type AlphaTexAstNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataNode, + AlphaTexNodeType, + type AlphaTexNumberLiteral, + type AlphaTexPropertyNode, + type AlphaTexStringLiteral, + type AlphaTexTextNode, + type AlphaTexValueList, + type IAlphaTexValueListItem +} from '@src/importer/alphaTex/AlphaTexAst'; +import { + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity, + StaffNoteKind, + type IAlphaTexImporter +} from '@src/importer/alphaTex/AlphaTexShared'; +import { Atnf } from '@src/importer/alphaTex/ATNF'; +import { + ApplyNodeResult, + ApplyStructuralMetaDataResult, + type IAlphaTexLanguageImportHandler +} from '@src/importer/alphaTex/IAlphaTexLanguageImportHandler'; +import { GeneralMidi } from '@src/midi/GeneralMidi'; +import { AccentuationType } from '@src/model/AccentuationType'; +import { Automation, AutomationType, type FlatSyncPoint } from '@src/model/Automation'; +import { type Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; +import { BarreShape } from '@src/model/BarreShape'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; +import { BendPoint } from '@src/model/BendPoint'; +import { BendStyle } from '@src/model/BendStyle'; +import { BrushType } from '@src/model/BrushType'; +import { Chord } from '@src/model/Chord'; +import { Clef } from '@src/model/Clef'; +import { Color } from '@src/model/Color'; +import { CrescendoType } from '@src/model/CrescendoType'; +import { Duration } from '@src/model/Duration'; +import { FadeType } from '@src/model/FadeType'; +import { Fermata } from '@src/model/Fermata'; +import { Fingers } from '@src/model/Fingers'; +import { GolpeType } from '@src/model/GolpeType'; +import { GraceType } from '@src/model/GraceType'; +import { HarmonicType } from '@src/model/HarmonicType'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { Lyrics } from '@src/model/Lyrics'; +import type { MasterBar } from '@src/model/MasterBar'; +import { ModelUtils } from '@src/model/ModelUtils'; +import type { Note } from '@src/model/Note'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { Ottavia } from '@src/model/Ottavia'; +import { PercussionMapper } from '@src/model/PercussionMapper'; +import { PickStroke } from '@src/model/PickStroke'; +import type { RenderStylesheet } from '@src/model/RenderStylesheet'; +import { HeaderFooterStyle, Score, ScoreStyle, ScoreSubElement } from '@src/model/Score'; +import { Section } from '@src/model/Section'; +import { SimileMark } from '@src/model/SimileMark'; +import { SlideInType } from '@src/model/SlideInType'; +import { SlideOutType } from '@src/model/SlideOutType'; +import { Staff } from '@src/model/Staff'; +import { Track } from '@src/model/Track'; +import { TripletFeel } from '@src/model/TripletFeel'; +import { Tuning } from '@src/model/Tuning'; +import { VibratoType } from '@src/model/VibratoType'; +import { WahPedal } from '@src/model/WahPedal'; +import { BeamDirection } from '@src/rendering/_barrel'; +import { SynthConstants } from '@src/synth/SynthConstants'; + +/** + * @internal + */ +export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler { + public static readonly instance = new AlphaTex1LanguageHandler(); + + public applyScoreMetaData( + importer: IAlphaTexImporter, + score: Score, + metaData: AlphaTexMetaDataNode + ): ApplyNodeResult { + const result = this._checkValueListTypes( + importer, + [AlphaTex1LanguageDefinitions.scoreMetaDataValueListTypes], + metaData, + metaData.tag.tag.text.toLowerCase(), + metaData.values + ); + if (result !== undefined) { + return result!; + } + + switch (metaData.tag.tag.text.toLowerCase()) { + case 'title': + score.title = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Title, metaData); + return ApplyNodeResult.Applied; + case 'subtitle': + score.subTitle = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.SubTitle, metaData); + return ApplyNodeResult.Applied; + case 'artist': + score.artist = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Artist, metaData); + return ApplyNodeResult.Applied; + case 'album': + score.album = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Album, metaData); + return ApplyNodeResult.Applied; + case 'words': + score.words = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Words, metaData); + return ApplyNodeResult.Applied; + case 'music': + score.music = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Music, metaData); + return ApplyNodeResult.Applied; + case 'copyright': + score.copyright = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Copyright, metaData); + return ApplyNodeResult.Applied; + case 'instructions': + score.instructions = (metaData.values!.values[0] as AlphaTexTextNode).text; + return ApplyNodeResult.Applied; + case 'notices': + score.notices = (metaData.values!.values[0] as AlphaTexTextNode).text; + return ApplyNodeResult.Applied; + case 'tab': + score.tab = (metaData.values!.values[0] as AlphaTexTextNode).text; + this._headerFooterStyle(importer, score, ScoreSubElement.Transcriber, metaData); + return ApplyNodeResult.Applied; + case 'copyright2': + this._headerFooterStyle(importer, score, ScoreSubElement.CopyrightSecondLine, metaData, 0); + return ApplyNodeResult.Applied; + case 'wordsandmusic': + this._headerFooterStyle(importer, score, ScoreSubElement.WordsAndMusic, metaData, 0); + return ApplyNodeResult.Applied; + case 'defaultsystemslayout': + score.defaultSystemsLayout = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + return ApplyNodeResult.Applied; + case 'systemslayout': + for (const v of metaData.values!.values) { + score.systemsLayout.push((v as AlphaTexNumberLiteral).value); + } + return ApplyNodeResult.Applied; + case 'hidedynamics': + score.stylesheet.hideDynamics = true; + return ApplyNodeResult.Applied; + case 'showdynamics': + score.stylesheet.hideDynamics = false; + return ApplyNodeResult.Applied; + case 'bracketextendmode': + const bracketExtendMode = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'bracket extend mode', + AlphaTex1EnumMappings.bracketExtendModes + ); + if (bracketExtendMode === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.bracketExtendMode = bracketExtendMode!; + return ApplyNodeResult.Applied; + case 'usesystemsignseparator': + score.stylesheet.useSystemSignSeparator = true; + return ApplyNodeResult.Applied; + case 'multibarrest': + score.stylesheet.multiTrackMultiBarRest = true; + return ApplyNodeResult.Applied; + case 'singletracktracknamepolicy': + const singleTrackTrackNamePolicy = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'track name policy', + AlphaTex1EnumMappings.trackNamePolicies + ); + if (singleTrackTrackNamePolicy === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.singleTrackTrackNamePolicy = singleTrackTrackNamePolicy!; + return ApplyNodeResult.Applied; + case 'multitracktracknamepolicy': + const multiTrackTrackNamePolicy = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'track name policy', + AlphaTex1EnumMappings.trackNamePolicies + ); + if (multiTrackTrackNamePolicy === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.multiTrackTrackNamePolicy = multiTrackTrackNamePolicy!; + return ApplyNodeResult.Applied; + case 'firstsystemtracknamemode': + const firstSystemTrackNameMode = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'track name mode', + AlphaTex1EnumMappings.trackNameMode + ); + if (firstSystemTrackNameMode === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.firstSystemTrackNameMode = firstSystemTrackNameMode!; + return ApplyNodeResult.Applied; + case 'othersystemstracknamemode': + const otherSystemsTrackNameMode = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'track name mode', + AlphaTex1EnumMappings.trackNameMode + ); + if (otherSystemsTrackNameMode === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.otherSystemsTrackNameMode = otherSystemsTrackNameMode!; + return ApplyNodeResult.Applied; + case 'firstsystemtracknameorientation': + const firstSystemTrackNameOrientation = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'track name orientation', + AlphaTex1EnumMappings.trackNameOrientations + ); + if (firstSystemTrackNameOrientation === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.firstSystemTrackNameOrientation = firstSystemTrackNameOrientation!; + return ApplyNodeResult.Applied; + case 'othersystemstracknameorientation': + const otherSystemsTrackNameOrientation = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'track name orientation', + AlphaTex1EnumMappings.trackNameOrientations + ); + if (otherSystemsTrackNameOrientation === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.otherSystemsTrackNameOrientation = otherSystemsTrackNameOrientation!; + return ApplyNodeResult.Applied; + default: + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + } + + private _checkValueListTypes( + importer: IAlphaTexImporter, + lookupList: Map[], + parent: AlphaTexAstNode, + tag: string, + values: AlphaTexValueList | undefined + ): ApplyNodeResult | undefined { + const lookup = lookupList.find(l => l.has(tag)); + if (!lookup) { + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + + const types = lookup.get(tag); + if (types === undefined) { + if (values) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT300, + message: `Expected no values, but found some. Values are ignored.`, + start: values.start, + end: values.end, + severity: AlphaTexDiagnosticsSeverity.Warning + }); + } + return undefined; + } + + if (!this._validateValueListTypes(importer, types, parent, values)) { + return ApplyNodeResult.NotAppliedSemanticError; + } + + return undefined; + } + + public applyStaffMetaData( + importer: IAlphaTexImporter, + staff: Staff, + metaData: AlphaTexMetaDataNode + ): ApplyNodeResult { + const result = this._checkValueListTypes( + importer, + [AlphaTex1LanguageDefinitions.staffMetaDataValueListTypes], + metaData, + metaData.tag.tag.text.toLowerCase(), + metaData.values + ); + if (result !== undefined) { + return result!; + } + + switch (metaData.tag.tag.text.toLowerCase()) { + case 'capo': + staff.capo = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + return ApplyNodeResult.Applied; + case 'tuning': + const tuning: number[] = []; + let hideTuning = false; + let tuningName = ''; + for (let i = 0; i < metaData.values!.values.length; i++) { + const v = metaData.values!.values[i]; + const text = (v as AlphaTexTextNode).text; + switch (text) { + case 'piano': + case 'none': + case 'voice': + importer.applyStaffNoteKind(staff, StaffNoteKind.Pitched); + i = metaData.values!.values.length; + break; + // backwards compatibility only + case 'hide': + hideTuning = true; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT305, + message: `This value should be rather specified via the properties.`, + start: v.start, + end: v.end, + severity: AlphaTexDiagnosticsSeverity.Warning + }); + break; + default: + const t = ModelUtils.parseTuning(text); + if (t) { + tuning.push(t.realValue); + } else if (i === metaData.values!.values.length - 1 && tuning.length > 0) { + tuningName = text; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT305, + message: `This value should be rather specified via the properties.`, + start: v.start, + end: v.end, + severity: AlphaTexDiagnosticsSeverity.Warning + }); + } else { + const tuningLetters = Array.from(ModelUtils.tuningLetters).join(','); + const accidentalModes = Array.from(ModelUtils.accidentalModeMapping.keys()).join(','); + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected tuning value '${text}', expected: where =oneOf(${tuningLetters}) =oneOf(${accidentalModes}), =number`, + start: v.start, + end: v.end, + severity: AlphaTexDiagnosticsSeverity.Error + }); + } + break; + } + } + + importer.state.staffHasExplicitTuning.add(staff); + importer.state.staffTuningApplied.delete(staff); + staff.stringTuning = new Tuning(); + staff.stringTuning.tunings = tuning; + staff.stringTuning.name = tuningName; + + this._tuningProperties(importer, staff, staff.stringTuning, metaData); + + if (hideTuning) { + if (!staff.track.score.stylesheet.perTrackDisplayTuning) { + staff.track.score.stylesheet.perTrackDisplayTuning = new Map(); + } + staff.track.score.stylesheet.perTrackDisplayTuning!.set(staff.track.index, false); + } + return ApplyNodeResult.Applied; + case 'instrument': + importer.state.staffTuningApplied.delete(staff); + this._readTrackInstrument(importer, staff.track, metaData.values!); + + return ApplyNodeResult.Applied; + case 'bank': + staff.track.playbackInfo.bank = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + return ApplyNodeResult.Applied; + case 'lyrics': + const lyrics: Lyrics = new Lyrics(); + lyrics.startBar = 0; + lyrics.text = ''; + if (metaData.values!.values.length === 2) { + lyrics.startBar = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + lyrics.text = (metaData.values!.values[1] as AlphaTexTextNode).text; + } else { + lyrics.text = (metaData.values!.values[0] as AlphaTexTextNode).text; + } + importer.state.lyrics.get(staff.track.index)!.push(lyrics); + + return ApplyNodeResult.Applied; + case 'chord': + const chord = new Chord(); + this._chordProperties(importer, chord, metaData); + chord.name = (metaData.values!.values[0] as AlphaTexTextNode).text; + + for (let i = 1; i < metaData.values!.values.length; i++) { + const v = metaData.values!.values[i]; + if (v.nodeType === AlphaTexNodeType.Number) { + chord.strings.push((v as AlphaTexNumberLiteral).value); + } else if (v.nodeType === AlphaTexNodeType.Ident) { + const txt = (v as AlphaTexIdentifier).text; + if (txt === 'x') { + chord.strings.push(-1); + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected chord value '${txt}', expected: 'x'`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: v.start, + end: v.end + }); + } + } + } + staff.addChord(AlphaTex1LanguageHandler._getChordId(staff, chord.name), chord); + return ApplyNodeResult.Applied; + case 'articulation': + const percussionArticulationNames = importer.state.percussionArticulationNames; + const articulationName = (metaData.values!.values[0] as AlphaTexTextNode).text; + if (articulationName === 'defaults') { + for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) { + percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue); + percussionArticulationNames.set(ModelUtils.toArticulationId(defaultName), defaultValue); + } + return ApplyNodeResult.Applied; + } + + if (metaData.values!.values.length === 2) { + const number = (metaData.values!.values[1] as AlphaTexNumberLiteral).value; + if (PercussionMapper.instrumentArticulations.has(number)) { + percussionArticulationNames.set(articulationName.toLowerCase(), number); + return ApplyNodeResult.Applied; + } else { + const articulations = Array.from(PercussionMapper.instrumentArticulations.keys()) + .map(n => `${n}`) + .join(','); + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected articulation value '${number}', expected: ${articulations}`, + start: metaData.values!.values[1].start, + end: metaData.values!.values[1].end, + severity: AlphaTexDiagnosticsSeverity.Error + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + } + + return ApplyNodeResult.Applied; + case 'accidentals': + return AlphaTex1LanguageHandler._handleAccidentalMode(importer, metaData.values!); + case 'displaytranspose': + staff.displayTranspositionPitch = (metaData.values!.values[0] as AlphaTexNumberLiteral).value * -1; + importer.state.staffHasExplicitDisplayTransposition.add(staff); + return ApplyNodeResult.Applied; + case 'transpose': + staff.transpositionPitch = (metaData.values!.values[0] as AlphaTexNumberLiteral).value * -1; + return ApplyNodeResult.Applied; + default: + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + } + + public applyBarMetaData(importer: IAlphaTexImporter, bar: Bar, metaData: AlphaTexMetaDataNode): ApplyNodeResult { + const result = this._checkValueListTypes( + importer, + [AlphaTex1LanguageDefinitions.barMetaDataValueListTypes], + metaData, + metaData.tag.tag.text.toLowerCase(), + metaData.values + ); + if (result !== undefined) { + return result!; + } + + switch (metaData.tag.tag.text.toLowerCase()) { + case 'sync': + const syncPoint = this._buildSyncPoint(metaData); + importer.state.syncPoints.push(syncPoint); + return ApplyNodeResult.Applied; + case 'tempo': + let ti = 0; + const tempo = (metaData.values!.values[ti++] as AlphaTexNumberLiteral).value; + let tempoLabel = ''; + let isVisible = true; + let ratioPosition = 0; + + while (ti < metaData.values!.values.length) { + switch (metaData.values!.values[ti].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const txt = (metaData.values!.values[ti] as AlphaTexTextNode).text; + if (txt === 'hide') { + isVisible = false; + } else { + tempoLabel = txt; + } + break; + case AlphaTexNodeType.Number: + ratioPosition = (metaData.values!.values[ti] as AlphaTexNumberLiteral).value; + break; + } + ti++; + } + + let tempoAutomation = bar.masterBar.tempoAutomations.find(a => a.ratioPosition === ratioPosition); + if (!tempoAutomation) { + tempoAutomation = new Automation(); + bar.masterBar.tempoAutomations.push(tempoAutomation); + } + tempoAutomation.isLinear = false; + tempoAutomation.type = AutomationType.Tempo; + tempoAutomation.value = tempo; + tempoAutomation.text = tempoLabel; + tempoAutomation.ratioPosition = ratioPosition; + tempoAutomation.isVisible = isVisible; + + return ApplyNodeResult.Applied; + case 'rc': + bar.masterBar.repeatCount = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + return ApplyNodeResult.Applied; + case 'ae': + for (const e of metaData.values!.values) { + if (e.nodeType === AlphaTexNodeType.Number) { + const num = (e as AlphaTexNumberLiteral).value; + if (num < 1 || num > 31) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT211, + message: `Value is out of valid range. Allowed range: %s, Actual Value: %s`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: e.start, + end: e.end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } else { + // Alternate endings bitflag starts from 0 + bar.masterBar.alternateEndings |= 1 << (num - 1); + } + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT202, + message: `Unexpected '${AlphaTexNodeType[e.nodeType]}' token. Expected one of following: ${AlphaTexNodeType[AlphaTexNodeType.Number]}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: e.start, + end: e.end + }); + } + } + return ApplyNodeResult.Applied; + case 'ts': + switch (metaData.values!.values[0].nodeType) { + case AlphaTexNodeType.Number: + bar.masterBar.timeSignatureNumerator = ( + metaData.values!.values[0] as AlphaTexNumberLiteral + ).value; + bar.masterBar.timeSignatureDenominator = ( + metaData.values!.values[1] as AlphaTexNumberLiteral + ).value; + break; + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const tsValue = (metaData.values!.values[0] as AlphaTexTextNode).text; + if (tsValue.toLowerCase() === 'common') { + bar.masterBar.timeSignatureCommon = true; + bar.masterBar.timeSignatureNumerator = 4; + bar.masterBar.timeSignatureDenominator = 4; + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected time signature value '${tsValue}', expected: common or two numbers`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: metaData.values!.values[0].start, + end: metaData.values!.values[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + break; + } + return ApplyNodeResult.Applied; + case 'ks': + const keySignature = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'key signature', + AlphaTex1EnumMappings.keySignatures + ); + if (keySignature === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + + const keySignatureType = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'key signature type', + AlphaTex1EnumMappings.keySignatureTypes + ); + if (keySignatureType === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + + bar.keySignature = keySignature!; + bar.keySignatureType = keySignatureType!; + + return ApplyNodeResult.Applied; + case 'clef': + switch (metaData.values!.values[0].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const clef = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'clef', + AlphaTex1EnumMappings.clefs + ); + if (clef === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.clef = clef!; + break; + + case AlphaTexNodeType.Number: + const clefValue = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + + switch (clefValue) { + case 0: + bar.clef = Clef.Neutral; + break; + case 43: + bar.clef = Clef.G2; + break; + case 65: + bar.clef = Clef.F4; + break; + case 48: + bar.clef = Clef.C3; + break; + case 60: + bar.clef = Clef.C4; + break; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected clef value '${clefValue}', expected: ${Array.from(AlphaTex1EnumMappings.clefs.keys()).join(',')}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: metaData.values!.values[0].start, + end: metaData.values!.values[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + + break; + } + + return ApplyNodeResult.Applied; + case 'section': + const section = new Section(); + if (metaData.values!.values.length === 1) { + section.text = (metaData.values!.values[0] as AlphaTexTextNode).text; + } else { + section.marker = (metaData.values!.values[0] as AlphaTexTextNode).text; + section.text = (metaData.values!.values[1] as AlphaTexTextNode).text; + } + bar.masterBar.section = section; + return ApplyNodeResult.Applied; + case 'tf': + switch (metaData.values!.values[0].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const tripletFeel = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'triplet feel', + AlphaTex1EnumMappings.tripletFeels + ); + if (tripletFeel === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.masterBar.tripletFeel = tripletFeel!; + break; + + case AlphaTexNodeType.Number: + const tripletFeelValue = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + + switch (tripletFeelValue) { + case 0: + bar.masterBar.tripletFeel = TripletFeel.NoTripletFeel; + break; + case 1: + bar.masterBar.tripletFeel = TripletFeel.Triplet16th; + break; + case 2: + bar.masterBar.tripletFeel = TripletFeel.Triplet8th; + break; + case 3: + bar.masterBar.tripletFeel = TripletFeel.Dotted16th; + break; + case 4: + bar.masterBar.tripletFeel = TripletFeel.Dotted8th; + break; + case 5: + bar.masterBar.tripletFeel = TripletFeel.Scottish16th; + break; + case 6: + bar.masterBar.tripletFeel = TripletFeel.Scottish8th; + break; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected triplet feel value '${tripletFeelValue}', expected: ${Array.from(AlphaTex1EnumMappings.tripletFeels.keys()).join(',')}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: metaData.values!.values[0].start, + end: metaData.values!.values[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + + break; + } + return ApplyNodeResult.Applied; + case 'barlineleft': + const barLineLeft = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'bar line', + AlphaTex1EnumMappings.barLines + ); + if (barLineLeft === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.barLineLeft = barLineLeft!; + return ApplyNodeResult.Applied; + case 'barlineright': + const barLineRight = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'bar line', + AlphaTex1EnumMappings.barLines + ); + if (barLineRight === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.barLineRight = barLineRight!; + return ApplyNodeResult.Applied; + case 'accidentals': + return AlphaTex1LanguageHandler._handleAccidentalMode(importer, metaData.values!); + case 'jump': + const direction = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'direction', + AlphaTex1EnumMappings.directions + ); + if (direction === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.masterBar.addDirection(direction!); + return ApplyNodeResult.Applied; + case 'ottava': + const ottava = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'clef ottava', + AlphaTex1EnumMappings.ottavia + ); + if (ottava === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.clefOttava = ottava!; + return ApplyNodeResult.Applied; + case 'simile': + const simile = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'simile mark', + AlphaTex1EnumMappings.simileMarks + ); + if (simile === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + bar.simileMark = simile!; + return ApplyNodeResult.Applied; + case 'width': + bar.masterBar.displayWidth = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + bar.displayWidth = bar.masterBar.displayWidth; + return ApplyNodeResult.Applied; + case 'scale': + bar.masterBar.displayScale = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + bar.displayScale = bar.masterBar.displayScale; + return ApplyNodeResult.Applied; + case 'spd': + const sustainPedalDown = new SustainPedalMarker(); + sustainPedalDown.pedalType = SustainPedalMarkerType.Down; + sustainPedalDown.ratioPosition = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + bar.sustainPedals.push(sustainPedalDown); + return ApplyNodeResult.Applied; + case 'spu': + const sustainPedalUp = new SustainPedalMarker(); + sustainPedalUp.pedalType = SustainPedalMarkerType.Up; + sustainPedalUp.ratioPosition = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + bar.sustainPedals.push(sustainPedalUp); + return ApplyNodeResult.Applied; + case 'sph': + const sustainPedalHold = new SustainPedalMarker(); + sustainPedalHold.pedalType = SustainPedalMarkerType.Hold; + sustainPedalHold.ratioPosition = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + bar.sustainPedals.push(sustainPedalHold); + return ApplyNodeResult.Applied; + case 'ft': + bar.masterBar.isFreeTime = true; + return ApplyNodeResult.Applied; + case 'ro': + bar.masterBar.isRepeatStart = true; + return ApplyNodeResult.Applied; + case 'ac': + bar.masterBar.isAnacrusis = true; + return ApplyNodeResult.Applied; + case 'db': + bar.masterBar.isDoubleBar = true; + bar.barLineRight = BarLineStyle.LightLight; + return ApplyNodeResult.Applied; + default: + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + } + + private static _handleAccidentalMode(importer: IAlphaTexImporter, values: AlphaTexValueList): ApplyNodeResult { + const accidentalMode = AlphaTex1LanguageHandler._parseEnumValue( + importer, + values, + 'accidental mode', + AlphaTex1EnumMappings.accidentalModes + ); + if (accidentalMode === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + importer.state.accidentalMode = accidentalMode!; + return ApplyNodeResult.Applied; + } + + private static _getChordId(currentStaff: Staff, chordName: string): string { + return chordName.toLowerCase() + currentStaff.index + currentStaff.track.index; + } + + private _buildSyncPoint(metaData: AlphaTexMetaDataNode): FlatSyncPoint { + const barIndex = (metaData.values!.values[0] as AlphaTexNumberLiteral).value; + const barOccurence = (metaData.values!.values[1] as AlphaTexNumberLiteral).value; + const millisecondOffset = (metaData.values!.values[2] as AlphaTexNumberLiteral).value; + let barPosition = 0; + if (metaData.values!.values.length > 3) { + barPosition = (metaData.values!.values[3] as AlphaTexNumberLiteral).value; + } + + return { + barIndex, + barOccurence, + barPosition, + millisecondOffset + }; + } + + private _validateValueListTypes( + importer: IAlphaTexImporter, + expectedValues: ValueListParseTypesExtended[], + parent: AlphaTexAstNode, + values: AlphaTexValueList | undefined + ) { + let error = false; + let expectedIndex = 0; + let actualIndex = 0; + + if (!values) { + const anyRequired = expectedValues.some( + v => + v.parseMode === ValueListParseTypesMode.Required || + v.parseMode === ValueListParseTypesMode.RequiredAsFloat || + v.parseMode === ValueListParseTypesMode.RequiredAsValueList + ); + if (anyRequired) { + const expectedTypes = AlphaTex1LanguageHandler._buildExpectedTypesMessage(expectedValues); + + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT210, + message: `Missing value. Expected following values: ${expectedTypes}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: parent.start, + end: parent.end + }); + return false; + } else { + return true; + } + } + + if (values.validated) { + return true; + } + + while (expectedIndex < expectedValues.length) { + const expected = expectedValues[expectedIndex]; + + const value: IAlphaTexValueListItem | undefined = + actualIndex < values.values.length ? values.values[actualIndex] : undefined; + + if (value && expected.expectedTypes.has(value.nodeType) && this._checkRestrictions(expected, value)) { + switch (expected.parseMode) { + case ValueListParseTypesMode.OptionalAndStop: + // stop reading values + expectedIndex = expectedValues.length; + actualIndex++; + break; + case ValueListParseTypesMode.ValueListWithoutParenthesis: + case ValueListParseTypesMode.RequiredAsValueList: + // stay on current element + actualIndex++; + break; + default: + // advance to next item + expectedIndex++; + actualIndex++; + break; + } + } + // not matched value? + else if (value) { + switch (expected.parseMode) { + case ValueListParseTypesMode.ValueListWithoutParenthesis: + case ValueListParseTypesMode.RequiredAsValueList: + // end of value list as soon we have a different type + expectedIndex++; + break; + case ValueListParseTypesMode.Required: + case ValueListParseTypesMode.RequiredAsFloat: + error = true; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected required value '${AlphaTexNodeType[value.nodeType]}', expected: ${AlphaTex1LanguageHandler._buildExpectedTypesMessage( + [expected] + )}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: value.start, + end: value.end + }); + expectedIndex++; + actualIndex++; + break; + case ValueListParseTypesMode.Optional: + case ValueListParseTypesMode.OptionalAsFloat: + case ValueListParseTypesMode.OptionalAsFloatInValueList: + case ValueListParseTypesMode.OptionalAndStop: + // Skip value and try next + expectedIndex++; + break; + } + } + // no value anymore + else { + switch (expected.parseMode) { + case ValueListParseTypesMode.ValueListWithoutParenthesis: + case ValueListParseTypesMode.RequiredAsValueList: + // end of list + expectedIndex++; + break; + + case ValueListParseTypesMode.Required: + case ValueListParseTypesMode.RequiredAsFloat: + error = true; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT210, + message: `Missing values. Expected following values: ${AlphaTex1LanguageHandler._buildExpectedTypesMessage( + [expected] + )}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: values.end, + end: values.end + }); + expectedIndex = expectedValues.length; + break; + + case ValueListParseTypesMode.Optional: + case ValueListParseTypesMode.OptionalAsFloat: + case ValueListParseTypesMode.OptionalAsFloatInValueList: + case ValueListParseTypesMode.OptionalAndStop: + // no value for optional item + expectedIndex++; + break; + } + } + } + + // remaining values? + if (actualIndex < values.values.length) { + while (actualIndex < values.values.length) { + const expectedTypes = AlphaTex1LanguageHandler._buildExpectedTypesMessage(expectedValues); + const value = values.values[actualIndex]; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected additional value '${AlphaTexNodeType[value.nodeType]}', expected: ${expectedTypes}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: value.start, + end: value.end + }); + actualIndex++; + } + } + + return !error; + } + + private _checkRestrictions(expected: ValueListParseTypesExtended, value: IAlphaTexValueListItem) { + switch (value.nodeType) { + case AlphaTexNodeType.Ident: + const identifier = value as AlphaTexIdentifier; + if (expected?.allowedValues) { + return expected.allowedValues.has(identifier.text.toLowerCase()); + } else if (expected?.reservedIdentifiers) { + return !expected.reservedIdentifiers.has(identifier.text.toLowerCase()); + } + return true; + case AlphaTexNodeType.String: + const str = value as AlphaTexStringLiteral; + if (expected?.allowedValues) { + return expected.allowedValues.has(str.text.toLowerCase()); + } + return true; + default: + return true; + } + } + + private _headerFooterStyle( + importer: IAlphaTexImporter, + score: Score, + element: ScoreSubElement, + metaData: AlphaTexMetaDataNode, + startIndex: number = 1 + ) { + const remaining = metaData.values!.values.length - startIndex; + if (remaining < 1) { + return; + } + + const style = ModelUtils.getOrCreateHeaderFooterStyle(score, element); + if (style.isVisible === undefined) { + style.isVisible = true; + } + + const value = (metaData.values!.values[startIndex] as AlphaTexTextNode).text; + if (value) { + style.template = value; + } else { + style.isVisible = false; + } + + if (remaining < 2) { + return; + } + + const textAlign = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.values!, + 'textAlign', + AlphaTex1EnumMappings.textAligns, + startIndex + 1 + ); + if (textAlign === undefined) { + return; + } + style.textAlign = textAlign!; + } + + private static _buildExpectedTypesMessage(values: ValueListParseTypesExtended[]) { + const parts: string[] = []; + + for (const v of values) { + const types = Array.from(v.expectedTypes) + .map(t => AlphaTexNodeType[t]) + .join('|'); + switch (v.parseMode) { + case ValueListParseTypesMode.Required: + case ValueListParseTypesMode.RequiredAsFloat: + parts.push(`required(${types})`); + break; + case ValueListParseTypesMode.Optional: + case ValueListParseTypesMode.OptionalAsFloat: + parts.push(`optional(${types})`); + break; + case ValueListParseTypesMode.OptionalAndStop: + parts.push(`only(${types})`); + break; + case ValueListParseTypesMode.ValueListWithoutParenthesis: + parts.push(`listOf(${types})`); + break; + } + } + + return parts.join(','); + } + + private _readTrackInstrument(importer: IAlphaTexImporter, track: Track, values: AlphaTexValueList) { + switch (values!.values[0].nodeType) { + case AlphaTexNodeType.Number: + const instrument = (values!.values[0] as AlphaTexNumberLiteral).value; + if (instrument >= 0 && instrument <= 127) { + track.playbackInfo.program = instrument; + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT211, + message: `Value is out of valid range. Allowed range: 0-127, Actual Value: ${instrument}`, + start: values!.values[0].start, + end: values!.values[0].end, + severity: AlphaTexDiagnosticsSeverity.Error + }); + } + break; + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const instrumentName = (values!.values[0] as AlphaTexTextNode).text.toLowerCase(); + if (instrumentName === 'percussion') { + for (const staff of track.staves) { + importer.applyStaffNoteKind(staff, StaffNoteKind.Articulation); + } + track.playbackInfo.primaryChannel = SynthConstants.PercussionChannel; + track.playbackInfo.secondaryChannel = SynthConstants.PercussionChannel; + } else { + track.playbackInfo.program = GeneralMidi.getValue(instrumentName); + } + break; + } + } + + private _chordProperties(importer: IAlphaTexImporter, chord: Chord, metaData: AlphaTexMetaDataNode) { + if (!metaData.properties) { + return; + } + + for (const p of metaData.properties.properties) { + if (!this._checkProperty(importer, [AlphaTex1LanguageDefinitions.chordPropertyValueListTypes], p)) { + continue; + } + + switch (p.property.text.toLowerCase()) { + case 'firstfret': + chord.firstFret = (p.values!.values[0] as AlphaTexNumberLiteral).value; + break; + case 'showdiagram': + chord.showDiagram = AlphaTex1LanguageHandler._booleanLikeValue(p.values!.values, 0); + break; + case 'showfingering': + chord.showFingering = AlphaTex1LanguageHandler._booleanLikeValue(p.values!.values, 0); + break; + case 'showname': + chord.showName = AlphaTex1LanguageHandler._booleanLikeValue(p.values!.values, 0); + break; + case 'barre': + chord.barreFrets = p.values!.values.map(v => (v as AlphaTexNumberLiteral).value); + break; + } + } + } + + private _tuningProperties( + importer: IAlphaTexImporter, + staff: Staff, + tuning: Tuning, + metaData: AlphaTexMetaDataNode + ) { + if (!metaData.properties) { + return; + } + + for (const p of metaData.properties.properties) { + if (!this._checkProperty(importer, [AlphaTex1LanguageDefinitions.tuningPropertyValueListTypes], p)) { + continue; + } + + switch (p.property.text.toLowerCase()) { + case 'hide': + if (!staff.track.score.stylesheet.perTrackDisplayTuning) { + staff.track.score.stylesheet.perTrackDisplayTuning = new Map(); + } + staff.track.score.stylesheet.perTrackDisplayTuning!.set(staff.track.index, false); + break; + case 'label': + tuning.name = (p.values!.values[0] as AlphaTexStringLiteral).text; + break; + } + } + } + + private static _booleanLikeValue(values: IAlphaTexValueListItem[], i: number): boolean { + if (i >= values.length) { + return true; + } + + const v = values[i]; + switch (v.nodeType) { + case AlphaTexNodeType.String: + case AlphaTexNodeType.Ident: + return (v as AlphaTexTextNode).text !== 'false'; + case AlphaTexNodeType.Number: + return (v as AlphaTexNumberLiteral).value !== 0; + default: + return false; + } + } + + public applyStructuralMetaData( + importer: IAlphaTexImporter, + metaData: AlphaTexMetaDataNode + ): ApplyStructuralMetaDataResult { + switch (metaData.tag.tag.text.toLowerCase()) { + case 'staff': + const staff = importer.startNewStaff(); + this._staffProperties(importer, staff, metaData); + + return ApplyStructuralMetaDataResult.AppliedNewStaff; + case 'track': + const track = importer.startNewTrack(); + + if (metaData.values && metaData.values.values.length > 0) { + track.name = (metaData.values!.values[0] as AlphaTexTextNode).text; + if (metaData.values!.values.length > 1) { + track.shortName = (metaData.values!.values[1] as AlphaTexTextNode).text; + } + } + + this._trackProperties(importer, track, metaData); + + return ApplyStructuralMetaDataResult.AppliedNewTrack; + case 'voice': + importer.startNewVoice(); + return ApplyStructuralMetaDataResult.AppliedNewVoice; + default: + return ApplyStructuralMetaDataResult.NotAppliedUnrecognizedMarker; + } + } + + private _checkProperty( + importer: IAlphaTexImporter, + lookupList: Map[], + p: AlphaTexPropertyNode + ): boolean { + const result = this._checkValueListTypes(importer, lookupList, p, p.property.text.toLowerCase(), p.values); + if (result !== undefined) { + switch (result!) { + case ApplyNodeResult.Applied: + case ApplyNodeResult.NotAppliedSemanticError: + return false; + + case ApplyNodeResult.NotAppliedUnrecognizedMarker: + const knownProps = lookupList.flatMap(l => Array.from(l.keys())); + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT212, + message: `Unrecogized property '${p.property.text}', expected one of ${knownProps}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.start, + end: p.end + }); + return false; + } + } + + return true; + } + + private _staffProperties(importer: IAlphaTexImporter, staff: Staff, metaData: AlphaTexMetaDataNode) { + if (!metaData.properties) { + return; + } + + let showStandardNotation: boolean = false; + let showTabs: boolean = false; + let showSlash: boolean = false; + let showNumbered: boolean = false; + + for (const p of metaData.properties.properties) { + if (!this._checkProperty(importer, [AlphaTex1LanguageDefinitions.staffPropertyValueListTypes], p)) { + continue; + } + + switch (p.property.text.toLowerCase()) { + case 'score': + showStandardNotation = true; + if (p.values && p.values.values.length > 0) { + staff.standardNotationLineCount = (p.values!.values[0] as AlphaTexNumberLiteral).value; + } + break; + case 'tabs': + showTabs = true; + break; + case 'slash': + showSlash = true; + break; + case 'numbered': + showNumbered = true; + break; + } + } + + if (showStandardNotation || showTabs || showSlash || showNumbered) { + staff.showStandardNotation = showStandardNotation; + staff.showTablature = showTabs; + staff.showSlash = showSlash; + staff.showNumbered = showNumbered; + } + } + + private _trackProperties(importer: IAlphaTexImporter, track: Track, metaData: AlphaTexMetaDataNode) { + if (!metaData.properties) { + return; + } + + for (const p of metaData.properties.properties) { + if (!this._checkProperty(importer, [AlphaTex1LanguageDefinitions.trackPropertyValueListTypes], p)) { + continue; + } + + switch (p.property.text.toLowerCase()) { + case 'color': + try { + track.color = Color.fromJson((p.values!.values[0] as AlphaTexTextNode).text)!; + } catch { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT213, + message: `Invalid format for color`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values!.values[0].start, + end: p.values!.values[0].end + }); + } + break; + case 'defaultsystemslayout': + track.defaultSystemsLayout = (p.values!.values[0] as AlphaTexNumberLiteral).value; + break; + case 'systemslayout': + track.systemsLayout = p.values!.values.map(v => (v as AlphaTexNumberLiteral).value); + break; + case 'volume': + track.playbackInfo.volume = (p.values!.values[0] as AlphaTexNumberLiteral).value; + break; + case 'balance': + track.playbackInfo.balance = (p.values!.values[0] as AlphaTexNumberLiteral).value; + break; + case 'mute': + track.playbackInfo.isMute = true; + break; + case 'solo': + track.playbackInfo.isSolo = true; + break; + case 'multibarrest': + if (!track.score.stylesheet.perTrackMultiBarRest) { + track.score.stylesheet.perTrackMultiBarRest = new Set(); + } + track.score.stylesheet.perTrackMultiBarRest!.add(track.index); + break; + case 'instrument': + this._readTrackInstrument(importer, track, p.values!); + break; + case 'bank': + track.playbackInfo.bank = (p.values!.values[0] as AlphaTexNumberLiteral).value; + break; + } + } + } + + public applyBeatDurationProperty(importer: IAlphaTexImporter, p: AlphaTexPropertyNode): ApplyNodeResult { + const result = this._checkValueListTypes( + importer, + [AlphaTex1LanguageDefinitions.beatDurationPropertyValueListTypes], + p, + p.property.text.toLowerCase(), + p.values + ); + if (result !== undefined) { + return result; + } + + switch (p.property.text.toLowerCase()) { + case 'tu': + if (p.values!.values.length === 2) { + importer.state.currentTupletNumerator = (p.values!.values[0] as AlphaTexNumberLiteral).value; + importer.state.currentTupletDenominator = (p.values!.values[1] as AlphaTexNumberLiteral).value; + } else { + const numerator = (p.values!.values[0] as AlphaTexNumberLiteral).value; + importer.state.currentTupletNumerator = numerator; + const denominator = AlphaTex1LanguageHandler._getTupletDenominator(numerator); + if (denominator < 0) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected default tuplet value '${numerator}', expected: 3, 5, 6, 7, 9, 10, 11 or 12`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values!.values[0].start, + end: p.values!.values[0].end + }); + importer.state.currentTupletNumerator = -1; + importer.state.currentTupletDenominator = -1; + } else { + importer.state.currentTupletDenominator = denominator; + } + } + return ApplyNodeResult.Applied; + } + + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + private static _getTupletDenominator(numerator: number) { + switch (numerator) { + case 3: + return 2; + case 5: + return 4; + case 6: + return 4; + case 7: + return 4; + case 9: + return 8; + case 10: + return 8; + case 11: + return 8; + case 12: + return 8; + default: + return -1; + } + } + + private _allKnownBarMetaDataTags: Set | undefined = undefined; + public get allKnownMetaDataTags() { + if (!this._allKnownBarMetaDataTags) { + this._allKnownBarMetaDataTags = new Set(); + const lists: Iterable[] = [ + AlphaTex1LanguageDefinitions.scoreMetaDataValueListTypes.keys(), + AlphaTex1LanguageDefinitions.structuralMetaDataValueListTypes.keys(), + AlphaTex1LanguageDefinitions.staffMetaDataValueListTypes.keys(), + AlphaTex1LanguageDefinitions.barMetaDataValueListTypes.keys() + ]; + for (const l of lists) { + for (const v of l) { + this._allKnownBarMetaDataTags.add(v); + } + } + } + return this._allKnownBarMetaDataTags; + } + + private _knownScoreMetaDataTags: Set | undefined = undefined; + public get knownScoreMetaDataTags() { + if (!this._knownScoreMetaDataTags) { + this._knownScoreMetaDataTags = new Set( + AlphaTex1LanguageDefinitions.scoreMetaDataValueListTypes.keys() + ); + } + return this._knownScoreMetaDataTags; + } + + private _knownStructuralMetaDataTags: Set | undefined = undefined; + public get knownStructuralMetaDataTags() { + if (!this._knownStructuralMetaDataTags) { + this._knownStructuralMetaDataTags = new Set( + AlphaTex1LanguageDefinitions.structuralMetaDataValueListTypes.keys() + ); + } + return this._knownStructuralMetaDataTags; + } + + private _knownBarMetaDataTags: Set | undefined = undefined; + public get knownBarMetaDataTags() { + if (!this._knownBarMetaDataTags) { + this._knownBarMetaDataTags = new Set(AlphaTex1LanguageDefinitions.barMetaDataValueListTypes.keys()); + } + return this._knownBarMetaDataTags; + } + + private _knownStaffMetaDataTags: Set | undefined = undefined; + public get knownStaffMetaDataTags() { + if (!this._knownStaffMetaDataTags) { + this._knownStaffMetaDataTags = new Set( + AlphaTex1LanguageDefinitions.staffMetaDataValueListTypes.keys() + ); + } + return this._knownStaffMetaDataTags; + } + + private _knownBeatDurationProperties: Set | undefined = undefined; + public get knownBeatDurationProperties() { + if (!this._knownBeatDurationProperties) { + this._knownBeatDurationProperties = new Set( + AlphaTex1LanguageDefinitions.beatDurationPropertyValueListTypes.keys() + ); + } + return this._knownBeatDurationProperties; + } + + private _knownBeatProperties: Set | undefined = undefined; + public get knownBeatProperties() { + if (!this._knownBeatProperties) { + this._knownBeatProperties = new Set(AlphaTex1LanguageDefinitions.beatPropertyValueListTypes.keys()); + } + return this._knownBeatProperties; + } + + private _knownNoteProperties: Set | undefined = undefined; + public get knownNoteProperties() { + if (!this._knownNoteProperties) { + this._knownNoteProperties = new Set(AlphaTex1LanguageDefinitions.notePropertyValueListTypes.keys()); + } + return this._knownNoteProperties; + } + + public applyBeatProperty(importer: IAlphaTexImporter, beat: Beat, p: AlphaTexPropertyNode): ApplyNodeResult { + const tag = p.property.text.toLowerCase(); + const result = this._checkValueListTypes( + importer, + [AlphaTex1LanguageDefinitions.beatPropertyValueListTypes], + p, + tag, + p.values + ); + if (result !== undefined) { + return result; + } + + switch (tag) { + case 'f': + beat.fade = FadeType.FadeIn; + return ApplyNodeResult.Applied; + case 'fo': + beat.fade = FadeType.FadeOut; + return ApplyNodeResult.Applied; + case 'vs': + beat.fade = FadeType.VolumeSwell; + return ApplyNodeResult.Applied; + case 'v': + beat.vibrato = VibratoType.Slight; + return ApplyNodeResult.Applied; + case 'vw': + beat.vibrato = VibratoType.Wide; + return ApplyNodeResult.Applied; + case 's': + beat.slap = true; + return ApplyNodeResult.Applied; + case 'p': + beat.pop = true; + return ApplyNodeResult.Applied; + case 'tt': + beat.tap = true; + return ApplyNodeResult.Applied; + case 'txt': + beat.text = (p.values!.values[0] as AlphaTexTextNode).text; + return ApplyNodeResult.Applied; + case 'lyrics': + let lyricsLine = 0; + let lyricsText = ''; + if (p.values!.values.length === 2) { + lyricsLine = (p.values!.values[0] as AlphaTexNumberLiteral).value; + lyricsText = (p.values!.values[1] as AlphaTexTextNode).text; + } else { + lyricsText = (p.values!.values[0] as AlphaTexTextNode).text; + } + + if (!beat.lyrics) { + beat.lyrics = []; + } + + while (beat.lyrics!.length <= lyricsLine) { + beat.lyrics.push(''); + } + + beat.lyrics[lyricsLine] = lyricsText; + return ApplyNodeResult.Applied; + case 'dd': + beat.dots = 2; + return ApplyNodeResult.Applied; + case 'd': + beat.dots = 1; + return ApplyNodeResult.Applied; + case 'su': + beat.pickStroke = PickStroke.Up; + return ApplyNodeResult.Applied; + case 'sd': + beat.pickStroke = PickStroke.Down; + return ApplyNodeResult.Applied; + case 'tu': + if (p.values!.values.length === 2) { + beat.tupletNumerator = (p.values!.values[0] as AlphaTexNumberLiteral).value; + beat.tupletDenominator = (p.values!.values[1] as AlphaTexNumberLiteral).value; + } else { + const numerator = (p.values!.values[0] as AlphaTexNumberLiteral).value; + beat.tupletNumerator = numerator; + const denominator = AlphaTex1LanguageHandler._getTupletDenominator(numerator); + if (denominator < 0) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected default tuplet value '${numerator}', expected: 3, 5, 6, 7, 9, 10, 11 or 12`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values!.values[0].start, + end: p.values!.values[0].end + }); + beat.tupletNumerator = -1; + beat.tupletDenominator = -1; + return ApplyNodeResult.NotAppliedSemanticError; + } else { + beat.tupletDenominator = denominator; + } + } + return ApplyNodeResult.Applied; + case 'tb': + case 'tbe': + let tbi = 0; + switch (p.values!.values[tbi].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const whammyBarType = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'whammy type', + AlphaTex1EnumMappings.whammyTypes, + tbi + ); + if (whammyBarType === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.whammyBarType = whammyBarType; + tbi++; + break; + } + + switch (p.values!.values[tbi].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const whammyBarStyle = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'whammy style', + AlphaTex1EnumMappings.bendStyles, + tbi + ); + if (whammyBarStyle === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.whammyStyle = whammyBarStyle!; + tbi++; + break; + } + + const points = this._getBendPoints(importer, p, tbi, tag === 'tbe'); + if (points) { + for (const point of points) { + beat.addWhammyBarPoint(point); + } + } + + return ApplyNodeResult.Applied; + case 'bu': + AlphaTex1LanguageHandler._applyBrush(beat, p, BrushType.BrushUp, 0.25); + return ApplyNodeResult.Applied; + case 'bd': + AlphaTex1LanguageHandler._applyBrush(beat, p, BrushType.BrushDown, 0.25); + return ApplyNodeResult.Applied; + case 'au': + AlphaTex1LanguageHandler._applyBrush(beat, p, BrushType.ArpeggioUp, 1); + return ApplyNodeResult.Applied; + case 'ad': + AlphaTex1LanguageHandler._applyBrush(beat, p, BrushType.ArpeggioDown, 1); + return ApplyNodeResult.Applied; + case 'ch': + const chordName: string = (p.values!.values[0] as AlphaTexTextNode).text; + const chordId: string = AlphaTex1LanguageHandler._getChordId(beat.voice.bar.staff, chordName); + if (!beat.voice.bar.staff.hasChord(chordId)) { + const chord: Chord = new Chord(); + chord.showDiagram = false; + chord.name = chordName; + beat.voice.bar.staff.addChord(chordId, chord); + } + beat.chordId = chordId; + return ApplyNodeResult.Applied; + case 'gr': + if (p.values && p.values.values.length > 0) { + const graceType = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'whammy style', + AlphaTex1EnumMappings.graceTypes + ); + if (graceType === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.graceType = graceType!; + } else { + beat.graceType = GraceType.BeforeBeat; + } + return ApplyNodeResult.Applied; + case 'dy': + const dyn = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'dynamic', + AlphaTex1EnumMappings.dynamics + ); + if (dyn === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.dynamics = dyn!; + importer.state.currentDynamics = dyn!; + return ApplyNodeResult.Applied; + case 'cre': + beat.crescendo = CrescendoType.Crescendo; + return ApplyNodeResult.Applied; + case 'dec': + beat.crescendo = CrescendoType.Decrescendo; + return ApplyNodeResult.Applied; + case 'tempo': + // NOTE: playbackRatio is calculated on score finish when playback positions are known + const tempo = (p.values!.values[0] as AlphaTexNumberLiteral).value; + let tempoLabel = ''; + let isVisible = true; + + if (p.values!.values.length > 2) { + tempoLabel = (p.values!.values[1] as AlphaTexTextNode).text; + const hideText = (p.values!.values[2] as AlphaTexTextNode).text; + if (hideText === 'hide') { + isVisible = false; + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected third tempo property value '${hideText}', expected: 'hide'`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values!.values[2].start, + end: p.values!.values[2].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + } else if (p.values!.values.length > 1) { + tempoLabel = (p.values!.values[1] as AlphaTexTextNode).text; + if (tempoLabel === 'hide') { + isVisible = false; + tempoLabel = ''; + } + } + + const tempoAutomation = new Automation(); + tempoAutomation.isLinear = false; + tempoAutomation.type = AutomationType.Tempo; + tempoAutomation.value = tempo; + tempoAutomation.text = tempoLabel; + tempoAutomation.isVisible = isVisible; + beat.automations.push(tempoAutomation); + beat.voice.bar.masterBar.tempoAutomations.push(tempoAutomation); + return ApplyNodeResult.Applied; + case 'volume': + // NOTE: playbackRatio is calculated on score finish when playback positions are known + const volumeAutomation: Automation = new Automation(); + volumeAutomation.isLinear = true; + volumeAutomation.type = AutomationType.Volume; + volumeAutomation.value = (p.values!.values[0] as AlphaTexNumberLiteral).value; + beat.automations.push(volumeAutomation); + return ApplyNodeResult.Applied; + case 'balance': + // NOTE: playbackRatio is calculated on score finish when playback positions are known + const balanceAutomation: Automation = new Automation(); + balanceAutomation.isLinear = true; + balanceAutomation.type = AutomationType.Balance; + balanceAutomation.value = ModelUtils.clamp((p.values!.values[0] as AlphaTexNumberLiteral).value, 0, 16); + beat.automations.push(balanceAutomation); + return ApplyNodeResult.Applied; + case 'tp': + beat.tremoloSpeed = Duration.Eighth; + if (p.values && p.values.values.length > 0) { + const tremoloSpeedValue = (p.values!.values[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.values!.values[0].start, + end: p.values!.values[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + } + return ApplyNodeResult.Applied; + case 'spd': + AlphaTex1LanguageHandler._applySustainPedal(importer, beat, SustainPedalMarkerType.Down); + return ApplyNodeResult.Applied; + case 'sph': + AlphaTex1LanguageHandler._applySustainPedal(importer, beat, SustainPedalMarkerType.Hold); + return ApplyNodeResult.Applied; + case 'spu': + AlphaTex1LanguageHandler._applySustainPedal(importer, beat, SustainPedalMarkerType.Up); + return ApplyNodeResult.Applied; + case 'spe': + AlphaTex1LanguageHandler._applySustainPedal(importer, beat, SustainPedalMarkerType.Up, true); + return ApplyNodeResult.Applied; + case 'slashed': + beat.slashed = true; + return ApplyNodeResult.Applied; + case 'ds': + beat.deadSlapped = true; + if (beat.notes.length === 1 && beat.notes[0].isDead) { + beat.removeNote(beat.notes[0]); + } + return ApplyNodeResult.Applied; + case 'glpf': + beat.golpe = GolpeType.Finger; + return ApplyNodeResult.Applied; + case 'glpt': + beat.golpe = GolpeType.Thumb; + return ApplyNodeResult.Applied; + case 'waho': + beat.wahPedal = WahPedal.Open; + return ApplyNodeResult.Applied; + case 'wahc': + beat.wahPedal = WahPedal.Closed; + return ApplyNodeResult.Applied; + case 'barre': + beat.barreFret = (p.values!.values[0] as AlphaTexNumberLiteral).value; + beat.barreShape = BarreShape.Full; + if (p.values!.values.length > 1) { + const barreShape = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'barre shape', + AlphaTex1EnumMappings.barreShapes, + 1 + ); + if (barreShape === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.barreShape = barreShape!; + } + return ApplyNodeResult.Applied; + case 'rasg': + const rasg = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'rasgueado pattern', + AlphaTex1EnumMappings.rasgueadoPatterns + ); + if (rasg === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.rasgueado = rasg!; + return ApplyNodeResult.Applied; + case 'ot': + const ottava = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'ottava', + AlphaTex1EnumMappings.ottava + ); + if (ottava === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + beat.ottava = ottava!; + return ApplyNodeResult.Applied; + case 'legatoorigin': + beat.isLegatoOrigin = true; + return ApplyNodeResult.Applied; + case 'instrument': + let program = 0; + + switch (p.values!.values[0].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + program = GeneralMidi.getValue((p.values!.values[0] as AlphaTexTextNode).text); + break; + + case AlphaTexNodeType.Number: + program = (p.values!.values[0] as AlphaTexNumberLiteral).value; + break; + } + + const instrumentAutomation = new Automation(); + instrumentAutomation.isLinear = false; + instrumentAutomation.type = AutomationType.Instrument; + instrumentAutomation.value = program; + beat.automations.push(instrumentAutomation); + return ApplyNodeResult.Applied; + case 'bank': + const bankAutomation = new Automation(); + bankAutomation.isLinear = false; + bankAutomation.type = AutomationType.Bank; + bankAutomation.value = (p.values!.values[0] as AlphaTexNumberLiteral).value; + beat.automations.push(bankAutomation); + return ApplyNodeResult.Applied; + case 'fermata': + const fermataType = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'fermata', + AlphaTex1EnumMappings.fermataTypes + ); + if (fermataType === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + + const fermata = new Fermata(); + fermata.type = fermataType!; + + if (p.values!.values.length > 1) { + fermata.length = (p.values!.values[1] as AlphaTexNumberLiteral).value; + } + + beat.fermata = fermata; + return ApplyNodeResult.Applied; + + case 'beam': + const beamMode = (p.values!.values[0] as AlphaTexTextNode).text; + switch (beamMode.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; + default: + const allowedValues = ['invert', 'up', 'down', 'auto', 'split', 'merge', 'splitsecondary']; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected beam value '${beamMode}', expected: ${allowedValues.join(',')}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values!.values[0].start, + end: p.values!.values[0].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + return ApplyNodeResult.Applied; + case 'timer': + beat.showTimer = true; + return ApplyNodeResult.Applied; + } + + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + + private static _applySustainPedal( + importer: IAlphaTexImporter, + beat: Beat, + pedalType: SustainPedalMarkerType, + end:boolean = false + ) { + const sustainPedal = new SustainPedalMarker(); + sustainPedal.pedalType = pedalType; + // exact ratio position will be applied after .finish() when times are known + // this means we have to at least aling them linearly to mitigate deduplication + if (end) { + sustainPedal.ratioPosition = 1; + } else { + sustainPedal.ratioPosition = 0.001 * beat.voice.bar.sustainPedals.length; + } + importer.state.sustainPedalToBeat.set(sustainPedal, beat); + beat.voice.bar.sustainPedals.push(sustainPedal); + } + + private static _applyBrush(beat: Beat, p: AlphaTexPropertyNode, brushType: BrushType, durationFactor: number) { + beat.brushType = brushType; + if (p.values && p.values.values.length > 0) { + beat.brushDuration = (p.values!.values[0] as AlphaTexNumberLiteral).value; + } else { + beat.updateDurations(); + beat.brushDuration = (beat.playbackDuration * durationFactor) / beat.notes.length; + } + } + + public applyNoteProperty(importer: IAlphaTexImporter, note: Note, p: AlphaTexPropertyNode): ApplyNodeResult { + const tag = p.property.text.toLowerCase(); + const result = this._checkValueListTypes( + importer, + [AlphaTex1LanguageDefinitions.notePropertyValueListTypes], + p, + tag, + p.values + ); + if (result !== undefined) { + return result; + } + + switch (tag) { + case 'b': + case 'be': + let tbi = 0; + switch (p.values!.values[tbi].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const bendType = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'bend type', + AlphaTex1EnumMappings.bendTypes, + tbi + ); + if (bendType === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + note.bendType = bendType; + tbi++; + break; + } + + switch (p.values!.values[tbi].nodeType) { + case AlphaTexNodeType.Ident: + case AlphaTexNodeType.String: + const bendStyle = AlphaTex1LanguageHandler._parseEnumValue( + importer, + p.values!, + 'bend style', + AlphaTex1EnumMappings.bendStyles, + tbi + ); + if (bendStyle === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + note.bendStyle = bendStyle!; + tbi++; + break; + } + + const points = this._getBendPoints(importer, p, tbi, tag === 'be'); + if (points) { + for (const point of points) { + note.addBendPoint(point); + } + } + + return ApplyNodeResult.Applied; + case 'nh': + note.harmonicType = HarmonicType.Natural; + note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret); + return ApplyNodeResult.Applied; + case 'ah': + note.harmonicType = HarmonicType.Artificial; + note.harmonicValue = AlphaTex1LanguageHandler._harmonicValue(p.values, note.harmonicValue); + return ApplyNodeResult.Applied; + case 'th': + note.harmonicType = HarmonicType.Tap; + note.harmonicValue = AlphaTex1LanguageHandler._harmonicValue(p.values, note.harmonicValue); + return ApplyNodeResult.Applied; + case 'ph': + note.harmonicType = HarmonicType.Pinch; + note.harmonicValue = AlphaTex1LanguageHandler._harmonicValue(p.values, note.harmonicValue); + return ApplyNodeResult.Applied; + case 'sh': + note.harmonicType = HarmonicType.Semi; + note.harmonicValue = AlphaTex1LanguageHandler._harmonicValue(p.values, note.harmonicValue); + return ApplyNodeResult.Applied; + case 'fh': + note.harmonicType = HarmonicType.Feedback; + note.harmonicValue = AlphaTex1LanguageHandler._harmonicValue(p.values, note.harmonicValue); + return ApplyNodeResult.Applied; + case 'tr': + const trillFret = (p.values!.values[0] as AlphaTexNumberLiteral).value; + let trillDuration: Duration = Duration.Sixteenth; + if (p.values!.values.length > 1) { + const trillDurationValue = (p.values!.values[1] as AlphaTexNumberLiteral).value; + switch (trillDurationValue) { + case 16: + trillDuration = Duration.Sixteenth; + break; + case 32: + trillDuration = Duration.ThirtySecond; + break; + case 64: + trillDuration = Duration.SixtyFourth; + break; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected trill duration value '${trillDurationValue}', expected: 16, 32 or 64`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values!.values[1].start, + end: p.values!.values[1].end + }); + return ApplyNodeResult.NotAppliedSemanticError; + } + } + note.trillValue = trillFret + note.stringTuning; + note.trillSpeed = trillDuration; + return ApplyNodeResult.Applied; + case 'v': + note.vibrato = VibratoType.Slight; + return ApplyNodeResult.Applied; + case 'vw': + note.vibrato = VibratoType.Wide; + return ApplyNodeResult.Applied; + case 'sl': + note.slideOutType = SlideOutType.Legato; + return ApplyNodeResult.Applied; + case 'ss': + note.slideOutType = SlideOutType.Shift; + return ApplyNodeResult.Applied; + case 'sib': + note.slideInType = SlideInType.IntoFromBelow; + return ApplyNodeResult.Applied; + case 'sia': + note.slideInType = SlideInType.IntoFromAbove; + return ApplyNodeResult.Applied; + case 'sou': + note.slideOutType = SlideOutType.OutUp; + return ApplyNodeResult.Applied; + case 'sod': + note.slideOutType = SlideOutType.OutDown; + return ApplyNodeResult.Applied; + case 'psd': + note.slideOutType = SlideOutType.PickSlideDown; + return ApplyNodeResult.Applied; + case 'psu': + note.slideOutType = SlideOutType.PickSlideUp; + return ApplyNodeResult.Applied; + case 'h': + note.isHammerPullOrigin = true; + return ApplyNodeResult.Applied; + case 'lht': + note.isLeftHandTapped = true; + return ApplyNodeResult.Applied; + case 'g': + note.isGhost = true; + return ApplyNodeResult.Applied; + case 'ac': + note.accentuated = AccentuationType.Normal; + return ApplyNodeResult.Applied; + case 'hac': + note.accentuated = AccentuationType.Heavy; + return ApplyNodeResult.Applied; + case 'ten': + note.accentuated = AccentuationType.Tenuto; + return ApplyNodeResult.Applied; + case 'pm': + note.isPalmMute = true; + return ApplyNodeResult.Applied; + case 'st': + note.isStaccato = true; + return ApplyNodeResult.Applied; + case 'lr': + note.isLetRing = true; + return ApplyNodeResult.Applied; + case 'x': + note.isDead = true; + return ApplyNodeResult.Applied; + case '-': + case 't': + note.isTieDestination = true; + return ApplyNodeResult.Applied; + case 'lf': + let leftFinger = Fingers.Thumb; + if (p.values && p.values.values.length > 0) { + const customFinger = AlphaTex1LanguageHandler._toFinger(importer, p.values); + if (customFinger === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + leftFinger = customFinger!; + } + note.leftHandFinger = leftFinger; + return ApplyNodeResult.Applied; + case 'rf': + let rightFinger = Fingers.Thumb; + if (p.values && p.values.values.length > 0) { + const customFinger = AlphaTex1LanguageHandler._toFinger(importer, p.values); + if (customFinger === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + rightFinger = customFinger!; + } + note.rightHandFinger = rightFinger; + return ApplyNodeResult.Applied; + case 'acc': + note.accidentalMode = ModelUtils.parseAccidentalMode((p.values!.values[0] as AlphaTexTextNode).text); + return ApplyNodeResult.Applied; + case 'turn': + note.ornament = NoteOrnament.Turn; + return ApplyNodeResult.Applied; + case 'iturn': + note.ornament = NoteOrnament.InvertedTurn; + return ApplyNodeResult.Applied; + case 'umordent': + note.ornament = NoteOrnament.UpperMordent; + return ApplyNodeResult.Applied; + case 'lmordent': + note.ornament = NoteOrnament.LowerMordent; + return ApplyNodeResult.Applied; + case 'string': + note.showStringNumber = true; + return ApplyNodeResult.Applied; + case 'hide': + note.isVisible = false; + return ApplyNodeResult.Applied; + case 'slur': + const slurId = (p.values!.values[0] as AlphaTexTextNode).text; + if (importer.state.slurs.has(slurId)) { + const slurOrigin = importer.state.slurs.get(slurId)!; + slurOrigin.slurDestination = note; + + note.slurOrigin = slurOrigin; + note.isSlurDestination = true; + } else { + importer.state.slurs.set(slurId, note); + } + return ApplyNodeResult.Applied; + default: + // fallback to beat + return this.applyBeatProperty(importer, note.beat, p); + } + + // biome-ignore lint/correctness/noUnreachable: for cross compilation + return ApplyNodeResult.NotAppliedUnrecognizedMarker; + } + + private static _toFinger(importer: IAlphaTexImporter, values: AlphaTexValueList): Fingers | undefined { + const value = (values.values[0] as AlphaTexNumberLiteral).value; + switch (value) { + 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; + default: + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT211, + message: `Value is out of valid range. Allowed range: 1-5, Actual Value: ${value}`, + start: values!.values[0].start, + end: values!.values[0].end, + severity: AlphaTexDiagnosticsSeverity.Error + }); + return undefined; + } + } + + private static _harmonicValue(values: AlphaTexValueList | undefined, harmonicValue: number): number { + if (values) { + harmonicValue = (values!.values[0] as AlphaTexNumberLiteral).value; + } + return harmonicValue; + } + + private _getBendPoints( + importer: IAlphaTexImporter, + p: AlphaTexPropertyNode, + valueStartIndex: number, + exact: boolean + ): BendPoint[] | undefined { + let values = p.values!.values; + let remainingValues = values.length - valueStartIndex; + let errorNode: AlphaTexAstNode = p.values!; + + // unwrap value list + if (remainingValues > 0 && values[valueStartIndex].nodeType === AlphaTexNodeType.Values) { + values = (values[valueStartIndex] as AlphaTexValueList).values; + valueStartIndex = 0; + remainingValues = values.length; + errorNode = values[valueStartIndex] as AlphaTexAstNode; + } + + const valuesPerItem = exact ? 2 : 1; + if (remainingValues % valuesPerItem !== 0) { + const pointCount = Math.ceil(remainingValues / valuesPerItem); + const neededValues = pointCount * valuesPerItem; + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT214, + message: `The '${p.property.text}' effect needs ${valuesPerItem} values per item. With ${pointCount} points, ${neededValues} values are needed, only ${remainingValues} values found.`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: errorNode!.end, + end: errorNode!.end + }); + return undefined; + } + + const points: BendPoint[] = []; + let vi = valueStartIndex; + while (vi < values.length) { + let offset = 0; + let value = 0; + if (exact) { + offset = (values[vi++] as AlphaTexNumberLiteral).value; + value = (values[vi++] as AlphaTexNumberLiteral).value; + } else { + offset = 0; + value = (values[vi++] as AlphaTexNumberLiteral).value; + } + points.push(new BendPoint(offset, value)); + } + + if (points.length > 0) { + if (points.length > 60) { + points.splice(60, points.length - 60); + } + + // set positions + if (exact) { + points.sort((a, b) => { + return a.offset - b.offset; + }); + } else { + const count = points.length; + const step = (BendPoint.MaxPosition / (count - 1)) | 0; + let i: number = 0; + while (i < count) { + points[i].offset = Math.min(BendPoint.MaxPosition, i * step); + i++; + } + } + return points; + } else { + return undefined; + } + } + + // + // string -> enum mappings + + private static _parseEnumValue( + importer: IAlphaTexImporter, + p: AlphaTexValueList, + name: string, + lookup: Map, + valueIndex: number = 0 + ): TValue | undefined { + if (valueIndex >= p.values.length) { + return undefined; + } + + const txt = (p.values[valueIndex] as AlphaTexTextNode).text; + if (lookup.has(txt.toLowerCase())) { + return lookup.get(txt.toLowerCase())!; + } else { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT209, + message: `Unexpected ${name} value '${txt}', expected: ${Array.from(lookup.keys()).join(',')}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: p.values[valueIndex].start, + end: p.values[valueIndex].end + }); + return undefined; + } + } + + // used to lookup some default values. + private static readonly _defaultScore = new Score(); + private static readonly _defaultTrack = new Track(); + + public buildScoreMetaDataNodes(score: Score): AlphaTexMetaDataNode[] { + const nodes: AlphaTexMetaDataNode[] = []; + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'album', score, score.album, ScoreSubElement.Album); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'artist', score, score.artist, ScoreSubElement.Artist); + AlphaTex1LanguageHandler._buildScoreInfoMeta( + nodes, + 'copyright', + score, + score.copyright, + ScoreSubElement.Copyright + ); + AlphaTex1LanguageHandler._buildScoreInfoMeta( + nodes, + 'copyright2', + score, + undefined, + ScoreSubElement.CopyrightSecondLine + ); + AlphaTex1LanguageHandler._buildScoreInfoMeta( + nodes, + 'wordsandmusic', + score, + undefined, + ScoreSubElement.WordsAndMusic, + true + ); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'instructions', score, score.instructions, undefined); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'music', score, score.music, ScoreSubElement.Music); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'notices', score, score.notices, undefined); + AlphaTex1LanguageHandler._buildScoreInfoMeta( + nodes, + 'subtitle', + score, + score.subTitle, + ScoreSubElement.SubTitle + ); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'title', score, score.title, ScoreSubElement.Title); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'words', score, score.words, ScoreSubElement.Words); + AlphaTex1LanguageHandler._buildScoreInfoMeta(nodes, 'tab', score, score.tab, ScoreSubElement.Transcriber); + + if (score.defaultSystemsLayout !== AlphaTex1LanguageHandler._defaultScore.defaultSystemsLayout) { + nodes.push(Atnf.numberMeta('defaultSystemsLayout', score.defaultSystemsLayout)); + } + if (score.systemsLayout.length > 0) { + nodes.push( + Atnf.meta( + 'systemsLayout', + Atnf.values(score.systemsLayout.map(l => Atnf.number(l) as IAlphaTexValueListItem)) + ) + ); + } + + AlphaTex1LanguageHandler._buildStyleSheetMetaData(nodes, score.stylesheet); + + if (nodes.length > 0) { + nodes[0].leadingComments = [ + { + text: 'Score Metadata', + multiLine: false + } + ]; + } + + return nodes; + } + + private static _buildStyleSheetMetaData(nodes: AlphaTexMetaDataNode[], stylesheet: RenderStylesheet) { + const firstStyleSheet = nodes.length; + + if (stylesheet.hideDynamics) { + nodes.push(Atnf.meta('hideDynamics')); + } + if (stylesheet.bracketExtendMode !== AlphaTex1LanguageHandler._defaultScore.stylesheet.bracketExtendMode) { + nodes.push( + Atnf.identMeta( + 'bracketExtendMode', + AlphaTex1EnumMappings.bracketExtendModesReversed.get(stylesheet.bracketExtendMode)! + ) + ); + } + if (stylesheet.useSystemSignSeparator) { + nodes.push(Atnf.meta('useSystemSignSeparator')); + } + if (stylesheet.multiTrackMultiBarRest) { + nodes.push(Atnf.meta('multiBarRest')); + } + if ( + stylesheet.singleTrackTrackNamePolicy !== + AlphaTex1LanguageHandler._defaultScore.stylesheet.singleTrackTrackNamePolicy + ) { + nodes.push( + Atnf.identMeta( + 'singleTrackTrackNamePolicy', + AlphaTex1EnumMappings.trackNamePoliciesReversed.get(stylesheet.singleTrackTrackNamePolicy)! + ) + ); + } + if ( + stylesheet.multiTrackTrackNamePolicy !== + AlphaTex1LanguageHandler._defaultScore.stylesheet.multiTrackTrackNamePolicy + ) { + nodes.push( + Atnf.identMeta( + 'multiTrackTrackNamePolicy', + AlphaTex1EnumMappings.trackNamePoliciesReversed.get(stylesheet.multiTrackTrackNamePolicy)! + ) + ); + } + if ( + stylesheet.firstSystemTrackNameMode !== + AlphaTex1LanguageHandler._defaultScore.stylesheet.firstSystemTrackNameMode + ) { + nodes.push( + Atnf.identMeta( + 'firstSystemTrackNameMode', + AlphaTex1EnumMappings.trackNameModeReversed.get(stylesheet.firstSystemTrackNameMode)! + ) + ); + } + if ( + stylesheet.otherSystemsTrackNameMode !== + AlphaTex1LanguageHandler._defaultScore.stylesheet.otherSystemsTrackNameMode + ) { + nodes.push( + Atnf.identMeta( + 'otherSystemsTrackNameMode', + AlphaTex1EnumMappings.trackNameModeReversed.get(stylesheet.otherSystemsTrackNameMode)! + ) + ); + } + if ( + stylesheet.firstSystemTrackNameOrientation !== + AlphaTex1LanguageHandler._defaultScore.stylesheet.firstSystemTrackNameOrientation + ) { + nodes.push( + Atnf.identMeta( + 'firstSystemTrackNameOrientation', + AlphaTex1EnumMappings.trackNameOrientationsReversed.get(stylesheet.firstSystemTrackNameOrientation)! + ) + ); + } + if ( + stylesheet.otherSystemsTrackNameOrientation !== + AlphaTex1LanguageHandler._defaultScore.stylesheet.otherSystemsTrackNameOrientation + ) { + nodes.push( + Atnf.identMeta( + 'otherSystemsTrackNameOrientation', + AlphaTex1EnumMappings.trackNameOrientationsReversed.get( + stylesheet.otherSystemsTrackNameOrientation + )! + ) + ); + } + + // Unsupported: + // 'globaldisplaychorddiagramsontop', + // 'pertrackchorddiagramsontop', + // 'globaldisplaytuning', + // 'pertrackdisplaytuning', + // 'pertrackchorddiagramsontop', + // 'pertrackmultibarrest', + + if (firstStyleSheet < nodes.length) { + nodes[firstStyleSheet].leadingComments = [ + { + multiLine: false, + text: 'Score Stylesheet' + } + ]; + } + } + + private static _buildScoreInfoMeta( + nodes: AlphaTexMetaDataNode[], + tag: string, + score: Score, + value: string | undefined, + element: ScoreSubElement | undefined, + writeIfEmpty: boolean = false + ): void { + if (value !== undefined && value.length === 0 && !writeIfEmpty) { + return; + } + + const values: IAlphaTexValueListItem[] = []; + + if (value !== undefined) { + values.push(Atnf.string(value)); + } + + if (element !== undefined) { + const style = + score.style && score.style.headerAndFooter.has(element) + ? score.style.headerAndFooter.get(element) + : undefined; + const defaultStyle = ScoreStyle.defaultHeaderAndFooter.has(element) + ? ScoreStyle.defaultHeaderAndFooter.get(element) + : undefined; + if (style && (!defaultStyle || !HeaderFooterStyle.equals(defaultStyle, style))) { + values.push(Atnf.string(style.isVisible === false ? '' : style.template)); + values.push(Atnf.ident(AlphaTex1EnumMappings.textAlignsReversed.get(style.textAlign)!)); + } + } + + // do not write with all defaults + if (value === undefined && values.length === 0) { + return; + } else if (value !== undefined && value.length === 0 && values.length === 1) { + return; + } + + nodes.push(Atnf.meta(tag, Atnf.values(values))); + } + + public buildSyncPointNodes(score: Score): AlphaTexMetaDataNode[] { + const nodes: AlphaTexMetaDataNode[] = []; + + const flatSyncPoints = score.exportFlatSyncPoints(); + for (const p of flatSyncPoints) { + nodes.push( + Atnf.meta( + 'sync', + Atnf.values([ + Atnf.number(p.barIndex), + Atnf.number(p.barOccurence), + Atnf.number(p.millisecondOffset), + p.barPosition > 0 ? Atnf.number(p.barPosition) : undefined + ]) + ) + ); + } + + return nodes; + } + + public buildBarMetaDataNodes( + staff: Staff, + bar: Bar | undefined, + voice: number, + isMultiVoice: boolean + ): AlphaTexMetaDataNode[] { + const nodes: AlphaTexMetaDataNode[] = []; + + AlphaTex1LanguageHandler._buildStructuralMetaDataNodes(bar, staff, nodes, isMultiVoice, voice); + if (!bar) { + return nodes; + } + + if (voice === 0) { + // Master Bar meta on first track + if (staff.index === 0 && staff.track.index === 0) { + AlphaTex1LanguageHandler._buildMasterBarMetaDataNodes(nodes, bar.masterBar); + } + } + + const firstBarMetaIndex = nodes.length; + + if (voice === 0 && bar.index === 0 && staff.index === 0 && staff.track.index === 0) { + nodes.push(Atnf.identMeta('accidentals', 'auto')); + } + + if (bar.index === 0 || bar.clef !== bar.previousBar?.clef) { + nodes.push(Atnf.identMeta('clef', AlphaTex1EnumMappings.clefsReversed.get(bar.clef)!)); + } + + if ((bar.index === 0 && bar.clefOttava !== Ottavia.Regular) || bar.clefOttava !== bar.previousBar?.clefOttava) { + nodes.push(Atnf.identMeta('ottava', AlphaTex1EnumMappings.ottavaReversed.get(bar.clefOttava)!)); + } + + if ((bar.index === 0 && bar.simileMark !== SimileMark.None) || bar.simileMark !== bar.previousBar?.simileMark) { + nodes.push(Atnf.identMeta('simile', AlphaTex1EnumMappings.simileMarksReversed.get(bar.simileMark)!)); + } + + if (bar.displayScale !== 1) { + nodes.push(Atnf.numberMeta('scale', bar.displayScale)); + } + + if (bar.displayWidth > 0) { + nodes.push(Atnf.numberMeta('width', bar.displayWidth)); + } + + // sustainPedals are on beat level + for (const sp of bar.sustainPedals) { + let pedalType = ''; + switch (sp.pedalType) { + case SustainPedalMarkerType.Down: + pedalType = 'spd'; + break; + case SustainPedalMarkerType.Hold: + pedalType = 'sph'; + break; + case SustainPedalMarkerType.Up: + pedalType = 'spu'; + break; + } + if (pedalType) { + nodes.push(Atnf.numberMeta(pedalType, sp.ratioPosition)); + } + } + + if (bar.barLineLeft !== BarLineStyle.Automatic) { + nodes.push(Atnf.identMeta('barLineLeft', AlphaTex1EnumMappings.barLinesReversed.get(bar.barLineLeft)!)); + } + + if (bar.barLineRight !== BarLineStyle.Automatic) { + nodes.push(Atnf.identMeta('barLineRight', AlphaTex1EnumMappings.barLinesReversed.get(bar.barLineRight)!)); + } + + if ( + bar.index === 0 || + bar.keySignature !== bar.previousBar!.keySignature || + bar.keySignatureType !== bar.previousBar!.keySignatureType + ) { + let ks = ''; + if (bar.keySignatureType === KeySignatureType.Minor) { + ks = AlphaTex1EnumMappings.keySignaturesMinorReversed.get(bar.keySignature)!; + } else { + ks = AlphaTex1EnumMappings.keySignaturesMajorReversed.get(bar.keySignature)!; + } + nodes.push(Atnf.identMeta('ks', ks)); + } + + if (firstBarMetaIndex < nodes.length) { + nodes[firstBarMetaIndex].leadingComments = [ + { + multiLine: false, + text: `Bar ${bar.index + 1} Metadata` + } + ]; + } + + return nodes; + } + private static _buildStaffMetaDataNodes(nodes: AlphaTexMetaDataNode[], staff: Staff) { + const firstStaffMetaIndex = nodes.length; + + if (staff.capo !== 0) { + nodes.push(Atnf.numberMeta('capo', staff.capo)); + } + if (staff.isPercussion) { + nodes.push(Atnf.identMeta('articulation', 'defaults')); + } else if (staff.isStringed) { + const tuning = Atnf.meta( + 'tuning', + Atnf.values( + staff.stringTuning.tunings.map( + t => Atnf.ident(Tuning.getTextForTuning(t, true)) as IAlphaTexValueListItem + ) + ) + ); + nodes.push(tuning); + + if ( + staff.track.score.stylesheet.perTrackDisplayTuning && + staff.track.score.stylesheet.perTrackDisplayTuning!.has(staff.track.index) + ) { + tuning.properties = Atnf.props([['hide', undefined]]); + } + + if (staff.stringTuning.name.length > 0) { + tuning.properties ??= Atnf.props([]); + Atnf.prop(tuning.properties!.properties, 'label', Atnf.stringValue(staff.stringTuning.name)); + } + } + + if (staff.transpositionPitch !== 0) { + nodes.push(Atnf.numberMeta('transpose', -staff.transpositionPitch)); + } + + const defaultTransposition = ModelUtils.displayTranspositionPitches.has(staff.track.playbackInfo.program) + ? ModelUtils.displayTranspositionPitches.get(staff.track.playbackInfo.program)! + : 0; + if (staff.displayTranspositionPitch !== defaultTransposition) { + nodes.push(Atnf.numberMeta('displaytranspose', -staff.displayTranspositionPitch)); + } + + if (staff.chords != null) { + for (const [_, chord] of staff.chords!) { + nodes.push(AlphaTex1LanguageHandler._buildChordNode(chord)); + } + } + + if (firstStaffMetaIndex < nodes.length) { + nodes[firstStaffMetaIndex].leadingComments = [ + { + multiLine: false, + text: `Staff ${staff.index + 1} Metadata` + } + ]; + } + } + + private static _buildChordNode(chord: Chord): AlphaTexMetaDataNode { + const chordNode = Atnf.meta( + 'chord', + Atnf.stringValue(chord.name), + Atnf.props([ + chord.firstFret >= 0 ? ['firstfret', Atnf.numberValue(chord.firstFret)] : undefined, + ['showdiagram', Atnf.identValue(chord.showDiagram ? 'true' : 'false')], + ['showfingering', Atnf.identValue(chord.showFingering ? 'true' : 'false')], + ['showname', Atnf.identValue(chord.showName ? 'true' : 'false')], + chord.barreFrets.length > 0 + ? ['barre', Atnf.values(chord.barreFrets.map(f => Atnf.number(f) as IAlphaTexValueListItem))] + : undefined + ]) + ); + chordNode.propertiesBeforeValues = true; + + for (let i = 0; i < chord.staff.tuning.length; i++) { + if (i < chord.strings.length && chord.strings[i] >= 0) { + chordNode.values!.values.push(Atnf.number(chord.strings[i])); + } else { + chordNode.values!.values.push(Atnf.ident('x')); + } + } + + return chordNode; + } + + private static _buildMasterBarMetaDataNodes(nodes: AlphaTexMetaDataNode[], masterBar: MasterBar) { + const firstMetaIndex = nodes.length; + + if (masterBar.alternateEndings !== 0) { + nodes.push( + Atnf.meta( + 'ae', + Atnf.values( + ModelUtils.getAlternateEndingsList(masterBar.alternateEndings).map( + i => Atnf.number(i + 1) as IAlphaTexValueListItem + ) + ) + ) + ); + } + + if (masterBar.isRepeatStart) { + nodes.push(Atnf.meta('ro')); + } + + if (masterBar.isRepeatEnd) { + nodes.push(Atnf.numberMeta('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) { + nodes.push(Atnf.identMeta('ts', 'common')); + } else { + nodes.push( + Atnf.meta( + 'ts', + Atnf.values([ + Atnf.number(masterBar.timeSignatureNumerator), + Atnf.number(masterBar.timeSignatureDenominator) + ]) + ) + ); + } + } + + if ( + (masterBar.index > 0 && masterBar.tripletFeel !== masterBar.previousMasterBar?.tripletFeel) || + (masterBar.index === 0 && masterBar.tripletFeel !== TripletFeel.NoTripletFeel) + ) { + nodes.push(Atnf.identMeta('tf', AlphaTex1EnumMappings.tripletFeelsReversed.get(masterBar.tripletFeel)!)); + } + + if (masterBar.isFreeTime) { + nodes.push(Atnf.meta('ft')); + } + + if (masterBar.section != null) { + nodes.push( + Atnf.meta( + 'section', + Atnf.values([Atnf.string(masterBar.section.marker), Atnf.string(masterBar.section.text)]) + ) + ); + } + + if (masterBar.isAnacrusis) { + nodes.push(Atnf.meta('ac')); + } + + if (masterBar.displayScale !== 1) { + nodes.push(Atnf.numberMeta('scale', masterBar.displayScale)); + } + + if (masterBar.displayWidth > 0) { + nodes.push(Atnf.numberMeta('width', masterBar.displayWidth)); + } + + if (masterBar.directions) { + for (const d of masterBar.directions!) { + nodes.push(Atnf.identMeta('jump', AlphaTex1EnumMappings.directionsReversed.get(d)!)); + } + } + + for (const a of masterBar.tempoAutomations) { + const tempo = Atnf.meta( + 'tempo', + Atnf.values([ + Atnf.number(a.value), + a.text ? Atnf.string(a.text) : undefined, + a.ratioPosition > 0 ? Atnf.number(a.ratioPosition) : undefined, + !a.isVisible ? Atnf.ident('hide') : undefined + ]) + ); + if (tempo.values!.values.length === 1) { + tempo.values!.openParenthesis = undefined; + tempo.values!.closeParenthesis = undefined; + } + nodes.push(tempo); + } + + if (firstMetaIndex < nodes.length) { + nodes[firstMetaIndex].leadingComments = [ + { + multiLine: false, + text: `Masterbar ${masterBar.index + 1} Metadata` + } + ]; + } + } + + private static _buildStructuralMetaDataNodes( + bar: Bar | undefined, + staff: Staff, + nodes: AlphaTexMetaDataNode[], + isMultiVoice: boolean, + voice: number + ) { + if (bar === undefined || bar.index === 0) { + if (voice === 0) { + if (staff.index === 0) { + nodes.push(AlphaTex1LanguageHandler._buildNewTrackNode(staff.track)); + } + nodes.push(AlphaTex1LanguageHandler._buildNewStaffNode(staff)); + AlphaTex1LanguageHandler._buildStaffMetaDataNodes(nodes, staff); + } + + if (isMultiVoice) { + const voiceNode = Atnf.meta('voice'); + voiceNode.trailingComments = [ + { + multiLine: true, + text: `Voice ${voice + 1}` + } + ]; + nodes.push(voiceNode); + } + } + } + + private static _buildNewStaffNode(staff: Staff): AlphaTexMetaDataNode { + const node = Atnf.meta( + 'staff', + undefined, + Atnf.props([ + staff.showStandardNotation + ? [ + 'score', + Atnf.values([ + staff.standardNotationLineCount !== Staff.DefaultStandardNotationLineCount + ? Atnf.number(staff.standardNotationLineCount) + : undefined + ]) + ] + : undefined, + staff.showTablature ? (['tabs', undefined] as [string, AlphaTexValueList | undefined]) : undefined, + staff.showSlash ? (['slash', undefined] as [string, AlphaTexValueList | undefined]) : undefined, + staff.showNumbered ? (['numbered', undefined] as [string, AlphaTexValueList | undefined]) : undefined + ]) + ); + + if (node.properties && node.properties.properties.length > 0) { + node.properties.properties[0]!.leadingComments = [ + { + multiLine: false, + text: 'Staff Properties' + } + ]; + } + + return node; + } + + private static _buildNewTrackNode(track: Track): AlphaTexMetaDataNode { + const node = Atnf.meta( + 'track', + Atnf.values([ + Atnf.string(track.name), + track.shortName.length > 0 ? Atnf.string(track.shortName) : undefined + ]), + Atnf.props([ + track.color.rgba !== AlphaTex1LanguageHandler._defaultTrack.color.rgba + ? ['color', Atnf.stringValue(track.color.rgba)] + : undefined, + track.defaultSystemsLayout !== AlphaTex1LanguageHandler._defaultTrack.defaultSystemsLayout + ? ['defaultSystemsLayout', Atnf.numberValue(track.defaultSystemsLayout)] + : undefined, + track.systemsLayout.length + ? [ + 'systemsLayout', + Atnf.values(track.systemsLayout.map(d => Atnf.number(d) as IAlphaTexValueListItem)) + ] + : undefined, + ['volume', Atnf.numberValue(track.playbackInfo.volume)], + ['balance', Atnf.numberValue(track.playbackInfo.balance)], + track.playbackInfo.isMute + ? (['mute', undefined] as [string, AlphaTexValueList | undefined]) + : undefined, + track.playbackInfo.isSolo + ? (['solo', undefined] as [string, AlphaTexValueList | undefined]) + : undefined, + track.score.stylesheet.perTrackMultiBarRest && + track.score.stylesheet.perTrackMultiBarRest!.has(track.index) + ? (['multiBarRest', undefined] as [string, AlphaTexValueList | undefined]) + : undefined, + [ + 'instrument', + Atnf.identValue(track.isPercussion ? 'percussion' : GeneralMidi.getName(track.playbackInfo.program)) + ], + track.playbackInfo.bank > 0 ? ['bank', Atnf.numberValue(track.playbackInfo.bank)] : undefined + ]) + ); + + if (node.properties && node.properties.properties.length > 0) { + node.properties.properties[0]!.leadingComments = [ + { + multiLine: false, + text: 'Track Properties' + } + ]; + } + + return node; + } + + public buildNoteEffects(note: Note): AlphaTexPropertyNode[] { + const properties: AlphaTexPropertyNode[] = []; + + if (note.hasBend) { + const beValue = Atnf.values( + [ + Atnf.ident(AlphaTex1EnumMappings.bendTypesReversed.get(note.bendType)!), + note.bendStyle !== BendStyle.Default + ? Atnf.ident(AlphaTex1EnumMappings.bendStylesReversed.get(note.bendStyle)!) + : undefined + ], + true + )!; + for (const p of note.bendPoints!) { + beValue.values.push(Atnf.number(p.offset)); + beValue.values.push(Atnf.number(p.value)); + } + + Atnf.prop(properties, 'be', beValue); + } + + let harmonicType = ''; + switch (note.harmonicType) { + case HarmonicType.Natural: + Atnf.prop(properties, 'nh'); + break; + case HarmonicType.Artificial: + harmonicType = 'ah'; + break; + case HarmonicType.Pinch: + harmonicType = 'ph'; + break; + case HarmonicType.Tap: + harmonicType = 'th'; + break; + case HarmonicType.Semi: + harmonicType = 'sh'; + break; + case HarmonicType.Feedback: + harmonicType = 'fh'; + break; + } + if (harmonicType) { + Atnf.prop(properties, harmonicType, Atnf.numberValue(note.harmonicValue)); + } + + if (note.showStringNumber) { + Atnf.prop(properties, 'string'); + } + + if (note.isTrill) { + Atnf.prop( + properties, + 'tr', + Atnf.values([Atnf.number(note.trillFret), Atnf.number(note.trillSpeed as number)]) + ); + } + + switch (note.vibrato) { + case VibratoType.Slight: + Atnf.prop(properties, 'v'); + break; + case VibratoType.Wide: + Atnf.prop(properties, 'vw'); + break; + } + + switch (note.slideInType) { + case SlideInType.IntoFromBelow: + Atnf.prop(properties, 'sib'); + break; + case SlideInType.IntoFromAbove: + Atnf.prop(properties, 'sia'); + break; + } + + switch (note.slideOutType) { + case SlideOutType.Shift: + Atnf.prop(properties, 'ss'); + break; + case SlideOutType.Legato: + Atnf.prop(properties, 'sl'); + break; + case SlideOutType.OutUp: + Atnf.prop(properties, 'sou'); + break; + case SlideOutType.OutDown: + Atnf.prop(properties, 'sod'); + break; + case SlideOutType.PickSlideDown: + Atnf.prop(properties, 'psd'); + break; + case SlideOutType.PickSlideUp: + Atnf.prop(properties, 'psu'); + break; + } + + if (note.isHammerPullOrigin) { + Atnf.prop(properties, 'h'); + } + + if (note.isLeftHandTapped) { + Atnf.prop(properties, 'lht'); + } + + if (note.isGhost) { + Atnf.prop(properties, 'g'); + } + + switch (note.accentuated) { + case AccentuationType.Normal: + Atnf.prop(properties, 'ac'); + break; + case AccentuationType.Heavy: + Atnf.prop(properties, 'hac'); + break; + case AccentuationType.Tenuto: + Atnf.prop(properties, 'ten'); + break; + } + + if (note.isPalmMute) { + Atnf.prop(properties, 'pm'); + } + + if (note.isStaccato) { + Atnf.prop(properties, 'st'); + } + + if (note.isLetRing) { + Atnf.prop(properties, 'lr'); + } + + if (note.isDead) { + Atnf.prop(properties, 'x'); + } + + if (note.isTieDestination) { + Atnf.prop(properties, 't'); + } + if (note.leftHandFinger >= Fingers.Thumb) { + Atnf.prop(properties, 'lf', Atnf.numberValue((note.leftHandFinger as number) + 1)); + } + if (note.rightHandFinger >= Fingers.Thumb) { + Atnf.prop(properties, 'rf', Atnf.numberValue((note.rightHandFinger as number) + 1)); + } + + if (!note.isVisible) { + Atnf.prop(properties, 'hide'); + } + + if (note.isSlurOrigin) { + const slurId = `s${note.id}`; + Atnf.prop(properties, 'slur', Atnf.identValue(slurId)); + } + + if (note.isSlurDestination) { + const slurId = `s${note.slurOrigin!.id}`; + Atnf.prop(properties, 'slur', Atnf.identValue(slurId)); + } + + if (note.accidentalMode !== NoteAccidentalMode.Default) { + Atnf.prop( + properties, + 'acc', + Atnf.identValue(ModelUtils.reverseAccidentalModeMapping.get(note.accidentalMode)!) + ); + } + + switch (note.ornament) { + case NoteOrnament.InvertedTurn: + Atnf.prop(properties, 'iturn'); + break; + case NoteOrnament.Turn: + Atnf.prop(properties, 'turn'); + break; + case NoteOrnament.UpperMordent: + Atnf.prop(properties, 'umordent'); + break; + case NoteOrnament.LowerMordent: + Atnf.prop(properties, 'lmordent'); + break; + } + + return properties; + } + + public buildBeatEffects(beat: Beat): AlphaTexPropertyNode[] { + const properties: AlphaTexPropertyNode[] = []; + + switch (beat.fade) { + case FadeType.FadeIn: + Atnf.prop(properties, 'f'); + break; + case FadeType.FadeOut: + Atnf.prop(properties, 'fo'); + break; + case FadeType.VolumeSwell: + Atnf.prop(properties, 'vs'); + break; + } + + if (beat.vibrato === VibratoType.Slight) { + Atnf.prop(properties, 'v'); + } else if (beat.vibrato === VibratoType.Wide) { + Atnf.prop(properties, 'vw'); + } + + if (beat.slap) { + Atnf.prop(properties, 's'); + } + + if (beat.pop) { + Atnf.prop(properties, 'p'); + } + + if (beat.tap) { + Atnf.prop(properties, 'tt'); + } + + if (beat.dots >= 2) { + Atnf.prop(properties, 'dd'); + } else if (beat.dots > 0) { + Atnf.prop(properties, 'd'); + } + + if (beat.pickStroke === PickStroke.Up) { + Atnf.prop(properties, 'su'); + } else if (beat.pickStroke === PickStroke.Down) { + Atnf.prop(properties, 'sd'); + } + + if (beat.hasTuplet) { + Atnf.prop( + properties, + 'tu', + Atnf.values([Atnf.number(beat.tupletNumerator), Atnf.number(beat.tupletDenominator)]) + ); + } + + if (beat.hasWhammyBar) { + const tbeValues = Atnf.values( + [ + Atnf.ident(AlphaTex1EnumMappings.whammyTypesReversed.get(beat.whammyBarType)!), + Atnf.ident(AlphaTex1EnumMappings.bendStylesReversed.get(beat.whammyStyle)!) + ], + true + )!; + for (const p of beat.whammyBarPoints!) { + tbeValues.values.push(Atnf.number(p.offset)); + tbeValues.values.push(Atnf.number(p.value)); + } + + Atnf.prop(properties, 'tbe', tbeValues); + } + + let brushType = ''; + switch (beat.brushType) { + case BrushType.BrushUp: + brushType = 'bu'; + + break; + case BrushType.BrushDown: + brushType = 'bd'; + break; + case BrushType.ArpeggioUp: + brushType = 'au'; + break; + case BrushType.ArpeggioDown: + brushType = 'ad'; + break; + } + if (brushType) { + Atnf.prop(properties, brushType, Atnf.numberValue(beat.brushDuration)); + } + + if (beat.chord != null) { + Atnf.prop(properties, 'ch', Atnf.stringValue(beat.chord.name)); + } + + if (beat.ottava !== Ottavia.Regular) { + Atnf.prop(properties, 'ot', Atnf.identValue(AlphaTex1EnumMappings.ottavaReversed.get(beat.ottava)!)); + } + + if (beat.hasRasgueado) { + Atnf.prop( + properties, + 'rasg', + Atnf.identValue(AlphaTex1EnumMappings.rasgueadoPatternsReversed.get(beat.rasgueado)!) + ); + } + + if (beat.text != null) { + Atnf.prop(properties, 'txt', Atnf.stringValue(beat.text)); + } + + if (beat.lyrics != null && beat.lyrics!.length > 0) { + if (beat.lyrics.length > 1) { + for (let i = 0; i < beat.lyrics.length; i++) { + Atnf.prop(properties, 'lyrics', Atnf.values([Atnf.number(i), Atnf.string(beat.lyrics[i])])); + } + } else { + Atnf.prop(properties, 'lyrics', Atnf.stringValue(beat.lyrics[0])); + } + } + + if (beat.graceType !== GraceType.None) { + Atnf.prop( + properties, + 'gr', + beat.graceType === GraceType.BeforeBeat + ? undefined + : Atnf.identValue(AlphaTex1EnumMappings.graceTypesReversed.get(beat.graceType)!) + ); + } + + if (beat.isTremolo) { + Atnf.prop(properties, 'tp', Atnf.numberValue(beat.tremoloSpeed! as number)); + } + + switch (beat.crescendo) { + case CrescendoType.Crescendo: + Atnf.prop(properties, 'cre'); + break; + case CrescendoType.Decrescendo: + Atnf.prop(properties, 'dec'); + break; + } + + if ((beat.voice.bar.index === 0 && beat.index === 0) || beat.dynamics !== beat.previousBeat?.dynamics) { + Atnf.prop(properties, 'dy', Atnf.identValue(AlphaTex1EnumMappings.dynamicsReversed.get(beat.dynamics)!)); + } + + const fermata = beat.fermata; + if (fermata != null) { + Atnf.prop( + properties, + 'fermata', + Atnf.values([ + Atnf.ident(AlphaTex1EnumMappings.fermataTypesReversed.get(beat.fermata!.type)!), + Atnf.number(beat.fermata!.length) + ]) + ); + } + + if (beat.isLegatoOrigin) { + Atnf.prop(properties, 'legatoorigin'); + } + + for (const automation of beat.automations) { + switch (automation.type) { + case AutomationType.Tempo: + Atnf.prop( + properties, + 'tempo', + Atnf.values([ + Atnf.number(automation.value), + automation.text.length === 0 ? undefined : Atnf.string(automation.text) + ]) + ); + break; + case AutomationType.Volume: + Atnf.prop(properties, 'volume', Atnf.numberValue(automation.value)); + break; + case AutomationType.Instrument: + if (!beat.voice.bar.staff.isPercussion) { + Atnf.prop(properties, 'instrument', Atnf.identValue(GeneralMidi.getName(automation.value))); + } + break; + case AutomationType.Balance: + Atnf.prop(properties, 'balance', Atnf.numberValue(automation.value)); + break; + } + } + + switch (beat.wahPedal) { + case WahPedal.Open: + Atnf.prop(properties, 'waho'); + break; + case WahPedal.Closed: + Atnf.prop(properties, 'wahc'); + break; + } + + if (beat.isBarre) { + Atnf.prop( + properties, + 'barre', + Atnf.values([ + Atnf.number(beat.barreFret), + Atnf.ident(AlphaTex1EnumMappings.barreShapesReversed.get(beat.barreShape)!) + ]) + ); + } + + if (beat.slashed) { + Atnf.prop(properties, 'slashed'); + } + + if (beat.deadSlapped) { + Atnf.prop(properties, 'ds'); + } + + switch (beat.golpe) { + case GolpeType.Thumb: + Atnf.prop(properties, 'glpt'); + break; + case GolpeType.Finger: + Atnf.prop(properties, 'glpf'); + break; + } + + if (beat.invertBeamDirection) { + Atnf.prop(properties, 'beam', Atnf.identValue('invert')); + } else if (beat.preferredBeamDirection !== null) { + Atnf.prop(properties, 'beam', Atnf.identValue(BeamDirection[beat.preferredBeamDirection!])); + } + + let beamingModeValue = ''; + switch (beat.beamingMode) { + case BeatBeamingMode.ForceSplitToNext: + beamingModeValue = 'split'; + break; + case BeatBeamingMode.ForceMergeWithNext: + beamingModeValue = 'merge'; + break; + case BeatBeamingMode.ForceSplitOnSecondaryToNext: + beamingModeValue = 'splitsecondary'; + break; + } + + if (beamingModeValue) { + Atnf.prop(properties, 'beam', Atnf.identValue(beamingModeValue)); + } + + if (beat.showTimer) { + Atnf.prop(properties, 'timer'); + } + + return properties; + } +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts new file mode 100644 index 000000000..af0ffc94c --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1MetaDataReader.ts @@ -0,0 +1,332 @@ +import { + AlphaTex1LanguageDefinitions, + type ValueListParseTypesExtended, + ValueListParseTypesMode +} from '@src/importer/alphaTex/AlphaTex1LanguageDefinitions'; +import { + type AlphaTexAstNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataTagNode, + AlphaTexNodeType, + type AlphaTexNumberLiteral, + type AlphaTexPropertyNode, + type AlphaTexStringLiteral, + type AlphaTexValueList, + type IAlphaTexValueListItem +} from '@src/importer/alphaTex/AlphaTexAst'; +import type { AlphaTexParser } from '@src/importer/alphaTex/AlphaTexParser'; +import { + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity, + AlphaTexParserAbort +} from '@src/importer/alphaTex/AlphaTexShared'; +import { Atnf } from '@src/importer/alphaTex/ATNF'; +import type { IAlphaTexMetaDataReader } from '@src/importer/alphaTex/IAlphaTexMetaDataReader'; + +/** + * @internal + */ +export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader { + public static readonly instance = new AlphaTex1MetaDataReader(); + + public readMetaDataValues( + parser: AlphaTexParser, + metaData: AlphaTexMetaDataTagNode + ): AlphaTexValueList | undefined { + const tag = metaData.tag.text.toLowerCase(); + for (const lookup of AlphaTex1LanguageDefinitions.metaDataValueListTypes) { + if (lookup.has(tag)) { + const types = lookup.get(tag); + if (types) { + return this._readTypedValueList(parser, types); + } else { + return undefined; + } + } + } + + parser.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT204, + message: `Unrecognized metadata '${metaData.tag.text}'.`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: metaData.start, + end: metaData.end + }); + throw new AlphaTexParserAbort(); + } + + public readMetaDataPropertyValues( + parser: AlphaTexParser, + metaData: AlphaTexMetaDataTagNode, + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined { + switch (metaData.tag.text.toLowerCase()) { + case 'track': + return this._readPropertyValues( + parser, + [AlphaTex1LanguageDefinitions.trackPropertyValueListTypes], + property + ); + case 'chord': + return this._readPropertyValues( + parser, + [AlphaTex1LanguageDefinitions.chordPropertyValueListTypes], + property + ); + case 'tuning': + return this._readPropertyValues( + parser, + [AlphaTex1LanguageDefinitions.tuningPropertyValueListTypes], + property + ); + case 'staff': + return this._readPropertyValues( + parser, + [AlphaTex1LanguageDefinitions.staffPropertyValueListTypes], + property + ); + default: + return undefined; + } + } + + public readBeatPropertyValues( + parser: AlphaTexParser, + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined { + return this._readPropertyValues(parser, [AlphaTex1LanguageDefinitions.beatPropertyValueListTypes], property); + } + + public readDurationChangePropertyValues( + parser: AlphaTexParser, + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined { + return this._readPropertyValues( + parser, + [AlphaTex1LanguageDefinitions.beatDurationPropertyValueListTypes], + property + ); + } + + public readNotePropertyValues( + parser: AlphaTexParser, + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined { + return this._readPropertyValues( + parser, + [ + AlphaTex1LanguageDefinitions.notePropertyValueListTypes, + AlphaTex1LanguageDefinitions.beatPropertyValueListTypes + ], + property + ); + } + + private _readPropertyValues( + parser: AlphaTexParser, + lookups: Map[], + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined { + const tag = property.property.text.toLowerCase(); + const endOfProperty = new Set([AlphaTexNodeType.Ident, AlphaTexNodeType.RBrace]); + for (const lookup of lookups) { + if (lookup.has(tag)) { + const types = lookup.get(tag); + if (types) { + return this._readTypedValueList(parser, types, endOfProperty); + } else { + return undefined; + } + } + } + parser.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT205, + message: `Unrecognized property '${property.property.text}'.`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: property.property.start, + end: property.property.end + }); + throw new AlphaTexParserAbort(); + } + + private _readTypedValueList( + parser: AlphaTexParser, + expectedValues: ValueListParseTypesExtended[], + endOfListTypes?: Set + ): AlphaTexValueList | undefined { + const values: IAlphaTexValueListItem[] = []; + const valueListStart = parser.lexer.peekToken()?.start; + let error = false; + let parseRemaining = endOfListTypes !== undefined; + let i = 0; + while (i < expectedValues.length) { + const expected = expectedValues[i]; + + const value = parser.lexer.peekToken(); + + // prevent parsing of special float values which could overlap + // with stringed notes + if (expected.parseMode === ValueListParseTypesMode.OptionalAsFloatInValueList) { + parseRemaining = false; + break; + } + + // NOTE: The parser already handles parenthesized value lists, we only need to handle this + // parse mode in the validation. + + if ( + value && + (expected.expectedTypes.has(value.nodeType) || + // value lists start with a parenthesis open token + AlphaTex1MetaDataReader._isValueListMatch(value, expected)) && + this._handleTypeValueListItem(parser, values, value, expected) + ) { + switch (expected.parseMode) { + case ValueListParseTypesMode.OptionalAndStop: + // stop reading values + i = expectedValues.length; + break; + case ValueListParseTypesMode.ValueListWithoutParenthesis: + // stay on current element + break; + default: + // advance to next item + i++; + break; + } + } else { + switch (expected.parseMode) { + // end of value list + case ValueListParseTypesMode.ValueListWithoutParenthesis: + i++; + break; + case ValueListParseTypesMode.Required: + case ValueListParseTypesMode.RequiredAsFloat: + parser.unexpectedToken(value, Array.from(expected.expectedTypes), true); + error = true; + break; + + case ValueListParseTypesMode.Optional: + case ValueListParseTypesMode.OptionalAsFloat: + case ValueListParseTypesMode.OptionalAndStop: + // optional not matched -> try next + i++; + break; + + case ValueListParseTypesMode.RequiredAsValueList: + // optional -> not matched, value listed ended, check next + i++; + break; + } + } + } + + if (error) { + throw new AlphaTexParserAbort(); + } + + // read remaining values user might have supplied + if (parseRemaining) { + let remaining = parser.lexer.peekToken(); + while (remaining && !endOfListTypes!.has(remaining.nodeType)) { + if (this._handleTypeValueListItem(parser, values, remaining, undefined)) { + remaining = parser.lexer.peekToken(); + } else { + remaining = undefined; + } + } + } + + if (values.length === 0) { + return undefined; + } + + const valueList = Atnf.values(values, false)!; + valueList.start = valueListStart; + valueList.end = parser.lexer.previousTokenEndLocation(); + valueList.validated = true; + + return valueList; + } + + private _handleTypeValueListItem( + parser: AlphaTexParser, + valueList: IAlphaTexValueListItem[], + value: AlphaTexAstNode, + expected: ValueListParseTypesExtended | undefined + ): boolean { + switch (value.nodeType) { + case AlphaTexNodeType.Ident: + const identifier = value as AlphaTexIdentifier; + if (expected?.allowedValues) { + if (expected.allowedValues.has(identifier.text.toLowerCase())) { + valueList.push(identifier); + parser.lexer.advance(); + } else { + return false; + } + } else if (expected?.reservedIdentifiers) { + if (!expected.reservedIdentifiers.has(identifier.text.toLowerCase())) { + valueList.push(identifier); + parser.lexer.advance(); + } else { + return false; + } + } else { + valueList.push(identifier); + parser.lexer.advance(); + } + + return true; + case AlphaTexNodeType.String: + const str = value as AlphaTexStringLiteral; + + if (expected?.allowedValues) { + if (expected.allowedValues.has(str.text.toLowerCase())) { + valueList.push(str); + parser.lexer.advance(); + } + } else { + valueList.push(str); + parser.lexer.advance(); + } + + return true; + case AlphaTexNodeType.Number: + const parseMode = expected?.parseMode ?? ValueListParseTypesMode.Optional; + switch (parseMode) { + case ValueListParseTypesMode.RequiredAsFloat: + case ValueListParseTypesMode.OptionalAsFloat: + valueList.push(parser.lexer.extendToFloat(value as AlphaTexNumberLiteral)); + parser.lexer.advance(); + break; + default: + valueList.push(value as AlphaTexNumberLiteral); + parser.lexer.advance(); + break; + } + return true; + case AlphaTexNodeType.LParen: + const nestedList = parser.valueList(); + if (nestedList) { + for (const v of nestedList.values) { + valueList.push(v); + } + } + return true; + } + return false; + } + + private static _isValueListMatch(value: AlphaTexAstNode, expected: ValueListParseTypesExtended): boolean { + if (value.nodeType !== AlphaTexNodeType.LParen) { + return false; + } + + return ( + expected.expectedTypes.has(AlphaTexNodeType.Values) || + expected.parseMode === ValueListParseTypesMode.ValueListWithoutParenthesis || + expected.parseMode === ValueListParseTypesMode.RequiredAsValueList + ); + } +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts new file mode 100644 index 000000000..176b7eedf --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexAst.ts @@ -0,0 +1,548 @@ +/** + * All node types for the alphaTex syntax tree. + * @public + */ +export enum AlphaTexNodeType { + // Tokens + Dot, + Backslash, + DoubleBackslash, + Pipe, + LBrace, + RBrace, + LParen, + RParen, + Colon, + Asterisk, + + // General Nodes + Ident, + Tag, + Meta, + Values, + Props, + Prop, + Number, + String, + + // Semantic Nodes + Score, + Bar, + Beat, + Duration, + NoteList, + Note +} + +// +// The general AST nodes describing the low level building blocks +// allowing to describe further semantic structures. + +/** + * Maps an AST node into its respective source code location. + * @record + * @public + */ +export interface AlphaTexAstNodeLocation { + /** + * The 1-based line index within the source code. + */ + line: number; + /** + * The 1-based column index within the source code. + */ + col: number; + /** + * The 0-based codepoint offset within the source code. + */ + offset: number; +} + +/** + * The base type for all alphaTex AST nodes + * @public + */ +export interface IAlphaTexAstNode { + /** + * The type of the node. + */ + nodeType: AlphaTexNodeType; + /** + * The start of this node when parsed from an input source file. + */ + start?: AlphaTexAstNodeLocation; + /** + * The end (inclusive) of this node when parsed from an input source file. + */ + end?: AlphaTexAstNodeLocation; + + /** + * The comments preceeding this node. + */ + leadingComments?: AlphaTexComment[]; + + /** + * The comments after this node (if starting on the same line). + */ + trailingComments?: AlphaTexComment[]; +} + +/** + * The base type for all alphaTex AST nodes + * @record + * @public + */ +export interface AlphaTexAstNode extends IAlphaTexAstNode {} + +/** + * A comment attached to a node. + * @record + * @public + */ +export interface AlphaTexComment { + /** + * The start of this node when parsed from an input source file. + */ + start?: AlphaTexAstNodeLocation; + /** + * The end (inclusive) of this node when parsed from an input source file. + */ + end?: AlphaTexAstNodeLocation; + /** + * Whether the comment is a multiline comment or a single line comment + */ + multiLine: boolean; + /** + * The comment text excluding the comment start/end markers. + */ + text: string; +} + +/** + * A node describing a single token like dots or colons. + * @record + * @public + */ +export interface AlphaTexTokenNode extends AlphaTexAstNode {} + +/** + * @record + * @public + */ +export interface AlphaTexDotTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.Dot; +} + +/** + * @record + * @public + */ +export interface AlphaTexBackSlashTokenNode extends AlphaTexTokenNode, IAlphaTexMetaDataTagPrefixNode { + nodeType: AlphaTexNodeType.Backslash; +} + +/** + * @record + * @public + */ +export interface AlphaTexDoubleBackSlashTokenNode extends AlphaTexTokenNode, IAlphaTexMetaDataTagPrefixNode { + nodeType: AlphaTexNodeType.DoubleBackslash; +} + +/** + * @record + * @public + */ +export interface AlphaTexPipeTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.Pipe; +} + +/** + * @record + * @public + */ +export interface AlphaTexBraceOpenTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.LBrace; +} + +/** + * @record + * @public + */ +export interface AlphaTexBraceCloseTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.RBrace; +} + +/** + * @record + * @public + */ +export interface AlphaTexParenthesisOpenTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.LParen; +} + +/** + * @record + * @public + */ +export interface AlphaTexParenthesisCloseTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.RParen; +} + +/** + * @record + * @public + */ +export interface AlphaTexColonTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.Colon; +} + +/** + * @record + * @public + */ +export interface AlphaTexAsteriskTokenNode extends AlphaTexTokenNode { + nodeType: AlphaTexNodeType.Asterisk; +} + +/** + * A number literal within alphaTex. Can be a integer or floating point number. + * @record + * @public + */ +export interface AlphaTexNumberLiteral extends AlphaTexAstNode, IAlphaTexValueListItem, IAlphaTexNoteValueNode { + nodeType: AlphaTexNodeType.Number; + /** + * The numeric value described by this literal. + */ + value: number; +} + +/** + * A string literal within alphaTex. + * @record + * @public + */ +export interface AlphaTexStringLiteral extends AlphaTexTextNode, IAlphaTexValueListItem, IAlphaTexNoteValueNode { + nodeType: AlphaTexNodeType.String; +} + +/** + * Defines the possible types for values in a {@link AlphaTexValueList} + * @public + */ +export interface IAlphaTexValueListItem extends IAlphaTexAstNode {} + +/** + * A node holding multiple values optionally grouped by parenthesis. + * Whether parenthesis are needed depends on the context. + * Used in contexts like bend effects `3.3{b (0 4)}`. + * @record + * @public + */ +export interface AlphaTexValueList extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Values; + /** + * The open parenthesis token grouping the values. + */ + openParenthesis?: AlphaTexParenthesisOpenTokenNode; + /** + * The list of values. + */ + values: IAlphaTexValueListItem[]; + /** + * The close parenthesis token grouping the values. + */ + closeParenthesis?: AlphaTexParenthesisCloseTokenNode; + + /** + * Whether the types of this list have been validated already to match + * the types expected of the property or metadata. + * @internal + */ + validated?: boolean; +} + +/** + * A metadata tag with optional values and optional properties like: + * `\track "Name" {color "#F00"}` . + * @record + * @public + */ +export interface AlphaTexMetaDataNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Meta; + /** + * The tag part of the metadata. + */ + tag: AlphaTexMetaDataTagNode; + + /** + * A value list directly listed after the metadata (not within braces). + */ + values?: AlphaTexValueList; + + /** + * The optional properties attached to the metadata. + */ + properties?: AlphaTexPropertiesNode; + + /** + * Whether the properties are listed before the values (if both are present). + */ + propertiesBeforeValues: boolean; +} + +/** + * A node describing a list of properties grouped by braces. + * Used in contexts like note effects, beat effects + * @record + * @public + */ +export interface AlphaTexPropertiesNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Props; + /** + * The open brace grouping the properties (if needed). + */ + openBrace?: AlphaTexBraceOpenTokenNode; + /** + * The individual properties + */ + properties: AlphaTexPropertyNode[]; + /** + * The close brace grouping the properties (if needed). + */ + closeBrace?: AlphaTexBraceCloseTokenNode; +} + +/** + * A node describing a property with attached values. + * @record + * @public + */ +export interface AlphaTexPropertyNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Prop; + + /** + * The identifier describing the property. + */ + property: AlphaTexIdentifier; + /** + * The values attached to the property. + */ + values?: AlphaTexValueList; +} + +/** + * A base interface for nodes holding a textual value + * like string literals or identifiers. + * @record + * @public + */ +export interface AlphaTexTextNode extends AlphaTexAstNode { + /** + * The text contained in the node. + */ + text: string; +} + +/** + * A node describing an identifier. This is typically a string-like value + * but not quoted. + * @record + * @public + */ +export interface AlphaTexIdentifier extends AlphaTexTextNode, IAlphaTexValueListItem, IAlphaTexNoteValueNode { + nodeType: AlphaTexNodeType.Ident; +} + +// +// The semantic AST nodes for the overall song structure + +/** + * A node describing the root of an alphaTex file for a musical score. + * @record + * @public + */ +export interface AlphaTexScoreNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Score; + /** + * The bars describing the contents of the song. + */ + bars: AlphaTexBarNode[]; +} + +/** + * @public + */ +export interface IAlphaTexMetaDataTagPrefixNode extends IAlphaTexAstNode {} + +/** + * A metadata tag marker like `\title` + * @record + * @public + */ +export interface AlphaTexMetaDataTagNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Tag; + /** + * The prefix indicating the start of the tag (e.g. `\` or `\\`) + */ + prefix?: IAlphaTexMetaDataTagPrefixNode; + /** + * The identifier of the tag (e.g. `title`) + */ + tag: AlphaTexIdentifier; +} + +/** + * A node describing the bar level contents as written in alphaTex. + * @record + * @public + */ +export interface AlphaTexBarNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Bar; + + /** + * The metadata tags preceeding the bar contents, might start + * new tracks, staves, voices etc. + */ + metaData: AlphaTexMetaDataNode[]; + + /** + * The beats contained in this bar. + */ + beats: AlphaTexBeatNode[]; + + /** + * The pipe symbol denoting the end of the bar. + */ + pipe?: AlphaTexPipeTokenNode; +} + +/** + * A node describing a beat within alphaTex. + * @record + * @public + */ +export interface AlphaTexBeatNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Beat; + /** + * An optional marker changing the beat duration via a marker like `:4` + */ + durationChange?: AlphaTexBeatDurationChangeNode; + + /** + * The notes contained in this beat (mutually exclusive with `rest`) + */ + notes?: AlphaTexNoteListNode; + /** + * The marker indicating that this beat is a rest beat. Currently always an identifier with `r` + */ + rest?: AlphaTexIdentifier; + + /** + * The dot separating the beat content and the postfix beat duration + */ + durationDot?: AlphaTexDotTokenNode; + + /** + * The postfix beat duration. + */ + durationValue?: AlphaTexNumberLiteral; + + /** + * The `*` marker for repeating this beat multiple times. Must have a filled `beatMultiplierValue` + */ + beatMultiplier?: AlphaTexAsteriskTokenNode; + + /** + * The numeric value how often the beat should be repeated. + */ + beatMultiplierValue?: AlphaTexNumberLiteral; + + /** + * The effect list for this beat. + */ + beatEffects?: AlphaTexPropertiesNode; +} + +/** + * A list of notes for the beat. + * @record + * @public + */ +export interface AlphaTexNoteListNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.NoteList; + /** + * An open parenthesis token to group multiple notes for a beat. + */ + openParenthesis?: AlphaTexParenthesisOpenTokenNode; + + /** + * The notes contained in this list. + */ + notes: AlphaTexNoteNode[]; + + /** + * A close parenthesis token to group multiple notes for a beat. + */ + closeParenthesis?: AlphaTexParenthesisCloseTokenNode; +} + +/** + * @public + */ +export interface IAlphaTexNoteValueNode extends IAlphaTexAstNode {} + +/** + * A node describing a single note. + * @record + * @public + */ +export interface AlphaTexNoteNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Note; + + /** + * The value of the note. Depending on whether it is a fretted, pitched or percussion + * note this value varies. + */ + noteValue: IAlphaTexNoteValueNode; + + /** + * The dot separating the note value and the string for fretted/stringed instruments like guitars. + */ + noteStringDot?: AlphaTexDotTokenNode; + + /** + * The string value for fretted/stringed notes like guitars. + */ + noteString?: AlphaTexNumberLiteral; + + /** + * The effect list for this note. Semantically this list might also contain + * effects applied to the beat level. This allows specifying beat effects + * on a single note beat like `C4{txt "Beat Text" turn}` instead of + * `C4{turn}{txt Beat}` + */ + noteEffects?: AlphaTexPropertiesNode; +} + +/** + * A note describing a duration change like `:4` or `:4 { tu 3 }` + * @record + * @public + */ +export interface AlphaTexBeatDurationChangeNode extends AlphaTexAstNode { + nodeType: AlphaTexNodeType.Duration; + /** + * The colon token marking the duration change node + */ + colon: AlphaTexColonTokenNode; + /** + * The numeric value describing the duration. + */ + value?: AlphaTexNumberLiteral; + /** + * Additional duration attributes like tuplets. + */ + properties?: AlphaTexPropertiesNode; +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts new file mode 100644 index 000000000..31f867763 --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexLexer.ts @@ -0,0 +1,625 @@ +import { + type AlphaTexAsteriskTokenNode, + type AlphaTexAstNode, + type AlphaTexAstNodeLocation, + type AlphaTexBackSlashTokenNode, + type AlphaTexBraceCloseTokenNode, + type AlphaTexBraceOpenTokenNode, + type AlphaTexColonTokenNode, + type AlphaTexComment, + type AlphaTexDotTokenNode, + type AlphaTexDoubleBackSlashTokenNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataTagNode, + AlphaTexNodeType, + type AlphaTexNumberLiteral, + type AlphaTexParenthesisCloseTokenNode, + type AlphaTexParenthesisOpenTokenNode, + type AlphaTexPipeTokenNode, + type AlphaTexStringLiteral, + type AlphaTexTokenNode, + type IAlphaTexMetaDataTagPrefixNode +} from '@src/importer/alphaTex/AlphaTexAst'; +import { + AlphaTexDiagnosticBag, + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity +} from '@src/importer/alphaTex/AlphaTexShared'; +import { IOHelper } from '@src/io/IOHelper'; + +/** + * @public + */ +export class AlphaTexLexer { + private static readonly _eof: number = 0; + + private readonly _codepoints: number[]; + private _codepoint: number = AlphaTexLexer._eof; + + private _offset: number = 0; + private _line: number = 1; + private _col: number = 1; + + private _fatalError = false; + + private _tokenStart: AlphaTexAstNodeLocation = { line: 0, col: 0, offset: 0 }; + private _leadingComments: AlphaTexComment[] | undefined; + private _trailingCommentNode: AlphaTexAstNode | undefined; + + private _previousToken: AlphaTexAstNode | undefined; + private _peekedToken: AlphaTexAstNode | undefined; + + public readonly lexerDiagnostics = new AlphaTexDiagnosticBag(); + + public constructor(input: string) { + this._codepoints = [...IOHelper.iterateCodepoints(input)]; + this._offset = 0; + this._line = 1; + this._col = 1; + this._codepoint = this._codepoints.length > 0 ? this._codepoints[0] : AlphaTexLexer._eof; + } + + public peekToken(): AlphaTexAstNode | undefined { + if (this._fatalError) { + return undefined; + } + + let peeked = this._peekedToken; + if (peeked) { + return peeked; + } + + peeked = this._readToken(); + this._peekedToken = peeked; + return peeked; + } + + public extendToFloat(peekedNode: AlphaTexNumberLiteral): AlphaTexNumberLiteral { + // float number tokenizing is a bit tricky in alphaTex + // we chose .. (or .) as + // syntax for fretted notes, this conflicts now with a context + // independent tokenization. + + // It would be a good idea to check for 3.3.3 patterns here + // and then handle the numbers as individual ones? as for now we + // use "extendToFloat" at dedicated areas when parsing. + + // a float needs a decimal separator and a digit after the peeked node + if ( + this._codepoint !== 0x2e /* . */ || + this._offset + 1 >= this._codepoints.length || + !AlphaTexLexer._isDigit(this._codepoints[this._offset + 1]) + ) { + return peekedNode; + } + + let offset = this._offset + 2; + // advance to end of fractional digits + while (offset < this._codepoints.length && AlphaTexLexer._isDigit(this._codepoints[offset])) { + offset++; + } + + // 1.1a -> handle as Number(1) Dot(.) Ident(1a) + if (offset < this._codepoints.length && AlphaTexLexer._isIdentifierCharacter(this._codepoints[offset])) { + return peekedNode; + } + + // jump to last digit + const characters = offset - this._offset - 1; + this._offset += characters; + this._col += characters; + this._nextCodepoint(); // consume last digit like usual + + // update end and value + peekedNode.end = this._currentLexerLocation(); + peekedNode.value = Number.parseFloat( + String.fromCodePoint(...this._codepoints.slice(peekedNode.start!.offset, peekedNode.end!.offset)) + ); + + return peekedNode; + } + + public advance() { + this._previousToken = this._peekedToken; + this._peekedToken = undefined; + } + + // public nextToken(): AlphaTexAstNode | undefined { + // if (this._fatalError) { + // return undefined; + // } + + // const reverted = this._revertedToken; + // if (reverted) { + // this._revertedToken = undefined; + // return reverted; + // } + + // const peeked = this._peekedToken; + // if (peeked) { + // this._peekedToken = undefined; + // return peeked; + // } + + // return this._readToken(); + // } + + private _nextCodepoint(): number { + const codePoints = this._codepoints; + let offset = this._offset; + + if (offset < codePoints.length - 1) { + ++offset; + const codepoint = codePoints[offset]; + this._codepoint = codepoint; + if (codepoint === 0x0a /* \n */) { + this._trailingCommentNode = undefined; + ++this._line; + this._col = 1; + } else { + ++this._col; + } + this._offset = offset; + return codepoint; + } else if (this._codepoint !== AlphaTexLexer._eof) { + this._codepoint = AlphaTexLexer._eof; + ++this._col; + this._offset = codePoints.length; + } + return AlphaTexLexer._eof; + } + + public previousTokenEndLocation(): AlphaTexAstNodeLocation { + return this._previousToken?.end ?? this._currentLexerLocation(); + } + + public currentTokenLocation(): AlphaTexAstNodeLocation { + return this._peekedToken?.start ?? this._currentLexerLocation(); + } + + private _currentLexerLocation(): AlphaTexAstNodeLocation { + return { + line: this._line, + col: this._col, + offset: this._offset + }; + } + + private _readToken(): AlphaTexAstNode | undefined { + this._leadingComments = undefined; + while (this._codepoint !== AlphaTexLexer._eof) { + this._tokenStart = this._currentLexerLocation(); + if (AlphaTexLexer._terminalTokens.has(this._codepoint)) { + const token = AlphaTexLexer._terminalTokens.get(this._codepoint)!(this); + if (token) { + this._trailingCommentNode = token; + return token; + } + } else if (AlphaTexLexer._isIdentifierCharacter(this._codepoint)) { + const identifier = this._numberOrIdentifier(); + this._trailingCommentNode = identifier; + return identifier; + } else { + // simply skip unknown characters + // there are only a few ascii control characters + // which can hit this path + this._codepoint = this._nextCodepoint(); + } + } + + return undefined; + } + + private _comment(): AlphaTexAstNode | undefined { + this._codepoint = this._nextCodepoint(); + if (this._codepoint === 0x2f /* / */) { + this._singleLineComment(); + } else if (this._codepoint === 0x2a /* * */) { + this._multiLineComment(); + } else { + this.lexerDiagnostics.push({ + code: AlphaTexDiagnosticCode.AT001, + message: `Unexpected character at comment start, expected '//' or '/*' but found '/${String.fromCodePoint(this._codepoint)}'`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: this._tokenStart, + end: this._currentLexerLocation() + }); + this._fatalError = true; + } + return undefined; + } + + private static _terminalTokens: Map AlphaTexAstNode | undefined> = new Map([ + [0x2f /* / */, l => l._comment()], + [0x22 /* " */, l => l._string()], + [0x27 /* ' */, l => l._string()], + [0x2d /* - */, l => l._numberOrIdentifier()], + [0x2e /* . */, l => l._token({ nodeType: AlphaTexNodeType.Dot } as AlphaTexDotTokenNode)], + [0x3a /* : */, l => l._token({ nodeType: AlphaTexNodeType.Colon } as AlphaTexColonTokenNode)], + [0x28 /* ( */, l => l._token({ nodeType: AlphaTexNodeType.LParen } as AlphaTexParenthesisOpenTokenNode)], + [0x29 /* ) */, l => l._token({ nodeType: AlphaTexNodeType.RParen } as AlphaTexParenthesisCloseTokenNode)], + [0x7b /* { */, l => l._token({ nodeType: AlphaTexNodeType.LBrace } as AlphaTexBraceOpenTokenNode)], + [0x7d /* } */, l => l._token({ nodeType: AlphaTexNodeType.RBrace } as AlphaTexBraceCloseTokenNode)], + [0x7c /* | */, l => l._token({ nodeType: AlphaTexNodeType.Pipe } as AlphaTexPipeTokenNode)], + [0x2a /* * */, l => l._token({ nodeType: AlphaTexNodeType.Asterisk } as AlphaTexAsteriskTokenNode)], + [0x5c /* \ */, l => l._metaCommand()], + + [0x09 /* \t */, l => l._whitespace()], + [0x0a /* \n */, l => l._whitespace()], + [0x0b /* \v */, l => l._whitespace()], + [0x0d /* \r */, l => l._whitespace()], + [0x20 /* space */, l => l._whitespace()] + ]); + + private _metaCommand() { + const prefixStart = this._currentLexerLocation(); + + this._codepoint = this._nextCodepoint(); + let prefix: IAlphaTexMetaDataTagPrefixNode; + + // allow double backslash (easier to test when copying from escaped Strings) + let prefixEnd: AlphaTexAstNodeLocation; + if (this._codepoint === 0x5c /* \ */) { + this._codepoint = this._nextCodepoint(); + prefixEnd = this._currentLexerLocation(); + prefix = { + nodeType: AlphaTexNodeType.DoubleBackslash, + start: prefixStart, + end: prefixEnd + } as AlphaTexDoubleBackSlashTokenNode; + } else { + prefixEnd = this._currentLexerLocation(); + prefix = { + nodeType: AlphaTexNodeType.Backslash, + start: prefixStart, + end: prefixEnd + } as AlphaTexBackSlashTokenNode; + } + + let text = ''; + while (AlphaTexLexer._isIdentifierCharacter(this._codepoint)) { + text += String.fromCodePoint(this._codepoint); + this._codepoint = this._nextCodepoint(); + } + + if (text.length === 0) { + this.lexerDiagnostics.push({ + code: AlphaTexDiagnosticCode.AT002, + message: 'Missing identifier after meta data start', + severity: AlphaTexDiagnosticsSeverity.Error, + start: this._tokenStart, + end: this._currentLexerLocation() + }); + return undefined; + } + + const token: AlphaTexMetaDataTagNode = { + nodeType: AlphaTexNodeType.Tag, + leadingComments: this._leadingComments, + start: this._tokenStart, + end: this._currentLexerLocation(), + prefix: prefix, + tag: { + nodeType: AlphaTexNodeType.Ident, + text: text, + start: prefixEnd, + end: this._currentLexerLocation() + } + }; + return token; + } + + private _token(t: T): T { + t.leadingComments = this._leadingComments; + t.start = this._tokenStart; + t.end = this._currentLexerLocation(); + // consume char + this._codepoint = this._nextCodepoint(); + return t; + } + + private _string() { + const startChar: number = this._codepoint; + this._codepoint = this._nextCodepoint(); + let s: string = ''; + + let previousCodepoint: number = -1; + + while (this._codepoint !== startChar && this._codepoint !== AlphaTexLexer._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 === AlphaTexLexer._eof) { + this.lexerDiagnostics.push({ + code: AlphaTexDiagnosticCode.AT003, + message: 'Unexpected end of file. Need 4 hex characters on a \\uXXXX escape sequence', + severity: AlphaTexDiagnosticsSeverity.Error, + start: this._tokenStart, + end: this._currentLexerLocation() + }); + this._fatalError = true; + return undefined; + } + hex += String.fromCodePoint(this._codepoint); + } + + codepoint = Number.parseInt(hex, 16); + if (Number.isNaN(codepoint)) { + this.lexerDiagnostics.push({ + code: AlphaTexDiagnosticCode.AT004, + message: 'Invalid unicode value. Need 4 hex characters on a \\uXXXX escape sequence.', + severity: AlphaTexDiagnosticsSeverity.Error, + start: this._tokenStart, + end: this._currentLexerLocation() + }); + this._fatalError = true; + return undefined; + } + } else { + this.lexerDiagnostics.push({ + code: AlphaTexDiagnosticCode.AT005, + message: `Unsupported escape sequence. Expected '\\n', '\\r', '\\t', or '\\uXXXX' but found '\\${String.fromCodePoint(this._codepoint)}'.`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: this._tokenStart, + end: this._currentLexerLocation() + }); + this._fatalError = true; + return undefined; + } + } 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 === AlphaTexLexer._eof) { + this.lexerDiagnostics.push({ + code: AlphaTexDiagnosticCode.AT006, + message: `Unexpected end of file. String not closed.`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: this._tokenStart, + end: this._currentLexerLocation() + }); + this._fatalError = true; + return undefined; + } + + const stringToken: AlphaTexStringLiteral = { + nodeType: AlphaTexNodeType.String, + text: s, + leadingComments: this._leadingComments, + start: this._tokenStart, + end: this._currentLexerLocation() + }; + + // string quote end + this._codepoint = this._nextCodepoint(); + + return stringToken; + } + + private _multiLineComment() { + const trailingCommentNode = this._trailingCommentNode; + const comment: AlphaTexComment = { + start: this._tokenStart, + end: this._currentLexerLocation(), + text: '', + multiLine: true + }; + + while (this._codepoint !== AlphaTexLexer._eof) { + if (this._codepoint === 0x2a /* * */) { + this._codepoint = this._nextCodepoint(); + if (this._codepoint === 0x2f /* / */) { + this._codepoint = this._nextCodepoint(); + break; + } else { + comment.text += `*${String.fromCodePoint(this._codepoint)}`; + comment.end!.line = this._line; + comment.end!.col = this._col; + comment.end!.offset = this._offset; + } + } else { + this._codepoint = this._nextCodepoint(); + comment.text += String.fromCodePoint(this._codepoint); + comment.end!.line = this._line; + comment.end!.col = this._col; + comment.end!.offset = this._offset; + } + } + + if (trailingCommentNode) { + trailingCommentNode.trailingComments ??= []; + trailingCommentNode.trailingComments.push(comment); + } else { + this._leadingComments ??= []; + this._leadingComments!.push(comment); + } + } + + private _numberOrIdentifier(): AlphaTexAstNode { + let str: string = ''; + + // assume number at start + let isNumber = true; + + // negative start or dash + let codepoint = this._codepoint; + if (codepoint === 0x2d) { + str += String.fromCodePoint(codepoint); + codepoint = this._nextCodepoint(); + + // need a number afterwards otherwise we have a string(-) + if (!AlphaTexLexer._isDigit(codepoint)) { + isNumber = false; + } + } + + let keepReading = true; + + do { + if (isNumber) { + // adding digits to the number + if (AlphaTexLexer._isDigit(codepoint)) { + str += String.fromCodePoint(codepoint); + codepoint = this._nextCodepoint(); + keepReading = true; + } + // letter in number -> fallback to name reading + else if (AlphaTexLexer._isIdentifierCharacter(codepoint)) { + isNumber = false; + str += String.fromCodePoint(codepoint); + codepoint = this._nextCodepoint(); + keepReading = true; + } + // general unknown character -> end reading + else { + keepReading = false; + } + } else { + if (AlphaTexLexer._isIdentifierCharacter(codepoint)) { + str += String.fromCodePoint(codepoint); + codepoint = this._nextCodepoint(); + keepReading = true; + } else { + keepReading = false; + } + } + } while (keepReading); + + if (isNumber) { + const numberLiteral: AlphaTexNumberLiteral = { + nodeType: AlphaTexNodeType.Number, + leadingComments: this._leadingComments, + start: this._tokenStart, + end: this._currentLexerLocation(), + value: Number.parseInt(str, 10) + }; + return numberLiteral; + } + + const identifier: AlphaTexIdentifier = { + nodeType: AlphaTexNodeType.Ident, + leadingComments: this._leadingComments, + start: this._tokenStart, + end: this._currentLexerLocation(), + text: str + }; + return identifier; + } + + private _singleLineComment() { + // single line comment + const trailingCommentNode = this._trailingCommentNode; + const comment: AlphaTexComment = { + start: this._tokenStart, + end: this._currentLexerLocation(), + text: '', + multiLine: false + }; + let codepoint = this._codepoint; + while (codepoint !== AlphaTexLexer._eof) { + codepoint = this._nextCodepoint(); + if (codepoint !== 0x0a /* \n */ && codepoint !== AlphaTexLexer._eof) { + comment.text += String.fromCodePoint(codepoint); + comment.end!.line = this._line; + comment.end!.col = this._col; + comment.end!.offset = this._offset; + } else { + break; + } + } + if (trailingCommentNode) { + trailingCommentNode.trailingComments ??= []; + trailingCommentNode.trailingComments.push(comment); + } else { + this._leadingComments ??= []; + this._leadingComments!.push(comment); + } + } + + private _whitespace(): AlphaTexAstNode | undefined { + // skip whitespaces + let codepoint = this._codepoint; + while (AlphaTexLexer._isWhiteSpace(codepoint)) { + if (codepoint === 0x0a /* \n */) { + this._trailingCommentNode = undefined; + } + codepoint = this._nextCodepoint(); + } + return undefined; + } + + private static _isDigit(ch: number): boolean { + return ch >= 0x30 && ch <= 0x39 /* 0-9 */; + } + + private static _buildNonIdentifierChars() { + const c = new Set(); + + for (const terminal of AlphaTexLexer._terminalTokens.keys()) { + c.add(terminal); + } + + // dashes allowed in names + c.delete(0x2d /* - */); + + // eof + c.add(AlphaTexLexer._eof); + + return c; + } + private static readonly _nonIdentifierChars = AlphaTexLexer._buildNonIdentifierChars(); + + private static _isIdentifierCharacter(ch: number): boolean { + return !AlphaTexLexer._nonIdentifierChars.has(ch); + } + + private static _isWhiteSpace(ch: number): boolean { + return ( + ch === 0x20 /* space */ || + ch === 0x0a /* \n */ || + ch === 0x0d /* \r */ || + ch === 0x09 /* \t */ || + ch === 0x0b /* \v */ + ); + } +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts new file mode 100644 index 000000000..ce79d397e --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts @@ -0,0 +1,706 @@ +import { AlphaTex1MetaDataReader } from '@src/importer/alphaTex/AlphaTex1MetaDataReader'; +import { + type AlphaTexAsteriskTokenNode, + type AlphaTexAstNode, + type AlphaTexBarNode, + type AlphaTexBeatDurationChangeNode, + type AlphaTexBeatNode, + type AlphaTexBraceCloseTokenNode, + type AlphaTexBraceOpenTokenNode, + type AlphaTexColonTokenNode, + type AlphaTexDotTokenNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataNode, + type AlphaTexMetaDataTagNode, + AlphaTexNodeType, + type AlphaTexNoteListNode, + type AlphaTexNoteNode, + type AlphaTexNumberLiteral, + type AlphaTexParenthesisCloseTokenNode, + type AlphaTexParenthesisOpenTokenNode, + type AlphaTexPipeTokenNode, + type AlphaTexPropertiesNode, + type AlphaTexPropertyNode, + type AlphaTexScoreNode, + type AlphaTexStringLiteral, + type AlphaTexValueList +} from '@src/importer/alphaTex/AlphaTexAst'; +import { AlphaTexLexer } from '@src/importer/alphaTex/AlphaTexLexer'; +import { + type AlphaTexDiagnostic, + AlphaTexDiagnosticBag, + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity, + AlphaTexParserAbort +} from '@src/importer/alphaTex/AlphaTexShared'; +import type { IAlphaTexMetaDataReader } from '@src/importer/alphaTex/IAlphaTexMetaDataReader'; + +/** + * A parser for translating a given alphaTex source into an AST for further use + * in the alphaTex importer, editors etc. + * @public + */ +export class AlphaTexParser { + public readonly lexer: AlphaTexLexer; + private _scoreNode!: AlphaTexScoreNode; + private _metaDataReader: IAlphaTexMetaDataReader = AlphaTex1MetaDataReader.instance; + + public get lexerDiagnostics(): AlphaTexDiagnosticBag { + return this.lexer.lexerDiagnostics; + } + + public readonly parserDiagnostics = new AlphaTexDiagnosticBag(); + + public addParserDiagnostic(diagnostics: AlphaTexDiagnostic) { + this.parserDiagnostics.push(diagnostics); + } + + /** + * @internal + */ + public unexpectedToken(actual: AlphaTexAstNode | undefined, expected: AlphaTexNodeType[], abort: boolean) { + if (!actual) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT203, + start: this.lexer.currentTokenLocation(), + end: this.lexer.currentTokenLocation(), + severity: AlphaTexDiagnosticsSeverity.Error, + message: 'Unexpected end of file.' + }); + } else { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT202, + message: `Unexpected '${AlphaTexNodeType[actual.nodeType]}' token. Expected one of following: ${expected.map(v => AlphaTexNodeType[v]).join(',')}`, + severity: AlphaTexDiagnosticsSeverity.Error, + start: actual.start, + end: actual.end + }); + } + + if (abort) { + throw new AlphaTexParserAbort(); + } + } + + public constructor(source: string) { + this.lexer = new AlphaTexLexer(source); + } + + public read(): AlphaTexScoreNode { + this._score(); + + return this._scoreNode; + } + + // recursive decent + + private _score() { + this._scoreNode = { + nodeType: AlphaTexNodeType.Score, + bars: [], + start: this.lexer.currentTokenLocation() + }; + + try { + this._bars(); + } catch (e) { + if (e instanceof AlphaTexParserAbort) { + // OK + } else { + throw e; + } + } finally { + this._scoreNode.end = this.lexer.previousTokenEndLocation(); + } + } + + private _bars() { + let token = this.lexer.peekToken(); + while (token) { + // still reading bars + this._bar(); + + token = this.lexer.peekToken(); + } + } + + private _bar() { + const bar: AlphaTexBarNode = { + nodeType: AlphaTexNodeType.Bar, + metaData: [], + beats: [], + pipe: undefined, + start: this.lexer.currentTokenLocation() + }; + try { + this._barMetaData(bar); + this._barBeats(bar); + + const next = this.lexer.peekToken(); + if (next?.nodeType === AlphaTexNodeType.Pipe) { + bar.pipe = next as AlphaTexPipeTokenNode; + this.lexer.advance(); + } + + if (bar.metaData.length > 0 || bar.beats.length > 0 || bar.pipe) { + bar.end = this.lexer.previousTokenEndLocation(); + this._scoreNode.bars.push(bar); + } + } finally { + bar.end = this.lexer.previousTokenEndLocation(); + } + } + + private _barMetaData(bar: AlphaTexBarNode) { + let token = this.lexer.peekToken(); + while (token && (token.nodeType === AlphaTexNodeType.Tag || token.nodeType === AlphaTexNodeType.Dot)) { + if (token.nodeType === AlphaTexNodeType.Dot) { + this.lexer.advance(); + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT400, + message: `The dots separating score metadata, score contents and the sync points can be removed.`, + severity: AlphaTexDiagnosticsSeverity.Hint, + start: token.start, + end: token.end + }); + } else { + bar.metaData.push(this._metaData()!); + } + + token = this.lexer.peekToken(); + } + } + + private _barBeats(bar: AlphaTexBarNode) { + let token = this.lexer.peekToken(); + while ( + token && + token.nodeType !== AlphaTexNodeType.Pipe && + token.nodeType !== AlphaTexNodeType.Dot && + token.nodeType !== AlphaTexNodeType.Tag + ) { + const beat = this._beat(); + if (beat) { + bar.beats.push(beat); + } + token = this.lexer.peekToken(); + } + } + + private _beat(): AlphaTexBeatNode | undefined { + const beat: AlphaTexBeatNode = { + nodeType: AlphaTexNodeType.Beat, + durationChange: undefined, + notes: undefined, + rest: undefined, + beatEffects: undefined, + beatMultiplier: undefined, + beatMultiplierValue: undefined, + start: this.lexer.peekToken()?.start + }; + + try { + beat.durationChange = this._beatDurationChange(); + + this._beatContent(beat); + if (!beat.notes && !beat.rest) { + return beat; + } + + this._beatDuration(beat); + + // pre 1.7 the multiplier was between the duration and effects + // for backwards compat we still allow it + this._beatMultiplier(beat); + + + beat.beatEffects = this._properties(property => + this._metaDataReader.readBeatPropertyValues(this, property) + ); + + if(beat.beatMultiplierValue !== undefined && beat.beatEffects?.openBrace) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT304, + message: 'The beat multiplier should be specified after the beat effects.', + severity: AlphaTexDiagnosticsSeverity.Warning, + start: beat.beatMultiplier!.start, + end: beat.beatMultiplierValue.end + }); + } + + this._beatMultiplier(beat); + + } finally { + beat.end = this.lexer.previousTokenEndLocation(); + } + + return beat; + } + + private _beatDuration(beat: AlphaTexBeatNode) { + const dot = this.lexer.peekToken(); + if (dot?.nodeType !== AlphaTexNodeType.Dot) { + return; + } + + beat.durationDot = dot as AlphaTexDotTokenNode; + this.lexer.advance(); + + const durationValue = this.lexer.peekToken(); + if (durationValue?.nodeType === AlphaTexNodeType.Number) { + beat.durationValue = durationValue as AlphaTexNumberLiteral; + this.lexer.advance(); + return; + } else if (durationValue?.nodeType === AlphaTexNodeType.Tag) { + // backwards compatibility with older alphaTex: there was a dot separator + // between the song content and sync points at the end + + // handle switch to sync points like: 3.3 . \sync 1 1 1 + // (we can drop the separation dot as it is not part of the AST) + beat.durationDot = undefined; + return; + } else if (!durationValue) { + // handle switch to sync points like: 3.3 . + // (no sync points yet) + return; + } else { + this.unexpectedToken(durationValue, [AlphaTexNodeType.Number], true); + } + } + + private _beatDurationChange(): AlphaTexBeatDurationChangeNode | undefined { + const colon = this.lexer.peekToken(); + if (colon?.nodeType !== AlphaTexNodeType.Colon) { + return undefined; + } + this.lexer.advance(); + + const durationChange: AlphaTexBeatDurationChangeNode = { + nodeType: AlphaTexNodeType.Duration, + colon: colon as AlphaTexColonTokenNode, + value: undefined, + properties: undefined, + start: colon.start + }; + try { + const durationValue = this.lexer.peekToken(); + if (!durationValue || durationValue.nodeType !== AlphaTexNodeType.Number) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT201, + message: "Missing duration value after ':'.", + severity: AlphaTexDiagnosticsSeverity.Error, + start: colon!.start, + end: colon!.end + }); + return undefined; + } + this.lexer.advance(); + durationChange.value = durationValue as AlphaTexNumberLiteral; + + durationChange.properties = this._properties(property => + this._metaDataReader.readDurationChangePropertyValues(this, property) + ); + } finally { + durationChange.end = this.lexer.previousTokenEndLocation(); + } + + return durationChange; + } + + private _beatContent(beat: AlphaTexBeatNode) { + const notes = this.lexer.peekToken(); + if (!notes) { + return; + } + if (notes.nodeType === AlphaTexNodeType.Ident && (notes as AlphaTexIdentifier).text === 'r') { + beat.rest = notes as AlphaTexIdentifier; + this.lexer.advance(); + } else if (notes.nodeType === AlphaTexNodeType.LParen) { + beat.notes = this._noteList(notes as AlphaTexParenthesisOpenTokenNode); + } else { + const note = this._note(notes); + if (note) { + beat.notes = { + nodeType: AlphaTexNodeType.NoteList, + openParenthesis: undefined, + notes: [note], + closeParenthesis: undefined, + start: note.start, + end: note.end + } as AlphaTexNoteListNode; + } + } + } + + private _beatMultiplier(beat: AlphaTexBeatNode) { + const multiplier = this.lexer.peekToken(); + if (!multiplier || multiplier.nodeType !== AlphaTexNodeType.Asterisk) { + return; + } + this.lexer.advance(); + beat.beatMultiplier = multiplier as AlphaTexAsteriskTokenNode; + + const multiplierValue = this.lexer.peekToken(); + if (!multiplierValue || multiplierValue.nodeType !== AlphaTexNodeType.Number) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT200, + message: "Missing beat multiplier value after '*'.", + severity: AlphaTexDiagnosticsSeverity.Error, + start: beat.beatMultiplier!.start, + end: beat.beatMultiplier!.end + }); + return; + } + + beat.beatMultiplierValue = multiplierValue as AlphaTexNumberLiteral; + this.lexer.advance(); + } + + private _noteList(openParenthesis: AlphaTexParenthesisOpenTokenNode): AlphaTexNoteListNode { + const noteList: AlphaTexNoteListNode = { + nodeType: AlphaTexNodeType.NoteList, + openParenthesis: undefined, + notes: [], + closeParenthesis: undefined, + start: this.lexer.currentTokenLocation() + }; + try { + noteList.openParenthesis = openParenthesis as AlphaTexParenthesisOpenTokenNode; + this.lexer.advance(); + + let token = this.lexer.peekToken(); + while (token && token.nodeType !== AlphaTexNodeType.RParen) { + const note = this._note(token); + if (note) { + noteList.notes.push(note); + } else { + break; + } + token = this.lexer.peekToken(); + } + + const closeParenthesis = this.lexer.peekToken(); + if (closeParenthesis?.nodeType === AlphaTexNodeType.RParen) { + noteList.closeParenthesis = closeParenthesis as AlphaTexParenthesisCloseTokenNode; + this.lexer.advance(); + } else { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT206, + message: 'Unexpected end of file. Group not closed.', + severity: AlphaTexDiagnosticsSeverity.Error, + start: closeParenthesis?.start ?? this.lexer.currentTokenLocation(), + end: closeParenthesis?.end ?? this.lexer.currentTokenLocation() + }); + } + } finally { + noteList.end = this.lexer.previousTokenEndLocation(); + } + + return noteList; + } + + private _note(noteValue: AlphaTexAstNode): AlphaTexNoteNode | undefined { + const note: AlphaTexNoteNode = { + nodeType: AlphaTexNodeType.Note, + noteValue: { + // placeholder value + nodeType: AlphaTexNodeType.Ident, + text: '' + } as AlphaTexIdentifier, + start: noteValue.start + }; + try { + let canHaveString = false; + switch (noteValue.nodeType) { + case AlphaTexNodeType.Number: + note.noteValue = noteValue as AlphaTexNumberLiteral; + this.lexer.advance(); + + canHaveString = true; + + break; + case AlphaTexNodeType.String: + note.noteValue = noteValue as AlphaTexStringLiteral; + this.lexer.advance(); + switch ((note.noteValue as AlphaTexStringLiteral).text) { + case 'x': + case '-': + canHaveString = true; + break; + } + break; + case AlphaTexNodeType.Ident: + note.noteValue = noteValue as AlphaTexIdentifier; + this.lexer.advance(); + switch ((note.noteValue as AlphaTexIdentifier).text) { + case 'x': + case '-': + canHaveString = true; + break; + } + break; + default: + this.unexpectedToken( + noteValue, + [AlphaTexNodeType.Number, AlphaTexNodeType.String, AlphaTexNodeType.Ident], + true + ); + return undefined; + } + + if (canHaveString) { + const dot = this.lexer.peekToken(); + if (dot?.nodeType === AlphaTexNodeType.Dot) { + const noteStringDot = dot as AlphaTexDotTokenNode; + this.lexer.advance(); + + const noteString = this.lexer.peekToken(); + if (!noteString) { + this.unexpectedToken(noteString, [AlphaTexNodeType.Number], true); + return undefined; + } + + if (noteString.nodeType === AlphaTexNodeType.Tag) { + // backwards compatibility with older alphaTex: there was a dot separator + // between the song content and sync points at the end + + // handle switch to sync points like: 3 4 5 . \sync 1 1 1 + // in this example the numbers are percussion articulations + + // (we can drop the separation dot as it is not part of the AST) + return note; + } else if (noteString.nodeType === AlphaTexNodeType.Number) { + note.noteStringDot = noteStringDot; + note.noteString = noteString as AlphaTexNumberLiteral; + this.lexer.advance(); + } else { + this.unexpectedToken(noteString, [AlphaTexNodeType.Number], true); + return undefined; + } + } + } + + note.noteEffects = this._properties(property => + this._metaDataReader.readNotePropertyValues(this, property) + ); + } finally { + note.end = this.lexer.previousTokenEndLocation(); + } + + return note; + } + + private _metaData(): AlphaTexMetaDataNode | undefined { + const tag = this.lexer.peekToken(); + if (!tag || tag.nodeType !== AlphaTexNodeType.Tag) { + return undefined; + } + + const metaData: AlphaTexMetaDataNode = { + nodeType: AlphaTexNodeType.Meta, + tag: tag as AlphaTexMetaDataTagNode, + start: tag.start, + properties: undefined, + propertiesBeforeValues: false + }; + this.lexer.advance(); + + try { + // properties can be before or after the values, this is a again a historical + // inconsistency on chords + const braceCandidate = this.lexer.peekToken(); + if (braceCandidate?.nodeType === AlphaTexNodeType.LBrace) { + metaData.propertiesBeforeValues = true; + metaData.properties = this._properties(property => + this._metaDataReader.readMetaDataPropertyValues(this, metaData.tag, property) + ); + metaData.values = this.valueList(); + if (!metaData.values) { + metaData.values = this._metaDataReader.readMetaDataValues(this, metaData.tag); + if (metaData.values && metaData.values.values.length > 1) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT301, + message: `Metadata values should be wrapped into parenthesis.`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: metaData.values?.start ?? metaData.start, + end: metaData.values?.end ?? metaData.end + }); + + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT302, + message: `Metadata values should be placed before metadata properties.`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: metaData.values?.start ?? metaData.start, + end: metaData.values?.end ?? metaData.end + }); + } + } + } else { + metaData.values = this.valueList(); + if (!metaData.values) { + metaData.values = this._metaDataReader.readMetaDataValues(this, metaData.tag); + if (metaData.values && metaData.values.values.length > 1) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT301, + message: `Metadata values should be wrapped into parenthesis.`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: metaData.values?.start ?? metaData.start, + end: metaData.values?.end ?? metaData.end + }); + } + } + + metaData.properties = this._properties(property => + this._metaDataReader.readMetaDataPropertyValues(this, metaData.tag, property) + ); + } + } finally { + metaData.end = this.lexer.previousTokenEndLocation(); + } + return metaData; + } + + private _properties( + readPropertyValues: (property: AlphaTexPropertyNode) => AlphaTexValueList | undefined + ): AlphaTexPropertiesNode | undefined { + const braceOpen = this.lexer.peekToken(); + if (!braceOpen || braceOpen.nodeType !== AlphaTexNodeType.LBrace) { + return undefined; + } + + const properties: AlphaTexPropertiesNode = { + nodeType: AlphaTexNodeType.Props, + openBrace: braceOpen as AlphaTexBraceOpenTokenNode, + properties: [], + closeBrace: undefined, + start: braceOpen.start + }; + this.lexer.advance(); + + try { + let token = this.lexer.peekToken(); + while (token?.nodeType === AlphaTexNodeType.Ident) { + properties.properties.push(this._property(token as AlphaTexIdentifier, readPropertyValues)); + token = this.lexer.peekToken(); + } + + const braceClose = this.lexer.peekToken(); + if (braceClose?.nodeType === AlphaTexNodeType.RBrace) { + properties.closeBrace = braceClose as AlphaTexBraceCloseTokenNode; + this.lexer.advance(); + } else { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT206, + message: 'Unexpected end of file. Group not closed.', + severity: AlphaTexDiagnosticsSeverity.Error, + start: this.lexer.currentTokenLocation(), + end: this.lexer.currentTokenLocation() + }); + } + } finally { + properties.end = this.lexer.previousTokenEndLocation(); + } + + return properties; + } + + private _property( + identifier: AlphaTexIdentifier, + readPropertyValues: (property: AlphaTexPropertyNode) => AlphaTexValueList | undefined + ): AlphaTexPropertyNode { + const property: AlphaTexPropertyNode = { + nodeType: AlphaTexNodeType.Prop, + property: identifier as AlphaTexIdentifier, + values: undefined + }; + this.lexer.advance(); + + property.start = property.property.start; + try { + property.values = this.valueList(); + if (!property.values) { + property.values = readPropertyValues(property); + if (property.values && property.values.values.length > 1) { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT303, + message: 'Property values should be wrapped into parenthesis.', + severity: AlphaTexDiagnosticsSeverity.Warning, + start: property.values.start, + end: property.values.end + }); + } + } + } finally { + property.end = this.lexer.previousTokenEndLocation(); + } + + return property; + } + + public valueList(): AlphaTexValueList | undefined { + const openParenthesis = this.lexer.peekToken(); + if (openParenthesis?.nodeType !== AlphaTexNodeType.LParen) { + return undefined; + } + + const valueList: AlphaTexValueList = { + nodeType: AlphaTexNodeType.Values, + openParenthesis: openParenthesis as AlphaTexParenthesisOpenTokenNode, + values: [], + closeParenthesis: undefined, + start: openParenthesis.start + }; + this.lexer.advance(); + + try { + let token = this.lexer.peekToken(); + while (token && token?.nodeType !== AlphaTexNodeType.RParen) { + switch (token.nodeType) { + case AlphaTexNodeType.Ident: + valueList.values.push(token as AlphaTexIdentifier); + this.lexer.advance(); + break; + case AlphaTexNodeType.String: + valueList.values.push(token as AlphaTexStringLiteral); + this.lexer.advance(); + break; + case AlphaTexNodeType.Number: + // in value lists we can always assume floats + // (within parenthesis we have no risk of syntax overlaps) + valueList.values.push(this.lexer.extendToFloat(token as AlphaTexNumberLiteral)); + this.lexer.advance(); + break; + default: + this.unexpectedToken( + token, + [AlphaTexNodeType.Ident, AlphaTexNodeType.String, AlphaTexNodeType.Number], + false + ); + // try to skip and continue parsing + this.lexer.advance(); + break; + } + + token = this.lexer.peekToken(); + } + + const closeParenthesis = this.lexer.peekToken(); + if (closeParenthesis?.nodeType === AlphaTexNodeType.RParen) { + valueList.closeParenthesis = closeParenthesis as AlphaTexParenthesisCloseTokenNode; + this.lexer.advance(); + } else { + this.addParserDiagnostic({ + code: AlphaTexDiagnosticCode.AT206, + message: 'Unexpected end of file. Group not closed.', + severity: AlphaTexDiagnosticsSeverity.Error, + start: this.lexer.currentTokenLocation(), + end: this.lexer.currentTokenLocation() + }); + } + } finally { + valueList.end = this.lexer.previousTokenEndLocation(); + } + + return valueList; + } +} diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts new file mode 100644 index 000000000..0a3aa60a8 --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts @@ -0,0 +1,314 @@ +import type { AlphaTexAstNodeLocation } from '@src/importer/alphaTex/AlphaTexAst'; +import type { FlatSyncPoint } from '@src/model/Automation'; +import type { SustainPedalMarker } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; +import type { DynamicValue } from '@src/model/DynamicValue'; +import type { Lyrics } from '@src/model/Lyrics'; +import type { Note } from '@src/model/Note'; +import type { Score } from '@src/model/Score'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; + +/** + * The different severity levels for diagnostics parsing alphaTex. + * @public + */ +export enum AlphaTexDiagnosticsSeverity { + Hint = 0, + Warning = 1, + Error = 2 +} + +/** + * A diagnostics message for the alphaTex parser. + * @record + * @public + */ +export interface AlphaTexDiagnostic { + /** + * The severity of the diagnostic. + */ + severity: AlphaTexDiagnosticsSeverity; + + /** + * The start location to which the diagnostic message belongs. + */ + start?: AlphaTexAstNodeLocation; + + /** + * The end location to which the diagnostic message belongs. + */ + end?: AlphaTexAstNodeLocation; + + /** + * A technical code describing the diagnostic message. + */ + code: AlphaTexDiagnosticCode; + + /** + * The textual message for the diagnostic. + */ + message: string; +} + +/** + * @public + */ +export enum AlphaTexDiagnosticCode { + // 000 - 99 Lexer Errors + // 100 - 199 Lexer Warnings + // 200 - 299 Parser Errors + // 300 - 399 Parser Warnings + // 400 - 499 Parser Hints + + /** + * Unexpected character at comment start, expected '//' or '/*' but found '/%s'. + */ + AT001 = 1, + + /** + * Missing identifier after meta data start. + */ + AT002 = 2, + + /** + * Unexpected end of file. Need 4 hex characters on a \uXXXX escape sequence. + */ + AT003 = 3, + + /** + * Invalid unicode value. Need 4 hex characters on a \uXXXX escape sequence. + */ + AT004 = 4, + + /** + * Unsupported escape sequence. Expected '\n', '\r', '\t', or '\uXXXX' but found '\%s'. + */ + AT005 = 5, + + /** + * Unexpected end of file. String not closed. + */ + AT006 = 6, + + /** + * Missing beat multiplier value after '*'. + */ + AT200 = 200, + + /** + * Missing duration value after ':'. + */ + AT201 = 201, + + /** + * Unexpected '%s' token. Expected one of following: %s + */ + AT202 = 202, + + /** + * Unexpected end of file. + */ + AT203 = 203, + + /** + * Unrecognized metadata '%s'. + */ + AT204 = 204, + + /** + * Unrecognized property '%s'. + */ + AT205 = 205, + + /** + * Unexpected end of file. Group not closed. + */ + AT206 = 206, + + /** + * Missing string for fretted note. + */ + AT207 = 207, + + /** + * Note string is out of range. Available range: 1-%s + */ + AT208 = 208, + + /** + * Unexpected %s value '%s', expected: %s + */ + AT209 = 209, + + /** + * Missing values. Expected following values: %s + */ + AT210 = 210, + + /** + * Value is out of valid range. Allowed range: %s, Actual Value: %s + */ + AT211 = 211, + + /** + * Unrecogized property '%s', expected one of %s + */ + AT212 = 212, + + /** + * Invalid format for color + */ + AT213 = 213, + + /** + * The '%s' effect needs %s values per item. With %s points, %s values are needed, only %s values found. + */ + AT214 = 214, + + /** + * Cannot use pitched note value '%s' on %s staff, please specify notes using the 'fret.string' syntax. + */ + AT215 = 215, + + /** + * Cannot use pitched note value '%s' on percussion staff, please specify percussion articulations with numbers or names. + */ + AT216 = 216, + + /** + * Unrecognized note value '%s'. + */ + AT217 = 217, + + /** + * Wrong note kind '%s' for staff with note kind '%s'. Do not mix incompatible staves and notes. + */ + AT218 = 218, + + /** + * Expected no values, but found some. Values are ignored. + */ + AT300 = 300, + + /** + * Metadata values should be wrapped into parenthesis. + */ + AT301 = 301, + + /** + * Metadata values should be placed before metadata properties. + */ + AT302 = 302, + + /** + * Property values should be wrapped into parenthesis. + */ + AT303 = 303, + + /** + * The beat multiplier should be specified after the beat effects. + */ + AT304 = 304, + + /** + * This value should be rather specified via the properties. + */ + AT305 = 305, + + /** + * This staff metadata tag should be specified as staff property. + */ + AT306 = 306, + + /** + * The dots separating score metadata, score contents and the sync points can be removed. + */ + AT400 = 400, +} + +/** + * @public + */ +export class AlphaTexDiagnosticBag implements Iterable { + private _hasErrors = false; + public readonly items: AlphaTexDiagnostic[] = []; + + public get errors(): AlphaTexDiagnostic[] { + return this.items.filter(i => i.severity === AlphaTexDiagnosticsSeverity.Error); + } + + public get hasErrors() { + return this._hasErrors; + } + + public push(diagnostic: AlphaTexDiagnostic) { + this.items.push(diagnostic); + if (diagnostic.severity === AlphaTexDiagnosticsSeverity.Error) { + this._hasErrors = true; + } + } + + [Symbol.iterator](): Iterator { + return this.items[Symbol.iterator](); + } +} + +/** + * An error used to abort the parsing of the alphaTex source into an + * @internal + */ +export class AlphaTexParserAbort extends Error {} + +/** + * @public + */ +export enum AlphaTexAccidentalMode { + Auto = 0, + Explicit = 1 +} + +/** + * @public + */ +export interface IAlphaTexImporterState { + score: Score; + accidentalMode: AlphaTexAccidentalMode; + currentDynamics: DynamicValue; + currentTupletNumerator: number; + currentTupletDenominator: number; + + readonly syncPoints: FlatSyncPoint[]; + readonly slurs: Map; + readonly percussionArticulationNames: Map; + readonly lyrics: Map; + readonly staffHasExplicitDisplayTransposition: Set; + readonly staffHasExplicitTuning: Set; + readonly staffTuningApplied: Set; + readonly sustainPedalToBeat: Map; +} + + +/** + * Lists the note kinds we can detect + * @public + */ +export enum StaffNoteKind { + Pitched, + Fretted, + Articulation +} + + +/** + * @public + */ +export interface IAlphaTexImporter { + readonly state: IAlphaTexImporterState; + + applyStaffNoteKind(staff: Staff, staffNoteKind: StaffNoteKind): void; + startNewVoice(): void; + startNewTrack(): Track; + startNewStaff(): Staff; + addSemanticDiagnostic(diagnostic: AlphaTexDiagnostic): void; +} diff --git a/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts b/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts new file mode 100644 index 000000000..1f73f0537 --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts @@ -0,0 +1,62 @@ +import type { AlphaTexMetaDataNode, AlphaTexPropertyNode } from '@src/importer/alphaTex/AlphaTexAst'; +import type { IAlphaTexImporter } from '@src/importer/alphaTex/AlphaTexShared'; +import type { Bar } from '@src/model/Bar'; +import type { Beat } from '@src/model/Beat'; +import type { Note } from '@src/model/Note'; +import type { Score } from '@src/model/Score'; +import type { Staff } from '@src/model/Staff'; + +/** + * @internal + */ +export enum ApplyNodeResult { + Applied, + NotAppliedSemanticError, + NotAppliedUnrecognizedMarker +} + +/** + * @internal + */ +export enum ApplyStructuralMetaDataResult { + AppliedNewTrack, + AppliedNewStaff, + AppliedNewVoice, + NotAppliedSemanticError, + NotAppliedUnrecognizedMarker +} + +/** + * @internal + */ +export interface IAlphaTexLanguageImportHandler { + applyStructuralMetaData(importer: IAlphaTexImporter, metaData: AlphaTexMetaDataNode): ApplyStructuralMetaDataResult; + applyScoreMetaData(importer: IAlphaTexImporter, score: Score, metaData: AlphaTexMetaDataNode): ApplyNodeResult; + applyStaffMetaData(importer: IAlphaTexImporter, staff: Staff, metaData: AlphaTexMetaDataNode): ApplyNodeResult; + applyBarMetaData(importer: IAlphaTexImporter, bar: Bar, metaData: AlphaTexMetaDataNode): ApplyNodeResult; + + applyBeatDurationProperty(importer: IAlphaTexImporter, property: AlphaTexPropertyNode): ApplyNodeResult; + applyBeatProperty(importer: IAlphaTexImporter, beat: Beat, property: AlphaTexPropertyNode): ApplyNodeResult; + applyNoteProperty(importer: IAlphaTexImporter, note: Note, p: AlphaTexPropertyNode): ApplyNodeResult; + + readonly allKnownMetaDataTags: Set; + readonly knownScoreMetaDataTags: Set; + readonly knownStructuralMetaDataTags: Set; + readonly knownStaffMetaDataTags: Set; + readonly knownBarMetaDataTags: Set; + + readonly knownBeatProperties: Set; + readonly knownBeatDurationProperties: Set; + readonly knownNoteProperties: Set; + + buildScoreMetaDataNodes(score: Score): AlphaTexMetaDataNode[]; + buildBarMetaDataNodes( + staff: Staff, + bar: Bar | undefined, + voice: number, + isMultiVoice: boolean + ): AlphaTexMetaDataNode[]; + buildSyncPointNodes(score: Score): AlphaTexMetaDataNode[]; + buildBeatEffects(beat: Beat): AlphaTexPropertyNode[]; + buildNoteEffects(data: Note): AlphaTexPropertyNode[]; +} diff --git a/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts b/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts new file mode 100644 index 000000000..732510de8 --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/IAlphaTexMetaDataReader.ts @@ -0,0 +1,28 @@ +import type { + AlphaTexMetaDataTagNode, + AlphaTexPropertyNode, + AlphaTexValueList +} from '@src/importer/alphaTex/AlphaTexAst'; +import type { AlphaTexParser } from '@src/importer/alphaTex/AlphaTexParser'; + +/** + * @internal + */ +export interface IAlphaTexMetaDataReader { + readMetaDataValues(parser: AlphaTexParser, metaData: AlphaTexMetaDataTagNode): AlphaTexValueList | undefined; + + readMetaDataPropertyValues( + parser: AlphaTexParser, + metaData: AlphaTexMetaDataTagNode, + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined; + + readBeatPropertyValues(parser: AlphaTexParser, property: AlphaTexPropertyNode): AlphaTexValueList | undefined; + + readDurationChangePropertyValues( + parser: AlphaTexParser, + property: AlphaTexPropertyNode + ): AlphaTexValueList | undefined; + + readNotePropertyValues(parser: AlphaTexParser, property: AlphaTexPropertyNode): AlphaTexValueList | undefined; +} diff --git a/packages/alphatab/src/importer/alphaTex/_barrel.ts b/packages/alphatab/src/importer/alphaTex/_barrel.ts new file mode 100644 index 000000000..842eaab7a --- /dev/null +++ b/packages/alphatab/src/importer/alphaTex/_barrel.ts @@ -0,0 +1,47 @@ +export { + type AlphaTexAsteriskTokenNode, + type AlphaTexAstNode, + type AlphaTexAstNodeLocation, + type AlphaTexBackSlashTokenNode, + type AlphaTexBarNode, + type AlphaTexBeatDurationChangeNode, + type AlphaTexBeatNode, + type AlphaTexBraceCloseTokenNode, + type AlphaTexBraceOpenTokenNode, + type AlphaTexColonTokenNode, + type AlphaTexComment, + type AlphaTexDotTokenNode, + type AlphaTexDoubleBackSlashTokenNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataNode, + type AlphaTexMetaDataTagNode, + AlphaTexNodeType, + type AlphaTexNoteListNode, + type AlphaTexNoteNode, + type AlphaTexNumberLiteral, + type AlphaTexParenthesisCloseTokenNode, + type AlphaTexParenthesisOpenTokenNode, + type AlphaTexPipeTokenNode, + type AlphaTexPropertiesNode, + type AlphaTexPropertyNode, + type AlphaTexScoreNode, + type AlphaTexStringLiteral, + type AlphaTexTextNode, + type AlphaTexTokenNode, + type AlphaTexValueList, + type IAlphaTexAstNode, + type IAlphaTexMetaDataTagPrefixNode, + type IAlphaTexNoteValueNode, + type IAlphaTexValueListItem +} from '@src/importer/alphaTex/AlphaTexAst'; +export { AlphaTexLexer } from '@src/importer/alphaTex/AlphaTexLexer'; +export { AlphaTexParser } from '@src/importer/alphaTex/AlphaTexParser'; +export { + AlphaTexAccidentalMode, + type AlphaTexDiagnostic, + AlphaTexDiagnosticBag, + AlphaTexDiagnosticCode, + AlphaTexDiagnosticsSeverity, + type IAlphaTexImporter, + type IAlphaTexImporterState +} from '@src/importer/alphaTex/AlphaTexShared'; diff --git a/packages/alphatab/src/io/IReadable.ts b/packages/alphatab/src/io/IReadable.ts index 6b107fde7..86f33366f 100644 --- a/packages/alphatab/src/io/IReadable.ts +++ b/packages/alphatab/src/io/IReadable.ts @@ -54,6 +54,5 @@ export interface IReadable { export class EndOfReaderError extends AlphaTabError { public constructor() { super(AlphaTabErrorType.Format, 'Unexpected end of data within reader'); - Object.setPrototypeOf(this, EndOfReaderError.prototype); } } diff --git a/packages/alphatab/src/model/Bar.ts b/packages/alphatab/src/model/Bar.ts index 978b0513c..c20057d29 100644 --- a/packages/alphatab/src/model/Bar.ts +++ b/packages/alphatab/src/model/Bar.ts @@ -490,6 +490,9 @@ export class Bar { if (this.previousBar && this.previousBar.sustainPedals.length > 0) { previousMarker = this.previousBar.sustainPedals[this.previousBar.sustainPedals.length - 1]; + if(previousMarker.pedalType === SustainPedalMarkerType.Up) { + previousMarker = null; + } } const isDown = previousMarker !== null && previousMarker.pedalType !== SustainPedalMarkerType.Up; diff --git a/packages/alphatab/src/model/ModelUtils.ts b/packages/alphatab/src/model/ModelUtils.ts index dba88d23c..f81d46fa6 100644 --- a/packages/alphatab/src/model/ModelUtils.ts +++ b/packages/alphatab/src/model/ModelUtils.ts @@ -26,6 +26,7 @@ export class TuningParseResult { } } + /** * @internal */ @@ -88,11 +89,14 @@ export class ModelUtils { return !!ModelUtils.parseTuning(name); } - private static readonly _tuningLetters = new Set([ + /** + * @internal + */ + public static readonly tuningLetters = new Set([ 0x43 /* C */, 0x44 /* D */, 0x45 /* E */, 0x46 /* F */, 0x47 /* G */, 0x41 /* A */, 0x42 /* B */, 0x63 /* c */, 0x64 /* d */, 0x65 /* e */, 0x66 /* f */, 0x67 /* g */, 0x61 /* a */, 0x62 /* b */, 0x23 /* # */ ]); - + public static parseTuning(name: string): TuningParseResult | null { let note: string = ''; let octave: string = ''; @@ -104,20 +108,29 @@ export class ModelUtils { return null; } octave += String.fromCharCode(c); - } else if (ModelUtils._tuningLetters.has(c)) { - note += String.fromCharCode(c); + } else if (note.length === 0) { + if (ModelUtils.tuningLetters.has(c)) { + note += String.fromCharCode(c); + } else { + return null; + } } else { - return null; + note += String.fromCharCode(c); } } if (!octave || !note) { return null; } + const result: TuningParseResult = new TuningParseResult(); result.octave = Number.parseInt(octave, 10) + 1; result.note = note.toLowerCase(); - result.tone = ModelUtils.getToneForText(result.note); + const tone = ModelUtils.getToneForText(result.note); + if (tone === null) { + return null; + } + result.tone = tone; // if tone.noteValue is negative (eg. on Cb note) // we adjust roll-over to a lower octave @@ -137,7 +150,7 @@ export class ModelUtils { return result.realValue; } - public static getToneForText(note: string): TuningParseResultTone { + public static getToneForText(note: string): TuningParseResultTone | null { const noteName = note.substring(0, 1); const accidental = note.substring(1); @@ -167,8 +180,11 @@ export class ModelUtils { noteValue = 11; break; default: - noteValue = 0; - break; + return null; + } + + if (!ModelUtils.accidentalModeMapping.has(accidental)) { + return null; } noteAccidenalMode = ModelUtils.parseAccidentalMode(accidental); @@ -196,12 +212,32 @@ export class ModelUtils { return new TuningParseResultTone(noteValue, noteAccidenalMode); } + /** + * @internal + */ + public static readonly reverseAccidentalModeMapping = new Map([ + [NoteAccidentalMode.Default, 'd'], + + [NoteAccidentalMode.ForceNone, 'forcenone'], + + [NoteAccidentalMode.ForceNatural, 'forcenatural'], + + [NoteAccidentalMode.ForceSharp, '#'], + + [NoteAccidentalMode.ForceDoubleSharp, 'x'], + + [NoteAccidentalMode.ForceFlat, 'b'], + + [NoteAccidentalMode.ForceDoubleFlat, 'bb'] + ]); + /** * @internal */ public static readonly accidentalModeMapping = new Map([ ['default', NoteAccidentalMode.Default], ['d', NoteAccidentalMode.Default], + ['', NoteAccidentalMode.Default], ['forcenone', NoteAccidentalMode.ForceNone], ['-', NoteAccidentalMode.ForceNone], @@ -717,6 +753,9 @@ export class ModelUtils { return keySignatures; } + /** + * @internal + */ private static readonly _keyTransposeTable: KeySignature[][] = ModelUtils._translateKeyTransposeTable([ /* Cb Gb Db Ab Eb Bb F C G D A E B F C# */ /* C 0 */ ['7b', '6b', '5b', '4b', '3b', '2b', '1b', '0#', '1#', '2#', '3#', '4#', '5#', '6#', '7#'], @@ -787,7 +826,7 @@ export class ModelUtils { * Contains the list of notes within an octave have accidentals set. * @internal */ - public static readonly accidentalNotes: boolean[] = [ + public static accidentalNotes: boolean[] = [ false, true, false, @@ -898,4 +937,14 @@ export class ModelUtils { return accidentalToSet; } + + + /** + * @internal + */ + public static toArticulationId(plain: string): string { + return plain.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + } + } + diff --git a/packages/alphatab/src/model/Score.ts b/packages/alphatab/src/model/Score.ts index 005284438..af4be4ed4 100644 --- a/packages/alphatab/src/model/Score.ts +++ b/packages/alphatab/src/model/Score.ts @@ -108,6 +108,22 @@ export class HeaderFooterStyle { this.textAlign = textAlign; } + public static equals(a: HeaderFooterStyle, b: HeaderFooterStyle) { + const aIsVisible = a.isVisible !== undefined ? a.isVisible! : true; + const bIsVisible = b.isVisible !== undefined ? b.isVisible! : true; + + if (aIsVisible !== bIsVisible) { + return false; + } + if (a.template !== b.template) { + return false; + } + if (a.textAlign !== b.textAlign) { + return false; + } + return true; + } + public buildText(score: Score) { let anyPlaceholderFilled = false; let anyPlaceholder = false; diff --git a/packages/alphatab/src/model/Staff.ts b/packages/alphatab/src/model/Staff.ts index a20f43a85..0fe245a80 100644 --- a/packages/alphatab/src/model/Staff.ts +++ b/packages/alphatab/src/model/Staff.ts @@ -125,6 +125,7 @@ export class Staff { if (this.isPercussion) { this.stringTuning.tunings = []; this.showTablature = false; + this.displayTranspositionPitch = 0; } this.stringTuning.finish(); for (let i: number = 0, j: number = this.bars.length; i < j; i++) { diff --git a/packages/alphatab/src/model/Tuning.ts b/packages/alphatab/src/model/Tuning.ts index 9c6cd95bd..a28020922 100644 --- a/packages/alphatab/src/model/Tuning.ts +++ b/packages/alphatab/src/model/Tuning.ts @@ -32,7 +32,8 @@ export class Tuning { */ public static getDefaultTuningFor(stringCount: number): Tuning | null { if (Tuning._defaultTunings.has(stringCount)) { - return Tuning._defaultTunings.get(stringCount)!; + const d = Tuning._defaultTunings.get(stringCount)!; + return new Tuning(d.name, d.tunings, d.isStandard); } return null; } @@ -133,7 +134,7 @@ export class Tuning { } } if (equals) { - return tuning; + return new Tuning(tuning.name, tuning.tunings, tuning.isStandard); } } return null; @@ -166,6 +167,13 @@ export class Tuning { this.tunings = tuning ?? []; } + public reset() { + this.isStandard = false; + this.name = ''; + this.tunings = []; + } + + /** * Tries to detect the name and standard flag of the tuning from a known tuning list based * on the string values. diff --git a/packages/alphatab/src/rendering/effects/TapEffectInfo.ts b/packages/alphatab/src/rendering/effects/TapEffectInfo.ts index c276f112b..ab13a27ae 100644 --- a/packages/alphatab/src/rendering/effects/TapEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/TapEffectInfo.ts @@ -36,12 +36,12 @@ export class TapEffectInfo extends EffectBarRendererInfo { public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { const res: RenderingResources = renderer.resources; if (beat.slap) { - return new TextGlyph(0, 0, 'S', res.effectFont, TextAlign.Left); + return new TextGlyph(0, 0, 'S', res.effectFont, TextAlign.Center); } if (beat.pop) { - return new TextGlyph(0, 0, 'P', res.effectFont, TextAlign.Left); + return new TextGlyph(0, 0, 'P', res.effectFont, TextAlign.Center); } - return new TextGlyph(0, 0, 'T', res.effectFont, TextAlign.Left); + return new TextGlyph(0, 0, 'T', res.effectFont, TextAlign.Center); } public canExpand(_from: Beat, _to: Beat): boolean { diff --git a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts index 6eb6001e1..a459a3d32 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -66,8 +66,8 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { ); if (accidentalToSet === AccidentalType.Natural) { - const ks: number = this.renderer.bar.keySignature; - const ksi: number = ks + 7; + const ks = this.renderer.bar.keySignature as number; + const ksi = ks + 7; const naturalizeAccidentalForKeySignature: AccidentalType = ksi < 7 ? AccidentalType.Sharp : AccidentalType.Flat; accidentalToSet = naturalizeAccidentalForKeySignature; @@ -247,9 +247,9 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { if (!this.container.beat.isEmpty) { let numberWithinOctave = '0'; if (this.container.beat.notes.length > 0) { - const kst: number = this.renderer.bar.keySignatureType; - const ks: number = this.renderer.bar.keySignature; - const ksi: number = ks + 7; + const kst = this.renderer.bar.keySignatureType; + const ks = this.renderer.bar.keySignature as number; + const ksi = ks + 7; const oneNoteValues = kst === KeySignatureType.Minor diff --git a/packages/alphatab/src/rendering/glyphs/SustainPedalGlyph.ts b/packages/alphatab/src/rendering/glyphs/SustainPedalGlyph.ts index c507aa61a..2004fb792 100644 --- a/packages/alphatab/src/rendering/glyphs/SustainPedalGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/SustainPedalGlyph.ts @@ -47,7 +47,7 @@ export class SustainPedalGlyph extends EffectGlyph { // line to next marker or end-of-bar if (marker.nextPedalMarker) { if (marker.nextPedalMarker.bar === marker.bar) { - let nextX = cx + this.renderer.getRatioPositionX(marker.nextPedalMarker.ratioPosition) - this.renderer.smuflMetrics.sustainPedalLinePadding; + let nextX = cx + this.renderer.getRatioPositionX(marker.nextPedalMarker.ratioPosition); switch (marker.nextPedalMarker.pedalType) { case SustainPedalMarkerType.Down: diff --git a/packages/alphatab/src/rendering/layout/ScoreLayout.ts b/packages/alphatab/src/rendering/layout/ScoreLayout.ts index 48d5ed529..d48cf05a6 100644 --- a/packages/alphatab/src/rendering/layout/ScoreLayout.ts +++ b/packages/alphatab/src/rendering/layout/ScoreLayout.ts @@ -210,7 +210,7 @@ export abstract class ScoreLayout { } break; case ScoreSubElement.CopyrightSecondLine: - if (!this.headerGlyphs.has(ScoreSubElement.Copyright)) { + if (!this.footerGlyphs.has(ScoreSubElement.Copyright)) { return undefined; } break; diff --git a/packages/alphatab/src/synth/ds/Queue.ts b/packages/alphatab/src/synth/ds/Queue.ts index 9ec2e6930..625ef07a5 100644 --- a/packages/alphatab/src/synth/ds/Queue.ts +++ b/packages/alphatab/src/synth/ds/Queue.ts @@ -39,6 +39,17 @@ export class Queue { } } + public enqueueFront(item: T) { + const queueItem = new QueueItem(item); + queueItem.next = this._head; + if (this._head) { + this._head = queueItem; + } else { + this._head = queueItem; + this._tail = queueItem; + } + } + public peek(): T | undefined { const head = this._head; if (!head) { @@ -53,7 +64,7 @@ export class Queue { return undefined; } - const newHead:QueueItem|undefined = head.next; + const newHead: QueueItem | undefined = head.next; this._head = newHead; // last item removed? if (!newHead) { diff --git a/packages/alphatab/src/util/Lazy.ts b/packages/alphatab/src/util/Lazy.ts index 8066d0330..da683c04a 100644 --- a/packages/alphatab/src/util/Lazy.ts +++ b/packages/alphatab/src/util/Lazy.ts @@ -6,6 +6,10 @@ export class Lazy { private _factory: () => T; private _value: T | undefined = undefined; + public get hasValue() { + return this._value !== undefined; + } + public constructor(factory: () => T) { this._factory = factory; } @@ -16,4 +20,8 @@ export class Lazy { } return this._value; } + + public reset() { + this._value = undefined; + } } diff --git a/packages/alphatab/src/xml/XmlError.ts b/packages/alphatab/src/xml/XmlError.ts index 25809ea43..048d49eef 100644 --- a/packages/alphatab/src/xml/XmlError.ts +++ b/packages/alphatab/src/xml/XmlError.ts @@ -34,6 +34,5 @@ export class XmlError extends AlphaTabError { super(AlphaTabErrorType.Format, message); this.xml = xml; this.pos = pos; - Object.setPrototypeOf(this, XmlError.prototype); } } diff --git a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex index 5a8f89a95..5799ff422 100644 --- a/packages/alphatab/test-data/exporter/notation-legend-formatted.atex +++ b/packages/alphatab/test-data/exporter/notation-legend-formatted.atex @@ -2,351 +2,296 @@ \artist "alphaTab" \subtitle "for test suite" \title "Notation Legend" -\tempo 60 \defaultSystemsLayout 4 \systemsLayout 4 // Score Stylesheet -\bracketExtendMode NoBrackets -\otherSystemsTrackNameOrientation Horizontal -. - -\track "Distortion Guitar" "dist.guit." { +\bracketExtendMode nobrackets +\otherSystemsTrackNameOrientation horizontal +\track ("Distortion Guitar" "dist.guit.") { // Track Properties - color "#EB987D" - systemsLayout 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 - volume 12 - balance 8 - instrument distortionguitar + color "#EB987D" + systemsLayout (3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1) + volume 12 + balance 8 + instrument distortionguitar } - \staff {/* Staff Properties */score tabs} + \staff { + // Staff Properties + score + tabs + } + // Staff 1 Metadata + \tuning (E4 B3 G3 D3 A2 E2) { + label "Guitar Standard Tuning" + } + \chord {firstfret 1 showdiagram true showfingering false showname true} "C9" 3 3 3 2 3 x + \chord {firstfret 1 showdiagram true showfingering false showname true} "C" 0 1 0 2 3 x + \chord {firstfret 1 showdiagram true showfingering false showname true} "G" 3 0 0 0 2 3 + \chord {firstfret 1 showdiagram true showfingering false showname true barre (1 1)} "Fm6" 1 3 1 3 3 1 + \chord {firstfret 3 showdiagram true showfingering false showname true barre (3 3)} "C7sus4" 3 6 3 5 3 x + \chord {firstfret 1 showdiagram true showfingering false showname true} "C5" x x x 5 3 x + \chord {firstfret 1 showdiagram true showfingering false showname true} "G(no5)/B" x x x 5 2 x \voice /* Voice 1 */ - // Staff 1 Metadata - \tuning E4 B3 G3 D3 A2 E2 "Guitar Standard Tuning" - \accidentals auto - \chord {firstfret 1 showdiagram true showfingering false showname true } "C9" 3 3 3 2 3 -1 - \chord {firstfret 1 showdiagram true showfingering false showname true } "C" 0 1 0 2 3 -1 - \chord {firstfret 1 showdiagram true showfingering false showname true } "G" 3 0 0 0 2 3 - \chord {firstfret 1 showdiagram true showfingering false showname true barre 1 1 } "Fm6" 1 3 1 3 3 1 - \chord {firstfret 3 showdiagram true showfingering false showname true barre 3 3 } "C7sus4" 3 6 3 5 3 -1 - \chord {firstfret 1 showdiagram true showfingering false showname true } "C5" -1 -1 -1 5 3 -1 - \chord {firstfret 1 showdiagram true showfingering false showname true } "G(no5)/B" -1 -1 -1 5 2 -1 // Masterbar 1 Metadata - \ts 4 4 - \tempo ( 60 0 ) - - // Bar 1 - // Bar 1 Metadata - \clef G2 - \ottava Regular - \simile None - \ks "C" - + \ts (4 4) + \tempo 60 + // Bar 1 Metadata + \accidentals auto + \clef g2 + \ottava regular + \simile none + \ks c // Bar 1 / Voice 1 contents - 9.4{be Bend(0 0 15.6 1)}.4{dy mf instrument distortionguitar beam Down} - 9.4{be Bend(0 0 15.6 2)}.4{beam Down} - 9.4{be Bend(0 0 14.399999999999999 4)}.4{beam Down} - 9.4{be Bend(0 0 15.6 6)}.8{beam Up} - 9.4{be Bend(0 0 15.6 8)}.8{beam Up} + 9.4{be (bend 0 0 15.6 1)}.4{dy mf instrument distortionguitar beam Down} + 9.4{be (bend 0 0 15.6 2)}.4{beam Down} + 9.4{be (bend 0 0 14.399999999999999 4)}.4{beam Down} + 9.4{be (bend 0 0 15.6 6)}.8{beam Up} + 9.4{be (bend 0 0 15.6 8)}.8{beam Up} | - // Bar 2 // Bar 2 / Voice 1 contents - 9.4{be Bend(0 0 60 4)}.2{beam Down} + 9.4{be (bend 0 0 60 4)}.2{beam Down} 9.4{t}.2{beam Down} | - // Bar 3 // Bar 3 / Voice 1 contents - 7.3{be Bend(0 0 59.4 4)}.4{beam Down} - 7.3{be Release(0 4 59.4 0) t}.4{beam Down} + 7.3{be (bend 0 0 59.4 4)}.4{beam Down} + 7.3{be (release 0 4 59.4 0) t}.4{beam Down} 7.3{t}.2{beam Down} | - // Bar 4 // Bar 4 / Voice 1 contents - 7.3{be Bend(0 0 59.4 4)}.8{gr ob beam Up} - 7.3{be Release(0 4 59.4 0) t}.4{beam Down} + 7.3{be (bend 0 0 59.4 4)}.8{gr ob beam Up} + 7.3{be (release 0 4 59.4 0) t}.4{beam Down} 7.3{t}.2{d beam Down} | - // Bar 5 // Bar 5 / Voice 1 contents - 7.3{be Bend(0 0 15 4)}.1{txt "grad." beam Down} + 7.3{be (bend 0 0 15 4)}.1{txt "grad." beam Down} | - // Bar 6 // Bar 6 / Voice 1 contents - 7.3{be Bend(0 0 15 4)}.2{txt "grad." beam Down} + 7.3{be (bend 0 0 15 4)}.2{txt "grad." beam Down} 7.3{t}.2{beam Down} | - // Bar 7 // Bar 7 / Voice 1 contents - 7.3{be Bend(0 0 59.4 4)}.4{beam Down} - 7.3{be Release(0 4 59.4 0) t}.4{txt "grad." beam Down} + 7.3{be (bend 0 0 59.4 4)}.4{beam Down} + 7.3{be (release 0 4 59.4 0) t}.4{txt "grad." beam Down} 7.3{t}.2{beam Down} | - // Bar 8 // Bar 8 / Voice 1 contents - 7.3{be Bend(0 0 59.4 4)}.8{gr ob beam Up} - 7.3{be Release(0 4 59.4 0) t}.4{txt "grad." beam Down} + 7.3{be (bend 0 0 59.4 4)}.8{gr ob beam Up} + 7.3{be (release 0 4 59.4 0) t}.4{txt "grad." beam Down} 7.3{t}.2{d beam Down} | - // Bar 9 // Bar 9 / Voice 1 contents - 7.3{be Prebend(0 4 60 4)}.1{beam Down} + 7.3{be (prebend 0 4 60 4)}.1{beam Down} | - // Bar 10 // Bar 10 / Voice 1 contents - 7.3{be PrebendRelease(0 4 15 0)}.2{beam Down} + 7.3{be (prebendrelease 0 4 15 0)}.2{beam Down} 7.3{t}.2{beam Down} | - // Bar 11 // Bar 11 / Voice 1 contents - 7.3{be PrebendRelease(0 4 15 0)}.1{txt "grad." beam Down} + 7.3{be (prebendrelease 0 4 15 0)}.1{txt "grad." beam Down} | - // Bar 12 // Bar 12 / Voice 1 contents - 7.3{be PrebendRelease(0 4 15 0)}.8{gr ob beam Up} + 7.3{be (prebendrelease 0 4 15 0)}.8{gr ob beam Up} 7.3{t}.1{beam Down} | - // Bar 13 // Bar 13 / Voice 1 contents - 7.3{be PrebendBend(0 4 15 6)}.4{d beam Down} + 7.3{be (prebendbend 0 4 15 6)}.4{d beam Down} 7.3{t}.8{beam Down} 7.3{t}.2{beam Down} | - // Bar 14 // Bar 14 / Voice 1 contents - 7.3{be PrebendBend(0 4 15 6)}.8{gr ob beam Up} + 7.3{be (prebendbend 0 4 15 6)}.8{gr ob beam Up} 7.3{t}.1{beam Down} | - // Bar 15 // Bar 15 / Voice 1 contents - 7.3{be PrebendBend(0 4 15 6)}.1{txt "grad." beam Down} + 7.3{be (prebendbend 0 4 15 6)}.1{txt "grad." beam Down} | - // Bar 16 // Bar 16 / Voice 1 contents - (7.2{be Bend(0 0 14.399999999999999 2) acc ForceSharp} 7.3{be Bend(0 0 15 4)}).1{beam Down} + (7.2{be (bend 0 0 14.399999999999999 2) acc #} 7.3{be (bend 0 0 15 4)}).1{beam Down} | - // Bar 17 // Bar 17 / Voice 1 contents - (7.2{be Bend(0 0 14.399999999999999 2) acc ForceSharp} 7.3{be Bend(0 0 15 4)}).4{beam Down} - (7.2{be Release(0 2 15 0) t} 7.3{be Release(0 4 15 0) t}).4{beam Down} - (7.3{t} 7.2{t acc ForceSharp}).2{beam Down} + (7.2{be (bend 0 0 14.399999999999999 2) acc #} 7.3{be (bend 0 0 15 4)}).4{beam Down} + (7.2{be (release 0 2 15 0) t} 7.3{be (release 0 4 15 0) t}).4{beam Down} + (7.3{t} 7.2{t acc #}).2{beam Down} | - // Bar 18 // Bar 18 / Voice 1 contents - (7.2{be Bend(0 0 14.399999999999999 2) acc ForceSharp} 7.3{be Bend(0 0 15 4)}).8{gr ob beam Up} - (7.2{be Release(0 2 15 0) t} 7.3{be Release(0 4 15 0) t}).4{beam Down} - (7.3{t} 7.2{t acc ForceSharp}).2{d beam Down} + (7.2{be (bend 0 0 14.399999999999999 2) acc #} 7.3{be (bend 0 0 15 4)}).8{gr ob beam Up} + (7.2{be (release 0 2 15 0) t} 7.3{be (release 0 4 15 0) t}).4{beam Down} + (7.3{t} 7.2{t acc #}).2{d beam Down} | - // Bar 19 // Bar 19 / Voice 1 contents - (10.2 12.3{be Bend(0 0 15 4)}).1{beam Down} + (10.2 12.3{be (bend 0 0 15 4)}).1{beam Down} | - // Bar 20 // Bar 20 / Voice 1 contents - (10.2{be Bend(0 0 15 4)} 12.3{be Bend(0 0 15 4)}).2{txt "grad." beam Down} - (10.2{be Hold(0 4 60 4) t} 12.3{be Bend(0 4 15 8) t}).4{beam Down} - (10.2{t} 12.3{be Hold(0 8 60 8) t}).4{beam Down} + (10.2{be (bend 0 0 15 4)} 12.3{be (bend 0 0 15 4)}).2{txt "grad." beam Down} + (10.2{be (hold 0 4 60 4) t} 12.3{be (bend 0 4 15 8) t}).4{beam Down} + (10.2{t} 12.3{be (hold 0 8 60 8) t}).4{beam Down} | - // Bar 21 // Bar 21 / Voice 1 contents - 12.3{be Bend(0 0 15 4)}.2{beam Down} - (15.2 12.3{t}).2{beam Down} + 12.3{be (bend 0 0 15 4)}.2{beam Down} + (15.2 12.3{t}).2{beam Down} | - // Bar 22 // Bar 22 / Voice 1 contents - 7.3{be Bend(0 0 15 4)}.8{gr ob beam Up} - 7.3{be Hold(0 4 60 4) t}.4{beam Down} - (7.3{be Release(0 4 15 0) lr t} 8.2{lr}).4{beam Down} + 7.3{be (bend 0 0 15 4)}.8{gr ob beam Up} + 7.3{be (hold 0 4 60 4) t}.4{beam Down} + (7.3{be (release 0 4 15 0) lr t} 8.2{lr}).4{beam Down} 7.3{lr t}.2{beam Down} | - // Bar 23 // Bar 23 / Voice 1 contents - 7.3{be Bend(0 0 15 4)}.8{gr ob beam Up} - 7.3{be Hold(0 4 60 4) t}.4{beam Up} - 7.3{be Release(0 4 15 0) t}.4{beam Up} + 7.3{be (bend 0 0 15 4)}.8{gr ob beam Up} + 7.3{be (hold 0 4 60 4) t}.4{beam Up} + 7.3{be (release 0 4 15 0) t}.4{beam Up} 7.3{t}.2{beam Up} | - // Bar 24 // Bar 24 / Voice 1 contents - 7.3{be Bend(0 0 15 4)}.8{gr ob beam Up} - 7.3{be Hold(0 4 60 4) t}.4{beam Up} - 7.3{be Hold(0 4 60 4) t}.4{beam Up} - 7.3{be PrebendRelease(0 4 15 0)}.8{beam Up} + 7.3{be (bend 0 0 15 4)}.8{gr ob beam Up} + 7.3{be (hold 0 4 60 4) t}.4{beam Up} + 7.3{be (hold 0 4 60 4) t}.4{beam Up} + 7.3{be (prebendrelease 0 4 15 0)}.8{beam Up} 7.3{t}.4{d beam Up} | - // Bar 25 // Bar 25 / Voice 1 contents - 7.3{be Prebend(0 4 60 4)}.4{beam Up} - 7.3{be Hold(0 4 60 4) t}.4{beam Up} - 7.3{be PrebendRelease(0 4 15 0)}.2{beam Up} + 7.3{be (prebend 0 4 60 4)}.4{beam Up} + 7.3{be (hold 0 4 60 4) t}.4{beam Up} + 7.3{be (prebendrelease 0 4 15 0)}.2{beam Up} | - // Bar 26 // Bar 26 / Voice 1 contents - 7.3{be Prebend(0 4 60 4)}.4{beam Up} - 7.3{be Release(0 4 15 0) t}.4{beam Up} + 7.3{be (prebend 0 4 60 4)}.4{beam Up} + 7.3{be (release 0 4 15 0) t}.4{beam Up} 7.3{t}.2{beam Up} | - // Bar 27 // Bar 27 / Voice 1 contents - 7.3{be Prebend(0 4 60 4)}.4{beam Up} - 7.3{be Hold(0 4 60 4) t}.4{beam Up} - 7.3{be PrebendRelease(0 4 15 0)}.4{beam Up} + 7.3{be (prebend 0 4 60 4)}.4{beam Up} + 7.3{be (hold 0 4 60 4) t}.4{beam Up} + 7.3{be (prebendrelease 0 4 15 0)}.4{beam Up} 7.3{t}.4{beam Up} | - // Bar 28 // Bar 28 / Voice 1 contents - 7.3{be Bend(0 0 15 4)}.8{beam Down} - 7.3{be Bend(0 4 15 8) t}.8{beam Down} - 7.3{be Release(0 8 15 4) t}.8{beam Down} - 7.3{be Release(0 4 15 0) t}.8{beam Down} + 7.3{be (bend 0 0 15 4)}.8{beam Down} + 7.3{be (bend 0 4 15 8) t}.8{beam Down} + 7.3{be (release 0 8 15 4) t}.8{beam Down} + 7.3{be (release 0 4 15 0) t}.8{beam Down} 7.3{t}.2{beam Down} | - // Bar 29 // Bar 29 / Voice 1 contents 7.3.8{beam Down} - 7.3{be Prebend(0 4 60 4)}.8{beam Down} - 7.3{be Prebend(0 8 60 8)}.8{beam Down} - 7.3{be Prebend(0 4 60 4)}.8{beam Down} + 7.3{be (prebend 0 4 60 4)}.8{beam Down} + 7.3{be (prebend 0 8 60 8)}.8{beam Down} + 7.3{be (prebend 0 4 60 4)}.8{beam Down} 7.3.8{beam Down} 5.3.4{d beam Down} | - // Bar 30 // Bar 30 / Voice 1 contents 5.3{h}.8{gr ob beam Up} 7.3.1{beam Down} | - // Bar 31 // Bar 31 / Voice 1 contents 5.3{sl}.8{gr ob beam Up} 7.3.1{beam Down} | - // Bar 32 // Bar 32 / Voice 1 contents 7.3{v}.1{beam Down} | - // Bar 33 // Bar 33 / Voice 1 contents 7.3{vw}.1{beam Down} | - // Bar 34 // Bar 34 / Voice 1 contents 7.3.1{v beam Down} | - // Bar 35 // Bar 35 / Voice 1 contents 7.3.1{vw beam Down} | - // Bar 36 // Bar 36 / Voice 1 contents 12.3{x}.16{sd gr ob beam Up} 12.2{x}.16{sd gr ob beam Up} 12.1.1{sd beam Down} | - // Bar 37 // Bar 37 / Voice 1 contents 5.1{x}.16{sd gr ob beam Up} 5.2{x}.16{sd gr ob beam Up} 5.3.1{sd beam Down} | - // Bar 38 // Bar 38 / Voice 1 contents 12.3.16{sd gr ob beam Up} 12.2.16{sd gr ob beam Up} 12.1.1{sd beam Down} | - // Bar 39 // Bar 39 / Voice 1 contents 5.1.16{sd gr ob beam Up} 5.2.16{sd gr ob beam Up} 5.3.1{sd beam Down} | - // Bar 40 // Bar 40 / Voice 1 contents 1.6.8{sd beam Up} 3.6.8{su beam Up} - 4.6{acc ForceFlat}.8{sd beam Up} + 4.6{acc b}.8{sd beam Up} 3.6.8{su beam Up} 1.6.8{sd beam Up} 3.6.8{su beam Up} - 4.6{acc ForceFlat}.8{sd beam Up} + 4.6{acc b}.8{sd beam Up} 3.6.8{su beam Up} | // Masterbar 41 Metadata - \ts 1 4 - - // Bar 41 + \ts (1 4) // Bar 41 / Voice 1 contents 5.3{sl}.8{beam Down} 7.3.8{beam Down} | - // Bar 42 // Bar 42 / Voice 1 contents 5.3{ss}.8{beam Down} 7.3.8{beam Down} | - // Bar 43 // Bar 43 / Voice 1 contents 7.3{sl}.8{beam Down} 5.3.8{beam Down} | - // Bar 44 // Bar 44 / Voice 1 contents 7.3{ss}.8{beam Down} 5.3.8{beam Down} | - // Bar 45 // Bar 45 / Voice 1 contents 7.3{sib}.4{beam Down} | - // Bar 46 // Bar 46 / Voice 1 contents 7.3{sia}.4{beam Down} | - // Bar 47 // Bar 47 / Voice 1 contents 7.3{sod}.4{beam Down} | - // Bar 48 // Bar 48 / Voice 1 contents 7.3{sou}.4{beam Down} | // Masterbar 49 Metadata - \ts 4 4 - - // Bar 49 + \ts (4 4) // Bar 49 / Voice 1 contents 5.4{h}.2{beam Up} 7.4.2{beam Up} | - // Bar 50 // Bar 50 / Voice 1 contents 7.4{h}.2{beam Up} 5.4.2{beam Up} | - // Bar 51 // Bar 51 / Voice 1 contents 5.3{h}.8{beam Down} 7.3{sl}.8{beam Down} - 8.3{sl acc ForceSharp}.8{beam Down} + 8.3{sl acc #}.8{beam Down} 7.3{h}.8{beam Down} 5.3{sl}.8{beam Down} - 3.3{h acc ForceSharp}.8{beam Down} + 3.3{h acc #}.8{beam Down} 5.3{h}.8{beam Down} - 3.3{acc ForceSharp}.8{beam Down} + 3.3{acc #}.8{beam Down} | - // Bar 52 // Bar 52 / Voice 1 contents 5.3{h}.8{beam Down} - 6.3{h acc ForceSharp}.8{beam Down} + 6.3{h acc #}.8{beam Down} 7.3{h}.8{beam Down} - 8.3{h acc ForceSharp}.8{beam Down} + 8.3{h acc #}.8{beam Down} 7.3{h}.8{beam Down} - 6.3{h acc ForceSharp}.8{beam Down} + 6.3{h acc #}.8{beam Down} 5.3.4{beam Down} | - // Bar 53 // Bar 53 / Voice 1 contents 7.3{g}.1{beam Down} | - // Bar 54 // Bar 54 / Voice 1 contents 7.5{st}.8{beam Up} 7.5{st}.8{beam Up} @@ -357,7 +302,6 @@ 7.5{st}.8{beam Up} 7.5{st}.8{beam Up} | - // Bar 55 // Bar 55 / Voice 1 contents 7.5{hac}.8{beam Up} 7.5.8{beam Up} @@ -368,7 +312,6 @@ 7.5{hac}.8{beam Up} 7.5{hac}.8{beam Up} | - // Bar 56 // Bar 56 / Voice 1 contents 7.5{ac}.8{beam Up} 7.5.8{beam Up} @@ -379,7 +322,6 @@ 7.5{ac}.8{beam Up} 7.5{ac}.8{beam Up} | - // Bar 57 // Bar 57 / Voice 1 contents 7.5{pm}.8{beam Up} 7.5{pm}.8{beam Up} @@ -390,17 +332,14 @@ 7.5.8{beam Up} 7.5{pm}.8{beam Up} | - // Bar 58 // Bar 58 / Voice 1 contents 7.5.4{tp 32 beam Up} 8.5.4{tp 32 beam Up} - 9.5{acc ForceSharp}.2{tp 32 beam Up} + 9.5{acc #}.2{tp 32 beam Up} | - // Bar 59 // Bar 59 / Voice 1 contents - 7.3{tr 9 16 tr 9 16}.1{beam Down} + 7.3{tr (9 16)}.1{beam Down} | - // Bar 60 // Bar 60 / Voice 1 contents 7.5{x}.8{beam Up} 7.5{x}.8{beam Up} @@ -411,149 +350,122 @@ 7.5{x}.8{beam Up} 7.5{x}.8{beam Up} | - // Bar 61 // Bar 61 / Voice 1 contents - (7.5{psd x} 6.4{psd x acc ForceSharp}).1{beam Up} + (7.5{psd x} 6.4{psd x acc #}).1{beam Up} | - // Bar 62 // Bar 62 / Voice 1 contents 12.2{nh}.4{beam Down} 12.3{nh}.4{beam Down} 12.1{nh}.4{beam Down} 12.2{nh}.4{beam Down} | - // Bar 63 // Bar 63 / Voice 1 contents - (12.2 12.1{nh}).4{beam Down} + (12.2 12.1{nh}).4{beam Down} 12.3.4{beam Down} 12.1.4{beam Down} 12.2.4{beam Down} | - // Bar 64 // Bar 64 / Voice 1 contents 5.2{ph 12}.4{beam Down} - 7.2{ph 12 acc ForceSharp}.4{beam Down} + 7.2{ph 12 acc #}.4{beam Down} 8.2.4{beam Down} 10.2{ph 12}.4{beam Down} | - // Bar 65 // Bar 65 / Voice 1 contents 5.2{th 12}.4{beam Down} - 7.2{th 12 acc ForceSharp}.4{beam Down} + 7.2{th 12 acc #}.4{beam Down} 8.2.4{beam Down} 10.2{th 12}.4{beam Down} | - // Bar 66 // Bar 66 / Voice 1 contents 5.2{sh 12}.1{beam Down} | - // Bar 67 // Bar 67 / Voice 1 contents 5.2{ah 5}.1{beam Down} | - // Bar 68 // Bar 68 / Voice 1 contents 5.2{fh 12}.1{beam Down} | - // Bar 69 // Bar 69 / Voice 1 contents - 5.6{h}.16{tu 6 4 beam Up} - 9.6{h acc ForceSharp}.16{tu 6 4 beam Up} - 12.6{h}.16{tt tu 6 4 beam Up} - 7.4.16{tu 6 4 beam Up} - 9.6{h acc ForceSharp}.16{tu 6 4 beam Up} - 12.6.16{tt tu 6 4 beam Up} + 5.6{h}.16{tu (6 4) beam Up} + 9.6{h acc #}.16{tu (6 4) beam Up} + 12.6{h}.16{tt tu (6 4) beam Up} + 7.4.16{tu (6 4) beam Up} + 9.6{h acc #}.16{tu (6 4) beam Up} + 12.6.16{tt tu (6 4) beam Up} r.2{d beam Up} | - // Bar 70 - // Bar 70 Metadata - \simile Simple - - // empty bar - | - // Bar 71 - // Bar 71 Metadata - \simile FirstOfDouble - - // empty bar - | - // Bar 72 - // Bar 72 Metadata - \simile SecondOfDouble - - // empty bar - | - // Bar 73 - // Bar 73 Metadata - \simile None - + // Bar 70 Metadata + \simile simple + // Bar 70 / Voice 1 no contents + | + // Bar 71 Metadata + \simile firstofdouble + // Bar 71 / Voice 1 no contents + | + // Bar 72 Metadata + \simile secondofdouble + // Bar 72 / Voice 1 no contents + | + // Bar 73 Metadata + \simile none // Bar 73 / Voice 1 contents 1.1.8{dy mf beam Up} - 2.1{acc ForceSharp}.8{beam Up} + 2.1{acc #}.8{beam Up} 3.1.8{beam Up} - 4.1{acc ForceSharp}.8{beam Up} + 4.1{acc #}.8{beam Up} 3.1.4{beam Up} 1.1.8{beam Up} - 4.1{acc ForceSharp}.8{beam Up} + 4.1{acc #}.8{beam Up} | - // Bar 74 // Bar 74 / Voice 1 contents - (0.6 2.5 4.4{acc ForceSharp} 0.3 0.2 0.1).1{ad 0 beam Up} + (0.6 2.5 4.4{acc #} 0.3 0.2 0.1).1{ad 0 beam Up} | - // Bar 75 // Bar 75 / Voice 1 contents - (0.6 2.5 4.4{acc ForceSharp} 0.3 0.2 0.1).1{au 0 beam Up} + (0.6 2.5 4.4{acc #} 0.3 0.2 0.1).1{au 0 beam Up} | // Masterbar 76 Metadata - \tf Triplet8th - - // Bar 76 + \tf triplet8th // Bar 76 / Voice 1 contents - 0.6.8{tu 3 2 beam Up} - 0.6.8{tu 3 2 beam Up} - 0.6.8{tu 3 2 beam Up} - 4.6{acc ForceSharp}.8{beam Up} - 4.6{acc ForceSharp}.8{beam Up} + 0.6.8{tu (3 2) beam Up} + 0.6.8{tu (3 2) beam Up} + 0.6.8{tu (3 2) beam Up} + 4.6{acc #}.8{beam Up} + 4.6{acc #}.8{beam Up} 5.6.8{beam Up} 5.6.8{beam Up} - 1.5{acc ForceSharp}.8{tu 3 2 beam Up} - 1.5{acc ForceSharp}.8{tu 3 2 beam Up} - 1.5{acc ForceSharp}.8{tu 3 2 beam Up} + 1.5{acc #}.8{tu (3 2) beam Up} + 1.5{acc #}.8{tu (3 2) beam Up} + 1.5{acc #}.8{tu (3 2) beam Up} | - // Bar 77 // Bar 77 / Voice 1 contents 2.5.8{beam Up} 2.5.8{beam Up} 0.5.8{beam Up} 0.5.8{beam Up} - 4.6{acc ForceSharp}.8{beam Up} - 4.6{acc ForceSharp}.8{beam Up} - 2.6{acc ForceSharp}.8{beam Up} - 2.6{acc ForceSharp}.8{beam Up} + 4.6{acc #}.8{beam Up} + 4.6{acc #}.8{beam Up} + 2.6{acc #}.8{beam Up} + 2.6{acc #}.8{beam Up} | - // Bar 78 // Bar 78 / Voice 1 contents - 0.6.1{fermata Medium 0.5 beam Up} + 0.6.1{fermata (medium 0.5) beam Up} | // Masterbar 79 Metadata - \tf NoTripletFeel - - // Bar 79 + \tf notripletfeel // Bar 79 / Voice 1 contents - 21.1{acc ForceSharp}.8{ot 8va beam Down} - 21.1{acc ForceSharp}.8{ot 8va beam Down} - 21.1{acc ForceSharp}.8{ot 8va beam Down} - 21.1{acc ForceSharp}.8{ot 8va beam Down} - 21.1{acc ForceSharp}.8{beam Down} - 21.1{acc ForceSharp}.8{ot 8va beam Down} - 21.1{acc ForceSharp}.8{beam Down} - 21.1{acc ForceSharp}.8{ot 8va beam Down} - | - // Bar 80 + 21.1{acc #}.8{ot 8va beam Down} + 21.1{acc #}.8{ot 8va beam Down} + 21.1{acc #}.8{ot 8va beam Down} + 21.1{acc #}.8{ot 8va beam Down} + 21.1{acc #}.8{beam Down} + 21.1{acc #}.8{ot 8va beam Down} + 21.1{acc #}.8{beam Down} + 21.1{acc #}.8{ot 8va beam Down} + | // Bar 80 / Voice 1 contents 24.1.1{ot 15ma beam Down} | - // Bar 81 // Bar 81 / Voice 1 contents 7.5.8{cre beam Up} 7.5.8{cre beam Up} @@ -565,9 +477,7 @@ 7.5.8{dec beam Up} | // Masterbar 82 Metadata - \tempo ( 164 0 ) - - // Bar 82 + \tempo 164 // Bar 82 / Voice 1 contents 7.5.8{beam Up} 7.5.8{beam Up} @@ -579,9 +489,7 @@ 7.5.8{beam Up} | // Masterbar 83 Metadata - \ts 5 8 - - // Bar 83 + \ts (5 8) // Bar 83 / Voice 1 contents 7.5.8{beam Up} 7.5.8{beam Up} @@ -589,10 +497,8 @@ r.4{beam Up} | // Masterbar 84 Metadata - \ts 4 4 - \tempo ( 60 0 ) - - // Bar 84 + \ts (4 4) + \tempo 60 // Bar 84 / Voice 1 contents 7.5.8{beam Up} 7.5.8{beam Up} @@ -603,65 +509,57 @@ 7.5.8{beam Up} 7.5.8{beam Up} | - // Bar 85 // Bar 85 / Voice 1 contents - (3.1 3.2 0.3 5.4 5.5 3.6{lf 1}).1{beam Up} + (3.1 3.2 0.3 5.4 5.5 3.6{lf 1}).1{beam Up} | - // Bar 86 // Bar 86 / Voice 1 contents - (0.5 2.4 2.3 2.2{acc ForceSharp} 0.1).8{beam Up} - (0.5 2.4 2.3 2.2{acc ForceSharp} 0.1).8{slashed beam Up} - (0.5 2.4 2.3 2.2{acc ForceSharp} 0.1).4{slashed beam Up} - (0.5 2.4 2.3 2.2{acc ForceSharp} 0.1).4{slashed beam Up} - (0.5 2.4 2.3 2.2{acc ForceSharp} 0.1).8{slashed beam Up} - (0.5 2.4 2.3 2.2{acc ForceSharp} 0.1).8{slashed beam Up} - | - // Bar 87 + (0.5 2.4 2.3 2.2{acc #} 0.1).8{beam Up} + (0.5 2.4 2.3 2.2{acc #} 0.1).8{slashed beam Up} + (0.5 2.4 2.3 2.2{acc #} 0.1).4{slashed beam Up} + (0.5 2.4 2.3 2.2{acc #} 0.1).4{slashed beam Up} + (0.5 2.4 2.3 2.2{acc #} 0.1).8{slashed beam Up} + (0.5 2.4 2.3 2.2{acc #} 0.1).8{slashed beam Up} + | // Bar 87 / Voice 1 contents - 5.5.8{txt "V." beam Up} - 6.5{acc ForceSharp}.8{beam Up} + 5.5.8{txt "V." beam Up} + 6.5{acc #}.8{beam Up} 8.5.8{beam Up} - 10.5.8{txt "X." beam Up} + 10.5.8{txt "X." beam Up} 12.5.4{beam Up} - 13.5{acc ForceSharp}.4{beam Up} + 13.5{acc #}.4{beam Up} | - // Bar 88 // Bar 88 / Voice 1 contents - (3.5 2.4 3.3{acc ForceSharp} 3.2 3.1).4{ch "C9" beam Up} - (3.5 2.4 0.3 1.2 0.1).8{ch "C" beam Up} - (3.5 2.4 0.3 1.2 0.1).8{beam Up} - (3.6 2.5 0.4 0.3 0.2 3.1).8{ch "G" beam Up} - (3.6 2.5 0.4 0.3 0.2 3.1).8{beam Up} - (1.6 3.5 3.4 1.3{acc ForceSharp} 3.2 1.1).8{ch "Fm6" beam Up} - (1.6 3.5 3.4 1.3{acc ForceSharp} 3.2 1.1).8{beam Up} - | - // Bar 89 + (3.5 2.4 3.3{acc #} 3.2 3.1).4{ch "C9" beam Up} + (3.5 2.4 0.3 1.2 0.1).8{ch "C" beam Up} + (3.5 2.4 0.3 1.2 0.1).8{beam Up} + (3.6 2.5 0.4 0.3 0.2 3.1).8{ch "G" beam Up} + (3.6 2.5 0.4 0.3 0.2 3.1).8{beam Up} + (1.6 3.5 3.4 1.3{acc #} 3.2 1.1).8{ch "Fm6" beam Up} + (1.6 3.5 3.4 1.3{acc #} 3.2 1.1).8{beam Up} + | // Bar 89 / Voice 1 contents - (3.5 5.4 3.3{acc ForceFlat} 6.2 3.1).4{ch "C7sus4" beam Up} - (3.5 5.4).4{ch "C5" beam Up} - (2.5 5.4).2{ch "G(no5)/B" beam Up} + (3.5 5.4 3.3{acc b} 6.2 3.1).4{ch "C7sus4" beam Up} + (3.5 5.4).4{ch "C5" beam Up} + (2.5 5.4).2{ch "G(no5)/B" beam Up} | - // Bar 90 // Bar 90 / Voice 1 contents 5.5.8{beam Up} - 6.5{acc ForceSharp}.8{beam Up} + 6.5{acc #}.8{beam Up} 8.5.8{beam Up} 10.5.8{beam Up} 12.5.4{beam Up} - 13.5{acc ForceSharp}.4{beam Up} + 13.5{acc #}.4{beam Up} | - // Bar 91 // Bar 91 / Voice 1 contents 5.5.8{waho beam Up} - 6.5{acc ForceSharp}.8{wahc beam Up} + 6.5{acc #}.8{wahc beam Up} 8.5.8{waho beam Up} 10.5.8{wahc beam Up} 12.5.4{waho beam Up} - 13.5{acc ForceSharp}.4{wahc beam Up} + 13.5{acc #}.4{wahc beam Up} | - // Bar 92 // Bar 92 / Voice 1 contents - 4.4{acc ForceSharp}.8{dy ppp beam Up} + 4.4{acc #}.8{dy ppp beam Up} 5.4.8{dy pp beam Up} 5.4.8{dy p beam Up} 5.4.8{dy mp beam Up} @@ -670,7 +568,6 @@ 5.4.8{dy ff beam Up} 5.4.8{dy fff beam Up} | - // Bar 93 // Bar 93 / Voice 1 contents 8.1.32{su dy ff beam Down} 8.2.32{su beam Down} @@ -689,138 +586,120 @@ 8.2.32{sd beam Down} 8.1.32{sd beam Down} 7.1.32{su beam Down} - 7.2{acc ForceSharp}.32{su beam Down} - 8.3{acc ForceSharp}.32{su beam Down} + 7.2{acc #}.32{su beam Down} + 8.3{acc #}.32{su beam Down} 9.4.32{su beam Down} 9.4.32{sd beam Down} - 8.3{acc ForceSharp}.32{sd beam Down} - 7.2{acc ForceSharp}.32{sd beam Down} + 8.3{acc #}.32{sd beam Down} + 7.2{acc #}.32{sd beam Down} 7.1.32{sd beam Down} - 6.1{acc ForceSharp}.32{su beam Down} + 6.1{acc #}.32{su beam Down} 6.2.32{su beam Down} 7.3.32{su beam Down} - 8.4{acc ForceSharp}.32{su beam Down} - 8.4{acc ForceSharp}.32{sd beam Down} + 8.4{acc #}.32{su beam Down} + 8.4{acc #}.32{sd beam Down} 7.3.32{sd beam Down} 6.2.32{sd beam Down} - 6.1{acc ForceSharp}.32{sd beam Down} + 6.1{acc #}.32{sd beam Down} | - // Bar 94 // Bar 94 / Voice 1 contents 5.5{lf 2}.8{dy mf beam Up} - 6.5{lf 3 acc ForceSharp}.8{beam Up} + 6.5{lf 3 acc #}.8{beam Up} 8.5{lf 5}.8{beam Up} 10.5{lf 2}.8{beam Up} 12.5{lf 4}.4{beam Up} - 13.5{lf 5 acc ForceSharp}.4{beam Up} + 13.5{lf 5 acc #}.4{beam Up} | - // Bar 95 // Bar 95 / Voice 1 contents 1.1{lf 2}.8{beam Up} - 2.1{lf 3 acc ForceSharp}.8{beam Up} + 2.1{lf 3 acc #}.8{beam Up} 3.1{lf 3}.8{beam Up} - 4.1{lf 5 acc ForceSharp}.8{beam Up} + 4.1{lf 5 acc #}.8{beam Up} 3.1{lf 3}.4{beam Up} 1.1{lf 2}.8{beam Up} - 4.1{lf 5 acc ForceSharp}.8{beam Up} + 4.1{lf 5 acc #}.8{beam Up} | - // Bar 96 // Bar 96 / Voice 1 contents - (5.2 5.3 5.4).4{tbe dive( 0 0 45 -4) beam Down} - (5.2{t} 5.3{t} 5.4{t}).8{tbe dive( 0 -4 44.4 0) beam Up} - (5.2{t} 5.3{t} 5.4{t}).4{d tbe dive( 0 0 44.4 4) beam Down} - (5.2{t} 5.3{t} 5.4{t}).4{tbe dive( 0 4 45 0) beam Down} + (5.2 5.3 5.4).4{tbe (dive default 0 0 45 -4) beam Down} + (5.2{t} 5.3{t} 5.4{t}).8{tbe (dive default 0 -4 44.4 0) beam Up} + (5.2{t} 5.3{t} 5.4{t}).4{d tbe (dive default 0 0 44.4 4) beam Down} + (5.2{t} 5.3{t} 5.4{t}).4{tbe (dive default 0 4 45 0) beam Down} | - // Bar 97 // Bar 97 / Voice 1 contents - (5.2{t} 5.3{t} 5.4{t}).1{beam Down} + (5.2{t} 5.3{t} 5.4{t}).1{beam Down} | - // Bar 98 // Bar 98 / Voice 1 contents - (5.2 5.3 5.4).2{tbe dive( 0 0 45 4) beam Down} - (5.3{t} 5.4{t} 5.2{t}).4{d tbe hold( 0 4 60 4) beam Down} + (5.2 5.3 5.4).2{tbe (dive default 0 0 45 4) beam Down} + (5.3{t} 5.4{t} 5.2{t}).4{d tbe (hold default 0 4 60 4) beam Down} r.8{beam Down} | - // Bar 99 // Bar 99 / Voice 1 contents - (5.2 5.3 5.4).2{tbe dive( 0 0 45 4) txt "grad." beam Down} - (5.3{t} 5.4{t} 5.2{t}).4{d tbe hold( 0 4 60 4) beam Down} + (5.2 5.3 5.4).2{tbe (dive default 0 0 45 4) txt "grad." beam Down} + (5.3{t} 5.4{t} 5.2{t}).4{d tbe (hold default 0 4 60 4) beam Down} r.8{beam Down} | - // Bar 100 // Bar 100 / Voice 1 contents 7.3.2{beam Down} - 7.3.4{tbe dive( 0 0 45 -4) beam Down} - 7.3{t}.4{tbe hold( 0 -4 60 -4) beam Down} + 7.3.4{tbe (dive default 0 0 45 -4) beam Down} + 7.3{t}.4{tbe (hold default 0 -4 60 -4) beam Down} | - // Bar 101 // Bar 101 / Voice 1 contents - 7.3{t}.4{tbe dive( 0 -4 45.6 0) beam Down} + 7.3{t}.4{tbe (dive default 0 -4 45.6 0) beam Down} 7.3{t}.4{beam Down} 7.3.4{beam Down} 7.3.4{beam Down} | - // Bar 102 // Bar 102 / Voice 1 contents - 0.6.1{tbe dive( 0 0 42.6 -32) txt "grad." beam Up} + 0.6.1{tbe (dive default 0 0 42.6 -32) txt "grad." beam Up} | - // Bar 103 // Bar 103 / Voice 1 contents r.4{beam Down} - 5.2.4{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3.4{tbe dip( 0 0 15 4 30 0) beam Down} + 5.2.4{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3.4{tbe (dip default 0 0 15 4 30 0) beam Down} r.4{beam Down} | - // Bar 104 // Bar 104 / Voice 1 contents - 5.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3{t}.16{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3{t}.16{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3{t}.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3{t}.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3{t}.2{tbe dip( 0 0 15 -4 30 0) beam Down} - | - // Bar 105 + 5.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3{t}.16{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3{t}.16{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3{t}.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3{t}.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3{t}.2{tbe (dip default 0 0 15 -4 30 0) beam Down} + | // Bar 105 / Voice 1 contents - 5.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3.16{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3.16{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 5.3.2{tbe dip( 0 0 15 -4 30 0) beam Down} - | - // Bar 106 + 5.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3.16{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3.16{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 5.3.2{tbe (dip default 0 0 15 -4 30 0) beam Down} + | // Bar 106 / Voice 1 contents - 7.3.4{tbe dip( 0 0 7.199999999999999 -4 7.8 -4 13.799999999999999 0) beam Down} - 8.3{acc ForceSharp}.4{tbe dip( 0 0 5.3999999999999995 -4 6 -4 12 0) beam Down} - 10.3.4{tbe dip( 0 0 6.6 -4 7.199999999999999 -4 13.799999999999999 0) beam Down} - 12.3.4{tbe dip( 0 0 15 -4 30 0) beam Down} + 7.3.4{tbe (dip default 0 0 7.199999999999999 -4 7.8 -4 13.799999999999999 0) beam Down} + 8.3{acc #}.4{tbe (dip default 0 0 5.3999999999999995 -4 6 -4 12 0) beam Down} + 10.3.4{tbe (dip default 0 0 6.6 -4 7.199999999999999 -4 13.799999999999999 0) beam Down} + 12.3.4{tbe (dip default 0 0 15 -4 30 0) beam Down} | - // Bar 107 // Bar 107 / Voice 1 contents - 7.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 8.3{acc ForceSharp}.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 10.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} - 12.3.8{tbe dip( 0 0 15 -4 30 0) beam Down} + 7.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 8.3{acc #}.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 10.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} + 12.3.8{tbe (dip default 0 0 15 -4 30 0) beam Down} r.2{beam Down} | - // Bar 108 // Bar 108 / Voice 1 contents - 5.3.2{v d tbe dive( 0 0 43.8 -12) beam Down} - 5.3{t}.8{tbe hold( 0 -12 60 -12) beam Up} + 5.3.2{v d tbe (dive default 0 0 43.8 -12) beam Down} + 5.3{t}.8{tbe (hold default 0 -12 60 -12) beam Up} r.8{beam Up} | - // Bar 109 // Bar 109 / Voice 1 contents - 5.3{nh}.4{tbe dive( 0 0 30.599999999999998 8) beam Down} - 5.3{nh t}.2{v tbe dive( 0 8 42 -32) beam Down} - 5.3{nh t}.4{v tbe hold( 0 -32 60 -32) beam Down} + 5.3{nh}.4{tbe (dive default 0 0 30.599999999999998 8) beam Down} + 5.3{nh t}.2{v tbe (dive default 0 8 42 -32) beam Down} + 5.3{nh t}.4{v tbe (hold default 0 -32 60 -32) beam Down} | - // Bar 110 // Bar 110 / Voice 1 contents r.1{beam Down} | - // Bar 111 // Bar 111 / Voice 1 contents 0.6{lr}.8{beam Up} 0.3{lr}.8{beam Up} @@ -831,100 +710,87 @@ 0.6{lr}.8{beam Up} 0.1{lr}.8{beam Up} | - // Bar 112 // Bar 112 / Voice 1 contents - (1.4{acc ForceSharp} 3.3{acc ForceSharp} 4.2{acc ForceSharp}).4{beam Up} + (1.4{acc #} 3.3{acc #} 4.2{acc #}).4{beam Up} 3.1{lr}.16{beam Down} - 4.2{lr acc ForceSharp}.16{beam Down} - 3.3{lr acc ForceSharp}.16{beam Down} - 4.2{lr acc ForceSharp}.16{beam Down} + 4.2{lr acc #}.16{beam Down} + 3.3{lr acc #}.16{beam Down} + 4.2{lr acc #}.16{beam Down} 3.1{lr}.16{beam Down} - 4.2{lr acc ForceSharp}.16{beam Down} - 3.3{lr acc ForceSharp}.16{beam Down} - 4.2{lr acc ForceSharp}.16{beam Down} + 4.2{lr acc #}.16{beam Down} + 3.3{lr acc #}.16{beam Down} + 4.2{lr acc #}.16{beam Down} 3.1{lr}.16{beam Down} - 4.2{lr acc ForceSharp}.16{beam Down} - 3.3{lr acc ForceSharp}.16{beam Down} - 4.2{lr acc ForceSharp}.16{beam Down} + 4.2{lr acc #}.16{beam Down} + 3.3{lr acc #}.16{beam Down} + 4.2{lr acc #}.16{beam Down} | - // Bar 113 // Bar 113 / Voice 1 contents r.1{beam Down} | - // Bar 114 // Bar 114 / Voice 1 contents 0.6{lr}.8{beam Up} - (0.3{lr} 0.6{t}).8{beam Up} - (0.2{lr} 0.6{t} 0.3{t}).8{beam Up} - (0.1{lr} 0.6{t} 0.3{t} 0.2{t}).8{beam Up} - (0.2{lr} 0.6{t} 0.3{t} 0.1{t}).8{beam Up} - (0.3{lr} 0.6{t} 0.2{t} 0.1{t}).8{beam Up} - (0.6{lr} 0.3{t} 0.2{t} 0.1{t}).8{beam Up} - (0.1{lr} 0.6{t} 0.3{t}).8{beam Up} - | - // Bar 115 + (0.3{lr} 0.6{t}).8{beam Up} + (0.2{lr} 0.6{t} 0.3{t}).8{beam Up} + (0.1{lr} 0.6{t} 0.3{t} 0.2{t}).8{beam Up} + (0.2{lr} 0.6{t} 0.3{t} 0.1{t}).8{beam Up} + (0.3{lr} 0.6{t} 0.2{t} 0.1{t}).8{beam Up} + (0.6{lr} 0.3{t} 0.2{t} 0.1{t}).8{beam Up} + (0.1{lr} 0.6{t} 0.3{t}).8{beam Up} + | // Bar 115 / Voice 1 contents - (1.4{acc ForceSharp} 3.3{acc ForceSharp} 4.2{acc ForceSharp}).4{beam Up} + (1.4{acc #} 3.3{acc #} 4.2{acc #}).4{beam Up} 3.1{lr}.16{beam Down} - (4.2{lr acc ForceSharp} 3.1{t}).16{beam Down} - (3.3{lr acc ForceSharp} 3.1{t} 4.2{t acc ForceSharp}).16{beam Down} - (4.2{lr acc ForceSharp} 3.1{t} 3.3{t acc ForceSharp}).16{beam Down} - (3.1{lr} 4.2{t acc ForceSharp} 3.3{t acc ForceSharp}).16{beam Down} - (4.2{lr acc ForceSharp} 3.1{t} 3.3{t acc ForceSharp}).16{beam Down} - (3.3{lr acc ForceSharp} 3.1{t} 4.2{t acc ForceSharp}).16{beam Down} - (4.2{lr acc ForceSharp} 3.1{t} 3.3{t acc ForceSharp}).16{beam Down} - (3.1{lr} 4.2{t acc ForceSharp} 3.3{t acc ForceSharp}).16{beam Down} - (4.2{lr acc ForceSharp} 3.1{t} 3.3{t acc ForceSharp}).16{beam Down} - (3.3{lr acc ForceSharp} 3.1{t} 4.2{t acc ForceSharp}).16{beam Down} - (4.2{lr acc ForceSharp} 3.1{t} 3.3{t acc ForceSharp}).16{beam Down} - | - // Bar 116 + (4.2{lr acc #} 3.1{t}).16{beam Down} + (3.3{lr acc #} 3.1{t} 4.2{t acc #}).16{beam Down} + (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} + (3.1{lr} 4.2{t acc #} 3.3{t acc #}).16{beam Down} + (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} + (3.3{lr acc #} 3.1{t} 4.2{t acc #}).16{beam Down} + (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} + (3.1{lr} 4.2{t acc #} 3.3{t acc #}).16{beam Down} + (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} + (3.3{lr acc #} 3.1{t} 4.2{t acc #}).16{beam Down} + (4.2{lr acc #} 3.1{t} 3.3{t acc #}).16{beam Down} + | // Bar 116 / Voice 1 contents r.1{beam Down} | // Masterbar 117 Metadata - \ts 6 8 - - // Bar 117 + \ts (6 8) // Bar 117 / Voice 1 contents 0.6{lr}.8{beam Up} - (0.3{lr} 0.6{lr t}).8{beam Up} - (0.2{lr} 0.6{lr t} 0.3{lr t}).8{beam Up} - (0.1{lr} 0.6{lr t} 0.3{lr t} 0.2{lr t}).8{beam Up} - (0.2{lr} 0.6{lr t} 0.3{lr t} 0.1{lr t}).8{beam Up} - (0.3{lr} 0.6{lr t} 0.2{lr t} 0.1{lr t}).8{beam Up} + (0.3{lr} 0.6{lr t}).8{beam Up} + (0.2{lr} 0.6{lr t} 0.3{lr t}).8{beam Up} + (0.1{lr} 0.6{lr t} 0.3{lr t} 0.2{lr t}).8{beam Up} + (0.2{lr} 0.6{lr t} 0.3{lr t} 0.1{lr t}).8{beam Up} + (0.3{lr} 0.6{lr t} 0.2{lr t} 0.1{lr t}).8{beam Up} | - // Bar 118 // Bar 118 / Voice 1 contents - 1.5{lr acc ForceSharp}.8{beam Up} - (3.4{lr} 1.5{lr t acc ForceSharp}).8{beam Up} - (3.3{lr acc ForceSharp} 1.5{lr t acc ForceSharp} 3.4{lr t}).8{beam Up} - (1.5{lr t acc ForceSharp} 3.4{lr} 3.3{lr t acc ForceSharp}).8{beam Up} - (1.5{lr acc ForceSharp} 3.4{lr t} 3.3{lr t acc ForceSharp}).8{beam Up} - (3.4{lr} 1.5{lr t acc ForceSharp} 3.3{lr t acc ForceSharp}).8{beam Up} - | - // Bar 119 + 1.5{lr acc #}.8{beam Up} + (3.4{lr} 1.5{lr t acc #}).8{beam Up} + (3.3{lr acc #} 1.5{lr t acc #} 3.4{lr t}).8{beam Up} + (1.5{lr t acc #} 3.4{lr} 3.3{lr t acc #}).8{beam Up} + (1.5{lr acc #} 3.4{lr t} 3.3{lr t acc #}).8{beam Up} + (3.4{lr} 1.5{lr t acc #} 3.3{lr t acc #}).8{beam Up} + | // Bar 119 / Voice 1 contents - 2.1{lr acc ForceSharp}.8{beam Down} - (3.2{lr} 2.1{lr t acc ForceSharp}).8{beam Down} - (2.3{lr} 2.1{lr t acc ForceSharp} 3.2{lr t}).8{beam Down} - (3.2{lr} 2.1{lr t acc ForceSharp} 2.3{lr t}).8{beam Down} - (2.1{lr acc ForceSharp} 3.2{lr t} 2.3{lr t}).8{beam Down} - (3.2{lr} 2.1{lr t acc ForceSharp} 2.3{lr t}).8{beam Down} - | - // Bar 120 + 2.1{lr acc #}.8{beam Down} + (3.2{lr} 2.1{lr t acc #}).8{beam Down} + (2.3{lr} 2.1{lr t acc #} 3.2{lr t}).8{beam Down} + (3.2{lr} 2.1{lr t acc #} 2.3{lr t}).8{beam Down} + (2.1{lr acc #} 3.2{lr t} 2.3{lr t}).8{beam Down} + (3.2{lr} 2.1{lr t acc #} 2.3{lr t}).8{beam Down} + | // Bar 120 / Voice 1 contents r.2{d beam Down} | - // Bar 121 // Bar 121 / Voice 1 contents - 9.3{lf 2}.1{sd tbe predivedive( 0 -32 59.4 -14) beam Up} + 9.3{lf 2}.1{sd tbe (predivedive default 0 -32 59.4 -14) beam Up} | - // Bar 122 // Bar 122 / Voice 1 contents - 9.3{t}.1{tbe dive( 0 -14 59.4 0) beam Up} + 9.3{t}.1{tbe (dive default 0 -14 59.4 0) beam Up} | - // Bar 123 // Bar 123 / Voice 1 contents r.8{beam Down} 2.3{st lf 2}.8{su beam Up} @@ -932,50 +798,45 @@ 2.3{lf 2}.8{su beam Up} 2.3{t}.8{beam Up} 2.3{lf 2}.8{su beam Up} - 3.5{be Bend(0 0 56.4 1) lf 3}.4{sd beam Up} + 3.5{be (bend 0 0 56.4 1) lf 3}.4{sd beam Up} | // Masterbar 124 Metadata - \ts 4 4 - - // Bar 124 + \ts (4 4) // Bar 124 / Voice 1 contents 12.3{lf 2}.4{sd beam Down} 14.3{lf 4}.8{sd beam Down} 12.3{lf 2}.8{su beam Down} - 14.3{be BendRelease(0 0 21.599999999999998 2 27.599999999999998 2 41.4 0) lf 4}.8{sd gr ob beam Up} + 14.3{be (bendrelease 0 0 21.599999999999998 2 27.599999999999998 2 41.4 0) lf 4}.8{sd gr ob beam Up} 14.3{t}.8{beam Down} 14.3{t}.8{beam Down} 14.4{lf 3}.4{su beam Down} | - // Bar 125 // Bar 125 / Voice 1 contents 5.5{lf 2}.8{sd dy f beam Up} 7.4{st lf 5}.8{sd beam Up} 7.6{pm lf 4}.16{sd beam Up} 7.6{pm lf 4}.16{su beam Up} 5.5{pm st lf 2}.8{sd beam Up} - 6.6{pm lf 3 acc ForceFlat}.16{sd beam Up} - 6.6{pm lf 3 acc ForceFlat}.16{su beam Up} + 6.6{pm lf 3 acc b}.16{sd beam Up} + 6.6{pm lf 3 acc b}.16{su beam Up} 5.5{pm st lf 2}.8{sd beam Up} 5.6{pm lf 2}.8{sd beam Up} 7.5{lf 5}.8{sd beam Up} | - // Bar 126 // Bar 126 / Voice 1 contents 0.5{lr}.8{sd dy mf beam Up} - (2.4{lr lf 3} 0.5{lr t}).8{sd beam Up} - (0.1 1.2 2.3 2.4 0.5).8{sd beam Up} - (0.1 1.2 2.3 2.4 0.5).16{sd beam Up} - (0.1 1.2 2.3 2.4 0.5).16{su beam Up} - (0.1 1.2 2.3 2.4 0.5).8{sd beam Up} - (0.1 1.2 2.3 2.4 0.5).16{sd beam Up} - (0.1 1.2 2.3 2.4 0.5).16{su beam Up} - (0.1{lr} 1.2{h lr} 2.3{lr} 2.4{lr} 0.5{lr}).16{sd beam Up} - (3.2{lr lf 5} 0.5{lr t} 2.4{lr t} 2.3{lr t} 0.1{lr t}).16{beam Up} - (0.1{lr} 0.2{h lr} 2.3{lr} 2.4{lr} 0.5{lr}).16{sd beam Up} - (1.2{lr lf 2} 0.1{lr t} 2.3{lr t} 2.4{lr t} 0.5{lr t}).16{beam Up} - | - // Bar 127 + (2.4{lr lf 3} 0.5{lr t}).8{sd beam Up} + (0.1 1.2 2.3 2.4 0.5).8{sd beam Up} + (0.1 1.2 2.3 2.4 0.5).16{sd beam Up} + (0.1 1.2 2.3 2.4 0.5).16{su beam Up} + (0.1 1.2 2.3 2.4 0.5).8{sd beam Up} + (0.1 1.2 2.3 2.4 0.5).16{sd beam Up} + (0.1 1.2 2.3 2.4 0.5).16{su beam Up} + (0.1{lr} 1.2{h lr} 2.3{lr} 2.4{lr} 0.5{lr}).16{sd beam Up} + (3.2{lr lf 5} 0.5{lr t} 2.4{lr t} 2.3{lr t} 0.1{lr t}).16{beam Up} + (0.1{lr} 0.2{h lr} 2.3{lr} 2.4{lr} 0.5{lr}).16{sd beam Up} + (1.2{lr lf 2} 0.1{lr t} 2.3{lr t} 2.4{lr t} 0.5{lr t}).16{beam Up} + | // Bar 127 / Voice 1 contents 15.1{h lf 4}.16{sd beam Down} 13.1{h lf 2}.16{beam Down} @@ -990,829 +851,565 @@ 13.2{h lf 2}.16{beam Down} 12.2{lf 1}.16{beam Down} \voice /* Voice 2 */ - // Bar 1 - // Bar 1 Metadata - \clef G2 - \ottava Regular - \simile None - \ks "C" - - // empty voice + // Bar 1 Metadata + \clef g2 + \ottava regular + \simile none + \ks c + // Bar 1 / Voice 2 no contents | - // Bar 2 - // empty voice + // Bar 2 / Voice 2 no contents | - // Bar 3 - // empty voice + // Bar 3 / Voice 2 no contents | - // Bar 4 - // empty voice + // Bar 4 / Voice 2 no contents | - // Bar 5 - // empty voice + // Bar 5 / Voice 2 no contents | - // Bar 6 - // empty voice + // Bar 6 / Voice 2 no contents | - // Bar 7 - // empty voice + // Bar 7 / Voice 2 no contents | - // Bar 8 - // empty voice + // Bar 8 / Voice 2 no contents | - // Bar 9 - // empty voice + // Bar 9 / Voice 2 no contents | - // Bar 10 - // empty voice + // Bar 10 / Voice 2 no contents | - // Bar 11 - // empty voice + // Bar 11 / Voice 2 no contents | - // Bar 12 - // empty voice + // Bar 12 / Voice 2 no contents | - // Bar 13 - // empty voice + // Bar 13 / Voice 2 no contents | - // Bar 14 - // empty voice + // Bar 14 / Voice 2 no contents | - // Bar 15 - // empty voice + // Bar 15 / Voice 2 no contents | - // Bar 16 - // empty voice + // Bar 16 / Voice 2 no contents | - // Bar 17 - // empty voice + // Bar 17 / Voice 2 no contents | - // Bar 18 - // empty voice + // Bar 18 / Voice 2 no contents | - // Bar 19 - // empty voice + // Bar 19 / Voice 2 no contents | - // Bar 20 - // empty voice + // Bar 20 / Voice 2 no contents | - // Bar 21 - // empty voice + // Bar 21 / Voice 2 no contents | - // Bar 22 - // empty voice + // Bar 22 / Voice 2 no contents | - // Bar 23 // Bar 23 / Voice 2 contents r.4{dy mf beam Down} 8.2.2{d beam Down} | - // Bar 24 // Bar 24 / Voice 2 contents r.4{beam Down} - 9.2{acc ForceSharp}.2{d beam Down} + 9.2{acc #}.2{d beam Down} | - // Bar 25 // Bar 25 / Voice 2 contents r.4{beam Down} 8.2.2{d beam Down} | - // Bar 26 // Bar 26 / Voice 2 contents r.4{beam Down} 8.2.2{d beam Down} | - // Bar 27 // Bar 27 / Voice 2 contents r.4{beam Down} 8.2.2{d beam Down} | - // Bar 28 - // empty voice + // Bar 28 / Voice 2 no contents | - // Bar 29 - // empty voice + // Bar 29 / Voice 2 no contents | - // Bar 30 - // empty voice + // Bar 30 / Voice 2 no contents | - // Bar 31 - // empty voice + // Bar 31 / Voice 2 no contents | - // Bar 32 - // empty voice + // Bar 32 / Voice 2 no contents | - // Bar 33 - // empty voice + // Bar 33 / Voice 2 no contents | - // Bar 34 - // empty voice + // Bar 34 / Voice 2 no contents | - // Bar 35 - // empty voice + // Bar 35 / Voice 2 no contents | - // Bar 36 - // empty voice + // Bar 36 / Voice 2 no contents | - // Bar 37 - // empty voice + // Bar 37 / Voice 2 no contents | - // Bar 38 - // empty voice + // Bar 38 / Voice 2 no contents | - // Bar 39 - // empty voice + // Bar 39 / Voice 2 no contents | - // Bar 40 - // empty voice + // Bar 40 / Voice 2 no contents | - // Bar 41 - // empty voice + // Bar 41 / Voice 2 no contents | - // Bar 42 - // empty voice + // Bar 42 / Voice 2 no contents | - // Bar 43 - // empty voice + // Bar 43 / Voice 2 no contents | - // Bar 44 - // empty voice + // Bar 44 / Voice 2 no contents | - // Bar 45 - // empty voice + // Bar 45 / Voice 2 no contents | - // Bar 46 - // empty voice + // Bar 46 / Voice 2 no contents | - // Bar 47 - // empty voice + // Bar 47 / Voice 2 no contents | - // Bar 48 - // empty voice + // Bar 48 / Voice 2 no contents | - // Bar 49 - // empty voice + // Bar 49 / Voice 2 no contents | - // Bar 50 - // empty voice + // Bar 50 / Voice 2 no contents | - // Bar 51 - // empty voice + // Bar 51 / Voice 2 no contents | - // Bar 52 - // empty voice + // Bar 52 / Voice 2 no contents | - // Bar 53 - // empty voice + // Bar 53 / Voice 2 no contents | - // Bar 54 - // empty voice + // Bar 54 / Voice 2 no contents | - // Bar 55 - // empty voice + // Bar 55 / Voice 2 no contents | - // Bar 56 - // empty voice + // Bar 56 / Voice 2 no contents | - // Bar 57 - // empty voice + // Bar 57 / Voice 2 no contents | - // Bar 58 - // empty voice + // Bar 58 / Voice 2 no contents | - // Bar 59 - // empty voice + // Bar 59 / Voice 2 no contents | - // Bar 60 - // empty voice + // Bar 60 / Voice 2 no contents | - // Bar 61 - // empty voice + // Bar 61 / Voice 2 no contents | - // Bar 62 - // empty voice + // Bar 62 / Voice 2 no contents | - // Bar 63 - // empty voice + // Bar 63 / Voice 2 no contents | - // Bar 64 - // empty voice + // Bar 64 / Voice 2 no contents | - // Bar 65 - // empty voice + // Bar 65 / Voice 2 no contents | - // Bar 66 - // empty voice + // Bar 66 / Voice 2 no contents | - // Bar 67 - // empty voice + // Bar 67 / Voice 2 no contents | - // Bar 68 - // empty voice + // Bar 68 / Voice 2 no contents | - // Bar 69 - // empty voice + // Bar 69 / Voice 2 no contents | - // Bar 70 - // Bar 70 Metadata - \simile Simple - - // empty bar + // Bar 70 Metadata + \simile simple + // Bar 70 / Voice 2 no contents | - // Bar 71 - // Bar 71 Metadata - \simile FirstOfDouble - - // empty bar + // Bar 71 Metadata + \simile firstofdouble + // Bar 71 / Voice 2 no contents | - // Bar 72 - // Bar 72 Metadata - \simile SecondOfDouble - - // empty bar + // Bar 72 Metadata + \simile secondofdouble + // Bar 72 / Voice 2 no contents | - // Bar 73 - // Bar 73 Metadata - \simile None - + // Bar 73 Metadata + \simile none // Bar 73 / Voice 2 contents - 3.3{acc ForceSharp}.4{dy mf beam Down} + 3.3{acc #}.4{dy mf beam Down} 4.3.4{beam Down} r.8{beam Down} 5.3.8{beam Down} - 3.3{acc ForceSharp}.8{beam Down} - 1.3{acc ForceSharp}.8{beam Down} + 3.3{acc #}.8{beam Down} + 1.3{acc #}.8{beam Down} | - // Bar 74 - // empty voice + // Bar 74 / Voice 2 no contents | - // Bar 75 - // empty voice + // Bar 75 / Voice 2 no contents | - // Bar 76 - // empty voice + // Bar 76 / Voice 2 no contents | - // Bar 77 - // empty voice + // Bar 77 / Voice 2 no contents | - // Bar 78 - // empty voice + // Bar 78 / Voice 2 no contents | - // Bar 79 - // empty voice + // Bar 79 / Voice 2 no contents | - // Bar 80 - // empty voice + // Bar 80 / Voice 2 no contents | - // Bar 81 - // empty voice + // Bar 81 / Voice 2 no contents | - // Bar 82 - // empty voice + // Bar 82 / Voice 2 no contents | - // Bar 83 - // empty voice + // Bar 83 / Voice 2 no contents | - // Bar 84 - // empty voice + // Bar 84 / Voice 2 no contents | - // Bar 85 - // empty voice + // Bar 85 / Voice 2 no contents | - // Bar 86 - // empty voice + // Bar 86 / Voice 2 no contents | - // Bar 87 - // empty voice + // Bar 87 / Voice 2 no contents | - // Bar 88 - // empty voice + // Bar 88 / Voice 2 no contents | - // Bar 89 - // empty voice + // Bar 89 / Voice 2 no contents | - // Bar 90 - // empty voice + // Bar 90 / Voice 2 no contents | - // Bar 91 - // empty voice + // Bar 91 / Voice 2 no contents | - // Bar 92 - // empty voice + // Bar 92 / Voice 2 no contents | - // Bar 93 - // empty voice + // Bar 93 / Voice 2 no contents | - // Bar 94 - // empty voice + // Bar 94 / Voice 2 no contents | - // Bar 95 // Bar 95 / Voice 2 contents - 3.3{lf 4 acc ForceSharp}.4{dy mf beam Down} + 3.3{lf 4 acc #}.4{dy mf beam Down} 4.3{lf 4}.4{beam Down} r.8{beam Down} 5.3{lf 5}.8{beam Down} - 3.3{lf 4 acc ForceSharp}.4{beam Down} + 3.3{lf 4 acc #}.4{beam Down} | - // Bar 96 - // empty voice + // Bar 96 / Voice 2 no contents | - // Bar 97 - // empty voice + // Bar 97 / Voice 2 no contents | - // Bar 98 - // empty voice + // Bar 98 / Voice 2 no contents | - // Bar 99 - // empty voice + // Bar 99 / Voice 2 no contents | - // Bar 100 - // empty voice + // Bar 100 / Voice 2 no contents | - // Bar 101 - // empty voice + // Bar 101 / Voice 2 no contents | - // Bar 102 - // empty voice + // Bar 102 / Voice 2 no contents | - // Bar 103 - // empty voice + // Bar 103 / Voice 2 no contents | - // Bar 104 - // empty voice + // Bar 104 / Voice 2 no contents | - // Bar 105 - // empty voice + // Bar 105 / Voice 2 no contents | - // Bar 106 - // empty voice + // Bar 106 / Voice 2 no contents | - // Bar 107 - // empty voice + // Bar 107 / Voice 2 no contents | - // Bar 108 - // empty voice + // Bar 108 / Voice 2 no contents | - // Bar 109 - // empty voice + // Bar 109 / Voice 2 no contents | - // Bar 110 - // empty voice + // Bar 110 / Voice 2 no contents | - // Bar 111 - // empty voice + // Bar 111 / Voice 2 no contents | - // Bar 112 - // empty voice + // Bar 112 / Voice 2 no contents | - // Bar 113 - // empty voice + // Bar 113 / Voice 2 no contents | - // Bar 114 - // empty voice + // Bar 114 / Voice 2 no contents | - // Bar 115 - // empty voice + // Bar 115 / Voice 2 no contents | - // Bar 116 - // empty voice + // Bar 116 / Voice 2 no contents | - // Bar 117 - // empty voice + // Bar 117 / Voice 2 no contents | - // Bar 118 - // empty voice + // Bar 118 / Voice 2 no contents | - // Bar 119 - // empty voice + // Bar 119 / Voice 2 no contents | - // Bar 120 - // empty voice + // Bar 120 / Voice 2 no contents | - // Bar 121 - // empty voice + // Bar 121 / Voice 2 no contents | - // Bar 122 - // empty voice + // Bar 122 / Voice 2 no contents | - // Bar 123 - // empty voice + // Bar 123 / Voice 2 no contents | - // Bar 124 - // empty voice + // Bar 124 / Voice 2 no contents | - // Bar 125 - // empty voice + // Bar 125 / Voice 2 no contents | - // Bar 126 - // empty voice + // Bar 126 / Voice 2 no contents | - // Bar 127 - // empty voice + // Bar 127 / Voice 2 no contents \voice /* Voice 3 */ - // Bar 1 - // Bar 1 Metadata - \clef G2 - \ottava Regular - \simile None - \ks "C" - - // empty voice + // Bar 1 Metadata + \clef g2 + \ottava regular + \simile none + \ks c + // Bar 1 / Voice 3 no contents | - // Bar 2 - // empty voice + // Bar 2 / Voice 3 no contents | - // Bar 3 - // empty voice + // Bar 3 / Voice 3 no contents | - // Bar 4 - // empty voice + // Bar 4 / Voice 3 no contents | - // Bar 5 - // empty voice + // Bar 5 / Voice 3 no contents | - // Bar 6 - // empty voice + // Bar 6 / Voice 3 no contents | - // Bar 7 - // empty voice + // Bar 7 / Voice 3 no contents | - // Bar 8 - // empty voice + // Bar 8 / Voice 3 no contents | - // Bar 9 - // empty voice + // Bar 9 / Voice 3 no contents | - // Bar 10 - // empty voice + // Bar 10 / Voice 3 no contents | - // Bar 11 - // empty voice + // Bar 11 / Voice 3 no contents | - // Bar 12 - // empty voice + // Bar 12 / Voice 3 no contents | - // Bar 13 - // empty voice + // Bar 13 / Voice 3 no contents | - // Bar 14 - // empty voice + // Bar 14 / Voice 3 no contents | - // Bar 15 - // empty voice + // Bar 15 / Voice 3 no contents | - // Bar 16 - // empty voice + // Bar 16 / Voice 3 no contents | - // Bar 17 - // empty voice + // Bar 17 / Voice 3 no contents | - // Bar 18 - // empty voice + // Bar 18 / Voice 3 no contents | - // Bar 19 - // empty voice + // Bar 19 / Voice 3 no contents | - // Bar 20 - // empty voice + // Bar 20 / Voice 3 no contents | - // Bar 21 - // empty voice + // Bar 21 / Voice 3 no contents | - // Bar 22 - // empty voice + // Bar 22 / Voice 3 no contents | - // Bar 23 - // empty voice + // Bar 23 / Voice 3 no contents | - // Bar 24 - // empty voice + // Bar 24 / Voice 3 no contents | - // Bar 25 - // empty voice + // Bar 25 / Voice 3 no contents | - // Bar 26 - // empty voice + // Bar 26 / Voice 3 no contents | - // Bar 27 - // empty voice + // Bar 27 / Voice 3 no contents | - // Bar 28 - // empty voice + // Bar 28 / Voice 3 no contents | - // Bar 29 - // empty voice + // Bar 29 / Voice 3 no contents | - // Bar 30 - // empty voice + // Bar 30 / Voice 3 no contents | - // Bar 31 - // empty voice + // Bar 31 / Voice 3 no contents | - // Bar 32 - // empty voice + // Bar 32 / Voice 3 no contents | - // Bar 33 - // empty voice + // Bar 33 / Voice 3 no contents | - // Bar 34 - // empty voice + // Bar 34 / Voice 3 no contents | - // Bar 35 - // empty voice + // Bar 35 / Voice 3 no contents | - // Bar 36 - // empty voice + // Bar 36 / Voice 3 no contents | - // Bar 37 - // empty voice + // Bar 37 / Voice 3 no contents | - // Bar 38 - // empty voice + // Bar 38 / Voice 3 no contents | - // Bar 39 - // empty voice + // Bar 39 / Voice 3 no contents | - // Bar 40 - // empty voice + // Bar 40 / Voice 3 no contents | - // Bar 41 - // empty voice + // Bar 41 / Voice 3 no contents | - // Bar 42 - // empty voice + // Bar 42 / Voice 3 no contents | - // Bar 43 - // empty voice + // Bar 43 / Voice 3 no contents | - // Bar 44 - // empty voice + // Bar 44 / Voice 3 no contents | - // Bar 45 - // empty voice + // Bar 45 / Voice 3 no contents | - // Bar 46 - // empty voice + // Bar 46 / Voice 3 no contents | - // Bar 47 - // empty voice + // Bar 47 / Voice 3 no contents | - // Bar 48 - // empty voice + // Bar 48 / Voice 3 no contents | - // Bar 49 - // empty voice + // Bar 49 / Voice 3 no contents | - // Bar 50 - // empty voice + // Bar 50 / Voice 3 no contents | - // Bar 51 - // empty voice + // Bar 51 / Voice 3 no contents | - // Bar 52 - // empty voice + // Bar 52 / Voice 3 no contents | - // Bar 53 - // empty voice + // Bar 53 / Voice 3 no contents | - // Bar 54 - // empty voice + // Bar 54 / Voice 3 no contents | - // Bar 55 - // empty voice + // Bar 55 / Voice 3 no contents | - // Bar 56 - // empty voice + // Bar 56 / Voice 3 no contents | - // Bar 57 - // empty voice + // Bar 57 / Voice 3 no contents | - // Bar 58 - // empty voice + // Bar 58 / Voice 3 no contents | - // Bar 59 - // empty voice + // Bar 59 / Voice 3 no contents | - // Bar 60 - // empty voice + // Bar 60 / Voice 3 no contents | - // Bar 61 - // empty voice + // Bar 61 / Voice 3 no contents | - // Bar 62 - // empty voice + // Bar 62 / Voice 3 no contents | - // Bar 63 - // empty voice + // Bar 63 / Voice 3 no contents | - // Bar 64 - // empty voice + // Bar 64 / Voice 3 no contents | - // Bar 65 - // empty voice + // Bar 65 / Voice 3 no contents | - // Bar 66 - // empty voice + // Bar 66 / Voice 3 no contents | - // Bar 67 - // empty voice + // Bar 67 / Voice 3 no contents | - // Bar 68 - // empty voice + // Bar 68 / Voice 3 no contents | - // Bar 69 - // empty voice + // Bar 69 / Voice 3 no contents | - // Bar 70 - // Bar 70 Metadata - \simile Simple - - // empty bar + // Bar 70 Metadata + \simile simple + // Bar 70 / Voice 3 no contents | - // Bar 71 - // Bar 71 Metadata - \simile FirstOfDouble - - // empty bar + // Bar 71 Metadata + \simile firstofdouble + // Bar 71 / Voice 3 no contents | - // Bar 72 - // Bar 72 Metadata - \simile SecondOfDouble - - // empty bar + // Bar 72 Metadata + \simile secondofdouble + // Bar 72 / Voice 3 no contents | - // Bar 73 - // Bar 73 Metadata - \simile None - + // Bar 73 Metadata + \simile none // Bar 73 / Voice 3 contents r.4{dy mf beam Down} r.4{beam Down} 0.6.4{beam Down} 0.6.4{beam Down} | - // Bar 74 - // empty voice + // Bar 74 / Voice 3 no contents | - // Bar 75 - // empty voice + // Bar 75 / Voice 3 no contents | - // Bar 76 - // empty voice + // Bar 76 / Voice 3 no contents | - // Bar 77 - // empty voice + // Bar 77 / Voice 3 no contents | - // Bar 78 - // empty voice + // Bar 78 / Voice 3 no contents | - // Bar 79 - // empty voice + // Bar 79 / Voice 3 no contents | - // Bar 80 - // empty voice + // Bar 80 / Voice 3 no contents | - // Bar 81 - // empty voice + // Bar 81 / Voice 3 no contents | - // Bar 82 - // empty voice + // Bar 82 / Voice 3 no contents | - // Bar 83 - // empty voice + // Bar 83 / Voice 3 no contents | - // Bar 84 - // empty voice + // Bar 84 / Voice 3 no contents | - // Bar 85 - // empty voice + // Bar 85 / Voice 3 no contents | - // Bar 86 - // empty voice + // Bar 86 / Voice 3 no contents | - // Bar 87 - // empty voice + // Bar 87 / Voice 3 no contents | - // Bar 88 - // empty voice + // Bar 88 / Voice 3 no contents | - // Bar 89 - // empty voice + // Bar 89 / Voice 3 no contents | - // Bar 90 - // empty voice + // Bar 90 / Voice 3 no contents | - // Bar 91 - // empty voice + // Bar 91 / Voice 3 no contents | - // Bar 92 - // empty voice + // Bar 92 / Voice 3 no contents | - // Bar 93 - // empty voice + // Bar 93 / Voice 3 no contents | - // Bar 94 - // empty voice + // Bar 94 / Voice 3 no contents | - // Bar 95 // Bar 95 / Voice 3 contents r.4{dy mf beam Down} r.4{beam Down} 0.6.4{beam Down} 1.6{lf 2}.4{beam Down} | - // Bar 96 - // empty voice + // Bar 96 / Voice 3 no contents | - // Bar 97 - // empty voice + // Bar 97 / Voice 3 no contents | - // Bar 98 - // empty voice + // Bar 98 / Voice 3 no contents | - // Bar 99 - // empty voice + // Bar 99 / Voice 3 no contents | - // Bar 100 - // empty voice + // Bar 100 / Voice 3 no contents | - // Bar 101 - // empty voice + // Bar 101 / Voice 3 no contents | - // Bar 102 - // empty voice + // Bar 102 / Voice 3 no contents | - // Bar 103 - // empty voice + // Bar 103 / Voice 3 no contents | - // Bar 104 - // empty voice + // Bar 104 / Voice 3 no contents | - // Bar 105 - // empty voice + // Bar 105 / Voice 3 no contents | - // Bar 106 - // empty voice + // Bar 106 / Voice 3 no contents | - // Bar 107 - // empty voice + // Bar 107 / Voice 3 no contents | - // Bar 108 - // empty voice + // Bar 108 / Voice 3 no contents | - // Bar 109 - // empty voice + // Bar 109 / Voice 3 no contents | - // Bar 110 - // empty voice + // Bar 110 / Voice 3 no contents | - // Bar 111 - // empty voice + // Bar 111 / Voice 3 no contents | - // Bar 112 - // empty voice + // Bar 112 / Voice 3 no contents | - // Bar 113 - // empty voice + // Bar 113 / Voice 3 no contents | - // Bar 114 - // empty voice + // Bar 114 / Voice 3 no contents | - // Bar 115 - // empty voice + // Bar 115 / Voice 3 no contents | - // Bar 116 - // empty voice + // Bar 116 / Voice 3 no contents | - // Bar 117 - // empty voice + // Bar 117 / Voice 3 no contents | - // Bar 118 - // empty voice + // Bar 118 / Voice 3 no contents | - // Bar 119 - // empty voice + // Bar 119 / Voice 3 no contents | - // Bar 120 - // empty voice + // Bar 120 / Voice 3 no contents | - // Bar 121 - // empty voice + // Bar 121 / Voice 3 no contents | - // Bar 122 - // empty voice + // Bar 122 / Voice 3 no contents | - // Bar 123 - // empty voice + // Bar 123 / Voice 3 no contents | - // Bar 124 - // empty voice + // Bar 124 / Voice 3 no contents | - // Bar 125 - // empty voice + // Bar 125 / Voice 3 no contents | - // Bar 126 - // empty voice + // Bar 126 / Voice 3 no contents | - // Bar 127 - // empty voice \ No newline at end of file + // Bar 127 / Voice 3 no contents \ No newline at end of file diff --git a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png index d0462dcee..05e57e8fa 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png and b/packages/alphatab/test-data/musicxml-samples/BeetAnGeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Binchois.png b/packages/alphatab/test-data/musicxml-samples/Binchois.png index 4c0c54f48..e4c84a6b6 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Binchois.png and b/packages/alphatab/test-data/musicxml-samples/Binchois.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png index 10654d57c..e536b59ac 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png and b/packages/alphatab/test-data/musicxml-samples/BrahWiMeSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png b/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png index 2fab8a863..bc3ea1fb0 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png and b/packages/alphatab/test-data/musicxml-samples/BrookeWestSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Chant.png b/packages/alphatab/test-data/musicxml-samples/Chant.png index 573c4959f..5db016908 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Chant.png and b/packages/alphatab/test-data/musicxml-samples/Chant.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png b/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png index 49dc87313..d9416b08f 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png and b/packages/alphatab/test-data/musicxml-samples/DebuMandSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png index f29092413..d80153abe 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png and b/packages/alphatab/test-data/musicxml-samples/Dichterliebe01.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Echigo.png b/packages/alphatab/test-data/musicxml-samples/Echigo.png index 1521f47e1..e83d47a3a 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Echigo.png and b/packages/alphatab/test-data/musicxml-samples/Echigo.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png b/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png index a29d9013d..0ec272d4c 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png and b/packages/alphatab/test-data/musicxml-samples/FaurReveSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png b/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png index 5cb2add04..0f0cc5366 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png and b/packages/alphatab/test-data/musicxml-samples/MahlFaGe4Sample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png b/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png index cb64afd5d..2721eaeba 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png and b/packages/alphatab/test-data/musicxml-samples/MozaChloSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png b/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png index 6b0e40978..678eab888 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png and b/packages/alphatab/test-data/musicxml-samples/MozaVeilSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png b/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png index 012affe21..956d234d6 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png and b/packages/alphatab/test-data/musicxml-samples/MozartPianoSonata.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png index dce4fcadf..ea412cce9 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/MozartTrio.png and b/packages/alphatab/test-data/musicxml-samples/MozartTrio.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Saltarello.png b/packages/alphatab/test-data/musicxml-samples/Saltarello.png index e24f7573a..1f6f775a8 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Saltarello.png and b/packages/alphatab/test-data/musicxml-samples/Saltarello.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png b/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png index 5276b0368..026983c35 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png and b/packages/alphatab/test-data/musicxml-samples/SchbAvMaSample.png differ diff --git a/packages/alphatab/test-data/musicxml-samples/Telemann.png b/packages/alphatab/test-data/musicxml-samples/Telemann.png index 313b6ae15..e7c9da4af 100644 Binary files a/packages/alphatab/test-data/musicxml-samples/Telemann.png and b/packages/alphatab/test-data/musicxml-samples/Telemann.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png b/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png index 305af70fb..7dca6d05c 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png and b/packages/alphatab/test-data/musicxml-testsuite/51b-Header-Quotes.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png b/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png index cd7664743..262d65b4c 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png and b/packages/alphatab/test-data/musicxml-testsuite/51c-MultipleRights.png differ diff --git a/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png b/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png index 6596034da..7333d4a79 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png and b/packages/alphatab/test-data/musicxml-testsuite/90a-Compressed-MusicXML.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png index 0bd24abc5..7dc88f97e 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png index 04c74654e..8129f63e3 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png index f9f48a753..d38fb2427 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png index eb8c977fd..958834176 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png index e884a31b5..5f262497b 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png index 321f17263..d7b1f266c 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png index 84ceae03d..2611acb7f 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png index 05b29f755..a64965d37 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png index 970e8f0fe..1bb825cb1 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png index e884a31b5..5f262497b 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png index ac82e32a3..b036ad443 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png index 4953139a3..b6a704780 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/legato.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png index 651f9a174..7133817e3 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/slides-line-break.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png index 19d03625b..e30c3b38d 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png and b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors.png b/packages/alphatab/test-data/visual-tests/general/colors.png index 1032bb4ad..4f54dea24 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors.png and b/packages/alphatab/test-data/visual-tests/general/colors.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/font-fallback.png b/packages/alphatab/test-data/visual-tests/general/font-fallback.png index babe0103a..e1f8c5f5a 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/font-fallback.png and b/packages/alphatab/test-data/visual-tests/general/font-fallback.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/song-details.png b/packages/alphatab/test-data/visual-tests/general/song-details.png index 52af30d7a..92c4886b1 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/song-details.png and b/packages/alphatab/test-data/visual-tests/general/song-details.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png index ffa2af9e6..4767b53f1 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png and b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout-5to8.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png index 47e70f094..afadfd28a 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png and b/packages/alphatab/test-data/visual-tests/layout/horizontal-layout.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/multi-track.png b/packages/alphatab/test-data/visual-tests/layout/multi-track.png index 4bbec494e..ff1319524 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/multi-track.png and b/packages/alphatab/test-data/visual-tests/layout/multi-track.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png b/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png index 8140fc5e8..d8c3bb507 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout-5barsperrow.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png b/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png index a861f5a36..c391ebba1 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout-5to8.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png b/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png index b7966e166..8cc7c2a08 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout-justify-last-row.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/page-layout.png b/packages/alphatab/test-data/visual-tests/layout/page-layout.png index 3578b6974..be35ceb34 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/page-layout.png and b/packages/alphatab/test-data/visual-tests/layout/page-layout.png differ diff --git a/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png b/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png index a03f3773d..4d8f52e29 100644 Binary files a/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png and b/packages/alphatab/test-data/visual-tests/layout/system-layout-tex.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png index 7a9f73999..777b4c759 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png index 54ebc1838..e0f8cd8e2 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/chord-diagrams-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png index 19190ce7c..6f03f72ed 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png index 0a1db86c1..307776269 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png index f30a9936b..1e50f8e3c 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png index a95945427..0b5b3f017 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/parenthesis-on-tied-bends-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png index 656634ef0..32c4f075d 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-album.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png index 9c9b71587..9906d0699 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-all.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png index d08a471f7..0b1a2f3b5 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-artist.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png index 28b2e742e..d44c9c9db 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-copyright.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png index 4d737c485..e169bf53e 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-music.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png index ceade15ee..ffcd28536 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-subtitle.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png index b45600b17..6f134a551 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-title.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png index ba8cad0b7..a9a3bb16e 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words-and-music.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png index 69d3de40e..049e3e716 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png and b/packages/alphatab/test-data/visual-tests/notation-elements/score-info-words.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png index 26c0e5c6c..ddc7c063a 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png index ed60e6a85..eda73d822 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/tab-notes-on-tied-bends-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png index 3bad22599..41fc6441b 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png index ea865bbea..94e6b1213 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/track-names-on.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png index 956afa406..da3c0d04c 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png and b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-off.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png index a28040ce5..30d6d537c 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png and b/packages/alphatab/test-data/visual-tests/notation-elements/zeros-on-dive-whammys-on.png differ diff --git a/packages/alphatab/test/PrettyFormat.ts b/packages/alphatab/test/PrettyFormat.ts index dba5abf32..efb7c4bb8 100644 --- a/packages/alphatab/test/PrettyFormat.ts +++ b/packages/alphatab/test/PrettyFormat.ts @@ -125,7 +125,7 @@ export class PrettyFormat { switch (typeof val) { case 'string': if (escapeString) { - return `"${(val as string).replaceAll(/"|\\/g, '\\$&')}"`; + return `"${(val as string).replaceAll(/"|\\/g, v => `\\${v}`)}"`; } return `"${val}"`; case 'number': @@ -209,6 +209,11 @@ export class PrettyFormat { ? '[Map]' : `Map {${PrettyFormat.printIteratorEntries(TestPlatform.mapAsUnknownIterable(val), config, indentation, depth, refs, PrettyFormat.printer, ' => ')}}`; } + if (val instanceof Set) { + return hitMaxDepth + ? '[Set]' + : `Set {${PrettyFormat.printIterableValues(TestPlatform.setAsUnknownIterable(val), config, indentation, depth, refs, PrettyFormat.printer)}}`; + } // Avoid failure to serialize global window object in jsdom test environment. // For example, not even relevant if window is prop of React element. @@ -355,15 +360,33 @@ export class PrettyFormat { } } -import { ScoreSerializer } from '@src/generated/model/ScoreSerializer'; -import { MasterBarSerializer } from '@src/generated/model/MasterBarSerializer'; -import { TrackSerializer } from '@src/generated/model/TrackSerializer'; -import { StaffSerializer } from '@src/generated/model/StaffSerializer'; import { BarSerializer } from '@src/generated/model/BarSerializer'; -import { VoiceSerializer } from '@src/generated/model/VoiceSerializer'; import { BeatSerializer } from '@src/generated/model/BeatSerializer'; +import { MasterBarSerializer } from '@src/generated/model/MasterBarSerializer'; import { NoteSerializer } from '@src/generated/model/NoteSerializer'; -import { TestPlatform } from './TestPlatform'; +import { ScoreSerializer } from '@src/generated/model/ScoreSerializer'; +import { StaffSerializer } from '@src/generated/model/StaffSerializer'; +import { TrackSerializer } from '@src/generated/model/TrackSerializer'; +import { VoiceSerializer } from '@src/generated/model/VoiceSerializer'; +import { + type AlphaTexAstNode, + type AlphaTexBarNode, + type AlphaTexBeatDurationChangeNode, + type AlphaTexBeatNode, + type AlphaTexIdentifier, + type AlphaTexMetaDataNode, + type AlphaTexMetaDataTagNode, + AlphaTexNodeType, + type AlphaTexNoteListNode, + type AlphaTexNoteNode, + type AlphaTexNumberLiteral, + type AlphaTexPropertiesNode, + type AlphaTexPropertyNode, + type AlphaTexScoreNode, + type AlphaTexStringLiteral, + type AlphaTexValueList +} from '@src/importer/alphaTex/AlphaTexAst'; +import { MidiEvent } from '@src/midi/MidiEvent'; import { Bar } from '@src/model/Bar'; import { Beat } from '@src/model/Beat'; import { JsonConverter } from '@src/model/JsonConverter'; @@ -373,7 +396,313 @@ import { Score } from '@src/model/Score'; import { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { Voice } from '@src/model/Voice'; -import { MidiEvent } from '@src/midi/MidiEvent'; +import { TestPlatform } from './TestPlatform'; +import type { AlphaTexDiagnostic } from '@src/importer/alphaTex/AlphaTexShared'; + +/** + * @partial + * @internal + */ +export class AlphaTexAstNodePlugin implements PrettyFormatNewPlugin { + public static readonly instance = new AlphaTexAstNodePlugin(); + + /** + * @partial + * @target web + */ + test(arg0: unknown): boolean { + return !!arg0 && typeof arg0 === 'object' && 'nodeType' in arg0; + } + + serialize( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[], + printer: PrettyFormatPrinter + ): string { + const node = val as AlphaTexAstNode; + let value: string | undefined = undefined; + switch (node.nodeType) { + case AlphaTexNodeType.Ident: + value = (node as AlphaTexIdentifier).text; + break; + case AlphaTexNodeType.Tag: + value = (node as AlphaTexMetaDataTagNode).tag.text; + break; + case AlphaTexNodeType.Number: + value = (node as AlphaTexNumberLiteral).value.toString(); + break; + case AlphaTexNodeType.String: + value = (node as AlphaTexStringLiteral).text; + break; + } + const serializedValue = value !== undefined ? ` ${JSON.stringify(value)}` : ''; + + // children + const children: [string, unknown][] = []; + + if (node.leadingComments && node.leadingComments.length > 0) { + const comments: string[] = []; + for (const c of node.leadingComments) { + if (c.multiLine) { + comments.push(`/*${c.text}*/`); + } else { + comments.push(`//${c.text}`); + } + } + children.push(['leadingComments', comments]); + } + + if (node.trailingComments && node.trailingComments.length > 0) { + const comments: string[] = []; + for (const c of node.trailingComments) { + if (c.multiLine) { + comments.push(`/*${c.text}*/`); + } else { + comments.push(`//${c.text}`); + } + } + children.push(['trailingComments', comments]); + } + + switch (node.nodeType) { + // case AlphaTexNodeType.DotToken: + // case AlphaTexNodeType.BackSlashToken: + // case AlphaTexNodeType.DoubleBackSlashToken: + // case AlphaTexNodeType.PipeToken: + // case AlphaTexNodeType.BraceOpenToken: + // case AlphaTexNodeType.BraceCloseToken: + // case AlphaTexNodeType.ParenthesisOpenToken: + // case AlphaTexNodeType.ParenthesisCloseToken: + // case AlphaTexNodeType.ColonToken: + // case AlphaTexNodeType.AsteriskToken: + + // case AlphaTexNodeType.Identifier: + case AlphaTexNodeType.Tag: + const tag = node as AlphaTexMetaDataTagNode; + if (tag.prefix) { + children.push(['prefix', tag.prefix]); + } + + if (tag.tag) { + children.push(['tag', tag.tag]); + } + break; + + case AlphaTexNodeType.Meta: + const metaData = node as AlphaTexMetaDataNode; + if (metaData.tag) { + children.push(['tag', metaData.tag]); + } + if (metaData.values) { + children.push(['values', metaData.values]); + } + if (metaData.properties) { + children.push(['properties', metaData.properties]); + } + break; + + case AlphaTexNodeType.Values: + const valueList = node as AlphaTexValueList; + if (valueList.openParenthesis) { + children.push(['openParenthesis', valueList.openParenthesis]); + } + if (valueList.values) { + children.push(['values', valueList.values]); + } + if (valueList.closeParenthesis) { + children.push(['closeParenthesis', valueList.closeParenthesis]); + } + break; + + case AlphaTexNodeType.Props: + const properties = node as AlphaTexPropertiesNode; + if (properties.openBrace) { + children.push(['openBrace', properties.openBrace]); + } + if (properties.properties) { + children.push(['properties', properties.properties]); + } + if (properties.closeBrace) { + children.push(['closeBrace', properties.closeBrace]); + } + break; + case AlphaTexNodeType.Prop: + const property = node as AlphaTexPropertyNode; + if (property.property) { + children.push(['property', property.property]); + } + if (property.values) { + children.push(['properties', property.values]); + } + break; + // case AlphaTexNodeType.NumberLiteral: + // case AlphaTexNodeType.StringLiteral: + case AlphaTexNodeType.Score: + const score = node as AlphaTexScoreNode; + if (score.bars && score.bars.length > 0) { + children.push(['bars', score.bars]); + } + break; + + case AlphaTexNodeType.Bar: + const bar = node as AlphaTexBarNode; + if (bar.metaData && bar.metaData.length > 0) { + children.push(['metaData', bar.metaData]); + } + if (bar.beats && bar.beats.length > 0) { + children.push(['beats', bar.beats]); + } + if (bar.pipe) { + children.push(['pipe', bar.pipe]); + } + break; + + case AlphaTexNodeType.Beat: + const beat = node as AlphaTexBeatNode; + if (beat.durationChange) { + children.push(['durationChange', beat.durationChange]); + } + if (beat.notes) { + children.push(['notes', beat.notes]); + } + if (beat.rest) { + children.push(['rest', beat.rest]); + } + if (beat.durationDot) { + children.push(['durationDot', beat.durationDot]); + } + if (beat.durationValue) { + children.push(['durationValue', beat.durationValue]); + } + if (beat.beatMultiplier) { + children.push(['beatMultiplier', beat.beatMultiplier]); + } + if (beat.beatMultiplierValue) { + children.push(['beatMultiplierValue', beat.beatMultiplierValue]); + } + if (beat.beatEffects) { + children.push(['beatEffects', beat.beatEffects]); + } + break; + + case AlphaTexNodeType.Duration: + const beatDurationChange = node as AlphaTexBeatDurationChangeNode; + if (beatDurationChange.colon) { + children.push(['colon', beatDurationChange.colon]); + } + if (beatDurationChange.value) { + children.push(['value', beatDurationChange.value]); + } + if (beatDurationChange.properties) { + children.push(['properties', beatDurationChange.properties]); + } + break; + case AlphaTexNodeType.NoteList: + const noteList = node as AlphaTexNoteListNode; + if (noteList.openParenthesis) { + children.push(['openParenthesis', noteList.openParenthesis]); + } + if (noteList.notes) { + children.push(['notes', noteList.notes]); + } + if (noteList.closeParenthesis) { + children.push(['closeParenthesis', noteList.closeParenthesis]); + } + break; + + case AlphaTexNodeType.Note: + const note = node as AlphaTexNoteNode; + if (note.noteValue) { + children.push(['noteValue', note.noteValue]); + } + if (note.noteStringDot) { + children.push(['noteStringDot', note.noteStringDot]); + } + if (note.noteString) { + children.push(['noteString', note.noteString]); + } + if (note.noteEffects) { + children.push(['noteEffects', note.noteEffects]); + } + break; + } + + let str = `${AlphaTexNodeType[node.nodeType]}${serializedValue} (${node.start?.line},${node.start?.col}) -> (${node.end?.line},${node.end?.col})`; + + if (children.length > 0) { + str += ` {${config.spacingOuter}`; + const indentationNext = indentation + config.indent; + + for (let i = 0; i < children.length; i++) { + const name = children[i][0]; + const childValue = printer(children[i][1], config, indentationNext, depth, refs); + + str += `${indentationNext + name}: ${childValue}`; + + if (i < children.length - 1) { + str += `,${config.spacingInner}`; + } else if (!config.min) { + str += ','; + } + } + + str += `${config.spacingOuter + indentation}}`; + } + + return str; + } +} + +/** + * @partial + * @internal + */ +export class AlphaTexDiagnosticPlugin implements PrettyFormatNewPlugin { + public static readonly instance = new AlphaTexDiagnosticPlugin(); + + /** + * @partial + * @target web + */ + test(arg0: unknown): boolean { + return !!arg0 && typeof arg0 === 'object' && 'severity' in arg0; + } + + serialize( + val: unknown, + config: PrettyFormatConfig, + indentation: string, + depth: number, + refs: unknown[], + printer: PrettyFormatPrinter + ): string { + const v = val as AlphaTexDiagnostic; + const map = new Map(); + + map.set('code', v.code); + map.set('severity', v.severity as number); + map.set('message', v.message); + if (v.start) { + const start = new Map(); + map.set('start', start); + start.set('col', v.start.col); + start.set('line', v.start.line); + start.set('offset', v.start.offset); + } + if (v.end) { + const end = new Map(); + map.set('start', end); + end.set('col', v.end.col); + end.set('line', v.end.line); + end.set('offset', v.end.offset); + } + + return printer(map, config, indentation, depth, refs); + } +} /** * A serializer plugin for pretty-format for creating simple MidiEbent snapshots @@ -622,6 +951,8 @@ export class SnapshotFile { const c = new PrettyFormatConfig(); c.plugins.push(ScoreSerializerPlugin.instance); c.plugins.push(MidiEventSerializerPlugin.instance); + c.plugins.push(AlphaTexDiagnosticPlugin.instance); + c.plugins.push(AlphaTexAstNodePlugin.instance); return c; } private static readonly _matchOptions: PrettyFormatConfig = SnapshotFile._createConfig(); diff --git a/packages/alphatab/test/TestPlatform.ts b/packages/alphatab/test/TestPlatform.ts index d6f333fb0..8fdf5868f 100644 --- a/packages/alphatab/test/TestPlatform.ts +++ b/packages/alphatab/test/TestPlatform.ts @@ -68,6 +68,16 @@ export class TestPlatform { return IOHelper.toString(data, 'UTF-8'); } + /** + * @target web + * @partial + */ + public static async saveFileAsString(name: string, data: string): Promise { + const directory = path.dirname(name); + await fs.promises.mkdir(directory, { recursive: true }); + await fs.promises.writeFile(name, data, { encoding: 'utf-8' }); + } + public static changeExtension(file: string, extension: string): string { const lastDot: number = file.lastIndexOf('.'); if (lastDot === -1) { diff --git a/packages/alphatab/test/audio/AlphaSynth.test.ts b/packages/alphatab/test/audio/AlphaSynth.test.ts index 9c2dfd6d8..0cfbe516d 100644 --- a/packages/alphatab/test/audio/AlphaSynth.test.ts +++ b/packages/alphatab/test/audio/AlphaSynth.test.ts @@ -1,21 +1,20 @@ +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { ByteBuffer } from '@src/io/ByteBuffer'; import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; -import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; +import { ControllerType } from '@src/midi/ControllerType'; +import { type ControlChangeEvent, MidiEventType } from '@src/midi/MidiEvent'; import { MidiFile } from '@src/midi/MidiFile'; -import { AlphaSynth } from '@src/synth/AlphaSynth'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; +import { AlphaSynth } from '@src/synth/AlphaSynth'; +import { AudioExportOptions } from '@src/synth/IAudioExporter'; +import { SynthConstants } from '@src/synth/SynthConstants'; +import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; +import { VorbisFile } from '@src/synth/vorbis/VorbisFile'; import { TestOutput } from '@test/audio/TestOutput'; import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; -import { SynthConstants } from '@src/synth/SynthConstants'; -import { VorbisFile } from '@src/synth/vorbis/VorbisFile'; -import { ByteBuffer } from '@src/io/ByteBuffer'; -import { ScoreLoader } from '@src/importer/ScoreLoader'; -import { AudioExportOptions } from '@src/synth/IAudioExporter'; -import { type ControlChangeEvent, MidiEventType } from '@src/midi/MidiEvent'; -import { ControllerType } from '@src/midi/ControllerType'; -import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; describe('AlphaSynthTests', () => { it('pcm-generation', async () => { @@ -26,14 +25,12 @@ describe('AlphaSynthTests', () => { ' r.8 (3.4 3.3 ).8 r.8 (6.3 6.4 ).8 (5.4 5.3 ).4 {d }r.8 |' + ' (0.4 0.3).8 r.8(3.4 3.3).8 r.8(5.4 5.3).4 r.8(3.4 3.3).8 | ' + 'r.8(0.4 0.3).8(-.3 - .4).2 { d } | '; - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - const score: Score = importer.readScore(); - const midi: MidiFile = new MidiFile(); - const gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); + const score = ScoreLoader.loadAlphaTex(tex); + const midi = new MidiFile(); + const gen = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); gen.generate(); - const testOutput: TestOutput = new TestOutput(); - const synth: AlphaSynth = new AlphaSynth(testOutput, 500); + const testOutput = new TestOutput(); + const synth = new AlphaSynth(testOutput, 500); synth.loadSoundFont(data, false); synth.loadMidiFile(midi); synth.play(); @@ -58,14 +55,12 @@ describe('AlphaSynthTests', () => { \\track "T02" \\instrument 30 4.4.4*4`; - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - const score: Score = importer.readScore(); - const midi: MidiFile = new MidiFile(); - const gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); + const score = ScoreLoader.loadAlphaTex(tex); + const midi = new MidiFile(); + const gen = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); gen.generate(); - const testOutput: TestOutput = new TestOutput(); - const synth: AlphaSynth = new AlphaSynth(testOutput, 500); + const testOutput = new TestOutput(); + const synth = new AlphaSynth(testOutput, 500); synth.loadSoundFont(data, false); synth.loadMidiFile(midi); @@ -90,14 +85,12 @@ describe('AlphaSynthTests', () => { \\track "T02" \\instrument 30 4.4.4*4`; - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - const score: Score = importer.readScore(); - const midi: MidiFile = new MidiFile(); - const gen: MidiFileGenerator = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); + const score = ScoreLoader.loadAlphaTex(tex); + const midi = new MidiFile(); + const gen = new MidiFileGenerator(score, null, new AlphaSynthMidiFileHandler(midi)); gen.generate(); - const testOutput: TestOutput = new TestOutput(); - const synth: AlphaSynth = new AlphaSynth(testOutput, 500); + const testOutput = new TestOutput(); + const synth = new AlphaSynth(testOutput, 500); synth.loadSoundFont(data, false); synth.loadMidiFile(midi); @@ -277,11 +270,11 @@ describe('AlphaSynthTests', () => { it('midi-bank', () => { const score = ScoreLoader.loadAlphaTex(` - \\track T1 { instrument 25 bank 77 } + \\track "T1" { instrument 25 bank 77 } C4 D4 E4 F4 | C4 { instrument 27 bank 1000 } D4 E4 F4 - \\track T1 { instrument 25 bank 50 } - C4 D4 E4 F4 | C4 D4 E4 { instrument 27 bank 4000 } F4 + \\track "T1" { instrument 25 bank 50 } + C4 D4 E4 F4 | C4 D4 E4 { instrument 27 bank 4000 } F4 `); const midi: MidiFile = new MidiFile(); diff --git a/packages/alphatab/test/audio/MidiFileGenerator.test.ts b/packages/alphatab/test/audio/MidiFileGenerator.test.ts index 79ef4b1c1..275524e11 100644 --- a/packages/alphatab/test/audio/MidiFileGenerator.test.ts +++ b/packages/alphatab/test/audio/MidiFileGenerator.test.ts @@ -1,46 +1,43 @@ +import { Gp3To5Importer } from '@src/importer/Gp3To5Importer'; +import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { ByteBuffer } from '@src/io/ByteBuffer'; +import { Logger } from '@src/Logger'; +import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; import { ControllerType } from '@src/midi/ControllerType'; import { type MidiEvent, MidiEventType, NoteOnEvent, type TimeSignatureEvent } from '@src/midi/MidiEvent'; -import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; import { MidiFile } from '@src/midi/MidiFile'; +import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; +import type { MidiTickLookup } from '@src/midi/MidiTickLookup'; import { MidiUtils } from '@src/midi/MidiUtils'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; -import { Gp3To5Importer } from '@src/importer/Gp3To5Importer'; -import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; -import { ByteBuffer } from '@src/io/ByteBuffer'; +import { AccentuationType } from '@src/model/AccentuationType'; import type { Beat } from '@src/model/Beat'; +import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; import { GraceType } from '@src/model/GraceType'; import type { Note } from '@src/model/Note'; import type { PlaybackInformation } from '@src/model/PlaybackInformation'; import type { Score } from '@src/model/Score'; +import { VibratoType } from '@src/model/VibratoType'; import { Settings } from '@src/Settings'; -import { Logger } from '@src/Logger'; import { - FlatNoteBendEvent, FlatControlChangeEvent, - FlatMidiEventGenerator, type FlatMidiEvent, + FlatMidiEventGenerator, + FlatNoteBendEvent, FlatNoteEvent, FlatProgramChangeEvent, + FlatRestEvent, FlatTempoEvent, FlatTimeSignatureEvent, - FlatTrackEndEvent, - FlatRestEvent + FlatTrackEndEvent } from '@test/audio/FlatMidiEventGenerator'; import { TestPlatform } from '@test/TestPlatform'; -import { AlphaSynthMidiFileHandler } from '@src/midi/AlphaSynthMidiFileHandler'; import { expect } from 'chai'; -import { ScoreLoader } from '@src/importer/ScoreLoader'; -import type { MidiTickLookup } from '@src/midi/MidiTickLookup'; -import { AccentuationType } from '@src/model/AccentuationType'; -import { Duration } from '@src/model/Duration'; -import { VibratoType } from '@src/model/VibratoType'; describe('MidiFileGeneratorTest', () => { const parseTex: (tex: string) => Score = (tex: string): Score => { - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - return importer.readScore(); + return ScoreLoader.loadAlphaTex(tex); }; const assertEvents: (actualEvents: FlatMidiEvent[], expectedEvents: FlatMidiEvent[]) => void = ( @@ -1764,7 +1761,7 @@ describe('MidiFileGeneratorTest', () => { 3.3.8 { timer } 3.3.4 { timer } 3.3.4 { timer } | - + 3.3.4 { timer tempo 60 } 3.3.8 { timer } 3.3.8 { timer tempo 240 } @@ -1804,9 +1801,9 @@ describe('MidiFileGeneratorTest', () => { it('transpose', () => { const score = parseTex(` - \\track \\staff \\instrument piano + \\track \\staff \\instrument piano C4.4| r.1 - \\track \\staff \\instrument piano + \\track \\staff \\instrument piano \\displayTranspose 12 r.1 | C4.4 | r.1 \\track \\staff \\instrument piano diff --git a/packages/alphatab/test/audio/MidiPlaybackController.test.ts b/packages/alphatab/test/audio/MidiPlaybackController.test.ts index 077421d7b..b1dbeed1e 100644 --- a/packages/alphatab/test/audio/MidiPlaybackController.test.ts +++ b/packages/alphatab/test/audio/MidiPlaybackController.test.ts @@ -1,8 +1,7 @@ +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { Logger } from '@src/Logger'; import { MidiPlaybackController } from '@src/midi/MidiPlaybackController'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import type { Score } from '@src/model/Score'; -import { Settings } from '@src/Settings'; -import { Logger } from '@src/Logger'; import { GpImporterTestHelper } from '@test/importer/GpImporterTestHelper'; import { assert, expect } from 'chai'; @@ -46,9 +45,7 @@ describe('MidiPlaybackControllerTest', () => { } function testAlphaTexRepeat(tex: string, expectedBars: number[], maxBars: number): void { - const importer: AlphaTexImporter = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - const score: Score = importer.readScore(); + const score: Score = ScoreLoader.loadAlphaTex(tex); testRepeat(score, expectedBars, maxBars); } @@ -112,8 +109,8 @@ describe('MidiPlaybackControllerTest', () => { it('multiple-closes', () => { const tex: string = ` . - 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | - \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | 4.3.4*4 + 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | + \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | 4.3.4*4 `; const expectedBars: number[] = [ 0, // no repeat @@ -151,7 +148,7 @@ describe('MidiPlaybackControllerTest', () => { . \\ro 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 | 3.3.4*4 | - \\ro 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 + \\ro 4.3.4*4 | \\ro 4.3.4*4 | \\rc 2 4.3.4*4 | 4.3.4*4 | \\rc 2 4.3.4*4 `; const expectedBars: number[] = [ 0, 1, 2, 1, 2, 3, 4, 0, 1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 6, 7, 8, 7, 8, 9, 10 @@ -174,7 +171,7 @@ describe('MidiPlaybackControllerTest', () => { it('da-capo', () => { const tex: string = ` . - \\ro :1 r | \\rc 2 r | \\jump DaCapo r | r + \\ro :1 r | \\rc 2 r | \\jump DaCapo r | r `; const expectedBars: number[] = [0, 1, 0, 1, 2, 0, 1, 2, 3]; testAlphaTexRepeat(tex, expectedBars, 50); @@ -222,7 +219,7 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | + \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; @@ -233,7 +230,7 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | + \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | r | r | \\ro \\rc 2 r `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; @@ -244,7 +241,7 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | + \\jump Segno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | r | r | \\jump DalSegnoAlDoubleCoda r | r | \\jump DoubleCoda r | r | \\ro \\rc 2 r `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; @@ -255,8 +252,8 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump Fine | - r | r | \\jump DalSegnoAlFine r | r | + \\jump Segno r | \\ro \\rc 2 r | \\jump Fine | + r | r | \\jump DalSegnoAlFine r | r | `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5]; testAlphaTexRepeat(tex, expectedBars, 50); @@ -286,7 +283,7 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaCoda r | + \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; @@ -297,7 +294,7 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | + \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | r | r | \\jump DalSegnoSegnoAlDoubleCoda r | r | \\jump DoubleCoda r | r | \\ro \\rc 2 r `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5, 10, 11, 12, 12]; @@ -308,8 +305,8 @@ describe('MidiPlaybackControllerTest', () => { const tex: string = ` . \\ro :1 r | \\rc 2 r | r | - \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump Fine | - r | r | \\jump DalSegnoSegnoAlFine r | r | + \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump Fine | + r | r | \\jump DalSegnoSegnoAlFine r | r | `; const expectedBars: number[] = [0, 1, 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 3, 4, 5]; testAlphaTexRepeat(tex, expectedBars, 50); @@ -318,14 +315,14 @@ describe('MidiPlaybackControllerTest', () => { it('multiple-jumps-same-target', () => { const tex: string = ` . - + \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | + \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r | \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | + \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r `; @@ -342,14 +339,14 @@ describe('MidiPlaybackControllerTest', () => { it('multiple-jumps-different-target', () => { const tex: string = ` . - + \\ro :1 r | \\rc 2 r | r | - \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | + \\jump Segno r | \\ro \\rc 2 r | \\jump DaCoda r | r | r | \\jump DalSegnoAlCoda r | r | \\jump Coda r | r | \\ro \\rc 2 r | \\ro :1 r | \\rc 2 r | r | - \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | + \\jump SegnoSegno r | \\ro \\rc 2 r | \\jump DaDoubleCoda r | r | r | \\jump DalSegnoSegnoAlDoubleCoda r | r | \\jump DoubleCoda r | r | \\ro \\rc 2 r `; diff --git a/packages/alphatab/test/audio/MidiTickLookup.test.ts b/packages/alphatab/test/audio/MidiTickLookup.test.ts index fc42d15b3..7dbd294b7 100644 --- a/packages/alphatab/test/audio/MidiTickLookup.test.ts +++ b/packages/alphatab/test/audio/MidiTickLookup.test.ts @@ -1,4 +1,3 @@ -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { ScoreLoader } from '@src/importer/ScoreLoader'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { Logger } from '@src/Logger'; @@ -626,9 +625,10 @@ describe('MidiTickLookupTest', () => { it('before-beat-grace-later-bars', () => { const settings = new Settings(); - const importer = new AlphaTexImporter(); - importer.initFromString('\\ts 2 4 1.1.2 | 2.1.4 3.1 | 4.1{gr} 5.1{gr} 6.1.2 | 7.1.4 8.1', settings); - const score = importer.readScore(); + const score = ScoreLoader.loadAlphaTex( + '\\ts 2 4 1.1.2 | 2.1.4 3.1 | 4.1{gr} 5.1{gr} 6.1.2 | 7.1.4 8.1', + settings + ); const lookup = buildLookup(score, settings); // bar 2 contains the grace notes which stole duration from fret 3 beat. @@ -744,7 +744,7 @@ describe('MidiTickLookupTest', () => { \\tempo 67 . \\track "T01" - \\ts 1 4 1.1.8 2.1.8 | 6.1.8 7.1.8 | + \\ts 1 4 1.1.8 2.1.8 | 6.1.8 7.1.8 | \\track "T02" 3.1.16 4.1.16 5.1.8 | 8.1.16 9.1.16 10.1.8 `, @@ -869,9 +869,9 @@ describe('MidiTickLookupTest', () => { ` \\track { multiBarRest } \\ts 2 4 - 1.1.4 2.1.4 | - r | r | r | r | - 3.1.4 | + 1.1.4 2.1.4 | + r | r | r | r | + 3.1.4 | r | r | r | \\ro r | r | r | \\rc 2 r | r diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index 79345e64f..828fb972c 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -1,11 +1,11 @@ import { AlphaTexExporter } from '@src/exporter/AlphaTexExporter'; -import { AlphaTexError, AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +import { AlphaTexErrorWithDiagnostics } from '@src/importer/AlphaTexImporter'; import { ScoreLoader } from '@src/importer/ScoreLoader'; import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; import { ComparisonHelpers } from '@test/model/ComparisonHelpers'; import { TestPlatform } from '@test/TestPlatform'; -import { assert, expect } from 'chai'; +import { assert } from 'chai'; describe('AlphaTexExporterTest', () => { async function loadScore(name: string): Promise { @@ -17,12 +17,6 @@ describe('AlphaTexExporterTest', () => { } } - function parseAlphaTex(tex: string): Score { - const readerBase: AlphaTexImporter = new AlphaTexImporter(); - readerBase.initFromString(tex, new Settings()); - return readerBase.readScore(); - } - function exportAlphaTex(score: Score, settings: Settings | null = null): string { return new AlphaTexExporter().exportToString(score, settings); } @@ -40,21 +34,25 @@ describe('AlphaTexExporterTest', () => { ComparisonHelpers.alphaTexExportRoundtripPrepare(expected); exported = exportAlphaTex(expected); - const actual = parseAlphaTex(exported); + const actual = ScoreLoader.loadAlphaTex(exported); ComparisonHelpers.alphaTexExportRoundtripEqual(fileName, actual, expected, ignoreKeys); } catch (e) { - let errorLine = ''; + const errorLines: string[] = []; const error = e as Error; - if (error.cause instanceof AlphaTexError) { - const alphaTexError = error.cause as AlphaTexError; - + const unwrapped = error.cause instanceof AlphaTexErrorWithDiagnostics ? error.cause! : error; + if (unwrapped instanceof AlphaTexErrorWithDiagnostics) { + const withDiag = unwrapped as AlphaTexErrorWithDiagnostics; const lines = exported.split('\n'); - errorLine = `Error Line: ${lines[alphaTexError.line - 1]}\n`; + for (const d of withDiag.iterateDiagnostics()) { + errorLines.push(`Error Line ${lines[d.start!.line - 1]}`); + } } - assert.fail(`<${fileName}>${e}\n${errorLine}${error.stack}\n Tex:\n${exported}`); + assert.fail( + `<${fileName}>${unwrapped.toString()}\n${errorLines.join('\n')}${error.stack}\n Tex:\n${exported}` + ); } } @@ -65,6 +63,69 @@ describe('AlphaTexExporterTest', () => { } } + it('notation-legend-roundtrip', async () => { + const score = (await loadScore('visual-tests/notation-legend/notation-legend.gp'))!; + // fill some more details to cover all features + score.title = 'Notation Legend'; + score.subTitle = 'for test suite'; + score.artist = 'alphaTab'; + + const settings = new Settings(); + settings.exporter.comments = true; + settings.exporter.indent = 2; + + ComparisonHelpers.alphaTexExportRoundtripPrepare(score); + const exported = exportAlphaTex(score!, settings); + + const reimportedScore = ScoreLoader.loadAlphaTex(exported); + ComparisonHelpers.alphaTexExportRoundtripPrepare(reimportedScore); + + ComparisonHelpers.alphaTexExportRoundtripEqual('export-roundtrip', reimportedScore, score); + }); + + it('exact-contents-formatted', async () => { + const score = (await loadScore('visual-tests/notation-legend/notation-legend.gp'))!; + + // fill some more details to cover all features + score.title = 'Notation Legend'; + score.subTitle = 'for test suite'; + score.artist = 'alphaTab'; + + const settings = new Settings(); + settings.exporter.comments = true; + settings.exporter.indent = 2; + + let data = exportAlphaTex(score!, settings); + let expected = await TestPlatform.loadFileAsString('test-data/exporter/notation-legend-formatted.atex'); + + data = data.replaceAll('\r', '').trim(); + expected = expected.replaceAll('\r', '').trim(); + + const expectedLines = expected.split('\n'); + const actualLines = data.split('\n'); + const lines = Math.min(expectedLines.length, actualLines.length); + const errors: string[] = []; + + if (expectedLines.length !== actualLines.length) { + errors.push(`Expected ${expectedLines.length} lines, but only got ${actualLines.length}`); + } + + for (let i = 0; i < lines; i++) { + if (actualLines[i].trimEnd() !== expectedLines[i].trimEnd()) { + errors.push(`Error on line ${i + 1}: `); + errors.push(`+ ${actualLines[i]}`); + errors.push(`- ${expectedLines[i]}`); + } + } + + if (errors.length > 0) { + await TestPlatform.saveFileAsString('test-data/exporter/notation-legend-formatted.atex.new', data); + assert.fail(errors.join('\n')); + } else { + await TestPlatform.deleteFile('test-data/exporter/notation-legend-formatted.atex.new'); + } + }); + // Note: we just test all our importer and visual tests to cover all features it('importer', async () => { @@ -114,25 +175,4 @@ describe('AlphaTexExporterTest', () => { it('gp7-to-alphaTex', async () => { await testRoundTripEqual(`conversion/full-song.gp`); }); - - it('exact-contents-formatted', async () => { - const score = (await loadScore('visual-tests/notation-legend/notation-legend.gp'))!; - - // fill some more details to cover all features - score.title = 'Notation Legend'; - score.subTitle = 'for test suite'; - score.artist = 'alphaTab'; - - const settings = new Settings(); - settings.exporter.comments = true; - settings.exporter.indent = 2; - - let data = exportAlphaTex(score!, settings); - let expected = await TestPlatform.loadFileAsString('test-data/exporter/notation-legend-formatted.atex'); - - data = data.replaceAll('\r', '').trim(); - expected = expected.replaceAll('\r', '').trim(); - - expect(data).to.equal(expected); - }); }); diff --git a/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts b/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts new file mode 100644 index 000000000..6c2dd4c4c --- /dev/null +++ b/packages/alphatab/test/exporter/AlphaTexExporterOld.test.ts @@ -0,0 +1,117 @@ +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import type { Score } from '@src/model/Score'; +import { Settings } from '@src/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 new file mode 100644 index 000000000..5788c2aae --- /dev/null +++ b/packages/alphatab/test/exporter/AlphaTexExporterOld.ts @@ -0,0 +1,1366 @@ +/* + * This file contains a copy of the "old" alphaTex importer + * it was never released but battle tested during implementation + */ +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { Environment } from '@src/Environment'; +import { ScoreExporter } from '@src/exporter/ScoreExporter'; +import { IOHelper } from '@src/io/IOHelper'; +import { GeneralMidi } from '@src/midi/GeneralMidi'; +import { AccentuationType } from '@src/model/AccentuationType'; +import { AutomationType } from '@src/model/Automation'; +import { type Bar, BarLineStyle, SustainPedalMarkerType } from '@src/model/Bar'; +import { BarreShape } from '@src/model/BarreShape'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; +import { BendStyle } from '@src/model/BendStyle'; +import { BendType } from '@src/model/BendType'; +import { BrushType } from '@src/model/BrushType'; +import type { Chord } from '@src/model/Chord'; +import { Clef } from '@src/model/Clef'; +import { CrescendoType } from '@src/model/CrescendoType'; +import { Direction } from '@src/model/Direction'; +import { DynamicValue } from '@src/model/DynamicValue'; +import { FadeType } from '@src/model/FadeType'; +import { FermataType } from '@src/model/Fermata'; +import { Fingers } from '@src/model/Fingers'; +import { GolpeType } from '@src/model/GolpeType'; +import { GraceType } from '@src/model/GraceType'; +import { HarmonicType } from '@src/model/HarmonicType'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import type { MasterBar } from '@src/model/MasterBar'; +import { ModelUtils } from '@src/model/ModelUtils'; +import type { Note } from '@src/model/Note'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { Ottavia } from '@src/model/Ottavia'; +import { PercussionMapper } from '@src/model/PercussionMapper'; +import { PickStroke } from '@src/model/PickStroke'; +import { Rasgueado } from '@src/model/Rasgueado'; +import { + BracketExtendMode, + type RenderStylesheet, + TrackNameMode, + TrackNameOrientation, + TrackNamePolicy +} from '@src/model/RenderStylesheet'; +import { Score } from '@src/model/Score'; +import { SimileMark } from '@src/model/SimileMark'; +import { SlideInType } from '@src/model/SlideInType'; +import { SlideOutType } from '@src/model/SlideOutType'; +import { Staff } from '@src/model/Staff'; +import { Track } from '@src/model/Track'; +import { TripletFeel } from '@src/model/TripletFeel'; +import { Tuning } from '@src/model/Tuning'; +import { VibratoType } from '@src/model/VibratoType'; +import type { Voice } from '@src/model/Voice'; +import { WahPedal } from '@src/model/WahPedal'; +import { WhammyType } from '@src/model/WhammyType'; +import { BeamDirection } from '@src/rendering/_barrel'; +import { Settings } from '@src/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/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 73b7d2d00..88cd07c59 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -1,13 +1,12 @@ +import { Gp7Exporter } from '@src/exporter/Gp7Exporter'; import { Gp7To8Importer } from '@src/importer/Gp7To8Importer'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; import { ByteBuffer } from '@src/io/ByteBuffer'; +import { JsonConverter } from '@src/model/JsonConverter'; import type { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; -import { TestPlatform } from '@test/TestPlatform'; -import { Gp7Exporter } from '@src/exporter/Gp7Exporter'; -import { JsonConverter } from '@src/model/JsonConverter'; -import { ScoreLoader } from '@src/importer/ScoreLoader'; import { ComparisonHelpers } from '@test/model/ComparisonHelpers'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +import { TestPlatform } from '@test/TestPlatform'; import { expect } from 'chai'; describe('Gp7ExporterTest', () => { @@ -119,19 +118,17 @@ describe('Gp7ExporterTest', () => { \\subtitle "JerryC" \\tempo 90 . - :2 19.2{v f} 17.2{v f} | - 15.2{v f} 14.2{v f}| - 12.2{v f} 10.2{v f}| + :2 19.2{v f} 17.2{v f} | + 15.2{v f} 14.2{v f}| + 12.2{v f} 10.2{v f}| 12.2{v f} 14.2{v f}.4 :8 15.2 17.2 | - 14.1.2 :8 17.2 15.1 14.1{h} 17.2 | + 14.1.2 :8 17.2 15.1 14.1{h} 17.2 | 15.2{v d}.4 :16 17.2{h} 15.2 :8 14.2 14.1 17.1{b(0 4 4 0)}.4 | - 15.1.8 :16 14.1{tu 3} 15.1{tu 3} 14.1{tu 3} :8 17.2 15.1 14.1 :16 12.1{tu 3} 14.1{tu 3} 12.1{tu 3} :8 15.2 14.2 | + 15.1.8 :16 14.1{tu 3} 15.1{tu 3} 14.1{tu 3} :8 17.2 15.1 14.1 :16 12.1{tu 3} 14.1{tu 3} 12.1{tu 3} :8 15.2 14.2 | 12.2 14.3 12.3 15.2 :32 14.2{h} 15.2{h} 14.2{h} 15.2{h}14.2{h} 15.2{h}14.2{h} 15.2{h}14.2{h} 15.2{h}14.2{h} 15.2{h}14.2{h} 15.2{h}14.2{h} 15.2{h} `; - const importer = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - const expected = importer.readScore(); + const expected = ScoreLoader.loadAlphaTex(tex); const exported = exportGp7(expected); const actual = prepareImporterWithBytes(exported).readScore(); @@ -142,18 +139,17 @@ describe('Gp7ExporterTest', () => { ComparisonHelpers.expectJsonEqual(expectedJson, actualJson, '', ['accidentalmode']); }); - it('alphatex-drumps-to-gp7', () => { + it('alphatex-drums-to-gp7', () => { const tex = `\\track "Drums" \\instrument percussion - \\clef neutral + \\clef neutral + \\articulation defaults \\articulation Kick 36 \\articulation Unused 46 - Kick.4 42.4 Kick.4 42.4 + Kick.4 "Hi-Hat (closed)".4 Kick.4 "Hi-Hat (closed)".4 `; - const importer = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - const expected = importer.readScore(); + const expected = ScoreLoader.loadAlphaTex(tex); const exported = exportGp7(expected); const actual = prepareImporterWithBytes(exported).readScore(); diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 052535490..cad5f45f0 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -1,56 +1,90 @@ -import { StaveProfile } from '@src/StaveProfile'; -import { AlphaTexError, AlphaTexImporter, AlphaTexLexer, AlphaTexSymbols } from '@src/importer/AlphaTexImporter'; +import { AlphaTexExporter } from '@src/exporter/AlphaTexExporter'; +import { StaffNoteKind } from '@src/importer/alphaTex/AlphaTexShared'; +import { AlphaTexErrorWithDiagnostics, AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; +import { AutomationType } from '@src/model/Automation'; +import { BarreShape } from '@src/model/BarreShape'; import { type Beat, BeatBeamingMode } from '@src/model/Beat'; +import { BendStyle } from '@src/model/BendStyle'; +import { BendType } from '@src/model/BendType'; import { BrushType } from '@src/model/BrushType'; import { Clef } from '@src/model/Clef'; import { CrescendoType } from '@src/model/CrescendoType'; +import { Direction } from '@src/model/Direction'; import { Duration } from '@src/model/Duration'; import { DynamicValue } from '@src/model/DynamicValue'; +import { FadeType } from '@src/model/FadeType'; +import { FermataType } from '@src/model/Fermata'; import { Fingers } from '@src/model/Fingers'; +import { GolpeType } from '@src/model/GolpeType'; import { GraceType } from '@src/model/GraceType'; import { HarmonicType } from '@src/model/HarmonicType'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { Ottavia } from '@src/model/Ottavia'; +import { Rasgueado } from '@src/model/Rasgueado'; +import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; import { type Score, ScoreSubElement } from '@src/model/Score'; +import { SimileMark } from '@src/model/SimileMark'; import { SlideInType } from '@src/model/SlideInType'; import { SlideOutType } from '@src/model/SlideOutType'; import type { Staff } from '@src/model/Staff'; import type { Track } from '@src/model/Track'; import { TripletFeel } from '@src/model/TripletFeel'; import { Tuning } from '@src/model/Tuning'; -import { HarmonicsEffectInfo } from '@src/rendering/effects/HarmonicsEffectInfo'; -import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; -import { Settings } from '@src/Settings'; -import { assert, expect } from 'chai'; -import { ModelUtils } from '@src/model/ModelUtils'; -import { GolpeType } from '@src/model/GolpeType'; -import { FadeType } from '@src/model/FadeType'; -import { BarreShape } from '@src/model/BarreShape'; -import { NoteOrnament } from '@src/model/NoteOrnament'; -import { Rasgueado } from '@src/model/Rasgueado'; -import { Direction } from '@src/model/Direction'; -import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; -import { BeamDirection } from '@src/rendering/utils/BeamDirection'; -import { AutomationType } from '@src/model/Automation'; -import { BendStyle } from '@src/model/BendStyle'; -import { BendType } from '@src/model/BendType'; -import { FermataType } from '@src/model/Fermata'; -import { KeySignature } from '@src/model/KeySignature'; -import { KeySignatureType } from '@src/model/KeySignatureType'; -import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; -import { Ottavia } from '@src/model/Ottavia'; -import { SimileMark } from '@src/model/SimileMark'; import { VibratoType } from '@src/model/VibratoType'; import { WhammyType } from '@src/model/WhammyType'; import { TextAlign } from '@src/platform/ICanvas'; -import { VisualTestHelper } from '@test/visualTests/VisualTestHelper'; +import { BeamDirection } from '@src/rendering/_barrel'; +import { HarmonicsEffectInfo } from '@src/rendering/effects/HarmonicsEffectInfo'; +import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import { Settings } from '@src/Settings'; +import { StaveProfile } from '@src/StaveProfile'; import { ComparisonHelpers } from '@test/model/ComparisonHelpers'; -import { AlphaTexExporter } from '@src/exporter/AlphaTexExporter'; +import { VisualTestHelper } from '@test/visualTests/VisualTestHelper'; +import { assert, expect } from 'chai'; describe('AlphaTexImporterTest', () => { + /** + * @static + */ + function importErrorTest(tex: string) { + const importer: AlphaTexImporter = new AlphaTexImporter(); + importer.initFromString(tex, new Settings()); + try { + importer.readScore(); + assert.fail('Expected error on import'); + } catch { + expect(importer.lexerDiagnostics.errors).toMatchSnapshot('lexer-diagnostics'); + expect(importer.parserDiagnostics.errors).toMatchSnapshot('parser-diagnostics'); + expect(importer.semanticDiagnostics.errors).toMatchSnapshot('semantic-diagnostics'); + } + } + + /** + * @static + */ function parseTex(tex: string): Score { const importer: AlphaTexImporter = new AlphaTexImporter(); + importer.logErrors = true; importer.initFromString(tex, new Settings()); - return importer.readScore(); + try { + return importer.readScore(); + } catch (e) { + const x = (e as Error).cause instanceof AlphaTexErrorWithDiagnostics ? (e as Error).cause : e; + if (x instanceof AlphaTexErrorWithDiagnostics) { + const withDiag = x as AlphaTexErrorWithDiagnostics; + if (e instanceof UnsupportedFormatError) { + throw new UnsupportedFormatError(withDiag.toString()); + } else { + assert.fail(`${withDiag.toString()}`); + } + } + throw e; + } } // as we often add tests here for new alphaTex features, this helper @@ -76,7 +110,7 @@ describe('AlphaTexImporterTest', () => { . 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); + const score = parseTex(tex); expect(score.title).to.equal('Test'); expect(score.words).to.equal('test'); expect(score.music).to.equal('alphaTab'); @@ -131,13 +165,13 @@ describe('AlphaTexImporterTest', () => { . 0.5.1`; - const score: Score = parseTex(tex); + const 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); + const 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); @@ -147,7 +181,7 @@ describe('AlphaTexImporterTest', () => { it('dead-notes2-issue79', () => { const tex: string = ':4 3.3{x}'; - const score: Score = parseTex(tex); + const 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); @@ -157,7 +191,7 @@ describe('AlphaTexImporterTest', () => { it('trill-issue79', () => { const tex: string = ':4 3.3{tr 5 16}'; - const score: Score = parseTex(tex); + const 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); @@ -169,7 +203,7 @@ describe('AlphaTexImporterTest', () => { it('tremolo-issue79', () => { const tex: string = ':4 3.3{tr 5 16}'; - const score: Score = parseTex(tex); + const 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); @@ -181,7 +215,7 @@ describe('AlphaTexImporterTest', () => { it('tremolo-picking-issue79', () => { const tex: string = ':4 3.3{tp 16}'; - const score: Score = parseTex(tex); + const 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); @@ -196,7 +230,7 @@ describe('AlphaTexImporterTest', () => { (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); + const 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); @@ -245,7 +279,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -262,7 +296,7 @@ describe('AlphaTexImporterTest', () => { 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); + const score = parseTex(tex); await VisualTestHelper.prepareAlphaSkia(); const settings: Settings = new Settings(); @@ -300,7 +334,7 @@ describe('AlphaTexImporterTest', () => { it('grace-issue79', () => { const tex: string = ':8 3.3{gr} 3.3{gr ob}'; - const score: Score = parseTex(tex); + const 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); @@ -311,7 +345,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -333,7 +367,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -355,7 +389,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -378,7 +412,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -400,10 +434,11 @@ describe('AlphaTexImporterTest', () => { }); it('unstringed', () => { - const tex: string = '\\tuning piano . c4 c#4 d4 d#4 | c4 db4 d4 eb4'; - const score: Score = parseTex(tex); + const tex: string = '\\instrument piano . c4 c#4 d4 d#4 | c4 db4 d4 eb4'; + const score = parseTex(tex); expect(score.tracks.length).to.equal(1); expect(score.masterBars.length).to.equal(2); + expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); 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); @@ -427,7 +462,7 @@ describe('AlphaTexImporterTest', () => { it('multi-staff-default-settings', () => { const tex: string = '1.1 | 1.1 | \\staff 2.1 | 2.1'; - const score: Score = parseTex(tex); + const 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); @@ -443,7 +478,7 @@ describe('AlphaTexImporterTest', () => { it('multi-staff-default-settings-braces', () => { const tex: string = '1.1 | 1.1 | \\staff{} 2.1 | 2.1'; - const score: Score = parseTex(tex); + const 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); @@ -459,7 +494,7 @@ describe('AlphaTexImporterTest', () => { it('single-staff-with-setting', () => { const tex: string = '\\staff{score} 1.1 | 1.1'; - const score: Score = parseTex(tex); + const 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); @@ -471,7 +506,7 @@ describe('AlphaTexImporterTest', () => { it('single-staff-with-slash', () => { const tex: string = '\\staff{slash} 1.1 | 1.1'; - const score: Score = parseTex(tex); + const 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); @@ -484,7 +519,7 @@ describe('AlphaTexImporterTest', () => { it('single-staff-with-score-and-slash', () => { const tex: string = '\\staff{score slash} 1.1 | 1.1'; - const score: Score = parseTex(tex); + const 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); @@ -499,7 +534,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -519,7 +554,7 @@ describe('AlphaTexImporterTest', () => { it('multi-track', () => { const tex: string = '\\track "First" 1.1 | 1.1 | \\track "Second" 2.2 | 2.2'; - const score: Score = parseTex(tex); + const 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); @@ -542,7 +577,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -587,7 +622,7 @@ describe('AlphaTexImporterTest', () => { \\track "Second Guitar" 1.2 3.2 0.1 1.1 `; - const score: Score = parseTex(tex); + const score = parseTex(tex); expect(score.tracks.length).to.equal(3); expect(score.masterBars.length).to.equal(1); { @@ -665,7 +700,7 @@ describe('AlphaTexImporterTest', () => { \\track "Second Guitar" 1.2 3.2 0.1 1.1 `; - const score: Score = parseTex(tex); + const score = parseTex(tex); expect(score.tracks.length).to.equal(3); expect(score.masterBars.length).to.equal(3); { @@ -741,7 +776,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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( @@ -775,7 +810,7 @@ describe('AlphaTexImporterTest', () => { it('section', () => { const tex: string = '\\section Intro 1.1 | 1.1 | \\section "Chorus 01" 1.1 | \\section S Solo'; - const score: Score = parseTex(tex); + const 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); @@ -795,7 +830,7 @@ describe('AlphaTexImporterTest', () => { 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 score = parseTex(tex); const bars = score.tracks[0].staves[0].bars; const expected: [KeySignature, KeySignatureType][] = [ @@ -826,17 +861,17 @@ describe('AlphaTexImporterTest', () => { it('key-signature-multi-staff', () => { const tex: string = ` - \\track T1 - \\staff + \\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 + \\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); + const score = parseTex(tex); let bars = score.tracks[0].staves[0].bars; const expected: [KeySignature, KeySignatureType][] = [ @@ -874,7 +909,7 @@ describe('AlphaTexImporterTest', () => { it('pop-slap-tap', () => { const tex: string = '3.3{p} 3.3{s} 3.3{tt} r'; - const score: Score = parseTex(tex); + const 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); @@ -883,7 +918,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -897,7 +932,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -910,7 +945,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -923,7 +958,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -938,7 +973,7 @@ describe('AlphaTexImporterTest', () => { it('tuplet-repeat', () => { const tex: string = ':8 5.3{tu 3}*3'; - const score: Score = parseTex(tex); + const score = parseTex(tex); const durations: Duration[] = [Duration.Eighth, Duration.Eighth, Duration.Eighth]; const tuplets = [3, 3, 3]; let i: number = 0; @@ -959,7 +994,7 @@ describe('AlphaTexImporterTest', () => { it('tuplet-custom', () => { const tex: string = ':8 5.3{tu 5 2}*5'; - const score: Score = parseTex(tex); + const score = parseTex(tex); const tupletNumerators = [5, 5, 5, 5, 5]; const tupletDenominators = [2, 2, 2, 2, 2]; @@ -976,7 +1011,7 @@ describe('AlphaTexImporterTest', () => { it('simple-anacrusis', () => { const tex: string = '\\ac 3.3 3.3 | 1.1 2.1 3.1 4.1'; - const score: Score = parseTex(tex); + const 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); @@ -985,7 +1020,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -996,7 +1031,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -1011,7 +1046,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -1025,7 +1060,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -1046,7 +1081,7 @@ describe('AlphaTexImporterTest', () => { 4.3.4*4 | \\ae (1 3) 1.1.1 | \\ae 2 \\rc 3 2.1 | `; - const score: Score = parseTex(tex); + const 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); @@ -1075,11 +1110,11 @@ describe('AlphaTexImporterTest', () => { c4 d4 e4 f4 | \\staff{score} \\tuning piano \\clef F4 c2 c2 c2 c2 | - \\track Guitar + \\track "Guitar" \\staff{tabs} \\instrument acousticguitarsteel \\capo 5 1.2 3.2 0.1 1.1 `; - const score: Score = parseTex(tex); + const score = parseTex(tex); expect(score.tracks[0].staves[0].transpositionPitch).to.equal(0); expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); @@ -1092,7 +1127,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -1106,7 +1141,7 @@ describe('AlphaTexImporterTest', () => { it('dynamics-auto', () => { const tex: string = '1.1.4{dy ppp} 1.1 1.1{dy mp} 1.1'; - const score: Score = parseTex(tex); + const 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); @@ -1116,7 +1151,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -1125,7 +1160,7 @@ describe('AlphaTexImporterTest', () => { 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); + const 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); @@ -1134,7 +1169,7 @@ describe('AlphaTexImporterTest', () => { it('crescendo', () => { const tex: string = '1.1.4{dec} 1.1{dec} 1.1{cre} 1.1{cre}'; - const score: Score = parseTex(tex); + const 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); @@ -1144,7 +1179,7 @@ describe('AlphaTexImporterTest', () => { it('left-hand-tapping', () => { const tex: string = ':4 1.1{lht} 1.1 1.1{lht} 1.1'; - const score: Score = parseTex(tex); + const 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); @@ -1173,7 +1208,7 @@ describe('AlphaTexImporterTest', () => { 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'); + score = parseTex('\\instrument acousticpiano . C4'); expect(score.tracks[0].staves[0].tuning.length).to.equal(0); expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); }); @@ -1192,16 +1227,8 @@ describe('AlphaTexImporterTest', () => { 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`); + 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); @@ -1227,7 +1254,7 @@ describe('AlphaTexImporterTest', () => { \\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: AlphaTexImporter = new AlphaTexImporter(); + const importer = new AlphaTexImporter(); for (const _i of [1, 2]) { importer.initFromString(tex, new Settings()); const score = importer.readScore(); @@ -1281,30 +1308,6 @@ describe('AlphaTexImporterTest', () => { } }); - 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); @@ -1402,7 +1405,7 @@ describe('AlphaTexImporterTest', () => { 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 + . \\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).to.have.length(2); expect(score.masterBars[0].tempoAutomations[0].value).to.equal(120); @@ -1416,8 +1419,10 @@ describe('AlphaTexImporterTest', () => { let tex = '. \n'; const expectedAccidentalModes: NoteAccidentalMode[] = []; for (const [k, v] of ModelUtils.accidentalModeMapping) { - tex += `3.3 { acc ${k} } \n`; - expectedAccidentalModes.push(v); + if (k) { + tex += `3.3 { acc ${k} } \n`; + expectedAccidentalModes.push(v); + } } const score = parseTex(tex); @@ -1547,9 +1552,9 @@ describe('AlphaTexImporterTest', () => { const score = parseTex(` \\track "Piano" \\staff{score} \\tuning piano \\instrument acousticgrandpiano - \\voice + \\voice c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice + \\voice c3 d3 e3 f3 | c3 d3 e3 f3 `); @@ -1563,9 +1568,9 @@ describe('AlphaTexImporterTest', () => { it('multi-voice-simple-all-voices', () => { const score = parseTex(` - \\voice + \\voice c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice + \\voice c3 d3 e3 f3 | c3 d3 e3 f3 `); @@ -1580,7 +1585,7 @@ describe('AlphaTexImporterTest', () => { it('multi-voice-simple-skip-initial', () => { const score = parseTex(` c4 d4 e4 f4 | c4 d4 e4 f4 - \\voice + \\voice c3 d3 e3 f3 | c3 d3 e3 f3 `); @@ -1623,7 +1628,7 @@ describe('AlphaTexImporterTest', () => { it('transpose', () => { const score = parseTex(` - \\staff + \\staff \\displaytranspose 12 \\transpose 6 . @@ -1671,7 +1676,7 @@ describe('AlphaTexImporterTest', () => { it('beat-ottava', () => { const score = parseTex(` - 3.3.4{ ot 15ma } 3.3.4{ ot 8vb } + 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); @@ -1807,9 +1812,9 @@ describe('AlphaTexImporterTest', () => { it('tempo-automation-text', () => { const score = parseTex(` - \\tempo 100 T1 + \\tempo 100 "T1" . - 3.3.4 * 4 | \\tempo 80 T2 4.3.4*4 + 3.3.4 * 4 | \\tempo 80 "T2" 4.3.4*4 `); expect(score.tempo).to.equal(100); expect(score.tempoLabel).to.equal('T1'); @@ -1822,7 +1827,7 @@ describe('AlphaTexImporterTest', () => { 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); + const score = parseTex(tex); expect(score.masterBars[1].isDoubleBar).to.be.equal(true); testExportRoundtrip(score); }); @@ -1872,13 +1877,13 @@ describe('AlphaTexImporterTest', () => { it('track-properties', () => { const score = parseTex(` - \\track "First" { - color "#FF0000" + \\track "First" { + color "#FF0000" defaultSystemsLayout 6 systemsLayout 3 2 3 volume 7 balance 3 - mute + mute solo } `); @@ -1923,7 +1928,7 @@ describe('AlphaTexImporterTest', () => { it('note-show-string', () => { const score = parseTex(` - :8 3.3{ string } + :8 3.3{ string } `); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].showStringNumber).to.be.true; @@ -1932,7 +1937,7 @@ describe('AlphaTexImporterTest', () => { it('note-hide', () => { const score = parseTex(` - :8 3.3{ hide } + :8 3.3{ hide } `); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].isVisible).to.be.false; @@ -1967,9 +1972,9 @@ describe('AlphaTexImporterTest', () => { 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 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; @@ -1979,11 +1984,26 @@ describe('AlphaTexImporterTest', () => { 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}`); + 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); @@ -1993,11 +2013,11 @@ describe('AlphaTexImporterTest', () => { const score = parseTex(` \\multiBarRest . - \\track A { multiBarRest } + \\track "A" { multiBarRest } 3.3 - \\track B + \\track "B" 3.3 - + `); expect(score.stylesheet.multiTrackMultiBarRest).to.be.true; expect(score.stylesheet.perTrackMultiBarRest).to.be.ok; @@ -2088,17 +2108,17 @@ describe('AlphaTexImporterTest', () => { \\instrument piano . \\track "T1" - \\staff - \\barlineleft dashed - \\barlineright dotted - | + \\staff + \\barlineleft dashed + \\barlineright dotted + | \\barlineleft heavyheavy \\barlineright heavyheavy - - \\staff - \\barlineleft lightlight - \\barlineright lightheavy - | + + \\staff + \\barlineleft lightlight + \\barlineright lightheavy + | \\barlineleft heavylight \\barlineright dashed `); @@ -2113,7 +2133,7 @@ describe('AlphaTexImporterTest', () => { \\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 0 \\sync 0 0 1000 0.5 \\sync 1 0 2000 \\sync 3 0 3000 @@ -2140,8 +2160,8 @@ describe('AlphaTexImporterTest', () => { . \\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) (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 @@ -2175,7 +2195,7 @@ describe('AlphaTexImporterTest', () => { it('volume-change', () => { const score = parseTex(` - \\track "T1" { + \\track "T1" { volume 7 } G4 G4 { volume 8 } G4 { volume 9 } @@ -2199,7 +2219,7 @@ describe('AlphaTexImporterTest', () => { it('balance-change', () => { const score = parseTex(` - \\track "T1" { + \\track "T1" { balance 7 } G4 G4 { balance 8 } G4 { balance 9 } @@ -2241,66 +2261,6 @@ describe('AlphaTexImporterTest', () => { testExportRoundtrip(score); }); - function parseNumberOrNameTest(tex: string, allowFloats: boolean, expectedSymbols: string[]) { - const lexer = new AlphaTexLexer(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" @@ -2319,10 +2279,10 @@ describe('AlphaTexImporterTest', () => { 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"} + 3.3.4 + 3.3.4 {lyrics "A"} + 3.3.4 {lyrics 0 "B C D"} + 3.3.4 {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; @@ -2343,7 +2303,7 @@ describe('AlphaTexImporterTest', () => { testExportRoundtrip(score); }); - + it('bank', () => { const score = parseTex(` \\track "Piano" { instrument electricpiano1} @@ -2362,19 +2322,123 @@ describe('AlphaTexImporterTest', () => { 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 - `); + // Here we should focus on all the semantic tests: + // - all metadata tags (valid and invalid variants) + // - all properties (valid and invalid variants) + // - proper diagnostics reporting + // - value list type validations + // - round-trip tests on old/new importer/exporter (backwards compatibility, and verification of new parser) + + describe('errors', () => { + describe('at209', () => { + it('tuning', () => importErrorTest('\\tuning Invalid')); + it('articulation', () => importErrorTest('\\articulation "Test" 0')); + it('score required', () => importErrorTest('\\title (1)')); + it('bar required', () => importErrorTest('. \\rc ("a")')); + it('score optional', () => importErrorTest('\\title ("Title" 1)')); + it('bar optional', () => importErrorTest('. \\section ("a" 1)')); + it('duration tuplet', () => importErrorTest('. :4 {tu 0}')); + it('beat tuplet', () => importErrorTest('. C4 {tu 0}')); + it('tremolo speed', () => importErrorTest('. C4 {tp 0}')); + it('beam', () => importErrorTest('. C4 {beam invalid}')); + it('trill', () => importErrorTest('. 3.3 {tr 4 0}')); + it('bracketextendmode', () => importErrorTest('\\bracketextendmode invalid')); + it('singletracktracknamepolicy', () => importErrorTest('\\singletracktracknamepolicy invalid')); + it('multitracktracknamepolicy', () => importErrorTest('\\multitracktracknamepolicy invalid')); + it('firstsystemtracknamemode', () => importErrorTest('\\firstsystemtracknamemode invalid')); + it('othersystemstracknamemode', () => importErrorTest('\\othersystemstracknamemode invalid')); + it('firstsystemtracknameorientation', () => importErrorTest('\\firstsystemtracknameorientation invalid')); + it('othersystemstracknameorientation', () => importErrorTest('\\firstsystemtracknameorientation invalid')); + it('accidentalmode', () => importErrorTest('\\accidentals invalid')); + it('textalign', () => importErrorTest('\\title "Test" "" invalid')); + it('whammybartype', () => importErrorTest('C4 {tb invalid (0 1)}')); + it('whammybarstyle', () => importErrorTest('C4 {tb none invalid (0 1)}')); + it('dynamic', () => importErrorTest('C4 {dy invalid}')); + it('rasg', () => importErrorTest('C4 {rasg invalid}')); + it('ottava', () => importErrorTest('C4 {ot invalid}')); + it('fermata', () => importErrorTest('C4 {fermata (invalid)}')); + it('bendtype', () => importErrorTest('C4 {b invalid (0 4)}')); + it('bendstyle', () => importErrorTest('C4 {b bend invalid (0 4)}')); + it('gracetype', () => importErrorTest('C4 {gr (invalid)}')); + it('barre', () => importErrorTest('C4 {barre (1 invalid) }')); + }); - expect(score.masterBars[0].tempoAutomations[0].isVisible).to.be.true; - expect(score.masterBars[1].tempoAutomations[0].isVisible).to.be.false; + describe('at210', () => { + it('score empty', () => importErrorTest('\\title ()')); + it('bar empty', () => importErrorTest('. \\ts ()')); + it('bar missing', () => importErrorTest('. \\ts (3)')); + }); + }); - testExportRoundtrip(score); + describe('bar-meta-interweaving', () => { + function test(tex: string) { + expect(parseTex(tex)).toMatchSnapshot(); + } + + describe('initial', () => { + it('meta-track', () => test(`\\clef C3 \\track "T1"`)); + it('meta-track-staff', () => test(`\\clef C3 \\track "T1" \\staff`)); + it('meta-track-staff-voice', () => test(`\\clef C3 \\track "T1" \\staff \\voice`)); + it('meta-track-staff-voice-voice', () => test(`\\clef C3 \\track "T1" \\staff \\voice \\voice`)); + it('meta-track-staff-staff', () => test(`\\clef C3 \\track "T1" \\staff \\staff`)); + it('meta-track-voice', () => test(`\\clef C3 \\track "T1" \\voice`)); + it('meta-track-voice-voice', () => test(`\\clef C3 \\track "T1" \\voice \\voice`)); + it('meta-track-track', () => test(`\\clef C3 \\track "T1" \\track "T2"`)); + + it('meta-staff', () => test(`\\clef C3 \\staff`)); + it('meta-staff-voice', () => test(`\\clef C3 \\staff \\voice`)); + it('meta-staff-voice-voice', () => test(`\\clef C3 \\staff \\voice \\voice`)); + it('meta-staff-staff', () => test(`\\clef C3 \\staff \\staff`)); + + it('meta-voice', () => test(`\\clef C3 \\voice`)); + it('meta-voice-voice', () => test(`\\clef C3 \\voice \\voice`)); + it('meta-track-track-meta', () => test(`\\clef C3 \\track "T1" \\track "T2" \\clef C4`)); + }); + + describe('with-previous-bars', () => { + it('meta-track', () => test(`C4 | C5 | \\clef C3 \\track "T1"`)); + it('meta-track-staff', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\staff`)); + it('meta-track-staff-voice', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\staff \\voice`)); + it('meta-track-staff-voice-voice', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\staff \\voice \\voice`)); + it('meta-track-staff-staff', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\staff \\staff`)); + it('meta-track-voice', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\voice`)); + it('meta-track-voice-voice', () => test(`\\clef C3 \\track "T1" \\voice \\voice`)); + it('meta-track-track', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\track "T2"`)); + + it('meta-staff', () => test(`C4 | C5 | \\clef C3 \\staff`)); + it('meta-staff-voice', () => test(`C4 | C5 | \\clef C3 \\staff \\voice`)); + it('meta-staff-voice-voice', () => test(`C4 | C5 | \\clef C3 \\staff \\voice \\voice`)); + it('meta-staff-staff', () => test(`C4 | C5 | \\clef C3 \\staff \\staff`)); + + it('meta-voice', () => test(`C4 | C5 | \\clef C3 \\voice`)); + it('meta-voice-voice', () => test(`C4 | C5 | \\clef C3 \\voice \\voice`)); + it('meta-track-track-meta', () => test(`C4 | C5 | \\clef C3 \\track "T1" \\track "T2" \\clef C4`)); + }); + }); + + describe('staff-autodetect', () => { + // tests for autodetecting staff note kinds + function test(tex: string, type: StaffNoteKind) { + const importer = new AlphaTexImporter(); + importer.initFromString(tex, new Settings()); + const s = importer.readScore(); + expect(importer.getStaffNoteKind(s.tracks[0].staves[0])).to.equal(type); + } + + // - direct values + describe('direct', () => { + it('pitch-value', () => test(`C4`, StaffNoteKind.Pitched)); + it('fretted', () => test(`3.3`, StaffNoteKind.Fretted)); + it('articulation', () => test(`"Ride (choke)"`, StaffNoteKind.Articulation)); + }); + + // - with tuning already specified + describe('tuning', () => { + it('pitch-value', () => test(`\\tuning (C4 C4 C4) C4`, StaffNoteKind.Pitched)); + it('fretted', () => test(`\\tuning (C4 C4 C4) 3.3`, StaffNoteKind.Fretted)); + it('articulation', () => test(`\\tuning (C4 C4 C4) "Ride (choke)"`, StaffNoteKind.Articulation)); + }); + + // - with instrument already specified }); }); diff --git a/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts b/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts new file mode 100644 index 000000000..2094d4ed2 --- /dev/null +++ b/packages/alphatab/test/importer/AlphaTexImporterOld.test.ts @@ -0,0 +1,2401 @@ +import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; +import { AutomationType } from '@src/model/Automation'; +import { BarreShape } from '@src/model/BarreShape'; +import { type Beat, BeatBeamingMode } from '@src/model/Beat'; +import { BendStyle } from '@src/model/BendStyle'; +import { BendType } from '@src/model/BendType'; +import { BrushType } from '@src/model/BrushType'; +import { Clef } from '@src/model/Clef'; +import { CrescendoType } from '@src/model/CrescendoType'; +import { Direction } from '@src/model/Direction'; +import { Duration } from '@src/model/Duration'; +import { DynamicValue } from '@src/model/DynamicValue'; +import { FadeType } from '@src/model/FadeType'; +import { FermataType } from '@src/model/Fermata'; +import { Fingers } from '@src/model/Fingers'; +import { GolpeType } from '@src/model/GolpeType'; +import { GraceType } from '@src/model/GraceType'; +import { HarmonicType } from '@src/model/HarmonicType'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { Ottavia } from '@src/model/Ottavia'; +import { Rasgueado } from '@src/model/Rasgueado'; +import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; +import { type Score, ScoreSubElement } from '@src/model/Score'; +import { SimileMark } from '@src/model/SimileMark'; +import { SlideInType } from '@src/model/SlideInType'; +import { SlideOutType } from '@src/model/SlideOutType'; +import type { Staff } from '@src/model/Staff'; +import type { Track } from '@src/model/Track'; +import { TripletFeel } from '@src/model/TripletFeel'; +import { Tuning } from '@src/model/Tuning'; +import { VibratoType } from '@src/model/VibratoType'; +import { WhammyType } from '@src/model/WhammyType'; +import { TextAlign } from '@src/platform/ICanvas'; +import { HarmonicsEffectInfo } from '@src/rendering/effects/HarmonicsEffectInfo'; +import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { Settings } from '@src/Settings'; +import { StaveProfile } from '@src/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).to.have.length(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).to.have.length(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).to.have.length(2); + + expect(score.tracks[0].staves[0].bars).to.have.length(2); + expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2); + expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(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).to.have.length(2); + + expect(score.tracks[0].staves[0].bars).to.have.length(2); + expect(score.tracks[0].staves[0].bars[0].voices).to.have.length(2); + expect(score.tracks[0].staves[0].bars[1].voices).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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).to.have.length(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 new file mode 100644 index 000000000..cfc8539d5 --- /dev/null +++ b/packages/alphatab/test/importer/AlphaTexImporterOld.ts @@ -0,0 +1,3733 @@ +import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { BeatCloner } from '@src/generated/model/BeatCloner'; +import { AlphaTexAccidentalMode } from '@src/importer/alphaTex/AlphaTexShared'; +import { ScoreImporter } from '@src/importer/ScoreImporter'; +import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; +import { ByteBuffer } from '@src/io/ByteBuffer'; +import { IOHelper } from '@src/io/IOHelper'; +import { Logger } from '@src/Logger'; +import { GeneralMidi } from '@src/midi/GeneralMidi'; +import { AccentuationType } from '@src/model/AccentuationType'; +import { Automation, AutomationType, type FlatSyncPoint } from '@src/model/Automation'; +import { Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; +import { BarreShape } from '@src/model/BarreShape'; +import { Beat, BeatBeamingMode } from '@src/model/Beat'; +import { BendPoint } from '@src/model/BendPoint'; +import { BendStyle } from '@src/model/BendStyle'; +import { BendType } from '@src/model/BendType'; +import { BrushType } from '@src/model/BrushType'; +import { Chord } from '@src/model/Chord'; +import { Clef } from '@src/model/Clef'; +import { Color } from '@src/model/Color'; +import { CrescendoType } from '@src/model/CrescendoType'; +import { Direction } from '@src/model/Direction'; +import { Duration } from '@src/model/Duration'; +import { DynamicValue } from '@src/model/DynamicValue'; +import { FadeType } from '@src/model/FadeType'; +import { Fermata, FermataType } from '@src/model/Fermata'; +import { Fingers } from '@src/model/Fingers'; +import { GolpeType } from '@src/model/GolpeType'; +import { GraceType } from '@src/model/GraceType'; +import { HarmonicType } from '@src/model/HarmonicType'; +import { KeySignature } from '@src/model/KeySignature'; +import { KeySignatureType } from '@src/model/KeySignatureType'; +import { Lyrics } from '@src/model/Lyrics'; +import { MasterBar } from '@src/model/MasterBar'; +import { ModelUtils, type TuningParseResult } from '@src/model/ModelUtils'; +import { Note } from '@src/model/Note'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { Ottavia } from '@src/model/Ottavia'; +import { PercussionMapper } from '@src/model/PercussionMapper'; +import { PickStroke } from '@src/model/PickStroke'; +import { Rasgueado } from '@src/model/Rasgueado'; +import { BracketExtendMode, TrackNameMode, TrackNameOrientation, TrackNamePolicy } from '@src/model/RenderStylesheet'; +import { Score, ScoreSubElement } from '@src/model/Score'; +import { Section } from '@src/model/Section'; +import { SimileMark } from '@src/model/SimileMark'; +import { SlideInType } from '@src/model/SlideInType'; +import { SlideOutType } from '@src/model/SlideOutType'; +import type { Staff } from '@src/model/Staff'; +import { Track } from '@src/model/Track'; +import { TripletFeel } from '@src/model/TripletFeel'; +import { Tuning } from '@src/model/Tuning'; +import { VibratoType } from '@src/model/VibratoType'; +import { Voice } from '@src/model/Voice'; +import { WahPedal } from '@src/model/WahPedal'; +import { WhammyType } from '@src/model/WhammyType'; +import { TextAlign } from '@src/platform/ICanvas'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import type { Settings } from '@src/Settings'; +import { SynthConstants } from '@src/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 new file mode 100644 index 000000000..7a550c208 --- /dev/null +++ b/packages/alphatab/test/importer/AlphaTexImporterOldNewCompat.test.ts @@ -0,0 +1,230 @@ +import { AlphaTexErrorWithDiagnostics, AlphaTexImporter } from '@src/importer/AlphaTexImporter'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { Logger } from '@src/Logger'; +import type { Score } from '@src/model/Score'; +import { Settings } from '@src/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 + + 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/AlphaTexLexer.test.ts b/packages/alphatab/test/importer/AlphaTexLexer.test.ts new file mode 100644 index 000000000..73d2a7e58 --- /dev/null +++ b/packages/alphatab/test/importer/AlphaTexLexer.test.ts @@ -0,0 +1,164 @@ +import { type AlphaTexAstNode, AlphaTexNodeType, type AlphaTexNumberLiteral } from '@src/importer/alphaTex/AlphaTexAst'; +import { AlphaTexLexer } from '@src/importer/alphaTex/AlphaTexLexer'; +import { expect } from 'chai'; + +describe('AlphaTexLexerTest', () => { + function lexerTest(source: string, diagnostics: boolean = false) { + const lexer = new AlphaTexLexer(source); + const actual: AlphaTexAstNode[] = []; + + while (true) { + const token = lexer.peekToken(); + if (!token) { + break; + } + actual.push(token); + lexer.advance(); + } + + expect(actual).toMatchSnapshot(); + if (diagnostics) { + expect(lexer.lexerDiagnostics.items).toMatchSnapshot(); + } + } + + function floatTest(source: string) { + const lexer = new AlphaTexLexer(source); + + const actual: AlphaTexAstNode[] = []; + + while (true) { + const token = lexer.peekToken(); + if (!token) { + break; + } + + if (token.nodeType === AlphaTexNodeType.Number) { + actual.push(lexer.extendToFloat(token as AlphaTexNumberLiteral)); + } else { + actual.push(token); + } + lexer.advance(); + } + + expect(actual).toMatchSnapshot(); + } + + it('strings', () => { + lexerTest(`"Double Quoted"`); + lexerTest(`'Single Quoted'`); + lexerTest(`'Multiple' "Strings"`); + lexerTest(`"Double \\"Quoted\\""`); + lexerTest(`'Single \\'Quoted\\''`); + lexerTest(`"\\r\\n\\t"`); + lexerTest(`"\\R\\N\\T"`); + lexerTest(`"\\uD83D\\uDE38"`); + lexerTest(`"😸🤘🏻"`); + }); + + it('numbers', () => { + lexerTest(`1`); + lexerTest(`1234`); + lexerTest(`-1`); + lexerTest(`-1234`); + lexerTest(`1234 5678`); + }); + + it('basic-tokens', () => { + lexerTest(`.`); + lexerTest(`:`); + lexerTest(`(`); + lexerTest(`)`); + lexerTest(`{`); + lexerTest(`}`); + lexerTest(`|`); + lexerTest(`*`); + }); + + it('meta-command', () => { + lexerTest('\\title'); + lexerTest('\\\\double'); + lexerTest('\\withNumber123'); + lexerTest('\\withUnicode😼'); + lexerTest('\\withUnicode😼 . \\withNumber123'); + }); + + it('whitespace', () => { + lexerTest(' . \r\n\t\v .'); + }); + + it('identifiers', () => { + lexerTest('true'); + lexerTest('false'); + lexerTest('HelloWorld'); + lexerTest('C4'); + lexerTest('C#4'); + lexerTest('Cb4'); + lexerTest('electricpiano1'); + lexerTest('Unicodeöäü Unicode😸 Utf16🤘🏻'); + lexerTest('dashed-identifier'); + lexerTest('HelloWorld Multiple Identifiers'); + }); + + it('floats', () => { + floatTest('1.1'); + floatTest('11.22 33.44'); + floatTest('1.1.4'); + floatTest('1.1 .4'); + floatTest('1 .1.4'); + floatTest('-1.1'); + floatTest('-.1'); + floatTest('1.1('); + floatTest('1.1{'); + floatTest('1.1|'); + floatTest('1.1a'); + floatTest('1a.1'); + floatTest('1.1\\test'); + }); + + it('leading-comments', () => { + lexerTest(` + // Single + true + /* Multi */ + /* Multi2 */ + // Single + false + `); + }); + + it('trailing-comments', () => { + lexerTest(` + true // Single After + false /* Multi After */ + /* before */ true /* middle */ false // after + `); + }); + + describe('errors', () => { + it('at001', () => { + lexerTest('/a */', true); + }); + + it('at002', () => { + lexerTest('\\ "Test"', true); + }); + + it('at003', () => { + lexerTest('"\\uAB"', true); + }); + + it('at004', () => { + lexerTest('"\\uXXXX"', true); + }); + + it('at005', () => { + lexerTest('"\\b01010101"', true); + }); + + it('at006', () => { + lexerTest('"double', true); + lexerTest("'single", true); + }); + }); +}); diff --git a/packages/alphatab/test/importer/AlphaTexParser.test.ts b/packages/alphatab/test/importer/AlphaTexParser.test.ts new file mode 100644 index 000000000..175658683 --- /dev/null +++ b/packages/alphatab/test/importer/AlphaTexParser.test.ts @@ -0,0 +1,237 @@ +import { AlphaTexParser } from '@src/importer/alphaTex/AlphaTexParser'; +import { expect } from 'chai'; + +describe('AlphaTexParserTest', () => { + function parserTest(source: string) { + const parser = new AlphaTexParser(source); + const node = parser.read(); + expect(node).to.be.ok; + expect(node).toMatchSnapshot(); + expect(parser.lexerDiagnostics.errors).toMatchSnapshot('lexer-diagnostics'); + expect(parser.parserDiagnostics.errors).toMatchSnapshot('parser-diagnostics'); + } + + describe('valid-empty', () => { + it('empty', () => parserTest('')); + }); + + describe('valid-score-metadata', () => { + it('empty', () => parserTest(' . ')); + it('known valuelist', () => parserTest('\\title ("Title") . ')); + it('known semantic', () => parserTest('\\title "Title" "Template" left . ')); + it('known multiple', () => parserTest('\\title "Title" \\subtitle "Sub" . ')); + it('known property', () => parserTest('\\track "Name" {color "red"} . ')); + it('known property before value', () => parserTest('\\track {color "red"} "Name" . ')); + it('unknown valuelist', () => parserTest('\\notExisting ("Value") . ')); + it('unknown multiple', () => parserTest('\\notExisting ("Value") \\notExisting ("") . ')); + it('valuelist propertylist empty', () => parserTest('\\notExisting ("Value") {} . ')); + it('valuelist propertylist unknown prop', () => parserTest('\\notExisting ("Value") {unknown (1 2 3)} . ')); + }); + + describe('valid-bars', () => { + it('no score meta', () => parserTest(' C4 | C4 ')); + it('with score meta', () => parserTest('\\title "Test" . C4 | C4 ')); + it('empty at end', () => parserTest('C4 | C4 | ')); + it('multiple empty', () => parserTest('C4 | C4 | | | | ')); + it('multiple empty then filled', () => parserTest('C4 | C4 | | | | C4 ')); + }); + + describe('valid-bar-meta', () => { + it('unknown no score meta', () => parserTest('\\notExisting ("Value") C4 | C4 ')); + it('unkonwn score meta', () => parserTest('. \\notExisting ("Value") C4 | C4 ')); + it('known no score meta', () => parserTest('\\ts 3 4 C4 | C4 ')); + it('known score meta', () => parserTest('. \\ts 3 4 ("Value") C4 | C4 ')); + }); + + describe('valid-beats-basic-pitched', () => { + it('basic', () => parserTest('C4 C5')); + it('duration change', () => parserTest(':2 C4 :4 C5')); + it('duration', () => parserTest('C4.4 C5.8')); + it('multiplier', () => parserTest('C4*4 C5*2')); + it('effects empty', () => parserTest('C4.4 {} C5.4 {}')); + it('effects known', () => parserTest('C4.4 {v f} C5.4 {cre tu 3 2}')); + it('complex', () => parserTest(':2 C4.8 * 2 {cre tu 3 2}')); + }); + + describe('valid-beats-chord-pitched', () => { + it('chord', () => parserTest('(C4 C5) (D4 D5)')); + it('duration change', () => parserTest(':2 (C4 C5) :4 (D4 D5)')); + it('duration', () => parserTest('(C4 C5).4 (D4 D5).8')); + it('multiplier', () => parserTest('(C4 C5)*4 (D4 D5)*2')); + it('effects empty', () => parserTest('(C4 C5) {} (D4 D5) {}')); + it('effects known', () => parserTest('(C4 C5) {v f} (D4 D5) {cre tu 3 2}')); + it('complex', () => parserTest(':2 (C4 C5).8 * 2 {cre tu 3 2}')); + }); + + describe('valid-beats-basic-fretted', () => { + it('basic', () => parserTest('3.3 4.2')); + it('duration change', () => parserTest(':2 3.3 :4 4.2')); + it('duration', () => parserTest('3.3.4 4.2.8')); + it('multiplier', () => parserTest('3.3*4 4.2*2')); + it('effects empty', () => parserTest('3.3.4 {} 4.2.4 {}')); + it('effects known', () => parserTest('3.3.4 {v f} 4.2.4 {cre tu 3 2}')); + it('complex', () => parserTest(':2 3.3.8 * 2 {cre tu 3 2}')); + it('spacing', () => parserTest('3.3.8 | 3 . 3 . 8 | 3.3 .8 | 3 . 3.8')); + }); + + describe('valid-beats-chord-fretted', () => { + it('chord', () => parserTest('(3.3 4.2) (1.2 6.1)')); + it('duration change', () => parserTest(':2 (3.3 4.2) :4 (1.2 6.1)')); + it('duration', () => parserTest('(3.3 4.2).4 (1.2 6.1).8')); + it('multiplier', () => parserTest('(3.3 4.2)*4 (1.2 6.1)*2')); + it('effects empty', () => parserTest('(3.3 4.2).4 {} (1.2 6.1).4 {}')); + it('effects known', () => parserTest('(3.3 4.2).4 {v f} (1.2 6.1).4 {cre tu 3 2}')); + it('complex', () => parserTest(':2 (3.3 4.2).8 * 2 {cre tu 3 2}')); + it('spacing', () => + parserTest(` + ( + 3.3 + 3 . 3 + ) . 8`)); + }); + + describe('valid-beats-rest', () => { + it('rest', () => parserTest('r')); + it('duration change', () => parserTest(':2 r :4 r')); + it('duration', () => parserTest('r.4 r.8')); + it('multiplier', () => parserTest('r*4 r*2')); + it('effects empty', () => parserTest('r.4 {} r.4 {}')); + it('effects known', () => parserTest('r.4 {v f} r.4 {cre tu 3 2}')); + it('complex', () => parserTest(':2 r.8 * 2 {cre tu 3 2}')); + }); + + describe('valid-beat-effects', () => { + it('empty', () => parserTest('C4.8 {}')); + it('unknown', () => parserTest('C4.8 {unknown (1 2 3)}')); + it('known', () => parserTest('C4.8 {tu 2 3}')); + it('known with list', () => parserTest('C4.8 {tb (0 -2 0)}')); + it('multiple', () => parserTest('C4.8 {cre tb (0 -2 0) tu 3 2}')); + }); + + describe('valid-note-effects', () => { + it('empty', () => parserTest('C4 {}')); + it('unknown', () => parserTest('C4 {unknown (1 2 3)}')); + it('known', () => parserTest('C4 {tr 4 4}')); + it('known with list', () => parserTest('C4 {b (0 4 0)}')); + it('multiple', () => parserTest('C4 {nh b (0 4 0) v}')); + it('multiple chord', () => parserTest('(C4 {nh b (0 4 0) v} C5 { v h unknown (1 2 3)})')); + it('with beat effects', () => parserTest('C4 {nh b (0 4 0) v} { tu 3 2 }')); + it('beat effects in note effect', () => parserTest('C4 { tu 3 2 }')); + it('multiple chord beat effects', () => + parserTest('(C4 {nh b (0 4 0) v} C5 { v h unknown (1 2 3)}) { tu 3 2 }')); + }); + + describe('valid-sync-points', () => { + it('empty', () => parserTest(' . . ')); + it('empty with bars', () => parserTest(' . C4 | C5 . ')); + it('empty with bars empty at end', () => parserTest(' . C4 | C5 | . ')); + it('basic simple pitched', () => parserTest(' . C4 . \\sync 1 1 1')); + it('basic simple numbered', () => parserTest(' . 32 . \\sync 1 1 1')); + it('basic fretted', () => parserTest(' . 3.3 . \\sync 1 1 1')); + it('full', () => parserTest(' \\title "Test" . C4.4 . \\sync 1 1 1')); + it('valuelist', () => parserTest(' . C4 . \\sync (1 1 1)')); + it('properties empty', () => parserTest(' . C4 . \\sync (1 1 1) {} ')); + it('properties unknown', () => parserTest(' . C4 . \\sync (1 1 1) { unknown (1 2 3) } ')); + }); + + describe('floats', () => { + it('tempo', () => parserTest('. \\tempo 120 "Moderate" 0.5')); + it('tempo parenthesis', () => parserTest('. \\tempo (120 "Moderate" 0.5)')); + it('valuelist parenthesis', () => parserTest('\\unknown (1.2 2.3)')); + it('valuelist', () => parserTest('. \\scale 0.5')); + }); + + describe('comments', () => { + it('score meta singleline', () => parserTest('// Single \n \\title "Test"')); + it('score meta multiline', () => parserTest('/* multi\nline*/ \\title "Test"')); + it('score meta multiline middle', () => parserTest('\\title /* multi\nline*/ "Test"')); + it('bar meta singleline', () => parserTest('. \n// Single \n \\ts 3 4')); + it('bar multiline', () => parserTest('.\n/* multi\nline*/ \\ts 3 4')); + it('bar multiline middle', () => parserTest('.\n\\ts 3 /* multi\nline*/ 4')); + it('beat singleline', () => parserTest('. C4\n // Single \n C5')); + it('beat multiline', () => parserTest('. C4\n /* multi\nline*/ C5')); + it('beat chord singleline', () => parserTest('. (C4\n // Single \n C5)')); + it('beat chord multiline', () => parserTest('. (C4\n /* multi\nline*/ C5)')); + it('beateffects singleline', () => parserTest('. (C4 C5) {\n // Single \n } ')); + it('beateffects multiline', () => parserTest('. (C4 C5) {\n /* multi\nline*/ }')); + it('noteeffects singleline', () => parserTest('. (C4 {\n // Single \n } C5)')); + it('noteeffects multiline', () => parserTest('. (C4 {\n /* multi\nline*/ } C5)')); + }); + + describe('ambiguous', () => { + it('tempo and stringed note', () => parserTest('\\tempo 120 3.3 3.4')); + it('tempo, temponame and stringed note', () => parserTest('\\tempo 120 "Moderate" 3.4')); + }); + + // TODO: check how much of the AST we need to provide code completion + // currently the parser/lexer are rather "fail fast" and do not + // provide many intermediately parsed nodes. + + describe('intermediate', () => { + it('started initial meta', () => parserTest('\\')); + it('started meta', () => parserTest('\\tr')); + it('started meta properties', () => parserTest('\\track "X" {')); + it('started meta property name', () => parserTest('\\track "X" { co')); + it('started meta property value', () => parserTest('\\track "X" { color "')); + it('finished meta property value, not closed ', () => parserTest('\\track "X" { color "red"')); + + it('started meta string', () => parserTest('\\title "Ti')); + + it('started pitched note', () => parserTest('\\title "Title" . C')); + it('started note effects', () => parserTest('\\title "Title" . C4 { ')); + it('started note effect name', () => parserTest('\\title "Title" . C4 { slu')); + it('started note effect value', () => parserTest('\\title "Title" . C4 { slur "S1')); + it('finished note effect value, not closed', () => parserTest('\\title "Title" . C4 { slur "S1"')); + it('started beat duration', () => parserTest('\\title "Title" . C4 { slur "S1" } .')); + it('started beat effects', () => parserTest('\\title "Title" . C4 { slur "S1" } . 4 { ')); + it('started beat effect name', () => parserTest('\\title "Title" . C4 { slur "S1" } . 4 { ras')); + it('started beat effect value', () => parserTest('\\title "Title" . C4 { slur "S1" } . 4 { rasg "i')); + it('finished beat effect value, not closed', () => + parserTest('\\title "Title" . C4 { slur "S1" } . 4 { rasg "i" ')); + }); + + describe('errors', () => { + describe('at200', () => { + it('missing', () => parserTest('(3.3) * ')); + it('type', () => parserTest('(3.3) * A')); + }); + + describe('at201', () => { + it('missing', () => parserTest(':')); + it('type', () => parserTest(':A')); + }); + + describe('at202', () => { + it('beat duration', () => parserTest('(C4).A')); + it('note string', () => parserTest('3.A')); + it('note value', () => parserTest(' . ( \\notevalue )')); + it('value list', () => parserTest('\\meta (\\metavalue)')); + it('meta value', () => parserTest('\\title 10')); + }); + + describe('at203', () => { + it('note string', () => parserTest('. (3.')); + it('meta value', () => parserTest('\\title')); + }); + + describe('at204', () => { + it('score', () => parserTest('\\unknown')); + it('bar', () => parserTest('. \\unknown')); + }); + + describe('at205', () => { + it('bar', () => parserTest('. \\track "Test" {unknown}')); + it('beat', () => parserTest('. (C4) {unknown}')); + it('note', () => parserTest('. (C4{unknown})')); + it('gracetype', () => parserTest('C4 {gr invalid}')); + it('barre', () => parserTest('C4 {barre 1 invalid}')); + it('fermata', () => parserTest('C4 {fermata invalid}')); + }); + + describe('at206', () => { + it('note list', () => parserTest('(C4')); + it('properties', () => parserTest('\\track "Test" { color red')); + it('values', () => parserTest('\\title ("Test"')); + }); + }); +}); diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap index f1357659a..bc091000a 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap @@ -1,5 +1,4816 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-staff 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-staff-staff 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-staff-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-staff-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-staff 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-staff-staff 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-staff-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-staff-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-track 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T2", + "shortname" => "T2", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-track-meta 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 1, + "clef" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T2", + "shortname" => "T2", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-track-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving initial meta-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-staff 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-staff-staff 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-staff-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-staff-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-staff 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-staff-staff 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-staff-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-staff-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-track 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 4, + "secondarychannel" => 5, + }, + "name" => "T2", + "shortname" => "T2", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-track-meta 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 6, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "clef" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 7, + "clef" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 8, + "clef" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 4, + "secondarychannel" => 5, + }, + "name" => "T2", + "shortname" => "T2", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + Map { + "__kind" => "Track", + "staves" => Array [ + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 4, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 5, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "primarychannel" => 2, + "secondarychannel" => 3, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-track-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "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, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 0, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 0, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + "displaytranspositionpitch" => -12, + "stringtuning" => Map { + "isstandard" => true, + "name" => "Guitar Standard Tuning", + "tunings" => Array [ + 64, + 59, + 55, + 50, + 45, + 40, + ], + }, + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + +exports[`AlphaTexImporterTest bar-meta-interweaving with-previous-bars meta-voice-voice 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => false, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "start" => 7680, + }, + ], + "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" => 25, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 6, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 4, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "notes" => Array [ + Map { + "__kind" => "Note", + "id" => 1, + "octave" => 6, + "tone" => 0, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 4, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 5, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 7, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 6, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + Map { + "__kind" => "Bar", + "id" => 2, + "clef" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 7, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 5, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 8, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + Map { + "__kind" => "Voice", + "id" => 8, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + }, + ], + }, + ], + "playbackinfo" => Map { + "program" => 25, + "secondarychannel" => 1, + }, + }, + ], +} +`; + exports[`AlphaTexImporterTest barlines 1`] = ` Map { "__kind" => "Score", @@ -13,7 +4824,7 @@ Map { "value" => 120, "ratioposition" => 0, "text" => "", - "isvisible" => true, + "isvisible" => false, }, ], }, @@ -70,7 +4881,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 1, + "id" => 3, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -96,7 +4907,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 2, + "id" => 1, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -117,7 +4928,7 @@ Map { "beats" => Array [ Map { "__kind" => "Beat", - "id" => 3, + "id" => 2, "isempty" => true, "displayduration" => 960, "playbackduration" => 960, @@ -141,6 +4952,663 @@ Map { } `; +exports[`AlphaTexImporterTest errors at209 accidentalmode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 accidentalmode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 accidentalmode: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected accidental mode value 'invalid', expected: auto,explicit", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 articulation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 articulation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 articulation: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected articulation value '0', expected: 38,37,91,42,92,46,44,35,36,50,48,47,45,43,93,51,53,94,55,95,52,96,49,97,57,98,99,100,56,101,102,103,77,76,60,104,105,61,106,107,66,65,68,67,64,108,109,63,110,62,72,71,73,74,86,87,54,111,112,113,79,78,58,81,80,114,115,116,69,117,85,75,70,118,119,120,82,122,84,123,83,124,125,39,40,31,41,59,126,127,29,30,33,34", + "start" => Map { + "col" => 23, + "line" => 1, + "offset" => 22, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bar optional: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar optional: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar optional: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected additional value 'Number', expected: required(String|Ident),optional(String|Ident)", + "start" => Map { + "col" => 18, + "line" => 1, + "offset" => 17, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bar required: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar required: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bar required: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected required value 'String', expected: required(Number)", + "start" => Map { + "col" => 10, + "line" => 1, + "offset" => 9, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 barre: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 barre: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 barre: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected additional value 'Ident', expected: required(Number),optional(String|Ident)", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected barre shape value 'invalid', expected: none,full,half", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 beam: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 beam: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 beam: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected beam value 'invalid', expected: invert,up,down,auto,split,merge,splitsecondary", + "start" => Map { + "col" => 19, + "line" => 1, + "offset" => 18, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 beat tuplet: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 beat tuplet: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 beat tuplet: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected default tuplet value '0', expected: 3, 5, 6, 7, 9, 10, 11 or 12", + "start" => Map { + "col" => 11, + "line" => 1, + "offset" => 10, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bendstyle: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bendstyle: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bendstyle: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected bend style value 'invalid', expected: default,gradual,fast", + "start" => Map { + "col" => 19, + "line" => 1, + "offset" => 18, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bendtype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bendtype: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bendtype: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected bend type value 'invalid', expected: none,custom,bend,release,bendrelease,hold,prebend,prebendbend,prebendrelease", + "start" => Map { + "col" => 14, + "line" => 1, + "offset" => 13, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 bracketextendmode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bracketextendmode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 bracketextendmode: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected bracket extend mode value 'invalid', expected: nobrackets,groupstaves,groupsimilarinstruments", + "start" => Map { + "col" => 27, + "line" => 1, + "offset" => 26, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 duration tuplet: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 duration tuplet: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 duration tuplet: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected default tuplet value '0', expected: 3, 5, 6, 7, 9, 10, 11 or 12", + "start" => Map { + "col" => 11, + "line" => 1, + "offset" => 10, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 dynamic: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 dynamic: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 dynamic: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected dynamic value 'invalid', expected: ppp,pp,p,mp,mf,f,ff,fff,pppp,ppppp,pppppp,ffff,fffff,ffffff,sf,sfp,sfpp,fp,rf,rfz,sfz,sffz,fz,n,pf,sfzp", + "start" => Map { + "col" => 15, + "line" => 1, + "offset" => 14, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 fermata: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 fermata: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 fermata: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected additional value 'Ident', expected: optional(String|Ident),optional(Number)", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected fermata value 'invalid', expected: short,medium,long", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 firstsystemtracknamemode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 firstsystemtracknamemode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 firstsystemtracknamemode: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected track name mode value 'invalid', expected: fullname,shortname", + "start" => Map { + "col" => 34, + "line" => 1, + "offset" => 33, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 firstsystemtracknameorientation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 firstsystemtracknameorientation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 firstsystemtracknameorientation: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected track name orientation value 'invalid', expected: horizontal,vertical", + "start" => Map { + "col" => 41, + "line" => 1, + "offset" => 40, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 gracetype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 gracetype: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 gracetype: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected additional value 'Ident', expected: optional(String|Ident)", + "start" => Map { + "col" => 16, + "line" => 1, + "offset" => 15, + }, + }, + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected whammy style value 'invalid', expected: ob,b,bb", + "start" => Map { + "col" => 16, + "line" => 1, + "offset" => 15, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 multitracktracknamepolicy: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 multitracktracknamepolicy: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 multitracktracknamepolicy: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected track name policy value 'invalid', expected: hidden,firstsystem,allsystems", + "start" => Map { + "col" => 35, + "line" => 1, + "offset" => 34, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 othersystemstracknamemode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 othersystemstracknamemode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 othersystemstracknamemode: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected track name mode value 'invalid', expected: fullname,shortname", + "start" => Map { + "col" => 35, + "line" => 1, + "offset" => 34, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 othersystemstracknameorientation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 othersystemstracknameorientation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 othersystemstracknameorientation: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected track name orientation value 'invalid', expected: horizontal,vertical", + "start" => Map { + "col" => 41, + "line" => 1, + "offset" => 40, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 ottava: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 ottava: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 ottava: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected ottava value 'invalid', expected: 15ma,8va,regular,8vb,15mb", + "start" => Map { + "col" => 15, + "line" => 1, + "offset" => 14, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 rasg: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 rasg: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 rasg: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected rasgueado pattern value 'invalid', expected: none,ii,mi,miitriplet,miianapaest,pmptriplet,pmpanapaest,peitriplet,peianapaest,paitriplet,paianapaest,amitriplet,amianapaest,ppp,amii,amip,eami,eamii,peami", + "start" => Map { + "col" => 17, + "line" => 1, + "offset" => 16, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 score optional: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 score optional: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 score optional: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected additional value 'Number', expected: required(String|Ident),optional(String),optional(String|Ident)", + "start" => Map { + "col" => 18, + "line" => 1, + "offset" => 17, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 score required: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 score required: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 score required: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected required value 'Number', expected: required(String|Ident)", + "start" => Map { + "col" => 10, + "line" => 1, + "offset" => 9, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 singletracktracknamepolicy: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 singletracktracknamepolicy: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 singletracktracknamepolicy: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected track name policy value 'invalid', expected: hidden,firstsystem,allsystems", + "start" => Map { + "col" => 36, + "line" => 1, + "offset" => 35, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 textalign: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 textalign: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 textalign: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected percussion articulation value 'invalid', expected: oneOf(ride (choke),ridechoke,cymbal (hit),cymbalhit,snare (side stick),snaresidestick,snare (side stick) 2,snaresidestick2,snare (hit),snarehit,kick (hit),kickhit,kick (hit) 2,kickhit2,snare (side stick) 3,snaresidestick3,snare (hit) 2,snarehit2,hand clap (hit),handclaphit,snare (hit) 3,snarehit3,low floor tom (hit),lowfloortomhit,hi-hat (closed),hihatclosed,very low tom (hit),verylowtomhit,pedal hi-hat (hit),pedalhihathit,low tom (hit),lowtomhit,hi-hat (open),hihatopen,mid tom (hit),midtomhit,high tom (hit),hightomhit,crash high (hit),crashhighhit,high floor tom (hit),highfloortomhit,ride (middle),ridemiddle,china (hit),chinahit,ride (bell),ridebell,tambourine (hit),tambourinehit,splash (hit),splashhit,cowbell medium (hit),cowbellmediumhit,crash medium (hit),crashmediumhit,vibraslap (hit),vibraslaphit,ride (edge),rideedge,hand (hit),handhit,bongo high (hit),bongohighhit,bongo low (hit),bongolowhit,conga high (mute),congahighmute,conga high (hit),congahighhit,conga low (hit),congalowhit,timbale high (hit),timbalehighhit,timbale low (hit),timbalelowhit,agogo high (hit),agogohighhit,agogo tow (hit),agogotowhit,cabasa (hit),cabasahit,left maraca (hit),leftmaracahit,whistle high (hit),whistlehighhit,whistle low (hit),whistlelowhit,guiro (hit),guirohit,guiro (scrap-return),guiroscrapreturn,claves (hit),claveshit,woodblock high (hit),woodblockhighhit,woodblock low (hit),woodblocklowhit,cuica (mute),cuicamute,cuica (open),cuicaopen,triangle (rnute),trianglernute,triangle (hit),trianglehit,shaker (hit),shakerhit,tinkle bell (hat),tinklebellhat,jingle bell (hit),jinglebellhit,bell tree (hit),belltreehit,castanets (hit),castanetshit,surdo (hit),surdohit,surdo (mute),surdomute,snare (rim shot),snarerimshot,hi-hat (half),hihathalf,ride (edge) 2,rideedge2,ride (choke) 2,ridechoke2,splash (choke),splashchoke,china (choke),chinachoke,crash high (choke),crashhighchoke,crash medium (choke),crashmediumchoke,cowbell low (hit),cowbelllowhit,cowbell low (tip),cowbelllowtip,cowbell medium (tip),cowbellmediumtip,cowbell high (hit),cowbellhighhit,cowbell high (tip),cowbellhightip,hand (mute),handmute,hand (slap),handslap,hand (mute) 2,handmute2,hand (slap) 2,handslap2,conga low (slap),congalowslap,conga low (mute),congalowmute,conga high (slap),congahighslap,tambourine (return),tambourinereturn,tambourine (roll),tambourineroll,tambourine (hand),tambourinehand,grancassa (hit),grancassahit,piatti (hat),piattihat,piatti (hand),piattihand,cabasa (return),cabasareturn,left maraca (return),leftmaracareturn,right maraca (hit),rightmaracahit,right maraca (return),rightmaracareturn,shaker (return),shakerreturn,bell tee (return),bellteereturn,golpe (thumb),golpethumb,golpe (finger),golpefinger,ride (middle) 2,ridemiddle2,ride (bell) 2,ridebell2).", + "start" => Map { + "col" => 25, + "line" => 1, + "offset" => 24, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 tremolo speed: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 tremolo speed: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 tremolo speed: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected tremolo speed value '0, expected: 8, 16 or 32", + "start" => Map { + "col" => 11, + "line" => 1, + "offset" => 10, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 trill: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 trill: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 trill: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected trill duration value '0', expected: 16, 32 or 64", + "start" => Map { + "col" => 14, + "line" => 1, + "offset" => 13, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 tuning: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 tuning: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 tuning: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected tuning value 'Invalid', expected: where =oneOf(67,68,69,70,71,65,66,99,100,101,102,103,97,98,35) =oneOf(default,d,,forcenone,-,forcenatural,n,forcesharp,#,forcedoublesharp,##,x,forceflat,b,forcedoubleflat,bb), =number", + "start" => Map { + "col" => 16, + "line" => 1, + "offset" => 15, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 whammybarstyle: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 whammybarstyle: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 whammybarstyle: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected whammy style value 'invalid', expected: default,gradual,fast", + "start" => Map { + "col" => 20, + "line" => 1, + "offset" => 19, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at209 whammybartype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 whammybartype: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at209 whammybartype: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 209, + "severity" => 2, + "message" => "Unexpected whammy type value 'invalid', expected: none,custom,dive,dip,hold,predive,predivedive", + "start" => Map { + "col" => 15, + "line" => 1, + "offset" => 14, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at210 bar empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at210 bar empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at210 bar empty: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 210, + "severity" => 2, + "message" => "Missing values. Expected following values: required(Number)", + "start" => Map { + "col" => 8, + "line" => 1, + "offset" => 7, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at210 bar missing: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at210 bar missing: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at210 bar missing: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 210, + "severity" => 2, + "message" => "Missing values. Expected following values: required(Number)", + "start" => Map { + "col" => 9, + "line" => 1, + "offset" => 8, + }, + }, +] +`; + +exports[`AlphaTexImporterTest errors at210 score empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at210 score empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterTest errors at210 score empty: semantic-diagnostics 1`] = ` +Array [ + Map { + "code" => 210, + "severity" => 2, + "message" => "Missing values. Expected following values: required(String|Ident)", + "start" => Map { + "col" => 9, + "line" => 1, + "offset" => 8, + }, + }, +] +`; + exports[`AlphaTexImporterTest sync 1`] = ` Map { "__kind" => "Score", @@ -373,25 +5841,5 @@ Map { "start" => 8640, }, ], - "style" => Map { - "headerandfooter" => Map { - "0" => Map { - "template" => "%TITLE%", - "isvisible" => true, - "textalign" => 1, - }, - "2" => Map { - "template" => "%ARTIST%", - "isvisible" => true, - "textalign" => 1, - }, - "8" => Map { - "template" => "%COPYRIGHT%", - "isvisible" => true, - "textalign" => 1, - }, - }, - "colors" => Map {}, - }, } `; diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterNew.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterNew.test.ts.snap new file mode 100644 index 000000000..6460fcc7a --- /dev/null +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterNew.test.ts.snap @@ -0,0 +1,1147 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`AlphaTexImporterNewTest barlines 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 2, + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 4, + "barlineright" => 4, + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 7, + "barlineright" => 6, + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 5, + "barlineright" => 1, + }, + ], + }, + ], + "playbackinfo" => Map { + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterNewTest errors at209 accidentalmode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 accidentalmode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 accidentalmode: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 21, + "line": 1, + "offset": 20, + }, + "message": "Unexpected accidental mode value 'invalid', expected: auto,explicit", + "severity": 2, + "start": Object { + "col": 14, + "line": 1, + "offset": 13, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 articulation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 articulation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 articulation: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 23, + "line": 1, + "offset": 22, + }, + "message": "Unexpected articulation value '0', expected: 38,37,91,42,92,46,44,35,36,50,48,47,45,43,93,51,53,94,55,95,52,96,49,97,57,98,99,100,56,101,102,103,77,76,60,104,105,61,106,107,66,65,68,67,64,108,109,63,110,62,72,71,73,74,86,87,54,111,112,113,79,78,58,81,80,114,115,116,69,117,85,75,70,118,119,120,82,122,84,123,83,124,125,39,40,31,41,59,126,127,29,30,33,34", + "severity": 2, + "start": Object { + "col": 22, + "line": 1, + "offset": 21, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 bar optional: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bar optional: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bar optional: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 18, + "line": 1, + "offset": 17, + }, + "message": "Unexpected additional value 'NumberLiteral', expected: required(StringLiteral|Identifier),optional(StringLiteral|Identifier)", + "severity": 2, + "start": Object { + "col": 17, + "line": 1, + "offset": 16, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 bar required: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bar required: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bar required: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + "message": "Unexpected required value 'StringLiteral', expected: required(NumberLiteral)", + "severity": 2, + "start": Object { + "col": 8, + "line": 1, + "offset": 7, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 barre: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 barre: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 barre: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 21, + "line": 1, + "offset": 20, + }, + "message": "Unexpected barre shape value 'invalid', expected: full,half", + "severity": 2, + "start": Object { + "col": 14, + "line": 1, + "offset": 13, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 beam: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 beam: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 beam: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 19, + "line": 1, + "offset": 18, + }, + "message": "Unexpected beam value 'invalid', expected: invert,up,down,auto,split,merge,splitsecondary", + "severity": 2, + "start": Object { + "col": 12, + "line": 1, + "offset": 11, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 beat tuplet: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 beat tuplet: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 beat tuplet: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 11, + "line": 1, + "offset": 10, + }, + "message": "Unexpected default tuplet value '0', expected: 3, 5, 6, 7, 9, 10, 11 or 12", + "severity": 2, + "start": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 bendstyle: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bendstyle: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bendstyle: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 19, + "line": 1, + "offset": 18, + }, + "message": "Unexpected bend style value 'invalid', expected: gradual,fast,default", + "severity": 2, + "start": Object { + "col": 12, + "line": 1, + "offset": 11, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 bendtype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bendtype: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bendtype: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 14, + "line": 1, + "offset": 13, + }, + "message": "Unexpected bend type value 'invalid', expected: none,custom,bend,release,bendrelease,hold,prebend,prebendbend,prebendrelease", + "severity": 2, + "start": Object { + "col": 7, + "line": 1, + "offset": 6, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 bracketextendmode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bracketextendmode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 bracketextendmode: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 27, + "line": 1, + "offset": 26, + }, + "message": "Unexpected bracket extend mode value 'invalid', expected: nobrackets,groupstaves,groupsimilarinstruments", + "severity": 2, + "start": Object { + "col": 20, + "line": 1, + "offset": 19, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 duration tuplet: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 duration tuplet: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 duration tuplet: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 11, + "line": 1, + "offset": 10, + }, + "message": "Unexpected default tuplet value '0', expected: 3, 5, 6, 7, 9, 10, 11 or 12", + "severity": 2, + "start": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 dynamic: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 dynamic: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 dynamic: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 15, + "line": 1, + "offset": 14, + }, + "message": "Unexpected dynamic value 'invalid', expected: ppp,pp,p,mp,mf,f,ff,fff,pppp,ppppp,pppppp,ffff,fffff,ffffff,sf,sfp,sfpp,fp,rf,rfz,sfz,sffz,fz,n,pf,sfzp", + "severity": 2, + "start": Object { + "col": 8, + "line": 1, + "offset": 7, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 fermata: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 fermata: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 fermata: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 21, + "line": 1, + "offset": 20, + }, + "message": "Unexpected fermata value 'invalid', expected: short,medium,long", + "severity": 2, + "start": Object { + "col": 14, + "line": 1, + "offset": 13, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 firstsystemtracknamemode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 firstsystemtracknamemode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 firstsystemtracknamemode: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 34, + "line": 1, + "offset": 33, + }, + "message": "Unexpected track name mode value 'invalid', expected: fullname,shortname", + "severity": 2, + "start": Object { + "col": 27, + "line": 1, + "offset": 26, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 firstsystemtracknameorientation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 firstsystemtracknameorientation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 firstsystemtracknameorientation: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 41, + "line": 1, + "offset": 40, + }, + "message": "Unexpected track name orientation value 'invalid', expected: horizontal,vertical", + "severity": 2, + "start": Object { + "col": 34, + "line": 1, + "offset": 33, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 gracetype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 gracetype: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 gracetype: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 16, + "line": 1, + "offset": 15, + }, + "message": "Unexpected whammy style value 'invalid', expected: ob,b,bb", + "severity": 2, + "start": Object { + "col": 9, + "line": 1, + "offset": 8, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 multitracktracknamepolicy: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 multitracktracknamepolicy: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 multitracktracknamepolicy: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 35, + "line": 1, + "offset": 34, + }, + "message": "Unexpected track name policy value 'invalid', expected: hidden,firstsystem,allsystems", + "severity": 2, + "start": Object { + "col": 28, + "line": 1, + "offset": 27, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 othersystemstracknamemode: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 othersystemstracknamemode: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 othersystemstracknamemode: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 35, + "line": 1, + "offset": 34, + }, + "message": "Unexpected track name mode value 'invalid', expected: fullname,shortname", + "severity": 2, + "start": Object { + "col": 28, + "line": 1, + "offset": 27, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 othersystemstracknameorientation: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 othersystemstracknameorientation: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 othersystemstracknameorientation: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 41, + "line": 1, + "offset": 40, + }, + "message": "Unexpected track name orientation value 'invalid', expected: horizontal,vertical", + "severity": 2, + "start": Object { + "col": 34, + "line": 1, + "offset": 33, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 ottava: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 ottava: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 ottava: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 15, + "line": 1, + "offset": 14, + }, + "message": "Unexpected ottava value 'invalid', expected: 15ma,8va,regular,8vb,15mb", + "severity": 2, + "start": Object { + "col": 8, + "line": 1, + "offset": 7, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 rasg: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 rasg: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 rasg: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 17, + "line": 1, + "offset": 16, + }, + "message": "Unexpected rasgueado pattern value 'invalid', expected: ii,mi,miitriplet,miianapaest,pmptriplet,pmpanapaest,peitriplet,peianapaest,paitriplet,paianapaest,amitriplet,amianapaest,ppp,amii,amip,eami,eamii,peami", + "severity": 2, + "start": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 score optional: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 score optional: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 score optional: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 18, + "line": 1, + "offset": 17, + }, + "message": "Unexpected additional value 'NumberLiteral', expected: required(StringLiteral|Identifier),optional(StringLiteral|Identifier),optional(StringLiteral|Identifier)", + "severity": 2, + "start": Object { + "col": 17, + "line": 1, + "offset": 16, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 score required: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 score required: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 score required: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + "message": "Unexpected required value 'NumberLiteral', expected: required(StringLiteral|Identifier)", + "severity": 2, + "start": Object { + "col": 9, + "line": 1, + "offset": 8, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 singletracktracknamepolicy: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 singletracktracknamepolicy: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 singletracktracknamepolicy: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 36, + "line": 1, + "offset": 35, + }, + "message": "Unexpected track name policy value 'invalid', expected: hidden,firstsystem,allsystems", + "severity": 2, + "start": Object { + "col": 29, + "line": 1, + "offset": 28, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 textalign: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 textalign: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 textalign: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 25, + "line": 1, + "offset": 24, + }, + "message": "Unexpected textAlign value 'invalid', expected: left,center,right", + "severity": 2, + "start": Object { + "col": 18, + "line": 1, + "offset": 17, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 tremolo speed: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 tremolo speed: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 tremolo speed: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 11, + "line": 1, + "offset": 10, + }, + "message": "Unexpected tremolo speed value '0, expected: 8, 16 or 32", + "severity": 2, + "start": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 trill: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 trill: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 trill: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 14, + "line": 1, + "offset": 13, + }, + "message": "Unexpected trill duration value '0', expected: 16, 32 or 64", + "severity": 2, + "start": Object { + "col": 13, + "line": 1, + "offset": 12, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 tuning: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 tuning: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 tuning: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 16, + "line": 1, + "offset": 15, + }, + "message": "Unexpected tuning value 'Invalid', expected: where =oneOf(67,68,69,70,71,65,66,99,100,101,102,103,97,98,35) =oneOf(default,d,forcenone,-,forcenatural,n,forcesharp,#,forcedoublesharp,##,x,forceflat,b,forcedoubleflat,bb), =number", + "severity": 2, + "start": Object { + "col": 9, + "line": 1, + "offset": 8, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 whammybarstyle: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 whammybarstyle: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 whammybarstyle: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 20, + "line": 1, + "offset": 19, + }, + "message": "Unexpected whammy style value 'invalid', expected: gradual,fast,default", + "severity": 2, + "start": Object { + "col": 13, + "line": 1, + "offset": 12, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at209 whammybartype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 whammybartype: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at209 whammybartype: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 209, + "end": Object { + "col": 15, + "line": 1, + "offset": 14, + }, + "message": "Unexpected whammy type value 'invalid', expected: none,custom,dive,dip,hold,predive,predivedive", + "severity": 2, + "start": Object { + "col": 8, + "line": 1, + "offset": 7, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at210 bar empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at210 bar empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at210 bar empty: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 210, + "end": Object { + "col": 9, + "line": 1, + "offset": 8, + }, + "message": "Missing values. Expected following values: required(NumberLiteral)", + "severity": 2, + "start": Object { + "col": 9, + "line": 1, + "offset": 8, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at210 bar missing: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at210 bar missing: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at210 bar missing: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 210, + "end": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + "message": "Missing values. Expected following values: required(NumberLiteral)", + "severity": 2, + "start": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest errors at210 score empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at210 score empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexImporterNewTest errors at210 score empty: semantic-diagnostics 1`] = ` +Array [ + Object { + "code": 210, + "end": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + "message": "Missing values. Expected following values: required(StringLiteral|Identifier)", + "severity": 2, + "start": Object { + "col": 10, + "line": 1, + "offset": 9, + }, + }, +] +`; + +exports[`AlphaTexImporterNewTest sync 1`] = ` +Map { + "__kind" => "Score", + "tempo" => 90, + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 90, + "ratioposition" => 0, + "text" => "", + }, + ], + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 0, + }, + "ratioposition" => 0, + "text" => "", + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 1000, + }, + "ratioposition" => 0.5, + "text" => "", + }, + ], + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 2000, + }, + "ratioposition" => 0, + "text" => "", + }, + ], + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "isrepeatstart" => true, + "start" => 7680, + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 3000, + }, + "ratioposition" => 0, + "text" => "", + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 1, + "millisecondoffset" => 4000, + }, + "ratioposition" => 0, + "text" => "", + }, + ], + "start" => 11520, + }, + Map { + "__kind" => "MasterBar", + "repeatcount" => 2, + "start" => 15360, + }, + Map { + "__kind" => "MasterBar", + "start" => 19200, + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 1, + "millisecondoffset" => 5000, + }, + "ratioposition" => 0, + "text" => "", + }, + ], + "start" => 23040, + }, + ], +} +`; + +exports[`AlphaTexImporterNewTest sync-expect-dot 1`] = ` +Map { + "__kind" => "Score", + "artist" => "J.S. Bach (1685-1750)", + "copyright" => "Public Domain", + "title" => "Prelude in D Minor", + "tempo" => 80, + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 80, + "ratioposition" => 0, + "text" => "", + }, + ], + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 0, + }, + "ratioposition" => 0, + "text" => "", + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 1500, + }, + "ratioposition" => 0.666, + "text" => "", + }, + ], + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 4075, + }, + "ratioposition" => 0.666, + "text" => "", + }, + ], + "start" => 2880, + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 6475, + }, + "ratioposition" => 0.333, + "text" => "", + }, + ], + "start" => 5760, + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 10223, + }, + "ratioposition" => 1, + "text" => "", + }, + ], + "start" => 8640, + }, + ], +} +`; diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap new file mode 100644 index 000000000..64ad3244a --- /dev/null +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporterOld.test.ts.snap @@ -0,0 +1,397 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`AlphaTexImporterOldTest barlines 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 120, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "start" => 3840, + }, + ], + "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, + "isempty" => true, + "automations" => Array [ + Map { + "islinear" => false, + "type" => 2, + "value" => 0, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 1, + "barlineright" => 2, + }, + Map { + "__kind" => "Bar", + "id" => 1, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 1, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 1, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 4, + "barlineright" => 4, + }, + ], + }, + Map { + "__kind" => "Staff", + "bars" => Array [ + Map { + "__kind" => "Bar", + "id" => 2, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 2, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 2, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 7, + "barlineright" => 6, + }, + Map { + "__kind" => "Bar", + "id" => 3, + "voices" => Array [ + Map { + "__kind" => "Voice", + "id" => 3, + "beats" => Array [ + Map { + "__kind" => "Beat", + "id" => 3, + "isempty" => true, + "displayduration" => 960, + "playbackduration" => 960, + }, + ], + }, + ], + "barlineleft" => 5, + "barlineright" => 1, + }, + ], + }, + ], + "playbackinfo" => Map { + "secondarychannel" => 1, + }, + "name" => "T1", + "shortname" => "T1", + }, + ], +} +`; + +exports[`AlphaTexImporterOldTest sync 1`] = ` +Map { + "__kind" => "Score", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 90, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 0, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 1000, + }, + "ratioposition" => 0.5, + "text" => "", + "isvisible" => true, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 2000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 3840, + }, + Map { + "__kind" => "MasterBar", + "isrepeatstart" => true, + "start" => 7680, + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 3000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 1, + "millisecondoffset" => 4000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 11520, + }, + Map { + "__kind" => "MasterBar", + "repeatcount" => 2, + "start" => 15360, + }, + Map { + "__kind" => "MasterBar", + "start" => 19200, + }, + Map { + "__kind" => "MasterBar", + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 1, + "millisecondoffset" => 5000, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 23040, + }, + ], +} +`; + +exports[`AlphaTexImporterOldTest sync-expect-dot 1`] = ` +Map { + "__kind" => "Score", + "artist" => "J.S. Bach (1685-1750)", + "copyright" => "Public Domain", + "title" => "Prelude in D Minor", + "masterbars" => Array [ + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "tempoautomations" => Array [ + Map { + "islinear" => false, + "type" => 0, + "value" => 80, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + ], + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 0, + }, + "ratioposition" => 0, + "text" => "", + "isvisible" => true, + }, + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 1500, + }, + "ratioposition" => 0.666, + "text" => "", + "isvisible" => true, + }, + ], + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 4075, + }, + "ratioposition" => 0.666, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 2880, + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 6475, + }, + "ratioposition" => 0.333, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 5760, + }, + Map { + "__kind" => "MasterBar", + "timesignaturenumerator" => 3, + "syncpoints" => Array [ + Map { + "islinear" => false, + "type" => 4, + "value" => 0, + "syncpointvalue" => Map { + "baroccurence" => 0, + "millisecondoffset" => 10223, + }, + "ratioposition" => 1, + "text" => "", + "isvisible" => true, + }, + ], + "start" => 8640, + }, + ], + "style" => Map { + "headerandfooter" => Map { + "0" => Map { + "template" => "%TITLE%", + "isvisible" => true, + "textalign" => 1, + }, + "2" => Map { + "template" => "%ARTIST%", + "isvisible" => true, + "textalign" => 1, + }, + "8" => Map { + "template" => "%COPYRIGHT%", + "isvisible" => true, + "textalign" => 1, + }, + }, + "colors" => Map {}, + }, +} +`; diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexLexer.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexLexer.test.ts.snap new file mode 100644 index 000000000..64bc6c488 --- /dev/null +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexLexer.test.ts.snap @@ -0,0 +1,534 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`AlphaTexLexerTest basic-tokens 1`] = ` +Array [ + Dot (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 2`] = ` +Array [ + Colon (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 3`] = ` +Array [ + LParen (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 4`] = ` +Array [ + RParen (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 5`] = ` +Array [ + LBrace (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 6`] = ` +Array [ + RBrace (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 7`] = ` +Array [ + Pipe (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest basic-tokens 8`] = ` +Array [ + Asterisk (1,1) -> (1,1), +] +`; + +exports[`AlphaTexLexerTest errors at001 1`] = ` +Array [ + Ident "a" (1,2) -> (1,3), +] +`; + +exports[`AlphaTexLexerTest errors at001 2`] = ` +Array [ + Map { + "code" => 1, + "severity" => 2, + "message" => "Unexpected character at comment start, expected '//' or '/*' but found '/a'", + "start" => Map { + "col" => 2, + "line" => 1, + "offset" => 1, + }, + }, +] +`; + +exports[`AlphaTexLexerTest errors at002 1`] = ` +Array [ + String "Test" (1,3) -> (1,8), +] +`; + +exports[`AlphaTexLexerTest errors at002 2`] = ` +Array [ + Map { + "code" => 2, + "severity" => 2, + "message" => "Missing identifier after meta data start", + "start" => Map { + "col" => 2, + "line" => 1, + "offset" => 1, + }, + }, +] +`; + +exports[`AlphaTexLexerTest errors at003 1`] = `Array []`; + +exports[`AlphaTexLexerTest errors at003 2`] = ` +Array [ + Map { + "code" => 3, + "severity" => 2, + "message" => "Unexpected end of file. Need 4 hex characters on a \\\\uXXXX escape sequence", + "start" => Map { + "col" => 7, + "line" => 1, + "offset" => 6, + }, + }, +] +`; + +exports[`AlphaTexLexerTest errors at004 1`] = ` +Array [ + Ident "X" (1,7) -> (1,8), +] +`; + +exports[`AlphaTexLexerTest errors at004 2`] = ` +Array [ + Map { + "code" => 4, + "severity" => 2, + "message" => "Invalid unicode value. Need 4 hex characters on a \\\\uXXXX escape sequence.", + "start" => Map { + "col" => 7, + "line" => 1, + "offset" => 6, + }, + }, +] +`; + +exports[`AlphaTexLexerTest errors at005 1`] = ` +Array [ + Ident "b01010101" (1,3) -> (1,12), +] +`; + +exports[`AlphaTexLexerTest errors at005 2`] = ` +Array [ + Map { + "code" => 5, + "severity" => 2, + "message" => "Unsupported escape sequence. Expected '\\\\n', '\\\\r', '\\\\t', or '\\\\uXXXX' but found '\\\\b'.", + "start" => Map { + "col" => 3, + "line" => 1, + "offset" => 2, + }, + }, +] +`; + +exports[`AlphaTexLexerTest errors at006 1`] = `Array []`; + +exports[`AlphaTexLexerTest errors at006 2`] = ` +Array [ + Map { + "code" => 6, + "severity" => 2, + "message" => "Unexpected end of file. String not closed.", + "start" => Map { + "col" => 8, + "line" => 1, + "offset" => 7, + }, + }, +] +`; + +exports[`AlphaTexLexerTest errors at006 3`] = `Array []`; + +exports[`AlphaTexLexerTest errors at006 4`] = ` +Array [ + Map { + "code" => 6, + "severity" => 2, + "message" => "Unexpected end of file. String not closed.", + "start" => Map { + "col" => 8, + "line" => 1, + "offset" => 7, + }, + }, +] +`; + +exports[`AlphaTexLexerTest floats 1`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest floats 2`] = ` +Array [ + Number "11.22" (1,1) -> (1,6), + Number "33.44" (1,7) -> (1,12), +] +`; + +exports[`AlphaTexLexerTest floats 3`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), + Dot (1,4) -> (1,4), + Number "4" (1,5) -> (1,6), +] +`; + +exports[`AlphaTexLexerTest floats 4`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), + Dot (1,5) -> (1,5), + Number "4" (1,6) -> (1,7), +] +`; + +exports[`AlphaTexLexerTest floats 5`] = ` +Array [ + Number "1" (1,1) -> (1,2), + Dot (1,3) -> (1,3), + Number "1.4" (1,4) -> (1,7), +] +`; + +exports[`AlphaTexLexerTest floats 6`] = ` +Array [ + Number "-1.1" (1,1) -> (1,5), +] +`; + +exports[`AlphaTexLexerTest floats 7`] = ` +Array [ + Ident "-" (1,1) -> (1,2), + Dot (1,2) -> (1,2), + Number "1" (1,3) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest floats 8`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), + LParen (1,4) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest floats 9`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), + LBrace (1,4) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest floats 10`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), + Pipe (1,4) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest floats 11`] = ` +Array [ + Number "1" (1,1) -> (1,2), + Dot (1,2) -> (1,2), + Ident "1a" (1,3) -> (1,5), +] +`; + +exports[`AlphaTexLexerTest floats 12`] = ` +Array [ + Ident "1a" (1,1) -> (1,3), + Dot (1,3) -> (1,3), + Number "1" (1,4) -> (1,5), +] +`; + +exports[`AlphaTexLexerTest floats 13`] = ` +Array [ + Number "1.1" (1,1) -> (1,4), + Tag "test" (1,4) -> (1,9) { + prefix: Backslash (1,4) -> (1,5), + tag: Ident "test" (1,5) -> (1,9), + }, +] +`; + +exports[`AlphaTexLexerTest identifiers 1`] = ` +Array [ + Ident "true" (1,1) -> (1,5), +] +`; + +exports[`AlphaTexLexerTest identifiers 2`] = ` +Array [ + Ident "false" (1,1) -> (1,6), +] +`; + +exports[`AlphaTexLexerTest identifiers 3`] = ` +Array [ + Ident "HelloWorld" (1,1) -> (1,11), +] +`; + +exports[`AlphaTexLexerTest identifiers 4`] = ` +Array [ + Ident "C4" (1,1) -> (1,3), +] +`; + +exports[`AlphaTexLexerTest identifiers 5`] = ` +Array [ + Ident "C#4" (1,1) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest identifiers 6`] = ` +Array [ + Ident "Cb4" (1,1) -> (1,4), +] +`; + +exports[`AlphaTexLexerTest identifiers 7`] = ` +Array [ + Ident "electricpiano1" (1,1) -> (1,15), +] +`; + +exports[`AlphaTexLexerTest identifiers 8`] = ` +Array [ + Ident "Unicodeöäü" (1,1) -> (1,11), + Ident "Unicode😸" (1,12) -> (1,20), + Ident "Utf16🤘🏻" (1,21) -> (1,28), +] +`; + +exports[`AlphaTexLexerTest identifiers 9`] = ` +Array [ + Ident "dashed-identifier" (1,1) -> (1,18), +] +`; + +exports[`AlphaTexLexerTest identifiers 10`] = ` +Array [ + Ident "HelloWorld" (1,1) -> (1,11), + Ident "Multiple" (1,12) -> (1,20), + Ident "Identifiers" (1,21) -> (1,32), +] +`; + +exports[`AlphaTexLexerTest leading-comments 1`] = ` +Array [ + Ident "true" (2,10) -> (3,1) { + leadingComments: Array [ + "// Single", + ], + }, + Ident "false" (6,10) -> (7,1) { + leadingComments: Array [ + "/** Multi **/", + "/** Multi2 **/", + "// Single", + ], + }, +] +`; + +exports[`AlphaTexLexerTest meta-command 1`] = ` +Array [ + Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, +] +`; + +exports[`AlphaTexLexerTest meta-command 2`] = ` +Array [ + Tag "double" (1,1) -> (1,9) { + prefix: DoubleBackslash (1,1) -> (1,3), + tag: Ident "double" (1,3) -> (1,9), + }, +] +`; + +exports[`AlphaTexLexerTest meta-command 3`] = ` +Array [ + Tag "withNumber123" (1,1) -> (1,15) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "withNumber123" (1,2) -> (1,15), + }, +] +`; + +exports[`AlphaTexLexerTest meta-command 4`] = ` +Array [ + Tag "withUnicode😼" (1,1) -> (1,14) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "withUnicode😼" (1,2) -> (1,14), + }, +] +`; + +exports[`AlphaTexLexerTest meta-command 5`] = ` +Array [ + Tag "withUnicode😼" (1,1) -> (1,14) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "withUnicode😼" (1,2) -> (1,14), + }, + Dot (1,15) -> (1,15), + Tag "withNumber123" (1,17) -> (1,31) { + prefix: Backslash (1,17) -> (1,18), + tag: Ident "withNumber123" (1,18) -> (1,31), + }, +] +`; + +exports[`AlphaTexLexerTest numbers 1`] = ` +Array [ + Number "1" (1,1) -> (1,2), +] +`; + +exports[`AlphaTexLexerTest numbers 2`] = ` +Array [ + Number "1234" (1,1) -> (1,5), +] +`; + +exports[`AlphaTexLexerTest numbers 3`] = ` +Array [ + Number "-1" (1,1) -> (1,3), +] +`; + +exports[`AlphaTexLexerTest numbers 4`] = ` +Array [ + Number "-1234" (1,1) -> (1,6), +] +`; + +exports[`AlphaTexLexerTest numbers 5`] = ` +Array [ + Number "1234" (1,1) -> (1,5), + Number "5678" (1,6) -> (1,10), +] +`; + +exports[`AlphaTexLexerTest strings 1`] = ` +Array [ + String "Double Quoted" (1,1) -> (1,15), +] +`; + +exports[`AlphaTexLexerTest strings 2`] = ` +Array [ + String "Single Quoted" (1,1) -> (1,15), +] +`; + +exports[`AlphaTexLexerTest strings 3`] = ` +Array [ + String "Multiple" (1,1) -> (1,10), + String "Strings" (1,12) -> (1,20), +] +`; + +exports[`AlphaTexLexerTest strings 4`] = ` +Array [ + String "Double \\"Quoted\\"" (1,1) -> (1,19), +] +`; + +exports[`AlphaTexLexerTest strings 5`] = ` +Array [ + String "Single 'Quoted'" (1,1) -> (1,19), +] +`; + +exports[`AlphaTexLexerTest strings 6`] = ` +Array [ + String "\\r\\n\\t" (1,1) -> (1,8), +] +`; + +exports[`AlphaTexLexerTest strings 7`] = ` +Array [ + String "\\r\\n\\t" (1,1) -> (1,8), +] +`; + +exports[`AlphaTexLexerTest strings 8`] = ` +Array [ + String "😸" (1,1) -> (1,14), +] +`; + +exports[`AlphaTexLexerTest strings 9`] = ` +Array [ + String "😸🤘🏻" (1,1) -> (1,5), +] +`; + +exports[`AlphaTexLexerTest trailing-comments 1`] = ` +Array [ + Ident "true" (1,10) -> (1,14) { + trailingComments: Array [ + "// Single After", + ], + }, + Ident "false" (2,10) -> (2,15) { + trailingComments: Array [ + "/** Multi After **/", + ], + }, + Ident "true" (3,23) -> (3,27) { + leadingComments: Array [ + "/** before **/", + ], + trailingComments: Array [ + "/** middle **/", + ], + }, + Ident "false" (3,41) -> (3,46) { + trailingComments: Array [ + "// after", + ], + }, +] +`; + +exports[`AlphaTexLexerTest whitespace 1`] = ` +Array [ + Dot (1,3) -> (1,3), + Dot (2,8) -> (2,8), +] +`; diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap new file mode 100644 index 000000000..792baeb87 --- /dev/null +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexParser.test.ts.snap @@ -0,0 +1,5652 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`AlphaTexParserTest ambiguous tempo and stringed note 1`] = ` +Score (1,1) -> (1,19) { + bars: Array [ + Bar (1,1) -> (1,19) { + metaData: Array [ + Meta (1,1) -> (1,11) { + tag: Tag "tempo" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "tempo" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,11) { + values: Array [ + Number "120" (1,8) -> (1,11), + ], + }, + }, + ], + beats: Array [ + Beat (1,12) -> (1,15) { + notes: NoteList (1,12) -> (1,15) { + notes: Array [ + Note (1,12) -> (1,15) { + noteValue: Number "3" (1,12) -> (1,13), + noteStringDot: Dot (1,13) -> (1,13), + noteString: Number "3" (1,14) -> (1,15), + }, + ], + }, + }, + Beat (1,16) -> (1,19) { + notes: NoteList (1,16) -> (1,19) { + notes: Array [ + Note (1,16) -> (1,19) { + noteValue: Number "3" (1,16) -> (1,17), + noteStringDot: Dot (1,17) -> (1,17), + noteString: Number "4" (1,18) -> (1,19), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest ambiguous tempo and stringed note: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest ambiguous tempo and stringed note: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note 1`] = ` +Score (1,1) -> (1,26) { + bars: Array [ + Bar (1,1) -> (1,26) { + metaData: Array [ + Meta (1,1) -> (1,21) { + tag: Tag "tempo" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "tempo" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,21) { + values: Array [ + Number "120" (1,8) -> (1,11), + String "Moderate" (1,12) -> (1,21), + ], + }, + }, + ], + beats: Array [ + Beat (1,23) -> (1,26) { + notes: NoteList (1,23) -> (1,26) { + notes: Array [ + Note (1,23) -> (1,26) { + noteValue: Number "3" (1,23) -> (1,24), + noteStringDot: Dot (1,24) -> (1,24), + noteString: Number "4" (1,25) -> (1,26), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest ambiguous tempo, temponame and stringed note: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments bar meta singleline 1`] = ` +Score (1,1) -> (3,10) { + bars: Array [ + Bar (1,1) -> (3,10) { + metaData: Array [ + Meta (3,3) -> (3,10) { + tag: Tag "ts" (3,3) -> (3,6) { + leadingComments: Array [ + "// Single ", + ], + prefix: Backslash (3,3) -> (3,4), + tag: Ident "ts" (3,4) -> (3,6), + }, + values: Values (3,7) -> (3,10) { + values: Array [ + Number "3" (3,7) -> (3,8), + Number "4" (3,9) -> (3,10), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments bar meta singleline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments bar meta singleline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments bar multiline 1`] = ` +Score (1,1) -> (3,16) { + bars: Array [ + Bar (1,1) -> (3,16) { + metaData: Array [ + Meta (3,9) -> (3,16) { + tag: Tag "ts" (3,9) -> (3,12) { + leadingComments: Array [ + "/** multi +line**/", + ], + prefix: Backslash (3,9) -> (3,10), + tag: Ident "ts" (3,10) -> (3,12), + }, + values: Values (3,13) -> (3,16) { + values: Array [ + Number "3" (3,13) -> (3,14), + Number "4" (3,15) -> (3,16), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments bar multiline middle 1`] = ` +Score (1,1) -> (3,10) { + bars: Array [ + Bar (1,1) -> (3,10) { + metaData: Array [ + Meta (2,2) -> (3,10) { + tag: Tag "ts" (2,2) -> (2,5) { + prefix: Backslash (2,2) -> (2,3), + tag: Ident "ts" (2,3) -> (2,5), + }, + values: Values (2,6) -> (3,10) { + values: Array [ + Number "3" (2,6) -> (2,7) { + trailingComments: Array [ + "/** multi +line**/", + ], + }, + Number "4" (3,9) -> (3,10), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments bar multiline middle: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments bar multiline middle: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments bar multiline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments bar multiline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat chord multiline 1`] = ` +Score (1,1) -> (3,11) { + bars: Array [ + Bar (1,1) -> (3,11) { + beats: Array [ + Beat (1,3) -> (3,11) { + notes: NoteList (1,3) -> (3,11) { + openParenthesis: LParen (1,3) -> (1,3), + notes: Array [ + Note (1,4) -> (2,1) { + noteValue: Ident "C4" (1,4) -> (2,1), + }, + Note (3,9) -> (3,11) { + noteValue: Ident "C5" (3,9) -> (3,11) { + leadingComments: Array [ + "/** multi +line**/", + ], + }, + }, + ], + closeParenthesis: RParen (3,11) -> (3,11), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments beat chord multiline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat chord multiline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat chord singleline 1`] = ` +Score (1,1) -> (3,5) { + bars: Array [ + Bar (1,1) -> (3,5) { + beats: Array [ + Beat (1,3) -> (3,5) { + notes: NoteList (1,3) -> (3,5) { + openParenthesis: LParen (1,3) -> (1,3), + notes: Array [ + Note (1,4) -> (2,1) { + noteValue: Ident "C4" (1,4) -> (2,1), + }, + Note (3,3) -> (3,5) { + noteValue: Ident "C5" (3,3) -> (3,5) { + leadingComments: Array [ + "// Single ", + ], + }, + }, + ], + closeParenthesis: RParen (3,5) -> (3,5), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments beat chord singleline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat chord singleline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat multiline 1`] = ` +Score (1,1) -> (3,11) { + bars: Array [ + Bar (1,1) -> (3,11) { + beats: Array [ + Beat (1,3) -> (2,1) { + notes: NoteList (1,3) -> (2,1) { + notes: Array [ + Note (1,3) -> (2,1) { + noteValue: Ident "C4" (1,3) -> (2,1), + }, + ], + }, + }, + Beat (3,9) -> (3,11) { + notes: NoteList (3,9) -> (3,11) { + notes: Array [ + Note (3,9) -> (3,11) { + noteValue: Ident "C5" (3,9) -> (3,11) { + leadingComments: Array [ + "/** multi +line**/", + ], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments beat multiline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat multiline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat singleline 1`] = ` +Score (1,1) -> (3,5) { + bars: Array [ + Bar (1,1) -> (3,5) { + beats: Array [ + Beat (1,3) -> (2,1) { + notes: NoteList (1,3) -> (2,1) { + notes: Array [ + Note (1,3) -> (2,1) { + noteValue: Ident "C4" (1,3) -> (2,1), + }, + ], + }, + }, + Beat (3,3) -> (3,5) { + notes: NoteList (3,3) -> (3,5) { + notes: Array [ + Note (3,3) -> (3,5) { + noteValue: Ident "C5" (3,3) -> (3,5) { + leadingComments: Array [ + "// Single ", + ], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments beat singleline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beat singleline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beateffects multiline 1`] = ` +Score (1,1) -> (3,9) { + bars: Array [ + Bar (1,1) -> (3,9) { + beats: Array [ + Beat (1,3) -> (3,9) { + notes: NoteList (1,3) -> (1,9) { + openParenthesis: LParen (1,3) -> (1,3), + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + Note (1,7) -> (1,9) { + noteValue: Ident "C5" (1,7) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + beatEffects: Props (1,11) -> (3,9) { + openBrace: LBrace (1,11) -> (1,11), + properties: Array [], + closeBrace: RBrace (3,9) -> (3,9) { + leadingComments: Array [ + "/** multi +line**/", + ], + }, + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments beateffects multiline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beateffects multiline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beateffects singleline 1`] = ` +Score (1,1) -> (3,3) { + bars: Array [ + Bar (1,1) -> (3,3) { + beats: Array [ + Beat (1,3) -> (3,3) { + notes: NoteList (1,3) -> (1,9) { + openParenthesis: LParen (1,3) -> (1,3), + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + Note (1,7) -> (1,9) { + noteValue: Ident "C5" (1,7) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + beatEffects: Props (1,11) -> (3,3) { + openBrace: LBrace (1,11) -> (1,11), + properties: Array [], + closeBrace: RBrace (3,3) -> (3,3) { + leadingComments: Array [ + "// Single ", + ], + }, + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments beateffects singleline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments beateffects singleline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments noteeffects multiline 1`] = ` +Score (1,1) -> (3,13) { + bars: Array [ + Bar (1,1) -> (3,13) { + beats: Array [ + Beat (1,3) -> (3,13) { + notes: NoteList (1,3) -> (3,13) { + openParenthesis: LParen (1,3) -> (1,3), + notes: Array [ + Note (1,4) -> (3,9) { + noteValue: Ident "C4" (1,4) -> (1,6), + noteEffects: Props (1,9) -> (3,9) { + openBrace: LBrace (1,9) -> (1,9), + properties: Array [], + closeBrace: RBrace (3,9) -> (3,9) { + leadingComments: Array [ + "/** multi +line**/", + ], + }, + }, + }, + Note (3,11) -> (3,13) { + noteValue: Ident "C5" (3,11) -> (3,13), + }, + ], + closeParenthesis: RParen (3,13) -> (3,13), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments noteeffects multiline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments noteeffects multiline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments noteeffects singleline 1`] = ` +Score (1,1) -> (3,7) { + bars: Array [ + Bar (1,1) -> (3,7) { + beats: Array [ + Beat (1,3) -> (3,7) { + notes: NoteList (1,3) -> (3,7) { + openParenthesis: LParen (1,3) -> (1,3), + notes: Array [ + Note (1,4) -> (3,3) { + noteValue: Ident "C4" (1,4) -> (1,6), + noteEffects: Props (1,7) -> (3,3) { + openBrace: LBrace (1,7) -> (1,7), + properties: Array [], + closeBrace: RBrace (3,3) -> (3,3) { + leadingComments: Array [ + "// Single ", + ], + }, + }, + }, + Note (3,5) -> (3,7) { + noteValue: Ident "C5" (3,5) -> (3,7), + }, + ], + closeParenthesis: RParen (3,7) -> (3,7), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments noteeffects singleline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments noteeffects singleline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments score meta multiline 1`] = ` +Score (1,1) -> (2,21) { + bars: Array [ + Bar (2,9) -> (2,21) { + metaData: Array [ + Meta (2,9) -> (2,21) { + tag: Tag "title" (2,9) -> (2,15) { + leadingComments: Array [ + "/** multi +line**/", + ], + prefix: Backslash (2,9) -> (2,10), + tag: Ident "title" (2,10) -> (2,15), + }, + values: Values (2,16) -> (2,21) { + values: Array [ + String "Test" (2,16) -> (2,21), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments score meta multiline middle 1`] = ` +Score (1,1) -> (2,14) { + bars: Array [ + Bar (1,1) -> (2,14) { + metaData: Array [ + Meta (1,1) -> (2,14) { + tag: Tag "title" (1,1) -> (1,7) { + trailingComments: Array [ + "/** multi +line**/", + ], + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (2,9) -> (2,14) { + values: Array [ + String "Test" (2,9) -> (2,14), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments score meta multiline middle: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments score meta multiline middle: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments score meta multiline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments score meta multiline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments score meta singleline 1`] = ` +Score (1,1) -> (2,15) { + bars: Array [ + Bar (2,3) -> (2,15) { + metaData: Array [ + Meta (2,3) -> (2,15) { + tag: Tag "title" (2,3) -> (2,9) { + leadingComments: Array [ + "// Single ", + ], + prefix: Backslash (2,3) -> (2,4), + tag: Ident "title" (2,4) -> (2,9), + }, + values: Values (2,10) -> (2,15) { + values: Array [ + String "Test" (2,10) -> (2,15), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest comments score meta singleline: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest comments score meta singleline: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at200 missing 1`] = ` +Score (1,1) -> (1,7) { + bars: Array [ + Bar (1,1) -> (1,7) { + beats: Array [ + Beat (1,1) -> (1,7) { + notes: NoteList (1,1) -> (1,5) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + ], + closeParenthesis: RParen (1,5) -> (1,5), + }, + beatMultiplier: Asterisk (1,7) -> (1,7), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at200 missing: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at200 missing: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 200, + "severity" => 2, + "message" => "Missing beat multiplier value after '*'.", + "start" => Map { + "col" => 7, + "line" => 1, + "offset" => 6, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at200 type 1`] = ` +Score (1,1) -> (1,10) { + bars: Array [ + Bar (1,1) -> (1,10) { + beats: Array [ + Beat (1,1) -> (1,7) { + notes: NoteList (1,1) -> (1,5) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + ], + closeParenthesis: RParen (1,5) -> (1,5), + }, + beatMultiplier: Asterisk (1,7) -> (1,7), + }, + Beat (1,9) -> (1,10) { + notes: NoteList (1,9) -> (1,10) { + notes: Array [ + Note (1,9) -> (1,10) { + noteValue: Ident "A" (1,9) -> (1,10), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at200 type: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at200 type: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 200, + "severity" => 2, + "message" => "Missing beat multiplier value after '*'.", + "start" => Map { + "col" => 7, + "line" => 1, + "offset" => 6, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at201 missing 1`] = ` +Score (1,1) -> (1,1) { + bars: Array [ + Bar (1,1) -> (1,1) { + beats: Array [ + Beat (1,1) -> (1,1), + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at201 missing: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at201 missing: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 201, + "severity" => 2, + "message" => "Missing duration value after ':'.", + "start" => Map { + "col" => 1, + "line" => 1, + "offset" => 0, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at201 type 1`] = ` +Score (1,1) -> (1,3) { + bars: Array [ + Bar (1,1) -> (1,3) { + beats: Array [ + Beat (1,1) -> (1,3) { + notes: NoteList (1,2) -> (1,3) { + notes: Array [ + Note (1,2) -> (1,3) { + noteValue: Ident "A" (1,2) -> (1,3), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at201 type: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at201 type: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 201, + "severity" => 2, + "message" => "Missing duration value after ':'.", + "start" => Map { + "col" => 1, + "line" => 1, + "offset" => 0, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at202 beat duration 1`] = `Score (1,1) -> (1,5)`; + +exports[`AlphaTexParserTest errors at202 beat duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at202 beat duration: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 202, + "severity" => 2, + "message" => "Unexpected 'Ident' token. Expected one of following: Number", + "start" => Map { + "col" => 7, + "line" => 1, + "offset" => 6, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at202 meta value 1`] = `Score (1,1) -> (1,7)`; + +exports[`AlphaTexParserTest errors at202 meta value: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at202 meta value: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 202, + "severity" => 2, + "message" => "Unexpected 'Number' token. Expected one of following: String,Ident", + "start" => Map { + "col" => 10, + "line" => 1, + "offset" => 9, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at202 note string 1`] = `Score (1,1) -> (1,2)`; + +exports[`AlphaTexParserTest errors at202 note string: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at202 note string: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 202, + "severity" => 2, + "message" => "Unexpected 'Ident' token. Expected one of following: Number", + "start" => Map { + "col" => 4, + "line" => 1, + "offset" => 3, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at202 note value 1`] = `Score (1,1) -> (1,4)`; + +exports[`AlphaTexParserTest errors at202 note value: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at202 note value: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 202, + "severity" => 2, + "message" => "Unexpected 'Tag' token. Expected one of following: Number,String,Ident", + "start" => Map { + "col" => 16, + "line" => 1, + "offset" => 15, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at202 value list 1`] = ` +Score (1,1) -> (1,18) { + bars: Array [ + Bar (1,1) -> (1,18) { + metaData: Array [ + Meta (1,1) -> (1,18) { + tag: Tag "meta" (1,1) -> (1,6) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "meta" (1,2) -> (1,6), + }, + values: Values (1,7) -> (1,18) { + openParenthesis: LParen (1,7) -> (1,7), + values: Array [], + closeParenthesis: RParen (1,18) -> (1,18), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at202 value list: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at202 value list: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 202, + "severity" => 2, + "message" => "Unexpected 'Tag' token. Expected one of following: Ident,String,Number", + "start" => Map { + "col" => 18, + "line" => 1, + "offset" => 17, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at203 meta value 1`] = `Score (1,1) -> (1,7)`; + +exports[`AlphaTexParserTest errors at203 meta value: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at203 meta value: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 203, + "severity" => 2, + "message" => "Unexpected end of file.", + "start" => Map { + "col" => 7, + "line" => 1, + "offset" => 6, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at203 note string 1`] = `Score (1,1) -> (1,5)`; + +exports[`AlphaTexParserTest errors at203 note string: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at203 note string: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 203, + "severity" => 2, + "message" => "Unexpected end of file.", + "start" => Map { + "col" => 6, + "line" => 1, + "offset" => 5, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at204 bar 1`] = `Score (1,1) -> (1,11)`; + +exports[`AlphaTexParserTest errors at204 bar: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at204 bar: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 204, + "severity" => 2, + "message" => "Unrecognized metadata 'unknown'.", + "start" => Map { + "col" => 11, + "line" => 1, + "offset" => 10, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at204 score 1`] = `Score (1,1) -> (1,9)`; + +exports[`AlphaTexParserTest errors at204 score: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at204 score: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 204, + "severity" => 2, + "message" => "Unrecognized metadata 'unknown'.", + "start" => Map { + "col" => 9, + "line" => 1, + "offset" => 8, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at205 bar 1`] = `Score (1,1) -> (1,25)`; + +exports[`AlphaTexParserTest errors at205 bar: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at205 bar: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'unknown'.", + "start" => Map { + "col" => 25, + "line" => 1, + "offset" => 24, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at205 barre 1`] = `Score (1,1) -> (1,20)`; + +exports[`AlphaTexParserTest errors at205 barre: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at205 barre: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'invalid'.", + "start" => Map { + "col" => 20, + "line" => 1, + "offset" => 19, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at205 beat 1`] = `Score (1,1) -> (1,16)`; + +exports[`AlphaTexParserTest errors at205 beat: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at205 beat: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'unknown'.", + "start" => Map { + "col" => 16, + "line" => 1, + "offset" => 15, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at205 fermata 1`] = `Score (1,1) -> (1,20)`; + +exports[`AlphaTexParserTest errors at205 fermata: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at205 fermata: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'invalid'.", + "start" => Map { + "col" => 20, + "line" => 1, + "offset" => 19, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at205 gracetype 1`] = `Score (1,1) -> (1,15)`; + +exports[`AlphaTexParserTest errors at205 gracetype: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at205 gracetype: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'invalid'.", + "start" => Map { + "col" => 15, + "line" => 1, + "offset" => 14, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at205 note 1`] = `Score (1,1) -> (1,14)`; + +exports[`AlphaTexParserTest errors at205 note: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at205 note: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'unknown'.", + "start" => Map { + "col" => 14, + "line" => 1, + "offset" => 13, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at206 note list 1`] = ` +Score (1,1) -> (1,4) { + bars: Array [ + Bar (1,1) -> (1,4) { + beats: Array [ + Beat (1,1) -> (1,4) { + notes: NoteList (1,1) -> (1,4) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at206 note list: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at206 note list: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 4, + "line" => 1, + "offset" => 3, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at206 properties 1`] = ` +Score (1,1) -> (1,26) { + bars: Array [ + Bar (1,1) -> (1,26) { + metaData: Array [ + Meta (1,1) -> (1,26) { + tag: Tag "track" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "track" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,13) { + values: Array [ + String "Test" (1,8) -> (1,13), + ], + }, + properties: Props (1,15) -> (1,26) { + openBrace: LBrace (1,15) -> (1,15), + properties: Array [ + Prop (1,17) -> (1,26) { + property: Ident "color" (1,17) -> (1,22), + properties: Values (1,23) -> (1,26) { + values: Array [ + Ident "red" (1,23) -> (1,26), + ], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at206 properties: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at206 properties: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 26, + "line" => 1, + "offset" => 25, + }, + }, +] +`; + +exports[`AlphaTexParserTest errors at206 values 1`] = ` +Score (1,1) -> (1,14) { + bars: Array [ + Bar (1,1) -> (1,14) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + openParenthesis: LParen (1,8) -> (1,8), + values: Array [ + String "Test" (1,9) -> (1,14), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest errors at206 values: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest errors at206 values: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 15, + "line" => 1, + "offset" => 14, + }, + }, +] +`; + +exports[`AlphaTexParserTest floats tempo 1`] = ` +Score (1,1) -> (1,28) { + bars: Array [ + Bar (1,1) -> (1,28) { + metaData: Array [ + Meta (1,3) -> (1,23) { + tag: Tag "tempo" (1,3) -> (1,9) { + prefix: Backslash (1,3) -> (1,4), + tag: Ident "tempo" (1,4) -> (1,9), + }, + values: Values (1,10) -> (1,23) { + values: Array [ + Number "120" (1,10) -> (1,13), + String "Moderate" (1,14) -> (1,23), + ], + }, + }, + ], + beats: Array [ + Beat (1,25) -> (1,28) { + notes: NoteList (1,25) -> (1,28) { + notes: Array [ + Note (1,25) -> (1,28) { + noteValue: Number "0" (1,25) -> (1,26), + noteStringDot: Dot (1,26) -> (1,26), + noteString: Number "5" (1,27) -> (1,28), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest floats tempo parenthesis 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,29) { + metaData: Array [ + Meta (1,3) -> (1,29) { + tag: Tag "tempo" (1,3) -> (1,9) { + prefix: Backslash (1,3) -> (1,4), + tag: Ident "tempo" (1,4) -> (1,9), + }, + values: Values (1,10) -> (1,29) { + openParenthesis: LParen (1,10) -> (1,10), + values: Array [ + Number "120" (1,11) -> (1,14), + String "Moderate" (1,15) -> (1,24), + Number "0.5" (1,26) -> (1,29), + ], + closeParenthesis: RParen (1,29) -> (1,29), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest floats tempo parenthesis: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats tempo parenthesis: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats tempo: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats tempo: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats valuelist 1`] = ` +Score (1,1) -> (1,13) { + bars: Array [ + Bar (1,1) -> (1,13) { + metaData: Array [ + Meta (1,3) -> (1,13) { + tag: Tag "scale" (1,3) -> (1,9) { + prefix: Backslash (1,3) -> (1,4), + tag: Ident "scale" (1,4) -> (1,9), + }, + values: Values (1,10) -> (1,13) { + values: Array [ + Number "0.5" (1,10) -> (1,13), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest floats valuelist parenthesis 1`] = ` +Score (1,1) -> (1,18) { + bars: Array [ + Bar (1,1) -> (1,18) { + metaData: Array [ + Meta (1,1) -> (1,18) { + tag: Tag "unknown" (1,1) -> (1,9) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "unknown" (1,2) -> (1,9), + }, + values: Values (1,10) -> (1,18) { + openParenthesis: LParen (1,10) -> (1,10), + values: Array [ + Number "1.2" (1,11) -> (1,14), + Number "2.3" (1,15) -> (1,18), + ], + closeParenthesis: RParen (1,18) -> (1,18), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest floats valuelist parenthesis: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats valuelist parenthesis: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats valuelist: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest floats valuelist: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate finished beat effect value, not closed 1`] = ` +Score (1,1) -> (1,48) { + bars: Array [ + Bar (1,1) -> (1,48) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,48) { + notes: NoteList (1,18) -> (1,33) { + notes: Array [ + Note (1,18) -> (1,33) { + noteValue: Ident "C4" (1,18) -> (1,20), + noteEffects: Props (1,21) -> (1,33) { + openBrace: LBrace (1,21) -> (1,21), + properties: Array [ + Prop (1,23) -> (1,31) { + property: Ident "slur" (1,23) -> (1,27), + properties: Values (1,28) -> (1,31) { + values: Array [ + String "S1" (1,28) -> (1,31), + ], + }, + }, + ], + closeBrace: RBrace (1,33) -> (1,33), + }, + }, + ], + }, + durationDot: Dot (1,35) -> (1,35), + durationValue: Number "4" (1,37) -> (1,38), + beatEffects: Props (1,39) -> (1,48) { + openBrace: LBrace (1,39) -> (1,39), + properties: Array [ + Prop (1,41) -> (1,48) { + property: Ident "rasg" (1,41) -> (1,45), + properties: Values (1,46) -> (1,48) { + values: Array [ + String "i" (1,46) -> (1,48), + ], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate finished beat effect value, not closed: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate finished beat effect value, not closed: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 50, + "line" => 1, + "offset" => 49, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate finished meta property value, not closed 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,1) -> (1,24) { + metaData: Array [ + Meta (1,1) -> (1,24) { + tag: Tag "track" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "track" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,10) { + values: Array [ + String "X" (1,8) -> (1,10), + ], + }, + properties: Props (1,12) -> (1,24) { + openBrace: LBrace (1,12) -> (1,12), + properties: Array [ + Prop (1,14) -> (1,24) { + property: Ident "color" (1,14) -> (1,19), + properties: Values (1,20) -> (1,24) { + values: Array [ + String "red" (1,20) -> (1,24), + ], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate finished meta property value, not closed : lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate finished meta property value, not closed : parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 25, + "line" => 1, + "offset" => 24, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate finished note effect value, not closed 1`] = ` +Score (1,1) -> (1,31) { + bars: Array [ + Bar (1,1) -> (1,31) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,31) { + notes: NoteList (1,18) -> (1,31) { + notes: Array [ + Note (1,18) -> (1,31) { + noteValue: Ident "C4" (1,18) -> (1,20), + noteEffects: Props (1,21) -> (1,31) { + openBrace: LBrace (1,21) -> (1,21), + properties: Array [ + Prop (1,23) -> (1,31) { + property: Ident "slur" (1,23) -> (1,27), + properties: Values (1,28) -> (1,31) { + values: Array [ + String "S1" (1,28) -> (1,31), + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate finished note effect value, not closed: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate finished note effect value, not closed: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 32, + "line" => 1, + "offset" => 31, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started beat duration 1`] = ` +Score (1,1) -> (1,35) { + bars: Array [ + Bar (1,1) -> (1,35) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,35) { + notes: NoteList (1,18) -> (1,33) { + notes: Array [ + Note (1,18) -> (1,33) { + noteValue: Ident "C4" (1,18) -> (1,20), + noteEffects: Props (1,21) -> (1,33) { + openBrace: LBrace (1,21) -> (1,21), + properties: Array [ + Prop (1,23) -> (1,31) { + property: Ident "slur" (1,23) -> (1,27), + properties: Values (1,28) -> (1,31) { + values: Array [ + String "S1" (1,28) -> (1,31), + ], + }, + }, + ], + closeBrace: RBrace (1,33) -> (1,33), + }, + }, + ], + }, + durationDot: Dot (1,35) -> (1,35), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate started beat duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started beat duration: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started beat effect name 1`] = `Score (1,1) -> (1,44)`; + +exports[`AlphaTexParserTest intermediate started beat effect name: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started beat effect name: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'ras'.", + "start" => Map { + "col" => 44, + "line" => 1, + "offset" => 43, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started beat effect value 1`] = `Score (1,1) -> (1,45)`; + +exports[`AlphaTexParserTest intermediate started beat effect value: lexer-diagnostics 1`] = ` +Array [ + Map { + "code" => 6, + "severity" => 2, + "message" => "Unexpected end of file. String not closed.", + "start" => Map { + "col" => 48, + "line" => 1, + "offset" => 47, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started beat effect value: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 203, + "severity" => 2, + "message" => "Unexpected end of file.", + "start" => Map { + "col" => 48, + "line" => 1, + "offset" => 47, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started beat effects 1`] = ` +Score (1,1) -> (1,39) { + bars: Array [ + Bar (1,1) -> (1,39) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,39) { + notes: NoteList (1,18) -> (1,33) { + notes: Array [ + Note (1,18) -> (1,33) { + noteValue: Ident "C4" (1,18) -> (1,20), + noteEffects: Props (1,21) -> (1,33) { + openBrace: LBrace (1,21) -> (1,21), + properties: Array [ + Prop (1,23) -> (1,31) { + property: Ident "slur" (1,23) -> (1,27), + properties: Values (1,28) -> (1,31) { + values: Array [ + String "S1" (1,28) -> (1,31), + ], + }, + }, + ], + closeBrace: RBrace (1,33) -> (1,33), + }, + }, + ], + }, + durationDot: Dot (1,35) -> (1,35), + durationValue: Number "4" (1,37) -> (1,38), + beatEffects: Props (1,39) -> (1,39) { + openBrace: LBrace (1,39) -> (1,39), + properties: Array [], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate started beat effects: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started beat effects: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 41, + "line" => 1, + "offset" => 40, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started initial meta 1`] = `Score (1,1) -> (1,2)`; + +exports[`AlphaTexParserTest intermediate started initial meta: lexer-diagnostics 1`] = ` +Array [ + Map { + "code" => 2, + "severity" => 2, + "message" => "Missing identifier after meta data start", + "start" => Map { + "col" => 2, + "line" => 1, + "offset" => 1, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started initial meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started meta 1`] = `Score (1,1) -> (1,4)`; + +exports[`AlphaTexParserTest intermediate started meta properties 1`] = ` +Score (1,1) -> (1,12) { + bars: Array [ + Bar (1,1) -> (1,12) { + metaData: Array [ + Meta (1,1) -> (1,12) { + tag: Tag "track" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "track" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,10) { + values: Array [ + String "X" (1,8) -> (1,10), + ], + }, + properties: Props (1,12) -> (1,12) { + openBrace: LBrace (1,12) -> (1,12), + properties: Array [], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate started meta properties: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started meta properties: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 13, + "line" => 1, + "offset" => 12, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started meta property name 1`] = `Score (1,1) -> (1,16)`; + +exports[`AlphaTexParserTest intermediate started meta property name: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started meta property name: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'co'.", + "start" => Map { + "col" => 16, + "line" => 1, + "offset" => 15, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started meta property value 1`] = `Score (1,1) -> (1,19)`; + +exports[`AlphaTexParserTest intermediate started meta property value: lexer-diagnostics 1`] = ` +Array [ + Map { + "code" => 6, + "severity" => 2, + "message" => "Unexpected end of file. String not closed.", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started meta property value: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 203, + "severity" => 2, + "message" => "Unexpected end of file.", + "start" => Map { + "col" => 21, + "line" => 1, + "offset" => 20, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started meta string 1`] = `Score (1,1) -> (1,7)`; + +exports[`AlphaTexParserTest intermediate started meta string: lexer-diagnostics 1`] = ` +Array [ + Map { + "code" => 6, + "severity" => 2, + "message" => "Unexpected end of file. String not closed.", + "start" => Map { + "col" => 11, + "line" => 1, + "offset" => 10, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started meta string: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 203, + "severity" => 2, + "message" => "Unexpected end of file.", + "start" => Map { + "col" => 11, + "line" => 1, + "offset" => 10, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started meta: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 204, + "severity" => 2, + "message" => "Unrecognized metadata 'tr'.", + "start" => Map { + "col" => 4, + "line" => 1, + "offset" => 3, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started note effect name 1`] = `Score (1,1) -> (1,26)`; + +exports[`AlphaTexParserTest intermediate started note effect name: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started note effect name: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 205, + "severity" => 2, + "message" => "Unrecognized property 'slu'.", + "start" => Map { + "col" => 26, + "line" => 1, + "offset" => 25, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started note effect value 1`] = `Score (1,1) -> (1,27)`; + +exports[`AlphaTexParserTest intermediate started note effect value: lexer-diagnostics 1`] = ` +Array [ + Map { + "code" => 6, + "severity" => 2, + "message" => "Unexpected end of file. String not closed.", + "start" => Map { + "col" => 31, + "line" => 1, + "offset" => 30, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started note effect value: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 203, + "severity" => 2, + "message" => "Unexpected end of file.", + "start" => Map { + "col" => 31, + "line" => 1, + "offset" => 30, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started note effects 1`] = ` +Score (1,1) -> (1,21) { + bars: Array [ + Bar (1,1) -> (1,21) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,21) { + notes: NoteList (1,18) -> (1,21) { + notes: Array [ + Note (1,18) -> (1,21) { + noteValue: Ident "C4" (1,18) -> (1,20), + noteEffects: Props (1,21) -> (1,21) { + openBrace: LBrace (1,21) -> (1,21), + properties: Array [], + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate started note effects: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started note effects: parser-diagnostics 1`] = ` +Array [ + Map { + "code" => 206, + "severity" => 2, + "message" => "Unexpected end of file. Group not closed.", + "start" => Map { + "col" => 23, + "line" => 1, + "offset" => 22, + }, + }, +] +`; + +exports[`AlphaTexParserTest intermediate started pitched note 1`] = ` +Score (1,1) -> (1,19) { + bars: Array [ + Bar (1,1) -> (1,19) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,19) { + notes: NoteList (1,18) -> (1,19) { + notes: Array [ + Note (1,18) -> (1,19) { + noteValue: Ident "C" (1,18) -> (1,19), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest intermediate started pitched note: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest intermediate started pitched note: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta known no score meta 1`] = ` +Score (1,1) -> (1,16) { + bars: Array [ + Bar (1,1) -> (1,12) { + metaData: Array [ + Meta (1,1) -> (1,8) { + tag: Tag "ts" (1,1) -> (1,4) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "ts" (1,2) -> (1,4), + }, + values: Values (1,5) -> (1,8) { + values: Array [ + Number "3" (1,5) -> (1,6), + Number "4" (1,7) -> (1,8), + ], + }, + }, + ], + beats: Array [ + Beat (1,9) -> (1,11) { + notes: NoteList (1,9) -> (1,11) { + notes: Array [ + Note (1,9) -> (1,11) { + noteValue: Ident "C4" (1,9) -> (1,11), + }, + ], + }, + }, + ], + pipe: Pipe (1,12) -> (1,12), + }, + Bar (1,14) -> (1,16) { + beats: Array [ + Beat (1,14) -> (1,16) { + notes: NoteList (1,14) -> (1,16) { + notes: Array [ + Note (1,14) -> (1,16) { + noteValue: Ident "C4" (1,14) -> (1,16), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bar-meta known no score meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta known no score meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta known score meta 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,25) { + metaData: Array [ + Meta (1,3) -> (1,10) { + tag: Tag "ts" (1,3) -> (1,6) { + prefix: Backslash (1,3) -> (1,4), + tag: Ident "ts" (1,4) -> (1,6), + }, + values: Values (1,7) -> (1,10) { + values: Array [ + Number "3" (1,7) -> (1,8), + Number "4" (1,9) -> (1,10), + ], + }, + }, + ], + beats: Array [ + Beat (1,12) -> (1,20) { + notes: NoteList (1,12) -> (1,20) { + openParenthesis: LParen (1,12) -> (1,12), + notes: Array [ + Note (1,13) -> (1,19) { + noteValue: String "Value" (1,13) -> (1,19), + }, + ], + closeParenthesis: RParen (1,20) -> (1,20), + }, + }, + Beat (1,22) -> (1,24) { + notes: NoteList (1,22) -> (1,24) { + notes: Array [ + Note (1,22) -> (1,24) { + noteValue: Ident "C4" (1,22) -> (1,24), + }, + ], + }, + }, + ], + pipe: Pipe (1,25) -> (1,25), + }, + Bar (1,27) -> (1,29) { + beats: Array [ + Beat (1,27) -> (1,29) { + notes: NoteList (1,27) -> (1,29) { + notes: Array [ + Note (1,27) -> (1,29) { + noteValue: Ident "C4" (1,27) -> (1,29), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bar-meta known score meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta known score meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta unknown no score meta 1`] = ` +Score (1,1) -> (1,31) { + bars: Array [ + Bar (1,1) -> (1,27) { + metaData: Array [ + Meta (1,1) -> (1,22) { + tag: Tag "notExisting" (1,1) -> (1,13) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "notExisting" (1,2) -> (1,13), + }, + values: Values (1,14) -> (1,22) { + openParenthesis: LParen (1,14) -> (1,14), + values: Array [ + String "Value" (1,15) -> (1,21), + ], + closeParenthesis: RParen (1,22) -> (1,22), + }, + }, + ], + beats: Array [ + Beat (1,24) -> (1,26) { + notes: NoteList (1,24) -> (1,26) { + notes: Array [ + Note (1,24) -> (1,26) { + noteValue: Ident "C4" (1,24) -> (1,26), + }, + ], + }, + }, + ], + pipe: Pipe (1,27) -> (1,27), + }, + Bar (1,29) -> (1,31) { + beats: Array [ + Beat (1,29) -> (1,31) { + notes: NoteList (1,29) -> (1,31) { + notes: Array [ + Note (1,29) -> (1,31) { + noteValue: Ident "C4" (1,29) -> (1,31), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bar-meta unknown no score meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta unknown no score meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta unkonwn score meta 1`] = ` +Score (1,1) -> (1,33) { + bars: Array [ + Bar (1,1) -> (1,29) { + metaData: Array [ + Meta (1,3) -> (1,24) { + tag: Tag "notExisting" (1,3) -> (1,15) { + prefix: Backslash (1,3) -> (1,4), + tag: Ident "notExisting" (1,4) -> (1,15), + }, + values: Values (1,16) -> (1,24) { + openParenthesis: LParen (1,16) -> (1,16), + values: Array [ + String "Value" (1,17) -> (1,23), + ], + closeParenthesis: RParen (1,24) -> (1,24), + }, + }, + ], + beats: Array [ + Beat (1,26) -> (1,28) { + notes: NoteList (1,26) -> (1,28) { + notes: Array [ + Note (1,26) -> (1,28) { + noteValue: Ident "C4" (1,26) -> (1,28), + }, + ], + }, + }, + ], + pipe: Pipe (1,29) -> (1,29), + }, + Bar (1,31) -> (1,33) { + beats: Array [ + Beat (1,31) -> (1,33) { + notes: NoteList (1,31) -> (1,33) { + notes: Array [ + Note (1,31) -> (1,33) { + noteValue: Ident "C4" (1,31) -> (1,33), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bar-meta unkonwn score meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bar-meta unkonwn score meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars empty at end 1`] = ` +Score (1,1) -> (1,9) { + bars: Array [ + Bar (1,1) -> (1,4) { + beats: Array [ + Beat (1,1) -> (1,3) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + }, + ], + pipe: Pipe (1,4) -> (1,4), + }, + Bar (1,6) -> (1,9) { + beats: Array [ + Beat (1,6) -> (1,8) { + notes: NoteList (1,6) -> (1,8) { + notes: Array [ + Note (1,6) -> (1,8) { + noteValue: Ident "C4" (1,6) -> (1,8), + }, + ], + }, + }, + ], + pipe: Pipe (1,9) -> (1,9), + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bars empty at end: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars empty at end: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars multiple empty 1`] = ` +Score (1,1) -> (1,15) { + bars: Array [ + Bar (1,1) -> (1,4) { + beats: Array [ + Beat (1,1) -> (1,3) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + }, + ], + pipe: Pipe (1,4) -> (1,4), + }, + Bar (1,6) -> (1,9) { + beats: Array [ + Beat (1,6) -> (1,8) { + notes: NoteList (1,6) -> (1,8) { + notes: Array [ + Note (1,6) -> (1,8) { + noteValue: Ident "C4" (1,6) -> (1,8), + }, + ], + }, + }, + ], + pipe: Pipe (1,9) -> (1,9), + }, + Bar (1,11) -> (1,11) { + pipe: Pipe (1,11) -> (1,11), + }, + Bar (1,13) -> (1,13) { + pipe: Pipe (1,13) -> (1,13), + }, + Bar (1,15) -> (1,15) { + pipe: Pipe (1,15) -> (1,15), + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bars multiple empty then filled 1`] = ` +Score (1,1) -> (1,19) { + bars: Array [ + Bar (1,1) -> (1,4) { + beats: Array [ + Beat (1,1) -> (1,3) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + }, + ], + pipe: Pipe (1,4) -> (1,4), + }, + Bar (1,6) -> (1,9) { + beats: Array [ + Beat (1,6) -> (1,8) { + notes: NoteList (1,6) -> (1,8) { + notes: Array [ + Note (1,6) -> (1,8) { + noteValue: Ident "C4" (1,6) -> (1,8), + }, + ], + }, + }, + ], + pipe: Pipe (1,9) -> (1,9), + }, + Bar (1,11) -> (1,11) { + pipe: Pipe (1,11) -> (1,11), + }, + Bar (1,13) -> (1,13) { + pipe: Pipe (1,13) -> (1,13), + }, + Bar (1,15) -> (1,15) { + pipe: Pipe (1,15) -> (1,15), + }, + Bar (1,17) -> (1,19) { + beats: Array [ + Beat (1,17) -> (1,19) { + notes: NoteList (1,17) -> (1,19) { + notes: Array [ + Note (1,17) -> (1,19) { + noteValue: Ident "C4" (1,17) -> (1,19), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bars multiple empty then filled: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars multiple empty then filled: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars multiple empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars multiple empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars no score meta 1`] = ` +Score (1,1) -> (1,9) { + bars: Array [ + Bar (1,2) -> (1,5) { + beats: Array [ + Beat (1,2) -> (1,4) { + notes: NoteList (1,2) -> (1,4) { + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + ], + }, + }, + ], + pipe: Pipe (1,5) -> (1,5), + }, + Bar (1,7) -> (1,9) { + beats: Array [ + Beat (1,7) -> (1,9) { + notes: NoteList (1,7) -> (1,9) { + notes: Array [ + Note (1,7) -> (1,9) { + noteValue: Ident "C4" (1,7) -> (1,9), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bars no score meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars no score meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars with score meta 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,1) -> (1,20) { + metaData: Array [ + Meta (1,1) -> (1,13) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,13) { + values: Array [ + String "Test" (1,8) -> (1,13), + ], + }, + }, + ], + beats: Array [ + Beat (1,17) -> (1,19) { + notes: NoteList (1,17) -> (1,19) { + notes: Array [ + Note (1,17) -> (1,19) { + noteValue: Ident "C4" (1,17) -> (1,19), + }, + ], + }, + }, + ], + pipe: Pipe (1,20) -> (1,20), + }, + Bar (1,22) -> (1,24) { + beats: Array [ + Beat (1,22) -> (1,24) { + notes: NoteList (1,22) -> (1,24) { + notes: Array [ + Note (1,22) -> (1,24) { + noteValue: Ident "C4" (1,22) -> (1,24), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-bars with score meta: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-bars with score meta: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects empty 1`] = ` +Score (1,1) -> (1,7) { + bars: Array [ + Bar (1,1) -> (1,7) { + beats: Array [ + Beat (1,1) -> (1,7) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "8" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,7) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [], + closeBrace: RBrace (1,7) -> (1,7), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beat-effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects known 1`] = ` +Score (1,1) -> (1,13) { + bars: Array [ + Bar (1,1) -> (1,13) { + beats: Array [ + Beat (1,1) -> (1,13) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "8" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,13) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [ + Prop (1,7) -> (1,13) { + property: Ident "tu" (1,7) -> (1,9), + properties: Values (1,10) -> (1,13) { + values: Array [ + Number "2" (1,10) -> (1,11), + Number "3" (1,12) -> (1,13), + ], + }, + }, + ], + closeBrace: RBrace (1,13) -> (1,13), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beat-effects known with list 1`] = ` +Score (1,1) -> (1,18) { + bars: Array [ + Bar (1,1) -> (1,18) { + beats: Array [ + Beat (1,1) -> (1,18) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "8" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,18) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [ + Prop (1,7) -> (1,17) { + property: Ident "tb" (1,7) -> (1,9), + properties: Values (1,10) -> (1,17) { + openParenthesis: LParen (1,10) -> (1,10), + values: Array [ + Number "0" (1,11) -> (1,12), + Number "-2" (1,13) -> (1,15), + Number "0" (1,16) -> (1,17), + ], + closeParenthesis: RParen (1,17) -> (1,17), + }, + }, + ], + closeBrace: RBrace (1,18) -> (1,18), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beat-effects known with list: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects known with list: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects multiple 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,29) { + beats: Array [ + Beat (1,1) -> (1,29) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "8" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,29) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [ + Prop (1,7) -> (1,10) { + property: Ident "cre" (1,7) -> (1,10), + }, + Prop (1,11) -> (1,21) { + property: Ident "tb" (1,11) -> (1,13), + properties: Values (1,14) -> (1,21) { + openParenthesis: LParen (1,14) -> (1,14), + values: Array [ + Number "0" (1,15) -> (1,16), + Number "-2" (1,17) -> (1,19), + Number "0" (1,20) -> (1,21), + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + }, + Prop (1,23) -> (1,29) { + property: Ident "tu" (1,23) -> (1,25), + properties: Values (1,26) -> (1,29) { + values: Array [ + Number "3" (1,26) -> (1,27), + Number "2" (1,28) -> (1,29), + ], + }, + }, + ], + closeBrace: RBrace (1,29) -> (1,29), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beat-effects multiple: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects multiple: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects unknown 1`] = ` +Score (1,1) -> (1,22) { + bars: Array [ + Bar (1,1) -> (1,22) { + beats: Array [ + Beat (1,1) -> (1,22) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "8" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,22) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [ + Prop (1,7) -> (1,21) { + property: Ident "unknown" (1,7) -> (1,14), + properties: Values (1,15) -> (1,21) { + openParenthesis: LParen (1,15) -> (1,15), + values: Array [ + Number "1" (1,16) -> (1,17), + Number "2" (1,18) -> (1,19), + Number "3" (1,20) -> (1,21), + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + }, + ], + closeBrace: RBrace (1,22) -> (1,22), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beat-effects unknown: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beat-effects unknown: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted basic 1`] = ` +Score (1,1) -> (1,8) { + bars: Array [ + Bar (1,1) -> (1,8) { + beats: Array [ + Beat (1,1) -> (1,4) { + notes: NoteList (1,1) -> (1,4) { + notes: Array [ + Note (1,1) -> (1,4) { + noteValue: Number "3" (1,1) -> (1,2), + noteStringDot: Dot (1,2) -> (1,2), + noteString: Number "3" (1,3) -> (1,4), + }, + ], + }, + }, + Beat (1,5) -> (1,8) { + notes: NoteList (1,5) -> (1,8) { + notes: Array [ + Note (1,5) -> (1,8) { + noteValue: Number "4" (1,5) -> (1,6), + noteStringDot: Dot (1,6) -> (1,6), + noteString: Number "2" (1,7) -> (1,8), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted basic: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted basic: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted complex 1`] = ` +Score (1,1) -> (1,25) { + bars: Array [ + Bar (1,1) -> (1,25) { + beats: Array [ + Beat (1,1) -> (1,25) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,7) { + notes: Array [ + Note (1,4) -> (1,7) { + noteValue: Number "3" (1,4) -> (1,5), + noteStringDot: Dot (1,5) -> (1,5), + noteString: Number "3" (1,6) -> (1,7), + }, + ], + }, + durationDot: Dot (1,7) -> (1,7), + durationValue: Number "8" (1,8) -> (1,9), + beatMultiplier: Asterisk (1,10) -> (1,10), + beatMultiplierValue: Number "2" (1,12) -> (1,13), + beatEffects: Props (1,14) -> (1,25) { + openBrace: LBrace (1,14) -> (1,14), + properties: Array [ + Prop (1,15) -> (1,18) { + property: Ident "cre" (1,15) -> (1,18), + }, + Prop (1,19) -> (1,25) { + property: Ident "tu" (1,19) -> (1,21), + properties: Values (1,22) -> (1,25) { + values: Array [ + Number "3" (1,22) -> (1,23), + Number "2" (1,24) -> (1,25), + ], + }, + }, + ], + closeBrace: RBrace (1,25) -> (1,25), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted complex: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted complex: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted duration 1`] = ` +Score (1,1) -> (1,12) { + bars: Array [ + Bar (1,1) -> (1,12) { + beats: Array [ + Beat (1,1) -> (1,6) { + notes: NoteList (1,1) -> (1,4) { + notes: Array [ + Note (1,1) -> (1,4) { + noteValue: Number "3" (1,1) -> (1,2), + noteStringDot: Dot (1,2) -> (1,2), + noteString: Number "3" (1,3) -> (1,4), + }, + ], + }, + durationDot: Dot (1,4) -> (1,4), + durationValue: Number "4" (1,5) -> (1,6), + }, + Beat (1,7) -> (1,12) { + notes: NoteList (1,7) -> (1,10) { + notes: Array [ + Note (1,7) -> (1,10) { + noteValue: Number "4" (1,7) -> (1,8), + noteStringDot: Dot (1,8) -> (1,8), + noteString: Number "2" (1,9) -> (1,10), + }, + ], + }, + durationDot: Dot (1,10) -> (1,10), + durationValue: Number "8" (1,11) -> (1,12), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted duration change 1`] = ` +Score (1,1) -> (1,14) { + bars: Array [ + Bar (1,1) -> (1,14) { + beats: Array [ + Beat (1,1) -> (1,7) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,7) { + notes: Array [ + Note (1,4) -> (1,7) { + noteValue: Number "3" (1,4) -> (1,5), + noteStringDot: Dot (1,5) -> (1,5), + noteString: Number "3" (1,6) -> (1,7), + }, + ], + }, + }, + Beat (1,8) -> (1,14) { + durationChange: Duration (1,8) -> (1,10) { + colon: Colon (1,8) -> (1,8), + value: Number "4" (1,9) -> (1,10), + }, + notes: NoteList (1,11) -> (1,14) { + notes: Array [ + Note (1,11) -> (1,14) { + noteValue: Number "4" (1,11) -> (1,12), + noteStringDot: Dot (1,12) -> (1,12), + noteString: Number "2" (1,13) -> (1,14), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted duration change: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted duration change: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted duration: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted effects empty 1`] = ` +Score (1,1) -> (1,17) { + bars: Array [ + Bar (1,1) -> (1,17) { + beats: Array [ + Beat (1,1) -> (1,8) { + notes: NoteList (1,1) -> (1,4) { + notes: Array [ + Note (1,1) -> (1,4) { + noteValue: Number "3" (1,1) -> (1,2), + noteStringDot: Dot (1,2) -> (1,2), + noteString: Number "3" (1,3) -> (1,4), + }, + ], + }, + durationDot: Dot (1,4) -> (1,4), + durationValue: Number "4" (1,5) -> (1,6), + beatEffects: Props (1,7) -> (1,8) { + openBrace: LBrace (1,7) -> (1,7), + properties: Array [], + closeBrace: RBrace (1,8) -> (1,8), + }, + }, + Beat (1,10) -> (1,17) { + notes: NoteList (1,10) -> (1,13) { + notes: Array [ + Note (1,10) -> (1,13) { + noteValue: Number "4" (1,10) -> (1,11), + noteStringDot: Dot (1,11) -> (1,11), + noteString: Number "2" (1,12) -> (1,13), + }, + ], + }, + durationDot: Dot (1,13) -> (1,13), + durationValue: Number "4" (1,14) -> (1,15), + beatEffects: Props (1,16) -> (1,17) { + openBrace: LBrace (1,16) -> (1,16), + properties: Array [], + closeBrace: RBrace (1,17) -> (1,17), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted effects known 1`] = ` +Score (1,1) -> (1,30) { + bars: Array [ + Bar (1,1) -> (1,30) { + beats: Array [ + Beat (1,1) -> (1,11) { + notes: NoteList (1,1) -> (1,4) { + notes: Array [ + Note (1,1) -> (1,4) { + noteValue: Number "3" (1,1) -> (1,2), + noteStringDot: Dot (1,2) -> (1,2), + noteString: Number "3" (1,3) -> (1,4), + }, + ], + }, + durationDot: Dot (1,4) -> (1,4), + durationValue: Number "4" (1,5) -> (1,6), + beatEffects: Props (1,7) -> (1,11) { + openBrace: LBrace (1,7) -> (1,7), + properties: Array [ + Prop (1,8) -> (1,9) { + property: Ident "v" (1,8) -> (1,9), + }, + Prop (1,10) -> (1,11) { + property: Ident "f" (1,10) -> (1,11), + }, + ], + closeBrace: RBrace (1,11) -> (1,11), + }, + }, + Beat (1,13) -> (1,30) { + notes: NoteList (1,13) -> (1,16) { + notes: Array [ + Note (1,13) -> (1,16) { + noteValue: Number "4" (1,13) -> (1,14), + noteStringDot: Dot (1,14) -> (1,14), + noteString: Number "2" (1,15) -> (1,16), + }, + ], + }, + durationDot: Dot (1,16) -> (1,16), + durationValue: Number "4" (1,17) -> (1,18), + beatEffects: Props (1,19) -> (1,30) { + openBrace: LBrace (1,19) -> (1,19), + properties: Array [ + Prop (1,20) -> (1,23) { + property: Ident "cre" (1,20) -> (1,23), + }, + Prop (1,24) -> (1,30) { + property: Ident "tu" (1,24) -> (1,26), + properties: Values (1,27) -> (1,30) { + values: Array [ + Number "3" (1,27) -> (1,28), + Number "2" (1,29) -> (1,30), + ], + }, + }, + ], + closeBrace: RBrace (1,30) -> (1,30), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted multiplier 1`] = ` +Score (1,1) -> (1,12) { + bars: Array [ + Bar (1,1) -> (1,12) { + beats: Array [ + Beat (1,1) -> (1,6) { + notes: NoteList (1,1) -> (1,4) { + notes: Array [ + Note (1,1) -> (1,4) { + noteValue: Number "3" (1,1) -> (1,2), + noteStringDot: Dot (1,2) -> (1,2), + noteString: Number "3" (1,3) -> (1,4), + }, + ], + }, + beatMultiplier: Asterisk (1,4) -> (1,4), + beatMultiplierValue: Number "4" (1,5) -> (1,6), + }, + Beat (1,7) -> (1,12) { + notes: NoteList (1,7) -> (1,10) { + notes: Array [ + Note (1,7) -> (1,10) { + noteValue: Number "4" (1,7) -> (1,8), + noteStringDot: Dot (1,8) -> (1,8), + noteString: Number "2" (1,9) -> (1,10), + }, + ], + }, + beatMultiplier: Asterisk (1,10) -> (1,10), + beatMultiplierValue: Number "2" (1,11) -> (1,12), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted multiplier: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted multiplier: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted spacing 1`] = ` +Score (1,1) -> (1,37) { + bars: Array [ + Bar (1,1) -> (1,7) { + beats: Array [ + Beat (1,1) -> (1,6) { + notes: NoteList (1,1) -> (1,4) { + notes: Array [ + Note (1,1) -> (1,4) { + noteValue: Number "3" (1,1) -> (1,2), + noteStringDot: Dot (1,2) -> (1,2), + noteString: Number "3" (1,3) -> (1,4), + }, + ], + }, + durationDot: Dot (1,4) -> (1,4), + durationValue: Number "8" (1,5) -> (1,6), + }, + ], + pipe: Pipe (1,7) -> (1,7), + }, + Bar (1,9) -> (1,19) { + beats: Array [ + Beat (1,9) -> (1,18) { + notes: NoteList (1,9) -> (1,14) { + notes: Array [ + Note (1,9) -> (1,14) { + noteValue: Number "3" (1,9) -> (1,10), + noteStringDot: Dot (1,11) -> (1,11), + noteString: Number "3" (1,13) -> (1,14), + }, + ], + }, + durationDot: Dot (1,15) -> (1,15), + durationValue: Number "8" (1,17) -> (1,18), + }, + ], + pipe: Pipe (1,19) -> (1,19), + }, + Bar (1,21) -> (1,28) { + beats: Array [ + Beat (1,21) -> (1,27) { + notes: NoteList (1,21) -> (1,24) { + notes: Array [ + Note (1,21) -> (1,24) { + noteValue: Number "3" (1,21) -> (1,22), + noteStringDot: Dot (1,22) -> (1,22), + noteString: Number "3" (1,23) -> (1,24), + }, + ], + }, + durationDot: Dot (1,25) -> (1,25), + durationValue: Number "8" (1,26) -> (1,27), + }, + ], + pipe: Pipe (1,28) -> (1,28), + }, + Bar (1,30) -> (1,37) { + beats: Array [ + Beat (1,30) -> (1,37) { + notes: NoteList (1,30) -> (1,35) { + notes: Array [ + Note (1,30) -> (1,35) { + noteValue: Number "3" (1,30) -> (1,31), + noteStringDot: Dot (1,32) -> (1,32), + noteString: Number "3" (1,34) -> (1,35), + }, + ], + }, + durationDot: Dot (1,35) -> (1,35), + durationValue: Number "8" (1,36) -> (1,37), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted spacing: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-fretted spacing: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched basic 1`] = ` +Score (1,1) -> (1,6) { + bars: Array [ + Bar (1,1) -> (1,6) { + beats: Array [ + Beat (1,1) -> (1,3) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + }, + Beat (1,4) -> (1,6) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C5" (1,4) -> (1,6), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched basic: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched basic: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched complex 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,1) -> (1,24) { + beats: Array [ + Beat (1,1) -> (1,24) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + durationDot: Dot (1,6) -> (1,6), + durationValue: Number "8" (1,7) -> (1,8), + beatMultiplier: Asterisk (1,9) -> (1,9), + beatMultiplierValue: Number "2" (1,11) -> (1,12), + beatEffects: Props (1,13) -> (1,24) { + openBrace: LBrace (1,13) -> (1,13), + properties: Array [ + Prop (1,14) -> (1,17) { + property: Ident "cre" (1,14) -> (1,17), + }, + Prop (1,18) -> (1,24) { + property: Ident "tu" (1,18) -> (1,20), + properties: Values (1,21) -> (1,24) { + values: Array [ + Number "3" (1,21) -> (1,22), + Number "2" (1,23) -> (1,24), + ], + }, + }, + ], + closeBrace: RBrace (1,24) -> (1,24), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched complex: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched complex: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched duration 1`] = ` +Score (1,1) -> (1,10) { + bars: Array [ + Bar (1,1) -> (1,10) { + beats: Array [ + Beat (1,1) -> (1,5) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "4" (1,4) -> (1,5), + }, + Beat (1,6) -> (1,10) { + notes: NoteList (1,6) -> (1,8) { + notes: Array [ + Note (1,6) -> (1,8) { + noteValue: Ident "C5" (1,6) -> (1,8), + }, + ], + }, + durationDot: Dot (1,8) -> (1,8), + durationValue: Number "8" (1,9) -> (1,10), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched duration change 1`] = ` +Score (1,1) -> (1,12) { + bars: Array [ + Bar (1,1) -> (1,12) { + beats: Array [ + Beat (1,1) -> (1,6) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + Beat (1,7) -> (1,12) { + durationChange: Duration (1,7) -> (1,9) { + colon: Colon (1,7) -> (1,7), + value: Number "4" (1,8) -> (1,9), + }, + notes: NoteList (1,10) -> (1,12) { + notes: Array [ + Note (1,10) -> (1,12) { + noteValue: Ident "C5" (1,10) -> (1,12), + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched duration change: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched duration change: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched duration: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched effects empty 1`] = ` +Score (1,1) -> (1,15) { + bars: Array [ + Bar (1,1) -> (1,15) { + beats: Array [ + Beat (1,1) -> (1,7) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "4" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,7) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [], + closeBrace: RBrace (1,7) -> (1,7), + }, + }, + Beat (1,9) -> (1,15) { + notes: NoteList (1,9) -> (1,11) { + notes: Array [ + Note (1,9) -> (1,11) { + noteValue: Ident "C5" (1,9) -> (1,11), + }, + ], + }, + durationDot: Dot (1,11) -> (1,11), + durationValue: Number "4" (1,12) -> (1,13), + beatEffects: Props (1,14) -> (1,15) { + openBrace: LBrace (1,14) -> (1,14), + properties: Array [], + closeBrace: RBrace (1,15) -> (1,15), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched effects known 1`] = ` +Score (1,1) -> (1,28) { + bars: Array [ + Bar (1,1) -> (1,28) { + beats: Array [ + Beat (1,1) -> (1,10) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + durationDot: Dot (1,3) -> (1,3), + durationValue: Number "4" (1,4) -> (1,5), + beatEffects: Props (1,6) -> (1,10) { + openBrace: LBrace (1,6) -> (1,6), + properties: Array [ + Prop (1,7) -> (1,8) { + property: Ident "v" (1,7) -> (1,8), + }, + Prop (1,9) -> (1,10) { + property: Ident "f" (1,9) -> (1,10), + }, + ], + closeBrace: RBrace (1,10) -> (1,10), + }, + }, + Beat (1,12) -> (1,28) { + notes: NoteList (1,12) -> (1,14) { + notes: Array [ + Note (1,12) -> (1,14) { + noteValue: Ident "C5" (1,12) -> (1,14), + }, + ], + }, + durationDot: Dot (1,14) -> (1,14), + durationValue: Number "4" (1,15) -> (1,16), + beatEffects: Props (1,17) -> (1,28) { + openBrace: LBrace (1,17) -> (1,17), + properties: Array [ + Prop (1,18) -> (1,21) { + property: Ident "cre" (1,18) -> (1,21), + }, + Prop (1,22) -> (1,28) { + property: Ident "tu" (1,22) -> (1,24), + properties: Values (1,25) -> (1,28) { + values: Array [ + Number "3" (1,25) -> (1,26), + Number "2" (1,27) -> (1,28), + ], + }, + }, + ], + closeBrace: RBrace (1,28) -> (1,28), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched multiplier 1`] = ` +Score (1,1) -> (1,10) { + bars: Array [ + Bar (1,1) -> (1,10) { + beats: Array [ + Beat (1,1) -> (1,5) { + notes: NoteList (1,1) -> (1,3) { + notes: Array [ + Note (1,1) -> (1,3) { + noteValue: Ident "C4" (1,1) -> (1,3), + }, + ], + }, + beatMultiplier: Asterisk (1,3) -> (1,3), + beatMultiplierValue: Number "4" (1,4) -> (1,5), + }, + Beat (1,6) -> (1,10) { + notes: NoteList (1,6) -> (1,8) { + notes: Array [ + Note (1,6) -> (1,8) { + noteValue: Ident "C5" (1,6) -> (1,8), + }, + ], + }, + beatMultiplier: Asterisk (1,8) -> (1,8), + beatMultiplierValue: Number "2" (1,9) -> (1,10), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched multiplier: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-basic-pitched multiplier: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted chord 1`] = ` +Score (1,1) -> (1,19) { + bars: Array [ + Bar (1,1) -> (1,19) { + beats: Array [ + Beat (1,1) -> (1,9) { + notes: NoteList (1,1) -> (1,9) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + Note (1,6) -> (1,9) { + noteValue: Number "4" (1,6) -> (1,7), + noteStringDot: Dot (1,7) -> (1,7), + noteString: Number "2" (1,8) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + }, + Beat (1,11) -> (1,19) { + notes: NoteList (1,11) -> (1,19) { + openParenthesis: LParen (1,11) -> (1,11), + notes: Array [ + Note (1,12) -> (1,15) { + noteValue: Number "1" (1,12) -> (1,13), + noteStringDot: Dot (1,13) -> (1,13), + noteString: Number "2" (1,14) -> (1,15), + }, + Note (1,16) -> (1,19) { + noteValue: Number "6" (1,16) -> (1,17), + noteStringDot: Dot (1,17) -> (1,17), + noteString: Number "1" (1,18) -> (1,19), + }, + ], + closeParenthesis: RParen (1,19) -> (1,19), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted chord: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted chord: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted complex 1`] = ` +Score (1,1) -> (1,31) { + bars: Array [ + Bar (1,1) -> (1,31) { + beats: Array [ + Beat (1,1) -> (1,31) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,12) { + openParenthesis: LParen (1,4) -> (1,4), + notes: Array [ + Note (1,5) -> (1,8) { + noteValue: Number "3" (1,5) -> (1,6), + noteStringDot: Dot (1,6) -> (1,6), + noteString: Number "3" (1,7) -> (1,8), + }, + Note (1,9) -> (1,12) { + noteValue: Number "4" (1,9) -> (1,10), + noteStringDot: Dot (1,10) -> (1,10), + noteString: Number "2" (1,11) -> (1,12), + }, + ], + closeParenthesis: RParen (1,12) -> (1,12), + }, + durationDot: Dot (1,13) -> (1,13), + durationValue: Number "8" (1,14) -> (1,15), + beatMultiplier: Asterisk (1,16) -> (1,16), + beatMultiplierValue: Number "2" (1,18) -> (1,19), + beatEffects: Props (1,20) -> (1,31) { + openBrace: LBrace (1,20) -> (1,20), + properties: Array [ + Prop (1,21) -> (1,24) { + property: Ident "cre" (1,21) -> (1,24), + }, + Prop (1,25) -> (1,31) { + property: Ident "tu" (1,25) -> (1,27), + properties: Values (1,28) -> (1,31) { + values: Array [ + Number "3" (1,28) -> (1,29), + Number "2" (1,30) -> (1,31), + ], + }, + }, + ], + closeBrace: RBrace (1,31) -> (1,31), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted complex: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted complex: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted duration 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,1) -> (1,24) { + beats: Array [ + Beat (1,1) -> (1,12) { + notes: NoteList (1,1) -> (1,9) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + Note (1,6) -> (1,9) { + noteValue: Number "4" (1,6) -> (1,7), + noteStringDot: Dot (1,7) -> (1,7), + noteString: Number "2" (1,8) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + durationDot: Dot (1,10) -> (1,10), + durationValue: Number "4" (1,11) -> (1,12), + }, + Beat (1,13) -> (1,24) { + notes: NoteList (1,13) -> (1,21) { + openParenthesis: LParen (1,13) -> (1,13), + notes: Array [ + Note (1,14) -> (1,17) { + noteValue: Number "1" (1,14) -> (1,15), + noteStringDot: Dot (1,15) -> (1,15), + noteString: Number "2" (1,16) -> (1,17), + }, + Note (1,18) -> (1,21) { + noteValue: Number "6" (1,18) -> (1,19), + noteStringDot: Dot (1,19) -> (1,19), + noteString: Number "1" (1,20) -> (1,21), + }, + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + durationDot: Dot (1,22) -> (1,22), + durationValue: Number "8" (1,23) -> (1,24), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted duration change 1`] = ` +Score (1,1) -> (1,25) { + bars: Array [ + Bar (1,1) -> (1,25) { + beats: Array [ + Beat (1,1) -> (1,12) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,12) { + openParenthesis: LParen (1,4) -> (1,4), + notes: Array [ + Note (1,5) -> (1,8) { + noteValue: Number "3" (1,5) -> (1,6), + noteStringDot: Dot (1,6) -> (1,6), + noteString: Number "3" (1,7) -> (1,8), + }, + Note (1,9) -> (1,12) { + noteValue: Number "4" (1,9) -> (1,10), + noteStringDot: Dot (1,10) -> (1,10), + noteString: Number "2" (1,11) -> (1,12), + }, + ], + closeParenthesis: RParen (1,12) -> (1,12), + }, + }, + Beat (1,14) -> (1,25) { + durationChange: Duration (1,14) -> (1,16) { + colon: Colon (1,14) -> (1,14), + value: Number "4" (1,15) -> (1,16), + }, + notes: NoteList (1,17) -> (1,25) { + openParenthesis: LParen (1,17) -> (1,17), + notes: Array [ + Note (1,18) -> (1,21) { + noteValue: Number "1" (1,18) -> (1,19), + noteStringDot: Dot (1,19) -> (1,19), + noteString: Number "2" (1,20) -> (1,21), + }, + Note (1,22) -> (1,25) { + noteValue: Number "6" (1,22) -> (1,23), + noteStringDot: Dot (1,23) -> (1,23), + noteString: Number "1" (1,24) -> (1,25), + }, + ], + closeParenthesis: RParen (1,25) -> (1,25), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted duration change: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted duration change: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted duration: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted effects empty 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,29) { + beats: Array [ + Beat (1,1) -> (1,14) { + notes: NoteList (1,1) -> (1,9) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + Note (1,6) -> (1,9) { + noteValue: Number "4" (1,6) -> (1,7), + noteStringDot: Dot (1,7) -> (1,7), + noteString: Number "2" (1,8) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + durationDot: Dot (1,10) -> (1,10), + durationValue: Number "4" (1,11) -> (1,12), + beatEffects: Props (1,13) -> (1,14) { + openBrace: LBrace (1,13) -> (1,13), + properties: Array [], + closeBrace: RBrace (1,14) -> (1,14), + }, + }, + Beat (1,16) -> (1,29) { + notes: NoteList (1,16) -> (1,24) { + openParenthesis: LParen (1,16) -> (1,16), + notes: Array [ + Note (1,17) -> (1,20) { + noteValue: Number "1" (1,17) -> (1,18), + noteStringDot: Dot (1,18) -> (1,18), + noteString: Number "2" (1,19) -> (1,20), + }, + Note (1,21) -> (1,24) { + noteValue: Number "6" (1,21) -> (1,22), + noteStringDot: Dot (1,22) -> (1,22), + noteString: Number "1" (1,23) -> (1,24), + }, + ], + closeParenthesis: RParen (1,24) -> (1,24), + }, + durationDot: Dot (1,25) -> (1,25), + durationValue: Number "4" (1,26) -> (1,27), + beatEffects: Props (1,28) -> (1,29) { + openBrace: LBrace (1,28) -> (1,28), + properties: Array [], + closeBrace: RBrace (1,29) -> (1,29), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted effects known 1`] = ` +Score (1,1) -> (1,42) { + bars: Array [ + Bar (1,1) -> (1,42) { + beats: Array [ + Beat (1,1) -> (1,17) { + notes: NoteList (1,1) -> (1,9) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + Note (1,6) -> (1,9) { + noteValue: Number "4" (1,6) -> (1,7), + noteStringDot: Dot (1,7) -> (1,7), + noteString: Number "2" (1,8) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + durationDot: Dot (1,10) -> (1,10), + durationValue: Number "4" (1,11) -> (1,12), + beatEffects: Props (1,13) -> (1,17) { + openBrace: LBrace (1,13) -> (1,13), + properties: Array [ + Prop (1,14) -> (1,15) { + property: Ident "v" (1,14) -> (1,15), + }, + Prop (1,16) -> (1,17) { + property: Ident "f" (1,16) -> (1,17), + }, + ], + closeBrace: RBrace (1,17) -> (1,17), + }, + }, + Beat (1,19) -> (1,42) { + notes: NoteList (1,19) -> (1,27) { + openParenthesis: LParen (1,19) -> (1,19), + notes: Array [ + Note (1,20) -> (1,23) { + noteValue: Number "1" (1,20) -> (1,21), + noteStringDot: Dot (1,21) -> (1,21), + noteString: Number "2" (1,22) -> (1,23), + }, + Note (1,24) -> (1,27) { + noteValue: Number "6" (1,24) -> (1,25), + noteStringDot: Dot (1,25) -> (1,25), + noteString: Number "1" (1,26) -> (1,27), + }, + ], + closeParenthesis: RParen (1,27) -> (1,27), + }, + durationDot: Dot (1,28) -> (1,28), + durationValue: Number "4" (1,29) -> (1,30), + beatEffects: Props (1,31) -> (1,42) { + openBrace: LBrace (1,31) -> (1,31), + properties: Array [ + Prop (1,32) -> (1,35) { + property: Ident "cre" (1,32) -> (1,35), + }, + Prop (1,36) -> (1,42) { + property: Ident "tu" (1,36) -> (1,38), + properties: Values (1,39) -> (1,42) { + values: Array [ + Number "3" (1,39) -> (1,40), + Number "2" (1,41) -> (1,42), + ], + }, + }, + ], + closeBrace: RBrace (1,42) -> (1,42), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted multiplier 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,1) -> (1,24) { + beats: Array [ + Beat (1,1) -> (1,12) { + notes: NoteList (1,1) -> (1,9) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,5) { + noteValue: Number "3" (1,2) -> (1,3), + noteStringDot: Dot (1,3) -> (1,3), + noteString: Number "3" (1,4) -> (1,5), + }, + Note (1,6) -> (1,9) { + noteValue: Number "4" (1,6) -> (1,7), + noteStringDot: Dot (1,7) -> (1,7), + noteString: Number "2" (1,8) -> (1,9), + }, + ], + closeParenthesis: RParen (1,9) -> (1,9), + }, + beatMultiplier: Asterisk (1,10) -> (1,10), + beatMultiplierValue: Number "4" (1,11) -> (1,12), + }, + Beat (1,13) -> (1,24) { + notes: NoteList (1,13) -> (1,21) { + openParenthesis: LParen (1,13) -> (1,13), + notes: Array [ + Note (1,14) -> (1,17) { + noteValue: Number "1" (1,14) -> (1,15), + noteStringDot: Dot (1,15) -> (1,15), + noteString: Number "2" (1,16) -> (1,17), + }, + Note (1,18) -> (1,21) { + noteValue: Number "6" (1,18) -> (1,19), + noteStringDot: Dot (1,19) -> (1,19), + noteString: Number "1" (1,20) -> (1,21), + }, + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + beatMultiplier: Asterisk (1,22) -> (1,22), + beatMultiplierValue: Number "2" (1,23) -> (1,24), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted multiplier: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted multiplier: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted spacing 1`] = ` +Score (1,1) -> (4,19) { + bars: Array [ + Bar (1,14) -> (4,19) { + beats: Array [ + Beat (1,14) -> (4,19) { + notes: NoteList (1,14) -> (4,14) { + openParenthesis: LParen (1,14) -> (1,14), + notes: Array [ + Note (2,18) -> (2,21) { + noteValue: Number "3" (2,18) -> (2,19), + noteStringDot: Dot (2,19) -> (2,19), + noteString: Number "3" (2,20) -> (2,21), + }, + Note (3,18) -> (4,1) { + noteValue: Number "3" (3,18) -> (3,19), + noteStringDot: Dot (3,20) -> (3,20), + noteString: Number "3" (3,22) -> (4,1), + }, + ], + closeParenthesis: RParen (4,14) -> (4,14), + }, + durationDot: Dot (4,16) -> (4,16), + durationValue: Number "8" (4,18) -> (4,19), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted spacing: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-fretted spacing: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched chord 1`] = ` +Score (1,1) -> (1,15) { + bars: Array [ + Bar (1,1) -> (1,15) { + beats: Array [ + Beat (1,1) -> (1,7) { + notes: NoteList (1,1) -> (1,7) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + Note (1,5) -> (1,7) { + noteValue: Ident "C5" (1,5) -> (1,7), + }, + ], + closeParenthesis: RParen (1,7) -> (1,7), + }, + }, + Beat (1,9) -> (1,15) { + notes: NoteList (1,9) -> (1,15) { + openParenthesis: LParen (1,9) -> (1,9), + notes: Array [ + Note (1,10) -> (1,12) { + noteValue: Ident "D4" (1,10) -> (1,12), + }, + Note (1,13) -> (1,15) { + noteValue: Ident "D5" (1,13) -> (1,15), + }, + ], + closeParenthesis: RParen (1,15) -> (1,15), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched chord: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched chord: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched complex 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,29) { + beats: Array [ + Beat (1,1) -> (1,29) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,10) { + openParenthesis: LParen (1,4) -> (1,4), + notes: Array [ + Note (1,5) -> (1,7) { + noteValue: Ident "C4" (1,5) -> (1,7), + }, + Note (1,8) -> (1,10) { + noteValue: Ident "C5" (1,8) -> (1,10), + }, + ], + closeParenthesis: RParen (1,10) -> (1,10), + }, + durationDot: Dot (1,11) -> (1,11), + durationValue: Number "8" (1,12) -> (1,13), + beatMultiplier: Asterisk (1,14) -> (1,14), + beatMultiplierValue: Number "2" (1,16) -> (1,17), + beatEffects: Props (1,18) -> (1,29) { + openBrace: LBrace (1,18) -> (1,18), + properties: Array [ + Prop (1,19) -> (1,22) { + property: Ident "cre" (1,19) -> (1,22), + }, + Prop (1,23) -> (1,29) { + property: Ident "tu" (1,23) -> (1,25), + properties: Values (1,26) -> (1,29) { + values: Array [ + Number "3" (1,26) -> (1,27), + Number "2" (1,28) -> (1,29), + ], + }, + }, + ], + closeBrace: RBrace (1,29) -> (1,29), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched complex: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched complex: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched duration 1`] = ` +Score (1,1) -> (1,20) { + bars: Array [ + Bar (1,1) -> (1,20) { + beats: Array [ + Beat (1,1) -> (1,10) { + notes: NoteList (1,1) -> (1,7) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + Note (1,5) -> (1,7) { + noteValue: Ident "C5" (1,5) -> (1,7), + }, + ], + closeParenthesis: RParen (1,7) -> (1,7), + }, + durationDot: Dot (1,8) -> (1,8), + durationValue: Number "4" (1,9) -> (1,10), + }, + Beat (1,11) -> (1,20) { + notes: NoteList (1,11) -> (1,17) { + openParenthesis: LParen (1,11) -> (1,11), + notes: Array [ + Note (1,12) -> (1,14) { + noteValue: Ident "D4" (1,12) -> (1,14), + }, + Note (1,15) -> (1,17) { + noteValue: Ident "D5" (1,15) -> (1,17), + }, + ], + closeParenthesis: RParen (1,17) -> (1,17), + }, + durationDot: Dot (1,18) -> (1,18), + durationValue: Number "8" (1,19) -> (1,20), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched duration change 1`] = ` +Score (1,1) -> (1,21) { + bars: Array [ + Bar (1,1) -> (1,21) { + beats: Array [ + Beat (1,1) -> (1,10) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + notes: NoteList (1,4) -> (1,10) { + openParenthesis: LParen (1,4) -> (1,4), + notes: Array [ + Note (1,5) -> (1,7) { + noteValue: Ident "C4" (1,5) -> (1,7), + }, + Note (1,8) -> (1,10) { + noteValue: Ident "C5" (1,8) -> (1,10), + }, + ], + closeParenthesis: RParen (1,10) -> (1,10), + }, + }, + Beat (1,12) -> (1,21) { + durationChange: Duration (1,12) -> (1,14) { + colon: Colon (1,12) -> (1,12), + value: Number "4" (1,13) -> (1,14), + }, + notes: NoteList (1,15) -> (1,21) { + openParenthesis: LParen (1,15) -> (1,15), + notes: Array [ + Note (1,16) -> (1,18) { + noteValue: Ident "D4" (1,16) -> (1,18), + }, + Note (1,19) -> (1,21) { + noteValue: Ident "D5" (1,19) -> (1,21), + }, + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched duration change: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched duration change: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched duration: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched effects empty 1`] = ` +Score (1,1) -> (1,21) { + bars: Array [ + Bar (1,1) -> (1,21) { + beats: Array [ + Beat (1,1) -> (1,10) { + notes: NoteList (1,1) -> (1,7) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + Note (1,5) -> (1,7) { + noteValue: Ident "C5" (1,5) -> (1,7), + }, + ], + closeParenthesis: RParen (1,7) -> (1,7), + }, + beatEffects: Props (1,9) -> (1,10) { + openBrace: LBrace (1,9) -> (1,9), + properties: Array [], + closeBrace: RBrace (1,10) -> (1,10), + }, + }, + Beat (1,12) -> (1,21) { + notes: NoteList (1,12) -> (1,18) { + openParenthesis: LParen (1,12) -> (1,12), + notes: Array [ + Note (1,13) -> (1,15) { + noteValue: Ident "D4" (1,13) -> (1,15), + }, + Note (1,16) -> (1,18) { + noteValue: Ident "D5" (1,16) -> (1,18), + }, + ], + closeParenthesis: RParen (1,18) -> (1,18), + }, + beatEffects: Props (1,20) -> (1,21) { + openBrace: LBrace (1,20) -> (1,20), + properties: Array [], + closeBrace: RBrace (1,21) -> (1,21), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched effects known 1`] = ` +Score (1,1) -> (1,34) { + bars: Array [ + Bar (1,1) -> (1,34) { + beats: Array [ + Beat (1,1) -> (1,13) { + notes: NoteList (1,1) -> (1,7) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + Note (1,5) -> (1,7) { + noteValue: Ident "C5" (1,5) -> (1,7), + }, + ], + closeParenthesis: RParen (1,7) -> (1,7), + }, + beatEffects: Props (1,9) -> (1,13) { + openBrace: LBrace (1,9) -> (1,9), + properties: Array [ + Prop (1,10) -> (1,11) { + property: Ident "v" (1,10) -> (1,11), + }, + Prop (1,12) -> (1,13) { + property: Ident "f" (1,12) -> (1,13), + }, + ], + closeBrace: RBrace (1,13) -> (1,13), + }, + }, + Beat (1,15) -> (1,34) { + notes: NoteList (1,15) -> (1,21) { + openParenthesis: LParen (1,15) -> (1,15), + notes: Array [ + Note (1,16) -> (1,18) { + noteValue: Ident "D4" (1,16) -> (1,18), + }, + Note (1,19) -> (1,21) { + noteValue: Ident "D5" (1,19) -> (1,21), + }, + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + beatEffects: Props (1,23) -> (1,34) { + openBrace: LBrace (1,23) -> (1,23), + properties: Array [ + Prop (1,24) -> (1,27) { + property: Ident "cre" (1,24) -> (1,27), + }, + Prop (1,28) -> (1,34) { + property: Ident "tu" (1,28) -> (1,30), + properties: Values (1,31) -> (1,34) { + values: Array [ + Number "3" (1,31) -> (1,32), + Number "2" (1,33) -> (1,34), + ], + }, + }, + ], + closeBrace: RBrace (1,34) -> (1,34), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched multiplier 1`] = ` +Score (1,1) -> (1,20) { + bars: Array [ + Bar (1,1) -> (1,20) { + beats: Array [ + Beat (1,1) -> (1,10) { + notes: NoteList (1,1) -> (1,7) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,4) { + noteValue: Ident "C4" (1,2) -> (1,4), + }, + Note (1,5) -> (1,7) { + noteValue: Ident "C5" (1,5) -> (1,7), + }, + ], + closeParenthesis: RParen (1,7) -> (1,7), + }, + beatMultiplier: Asterisk (1,8) -> (1,8), + beatMultiplierValue: Number "4" (1,9) -> (1,10), + }, + Beat (1,11) -> (1,20) { + notes: NoteList (1,11) -> (1,17) { + openParenthesis: LParen (1,11) -> (1,11), + notes: Array [ + Note (1,12) -> (1,14) { + noteValue: Ident "D4" (1,12) -> (1,14), + }, + Note (1,15) -> (1,17) { + noteValue: Ident "D5" (1,15) -> (1,17), + }, + ], + closeParenthesis: RParen (1,17) -> (1,17), + }, + beatMultiplier: Asterisk (1,18) -> (1,18), + beatMultiplierValue: Number "2" (1,19) -> (1,20), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched multiplier: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-chord-pitched multiplier: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest complex 1`] = ` +Score (1,1) -> (1,23) { + bars: Array [ + Bar (1,1) -> (1,23) { + beats: Array [ + Beat (1,1) -> (1,23) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + rest: Ident "r" (1,4) -> (1,5), + durationDot: Dot (1,5) -> (1,5), + durationValue: Number "8" (1,6) -> (1,7), + beatMultiplier: Asterisk (1,8) -> (1,8), + beatMultiplierValue: Number "2" (1,10) -> (1,11), + beatEffects: Props (1,12) -> (1,23) { + openBrace: LBrace (1,12) -> (1,12), + properties: Array [ + Prop (1,13) -> (1,16) { + property: Ident "cre" (1,13) -> (1,16), + }, + Prop (1,17) -> (1,23) { + property: Ident "tu" (1,17) -> (1,19), + properties: Values (1,20) -> (1,23) { + values: Array [ + Number "3" (1,20) -> (1,21), + Number "2" (1,22) -> (1,23), + ], + }, + }, + ], + closeBrace: RBrace (1,23) -> (1,23), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest complex: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest complex: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest duration 1`] = ` +Score (1,1) -> (1,8) { + bars: Array [ + Bar (1,1) -> (1,8) { + beats: Array [ + Beat (1,1) -> (1,4) { + rest: Ident "r" (1,1) -> (1,2), + durationDot: Dot (1,2) -> (1,2), + durationValue: Number "4" (1,3) -> (1,4), + }, + Beat (1,5) -> (1,8) { + rest: Ident "r" (1,5) -> (1,6), + durationDot: Dot (1,6) -> (1,6), + durationValue: Number "8" (1,7) -> (1,8), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest duration change 1`] = ` +Score (1,1) -> (1,10) { + bars: Array [ + Bar (1,1) -> (1,10) { + beats: Array [ + Beat (1,1) -> (1,5) { + durationChange: Duration (1,1) -> (1,3) { + colon: Colon (1,1) -> (1,1), + value: Number "2" (1,2) -> (1,3), + }, + rest: Ident "r" (1,4) -> (1,5), + }, + Beat (1,6) -> (1,10) { + durationChange: Duration (1,6) -> (1,8) { + colon: Colon (1,6) -> (1,6), + value: Number "4" (1,7) -> (1,8), + }, + rest: Ident "r" (1,9) -> (1,10), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest duration change: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest duration change: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest duration: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest duration: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest effects empty 1`] = ` +Score (1,1) -> (1,13) { + bars: Array [ + Bar (1,1) -> (1,13) { + beats: Array [ + Beat (1,1) -> (1,6) { + rest: Ident "r" (1,1) -> (1,2), + durationDot: Dot (1,2) -> (1,2), + durationValue: Number "4" (1,3) -> (1,4), + beatEffects: Props (1,5) -> (1,6) { + openBrace: LBrace (1,5) -> (1,5), + properties: Array [], + closeBrace: RBrace (1,6) -> (1,6), + }, + }, + Beat (1,8) -> (1,13) { + rest: Ident "r" (1,8) -> (1,9), + durationDot: Dot (1,9) -> (1,9), + durationValue: Number "4" (1,10) -> (1,11), + beatEffects: Props (1,12) -> (1,13) { + openBrace: LBrace (1,12) -> (1,12), + properties: Array [], + closeBrace: RBrace (1,13) -> (1,13), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest effects known 1`] = ` +Score (1,1) -> (1,26) { + bars: Array [ + Bar (1,1) -> (1,26) { + beats: Array [ + Beat (1,1) -> (1,9) { + rest: Ident "r" (1,1) -> (1,2), + durationDot: Dot (1,2) -> (1,2), + durationValue: Number "4" (1,3) -> (1,4), + beatEffects: Props (1,5) -> (1,9) { + openBrace: LBrace (1,5) -> (1,5), + properties: Array [ + Prop (1,6) -> (1,7) { + property: Ident "v" (1,6) -> (1,7), + }, + Prop (1,8) -> (1,9) { + property: Ident "f" (1,8) -> (1,9), + }, + ], + closeBrace: RBrace (1,9) -> (1,9), + }, + }, + Beat (1,11) -> (1,26) { + rest: Ident "r" (1,11) -> (1,12), + durationDot: Dot (1,12) -> (1,12), + durationValue: Number "4" (1,13) -> (1,14), + beatEffects: Props (1,15) -> (1,26) { + openBrace: LBrace (1,15) -> (1,15), + properties: Array [ + Prop (1,16) -> (1,19) { + property: Ident "cre" (1,16) -> (1,19), + }, + Prop (1,20) -> (1,26) { + property: Ident "tu" (1,20) -> (1,22), + properties: Values (1,23) -> (1,26) { + values: Array [ + Number "3" (1,23) -> (1,24), + Number "2" (1,25) -> (1,26), + ], + }, + }, + ], + closeBrace: RBrace (1,26) -> (1,26), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest multiplier 1`] = ` +Score (1,1) -> (1,8) { + bars: Array [ + Bar (1,1) -> (1,8) { + beats: Array [ + Beat (1,1) -> (1,4) { + rest: Ident "r" (1,1) -> (1,2), + beatMultiplier: Asterisk (1,2) -> (1,2), + beatMultiplierValue: Number "4" (1,3) -> (1,4), + }, + Beat (1,5) -> (1,8) { + rest: Ident "r" (1,5) -> (1,6), + beatMultiplier: Asterisk (1,6) -> (1,6), + beatMultiplierValue: Number "2" (1,7) -> (1,8), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest multiplier: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest multiplier: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest rest 1`] = ` +Score (1,1) -> (1,2) { + bars: Array [ + Bar (1,1) -> (1,2) { + beats: Array [ + Beat (1,1) -> (1,2) { + rest: Ident "r" (1,1) -> (1,2), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-beats-rest rest: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-beats-rest rest: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-empty empty 1`] = `Score (1,1) -> (1,1)`; + +exports[`AlphaTexParserTest valid-empty empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-empty empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects beat effects in note effect 1`] = ` +Score (1,1) -> (1,13) { + bars: Array [ + Bar (1,1) -> (1,13) { + beats: Array [ + Beat (1,1) -> (1,13) { + notes: NoteList (1,1) -> (1,13) { + notes: Array [ + Note (1,1) -> (1,13) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,13) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [ + Prop (1,6) -> (1,12) { + property: Ident "tu" (1,6) -> (1,8), + properties: Values (1,9) -> (1,12) { + values: Array [ + Number "3" (1,9) -> (1,10), + Number "2" (1,11) -> (1,12), + ], + }, + }, + ], + closeBrace: RBrace (1,13) -> (1,13), + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects beat effects in note effect: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects beat effects in note effect: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects empty 1`] = ` +Score (1,1) -> (1,5) { + bars: Array [ + Bar (1,1) -> (1,5) { + beats: Array [ + Beat (1,1) -> (1,5) { + notes: NoteList (1,1) -> (1,5) { + notes: Array [ + Note (1,1) -> (1,5) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,5) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [], + closeBrace: RBrace (1,5) -> (1,5), + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects known 1`] = ` +Score (1,1) -> (1,11) { + bars: Array [ + Bar (1,1) -> (1,11) { + beats: Array [ + Beat (1,1) -> (1,11) { + notes: NoteList (1,1) -> (1,11) { + notes: Array [ + Note (1,1) -> (1,11) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,11) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [ + Prop (1,5) -> (1,11) { + property: Ident "tr" (1,5) -> (1,7), + properties: Values (1,8) -> (1,11) { + values: Array [ + Number "4" (1,8) -> (1,9), + Number "4" (1,10) -> (1,11), + ], + }, + }, + ], + closeBrace: RBrace (1,11) -> (1,11), + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects known with list 1`] = ` +Score (1,1) -> (1,14) { + bars: Array [ + Bar (1,1) -> (1,14) { + beats: Array [ + Beat (1,1) -> (1,14) { + notes: NoteList (1,1) -> (1,14) { + notes: Array [ + Note (1,1) -> (1,14) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,14) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [ + Prop (1,5) -> (1,13) { + property: Ident "b" (1,5) -> (1,6), + properties: Values (1,7) -> (1,13) { + openParenthesis: LParen (1,7) -> (1,7), + values: Array [ + Number "0" (1,8) -> (1,9), + Number "4" (1,10) -> (1,11), + Number "0" (1,12) -> (1,13), + ], + closeParenthesis: RParen (1,13) -> (1,13), + }, + }, + ], + closeBrace: RBrace (1,14) -> (1,14), + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects known with list: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects known with list: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects known: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects known: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects multiple 1`] = ` +Score (1,1) -> (1,19) { + bars: Array [ + Bar (1,1) -> (1,19) { + beats: Array [ + Beat (1,1) -> (1,19) { + notes: NoteList (1,1) -> (1,19) { + notes: Array [ + Note (1,1) -> (1,19) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,19) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [ + Prop (1,5) -> (1,7) { + property: Ident "nh" (1,5) -> (1,7), + }, + Prop (1,8) -> (1,16) { + property: Ident "b" (1,8) -> (1,9), + properties: Values (1,10) -> (1,16) { + openParenthesis: LParen (1,10) -> (1,10), + values: Array [ + Number "0" (1,11) -> (1,12), + Number "4" (1,13) -> (1,14), + Number "0" (1,15) -> (1,16), + ], + closeParenthesis: RParen (1,16) -> (1,16), + }, + }, + Prop (1,18) -> (1,19) { + property: Ident "v" (1,18) -> (1,19), + }, + ], + closeBrace: RBrace (1,19) -> (1,19), + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects multiple chord 1`] = ` +Score (1,1) -> (1,47) { + bars: Array [ + Bar (1,1) -> (1,47) { + beats: Array [ + Beat (1,1) -> (1,47) { + notes: NoteList (1,1) -> (1,47) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,20) { + noteValue: Ident "C4" (1,2) -> (1,4), + noteEffects: Props (1,5) -> (1,20) { + openBrace: LBrace (1,5) -> (1,5), + properties: Array [ + Prop (1,6) -> (1,8) { + property: Ident "nh" (1,6) -> (1,8), + }, + Prop (1,9) -> (1,17) { + property: Ident "b" (1,9) -> (1,10), + properties: Values (1,11) -> (1,17) { + openParenthesis: LParen (1,11) -> (1,11), + values: Array [ + Number "0" (1,12) -> (1,13), + Number "4" (1,14) -> (1,15), + Number "0" (1,16) -> (1,17), + ], + closeParenthesis: RParen (1,17) -> (1,17), + }, + }, + Prop (1,19) -> (1,20) { + property: Ident "v" (1,19) -> (1,20), + }, + ], + closeBrace: RBrace (1,20) -> (1,20), + }, + }, + Note (1,22) -> (1,46) { + noteValue: Ident "C5" (1,22) -> (1,24), + noteEffects: Props (1,25) -> (1,46) { + openBrace: LBrace (1,25) -> (1,25), + properties: Array [ + Prop (1,27) -> (1,28) { + property: Ident "v" (1,27) -> (1,28), + }, + Prop (1,29) -> (1,30) { + property: Ident "h" (1,29) -> (1,30), + }, + Prop (1,31) -> (1,45) { + property: Ident "unknown" (1,31) -> (1,38), + properties: Values (1,39) -> (1,45) { + openParenthesis: LParen (1,39) -> (1,39), + values: Array [ + Number "1" (1,40) -> (1,41), + Number "2" (1,42) -> (1,43), + Number "3" (1,44) -> (1,45), + ], + closeParenthesis: RParen (1,45) -> (1,45), + }, + }, + ], + closeBrace: RBrace (1,46) -> (1,46), + }, + }, + ], + closeParenthesis: RParen (1,47) -> (1,47), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects multiple chord beat effects 1`] = ` +Score (1,1) -> (1,58) { + bars: Array [ + Bar (1,1) -> (1,58) { + beats: Array [ + Beat (1,1) -> (1,58) { + notes: NoteList (1,1) -> (1,47) { + openParenthesis: LParen (1,1) -> (1,1), + notes: Array [ + Note (1,2) -> (1,20) { + noteValue: Ident "C4" (1,2) -> (1,4), + noteEffects: Props (1,5) -> (1,20) { + openBrace: LBrace (1,5) -> (1,5), + properties: Array [ + Prop (1,6) -> (1,8) { + property: Ident "nh" (1,6) -> (1,8), + }, + Prop (1,9) -> (1,17) { + property: Ident "b" (1,9) -> (1,10), + properties: Values (1,11) -> (1,17) { + openParenthesis: LParen (1,11) -> (1,11), + values: Array [ + Number "0" (1,12) -> (1,13), + Number "4" (1,14) -> (1,15), + Number "0" (1,16) -> (1,17), + ], + closeParenthesis: RParen (1,17) -> (1,17), + }, + }, + Prop (1,19) -> (1,20) { + property: Ident "v" (1,19) -> (1,20), + }, + ], + closeBrace: RBrace (1,20) -> (1,20), + }, + }, + Note (1,22) -> (1,46) { + noteValue: Ident "C5" (1,22) -> (1,24), + noteEffects: Props (1,25) -> (1,46) { + openBrace: LBrace (1,25) -> (1,25), + properties: Array [ + Prop (1,27) -> (1,28) { + property: Ident "v" (1,27) -> (1,28), + }, + Prop (1,29) -> (1,30) { + property: Ident "h" (1,29) -> (1,30), + }, + Prop (1,31) -> (1,45) { + property: Ident "unknown" (1,31) -> (1,38), + properties: Values (1,39) -> (1,45) { + openParenthesis: LParen (1,39) -> (1,39), + values: Array [ + Number "1" (1,40) -> (1,41), + Number "2" (1,42) -> (1,43), + Number "3" (1,44) -> (1,45), + ], + closeParenthesis: RParen (1,45) -> (1,45), + }, + }, + ], + closeBrace: RBrace (1,46) -> (1,46), + }, + }, + ], + closeParenthesis: RParen (1,47) -> (1,47), + }, + beatEffects: Props (1,49) -> (1,58) { + openBrace: LBrace (1,49) -> (1,49), + properties: Array [ + Prop (1,51) -> (1,57) { + property: Ident "tu" (1,51) -> (1,53), + properties: Values (1,54) -> (1,57) { + values: Array [ + Number "3" (1,54) -> (1,55), + Number "2" (1,56) -> (1,57), + ], + }, + }, + ], + closeBrace: RBrace (1,58) -> (1,58), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects multiple chord beat effects: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects multiple chord beat effects: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects multiple chord: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects multiple chord: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects multiple: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects multiple: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects unknown 1`] = ` +Score (1,1) -> (1,20) { + bars: Array [ + Bar (1,1) -> (1,20) { + beats: Array [ + Beat (1,1) -> (1,20) { + notes: NoteList (1,1) -> (1,20) { + notes: Array [ + Note (1,1) -> (1,20) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,20) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [ + Prop (1,5) -> (1,19) { + property: Ident "unknown" (1,5) -> (1,12), + properties: Values (1,13) -> (1,19) { + openParenthesis: LParen (1,13) -> (1,13), + values: Array [ + Number "1" (1,14) -> (1,15), + Number "2" (1,16) -> (1,17), + Number "3" (1,18) -> (1,19), + ], + closeParenthesis: RParen (1,19) -> (1,19), + }, + }, + ], + closeBrace: RBrace (1,20) -> (1,20), + }, + }, + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects unknown: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects unknown: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects with beat effects 1`] = ` +Score (1,1) -> (1,30) { + bars: Array [ + Bar (1,1) -> (1,30) { + beats: Array [ + Beat (1,1) -> (1,30) { + notes: NoteList (1,1) -> (1,19) { + notes: Array [ + Note (1,1) -> (1,19) { + noteValue: Ident "C4" (1,1) -> (1,3), + noteEffects: Props (1,4) -> (1,19) { + openBrace: LBrace (1,4) -> (1,4), + properties: Array [ + Prop (1,5) -> (1,7) { + property: Ident "nh" (1,5) -> (1,7), + }, + Prop (1,8) -> (1,16) { + property: Ident "b" (1,8) -> (1,9), + properties: Values (1,10) -> (1,16) { + openParenthesis: LParen (1,10) -> (1,10), + values: Array [ + Number "0" (1,11) -> (1,12), + Number "4" (1,13) -> (1,14), + Number "0" (1,15) -> (1,16), + ], + closeParenthesis: RParen (1,16) -> (1,16), + }, + }, + Prop (1,18) -> (1,19) { + property: Ident "v" (1,18) -> (1,19), + }, + ], + closeBrace: RBrace (1,19) -> (1,19), + }, + }, + ], + }, + beatEffects: Props (1,21) -> (1,30) { + openBrace: LBrace (1,21) -> (1,21), + properties: Array [ + Prop (1,23) -> (1,29) { + property: Ident "tu" (1,23) -> (1,25), + properties: Values (1,26) -> (1,29) { + values: Array [ + Number "3" (1,26) -> (1,27), + Number "2" (1,28) -> (1,29), + ], + }, + }, + ], + closeBrace: RBrace (1,30) -> (1,30), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-note-effects with beat effects: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-note-effects with beat effects: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata empty 1`] = `Score (1,1) -> (1,2)`; + +exports[`AlphaTexParserTest valid-score-metadata empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known multiple 1`] = ` +Score (1,1) -> (1,32) { + bars: Array [ + Bar (1,1) -> (1,32) { + metaData: Array [ + Meta (1,1) -> (1,14) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,14) { + values: Array [ + String "Title" (1,8) -> (1,14), + ], + }, + }, + Meta (1,16) -> (1,30) { + tag: Tag "subtitle" (1,16) -> (1,25) { + prefix: Backslash (1,16) -> (1,17), + tag: Ident "subtitle" (1,17) -> (1,25), + }, + values: Values (1,26) -> (1,30) { + values: Array [ + String "Sub" (1,26) -> (1,30), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata known multiple: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known multiple: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known property 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,29) { + metaData: Array [ + Meta (1,1) -> (1,27) { + tag: Tag "track" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "track" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,13) { + values: Array [ + String "Name" (1,8) -> (1,13), + ], + }, + properties: Props (1,15) -> (1,27) { + openBrace: LBrace (1,15) -> (1,15), + properties: Array [ + Prop (1,16) -> (1,26) { + property: Ident "color" (1,16) -> (1,21), + properties: Values (1,22) -> (1,26) { + values: Array [ + String "red" (1,22) -> (1,26), + ], + }, + }, + ], + closeBrace: RBrace (1,27) -> (1,27), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata known property before value 1`] = ` +Score (1,1) -> (1,29) { + bars: Array [ + Bar (1,1) -> (1,29) { + metaData: Array [ + Meta (1,1) -> (1,27) { + tag: Tag "track" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "track" (1,2) -> (1,7), + }, + values: Values (1,22) -> (1,27) { + values: Array [ + String "Name" (1,22) -> (1,27), + ], + }, + properties: Props (1,8) -> (1,20) { + openBrace: LBrace (1,8) -> (1,8), + properties: Array [ + Prop (1,9) -> (1,19) { + property: Ident "color" (1,9) -> (1,14), + properties: Values (1,15) -> (1,19) { + values: Array [ + String "red" (1,15) -> (1,19), + ], + }, + }, + ], + closeBrace: RBrace (1,20) -> (1,20), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata known property before value: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known property before value: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known property: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known property: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known semantic 1`] = ` +Score (1,1) -> (1,32) { + bars: Array [ + Bar (1,1) -> (1,32) { + metaData: Array [ + Meta (1,1) -> (1,31) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,31) { + values: Array [ + String "Title" (1,8) -> (1,14), + String "Template" (1,16) -> (1,25), + Ident "left" (1,27) -> (1,31), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata known semantic: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known semantic: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known valuelist 1`] = ` +Score (1,1) -> (1,18) { + bars: Array [ + Bar (1,1) -> (1,18) { + metaData: Array [ + Meta (1,1) -> (1,16) { + tag: Tag "title" (1,1) -> (1,7) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "title" (1,2) -> (1,7), + }, + values: Values (1,8) -> (1,16) { + openParenthesis: LParen (1,8) -> (1,8), + values: Array [ + String "Title" (1,9) -> (1,15), + ], + closeParenthesis: RParen (1,16) -> (1,16), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata known valuelist: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata known valuelist: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata unknown multiple 1`] = ` +Score (1,1) -> (1,42) { + bars: Array [ + Bar (1,1) -> (1,42) { + metaData: Array [ + Meta (1,1) -> (1,22) { + tag: Tag "notExisting" (1,1) -> (1,13) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "notExisting" (1,2) -> (1,13), + }, + values: Values (1,14) -> (1,22) { + openParenthesis: LParen (1,14) -> (1,14), + values: Array [ + String "Value" (1,15) -> (1,21), + ], + closeParenthesis: RParen (1,22) -> (1,22), + }, + }, + Meta (1,24) -> (1,40) { + tag: Tag "notExisting" (1,24) -> (1,36) { + prefix: Backslash (1,24) -> (1,25), + tag: Ident "notExisting" (1,25) -> (1,36), + }, + values: Values (1,37) -> (1,40) { + openParenthesis: LParen (1,37) -> (1,37), + values: Array [ + String "" (1,38) -> (1,39), + ], + closeParenthesis: RParen (1,40) -> (1,40), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata unknown multiple: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata unknown multiple: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata unknown valuelist 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,1) -> (1,24) { + metaData: Array [ + Meta (1,1) -> (1,22) { + tag: Tag "notExisting" (1,1) -> (1,13) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "notExisting" (1,2) -> (1,13), + }, + values: Values (1,14) -> (1,22) { + openParenthesis: LParen (1,14) -> (1,14), + values: Array [ + String "Value" (1,15) -> (1,21), + ], + closeParenthesis: RParen (1,22) -> (1,22), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata unknown valuelist: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata unknown valuelist: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata valuelist propertylist empty 1`] = ` +Score (1,1) -> (1,27) { + bars: Array [ + Bar (1,1) -> (1,27) { + metaData: Array [ + Meta (1,1) -> (1,25) { + tag: Tag "notExisting" (1,1) -> (1,13) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "notExisting" (1,2) -> (1,13), + }, + values: Values (1,14) -> (1,22) { + openParenthesis: LParen (1,14) -> (1,14), + values: Array [ + String "Value" (1,15) -> (1,21), + ], + closeParenthesis: RParen (1,22) -> (1,22), + }, + properties: Props (1,24) -> (1,25) { + openBrace: LBrace (1,24) -> (1,24), + properties: Array [], + closeBrace: RBrace (1,25) -> (1,25), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata valuelist propertylist empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata valuelist propertylist empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata valuelist propertylist unknown prop 1`] = ` +Score (1,1) -> (1,42) { + bars: Array [ + Bar (1,1) -> (1,42) { + metaData: Array [ + Meta (1,1) -> (1,40) { + tag: Tag "notExisting" (1,1) -> (1,13) { + prefix: Backslash (1,1) -> (1,2), + tag: Ident "notExisting" (1,2) -> (1,13), + }, + values: Values (1,14) -> (1,22) { + openParenthesis: LParen (1,14) -> (1,14), + values: Array [ + String "Value" (1,15) -> (1,21), + ], + closeParenthesis: RParen (1,22) -> (1,22), + }, + properties: Props (1,24) -> (1,40) { + openBrace: LBrace (1,24) -> (1,24), + properties: Array [ + Prop (1,25) -> (1,39) { + property: Ident "unknown" (1,25) -> (1,32), + properties: Values (1,33) -> (1,39) { + openParenthesis: LParen (1,33) -> (1,33), + values: Array [ + Number "1" (1,34) -> (1,35), + Number "2" (1,36) -> (1,37), + Number "3" (1,38) -> (1,39), + ], + closeParenthesis: RParen (1,39) -> (1,39), + }, + }, + ], + closeBrace: RBrace (1,40) -> (1,40), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-score-metadata valuelist propertylist unknown prop: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-score-metadata valuelist propertylist unknown prop: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points basic fretted 1`] = ` +Score (1,1) -> (1,21) { + bars: Array [ + Bar (1,2) -> (1,8) { + beats: Array [ + Beat (1,4) -> (1,8) { + notes: NoteList (1,4) -> (1,7) { + notes: Array [ + Note (1,4) -> (1,7) { + noteValue: Number "3" (1,4) -> (1,5), + noteStringDot: Dot (1,5) -> (1,5), + noteString: Number "3" (1,6) -> (1,7), + }, + ], + }, + }, + ], + }, + Bar (1,10) -> (1,21) { + metaData: Array [ + Meta (1,10) -> (1,21) { + tag: Tag "sync" (1,10) -> (1,15) { + prefix: Backslash (1,10) -> (1,11), + tag: Ident "sync" (1,11) -> (1,15), + }, + values: Values (1,16) -> (1,21) { + values: Array [ + Number "1" (1,16) -> (1,17), + Number "1" (1,18) -> (1,19), + Number "1" (1,20) -> (1,21), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points basic fretted: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points basic fretted: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points basic simple numbered 1`] = ` +Score (1,1) -> (1,20) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,7) { + notes: NoteList (1,4) -> (1,7) { + notes: Array [ + Note (1,4) -> (1,7) { + noteValue: Number "32" (1,4) -> (1,6), + }, + ], + }, + }, + ], + }, + Bar (1,9) -> (1,20) { + metaData: Array [ + Meta (1,9) -> (1,20) { + tag: Tag "sync" (1,9) -> (1,14) { + prefix: Backslash (1,9) -> (1,10), + tag: Ident "sync" (1,10) -> (1,14), + }, + values: Values (1,15) -> (1,20) { + values: Array [ + Number "1" (1,15) -> (1,16), + Number "1" (1,17) -> (1,18), + Number "1" (1,19) -> (1,20), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points basic simple numbered: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points basic simple numbered: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points basic simple pitched 1`] = ` +Score (1,1) -> (1,20) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,7) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + ], + }, + Bar (1,9) -> (1,20) { + metaData: Array [ + Meta (1,9) -> (1,20) { + tag: Tag "sync" (1,9) -> (1,14) { + prefix: Backslash (1,9) -> (1,10), + tag: Ident "sync" (1,10) -> (1,14), + }, + values: Values (1,15) -> (1,20) { + values: Array [ + Number "1" (1,15) -> (1,16), + Number "1" (1,17) -> (1,18), + Number "1" (1,19) -> (1,20), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points basic simple pitched: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points basic simple pitched: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points empty 1`] = `Score (1,1) -> (1,4)`; + +exports[`AlphaTexParserTest valid-sync-points empty with bars 1`] = ` +Score (1,1) -> (1,12) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,6) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + ], + pipe: Pipe (1,7) -> (1,7), + }, + Bar (1,9) -> (1,12) { + beats: Array [ + Beat (1,9) -> (1,12) { + notes: NoteList (1,9) -> (1,11) { + notes: Array [ + Note (1,9) -> (1,11) { + noteValue: Ident "C5" (1,9) -> (1,11), + }, + ], + }, + durationDot: Dot (1,12) -> (1,12), + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points empty with bars empty at end 1`] = ` +Score (1,1) -> (1,14) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,6) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + ], + pipe: Pipe (1,7) -> (1,7), + }, + Bar (1,9) -> (1,12) { + beats: Array [ + Beat (1,9) -> (1,11) { + notes: NoteList (1,9) -> (1,11) { + notes: Array [ + Note (1,9) -> (1,11) { + noteValue: Ident "C5" (1,9) -> (1,11), + }, + ], + }, + }, + ], + pipe: Pipe (1,12) -> (1,12), + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points empty with bars empty at end: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points empty with bars empty at end: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points empty with bars: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points empty with bars: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points full 1`] = ` +Score (1,1) -> (1,36) { + bars: Array [ + Bar (1,2) -> (1,22) { + metaData: Array [ + Meta (1,2) -> (1,14) { + tag: Tag "title" (1,2) -> (1,8) { + prefix: Backslash (1,2) -> (1,3), + tag: Ident "title" (1,3) -> (1,8), + }, + values: Values (1,9) -> (1,14) { + values: Array [ + String "Test" (1,9) -> (1,14), + ], + }, + }, + ], + beats: Array [ + Beat (1,18) -> (1,22) { + notes: NoteList (1,18) -> (1,20) { + notes: Array [ + Note (1,18) -> (1,20) { + noteValue: Ident "C4" (1,18) -> (1,20), + }, + ], + }, + durationDot: Dot (1,20) -> (1,20), + durationValue: Number "4" (1,21) -> (1,22), + }, + ], + }, + Bar (1,23) -> (1,36) { + metaData: Array [ + Meta (1,25) -> (1,36) { + tag: Tag "sync" (1,25) -> (1,30) { + prefix: Backslash (1,25) -> (1,26), + tag: Ident "sync" (1,26) -> (1,30), + }, + values: Values (1,31) -> (1,36) { + values: Array [ + Number "1" (1,31) -> (1,32), + Number "1" (1,33) -> (1,34), + Number "1" (1,35) -> (1,36), + ], + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points full: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points full: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points properties empty 1`] = ` +Score (1,1) -> (1,24) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,7) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + ], + }, + Bar (1,9) -> (1,24) { + metaData: Array [ + Meta (1,9) -> (1,24) { + tag: Tag "sync" (1,9) -> (1,14) { + prefix: Backslash (1,9) -> (1,10), + tag: Ident "sync" (1,10) -> (1,14), + }, + values: Values (1,15) -> (1,21) { + openParenthesis: LParen (1,15) -> (1,15), + values: Array [ + Number "1" (1,16) -> (1,17), + Number "1" (1,18) -> (1,19), + Number "1" (1,20) -> (1,21), + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + properties: Props (1,23) -> (1,24) { + openBrace: LBrace (1,23) -> (1,23), + properties: Array [], + closeBrace: RBrace (1,24) -> (1,24), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points properties empty: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points properties empty: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points properties unknown 1`] = ` +Score (1,1) -> (1,41) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,7) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + ], + }, + Bar (1,9) -> (1,41) { + metaData: Array [ + Meta (1,9) -> (1,41) { + tag: Tag "sync" (1,9) -> (1,14) { + prefix: Backslash (1,9) -> (1,10), + tag: Ident "sync" (1,10) -> (1,14), + }, + values: Values (1,15) -> (1,21) { + openParenthesis: LParen (1,15) -> (1,15), + values: Array [ + Number "1" (1,16) -> (1,17), + Number "1" (1,18) -> (1,19), + Number "1" (1,20) -> (1,21), + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + properties: Props (1,23) -> (1,41) { + openBrace: LBrace (1,23) -> (1,23), + properties: Array [ + Prop (1,25) -> (1,39) { + property: Ident "unknown" (1,25) -> (1,32), + properties: Values (1,33) -> (1,39) { + openParenthesis: LParen (1,33) -> (1,33), + values: Array [ + Number "1" (1,34) -> (1,35), + Number "2" (1,36) -> (1,37), + Number "3" (1,38) -> (1,39), + ], + closeParenthesis: RParen (1,39) -> (1,39), + }, + }, + ], + closeBrace: RBrace (1,41) -> (1,41), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points properties unknown: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points properties unknown: parser-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points valuelist 1`] = ` +Score (1,1) -> (1,21) { + bars: Array [ + Bar (1,2) -> (1,7) { + beats: Array [ + Beat (1,4) -> (1,7) { + notes: NoteList (1,4) -> (1,6) { + notes: Array [ + Note (1,4) -> (1,6) { + noteValue: Ident "C4" (1,4) -> (1,6), + }, + ], + }, + }, + ], + }, + Bar (1,9) -> (1,21) { + metaData: Array [ + Meta (1,9) -> (1,21) { + tag: Tag "sync" (1,9) -> (1,14) { + prefix: Backslash (1,9) -> (1,10), + tag: Ident "sync" (1,10) -> (1,14), + }, + values: Values (1,15) -> (1,21) { + openParenthesis: LParen (1,15) -> (1,15), + values: Array [ + Number "1" (1,16) -> (1,17), + Number "1" (1,18) -> (1,19), + Number "1" (1,20) -> (1,21), + ], + closeParenthesis: RParen (1,21) -> (1,21), + }, + }, + ], + }, + ], +} +`; + +exports[`AlphaTexParserTest valid-sync-points valuelist: lexer-diagnostics 1`] = `Array []`; + +exports[`AlphaTexParserTest valid-sync-points valuelist: parser-diagnostics 1`] = `Array []`; diff --git a/packages/alphatab/test/mocha.jest-snapshot.ts b/packages/alphatab/test/mocha.jest-snapshot.ts index ae8fd53de..7248c866e 100644 --- a/packages/alphatab/test/mocha.jest-snapshot.ts +++ b/packages/alphatab/test/mocha.jest-snapshot.ts @@ -16,6 +16,8 @@ import type { SyncExpectationResult } from 'expect'; import { equals, iterableEquality, subsetEquality } from '@jest/expect-utils'; import * as matcherUtils from 'jest-matcher-utils'; import { + AlphaTexAstNodePlugin, + AlphaTexDiagnosticPlugin, MidiEventSerializerPlugin, type PrettyFormatConfig, type PrettyFormatPrinter, @@ -66,6 +68,38 @@ export async function initializeJestSnapshot() { ); } }); + addSerializer({ + test(val) { + return AlphaTexDiagnosticPlugin.instance.test(val); + }, + serialize(val, config, indentation, depth, refs, printer) { + // + return AlphaTexDiagnosticPlugin.instance.serialize( + val, + config as PrettyFormatConfig, + indentation, + depth, + refs, + printer as PrettyFormatPrinter + ); + } + }); + addSerializer({ + test(val) { + return AlphaTexAstNodePlugin.instance.test(val); + }, + serialize(val, config, indentation, depth, refs, printer) { + // + return AlphaTexAstNodePlugin.instance.serialize( + val, + config as PrettyFormatConfig, + indentation, + depth, + refs, + printer as PrettyFormatPrinter + ); + } + }); currentResolver = await buildSnapshotResolver(globalConfig); diff --git a/packages/alphatab/test/model/ModelUtils.test.ts b/packages/alphatab/test/model/ModelUtils.test.ts index 2aae7ecbe..6b98caf35 100644 --- a/packages/alphatab/test/model/ModelUtils.test.ts +++ b/packages/alphatab/test/model/ModelUtils.test.ts @@ -1,13 +1,11 @@ -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; -import { Settings } from '@src/Settings'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; import { expect } from 'chai'; describe('ModelUtilsTests', () => { function trimTest(tex: string, expectedBars: number) { - const importer = new AlphaTexImporter(); - importer.initFromString(tex, new Settings()); - - const score = importer.readScore(); + const score = ScoreLoader.loadAlphaTex(tex); expect(score.masterBars.length).to.equal(expectedBars); } @@ -22,10 +20,10 @@ describe('ModelUtilsTests', () => { it('trimEmptyBarsAtEndMultiTrackMixed', () => { trimTest( ` - \\track T1 + \\track "T1" C4 | C4 | C4 | C4 | | | | - \\track T2 - C4 | C4 | C4 | C4 | C4 | | | + \\track "T2" + C4 | C4 | C4 | C4 | C4 | | | `, 5 ); @@ -39,4 +37,45 @@ describe('ModelUtilsTests', () => { 4 ); }); + + describe('parseTuning', () => { + function test(s:string, toneValue:number, accidental:NoteAccidentalMode, octave:number) { + const result = ModelUtils.parseTuning(s); + expect(result).to.be.ok; + expect(result!.tone.noteValue).to.equal(toneValue); + expect(result!.tone.accidentalMode).to.equal(accidental); + expect(result!.octave).to.equal(octave); + } + + it('octave-c', () => test('C4', 0, NoteAccidentalMode.Default, 5)) + it('octave-d', () => test('D4', 2, NoteAccidentalMode.Default, 5)) + it('octave-e', () => test('E4', 4, NoteAccidentalMode.Default, 5)) + it('octave-f', () => test('F4', 5, NoteAccidentalMode.Default, 5)) + it('octave-g', () => test('G4', 7, NoteAccidentalMode.Default, 5)) + it('octave-a', () => test('A4', 9, NoteAccidentalMode.Default, 5)) + it('octave-b', () => test('B4', 11, NoteAccidentalMode.Default, 5)) + + it('c-forceNone-long', () => test('CforceNone4', 0, NoteAccidentalMode.ForceNone, 5)) + it('c-forceNatural-long', () => test('CforceNatural4', 0, NoteAccidentalMode.ForceNatural, 5)) + it('c-forceSharp-long', () => test('CforceSharp4', 1, NoteAccidentalMode.ForceSharp, 5)) + it('c-forceDoubleSharp-long', () => test('CforceDoubleSharp4', 2, NoteAccidentalMode.ForceDoubleSharp, 5)) + it('c-forceFlat-long', () => test('CforceFlat4', 11, NoteAccidentalMode.ForceFlat, 4)) + it('c-forceDoubleFlat-long', () => test('CforceDoubleFlat4', 10, NoteAccidentalMode.ForceDoubleFlat, 4)) + + it('d-forceNone-long', () => test('DforceNone4', 2, NoteAccidentalMode.ForceNone, 5)) + it('d-forceNatural-long', () => test('DforceNatural4', 2, NoteAccidentalMode.ForceNatural, 5)) + it('d-forceSharp-long', () => test('DforceSharp4', 3, NoteAccidentalMode.ForceSharp, 5)) + it('d-forceDoubleSharp-long', () => test('DforceDoubleSharp4', 4, NoteAccidentalMode.ForceDoubleSharp, 5)) + it('d-forceFlat-long', () => test('DforceFlat4', 1, NoteAccidentalMode.ForceFlat, 5)) + it('d-forceDoubleFlat-long', () => test('DforceDoubleFlat4', 0, NoteAccidentalMode.ForceDoubleFlat, 5)) + + + it('c-forceNone-short', () => test('C-4', 0, NoteAccidentalMode.ForceNone, 5)) + it('c-forceNatural-short', () => test('Cn4', 0, NoteAccidentalMode.ForceNatural, 5)) + it('c-forceSharp-short', () => test('C#4', 1, NoteAccidentalMode.ForceSharp, 5)) + it('c-forceDoubleSharp-short1', () => test('C##4', 2, NoteAccidentalMode.ForceDoubleSharp, 5)) + it('c-forceDoubleSharp-short2', () => test('Cx4', 2, NoteAccidentalMode.ForceDoubleSharp, 5)) + it('c-forceFlat-short', () => test('Cb4', 11, NoteAccidentalMode.ForceFlat, 4)) + it('c-forceDoubleFlat-short', () => test('Cbb4', 10, NoteAccidentalMode.ForceDoubleFlat, 4)) + }); }); diff --git a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts index 55730c30b..40ef888c2 100644 --- a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -1,7 +1,5 @@ import { SystemsLayoutMode } from '@src/DisplaySettings'; import { ScoreLoader } from '@src/importer/ScoreLoader'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; -import { ByteBuffer } from '@src/io/ByteBuffer'; import { BeatBarreEffectInfo } from '@src/rendering/effects/BeatBarreEffectInfo'; import { Settings } from '@src/Settings'; import { TestPlatform } from '@test/TestPlatform'; @@ -98,9 +96,7 @@ describe('EffectsAndAnnotationsTests', () => { const settings = new Settings(); settings.display.barsPerRow = 1; - const importer = new AlphaTexImporter(); - importer.init(ByteBuffer.fromString(tex), settings); - const score = importer.readScore(); + const score = ScoreLoader.loadAlphaTex(tex); await VisualTestHelper.runVisualTestFull( new VisualTestOptions( @@ -168,11 +164,10 @@ describe('EffectsAndAnnotationsTests', () => { }); it('sustain-pedal-alphatex', async () => { - const importer = new AlphaTexImporter(); const settings = new Settings(); - importer.initFromString( + const score = ScoreLoader.loadAlphaTex( ` - . + \\tempo 120 \\track "pno." :8 G4 { spd } G4 G4 { spu } G4 G4 { spd } G4 {spu} G4 G4 {spd} | G4 { spu } G4 G4 G4 G4 G4 G4 G4 | @@ -183,7 +178,6 @@ describe('EffectsAndAnnotationsTests', () => { `, settings ); - const score = importer.readScore(); score.stylesheet.hideDynamics = true; expect(score.tracks[0].staves[0].displayTranspositionPitch).to.equal(0); diff --git a/packages/alphatab/test/visualTests/features/MusicNotation.test.ts b/packages/alphatab/test/visualTests/features/MusicNotation.test.ts index fa2082a20..8ee11e995 100644 --- a/packages/alphatab/test/visualTests/features/MusicNotation.test.ts +++ b/packages/alphatab/test/visualTests/features/MusicNotation.test.ts @@ -1,9 +1,9 @@ import { LayoutMode } from '@src/LayoutMode'; -import { StaveProfile } from '@src/StaveProfile'; +import { NotationElement } from '@src/NotationSettings'; import { Settings } from '@src/Settings'; +import { StaveProfile } from '@src/StaveProfile'; +import { ScoreLoader } from '@src/importer/ScoreLoader'; import { VisualTestHelper, VisualTestOptions, VisualTestRun } from '@test/visualTests/VisualTestHelper'; -import { NotationElement } from '@src/NotationSettings'; -import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; describe('MusicNotationTests', () => { it('clefs', async () => { @@ -165,9 +165,7 @@ describe('MusicNotationTests', () => { const settings = new Settings(); settings.display.barsPerRow = 5; - const importer = new AlphaTexImporter(); - importer.initFromString(tex, settings); - const score = importer.readScore(); + const score = ScoreLoader.loadAlphaTex(tex, settings); score.tracks[0].shortName = 'pno.'; score.stylesheet.hideDynamics = true; diff --git a/packages/alphatab/vite.config.ts b/packages/alphatab/vite.config.ts index 428b6f011..0033f454b 100644 --- a/packages/alphatab/vite.config.ts +++ b/packages/alphatab/vite.config.ts @@ -43,10 +43,6 @@ export default defineConfig(({ mode }) => { ); const lib = config.build!.lib! as LibraryOptions; - lib.entry['alphatab.core'] = path.resolve(__dirname, 'src/alphaTab.core.ts'); - lib.entry['alphatab'] = path.resolve(__dirname, 'src/alphaTab.main.ts'); - lib.entry['alphatab.worker'] = path.resolve(__dirname, 'src/alphaTab.worker.ts'); - lib.entry['alphatab.worklet'] = path.resolve(__dirname, 'src/AlphaTab.worklet.ts'); lib.name = 'alphaTab'; const typeScriptOptions = (): Partial => { @@ -82,10 +78,11 @@ export default defineConfig(({ mode }) => { //case 'esm': default: esm('alphaTab', 'src/alphaTab.main.ts'); - lib.entry['alphaTab.core'] = path.resolve(__dirname, 'src/alphaTab.core.ts'); - lib.entry['alphaTab.worker'] = path.resolve(__dirname, 'src/alphaTab.worker.ts'); - lib.entry['alphaTab.worklet'] = path.resolve(__dirname, 'src/alphaTab.worklet.ts'); - + const entry = lib.entry! as Record; + entry['alphaTab.core'] = path.resolve(__dirname, 'src/alphaTab.core.ts'); + entry['alphaTab.worker'] = path.resolve(__dirname, 'src/alphaTab.worker.ts'); + entry['alphaTab.worklet'] = path.resolve(__dirname, 'src/alphaTab.worklet.ts'); + for (const output of config.build!.rollupOptions!.output as OutputOptions[]) { const isMin = (output.entryFileNames as string).includes('.min'); (output.plugins as Plugin[]).push(adjustScriptPathsPlugin(isMin)); diff --git a/packages/csharp/src/AlphaTab.Test/AlphaTexAstNodePlugin.cs b/packages/csharp/src/AlphaTab.Test/AlphaTexAstNodePlugin.cs new file mode 100644 index 000000000..dd3b94e1a --- /dev/null +++ b/packages/csharp/src/AlphaTab.Test/AlphaTexAstNodePlugin.cs @@ -0,0 +1,19 @@ +using AlphaTab.Importer.AlphaTex; + +namespace AlphaTab; + +internal partial class AlphaTexAstNodePlugin +{ + public bool Test(object? arg0) + { + return arg0 is AlphaTexAstNode; + } +} + +internal partial class AlphaTexDiagnosticPlugin +{ + public bool Test(object? arg0) + { + return arg0 is AlphaTexDiagnostic; + } +} diff --git a/packages/csharp/src/AlphaTab/Collections/ValueTypeMap.cs b/packages/csharp/src/AlphaTab/Collections/ValueTypeMap.cs index b03bcfe1e..e0ecca46d 100644 --- a/packages/csharp/src/AlphaTab/Collections/ValueTypeMap.cs +++ b/packages/csharp/src/AlphaTab/Collections/ValueTypeMap.cs @@ -50,6 +50,14 @@ public ValueTypeMap(IEnumerable> entries) } } + internal ValueTypeMap(IEnumerator> entries) + { + foreach (var entry in entries) + { + this[entry.V0] = entry.V1; + } + } + public ValueTypeMap(IEnumerable> entries) { foreach (var entry in entries) diff --git a/packages/csharp/src/AlphaTab/Core/IRecord.cs b/packages/csharp/src/AlphaTab/Core/IRecord.cs index 4d09d42b7..6c96edf59 100644 --- a/packages/csharp/src/AlphaTab/Core/IRecord.cs +++ b/packages/csharp/src/AlphaTab/Core/IRecord.cs @@ -7,4 +7,4 @@ namespace AlphaTab.Core; public interface IRecord { -} \ No newline at end of file +} diff --git a/packages/csharp/src/AlphaTab/Importer/AlphaTex/AlphaTex1EnumMappings.cs b/packages/csharp/src/AlphaTab/Importer/AlphaTex/AlphaTex1EnumMappings.cs new file mode 100644 index 000000000..47632d2d2 --- /dev/null +++ b/packages/csharp/src/AlphaTab/Importer/AlphaTex/AlphaTex1EnumMappings.cs @@ -0,0 +1,10 @@ +namespace AlphaTab.Importer.AlphaTex; + +partial class AlphaTex1EnumMappings +{ + private static T _toEnum(object? type, double value) + { + // Unsafe.BitCast in future + return (T)(object)(int)value; + } +} diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/IAlphaTabEnum.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/IAlphaTabEnum.kt index 00a234d9b..3193c82f9 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/IAlphaTabEnum.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/IAlphaTabEnum.kt @@ -4,6 +4,7 @@ interface IAlphaTabEnum { val value: Int } + interface IAlphaTabEnumCompanion { val values:Array fun fromValue(value:Double): T diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/importer/alphaTex/AlphaTex1EnumMappingsPartials.kt b/packages/kotlin/src/android/src/main/java/alphaTab/importer/alphaTex/AlphaTex1EnumMappingsPartials.kt new file mode 100644 index 000000000..9850424b4 --- /dev/null +++ b/packages/kotlin/src/android/src/main/java/alphaTab/importer/alphaTex/AlphaTex1EnumMappingsPartials.kt @@ -0,0 +1,32 @@ +package alphaTab.importer.alphaTex + +import alphaTab.core.IAlphaTabEnum +import alphaTab.core.IAlphaTabEnumCompanion + +internal class AlphaTex1EnumMappingsPartials { + companion object { + val enumFactory = HashMap, (v: Double) -> IAlphaTabEnum>() + + fun _toEnum(type: Any?, value: Double): T + where T : IAlphaTabEnum { + + val clz = type as kotlin.reflect.KClass<*> + val factory = enumFactory.getOrPut(clz, { + + val companionField = clz.java.declaredFields.find { it.name === "Companion" } + if (companionField == null) { + throw IllegalArgumentException("Provided class has no companion object") + } + + val companion = companionField.get(null) as IAlphaTabEnumCompanion<*> + val factory: (v: Double) -> IAlphaTabEnum = { v -> + companion.fromValue(v) as IAlphaTabEnum + } + factory + }) + + @Suppress("UNCHECKED_CAST") + return factory(value) as T + } + } +} diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt b/packages/kotlin/src/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt index d60f9e026..625cc55f2 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/ScoreSerializerPluginPartials.kt @@ -2,6 +2,8 @@ package alphaTab import alphaTab.collections.BooleanList import alphaTab.collections.DoubleList +import alphaTab.importer.alphaTex.AlphaTexAstNode +import alphaTab.importer.alphaTex.AlphaTexDiagnostic import kotlin.contracts.ExperimentalContracts class ScoreSerializerPluginPartials { @@ -16,4 +18,24 @@ class ScoreSerializerPluginPartials { throw Error("Unexpected value in serialized json " + dv?.javaClass?.name) } } +} + +class AlphaTexAstNodePluginPartials { + companion object { + @ExperimentalContracts + @ExperimentalUnsignedTypes + fun test(arg0: Any?): Boolean { + return arg0 is AlphaTexAstNode + } + } +} + +class AlphaTexDiagnosticPluginPartials { + companion object { + @ExperimentalContracts + @ExperimentalUnsignedTypes + fun test(arg0: Any?): Boolean { + return arg0 is AlphaTexDiagnostic + } + } } \ No newline at end of file diff --git a/packages/kotlin/src/gradle/libs.versions.toml b/packages/kotlin/src/gradle/libs.versions.toml index fb22b9d67..5b447bcd7 100644 --- a/packages/kotlin/src/gradle/libs.versions.toml +++ b/packages/kotlin/src/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -agp = "8.12.1" -kotlin = "2.2.10" +agp = "8.12.3" +kotlin = "2.2.20" kotlinx-coroutines = "1.10.2" dokka = "2.0.0" diff --git a/packages/playground/alphatex-editor.html b/packages/playground/alphatex-editor.html index f0496702b..b4af5743b 100644 --- a/packages/playground/alphatex-editor.html +++ b/packages/playground/alphatex-editor.html @@ -7,10 +7,6 @@ - - - - @@ -35,7 +31,10 @@