diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 25d7793b6..cea405415 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -731,6 +731,7 @@ export class AlphaTabApiBase { private _previousTick: number = 0; private _playerState: PlayerState = PlayerState.Paused; private _currentBeat: MidiTickLookupFindBeatResult | null = null; + private _currentBarBounds: MasterBarBounds | null = null; private _previousStateForCursor: PlayerState = PlayerState.Paused; private _previousCursorCache: BoundsLookup | null = null; private _lastScroll: number = 0; @@ -766,7 +767,7 @@ export class AlphaTabApiBase { this._playerState = PlayerState.Paused; // we need to update our position caches if we render a tablature this.renderer.postRenderFinished.on(() => { - this.cursorUpdateTick(this._previousTick, false); + this.cursorUpdateTick(this._previousTick, false, this._previousTick > 10); }); if (this.player) { this.player.positionChanged.on(e => { @@ -794,15 +795,16 @@ export class AlphaTabApiBase { * updates the cursors to highlight the beat at the specified tick position * @param tick * @param stop + * @param shouldScroll whether we should scroll to the bar (if scrolling is active) */ - private cursorUpdateTick(tick: number, stop: boolean): void { + private cursorUpdateTick(tick: number, stop: boolean, shouldScroll: boolean = false): void { let cache: MidiTickLookup | null = this._tickCache; if (cache) { let tracks: Track[] = this.tracks; if (tracks.length > 0) { let beat: MidiTickLookupFindBeatResult | null = cache.findBeat(tracks, tick, this._currentBeat); if (beat) { - this.cursorUpdateBeat(beat, stop); + this.cursorUpdateBeat(beat, stop, shouldScroll); } } } @@ -811,7 +813,7 @@ export class AlphaTabApiBase { /** * updates the cursors to highlight the specified beat */ - private cursorUpdateBeat(lookupResult: MidiTickLookupFindBeatResult, stop: boolean): void { + private cursorUpdateBeat(lookupResult: MidiTickLookupFindBeatResult, stop: boolean, shouldScroll: boolean): void { const beat: Beat = lookupResult.currentBeat; const nextBeat: Beat | null = lookupResult.nextBeat; const duration: number = lookupResult.duration; @@ -827,9 +829,6 @@ export class AlphaTabApiBase { let previousBeat = this._currentBeat; let previousCache: BoundsLookup | null = this._previousCursorCache; let previousState: PlayerState | null = this._previousStateForCursor; - this._currentBeat = lookupResult; - this._previousCursorCache = cache; - this._previousStateForCursor = this._playerState; if (beat === previousBeat?.currentBeat && cache === previousCache && previousState === this._playerState) { return; } @@ -838,11 +837,99 @@ export class AlphaTabApiBase { return; } + // only if we really found some bounds we remember the beat and cache we used to + // actually show the cursor + this._currentBeat = lookupResult; + this._previousCursorCache = cache; + this._previousStateForCursor = this._playerState; + this.uiFacade.beginInvoke(() => { - this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache!, beatBoundings!); + this.internalCursorUpdateBeat( + beat, + nextBeat, + duration, + stop, + beatsToHighlight, + cache!, + beatBoundings!, + shouldScroll + ); }); } + /** + * Initiates a scroll to the cursor + */ + public scrollToCursor() { + const barBounds = this._currentBarBounds; + if (barBounds) { + this.internalScrollToCursor(barBounds); + } + } + + public internalScrollToCursor(barBoundings: MasterBarBounds) { + let scrollElement: IContainer = this.uiFacade.getScrollContainer(); + let isVertical: boolean = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical; + let mode: ScrollMode = this.settings.player.scrollMode; + if (isVertical) { + // when scrolling on the y-axis, we preliminary check if the new beat/bar have + // moved on the y-axis + let y: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; + if (y !== this._lastScroll) { + this._lastScroll = y; + switch (mode) { + case ScrollMode.Continuous: + let elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); + this.uiFacade.scrollToY(scrollElement, elementOffset.y + y, this.settings.player.scrollSpeed); + break; + case ScrollMode.OffScreen: + let elementBottom: number = + scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h; + if ( + barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || + barBoundings.visualBounds.y < scrollElement.scrollTop + ) { + let scrollTop: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; + this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed); + } + break; + } + } + } else { + // when scrolling on the x-axis, we preliminary check if the new bar has + // moved on the x-axis + let x: number = barBoundings.visualBounds.x; + if (x !== this._lastScroll) { + this._lastScroll = x; + switch (mode) { + case ScrollMode.Continuous: + let scrollLeftContinuous: number = + barBoundings.realBounds.x + this.settings.player.scrollOffsetX; + this._lastScroll = barBoundings.visualBounds.x; + this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed); + break; + case ScrollMode.OffScreen: + let elementRight: number = + scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w; + if ( + barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || + barBoundings.visualBounds.x < scrollElement.scrollLeft + ) { + let scrollLeftOffScreen: number = + barBoundings.realBounds.x + this.settings.player.scrollOffsetX; + this._lastScroll = barBoundings.visualBounds.x; + this.uiFacade.scrollToX( + scrollElement, + scrollLeftOffScreen, + this.settings.player.scrollSpeed + ); + } + break; + } + } + } + } + private internalCursorUpdateBeat( beat: Beat, nextBeat: Beat | null, @@ -850,7 +937,8 @@ export class AlphaTabApiBase { stop: boolean, beatsToHighlight: Beat[] | null, cache: BoundsLookup, - beatBoundings: BeatBounds + beatBoundings: BeatBounds, + shouldScroll: boolean ) { let barCursor: IContainer = this._barCursor!; let beatCursor: IContainer = this._beatCursor!; @@ -858,6 +946,7 @@ export class AlphaTabApiBase { let barBoundings: MasterBarBounds = beatBoundings.barBounds.masterBarBounds; let barBounds: Bounds = barBoundings.visualBounds; + this._currentBarBounds = barBoundings; barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h); // move beat to start position immediately @@ -870,116 +959,54 @@ export class AlphaTabApiBase { if (this.settings.player.enableElementHighlighting) { this.uiFacade.removeHighlights(); } - if (this._playerState === PlayerState.Playing || stop) { - duration /= this.playbackSpeed; - if (!stop) { - if (this.settings.player.enableElementHighlighting && beatsToHighlight) { - for (let highlight of beatsToHighlight) { - let className: string = BeatContainerGlyph.getGroupId(highlight); - this.uiFacade.highlightElements(className, beat.voice.bar.index); - } + // actively playing? -> animate cursor and highlight items + let shouldNotifyBeatChange = false; + if (this._playerState === PlayerState.Playing && !stop) { + if (this.settings.player.enableElementHighlighting && beatsToHighlight) { + for (let highlight of beatsToHighlight) { + let className: string = BeatContainerGlyph.getGroupId(highlight); + this.uiFacade.highlightElements(className, beat.voice.bar.index); } + } - if (this.settings.player.enableAnimatedBeatCursor) { - let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; - // get position of next beat on same stavegroup - if (nextBeat) { - // if we are moving within the same bar or to the next bar - // transition to the next beat, otherwise transition to the end of the bar. + if (this.settings.player.enableAnimatedBeatCursor) { + let nextBeatX: number = barBoundings.visualBounds.x + barBoundings.visualBounds.w; + // get position of next beat on same stavegroup + if (nextBeat) { + // if we are moving within the same bar or to the next bar + // transition to the next beat, otherwise transition to the end of the bar. + if ( + (nextBeat.voice.bar.index === beat.voice.bar.index && nextBeat.index > beat.index) || + nextBeat.voice.bar.index === beat.voice.bar.index + 1 + ) { + let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat); if ( - (nextBeat.voice.bar.index === beat.voice.bar.index && nextBeat.index > beat.index) || - nextBeat.voice.bar.index === beat.voice.bar.index + 1 + nextBeatBoundings && + nextBeatBoundings.barBounds.masterBarBounds.staveGroupBounds === + barBoundings.staveGroupBounds ) { - let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat); - if ( - nextBeatBoundings && - nextBeatBoundings.barBounds.masterBarBounds.staveGroupBounds === - barBoundings.staveGroupBounds - ) { - nextBeatX = nextBeatBoundings.visualBounds.x; - } - } - } - - // we need to put the transition to an own animation frame - // otherwise the stop animation above is not applied. - this.uiFacade.beginInvoke(() => { - beatCursor!.transitionToX(duration, nextBeatX); - }); - } - } - if (!this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) { - let scrollElement: IContainer = this.uiFacade.getScrollContainer(); - let isVertical: boolean = Environment.getLayoutEngineFactory(this.settings.display.layoutMode).vertical; - let mode: ScrollMode = this.settings.player.scrollMode; - if (isVertical) { - // when scrolling on the y-axis, we preliminary check if the new beat/bar have - // moved on the y-axis - let y: number = barBoundings.realBounds.y + this.settings.player.scrollOffsetY; - if (y !== this._lastScroll) { - this._lastScroll = y; - switch (mode) { - case ScrollMode.Continuous: - let elementOffset: Bounds = this.uiFacade.getOffset(scrollElement, this.container); - this.uiFacade.scrollToY( - scrollElement, - elementOffset.y + y, - this.settings.player.scrollSpeed - ); - break; - case ScrollMode.OffScreen: - let elementBottom: number = - scrollElement.scrollTop + this.uiFacade.getOffset(null, scrollElement).h; - if ( - barBoundings.visualBounds.y + barBoundings.visualBounds.h >= elementBottom || - barBoundings.visualBounds.y < scrollElement.scrollTop - ) { - let scrollTop: number = - barBoundings.realBounds.y + this.settings.player.scrollOffsetY; - this.uiFacade.scrollToY(scrollElement, scrollTop, this.settings.player.scrollSpeed); - } - break; - } - } - } else { - // when scrolling on the x-axis, we preliminary check if the new bar has - // moved on the x-axis - let x: number = barBoundings.visualBounds.x; - if (x !== this._lastScroll) { - this._lastScroll = x; - switch (mode) { - case ScrollMode.Continuous: - let scrollLeftContinuous: number = - barBoundings.realBounds.x + this.settings.player.scrollOffsetX; - this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX( - scrollElement, - scrollLeftContinuous, - this.settings.player.scrollSpeed - ); - break; - case ScrollMode.OffScreen: - let elementRight: number = - scrollElement.scrollLeft + this.uiFacade.getOffset(null, scrollElement).w; - if ( - barBoundings.visualBounds.x + barBoundings.visualBounds.w >= elementRight || - barBoundings.visualBounds.x < scrollElement.scrollLeft - ) { - let scrollLeftOffScreen: number = - barBoundings.realBounds.x + this.settings.player.scrollOffsetX; - this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX( - scrollElement, - scrollLeftOffScreen, - this.settings.player.scrollSpeed - ); - } - break; + nextBeatX = nextBeatBoundings.visualBounds.x; } } } + + // we need to put the transition to an own animation frame + // otherwise the stop animation above is not applied. + this.uiFacade.beginInvoke(() => { + beatCursor!.transitionToX(duration / this.playbackSpeed, nextBeatX); + }); } - // trigger an event for others to indicate which beat/bar is played + + shouldScroll = !stop; + shouldNotifyBeatChange = true; + } + + if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) { + this.internalScrollToCursor(barBoundings); + } + + // trigger an event for others to indicate which beat/bar is played + if (shouldNotifyBeatChange) { this.onPlayedBeatChanged(beat); } } @@ -1362,8 +1389,10 @@ export class AlphaTabApiBase { if (this._isDestroyed) { return; } - (this.playerPositionChanged as EventEmitterOfT).trigger(e); - this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e); + if (this.score !== null && this.tracks.length > 0) { + (this.playerPositionChanged as EventEmitterOfT).trigger(e); + this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e); + } } public midiEventsPlayed: IEventEmitterOfT = diff --git a/src/model/Score.ts b/src/model/Score.ts index 6cd580e2a..4b727b67b 100644 --- a/src/model/Score.ts +++ b/src/model/Score.ts @@ -110,7 +110,12 @@ export class Score { if (this.masterBars.length !== 0) { bar.previousMasterBar = this.masterBars[this.masterBars.length - 1]; bar.previousMasterBar.nextMasterBar = bar; - bar.start = bar.previousMasterBar.start + bar.previousMasterBar.calculateDuration(); + // TODO: this will not work on anacrusis. Correct anacrusis durations are only working + // when there are beats with playback positions already computed which requires full finish + // chicken-egg problem here. temporarily forcing anacrusis length here to 0 + bar.start = + bar.previousMasterBar.start + + (bar.previousMasterBar.isAnacrusis ? 0 : bar.previousMasterBar.calculateDuration()); } // if the group is closed only the next upcoming header can // reopen the group in case of a repeat alternative, so we @@ -129,7 +134,7 @@ export class Score { } public finish(settings: Settings): void { - const sharedDataBag = new Map() + const sharedDataBag = new Map(); for (let i: number = 0, j: number = this.tracks.length; i < j; i++) { this.tracks[i].finish(settings, sharedDataBag); } diff --git a/src/rendering/ScoreRenderer.ts b/src/rendering/ScoreRenderer.ts index 4662fd92a..d477efb46 100644 --- a/src/rendering/ScoreRenderer.ts +++ b/src/rendering/ScoreRenderer.ts @@ -23,6 +23,9 @@ export class ScoreRenderer implements IScoreRenderer { public canvas: ICanvas | null = null; public score: Score | null = null; public tracks: Track[] | null = null; + /** + * @internal + */ public layout: ScoreLayout | null = null; public settings: Settings; public boundsLookup: BoundsLookup | null = null; diff --git a/src/rendering/layout/HorizontalScreenLayout.ts b/src/rendering/layout/HorizontalScreenLayout.ts index 924d5f3d5..51354d459 100644 --- a/src/rendering/layout/HorizontalScreenLayout.ts +++ b/src/rendering/layout/HorizontalScreenLayout.ts @@ -8,6 +8,7 @@ import { StaveGroup } from '@src/rendering/staves/StaveGroup'; import { Logger } from '@src/Logger'; export class HorizontalScreenLayoutPartialInfo { + public x: number = 0; public width: number = 0; public masterBars: MasterBar[] = []; } @@ -33,9 +34,9 @@ export class HorizontalScreenLayout extends ScoreLayout { return false; } - public get firstBarX(): number{ - let x= this._pagePadding![0]; - if(this._group) { + public get firstBarX(): number { + let x = this._pagePadding![0]; + if (this._group) { x += this._group.accoladeSpacing; } return x; @@ -85,6 +86,7 @@ export class HorizontalScreenLayout extends ScoreLayout { let countPerPartial: number = this.renderer.settings.display.barCountPerPartial; let partials: HorizontalScreenLayoutPartialInfo[] = []; let currentPartial: HorizontalScreenLayoutPartialInfo = new HorizontalScreenLayoutPartialInfo(); + let renderX = 0; while (currentBarIndex <= endBarIndex) { let result = this._group.addBars(this.renderer.tracks!, currentBarIndex); if (result) { @@ -94,14 +96,18 @@ export class HorizontalScreenLayout extends ScoreLayout { let previousPartial: HorizontalScreenLayoutPartialInfo = partials[partials.length - 1]; previousPartial.masterBars.push(score.masterBars[currentBarIndex]); previousPartial.width += result.width; + renderX += result.width; + currentPartial.x += renderX; } else { currentPartial.masterBars.push(score.masterBars[currentBarIndex]); currentPartial.width += result.width; // no targetPartial here because previous partials already handled this code if (currentPartial.masterBars.length >= countPerPartial) { if (partials.length === 0) { - currentPartial.width += this._group.x + this._group.accoladeSpacing; + // respect accolade and on first partial + currentPartial.width += this._group.accoladeSpacing + this._pagePadding[0]; } + renderX += currentPartial.width; partials.push(currentPartial); Logger.debug( this.name, @@ -112,6 +118,7 @@ export class HorizontalScreenLayout extends ScoreLayout { null ); currentPartial = new HorizontalScreenLayoutPartialInfo(); + currentPartial.x = renderX; } } } @@ -120,7 +127,7 @@ export class HorizontalScreenLayout extends ScoreLayout { // don't miss the last partial if not empty if (currentPartial.masterBars.length > 0) { if (partials.length === 0) { - currentPartial.width += this._group.x + this._group.accoladeSpacing; + currentPartial.width += this._group.accoladeSpacing + this._pagePadding[0]; } partials.push(currentPartial); Logger.debug( @@ -139,7 +146,7 @@ export class HorizontalScreenLayout extends ScoreLayout { let x = 0; for (let i: number = 0; i < partials.length; i++) { - let partial: HorizontalScreenLayoutPartialInfo = partials[i]; + const partial: HorizontalScreenLayoutPartialInfo = partials[i]; const e = new RenderFinishedEventArgs(); e.x = x; @@ -153,14 +160,20 @@ export class HorizontalScreenLayout extends ScoreLayout { x += partial.width; - const partialBarIndex = currentBarIndex; + // pull to local scope for lambda + const partialBarIndex = currentBarIndex; + const partialIndex = i; + this._group.buildBoundingsLookup(this._group!.x, this._group!.y); this.registerPartial(e, canvas => { canvas.color = this.renderer.settings.display.resources.mainGlyphColor; canvas.textAlign = TextAlign.Left; let renderX: number = this._group!.getBarX(partial.masterBars[0].index) + this._group!.accoladeSpacing; - if (i === 0) { + if (partialIndex === 0) { renderX -= this._group!.x + this._group!.accoladeSpacing; } + + canvas.color = this.renderer.settings.display.resources.mainGlyphColor; + canvas.textAlign = TextAlign.Left; Logger.debug( this.name, 'Rendering partial from bar ' + diff --git a/src/rendering/layout/PageViewLayout.ts b/src/rendering/layout/PageViewLayout.ts index 1acdf322c..c5ff89bde 100644 --- a/src/rendering/layout/PageViewLayout.ts +++ b/src/rendering/layout/PageViewLayout.ts @@ -325,6 +325,7 @@ export class PageViewLayout extends ScoreLayout { args.firstMasterBarIndex = group.firstBarIndex; args.lastMasterBarIndex = group.lastBarIndex; + group.buildBoundingsLookup(0, 0); this.registerPartial(args, canvas => { this.renderer.canvas!.color = this.renderer.settings.display.resources.mainGlyphColor; this.renderer.canvas!.textAlign = TextAlign.Left; diff --git a/src/rendering/layout/ScoreLayout.ts b/src/rendering/layout/ScoreLayout.ts index 982b84dcb..37aa68ff7 100644 --- a/src/rendering/layout/ScoreLayout.ts +++ b/src/rendering/layout/ScoreLayout.ts @@ -31,7 +31,7 @@ class LazyPartial { } /** - * This is the base public class for creating new layouting engines for the score renderer. + * This is the base class for creating new layouting engines for the score renderer. */ export abstract class ScoreLayout { private _barRendererLookup: Map> = new Map(); @@ -296,7 +296,7 @@ export abstract class ScoreLayout { : this.firstBarX; e.y = y; e.totalWidth = this.width; - e.totalHeight = this.height; + e.totalHeight = y + height; e.firstMasterBarIndex = -1; e.lastMasterBarIndex = -1; diff --git a/src/rendering/staves/StaveGroup.ts b/src/rendering/staves/StaveGroup.ts index 04e0621c9..ca40f45db 100644 --- a/src/rendering/staves/StaveGroup.ts +++ b/src/rendering/staves/StaveGroup.ts @@ -215,7 +215,6 @@ export class StaveGroup { } public paintPartial(cx: number, cy: number, canvas: ICanvas, startIndex: number, count: number): void { - this.buildBoundingsLookup(cx, cy); for (let i: number = 0, j: number = this._allStaves.length; i < j; i++) { this._allStaves[i].paint(cx, cy, canvas, startIndex, count); } @@ -345,7 +344,7 @@ export class StaveGroup { } } - private buildBoundingsLookup(cx: number, cy: number): void { + public buildBoundingsLookup(cx: number, cy: number): void { if (this.layout.renderer.boundsLookup!.isFinished) { return; } diff --git a/src/synth/MidiFileSequencer.ts b/src/synth/MidiFileSequencer.ts index d90bedb5c..c70d91fb0 100644 --- a/src/synth/MidiFileSequencer.ts +++ b/src/synth/MidiFileSequencer.ts @@ -8,6 +8,7 @@ import { SynthEvent } from '@src/synth/synthesis/SynthEvent'; import { TinySoundFont } from '@src/synth/synthesis/TinySoundFont'; import { Logger } from '@src/Logger'; import { SynthConstants } from '@src/synth/SynthConstants'; +import { MidiUtils } from '@src/midi/MidiUtils'; export class MidiFileSequencerTempoChange { public bpm: number; @@ -27,7 +28,7 @@ class MidiSequencerState { public firstTimeSignatureNumerator: number = 0; public firstTimeSignatureDenominator: number = 0; public synthData: SynthEvent[] = []; - public division: number = 0; + public division: number = MidiUtils.QuarterTime; public eventIndex: number = 0; public currentTime: number = 0; public playbackRange: PlaybackRange | null = null; diff --git a/src/synth/soundfont/Hydra.ts b/src/synth/soundfont/Hydra.ts index 4ac689686..b89efdecc 100644 --- a/src/synth/soundfont/Hydra.ts +++ b/src/synth/soundfont/Hydra.ts @@ -146,13 +146,12 @@ export class Hydra { let samplesPos: number = 0; const sampleBuffer: Uint8Array = new Uint8Array(2048); - const testBuffer: Int16Array = new Int16Array((sampleBuffer.length / 2) | 0); while (samplesLeft > 0) { let samplesToRead: number = Math.min(samplesLeft, (sampleBuffer.length / 2) | 0); reader.read(sampleBuffer, 0, samplesToRead * 2); for (let i: number = 0; i < samplesToRead; i++) { - testBuffer[i] = (sampleBuffer[i * 2 + 1] << 8) | sampleBuffer[i * 2]; - samples[samplesPos + i] = testBuffer[i] / 32767; + const shortSample = TypeConversions.int32ToInt16((sampleBuffer[i * 2 + 1] << 8) | sampleBuffer[i * 2]); + samples[samplesPos + i] = shortSample / 32767; } samplesLeft -= samplesToRead; samplesPos += samplesToRead;