From 9c34ac5fcaf82097fbc7d7f769c430540a798941 Mon Sep 17 00:00:00 2001 From: Mateusz Date: Mon, 22 Jun 2026 16:10:58 +0200 Subject: [PATCH 1/6] feat: tuning on staff lines (#1) * feat: add inline tuning * feat: allow to adjust tuning padding * test: cover inline tuning rendering * refactor: create InlineTuningGlyph --- .../src/rendering/staves/StaffSystem.ts | 3 +- .../test/visualTests/features/Layout.test.ts | 76 +++++++++---------- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 19835b8bd..2b14b453c 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -1109,7 +1109,8 @@ export class StaffSystem { if (bracket.canPaint) { const barStartX: number = cx + bracket.firstVisibleStaffInBracket!.x; const barSize: number = bracket.width; - const barOffset: number = settings.display.accoladeBarPaddingRight; + const barOffset: number = + settings.display.accoladeBarPaddingRight + this._getInlineTuningWidthForBracket(bracket); const firstStart: number = cy + bracket.firstVisibleStaffInBracket!.contentTop; const lastEnd: number = cy + bracket.lastVisibleStaffInBracket!.contentBottom; let accoladeStart: number = firstStart; diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index 3ebe04c86..eb0747fe8 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -207,9 +207,9 @@ describe('LayoutTests', () => { await VisualTestHelper.runVisualTestTex( ` \\track { defaultSystemsLayout 3 } - \\scale 0.25 :1 c4 | \\scale 0.5 c4 | \\scale 0.25 c4 | + \\scale 0.25 :1 c4 | \\scale 0.5 c4 | \\scale 0.25 c4 | \\scale 0.5 c4 | \\scale 2 c4 | \\scale 0.5 c4 | - c4 | c4 + c4 | c4 `, 'test-data/visual-tests/layout/system-layout-tex.png', settings @@ -247,9 +247,9 @@ describe('LayoutTests', () => { it('multi-system-slur-scale-down', async () => { await VisualTestHelper.runVisualTestTex( ` - C4 {slur S1} + C4 {slur S1} | r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r - A4 {slur S1} + A4 {slur S1} `, '', undefined, @@ -268,9 +268,9 @@ describe('LayoutTests', () => { it('multi-system-slur-scale-up', async () => { await VisualTestHelper.runVisualTestTex( ` - C4 {slur S1} + C4 {slur S1} | r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r| r - A4 {slur S1} + A4 {slur S1} `, '', undefined, @@ -309,16 +309,16 @@ describe('LayoutTests', () => { \\track "T1" C4.4 *4 | r.1 | r.1 | r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | - + \\track "T2" \\clef C3 - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | c4 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | `, 'test-data/visual-tests/layout/hide-empty-staves.png', @@ -341,16 +341,16 @@ describe('LayoutTests', () => { \\track "T1" C4.4 *4 | r.1 | r.1 | r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | - + \\track "T2" \\clef C3 - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | c4 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | `, 'test-data/visual-tests/layout/hide-empty-staves-in-first.png', @@ -373,15 +373,15 @@ describe('LayoutTests', () => { \\staff {score} C4.4 *4 | r.1 | r.1 | r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | \\staff {score} \\clef C3 - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | c4 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | `, 'test-data/visual-tests/layout/single-staff-brackets-show.png', @@ -401,15 +401,15 @@ describe('LayoutTests', () => { \\staff {score} C4.4 *4 | r.1 | r.1 | r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | \\staff {score} \\clef C3 - r.1 | r.1 | r.1 | - r.1 | r.1 | r.1 | - r.1 | c4 | r.1 | - r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | r.1 | r.1 | + r.1 | c4 | r.1 | + r.1 | r.1 | r.1 | r.1 | C4 | `, 'test-data/visual-tests/layout/single-staff-brackets-hide.png', @@ -488,7 +488,7 @@ describe('LayoutTests', () => { ` \\defaultBarNumberDisplay allBars C4.1 | C4.1 | C4.1 | - C4.1 | C4.1 | C4.1 + C4.1 | C4.1 | C4.1 `, 'test-data/visual-tests/layout/barnumberdisplay-stylesheet-all.png', undefined, @@ -501,7 +501,7 @@ describe('LayoutTests', () => { ` \\defaultBarNumberDisplay firstOfSystem C4.1 | C4.1 | C4.1 | - C4.1 | C4.1 | C4.1 + C4.1 | C4.1 | C4.1 `, 'test-data/visual-tests/layout/barnumberdisplay-stylesheet-first.png', undefined, @@ -514,7 +514,7 @@ describe('LayoutTests', () => { ` \\defaultBarNumberDisplay hide C4.1 | C4.1 | C4.1 | - C4.1 | C4.1 | C4.1 + C4.1 | C4.1 | C4.1 `, 'test-data/visual-tests/layout/barnumberdisplay-stylesheet-hide.png', undefined, @@ -530,7 +530,7 @@ describe('LayoutTests', () => { ` \\defaultBarNumberDisplay allBars C4.1 | \\barNumberDisplay hide C4.1 | C4.1 | - C4.1 | C4.1 | C4.1 + C4.1 | C4.1 | C4.1 `, 'test-data/visual-tests/layout/barnumberdisplay-bar-override-all.png', undefined, @@ -543,7 +543,7 @@ describe('LayoutTests', () => { ` \\defaultBarNumberDisplay firstOfSystem C4.1 | \\barNumberDisplay allBars C4.1 | C4.1 | - \\barNumberDisplay hide C4.1 | C4.1 | C4.1 + \\barNumberDisplay hide C4.1 | C4.1 | C4.1 `, 'test-data/visual-tests/layout/barnumberdisplay-bar-override-first.png', undefined, @@ -556,7 +556,7 @@ describe('LayoutTests', () => { ` \\defaultBarNumberDisplay hide C4.1 | \\barNumberDisplay allBars C4.1 | C4.1 | - \\barNumberDisplay firstOfSystem C4.1 | \\barNumberDisplay firstOfSystem C4.1 | C4.1 + \\barNumberDisplay firstOfSystem C4.1 | \\barNumberDisplay firstOfSystem C4.1 | C4.1 `, 'test-data/visual-tests/layout/barnumberdisplay-bar-override-hide.png', undefined, From 72b5456c3bfe07b8417cb89aae05c14b6fafff59 Mon Sep 17 00:00:00 2001 From: Mateusz Date: Sun, 28 Jun 2026 11:24:34 +0200 Subject: [PATCH 2/6] feat: add playback selection on mobile (#2) * feat: add playback selection on mobile * feat: tuning toggle between flat and sharp * fix: more responsive selection * fix: more reliable touch support --- packages/alphatab/src/DisplaySettings.ts | 9 + .../alphatab/src/exporter/AlphaTexExporter.ts | 22 +- .../src/generated/DisplaySettingsJson.ts | 8 + .../generated/DisplaySettingsSerializer.ts | 5 + .../alphaTex/AlphaTex1LanguageHandler.ts | 27 +- .../IAlphaTexLanguageImportHandler.ts | 4 +- packages/alphatab/src/model/Tuning.ts | 36 +- packages/alphatab/src/model/_barrel.ts | 2 +- .../javascript/BrowserMouseEventArgs.ts | 9 +- .../platform/javascript/BrowserUiFacade.ts | 6 +- .../javascript/HtmlElementContainer.ts | 361 ++++++++++-- .../src/rendering/glyphs/InlineTuningGlyph.ts | 9 +- .../src/rendering/glyphs/TuningGlyph.ts | 6 +- .../test/exporter/AlphaTexExporter.test.ts | 14 +- .../alphatab/test/model/TuningParser.test.ts | 9 +- .../javascript/HtmlElementContainer.test.ts | 514 ++++++++++++++++++ 16 files changed, 982 insertions(+), 59 deletions(-) create mode 100644 packages/alphatab/test/platform/javascript/HtmlElementContainer.test.ts diff --git a/packages/alphatab/src/DisplaySettings.ts b/packages/alphatab/src/DisplaySettings.ts index 0782d660f..c99a5649c 100644 --- a/packages/alphatab/src/DisplaySettings.ts +++ b/packages/alphatab/src/DisplaySettings.ts @@ -2,6 +2,7 @@ import { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { StaveProfile } from '@coderline/alphatab/StaveProfile'; import type { Staff } from '@coderline/alphatab/model/Staff'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; /** * Lists the different modes in which the staves and systems are arranged. @@ -331,6 +332,14 @@ export class DisplaySettings { */ public inlineTuningPaddingRight: number = 5; + /** + * The accidental style used to display tuning note names. + * @since 1.9.0 + * @category Display + * @defaultValue `TuningAccidentalMode.Flat` + */ + public tuningAccidentalMode: TuningAccidentalMode = TuningAccidentalMode.Flat; + // Staff padding /** diff --git a/packages/alphatab/src/exporter/AlphaTexExporter.ts b/packages/alphatab/src/exporter/AlphaTexExporter.ts index 57d290dff..4dcd8c01f 100644 --- a/packages/alphatab/src/exporter/AlphaTexExporter.ts +++ b/packages/alphatab/src/exporter/AlphaTexExporter.ts @@ -512,7 +512,13 @@ export class AlphaTexExporter extends ScoreExporter { if (data.bars.length === 0) { const bar: AlphaTexBarNode = { nodeType: AlphaTexNodeType.Bar, - metaData: this._handler.buildBarMetaDataNodes(data, undefined, 0, false), + metaData: this._handler.buildBarMetaDataNodes( + data, + undefined, + 0, + false, + this.settings.display.tuningAccidentalMode + ), beats: [], pipe: undefined }; @@ -533,7 +539,13 @@ export class AlphaTexExporter extends ScoreExporter { private _bar(score: AlphaTexScoreNode, data: Bar, voiceIndex: number, isMultiVoice: boolean) { const bar: AlphaTexBarNode = { nodeType: AlphaTexNodeType.Bar, - metaData: this._handler.buildBarMetaDataNodes(data.staff, data, voiceIndex, isMultiVoice), + metaData: this._handler.buildBarMetaDataNodes( + data.staff, + data, + voiceIndex, + isMultiVoice, + this.settings.display.tuningAccidentalMode + ), beats: [], pipe: undefined }; @@ -677,7 +689,11 @@ export class AlphaTexExporter extends ScoreExporter { } else if (data.isPiano) { note.noteValue = { nodeType: AlphaTexNodeType.Ident, - text: Tuning.getTextForTuning(data.realValueWithoutHarmonic, true) + text: Tuning.getTextForTuning( + data.realValueWithoutHarmonic, + true, + this.settings.display.tuningAccidentalMode + ) } as AlphaTexIdentifier; } else if (data.isStringed) { note.noteValue = { diff --git a/packages/alphatab/src/generated/DisplaySettingsJson.ts b/packages/alphatab/src/generated/DisplaySettingsJson.ts index 99aedb8f0..6cc7afae2 100644 --- a/packages/alphatab/src/generated/DisplaySettingsJson.ts +++ b/packages/alphatab/src/generated/DisplaySettingsJson.ts @@ -6,6 +6,7 @@ import { LayoutMode } from "@coderline/alphatab/LayoutMode"; import { StaveProfile } from "@coderline/alphatab/StaveProfile"; import { RenderingResourcesJson } from "@coderline/alphatab/generated/RenderingResourcesJson"; +import { TuningAccidentalMode } from "@coderline/alphatab/model/Tuning"; import { SystemsLayoutMode } from "@coderline/alphatab/DisplaySettings"; /** * The display settings control how the general layout and display of alphaTab is done. @@ -293,6 +294,13 @@ export interface DisplaySettingsJson { * @defaultValue `5` */ inlineTuningPaddingRight?: number; + /** + * The accidental style used to display tuning note names. + * @since 1.9.0 + * @category Display + * @defaultValue `TuningAccidentalMode.Flat` + */ + tuningAccidentalMode?: TuningAccidentalMode | keyof typeof TuningAccidentalMode | Lowercase; /** * The top padding applied to the first main notation staff (standard, tabs, numbered, slash). * @since 1.8.0 diff --git a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts index 03ed85be3..e0ab33aba 100644 --- a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts +++ b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts @@ -8,6 +8,7 @@ import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { RenderingResourcesSerializer } from "@coderline/alphatab/generated/RenderingResourcesSerializer"; import { LayoutMode } from "@coderline/alphatab/LayoutMode"; import { StaveProfile } from "@coderline/alphatab/StaveProfile"; +import { TuningAccidentalMode } from "@coderline/alphatab/model/Tuning"; import { SystemsLayoutMode } from "@coderline/alphatab/DisplaySettings"; /** * @internal @@ -44,6 +45,7 @@ export class DisplaySettingsSerializer { o.set("systemlabelpaddingright", obj.systemLabelPaddingRight); o.set("accoladebarpaddingright", obj.accoladeBarPaddingRight); o.set("inlinetuningpaddingright", obj.inlineTuningPaddingRight); + o.set("tuningaccidentalmode", obj.tuningAccidentalMode as number); o.set("firstnotationstaffpaddingtop", obj.firstNotationStaffPaddingTop); o.set("lastnotationstaffpaddingbottom", obj.lastNotationStaffPaddingBottom); o.set("notationstaffpaddingtop", obj.notationStaffPaddingTop); @@ -120,6 +122,9 @@ export class DisplaySettingsSerializer { case "inlinetuningpaddingright": obj.inlineTuningPaddingRight = v! as number; return true; + case "tuningaccidentalmode": + obj.tuningAccidentalMode = JsonHelper.parseEnum(v, TuningAccidentalMode)!; + return true; case "firstnotationstaffpaddingtop": obj.firstNotationStaffPaddingTop = v! as number; return true; diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index 0bd5ab102..c1ea5a397 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -81,7 +81,7 @@ import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { Tuning, TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; @@ -2780,11 +2780,19 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff: Staff, bar: Bar | undefined, voice: number, - isMultiVoice: boolean + isMultiVoice: boolean, + tuningAccidentalMode: TuningAccidentalMode ): AlphaTexMetaDataNode[] { const nodes: AlphaTexMetaDataNode[] = []; - AlphaTex1LanguageHandler._buildStructuralMetaDataNodes(bar, staff, nodes, isMultiVoice, voice); + AlphaTex1LanguageHandler._buildStructuralMetaDataNodes( + bar, + staff, + nodes, + isMultiVoice, + voice, + tuningAccidentalMode + ); if (!bar) { return nodes; } @@ -2880,7 +2888,11 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler return nodes; } - private static _buildStaffMetaDataNodes(nodes: AlphaTexMetaDataNode[], staff: Staff) { + private static _buildStaffMetaDataNodes( + nodes: AlphaTexMetaDataNode[], + staff: Staff, + tuningAccidentalMode: TuningAccidentalMode + ) { const firstStaffMetaIndex = nodes.length; if (staff.capo !== 0) { @@ -2893,7 +2905,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler 'tuning', Atnf.args( staff.stringTuning.tunings.map( - t => Atnf.ident(Tuning.getTextForTuning(t, true)) as IAlphaTexArgumentValue + t => Atnf.ident(Tuning.getTextForTuning(t, true, tuningAccidentalMode)) as IAlphaTexArgumentValue ) ) ); @@ -3091,7 +3103,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff: Staff, nodes: AlphaTexMetaDataNode[], isMultiVoice: boolean, - voice: number + voice: number, + tuningAccidentalMode: TuningAccidentalMode ) { if (bar === undefined || bar.index === 0) { if (voice === 0) { @@ -3099,7 +3112,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(AlphaTex1LanguageHandler._buildNewTrackNode(staff.track)); } nodes.push(AlphaTex1LanguageHandler._buildNewStaffNode(staff)); - AlphaTex1LanguageHandler._buildStaffMetaDataNodes(nodes, staff); + AlphaTex1LanguageHandler._buildStaffMetaDataNodes(nodes, staff, tuningAccidentalMode); } if (isMultiVoice) { diff --git a/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts b/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts index 39a792c6e..1db9afc5e 100644 --- a/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts @@ -5,6 +5,7 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Score } from '@coderline/alphatab/model/Score'; import type { Staff } from '@coderline/alphatab/model/Staff'; +import type { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; /** * @internal @@ -54,7 +55,8 @@ export interface IAlphaTexLanguageImportHandler { staff: Staff, bar: Bar | undefined, voice: number, - isMultiVoice: boolean + isMultiVoice: boolean, + tuningAccidentalMode: TuningAccidentalMode ): AlphaTexMetaDataNode[]; buildSyncPointNodes(score: Score): AlphaTexMetaDataNode[]; buildBeatEffects(beat: Beat): AlphaTexPropertyNode[]; diff --git a/packages/alphatab/src/model/Tuning.ts b/packages/alphatab/src/model/Tuning.ts index a28020922..5bb34300f 100644 --- a/packages/alphatab/src/model/Tuning.ts +++ b/packages/alphatab/src/model/Tuning.ts @@ -1,3 +1,19 @@ +/** + * Lists the different accidental styles used to format tuning note names. + * @public + */ +export enum TuningAccidentalMode { + /** + * Use flat note names for enharmonic tuning notes. + */ + Flat = 0, + + /** + * Use sharp note names for enharmonic tuning notes. + */ + Sharp = 1 +} + /** * This public class represents a predefined string tuning. * @json @@ -11,17 +27,27 @@ export class Tuning { private static _fourStrings: Tuning[] = []; private static _defaultTunings: Map = new Map(); - public static readonly noteNames: string[] = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; + public static readonly flatNoteNames: string[] = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; + public static readonly sharpNoteNames: string[] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + public static readonly noteNames: string[] = Tuning.flatNoteNames; - public static getTextForTuning(tuning: number, includeOctave: boolean): string { - const parts = Tuning.getTextPartsForTuning(tuning); + public static getTextForTuning( + tuning: number, + includeOctave: boolean, + accidentalMode: TuningAccidentalMode = TuningAccidentalMode.Flat + ): string { + const parts = Tuning.getTextPartsForTuning(tuning, -1, accidentalMode); return includeOctave ? parts.join('') : parts[0]; } - public static getTextPartsForTuning(tuning: number, octaveShift: number = -1): string[] { + public static getTextPartsForTuning( + tuning: number, + octaveShift: number = -1, + accidentalMode: TuningAccidentalMode = TuningAccidentalMode.Flat + ): string[] { const octave: number = (tuning / 12) | 0; const note: number = tuning % 12; - const notes: string[] = Tuning.noteNames; + const notes = accidentalMode === TuningAccidentalMode.Sharp ? Tuning.sharpNoteNames : Tuning.flatNoteNames; return [notes[note], (octave + octaveShift).toString()]; } diff --git a/packages/alphatab/src/model/_barrel.ts b/packages/alphatab/src/model/_barrel.ts index 5edccbd36..69bad5bd9 100644 --- a/packages/alphatab/src/model/_barrel.ts +++ b/packages/alphatab/src/model/_barrel.ts @@ -63,7 +63,7 @@ export { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; export { Staff } from '@coderline/alphatab/model/Staff'; export { Track, TrackSubElement, TrackStyle } from '@coderline/alphatab/model/Track'; export { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -export { Tuning } from '@coderline/alphatab/model/Tuning'; +export { Tuning, TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; export { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; export { VibratoType } from '@coderline/alphatab/model/VibratoType'; export { Voice, VoiceSubElement, VoiceStyle } from '@coderline/alphatab/model/Voice'; diff --git a/packages/alphatab/src/platform/javascript/BrowserMouseEventArgs.ts b/packages/alphatab/src/platform/javascript/BrowserMouseEventArgs.ts index 5e72ada6b..1a13498d1 100644 --- a/packages/alphatab/src/platform/javascript/BrowserMouseEventArgs.ts +++ b/packages/alphatab/src/platform/javascript/BrowserMouseEventArgs.ts @@ -9,6 +9,8 @@ import type { HtmlElementContainer } from '@coderline/alphatab/platform/javascri export class BrowserMouseEventArgs implements IMouseEventArgs { public readonly mouseEvent: MouseEvent; + private readonly _allowPreventDefault: boolean; + public get isLeftMouseButton(): boolean { return this.mouseEvent.button === 0; } @@ -28,10 +30,13 @@ export class BrowserMouseEventArgs implements IMouseEventArgs { } public preventDefault(): void { - this.mouseEvent.preventDefault(); + if (this._allowPreventDefault) { + this.mouseEvent.preventDefault(); + } } - public constructor(e: MouseEvent) { + public constructor(e: MouseEvent, allowPreventDefault: boolean = true) { this.mouseEvent = e; + this._allowPreventDefault = allowPreventDefault; } } diff --git a/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts b/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts index 4f2c7d147..ac10d4f97 100644 --- a/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts +++ b/packages/alphatab/src/platform/javascript/BrowserUiFacade.ts @@ -277,7 +277,11 @@ export class BrowserUiFacade implements IUiFacade { canvasElement.style.overflow = 'hidden'; canvasElement.style.lineHeight = '0'; canvasElement.style.position = 'relative'; - return new HtmlElementContainer(canvasElement); + return new HtmlElementContainer( + canvasElement, + () => this.getScrollContainer(), + () => this._api.settings.player.enableUserInteraction + ); } public setCanvasOverflow(canvasElement: IContainer, overflow: number, isVertical: boolean): void { diff --git a/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts b/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts index a440ec5eb..cdadc30e4 100644 --- a/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts +++ b/packages/alphatab/src/platform/javascript/HtmlElementContainer.ts @@ -17,11 +17,19 @@ export interface IHtmlElementContainer extends IContainer{ readonly element:HTMLElement; } +type MouseEventListener = (arg: IMouseEventArgs) => void; + /** * @target web * @internal */ export class HtmlElementContainer implements IHtmlElementContainer { + private static readonly _touchLongPressDelay = 60; + private static readonly _touchMoveSlop = 80; + private static readonly _edgeScrollThreshold = 50; + private static readonly _edgeScrollMaxStep = 32; + private static readonly _compatibilityMouseSuppressionDelay = 800; + private static _resizeObserver: Lazy = new Lazy( () => new ResizeObserver((entries: ResizeObserverEntry[]) => { @@ -35,6 +43,24 @@ export class HtmlElementContainer implements IHtmlElementContainer { ); private _resizeListeners: number = 0; + private _mouseDownListeners: MouseEventListener[] = []; + private _mouseMoveListeners: MouseEventListener[] = []; + private _mouseUpListeners: MouseEventListener[] = []; + private _nativeMouseListenersActive = false; + private _activeTouchPointerId: number | null = null; + private _pendingTouchDown: PointerEvent | null = null; + private _touchLongPressTimer: ReturnType | null = null; + private _touchSelectionActive = false; + private _touchGestureCancelled = false; + private _touchPointerCaptured = false; + private _suppressCompatibilityMouseEvents = false; + private _compatibilityMouseSuppressionTimer: ReturnType | null = null; + private _touchStartPageX = 0; + private _touchStartPageY = 0; + private readonly _touchMoveOptions: AddEventListenerOptions = { + capture: true, + passive: false + }; public get width(): number { return this.element.offsetWidth; @@ -78,55 +104,35 @@ export class HtmlElementContainer implements IHtmlElementContainer { public readonly element: HTMLElement; - public constructor(element: HTMLElement) { + public constructor( + element: HTMLElement, + private readonly _edgeScrollContainer: (() => IContainer) | null = null, + private readonly _canStartTouchSelection: () => boolean = () => true + ) { this.element = element; this.mouseDown = { on: (value: any) => { - const nativeListener: (e: MouseEvent) => void = e => { - value(new BrowserMouseEventArgs(e)); - }; - this.element.addEventListener('mousedown', nativeListener, true); - return () => { - this.element.removeEventListener('mousedown', nativeListener, true); - }; + this._addMouseEventListener(this._mouseDownListeners, value); + return () => this._removeMouseEventListener(this._mouseDownListeners, value); }, - off: (_value: any) => { - // not supported due to wrapping - } + off: (value: any) => this._removeMouseEventListener(this._mouseDownListeners, value) }; this.mouseUp = { on: (value: any) => { - const nativeListener: (e: MouseEvent) => void = e => { - value(new BrowserMouseEventArgs(e)); - }; - - this.element.addEventListener('mouseup', nativeListener, true); - - return () => { - this.element.removeEventListener('mouseup', nativeListener, true); - }; + this._addMouseEventListener(this._mouseUpListeners, value); + return () => this._removeMouseEventListener(this._mouseUpListeners, value); }, - off: (_value: any) => { - // not supported due to wrapping - } + off: (value: any) => this._removeMouseEventListener(this._mouseUpListeners, value) }; this.mouseMove = { on: (value: any) => { - const nativeListener: (e: MouseEvent) => void = e => { - value(new BrowserMouseEventArgs(e)); - }; - this.element.addEventListener('mousemove', nativeListener, true); - - return () => { - this.element.removeEventListener('mousemove', nativeListener, true); - }; + this._addMouseEventListener(this._mouseMoveListeners, value); + return () => this._removeMouseEventListener(this._mouseMoveListeners, value); }, - off: (_: any) => { - // not supported due to wrapping - } + off: (value: any) => this._removeMouseEventListener(this._mouseMoveListeners, value) }; const container = this; @@ -151,6 +157,293 @@ export class HtmlElementContainer implements IHtmlElementContainer { }; } + private _addMouseEventListener(listeners: MouseEventListener[], value: MouseEventListener): void { + listeners.push(value); + this._ensureNativeMouseListeners(); + } + + private _removeMouseEventListener(listeners: MouseEventListener[], value: MouseEventListener): void { + const index = listeners.indexOf(value); + if (index >= 0) { + listeners.splice(index, 1); + } + this._releaseNativeMouseListenersIfUnused(); + } + + private _ensureNativeMouseListeners(): void { + if (this._nativeMouseListenersActive) { + return; + } + + this.element.addEventListener('mousedown', this._onMouseDown, true); + this.element.addEventListener('mousemove', this._onMouseMove, true); + this.element.addEventListener('mouseup', this._onMouseUp, true); + if (this._supportsPointerEvents) { + this.element.addEventListener('pointerdown', this._onPointerDown, true); + this.element.addEventListener('pointermove', this._onPointerMove, true); + this.element.addEventListener('pointerup', this._onPointerUp, true); + this.element.addEventListener('pointercancel', this._onPointerCancel, true); + } + this._nativeMouseListenersActive = true; + } + + private _releaseNativeMouseListenersIfUnused(): void { + if ( + !this._nativeMouseListenersActive || + this._mouseDownListeners.length + this._mouseMoveListeners.length + this._mouseUpListeners.length > 0 + ) { + return; + } + + this._resetTouchGesture(true); + this._clearCompatibilityMouseSuppression(); + this.element.removeEventListener('mousedown', this._onMouseDown, true); + this.element.removeEventListener('mousemove', this._onMouseMove, true); + this.element.removeEventListener('mouseup', this._onMouseUp, true); + if (this._supportsPointerEvents) { + this.element.removeEventListener('pointerdown', this._onPointerDown, true); + this.element.removeEventListener('pointermove', this._onPointerMove, true); + this.element.removeEventListener('pointerup', this._onPointerUp, true); + this.element.removeEventListener('pointercancel', this._onPointerCancel, true); + } + this._nativeMouseListenersActive = false; + } + + private get _supportsPointerEvents(): boolean { + const ownerWindow = this.element.ownerDocument?.defaultView; + return !!ownerWindow && 'PointerEvent' in ownerWindow; + } + + private readonly _onMouseDown = (e: MouseEvent): void => { + if (this._suppressCompatibilityMouseEvents) { + return; + } + this._emitMouseEvent(this._mouseDownListeners, e); + }; + + private readonly _onMouseMove = (e: MouseEvent): void => { + if (this._suppressCompatibilityMouseEvents) { + return; + } + this._emitMouseEvent(this._mouseMoveListeners, e); + }; + + private readonly _onMouseUp = (e: MouseEvent): void => { + if (this._suppressCompatibilityMouseEvents) { + return; + } + this._emitMouseEvent(this._mouseUpListeners, e); + }; + + private readonly _onPointerDown = (e: PointerEvent): void => { + if (e.pointerType !== 'touch') { + return; + } + + if (!e.isPrimary || this._activeTouchPointerId !== null) { + return; + } + + this._activeTouchPointerId = e.pointerId; + this._pendingTouchDown = e; + this._touchGestureCancelled = false; + this._touchSelectionActive = false; + this._touchStartPageX = e.pageX; + this._touchStartPageY = e.pageY; + this._clearTouchLongPressTimer(); + if (this._canStartTouchSelection()) { + this._touchLongPressTimer = setTimeout(() => { + if ( + this._activeTouchPointerId === e.pointerId && + !this._touchGestureCancelled && + this._canStartTouchSelection() + ) { + this._activateTouchSelection(e); + } + }, HtmlElementContainer._touchLongPressDelay); + } + }; + + private readonly _onPointerMove = (e: PointerEvent): void => { + if (e.pointerType !== 'touch') { + return; + } + + if (e.pointerId !== this._activeTouchPointerId || !e.isPrimary) { + return; + } + + if (!this._touchSelectionActive) { + if (this._isPastTouchMoveSlop(e)) { + this._touchGestureCancelled = true; + this._clearTouchLongPressTimer(); + } + return; + } + + if (this._canStartTouchSelection()) { + e.preventDefault(); + } + this._emitMouseEvent(this._mouseMoveListeners, e); + if (this._canStartTouchSelection()) { + this._autoScrollAtEdge(new BrowserMouseEventArgs(e)); + } + }; + + private readonly _onPointerUp = (e: PointerEvent): void => { + if (e.pointerType !== 'touch') { + return; + } + + if (e.pointerId !== this._activeTouchPointerId || !e.isPrimary) { + return; + } + + if (this._touchSelectionActive) { + if (this._canStartTouchSelection()) { + e.preventDefault(); + } + this._emitMouseEvent(this._mouseUpListeners, e); + this._suppressCompatibilityMouseEventsAfterTouch(); + } else if (!this._touchGestureCancelled && this._pendingTouchDown) { + this._emitMouseEvent(this._mouseDownListeners, this._pendingTouchDown, false); + this._emitMouseEvent(this._mouseUpListeners, e, false); + this._suppressCompatibilityMouseEventsAfterTouch(); + } + + this._resetTouchGesture(true); + }; + + private readonly _onPointerCancel = (e: PointerEvent): void => { + if (e.pointerType !== 'touch' || e.pointerId !== this._activeTouchPointerId) { + return; + } + + if (this._touchSelectionActive) { + this._emitMouseEvent(this._mouseUpListeners, e); + this._suppressCompatibilityMouseEventsAfterTouch(); + } + this._resetTouchGesture(true); + }; + + private readonly _onActiveTouchMove = (e: TouchEvent): void => { + if (this._touchSelectionActive && this._canStartTouchSelection()) { + e.preventDefault(); + } + }; + + private _activateTouchSelection(e: PointerEvent): void { + if (!this._canStartTouchSelection()) { + return; + } + this._touchSelectionActive = true; + this._pendingTouchDown = null; + if (this.element.setPointerCapture) { + this.element.setPointerCapture(e.pointerId); + this._touchPointerCaptured = true; + } + this.element.addEventListener('touchmove', this._onActiveTouchMove, this._touchMoveOptions); + this._emitMouseEvent(this._mouseDownListeners, e); + } + + private _resetTouchGesture(releasePointerCapture: boolean): void { + const pointerId = this._activeTouchPointerId; + this._clearTouchLongPressTimer(); + this.element.removeEventListener('touchmove', this._onActiveTouchMove, true); + if (releasePointerCapture && this._touchPointerCaptured && pointerId !== null && this.element.releasePointerCapture) { + this.element.releasePointerCapture(pointerId); + } + this._activeTouchPointerId = null; + this._pendingTouchDown = null; + this._touchSelectionActive = false; + this._touchGestureCancelled = false; + this._touchPointerCaptured = false; + } + + private _isPastTouchMoveSlop(e: PointerEvent): boolean { + const x = e.pageX - this._touchStartPageX; + const y = e.pageY - this._touchStartPageY; + return x * x + y * y > HtmlElementContainer._touchMoveSlop * HtmlElementContainer._touchMoveSlop; + } + + private _clearTouchLongPressTimer(): void { + if (this._touchLongPressTimer !== null) { + clearTimeout(this._touchLongPressTimer); + this._touchLongPressTimer = null; + } + } + + private _suppressCompatibilityMouseEventsAfterTouch(): void { + this._clearCompatibilityMouseSuppression(); + this._suppressCompatibilityMouseEvents = true; + this._compatibilityMouseSuppressionTimer = setTimeout(() => { + this._suppressCompatibilityMouseEvents = false; + this._compatibilityMouseSuppressionTimer = null; + }, HtmlElementContainer._compatibilityMouseSuppressionDelay); + } + + private _clearCompatibilityMouseSuppression(): void { + if (this._compatibilityMouseSuppressionTimer !== null) { + clearTimeout(this._compatibilityMouseSuppressionTimer); + this._compatibilityMouseSuppressionTimer = null; + } + this._suppressCompatibilityMouseEvents = false; + } + + private _emitMouseEvent(listeners: MouseEventListener[], e: MouseEvent, allowPreventDefault: boolean = true): void { + const args = new BrowserMouseEventArgs(e, allowPreventDefault); + for (const listener of [...listeners]) { + listener(args); + } + } + + private _autoScrollAtEdge(e: IMouseEventArgs): void { + if (!this._edgeScrollContainer) { + return; + } + + const scrollContainer = this._edgeScrollContainer(); + const scrollElement = (scrollContainer as HtmlElementContainer).element; + const x = e.getX(scrollContainer); + const y = e.getY(scrollContainer); + const width = scrollElement.clientWidth || scrollContainer.width; + const height = scrollElement.clientHeight || scrollContainer.height; + const scrollLeftStep = HtmlElementContainer._getEdgeScrollStep(x, width); + const scrollTopStep = HtmlElementContainer._getEdgeScrollStep(y, height); + + if (scrollLeftStep !== 0) { + scrollContainer.scrollLeft = HtmlElementContainer._clamp( + scrollContainer.scrollLeft + scrollLeftStep, + 0, + Math.max(0, scrollElement.scrollWidth - width) + ); + } + + if (scrollTopStep !== 0) { + scrollContainer.scrollTop = HtmlElementContainer._clamp( + scrollContainer.scrollTop + scrollTopStep, + 0, + Math.max(0, scrollElement.scrollHeight - height) + ); + } + } + + private static _getEdgeScrollStep(distanceFromStart: number, viewportSize: number): number { + const threshold = HtmlElementContainer._edgeScrollThreshold; + const distanceFromEnd = viewportSize - distanceFromStart; + if (distanceFromStart < threshold) { + return -Math.min(HtmlElementContainer._edgeScrollMaxStep, threshold - distanceFromStart); + } + if (distanceFromEnd < threshold) { + return Math.min(HtmlElementContainer._edgeScrollMaxStep, threshold - distanceFromEnd); + } + return 0; + } + + private static _clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); + } + public stopAnimation(): void { this.element.style.transition = 'none'; } diff --git a/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts b/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts index 0c021d440..e1388ec24 100644 --- a/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts @@ -28,7 +28,12 @@ export class InlineTuningGlyph extends Glyph { let textWidth = 0; for (const tuning of this._tunings) { - textWidth = Math.max(textWidth, canvas.measureText(Tuning.getTextForTuning(tuning, false)).width); + textWidth = Math.max( + textWidth, + canvas.measureText( + Tuning.getTextForTuning(tuning, false, this.renderer.settings.display.tuningAccidentalMode) + ).width + ); } canvas.font = oldFont; @@ -56,7 +61,7 @@ export class InlineTuningGlyph extends Glyph { for (let i = 0, j = this._tunings.length; i < j; i++) { canvas.fillText( - Tuning.getTextForTuning(this._tunings[i], false), + Tuning.getTextForTuning(this._tunings[i], false, this.renderer.settings.display.tuningAccidentalMode), textEndX, cy + this.renderer.y + (this.renderer as LineBarRenderer).getLineY(i) ); diff --git a/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts b/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts index 25fc6c388..1bcf4e788 100644 --- a/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts @@ -100,7 +100,11 @@ export class TuningGlyph extends GlyphGroup { const symbol = ((MusicFontSymbol.GuitarString0 as number) + (i + 1)) as MusicFontSymbol; this.addGlyph(new MusicFontGlyph(currentX, currentY + circleHeight, circleScale, symbol)); - const str: string = ` = ${Tuning.getTextForTuning(tuning.tunings[i], false)}`; + const str: string = ` = ${Tuning.getTextForTuning( + tuning.tunings[i], + false, + this.renderer.settings.display.tuningAccidentalMode + )}`; this.addGlyph( new TextGlyph( currentX + circleHeight, diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index f0c6c17a3..1ed94ceb8 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -1,8 +1,9 @@ -import { describe, it } from 'vitest'; +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 type { Score } from '@coderline/alphatab/model/Score'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { Settings } from '@coderline/alphatab/Settings'; import { ComparisonHelpers } from 'test/model/ComparisonHelpers'; import { TestPlatform } from 'test/TestPlatform'; @@ -128,6 +129,17 @@ describe('AlphaTexExporterTest', () => { } }); + it('exports-tuning-accidental-mode', () => { + const score = ScoreLoader.loadAlphaTex('\\tuning E4 B3 G3 D3 A2 Gb2 . r.4'); + + expect(exportAlphaTex(score)).toContain('\\tuning (E4 B3 G3 D3 A2 Gb2)'); + + const settings = new Settings(); + settings.display.tuningAccidentalMode = TuningAccidentalMode.Sharp; + + expect(exportAlphaTex(score, settings)).toContain('\\tuning (E4 B3 G3 D3 A2 F#2)'); + }); + // Note: we just test all our importer and visual tests to cover all features it('importer', async () => { diff --git a/packages/alphatab/test/model/TuningParser.test.ts b/packages/alphatab/test/model/TuningParser.test.ts index b2cc62833..25e313316 100644 --- a/packages/alphatab/test/model/TuningParser.test.ts +++ b/packages/alphatab/test/model/TuningParser.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { Tuning, TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; describe('TuningParserTest', () => { it('standard', () => { @@ -14,4 +14,11 @@ describe('TuningParserTest', () => { expect(tuning.join(',')).toBe(standard.tunings.join(',')); expect(tuningText2.join(',')).toBe(tuningText.join(',')); }); + + it('formats-accidental-mode', () => { + const tuning = ModelUtils.getTuningForText('Gb2'); + + expect(Tuning.getTextForTuning(tuning, true)).toBe('Gb2'); + expect(Tuning.getTextForTuning(tuning, true, TuningAccidentalMode.Sharp)).toBe('F#2'); + }); }); diff --git a/packages/alphatab/test/platform/javascript/HtmlElementContainer.test.ts b/packages/alphatab/test/platform/javascript/HtmlElementContainer.test.ts new file mode 100644 index 000000000..32a349fa4 --- /dev/null +++ b/packages/alphatab/test/platform/javascript/HtmlElementContainer.test.ts @@ -0,0 +1,514 @@ +import type { IContainer } from '@coderline/alphatab/platform/IContainer'; +import type { IMouseEventArgs } from '@coderline/alphatab/platform/IMouseEventArgs'; +import { HtmlElementContainer } from '@coderline/alphatab/platform/javascript/HtmlElementContainer'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +type TestEventListener = EventListenerOrEventListenerObject; + +interface TestListenerRegistration { + listener: TestEventListener; + options?: boolean | AddEventListenerOptions; +} + +interface TestPointerEventOptions { + pointerType?: string; + pointerId?: number; + isPrimary?: boolean; + button?: number; + pageX?: number; + pageY?: number; +} + +interface TestMouseEventOptions { + button?: number; + pageX?: number; + pageY?: number; +} + +interface MutableTestEvent { + type: string; + defaultPrevented: boolean; + preventDefault(): void; +} + +class TestHtmlElement { + public readonly style: Partial = {}; + public readonly ownerDocument = { + defaultView: { + pageXOffset: 5, + pageYOffset: 7, + PointerEvent: class PointerEvent {} + } + }; + public offsetWidth = 100; + public offsetHeight = 100; + public scrollLeft = 0; + public scrollTop = 0; + public scrollWidth = 100; + public scrollHeight = 100; + public clientWidth = 100; + public clientHeight = 100; + public readonly setPointerCapture = vi.fn(); + public readonly releasePointerCapture = vi.fn(); + private readonly _listeners = new Map(); + private _bounds = { left: 10, top: 20, width: 100, height: 100 }; + + public setBounds(left: number, top: number, width: number, height: number): void { + this._bounds = { left, top, width, height }; + this.clientWidth = width; + this.clientHeight = height; + } + + public getBoundingClientRect(): DOMRect { + const { left, top, width, height } = this._bounds; + return { + left, + top, + width, + height, + x: left, + y: top, + right: left + width, + bottom: top + height, + toJSON() { + return this; + } + } as DOMRect; + } + + public getClientRects(): DOMRect[] { + return [this.getBoundingClientRect()]; + } + + public appendChild(_child: HTMLElement): void {} + + public addEventListener( + type: string, + listener: TestEventListener, + options?: boolean | AddEventListenerOptions + ): void { + const listeners = this._listeners.get(type) ?? []; + listeners.push({ listener, options }); + this._listeners.set(type, listeners); + } + + public removeEventListener( + type: string, + listener: TestEventListener, + options?: boolean | EventListenerOptions + ): void { + const listeners = this._listeners.get(type) ?? []; + const capture = typeof options === 'boolean' ? options : options?.capture; + this._listeners.set( + type, + listeners.filter(registration => { + const registrationCapture = + typeof registration.options === 'boolean' + ? registration.options + : registration.options?.capture; + return registration.listener !== listener || registrationCapture !== capture; + }) + ); + } + + public dispatch(type: string, event: MutableTestEvent): void { + event.type = type; + const listeners = [...(this._listeners.get(type) ?? [])]; + for (const registration of listeners) { + if (typeof registration.listener === 'function') { + registration.listener.call(this, event as Event); + } else { + registration.listener.handleEvent(event as Event); + } + } + } + + public listenerCount(type: string): number { + return this._listeners.get(type)?.length ?? 0; + } + + public listenerOptions(type: string): (boolean | AddEventListenerOptions | undefined)[] { + return (this._listeners.get(type) ?? []).map(registration => registration.options); + } +} + +function createContainer( + edgeScrollContainer: IContainer | null = null, + canStartTouchSelection: () => boolean = () => true +): { + element: TestHtmlElement; + container: HtmlElementContainer; +} { + const element = new TestHtmlElement(); + const container = new HtmlElementContainer( + element as unknown as HTMLElement, + edgeScrollContainer ? () => edgeScrollContainer : null, + canStartTouchSelection + ); + return { element, container }; +} + +function createPointerEvent(options: TestPointerEventOptions = {}): PointerEvent & MutableTestEvent { + const event = { + type: '', + pointerType: options.pointerType ?? 'mouse', + pointerId: options.pointerId ?? 1, + isPrimary: options.isPrimary ?? true, + button: options.button ?? 0, + pageX: options.pageX ?? 0, + pageY: options.pageY ?? 0, + defaultPrevented: false, + preventDefault() { + event.defaultPrevented = true; + } + }; + return event as PointerEvent & MutableTestEvent; +} + +function createMouseEvent(options: TestMouseEventOptions = {}): MouseEvent & MutableTestEvent { + const event = { + type: '', + button: options.button ?? 0, + pageX: options.pageX ?? 0, + pageY: options.pageY ?? 0, + defaultPrevented: false, + preventDefault() { + event.defaultPrevented = true; + } + }; + return event as MouseEvent & MutableTestEvent; +} + +function createTouchEvent(): TouchEvent & MutableTestEvent { + const event = { + type: '', + defaultPrevented: false, + preventDefault() { + event.defaultPrevented = true; + } + }; + return event as TouchEvent & MutableTestEvent; +} + +describe('HtmlElementContainer', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('emits desktop mouse events from native mouse listeners and preserves button state', () => { + const { element, container } = createContainer(); + const calls: string[] = []; + const downArgs: IMouseEventArgs[] = []; + container.mouseDown.on(e => { + calls.push('down'); + downArgs.push(e); + }); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('mousedown', createMouseEvent({ button: 0 })); + element.dispatch('mousemove', createMouseEvent({ button: 0 })); + element.dispatch('mouseup', createMouseEvent({ button: 0 })); + element.dispatch('mousedown', createMouseEvent({ button: 2 })); + + expect(calls).toEqual(['down', 'move', 'up', 'down']); + expect(downArgs[0].isLeftMouseButton).toBe(true); + expect(downArgs[1].isLeftMouseButton).toBe(false); + expect(element.listenerCount('pointerdown')).toBe(1); + expect(element.listenerCount('mousedown')).toBe(1); + }); + + it('ignores mouse pointer events so desktop mouse is not emitted twice', () => { + const { element, container } = createContainer(); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'mouse' })); + element.dispatch('pointermove', createPointerEvent({ pointerType: 'mouse' })); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'mouse' })); + + expect(calls).toEqual([]); + }); + + it('forwards mouse preventDefault to the native MouseEvent', () => { + const { element, container } = createContainer(); + container.mouseDown.on(e => e.preventDefault()); + + const mouseDown = createMouseEvent(); + element.dispatch('mousedown', mouseDown); + + expect(mouseDown.defaultPrevented).toBe(true); + }); + + it('emits touch tap down and up without preventing native tap events', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const downArgs: IMouseEventArgs[] = []; + const upArgs: IMouseEventArgs[] = []; + container.mouseDown.on(e => { + downArgs.push(e); + e.preventDefault(); + }); + container.mouseUp.on(e => { + upArgs.push(e); + e.preventDefault(); + }); + + const pointerDown = createPointerEvent({ pointerType: 'touch', pageX: 45, pageY: 68 }); + const pointerUp = createPointerEvent({ pointerType: 'touch', pageX: 46, pageY: 69 }); + element.dispatch('pointerdown', pointerDown); + element.dispatch('pointerup', pointerUp); + vi.advanceTimersByTime(100); + + expect(downArgs).toHaveLength(1); + expect(upArgs).toHaveLength(1); + expect(downArgs[0].getX(container)).toBe(30); + expect(downArgs[0].getY(container)).toBe(41); + expect(pointerDown.defaultPrevented).toBe(false); + expect(pointerUp.defaultPrevented).toBe(false); + }); + + it('suppresses touch compatibility mouse alphaTab events without blocking native listeners', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const calls: string[] = []; + const nativeMouseDown = vi.fn(); + container.mouseDown.on(e => { + calls.push('down'); + e.preventDefault(); + }); + container.mouseUp.on(() => calls.push('up')); + element.addEventListener('mousedown', nativeMouseDown); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch' })); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch' })); + const compatibilityMouseDown = createMouseEvent(); + element.dispatch('mousedown', compatibilityMouseDown); + element.dispatch('mouseup', createMouseEvent()); + + expect(calls).toEqual(['down', 'up']); + expect(nativeMouseDown).toHaveBeenCalledTimes(1); + expect(compatibilityMouseDown.defaultPrevented).toBe(false); + }); + + it('keeps long press alive through small touch jitter before activation', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 50 })); + vi.advanceTimersByTime(50); + element.dispatch('pointermove', createPointerEvent({ pointerType: 'touch', pageX: 54, pageY: 53 })); + vi.advanceTimersByTime(50); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pageX: 54, pageY: 53 })); + + expect(calls).toEqual(['down', 'up']); + }); + + it('cancels touch long press when the finger moves before activation', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 50 })); + vi.advanceTimersByTime(50); + element.dispatch('pointermove', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 70 })); + vi.advanceTimersByTime(100); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 70 })); + + expect(calls).toEqual([]); + }); + + it('emits long-press touch selection, prevents native touchmove, and edge-scrolls while active', () => { + vi.useFakeTimers(); + const scrollElement = new TestHtmlElement(); + scrollElement.setBounds(0, 0, 100, 100); + scrollElement.scrollTop = 50; + scrollElement.scrollHeight = 200; + const scrollContainer = new HtmlElementContainer(scrollElement as unknown as HTMLElement); + const { element, container } = createContainer(scrollContainer); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 50 })); + vi.advanceTimersByTime(100); + + const touchMove = createTouchEvent(); + element.dispatch('touchmove', touchMove); + const pointerMove = createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 102 }); + element.dispatch('pointermove', pointerMove); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 102 })); + + expect(calls).toEqual(['down', 'move', 'up']); + expect(touchMove.defaultPrevented).toBe(true); + expect(pointerMove.defaultPrevented).toBe(true); + expect(element.setPointerCapture).toHaveBeenCalledWith(1); + expect(element.releasePointerCapture).toHaveBeenCalledWith(1); + expect(element.listenerOptions('touchmove')).toEqual([]); + expect(scrollContainer.scrollTop).toBe(82); + }); + + it('does not capture or prevent touch selection when interaction is disabled', () => { + vi.useFakeTimers(); + const scrollElement = new TestHtmlElement(); + scrollElement.setBounds(0, 0, 100, 100); + scrollElement.scrollTop = 50; + scrollElement.scrollHeight = 200; + const scrollContainer = new HtmlElementContainer(scrollElement as unknown as HTMLElement); + const { element, container } = createContainer(scrollContainer, () => false); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 50 })); + vi.advanceTimersByTime(100); + const touchMove = createTouchEvent(); + element.dispatch('touchmove', touchMove); + const pointerMove = createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 102 }); + element.dispatch('pointermove', pointerMove); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 102 })); + + expect(calls).toEqual([]); + expect(touchMove.defaultPrevented).toBe(false); + expect(pointerMove.defaultPrevented).toBe(false); + expect(element.setPointerCapture).not.toHaveBeenCalled(); + expect(scrollContainer.scrollTop).toBe(50); + }); + + it('still emits touch tap events when interaction is disabled', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(null, () => false); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 50 })); + vi.advanceTimersByTime(100); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pageX: 50, pageY: 50 })); + + expect(calls).toEqual(['down', 'up']); + expect(element.setPointerCapture).not.toHaveBeenCalled(); + }); + + it('resets active touch selection on pointercancel and emits a final up only when active', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pointerId: 1 })); + element.dispatch('pointercancel', createPointerEvent({ pointerType: 'touch', pointerId: 1 })); + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pointerId: 2 })); + vi.advanceTimersByTime(100); + element.dispatch('pointercancel', createPointerEvent({ pointerType: 'touch', pointerId: 2 })); + + expect(calls).toEqual(['down', 'up']); + expect(element.releasePointerCapture).toHaveBeenCalledWith(2); + }); + + it('ignores secondary touch pointers while a primary touch is pending', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pointerId: 1 })); + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pointerId: 2 })); + element.dispatch('pointermove', createPointerEvent({ pointerType: 'touch', pointerId: 2 })); + vi.advanceTimersByTime(100); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pointerId: 2 })); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'touch', pointerId: 1 })); + + expect(calls).toEqual(['down', 'up']); + }); + + it('removes native listeners and clears pending touch timers after unsubscribe', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const down = vi.fn(); + const up = vi.fn(); + const unregisterDown = container.mouseDown.on(down); + const unregisterUp = container.mouseUp.on(up); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch' })); + unregisterDown(); + unregisterUp(); + vi.advanceTimersByTime(100); + + expect(down).not.toHaveBeenCalled(); + expect(up).not.toHaveBeenCalled(); + expect(element.listenerCount('pointerdown')).toBe(0); + expect(element.listenerCount('pointermove')).toBe(0); + expect(element.listenerCount('pointerup')).toBe(0); + expect(element.listenerCount('pointercancel')).toBe(0); + expect(element.listenerCount('mousedown')).toBe(0); + expect(element.listenerCount('mousemove')).toBe(0); + expect(element.listenerCount('mouseup')).toBe(0); + expect(element.listenerCount('touchmove')).toBe(0); + }); + + it('releases pointer capture when unsubscribing during active touch selection', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const unregisterDown = container.mouseDown.on(vi.fn()); + const unregisterMove = container.mouseMove.on(vi.fn()); + const unregisterUp = container.mouseUp.on(vi.fn()); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'touch', pointerId: 3 })); + vi.advanceTimersByTime(100); + unregisterDown(); + unregisterMove(); + unregisterUp(); + + expect(element.releasePointerCapture).toHaveBeenCalledWith(3); + expect(element.listenerCount('touchmove')).toBe(0); + }); + + it('continues emitting to remaining listeners when a listener unsubscribes during dispatch', () => { + const { element, container } = createContainer(); + const calls: string[] = []; + let unregisterFirst = () => {}; + unregisterFirst = container.mouseDown.on(() => { + calls.push('first'); + unregisterFirst(); + }); + container.mouseDown.on(() => calls.push('second')); + + element.dispatch('mousedown', createMouseEvent()); + + expect(calls).toEqual(['first', 'second']); + }); + + it('treats pen as mouse-like compatibility input instead of touch long-press input', () => { + vi.useFakeTimers(); + const { element, container } = createContainer(); + const calls: string[] = []; + container.mouseDown.on(() => calls.push('down')); + container.mouseMove.on(() => calls.push('move')); + container.mouseUp.on(() => calls.push('up')); + + element.dispatch('pointerdown', createPointerEvent({ pointerType: 'pen' })); + vi.advanceTimersByTime(100); + element.dispatch('pointermove', createPointerEvent({ pointerType: 'pen' })); + element.dispatch('pointerup', createPointerEvent({ pointerType: 'pen' })); + element.dispatch('mousedown', createMouseEvent()); + element.dispatch('mousemove', createMouseEvent()); + element.dispatch('mouseup', createMouseEvent()); + + expect(calls).toEqual(['down', 'move', 'up']); + expect(element.setPointerCapture).not.toHaveBeenCalled(); + }); +}); From 496ac60023f857068b0fa8bad857239c7efdd7bd Mon Sep 17 00:00:00 2001 From: Mateusz Date: Sun, 28 Jun 2026 11:26:04 +0200 Subject: [PATCH 3/6] feat: tuning toggle between flat and sharp (#3) From f0ea4cd28960083f1db31ea2027b2fbc81560de9 Mon Sep 17 00:00:00 2001 From: kulek Date: Mon, 13 Jul 2026 17:54:27 +0200 Subject: [PATCH 4/6] chore: fix after rebase --- packages/alphatab/src/rendering/staves/StaffSystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alphatab/src/rendering/staves/StaffSystem.ts b/packages/alphatab/src/rendering/staves/StaffSystem.ts index 2b14b453c..b852cce1f 100644 --- a/packages/alphatab/src/rendering/staves/StaffSystem.ts +++ b/packages/alphatab/src/rendering/staves/StaffSystem.ts @@ -1110,7 +1110,7 @@ export class StaffSystem { const barStartX: number = cx + bracket.firstVisibleStaffInBracket!.x; const barSize: number = bracket.width; const barOffset: number = - settings.display.accoladeBarPaddingRight + this._getInlineTuningWidthForBracket(bracket); + settings.display.accoladeBarPaddingRight; const firstStart: number = cy + bracket.firstVisibleStaffInBracket!.contentTop; const lastEnd: number = cy + bracket.lastVisibleStaffInBracket!.contentBottom; let accoladeStart: number = firstStart; From e38a299edfa1ab3f0127d7e040bb499871db0825 Mon Sep 17 00:00:00 2001 From: Mateusz Date: Sun, 28 Jun 2026 11:26:04 +0200 Subject: [PATCH 5/6] feat: tuning toggle between flat and sharp (#3) From c6e69b0bc3d0b62276a97a913ac96fad59693cb6 Mon Sep 17 00:00:00 2001 From: kulek Date: Fri, 14 Aug 2026 21:47:33 +0300 Subject: [PATCH 6/6] feat: add support for tuning alterations --- .../scripts/Serializer.setProperty.ts | 96 ++++++++------ .../alphatab/scripts/Serializer.toJson.ts | 30 ++++- packages/alphatab/src/DisplaySettings.ts | 9 -- .../alphatab/src/exporter/AlphaTexExporter.ts | 9 +- packages/alphatab/src/exporter/GpifWriter.ts | 35 ++++- .../src/generated/DisplaySettingsJson.ts | 8 -- .../generated/DisplaySettingsSerializer.ts | 5 - .../src/generated/model/TuningSerializer.ts | 9 ++ .../alphatab/src/importer/AlphaTexImporter.ts | 1 + packages/alphatab/src/importer/GpifParser.ts | 123 +++++++++++++----- .../alphatab/src/importer/MusicXmlImporter.ts | 11 +- .../alphaTex/AlphaTex1LanguageHandler.ts | 37 ++++-- .../IAlphaTexLanguageImportHandler.ts | 4 +- packages/alphatab/src/model/Staff.ts | 1 + packages/alphatab/src/model/Tuning.ts | 31 ++++- .../src/rendering/glyphs/InlineTuningGlyph.ts | 16 ++- .../src/rendering/glyphs/TuningGlyph.ts | 22 +++- .../musicxml-testsuite/71e-TabStaves.png | Bin 68246 -> 67470 bytes .../musicxml4/tuning-accidentals.xml | 39 ++++++ .../inline-tuning-mixed-accidentals.png | Bin 0 -> 7812 bytes .../guitar-tuning-mixed-accidentals.png | Bin 0 -> 14458 bytes .../test/exporter/AlphaTexExporter.test.ts | 19 +-- .../test/exporter/Gp7Exporter.test.ts | 36 +++++ .../test/importer/Gp7Importer.test.ts | 17 +++ .../test/importer/MusicXmlImporter.test.ts | 19 +++ .../alphatab/test/model/JsonConverter.test.ts | 22 ++++ .../alphatab/test/model/TuningParser.test.ts | 17 +++ .../test/visualTests/features/Layout.test.ts | 15 +++ .../features/NotationElements.test.ts | 14 ++ .../alphatex/src/metadata/staff/tuning.ts | 4 +- 30 files changed, 506 insertions(+), 143 deletions(-) create mode 100644 packages/alphatab/test-data/musicxml4/tuning-accidentals.xml create mode 100644 packages/alphatab/test-data/visual-tests/layout/inline-tuning-mixed-accidentals.png create mode 100644 packages/alphatab/test-data/visual-tests/notation-elements/guitar-tuning-mixed-accidentals.png diff --git a/packages/alphatab/scripts/Serializer.setProperty.ts b/packages/alphatab/scripts/Serializer.setProperty.ts index f216d996a..6ebde70ef 100644 --- a/packages/alphatab/scripts/Serializer.setProperty.ts +++ b/packages/alphatab/scripts/Serializer.setProperty.ts @@ -95,47 +95,67 @@ function generateSetPropertyBody(serializable: TypeSchema, importer: (name: stri caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); } else if (prop.type.isArray) { const arrayItemType = prop.type.arrayItemType!; - const collectionAddMethod = - (prop.jsDocTags.filter(t => t.tagName.text === 'json_add').map(t => t.comment ?? '')[0] as string) ?? - `${fieldName}.push`; - - // obj.fieldName = []; - // for(const i of value) { - // obj.addFieldName(Type.FromJson(i)); - // } - // or - // for(const __li of value) { - // obj.fieldName.push(Type.FromJson(__li)); - // } - - const itemSerializer = `${arrayItemType.typeAsString}Serializer`; - importer(itemSerializer, findSerializerModule(arrayItemType)); - importer(arrayItemType.typeAsString, arrayItemType.modulePath); - - const loopItems = [ - createNodeFromSource( - `obj.${fieldName} = [];`, + if (arrayItemType.isEnumType) { + importer(arrayItemType.typeAsString, arrayItemType.modulePath); + importer('JsonHelper', '@coderline/alphatab/io/JsonHelper'); + const parseEnumArray = createNodeFromSource( + `obj.${fieldName} = (v as number[]).map(i => JsonHelper.parseEnum<${arrayItemType.typeAsString}>(i, ${arrayItemType.typeAsString})!);`, ts.SyntaxKind.ExpressionStatement - ), - createNodeFromSource( - `for(const o of (v as (Map | null)[])) { - const i = new ${arrayItemType.typeAsString}(); - ${itemSerializer}.fromJson(i, o); - obj.${collectionAddMethod}(i) - }`, - ts.SyntaxKind.ForOfStatement - ) - ]; - - if (prop.type.isNullable || prop.type.isOptional) { - caseStatements.push( - ts.factory.createIfStatement( - ts.factory.createIdentifier('v'), - ts.factory.createBlock(loopItems, true) - ) ); + if (prop.type.isNullable || prop.type.isOptional) { + caseStatements.push( + ts.factory.createIfStatement( + ts.factory.createIdentifier('v'), + ts.factory.createBlock([parseEnumArray], true) + ) + ); + } else { + caseStatements.push(parseEnumArray); + } } else { - caseStatements.push(...loopItems); + const collectionAddMethod = + (prop.jsDocTags.filter(t => t.tagName.text === 'json_add').map(t => t.comment ?? '')[ + 0 + ] as string) ?? `${fieldName}.push`; + + // obj.fieldName = []; + // for(const i of value) { + // obj.addFieldName(Type.FromJson(i)); + // } + // or + // for(const __li of value) { + // obj.fieldName.push(Type.FromJson(__li)); + // } + + const itemSerializer = `${arrayItemType.typeAsString}Serializer`; + importer(itemSerializer, findSerializerModule(arrayItemType)); + importer(arrayItemType.typeAsString, arrayItemType.modulePath); + + const loopItems = [ + createNodeFromSource( + `obj.${fieldName} = [];`, + ts.SyntaxKind.ExpressionStatement + ), + createNodeFromSource( + `for(const o of (v as (Map | null)[])) { + const i = new ${arrayItemType.typeAsString}(); + ${itemSerializer}.fromJson(i, o); + obj.${collectionAddMethod}(i) + }`, + ts.SyntaxKind.ForOfStatement + ) + ]; + + if (prop.type.isNullable || prop.type.isOptional) { + caseStatements.push( + ts.factory.createIfStatement( + ts.factory.createIdentifier('v'), + ts.factory.createBlock(loopItems, true) + ) + ); + } else { + caseStatements.push(...loopItems); + } } caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); } else if (prop.type.isMap) { diff --git a/packages/alphatab/scripts/Serializer.toJson.ts b/packages/alphatab/scripts/Serializer.toJson.ts index 2d7c301b3..9132c3e4d 100644 --- a/packages/alphatab/scripts/Serializer.toJson.ts +++ b/packages/alphatab/scripts/Serializer.toJson.ts @@ -76,7 +76,35 @@ function generateToJsonBody(serializable: TypeSchema, importer: (name: string, m } } else if (prop.type.isArray) { const arrayItemType = prop.type.arrayItemType!; - if (arrayItemType.isOwnType && !arrayItemType.isEnumType) { + if (arrayItemType.isEnumType) { + const serializeStatement = createNodeFromSource( + ` + o.set(${JSON.stringify(jsonName)}, obj.${fieldName}); + `, + ts.SyntaxKind.ExpressionStatement + ); + if (prop.type.isNullable) { + propertyStatements.push( + createNodeFromSource( + `if(obj.${fieldName} !== null) { + o.set(${JSON.stringify(jsonName)}, obj.${fieldName}); + }`, + ts.SyntaxKind.IfStatement + ) + ); + } else if (prop.type.isOptional) { + propertyStatements.push( + createNodeFromSource( + `if(obj.${fieldName} !== undefined) { + o.set(${JSON.stringify(jsonName)}, obj.${fieldName}); + }`, + ts.SyntaxKind.IfStatement + ) + ); + } else { + propertyStatements.push(serializeStatement); + } + } else if (arrayItemType.isOwnType) { const itemSerializer = `${arrayItemType.typeAsString}Serializer`; importer(itemSerializer, findSerializerModule(arrayItemType)); if (prop.type.isNullable) { diff --git a/packages/alphatab/src/DisplaySettings.ts b/packages/alphatab/src/DisplaySettings.ts index c99a5649c..0782d660f 100644 --- a/packages/alphatab/src/DisplaySettings.ts +++ b/packages/alphatab/src/DisplaySettings.ts @@ -2,7 +2,6 @@ import { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { LayoutMode } from '@coderline/alphatab/LayoutMode'; import { StaveProfile } from '@coderline/alphatab/StaveProfile'; import type { Staff } from '@coderline/alphatab/model/Staff'; -import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; /** * Lists the different modes in which the staves and systems are arranged. @@ -332,14 +331,6 @@ export class DisplaySettings { */ public inlineTuningPaddingRight: number = 5; - /** - * The accidental style used to display tuning note names. - * @since 1.9.0 - * @category Display - * @defaultValue `TuningAccidentalMode.Flat` - */ - public tuningAccidentalMode: TuningAccidentalMode = TuningAccidentalMode.Flat; - // Staff padding /** diff --git a/packages/alphatab/src/exporter/AlphaTexExporter.ts b/packages/alphatab/src/exporter/AlphaTexExporter.ts index 4dcd8c01f..ebe732691 100644 --- a/packages/alphatab/src/exporter/AlphaTexExporter.ts +++ b/packages/alphatab/src/exporter/AlphaTexExporter.ts @@ -516,8 +516,7 @@ export class AlphaTexExporter extends ScoreExporter { data, undefined, 0, - false, - this.settings.display.tuningAccidentalMode + false ), beats: [], pipe: undefined @@ -543,8 +542,7 @@ export class AlphaTexExporter extends ScoreExporter { data.staff, data, voiceIndex, - isMultiVoice, - this.settings.display.tuningAccidentalMode + isMultiVoice ), beats: [], pipe: undefined @@ -691,8 +689,7 @@ export class AlphaTexExporter extends ScoreExporter { nodeType: AlphaTexNodeType.Ident, text: Tuning.getTextForTuning( data.realValueWithoutHarmonic, - true, - this.settings.display.tuningAccidentalMode + true ) } as AlphaTexIdentifier; } else if (data.isStringed) { diff --git a/packages/alphatab/src/exporter/GpifWriter.ts b/packages/alphatab/src/exporter/GpifWriter.ts index ed1fcc3a5..25554ea70 100644 --- a/packages/alphatab/src/exporter/GpifWriter.ts +++ b/packages/alphatab/src/exporter/GpifWriter.ts @@ -45,7 +45,7 @@ import { SlideOutType } from '@coderline/alphatab/model/SlideOutType'; import type { Staff } from '@coderline/alphatab/model/Staff'; import type { Track } from '@coderline/alphatab/model/Track'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { Tuning, TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import type { Voice } from '@coderline/alphatab/model/Voice'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; @@ -1300,6 +1300,7 @@ export class GpifWriter { private _writeStaffNode(parent: XmlNode, staff: Staff) { const staffNode = parent.addElement('Staff'); const properties = staffNode.addElement('Properties'); + const tuningAccidentalMode = this._getTuningAccidentalMode(staff); this._writeSimplePropertyNode(properties, 'CapoFret', 'Fret', staff.capo.toString()); this._writeSimplePropertyNode(properties, 'FretCount', 'Fret', '24'); @@ -1326,7 +1327,9 @@ export class GpifWriter { tuningProperty.addElement('Pitches').innerText = tuning.slice().reverse().join(' '); tuningProperty.addElement('Label').setCData(tuningName); tuningProperty.addElement('LabelVisible').innerText = tuningName ? 'true' : 'false'; - tuningProperty.addElement('Flat'); + if (tuningAccidentalMode !== TuningAccidentalMode.Sharp) { + tuningProperty.addElement('Flat'); + } if (staff.isPercussion) { tuningProperty.addElement('Instrument').innerText = 'Undefined'; @@ -1388,12 +1391,38 @@ export class GpifWriter { staff.tuning.map(_ => '0').join('') ); - this._writeSimplePropertyNode(properties, 'TuningFlat', 'Enable', null); + if (tuningAccidentalMode !== TuningAccidentalMode.Sharp) { + this._writeSimplePropertyNode(properties, 'TuningFlat', 'Enable', null); + } this._writeDiagramCollection(properties, staff, 'DiagramCollection'); this._writeDiagramCollection(properties, staff, 'DiagramWorkingSet'); } + private _getTuningAccidentalMode(staff: Staff): TuningAccidentalMode | undefined { + if (staff.stringTuning.accidentalModes === undefined) { + return undefined; + } + let mode: TuningAccidentalMode | null = null; + for (let i = 0; i < staff.tuning.length; i++) { + const flatName = Tuning.getTextForTuning(staff.tuning[i], false, TuningAccidentalMode.Flat); + const sharpName = Tuning.getTextForTuning(staff.tuning[i], false, TuningAccidentalMode.Sharp); + if (flatName === sharpName) { + continue; + } + + const stringMode = staff.stringTuning.getAccidentalMode(i); + if (mode === null) { + mode = stringMode; + } else if (mode !== stringMode) { + // GPIF has one alteration preference for the whole tuning. + // Keep the legacy flat output when the model contains mixed modes. + return TuningAccidentalMode.Flat; + } + } + return mode ?? undefined; + } + private _writeDiagramCollection(properties: XmlNode, staff: Staff, name: string) { const diagramCollectionProperty = properties.addElement('Property'); diagramCollectionProperty.attributes.set('name', name); diff --git a/packages/alphatab/src/generated/DisplaySettingsJson.ts b/packages/alphatab/src/generated/DisplaySettingsJson.ts index 6cc7afae2..99aedb8f0 100644 --- a/packages/alphatab/src/generated/DisplaySettingsJson.ts +++ b/packages/alphatab/src/generated/DisplaySettingsJson.ts @@ -6,7 +6,6 @@ import { LayoutMode } from "@coderline/alphatab/LayoutMode"; import { StaveProfile } from "@coderline/alphatab/StaveProfile"; import { RenderingResourcesJson } from "@coderline/alphatab/generated/RenderingResourcesJson"; -import { TuningAccidentalMode } from "@coderline/alphatab/model/Tuning"; import { SystemsLayoutMode } from "@coderline/alphatab/DisplaySettings"; /** * The display settings control how the general layout and display of alphaTab is done. @@ -294,13 +293,6 @@ export interface DisplaySettingsJson { * @defaultValue `5` */ inlineTuningPaddingRight?: number; - /** - * The accidental style used to display tuning note names. - * @since 1.9.0 - * @category Display - * @defaultValue `TuningAccidentalMode.Flat` - */ - tuningAccidentalMode?: TuningAccidentalMode | keyof typeof TuningAccidentalMode | Lowercase; /** * The top padding applied to the first main notation staff (standard, tabs, numbered, slash). * @since 1.8.0 diff --git a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts index e0ab33aba..03ed85be3 100644 --- a/packages/alphatab/src/generated/DisplaySettingsSerializer.ts +++ b/packages/alphatab/src/generated/DisplaySettingsSerializer.ts @@ -8,7 +8,6 @@ import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; import { RenderingResourcesSerializer } from "@coderline/alphatab/generated/RenderingResourcesSerializer"; import { LayoutMode } from "@coderline/alphatab/LayoutMode"; import { StaveProfile } from "@coderline/alphatab/StaveProfile"; -import { TuningAccidentalMode } from "@coderline/alphatab/model/Tuning"; import { SystemsLayoutMode } from "@coderline/alphatab/DisplaySettings"; /** * @internal @@ -45,7 +44,6 @@ export class DisplaySettingsSerializer { o.set("systemlabelpaddingright", obj.systemLabelPaddingRight); o.set("accoladebarpaddingright", obj.accoladeBarPaddingRight); o.set("inlinetuningpaddingright", obj.inlineTuningPaddingRight); - o.set("tuningaccidentalmode", obj.tuningAccidentalMode as number); o.set("firstnotationstaffpaddingtop", obj.firstNotationStaffPaddingTop); o.set("lastnotationstaffpaddingbottom", obj.lastNotationStaffPaddingBottom); o.set("notationstaffpaddingtop", obj.notationStaffPaddingTop); @@ -122,9 +120,6 @@ export class DisplaySettingsSerializer { case "inlinetuningpaddingright": obj.inlineTuningPaddingRight = v! as number; return true; - case "tuningaccidentalmode": - obj.tuningAccidentalMode = JsonHelper.parseEnum(v, TuningAccidentalMode)!; - return true; case "firstnotationstaffpaddingtop": obj.firstNotationStaffPaddingTop = v! as number; return true; diff --git a/packages/alphatab/src/generated/model/TuningSerializer.ts b/packages/alphatab/src/generated/model/TuningSerializer.ts index 037ea0ed7..b1809cde3 100644 --- a/packages/alphatab/src/generated/model/TuningSerializer.ts +++ b/packages/alphatab/src/generated/model/TuningSerializer.ts @@ -5,6 +5,7 @@ // import { Tuning } from "@coderline/alphatab/model/Tuning"; import { JsonHelper } from "@coderline/alphatab/io/JsonHelper"; +import { TuningAccidentalMode } from "@coderline/alphatab/model/Tuning"; /** * @internal */ @@ -23,6 +24,9 @@ export class TuningSerializer { o.set("isstandard", obj.isStandard); o.set("name", obj.name); o.set("tunings", obj.tunings); + if (obj.accidentalModes !== undefined) { + o.set("accidentalmodes", obj.accidentalModes); + } return o; } public static setProperty(obj: Tuning, property: string, v: unknown): boolean { @@ -36,6 +40,11 @@ export class TuningSerializer { case "tunings": obj.tunings = v! as number[]; return true; + case "accidentalmodes": + if (v) { + obj.accidentalModes = (v as number[]).map(i => JsonHelper.parseEnum(i, TuningAccidentalMode)!); + } + return true; } return false; } diff --git a/packages/alphatab/src/importer/AlphaTexImporter.ts b/packages/alphatab/src/importer/AlphaTexImporter.ts index 853b8cce1..9d78a54fa 100644 --- a/packages/alphatab/src/importer/AlphaTexImporter.ts +++ b/packages/alphatab/src/importer/AlphaTexImporter.ts @@ -785,6 +785,7 @@ export class AlphaTexImporter extends ScoreImporter implements IAlphaTexImporter staff.isPercussion = true; staff.stringTuning.reset(); staff.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; + staff.stringTuning.accidentalModes = undefined; if (!this._state.staffHasExplicitDisplayTransposition.has(staff)) { staff.displayTranspositionPitch = 0; } diff --git a/packages/alphatab/src/importer/GpifParser.ts b/packages/alphatab/src/importer/GpifParser.ts index 624284d74..946ae0bce 100644 --- a/packages/alphatab/src/importer/GpifParser.ts +++ b/packages/alphatab/src/importer/GpifParser.ts @@ -48,7 +48,7 @@ import type { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { Tuning, TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { Voice } from '@coderline/alphatab/model/Voice'; import { WahPedal } from '@coderline/alphatab/model/WahPedal'; @@ -84,6 +84,12 @@ class GpifSound { public bank: number = 0; } +interface GpifTuningData { + tunings: number[]; + accidentalMode?: TuningAccidentalMode; + label: string; +} + /** * This class can parse a score.gpif xml file into the model structure * @internal @@ -623,6 +629,7 @@ export class GpifParser { const track: Track = new Track(); track.ensureStaveCount(1); const trackId: string = node.getAttribute('id'); + let trackTuning: GpifTuningData | null = null; for (const c of node.childElements()) { switch (c.localName) { @@ -664,7 +671,7 @@ export class GpifParser { this._parseLyrics(trackId, c); break; case 'Properties': - this._parseTrackProperties(track, c); + trackTuning = this._parseTrackProperties(track, c) ?? trackTuning; break; case 'GeneralMidi': case 'MidiConnection': @@ -696,6 +703,19 @@ export class GpifParser { break; } } + + if (trackTuning) { + for (const staff of track.staves) { + if (staff.stringTuning.tunings.length === 0) { + staff.stringTuning.tunings = trackTuning.tunings.slice(); + staff.stringTuning.accidentalModes = this._getTuningAccidentalModes(trackTuning); + if (trackTuning.label.length > 0) { + staff.stringTuning.name = trackTuning.label; + } + } + } + } + this._tracksById.set(trackId, track); } @@ -962,38 +982,36 @@ export class GpifParser { } private _parseStaffProperties(staff: Staff, node: XmlNode): void { - for (const c of node.childElements()) { + const properties = Array.from(node.childElements()); + const tuningProperty = properties.find(c => c.localName === 'Property' && c.getAttribute('name') === 'Tuning'); + const tuningFlatProperty = properties.find( + c => c.localName === 'Property' && c.getAttribute('name') === 'TuningFlat' + ); + + for (const c of properties) { + if (c === tuningProperty || c === tuningFlatProperty) { + continue; + } switch (c.localName) { case 'Property': this._parseStaffProperty(staff, c); break; } } + + if (tuningProperty) { + const tuning = this._parseTuningProperty(tuningProperty, tuningFlatProperty); + staff.stringTuning.tunings = tuning.tunings; + staff.stringTuning.accidentalModes = this._getTuningAccidentalModes(tuning); + if (tuning.label.length > 0) { + staff.stringTuning.name = tuning.label; + } + } } private _parseStaffProperty(staff: Staff, node: XmlNode): void { const propertyName: string = node.getAttribute('name'); switch (propertyName) { - case 'Tuning': - for (const c of node.childElements()) { - switch (c.localName) { - case 'Pitches': - const tuningParts: string[] = GpifParser._splitSafe( - node.findChildElement('Pitches')?.innerText - ); - const tuning = new Array(tuningParts.length); - for (let i: number = 0; i < tuning.length; i++) { - tuning[tuning.length - 1 - i] = GpifParser._parseIntSafe(tuningParts[i], 0); - } - staff.stringTuning.tunings = tuning; - break; - case 'Label': - staff.stringTuning.name = c.innerText; - break; - } - } - - break; case 'DiagramCollection': case 'ChordCollection': this._parseDiagramCollectionForStaff(staff, node); @@ -1150,29 +1168,30 @@ export class GpifParser { } } - private _parseTrackProperties(track: Track, node: XmlNode): void { - for (const c of node.childElements()) { + private _parseTrackProperties(track: Track, node: XmlNode): GpifTuningData | null { + const properties = Array.from(node.childElements()); + const tuningProperty = properties.find(c => c.localName === 'Property' && c.getAttribute('name') === 'Tuning'); + const tuningFlatProperty = properties.find( + c => c.localName === 'Property' && c.getAttribute('name') === 'TuningFlat' + ); + + for (const c of properties) { + if (c === tuningProperty || c === tuningFlatProperty) { + continue; + } switch (c.localName) { case 'Property': this._parseTrackProperty(track, c); break; } } + + return tuningProperty ? this._parseTuningProperty(tuningProperty, tuningFlatProperty) : null; } private _parseTrackProperty(track: Track, node: XmlNode): void { const propertyName: string = node.getAttribute('name'); switch (propertyName) { - case 'Tuning': - const tuningParts: string[] = GpifParser._splitSafe(node.findChildElement('Pitches')?.innerText); - const tuning = new Array(tuningParts.length); - for (let i: number = 0; i < tuning.length; i++) { - tuning[tuning.length - 1 - i] = GpifParser._parseIntSafe(tuningParts[i], 0); - } - for (const staff of track.staves) { - staff.stringTuning.tunings = tuning; - } - break; case 'DiagramCollection': case 'ChordCollection': this._parseDiagramCollectionForTrack(track, node); @@ -1186,6 +1205,40 @@ export class GpifParser { } } + private _parseTuningProperty(node: XmlNode, tuningFlatProperty: XmlNode | undefined): GpifTuningData { + const tuningParts: string[] = GpifParser._splitSafe(node.findChildElement('Pitches')?.innerText); + const tunings = new Array(tuningParts.length); + for (let i: number = 0; i < tunings.length; i++) { + tunings[tunings.length - 1 - i] = GpifParser._parseIntSafe(tuningParts[i], 0); + } + + const isFlat = + node.findChildElement('Flat') !== null || + (tuningFlatProperty !== undefined && tuningFlatProperty.findChildElement('Enable') !== null); + return { + tunings, + accidentalMode: isFlat ? TuningAccidentalMode.Flat : undefined, + label: node.findChildElement('Label')?.innerText ?? '' + }; + } + + private _getTuningAccidentalModes(tuning: GpifTuningData): TuningAccidentalMode[] | undefined { + const accidentalMode = tuning.accidentalMode; + if (accidentalMode === undefined) { + return undefined; + } + let hasEnharmonicTuning = false; + const modes = tuning.tunings.map(value => { + const flatName = Tuning.getTextForTuning(value, false, TuningAccidentalMode.Flat); + const sharpName = Tuning.getTextForTuning(value, false, TuningAccidentalMode.Sharp); + if (flatName !== sharpName) { + hasEnharmonicTuning = true; + } + return flatName === sharpName ? TuningAccidentalMode.Flat : accidentalMode; + }); + return hasEnharmonicTuning ? modes : undefined; + } + private _parseGeneralMidi(track: Track, node: XmlNode): void { for (const c of node.childElements()) { switch (c.localName) { diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 4513c75e2..ae7559b58 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -40,6 +40,7 @@ import { Staff } from '@coderline/alphatab/model/Staff'; import { Track } from '@coderline/alphatab/model/Track'; import { TremoloPickingEffect, TremoloPickingStyle } from '@coderline/alphatab/model/TremoloPickingEffect'; import { TripletFeel } from '@coderline/alphatab/model/TripletFeel'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { VibratoType } from '@coderline/alphatab/model/VibratoType'; import { Voice } from '@coderline/alphatab/model/Voice'; import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper'; @@ -1879,7 +1880,15 @@ export class MusicXmlImporter extends ScoreImporter { } } const tuning: number = ModelUtils.getTuningForText(tuningStep + tuningOctave) + tuningAlter; - staff.tuning[staff.tuning.length - line] = tuning; + const tuningIndex = staff.tuning.length - line; + staff.tuning[tuningIndex] = tuning; + if (tuningAlter !== 0) { + staff.stringTuning.accidentalModes ??= new Array( + staff.standardNotationLineCount + ).fill(TuningAccidentalMode.Flat); + staff.stringTuning.accidentalModes[tuningIndex] = + tuningAlter > 0 ? TuningAccidentalMode.Sharp : TuningAccidentalMode.Flat; + } } private _parseClef(element: XmlNode, bar: Bar): void { diff --git a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts index c1ea5a397..ed638a8f3 100644 --- a/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts @@ -362,6 +362,8 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler return ApplyNodeResult.Applied; case 'tuning': const tuning: number[] = []; + const accidentalModes: TuningAccidentalMode[] = []; + let hasExplicitAccidental = false; let hideTuning = false; let tuningName = ''; for (let i = 0; i < metaData.arguments!.arguments.length; i++) { @@ -389,6 +391,21 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler const t = ModelUtils.parseTuning(text); if (t) { tuning.push(t.realValue); + switch (t.tone.accidentalMode) { + case NoteAccidentalMode.ForceSharp: + case NoteAccidentalMode.ForceDoubleSharp: + accidentalModes.push(TuningAccidentalMode.Sharp); + hasExplicitAccidental = true; + break; + case NoteAccidentalMode.ForceFlat: + case NoteAccidentalMode.ForceDoubleFlat: + accidentalModes.push(TuningAccidentalMode.Flat); + hasExplicitAccidental = true; + break; + default: + accidentalModes.push(TuningAccidentalMode.Flat); + break; + } } else if (i === metaData.arguments!.arguments.length - 1 && tuning.length > 0) { tuningName = text; importer.addSemanticDiagnostic({ @@ -417,6 +434,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler importer.state.staffTuningApplied.delete(staff); staff.stringTuning = new Tuning(); staff.stringTuning.tunings = tuning; + staff.stringTuning.accidentalModes = hasExplicitAccidental ? accidentalModes : undefined; staff.stringTuning.name = tuningName; this._tuningProperties(importer, staff, staff.stringTuning, metaData); @@ -2780,8 +2798,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff: Staff, bar: Bar | undefined, voice: number, - isMultiVoice: boolean, - tuningAccidentalMode: TuningAccidentalMode + isMultiVoice: boolean ): AlphaTexMetaDataNode[] { const nodes: AlphaTexMetaDataNode[] = []; @@ -2790,8 +2807,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff, nodes, isMultiVoice, - voice, - tuningAccidentalMode + voice ); if (!bar) { return nodes; @@ -2890,8 +2906,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler } private static _buildStaffMetaDataNodes( nodes: AlphaTexMetaDataNode[], - staff: Staff, - tuningAccidentalMode: TuningAccidentalMode + staff: Staff ) { const firstStaffMetaIndex = nodes.length; @@ -2905,7 +2920,10 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler 'tuning', Atnf.args( staff.stringTuning.tunings.map( - t => Atnf.ident(Tuning.getTextForTuning(t, true, tuningAccidentalMode)) as IAlphaTexArgumentValue + (t, i) => + Atnf.ident( + Tuning.getTextForTuning(t, true, staff.stringTuning.getAccidentalMode(i)) + ) as IAlphaTexArgumentValue ) ) ); @@ -3103,8 +3121,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler staff: Staff, nodes: AlphaTexMetaDataNode[], isMultiVoice: boolean, - voice: number, - tuningAccidentalMode: TuningAccidentalMode + voice: number ) { if (bar === undefined || bar.index === 0) { if (voice === 0) { @@ -3112,7 +3129,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler nodes.push(AlphaTex1LanguageHandler._buildNewTrackNode(staff.track)); } nodes.push(AlphaTex1LanguageHandler._buildNewStaffNode(staff)); - AlphaTex1LanguageHandler._buildStaffMetaDataNodes(nodes, staff, tuningAccidentalMode); + AlphaTex1LanguageHandler._buildStaffMetaDataNodes(nodes, staff); } if (isMultiVoice) { diff --git a/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts b/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts index 1db9afc5e..39a792c6e 100644 --- a/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts +++ b/packages/alphatab/src/importer/alphaTex/IAlphaTexLanguageImportHandler.ts @@ -5,7 +5,6 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Score } from '@coderline/alphatab/model/Score'; import type { Staff } from '@coderline/alphatab/model/Staff'; -import type { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; /** * @internal @@ -55,8 +54,7 @@ export interface IAlphaTexLanguageImportHandler { staff: Staff, bar: Bar | undefined, voice: number, - isMultiVoice: boolean, - tuningAccidentalMode: TuningAccidentalMode + isMultiVoice: boolean ): AlphaTexMetaDataNode[]; buildSyncPointNodes(score: Score): AlphaTexMetaDataNode[]; buildBeatEffects(beat: Beat): AlphaTexPropertyNode[]; diff --git a/packages/alphatab/src/model/Staff.ts b/packages/alphatab/src/model/Staff.ts index f016c5670..7ae521095 100644 --- a/packages/alphatab/src/model/Staff.ts +++ b/packages/alphatab/src/model/Staff.ts @@ -152,6 +152,7 @@ export class Staff { if (this.isPercussion) { this.displayTranspositionPitch = 0; this.stringTuning.tunings = [0, 0, 0, 0, 0, 0]; + this.stringTuning.accidentalModes = undefined; } if (this.stringTuning.tunings.length === 0) { this.showTablature = false; diff --git a/packages/alphatab/src/model/Tuning.ts b/packages/alphatab/src/model/Tuning.ts index 5bb34300f..477a936e1 100644 --- a/packages/alphatab/src/model/Tuning.ts +++ b/packages/alphatab/src/model/Tuning.ts @@ -59,7 +59,7 @@ export class Tuning { public static getDefaultTuningFor(stringCount: number): Tuning | null { if (Tuning._defaultTunings.has(stringCount)) { const d = Tuning._defaultTunings.get(stringCount)!; - return new Tuning(d.name, d.tunings, d.isStandard); + return new Tuning(d.name, d.tunings, d.isStandard, d.accidentalModes); } return null; } @@ -160,7 +160,7 @@ export class Tuning { } } if (equals) { - return new Tuning(tuning.name, tuning.tunings, tuning.isStandard); + return new Tuning(tuning.name, tuning.tunings, tuning.isStandard, tuning.accidentalModes); } } return null; @@ -181,24 +181,47 @@ export class Tuning { */ public tunings: number[]; + /** + * Gets or sets the optional accidental style used to display each string tuning. + * The entries use the same top-string-first order as {@link tunings}. + * If omitted, the legacy display fallback is used. + * @since 1.10.0 + */ + public accidentalModes: TuningAccidentalMode[] | undefined; + /** * Initializes a new instance of the {@link Tuning} class. * @param name The name. * @param tuning The tuning. * @param isStandard if set to`true`[is standard]. + * @param accidentalModes The accidental style for each tuning value. */ - public constructor(name: string = '', tuning: number[] | null = null, isStandard: boolean = false) { + public constructor( + name: string = '', + tuning: number[] | null = null, + isStandard: boolean = false, + accidentalModes?: TuningAccidentalMode[] | null + ) { this.isStandard = isStandard; this.name = name; this.tunings = tuning ?? []; + this.accidentalModes = accidentalModes?.slice(); + } + + /** + * Gets the accidental style for a string, falling back to {@link TuningAccidentalMode.Flat} for missing entries. + * @param index The top-string-first tuning index. + */ + public getAccidentalMode(index: number): TuningAccidentalMode { + return this.accidentalModes?.[index] ?? TuningAccidentalMode.Flat; } public reset() { this.isStandard = false; this.name = ''; this.tunings = []; + this.accidentalModes = undefined; } - /** * Tries to detect the name and standard flag of the tuning from a known tuning list based diff --git a/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts b/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts index e1388ec24..95e6dd5f6 100644 --- a/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/InlineTuningGlyph.ts @@ -13,12 +13,12 @@ import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementS export class InlineTuningGlyph extends Glyph { public readonly staff: RenderStaff; - private readonly _tunings: number[]; + private readonly _tuning: Tuning; public constructor(staff: RenderStaff) { super(0, 0); this.staff = staff; - this._tunings = staff.modelStaff.stringTuning.tunings; + this._tuning = staff.modelStaff.stringTuning; } public override doLayout(): void { @@ -27,11 +27,15 @@ export class InlineTuningGlyph extends Glyph { canvas.font = this.renderer.resources.elementFonts.get(NotationElement.GuitarTuning)!; let textWidth = 0; - for (const tuning of this._tunings) { + for (let i = 0, j = this._tuning.tunings.length; i < j; i++) { textWidth = Math.max( textWidth, canvas.measureText( - Tuning.getTextForTuning(tuning, false, this.renderer.settings.display.tuningAccidentalMode) + Tuning.getTextForTuning( + this._tuning.tunings[i], + false, + this._tuning.getAccidentalMode(i) + ) ).width ); } @@ -59,9 +63,9 @@ export class InlineTuningGlyph extends Glyph { using _ = ElementStyleHelper.track(canvas, TrackSubElement.StringTuning, this.staff.modelStaff.track, true); - for (let i = 0, j = this._tunings.length; i < j; i++) { + for (let i = 0, j = this._tuning.tunings.length; i < j; i++) { canvas.fillText( - Tuning.getTextForTuning(this._tunings[i], false, this.renderer.settings.display.tuningAccidentalMode), + Tuning.getTextForTuning(this._tuning.tunings[i], false, this._tuning.getAccidentalMode(i)), textEndX, cy + this.renderer.y + (this.renderer as LineBarRenderer).getLineY(i) ); diff --git a/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts b/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts index 1bcf4e788..32bda0530 100644 --- a/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TuningGlyph.ts @@ -1,5 +1,5 @@ import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; -import { Tuning } from '@coderline/alphatab/model/Tuning'; +import { Tuning, TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; import { GlyphGroup } from '@coderline/alphatab/rendering/glyphs/GlyphGroup'; import { TextGlyph } from '@coderline/alphatab/rendering/glyphs/TextGlyph'; @@ -81,9 +81,23 @@ export class TuningGlyph extends GlyphGroup { const circleHeight = this.renderer.smuflMetrics.glyphHeights.get(MusicFontSymbol.GuitarString0)! * circleScale; this.renderer.scoreRenderer.canvas!.font = res.elementFonts.get(NotationElement.GuitarTuning)!; + const hasExplicitSharp = tuning.accidentalModes?.some(mode => mode === TuningAccidentalMode.Sharp) ?? false; + let tuningLabelWidth: number; + if (hasExplicitSharp && !tuning.isStandard && tuning.tunings.length > 0) { + tuningLabelWidth = 0; + for (let i = 0; i < tuning.tunings.length; i++) { + const labelWidth = this.renderer.scoreRenderer.canvas!.measureText( + ` = ${Tuning.getTextForTuning(tuning.tunings[i], false, tuning.getAccidentalMode(i))}` + ).width; + if (labelWidth > tuningLabelWidth) { + tuningLabelWidth = labelWidth; + } + } + } else { + tuningLabelWidth = this.renderer.scoreRenderer.canvas!.measureText(' = Gb').width; + } const stringColumnWidth = - (circleHeight + this.renderer.scoreRenderer.canvas!.measureText(' = Gb').width) * - res.engravingSettings.tuningGlyphStringColumnScale; + (circleHeight + tuningLabelWidth) * res.engravingSettings.tuningGlyphStringColumnScale; this.width = Math.max( this.renderer.scoreRenderer.canvas!.measureText(this._trackLabel).width, @@ -103,7 +117,7 @@ export class TuningGlyph extends GlyphGroup { const str: string = ` = ${Tuning.getTextForTuning( tuning.tunings[i], false, - this.renderer.settings.display.tuningAccidentalMode + tuning.getAccidentalMode(i) )}`; this.addGlyph( new TextGlyph( diff --git a/packages/alphatab/test-data/musicxml-testsuite/71e-TabStaves.png b/packages/alphatab/test-data/musicxml-testsuite/71e-TabStaves.png index 769c09d7d756f4113361496959f4b6131f1b7e9a..5ad7ca7060e635e66ba575a069eed129487af13e 100644 GIT binary patch literal 67470 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 literal 68246 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 LqZ9c$_P7264&PT& diff --git a/packages/alphatab/test-data/musicxml4/tuning-accidentals.xml b/packages/alphatab/test-data/musicxml4/tuning-accidentals.xml new file mode 100644 index 000000000..34c5f9d03 --- /dev/null +++ b/packages/alphatab/test-data/musicxml4/tuning-accidentals.xml @@ -0,0 +1,39 @@ + + + + + Guitar + + + + + + 1 + + 0 + + + + TAB + 5 + + + 2 + + F + 1 + 3 + + + G + -1 + 3 + + + + + + diff --git a/packages/alphatab/test-data/visual-tests/layout/inline-tuning-mixed-accidentals.png b/packages/alphatab/test-data/visual-tests/layout/inline-tuning-mixed-accidentals.png new file mode 100644 index 0000000000000000000000000000000000000000..1e82c713b10940bec0aa0e7123a5ec28aa11bec3 GIT binary patch literal 7812 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 literal 0 HcmV?d00001 diff --git a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts index 1ed94ceb8..03c93ab00 100644 --- a/packages/alphatab/test/exporter/AlphaTexExporter.test.ts +++ b/packages/alphatab/test/exporter/AlphaTexExporter.test.ts @@ -130,14 +130,17 @@ describe('AlphaTexExporterTest', () => { }); it('exports-tuning-accidental-mode', () => { - const score = ScoreLoader.loadAlphaTex('\\tuning E4 B3 G3 D3 A2 Gb2 . r.4'); - - expect(exportAlphaTex(score)).toContain('\\tuning (E4 B3 G3 D3 A2 Gb2)'); - - const settings = new Settings(); - settings.display.tuningAccidentalMode = TuningAccidentalMode.Sharp; - - expect(exportAlphaTex(score, settings)).toContain('\\tuning (E4 B3 G3 D3 A2 F#2)'); + const score = ScoreLoader.loadAlphaTex('\\tuning E4 B3 F#3 D3 Gb2 E2 . r.4'); + + expect(score.tracks[0].staves[0].stringTuning.accidentalModes).toEqual([ + TuningAccidentalMode.Flat, + TuningAccidentalMode.Flat, + TuningAccidentalMode.Sharp, + TuningAccidentalMode.Flat, + TuningAccidentalMode.Flat, + TuningAccidentalMode.Flat + ]); + expect(exportAlphaTex(score)).toContain('\\tuning (E4 B3 F#3 D3 Gb2 E2)'); }); // Note: we just test all our importer and visual tests to cover all features diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 86760ed5c..8236b4efd 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -13,6 +13,7 @@ import { TechniqueSymbolPlacement } from '@coderline/alphatab/model/InstrumentAr import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Score } from '@coderline/alphatab/model/Score'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { Settings } from '@coderline/alphatab/Settings'; import { XmlDocument } from '@coderline/alphatab/xml/XmlDocument'; import { ZipReader } from '@coderline/alphatab/zip/ZipReader'; @@ -723,4 +724,39 @@ describe('Gp7ExporterTest', () => { // await TestPlatform.saveFile('test-data/exporter/articulations.exported.gp', exported); }); + + it('tuning-accidental-mode', () => { + const sharpScore = ScoreLoader.loadAlphaTex('\\tuning F#4 B3 G#3 D3 A2 E2 . r.4'); + const sharpGpif = readExportedGpif(exportGp7(sharpScore)); + expect(sharpGpif).not.toContain(' importedSharpTuning.getAccidentalMode(i))).toEqual( + new Array(importedSharpTuning.tunings.length).fill(TuningAccidentalMode.Flat) + ); + + const flatScore = ScoreLoader.loadAlphaTex('\\tuning Gb4 Bb3 Eb3 Ab2 Db2 Gb2 . r.4'); + const flatGpif = readExportedGpif(exportGp7(flatScore)); + expect(flatGpif).toContain('(6).fill(TuningAccidentalMode.Flat) + ); + }); }); diff --git a/packages/alphatab/test/importer/Gp7Importer.test.ts b/packages/alphatab/test/importer/Gp7Importer.test.ts index e7a551bbd..a83d26d65 100644 --- a/packages/alphatab/test/importer/Gp7Importer.test.ts +++ b/packages/alphatab/test/importer/Gp7Importer.test.ts @@ -20,6 +20,7 @@ import { TestPlatform } from 'test/TestPlatform'; import { AutomationType } from '@coderline/alphatab/model/Automation'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; describe('Gp7ImporterTest', () => { async function prepareImporterWithFile(name: string): Promise { @@ -52,6 +53,22 @@ describe('Gp7ImporterTest', () => { expect(score.tracks[1].name).toBe('Track 2'); }); + it('tuning-accidental-mode', async () => { + const naturalScore = (await prepareImporterWithFile('guitarpro7/strings.gp')).readScore(); + const naturalTuning = naturalScore.tracks[0].staves[0].stringTuning; + expect(naturalTuning.accidentalModes).toBeUndefined(); + expect(Array.from({ length: naturalTuning.tunings.length }, (_, i) => naturalTuning.getAccidentalMode(i))).toEqual( + new Array(naturalTuning.tunings.length).fill(TuningAccidentalMode.Flat) + ); + + const flatScore = (await prepareImporterWithFile('guitarpro7/bends-advanced.gp')).readScore(); + const flatTuning = flatScore.tracks[0].staves[0].stringTuning; + expect(flatTuning.accidentalModes).toBeUndefined(); + expect(Array.from({ length: flatTuning.tunings.length }, (_, i) => flatTuning.getAccidentalMode(i))).toEqual( + new Array(flatTuning.tunings.length).fill(TuningAccidentalMode.Flat) + ); + }); + it('notes', async () => { const reader = await prepareImporterWithFile('guitarpro7/notes.gp'); const score: Score = reader.readScore(); diff --git a/packages/alphatab/test/importer/MusicXmlImporter.test.ts b/packages/alphatab/test/importer/MusicXmlImporter.test.ts index 9d9ebd9f1..0fdb817f3 100644 --- a/packages/alphatab/test/importer/MusicXmlImporter.test.ts +++ b/packages/alphatab/test/importer/MusicXmlImporter.test.ts @@ -3,6 +3,8 @@ import { BendType } from '@coderline/alphatab/model/BendType'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import { BarNumberDisplay } from '@coderline/alphatab/model/RenderStylesheet'; import type { Score } from '@coderline/alphatab/model/Score'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; +import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { MusicXmlImporterTestHelper } from 'test/importer/MusicXmlImporterTestHelper'; describe('MusicXmlImporterTests', () => { @@ -233,6 +235,23 @@ describe('MusicXmlImporterTests', () => { expect(score).toMatchSnapshot(); }); + it('tuning-alter-selects-accidental-mode', async () => { + let score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/tuning-accidentals.xml'); + const tuning = score.tracks[0].staves[0].stringTuning; + + expect(tuning.tunings).toEqual([ + ModelUtils.getTuningForText('G3') - 1, + ModelUtils.getTuningForText('F3') + 1 + ]); + expect(tuning.accidentalModes).toEqual([TuningAccidentalMode.Flat, TuningAccidentalMode.Sharp]); + + score = JsonConverter.jsObjectToScore(JsonConverter.scoreToJsObject(score)); + expect(score.tracks[0].staves[0].stringTuning.accidentalModes).toEqual([ + TuningAccidentalMode.Flat, + TuningAccidentalMode.Sharp + ]); + }); + it('timewise-basic', async () => { const score = await MusicXmlImporterTestHelper.loadFile('test-data/musicxml4/timewise-basic.xml'); expect(score).toMatchSnapshot(); diff --git a/packages/alphatab/test/model/JsonConverter.test.ts b/packages/alphatab/test/model/JsonConverter.test.ts index 0ee729c72..3ce0399aa 100644 --- a/packages/alphatab/test/model/JsonConverter.test.ts +++ b/packages/alphatab/test/model/JsonConverter.test.ts @@ -7,6 +7,7 @@ import { Color } from '@coderline/alphatab/model/Color'; import { Font, FontStyle } from '@coderline/alphatab/model/Font'; import { JsonConverter } from '@coderline/alphatab/model/JsonConverter'; import type { Score } from '@coderline/alphatab/model/Score'; +import { TuningAccidentalMode } from '@coderline/alphatab/model/Tuning'; import { FingeringMode, NotationElement, NotationMode, TabRhythmMode } from '@coderline/alphatab/NotationSettings'; import { Settings } from '@coderline/alphatab/Settings'; import { TestPlatform } from 'test/TestPlatform'; @@ -211,4 +212,25 @@ describe('JsonConverterTest', () => { expect(settings.display.resources.mainGlyphColor.g).toBe(0); expect(settings.display.resources.mainGlyphColor.b).toBe(0); }); + + it('tuning-accidental-modes', () => { + const score = ScoreLoader.loadAlphaTex('\\tuning E4 B3 F#3 D3 Gb2 E2 . r.4'); + const roundTrip = JsonConverter.jsObjectToScore(JsonConverter.scoreToJsObject(score)); + + expect(roundTrip.tracks[0].staves[0].stringTuning.accidentalModes).toEqual([0, 0, 1, 0, 0, 0]); + }); + + it('tuning-accidental-modes-legacy-json', () => { + const score = ScoreLoader.loadAlphaTex('\\tuning E4 B3 G3 D3 A2 E2 . r.4'); + const json = JSON.parse(JsonConverter.scoreToJson(score)) as { + tracks: Array<{ staves: Array<{ stringtuning: Record }> }>; + }; + expect(json.tracks[0].staves[0].stringtuning.accidentalmodes).toBeUndefined(); + + const legacyScore = JsonConverter.jsonToScore(JSON.stringify(json)); + const tuning = legacyScore.tracks[0].staves[0].stringTuning; + expect(Array.from({ length: 6 }, (_, i) => tuning.getAccidentalMode(i))).toEqual( + new Array(6).fill(TuningAccidentalMode.Flat) + ); + }); }); diff --git a/packages/alphatab/test/model/TuningParser.test.ts b/packages/alphatab/test/model/TuningParser.test.ts index 25e313316..59d2f2b64 100644 --- a/packages/alphatab/test/model/TuningParser.test.ts +++ b/packages/alphatab/test/model/TuningParser.test.ts @@ -21,4 +21,21 @@ describe('TuningParserTest', () => { expect(Tuning.getTextForTuning(tuning, true)).toBe('Gb2'); expect(Tuning.getTextForTuning(tuning, true, TuningAccidentalMode.Sharp)).toBe('F#2'); }); + + it('stores-accidental-mode-per-string', () => { + const tuning = new Tuning('Mixed', [66, 59, 54], false, [TuningAccidentalMode.Sharp]); + + expect(tuning.getAccidentalMode(0)).toBe(TuningAccidentalMode.Sharp); + expect(tuning.getAccidentalMode(1)).toBe(TuningAccidentalMode.Flat); + expect(tuning.getAccidentalMode(2)).toBe(TuningAccidentalMode.Flat); + expect(Tuning.getTextForTuning(tuning.tunings[0], true, tuning.getAccidentalMode(0))).toBe('F#4'); + expect(Tuning.getTextForTuning(tuning.tunings[1], true, tuning.getAccidentalMode(1))).toBe('B3'); + }); + + it('keeps-accidental-mode-opt-in', () => { + const tuning = new Tuning('Legacy', [66, 59, 54]); + + expect(tuning.accidentalModes).toBeUndefined(); + expect(tuning.getAccidentalMode(0)).toBe(TuningAccidentalMode.Flat); + }); }); diff --git a/packages/alphatab/test/visualTests/features/Layout.test.ts b/packages/alphatab/test/visualTests/features/Layout.test.ts index eb0747fe8..36612e985 100644 --- a/packages/alphatab/test/visualTests/features/Layout.test.ts +++ b/packages/alphatab/test/visualTests/features/Layout.test.ts @@ -179,6 +179,21 @@ describe('LayoutTests', () => { ); }); + it('inline-tuning-mixed-accidentals', async () => { + const settings: Settings = new Settings(); + settings.display.layoutMode = LayoutMode.Parchment; + await VisualTestHelper.runVisualTestTex( + ` + \\tuningDisplayMode staff + \\tuning F#4 B3 G#3 D3 Ab2 E2 + \\staff { tabs } + 0.6.4 2.6.4 3.6.4 0.5.4 | + `, + 'test-data/visual-tests/layout/inline-tuning-mixed-accidentals.png', + settings + ); + }); + it('inline-tuning-per-track-hidden', async () => { const settings: Settings = new Settings(); settings.display.layoutMode = LayoutMode.Parchment; diff --git a/packages/alphatab/test/visualTests/features/NotationElements.test.ts b/packages/alphatab/test/visualTests/features/NotationElements.test.ts index a80dcf7b2..5a68d3ec0 100644 --- a/packages/alphatab/test/visualTests/features/NotationElements.test.ts +++ b/packages/alphatab/test/visualTests/features/NotationElements.test.ts @@ -99,6 +99,20 @@ describe('NotationElements', () => { ); }); + it('guitar-tuning-mixed-accidentals', async () => { + const tex = '\\tuning F#4 B3 G#3 D3 Ab2 E2 . 3.3*4'; + + const settings: Settings = new Settings(); + settings.display.layoutMode = LayoutMode.Page; + + settings.notation.elements.set(NotationElement.GuitarTuning, true); + await VisualTestHelper.runVisualTestTex( + tex, + 'test-data/visual-tests/notation-elements/guitar-tuning-mixed-accidentals.png', + settings + ); + }); + it('guitar-tuning-off', async () => { const tex = '\\tuning d5 b4 g4 d4 a3 d3 . 3.3*4'; diff --git a/packages/alphatex/src/metadata/staff/tuning.ts b/packages/alphatex/src/metadata/staff/tuning.ts index c8c377c97..a7890c975 100644 --- a/packages/alphatex/src/metadata/staff/tuning.ts +++ b/packages/alphatex/src/metadata/staff/tuning.ts @@ -6,7 +6,7 @@ export const tuning: MetadataTagDefinition = { tag: '\\tuning', snippet: '\\tuning {$1}$0', shortDescription: 'Set the string tuning for the staff.', - longDescription: `Defines the number of strings and their tuning for stringed (and fretted) instruments.`, + longDescription: `Defines the number of strings and their tuning for stringed (and fretted) instruments. Explicit sharp and flat accidentals, such as F# or Gb, are retained for the corresponding string when the tuning is rendered or exported.`, signatures: [ { parameters: [ @@ -44,7 +44,7 @@ export const tuning: MetadataTagDefinition = { { name: 'strings', shortDescription: 'The tuning values as pitched notes', - longDescription: `The tuning values as [pitched notes](https://alphatab.net/docs/alphatex/document-structure#pitched-notes)`, + longDescription: `The tuning values as [pitched notes](https://alphatab.net/docs/alphatex/document-structure#pitched-notes). Include an explicit # or b when the tuning should retain a sharp or flat preference for that string.`, type: alphaTab.importer.alphaTex.AlphaTexNodeType.Ident, allowAllStringTypes: true, parseMode: alphaTab.importer.alphaTex.ArgumentListParseTypesMode.ValueListWithoutParenthesis