Skip to content

Commit e52f8e8

Browse files
feat(rendering): add per-beat rest display position via beat.restDisplayTone, beat.restDisplayOctave (#2721)
Co-authored-by: Danielku15 <danielku15@coderline.net>
1 parent 4ab6c5f commit e52f8e8

21 files changed

Lines changed: 284 additions & 17 deletions

packages/alphatab/src/generated/model/BeatCloner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export class BeatCloner {
3939
clone.text = original.text;
4040
clone.slashed = original.slashed;
4141
clone.deadSlapped = original.deadSlapped;
42+
clone.restDisplayTone = original.restDisplayTone;
43+
clone.restDisplayOctave = original.restDisplayOctave;
4244
clone.brushType = original.brushType;
4345
clone.brushDuration = original.brushDuration;
4446
clone.tupletDenominator = original.tupletDenominator;

packages/alphatab/src/generated/model/BeatSerializer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export class BeatSerializer {
6464
o.set("text", obj.text);
6565
o.set("slashed", obj.slashed);
6666
o.set("deadslapped", obj.deadSlapped);
67+
o.set("restdisplaytone", obj.restDisplayTone);
68+
o.set("restdisplayoctave", obj.restDisplayOctave);
6769
o.set("brushtype", obj.brushType as number);
6870
o.set("brushduration", obj.brushDuration);
6971
o.set("tupletdenominator", obj.tupletDenominator);
@@ -168,6 +170,12 @@ export class BeatSerializer {
168170
case "deadslapped":
169171
obj.deadSlapped = v! as boolean;
170172
return true;
173+
case "restdisplaytone":
174+
obj.restDisplayTone = v! as number;
175+
return true;
176+
case "restdisplayoctave":
177+
obj.restDisplayOctave = v! as number;
178+
return true;
171179
case "brushtype":
172180
obj.brushType = JsonHelper.parseEnum<BrushType>(v, BrushType)!;
173181
return true;

packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageDefinitions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ export class AlphaTex1LanguageDefinitions {
664664
]
665665
],
666666
['txt', [[[[17, 10], 0]]]],
667+
['restdisplaypitch', [[[[10, 17], 0]]]],
667668
[
668669
'lyrics',
669670
[

packages/alphatab/src/importer/alphaTex/AlphaTex1LanguageHandler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,23 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler
16161616
case 'txt':
16171617
beat.text = (p.arguments!.arguments[0] as AlphaTexTextNode).text;
16181618
return ApplyNodeResult.Applied;
1619+
case 'restdisplaypitch': {
1620+
const tuning = ModelUtils.parseTuning((p.arguments!.arguments[0] as AlphaTexTextNode).text);
1621+
if (tuning !== null) {
1622+
beat.restDisplayTone = tuning.tone.noteValue;
1623+
beat.restDisplayOctave = tuning.octave - 1;
1624+
} else {
1625+
importer.addSemanticDiagnostic({
1626+
code: AlphaTexDiagnosticCode.AT212,
1627+
message: `Invalid pitch value '${(p.arguments!.arguments[0] as AlphaTexTextNode).text}', expected format like 'C5' or 'G4'`,
1628+
severity: AlphaTexDiagnosticsSeverity.Error,
1629+
start: p.arguments!.arguments[0].start,
1630+
end: p.arguments!.arguments[0].end
1631+
});
1632+
return ApplyNodeResult.NotAppliedSemanticError;
1633+
}
1634+
return ApplyNodeResult.Applied;
1635+
}
16191636
case 'lyrics':
16201637
let lyricsLine = 0;
16211638
let lyricsText = '';

packages/alphatab/src/model/Beat.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,18 @@ export class Beat {
425425
*/
426426
public deadSlapped: boolean = false;
427427

428+
/**
429+
* Gets or sets the chromatic tone value (0–11) of the pitch at which this rest should be displayed.
430+
* A value of -1 means use the default position formula.
431+
*/
432+
public restDisplayTone: number = -1;
433+
434+
/**
435+
* Gets or sets the octave at which this rest should be displayed.
436+
* Only relevant when {@link restDisplayTone} is set. -1 means use the default position formula.
437+
*/
438+
public restDisplayOctave: number = -1;
439+
428440
/**
429441
* Gets or sets the brush type applied to the notes of this beat.
430442
*/

packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Logger } from '@coderline/alphatab/Logger';
22
import { AccentuationType } from '@coderline/alphatab/model/AccentuationType';
3+
import { AccidentalHelper } from '@coderline/alphatab/rendering/utils/AccidentalHelper';
34
import { BeatSubElement } from '@coderline/alphatab/model/Beat';
45
import { Duration } from '@coderline/alphatab/model/Duration';
56
import { GraceType } from '@coderline/alphatab/model/GraceType';
@@ -301,18 +302,23 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
301302

302303
private _createRestGlyphs() {
303304
const sr = this.renderer as ScoreBarRenderer;
304-
305-
let steps = Math.ceil((this.renderer.bar.staff.standardNotationLineCount - 1) / 2) * 2;
306-
307-
// this positioning is quite strange, for most staff line counts
308-
// the whole/rest are aligned as half below the whole rest.
309-
// but for staff line count 1 and 3 they are aligned centered on the same line.
310-
if (
311-
this.container.beat.duration === Duration.Whole &&
312-
this.renderer.bar.staff.standardNotationLineCount !== 1 &&
313-
this.renderer.bar.staff.standardNotationLineCount !== 3
314-
) {
315-
steps -= 2;
305+
const beat = this.container.beat;
306+
const lineCount = this.renderer.bar.staff.standardNotationLineCount;
307+
308+
let steps: number;
309+
if (beat.restDisplayTone !== -1 && beat.restDisplayOctave !== -1) {
310+
// Per-beat override: same step as a note at that pitch. SMuFL rest glyphs use the same
311+
// baseline convention as note heads, so no further adjustment is applied.
312+
steps = AccidentalHelper.calculateRestDisplaySteps(sr.bar, beat.restDisplayTone, beat.restDisplayOctave);
313+
} else {
314+
// Default placement: centred on the staff. Whole rests sit one line above (per SMuFL/Guitar Pro
315+
// convention) so their hanging body lines up with where half/shorter rest bodies appear.
316+
// 1- and 3-line staves keep the whole rest on the default rest line (Guitar Pro convention;
317+
// see musescore/MuseScore#25279).
318+
steps = Math.ceil((lineCount - 1) / 2) * 2;
319+
if (beat.duration === Duration.Whole && lineCount !== 1 && lineCount !== 3) {
320+
steps -= 2;
321+
}
316322
}
317323

318324
const restGlyph = new ScoreRestGlyph(0, sr.getScoreY(steps), this.container.beat.duration);

packages/alphatab/src/rendering/utils/AccidentalHelper.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Clef } from '@coderline/alphatab/model/Clef';
55
import { ModelUtils, type ResolvedSpelling } from '@coderline/alphatab/model/ModelUtils';
66
import type { Note } from '@coderline/alphatab/model/Note';
77
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
8+
import { Ottavia } from '@coderline/alphatab/model/Ottavia';
89
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
910
import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer';
1011
import type { ScoreBarRenderer } from '@coderline/alphatab/rendering/ScoreBarRenderer';
@@ -267,6 +268,27 @@ export class AccidentalHelper {
267268
return steps;
268269
}
269270

271+
public static calculateRestDisplaySteps(bar: Bar, tone: number, octave: number): number {
272+
let noteValue = (octave + 1) * 12 + tone;
273+
switch (bar.clefOttava) {
274+
case Ottavia._15ma:
275+
noteValue -= 24;
276+
break;
277+
case Ottavia._8va:
278+
noteValue -= 12;
279+
break;
280+
case Ottavia._8vb:
281+
noteValue += 12;
282+
break;
283+
case Ottavia._15mb:
284+
noteValue += 24;
285+
break;
286+
}
287+
288+
const spelling = ModelUtils.resolveSpelling(bar.keySignature, noteValue, NoteAccidentalMode.Default);
289+
return AccidentalHelper.calculateNoteSteps(bar.clef, spelling);
290+
}
291+
270292
public getNoteSteps(n: Note): number {
271293
return this._appliedScoreSteps.get(n.id)!;
272294
}
19.7 KB
Loading
18.9 KB
Loading
21.1 KB
Loading

0 commit comments

Comments
 (0)