Skip to content
Merged
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
3 changes: 3 additions & 0 deletions packages/alphatab/src/NotationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,9 @@ export class NotationSettings {
* @remarks
* This setting allows transposing of tracks for display and playback.
* The `transpositionPitches` setting allows defining an additional pitch offset per track, that is then considered when displaying the music sheet.
*
* {@since 1.10.0} Guitar tabs of transposed tracks move notes to other strings when the shifted fret is not playable
* on their string. Notes which fit on no string keep their string and are painted in `display.resources.tabUnplayableFretColor`.
*/
public transpositionPitches: number[] = [];

Expand Down
7 changes: 7 additions & 0 deletions packages/alphatab/src/RenderingResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,13 @@ export class RenderingResources {
*/
public scoreInfoColor: Color = new Color(0, 0, 0, 0xff);

/**
* The color of tab fret numbers which cannot be played on the tuning of the staff after transposition.
* @defaultValue `rgb(200, 0, 0)`
* @since 1.10.0
*/
public tabUnplayableFretColor: Color = new Color(200, 0, 0, 0xff);

public constructor() {
for (const [k, v] of RenderingResources.defaultFonts) {
this.elementFonts.set(k, v.withSize(v.size));
Expand Down
3 changes: 3 additions & 0 deletions packages/alphatab/src/generated/NotationSettingsJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ export interface NotationSettingsJson {
* @remarks
* This setting allows transposing of tracks for display and playback.
* The `transpositionPitches` setting allows defining an additional pitch offset per track, that is then considered when displaying the music sheet.
*
* {@since 1.10.0} Guitar tabs of transposed tracks move notes to other strings when the shifted fret is not playable
* on their string. Notes which fit on no string keep their string and are painted in `display.resources.tabUnplayableFretColor`.
*/
transpositionPitches?: number[];
/**
Expand Down
6 changes: 6 additions & 0 deletions packages/alphatab/src/generated/RenderingResourcesJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,10 @@ export interface RenderingResourcesJson {
* @since 0.9.6
*/
scoreInfoColor?: ColorJson;
/**
* The color of tab fret numbers which cannot be played on the tuning of the staff after transposition.
* @defaultValue `rgb(200, 0, 0)`
* @since 1.10.0
*/
tabUnplayableFretColor?: ColorJson;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { IObjectWriter } from "@coderline/alphatab/io/IObjectWriter";
* @internal
*/
export class RenderingResourcesSerializer {
private static readonly _fields: RecordFields = new RecordFields(["smuflfontfamilyname", "engravingsettings", "copyrightfont", "titlefont", "subtitlefont", "wordsfont", "timerfont", "directionsfont", "fretboardnumberfont", "fingeringfont", "inlinefingeringfont", "markerfont", "effectfont", "barnumberfont", "elementfonts", "numberednotationfont", "numberednotationgracefont", "tablaturefont", "gracefont", "stafflinecolor", "barseparatorcolor", "barnumbercolor", "mainglyphcolor", "secondaryglyphcolor", "scoreinfocolor"]);
private static readonly _fields: RecordFields = new RecordFields(["smuflfontfamilyname", "engravingsettings", "copyrightfont", "titlefont", "subtitlefont", "wordsfont", "timerfont", "directionsfont", "fretboardnumberfont", "fingeringfont", "inlinefingeringfont", "markerfont", "effectfont", "barnumberfont", "elementfonts", "numberednotationfont", "numberednotationgracefont", "tablaturefont", "gracefont", "stafflinecolor", "barseparatorcolor", "barnumbercolor", "mainglyphcolor", "secondaryglyphcolor", "scoreinfocolor", "tabunplayablefretcolor"]);
public static read(obj: RenderingResources, reader: IObjectReader): void {
reader.readRecord(RenderingResourcesSerializer._fields, (id, k, rr) => RenderingResourcesSerializer._readField(obj, id, rr, false));
}
Expand Down Expand Up @@ -101,6 +101,9 @@ export class RenderingResourcesSerializer {
case 24:
obj.scoreInfoColor = Color.fromJson(r.readValue())!;
return true;
case 25:
obj.tabUnplayableFretColor = Color.fromJson(r.readValue())!;
return true;
}
return false;
}
Expand Down Expand Up @@ -148,6 +151,7 @@ export class RenderingResourcesSerializer {
w.valueField(22, Color.toJson(obj.mainGlyphColor));
w.valueField(23, Color.toJson(obj.secondaryGlyphColor));
w.valueField(24, Color.toJson(obj.scoreInfoColor));
w.valueField(25, Color.toJson(obj.tabUnplayableFretColor));
w.endObject();
}
}
2 changes: 1 addition & 1 deletion packages/alphatab/src/generated/SerializerSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export class SerializerSchema {
/**
* A hash of the field ids, keys and types of all serialized classes (50 classes).
*/
public static readonly fingerprint: number = 0xacc550b1;
public static readonly fingerprint: number = 0xa8bdf264;
}
84 changes: 77 additions & 7 deletions packages/alphatab/src/rendering/TabBarRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection
import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper';
import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper';
import type { ScrambledPath } from '@coderline/alphatab/rendering/utils/ScrambledPath';
import { TabFingering } from '@coderline/alphatab/rendering/utils/TabFingering';
import type { NoteNumberGlyph } from '@coderline/alphatab/rendering/glyphs/NoteNumberGlyph';

/**
* This BarRenderer renders a bar using guitar tablature notation
Expand Down Expand Up @@ -137,7 +139,73 @@ export class TabBarRenderer extends LineBarRenderer {
}

public override getNoteLine(note: Note): number {
return this.bar.staff.tuning.length - note.string;
return this.bar.staff.tuning.length - this.getDisplayString(note);
}

/**
* The glyph showing the given note in the tab of its bar, null if the note is not shown.
*/
public getNoteGlyph(note: Note): NoteNumberGlyph | null {
const bar = note.beat.voice.bar;
// a renderer is registered in the layout only after its own layout
const renderer =
bar === this.bar ? this : this.scoreRenderer.layout!.getRendererForBar(TabBarRenderer.StaffId, bar);
const container = renderer instanceof TabBarRenderer ? renderer.getBeatContainer(note.beat) : undefined;
const noteNumbers = container
? ((container as TabBeatContainerGlyph).onNotes as TabBeatGlyph).noteNumbers
: null;
return noteNumbers ? noteNumbers.findNoteGlyph(note) : null;
}

/**
* The string (1 = lowest) on which the note is shown, see {@link TabFingering}.
*/
public getDisplayString(note: Note): number {
const glyph = this.getNoteGlyph(note);
return glyph !== null && glyph.displayString > 0 ? glyph.displayString : note.string;
}

/**
* The fret shown for the note, the node for natural harmonics.
*/
public getDisplayFret(note: Note): number {
const glyph = this.getNoteGlyph(note);
return glyph !== null && glyph.displayString > 0
? glyph.displayFret
: note.fret - note.beat.voice.bar.staff.transpositionPitch;
}

/**
* The stringed note of the beat shown on the highest string.
*/
public getDisplayMaxStringNote(beat: Beat): Note | null {
const container = this.getBeatContainer(beat);
const noteNumbers = container
? ((container as TabBeatContainerGlyph).onNotes as TabBeatGlyph).noteNumbers
: null;
return noteNumbers ? noteNumbers.maxStringNote : beat.maxStringNote;
}

/**
* The stringed note of the beat shown on the lowest string.
*/
public getDisplayMinStringNote(beat: Beat): Note | null {
const container = this.getBeatContainer(beat);
const noteNumbers = container
? ((container as TabBeatContainerGlyph).onNotes as TabBeatGlyph).noteNumbers
: null;
return noteNumbers ? noteNumbers.minStringNote : beat.minStringNote;
}

/**
* The tab glyph of the note for renderers of other notations, resolved on paint when all tab bars are laid out.
*/
public static findTabNoteGlyph(renderer: BarRendererBase, note: Note): NoteNumberGlyph | null {
if (!TabFingering.isActive(note.beat.voice.bar.staff)) {
return null;
}
const tab = renderer.scoreRenderer.layout!.getRendererForBar(TabBarRenderer.StaffId, note.beat.voice.bar);
return tab instanceof TabBarRenderer ? tab.getNoteGlyph(note) : null;
}

public minString = Number.NaN;
Expand Down Expand Up @@ -295,7 +363,7 @@ export class TabBarRenderer extends LineBarRenderer {
const noteGlyphs = noteNumbers.noteGlyphs;
for (let k = 0, nk = noteGlyphs.length; k < nk; k++) {
const g = noteGlyphs[k];
const str = g.note.string;
const str = g.displayString;
if (!g.isEmpty && noteNumbers.getNoteGlyphForString(str) === g) {
const lineIdx = tuningLen - str;
if (lineIdx >= 0 && lineIdx < drawnLineCount) {
Expand Down Expand Up @@ -342,7 +410,7 @@ export class TabBarRenderer extends LineBarRenderer {
const noteGlyphs = noteNumbers.noteGlyphs;
for (let k = 0, nk = noteGlyphs.length; k < nk; k++) {
const g = noteGlyphs[k];
const str = g.note.string;
const str = g.displayString;
if (!g.isEmpty && noteNumbers.getNoteGlyphForString(str) === g) {
const lineIdx = tuningLen - str;
if (lineIdx >= 0 && lineIdx < drawnLineCount) {
Expand Down Expand Up @@ -402,8 +470,10 @@ export class TabBarRenderer extends LineBarRenderer {
// Per-beat half-line overflow, not bar-wide. Strings are 1-indexed: top = tuning.length, bottom = 1.
const beat = beatContainer.beat;
const stringCount = this.bar.staff.tuning.length;
const hasTop = beat.maxStringNote !== null && beat.maxStringNote.string === stringCount;
const hasBottom = beat.minStringNote !== null && beat.minStringNote.string === 1;
const maxStringNote = this.getDisplayMaxStringNote(beat);
const minStringNote = this.getDisplayMinStringNote(beat);
const hasTop = maxStringNote !== null && this.getDisplayString(maxStringNote) === stringCount;
const hasBottom = minStringNote !== null && this.getDisplayString(minStringNote) === 1;
if (!hasTop && !hasBottom) {
return;
}
Expand Down Expand Up @@ -516,7 +586,7 @@ export class TabBarRenderer extends LineBarRenderer {
}

protected override getFlagTopY(beat: Beat, direction: BeamDirection): number {
const maxNote = beat.maxStringNote;
const maxNote = this.getDisplayMaxStringNote(beat);
const position = direction === BeamDirection.Up ? NoteYPosition.TopWithStem : NoteYPosition.StemDown;
if (maxNote) {
return this.getNoteY(maxNote, position);
Expand All @@ -526,7 +596,7 @@ export class TabBarRenderer extends LineBarRenderer {
}

protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number {
const maxNote = beat.minStringNote;
const maxNote = this.getDisplayMinStringNote(beat);
const position = direction === BeamDirection.Up ? NoteYPosition.StemUp : NoteYPosition.BottomWithStem;

if (maxNote) {
Expand Down
53 changes: 48 additions & 5 deletions packages/alphatab/src/rendering/glyphs/FingeringGroupGlyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type { Settings } from '@coderline/alphatab/Settings';
import type { Beat } from '@coderline/alphatab/model/Beat';
import { Fingers } from '@coderline/alphatab/model/Fingers';
import { GeneralMidi } from '@coderline/alphatab/midi/GeneralMidi';
import type { ICanvas } from '@coderline/alphatab/platform/ICanvas';
import { TabBarRenderer } from '@coderline/alphatab/rendering/TabBarRenderer';

/**
* @internal
Expand All @@ -18,6 +20,11 @@ export class FingeringInfo {
public line: number = 0;
public symbols: MusicFontSymbol[];
public color: Color | undefined;
/**
* The notes of the left-hand fingers in {@link symbols}.
*/
public leftHandNotes: Note[] = [];
public otherSymbolCount: number = 0;

public constructor(line: number, symbols: MusicFontSymbol[]) {
this.line = line;
Expand All @@ -30,6 +37,7 @@ export class FingeringInfo {
*/
export class FingeringGroupGlyph extends GlyphGroup {
private _infos: Map<number, FingeringInfo> = new Map<number, FingeringInfo>();
private _glyphInfos: FingeringInfo[] = [];

public constructor() {
super(0, 0);
Expand Down Expand Up @@ -72,7 +80,7 @@ export class FingeringGroupGlyph extends GlyphGroup {
true
);
if (symbolLeft !== MusicFontSymbol.None) {
this._addFinger(note, symbolLeft);
this._addFinger(note, symbolLeft, true);
}
const symbolRight = FingeringGroupGlyph.fingerToMusicFontSymbol(
this.renderer.settings,
Expand All @@ -81,7 +89,7 @@ export class FingeringGroupGlyph extends GlyphGroup {
false
);
if (symbolRight !== MusicFontSymbol.None) {
this._addFinger(note, symbolRight);
this._addFinger(note, symbolRight, false);
}
}

Expand Down Expand Up @@ -153,18 +161,52 @@ export class FingeringGroupGlyph extends GlyphGroup {
}
}

private _addFinger(note: Note, symbol: MusicFontSymbol) {
private _addFinger(note: Note, symbol: MusicFontSymbol, isLeftHand: boolean) {
const sr = this.renderer as ScoreBarRenderer;
const steps: number = sr.getNoteSteps(note);

let info: FingeringInfo;
if (!this._infos.has(steps)) {
const info = new FingeringInfo(steps, [symbol]);
info = new FingeringInfo(steps, [symbol]);
info.color = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note);
this._infos.set(steps, info);
} else {
const info = this._infos.get(steps)!;
info = this._infos.get(steps)!;
info.symbols.push(symbol);
}
if (isLeftHand) {
info.leftHandNotes.push(note);
} else {
info.otherSymbolCount++;
}
}

public override paint(cx: number, cy: number, canvas: ICanvas): void {
const glyphs = this.glyphs;
if (!glyphs) {
return;
}
const x = cx + this.x;
const y = cy + this.y;
for (let i = 0; i < glyphs.length; i++) {
if (!this._isReFingered(this._glyphInfos[i])) {
glyphs[i].paint(x, y, canvas);
}
}
}

// resolved on paint: the tab of the bar is laid out after this staff
private _isReFingered(info: FingeringInfo): boolean {
if (info.otherSymbolCount > 0 || info.leftHandNotes.length === 0) {
return false;
}
for (const note of info.leftHandNotes) {
const tabGlyph = TabBarRenderer.findTabNoteGlyph(this.renderer, note);
if (tabGlyph === null || !(tabGlyph.isMoved || tabGlyph.displayFret === 0)) {
return false;
}
}
return true;
}

public override doLayout(): void {
Expand All @@ -178,6 +220,7 @@ export class FingeringGroupGlyph extends GlyphGroup {
g.doLayout();
g.offsetY = g.height / 2;
this.addGlyph(g);
this._glyphInfos.push(info);
this.width = Math.max(this.width, g.width);
}

Expand Down
Loading
Loading