Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4960456
refactor: introduce glyph for handling all voices
Danielku15 Dec 13, 2025
b2d4525
refactor: added notehead base
Danielku15 Dec 13, 2025
c457bcc
refactor: foundation for new note head placement algorithm
Danielku15 Dec 13, 2025
64ef254
fix(alphatex): do not warn on explicit empty args
Danielku15 Dec 13, 2025
eeebf3c
fix: draw voices bottom up
Danielku15 Dec 13, 2025
6713efd
fix: lookup notehead info at same display position
Danielku15 Dec 13, 2025
d08172c
refactor: base flow for displacing voices
Danielku15 Dec 14, 2025
f89224c
fix: barline misalignment
Danielku15 Dec 14, 2025
d5dd6b8
refactor: adjust bend and whammy note head alignment
Danielku15 Dec 14, 2025
5305134
fix: masterbar start for anacrusis
Danielku15 Dec 14, 2025
12beb92
fix: corrected onTimeX calculation
Danielku15 Dec 14, 2025
4cf8c52
wip: multivoice displacement
Danielku15 Dec 14, 2025
87ceb1d
fix(alphatex): skip args parsing if none are defined
Danielku15 Dec 14, 2025
7a814db
feat(playground) : log parse errors
Danielku15 Dec 14, 2025
81e14ea
feat(playground): auto-detect global systems layout
Danielku15 Dec 14, 2025
35e688b
fix: ensure line-breaks are respected on resize
Danielku15 Dec 14, 2025
801eb80
test: setup voice and note displace test
Danielku15 Dec 14, 2025
101f790
fix: bars 1-9 review/fix round 1
Danielku15 Dec 15, 2025
c5d91ee
fix: bars 10-19 review/fix round 1
Danielku15 Dec 15, 2025
a25fe7d
fix: adjust some more alignments
Danielku15 Dec 15, 2025
6cbb0f6
test: splitup test
Danielku15 Dec 15, 2025
e79f819
test: update test data
Danielku15 Dec 15, 2025
43f052e
build: test name for c# and kotlin
Danielku15 Dec 15, 2025
600269a
fix: ledger line width
Danielku15 Dec 15, 2025
9b6052e
fix: hasStem check
Danielku15 Dec 15, 2025
22362e9
test: some beaming tests
Danielku15 Dec 15, 2025
773e227
test: update tests data
Danielku15 Dec 15, 2025
45004c1
fix(playground): css warning
Danielku15 Dec 15, 2025
117920f
test: update test data
Danielku15 Dec 15, 2025
dbaa404
build: fix cross compilation
Danielku15 Dec 15, 2025
55869ca
fix: check for existence of octave dots.
Danielku15 Dec 15, 2025
2516271
build: fix cross compilation
Danielku15 Dec 15, 2025
b12926d
fix: minor alignment adjustment
Danielku15 Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/alphatab/.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
FORCE_COLOR=1
NODE_OPTIONS=--expose-gc
UPDATE_SNAPSHOT=true
37 changes: 24 additions & 13 deletions packages/alphatab/src/EngravingSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { EngravingSettingsCloner } from '@coderline/alphatab/generated/Engraving
import { JsonHelper } from '@coderline/alphatab/io/JsonHelper';
import { Logger } from '@coderline/alphatab/Logger';
import { Duration } from '@coderline/alphatab/model/Duration';
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
import { MusicFontSymbol, MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol';
import type { SmuflMetadata } from '@coderline/alphatab/SmuflMetadata';

/**
Expand Down Expand Up @@ -53,6 +52,12 @@ export class EngravingStemInfo {
export class EngravingSettings {
private static _bravuraDefaults?: EngravingSettings;

// NOTE: configurable in future?
/**
* @internal
*/
public static readonly GraceScale: number = 0.75;

/**
* A {@link EngravingSettings} copy filled with the settings of the Bravura font used by default in alphaTab.
*/
Expand Down Expand Up @@ -431,7 +436,7 @@ export class EngravingSettings {
MusicFontSymbol.NoteheadNull
]);

