Skip to content

feat: add beat slash reading and rendering #1646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 25, 2024
Merged
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
5 changes: 5 additions & 0 deletions src.csharp/AlphaTab/Core/TypeHelper.cs
Original file line number Diff line number Diff line change
@@ -35,6 +35,11 @@ public static T Find<T>(this IList<T> list, Func<T, bool> predicate)
return list.FirstOrDefault(predicate);
}

public static bool Some<T>(this IList<T> list, Func<T, bool> predicate)
{
return list.Any(predicate);
}

public static IList<T> Splice<T>(this IList<T> data, double start, double deleteCount)
{
var items = data.GetRange((int) start, (int) deleteCount);
Original file line number Diff line number Diff line change
@@ -59,6 +59,10 @@ public class List<T> : Iterable<T> {
return List(_data.filter(predicate))
}

public fun some(predicate: (T) -> Boolean): Boolean {
return _data.any(predicate)
}

public fun indexOf(value: T): Double {
return _data.indexOf(value).toDouble()
}
3 changes: 3 additions & 0 deletions src/exporter/GpifWriter.ts
Original file line number Diff line number Diff line change
@@ -737,6 +737,9 @@ export class GpifWriter {
}

beatNode.addElement('ConcertPitchStemOrientation').innerText = 'Undefined';
if(beat.slashed) {
beatNode.addElement('Slashed');
}
if (!beat.isRest) {
beatNode.addElement('Notes').innerText = beat.notes.map(n => n.id).join(' ');
}
1 change: 1 addition & 0 deletions src/generated/model/BeatCloner.ts
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ export class BeatCloner {
clone.slap = original.slap;
clone.tap = original.tap;
clone.text = original.text;
clone.slashed = original.slashed;
clone.brushType = original.brushType;
clone.brushDuration = original.brushDuration;
clone.tupletDenominator = original.tupletDenominator;
4 changes: 4 additions & 0 deletions src/generated/model/BeatSerializer.ts
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ export class BeatSerializer {
o.set("slap", obj.slap);
o.set("tap", obj.tap);
o.set("text", obj.text);
o.set("slashed", obj.slashed);
o.set("brushtype", obj.brushType as number);
o.set("brushduration", obj.brushDuration);
o.set("tupletdenominator", obj.tupletDenominator);
@@ -136,6 +137,9 @@ export class BeatSerializer {
case "text":
obj.text = v as string | null;
return true;
case "slashed":
obj.slashed = v! as boolean;
return true;
case "brushtype":
obj.brushType = JsonHelper.parseEnum<BrushType>(v, BrushType)!;
return true;
4 changes: 4 additions & 0 deletions src/importer/AlphaTexImporter.ts
Original file line number Diff line number Diff line change
@@ -1536,6 +1536,10 @@ export class AlphaTexImporter extends ScoreImporter {
this._sy = this.newSy();
}
return true;
} else if (syData === 'slashed') {
beat.slashed = true;
this._sy = this.newSy();
return true;
} else {
// string didn't match any beat effect syntax
return false;
3 changes: 3 additions & 0 deletions src/importer/GpifParser.ts
Original file line number Diff line number Diff line change
@@ -1565,6 +1565,9 @@ export class GpifParser {
beat.lyrics = this.parseBeatLyrics(c);
this._skipApplyLyrics = true;
break;
case 'Slashed':
beat.slashed = true;
break;
}
}
}
5 changes: 5 additions & 0 deletions src/model/Beat.ts
Original file line number Diff line number Diff line change
@@ -241,6 +241,11 @@ export class Beat {
*/
public text: string | null = null;

/**
* Gets or sets whether this beat should be rendered as slashed note.
*/
public slashed: boolean = false;

/**
* Gets or sets the brush type applied to the notes of this beat.
*/
20 changes: 12 additions & 8 deletions src/rendering/LineBarRenderer.ts
Original file line number Diff line number Diff line change
@@ -311,8 +311,10 @@ export abstract class LineBarRenderer extends BarRendererBase {
canvas.moveTo(cx + this.x + startX, (cy + this.y + startY - offset) | 0);
if (bracketsAsArcs) {
canvas.quadraticCurveTo(
cx + this.x + (offset1X + startX) / 2, (cy + this.y + offset1Y - offset - size) | 0,
cx + this.x + offset1X, (cy + this.y + offset1Y - offset - size) | 0
cx + this.x + (offset1X + startX) / 2,
(cy + this.y + offset1Y - offset - size) | 0,
cx + this.x + offset1X,
(cy + this.y + offset1Y - offset - size) | 0
);
} else {
canvas.lineTo(cx + this.x + startX, (cy + this.y + startY - offset - size) | 0);
@@ -324,8 +326,10 @@ export abstract class LineBarRenderer extends BarRendererBase {
canvas.moveTo(cx + this.x + offset2X, (cy + this.y + offset2Y - offset - size) | 0);
if (bracketsAsArcs) {
canvas.quadraticCurveTo(
cx + this.x + (endX + offset2X) / 2, (cy + this.y + offset2Y - offset - size) | 0,
cx + this.x + endX, (cy + this.y + endY - offset) | 0,
cx + this.x + (endX + offset2X) / 2,
(cy + this.y + offset2Y - offset - size) | 0,
cx + this.x + endX,
(cy + this.y + endY - offset) | 0
);
} else {
canvas.lineTo(cx + this.x + endX, (cy + this.y + endY - offset - size) | 0);
@@ -367,8 +371,8 @@ export abstract class LineBarRenderer extends BarRendererBase {
}
}

protected abstract getFlagTopY(beat: Beat): number;
protected abstract getFlagBottomY(beat: Beat): number;
protected abstract getFlagTopY(beat: Beat, direction: BeamDirection): number;
protected abstract getFlagBottomY(beat: Beat, direction: BeamDirection): number;
protected shouldPaintFlag(beat: Beat, h: BeamingHelper): boolean {
// no flags for bend grace beats
if (beat.graceType === GraceType.BendGrace) {
@@ -411,8 +415,8 @@ export abstract class LineBarRenderer extends BarRendererBase {
let stemSize: number = this.getFlagStemSize(h.shortestDuration);
let beatLineX: number = h.getBeatLineX(beat);
let direction: BeamDirection = this.getBeamDirection(h);
let topY: number = this.getFlagTopY(beat);
let bottomY: number = this.getFlagBottomY(beat);
let topY: number = this.getFlagTopY(beat, direction);
let bottomY: number = this.getFlagBottomY(beat, direction);
let beamY: number = 0;
if (direction === BeamDirection.Down) {
bottomY += stemSize * scaleMod;
4 changes: 2 additions & 2 deletions src/rendering/NumberedBarRenderer.ts
Original file line number Diff line number Diff line change
@@ -197,11 +197,11 @@ export class NumberedBarRenderer extends LineBarRenderer {
return super.tupletOffset + this.resources.numberedNotationFont.size * this.scale;
}

protected override getFlagTopY(_beat: Beat): number {
protected override getFlagTopY(_beat: Beat, _direction:BeamDirection): number {
return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale;
}

protected override getFlagBottomY(_beat: Beat): number {
protected override getFlagBottomY(_beat: Beat, _direction:BeamDirection): number {
return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale;
}

73 changes: 66 additions & 7 deletions src/rendering/ScoreBarRenderer.ts
Original file line number Diff line number Diff line change
@@ -143,11 +143,41 @@ export class ScoreBarRenderer extends LineBarRenderer {
this.paintTuplets(cx, cy, canvas);
}

protected override getFlagTopY(beat: Beat): number {
private getSlashFlagY(duration: Duration, direction: BeamDirection) {
let line = (this.heightLineCount - 1) / 2;
let offset = 0;
switch (duration) {
case Duration.QuadrupleWhole:
case Duration.DoubleWhole:
case Duration.Whole:
offset += 2;
break;
default:
offset += 1;
break;
}

if (direction == BeamDirection.Down) {
line += offset;
} else {
line -= offset;
}

const slashY = this.getLineY(line);
return slashY;
}

protected override getFlagTopY(beat: Beat, direction: BeamDirection): number {
if (beat.slashed) {
return this.getSlashFlagY(beat.duration, direction);
}
return this.getScoreY(this.accidentalHelper.getMinLine(beat));
}

protected override getFlagBottomY(beat: Beat): number {
protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number {
if (beat.slashed) {
return this.getSlashFlagY(beat.duration, direction);
}
return this.getScoreY(this.accidentalHelper.getMaxLine(beat));
}

@@ -200,6 +230,11 @@ export class ScoreBarRenderer extends LineBarRenderer {
}

public override getNoteY(note: Note, requestedPosition: NoteYPosition): number {
if(note.beat.slashed) {
let line = (this.heightLineCount - 1) / 2;
return this.getLineY(line);
}

let y = super.getNoteY(note, requestedPosition);
if (isNaN(y)) {
// NOTE: some might request the note position before the glyphs have been created
@@ -244,7 +279,7 @@ export class ScoreBarRenderer extends LineBarRenderer {
const firstBeat = h.beats[0];
const lastBeat = h.beats[h.beats.length - 1];

let isRest = h.isRestBeamHelper;
const isRest = h.isRestBeamHelper;

// 1. put direct diagonal line.
drawingInfo.startBeat = firstBeat;
@@ -255,8 +290,8 @@ export class ScoreBarRenderer extends LineBarRenderer {
} else {
drawingInfo.startY =
direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMinLine(firstBeat)) - stemSize
: this.getScoreY(this.accidentalHelper.getMaxLine(firstBeat)) + stemSize;
? this.getFlagTopY(firstBeat, direction) - stemSize
: this.getFlagBottomY(firstBeat, direction) + stemSize;
}

drawingInfo.endBeat = lastBeat;
@@ -267,8 +302,8 @@ export class ScoreBarRenderer extends LineBarRenderer {
} else {
drawingInfo.endY =
direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMinLine(lastBeat)) - stemSize
: this.getScoreY(this.accidentalHelper.getMaxLine(lastBeat)) + stemSize;
? this.getFlagTopY(lastBeat, direction) - stemSize
: this.getFlagBottomY(lastBeat, direction) + stemSize;
}
// 2. ensure max height
// we use the min/max notes to place the beam along their real position
@@ -359,13 +394,37 @@ export class ScoreBarRenderer extends LineBarRenderer {
}
}
}

// check if slash shifts bar up or down
if (h.slashBeats.length > 0) {
for (const b of h.slashBeats) {
const yGivenByCurrentValues = drawingInfo.calcY(h.getBeatLineX(b));
let yNeededForSlash = this.getSlashFlagY(b.duration, direction);

if (direction === BeamDirection.Up) {
yNeededForSlash -= stemSize;
} else if (direction === BeamDirection.Down) {
yNeededForSlash += stemSize;
}

const diff = yNeededForSlash - yGivenByCurrentValues;
if (diff > 0) {
drawingInfo.startY += diff;
drawingInfo.endY += diff;
}
}
}
}
}

return h.drawingInfos.get(direction)!.calcY(x);
}

protected override getBarLineStart(beat: Beat, direction: BeamDirection): number {
if (beat.slashed) {
return this.getSlashFlagY(beat.duration, direction);
}

return direction === BeamDirection.Up
? this.getScoreY(this.accidentalHelper.getMaxLine(beat))
: this.getScoreY(this.accidentalHelper.getMinLine(beat));
5 changes: 3 additions & 2 deletions src/rendering/SlashBarRenderer.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ export class SlashBarRenderer extends LineBarRenderer {
super(renderer, bar);
// ignore numbered notation here
this._isOnlySlash = !bar.staff.showTablature && !bar.staff.showStandardNotation;
this.helpers.preferredBeamDirection = BeamDirection.Up;
}

public override get lineSpacing(): number {
@@ -74,11 +75,11 @@ export class SlashBarRenderer extends LineBarRenderer {
return 0;
}

protected override getFlagTopY(_beat: Beat): number {
protected override getFlagTopY(_beat: Beat, _direction:BeamDirection): number {
return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale;
}

protected override getFlagBottomY(_beat: Beat): number {
protected override getFlagBottomY(_beat: Beat, _direction:BeamDirection): number {
return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale;
}

4 changes: 2 additions & 2 deletions src/rendering/TabBarRenderer.ts
Original file line number Diff line number Diff line change
@@ -177,7 +177,7 @@ export class TabBarRenderer extends LineBarRenderer {
return super.drawBeamHelperAsFlags(h) || this.settings.notation.rhythmMode === TabRhythmMode.ShowWithBeams;
}

protected override getFlagTopY(beat: Beat): number {
protected override getFlagTopY(beat: Beat, _direction:BeamDirection): number {
const startGlyph: TabBeatGlyph = this.getOnNotesGlyphForBeat(beat) as TabBeatGlyph;
if (!startGlyph.noteNumbers || beat.duration === Duration.Half) {
return this.height - this.settings.notation.rhythmHeight * this.settings.display.scale - this.tupletSize;
@@ -186,7 +186,7 @@ export class TabBarRenderer extends LineBarRenderer {
}
}

protected override getFlagBottomY(_beat: Beat): number {
protected override getFlagBottomY(_beat: Beat, _direction:BeamDirection): number {
return this.getFlagAndBarPos();
}

Loading