Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions packages/alphatab/src/EngravingSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { JsonHelper } from '@coderline/alphatab/io/JsonHelper';
import { Logger } from '@coderline/alphatab/Logger';
import { Duration } from '@coderline/alphatab/model/Duration';
import { MusicFontSymbol, MusicFontSymbolLookup } from '@coderline/alphatab/model/MusicFontSymbol';
import { RestPosition } from '@coderline/alphatab/model/RestPosition';
import type { SmuflMetadata } from '@coderline/alphatab/SmuflMetadata';

/**
Expand Down Expand Up @@ -256,6 +257,22 @@ export class EngravingSettings {
*/
public stemFlagOffsets: Map<Duration, number> = new Map<Duration, number>();

/**
* Overrides the vertical position of rests for the primary voice (voice 0).
* Use {@link RestPosition} constants for named staff line positions.
* When null, the position is calculated automatically from the staff line count.
* @public
*/
public restPositionMain: RestPosition | null = null;

/**
* Overrides the vertical position of rests for secondary voices (voice 1+).
* Use {@link RestPosition} constants for named staff line positions.
* When null, the position is calculated automatically from the staff line count.
* @public
*/
public restPositionSecondary: RestPosition | null = null;

/**
* A lookup containing the offset from the visual top to the glyph center.
* The glyph center is the origin coordinate at which the glyph paths start when drawn on the alphabetic baseline.
Expand Down
2 changes: 2 additions & 0 deletions packages/alphatab/src/generated/EngravingSettingsCloner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class EngravingSettingsCloner {
clone.repeatOffsetX = new Map(original.repeatOffsetX);
clone.standardStemLength = original.standardStemLength;
clone.stemFlagOffsets = new Map(original.stemFlagOffsets);
clone.restPositionMain = original.restPositionMain;
clone.restPositionSecondary = original.restPositionSecondary;
clone.glyphTop = new Map(original.glyphTop);
clone.glyphBottom = new Map(original.glyphBottom);
clone.glyphWidths = new Map(original.glyphWidths);
Expand Down
15 changes: 15 additions & 0 deletions packages/alphatab/src/generated/EngravingSettingsJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { MusicFontSymbol } from "@coderline/alphatab/model/MusicFontSymbol";
import { EngravingStemInfoJson } from "@coderline/alphatab/generated/EngravingStemInfoJson";
import { Duration } from "@coderline/alphatab/model/Duration";
import { RestPosition } from "@coderline/alphatab/model/RestPosition";
/**
* This class holds all all spacing, thickness and scaling metrics
* related to engraving the music notation.
Expand Down Expand Up @@ -184,6 +185,20 @@ export interface EngravingSettingsJson {
* @smufl 1.4
*/
stemFlagOffsets?: Map<Duration | keyof typeof Duration | Lowercase<keyof typeof Duration>, number>;
/**
* Overrides the vertical position of rests for the primary voice (voice 0).
* Use {@link RestPosition} constants for named staff line positions.
* When null, the position is calculated automatically from the staff line count.
* @public
*/
restPositionMain?: (RestPosition | null) | keyof typeof RestPosition | Lowercase<keyof typeof RestPosition>;
/**
* Overrides the vertical position of rests for secondary voices (voice 1+).
* Use {@link RestPosition} constants for named staff line positions.
* When null, the position is calculated automatically from the staff line count.
* @public
*/
restPositionSecondary?: (RestPosition | null) | keyof typeof RestPosition | Lowercase<keyof typeof RestPosition>;
/**
* A lookup containing the offset from the visual top to the glyph center.
* The glyph center is the origin coordinate at which the glyph paths start when drawn on the alphabetic baseline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EngravingStemInfoSerializer } from "@coderline/alphatab/generated/Engra
import { MusicFontSymbol } from "@coderline/alphatab/model/MusicFontSymbol";
import { EngravingStemInfo } from "@coderline/alphatab/EngravingSettings";
import { Duration } from "@coderline/alphatab/model/Duration";
import { RestPosition } from "@coderline/alphatab/model/RestPosition";
/**
* @internal
*/
Expand Down Expand Up @@ -79,6 +80,8 @@ export class EngravingSettingsSerializer {
m.set(k.toString(), v);
}
}
o.set("restpositionmain", obj.restPositionMain as number | null);
o.set("restpositionsecondary", obj.restPositionSecondary as number | null);
{
const m = new Map<string, unknown>();
o.set("glyphtop", m);
Expand Down Expand Up @@ -275,6 +278,12 @@ export class EngravingSettingsSerializer {
obj.stemFlagOffsets.set(JsonHelper.parseEnum<Duration>(k, Duration)!, v as number);
});
return true;
case "restpositionmain":
obj.restPositionMain = JsonHelper.parseEnum<RestPosition>(v, RestPosition) ?? null;
return true;
case "restpositionsecondary":
obj.restPositionSecondary = JsonHelper.parseEnum<RestPosition>(v, RestPosition) ?? null;
return true;
case "glyphtop":
obj.glyphTop = new Map<MusicFontSymbol, number>();
JsonHelper.forEach(v, (v, k) => {
Expand Down
54 changes: 54 additions & 0 deletions packages/alphatab/src/model/RestPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Defines named vertical positions for rests on a standard 5-line staff,
* expressed as steps in alphaTab's internal coordinate system.
* Use these values with {@link EngravingSettings.restPositionMain} and
* {@link EngravingSettings.restPositionSecondary}.
* @public
*/

/*
* The settings assume a 5 line staff or lower and are adjusted proportionally within ScoreBeatGlypth._createRestGlyphs()
* based upon the renderer.bar.staff.standardNotationLineCount value
*/
export enum RestPosition {
/**
* Bottom line of the staff (line 1 in standard notation).
*/
Line1 = 8.5,
/**
* Space between line 1 and line 2.
*/
Line1Space = 7.5,
/**
* Second line from the bottom.
*/
Line2 = 6.5,
/**
* Space between line 2 and line 3.
*/
Line2Space = 5.5,
/**
* Middle line of the staff.
*/
Line3 = 4.5,
/**
* Space between line 3 and line 4.
*/
Line3Space = 3.5,
/**
* Second line from the top.
*/
Line4 = 2.5,
/**
* Space between line 4 and line 5.
*/
Line4Space = 1.5,
/**
* Top line of the staff (line 5 in standard notation).
*/
Line5 = 0.5,
/**
* Space above the top line.
*/
Line5Space = -0.5
}
1 change: 1 addition & 0 deletions packages/alphatab/src/model/_barrel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { Color } from '@coderline/alphatab/model/Color';
export { CrescendoType } from '@coderline/alphatab/model/CrescendoType';
export { Direction } from '@coderline/alphatab/model/Direction';
export { Duration } from '@coderline/alphatab/model/Duration';
export { RestPosition } from '@coderline/alphatab/model/RestPosition';
export { DynamicValue } from '@coderline/alphatab/model/DynamicValue';
export { FadeType } from '@coderline/alphatab/model/FadeType';
export { FermataType, Fermata } from '@coderline/alphatab/model/Fermata';
Expand Down
26 changes: 23 additions & 3 deletions packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,17 +300,37 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
}

private _createRestGlyphs() {

const sr = this.renderer as ScoreBarRenderer;
const engraving = sr.resources.engravingSettings;
const override = this.container.beat.voice.index === 0 ? engraving.restPositionMain : engraving.restPositionSecondary;
const lineCount = this.renderer.bar.staff.standardNotationLineCount;

let steps: number;

let steps = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2;
// If there is a restPosition settings override and there are 5 staff lines or fewer then use
// it after adjusting its value based on the lineCount other wise just use the standard placement
if (override !== null && lineCount <= 5) {
steps = override - (5 - lineCount) * 2;

if (
this.container.beat.duration === Duration.Whole &&
(lineCount == 1 || lineCount == 3)
) {
steps -= 1;
}
}
else {
steps = Math.ceil((lineCount - 1) / 2) * 2;
}

// this positioning is quite strange, for most staff line counts
// the whole/rest are aligned as half below the whole rest.
// but for staff line count 1 and 3 they are aligned centered on the same line.
if (
this.container.beat.duration === Duration.Whole &&
this.renderer.bar.staff.standardNotationLineCount !== 1 &&
this.renderer.bar.staff.standardNotationLineCount !== 3
lineCount !== 1 &&
lineCount !== 3
) {
steps -= 2;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions packages/alphatab/test/visualTests/features/RestPosition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it } from 'vitest';
import { Settings } from '@coderline/alphatab/Settings';
import { RestPosition } from '@coderline/alphatab/model/RestPosition';
import { VisualTestHelper } from 'test/visualTests/VisualTestHelper';
const primaryDurations = `r.1 | r.2 e5.2 | r.4 e5.4 *3 | r.8 e5.8 *7 | r.16 e5.16 *15 | r.32 e5.32 *31`;
const secondaryDurations = `e4.1 | e4.2 r.2 | e4.4 *3 r.4 | e4.8 *7 r.8 | e4.16 *15 r.16 | e4.32 *31 r.32`;
const restPositionEntries = Object.entries(RestPosition).filter(([k]) => isNaN(Number(k))) as [string, RestPosition][];

describe('RestPositionTests', () => {
it('rest-position-default', async () => {
await VisualTestHelper.runVisualTestTex(
primaryDurations,
'test-data/visual-tests/rest-position/rest-position-default.png'
);
});

it('rest-position-default-multi-voice', async () => {
await VisualTestHelper.runVisualTestTex(
`\\voice ${primaryDurations} \\voice ${secondaryDurations}`,
'test-data/visual-tests/rest-position/rest-position-default-multi-voice.png'
);
});

describe('rest-position-main', () => {
for (const [name, value] of restPositionEntries) {
it(`position-${name}`, async () => {
const settings = new Settings();
settings.display.resources.engravingSettings.restPositionMain = value;
await VisualTestHelper.runVisualTestTex(
primaryDurations,
`test-data/visual-tests/rest-position/rest-position-main-${name}.png`,
settings
);
});
}
});

it('rest-position-durations', async () => {
const settings = new Settings();
settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line5;
await VisualTestHelper.runVisualTestTex(
primaryDurations,
'test-data/visual-tests/rest-position/rest-position-durations.png',
settings
);
});

it('rest-position-multi-voice-both-set', async () => {
const settings = new Settings();
settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line5;
settings.display.resources.engravingSettings.restPositionSecondary = RestPosition.Line1;
await VisualTestHelper.runVisualTestTex(
`\\voice ${primaryDurations} \\voice ${secondaryDurations}`,
'test-data/visual-tests/rest-position/rest-position-multi-voice-both-set.png',
settings
);
});

it('rest-position-multi-voice-main-only', async () => {
const settings = new Settings();
settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line5;
await VisualTestHelper.runVisualTestTex(
`\\voice ${primaryDurations} \\voice ${secondaryDurations}`,
'test-data/visual-tests/rest-position/rest-position-multi-voice-main-only.png',
settings
);
});

it('rest-position-multi-voice-secondary-only', async () => {
const settings = new Settings();
settings.display.resources.engravingSettings.restPositionSecondary = RestPosition.Line1;
await VisualTestHelper.runVisualTestTex(
`\\voice ${primaryDurations} \\voice ${secondaryDurations}`,
'test-data/visual-tests/rest-position/rest-position-multi-voice-secondary-only.png',
settings
);
});

describe('rest-position-staff-lines', () => {
for (const lineCount of [1, 2, 3, 4, 5]) {
it(`linecount-${lineCount}`, async () => {
const settings = new Settings();
settings.display.resources.engravingSettings.restPositionMain = RestPosition.Line1;
await VisualTestHelper.runVisualTestTex(
`\\staff { score ${lineCount} } ${primaryDurations}`,
`test-data/visual-tests/rest-position/rest-position-staff-lines-${lineCount}.png`,
settings
);
});
}
});
});