@@ -10,7 +10,7 @@ import {
1010import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter' ;
1111import { Logger } from '@coderline/alphatab/Logger' ;
1212import { AlphaSynthMidiFileHandler } from '@coderline/alphatab/midi/AlphaSynthMidiFileHandler' ;
13- import type { BeatTickLookupItem , IBeatVisibilityChecker } from '@coderline/alphatab/midi/BeatTickLookup' ;
13+ import type { IBeatVisibilityChecker } from '@coderline/alphatab/midi/BeatTickLookup' ;
1414import type {
1515 MetaDataEvent ,
1616 MetaEvent ,
@@ -42,16 +42,20 @@ import {
4242 MidiTickLookupFindBeatResultCursorMode
4343} from '@coderline/alphatab/midi/MidiTickLookup' ;
4444
45+ import {
46+ type ICursorHandler ,
47+ NonAnimatingCursorHandler ,
48+ ToNextBeatAnimatingCursorHandler
49+ } from '@coderline/alphatab/CursorHandler' ;
4550import type { Beat } from '@coderline/alphatab/model/Beat' ;
4651import { ModelUtils } from '@coderline/alphatab/model/ModelUtils' ;
4752import type { Note } from '@coderline/alphatab/model/Note' ;
4853import type { Score } from '@coderline/alphatab/model/Score' ;
4954import type { Track } from '@coderline/alphatab/model/Track' ;
50- import { PlayerMode , ScrollMode } from '@coderline/alphatab/PlayerSettings' ;
5155import type { IContainer } from '@coderline/alphatab/platform/IContainer' ;
5256import type { IMouseEventArgs } from '@coderline/alphatab/platform/IMouseEventArgs' ;
5357import type { IUiFacade } from '@coderline/alphatab/platform/IUiFacade' ;
54- import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs ' ;
58+ import { PlayerMode , ScrollMode } from '@coderline/alphatab/PlayerSettings ' ;
5559import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph' ;
5660import type { IScoreRenderer , RenderHints } from '@coderline/alphatab/rendering/IScoreRenderer' ;
5761import type { RenderFinishedEventArgs } from '@coderline/alphatab/rendering/RenderFinishedEventArgs' ;
@@ -62,6 +66,7 @@ import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds';
6266import type { BoundsLookup } from '@coderline/alphatab/rendering/utils/BoundsLookup' ;
6367import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds' ;
6468import type { StaffSystemBounds } from '@coderline/alphatab/rendering/utils/StaffSystemBounds' ;
69+ import { ResizeEventArgs } from '@coderline/alphatab/ResizeEventArgs' ;
6570import {
6671 HorizontalContinuousScrollHandler ,
6772 HorizontalOffScreenScrollHandler ,
@@ -88,6 +93,7 @@ import type { PlaybackRangeChangedEventArgs } from '@coderline/alphatab/synth/Pl
8893import { PlayerState } from '@coderline/alphatab/synth/PlayerState' ;
8994import type { PlayerStateChangedEventArgs } from '@coderline/alphatab/synth/PlayerStateChangedEventArgs' ;
9095import type { PositionChangedEventArgs } from '@coderline/alphatab/synth/PositionChangedEventArgs' ;
96+ import { Cursors } from '@coderline/alphatab/platform/Cursors' ;
9197
9298/**
9399 * @internal
@@ -166,6 +172,8 @@ export class AlphaTabApiBase<TSettings> {
166172 private _renderer : ScoreRendererWrapper ;
167173
168174 private _defaultScrollHandler ?: IScrollHandler ;
175+ private _defaultCursorHandler ?: ICursorHandler ;
176+ private _customCursorHandler ?: ICursorHandler ;
169177
170178 /**
171179 * An indicator by how many midi-ticks the song contents are shifted.
@@ -988,6 +996,88 @@ export class AlphaTabApiBase<TSettings> {
988996 }
989997 }
990998
999+ /**
1000+ * A custom cursor handler which will be used to update the cursor positions during playback.
1001+ *
1002+ * @category Properties - Player
1003+ * @since 1.8.1
1004+ * @example
1005+ * JavaScript
1006+ * ```js
1007+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
1008+ * api.customCursorHandler = {
1009+ * _customAdorner: undefined,
1010+ * onAttach(cursors) {
1011+ * this._customAdorner = document.createElement('div');
1012+ * this._customAdorner.classList.add('cursor-adorner');
1013+ * cursors.cursorWrapper.element.appendChild(this._customAdorner);
1014+ * },
1015+ * onDetach(cursors) { this._customAdorner.remove(); },
1016+ * placeBarCursor(barCursor, beatBounds) {
1017+ * const barBoundings = beatBounds.barBounds.masterBarBounds;
1018+ * const barBounds = barBoundings.visualBounds;
1019+ * barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
1020+ * },
1021+ * placeBeatCursor(beatCursor, beatBounds, startBeatX) {
1022+ * const barBoundings = beatBounds.barBounds.masterBarBounds;
1023+ * const barBounds = barBoundings.visualBounds;
1024+ * beatCursor.transitionToX(0, startBeatX);
1025+ * beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
1026+ * this._customAdorner.style.left = startBeatX + 'px';
1027+ * this._customAdorner.style.top = (barBounds.y - 10) + 'px';
1028+ * this._customAdorner.style.width = '1px';
1029+ * this._customAdorner.style.height = '10px';
1030+ * this._customAdorner.style.transition = 'left 0ms linear'; // stop animation
1031+ * },
1032+ * transitionBeatCursor(beatCursor, beatBounds, startBeatX, endBeatX, duration, cursorMode) {
1033+ * this._customAdorner.style.transition = `left ${duration}ms linear`; // start animation
1034+ * this._customAdorner.style.left = endBeatX + 'px';
1035+ * }
1036+ * }
1037+ * ```
1038+ *
1039+ * @example
1040+ * C#
1041+ * ```cs
1042+ * var api = new AlphaTabApi<MyControl>(...);
1043+ * api.CustomCursorHandler = new CustomCursorHandler();
1044+ * ```
1045+ *
1046+ * @example
1047+ * Android
1048+ * ```kotlin
1049+ * val api = AlphaTabApi<MyControl>(...)
1050+ * api.customCursorHandler = CustomCursorHandler();
1051+ * ```
1052+ */
1053+ public get customCursorHandler ( ) : ICursorHandler | undefined {
1054+ return this . _customCursorHandler ;
1055+ }
1056+
1057+ public set customCursorHandler ( value : ICursorHandler | undefined ) {
1058+ if ( this . _customCursorHandler === value ) {
1059+ return ;
1060+ }
1061+ const currentHandler = this . _customCursorHandler ?? this . _defaultCursorHandler ;
1062+
1063+ this . _customCursorHandler = value ;
1064+ if ( this . _cursorWrapper ) {
1065+ const cursors = new Cursors (
1066+ this . _cursorWrapper ,
1067+ this . _barCursor ! ,
1068+ this . _beatCursor ! ,
1069+ this . _selectionWrapper !
1070+ ) ;
1071+
1072+ currentHandler ?. onDetach ( cursors ) ;
1073+ if ( value ) {
1074+ value ?. onDetach ( cursors ) ;
1075+ } else if ( this . _defaultCursorHandler ) {
1076+ this . _defaultCursorHandler ! . onAttach ( cursors ) ;
1077+ }
1078+ }
1079+ }
1080+
9911081 private _tickCache : MidiTickLookup | null = null ;
9921082
9931083 /**
@@ -1487,6 +1577,9 @@ export class AlphaTabApiBase<TSettings> {
14871577
14881578 public set playbackRange ( value : PlaybackRange | null ) {
14891579 this . _player . playbackRange = value ;
1580+ if ( this . _tickCache ) {
1581+ this . _tickCache . playbackRange = value ;
1582+ }
14901583 this . _updateSelectionCursor ( value ) ;
14911584 }
14921585
@@ -1650,6 +1743,8 @@ export class AlphaTabApiBase<TSettings> {
16501743
16511744 generator . generate ( ) ;
16521745 this . _tickCache = generator . tickLookup ;
1746+ this . _tickCache . playbackRange = this . playbackRange ;
1747+
16531748 this . _onMidiLoad ( midiFile ) ;
16541749
16551750 const player = this . _player ;
@@ -2070,6 +2165,10 @@ export class AlphaTabApiBase<TSettings> {
20702165 if ( ! this . _cursorWrapper ) {
20712166 return ;
20722167 }
2168+ const cursorHandler = this . customCursorHandler ?? this . _defaultCursorHandler ! ;
2169+ cursorHandler ?. onDetach (
2170+ new Cursors ( this . _cursorWrapper , this . _barCursor ! , this . _beatCursor ! , this . _selectionWrapper ! )
2171+ ) ;
20732172 this . uiFacade . destroyCursors ( ) ;
20742173 this . _cursorWrapper = null ;
20752174 this . _barCursor = null ;
@@ -2088,6 +2187,9 @@ export class AlphaTabApiBase<TSettings> {
20882187 this . _barCursor = cursors . barCursor ;
20892188 this . _beatCursor = cursors . beatCursor ;
20902189 this . _selectionWrapper = cursors . selectionWrapper ;
2190+ const cursorHandler = this . customCursorHandler ?? this . _defaultCursorHandler ! ;
2191+ cursorHandler ?. onAttach ( cursors ) ;
2192+
20912193 this . _isInitialBeatCursorUpdate = true ;
20922194 }
20932195 if ( this . _currentBeat !== null ) {
@@ -2096,6 +2198,7 @@ export class AlphaTabApiBase<TSettings> {
20962198 }
20972199
20982200 private _updateCursors ( ) {
2201+ this . _updateCursorHandler ( ) ;
20992202 this . _updateScrollHandler ( ) ;
21002203
21012204 const enable = this . _hasCursor ;
@@ -2106,6 +2209,23 @@ export class AlphaTabApiBase<TSettings> {
21062209 }
21072210 }
21082211
2212+ private _cursorHandlerMode = false ;
2213+ private _updateCursorHandler ( ) {
2214+ const currentHandler = this . _defaultCursorHandler ;
2215+
2216+ const cursorHandlerMode = this . settings . player . enableAnimatedBeatCursor ;
2217+ // no change
2218+ if ( currentHandler !== undefined && this . _cursorHandlerMode === cursorHandlerMode ) {
2219+ return ;
2220+ }
2221+
2222+ if ( cursorHandlerMode ) {
2223+ this . _defaultCursorHandler = new ToNextBeatAnimatingCursorHandler ( ) ;
2224+ } else {
2225+ this . _defaultCursorHandler = new NonAnimatingCursorHandler ( ) ;
2226+ }
2227+ }
2228+
21092229 private _scrollHandlerMode = ScrollMode . Off ;
21102230 private _scrollHandlerVertical = true ;
21112231 private _updateScrollHandler ( ) {
@@ -2199,10 +2319,6 @@ export class AlphaTabApiBase<TSettings> {
21992319 forceUpdate : boolean = false
22002320 ) : void {
22012321 const beat : Beat = lookupResult . beat ;
2202- const nextBeat : Beat | null = lookupResult . nextBeat ?. beat ?? null ;
2203- const duration : number = lookupResult . duration ;
2204- const beatsToHighlight = lookupResult . beatLookup . highlightedBeats ;
2205-
22062322 if ( ! beat ) {
22072323 return ;
22082324 }
@@ -2235,18 +2351,7 @@ export class AlphaTabApiBase<TSettings> {
22352351 this . _previousStateForCursor = this . _player . state ;
22362352
22372353 this . uiFacade . beginInvoke ( ( ) => {
2238- this . _internalCursorUpdateBeat (
2239- beat ,
2240- nextBeat ,
2241- duration ,
2242- stop ,
2243- beatsToHighlight ,
2244- cache ! ,
2245- beatBoundings ! ,
2246- shouldScroll ,
2247- lookupResult . cursorMode ,
2248- cursorSpeed
2249- ) ;
2354+ this . _internalCursorUpdateBeat ( lookupResult , stop , cache ! , beatBoundings ! , shouldScroll , cursorSpeed ) ;
22502355 } ) ;
22512356 }
22522357
@@ -2266,19 +2371,22 @@ export class AlphaTabApiBase<TSettings> {
22662371 }
22672372
22682373 private _internalCursorUpdateBeat (
2269- beat : Beat ,
2270- nextBeat : Beat | null ,
2271- duration : number ,
2374+ lookupResult : MidiTickLookupFindBeatResult ,
22722375 stop : boolean ,
2273- beatsToHighlight : BeatTickLookupItem [ ] ,
2274- cache : BoundsLookup ,
2376+ boundsLookup : BoundsLookup ,
22752377 beatBoundings : BeatBounds ,
22762378 shouldScroll : boolean ,
2277- cursorMode : MidiTickLookupFindBeatResultCursorMode ,
22782379 cursorSpeed : number
22792380 ) {
2280- const barCursor = this . _barCursor ;
2381+ const beat = lookupResult . beat ;
2382+ const nextBeat = lookupResult . nextBeat ?. beat ;
2383+ let duration = lookupResult . duration ;
2384+ const beatsToHighlight = lookupResult . beatLookup . highlightedBeats ;
2385+ const cursorMode = lookupResult . cursorMode ;
2386+ const cursorHandler = this . customCursorHandler ?? this . _defaultCursorHandler ! ;
2387+
22812388 const beatCursor = this . _beatCursor ;
2389+ const barCursor = this . _barCursor ;
22822390
22832391 const barBoundings : MasterBarBounds = beatBoundings . barBounds . masterBarBounds ;
22842392 const barBounds : Bounds = barBoundings . visualBounds ;
@@ -2287,18 +2395,18 @@ export class AlphaTabApiBase<TSettings> {
22872395 this . _currentBeatBounds = beatBoundings ;
22882396
22892397 if ( barCursor ) {
2290- barCursor . setBounds ( barBounds . x , barBounds . y , barBounds . w , barBounds . h ) ;
2398+ cursorHandler . placeBarCursor ( barCursor , beatBoundings ) ;
22912399 }
22922400
22932401 const isPlayingUpdate = this . _player . state === PlayerState . Playing && ! stop ;
22942402
2295- let nextBeatX : number = barBoundings . visualBounds . x + barBoundings . visualBounds . w ;
2403+ let nextBeatX : number = beatBoundings . realBounds . x + beatBoundings . realBounds . w ;
22962404 let nextBeatBoundings : BeatBounds | null = null ;
22972405 // get position of next beat on same system
22982406 if ( nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode . ToNextBext ) {
22992407 // if we are moving within the same bar or to the next bar
23002408 // transition to the next beat, otherwise transition to the end of the bar.
2301- nextBeatBoundings = cache . findBeat ( nextBeat ) ;
2409+ nextBeatBoundings = boundsLookup . findBeat ( nextBeat ) ;
23022410 if (
23032411 nextBeatBoundings &&
23042412 nextBeatBoundings . barBounds . masterBarBounds . staffSystemBounds === barBoundings . staffSystemBounds
@@ -2309,52 +2417,42 @@ export class AlphaTabApiBase<TSettings> {
23092417
23102418 let startBeatX = beatBoundings . onNotesX ;
23112419 if ( beatCursor ) {
2312- // relative positioning of the cursor
2313- if ( this . settings . player . enableAnimatedBeatCursor ) {
2314- const animationWidth = nextBeatX - beatBoundings . onNotesX ;
2315- const relativePosition = this . _previousTick - this . _currentBeat ! . start ;
2316- const ratioPosition =
2317- this . _currentBeat ! . tickDuration > 0 ? relativePosition / this . _currentBeat ! . tickDuration : 0 ;
2318- startBeatX = beatBoundings . onNotesX + animationWidth * ratioPosition ;
2319- duration -= duration * ratioPosition ;
2320-
2321- if ( isPlayingUpdate ) {
2322- // we do not "reset" the cursor if we are smoothly moving from left to right.
2323- const jumpCursor =
2324- ! previousBeatBounds ||
2325- this . _isInitialBeatCursorUpdate ||
2326- barBounds . y !== previousBeatBounds . barBounds . masterBarBounds . visualBounds . y ||
2327- startBeatX < previousBeatBounds . onNotesX ||
2328- barBoundings . index > previousBeatBounds . barBounds . masterBarBounds . index + 1 ;
2329-
2330- if ( jumpCursor ) {
2331- beatCursor . transitionToX ( 0 , startBeatX ) ;
2332- beatCursor . setBounds ( startBeatX , barBounds . y , 1 , barBounds . h ) ;
2333- }
2334-
2335- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
2336- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
2337- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
2338- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode . ToNextBext ? 2 : 1 ;
2339- nextBeatX = startBeatX + ( nextBeatX - startBeatX ) * factor ;
2340- duration = ( duration / cursorSpeed ) * factor ;
2341-
2342- // we need to put the transition to an own animation frame
2343- // otherwise the stop animation above is not applied.
2344- this . uiFacade . beginInvoke ( ( ) => {
2345- beatCursor ! . transitionToX ( duration , nextBeatX ) ;
2346- } ) ;
2347- } else {
2348- duration = 0 ;
2349- beatCursor . transitionToX ( duration , nextBeatX ) ;
2350- beatCursor . setBounds ( startBeatX , barBounds . y , 1 , barBounds . h ) ;
2420+ const animationWidth = nextBeatX - beatBoundings . onNotesX ;
2421+ const relativePosition = this . _previousTick - this . _currentBeat ! . start ;
2422+ const ratioPosition =
2423+ this . _currentBeat ! . tickDuration > 0 ? relativePosition / this . _currentBeat ! . tickDuration : 0 ;
2424+ startBeatX = beatBoundings . onNotesX + animationWidth * ratioPosition ;
2425+ duration -= duration * ratioPosition ;
2426+
2427+ // respect speed
2428+ duration = duration / cursorSpeed ;
2429+
2430+ if ( isPlayingUpdate ) {
2431+ // we do not "reset" the cursor if we are smoothly moving from left to right.
2432+ const jumpCursor =
2433+ ! previousBeatBounds ||
2434+ this . _isInitialBeatCursorUpdate ||
2435+ barBounds . y !== previousBeatBounds . barBounds . masterBarBounds . visualBounds . y ||
2436+ startBeatX < previousBeatBounds . onNotesX ||
2437+ barBoundings . index > previousBeatBounds . barBounds . masterBarBounds . index + 1 ;
2438+
2439+ if ( jumpCursor ) {
2440+ cursorHandler . placeBeatCursor ( beatCursor , beatBoundings , startBeatX ) ;
23512441 }
2442+
2443+ this . uiFacade . beginInvoke ( ( ) => {
2444+ cursorHandler . transitionBeatCursor (
2445+ beatCursor ,
2446+ beatBoundings ,
2447+ startBeatX ,
2448+ nextBeatX ,
2449+ duration ,
2450+ cursorMode
2451+ ) ;
2452+ } ) ;
23522453 } else {
2353- // ticking cursor
23542454 duration = 0 ;
2355- nextBeatX = startBeatX ;
2356- beatCursor . transitionToX ( duration , nextBeatX ) ;
2357- beatCursor . setBounds ( startBeatX , barBounds . y , 1 , barBounds . h ) ;
2455+ cursorHandler . placeBeatCursor ( beatCursor , beatBoundings , startBeatX ) ;
23582456 }
23592457
23602458 this . _isInitialBeatCursorUpdate = false ;
0 commit comments