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
4,422 changes: 2,707 additions & 1,715 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/alphatab/src/AlphaTabApiBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2450,7 +2450,8 @@ export class AlphaTabApiBase<TSettings> {
this._isInitialBeatCursorUpdate ||
barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
startBeatX < previousBeatBounds.onNotesX ||
barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 ||
barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h;

if (jumpCursor) {
cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
Expand Down
13 changes: 10 additions & 3 deletions packages/alphatab/src/DisplaySettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,16 @@ export class DisplaySettings {
*
* The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
*
* Also note that the sizing is including any glyphs and notation elements within the bar. e.g. if there are clefs in the bar, they are still "squeezed" into the available size.
* It is not the case that the actual notes with their lengths are sized accordingly. This fits the sizing system of Guitar Pro and when files are customized there,
* alphaTab will match this layout quite close.
* In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
* natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
* Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
* are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
*
* * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
* `displayScale` on the model is ignored.
* * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
* `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
* author hasn't customized it).
*
* ### Horizontal Layout
*
Expand Down
13 changes: 10 additions & 3 deletions packages/alphatab/src/generated/DisplaySettingsJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,16 @@ export interface DisplaySettingsJson {
*
* The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
*
* Also note that the sizing is including any glyphs and notation elements within the bar. e.g. if there are clefs in the bar, they are still "squeezed" into the available size.
* It is not the case that the actual notes with their lengths are sized accordingly. This fits the sizing system of Guitar Pro and when files are customized there,
* alphaTab will match this layout quite close.
* In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
* natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
* Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
* are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
*
* * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
* `displayScale` on the model is ignored.
* * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
* `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
* author hasn't customized it).
*
* ### Horizontal Layout
*
Expand Down
3 changes: 0 additions & 3 deletions packages/alphatab/src/midi/BeatTickLookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ export class BeatTickLookup {
* @param beat The beat to add.
*/
public highlightBeat(beat: Beat, playbackStart: number): void {
if (beat.isEmpty && !beat.voice.isEmpty) {
return;
}
if (!this._highlightedBeats.has(beat.id)) {
this._highlightedBeats.set(beat.id, true);
this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart));
Expand Down
8 changes: 7 additions & 1 deletion packages/alphatab/src/midi/MidiFileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,13 @@ export class MidiFileGenerator {
let audioDuration: number = beat.playbackDuration;
const masterBarDuration = beat.voice.bar.masterBar.calculateDuration();

if (beat.voice.bar.isEmpty) {
// For a bar whose voice contains a single empty beat (the typical "whole-bar rest"
// placeholder inserted during score.finish), extend the beat's audio duration to cover
// the full bar so cursor navigation has a beat to follow across the whole bar. Don't
// apply this when the voice has multiple beats: those represent explicit rhythmic
// subdivisions even when each beat is empty (e.g. a recording grid of placeholder
// slots), and overriding would make every beat overlap the whole bar.
if (beat.voice.bar.isEmpty && beat.voice.beats.length === 1) {
audioDuration = masterBarDuration;
} else if (
beat.voice.bar.masterBar.tripletFeel !== TripletFeel.NoTripletFeel &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ declare let AudioWorkletProcessor: {
* @internal
*/
interface AudioWorkletNode<T> extends AudioNode {
readonly port: AudioWorkletProcessorMessagePort<IAlphaSynthWorkerMessage>;
readonly port: AudioWorkletProcessorMessagePort<T>;
}

// Bug 646: Safari 14.1 is buggy regarding audio worklets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { BackingTrackPlayer } from '@coderline/alphatab/synth/BackingTrackPlayer
import { CoreSettings, FontFileFormat } from '@coderline/alphatab/CoreSettings';
import type { IAudioExporterWorker } from '@coderline/alphatab/synth/IAudioExporter';
import { AlphaSynthAudioExporterWorkerApi } from '@coderline/alphatab/platform/worker/AlphaSynthAudioExporterWorkerApi';
import { IAlphaTabRenderingWorker, IAlphaSynthWorker } from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol';
import type { IAlphaTabRenderingWorker, IAlphaSynthWorker } from '@coderline/alphatab/platform/worker/AlphaTabWorkerProtocol';
import { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/alphatab/src/platform/worker/AlphaTabWebWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ export class AlphaTabWebWorker {
break;
case 'alphaTab.renderScore':
this._updateFontSizes(data.fontSizes);
const renderHints = data.renderHints;
const score =
data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings);
this._renderMultiple(score, data.trackIndexes);
this._renderMultiple(score, data.trackIndexes, renderHints);
break;
case 'alphaTab.updateSettings':
this._updateSettings(data.settings);
Expand Down
10 changes: 10 additions & 0 deletions packages/alphatab/src/rendering/BarRendererBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ export class BarRendererBase {
return false;
}

/**
* The fixed-overhead width of this renderer: glyphs that do not stretch when
* the bar is scaled (clef, key signature, time signature, barlines, courtesy
* accidentals, etc). Treated as a fixed allocation by the system-level layout
* before distributing remaining width across bars by {@link Bar.displayScale}.
*/
public get fixedOverhead(): number {
return this._preBeatGlyphs.width + this._postBeatGlyphs.width;
}

public scaleToWidth(width: number): void {
// preBeat and postBeat glyphs do not get resized
const containerWidth: number = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width;
Expand Down
8 changes: 8 additions & 0 deletions packages/alphatab/src/rendering/IScoreRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export interface RenderHints {
* internally it might still be decided to clear the viewport.
*/
reuseViewport?: boolean;

/**
* Indicates the index of the first masterbar which was modified in the data model.
* @remarks
* AlphaTab will try to optimize the rendering and other updates to keep unchanged parts.
* At this point only the rendering is affected and the generated MIDI has to be updated separately.
*/
firstChangedMasterBar?: number;
}

/**
Expand Down
50 changes: 46 additions & 4 deletions packages/alphatab/src/rendering/LineBarRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,27 @@ export abstract class LineBarRenderer extends BarRendererBase {
const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat)!;
const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat)!;
const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper);
let startY: number = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
let endY: number = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
let startY: number;
let endY: number;
if (isRestOnly) {
startY = Math.max(startY, endY);
// rests have no stems, so anchor to the actual rest glyph bounds
// instead of a stem-adjusted flag position (which would place the bracket
// a full quarter-stem length away from the rests).
if (direction === BeamDirection.Up) {
startY = Math.min(
this.getRestY(firstNonRestBeat, NoteYPosition.Top),
this.getRestY(lastNonRestBeat, NoteYPosition.Top)
);
} else {
startY = Math.max(
this.getRestY(firstNonRestBeat, NoteYPosition.Bottom),
this.getRestY(lastNonRestBeat, NoteYPosition.Bottom)
);
}
endY = startY;
} else {
startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
}

// align line centered in available space
Expand Down Expand Up @@ -874,7 +890,33 @@ export abstract class LineBarRenderer extends BarRendererBase {
for (const v of this.helpers.beamHelpers) {
for (const h of v) {
if (!this.shouldPaintBeamingHelper(h)) {
// no visible helper
// beam is not drawn, but a rest-only tuplet still draws a bracket
// anchored to the rest glyph bounds and needs overflow reserved.
if (h.hasTuplet && h.isRestBeamHelper) {
const tupletGroup = h.beats[0].tupletGroup!;
const tupletFirst = tupletGroup.beats[0];
const tupletLast = tupletGroup.beats[tupletGroup.beats.length - 1];
const tupletDirection = this.getTupletBeamDirection(h);
if (tupletDirection === BeamDirection.Up) {
const restTop = Math.min(
this.getRestY(tupletFirst, NoteYPosition.Top),
this.getRestY(tupletLast, NoteYPosition.Top)
);
const topY = restTop - this.tupletSize - this.tupletOffset;
if (topY < maxNoteY) {
maxNoteY = topY;
}
} else {
const restBottom = Math.max(
this.getRestY(tupletFirst, NoteYPosition.Bottom),
this.getRestY(tupletLast, NoteYPosition.Bottom)
);
const bottomY = restBottom + this.tupletSize + this.tupletOffset;
if (bottomY > minNoteY) {
minNoteY = bottomY;
}
}
}
}
// notes with stems (and potential flags)
else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
Expand Down
9 changes: 8 additions & 1 deletion packages/alphatab/src/rendering/ScoreRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,14 @@ export class ScoreRenderer implements IScoreRenderer {
Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null);
return;
}
this.boundsLookup = new BoundsLookup();
// For partial renders we preserve the existing lookup so bars outside the re-layouted
// range keep their already-scaled bounds - the layout will clear the changed range
// before the paint pass re-registers fresh entries for it.
if (renderHints?.firstChangedMasterBar !== undefined && this.boundsLookup) {
this.boundsLookup.resetForPartialUpdate();
} else {
this.boundsLookup = new BoundsLookup();
}
this._recreateCanvas();
this.canvas!.lineWidth = 1;
this.canvas!.settings = this.settings;
Expand Down
15 changes: 14 additions & 1 deletion packages/alphatab/src/rendering/layout/HorizontalScreenLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export class HorizontalScreenLayout extends ScoreLayout {
public doResize(): void {
// not supported
}

public override doUpdateForBars(_renderHints: RenderHints): boolean {
// not supported yet, modifications likely cause anyhow full updates
// as we do not optimize effect bands yet. with effect bands being more
// isolated in bars we could try updating dynamically
return false;
}


protected doLayoutAndRender(renderHints: RenderHints | undefined): void {
const score: Score = this.renderer.score!;
Expand All @@ -61,6 +69,11 @@ export class HorizontalScreenLayout extends ScoreLayout {

endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
this._system = this.createEmptyStaffSystem(0);
// Each bar in horizontal layout is sized independently (by bar.displayWidth or the bar's
// intrinsic width), so there is no shared staff width to distribute across bars. Keep each
// bar's spring constants referenced against its own local minimum-duration so rendering
// matches the historical per-bar behaviour.
this._system.shareMinDurationAcrossBars = false;
this._system.isLast = true;
this._system.x = this.pagePadding![0];
this._system.y = this.pagePadding![1];
Expand Down Expand Up @@ -150,7 +163,7 @@ export class HorizontalScreenLayout extends ScoreLayout {
}

this.height = this.layoutAndRenderBottomScoreInfo(this.height);
this.height = this.layoutAndRenderAnnotation(this.height);
this.height = this._layoutAndRenderAnnotation(this.height);

this.height += this.pagePadding![3];

Expand Down
43 changes: 29 additions & 14 deletions packages/alphatab/src/rendering/layout/ScoreLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,11 @@ import { Lazy } from '@coderline/alphatab/util/Lazy';

/**
* @internal
* @record
*/
class LazyPartial {
public args: RenderFinishedEventArgs;
public renderCallback: (canvas: ICanvas) => void;
public constructor(args: RenderFinishedEventArgs, renderCallback: (canvas: ICanvas) => void) {
this.args = args;
this.renderCallback = renderCallback;
}
interface LazyPartial {
args: RenderFinishedEventArgs;
renderCallback: (canvas: ICanvas) => void;
}

/**
Expand Down Expand Up @@ -84,13 +81,10 @@ export abstract class ScoreLayout {
}
public abstract doResize(): void;

public abstract doUpdateForBars(renderHints: RenderHints): boolean;

public layoutAndRender(renderHints?: RenderHints): void {
this._lazyPartials.clear();
this.slurRegistry.clear();
this.beamingRuleLookups.clear();
this._barRendererLookup.clear();

this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!;

const score: Score = this.renderer.score!;

Expand All @@ -102,6 +96,19 @@ export abstract class ScoreLayout {
this.lastBarIndex
);

const firstChangedMasterBar = renderHints?.firstChangedMasterBar;
if (firstChangedMasterBar !== undefined) {
if (this.doUpdateForBars(renderHints!)) {
return;
}
}

this._lazyPartials.clear();
this.beamingRuleLookups.clear();
this._barRendererLookup.clear();

this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile)!;

this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale);
if (!this.pagePadding) {
this.pagePadding = [0, 0, 0, 0];
Expand All @@ -118,6 +125,10 @@ export abstract class ScoreLayout {

private _lazyPartials: Map<string, LazyPartial> = new Map<string, LazyPartial>();

protected getExistingPartialArgs(id:string): RenderFinishedEventArgs|undefined {
return this._lazyPartials.has(id) ? this._lazyPartials.get(id)!.args : undefined;
}

protected registerPartial(args: RenderFinishedEventArgs, callback: (canvas: ICanvas) => void) {
if (args.height === 0) {
return;
Expand All @@ -137,7 +148,11 @@ export abstract class ScoreLayout {
this._internalRenderLazyPartial(args, callback);
} else {
// in case of lazy loading -> first register lazy, then notify
this._lazyPartials.set(args.id, new LazyPartial(args, callback));
const partial: LazyPartial = {
args,
renderCallback: callback
};
this._lazyPartials.set(args.id, partial);
(this.renderer.partialLayoutFinished as EventEmitterOfT<RenderFinishedEventArgs>).trigger(args);
}
}
Expand Down Expand Up @@ -500,7 +515,7 @@ export abstract class ScoreLayout {
}
}

public layoutAndRenderAnnotation(y: number): number {
protected _layoutAndRenderAnnotation(y: number): number {
// attention, you are not allowed to remove change this notice within any version of this library without permission!
const msg: string = 'rendered by alphaTab';
const resources: RenderingResources = this.renderer.settings.display.resources;
Expand Down
Loading