@coderline/alphatab 1.6.0-alpha.1405 → 1.6.0-alpha.1409

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * alphaTab v1.6.0-alpha.1405 (develop, build 1405)
2
+ * alphaTab v1.6.0-alpha.1409 (develop, build 1409)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -22397,6 +22397,11 @@ class MidiFileSequencer {
22397
22397
  this._mainState.eventIndex = 0;
22398
22398
  this._mainState.syncPointIndex = 0;
22399
22399
  this._mainState.tempoChangeIndex = 0;
22400
+ this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22401
+ this._mainState.modifiedTempo =
22402
+ this._mainState.syncPoints.length > 0
22403
+ ? this._mainState.syncPoints[0].data.modifiedTempo
22404
+ : this._mainState.currentTempo;
22400
22405
  if (this.isPlayingMain) {
22401
22406
  const metronomeVolume = this._synthesizer.metronomeVolume;
22402
22407
  this._synthesizer.noteOffAll(true);
@@ -22535,18 +22540,6 @@ class MidiFileSequencer {
22535
22540
  this._currentState.synthData[this._currentState.eventIndex].time < this._currentState.currentTime) {
22536
22541
  const synthEvent = this._currentState.synthData[this._currentState.eventIndex];
22537
22542
  this._synthesizer.dispatchEvent(synthEvent);
22538
- while (this._currentState.syncPointIndex < this._currentState.syncPoints.length &&
22539
- this._currentState.syncPoints[this._currentState.syncPointIndex].tick < synthEvent.event.tick) {
22540
- this._currentState.modifiedTempo =
22541
- this._currentState.syncPoints[this._currentState.syncPointIndex].data.modifiedTempo;
22542
- this._currentState.syncPointIndex++;
22543
- }
22544
- while (this._currentState.tempoChangeIndex < this._currentState.tempoChanges.length &&
22545
- this._currentState.tempoChanges[this._currentState.tempoChangeIndex].time <= synthEvent.time) {
22546
- this._currentState.currentTempo =
22547
- this._currentState.tempoChanges[this._currentState.tempoChangeIndex].bpm;
22548
- this._currentState.tempoChangeIndex++;
22549
- }
22550
22543
  this._currentState.eventIndex++;
22551
22544
  anyEventsDispatched = true;
22552
22545
  }
@@ -22576,9 +22569,6 @@ class MidiFileSequencer {
22576
22569
  mainTickPositionToTimePosition(tickPosition) {
22577
22570
  return this.tickPositionToTimePositionWithSpeed(this._mainState, tickPosition, this.playbackSpeed);
22578
22571
  }
22579
- mainTimePositionToTickPosition(timePosition) {
22580
- return this.timePositionToTickPositionWithSpeed(this._mainState, timePosition, this.playbackSpeed);
22581
- }
22582
22572
  mainUpdateSyncPoints(syncPoints) {
22583
22573
  const state = this._mainState;
22584
22574
  syncPoints.sort((a, b) => a.tick - b.tick); // just in case
@@ -22606,7 +22596,49 @@ class MidiFileSequencer {
22606
22596
  state.syncPointIndex = 0;
22607
22597
  }
22608
22598
  currentTimePositionToTickPosition(timePosition) {
22609
- return this.timePositionToTickPositionWithSpeed(this._currentState, timePosition, this.playbackSpeed);
22599
+ const state = this._currentState;
22600
+ if (state.tempoChanges.length === 0) {
22601
+ return 0;
22602
+ }
22603
+ timePosition *= this.playbackSpeed;
22604
+ this.updateCurrentTempo(state, timePosition);
22605
+ const lastTempoChange = state.tempoChanges[state.tempoChangeIndex];
22606
+ const timeDiff = timePosition - lastTempoChange.time;
22607
+ const ticks = ((timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0);
22608
+ // we add 1 for possible rounding errors.(floating point issuses)
22609
+ return lastTempoChange.ticks + ticks + 1;
22610
+ }
22611
+ updateCurrentTempo(state, timePosition) {
22612
+ let tempoChangeIndex = state.tempoChangeIndex;
22613
+ if (timePosition < state.tempoChanges[tempoChangeIndex].time) {
22614
+ tempoChangeIndex = 0;
22615
+ }
22616
+ while (tempoChangeIndex + 1 < state.tempoChanges.length &&
22617
+ state.tempoChanges[tempoChangeIndex + 1].time <= timePosition) {
22618
+ tempoChangeIndex++;
22619
+ }
22620
+ if (tempoChangeIndex !== state.tempoChangeIndex) {
22621
+ state.tempoChangeIndex = tempoChangeIndex;
22622
+ state.currentTempo = state.tempoChanges[state.tempoChangeIndex].bpm;
22623
+ }
22624
+ const syncPoints = state.syncPoints;
22625
+ if (syncPoints.length > 0) {
22626
+ let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22627
+ if (timePosition < syncPoints[syncPointIndex].data.millisecondOffset) {
22628
+ syncPointIndex = 0;
22629
+ }
22630
+ while (syncPointIndex + 1 < syncPoints.length &&
22631
+ syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22632
+ syncPointIndex++;
22633
+ }
22634
+ if (syncPointIndex !== state.syncPointIndex) {
22635
+ state.syncPointIndex = syncPointIndex;
22636
+ state.modifiedTempo = syncPoints[syncPointIndex].data.modifiedTempo;
22637
+ }
22638
+ }
22639
+ else {
22640
+ state.modifiedTempo = state.currentTempo;
22641
+ }
22610
22642
  }
22611
22643
  mainTimePositionFromBackingTrack(timePosition, backingTrackLength) {
22612
22644
  const mainState = this._mainState;
@@ -22614,11 +22646,8 @@ class MidiFileSequencer {
22614
22646
  if (timePosition < 0 || syncPoints.length === 0) {
22615
22647
  return timePosition;
22616
22648
  }
22617
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].data.millisecondOffset ? mainState.syncPointIndex : 0;
22618
- while (syncPointIndex + 1 < syncPoints.length &&
22619
- syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22620
- syncPointIndex++;
22621
- }
22649
+ this.updateCurrentTempo(this._mainState, timePosition);
22650
+ const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22622
22651
  const currentSyncPoint = syncPoints[syncPointIndex];
22623
22652
  const timeDiff = timePosition - currentSyncPoint.data.millisecondOffset;
22624
22653
  let alphaTabTimeDiff;
@@ -22640,7 +22669,10 @@ class MidiFileSequencer {
22640
22669
  return timePosition;
22641
22670
  }
22642
22671
  timePosition *= this.playbackSpeed;
22643
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].time ? mainState.syncPointIndex : 0;
22672
+ let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22673
+ if (timePosition < syncPoints[syncPointIndex].time) {
22674
+ syncPointIndex = 0;
22675
+ }
22644
22676
  while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].time <= timePosition) {
22645
22677
  syncPointIndex++;
22646
22678
  }
@@ -22678,26 +22710,6 @@ class MidiFileSequencer {
22678
22710
  timePosition += tickPosition * (60000.0 / (bpm * state.division));
22679
22711
  return timePosition / playbackSpeed;
22680
22712
  }
22681
- timePositionToTickPositionWithSpeed(state, timePosition, playbackSpeed) {
22682
- timePosition *= playbackSpeed;
22683
- let ticks = 0;
22684
- let bpm = 120.0;
22685
- let lastChange = 0;
22686
- // find start and bpm of last tempo change before time
22687
- for (const c of state.tempoChanges) {
22688
- if (timePosition < c.time) {
22689
- break;
22690
- }
22691
- ticks = c.ticks;
22692
- bpm = c.bpm;
22693
- lastChange = c.time;
22694
- }
22695
- // add the missing ticks
22696
- timePosition -= lastChange;
22697
- ticks += (timePosition / (60000.0 / (bpm * state.division))) | 0;
22698
- // we add 1 for possible rounding errors.(floating point issuses)
22699
- return ticks + 1;
22700
- }
22701
22713
  get internalEndTime() {
22702
22714
  if (this.isPlayingMain) {
22703
22715
  return !this.mainPlaybackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime;
@@ -26916,7 +26928,7 @@ class TinySoundFont {
26916
26928
  return processedEvents;
26917
26929
  }
26918
26930
  processMidiMessage(e) {
26919
- Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26931
+ //Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26920
26932
  const command = e.type;
26921
26933
  switch (command) {
26922
26934
  case MidiEventType.TimeSignature:
@@ -28037,6 +28049,9 @@ class EventEmitter {
28037
28049
  }
28038
28050
  on(value) {
28039
28051
  this._listeners.push(value);
28052
+ return () => {
28053
+ this.off(value);
28054
+ };
28040
28055
  }
28041
28056
  off(value) {
28042
28057
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28056,6 +28071,9 @@ class EventEmitterOfT {
28056
28071
  }
28057
28072
  on(value) {
28058
28073
  this._listeners.push(value);
28074
+ return () => {
28075
+ this.off(value);
28076
+ };
28059
28077
  }
28060
28078
  off(value) {
28061
28079
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28452,7 +28470,7 @@ class AlphaSynthBase {
28452
28470
  endTick = this.sequencer.currentEndTick;
28453
28471
  }
28454
28472
  if (this._tickPosition >= endTick) {
28455
- // fully done with playback of remaining samples?
28473
+ // fully done with playback of remaining samples?
28456
28474
  if (this._notPlayedSamples <= 0) {
28457
28475
  this._notPlayedSamples = 0;
28458
28476
  if (this.sequencer.isPlayingCountIn) {
@@ -35802,6 +35820,27 @@ class MidiFileGenerator {
35802
35820
  controller.moveNext();
35803
35821
  previousMasterBar = bar;
35804
35822
  }
35823
+ // here we interpolate the sync point which marks the end of the sync.
35824
+ // Sync points define new tempos at certain positions.
35825
+ // looking from the last sync point to the end we do not assume the end where the audio ends,
35826
+ // but where it ends according to the BPM and the remaining ticks.
35827
+ if (this.syncPoints.length > 0) {
35828
+ const lastSyncPoint = this.syncPoints[this.syncPoints.length - 1];
35829
+ const endTick = controller.currentTick;
35830
+ const remainingTicks = endTick - lastSyncPoint.tick;
35831
+ if (remainingTicks > 0) {
35832
+ const syncPointData = new SyncPointData();
35833
+ // last occurence of the last bar
35834
+ syncPointData.barOccurence = barOccurence.get(this._score.masterBars.length - 1);
35835
+ // same tempo as last point
35836
+ syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35837
+ // interpolated end from last syncPoint
35838
+ syncPointData.millisecondOffset =
35839
+ lastSyncPoint.data.millisecondOffset +
35840
+ MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35841
+ this.syncPoints.push(new BackingTrackSyncPoint(endTick, syncPointData));
35842
+ }
35843
+ }
35805
35844
  for (const track of this._score.tracks) {
35806
35845
  this._handler.finishTrack(track.index, controller.currentTick);
35807
35846
  }
@@ -38293,6 +38332,270 @@ class ExternalMediaPlayer extends BackingTrackPlayer {
38293
38332
  }
38294
38333
  }
38295
38334
 
38335
+ /**
38336
+ * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
38337
+ * allowing dynamic changing of the underlying instance without loosing aspects like the
38338
+ * main playback information and event listeners.
38339
+ *
38340
+ * @remarks
38341
+ * This wrapper is used when re-exposing the underlying player via {@link AlphaTabApiBase} to integrators.
38342
+ * Even with dynamic switching between synthesizer, backing tracks etc. aspects like volume, playbackspeed,
38343
+ * event listeners etc. should not be lost.
38344
+ */
38345
+ class AlphaSynthWrapper {
38346
+ constructor() {
38347
+ // relevant state information we want to remember when switching between player instances
38348
+ this._masterVolume = 1;
38349
+ this._metronomeVolume = 0;
38350
+ this._countInVolume = 0;
38351
+ this._playbackSpeed = 1;
38352
+ this._isLooping = false;
38353
+ this._midiEventsPlayedFilter = [];
38354
+ this.ready = new EventEmitter();
38355
+ this.readyForPlayback = new EventEmitter();
38356
+ this.finished = new EventEmitter();
38357
+ this.soundFontLoaded = new EventEmitter();
38358
+ this.soundFontLoadFailed = new EventEmitterOfT();
38359
+ this.midiLoaded = new EventEmitterOfT();
38360
+ this.midiLoadFailed = new EventEmitterOfT();
38361
+ this.stateChanged = new EventEmitterOfT();
38362
+ this.positionChanged = new EventEmitterOfT();
38363
+ this.midiEventsPlayed = new EventEmitterOfT();
38364
+ this.playbackRangeChanged = new EventEmitterOfT();
38365
+ }
38366
+ get instance() {
38367
+ return this._instance;
38368
+ }
38369
+ set instance(value) {
38370
+ this._instance = value;
38371
+ // unregister all events from previous instance
38372
+ const unregister = this._instanceEventUnregister;
38373
+ if (unregister) {
38374
+ for (const e of unregister) {
38375
+ e();
38376
+ }
38377
+ }
38378
+ if (value) {
38379
+ // regsiter to events of new player and forward them to existing listeners
38380
+ const newUnregister = [];
38381
+ newUnregister.push(value.ready.on(() => this.ready.trigger()));
38382
+ newUnregister.push(value.readyForPlayback.on(() => this.readyForPlayback.trigger()));
38383
+ newUnregister.push(value.finished.on(() => this.finished.trigger()));
38384
+ newUnregister.push(value.soundFontLoaded.on(() => this.soundFontLoaded.trigger()));
38385
+ newUnregister.push(value.soundFontLoadFailed.on(e => this.soundFontLoadFailed.trigger(e)));
38386
+ newUnregister.push(value.midiLoaded.on(e => this.midiLoaded.trigger(e)));
38387
+ newUnregister.push(value.midiLoadFailed.on(e => this.midiLoadFailed.trigger(e)));
38388
+ newUnregister.push(value.stateChanged.on(e => this.stateChanged.trigger(e)));
38389
+ newUnregister.push(value.positionChanged.on(e => this.positionChanged.trigger(e)));
38390
+ newUnregister.push(value.midiEventsPlayed.on(e => this.midiEventsPlayed.trigger(e)));
38391
+ newUnregister.push(value.playbackRangeChanged.on(e => this.playbackRangeChanged.trigger(e)));
38392
+ this._instanceEventUnregister = newUnregister;
38393
+ // restore state on new player
38394
+ if (this.isReady) {
38395
+ value.masterVolume = this._masterVolume;
38396
+ value.metronomeVolume = this._metronomeVolume;
38397
+ value.countInVolume = this._countInVolume;
38398
+ value.playbackSpeed = this._playbackSpeed;
38399
+ value.isLooping = this._isLooping;
38400
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38401
+ }
38402
+ else {
38403
+ newUnregister.push(value.ready.on(() => {
38404
+ value.masterVolume = this._masterVolume;
38405
+ value.metronomeVolume = this._metronomeVolume;
38406
+ value.countInVolume = this._countInVolume;
38407
+ value.playbackSpeed = this._playbackSpeed;
38408
+ value.isLooping = this._isLooping;
38409
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38410
+ }));
38411
+ }
38412
+ }
38413
+ else {
38414
+ this._instanceEventUnregister = undefined;
38415
+ }
38416
+ }
38417
+ get output() {
38418
+ return this._instance.output;
38419
+ }
38420
+ get isReady() {
38421
+ return this._instance ? this._instance.isReady : false;
38422
+ }
38423
+ get isReadyForPlayback() {
38424
+ return this._instance ? this._instance.isReadyForPlayback : false;
38425
+ }
38426
+ get state() {
38427
+ return this._instance ? this._instance.state : PlayerState.Paused;
38428
+ }
38429
+ get logLevel() {
38430
+ return Logger.logLevel;
38431
+ }
38432
+ set logLevel(value) {
38433
+ Logger.logLevel = value;
38434
+ if (this._instance) {
38435
+ this._instance.logLevel = value;
38436
+ }
38437
+ }
38438
+ get masterVolume() {
38439
+ return this._masterVolume;
38440
+ }
38441
+ set masterVolume(value) {
38442
+ value = Math.max(value, SynthConstants.MinVolume);
38443
+ this._masterVolume = value;
38444
+ if (this._instance) {
38445
+ this._instance.masterVolume = value;
38446
+ }
38447
+ }
38448
+ get metronomeVolume() {
38449
+ return this._metronomeVolume;
38450
+ }
38451
+ set metronomeVolume(value) {
38452
+ value = Math.max(value, SynthConstants.MinVolume);
38453
+ this._metronomeVolume = value;
38454
+ if (this._instance) {
38455
+ this._instance.metronomeVolume = value;
38456
+ }
38457
+ }
38458
+ get playbackSpeed() {
38459
+ return this._playbackSpeed;
38460
+ }
38461
+ set playbackSpeed(value) {
38462
+ this._playbackSpeed = value;
38463
+ if (this._instance) {
38464
+ this._instance.playbackSpeed = value;
38465
+ }
38466
+ }
38467
+ get tickPosition() {
38468
+ return this._instance ? this._instance.tickPosition : 0;
38469
+ }
38470
+ set tickPosition(value) {
38471
+ if (this._instance) {
38472
+ this._instance.tickPosition = value;
38473
+ }
38474
+ }
38475
+ get timePosition() {
38476
+ return this._instance ? this._instance.timePosition : 0;
38477
+ }
38478
+ set timePosition(value) {
38479
+ if (this._instance) {
38480
+ this._instance.timePosition = value;
38481
+ }
38482
+ }
38483
+ get playbackRange() {
38484
+ return this._instance ? this._instance.playbackRange : null;
38485
+ }
38486
+ set playbackRange(value) {
38487
+ if (this._instance) {
38488
+ this._instance.playbackRange = value;
38489
+ }
38490
+ }
38491
+ get isLooping() {
38492
+ return this._isLooping;
38493
+ }
38494
+ set isLooping(value) {
38495
+ this._isLooping = value;
38496
+ if (this._instance) {
38497
+ this._instance.isLooping = value;
38498
+ }
38499
+ }
38500
+ get countInVolume() {
38501
+ return this._countInVolume;
38502
+ }
38503
+ set countInVolume(value) {
38504
+ this._countInVolume = value;
38505
+ if (this._instance) {
38506
+ this._instance.countInVolume = value;
38507
+ }
38508
+ }
38509
+ get midiEventsPlayedFilter() {
38510
+ return this._midiEventsPlayedFilter;
38511
+ }
38512
+ set midiEventsPlayedFilter(value) {
38513
+ this._midiEventsPlayedFilter = value;
38514
+ if (this._instance) {
38515
+ this._instance.midiEventsPlayedFilter = value;
38516
+ }
38517
+ }
38518
+ destroy() {
38519
+ if (this._instance) {
38520
+ this._instance.destroy();
38521
+ this._instance = undefined;
38522
+ }
38523
+ }
38524
+ play() {
38525
+ return this._instance ? this._instance.play() : false;
38526
+ }
38527
+ pause() {
38528
+ if (this._instance) {
38529
+ this._instance.pause();
38530
+ }
38531
+ }
38532
+ playPause() {
38533
+ if (this._instance) {
38534
+ this._instance.playPause();
38535
+ }
38536
+ }
38537
+ stop() {
38538
+ if (this._instance) {
38539
+ this._instance.stop();
38540
+ }
38541
+ }
38542
+ playOneTimeMidiFile(midi) {
38543
+ if (this._instance) {
38544
+ this._instance.playOneTimeMidiFile(midi);
38545
+ }
38546
+ }
38547
+ loadSoundFont(data, append) {
38548
+ if (this._instance) {
38549
+ this._instance.loadSoundFont(data, append);
38550
+ }
38551
+ }
38552
+ resetSoundFonts() {
38553
+ if (this._instance) {
38554
+ this._instance.resetSoundFonts();
38555
+ }
38556
+ }
38557
+ loadMidiFile(midi) {
38558
+ if (this._instance) {
38559
+ this._instance.loadMidiFile(midi);
38560
+ }
38561
+ }
38562
+ loadBackingTrack(score, syncPoints) {
38563
+ if (this._instance) {
38564
+ this._instance.loadBackingTrack(score, syncPoints);
38565
+ }
38566
+ }
38567
+ applyTranspositionPitches(transpositionPitches) {
38568
+ if (this._instance) {
38569
+ this._instance.applyTranspositionPitches(transpositionPitches);
38570
+ }
38571
+ }
38572
+ setChannelTranspositionPitch(channel, semitones) {
38573
+ if (this._instance) {
38574
+ this._instance.setChannelTranspositionPitch(channel, semitones);
38575
+ }
38576
+ }
38577
+ setChannelMute(channel, mute) {
38578
+ if (this._instance) {
38579
+ this._instance.setChannelMute(channel, mute);
38580
+ }
38581
+ }
38582
+ resetChannelStates() {
38583
+ if (this._instance) {
38584
+ this._instance.resetChannelStates();
38585
+ }
38586
+ }
38587
+ setChannelSolo(channel, solo) {
38588
+ if (this._instance) {
38589
+ this._instance.setChannelSolo(channel, solo);
38590
+ }
38591
+ }
38592
+ setChannelVolume(channel, volume) {
38593
+ if (this._instance) {
38594
+ this._instance.setChannelVolume(channel, volume);
38595
+ }
38596
+ }
38597
+ }
38598
+
38296
38599
  class SelectionInfo {
38297
38600
  constructor(beat) {
38298
38601
  this.bounds = null;
@@ -38383,43 +38686,13 @@ class AlphaTabApiBase {
38383
38686
  this._tracks = [];
38384
38687
  this._actualPlayerMode = PlayerMode.Disabled;
38385
38688
  this._tickCache = null;
38386
- /**
38387
- * The alphaSynth player used for playback.
38388
- * @remarks
38389
- * This is the low-level API to the Midi synthesizer used for playback.
38390
- * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
38391
- * @category Properties - Player
38392
- * @since 0.9.4
38393
- * @example
38394
- * JavaScript
38395
- * ```js
38396
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
38397
- * setupPlayerEvents(api.settings);
38398
- * ```
38399
- *
38400
- * @example
38401
- * C#
38402
- * ```cs
38403
- * var api = new AlphaTabApi<MyControl>(...);
38404
- * SetupPlayerEvents(api.Player);
38405
- * ```
38406
- *
38407
- * @example
38408
- * Android
38409
- * ```kotlin
38410
- * val api = AlphaTabApi<MyControl>(...)
38411
- * setupPlayerEvents(api.player)
38412
- * ```
38413
- */
38414
- this.player = null;
38415
38689
  this._cursorWrapper = null;
38416
38690
  this._barCursor = null;
38417
38691
  this._beatCursor = null;
38418
38692
  this._selectionWrapper = null;
38419
38693
  this._previousTick = 0;
38420
- this._playerState = PlayerState.Paused;
38421
38694
  this._currentBeat = null;
38422
- this._currentBarBounds = null;
38695
+ this._currentBeatBounds = null;
38423
38696
  this._previousStateForCursor = PlayerState.Paused;
38424
38697
  this._previousCursorCache = null;
38425
38698
  this._lastScroll = 0;
@@ -38990,133 +39263,6 @@ class AlphaTabApiBase {
38990
39263
  *
38991
39264
  */
38992
39265
  this.error = new EventEmitterOfT();
38993
- /**
38994
- * This event is fired when all required data for playback is loaded and ready.
38995
- * @remarks
38996
- * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
38997
- * all background workers are started, the audio output is initialized, a soundfont is loaded, and a song was loaded into the player as midi file.
38998
- *
38999
- * @eventProperty
39000
- * @category Events - Player
39001
- * @since 0.9.4
39002
- *
39003
- * @example
39004
- * JavaScript
39005
- * ```js
39006
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39007
- * api.playerReady.on(() => {
39008
- * enablePlayerControls();
39009
- * });
39010
- * ```
39011
- *
39012
- * @example
39013
- * C#
39014
- * ```cs
39015
- * var api = new AlphaTabApi<MyControl>(...);
39016
- * api.PlayerReady.On(() =>
39017
- * {
39018
- * EnablePlayerControls()
39019
- * });
39020
- * ```
39021
- *
39022
- * @example
39023
- * Android
39024
- * ```kotlin
39025
- * val api = AlphaTabApi<MyControl>(...)
39026
- * api.playerReady.on {
39027
- * enablePlayerControls()
39028
- * }
39029
- * ```
39030
- */
39031
- this.playerReady = new EventEmitter();
39032
- /**
39033
- * This event is fired when the playback of the whole song finished.
39034
- * @remarks
39035
- * This event is finished regardless on whether looping is enabled or not.
39036
- *
39037
- * @eventProperty
39038
- * @category Events - Player
39039
- * @since 0.9.4
39040
- *
39041
- * @example
39042
- * JavaScript
39043
- * ```js
39044
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39045
- * api.playerFinished.on((args) => {
39046
- * // speed trainer
39047
- * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
39048
- * });
39049
- * api.isLooping = true;
39050
- * api.playbackSpeed = 0.5;
39051
- * api.play()
39052
- * ```
39053
- *
39054
- * @example
39055
- * C#
39056
- * ```cs
39057
- * var api = new AlphaTabApi<MyControl>(...);
39058
- * api.PlayerFinished.On(() =>
39059
- * {
39060
- * // speed trainer
39061
- * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
39062
- * });
39063
- * api.IsLooping = true;
39064
- * api.PlaybackSpeed = 0.5;
39065
- * api.Play();
39066
- * ```
39067
- *
39068
- * @example
39069
- * Android
39070
- * ```kotlin
39071
- * val api = AlphaTabApi<MyControl>(...)
39072
- * api.playerFinished.on {
39073
- * // speed trainer
39074
- * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
39075
- * }
39076
- * api.isLooping = true
39077
- * api.playbackSpeed = 0.5
39078
- * api.play()
39079
- * ```
39080
- *
39081
- */
39082
- this.playerFinished = new EventEmitter();
39083
- /**
39084
- * This event is fired when the SoundFont needed for playback was loaded.
39085
- *
39086
- * @eventProperty
39087
- * @category Events - Player
39088
- * @since 0.9.4
39089
- *
39090
- * @example
39091
- * JavaScript
39092
- * ```js
39093
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39094
- * api.soundFontLoaded.on(() => {
39095
- * hideSoundFontLoadingIndicator();
39096
- * });
39097
- * ```
39098
- *
39099
- * @example
39100
- * C#
39101
- * ```cs
39102
- * var api = new AlphaTabApi<MyControl>(...);
39103
- * api.SoundFontLoaded.On(() =>
39104
- * {
39105
- * HideSoundFontLoadingIndicator();
39106
- * });
39107
- * ```
39108
- *
39109
- * @example
39110
- * Android
39111
- * ```kotlin
39112
- * val api = AlphaTabApi<MyControl>(...);
39113
- * api.soundFontLoaded.on {
39114
- * hideSoundFontLoadingIndicator();
39115
- * }
39116
- * ```
39117
- *
39118
- */
39119
- this.soundFontLoaded = new EventEmitter();
39120
39266
  /**
39121
39267
  * This event is fired when a Midi file is being loaded.
39122
39268
  *
@@ -39204,213 +39350,6 @@ class AlphaTabApiBase {
39204
39350
  *
39205
39351
  */
39206
39352
  this.midiLoaded = new EventEmitterOfT();
39207
- /**
39208
- * This event is fired when the playback state changed.
39209
- *
39210
- * @eventProperty
39211
- * @category Events - Player
39212
- * @since 0.9.4
39213
- *
39214
- * @example
39215
- * JavaScript
39216
- * ```js
39217
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39218
- * api.playerStateChanged.on((args) => {
39219
- * updatePlayerControls(args.state, args.stopped);
39220
- * });
39221
- * ```
39222
- *
39223
- * @example
39224
- * C#
39225
- * ```cs
39226
- * var api = new AlphaTabApi<MyControl>(...);
39227
- * api.PlayerStateChanged.On(args =>
39228
- * {
39229
- * UpdatePlayerControls(args);
39230
- * });
39231
- * ```
39232
- *
39233
- * @example
39234
- * Android
39235
- * ```kotlin
39236
- * val api = AlphaTabApi<MyControl>(...)
39237
- * api.playerStateChanged.on { args ->
39238
- * updatePlayerControls(args)
39239
- * }
39240
- * ```
39241
- *
39242
- */
39243
- this.playerStateChanged = new EventEmitterOfT();
39244
- /**
39245
- * This event is fired when the current playback position of the song changed.
39246
- *
39247
- * @eventProperty
39248
- * @category Events - Player
39249
- * @since 0.9.4
39250
- *
39251
- * @example
39252
- * JavaScript
39253
- * ```js
39254
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39255
- * api.playerPositionChanged.on((args) => {
39256
- * updatePlayerPosition(args);
39257
- * });
39258
- * ```
39259
- *
39260
- * @example
39261
- * C#
39262
- * ```cs
39263
- * var api = new AlphaTabApi<MyControl>(...);
39264
- * api.PlayerPositionChanged.On(args =>
39265
- * {
39266
- * UpdatePlayerPosition(args);
39267
- * });
39268
- * ```
39269
- *
39270
- * @example
39271
- * Android
39272
- * ```kotlin
39273
- * val api = AlphaTabApi<MyControl>(...)
39274
- * api.playerPositionChanged.on { args ->
39275
- * updatePlayerPosition(args)
39276
- * }
39277
- * ```
39278
- *
39279
- */
39280
- this.playerPositionChanged = new EventEmitterOfT();
39281
- /**
39282
- * This event is fired when the synthesizer played certain midi events.
39283
- *
39284
- * @remarks
39285
- * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
39286
- * audio playback elements like notes/rests played or metronome ticks.
39287
- *
39288
- * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
39289
- *
39290
- * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
39291
- * until some extend but highly encourage changing to the new models in case of problems.
39292
- *
39293
- * @eventProperty
39294
- * @category Events - Player
39295
- * @since 1.2.0
39296
- *
39297
- * @example
39298
- * JavaScript
39299
- * ```js
39300
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39301
- * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
39302
- * api.midiEventsPlayed.on(function(e) {
39303
- * for(const midi of e.events) {
39304
- * if(midi.isMetronome) {
39305
- * console.log('Metronome tick ' + midi.tick);
39306
- * }
39307
- * }
39308
- * });
39309
- * ```
39310
- *
39311
- * @example
39312
- * C#
39313
- * ```cs
39314
- * var api = new AlphaTabApi<MyControl>(...);
39315
- * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
39316
- * api.MidiEventsPlayed.On(e =>
39317
- * {
39318
- * foreach(var midi of e.events)
39319
- * {
39320
- * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
39321
- * {
39322
- * Console.WriteLine("Metronome tick " + midi.Tick);
39323
- * }
39324
- * }
39325
- * });
39326
- * ```
39327
- *
39328
- * @example
39329
- * Android
39330
- * ```kotlin
39331
- * val api = AlphaTabApi<MyControl>(...);
39332
- * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
39333
- * api.midiEventsPlayed.on { e ->
39334
- * for (midi in e.events) {
39335
- * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
39336
- * println("Metronome tick " + midi.tick);
39337
- * }
39338
- * }
39339
- * }
39340
- * ```
39341
- * @see {@link MidiEvent}
39342
- * @see {@link TimeSignatureEvent}
39343
- * @see {@link AlphaTabMetronomeEvent}
39344
- * @see {@link AlphaTabRestEvent}
39345
- * @see {@link NoteOnEvent}
39346
- * @see {@link NoteOffEvent}
39347
- * @see {@link ControlChangeEvent}
39348
- * @see {@link ProgramChangeEvent}
39349
- * @see {@link TempoChangeEvent}
39350
- * @see {@link PitchBendEvent}
39351
- * @see {@link NoteBendEvent}
39352
- * @see {@link EndOfTrackEvent}
39353
- * @see {@link MetaEvent}
39354
- * @see {@link MetaDataEvent}
39355
- * @see {@link MetaNumberEvent}
39356
- * @see {@link Midi20PerNotePitchBendEvent}
39357
- * @see {@link SystemCommonEvent}
39358
- * @see {@link SystemExclusiveEvent}
39359
- */
39360
- this.midiEventsPlayed = new EventEmitterOfT();
39361
- /**
39362
- * This event is fired when the playback range changed.
39363
- *
39364
- * @eventProperty
39365
- * @category Events - Player
39366
- * @since 1.2.3
39367
- *
39368
- * @example
39369
- * JavaScript
39370
- * ```js
39371
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39372
- * api.playbackRangeChanged.on((args) => {
39373
- * if (args.playbackRange) {
39374
- * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
39375
- * } else {
39376
- * clearHighlightInProgressBar();
39377
- * }
39378
- * });
39379
- * ```
39380
- *
39381
- * @example
39382
- * C#
39383
- * ```cs
39384
- * var api = new AlphaTabApi<MyControl>(...);
39385
- * api.PlaybackRangeChanged.On(args =>
39386
- * {
39387
- * if (args.PlaybackRange != null)
39388
- * {
39389
- * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
39390
- * }
39391
- * else
39392
- * {
39393
- * ClearHighlightInProgressBar();
39394
- * }
39395
- * });
39396
- * ```
39397
- *
39398
- * @example
39399
- * Android
39400
- * ```kotlin
39401
- * val api = AlphaTabApi<MyControl>(...)
39402
- * api.playbackRangeChanged.on { args ->
39403
- * val playbackRange = args.playbackRange
39404
- * if (playbackRange != null) {
39405
- * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
39406
- * } else {
39407
- * clearHighlightInProgressBar()
39408
- * }
39409
- * }
39410
- * ```
39411
- *
39412
- */
39413
- this.playbackRangeChanged = new EventEmitterOfT();
39414
39353
  /**
39415
39354
  * @internal
39416
39355
  */
@@ -39466,8 +39405,9 @@ class AlphaTabApiBase {
39466
39405
  this.appendRenderResult(null); // marks last element
39467
39406
  });
39468
39407
  this.renderer.error.on(this.onError.bind(this));
39408
+ this.setupPlayerWrapper();
39469
39409
  if (this.settings.player.playerMode !== PlayerMode.Disabled) {
39470
- this.setupPlayer();
39410
+ this.setupOrDestroyPlayer();
39471
39411
  }
39472
39412
  this.setupClickHandling();
39473
39413
  // delay rendering to allow ui to hook up with events first.
@@ -39475,6 +39415,36 @@ class AlphaTabApiBase {
39475
39415
  this.uiFacade.initialRender();
39476
39416
  });
39477
39417
  }
39418
+ setupPlayerWrapper() {
39419
+ const player = new AlphaSynthWrapper();
39420
+ this._player = player;
39421
+ player.ready.on(() => {
39422
+ this.loadMidiForScore();
39423
+ });
39424
+ player.readyForPlayback.on(() => {
39425
+ this.onPlayerReady();
39426
+ if (this.tracks) {
39427
+ for (const track of this.tracks) {
39428
+ const volume = track.playbackInfo.volume / 16;
39429
+ player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
39430
+ player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
39431
+ }
39432
+ }
39433
+ });
39434
+ player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
39435
+ player.soundFontLoadFailed.on(e => {
39436
+ this.onError(e);
39437
+ });
39438
+ player.midiLoaded.on(this.onMidiLoaded.bind(this));
39439
+ player.midiLoadFailed.on(e => {
39440
+ this.onError(e);
39441
+ });
39442
+ player.stateChanged.on(this.onPlayerStateChanged.bind(this));
39443
+ player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
39444
+ player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
39445
+ player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
39446
+ player.finished.on(this.onPlayerFinished.bind(this));
39447
+ }
39478
39448
  /**
39479
39449
  * Destroys the alphaTab control and restores the initial state of the UI.
39480
39450
  * @remarks
@@ -39507,9 +39477,7 @@ class AlphaTabApiBase {
39507
39477
  */
39508
39478
  destroy() {
39509
39479
  this._isDestroyed = true;
39510
- if (this.player) {
39511
- this.player.destroy();
39512
- }
39480
+ this._player.destroy();
39513
39481
  this.uiFacade.destroy();
39514
39482
  this.renderer.destroy();
39515
39483
  }
@@ -39557,14 +39525,8 @@ class AlphaTabApiBase {
39557
39525
  ModelUtils.applyPitchOffsets(this.settings, score);
39558
39526
  }
39559
39527
  this.renderer.updateSettings(this.settings);
39560
- // enable/disable player if needed
39561
- if (this.settings.player.playerMode !== PlayerMode.Disabled) {
39562
- if (this.setupPlayer() && score) {
39563
- this.loadMidiForScore();
39564
- }
39565
- }
39566
- else {
39567
- this.destroyPlayer();
39528
+ if (this.setupOrDestroyPlayer()) {
39529
+ this.loadMidiForScore();
39568
39530
  }
39569
39531
  this.onSettingsUpdated();
39570
39532
  }
@@ -39871,9 +39833,6 @@ class AlphaTabApiBase {
39871
39833
  * ```
39872
39834
  */
39873
39835
  loadSoundFont(data, append = false) {
39874
- if (!this.player) {
39875
- return false;
39876
- }
39877
39836
  return this.uiFacade.loadSoundFont(data, append);
39878
39837
  }
39879
39838
  /**
@@ -39919,10 +39878,7 @@ class AlphaTabApiBase {
39919
39878
  * ```
39920
39879
  */
39921
39880
  resetSoundFonts() {
39922
- if (!this.player) {
39923
- return;
39924
- }
39925
- this.player.resetSoundFonts();
39881
+ this._player.resetSoundFonts();
39926
39882
  }
39927
39883
  /**
39928
39884
  * Initiates a re-rendering of the current setup.
@@ -40036,6 +39992,37 @@ class AlphaTabApiBase {
40036
39992
  get boundsLookup() {
40037
39993
  return this.renderer.boundsLookup;
40038
39994
  }
39995
+ /**
39996
+ * The alphaSynth player used for playback.
39997
+ * @remarks
39998
+ * This is the low-level API to the Midi synthesizer used for playback.
39999
+ * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
40000
+ * @category Properties - Player
40001
+ * @since 0.9.4
40002
+ * @example
40003
+ * JavaScript
40004
+ * ```js
40005
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
40006
+ * setupPlayerEvents(api.settings);
40007
+ * ```
40008
+ *
40009
+ * @example
40010
+ * C#
40011
+ * ```cs
40012
+ * var api = new AlphaTabApi<MyControl>(...);
40013
+ * SetupPlayerEvents(api.Player);
40014
+ * ```
40015
+ *
40016
+ * @example
40017
+ * Android
40018
+ * ```kotlin
40019
+ * val api = AlphaTabApi<MyControl>(...)
40020
+ * setupPlayerEvents(api.player)
40021
+ * ```
40022
+ */
40023
+ get player() {
40024
+ return this._player.instance ? this._player : null;
40025
+ }
40039
40026
  /**
40040
40027
  * Whether the player is ready for starting the playback.
40041
40028
  * @remarks
@@ -40065,10 +40052,7 @@ class AlphaTabApiBase {
40065
40052
  * ```
40066
40053
  */
40067
40054
  get isReadyForPlayback() {
40068
- if (!this.player) {
40069
- return false;
40070
- }
40071
- return this.player.isReadyForPlayback;
40055
+ return this._player.isReadyForPlayback;
40072
40056
  }
40073
40057
  /**
40074
40058
  * The current player state.
@@ -40098,10 +40082,7 @@ class AlphaTabApiBase {
40098
40082
  * ```
40099
40083
  */
40100
40084
  get playerState() {
40101
- if (!this.player) {
40102
- return PlayerState.Paused;
40103
- }
40104
- return this.player.state;
40085
+ return this._player.state;
40105
40086
  }
40106
40087
  /**
40107
40088
  * The current master volume as percentage (0-1).
@@ -40131,15 +40112,10 @@ class AlphaTabApiBase {
40131
40112
  * ```
40132
40113
  */
40133
40114
  get masterVolume() {
40134
- if (!this.player) {
40135
- return 0;
40136
- }
40137
- return this.player.masterVolume;
40115
+ return this._player.masterVolume;
40138
40116
  }
40139
40117
  set masterVolume(value) {
40140
- if (this.player) {
40141
- this.player.masterVolume = value;
40142
- }
40118
+ this._player.masterVolume = value;
40143
40119
  }
40144
40120
  /**
40145
40121
  * The metronome volume as percentage (0-1).
@@ -40170,15 +40146,10 @@ class AlphaTabApiBase {
40170
40146
  * ```
40171
40147
  */
40172
40148
  get metronomeVolume() {
40173
- if (!this.player) {
40174
- return 0;
40175
- }
40176
- return this.player.metronomeVolume;
40149
+ return this._player.metronomeVolume;
40177
40150
  }
40178
40151
  set metronomeVolume(value) {
40179
- if (this.player) {
40180
- this.player.metronomeVolume = value;
40181
- }
40152
+ this._player.metronomeVolume = value;
40182
40153
  }
40183
40154
  /**
40184
40155
  * The volume of the count-in metronome ticks.
@@ -40209,15 +40180,10 @@ class AlphaTabApiBase {
40209
40180
  * ```
40210
40181
  */
40211
40182
  get countInVolume() {
40212
- if (!this.player) {
40213
- return 0;
40214
- }
40215
- return this.player.countInVolume;
40183
+ return this._player.countInVolume;
40216
40184
  }
40217
40185
  set countInVolume(value) {
40218
- if (this.player) {
40219
- this.player.countInVolume = value;
40220
- }
40186
+ this._player.countInVolume = value;
40221
40187
  }
40222
40188
  /**
40223
40189
  * The midi events which will trigger the `midiEventsPlayed` event
@@ -40276,15 +40242,10 @@ class AlphaTabApiBase {
40276
40242
  * ```
40277
40243
  */
40278
40244
  get midiEventsPlayedFilter() {
40279
- if (!this.player) {
40280
- return [];
40281
- }
40282
- return this.player.midiEventsPlayedFilter;
40245
+ return this._player.midiEventsPlayedFilter;
40283
40246
  }
40284
40247
  set midiEventsPlayedFilter(value) {
40285
- if (this.player) {
40286
- this.player.midiEventsPlayedFilter = value;
40287
- }
40248
+ this._player.midiEventsPlayedFilter = value;
40288
40249
  }
40289
40250
  /**
40290
40251
  * The position within the song in midi ticks.
@@ -40312,15 +40273,10 @@ class AlphaTabApiBase {
40312
40273
  * ```
40313
40274
  */
40314
40275
  get tickPosition() {
40315
- if (!this.player) {
40316
- return 0;
40317
- }
40318
- return this.player.tickPosition;
40276
+ return this._player.tickPosition;
40319
40277
  }
40320
40278
  set tickPosition(value) {
40321
- if (this.player) {
40322
- this.player.tickPosition = value;
40323
- }
40279
+ this._player.tickPosition = value;
40324
40280
  }
40325
40281
  /**
40326
40282
  * The position within the song in milliseconds
@@ -40348,15 +40304,10 @@ class AlphaTabApiBase {
40348
40304
  * ```
40349
40305
  */
40350
40306
  get timePosition() {
40351
- if (!this.player) {
40352
- return 0;
40353
- }
40354
- return this.player.timePosition;
40307
+ return this._player.timePosition;
40355
40308
  }
40356
40309
  set timePosition(value) {
40357
- if (this.player) {
40358
- this.player.timePosition = value;
40359
- }
40310
+ this._player.timePosition = value;
40360
40311
  }
40361
40312
  /**
40362
40313
  * The range of the song that should be played.
@@ -40390,17 +40341,12 @@ class AlphaTabApiBase {
40390
40341
  * ```
40391
40342
  */
40392
40343
  get playbackRange() {
40393
- if (!this.player) {
40394
- return null;
40395
- }
40396
- return this.player.playbackRange;
40344
+ return this._player.playbackRange;
40397
40345
  }
40398
40346
  set playbackRange(value) {
40399
- if (this.player) {
40400
- this.player.playbackRange = value;
40401
- if (this.settings.player.enableCursor) {
40402
- this.updateSelectionCursor(value);
40403
- }
40347
+ this._player.playbackRange = value;
40348
+ if (this.settings.player.enableCursor) {
40349
+ this.updateSelectionCursor(value);
40404
40350
  }
40405
40351
  }
40406
40352
  /**
@@ -40432,15 +40378,10 @@ class AlphaTabApiBase {
40432
40378
  * ```
40433
40379
  */
40434
40380
  get playbackSpeed() {
40435
- if (!this.player) {
40436
- return 0;
40437
- }
40438
- return this.player.playbackSpeed;
40381
+ return this._player.playbackSpeed;
40439
40382
  }
40440
40383
  set playbackSpeed(value) {
40441
- if (this.player) {
40442
- this.player.playbackSpeed = value;
40443
- }
40384
+ this._player.playbackSpeed = value;
40444
40385
  }
40445
40386
  /**
40446
40387
  * Whether the playback should automatically restart after it finished.
@@ -40471,27 +40412,17 @@ class AlphaTabApiBase {
40471
40412
  * ```
40472
40413
  */
40473
40414
  get isLooping() {
40474
- if (!this.player) {
40475
- return false;
40476
- }
40477
- return this.player.isLooping;
40415
+ return this._player.isLooping;
40478
40416
  }
40479
40417
  set isLooping(value) {
40480
- if (this.player) {
40481
- this.player.isLooping = value;
40482
- }
40418
+ this._player.isLooping = value;
40483
40419
  }
40484
40420
  destroyPlayer() {
40485
- if (!this.player) {
40486
- return;
40487
- }
40488
- this.player.destroy();
40489
- this.player = null;
40421
+ this._player.destroy();
40490
40422
  this._previousTick = 0;
40491
- this._playerState = PlayerState.Paused;
40492
40423
  this.destroyCursors();
40493
40424
  }
40494
- setupPlayer() {
40425
+ setupOrDestroyPlayer() {
40495
40426
  let mode = this.settings.player.playerMode;
40496
40427
  if (mode === PlayerMode.EnabledAutomatic) {
40497
40428
  const score = this.score;
@@ -40505,66 +40436,34 @@ class AlphaTabApiBase {
40505
40436
  mode = PlayerMode.EnabledSynthesizer;
40506
40437
  }
40507
40438
  }
40439
+ let newPlayer = null;
40508
40440
  if (mode !== this._actualPlayerMode) {
40509
40441
  this.destroyPlayer();
40442
+ switch (mode) {
40443
+ case PlayerMode.Disabled:
40444
+ newPlayer = null;
40445
+ break;
40446
+ case PlayerMode.EnabledSynthesizer:
40447
+ newPlayer = this.uiFacade.createWorkerPlayer();
40448
+ break;
40449
+ case PlayerMode.EnabledBackingTrack:
40450
+ newPlayer = this.uiFacade.createBackingTrackPlayer();
40451
+ break;
40452
+ case PlayerMode.EnabledExternalMedia:
40453
+ newPlayer = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40454
+ break;
40455
+ }
40456
+ }
40457
+ else {
40458
+ // no change in player mode, just update song info if needed
40459
+ return true;
40510
40460
  }
40511
40461
  this.updateCursors();
40512
40462
  this._actualPlayerMode = mode;
40513
- switch (mode) {
40514
- case PlayerMode.Disabled:
40515
- this.destroyPlayer();
40516
- return false;
40517
- case PlayerMode.EnabledSynthesizer:
40518
- if (this.player) {
40519
- return true;
40520
- }
40521
- // new player needed
40522
- this.player = this.uiFacade.createWorkerPlayer();
40523
- break;
40524
- case PlayerMode.EnabledBackingTrack:
40525
- if (this.player) {
40526
- return true;
40527
- }
40528
- // new player needed
40529
- this.player = this.uiFacade.createBackingTrackPlayer();
40530
- break;
40531
- case PlayerMode.EnabledExternalMedia:
40532
- if (this.player) {
40533
- return true;
40534
- }
40535
- this.player = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40536
- break;
40537
- }
40538
- if (!this.player) {
40463
+ if (!newPlayer) {
40539
40464
  return false;
40540
40465
  }
40541
- this.player.ready.on(() => {
40542
- this.loadMidiForScore();
40543
- });
40544
- this.player.readyForPlayback.on(() => {
40545
- this.onPlayerReady();
40546
- if (this.tracks) {
40547
- for (const track of this.tracks) {
40548
- const volume = track.playbackInfo.volume / 16;
40549
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40550
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40551
- }
40552
- }
40553
- });
40554
- this.player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
40555
- this.player.soundFontLoadFailed.on(e => {
40556
- this.onError(e);
40557
- });
40558
- this.player.midiLoaded.on(this.onMidiLoaded.bind(this));
40559
- this.player.midiLoadFailed.on(e => {
40560
- this.onError(e);
40561
- });
40562
- this.player.stateChanged.on(this.onPlayerStateChanged.bind(this));
40563
- this.player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
40564
- this.player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
40565
- this.player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
40566
- this.player.finished.on(this.onPlayerFinished.bind(this));
40567
- this.setupPlayerEvents();
40466
+ this._player.instance = newPlayer;
40568
40467
  return false;
40569
40468
  }
40570
40469
  loadMidiForScore() {
@@ -40584,12 +40483,10 @@ class AlphaTabApiBase {
40584
40483
  generator.generate();
40585
40484
  this._tickCache = generator.tickLookup;
40586
40485
  this.onMidiLoad(midiFile);
40587
- const player = this.player;
40588
- if (player) {
40589
- player.loadMidiFile(midiFile);
40590
- player.loadBackingTrack(score, generator.syncPoints);
40591
- player.applyTranspositionPitches(generator.transpositionPitches);
40592
- }
40486
+ const player = this._player;
40487
+ player.loadMidiFile(midiFile);
40488
+ player.loadBackingTrack(score, generator.syncPoints);
40489
+ player.applyTranspositionPitches(generator.transpositionPitches);
40593
40490
  }
40594
40491
  /**
40595
40492
  * Changes the volume of the given tracks.
@@ -40627,12 +40524,9 @@ class AlphaTabApiBase {
40627
40524
  * ```
40628
40525
  */
40629
40526
  changeTrackVolume(tracks, volume) {
40630
- if (!this.player) {
40631
- return;
40632
- }
40633
40527
  for (const track of tracks) {
40634
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40635
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40528
+ this._player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40529
+ this._player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40636
40530
  }
40637
40531
  }
40638
40532
  /**
@@ -40669,12 +40563,9 @@ class AlphaTabApiBase {
40669
40563
  * ```
40670
40564
  */
40671
40565
  changeTrackSolo(tracks, solo) {
40672
- if (!this.player) {
40673
- return;
40674
- }
40675
40566
  for (const track of tracks) {
40676
- this.player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40677
- this.player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40567
+ this._player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40568
+ this._player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40678
40569
  }
40679
40570
  }
40680
40571
  /**
@@ -40710,12 +40601,9 @@ class AlphaTabApiBase {
40710
40601
  * ```
40711
40602
  */
40712
40603
  changeTrackMute(tracks, mute) {
40713
- if (!this.player) {
40714
- return;
40715
- }
40716
40604
  for (const track of tracks) {
40717
- this.player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40718
- this.player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40605
+ this._player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40606
+ this._player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40719
40607
  }
40720
40608
  }
40721
40609
  /**
@@ -40753,12 +40641,9 @@ class AlphaTabApiBase {
40753
40641
  * ```
40754
40642
  */
40755
40643
  changeTrackTranspositionPitch(tracks, semitones) {
40756
- if (!this.player) {
40757
- return;
40758
- }
40759
40644
  for (const track of tracks) {
40760
- this.player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40761
- this.player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40645
+ this._player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40646
+ this._player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40762
40647
  }
40763
40648
  }
40764
40649
  /**
@@ -40789,10 +40674,7 @@ class AlphaTabApiBase {
40789
40674
  * ```
40790
40675
  */
40791
40676
  play() {
40792
- if (!this.player) {
40793
- return false;
40794
- }
40795
- return this.player.play();
40677
+ return this._player.play();
40796
40678
  }
40797
40679
  /**
40798
40680
  * Pauses the playback of the current song.
@@ -40821,10 +40703,7 @@ class AlphaTabApiBase {
40821
40703
  * ```
40822
40704
  */
40823
40705
  pause() {
40824
- if (!this.player) {
40825
- return;
40826
- }
40827
- this.player.pause();
40706
+ this._player.pause();
40828
40707
  }
40829
40708
  /**
40830
40709
  * Toggles between play/pause depending on the current player state.
@@ -40855,10 +40734,7 @@ class AlphaTabApiBase {
40855
40734
  * ```
40856
40735
  */
40857
40736
  playPause() {
40858
- if (!this.player) {
40859
- return;
40860
- }
40861
- this.player.playPause();
40737
+ this._player.playPause();
40862
40738
  }
40863
40739
  /**
40864
40740
  * Stops the playback of the current song, and moves the playback position back to the start.
@@ -40889,10 +40765,7 @@ class AlphaTabApiBase {
40889
40765
  * ```
40890
40766
  */
40891
40767
  stop() {
40892
- if (!this.player) {
40893
- return;
40894
- }
40895
- this.player.stop();
40768
+ this._player.stop();
40896
40769
  }
40897
40770
  /**
40898
40771
  * Triggers the play of the given beat.
@@ -40928,15 +40801,12 @@ class AlphaTabApiBase {
40928
40801
  * ```
40929
40802
  */
40930
40803
  playBeat(beat) {
40931
- if (!this.player) {
40932
- return;
40933
- }
40934
40804
  // we generate a new midi file containing only the beat
40935
40805
  const midiFile = new MidiFile();
40936
40806
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40937
40807
  const generator = new MidiFileGenerator(beat.voice.bar.staff.track.score, this.settings, handler);
40938
40808
  generator.generateSingleBeat(beat);
40939
- this.player.playOneTimeMidiFile(midiFile);
40809
+ this._player.playOneTimeMidiFile(midiFile);
40940
40810
  }
40941
40811
  /**
40942
40812
  * Triggers the play of the given note.
@@ -40971,15 +40841,12 @@ class AlphaTabApiBase {
40971
40841
  * ```
40972
40842
  */
40973
40843
  playNote(note) {
40974
- if (!this.player) {
40975
- return;
40976
- }
40977
40844
  // we generate a new midi file containing only the beat
40978
40845
  const midiFile = new MidiFile();
40979
40846
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40980
40847
  const generator = new MidiFileGenerator(note.beat.voice.bar.staff.track.score, this.settings, handler);
40981
40848
  generator.generateSingleNote(note);
40982
- this.player.playOneTimeMidiFile(midiFile);
40849
+ this._player.playOneTimeMidiFile(midiFile);
40983
40850
  }
40984
40851
  destroyCursors() {
40985
40852
  if (!this._cursorWrapper) {
@@ -41011,36 +40878,6 @@ class AlphaTabApiBase {
41011
40878
  this.destroyCursors();
41012
40879
  }
41013
40880
  }
41014
- setupPlayerEvents() {
41015
- //
41016
- // Hook into events
41017
- this._previousTick = 0;
41018
- this._playerState = PlayerState.Paused;
41019
- // we need to update our position caches if we render a tablature
41020
- this.renderer.postRenderFinished.on(() => {
41021
- this._currentBeat = null;
41022
- this.cursorUpdateTick(this._previousTick, false, 1, this._previousTick > 10);
41023
- });
41024
- if (this.player) {
41025
- this.player.positionChanged.on(e => {
41026
- this._previousTick = e.currentTick;
41027
- this.uiFacade.beginInvoke(() => {
41028
- const cursorSpeed = e.modifiedTempo / e.originalTempo;
41029
- this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41030
- });
41031
- });
41032
- this.player.stateChanged.on(e => {
41033
- this._playerState = e.state;
41034
- if (!e.stopped && e.state === PlayerState.Paused) {
41035
- const currentBeat = this._currentBeat;
41036
- const tickCache = this._tickCache;
41037
- if (currentBeat && tickCache) {
41038
- this.player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41039
- }
41040
- }
41041
- });
41042
- }
41043
- }
41044
40881
  /**
41045
40882
  * updates the cursors to highlight the beat at the specified tick position
41046
40883
  * @param tick
@@ -41081,7 +40918,7 @@ class AlphaTabApiBase {
41081
40918
  if (!forceUpdate &&
41082
40919
  beat === previousBeat?.beat &&
41083
40920
  cache === previousCache &&
41084
- previousState === this._playerState &&
40921
+ previousState === this._player.state &&
41085
40922
  previousBeat?.start === lookupResult.start) {
41086
40923
  return;
41087
40924
  }
@@ -41093,7 +40930,7 @@ class AlphaTabApiBase {
41093
40930
  // actually show the cursor
41094
40931
  this._currentBeat = lookupResult;
41095
40932
  this._previousCursorCache = cache;
41096
- this._previousStateForCursor = this._playerState;
40933
+ this._previousStateForCursor = this._player.state;
41097
40934
  this.uiFacade.beginInvoke(() => {
41098
40935
  this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
41099
40936
  });
@@ -41104,9 +40941,9 @@ class AlphaTabApiBase {
41104
40941
  * @category Methods - Player
41105
40942
  */
41106
40943
  scrollToCursor() {
41107
- const barBounds = this._currentBarBounds;
41108
- if (barBounds) {
41109
- this.internalScrollToCursor(barBounds);
40944
+ const beatBounds = this._currentBeatBounds;
40945
+ if (beatBounds) {
40946
+ this.internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
41110
40947
  }
41111
40948
  }
41112
40949
  internalScrollToCursor(barBoundings) {
@@ -41165,10 +41002,12 @@ class AlphaTabApiBase {
41165
41002
  const beatCursor = this._beatCursor;
41166
41003
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
41167
41004
  const barBounds = barBoundings.visualBounds;
41168
- this._currentBarBounds = barBoundings;
41005
+ const previousBeatBounds = this._currentBeatBounds;
41006
+ this._currentBeatBounds = beatBoundings;
41169
41007
  if (barCursor) {
41170
41008
  barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
41171
41009
  }
41010
+ const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
41172
41011
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
41173
41012
  // get position of next beat on same system
41174
41013
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
@@ -41186,12 +41025,38 @@ class AlphaTabApiBase {
41186
41025
  if (this.settings.player.enableAnimatedBeatCursor) {
41187
41026
  const animationWidth = nextBeatX - beatBoundings.onNotesX;
41188
41027
  const relativePosition = this._previousTick - this._currentBeat.start;
41189
- const ratioPosition = relativePosition / this._currentBeat.tickDuration;
41028
+ const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
41190
41029
  startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
41191
41030
  duration -= duration * ratioPosition;
41192
- beatCursor.transitionToX(0, startBeatX);
41031
+ if (isPlayingUpdate) {
41032
+ // we do not "reset" the cursor if we are smoothly moving from left to right.
41033
+ const jumpCursor = !previousBeatBounds ||
41034
+ barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
41035
+ startBeatX < previousBeatBounds.onNotesX ||
41036
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
41037
+ if (jumpCursor) {
41038
+ beatCursor.transitionToX(0, startBeatX);
41039
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41040
+ }
41041
+ // we need to put the transition to an own animation frame
41042
+ // otherwise the stop animation above is not applied.
41043
+ this.uiFacade.beginInvoke(() => {
41044
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
41045
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
41046
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
41047
+ const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * 2;
41048
+ beatCursor.transitionToX((duration / cursorSpeed) * 2, doubleEndBeatX);
41049
+ });
41050
+ }
41051
+ else {
41052
+ beatCursor.transitionToX(0, startBeatX);
41053
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41054
+ }
41055
+ }
41056
+ else {
41057
+ // ticking cursor
41058
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41193
41059
  }
41194
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41195
41060
  }
41196
41061
  // if playing, animate the cursor to the next beat
41197
41062
  if (this.settings.player.enableElementHighlighting) {
@@ -41199,7 +41064,6 @@ class AlphaTabApiBase {
41199
41064
  }
41200
41065
  // actively playing? -> animate cursor and highlight items
41201
41066
  let shouldNotifyBeatChange = false;
41202
- const isPlayingUpdate = this._playerState === PlayerState.Playing && !stop;
41203
41067
  if (isPlayingUpdate) {
41204
41068
  if (this.settings.player.enableElementHighlighting) {
41205
41069
  for (const highlight of beatsToHighlight) {
@@ -41210,15 +41074,6 @@ class AlphaTabApiBase {
41210
41074
  shouldScroll = !stop;
41211
41075
  shouldNotifyBeatChange = true;
41212
41076
  }
41213
- if (this.settings.player.enableAnimatedBeatCursor && beatCursor) {
41214
- if (isPlayingUpdate) {
41215
- // we need to put the transition to an own animation frame
41216
- // otherwise the stop animation above is not applied.
41217
- this.uiFacade.beginInvoke(() => {
41218
- beatCursor.transitionToX(duration / cursorSpeed, nextBeatX);
41219
- });
41220
- }
41221
- }
41222
41077
  if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) {
41223
41078
  this.internalScrollToCursor(barBoundings);
41224
41079
  }
@@ -41308,7 +41163,7 @@ class AlphaTabApiBase {
41308
41163
  const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
41309
41164
  // move to selection start
41310
41165
  this._currentBeat = null; // reset current beat so it is updating the cursor
41311
- if (this._playerState === PlayerState.Paused) {
41166
+ if (this._player.state === PlayerState.Paused) {
41312
41167
  this.cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
41313
41168
  }
41314
41169
  this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
@@ -41499,7 +41354,7 @@ class AlphaTabApiBase {
41499
41354
  }
41500
41355
  this.scoreLoaded.trigger(score);
41501
41356
  this.uiFacade.triggerEvent(this.container, 'scoreLoaded', score);
41502
- if (this.setupPlayer()) {
41357
+ if (this.setupOrDestroyPlayer()) {
41503
41358
  this.loadMidiForScore();
41504
41359
  }
41505
41360
  }
@@ -41528,6 +41383,8 @@ class AlphaTabApiBase {
41528
41383
  if (this._isDestroyed) {
41529
41384
  return;
41530
41385
  }
41386
+ this._currentBeat = null;
41387
+ this.cursorUpdateTick(this._previousTick, false, 1, this._previousTick > 10);
41531
41388
  this.postRenderFinished.trigger();
41532
41389
  this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null);
41533
41390
  }
@@ -41542,25 +41399,155 @@ class AlphaTabApiBase {
41542
41399
  this.error.trigger(error);
41543
41400
  this.uiFacade.triggerEvent(this.container, 'error', error);
41544
41401
  }
41402
+ /**
41403
+ * This event is fired when all required data for playback is loaded and ready.
41404
+ * @remarks
41405
+ * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
41406
+ * all background workers are started, the audio output is initialized, a soundfont is loaded, and a song was loaded into the player as midi file.
41407
+ *
41408
+ * @eventProperty
41409
+ * @category Events - Player
41410
+ * @since 0.9.4
41411
+ *
41412
+ * @example
41413
+ * JavaScript
41414
+ * ```js
41415
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41416
+ * api.playerReady.on(() => {
41417
+ * enablePlayerControls();
41418
+ * });
41419
+ * ```
41420
+ *
41421
+ * @example
41422
+ * C#
41423
+ * ```cs
41424
+ * var api = new AlphaTabApi<MyControl>(...);
41425
+ * api.PlayerReady.On(() =>
41426
+ * {
41427
+ * EnablePlayerControls()
41428
+ * });
41429
+ * ```
41430
+ *
41431
+ * @example
41432
+ * Android
41433
+ * ```kotlin
41434
+ * val api = AlphaTabApi<MyControl>(...)
41435
+ * api.playerReady.on {
41436
+ * enablePlayerControls()
41437
+ * }
41438
+ * ```
41439
+ */
41440
+ get playerReady() {
41441
+ return this._player.readyForPlayback;
41442
+ }
41545
41443
  onPlayerReady() {
41546
41444
  if (this._isDestroyed) {
41547
41445
  return;
41548
41446
  }
41549
- this.playerReady.trigger();
41550
41447
  this.uiFacade.triggerEvent(this.container, 'playerReady', null);
41551
41448
  }
41449
+ /**
41450
+ * This event is fired when the playback of the whole song finished.
41451
+ * @remarks
41452
+ * This event is finished regardless on whether looping is enabled or not.
41453
+ *
41454
+ * @eventProperty
41455
+ * @category Events - Player
41456
+ * @since 0.9.4
41457
+ *
41458
+ * @example
41459
+ * JavaScript
41460
+ * ```js
41461
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41462
+ * api.playerFinished.on((args) => {
41463
+ * // speed trainer
41464
+ * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
41465
+ * });
41466
+ * api.isLooping = true;
41467
+ * api.playbackSpeed = 0.5;
41468
+ * api.play()
41469
+ * ```
41470
+ *
41471
+ * @example
41472
+ * C#
41473
+ * ```cs
41474
+ * var api = new AlphaTabApi<MyControl>(...);
41475
+ * api.PlayerFinished.On(() =>
41476
+ * {
41477
+ * // speed trainer
41478
+ * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
41479
+ * });
41480
+ * api.IsLooping = true;
41481
+ * api.PlaybackSpeed = 0.5;
41482
+ * api.Play();
41483
+ * ```
41484
+ *
41485
+ * @example
41486
+ * Android
41487
+ * ```kotlin
41488
+ * val api = AlphaTabApi<MyControl>(...)
41489
+ * api.playerFinished.on {
41490
+ * // speed trainer
41491
+ * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
41492
+ * }
41493
+ * api.isLooping = true
41494
+ * api.playbackSpeed = 0.5
41495
+ * api.play()
41496
+ * ```
41497
+ *
41498
+ */
41499
+ get playerFinished() {
41500
+ return this._player.finished;
41501
+ }
41552
41502
  onPlayerFinished() {
41553
41503
  if (this._isDestroyed) {
41554
41504
  return;
41555
41505
  }
41556
- this.playerFinished.trigger();
41557
41506
  this.uiFacade.triggerEvent(this.container, 'playerFinished', null);
41558
41507
  }
41508
+ /**
41509
+ * This event is fired when the SoundFont needed for playback was loaded.
41510
+ *
41511
+ * @eventProperty
41512
+ * @category Events - Player
41513
+ * @since 0.9.4
41514
+ *
41515
+ * @example
41516
+ * JavaScript
41517
+ * ```js
41518
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41519
+ * api.soundFontLoaded.on(() => {
41520
+ * hideSoundFontLoadingIndicator();
41521
+ * });
41522
+ * ```
41523
+ *
41524
+ * @example
41525
+ * C#
41526
+ * ```cs
41527
+ * var api = new AlphaTabApi<MyControl>(...);
41528
+ * api.SoundFontLoaded.On(() =>
41529
+ * {
41530
+ * HideSoundFontLoadingIndicator();
41531
+ * });
41532
+ * ```
41533
+ *
41534
+ * @example
41535
+ * Android
41536
+ * ```kotlin
41537
+ * val api = AlphaTabApi<MyControl>(...);
41538
+ * api.soundFontLoaded.on {
41539
+ * hideSoundFontLoadingIndicator();
41540
+ * }
41541
+ * ```
41542
+ *
41543
+ */
41544
+ get soundFontLoaded() {
41545
+ return this._player.soundFontLoaded;
41546
+ }
41559
41547
  onSoundFontLoaded() {
41560
41548
  if (this._isDestroyed) {
41561
41549
  return;
41562
41550
  }
41563
- this.soundFontLoaded.trigger();
41564
41551
  this.uiFacade.triggerEvent(this.container, 'soundFontLoaded', null);
41565
41552
  }
41566
41553
  onMidiLoad(e) {
@@ -41577,34 +41564,255 @@ class AlphaTabApiBase {
41577
41564
  this.midiLoaded.trigger(e);
41578
41565
  this.uiFacade.triggerEvent(this.container, 'midiFileLoaded', e);
41579
41566
  }
41567
+ /**
41568
+ * This event is fired when the playback state changed.
41569
+ *
41570
+ * @eventProperty
41571
+ * @category Events - Player
41572
+ * @since 0.9.4
41573
+ *
41574
+ * @example
41575
+ * JavaScript
41576
+ * ```js
41577
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41578
+ * api.playerStateChanged.on((args) => {
41579
+ * updatePlayerControls(args.state, args.stopped);
41580
+ * });
41581
+ * ```
41582
+ *
41583
+ * @example
41584
+ * C#
41585
+ * ```cs
41586
+ * var api = new AlphaTabApi<MyControl>(...);
41587
+ * api.PlayerStateChanged.On(args =>
41588
+ * {
41589
+ * UpdatePlayerControls(args);
41590
+ * });
41591
+ * ```
41592
+ *
41593
+ * @example
41594
+ * Android
41595
+ * ```kotlin
41596
+ * val api = AlphaTabApi<MyControl>(...)
41597
+ * api.playerStateChanged.on { args ->
41598
+ * updatePlayerControls(args)
41599
+ * }
41600
+ * ```
41601
+ *
41602
+ */
41603
+ get playerStateChanged() {
41604
+ return this._player.stateChanged;
41605
+ }
41580
41606
  onPlayerStateChanged(e) {
41581
41607
  if (this._isDestroyed) {
41582
41608
  return;
41583
41609
  }
41584
- this.playerStateChanged.trigger(e);
41610
+ if (!e.stopped && e.state === PlayerState.Paused) {
41611
+ const currentBeat = this._currentBeat;
41612
+ const tickCache = this._tickCache;
41613
+ if (currentBeat && tickCache) {
41614
+ this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41615
+ }
41616
+ }
41585
41617
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
41586
41618
  }
41619
+ /**
41620
+ * This event is fired when the current playback position of the song changed.
41621
+ *
41622
+ * @eventProperty
41623
+ * @category Events - Player
41624
+ * @since 0.9.4
41625
+ *
41626
+ * @example
41627
+ * JavaScript
41628
+ * ```js
41629
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41630
+ * api.playerPositionChanged.on((args) => {
41631
+ * updatePlayerPosition(args);
41632
+ * });
41633
+ * ```
41634
+ *
41635
+ * @example
41636
+ * C#
41637
+ * ```cs
41638
+ * var api = new AlphaTabApi<MyControl>(...);
41639
+ * api.PlayerPositionChanged.On(args =>
41640
+ * {
41641
+ * UpdatePlayerPosition(args);
41642
+ * });
41643
+ * ```
41644
+ *
41645
+ * @example
41646
+ * Android
41647
+ * ```kotlin
41648
+ * val api = AlphaTabApi<MyControl>(...)
41649
+ * api.playerPositionChanged.on { args ->
41650
+ * updatePlayerPosition(args)
41651
+ * }
41652
+ * ```
41653
+ *
41654
+ */
41655
+ get playerPositionChanged() {
41656
+ return this._player.positionChanged;
41657
+ }
41587
41658
  onPlayerPositionChanged(e) {
41588
41659
  if (this._isDestroyed) {
41589
41660
  return;
41590
41661
  }
41591
- if (this.score !== null && this.tracks.length > 0) {
41592
- this.playerPositionChanged.trigger(e);
41593
- this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41594
- }
41662
+ this._previousTick = e.currentTick;
41663
+ this.uiFacade.beginInvoke(() => {
41664
+ const cursorSpeed = e.modifiedTempo / e.originalTempo;
41665
+ this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41666
+ });
41667
+ this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41668
+ }
41669
+ /**
41670
+ * This event is fired when the synthesizer played certain midi events.
41671
+ *
41672
+ * @remarks
41673
+ * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
41674
+ * audio playback elements like notes/rests played or metronome ticks.
41675
+ *
41676
+ * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
41677
+ *
41678
+ * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
41679
+ * until some extend but highly encourage changing to the new models in case of problems.
41680
+ *
41681
+ * @eventProperty
41682
+ * @category Events - Player
41683
+ * @since 1.2.0
41684
+ *
41685
+ * @example
41686
+ * JavaScript
41687
+ * ```js
41688
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41689
+ * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
41690
+ * api.midiEventsPlayed.on(function(e) {
41691
+ * for(const midi of e.events) {
41692
+ * if(midi.isMetronome) {
41693
+ * console.log('Metronome tick ' + midi.tick);
41694
+ * }
41695
+ * }
41696
+ * });
41697
+ * ```
41698
+ *
41699
+ * @example
41700
+ * C#
41701
+ * ```cs
41702
+ * var api = new AlphaTabApi<MyControl>(...);
41703
+ * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
41704
+ * api.MidiEventsPlayed.On(e =>
41705
+ * {
41706
+ * foreach(var midi of e.events)
41707
+ * {
41708
+ * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
41709
+ * {
41710
+ * Console.WriteLine("Metronome tick " + midi.Tick);
41711
+ * }
41712
+ * }
41713
+ * });
41714
+ * ```
41715
+ *
41716
+ * @example
41717
+ * Android
41718
+ * ```kotlin
41719
+ * val api = AlphaTabApi<MyControl>(...);
41720
+ * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
41721
+ * api.midiEventsPlayed.on { e ->
41722
+ * for (midi in e.events) {
41723
+ * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
41724
+ * println("Metronome tick " + midi.tick);
41725
+ * }
41726
+ * }
41727
+ * }
41728
+ * ```
41729
+ * @see {@link MidiEvent}
41730
+ * @see {@link TimeSignatureEvent}
41731
+ * @see {@link AlphaTabMetronomeEvent}
41732
+ * @see {@link AlphaTabRestEvent}
41733
+ * @see {@link NoteOnEvent}
41734
+ * @see {@link NoteOffEvent}
41735
+ * @see {@link ControlChangeEvent}
41736
+ * @see {@link ProgramChangeEvent}
41737
+ * @see {@link TempoChangeEvent}
41738
+ * @see {@link PitchBendEvent}
41739
+ * @see {@link NoteBendEvent}
41740
+ * @see {@link EndOfTrackEvent}
41741
+ * @see {@link MetaEvent}
41742
+ * @see {@link MetaDataEvent}
41743
+ * @see {@link MetaNumberEvent}
41744
+ * @see {@link Midi20PerNotePitchBendEvent}
41745
+ * @see {@link SystemCommonEvent}
41746
+ * @see {@link SystemExclusiveEvent}
41747
+ */
41748
+ get midiEventsPlayed() {
41749
+ return this._player.midiEventsPlayed;
41595
41750
  }
41596
41751
  onMidiEventsPlayed(e) {
41597
41752
  if (this._isDestroyed) {
41598
41753
  return;
41599
41754
  }
41600
- this.midiEventsPlayed.trigger(e);
41601
41755
  this.uiFacade.triggerEvent(this.container, 'midiEventsPlayed', e);
41602
41756
  }
41757
+ /**
41758
+ * This event is fired when the playback range changed.
41759
+ *
41760
+ * @eventProperty
41761
+ * @category Events - Player
41762
+ * @since 1.2.3
41763
+ *
41764
+ * @example
41765
+ * JavaScript
41766
+ * ```js
41767
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41768
+ * api.playbackRangeChanged.on((args) => {
41769
+ * if (args.playbackRange) {
41770
+ * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
41771
+ * } else {
41772
+ * clearHighlightInProgressBar();
41773
+ * }
41774
+ * });
41775
+ * ```
41776
+ *
41777
+ * @example
41778
+ * C#
41779
+ * ```cs
41780
+ * var api = new AlphaTabApi<MyControl>(...);
41781
+ * api.PlaybackRangeChanged.On(args =>
41782
+ * {
41783
+ * if (args.PlaybackRange != null)
41784
+ * {
41785
+ * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
41786
+ * }
41787
+ * else
41788
+ * {
41789
+ * ClearHighlightInProgressBar();
41790
+ * }
41791
+ * });
41792
+ * ```
41793
+ *
41794
+ * @example
41795
+ * Android
41796
+ * ```kotlin
41797
+ * val api = AlphaTabApi<MyControl>(...)
41798
+ * api.playbackRangeChanged.on { args ->
41799
+ * val playbackRange = args.playbackRange
41800
+ * if (playbackRange != null) {
41801
+ * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
41802
+ * } else {
41803
+ * clearHighlightInProgressBar()
41804
+ * }
41805
+ * }
41806
+ * ```
41807
+ *
41808
+ */
41809
+ get playbackRangeChanged() {
41810
+ return this._player.playbackRangeChanged;
41811
+ }
41603
41812
  onPlaybackRangeChanged(e) {
41604
41813
  if (this._isDestroyed) {
41605
41814
  return;
41606
41815
  }
41607
- this.playbackRangeChanged.trigger(e);
41608
41816
  this.uiFacade.triggerEvent(this.container, 'playbackRangeChanged', e);
41609
41817
  }
41610
41818
  onSettingsUpdated() {
@@ -41666,10 +41874,7 @@ class AlphaTabApiBase {
41666
41874
  * ```
41667
41875
  */
41668
41876
  async enumerateOutputDevices() {
41669
- if (this.player) {
41670
- return await this.player.output.enumerateOutputDevices();
41671
- }
41672
- return [];
41877
+ return await this._player.output.enumerateOutputDevices();
41673
41878
  }
41674
41879
  /**
41675
41880
  * Changes the output device which should be used for playing the audio (player must be enabled).
@@ -41720,9 +41925,7 @@ class AlphaTabApiBase {
41720
41925
  * ```
41721
41926
  */
41722
41927
  async setOutputDevice(device) {
41723
- if (this.player) {
41724
- await this.player.output.setOutputDevice(device);
41725
- }
41928
+ await this._player.output.setOutputDevice(device);
41726
41929
  }
41727
41930
  /**
41728
41931
  * The currently configured output device if changed via {@link setOutputDevice}.
@@ -41762,10 +41965,7 @@ class AlphaTabApiBase {
41762
41965
  *
41763
41966
  */
41764
41967
  async getOutputDevice() {
41765
- if (this.player) {
41766
- return await this.player.output.getOutputDevice();
41767
- }
41768
- return null;
41968
+ return await this._player.output.getOutputDevice();
41769
41969
  }
41770
41970
  }
41771
41971
 
@@ -41942,38 +42142,52 @@ class HtmlElementContainer {
41942
42142
  this.element = element;
41943
42143
  this.mouseDown = {
41944
42144
  on: (value) => {
41945
- this.element.addEventListener('mousedown', e => {
42145
+ const nativeListener = e => {
41946
42146
  value(new BrowserMouseEventArgs(e));
41947
- }, true);
42147
+ };
42148
+ this.element.addEventListener('mousedown', nativeListener, true);
42149
+ return () => {
42150
+ this.element.removeEventListener('mousedown', nativeListener, true);
42151
+ };
41948
42152
  },
41949
42153
  off: (value) => {
41950
42154
  }
41951
42155
  };
41952
42156
  this.mouseUp = {
41953
42157
  on: (value) => {
41954
- this.element.addEventListener('mouseup', e => {
42158
+ const nativeListener = e => {
41955
42159
  value(new BrowserMouseEventArgs(e));
41956
- }, true);
42160
+ };
42161
+ this.element.addEventListener('mouseup', nativeListener, true);
42162
+ return () => {
42163
+ this.element.removeEventListener('mouseup', nativeListener, true);
42164
+ };
41957
42165
  },
41958
42166
  off: (value) => {
41959
42167
  }
41960
42168
  };
41961
42169
  this.mouseMove = {
41962
42170
  on: (value) => {
41963
- this.element.addEventListener('mousemove', e => {
42171
+ const nativeListener = e => {
41964
42172
  value(new BrowserMouseEventArgs(e));
41965
- }, true);
42173
+ };
42174
+ this.element.addEventListener('mousemove', nativeListener, true);
42175
+ return () => {
42176
+ this.element.removeEventListener('mousemove', nativeListener, true);
42177
+ };
41966
42178
  },
41967
42179
  off: (_) => {
41968
42180
  }
41969
42181
  };
42182
+ const container = this;
41970
42183
  this.resize = {
41971
- on: (value) => {
41972
- if (this._resizeListeners === 0) {
41973
- HtmlElementContainer.resizeObserver.value.observe(this.element);
42184
+ on: function (value) {
42185
+ if (container._resizeListeners === 0) {
42186
+ HtmlElementContainer.resizeObserver.value.observe(container.element);
41974
42187
  }
41975
- this.element.addEventListener('resize', value, true);
41976
- this._resizeListeners++;
42188
+ container.element.addEventListener('resize', value, true);
42189
+ container._resizeListeners++;
42190
+ return () => this.off(value);
41977
42191
  },
41978
42192
  off: (value) => {
41979
42193
  this.element.removeEventListener('resize', value, true);
@@ -42554,21 +42768,6 @@ class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBase {
42554
42768
  }
42555
42769
  }
42556
42770
 
42557
- /**
42558
- * Represents the progress of any data being loaded.
42559
- */
42560
- class ProgressEventArgs {
42561
- /**
42562
- * Initializes a new instance of the {@link ProgressEventArgs} class.
42563
- * @param loaded
42564
- * @param total
42565
- */
42566
- constructor(loaded, total) {
42567
- this.loaded = loaded;
42568
- this.total = total;
42569
- }
42570
- }
42571
-
42572
42771
  /**
42573
42772
  * a WebWorker based alphaSynth which uses the given player as output.
42574
42773
  * @target web
@@ -42805,25 +43004,6 @@ class AlphaSynthWebWorkerApi {
42805
43004
  append: append
42806
43005
  });
42807
43006
  }
42808
- loadSoundFontFromUrl(url, append, progress) {
42809
- Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
42810
- const request = new XMLHttpRequest();
42811
- request.open('GET', url, true, null, null);
42812
- request.responseType = 'arraybuffer';
42813
- request.onload = _ => {
42814
- const buffer = new Uint8Array(request.response);
42815
- this.loadSoundFont(buffer, append);
42816
- };
42817
- request.onerror = e => {
42818
- Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
42819
- this.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
42820
- };
42821
- request.onprogress = e => {
42822
- Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
42823
- progress(new ProgressEventArgs(e.loaded, e.total));
42824
- };
42825
- request.send();
42826
- }
42827
43007
  resetSoundFonts() {
42828
43008
  this._synth.postMessage({
42829
43009
  cmd: 'alphaSynth.resetSoundFonts'
@@ -43375,7 +43555,11 @@ class AudioElementBackingTrackSynthOutput {
43375
43555
  }
43376
43556
  this._padding = backingTrack.padding / 1000;
43377
43557
  const blob = new Blob([backingTrack.rawAudioFile]);
43558
+ // https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource
43559
+ // Step 8. resets the playbackRate, we need to remember and restore it.
43560
+ const playbackRate = this.audioElement.playbackRate;
43378
43561
  this.audioElement.src = URL.createObjectURL(blob);
43562
+ this.audioElement.playbackRate = playbackRate;
43379
43563
  }
43380
43564
  open(_bufferTimeInMilliseconds) {
43381
43565
  const audioElement = document.createElement('audio');
@@ -44094,6 +44278,21 @@ class BrowserUiFacade {
44094
44278
  }
44095
44279
  }
44096
44280
 
44281
+ /**
44282
+ * Represents the progress of any data being loaded.
44283
+ */
44284
+ class ProgressEventArgs {
44285
+ /**
44286
+ * Initializes a new instance of the {@link ProgressEventArgs} class.
44287
+ * @param loaded
44288
+ * @param total
44289
+ */
44290
+ constructor(loaded, total) {
44291
+ this.loaded = loaded;
44292
+ this.total = total;
44293
+ }
44294
+ }
44295
+
44097
44296
  /**
44098
44297
  * @target web
44099
44298
  */
@@ -44337,13 +44536,29 @@ class AlphaTabApi extends AlphaTabApiBase {
44337
44536
  * @since 0.9.4
44338
44537
  */
44339
44538
  loadSoundFontFromUrl(url, append) {
44340
- if (!this.player) {
44539
+ const player = this.player;
44540
+ if (!player) {
44341
44541
  return;
44342
44542
  }
44343
- this.player.loadSoundFontFromUrl(url, append, e => {
44344
- this.soundFontLoad.trigger(e);
44345
- this.uiFacade.triggerEvent(this.container, 'soundFontLoad', e);
44346
- });
44543
+ Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
44544
+ const request = new XMLHttpRequest();
44545
+ request.open('GET', url, true, null, null);
44546
+ request.responseType = 'arraybuffer';
44547
+ request.onload = _ => {
44548
+ const buffer = new Uint8Array(request.response);
44549
+ this.loadSoundFont(buffer, append);
44550
+ };
44551
+ request.onerror = e => {
44552
+ Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
44553
+ player.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
44554
+ };
44555
+ request.onprogress = e => {
44556
+ Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
44557
+ const args = new ProgressEventArgs(e.loaded, e.total);
44558
+ this.soundFontLoad.trigger(args);
44559
+ this.uiFacade.triggerEvent(this.container, 'soundFontLoad', args);
44560
+ };
44561
+ request.send();
44347
44562
  }
44348
44563
  }
44349
44564
 
@@ -60310,9 +60525,9 @@ class VersionInfo {
60310
60525
  print(`build date: ${VersionInfo.date}`);
60311
60526
  }
60312
60527
  }
60313
- VersionInfo.version = '1.6.0-alpha.1405';
60314
- VersionInfo.date = '2025-05-10T17:25:30.743Z';
60315
- VersionInfo.commit = 'a9f729a65e195d4fec684444cd2c2a259dc9729b';
60528
+ VersionInfo.version = '1.6.0-alpha.1409';
60529
+ VersionInfo.date = '2025-05-14T02:06:32.990Z';
60530
+ VersionInfo.commit = 'f91fed13b0a0946b3fa9306b480ddf8044b948e2';
60316
60531
 
60317
60532
  /**
60318
60533
  * A factory for custom layout engines.