diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts index a3fe7fab71..401d24a217 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -1,4 +1,9 @@ -import type { AlphaTexAccidentalMode, AlphaTexVoiceMode } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; +import type { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; +import type { + AlphaTexAccidentalMode, + AlphaTexStaffType, + AlphaTexVoiceMode +} from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; import type { BarLineStyle } from '@coderline/alphatab/model/Bar'; import type { BarreShape } from '@coderline/alphatab/model/BarreShape'; import type { BendStyle } from '@coderline/alphatab/model/BendStyle'; @@ -6,6 +11,7 @@ import type { BendType } from '@coderline/alphatab/model/BendType'; import type { Clef } from '@coderline/alphatab/model/Clef'; import type { Direction } from '@coderline/alphatab/model/Direction'; import type { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; +import type { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; import type { FermataType } from '@coderline/alphatab/model/Fermata'; import type { GraceType } from '@coderline/alphatab/model/GraceType'; import type { KeySignature } from '@coderline/alphatab/model/KeySignature'; @@ -84,6 +90,15 @@ export class AlphaTex1EnumMappings { public static readonly alphaTexVoiceModeReversed = AlphaTex1EnumMappings._reverse( AlphaTex1EnumMappings.alphaTexVoiceMode ); + public static readonly alphaTexStaffType = new Map([ + ['score', 0], + ['tabs', 1], + ['slash', 2], + ['numbered', 3] + ]); + public static readonly alphaTexStaffTypeReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.alphaTexStaffType + ); public static readonly noteAccidentalMode = new Map([ ['default', 0], ['forcenone', 1], @@ -422,6 +437,25 @@ export class AlphaTex1EnumMappings { public static readonly barNumberDisplayReversed = AlphaTex1EnumMappings._reverse( AlphaTex1EnumMappings.barNumberDisplay ); + public static readonly staffPlacement = new Map([ + ['allstaves', 0], + ['primary', 1] + ]); + public static readonly staffPlacementReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.staffPlacement + ); + public static readonly systemDisplay = new Map([ + ['allsystems', 0], + ['firstsystemonly', 1] + ]); + public static readonly systemDisplayReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.systemDisplay); + public static readonly tabRhythmMode = new Map([ + ['hidden', 0], + ['showwithbeams', 1], + ['showwithbars', 2], + ['automatic', 3] + ]); + public static readonly tabRhythmModeReversed = AlphaTex1EnumMappings._reverse(AlphaTex1EnumMappings.tabRhythmMode); public static readonly keySignaturesMinorReversed = new Map([ [-7, 'abminor'], [-6, 'ebminor'], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 175ef83784..c2c23b33d8 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -195,7 +195,8 @@ export class AlphaTex1LanguageDefinitions { ['hideemptystaves', null], ['hideemptystavesinfirstsystem', null], ['showsinglestaffbrackets', null], - ['defaultbarnumberdisplay', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]] + ['defaultbarnumberdisplay', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['defaultstaffdisplay', [[[[100, 107], 5, ['score', 'tabs', 'slash', 'numbered']]]]] ]); public static readonly staffMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ ['tuning', [[[[100, 107], 0, ['piano', 'none', 'voice']]], [[[100, 107], 5]]]], @@ -231,7 +232,8 @@ export class AlphaTex1LanguageDefinitions { ], ['displaytranspose', [[[[106], 0]]]], ['transpose', [[[[106], 0]]]], - ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]] + ['instrument', [[[[106], 0]], [[[107, 100], 0]], [[[100], 0, ['percussion']]]]], + ['staffdisplay', [[[[100, 107], 5, ['score', 'tabs', 'slash', 'numbered']]]]] ]); public static readonly structuralMetaDataSignatures = AlphaTex1LanguageDefinitions._signatures([ [ @@ -491,7 +493,8 @@ export class AlphaTex1LanguageDefinitions { ] ] ], - ['barnumber', [[[[107, 100], 0]], [[[106], 0]]]] + ['barnumber', [[[[107, 100], 0]], [[[106], 0]]]], + ['bardisplay', [[[[100, 107], 5, ['score', 'tabs', 'slash', 'numbered']]]]] ]); public static readonly metaDataProperties = AlphaTex1LanguageDefinitions._metaProps([ [ @@ -551,6 +554,37 @@ export class AlphaTex1LanguageDefinitions { ['hideemptystavesinfirstsystem', null], ['showsinglestaffbrackets', null], ['defaultbarnumberdisplay', null], + [ + 'defaultstaffdisplay', + [ + [ + 'clefvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['clefplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['clefsystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'ksvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['ksplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['kssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'tsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['tsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['tssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'restsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['restsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['restssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + ['barnumber', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['rhythm', [[[[100, 107], 0, ['hidden', 'showwithbeams', 'showwithbars', 'automatic']]]]] + ] + ], [ 'tuning', [ @@ -583,6 +617,37 @@ export class AlphaTex1LanguageDefinitions { ['displaytranspose', null], ['transpose', null], ['instrument', null], + [ + 'staffdisplay', + [ + [ + 'clefvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['clefplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['clefsystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'ksvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['ksplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['kssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'tsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['tsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['tssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'restsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['restsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['restssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + ['barnumber', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]], + ['rhythm', [[[[100, 107], 0, ['hidden', 'showwithbeams', 'showwithbars', 'automatic']]]]] + ] + ], ['ts', null], ['ro', null], ['rc', null], @@ -610,7 +675,31 @@ export class AlphaTex1LanguageDefinitions { ['voicemode', null], ['barnumberdisplay', null], ['beaming', null], - ['barnumber', null] + ['barnumber', null], + [ + 'bardisplay', + [ + [ + 'clefvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['clefplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['clefsystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'ksvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['ksplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['kssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + [ + 'tsvisibility', + [[], [[[107], 0, ['true', 'false']]], [[[100], 0, ['true', 'false']]], [[[106], 0, ['1', '0']]]] + ], + ['tsplacement', [[[[100, 107], 0, ['allstaves', 'primary']]]]], + ['tssystems', [[[[100, 107], 0, ['allsystems', 'firstsystemonly']]]]], + ['barnumber', [[[[100, 107], 0, ['allbars', 'firstofsystem', 'hide']]]]] + ] + ] ]); public static readonly metaDataSignatures = [ AlphaTex1LanguageDefinitions.scoreMetaDataSignatures, diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index c09255fb39..fbc11f7e54 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -20,6 +20,7 @@ import { type AlphaTexMetaDataNode, AlphaTexNodeType, type AlphaTexNumberLiteral, + type AlphaTexPropertiesNode, type AlphaTexPropertyNode, type AlphaTexStringLiteral, type AlphaTexTextNode, @@ -31,6 +32,7 @@ import { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, + AlphaTexStaffType, ArgumentListParseTypesMode, type IAlphaTexImporter } from '@coderline/alphatab/importer/alphaTex/AlphaTexShared'; @@ -44,6 +46,12 @@ import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi'; import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; import { Automation, AutomationType, type FlatSyncPoint } from '@coderline/alphatab/model/Automation'; import { type Bar, BarLineStyle, SustainPedalMarker, SustainPedalMarkerType } from '@coderline/alphatab/model/Bar'; +import type { + NumberedBarOverride, + ScoreBarOverride, + SlashBarOverride, + TabBarOverride +} from '@coderline/alphatab/model/BarOverrides'; import { BarreShape } from '@coderline/alphatab/model/BarreShape'; import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; import { BendPoint } from '@coderline/alphatab/model/BendPoint'; @@ -54,6 +62,7 @@ import { Clef } from '@coderline/alphatab/model/Clef'; import { Color } from '@coderline/alphatab/model/Color'; import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; import { Duration } from '@coderline/alphatab/model/Duration'; +import { type ElementDisplay, StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; import { FadeType } from '@coderline/alphatab/model/FadeType'; import { Fermata } from '@coderline/alphatab/model/Fermata'; import { Fingers } from '@coderline/alphatab/model/Fingers'; @@ -77,12 +86,19 @@ import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { SlideInType } from '@coderline/alphatab/model/SlideInType'; import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import { Staff } from '@coderline/alphatab/model/Staff'; +import type { + NumberedStaffConfig, + ScoreStaffConfig, + SlashStaffConfig, + TabStaffConfig +} from '@coderline/alphatab/model/StaffConfigs'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; import { Tuning } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants'; @@ -300,6 +316,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } score.stylesheet.barNumberDisplay = barNumberDisplay!; return ApplyNodeResult.Applied; + case 'defaultstaffdisplay': + this._staffDisplayProperties(importer, metaData, score.stylesheet, undefined, undefined); + return ApplyNodeResult.Applied; default: return ApplyNodeResult.NotAppliedUnrecognizedMarker; @@ -431,6 +450,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler importer.state.staffTuningApplied.delete(staff); this._readTrackInstrument(importer, staff.track, metaData.arguments!); + return ApplyNodeResult.Applied; + case 'staffdisplay': + this._staffDisplayProperties(importer, metaData, undefined, staff, undefined); return ApplyNodeResult.Applied; case 'bank': staff.track.playbackInfo.bank = (metaData.arguments!.arguments[0] as AlphaTexNumberLiteral).value; @@ -922,6 +944,9 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } bar.barNumberDisplay = barNumberDisplay!; return ApplyNodeResult.Applied; + case 'bardisplay': + this._staffDisplayProperties(importer, metaData, undefined, undefined, bar); + return ApplyNodeResult.Applied; case 'barnumber': switch (metaData.arguments!.arguments[0].nodeType) { case AlphaTexNodeType.Number: @@ -1290,6 +1315,154 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } + private _staffDisplayProperties( + importer: IAlphaTexImporter, + metaData: AlphaTexMetaDataNode, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ) { + if (!metaData.properties) { + return; + } + + // without arguments the properties apply to all staff types supporting them + const staffTypes: AlphaTexStaffType[] = []; + if (metaData.arguments) { + for (const a of metaData.arguments!.arguments) { + staffTypes.push( + AlphaTex1EnumMappings.alphaTexStaffType.get((a as AlphaTexTextNode).text.toLowerCase())! + ); + } + } + const isExplicit = staffTypes.length > 0; + if (!isExplicit) { + for (const t of AlphaTex1EnumMappings.alphaTexStaffType.values()) { + staffTypes.push(t); + } + } + + const lookup = [AlphaTex1LanguageDefinitions.metaDataProperties.get(metaData.tag.tag.text.toLowerCase())!]; + for (const p of metaData.properties.properties) { + if (!this._checkProperty(importer, lookup, p)) { + continue; + } + + for (const t of staffTypes) { + if (!AlphaTex1LanguageHandler._applyStaffDisplayProperty(p, t, stylesheet, staff, bar) && isExplicit) { + importer.addSemanticDiagnostic({ + code: AlphaTexDiagnosticCode.AT307, + message: `The property '${p.property.text}' has no effect on the staff type '${AlphaTex1EnumMappings.alphaTexStaffTypeReversed.get(t)}'`, + severity: AlphaTexDiagnosticsSeverity.Warning, + start: p.start, + end: p.end + }); + } + } + } + } + + /** + * Applies the property to the display settings of the staff type on the stylesheet, staff or bar + * (created on demand). Returns false if the staff type has no such setting. + */ + private static _applyStaffDisplayProperty( + p: AlphaTexPropertyNode, + t: AlphaTexStaffType, + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined + ): boolean { + const name = p.property.text.toLowerCase(); + const value = () => (p.arguments!.arguments[0] as AlphaTexTextNode).text.toLowerCase(); + // all staff types have a bar number display + let applied = name === 'barnumber'; + const barNumber = (v: BarNumberDisplay | undefined): BarNumberDisplay | undefined => + name === 'barnumber' ? AlphaTex1EnumMappings.barNumberDisplay.get(value())! : v; + // Visibility, Placement and Systems + const element = (prefix: string, display: ElementDisplay | undefined): ElementDisplay | undefined => { + if (!name.startsWith(prefix)) { + return display; + } + applied = true; + const d: ElementDisplay = display ?? {}; + if (name.endsWith('visibility')) { + d.isVisible = p.arguments + ? AlphaTex1LanguageHandler._booleanLikeValue(p.arguments!.arguments, 0) + : true; + } else if (name.endsWith('placement')) { + d.staffPlacement = AlphaTex1EnumMappings.staffPlacement.get(value())!; + } else { + d.systemDisplay = AlphaTex1EnumMappings.systemDisplay.get(value())!; + } + return d; + }; + + if (bar && t === AlphaTexStaffType.Score) { + const c: ScoreBarOverride = bar.scoreDisplay ?? {}; + c.clef = element('clef', c.clef); + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.scoreDisplay = applied ? c : bar.scoreDisplay; + } else if (bar && t === AlphaTexStaffType.Tabs) { + const c: TabBarOverride = bar.tabDisplay ?? {}; + c.clef = element('clef', c.clef); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.tabDisplay = applied ? c : bar.tabDisplay; + } else if (bar && t === AlphaTexStaffType.Slash) { + const c: SlashBarOverride = bar.slashDisplay ?? {}; + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.slashDisplay = applied ? c : bar.slashDisplay; + } else if (bar) { + const c: NumberedBarOverride = bar.numberedDisplay ?? {}; + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + bar.numberedDisplay = applied ? c : bar.numberedDisplay; + } else if (t === AlphaTexStaffType.Score) { + const c: ScoreStaffConfig = staff?.scoreConfig ?? stylesheet?.scoreConfig ?? {}; + c.clef = element('clef', c.clef); + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + if (staff && applied) { + staff.scoreConfig = c; + } + } else if (t === AlphaTexStaffType.Tabs) { + const c: TabStaffConfig = staff?.tabConfig ?? stylesheet?.tabConfig ?? {}; + c.clef = element('clef', c.clef); + c.timeSignature = element('ts', c.timeSignature); + c.rests = element('rests', c.rests); + c.barNumber = barNumber(c.barNumber); + if (name === 'rhythm') { + c.rhythm = AlphaTex1EnumMappings.tabRhythmMode.get(value())!; + applied = true; + } + if (staff && applied) { + staff.tabConfig = c; + } + } else if (t === AlphaTexStaffType.Slash) { + const c: SlashStaffConfig = staff?.slashConfig ?? stylesheet?.slashConfig ?? {}; + c.keySignature = element('ks', c.keySignature); + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + if (staff && applied) { + staff.slashConfig = c; + } + } else { + const c: NumberedStaffConfig = staff?.numberedConfig ?? stylesheet?.numberedConfig ?? {}; + c.timeSignature = element('ts', c.timeSignature); + c.barNumber = barNumber(c.barNumber); + if (staff && applied) { + staff.numberedConfig = c; + } + } + return applied; + } + public applyStructuralMetaData( importer: IAlphaTexImporter, metaData: AlphaTexMetaDataNode @@ -2738,9 +2911,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(Atnf.meta('showSingleStaffBrackets')); } - if (stylesheet.barNumberDisplay !== BarNumberDisplay.FirstOfSystem) { - nodes.push(Atnf.identMeta('defaultBarNumberDisplay', BarNumberDisplay[stylesheet.barNumberDisplay])); - } + AlphaTex1LanguageHandler._buildStaffDisplayNodes(nodes, stylesheet, undefined, undefined, false); // Unsupported: // 'globaldisplaychorddiagramsontop', @@ -2921,15 +3092,117 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ]; } - if ( - bar.barNumberDisplay !== undefined && - !AlphaTex1LanguageHandler._isImpliedBarNumberDisplay(staff, bar, voice) - ) { - nodes.push(Atnf.identMeta('barNumberDisplay', BarNumberDisplay[bar.barNumberDisplay])); - } + AlphaTex1LanguageHandler._buildStaffDisplayNodes( + nodes, + undefined, + undefined, + bar, + AlphaTex1LanguageHandler._isImpliedBarNumberDisplay(staff, bar, voice) + ); return nodes; } + /** + * Builds the staff display tags (one per staff type) of the stylesheet, a staff or a bar. Values equal to the + * defaults (stylesheet) or implied by a custom bar number (bar) are skipped, a bar number display shared by all + * staff types is written via the shorthand tag. + */ + private static _buildStaffDisplayNodes( + nodes: AlphaTexMetaDataNode[], + stylesheet: RenderStylesheet | undefined, + staff: Staff | undefined, + bar: Bar | undefined, + isImpliedBarNumber: boolean + ) { + const element = ( + props: AlphaTexPropertiesNode, + prefix: string, + v: ElementDisplay | undefined, + d: ElementDisplay | undefined + ) => { + if (v?.isVisible !== undefined && v!.isVisible !== d?.isVisible) { + Atnf.prop(props.properties, `${prefix}Visibility`, Atnf.identValue(v!.isVisible! ? 'true' : 'false')); + } + if (v?.staffPlacement !== undefined && v!.staffPlacement !== d?.staffPlacement) { + Atnf.prop(props.properties, `${prefix}Placement`, Atnf.identValue(StaffPlacement[v!.staffPlacement!])); + } + if (v?.systemDisplay !== undefined && v!.systemDisplay !== d?.systemDisplay) { + Atnf.prop(props.properties, `${prefix}Systems`, Atnf.identValue(SystemDisplay[v!.systemDisplay!])); + } + }; + + const score = Atnf.props([]); + const tabs = Atnf.props([]); + const slash = Atnf.props([]); + const numbered = Atnf.props([]); + const barNumbers: (BarNumberDisplay | undefined)[] = []; + if (bar) { + element(score, 'clef', bar.scoreDisplay?.clef, undefined); + element(score, 'ks', bar.scoreDisplay?.keySignature, undefined); + element(score, 'ts', bar.scoreDisplay?.timeSignature, undefined); + element(tabs, 'clef', bar.tabDisplay?.clef, undefined); + element(tabs, 'ts', bar.tabDisplay?.timeSignature, undefined); + element(slash, 'ks', bar.slashDisplay?.keySignature, undefined); + element(slash, 'ts', bar.slashDisplay?.timeSignature, undefined); + element(numbered, 'ts', bar.numberedDisplay?.timeSignature, undefined); + barNumbers.push(bar.scoreDisplay?.barNumber); + barNumbers.push(bar.tabDisplay?.barNumber); + barNumbers.push(bar.slashDisplay?.barNumber); + barNumbers.push(bar.numberedDisplay?.barNumber); + } else { + // staves have no defaults, every defined value overrides the stylesheet + const defaults = staff ? undefined : AlphaTex1LanguageHandler._defaultScore.stylesheet; + const sc = staff ? staff.scoreConfig : stylesheet!.scoreConfig; + const tc = staff ? staff.tabConfig : stylesheet!.tabConfig; + const slc = staff ? staff.slashConfig : stylesheet!.slashConfig; + const nc = staff ? staff.numberedConfig : stylesheet!.numberedConfig; + element(score, 'clef', sc?.clef, defaults?.scoreConfig?.clef); + element(score, 'ks', sc?.keySignature, defaults?.scoreConfig?.keySignature); + element(score, 'ts', sc?.timeSignature, defaults?.scoreConfig?.timeSignature); + element(tabs, 'clef', tc?.clef, defaults?.tabConfig?.clef); + element(tabs, 'ts', tc?.timeSignature, defaults?.tabConfig?.timeSignature); + element(tabs, 'rests', tc?.rests, defaults?.tabConfig?.rests); + if (tc?.rhythm !== undefined && tc!.rhythm !== defaults?.tabConfig?.rhythm) { + Atnf.prop(tabs.properties, 'rhythm', Atnf.identValue(TabRhythmMode[tc!.rhythm!])); + } + element(slash, 'ks', slc?.keySignature, defaults?.slashConfig?.keySignature); + element(slash, 'ts', slc?.timeSignature, defaults?.slashConfig?.timeSignature); + element(numbered, 'ts', nc?.timeSignature, defaults?.numberedConfig?.timeSignature); + barNumbers.push(sc?.barNumber); + barNumbers.push(tc?.barNumber); + barNumbers.push(slc?.barNumber); + barNumbers.push(nc?.barNumber); + } + + // bar numbers equal to the default (stylesheet) or implied by a custom bar number (bar) are not written, + // one shared by all staff types is written via the shorthand tag (there is none for staves) + let defaultBarNumber: BarNumberDisplay | undefined = undefined; + if (stylesheet) { + defaultBarNumber = BarNumberDisplay.FirstOfSystem; + } else if (isImpliedBarNumber) { + defaultBarNumber = BarNumberDisplay.AllBars; + } + const isShared = !staff && barNumbers.filter(b => b === barNumbers[0]).length === barNumbers.length; + if (isShared && barNumbers[0] !== undefined && barNumbers[0] !== defaultBarNumber) { + nodes.push( + Atnf.identMeta(bar ? 'barNumberDisplay' : 'defaultBarNumberDisplay', BarNumberDisplay[barNumbers[0]!]) + ); + } + + const tag = bar ? 'barDisplay' : staff ? 'staffDisplay' : 'defaultStaffDisplay'; + const staffTypes = ['score', 'tabs', 'slash', 'numbered']; + const all = [score, tabs, slash, numbered]; + for (let i = 0; i < all.length; i++) { + const barNumber = barNumbers[i]; + if (!isShared && barNumber !== undefined && barNumber !== defaultBarNumber) { + Atnf.prop(all[i].properties, 'barNumber', Atnf.identValue(BarNumberDisplay[barNumber!])); + } + if (all[i].properties.length > 0) { + nodes.push(Atnf.meta(tag, Atnf.identValue(staffTypes[i]), all[i])); + } + } + } + private static _buildStaffMetaDataNodes(nodes: AlphaTexMetaDataNode[], staff: Staff) { const firstStaffMetaIndex = nodes.length; @@ -2979,6 +3252,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } } + AlphaTex1LanguageHandler._buildStaffDisplayNodes(nodes, undefined, staff, undefined, false); if (firstStaffMetaIndex < nodes.length) { nodes[firstStaffMetaIndex].leadingComments = [ { @@ -3016,8 +3290,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } /** - * A custom bar number (`\barNumber`) implies a forced bar number display on the bar it is written on - * (the first staff of the first track). In this case the display does not need to be exported. + * A custom bar number (`\barNumber`) implies a forced bar number display (`allBars` on all staff types) + * on the bar it is written on (the first staff of the first track). In this case these values do not need to be exported. */ private static _isImpliedBarNumberDisplay(staff: Staff, bar: Bar, voice: number): boolean { const masterBar = bar.masterBar; @@ -3025,8 +3299,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler voice === 0 && staff.index === 0 && staff.track.index === 0 && - (masterBar.customBarNumber !== undefined || masterBar.customBarNumberText !== undefined) && - bar.barNumberDisplay === BarNumberDisplay.AllBars + (masterBar.customBarNumber !== undefined || masterBar.customBarNumberText !== undefined) ); } diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts index 51c50b8803..b609fafe59 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts @@ -237,6 +237,11 @@ export enum AlphaTexDiagnosticCode { */ AT306 = 306, + /** + * The staff display property is not supported by the staff type, the value has no effect. + */ + AT307 = 307, + /** * The dots separating score metadata, score contents and the sync points can be removed. */ @@ -286,6 +291,21 @@ export enum AlphaTexVoiceMode { BarWise = 1 } +/** + * The staff types selected by the alphaTex staff display tags (`\defaultStaffDisplay`, `\staffDisplay`, `\barDisplay`). + * @public + */ +export enum AlphaTexStaffType { + /** Standard notation */ + Score = 0, + /** Guitar tablature */ + Tabs = 1, + /** Slash notation */ + Slash = 2, + /** Numbered notation (Jianpu) */ + Numbered = 3 +} + /** * @public */ diff --git a/packages/alphatab/src/importer/alphaTex/_barrel.ts b/packages/alphatab/src/importer/alphaTex/_barrel.ts index b814cfddf7..fe034e20b0 100644 --- a/packages/alphatab/src/importer/alphaTex/_barrel.ts +++ b/packages/alphatab/src/importer/alphaTex/_barrel.ts @@ -44,6 +44,7 @@ export { AlphaTexDiagnosticCode, AlphaTexDiagnosticsSeverity, AlphaTexStaffNoteKind, + AlphaTexStaffType, AlphaTexVoiceMode, ArgumentListParseTypesMode, type IAlphaTexImporter, diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index f262ba4da9..999491827c 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest'; import { AlphaTexExporter } from '@coderline/alphatab/exporter/AlphaTexExporter'; import { AlphaTexErrorWithDiagnostics } from '@coderline/alphatab/importer/AlphaTexImporter'; import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; +import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import type { Score } from '@coderline/alphatab/model/Score'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { Settings } from '@coderline/alphatab/Settings'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { TestPlatform } from 'test/TestPlatform'; @@ -206,4 +209,40 @@ describe('AlphaTexExporterTest', () => { const reimported = ScoreLoader.loadAlphaTex(exported); ComparisonHelpers.alphaTexExportRoundtripEqual('accidental-mode-only-when-needed', reimported, score); }); + + it('staff-display', () => { + const score = ScoreLoader.loadAlphaTex( + '\\track \\staff {score tabs} \\tuning (E4 B3 G3 D3 A2 E2) 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3' + ); + const stylesheet = score.stylesheet; + stylesheet.scoreConfig.clef!.systemDisplay = SystemDisplay.FirstSystemOnly; + stylesheet.tabConfig.clef!.systemDisplay = SystemDisplay.FirstSystemOnly; + stylesheet.tabConfig.timeSignature!.staffPlacement = StaffPlacement.AllStaves; + stylesheet.tabConfig.rhythm = TabRhythmMode.ShowWithBars; + + const staff = score.tracks[0].staves[0]; + staff.scoreConfig = { barNumber: BarNumberDisplay.AllBars }; + staff.slashConfig = { keySignature: { isVisible: true } }; + + staff.bars[1].scoreDisplay = { timeSignature: { isVisible: false } }; + staff.bars[1].tabDisplay = { timeSignature: { isVisible: false }, barNumber: BarNumberDisplay.Hide }; + staff.bars[2].barNumberDisplay = BarNumberDisplay.Hide; + + const exported = exportAlphaTex(score) + .split('\n') + .map(l => l.trim()) + .join(' '); + + expect(exported).toContain('\\defaultStaffDisplay score { clefSystems FirstSystemOnly }'); + expect(exported).toContain( + '\\defaultStaffDisplay tabs { clefSystems FirstSystemOnly tsPlacement AllStaves rhythm ShowWithBars }' + ); + expect(exported).toContain('\\staffDisplay slash { ksVisibility true }'); + expect(exported).toContain('\\barDisplay tabs { tsVisibility false barNumber Hide }'); + // a bar number display shared by all staff types uses the shorthand + expect(exported).toContain('\\barNumberDisplay Hide'); + + const reimported = ScoreLoader.loadAlphaTex(exportAlphaTex(score)); + ComparisonHelpers.alphaTexExportRoundtripEqual('staff-display', reimported, score); + }); }); diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index cd240cdd66..27eea8bb0e 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -15,6 +15,7 @@ import { CrescendoType } from '@coderline/alphatab/model/CrescendoType'; import { Direction } from '@coderline/alphatab/model/Direction'; import { Duration } from '@coderline/alphatab/model/Duration'; import { DynamicValue } from '@coderline/alphatab/model/DynamicValue'; +import { StaffPlacement, SystemDisplay } from '@coderline/alphatab/model/ElementDisplay'; import { FadeType } from '@coderline/alphatab/model/FadeType'; import { FermataType } from '@coderline/alphatab/model/Fermata'; import { Fingers } from '@coderline/alphatab/model/Fingers'; @@ -50,6 +51,7 @@ import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; import { harmonicToString } from '@coderline/alphatab/rendering/effects/HarmonicsEffectInfo'; import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; +import { TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { Settings } from '@coderline/alphatab/Settings'; import { StaveProfile } from '@coderline/alphatab/StaveProfile'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; @@ -2690,6 +2692,89 @@ describe('AlphaTexImporterTest', () => { test('\\defaultBarNumberDisplay allBars C4 | \\barNumberDisplay hide C4 ', BarNumberDisplay.Hide)); }); + describe('staffDisplay', () => { + it('levels', () => { + const score = parseTex(` + \\defaultStaffDisplay tabs { tsVisibility false tsPlacement allStaves rhythm showWithBeams } + \\staff {score tabs} + \\staffDisplay { ksVisibility false barNumber hide } + C4 | \\barDisplay (score tabs) { clefVisibility false tsSystems firstSystemOnly } C4 | C4 + `); + const tabs = score.stylesheet.tabConfig; + expect(tabs.timeSignature!.isVisible).toBe(false); + expect(tabs.timeSignature!.staffPlacement).toBe(StaffPlacement.AllStaves); + expect(tabs.timeSignature!.systemDisplay).toBe(SystemDisplay.AllSystems); + expect(tabs.rhythm).toBe(TabRhythmMode.ShowWithBeams); + expect(score.stylesheet.scoreConfig.timeSignature!.isVisible).toBe(true); + + const staff = score.tracks[0].staves[0]; + expect(staff.scoreConfig!.keySignature!.isVisible).toBe(false); + expect(staff.slashConfig!.keySignature!.isVisible).toBe(false); + expect(staff.scoreConfig!.clef).toBeUndefined(); + expect(staff.tabConfig!.barNumber).toBe(BarNumberDisplay.Hide); + expect(staff.numberedConfig!.barNumber).toBe(BarNumberDisplay.Hide); + + const bars = staff.bars; + expect(bars[0].scoreDisplay).toBeUndefined(); + expect(bars[1].scoreDisplay!.clef!.isVisible).toBe(false); + expect(bars[1].tabDisplay!.clef!.isVisible).toBe(false); + expect(bars[1].tabDisplay!.timeSignature!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); + expect(bars[1].slashDisplay).toBeUndefined(); + expect(bars[2].scoreDisplay).toBeUndefined(); + + testExportRoundtrip(score); + }); + + it('later-tags-win', () => { + const score = parseTex( + '\\defaultStaffDisplay { tsVisibility false } \\defaultStaffDisplay tabs { tsVisibility true tsSystems firstSystemOnly } C4' + ); + expect(score.stylesheet.scoreConfig.timeSignature!.isVisible).toBe(false); + expect(score.stylesheet.tabConfig.timeSignature!.isVisible).toBe(true); + expect(score.stylesheet.tabConfig.timeSignature!.systemDisplay).toBe(SystemDisplay.FirstSystemOnly); + expect(score.stylesheet.tabConfig.timeSignature!.staffPlacement).toBe(StaffPlacement.Primary); + + testExportRoundtrip(score); + }); + + it('visibility-values', () => { + const values = ['', 'true', 'false', '"false"', '1', '0']; + const expected = [true, true, false, false, true, false]; + for (let i = 0; i < values.length; i++) { + const score = parseTex(`\\staffDisplay score { tsVisibility ${values[i]} } C4`); + expect(score.tracks[0].staves[0].scoreConfig!.timeSignature!.isVisible).toBe(expected[i]); + } + }); + + it('diagnostics', () => { + const importer = (tex: string) => { + const i = new AlphaTexImporter(); + i.initFromString(tex, new Settings()); + try { + i.readScore(); + } catch { + // checked below + } + return i; + }; + const unsupported = importer('\\staffDisplay tabs { ksVisibility false } C4'); + expect(unsupported.semanticDiagnostics.items.map(d => d.code)).toContain(AlphaTexDiagnosticCode.AT307); + const barLevel = importer('C4 | \\barDisplay { restsVisibility false } C4'); + expect(barLevel.parserDiagnostics.items.map(d => d.code)).toContain(AlphaTexDiagnosticCode.AT205); + expect(parseTex('\\staffDisplay tabs { ksVisibility false } C4').tracks[0].staves[0].tabConfig).toBe( + undefined + ); + }); + + it('legacy-shorthands', () => { + const score = parseTex('\\defaultBarNumberDisplay hide C4 | \\barNumberDisplay allBars C4'); + expect(score.stylesheet.tabConfig.barNumber).toBe(BarNumberDisplay.Hide); + expect(score.tracks[0].staves[0].bars[1].tabDisplay!.barNumber).toBe(BarNumberDisplay.AllBars); + + testExportRoundtrip(score); + }); + }); + it('custom-beaming', () => { const score = parseTex(` \\ts (4 4) diff --git a/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts b/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts index 1912618505..1fefcac655 100644 --- a/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts +++ b/packages/alphatab/test/visualTests/features/StaffDisplayConfig.test.ts @@ -87,15 +87,11 @@ describe('StaffDisplayConfig', () => { ` \\track \\staff {tabs} + \\staffDisplay tabs { tsVisibility false } 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png', - undefined, - o => { - const staff = o.score.tracks[0].staves[0]; - staff.tabConfig = { timeSignature: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l2-tab-ts-hide.png' ); }); @@ -104,16 +100,12 @@ describe('StaffDisplayConfig', () => { ` \\track \\staff {score} + \\staffDisplay score { clefVisibility false } \\ks D 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png', - undefined, - o => { - const staff = o.score.tracks[0].staves[0]; - staff.scoreConfig = { clef: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l2-score-clef-hide.png' ); }); }); @@ -127,15 +119,11 @@ describe('StaffDisplayConfig', () => { \\ks D 3.3.4 3.3 3.3 3.3 | \\ks A + \\barDisplay score { ksVisibility false } 3.3.4 3.3 3.3 3.3 | 3.3.4 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png', - undefined, - o => { - const bars = o.score.tracks[0].staves[0].bars; - bars[1].scoreDisplay = { keySignature: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l1-ks-change-hide.png' ); }); @@ -144,15 +132,11 @@ describe('StaffDisplayConfig', () => { ` \\track \\staff {score tabs} + \\barDisplay score { tsVisibility false } 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 | `, - 'test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png', - undefined, - o => { - const bars = o.score.tracks[0].staves[0].bars; - bars[0].scoreDisplay = { timeSignature: { isVisible: false } }; - } + 'test-data/visual-tests/staff-display-config/override-l1-ts-hide-firstbar.png' ); }); }); diff --git a/packages/alphatex/src/common.ts b/packages/alphatex/src/common.ts index c57640b534..eb32780039 100644 --- a/packages/alphatex/src/common.ts +++ b/packages/alphatex/src/common.ts @@ -1,4 +1,9 @@ -import type { MetadataTagDefinition, PropertyDefinition } from '@coderline/alphatab-alphatex/types'; +import * as alphaTab from '@coderline/alphatab'; +import type { + MetadataTagDefinition, + PropertyDefinition, + SignatureDefinition +} from '@coderline/alphatab-alphatex/types'; export const generalMidiInstruments = [ 'Acoustic Grand Piano', @@ -138,3 +143,35 @@ export function properties(...props: PropertyDefinition[]): Map(metadata.map(t => [t.tag.toLowerCase(), t])); } + +/** + * Creates the signatures for a boolean-like value: no value (= true), `true|false`, `"true"|"false"` or `1|0`. + */ +export function booleanSignatures( + name: string, + shortDescription: string, + trueDescription: string, + falseDescription: string +): SignatureDefinition[] { + const overload = (type: alphaTab.importer.alphaTex.AlphaTexNodeType, t: string, f: string, quote: string) => ({ + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + type, + values: [ + { name: t, snippet: `${quote}${t}${quote}`, shortDescription: trueDescription }, + { name: f, snippet: `${quote}${f}${quote}`, shortDescription: falseDescription } + ] + } + ] + }); + const types = alphaTab.importer.alphaTex.AlphaTexNodeType; + return [ + { parameters: [] }, + overload(types.String, 'true', 'false', '"'), + overload(types.Ident, 'true', 'false', ''), + overload(types.Number, '1', '0', '') + ]; +} diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index e6227fe9ee..0770afb988 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -161,6 +161,9 @@ import { barNumberDisplay } from '@coderline/alphatab-alphatex/metadata/bar/barn import { beaming } from '@coderline/alphatab-alphatex/metadata/bar/beamingRule'; import { restDisplayPitch } from '@coderline/alphatab-alphatex/properties/beat/restDisplayPitch'; import { barNumber } from '@coderline/alphatab-alphatex/metadata/bar/barNumber'; +import { defaultStaffDisplay } from '@coderline/alphatab-alphatex/metadata/score/defaultStaffDisplay'; +import { staffDisplay } from '@coderline/alphatab-alphatex/metadata/staff/staffDisplay'; +import { barDisplay } from '@coderline/alphatab-alphatex/metadata/bar/barDisplay'; export const structuralMetaData = metadata(track, staff, voice); export const scoreMetaData = metadata( @@ -195,7 +198,8 @@ export const scoreMetaData = metadata( hideEmptyStaves, hideEmptyStavesInFirstSystem, showSingleStaffBrackets, - defaultBarNumberDisplay + defaultBarNumberDisplay, + defaultStaffDisplay ); export const staffMetaData = metadata( @@ -206,7 +210,8 @@ export const staffMetaData = metadata( articulation, displayTranspose, transpose, - instrumentMeta + instrumentMeta, + staffDisplay ); export const barMetaData = metadata( @@ -237,7 +242,8 @@ export const barMetaData = metadata( voiceMode, barNumberDisplay, beaming, - barNumber + barNumber, + barDisplay ); export const allMetadata = new Map([ diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 65331f264b..3937c06f60 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -10,6 +10,7 @@ export const alphaTexMappedEnumLookup = { FermataType: alphaTab.model.FermataType, AlphaTexAccidentalMode: alphaTab.importer.alphaTex.AlphaTexAccidentalMode, AlphaTexVoiceMode: alphaTab.importer.alphaTex.AlphaTexVoiceMode, + AlphaTexStaffType: alphaTab.importer.alphaTex.AlphaTexStaffType, NoteAccidentalMode: alphaTab.model.NoteAccidentalMode, BarreShape: alphaTab.model.BarreShape, Ottavia: alphaTab.model.Ottavia, @@ -30,7 +31,10 @@ export const alphaTexMappedEnumLookup = { SimileMark: alphaTab.model.SimileMark, Direction: alphaTab.model.Direction, TremoloPickingStyle: alphaTab.model.TremoloPickingStyle, - BarNumberDisplay: alphaTab.model.BarNumberDisplay + BarNumberDisplay: alphaTab.model.BarNumberDisplay, + StaffPlacement: alphaTab.model.StaffPlacement, + SystemDisplay: alphaTab.model.SystemDisplay, + TabRhythmMode: alphaTab.TabRhythmMode }; export type AlphaTexMappedEnumName = keyof typeof alphaTexMappedEnumLookup; @@ -122,6 +126,12 @@ export const alphaTexMappedEnumMapping: { longDescription: 'A new voice adds a new voice to the current bar only.' } }, + AlphaTexStaffType: { + Score: { snippet: 'score', shortDescription: 'Standard notation' }, + Tabs: { snippet: 'tabs', shortDescription: 'Guitar tablature' }, + Slash: { snippet: 'slash', shortDescription: 'Slash notation' }, + Numbered: { snippet: 'numbered', shortDescription: 'Numbered notation (Jianpu)' } + }, NoteAccidentalMode: { Default: { snippet: 'default', shortDescription: 'Auto-detect the accidentals', aliases: ['d'] }, ForceNone: { snippet: 'forceNone', shortDescription: 'Force no accidentals', aliases: ['-'] }, @@ -464,6 +474,20 @@ export const alphaTexMappedEnumMapping: { AllBars: { snippet: 'allBars', shortDescription: 'All bars' }, FirstOfSystem: { snippet: 'firstOfSystem', shortDescription: 'First bar of every system' }, Hide: { snippet: 'hide', shortDescription: 'Hide' } + }, + StaffPlacement: { + AllStaves: { snippet: 'allStaves', shortDescription: 'All staves' }, + Primary: { snippet: 'primary', shortDescription: 'Only the primary (topmost) staff' } + }, + SystemDisplay: { + AllSystems: { snippet: 'allSystems', shortDescription: 'All systems' }, + FirstSystemOnly: { snippet: 'firstSystemOnly', shortDescription: 'Only the first system' } + }, + TabRhythmMode: { + Hidden: { snippet: 'hidden', shortDescription: 'Hidden' }, + ShowWithBeams: { snippet: 'showWithBeams', shortDescription: 'Individual beams per beat' }, + ShowWithBars: { snippet: 'showWithBars', shortDescription: 'Connected bars like standard notation' }, + Automatic: { snippet: 'automatic', shortDescription: 'Shown if no standard notation is shown' } } }; diff --git a/packages/alphatex/src/metadata/bar/barDisplay.ts b/packages/alphatex/src/metadata/bar/barDisplay.ts new file mode 100644 index 0000000000..f1fc98e130 --- /dev/null +++ b/packages/alphatex/src/metadata/bar/barDisplay.ts @@ -0,0 +1,24 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; +import { staffDisplayProperties, staffDisplayTargetSignatures } from '@coderline/alphatab-alphatex/staffDisplay'; + +export const barDisplay: MetadataTagDefinition = { + tag: '\\barDisplay', + snippet: '\\barDisplay $1 {$2}$0', + shortDescription: 'Sets how elements are displayed on the current bar.', + longDescription: ` + Defines the display of elements like clefs, key signatures, time signatures and bar numbers for the current bar. + + The arguments select the staff types to which the properties apply. If no staff types are specified, every property applies to all staff types supporting the related element. + + All properties are optional. Properties which are not specified are inherited from [\`\\staffDisplay\`](https://alphatab.net/docs/alphatex/staff-metadata#staffdisplay) and [\`\\defaultStaffDisplay\`](https://alphatab.net/docs/alphatex/score-metadata#defaultstaffdisplay). The values only apply to the bar they are specified on. + `, + signatures: staffDisplayTargetSignatures(), + examples: ` + \\track + \\staff {score tabs} + \\ks D 3.3.4 3.3 3.3 3.3 | + \\barDisplay score { ksVisibility false } \\ks A 3.3 3.3 3.3 3.3 | + 3.3 3.3 3.3 3.3 + `, + properties: staffDisplayProperties('\\barDisplay', 'bar') +}; diff --git a/packages/alphatex/src/metadata/score/defaultStaffDisplay.ts b/packages/alphatex/src/metadata/score/defaultStaffDisplay.ts new file mode 100644 index 0000000000..e6f66a9f2a --- /dev/null +++ b/packages/alphatex/src/metadata/score/defaultStaffDisplay.ts @@ -0,0 +1,24 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; +import { staffDisplayProperties, staffDisplayTargetSignatures } from '@coderline/alphatab-alphatex/staffDisplay'; + +export const defaultStaffDisplay: MetadataTagDefinition = { + tag: '\\defaultStaffDisplay', + snippet: '\\defaultStaffDisplay $1 {$2}$0', + shortDescription: 'Sets how elements are displayed on the staves of the whole song.', + longDescription: ` + Defines the song-wide display of elements like clefs, key signatures, time signatures, rests and bar numbers. + + The arguments select the staff types to which the properties apply. If no staff types are specified, every property applies to all staff types supporting the related element. + + All properties are optional. Properties which are not specified keep their default. The values can be overridden per staff with [\`\\staffDisplay\`](https://alphatab.net/docs/alphatex/staff-metadata#staffdisplay) and per bar with [\`\\barDisplay\`](https://alphatab.net/docs/alphatex/bar-metadata#bardisplay). + `, + signatures: staffDisplayTargetSignatures(), + examples: ` + \\defaultStaffDisplay { clefPlacement primary } + \\defaultStaffDisplay tabs { tsVisibility true tsPlacement allStaves } + \\track + \\staff {score tabs} + 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 + `, + properties: staffDisplayProperties('\\defaultStaffDisplay', 'score') +}; diff --git a/packages/alphatex/src/metadata/staff/staffDisplay.ts b/packages/alphatex/src/metadata/staff/staffDisplay.ts new file mode 100644 index 0000000000..5c781b34a0 --- /dev/null +++ b/packages/alphatex/src/metadata/staff/staffDisplay.ts @@ -0,0 +1,23 @@ +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; +import { staffDisplayProperties, staffDisplayTargetSignatures } from '@coderline/alphatab-alphatex/staffDisplay'; + +export const staffDisplay: MetadataTagDefinition = { + tag: '\\staffDisplay', + snippet: '\\staffDisplay $1 {$2}$0', + shortDescription: 'Sets how elements are displayed on the current staff.', + longDescription: ` + Defines the display of elements like clefs, key signatures, time signatures, rests and bar numbers for the current staff. + + The arguments select the staff types to which the properties apply. If no staff types are specified, every property applies to all staff types supporting the related element. + + All properties are optional. Properties which are not specified are inherited from [\`\\defaultStaffDisplay\`](https://alphatab.net/docs/alphatex/score-metadata#defaultstaffdisplay). The values can be overridden per bar with [\`\\barDisplay\`](https://alphatab.net/docs/alphatex/bar-metadata#bardisplay). + `, + signatures: staffDisplayTargetSignatures(), + examples: ` + \\track + \\staff {score tabs} + \\staffDisplay tabs { tsPlacement allStaves rhythm showWithBeams } + 3.3.4 3.3 3.3 3.3 | 3.3 3.3 3.3 3.3 + `, + properties: staffDisplayProperties('\\staffDisplay', 'staff') +}; diff --git a/packages/alphatex/src/staffDisplay.ts b/packages/alphatex/src/staffDisplay.ts new file mode 100644 index 0000000000..ff8dd10a03 --- /dev/null +++ b/packages/alphatex/src/staffDisplay.ts @@ -0,0 +1,138 @@ +import * as alphaTab from '@coderline/alphatab'; +import { booleanSignatures, properties } from '@coderline/alphatab-alphatex/common'; +import { type AlphaTexMappedEnumName, enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { AlphaTexExample, PropertyDefinition, SignatureDefinition } from '@coderline/alphatab-alphatex/types'; + +/** + * The level of a staff display tag: score (`\defaultStaffDisplay`), staff (`\staffDisplay`) or bar (`\barDisplay`). + */ +export type StaffDisplayLevel = 'score' | 'staff' | 'bar'; + +/** + * Creates the signatures selecting the staff types to which the staff display properties apply. + */ +export function staffDisplayTargetSignatures(): SignatureDefinition[] { + return [ + { + parameters: [ + { + name: 'staffTypes', + shortDescription: + 'The staff types to which the properties apply. If omitted, every property applies to all staff types supporting it.', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.ValueListWithoutParenthesis, + ...enumParameter('AlphaTexStaffType') + } + ] + } + ]; +} + +function enumSignatures(name: string, shortDescription: string, type: AlphaTexMappedEnumName): SignatureDefinition[] { + return [ + { + parameters: [ + { + name, + shortDescription, + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter(type) + } + ] + } + ]; +} + +function staffDisplayExample(tag: string, level: StaffDisplayLevel, props: string): AlphaTexExample { + const meta = `${tag} { ${props} }`; + const bars = ['\\ks D 3.3.4 r 3.3 3.3', '3.3.4 3.3 r 3.3', '\\ts (3 4) \\ks A 3.3.4 3.3 r', '3.3.4 r 3.3']; + if (level === 'bar') { + bars[2] = `${meta} ${bars[2]}`; + } + const lines = [ + level === 'score' ? meta : '', + '\\track { defaultSystemsLayout 2 }', + ' \\staff {score tabs}', + level === 'staff' ? ` ${meta}` : '', + ` ${bars.join(' | ')}` + ]; + return { options: { display: { layoutMode: 'Parchment' } }, tex: lines.filter(l => l.length > 0).join('\n') }; +} + +/** + * The elements configured via `Visibility`, `Placement` and `Systems`: + * prefix, name, supported staff types, available on bar level. + */ +const staffDisplayElements: [string, string, string, boolean][] = [ + ['clef', 'clef', '`score`, `tabs`', true], + ['ks', 'key signature', '`score`, `slash`', true], + ['ts', 'time signature', '`score`, `tabs`, `slash`, `numbered`', true], + ['rests', 'rests', '`tabs`', false] +]; + +/** + * Creates the properties of a staff display tag. Every tag gets own definitions as the definitions + * are prepared in place and the examples use the tag. + */ +export function staffDisplayProperties(tag: string, level: StaffDisplayLevel): Map { + const prop = ( + property: string, + shortDescription: string, + signatures: SignatureDefinition[], + value: string + ): PropertyDefinition => ({ + property, + snippet: `${property} $0`, + shortDescription, + signatures, + examples: staffDisplayExample(tag, level, `${property} ${value}`) + }); + + const props: PropertyDefinition[] = []; + for (const [prefix, name, staffTypes, barLevel] of staffDisplayElements) { + if (level === 'bar' && !barLevel) { + continue; + } + const supported = `Supported staff types: ${staffTypes}.`; + props.push( + prop( + `${prefix}Visibility`, + `Show or hide the ${name}. ${supported}`, + booleanSignatures('visibility', `Whether the ${name} is shown`, `Show the ${name}`, `Hide the ${name}`), + 'false' + ), + prop( + `${prefix}Placement`, + `Show the ${name} on all staves or only on the primary (topmost) staff. ${supported}`, + enumSignatures('placement', `On which staves the ${name} is shown`, 'StaffPlacement'), + 'primary' + ), + prop( + `${prefix}Systems`, + `Show the ${name} on all systems or only on the first system. ${supported}`, + enumSignatures('systems', `On which systems the ${name} is shown`, 'SystemDisplay'), + 'firstSystemOnly' + ) + ); + } + + props.push( + prop( + 'barNumber', + 'Set how bar numbers are shown. Supported staff types: all.', + enumSignatures('mode', 'The mode to use', 'BarNumberDisplay'), + 'allBars' + ) + ); + if (level !== 'bar') { + props.push( + prop( + 'rhythm', + 'Set how the rhythm notation is shown. Supported staff types: `tabs`.', + enumSignatures('mode', 'The mode to use', 'TabRhythmMode'), + 'showWithBeams' + ) + ); + } + + return properties(...props); +}