for (const symbol of ModelUtils.getAllMusicFontSymbols()) {
for (const symbol of MusicFontSymbolLookup.getAllMusicFontSymbols()) {
if (!handledSymbols.has(symbol)) {
if (!ignoredSymbols.has(symbol)) {
Logger.warning(
Expand Down Expand Up @@ -498,6 +503,7 @@ export class EngravingSettings {
this.tripletFeelBracketPadding = 0.2 * this.oneStaffSpace;
this.accidentalPadding = 0.1 * this.oneStaffSpace;
this.preBeatGlyphSpacing = 0.5 * this.oneStaffSpace;
this.multiVoiceDisplacedNoteHeadSpacing = 0.2 * this.oneStaffSpace;

this.tuningGlyphStringRowPadding = 0.2 * this.oneStaffSpace;
}
Expand Down Expand Up @@ -554,21 +560,20 @@ export class EngravingSettings {
public lineRangedGlyphDashSize = 0;

/**
* The padding between effects and glyphs placed before the note heads, e.g. accidentals or brushes
* The padding between effects and glyphs placed before the note heads, e.g. accidentals or brushes
*/
public preNoteEffectPadding = 0;

/**
* The padding between effects and glyphs placed after the note heads, e.g. slides or bends
* The padding between effects and glyphs placed after the note heads, e.g. slides or bends
*/
public postNoteEffectPadding = 0;

/**
* The padding between effects and glyphs placed above/blow the note heads e.g. staccato
* The padding between effects and glyphs placed above/blow the note heads e.g. staccato
*/
public onNoteEffectPadding = 0;


/**
* The padding between the circles around string numbers.
*/
Expand Down Expand Up @@ -600,7 +605,7 @@ export class EngravingSettings {
public tieHeight = 0;

/**
* The padding between the border and text of beat timers.
* The padding between the border and text of beat timers.
*/
public beatTimerPadding = 0;

Expand Down Expand Up @@ -630,7 +635,7 @@ export class EngravingSettings {
public tabWhammyTextPadding = 0;

/**
* The height applied per half-note whammy.
* The height applied per half-note whammy.
*/
public tabWhammyPerHalfHeight = 0;

Expand Down Expand Up @@ -658,15 +663,15 @@ export class EngravingSettings {
* The size of the dashes on bends (e.g. on holds)
*/
public tabBendDashSize = 0;

/**
* The additional padding between the staff and the point
* where bend values are calculated from.
*/
public tabBendStaffPadding = 0;

/**
* The height applied per quarter-note.
* The height applied per quarter-note.
*/
public tabBendPerValueHeight = 0;

Expand Down Expand Up @@ -714,7 +719,7 @@ export class EngravingSettings {
public chordDiagramLineWidth = 0;

/**
* The padding between the bracket lines and numbers of tuplets
* The padding between the bracket lines and numbers of tuplets
*/
public tripletFeelBracketPadding = 0;

Expand Down Expand Up @@ -753,6 +758,12 @@ export class EngravingSettings {
*/
public directionsScale = 0.6;

/**
* The spacing between displaced displaced note heads
* in case of multi-voice note head overlaps.
*/
public multiVoiceDisplacedNoteHeadSpacing = 0;

// Idea: maybe we can encode and pack this large metadata into a more compact format (e.g. BSON or a custom binary blob?)
// This metadata below is updated automatically from the bravura_metadata.json via npm script

Expand Down
13 changes: 13 additions & 0 deletions packages/alphatab/src/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,4 +831,17 @@ export class Environment {
public static quoteJsonString(text: string) {
return JSON.stringify(text);
}

/**
* @internal
* @target web
* @partial
*/
public static sortDescending(array: number[]) {
// java is a joke:
// no primitive sorting of arrays with custom comparer in 2025
// so we need to declare this specific helper function and implement it in Kotlin ourselves.
array.sort((a, b) => b - a);
}

}
1 change: 1 addition & 0 deletions packages/alphatab/src/generated/EngravingSettingsCloner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export class EngravingSettingsCloner {
clone.tuningGlyphStringColumnScale = original.tuningGlyphStringColumnScale;
clone.tuningGlyphStringRowPadding = original.tuningGlyphStringRowPadding;
clone.directionsScale = original.directionsScale;
clone.multiVoiceDisplacedNoteHeadSpacing = original.multiVoiceDisplacedNoteHeadSpacing;
return clone;
}
}
5 changes: 5 additions & 0 deletions packages/alphatab/src/generated/EngravingSettingsJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,9 @@ export interface EngravingSettingsJson {
* The relative scale of any directions glyphs drawn like coda or segno.
*/
directionsScale?: number;
/**
* The spacing between displaced displaced note heads
* in case of multi-voice note head overlaps.
*/
multiVoiceDisplacedNoteHeadSpacing?: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export class EngravingSettingsSerializer {
o.set("tuningglyphstringcolumnscale", obj.tuningGlyphStringColumnScale);
o.set("tuningglyphstringrowpadding", obj.tuningGlyphStringRowPadding);
o.set("directionsscale", obj.directionsScale);
o.set("multivoicedisplacednoteheadspacing", obj.multiVoiceDisplacedNoteHeadSpacing);
return o;
}
public static setProperty(obj: EngravingSettings, property: string, v: unknown): boolean {
Expand Down Expand Up @@ -432,6 +433,9 @@ export class EngravingSettingsSerializer {
case "directionsscale":
obj.directionsScale = v! as number;
return true;
case "multivoicedisplacednoteheadspacing":
obj.multiVoiceDisplacedNoteHeadSpacing = v! as number;
return true;
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler

const types = lookup.get(tag);
if (!types) {
if (args) {
if (args && args.arguments.length > 0) {
importer.addSemanticDiagnostic({
code: AlphaTexDiagnosticCode.AT300,
message: `Expected no arguments, but found some.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader {
AlphaTexNodeType.Number
]);

public hasMetaDataArguments(metaData: AlphaTexMetaDataTagNode): boolean {
const tag = metaData.tag.text.toLowerCase();
for (const lookup of AlphaTex1LanguageDefinitions.metaDataSignatures) {
if (lookup.has(tag)) {
const types = lookup.get(tag);
return types !== null;
}
}

// unknown meta -> assume args exist
return true;
}

public readMetaDataArguments(
parser: AlphaTexParser,
metaData: AlphaTexMetaDataTagNode
Expand Down Expand Up @@ -83,7 +96,10 @@ export class AlphaTex1MetaDataReader implements IAlphaTexMetaDataReader {
if (!AlphaTex1LanguageDefinitions.metaDataProperties.has(tag)) {
return undefined;
}
const props = AlphaTex1LanguageDefinitions.metaDataProperties.get(tag)!;
const props = AlphaTex1LanguageDefinitions.metaDataProperties.get(tag);
if (!props) {
return this._readPropertyArguments(parser, [], property);
}
return this._readPropertyArguments(parser, [props], property);
}

Expand Down
27 changes: 14 additions & 13 deletions packages/alphatab/src/importer/alphaTex/AlphaTexParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class AlphaTexParser {
/**
* The parsing mode.
*/
public mode:AlphaTexParseMode = AlphaTexParseMode.ForModelImport;
public mode: AlphaTexParseMode = AlphaTexParseMode.ForModelImport;

public get lexerDiagnostics(): AlphaTexDiagnosticBag {
return this.lexer.lexerDiagnostics;
Expand Down Expand Up @@ -509,7 +509,6 @@ export class AlphaTexParser {

private static readonly _allowValuesAfterProperties = new Set<string>(['chord']);


private _metaData(metaDataList: AlphaTexMetaDataNode[]) {
const tag = this.lexer.peekToken();
if (!tag || tag.nodeType !== AlphaTexNodeType.Tag) {
Expand Down Expand Up @@ -558,17 +557,19 @@ export class AlphaTexParser {
}
}
} else {
metaData.arguments = this.argumentList();
if (!metaData.arguments) {
metaData.arguments = this._metaDataReader.readMetaDataArguments(this, metaData.tag);
if (metaData.arguments && metaData.arguments.arguments.length > 1) {
this.addParserDiagnostic({
code: AlphaTexDiagnosticCode.AT301,
message: `Metadata arguments should be wrapped into parenthesis.`,
severity: AlphaTexDiagnosticsSeverity.Warning,
start: metaData.arguments?.start ?? metaData.start,
end: metaData.arguments?.end ?? metaData.end
});
if (this._metaDataReader.hasMetaDataArguments(metaData.tag)) {
metaData.arguments = this.argumentList();
if (!metaData.arguments) {
metaData.arguments = this._metaDataReader.readMetaDataArguments(this, metaData.tag);
if (metaData.arguments && metaData.arguments.arguments.length > 1) {
this.addParserDiagnostic({
code: AlphaTexDiagnosticCode.AT301,
message: `Metadata arguments should be wrapped into parenthesis.`,
severity: AlphaTexDiagnosticsSeverity.Warning,
start: metaData.arguments?.start ?? metaData.start,
end: metaData.arguments?.end ?? metaData.end
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type {
AlphaTexArgumentList,
AlphaTexMetaDataTagNode,
AlphaTexPropertyNode,
AlphaTexArgumentList
AlphaTexPropertyNode
} from '@coderline/alphatab/importer/alphaTex/AlphaTexAst';
import type { AlphaTexParser } from '@coderline/alphatab/importer/alphaTex/AlphaTexParser';

/**
* @internal
*/
export interface IAlphaTexMetaDataReader {
hasMetaDataArguments(metaData: AlphaTexMetaDataTagNode): boolean;
readMetaDataArguments(parser: AlphaTexParser, metaData: AlphaTexMetaDataTagNode): AlphaTexArgumentList | undefined;

readMetaDataPropertyArguments(
Expand Down
1 change: 0 additions & 1 deletion packages/alphatab/src/model/Beat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,6 @@ export class Beat {
return this.noteStringLookup.has(noteString);
}

// TODO: can be likely eliminated
public getNoteWithRealValue(noteRealValue: number): Note | null {
if (this.noteValueLookup.has(noteRealValue)) {
return this.noteValueLookup.get(noteRealValue)!;
Expand Down
33 changes: 8 additions & 25 deletions packages/alphatab/src/model/ModelUtils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { AccidentalType } from '@coderline/alphatab/model/AccidentalType';
import { Automation, AutomationType } from '@coderline/alphatab/model/Automation';
import { Bar } from '@coderline/alphatab/model/Bar';
import { Beat } from '@coderline/alphatab/model/Beat';
import type { Duration } from '@coderline/alphatab/model/Duration';
import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@coderline/alphatab/model/Score';
import type { Settings } from '@coderline/alphatab/Settings';
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
import type { KeySignature } from '@coderline/alphatab/model/KeySignature';
import { MasterBar } from '@coderline/alphatab/model/MasterBar';
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
import { HeaderFooterStyle, type Score, ScoreStyle, type ScoreSubElement } from '@coderline/alphatab/model/Score';
import type { Track } from '@coderline/alphatab/model/Track';
import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants';
import { Bar } from '@coderline/alphatab/model/Bar';
import { Voice } from '@coderline/alphatab/model/Voice';
import { Automation, AutomationType } from '@coderline/alphatab/model/Automation';
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
import type { KeySignature } from '@coderline/alphatab/model/KeySignature';
import { AccidentalType } from '@coderline/alphatab/model/AccidentalType';
import type { Settings } from '@coderline/alphatab/Settings';
import { SynthConstants } from '@coderline/alphatab/synth/SynthConstants';

/**
* @internal
Expand Down Expand Up @@ -671,22 +670,6 @@ export class ModelUtils {
masterBar.previousMasterBar!.nextMasterBar = null;
}
}

private static _allMusicFontSymbols: MusicFontSymbol[] = [];

/**
* Gets a list of all music font symbols used in alphaTab.
*/
public static getAllMusicFontSymbols(): MusicFontSymbol[] {
if (ModelUtils._allMusicFontSymbols.length === 0) {
ModelUtils._allMusicFontSymbols = Object.values(MusicFontSymbol)
.filter<any>((k: any) => typeof k === 'number')
.map(v => v as number as MusicFontSymbol) as MusicFontSymbol[];
}

return ModelUtils._allMusicFontSymbols;
}

/**
* Lists the display transpositions for some known midi instruments.
* It is a common practice to transpose the standard notation for instruments like guitars.
Expand Down
35 changes: 35 additions & 0 deletions packages/alphatab/src/model/MusicFontSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,38 @@ export enum MusicFontSymbol {
FingeringALower = 0xed1b,
FingeringCLower = 0xed1c
}

/**
* @internal
*/
export class MusicFontSymbolLookup {
private static _allMusicFontSymbols: MusicFontSymbol[] = [];
private static readonly _blackNoteHeadGlyphs = new Set<MusicFontSymbol>();

private static _initialize() {
const all = MusicFontSymbolLookup._allMusicFontSymbols;
if (all.length === 0) {
for (const v of Object.values(MusicFontSymbol).filter<any>((k: any) => typeof k === 'number')) {
const symbol = v as number as MusicFontSymbol;
all.push(symbol);
const name = MusicFontSymbol[symbol].toLowerCase();
if (name.endsWith('black')) {
MusicFontSymbolLookup._blackNoteHeadGlyphs.add(symbol);
}
}
}
}

/**
* Gets a list of all music font symbols used in alphaTab.
*/
public static getAllMusicFontSymbols(): MusicFontSymbol[] {
MusicFontSymbolLookup._initialize();
return MusicFontSymbolLookup._allMusicFontSymbols;
}

public static isBlackNoteHead(glph: MusicFontSymbol): boolean {
MusicFontSymbolLookup._initialize();
return MusicFontSymbolLookup._blackNoteHeadGlyphs.has(glph);
}
}
7 changes: 7 additions & 0 deletions packages/alphatab/src/model/Score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,13 @@ export class Score {
for (let i: number = 0, j: number = this.tracks.length; i < j; i++) {
this.tracks[i].finish(settings, sharedDataBag);
}

// fixup masterbar starts to handle anacrusis lengths
for (const mb of this.masterBars) {
if (mb.index > 0) {
mb.start = mb.previousMasterBar!.start + mb.previousMasterBar!.calculateDuration();
}
}
}

/**
Expand Down
Loading