diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index a7c8f27f7..215265ccf 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -725,7 +725,7 @@ export class AlphaTabApiBase { private _selectionWrapper: IContainer | null = null; private _previousTick: number = 0; private _playerState: PlayerState = PlayerState.Paused; - private _currentBeat: Beat | null = null; + private _currentBeat: MidiTickLookupFindBeatResult | null = null; private _previousStateForCursor: PlayerState = PlayerState.Paused; private _previousCursorCache: BoundsLookup | null = null; private _lastScroll: number = 0; @@ -761,23 +761,24 @@ 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, true); }); if (this.player) { this.player.positionChanged.on(e => { this._previousTick = e.currentTick; this.uiFacade.beginInvoke(() => { - this.cursorUpdateTick(e.currentTick, false); + this.cursorUpdateTick(e.currentTick, false, e.isSeek); }); }); this.player.stateChanged.on(e => { this._playerState = e.state; if (!e.stopped && e.state === PlayerState.Paused) { - let currentBeat: Beat | null = this._currentBeat; - let tickCache: MidiTickLookup | null = this._tickCache; + let currentBeat = this._currentBeat; + let tickCache = this._tickCache; if (currentBeat && tickCache) { this.player!.tickPosition = - tickCache.getMasterBarStart(currentBeat.voice.bar.masterBar) + currentBeat.playbackStart; + tickCache.getMasterBarStart(currentBeat.currentBeat.voice.bar.masterBar) + + currentBeat.currentBeat.playbackStart; } } }); @@ -789,37 +790,32 @@ export class AlphaTabApiBase { * @param tick * @param stop */ - private cursorUpdateTick(tick: number, stop: boolean = false): void { - this.uiFacade.beginInvoke(() => { - 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); - if (beat) { - this.cursorUpdateBeat( - beat.currentBeat, - beat.nextBeat, - beat.duration, - stop, - beat.beatsToHighlight - ); - } + private cursorUpdateTick(tick: number, stop: boolean, forceImmediateUpdate: boolean): 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, forceImmediateUpdate); } } - }); + } } /** * updates the cursors to highlight the specified beat */ private cursorUpdateBeat( - beat: Beat, - nextBeat: Beat | null, - duration: number, + lookupResult: MidiTickLookupFindBeatResult, stop: boolean, - beatsToHighlight: Beat[] | null = null + forceImmediateUpdate: boolean ): void { + const beat: Beat = lookupResult.currentBeat; + const nextBeat: Beat | null = lookupResult.nextBeat; + const duration: number = lookupResult.duration; + const beatsToHighlight = lookupResult.beatsToHighlight; + if (!beat) { return; } @@ -827,27 +823,59 @@ export class AlphaTabApiBase { if (!cache) { return; } - let previousBeat: Beat | null = this._currentBeat; + let previousBeat = this._currentBeat; let previousCache: BoundsLookup | null = this._previousCursorCache; let previousState: PlayerState | null = this._previousStateForCursor; - this._currentBeat = beat; + this._currentBeat = lookupResult; this._previousCursorCache = cache; this._previousStateForCursor = this._playerState; - if (beat === previousBeat && cache === previousCache && previousState === this._playerState) { + if (beat === previousBeat?.currentBeat && cache === previousCache && previousState === this._playerState) { return; } - let barCursor: IContainer = this._barCursor!; - let beatCursor: IContainer = this._beatCursor!; let beatBoundings: BeatBounds | null = cache.findBeat(beat); if (!beatBoundings) { return; } + this.uiFacade.beginInvoke(() => { + this.internalCursorUpdateBeat( + beat, + nextBeat, + duration, + stop, + beatsToHighlight, + cache!, + beatBoundings!, + forceImmediateUpdate + ); + }); + } + + private internalCursorUpdateBeat( + beat: Beat, + nextBeat: Beat | null, + duration: number, + stop: boolean, + beatsToHighlight: Beat[] | null, + cache: BoundsLookup, + beatBoundings: BeatBounds, + forceImmediateUpdate: boolean + ) { + let barCursor: IContainer = this._barCursor!; + let beatCursor: IContainer = this._beatCursor!; + let barBoundings: MasterBarBounds = beatBoundings.barBounds.masterBarBounds; let barBounds: Bounds = barBoundings.visualBounds; + let needsNewAnimationFrame = false; + barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h); // move beat to start position immediately + const previousBeatBounds: Bounds = beatCursor.getBounds(); + needsNewAnimationFrame = + forceImmediateUpdate || + previousBeatBounds!.y !== barBounds.y || + beatBoundings.visualBounds.x < previousBeatBounds!.x; if (this.settings.player.enableAnimatedBeatCursor) { beatCursor.stopAnimation(); } @@ -874,7 +902,7 @@ export class AlphaTabApiBase { // 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.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); @@ -888,13 +916,17 @@ export class AlphaTabApiBase { } } - if (beatCursor) { + // we need to put the transition to an own animation frame + // otherwise the stop animation above is not applied. + // but only if we changed on the y axis. + // as long we scroll horizontally we can keep the animation + // alive. + if (needsNewAnimationFrame) { this.uiFacade.beginInvoke(() => { - // Logger.Info("Player", - // "Transition from " + beatBoundings.VisualBounds.X + " to " + nextBeatX + " in " + duration + - // "(" + Player.PlaybackRange + ")"); beatCursor!.transitionToX(duration, nextBeatX); }); + } else { + beatCursor.transitionToX(duration, nextBeatX); } } } @@ -939,9 +971,14 @@ export class AlphaTabApiBase { this._lastScroll = x; switch (mode) { case ScrollMode.Continuous: - let scrollLeftContinuous: number = barBoundings.realBounds.x + this.settings.player.scrollOffsetX; + let scrollLeftContinuous: number = + barBoundings.realBounds.x + this.settings.player.scrollOffsetX; this._lastScroll = barBoundings.visualBounds.x; - this.uiFacade.scrollToX(scrollElement, scrollLeftContinuous, this.settings.player.scrollSpeed); + this.uiFacade.scrollToX( + scrollElement, + scrollLeftContinuous, + this.settings.player.scrollSpeed + ); break; case ScrollMode.OffScreen: let elementRight: number = @@ -1044,7 +1081,7 @@ export class AlphaTabApiBase { // move to selection start this._currentBeat = null; // reset current beat so it is updating the cursor if (this._playerState === PlayerState.Paused) { - this.cursorUpdateBeat(this._selectionStart.beat, null, 0, false, [this._selectionStart.beat]); + this.cursorUpdateTick(this._selectionStart.beat.absolutePlaybackStart, false, true); } this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart; // set playback range diff --git a/src/midi/BeatTickLookup.ts b/src/midi/BeatTickLookup.ts index 1662ac98e..acb426780 100644 --- a/src/midi/BeatTickLookup.ts +++ b/src/midi/BeatTickLookup.ts @@ -1,4 +1,5 @@ import { Beat } from '@src/model/Beat'; +import { MasterBarTickLookup } from './MasterBarTickLookup'; /** * Represents the time period, for which a {@link Beat} is played. @@ -6,6 +7,16 @@ import { Beat } from '@src/model/Beat'; export class BeatTickLookup { private _highlightedBeats: Map = new Map(); + /** + * Gets or sets the index of the lookup within the parent MasterBarTickLookup. + */ + public index: number = 0; + + /** + * Gets or sets the parent MasterBarTickLookup to which this beat lookup belongs to. + */ + public masterBar!: MasterBarTickLookup; + /** * Gets or sets the start time in midi ticks at which the given beat is played. */ diff --git a/src/midi/MasterBarTickLookup.ts b/src/midi/MasterBarTickLookup.ts index fadcc0e53..ed5b5e16a 100644 --- a/src/midi/MasterBarTickLookup.ts +++ b/src/midi/MasterBarTickLookup.ts @@ -50,6 +50,8 @@ export class MasterBarTickLookup { * @param beat */ public addBeat(beat: BeatTickLookup): void { + beat.masterBar = this; + beat.index = this.beats.length; this.beats.push(beat); } } diff --git a/src/midi/MidiTickLookup.ts b/src/midi/MidiTickLookup.ts index d6151f153..b8c1e305e 100644 --- a/src/midi/MidiTickLookup.ts +++ b/src/midi/MidiTickLookup.ts @@ -13,12 +13,16 @@ export class MidiTickLookupFindBeatResult { /** * Gets or sets the beat that is currently played. */ - public currentBeat!: Beat; + public get currentBeat(): Beat { + return this.currentBeatLookup.beat; + } /** * Gets or sets the beat that will be played next. */ - public nextBeat: Beat | null = null; + public get nextBeat(): Beat | null { + return this.nextBeatLookup?.beat ?? null; + } /** * Gets or sets the duration in milliseconds how long this beat is playing. @@ -29,6 +33,17 @@ export class MidiTickLookupFindBeatResult { * Gets or sets the beats ot highlight along the current beat. */ public beatsToHighlight!: Beat[]; + + /** + * Gets or sets the underlying beat lookup which + * was used for building this MidiTickLookupFindBeatResult. + */ + public currentBeatLookup!: BeatTickLookup; + + /** + * Gets or sets the beat lookup for the next beat. + */ + public nextBeatLookup: BeatTickLookup | null = null; } /** @@ -89,20 +104,57 @@ export class MidiTickLookup { * @param tick The current time in midi ticks. * @returns The information about the current beat or null if no beat could be found. */ - public findBeat(tracks: Track[], tick: number): MidiTickLookupFindBeatResult | null { + public findBeat( + tracks: Track[], + tick: number, + currentBeatHint: MidiTickLookupFindBeatResult | null = null + ): MidiTickLookupFindBeatResult | null { + const trackLookup: Map = new Map(); + for (const track of tracks) { + trackLookup.set(track.index, true); + } + + let result: MidiTickLookupFindBeatResult | null = null; + if (currentBeatHint) { + result = this.findBeatFast(trackLookup, currentBeatHint, tick); + } + + if (!result) { + result = this.findBeatSlow(trackLookup, tick); + } + + return result; + } + + private findBeatFast( + trackLookup: Map, + currentBeatHint: MidiTickLookupFindBeatResult, + tick: number + ): MidiTickLookupFindBeatResult | null { + if (tick >= currentBeatHint.currentBeatLookup.start && tick < currentBeatHint.currentBeatLookup.end) { + // still same beat? + return currentBeatHint; + } else if ( + currentBeatHint.nextBeatLookup && + tick >= currentBeatHint.nextBeatLookup.start && + tick < currentBeatHint.nextBeatLookup.end + ) { + // maybe next beat? + return this.createResult(currentBeatHint.nextBeatLookup, trackLookup); + } + + // likely a loop or manual seek, need to fallback to slow path + return null; + } + + private findBeatSlow(trackLookup: Map, tick: number): MidiTickLookupFindBeatResult | null { // get all beats within the masterbar const masterBar = this.findMasterBar(tick); if (!masterBar) { return null; } - const trackLookup: Map = new Map(); - for (const track of tracks) { - trackLookup.set(track.index, true); - } - let beat: BeatTickLookup | null = null; - let index: number = 0; let beats: BeatTickLookup[] = masterBar.beats; for (let b: number = 0; b < beats.length; b++) { // is the current beat played on the given tick? @@ -116,7 +168,6 @@ export class MidiTickLookup { // take the latest played beat we can find. (most right) if (!beat || beat.start < currentBeat.start) { beat = beats[b]; - index = b; } } else if (currentBeat.end > tick) { break; @@ -127,14 +178,30 @@ export class MidiTickLookup { return null; } + return this.createResult(beat, trackLookup); + } + + private createResult(beat: BeatTickLookup, trackLookup: Map): MidiTickLookupFindBeatResult | null { + // search for next relevant beat in masterbar + const nextBeat = this.findNextBeat(beat, trackLookup); + const result = new MidiTickLookupFindBeatResult(); + result.currentBeatLookup = beat; + result.nextBeatLookup = nextBeat; + result.duration = !nextBeat + ? MidiUtils.ticksToMillis(beat.end - beat.start, beat.masterBar.tempo) + : MidiUtils.ticksToMillis(nextBeat.start - beat.start, beat.masterBar.tempo); + result.beatsToHighlight = beat.beatsToHighlight; + return result; + } + + private findNextBeat(beat: BeatTickLookup, trackLookup: Map): BeatTickLookup | null { + const masterBar = beat.masterBar; + let beats = masterBar.beats; // search for next relevant beat in masterbar let nextBeat: BeatTickLookup | null = null; - for (let b: number = index + 1; b < beats.length; b++) { + for (let b: number = beat.index + 1; b < beats.length; b++) { const currentBeat: BeatTickLookup = beats[b]; - if ( - currentBeat.start > beat.start && - trackLookup.has(currentBeat.beat.voice.bar.staff.track.index) - ) { + if (currentBeat.start > beat.start && trackLookup.has(currentBeat.beat.voice.bar.staff.track.index)) { nextBeat = currentBeat; break; } @@ -145,23 +212,14 @@ export class MidiTickLookup { beats = masterBar.nextMasterBar.beats; for (let b: number = 0; b < beats.length; b++) { const currentBeat: BeatTickLookup = beats[b]; - if ( - trackLookup.has(currentBeat.beat.voice.bar.staff.track.index) - ) { + if (trackLookup.has(currentBeat.beat.voice.bar.staff.track.index)) { nextBeat = currentBeat; break; } } } - const result: MidiTickLookupFindBeatResult = new MidiTickLookupFindBeatResult(); - result.currentBeat = beat.beat; - result.nextBeat = !nextBeat ? null : nextBeat.beat; - result.duration = !nextBeat - ? MidiUtils.ticksToMillis(beat.end - beat.start, masterBar.tempo) - : MidiUtils.ticksToMillis(nextBeat.start - beat.start, masterBar.tempo); - result.beatsToHighlight = beat.beatsToHighlight; - return result; + return nextBeat; } private findMasterBar(tick: number): MasterBarTickLookup | null { diff --git a/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts b/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts index 5be3c0db8..38bf045b8 100644 --- a/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts +++ b/src/platform/javascript/AlphaSynthAudioWorkletOutput.ts @@ -79,6 +79,11 @@ export class AlphaSynthWebWorklet { } let left: Float32Array = outputs[0][0]; let right: Float32Array = outputs[0][1]; + + if (!left || !right) { + return true; + } + let samples: number = left.length + right.length; let buffer = this._outputBuffer; if (buffer.length !== samples) { diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index b9662e52e..4a0abe13c 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -618,6 +618,8 @@ export class BrowserUiFacade implements IUiFacade { public createSelectionElement(): IContainer | null { let element: HTMLElement = document.createElement('div'); element.style.position = 'absolute'; + element.style.width = '1px'; + element.style.height = '1px'; return new HtmlElementContainer(element); }