diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index fe6bceb03..a4f8f6539 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -43,7 +43,10 @@ import { createGolpeEffectInfo } from '@coderline/alphatab/rendering/effects/Gol import { createHarmonicsEffectInfo } from '@coderline/alphatab/rendering/effects/HarmonicsEffectInfo'; import { leftHandTapEffectInfo } from '@coderline/alphatab/rendering/effects/LeftHandTapEffectInfo'; import { letRingEffectInfo } from '@coderline/alphatab/rendering/effects/LetRingEffectInfo'; -import { lyricsEffectInfo } from '@coderline/alphatab/rendering/effects/LyricsEffectInfo'; +import { + lyricsAboveEffectInfo, + lyricsBelowEffectInfo +} from '@coderline/alphatab/rendering/effects/LyricsEffectInfo'; import { markerEffectInfo } from '@coderline/alphatab/rendering/effects/MarkerEffectInfo'; import { noteOrnamentEffectInfo } from '@coderline/alphatab/rendering/effects/NoteOrnamentEffectInfo'; import { numberedBarKeySignatureEffectInfo } from '@coderline/alphatab/rendering/effects/NumberedBarKeySignatureEffectInfo'; @@ -467,6 +470,8 @@ export class Environment { // // Slash new SlashBarRendererFactory([ + { effect: lyricsAboveEffectInfo, mode: EffectBandMode.OwnedTop }, + { effect: lyricsBelowEffectInfo, mode: EffectBandMode.OwnedBottom }, { effect: tempoEffectInfo, mode: EffectBandMode.SharedTop }, { effect: tripletFeelEffectInfo, mode: EffectBandMode.SharedTop }, { effect: markerEffectInfo, mode: EffectBandMode.SharedTop }, @@ -482,6 +487,8 @@ export class Environment { // // Score (standard notation) new ScoreBarRendererFactory([ + { effect: lyricsAboveEffectInfo, mode: EffectBandMode.OwnedTop }, + { effect: lyricsBelowEffectInfo, mode: EffectBandMode.OwnedBottom }, { effect: capoEffectInfo, mode: EffectBandMode.SharedTop }, { effect: fermataEffectInfo, mode: EffectBandMode.SharedTop }, { effect: beatBarreEffectInfo, mode: EffectBandMode.SharedTop }, @@ -535,6 +542,8 @@ export class Environment { // // Numbered new NumberedBarRendererFactory([ + { effect: lyricsAboveEffectInfo, mode: EffectBandMode.OwnedTop }, + { effect: lyricsBelowEffectInfo, mode: EffectBandMode.OwnedBottom }, { effect: numberedBarKeySignatureEffectInfo, mode: EffectBandMode.OwnedTop, order: 1000 }, { effect: slideLabelEffectInfo, mode: EffectBandMode.OwnedTop }, { effect: hammerPullLabelEffectInfo, mode: EffectBandMode.OwnedTop } @@ -543,8 +552,8 @@ export class Environment { // // Tabs new TabBarRendererFactory([ - { effect: lyricsEffectInfo, mode: EffectBandMode.SharedTop }, - + { effect: lyricsAboveEffectInfo, mode: EffectBandMode.OwnedTop }, + { effect: lyricsBelowEffectInfo, mode: EffectBandMode.OwnedBottom }, { effect: tabWhammyEffectInfo, mode: EffectBandMode.OwnedTop }, { effect: trillEffectInfo, mode: EffectBandMode.OwnedTop }, { effect: wideBeatVibratoEffectInfo, mode: EffectBandMode.OwnedTop }, diff --git a/packages/alphatab/src/NotationSettings.ts b/packages/alphatab/src/NotationSettings.ts index 03124efde..e8df62b7e 100644 --- a/packages/alphatab/src/NotationSettings.ts +++ b/packages/alphatab/src/NotationSettings.ts @@ -212,7 +212,7 @@ export enum NotationElement { EffectLetRing = 23, /** - * The lyrics of the track shown above the staff. + * The lyrics of the track, placed as configured in `RenderStylesheet.lyricsPlacement`. */ EffectLyrics = 24, diff --git a/packages/alphatab/src/generated/SerializerSchema.ts b/packages/alphatab/src/generated/SerializerSchema.ts index f403f4ec2..cfaa1c042 100644 --- a/packages/alphatab/src/generated/SerializerSchema.ts +++ b/packages/alphatab/src/generated/SerializerSchema.ts @@ -12,5 +12,5 @@ export class SerializerSchema { /** * A hash of the field ids, keys and types of all serialized classes (50 classes). */ - public static readonly fingerprint: number = 0xb9644837; + public static readonly fingerprint: number = 0xc4694831; } diff --git a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts index 28be070fc..411295195 100644 --- a/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts +++ b/packages/alphatab/src/generated/model/RenderStylesheetSerializer.ts @@ -9,6 +9,7 @@ import { RecordFields } from "@coderline/alphatab/io/RecordFields"; import { BracketExtendMode } from "@coderline/alphatab/model/RenderStylesheet"; import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { TuningDisplayMode } from "@coderline/alphatab/model/RenderStylesheet"; +import { LyricsPlacement } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNamePolicy } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameOrientation } from "@coderline/alphatab/model/RenderStylesheet"; @@ -22,7 +23,7 @@ import type { IObjectWriter } from "@coderline/alphatab/io/IObjectWriter"; * @internal */ export class RenderStylesheetSerializer { - private static readonly _fields: RecordFields = new RecordFields(["hidedynamics", "bracketextendmode", "usesystemsignseparator", "globaldisplaytuning", "tuningdisplaymode", "pertrackdisplaytuning", "globaldisplaychorddiagramsontop", "pertrackchorddiagramsontop", "globaldisplaychorddiagramsinscore", "singletracktracknamepolicy", "multitracktracknamepolicy", "firstsystemtracknamemode", "othersystemstracknamemode", "firstsystemtracknameorientation", "othersystemstracknameorientation", "multitrackmultibarrest", "pertrackmultibarrest", "extendbarlines", "hideemptystaves", "hideemptystavesinfirstsystem", "showsinglestaffbrackets", "barnumberdisplay", "scoreconfig", "tabconfig", "slashconfig", "numberedconfig"]); + private static readonly _fields: RecordFields = new RecordFields(["hidedynamics", "bracketextendmode", "usesystemsignseparator", "globaldisplaytuning", "tuningdisplaymode", "lyricsplacement", "pertrackdisplaytuning", "globaldisplaychorddiagramsontop", "pertrackchorddiagramsontop", "globaldisplaychorddiagramsinscore", "singletracktracknamepolicy", "multitracktracknamepolicy", "firstsystemtracknamemode", "othersystemstracknamemode", "firstsystemtracknameorientation", "othersystemstracknameorientation", "multitrackmultibarrest", "pertrackmultibarrest", "extendbarlines", "hideemptystaves", "hideemptystavesinfirstsystem", "showsinglestaffbrackets", "barnumberdisplay", "scoreconfig", "tabconfig", "slashconfig", "numberedconfig"]); public static read(obj: RenderStylesheet, reader: IObjectReader): void { reader.readRecord(RenderStylesheetSerializer._fields, (id, k, r) => { switch (id) { @@ -42,66 +43,69 @@ export class RenderStylesheetSerializer { obj.tuningDisplayMode = JsonHelper.parseEnum(r.readValue(), TuningDisplayMode)!; return true; case 5: - RenderStylesheetSerializer._readPerTrackDisplayTuning(obj, r); + obj.lyricsPlacement = JsonHelper.parseEnum(r.readValue(), LyricsPlacement)!; return true; case 6: - obj.globalDisplayChordDiagramsOnTop = r.readBoolean(); + RenderStylesheetSerializer._readPerTrackDisplayTuning(obj, r); return true; case 7: - RenderStylesheetSerializer._readPerTrackChordDiagramsOnTop(obj, r); + obj.globalDisplayChordDiagramsOnTop = r.readBoolean(); return true; case 8: - obj.globalDisplayChordDiagramsInScore = r.readBoolean(); + RenderStylesheetSerializer._readPerTrackChordDiagramsOnTop(obj, r); return true; case 9: - obj.singleTrackTrackNamePolicy = JsonHelper.parseEnum(r.readValue(), TrackNamePolicy)!; + obj.globalDisplayChordDiagramsInScore = r.readBoolean(); return true; case 10: - obj.multiTrackTrackNamePolicy = JsonHelper.parseEnum(r.readValue(), TrackNamePolicy)!; + obj.singleTrackTrackNamePolicy = JsonHelper.parseEnum(r.readValue(), TrackNamePolicy)!; return true; case 11: - obj.firstSystemTrackNameMode = JsonHelper.parseEnum(r.readValue(), TrackNameMode)!; + obj.multiTrackTrackNamePolicy = JsonHelper.parseEnum(r.readValue(), TrackNamePolicy)!; return true; case 12: - obj.otherSystemsTrackNameMode = JsonHelper.parseEnum(r.readValue(), TrackNameMode)!; + obj.firstSystemTrackNameMode = JsonHelper.parseEnum(r.readValue(), TrackNameMode)!; return true; case 13: - obj.firstSystemTrackNameOrientation = JsonHelper.parseEnum(r.readValue(), TrackNameOrientation)!; + obj.otherSystemsTrackNameMode = JsonHelper.parseEnum(r.readValue(), TrackNameMode)!; return true; case 14: - obj.otherSystemsTrackNameOrientation = JsonHelper.parseEnum(r.readValue(), TrackNameOrientation)!; + obj.firstSystemTrackNameOrientation = JsonHelper.parseEnum(r.readValue(), TrackNameOrientation)!; return true; case 15: - obj.multiTrackMultiBarRest = r.readBoolean(); + obj.otherSystemsTrackNameOrientation = JsonHelper.parseEnum(r.readValue(), TrackNameOrientation)!; return true; case 16: - obj.perTrackMultiBarRest = new Set(r.readValue() as number[]); + obj.multiTrackMultiBarRest = r.readBoolean(); return true; case 17: - obj.extendBarLines = r.readBoolean(); + obj.perTrackMultiBarRest = new Set(r.readValue() as number[]); return true; case 18: - obj.hideEmptyStaves = r.readBoolean(); + obj.extendBarLines = r.readBoolean(); return true; case 19: - obj.hideEmptyStavesInFirstSystem = r.readBoolean(); + obj.hideEmptyStaves = r.readBoolean(); return true; case 20: - obj.showSingleStaffBrackets = r.readBoolean(); + obj.hideEmptyStavesInFirstSystem = r.readBoolean(); return true; case 21: - obj.barNumberDisplay = JsonHelper.parseEnum(r.readValue(), BarNumberDisplay)!; + obj.showSingleStaffBrackets = r.readBoolean(); return true; case 22: - ScoreStaffConfigSerializer.readFlexible(obj.scoreConfig, r); + obj.barNumberDisplay = JsonHelper.parseEnum(r.readValue(), BarNumberDisplay)!; return true; case 23: - TabStaffConfigSerializer.readFlexible(obj.tabConfig, r); + ScoreStaffConfigSerializer.readFlexible(obj.scoreConfig, r); return true; case 24: - SlashStaffConfigSerializer.readFlexible(obj.slashConfig, r); + TabStaffConfigSerializer.readFlexible(obj.tabConfig, r); return true; case 25: + SlashStaffConfigSerializer.readFlexible(obj.slashConfig, r); + return true; + case 26: NumberedStaffConfigSerializer.readFlexible(obj.numberedConfig, r); return true; } @@ -133,50 +137,51 @@ export class RenderStylesheetSerializer { w.booleanField(2, obj.useSystemSignSeparator); w.booleanField(3, obj.globalDisplayTuning); w.numberField(4, obj.tuningDisplayMode as number); + w.numberField(5, obj.lyricsPlacement as number); if (obj.perTrackDisplayTuning !== null) { - w.mapField(5); + w.mapField(6); for (const [k, v] of obj.perTrackDisplayTuning!) { w.prop(k.toString()); w.writeBoolean(v); } w.endMap(); } - w.booleanField(6, obj.globalDisplayChordDiagramsOnTop); + w.booleanField(7, obj.globalDisplayChordDiagramsOnTop); if (obj.perTrackChordDiagramsOnTop !== null) { - w.mapField(7); + w.mapField(8); for (const [k, v] of obj.perTrackChordDiagramsOnTop!) { w.prop(k.toString()); w.writeBoolean(v); } w.endMap(); } - w.booleanField(8, obj.globalDisplayChordDiagramsInScore); - w.numberField(9, obj.singleTrackTrackNamePolicy as number); - w.numberField(10, obj.multiTrackTrackNamePolicy as number); - w.numberField(11, obj.firstSystemTrackNameMode as number); - w.numberField(12, obj.otherSystemsTrackNameMode as number); - w.numberField(13, obj.firstSystemTrackNameOrientation as number); - w.numberField(14, obj.otherSystemsTrackNameOrientation as number); - w.booleanField(15, obj.multiTrackMultiBarRest); + w.booleanField(9, obj.globalDisplayChordDiagramsInScore); + w.numberField(10, obj.singleTrackTrackNamePolicy as number); + w.numberField(11, obj.multiTrackTrackNamePolicy as number); + w.numberField(12, obj.firstSystemTrackNameMode as number); + w.numberField(13, obj.otherSystemsTrackNameMode as number); + w.numberField(14, obj.firstSystemTrackNameOrientation as number); + w.numberField(15, obj.otherSystemsTrackNameOrientation as number); + w.booleanField(16, obj.multiTrackMultiBarRest); if (obj.perTrackMultiBarRest !== null) { const a: number[] = []; for (const v of obj.perTrackMultiBarRest!) { a.push(v); } - w.valueField(16, a); + w.valueField(17, a); } - w.booleanField(17, obj.extendBarLines); - w.booleanField(18, obj.hideEmptyStaves); - w.booleanField(19, obj.hideEmptyStavesInFirstSystem); - w.booleanField(20, obj.showSingleStaffBrackets); - w.numberField(21, obj.barNumberDisplay as number); - w.field(22); - ScoreStaffConfigSerializer.write(obj.scoreConfig, w); + w.booleanField(18, obj.extendBarLines); + w.booleanField(19, obj.hideEmptyStaves); + w.booleanField(20, obj.hideEmptyStavesInFirstSystem); + w.booleanField(21, obj.showSingleStaffBrackets); + w.numberField(22, obj.barNumberDisplay as number); w.field(23); - TabStaffConfigSerializer.write(obj.tabConfig, w); + ScoreStaffConfigSerializer.write(obj.scoreConfig, w); w.field(24); - SlashStaffConfigSerializer.write(obj.slashConfig, w); + TabStaffConfigSerializer.write(obj.tabConfig, w); w.field(25); + SlashStaffConfigSerializer.write(obj.slashConfig, w); + w.field(26); NumberedStaffConfigSerializer.write(obj.numberedConfig, w); w.endObject(); } diff --git a/packages/alphatab/src/importer/BinaryStylesheet.ts b/packages/alphatab/src/importer/BinaryStylesheet.ts index b0a9b68f0..e9b5b4a4d 100644 --- a/packages/alphatab/src/importer/BinaryStylesheet.ts +++ b/packages/alphatab/src/importer/BinaryStylesheet.ts @@ -8,6 +8,7 @@ import { Color } from '@coderline/alphatab/model/Color'; import { BarNumberDisplay, type BracketExtendMode, + LyricsPlacement, TrackNameMode, TrackNameOrientation, TrackNamePolicy, @@ -145,8 +146,16 @@ export class BinaryStylesheet { } public apply(score: Score): void { + let hideLyrics = false; + let drawLyricsUpside = false; for (const [key, value] of this.raw) { switch (key) { + case 'System/hideLyrics': + hideLyrics = value as boolean; + break; + case 'System/drawLyricsUpside': + drawLyricsUpside = value as boolean; + break; case 'StandardNotation/hideDynamics': score.stylesheet.hideDynamics = value as boolean; break; @@ -385,6 +394,12 @@ export class BinaryStylesheet { break; } } + + if (hideLyrics) { + score.stylesheet.lyricsPlacement = LyricsPlacement.Hidden; + } else if (drawLyricsUpside) { + score.stylesheet.lyricsPlacement = LyricsPlacement.Above; + } } private _toTextAlign(value: number): TextAlign { switch (value) { @@ -566,6 +581,16 @@ export class BinaryStylesheet { } binaryStylesheet.addValue('System/ExtendedBarLines', score.stylesheet.extendBarLines, DataType.Boolean); + binaryStylesheet.addValue( + 'System/hideLyrics', + score.stylesheet.lyricsPlacement === LyricsPlacement.Hidden, + DataType.Boolean + ); + binaryStylesheet.addValue( + 'System/drawLyricsUpside', + score.stylesheet.lyricsPlacement === LyricsPlacement.Above, + DataType.Boolean + ); const scoreStyle = score.style; if (scoreStyle) { diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 5e7ce159d..795b339f9 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -41,7 +41,7 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import { BarNumberDisplay, LyricsPlacement } from '@coderline/alphatab/model/RenderStylesheet'; import { Score } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; import { Staff } from '@coderline/alphatab/model/Staff'; @@ -282,6 +282,8 @@ export class MusicXmlImporter extends ScoreImporter { * Used to only store custom bar numbers where the measure number differs from it. */ private _nextBarNumber: number = 1; + private _lyricsCount: number = 0; + private _lyricsAboveCount: number = 0; private _divisionsPerQuarterNote: number = 1; /** @@ -308,6 +310,7 @@ export class MusicXmlImporter extends ScoreImporter { this._parseDom(dom); this._applySpans(); this._applyDynamics(); + this._applyLyricsPlacement(); ModelUtils.consolidate(this._score); this._score.finish(this.settings); this._score.rebuildRepeatGroups(); @@ -3654,7 +3657,19 @@ export class MusicXmlImporter extends ScoreImporter { newBeat.updateDurations(); } + private _applyLyricsPlacement() { + // the placement is per lyric, the stylesheet score-wide: above only if no lyric is below + if (this._lyricsCount > 0 && this._lyricsAboveCount === this._lyricsCount) { + this._score.stylesheet.lyricsPlacement = LyricsPlacement.Above; + } + } + private _parseLyric(element: XmlNode, beat: Beat, track: Track) { + this._lyricsCount++; + if (element.getAttribute('placement', '') === 'above') { + this._lyricsAboveCount++; + } + const info = this._indexToTrackInfo.get(track.index)!; const index = info.getLyricLine(element.getAttribute('number', '')); if (beat.lyrics === null) { diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts index 04ca53a3e..b82fba3d5 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1EnumMappings.ts @@ -23,6 +23,7 @@ import type { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import type { BarNumberDisplay, BracketExtendMode, + LyricsPlacement, TrackNameMode, TrackNameOrientation, TrackNamePolicy, @@ -222,6 +223,14 @@ export class AlphaTex1EnumMappings { public static readonly tuningDisplayModeReversed = AlphaTex1EnumMappings._reverse( AlphaTex1EnumMappings.tuningDisplayMode ); + public static readonly lyricsPlacement = new Map([ + ['below', 0], + ['above', 1], + ['hidden', 2] + ]); + public static readonly lyricsPlacementReversed = AlphaTex1EnumMappings._reverse( + AlphaTex1EnumMappings.lyricsPlacement + ); public static readonly textAlign = new Map([ ['left', 0], ['center', 1], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts index 52e1a97f8..6c694252b 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts @@ -182,6 +182,7 @@ export class AlphaTex1LanguageDefinitions { ['hidedynamics', null], ['usesystemsignseparator', null], ['tuningdisplaymode', [[[[100, 107], 0, ['score', 'staff']]]]], + ['lyricsplacement', [[[[100, 107], 0, ['below', 'above', 'hidden']]]]], ['multibarrest', null], ['bracketextendmode', [[[[100, 107], 0, ['nobrackets', 'groupstaves', 'groupsimilarinstruments']]]]], ['singletracktracknamepolicy', [[[[100, 107], 0, ['hidden', 'firstsystem', 'allsystems']]]]], @@ -540,6 +541,7 @@ export class AlphaTex1LanguageDefinitions { ['hidedynamics', null], ['usesystemsignseparator', null], ['tuningdisplaymode', null], + ['lyricsplacement', null], ['multibarrest', null], ['bracketextendmode', null], ['singletracktracknamepolicy', null], diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 8b0c97f97..377b393d6 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -79,7 +79,12 @@ import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; import { PickStroke } from '@coderline/alphatab/model/PickStroke'; -import { BarNumberDisplay, type RenderStylesheet, TuningDisplayMode } from '@coderline/alphatab/model/RenderStylesheet'; +import { + BarNumberDisplay, + LyricsPlacement, + type RenderStylesheet, + TuningDisplayMode +} from '@coderline/alphatab/model/RenderStylesheet'; import { HeaderFooterStyle, Score, ScoreStyle, ScoreSubElement } from '@coderline/alphatab/model/Score'; import { Section } from '@coderline/alphatab/model/Section'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; @@ -216,6 +221,18 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } score.stylesheet.tuningDisplayMode = tuningDisplayMode!; return ApplyNodeResult.Applied; + case 'lyricsplacement': + const lyricsPlacement = AlphaTex1LanguageHandler._parseEnumValue( + importer, + metaData.arguments!, + 'lyrics placement', + AlphaTex1EnumMappings.lyricsPlacement + ); + if (lyricsPlacement === undefined) { + return ApplyNodeResult.NotAppliedSemanticError; + } + score.stylesheet.lyricsPlacement = lyricsPlacement!; + return ApplyNodeResult.Applied; case 'multibarrest': score.stylesheet.multiTrackMultiBarRest = true; return ApplyNodeResult.Applied; @@ -2826,6 +2843,14 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler ) ); } + if (stylesheet.lyricsPlacement !== LyricsPlacement.Below) { + nodes.push( + Atnf.identMeta( + 'lyricsPlacement', + AlphaTex1EnumMappings.lyricsPlacementReversed.get(stylesheet.lyricsPlacement)! + ) + ); + } if (stylesheet.multiTrackMultiBarRest) { nodes.push(Atnf.meta('multiBarRest')); } diff --git a/packages/alphatab/src/model/RenderStylesheet.ts b/packages/alphatab/src/model/RenderStylesheet.ts index ac8df7a15..1b73b4085 100644 --- a/packages/alphatab/src/model/RenderStylesheet.ts +++ b/packages/alphatab/src/model/RenderStylesheet.ts @@ -117,6 +117,30 @@ export enum BarNumberDisplay { Hide = 2 } +/** + * Lists the places where lyrics are displayed. + * @since 1.10.0 + * @public + */ +export enum LyricsPlacement { + /** + * Lyrics are displayed below the standard notation staff. Without standard notation, + * they are displayed below the next shown staff (tablature, slash, numbered). + */ + Below = 0, + + /** + * Lyrics are displayed above the standard notation staff. Without standard notation, + * they are displayed above the next shown staff (tablature, slash, numbered). + */ + Above = 1, + + /** + * Lyrics are not displayed. + */ + Hidden = 2 +} + /** * This class represents the rendering stylesheet. * It contains settings which control the display of the score when rendered. @@ -151,6 +175,12 @@ export class RenderStylesheet { */ public tuningDisplayMode: TuningDisplayMode = TuningDisplayMode.Score; + /** + * The place where lyrics are displayed. + * @since 1.10.0 + */ + public lyricsPlacement: LyricsPlacement = LyricsPlacement.Below; + /** * Whether to show the tuning.(per-track) */ diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index e68b6e194..18696b985 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -52,7 +52,8 @@ export { TrackNameMode, TrackNameOrientation, TuningDisplayMode, - BarNumberDisplay + BarNumberDisplay, + LyricsPlacement } from '@coderline/alphatab/model/RenderStylesheet'; export { RepeatGroup } from '@coderline/alphatab/model/RepeatGroup'; export { Score, ScoreSubElement, ScoreStyle, HeaderFooterStyle } from '@coderline/alphatab/model/Score'; diff --git a/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts b/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts index b7d1e7713..e491c3249 100644 --- a/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts +++ b/packages/alphatab/src/rendering/effects/LyricsEffectInfo.ts @@ -1,4 +1,5 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; +import { LyricsPlacement } from '@coderline/alphatab/model/RenderStylesheet'; import { NotationElement } from '@coderline/alphatab/NotationSettings'; import { TextAlign } from '@coderline/alphatab/platform/ICanvas'; import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; @@ -9,26 +10,45 @@ import { LyricsGlyph } from '@coderline/alphatab/rendering/glyphs/LyricsGlyph'; import { OverlayRodPolicy } from '@coderline/alphatab/rendering/OverlayRodPolicy'; /** + * Creates the lyrics effect for one side of the staff. Glyphs are created only on the + * cascade-primary staff (standard notation, else the next shown one) and only for the + * `lyricsPlacement` of the score stylesheet. * @internal */ -export const lyricsEffectInfo: EffectInfo = { - effectId: 'EffectLyrics', - notationElement: NotationElement.EffectLyrics, - hideOnMultiTrack: false, - sizingMode: EffectBarGlyphSizing.SingleOnBeat, - placementCategory: EffectBandPlacementCategory.HorizontalRow, - overlayRodPolicy: OverlayRodPolicy.Centered, - shouldCreateGlyph: (_renderer: BarRendererBase, beat: Beat): boolean => { - return !!beat.lyrics; - }, - createNewGlyph: (renderer: BarRendererBase, beat: Beat): EffectGlyph => { - return new LyricsGlyph( - 0, - 0, - beat.lyrics!, - renderer.resources.elementFonts.get(NotationElement.EffectLyrics)!, - TextAlign.Center - ); - }, - canExpand: (_from: Beat, _to: Beat): boolean => true -}; +function createLyricsEffectInfo(placement: LyricsPlacement): EffectInfo { + return { + effectId: placement === LyricsPlacement.Above ? 'EffectLyricsAbove' : 'EffectLyrics', + notationElement: NotationElement.EffectLyrics, + hideOnMultiTrack: false, + sizingMode: EffectBarGlyphSizing.SingleOnBeat, + placementCategory: EffectBandPlacementCategory.HorizontalRow, + overlayRodPolicy: OverlayRodPolicy.Centered, + shouldCreateGlyph: (renderer: BarRendererBase, beat: Beat): boolean => { + return ( + !!beat.lyrics && + beat.voice.bar.staff.track.score.stylesheet.lyricsPlacement === placement && + renderer.staff!.isCascadePrimary + ); + }, + createNewGlyph: (renderer: BarRendererBase, beat: Beat): EffectGlyph => { + return new LyricsGlyph( + 0, + 0, + beat.lyrics!, + renderer.resources.elementFonts.get(NotationElement.EffectLyrics)!, + TextAlign.Center + ); + }, + canExpand: (_from: Beat, _to: Beat): boolean => true + }; +} + +/** + * @internal + */ +export const lyricsAboveEffectInfo: EffectInfo = createLyricsEffectInfo(LyricsPlacement.Above); + +/** + * @internal + */ +export const lyricsBelowEffectInfo: EffectInfo = createLyricsEffectInfo(LyricsPlacement.Below); diff --git a/packages/alphatab/test-data/guitarpro8/lyrics-placement-above-score-tab.gp b/packages/alphatab/test-data/guitarpro8/lyrics-placement-above-score-tab.gp new file mode 100644 index 000000000..30c9ea6be Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/lyrics-placement-above-score-tab.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/lyrics-placement-below-score-tab.gp b/packages/alphatab/test-data/guitarpro8/lyrics-placement-below-score-tab.gp new file mode 100644 index 000000000..83514b9b8 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/lyrics-placement-below-score-tab.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/lyrics-placement-below-tab.gp b/packages/alphatab/test-data/guitarpro8/lyrics-placement-below-tab.gp new file mode 100644 index 000000000..fdde8b09e Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/lyrics-placement-below-tab.gp differ diff --git a/packages/alphatab/test-data/guitarpro8/lyrics-placement-hide-score-tab.gp b/packages/alphatab/test-data/guitarpro8/lyrics-placement-hide-score-tab.gp new file mode 100644 index 000000000..4e47d5d2b Binary files /dev/null and b/packages/alphatab/test-data/guitarpro8/lyrics-placement-hide-score-tab.gp differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-above-score-tab.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-above-score-tab.png new file mode 100644 index 000000000..b89a5ff85 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-above-score-tab.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-above-tab.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-above-tab.png new file mode 100644 index 000000000..e06f15b70 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-above-tab.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-score-tab.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-score-tab.png new file mode 100644 index 000000000..823b0b7bf Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-score-tab.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-score.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-score.png new file mode 100644 index 000000000..6b2bca231 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-score.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-tab.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-tab.png new file mode 100644 index 000000000..ff5f5e504 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-below-tab.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-hidden.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-hidden.png new file mode 100644 index 000000000..af33175d0 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/lyrics-placement-hidden.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 6510342ad..1478d1a5e 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/overlay-rod-spacing/overlay-rod-multi-staff-aggregation.png b/packages/alphatab/test-data/visual-tests/overlay-rod-spacing/overlay-rod-multi-staff-aggregation.png index af6f0d8af..f39dc52fc 100644 Binary files a/packages/alphatab/test-data/visual-tests/overlay-rod-spacing/overlay-rod-multi-staff-aggregation.png and b/packages/alphatab/test-data/visual-tests/overlay-rod-spacing/overlay-rod-multi-staff-aggregation.png differ diff --git a/packages/alphatab/test/generated/SerializerFill.ts b/packages/alphatab/test/generated/SerializerFill.ts index 2410b1017..1e19800bd 100644 --- a/packages/alphatab/test/generated/SerializerFill.ts +++ b/packages/alphatab/test/generated/SerializerFill.ts @@ -138,6 +138,7 @@ import { RenderStylesheet } from "@coderline/alphatab/model/RenderStylesheet"; import { RenderStylesheetSerializer } from "@coderline/alphatab/generated/model/RenderStylesheetSerializer"; import { BracketExtendMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TuningDisplayMode } from "@coderline/alphatab/model/RenderStylesheet"; +import { LyricsPlacement } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNamePolicy } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameMode } from "@coderline/alphatab/model/RenderStylesheet"; import { TrackNameOrientation } from "@coderline/alphatab/model/RenderStylesheet"; @@ -1027,6 +1028,7 @@ export class SerializerFill { obj.useSystemSignSeparator = !obj.useSystemSignSeparator; obj.globalDisplayTuning = !obj.globalDisplayTuning; obj.tuningDisplayMode = SerializerFill.otherEnumValue(TuningDisplayMode, obj.tuningDisplayMode) as TuningDisplayMode; + obj.lyricsPlacement = SerializerFill.otherEnumValue(LyricsPlacement, obj.lyricsPlacement) as LyricsPlacement; { obj.perTrackDisplayTuning = new Map(); obj.perTrackDisplayTuning!.set(3, true); diff --git a/packages/alphatab/test/importer/AlphaTexImporter.test.ts b/packages/alphatab/test/importer/AlphaTexImporter.test.ts index 517db5d33..d28232b04 100644 --- a/packages/alphatab/test/importer/AlphaTexImporter.test.ts +++ b/packages/alphatab/test/importer/AlphaTexImporter.test.ts @@ -34,6 +34,7 @@ import { Rasgueado } from '@coderline/alphatab/model/Rasgueado'; import { BarNumberDisplay, BracketExtendMode, + LyricsPlacement, TrackNameMode, TrackNameOrientation, TrackNamePolicy, @@ -1850,6 +1851,7 @@ describe('AlphaTexImporterTest', () => { \\bracketExtendMode nobrackets \\useSystemSignSeparator \\tuningDisplayMode staff + \\lyricsPlacement above \\singleTrackTrackNamePolicy allsystems \\multiTrackTrackNamePolicy Hidden \\firstSystemTrackNameMode fullname @@ -1868,6 +1870,7 @@ describe('AlphaTexImporterTest', () => { expect(score.stylesheet.bracketExtendMode).toBe(BracketExtendMode.NoBrackets); expect(score.stylesheet.useSystemSignSeparator).toBe(true); expect(score.stylesheet.tuningDisplayMode).toBe(TuningDisplayMode.Staff); + expect(score.stylesheet.lyricsPlacement).toBe(LyricsPlacement.Above); expect(score.stylesheet.singleTrackTrackNamePolicy).toBe(TrackNamePolicy.AllSystems); expect(score.stylesheet.multiTrackTrackNamePolicy).toBe(TrackNamePolicy.Hidden); expect(score.stylesheet.firstSystemTrackNameMode).toBe(TrackNameMode.FullName); @@ -1877,6 +1880,15 @@ describe('AlphaTexImporterTest', () => { testExportRoundtrip(score); }); + it('lyrics-placement', () => { + expect(parseTex('.').stylesheet.lyricsPlacement).toBe(LyricsPlacement.Below); + expect(parseTex('\\lyricsPlacement below .').stylesheet.lyricsPlacement).toBe(LyricsPlacement.Below); + + const hidden = parseTex('\\lyricsPlacement hidden .'); + expect(hidden.stylesheet.lyricsPlacement).toBe(LyricsPlacement.Hidden); + testExportRoundtrip(hidden); + }); + it('bar-sizing', () => { const score = parseTex(` 3.3.4 | \\scale 0.5 3.3.4 | \\width 300 3.3.4 @@ -2430,6 +2442,7 @@ describe('AlphaTexImporterTest', () => { it('bar optional', () => importErrorTest('. \\section ("a" 1)')); it('bracketextendmode', () => importErrorTest('\\bracketextendmode invalid')); + it('lyricsplacement', () => importErrorTest('\\lyricsplacement invalid')); it('singletracktracknamepolicy', () => importErrorTest('\\singletracktracknamepolicy invalid')); it('multitracktracknamepolicy', () => importErrorTest('\\multitracktracknamepolicy invalid')); it('firstsystemtracknamemode', () => importErrorTest('\\firstsystemtracknamemode invalid')); diff --git a/packages/alphatab/test/importer/Gp8Importer.test.ts b/packages/alphatab/test/importer/Gp8Importer.test.ts index 8b3751611..3435ae5c6 100644 --- a/packages/alphatab/test/importer/Gp8Importer.test.ts +++ b/packages/alphatab/test/importer/Gp8Importer.test.ts @@ -8,6 +8,7 @@ import { Duration } from '@coderline/alphatab/model/Duration'; import { BarNumberDisplay, BracketExtendMode, + LyricsPlacement, TrackNameMode, TrackNameOrientation, TrackNamePolicy, @@ -564,4 +565,16 @@ describe('Gp8ImporterTest', () => { expect(score.stylesheet.tuningDisplayMode).toBe(TuningDisplayMode.Staff); }) + + async function lyricsPlacement(name: string): Promise { + const reader = await prepareImporterWithFile(`guitarpro8/lyrics-placement-${name}.gp`); + return reader.readScore().stylesheet.lyricsPlacement; + } + + it('lyrics-placement', async () => { + expect(await lyricsPlacement('above-score-tab')).toBe(LyricsPlacement.Above); + expect(await lyricsPlacement('below-score-tab')).toBe(LyricsPlacement.Below); + expect(await lyricsPlacement('below-tab')).toBe(LyricsPlacement.Below); + expect(await lyricsPlacement('hide-score-tab')).toBe(LyricsPlacement.Hidden); + }); }); diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts index 85b89019a..7569b8c09 100644 --- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts +++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts @@ -15,7 +15,7 @@ import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; import { PlaybackInformation } from '@coderline/alphatab/model/PlaybackInformation'; import type { Note } from '@coderline/alphatab/model/Note'; -import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; +import { BarNumberDisplay, LyricsPlacement } from '@coderline/alphatab/model/RenderStylesheet'; import type { Score } from '@coderline/alphatab/model/Score'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; @@ -2073,4 +2073,43 @@ describe('MusicXmlImporterTests', () => { expect(tempoPosition(' sound="yes"')).toBe(0.5); }); }); + + describe('lyrics-placement', () => { + function placement(lyricAttributes: string[]): LyricsPlacement { + let notes = ''; + for (const attributes of lyricAttributes) { + notes += `C41quarterla`; + } + const xml = `P11G2${notes}`; + const score = MusicXmlImporterTestHelper.prepareImporterWithBytes(IOHelper.stringToBytes(xml)).readScore(); + return score.stylesheet.lyricsPlacement; + } + + it('all-above', () => { + expect(placement([' placement="above"', ' placement="above"'])).toBe(LyricsPlacement.Above); + }); + + it('mixed', () => { + expect(placement([' placement="above"', ' placement="below"'])).toBe(LyricsPlacement.Below); + expect(placement([' placement="above"', ''])).toBe(LyricsPlacement.Below); + }); + + it('unspecified', () => { + expect(placement(['', ''])).toBe(LyricsPlacement.Below); + const noLyrics: string[] = []; + expect(placement(noLyrics)).toBe(LyricsPlacement.Below); + }); + + it('testsuite-mixed-voices', async () => { + // voice 1 above, voice 2 below the staff: the score-wide placement stays below + const twoVoices = await MusicXmlImporterTestHelper.loadFile( + 'test-data/musicxml-testsuite/42a-MultiVoice-TwoVoicesOnStaff-Lyrics.xml' + ); + expect(twoVoices.stylesheet.lyricsPlacement).toBe(LyricsPlacement.Below); + const multiStaff = await MusicXmlImporterTestHelper.loadFile( + 'test-data/musicxml-testsuite/43f-MultiStaff-Lyrics.xml' + ); + expect(multiStaff.stylesheet.lyricsPlacement).toBe(LyricsPlacement.Below); + }); + }); }); diff --git a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap index 26a102a43..6fe4c266f 100644 --- a/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap +++ b/packages/alphatab/test/importer/__snapshots__/AlphaTexImporter.test.ts.snap @@ -6170,6 +6170,26 @@ exports[`AlphaTexImporterTest > errors > at219 > gracetype > semantic-diagnostic ] `; +exports[`AlphaTexImporterTest > errors > at219 > lyricsplacement > lexer-diagnostics 1`] = `[]`; + +exports[`AlphaTexImporterTest > errors > at219 > lyricsplacement > parser-diagnostics 1`] = ` +[ + Map { + "code" => 219, + "severity" => 2, + "message" => "Error parsing arguments: no overload matched arguments (). Signatures: +(below|above|hidden)", + "start" => Map { + "col" => 17, + "line" => 1, + "offset" => 16, + }, + }, +] +`; + +exports[`AlphaTexImporterTest > errors > at219 > lyricsplacement > semantic-diagnostics 1`] = `[]`; + exports[`AlphaTexImporterTest > errors > at219 > multitracktracknamepolicy > lexer-diagnostics 1`] = `[]`; exports[`AlphaTexImporterTest > errors > at219 > multitracktracknamepolicy > parser-diagnostics 1`] = ` diff --git a/packages/alphatab/test/rendering/EffectPlacement.test.ts b/packages/alphatab/test/rendering/EffectPlacement.test.ts index 997690ccf..624aa161f 100644 --- a/packages/alphatab/test/rendering/EffectPlacement.test.ts +++ b/packages/alphatab/test/rendering/EffectPlacement.test.ts @@ -203,6 +203,71 @@ describe('EffectPlacementTests', () => { expect(band(staff.barRenderers[0], 'EffectLyrics', false)).toBeTruthy(); }); + describe('lyrics-placement', () => { + /** + * @returns the staves and sides with a lyrics band in the first bar, as `staffId-top` or `staffId-bottom` + */ + async function lyricsBands(placement: string, staff: string): Promise { + const renderer = await render(` + ${placement ? `\\lyricsPlacement ${placement}` : ''} + \\lyrics "this is my life" + \\staff {${staff}} + :4 3.3 5.3 7.3 8.3 + `); + const locations: string[] = []; + for (const renderStaff of renderer.layout!.systems[0].allStaves) { + const r = renderStaff.barRenderers[0]; + for (const top of [true, false]) { + for (const effectId of ['EffectLyrics', 'EffectLyricsAbove']) { + if (band(r, effectId, top)) { + locations.push(`${renderStaff.staffId}-${top ? 'top' : 'bottom'}`); + } + } + } + } + return locations; + } + + it('below-default-score-tab', async () => { + expect(await lyricsBands('', 'score tabs')).toEqual(['score-bottom']); + }); + + it('below-score-tab', async () => { + expect(await lyricsBands('below', 'score tabs')).toEqual(['score-bottom']); + }); + + it('below-tab', async () => { + expect(await lyricsBands('below', 'tabs')).toEqual(['tab-bottom']); + }); + + it('below-score', async () => { + expect(await lyricsBands('below', 'score')).toEqual(['score-bottom']); + }); + + it('below-slash', async () => { + expect(await lyricsBands('below', 'slash')).toEqual(['slash-bottom']); + }); + + it('above-score-tab', async () => { + expect(await lyricsBands('above', 'score tabs')).toEqual(['score-top']); + }); + + it('above-tab', async () => { + expect(await lyricsBands('above', 'tabs')).toEqual(['tab-top']); + }); + + it('above-slash-tab', async () => { + // tabs come before slash in the staff cascade + expect(await lyricsBands('above', 'slash tabs')).toEqual(['tab-top']); + }); + + it('hidden', async () => { + expect((await lyricsBands('hidden', 'score tabs')).length).toBe(0); + expect((await lyricsBands('hidden', 'tabs')).length).toBe(0); + expect((await lyricsBands('hidden', 'score')).length).toBe(0); + }); + }); + it('key-signature-skyline-per-accidental', async () => { // the flats of A flat major sit at different heights, each takes only its own width const renderer = await render(` diff --git a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts index b5b4448e1..c85894fd3 100644 --- a/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/packages/alphatab/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -631,6 +631,33 @@ describe('EffectsAndAnnotationsTests', () => { } ); }); + + describe('placement', () => { + async function test(name: string, placement: string, staff: string) { + await VisualTestHelper.runVisualTestTex( + ` + \\lyricsPlacement ${placement} + \\lyrics "Do Re Mi Fa So La Ti Do" + \\lyrics "One Two Three Four" + \\staff {${staff}} + :4 3.3{b (0 4)} 5.3{v} 7.3 8.3 | 5.2{b (0 4 0)} 5.2 7.2{v} 8.2 + `, + `test-data/visual-tests/effects-and-annotations/lyrics-placement-${name}.png`, + undefined, + o => { + // second system: the lyrics below must not run into it + o.settings.display.barsPerRow = 1; + } + ); + } + + it('below-score-tab', async () => await test('below-score-tab', 'below', 'score tabs')); + it('below-tab', async () => await test('below-tab', 'below', 'tabs')); + it('below-score', async () => await test('below-score', 'below', 'score')); + it('above-score-tab', async () => await test('above-score-tab', 'above', 'score tabs')); + it('above-tab', async () => await test('above-tab', 'above', 'tabs')); + it('hidden', async () => await test('hidden', 'hidden', 'score tabs')); + }); }); describe('hopo-arcs', () => { diff --git a/packages/alphatex/src/definitions.ts b/packages/alphatex/src/definitions.ts index 0770afb98..485110c79 100644 --- a/packages/alphatex/src/definitions.ts +++ b/packages/alphatex/src/definitions.ts @@ -44,6 +44,7 @@ import { systemsLayout } from '@coderline/alphatab-alphatex//metadata/score/syst import { tab } from '@coderline/alphatab-alphatex//metadata/score/tab'; import { title } from '@coderline/alphatab-alphatex//metadata/score/title'; import { tuningDisplayMode } from '@coderline/alphatab-alphatex//metadata/score/tuningdisplaymode'; +import { lyricsPlacement } from '@coderline/alphatab-alphatex/metadata/score/lyricsplacement'; import { useSystemSignSeparator } from '@coderline/alphatab-alphatex//metadata/score/usesystemsignseparator'; import { words } from '@coderline/alphatab-alphatex//metadata/score/words'; import { wordsAndMusic } from '@coderline/alphatab-alphatex//metadata/score/wordsandmusic'; @@ -185,6 +186,7 @@ export const scoreMetaData = metadata( hideDynamics, useSystemSignSeparator, tuningDisplayMode, + lyricsPlacement, multiBarRest, bracketExtendMode, singleTrackTrackNamePolicy, diff --git a/packages/alphatex/src/enum.ts b/packages/alphatex/src/enum.ts index 67f9c11b3..3f6777aa4 100644 --- a/packages/alphatex/src/enum.ts +++ b/packages/alphatex/src/enum.ts @@ -21,6 +21,7 @@ export const alphaTexMappedEnumLookup = { TrackNameOrientation: alphaTab.model.TrackNameOrientation, TrackNameMode: alphaTab.model.TrackNameMode, TuningDisplayMode: alphaTab.model.TuningDisplayMode, + LyricsPlacement: alphaTab.model.LyricsPlacement, TextAlign: alphaTab.platform.TextAlign, BendType: alphaTab.model.BendType, KeySignature: alphaTab.model.KeySignature, @@ -247,6 +248,11 @@ export const alphaTexMappedEnumMapping: { Score: { snippet: 'score', shortDescription: 'Display tuning information above the score.' }, Staff: { snippet: 'staff', shortDescription: 'Display tuning note names beside tab staff lines.' } }, + LyricsPlacement: { + Below: { snippet: 'below', shortDescription: 'Display lyrics below the standard notation staff.' }, + Above: { snippet: 'above', shortDescription: 'Display lyrics above the standard notation staff.' }, + Hidden: { snippet: 'hidden', shortDescription: 'Do not display lyrics.' } + }, TextAlign: { Left: { snippet: 'left', diff --git a/packages/alphatex/src/metadata/score/lyricsplacement.ts b/packages/alphatex/src/metadata/score/lyricsplacement.ts new file mode 100644 index 000000000..6f62cc64a --- /dev/null +++ b/packages/alphatex/src/metadata/score/lyricsplacement.ts @@ -0,0 +1,28 @@ +import * as alphaTab from '@coderline/alphatab'; +import { enumParameter } from '@coderline/alphatab-alphatex/enum'; +import type { MetadataTagDefinition } from '@coderline/alphatab-alphatex/types'; + +export const lyricsPlacement: MetadataTagDefinition = { + tag: '\\lyricsPlacement', + snippet: '\\lyricsPlacement ${1:above}$0', + shortDescription: 'Sets where lyrics are displayed.', + signatures: [ + { + parameters: [ + { + name: 'placement', + shortDescription: 'The placement to use', + parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.Required, + ...enumParameter('LyricsPlacement') + } + ] + } + ], + examples: ` + \\lyricsPlacement above + \\track "Guitar" + \\staff { score tabs } + \\lyrics "Do Re Mi Fa" + 0.6.4 2.6.4 3.6.4 0.5.4 + ` +}; diff --git a/packages/ui/src/player/settings/schema.ts b/packages/ui/src/player/settings/schema.ts index 4dfe8f4cb..eb08cf939 100644 --- a/packages/ui/src/player/settings/schema.ts +++ b/packages/ui/src/player/settings/schema.ts @@ -502,6 +502,7 @@ export function buildSchema(module: AlphaTabModule): SchemaGroup[] { stylesheet('useSystemSignSeparator', 'System sign separator', { type: 'toggle' }), stylesheet('globalDisplayTuning', 'Show guitar tuning', { type: 'toggle' }), stylesheet('tuningDisplayMode', 'Tuning placement', { type: 'enum', options: enumOptions(m.TuningDisplayMode as unknown as EnumObject, { Score: 'Above the score', Staff: 'At the staff' }) }), + stylesheet('lyricsPlacement', 'Lyrics placement', { type: 'enum', options: enumOptions(m.LyricsPlacement as unknown as EnumObject, { Below: 'Below the staff', Above: 'Above the staff', Hidden: 'Hidden' }) }), stylesheet('globalDisplayChordDiagramsOnTop', 'Chord diagrams on top', { type: 'toggle' }), stylesheet('globalDisplayChordDiagramsInScore', 'Chord diagrams in the score', { type: 'toggle' }), stylesheet('singleTrackTrackNamePolicy', 'Track names (single track)', { type: 'enum', options: enumOptions(m.TrackNamePolicy as unknown as EnumObject) }), diff --git a/website/docs/features/render/coverage-lyrics.mdx b/website/docs/features/render/coverage-lyrics.mdx index c7e37e285..077b1817b 100644 --- a/website/docs/features/render/coverage-lyrics.mdx +++ b/website/docs/features/render/coverage-lyrics.mdx @@ -11,7 +11,7 @@ keywords: import { AlphaTab } from '@site/src/components/AlphaTab'; -Lyrics render below the staff with each syllable positioned under the beat it belongs to. alphaTab reads lyric lines from the source file or accepts them programmatically through the API. +Lyrics render with each syllable positioned under the beat it belongs to. alphaTab reads lyric lines from the source file or accepts them programmatically through the API. ## Aligning to beats {#alignment} @@ -42,6 +42,18 @@ Multiple verses stack below the staff, one line per verse — the way hymnals an :4 G3 :8 C4 :4 E4 D4 | :4 C4 :8 D4 :4 E4 C4 `} +## Placement {#placement} + +[`score.stylesheet.lyricsPlacement`](/docs/reference/types/model/renderstylesheet/#lyricsplacement) decides where the lyrics go, like the lyrics setting of the Guitar Pro stylesheet. The placement is relative to the standard notation staff: [`LyricsPlacement.Below`](/docs/reference/types/model/lyricsplacement) (the default) shows them below it, `LyricsPlacement.Above` above it. When standard notation is not shown, the lyrics move to the next shown staff, in the order tablature, slash, numbered: a tab-only guitar part shows them below the tabs, which keeps the space above the tabs free for bends, whammy bar and vibrato. `LyricsPlacement.Hidden` hides the lyrics of the score. In alphaTex, use `\lyricsPlacement below|above|hidden`. The Guitar Pro importer and exporter read and write the setting; the MusicXML importer sets `Above` when all lyrics of the file are placed above the staff. + +{` +\\lyricsPlacement above +\\track "Guitar" +\\staff {score tabs} +\\lyrics "Bend it up and let it ring" +:4 7.3{b (0 4)} 9.3 :8 7.3 9.3 :4 7.2{v} | :2 9.3{b (0 4 0)} 7.3 +`} + ## Rendering fonts {#fonts} Lyrics use a dedicated font entry so a chart can restyle them without touching the rest of the score — the typical case for a hymnal that pairs a serif melody with a large sans-serif lyric line. Point `display.resources.wordsFont` at any registered family to change size, weight, or face. @@ -82,6 +94,7 @@ const api = new alphaTab.AlphaTabApi(container, { ## Where to look next - [Toggling notation elements off](./coverage-toggle-elements) — hide any category globally. +- [`RenderStylesheet.lyricsPlacement`](/docs/reference/types/model/renderstylesheet/#lyricsplacement) — the stylesheet entry for the lyrics placement. - [Custom text fonts](./custom-text-fonts) — configure the lyric font family and size. - [Font family fallbacks](./font-family-fallbacks) — handle missing font families gracefully. - [`notation.elements`](/docs/reference/settings/notation/elements) — the settings entry point for the element map. diff --git a/website/docs/releases/release1_10.mdx b/website/docs/releases/release1_10.mdx index e20f508f9..de0c380d0 100644 --- a/website/docs/releases/release1_10.mdx +++ b/website/docs/releases/release1_10.mdx @@ -39,6 +39,19 @@ Numbered staves write every note instead of a single digit per beat. A chord is The previous display with a single digit of the first voice, as in Guitar Pro, is available as the new [`NumberedNoteMode.SingleNote`](/docs/reference/types/model/numberednotemode); it now shows the highest note of a chord. Details: [Chords and multiple voices](/docs/features/render/numbered-notation#chords-and-multiple-voices). +#### render: lyrics above or below the staff, or hidden +https://github.com/CoderLine/alphaTab/issues/2776 + +The new [`score.stylesheet.lyricsPlacement`](/docs/reference/types/model/renderstylesheet/#lyricsplacement) places lyrics [`LyricsPlacement.Below`](/docs/reference/types/model/lyricsplacement) (default) or `LyricsPlacement.Above` the standard notation staff, or hides them with `LyricsPlacement.Hidden`, like the lyrics setting of the Guitar Pro stylesheet. Without standard notation, the lyrics move to the next shown staff (tablature, slash, numbered). alphaTex sets it with `\lyricsPlacement`, the Guitar Pro importer and exporter read and write it, and the MusicXML importer sets `Above` when all lyrics are placed above the staff. Details: [Lyrics placement](/docs/features/render/coverage-lyrics#placement). + +{` +\\lyricsPlacement above +\\track "Guitar" +\\staff {score tabs} +\\lyrics "Bend it up and let it ring" +:4 7.3{b (0 4)} 9.3 :8 7.3 9.3 :4 7.2{v} | :2 9.3{b (0 4 0)} 7.3 +`} + ### Kotlin #### kotlin: platform-independent alphaTab-core library @@ -52,6 +65,7 @@ alphaTab for Kotlin is published as two packages, like the .NET version. The new * Low octave dots on numbered staves sit below the duration lines instead of between digit and lines, as in printed jiǎnpǔ. * Numbered staves show all notes of chords and all voices. Set `notes` of the numbered staff configuration to `NumberedNoteMode.SingleNote` (alphaTex: `\defaultStaffDisplay numbered { notes singleNote }`) for one digit per beat; see [Single note mode](/docs/features/render/numbered-notation#single-note-mode). +* Lyrics of tab-only tracks are shown below the tab staff instead of above it. With standard notation and tabs, they sit directly below the standard notation staff. Set [`score.stylesheet.lyricsPlacement`](/docs/reference/types/model/renderstylesheet/#lyricsplacement) to `LyricsPlacement.Above` (alphaTex: `\lyricsPlacement above`) to show them above the staff. #### Kotlin packages