@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -22403,6 +22403,11 @@
22403
22403
  this._mainState.eventIndex = 0;
22404
22404
  this._mainState.syncPointIndex = 0;
22405
22405
  this._mainState.tempoChangeIndex = 0;
22406
+ this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22407
+ this._mainState.modifiedTempo =
22408
+ this._mainState.syncPoints.length > 0
22409
+ ? this._mainState.syncPoints[0].data.modifiedTempo
22410
+ : this._mainState.currentTempo;
22406
22411
  if (this.isPlayingMain) {
22407
22412
  const metronomeVolume = this._synthesizer.metronomeVolume;
22408
22413
  this._synthesizer.noteOffAll(true);
@@ -22541,18 +22546,6 @@
22541
22546
  this._currentState.synthData[this._currentState.eventIndex].time < this._currentState.currentTime) {
22542
22547
  const synthEvent = this._currentState.synthData[this._currentState.eventIndex];
22543
22548
  this._synthesizer.dispatchEvent(synthEvent);
22544
- while (this._currentState.syncPointIndex < this._currentState.syncPoints.length &&
22545
- this._currentState.syncPoints[this._currentState.syncPointIndex].tick < synthEvent.event.tick) {
22546
- this._currentState.modifiedTempo =
22547
- this._currentState.syncPoints[this._currentState.syncPointIndex].data.modifiedTempo;
22548
- this._currentState.syncPointIndex++;
22549
- }
22550
- while (this._currentState.tempoChangeIndex < this._currentState.tempoChanges.length &&
22551
- this._currentState.tempoChanges[this._currentState.tempoChangeIndex].time <= synthEvent.time) {
22552
- this._currentState.currentTempo =
22553
- this._currentState.tempoChanges[this._currentState.tempoChangeIndex].bpm;
22554
- this._currentState.tempoChangeIndex++;
22555
- }
22556
22549
  this._currentState.eventIndex++;
22557
22550
  anyEventsDispatched = true;
22558
22551
  }
@@ -22582,9 +22575,6 @@
22582
22575
  mainTickPositionToTimePosition(tickPosition) {
22583
22576
  return this.tickPositionToTimePositionWithSpeed(this._mainState, tickPosition, this.playbackSpeed);
22584
22577
  }
22585
- mainTimePositionToTickPosition(timePosition) {
22586
- return this.timePositionToTickPositionWithSpeed(this._mainState, timePosition, this.playbackSpeed);
22587
- }
22588
22578
  mainUpdateSyncPoints(syncPoints) {
22589
22579
  const state = this._mainState;
22590
22580
  syncPoints.sort((a, b) => a.tick - b.tick); // just in case
@@ -22612,7 +22602,49 @@
22612
22602
  state.syncPointIndex = 0;
22613
22603
  }
22614
22604
  currentTimePositionToTickPosition(timePosition) {
22615
- return this.timePositionToTickPositionWithSpeed(this._currentState, timePosition, this.playbackSpeed);
22605
+ const state = this._currentState;
22606
+ if (state.tempoChanges.length === 0) {
22607
+ return 0;
22608
+ }
22609
+ timePosition *= this.playbackSpeed;
22610
+ this.updateCurrentTempo(state, timePosition);
22611
+ const lastTempoChange = state.tempoChanges[state.tempoChangeIndex];
22612
+ const timeDiff = timePosition - lastTempoChange.time;
22613
+ const ticks = ((timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0);
22614
+ // we add 1 for possible rounding errors.(floating point issuses)
22615
+ return lastTempoChange.ticks + ticks + 1;
22616
+ }
22617
+ updateCurrentTempo(state, timePosition) {
22618
+ let tempoChangeIndex = state.tempoChangeIndex;
22619
+ if (timePosition < state.tempoChanges[tempoChangeIndex].time) {
22620
+ tempoChangeIndex = 0;
22621
+ }
22622
+ while (tempoChangeIndex + 1 < state.tempoChanges.length &&
22623
+ state.tempoChanges[tempoChangeIndex + 1].time <= timePosition) {
22624
+ tempoChangeIndex++;
22625
+ }
22626
+ if (tempoChangeIndex !== state.tempoChangeIndex) {
22627
+ state.tempoChangeIndex = tempoChangeIndex;
22628
+ state.currentTempo = state.tempoChanges[state.tempoChangeIndex].bpm;
22629
+ }
22630
+ const syncPoints = state.syncPoints;
22631
+ if (syncPoints.length > 0) {
22632
+ let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22633
+ if (timePosition < syncPoints[syncPointIndex].data.millisecondOffset) {
22634
+ syncPointIndex = 0;
22635
+ }
22636
+ while (syncPointIndex + 1 < syncPoints.length &&
22637
+ syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22638
+ syncPointIndex++;
22639
+ }
22640
+ if (syncPointIndex !== state.syncPointIndex) {
22641
+ state.syncPointIndex = syncPointIndex;
22642
+ state.modifiedTempo = syncPoints[syncPointIndex].data.modifiedTempo;
22643
+ }
22644
+ }
22645
+ else {
22646
+ state.modifiedTempo = state.currentTempo;
22647
+ }
22616
22648
  }
22617
22649
  mainTimePositionFromBackingTrack(timePosition, backingTrackLength) {
22618
22650
  const mainState = this._mainState;
@@ -22620,11 +22652,8 @@
22620
22652
  if (timePosition < 0 || syncPoints.length === 0) {
22621
22653
  return timePosition;
22622
22654
  }
22623
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].data.millisecondOffset ? mainState.syncPointIndex : 0;
22624
- while (syncPointIndex + 1 < syncPoints.length &&
22625
- syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22626
- syncPointIndex++;
22627
- }
22655
+ this.updateCurrentTempo(this._mainState, timePosition);
22656
+ const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22628
22657
  const currentSyncPoint = syncPoints[syncPointIndex];
22629
22658
  const timeDiff = timePosition - currentSyncPoint.data.millisecondOffset;
22630
22659
  let alphaTabTimeDiff;
@@ -22646,7 +22675,10 @@
22646
22675
  return timePosition;
22647
22676
  }
22648
22677
  timePosition *= this.playbackSpeed;
22649
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].time ? mainState.syncPointIndex : 0;
22678
+ let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22679
+ if (timePosition < syncPoints[syncPointIndex].time) {
22680
+ syncPointIndex = 0;
22681
+ }
22650
22682
  while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].time <= timePosition) {
22651
22683
  syncPointIndex++;
22652
22684
  }
@@ -22684,26 +22716,6 @@
22684
22716
  timePosition += tickPosition * (60000.0 / (bpm * state.division));
22685
22717
  return timePosition / playbackSpeed;
22686
22718
  }
22687
- timePositionToTickPositionWithSpeed(state, timePosition, playbackSpeed) {
22688
- timePosition *= playbackSpeed;
22689
- let ticks = 0;
22690
- let bpm = 120.0;
22691
- let lastChange = 0;
22692
- // find start and bpm of last tempo change before time
22693
- for (const c of state.tempoChanges) {
22694
- if (timePosition < c.time) {
22695
- break;
22696
- }
22697
- ticks = c.ticks;
22698
- bpm = c.bpm;
22699
- lastChange = c.time;
22700
- }
22701
- // add the missing ticks
22702
- timePosition -= lastChange;
22703
- ticks += (timePosition / (60000.0 / (bpm * state.division))) | 0;
22704
- // we add 1 for possible rounding errors.(floating point issuses)
22705
- return ticks + 1;
22706
- }
22707
22719
  get internalEndTime() {
22708
22720
  if (this.isPlayingMain) {
22709
22721
  return !this.mainPlaybackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime;
@@ -26922,7 +26934,7 @@
26922
26934
  return processedEvents;
26923
26935
  }
26924
26936
  processMidiMessage(e) {
26925
- Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26937
+ //Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26926
26938
  const command = e.type;
26927
26939
  switch (command) {
26928
26940
  case MidiEventType.TimeSignature:
@@ -28043,6 +28055,9 @@
28043
28055
  }
28044
28056
  on(value) {
28045
28057
  this._listeners.push(value);
28058
+ return () => {
28059
+ this.off(value);
28060
+ };
28046
28061
  }
28047
28062
  off(value) {
28048
28063
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28062,6 +28077,9 @@
28062
28077
  }
28063
28078
  on(value) {
28064
28079
  this._listeners.push(value);
28080
+ return () => {
28081
+ this.off(value);
28082
+ };
28065
28083
  }
28066
28084
  off(value) {
28067
28085
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28458,7 +28476,7 @@
28458
28476
  endTick = this.sequencer.currentEndTick;
28459
28477
  }
28460
28478
  if (this._tickPosition >= endTick) {
28461
- // fully done with playback of remaining samples?
28479
+ // fully done with playback of remaining samples?
28462
28480
  if (this._notPlayedSamples <= 0) {
28463
28481
  this._notPlayedSamples = 0;
28464
28482
  if (this.sequencer.isPlayingCountIn) {
@@ -35808,6 +35826,27 @@
35808
35826
  controller.moveNext();
35809
35827
  previousMasterBar = bar;
35810
35828
  }
35829
+ // here we interpolate the sync point which marks the end of the sync.
35830
+ // Sync points define new tempos at certain positions.
35831
+ // looking from the last sync point to the end we do not assume the end where the audio ends,
35832
+ // but where it ends according to the BPM and the remaining ticks.
35833
+ if (this.syncPoints.length > 0) {
35834
+ const lastSyncPoint = this.syncPoints[this.syncPoints.length - 1];
35835
+ const endTick = controller.currentTick;
35836
+ const remainingTicks = endTick - lastSyncPoint.tick;
35837
+ if (remainingTicks > 0) {
35838
+ const syncPointData = new SyncPointData();
35839
+ // last occurence of the last bar
35840
+ syncPointData.barOccurence = barOccurence.get(this._score.masterBars.length - 1);
35841
+ // same tempo as last point
35842
+ syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35843
+ // interpolated end from last syncPoint
35844
+ syncPointData.millisecondOffset =
35845
+ lastSyncPoint.data.millisecondOffset +
35846
+ MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35847
+ this.syncPoints.push(new BackingTrackSyncPoint(endTick, syncPointData));
35848
+ }
35849
+ }
35811
35850
  for (const track of this._score.tracks) {
35812
35851
  this._handler.finishTrack(track.index, controller.currentTick);
35813
35852
  }
@@ -38299,6 +38338,270 @@
38299
38338
  }
38300
38339
  }
38301
38340
 
38341
+ /**
38342
+ * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
38343
+ * allowing dynamic changing of the underlying instance without loosing aspects like the
38344
+ * main playback information and event listeners.
38345
+ *
38346
+ * @remarks
38347
+ * This wrapper is used when re-exposing the underlying player via {@link AlphaTabApiBase} to integrators.
38348
+ * Even with dynamic switching between synthesizer, backing tracks etc. aspects like volume, playbackspeed,
38349
+ * event listeners etc. should not be lost.
38350
+ */
38351
+ class AlphaSynthWrapper {
38352
+ constructor() {
38353
+ // relevant state information we want to remember when switching between player instances
38354
+ this._masterVolume = 1;
38355
+ this._metronomeVolume = 0;
38356
+ this._countInVolume = 0;
38357
+ this._playbackSpeed = 1;
38358
+ this._isLooping = false;
38359
+ this._midiEventsPlayedFilter = [];
38360
+ this.ready = new EventEmitter();
38361
+ this.readyForPlayback = new EventEmitter();
38362
+ this.finished = new EventEmitter();
38363
+ this.soundFontLoaded = new EventEmitter();
38364
+ this.soundFontLoadFailed = new EventEmitterOfT();
38365
+ this.midiLoaded = new EventEmitterOfT();
38366
+ this.midiLoadFailed = new EventEmitterOfT();
38367
+ this.stateChanged = new EventEmitterOfT();
38368
+ this.positionChanged = new EventEmitterOfT();
38369
+ this.midiEventsPlayed = new EventEmitterOfT();
38370
+ this.playbackRangeChanged = new EventEmitterOfT();
38371
+ }
38372
+ get instance() {
38373
+ return this._instance;
38374
+ }
38375
+ set instance(value) {
38376
+ this._instance = value;
38377
+ // unregister all events from previous instance
38378
+ const unregister = this._instanceEventUnregister;
38379
+ if (unregister) {
38380
+ for (const e of unregister) {
38381
+ e();
38382
+ }
38383
+ }
38384
+ if (value) {
38385
+ // regsiter to events of new player and forward them to existing listeners
38386
+ const newUnregister = [];
38387
+ newUnregister.push(value.ready.on(() => this.ready.trigger()));
38388
+ newUnregister.push(value.readyForPlayback.on(() => this.readyForPlayback.trigger()));
38389
+ newUnregister.push(value.finished.on(() => this.finished.trigger()));
38390
+ newUnregister.push(value.soundFontLoaded.on(() => this.soundFontLoaded.trigger()));
38391
+ newUnregister.push(value.soundFontLoadFailed.on(e => this.soundFontLoadFailed.trigger(e)));
38392
+ newUnregister.push(value.midiLoaded.on(e => this.midiLoaded.trigger(e)));
38393
+ newUnregister.push(value.midiLoadFailed.on(e => this.midiLoadFailed.trigger(e)));
38394
+ newUnregister.push(value.stateChanged.on(e => this.stateChanged.trigger(e)));
38395
+ newUnregister.push(value.positionChanged.on(e => this.positionChanged.trigger(e)));
38396
+ newUnregister.push(value.midiEventsPlayed.on(e => this.midiEventsPlayed.trigger(e)));
38397
+ newUnregister.push(value.playbackRangeChanged.on(e => this.playbackRangeChanged.trigger(e)));
38398
+ this._instanceEventUnregister = newUnregister;
38399
+ // restore state on new player
38400
+ if (this.isReady) {
38401
+ value.masterVolume = this._masterVolume;
38402
+ value.metronomeVolume = this._metronomeVolume;
38403
+ value.countInVolume = this._countInVolume;
38404
+ value.playbackSpeed = this._playbackSpeed;
38405
+ value.isLooping = this._isLooping;
38406
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38407
+ }
38408
+ else {
38409
+ newUnregister.push(value.ready.on(() => {
38410
+ value.masterVolume = this._masterVolume;
38411
+ value.metronomeVolume = this._metronomeVolume;
38412
+ value.countInVolume = this._countInVolume;
38413
+ value.playbackSpeed = this._playbackSpeed;
38414
+ value.isLooping = this._isLooping;
38415
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38416
+ }));
38417
+ }
38418
+ }
38419
+ else {
38420
+ this._instanceEventUnregister = undefined;
38421
+ }
38422
+ }
38423
+ get output() {
38424
+ return this._instance.output;
38425
+ }
38426
+ get isReady() {
38427
+ return this._instance ? this._instance.isReady : false;
38428
+ }
38429
+ get isReadyForPlayback() {
38430
+ return this._instance ? this._instance.isReadyForPlayback : false;
38431
+ }
38432
+ get state() {
38433
+ return this._instance ? this._instance.state : PlayerState.Paused;
38434
+ }
38435
+ get logLevel() {
38436
+ return Logger.logLevel;
38437
+ }
38438
+ set logLevel(value) {
38439
+ Logger.logLevel = value;
38440
+ if (this._instance) {
38441
+ this._instance.logLevel = value;
38442
+ }
38443
+ }
38444
+ get masterVolume() {
38445
+ return this._masterVolume;
38446
+ }
38447
+ set masterVolume(value) {
38448
+ value = Math.max(value, SynthConstants.MinVolume);
38449
+ this._masterVolume = value;
38450
+ if (this._instance) {
38451
+ this._instance.masterVolume = value;
38452
+ }
38453
+ }
38454
+ get metronomeVolume() {
38455
+ return this._metronomeVolume;
38456
+ }
38457
+ set metronomeVolume(value) {
38458
+ value = Math.max(value, SynthConstants.MinVolume);
38459
+ this._metronomeVolume = value;
38460
+ if (this._instance) {
38461
+ this._instance.metronomeVolume = value;
38462
+ }
38463
+ }
38464
+ get playbackSpeed() {
38465
+ return this._playbackSpeed;
38466
+ }
38467
+ set playbackSpeed(value) {
38468
+ this._playbackSpeed = value;
38469
+ if (this._instance) {
38470
+ this._instance.playbackSpeed = value;
38471
+ }
38472
+ }
38473
+ get tickPosition() {
38474
+ return this._instance ? this._instance.tickPosition : 0;
38475
+ }
38476
+ set tickPosition(value) {
38477
+ if (this._instance) {
38478
+ this._instance.tickPosition = value;
38479
+ }
38480
+ }
38481
+ get timePosition() {
38482
+ return this._instance ? this._instance.timePosition : 0;
38483
+ }
38484
+ set timePosition(value) {
38485
+ if (this._instance) {
38486
+ this._instance.timePosition = value;
38487
+ }
38488
+ }
38489
+ get playbackRange() {
38490
+ return this._instance ? this._instance.playbackRange : null;
38491
+ }
38492
+ set playbackRange(value) {
38493
+ if (this._instance) {
38494
+ this._instance.playbackRange = value;
38495
+ }
38496
+ }
38497
+ get isLooping() {
38498
+ return this._isLooping;
38499
+ }
38500
+ set isLooping(value) {
38501
+ this._isLooping = value;
38502
+ if (this._instance) {
38503
+ this._instance.isLooping = value;
38504
+ }
38505
+ }
38506
+ get countInVolume() {
38507
+ return this._countInVolume;
38508
+ }
38509
+ set countInVolume(value) {
38510
+ this._countInVolume = value;
38511
+ if (this._instance) {
38512
+ this._instance.countInVolume = value;
38513
+ }
38514
+ }
38515
+ get midiEventsPlayedFilter() {
38516
+ return this._midiEventsPlayedFilter;
38517
+ }
38518
+ set midiEventsPlayedFilter(value) {
38519
+ this._midiEventsPlayedFilter = value;
38520
+ if (this._instance) {
38521
+ this._instance.midiEventsPlayedFilter = value;
38522
+ }
38523
+ }
38524
+ destroy() {
38525
+ if (this._instance) {
38526
+ this._instance.destroy();
38527
+ this._instance = undefined;
38528
+ }
38529
+ }
38530
+ play() {
38531
+ return this._instance ? this._instance.play() : false;
38532
+ }
38533
+ pause() {
38534
+ if (this._instance) {
38535
+ this._instance.pause();
38536
+ }
38537
+ }
38538
+ playPause() {
38539
+ if (this._instance) {
38540
+ this._instance.playPause();
38541
+ }
38542
+ }
38543
+ stop() {
38544
+ if (this._instance) {
38545
+ this._instance.stop();
38546
+ }
38547
+ }
38548
+ playOneTimeMidiFile(midi) {
38549
+ if (this._instance) {
38550
+ this._instance.playOneTimeMidiFile(midi);
38551
+ }
38552
+ }
38553
+ loadSoundFont(data, append) {
38554
+ if (this._instance) {
38555
+ this._instance.loadSoundFont(data, append);
38556
+ }
38557
+ }
38558
+ resetSoundFonts() {
38559
+ if (this._instance) {
38560
+ this._instance.resetSoundFonts();
38561
+ }
38562
+ }
38563
+ loadMidiFile(midi) {
38564
+ if (this._instance) {
38565
+ this._instance.loadMidiFile(midi);
38566
+ }
38567
+ }
38568
+ loadBackingTrack(score, syncPoints) {
38569
+ if (this._instance) {
38570
+ this._instance.loadBackingTrack(score, syncPoints);
38571
+ }
38572
+ }
38573
+ applyTranspositionPitches(transpositionPitches) {
38574
+ if (this._instance) {
38575
+ this._instance.applyTranspositionPitches(transpositionPitches);
38576
+ }
38577
+ }
38578
+ setChannelTranspositionPitch(channel, semitones) {
38579
+ if (this._instance) {
38580
+ this._instance.setChannelTranspositionPitch(channel, semitones);
38581
+ }
38582
+ }
38583
+ setChannelMute(channel, mute) {
38584
+ if (this._instance) {
38585
+ this._instance.setChannelMute(channel, mute);
38586
+ }
38587
+ }
38588
+ resetChannelStates() {
38589
+ if (this._instance) {
38590
+ this._instance.resetChannelStates();
38591
+ }
38592
+ }
38593
+ setChannelSolo(channel, solo) {
38594
+ if (this._instance) {
38595
+ this._instance.setChannelSolo(channel, solo);
38596
+ }
38597
+ }
38598
+ setChannelVolume(channel, volume) {
38599
+ if (this._instance) {
38600
+ this._instance.setChannelVolume(channel, volume);
38601
+ }
38602
+ }
38603
+ }
38604
+
38302
38605
  class SelectionInfo {
38303
38606
  constructor(beat) {
38304
38607
  this.bounds = null;
@@ -38389,43 +38692,13 @@
38389
38692
  this._tracks = [];
38390
38693
  this._actualPlayerMode = exports.PlayerMode.Disabled;
38391
38694
  this._tickCache = null;
38392
- /**
38393
- * The alphaSynth player used for playback.
38394
- * @remarks
38395
- * This is the low-level API to the Midi synthesizer used for playback.
38396
- * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
38397
- * @category Properties - Player
38398
- * @since 0.9.4
38399
- * @example
38400
- * JavaScript
38401
- * ```js
38402
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
38403
- * setupPlayerEvents(api.settings);
38404
- * ```
38405
- *
38406
- * @example
38407
- * C#
38408
- * ```cs
38409
- * var api = new AlphaTabApi<MyControl>(...);
38410
- * SetupPlayerEvents(api.Player);
38411
- * ```
38412
- *
38413
- * @example
38414
- * Android
38415
- * ```kotlin
38416
- * val api = AlphaTabApi<MyControl>(...)
38417
- * setupPlayerEvents(api.player)
38418
- * ```
38419
- */
38420
- this.player = null;
38421
38695
  this._cursorWrapper = null;
38422
38696
  this._barCursor = null;
38423
38697
  this._beatCursor = null;
38424
38698
  this._selectionWrapper = null;
38425
38699
  this._previousTick = 0;
38426
- this._playerState = PlayerState.Paused;
38427
38700
  this._currentBeat = null;
38428
- this._currentBarBounds = null;
38701
+ this._currentBeatBounds = null;
38429
38702
  this._previousStateForCursor = PlayerState.Paused;
38430
38703
  this._previousCursorCache = null;
38431
38704
  this._lastScroll = 0;
@@ -38996,133 +39269,6 @@
38996
39269
  *
38997
39270
  */
38998
39271
  this.error = new EventEmitterOfT();
38999
- /**
39000
- * This event is fired when all required data for playback is loaded and ready.
39001
- * @remarks
39002
- * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
39003
- * 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.
39004
- *
39005
- * @eventProperty
39006
- * @category Events - Player
39007
- * @since 0.9.4
39008
- *
39009
- * @example
39010
- * JavaScript
39011
- * ```js
39012
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39013
- * api.playerReady.on(() => {
39014
- * enablePlayerControls();
39015
- * });
39016
- * ```
39017
- *
39018
- * @example
39019
- * C#
39020
- * ```cs
39021
- * var api = new AlphaTabApi<MyControl>(...);
39022
- * api.PlayerReady.On(() =>
39023
- * {
39024
- * EnablePlayerControls()
39025
- * });
39026
- * ```
39027
- *
39028
- * @example
39029
- * Android
39030
- * ```kotlin
39031
- * val api = AlphaTabApi<MyControl>(...)
39032
- * api.playerReady.on {
39033
- * enablePlayerControls()
39034
- * }
39035
- * ```
39036
- */
39037
- this.playerReady = new EventEmitter();
39038
- /**
39039
- * This event is fired when the playback of the whole song finished.
39040
- * @remarks
39041
- * This event is finished regardless on whether looping is enabled or not.
39042
- *
39043
- * @eventProperty
39044
- * @category Events - Player
39045
- * @since 0.9.4
39046
- *
39047
- * @example
39048
- * JavaScript
39049
- * ```js
39050
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39051
- * api.playerFinished.on((args) => {
39052
- * // speed trainer
39053
- * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
39054
- * });
39055
- * api.isLooping = true;
39056
- * api.playbackSpeed = 0.5;
39057
- * api.play()
39058
- * ```
39059
- *
39060
- * @example
39061
- * C#
39062
- * ```cs
39063
- * var api = new AlphaTabApi<MyControl>(...);
39064
- * api.PlayerFinished.On(() =>
39065
- * {
39066
- * // speed trainer
39067
- * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
39068
- * });
39069
- * api.IsLooping = true;
39070
- * api.PlaybackSpeed = 0.5;
39071
- * api.Play();
39072
- * ```
39073
- *
39074
- * @example
39075
- * Android
39076
- * ```kotlin
39077
- * val api = AlphaTabApi<MyControl>(...)
39078
- * api.playerFinished.on {
39079
- * // speed trainer
39080
- * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
39081
- * }
39082
- * api.isLooping = true
39083
- * api.playbackSpeed = 0.5
39084
- * api.play()
39085
- * ```
39086
- *
39087
- */
39088
- this.playerFinished = new EventEmitter();
39089
- /**
39090
- * This event is fired when the SoundFont needed for playback was loaded.
39091
- *
39092
- * @eventProperty
39093
- * @category Events - Player
39094
- * @since 0.9.4
39095
- *
39096
- * @example
39097
- * JavaScript
39098
- * ```js
39099
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39100
- * api.soundFontLoaded.on(() => {
39101
- * hideSoundFontLoadingIndicator();
39102
- * });
39103
- * ```
39104
- *
39105
- * @example
39106
- * C#
39107
- * ```cs
39108
- * var api = new AlphaTabApi<MyControl>(...);
39109
- * api.SoundFontLoaded.On(() =>
39110
- * {
39111
- * HideSoundFontLoadingIndicator();
39112
- * });
39113
- * ```
39114
- *
39115
- * @example
39116
- * Android
39117
- * ```kotlin
39118
- * val api = AlphaTabApi<MyControl>(...);
39119
- * api.soundFontLoaded.on {
39120
- * hideSoundFontLoadingIndicator();
39121
- * }
39122
- * ```
39123
- *
39124
- */
39125
- this.soundFontLoaded = new EventEmitter();
39126
39272
  /**
39127
39273
  * This event is fired when a Midi file is being loaded.
39128
39274
  *
@@ -39210,213 +39356,6 @@
39210
39356
  *
39211
39357
  */
39212
39358
  this.midiLoaded = new EventEmitterOfT();
39213
- /**
39214
- * This event is fired when the playback state changed.
39215
- *
39216
- * @eventProperty
39217
- * @category Events - Player
39218
- * @since 0.9.4
39219
- *
39220
- * @example
39221
- * JavaScript
39222
- * ```js
39223
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39224
- * api.playerStateChanged.on((args) => {
39225
- * updatePlayerControls(args.state, args.stopped);
39226
- * });
39227
- * ```
39228
- *
39229
- * @example
39230
- * C#
39231
- * ```cs
39232
- * var api = new AlphaTabApi<MyControl>(...);
39233
- * api.PlayerStateChanged.On(args =>
39234
- * {
39235
- * UpdatePlayerControls(args);
39236
- * });
39237
- * ```
39238
- *
39239
- * @example
39240
- * Android
39241
- * ```kotlin
39242
- * val api = AlphaTabApi<MyControl>(...)
39243
- * api.playerStateChanged.on { args ->
39244
- * updatePlayerControls(args)
39245
- * }
39246
- * ```
39247
- *
39248
- */
39249
- this.playerStateChanged = new EventEmitterOfT();
39250
- /**
39251
- * This event is fired when the current playback position of the song changed.
39252
- *
39253
- * @eventProperty
39254
- * @category Events - Player
39255
- * @since 0.9.4
39256
- *
39257
- * @example
39258
- * JavaScript
39259
- * ```js
39260
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39261
- * api.playerPositionChanged.on((args) => {
39262
- * updatePlayerPosition(args);
39263
- * });
39264
- * ```
39265
- *
39266
- * @example
39267
- * C#
39268
- * ```cs
39269
- * var api = new AlphaTabApi<MyControl>(...);
39270
- * api.PlayerPositionChanged.On(args =>
39271
- * {
39272
- * UpdatePlayerPosition(args);
39273
- * });
39274
- * ```
39275
- *
39276
- * @example
39277
- * Android
39278
- * ```kotlin
39279
- * val api = AlphaTabApi<MyControl>(...)
39280
- * api.playerPositionChanged.on { args ->
39281
- * updatePlayerPosition(args)
39282
- * }
39283
- * ```
39284
- *
39285
- */
39286
- this.playerPositionChanged = new EventEmitterOfT();
39287
- /**
39288
- * This event is fired when the synthesizer played certain midi events.
39289
- *
39290
- * @remarks
39291
- * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
39292
- * audio playback elements like notes/rests played or metronome ticks.
39293
- *
39294
- * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
39295
- *
39296
- * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
39297
- * until some extend but highly encourage changing to the new models in case of problems.
39298
- *
39299
- * @eventProperty
39300
- * @category Events - Player
39301
- * @since 1.2.0
39302
- *
39303
- * @example
39304
- * JavaScript
39305
- * ```js
39306
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39307
- * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
39308
- * api.midiEventsPlayed.on(function(e) {
39309
- * for(const midi of e.events) {
39310
- * if(midi.isMetronome) {
39311
- * console.log('Metronome tick ' + midi.tick);
39312
- * }
39313
- * }
39314
- * });
39315
- * ```
39316
- *
39317
- * @example
39318
- * C#
39319
- * ```cs
39320
- * var api = new AlphaTabApi<MyControl>(...);
39321
- * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
39322
- * api.MidiEventsPlayed.On(e =>
39323
- * {
39324
- * foreach(var midi of e.events)
39325
- * {
39326
- * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
39327
- * {
39328
- * Console.WriteLine("Metronome tick " + midi.Tick);
39329
- * }
39330
- * }
39331
- * });
39332
- * ```
39333
- *
39334
- * @example
39335
- * Android
39336
- * ```kotlin
39337
- * val api = AlphaTabApi<MyControl>(...);
39338
- * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
39339
- * api.midiEventsPlayed.on { e ->
39340
- * for (midi in e.events) {
39341
- * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
39342
- * println("Metronome tick " + midi.tick);
39343
- * }
39344
- * }
39345
- * }
39346
- * ```
39347
- * @see {@link MidiEvent}
39348
- * @see {@link TimeSignatureEvent}
39349
- * @see {@link AlphaTabMetronomeEvent}
39350
- * @see {@link AlphaTabRestEvent}
39351
- * @see {@link NoteOnEvent}
39352
- * @see {@link NoteOffEvent}
39353
- * @see {@link ControlChangeEvent}
39354
- * @see {@link ProgramChangeEvent}
39355
- * @see {@link TempoChangeEvent}
39356
- * @see {@link PitchBendEvent}
39357
- * @see {@link NoteBendEvent}
39358
- * @see {@link EndOfTrackEvent}
39359
- * @see {@link MetaEvent}
39360
- * @see {@link MetaDataEvent}
39361
- * @see {@link MetaNumberEvent}
39362
- * @see {@link Midi20PerNotePitchBendEvent}
39363
- * @see {@link SystemCommonEvent}
39364
- * @see {@link SystemExclusiveEvent}
39365
- */
39366
- this.midiEventsPlayed = new EventEmitterOfT();
39367
- /**
39368
- * This event is fired when the playback range changed.
39369
- *
39370
- * @eventProperty
39371
- * @category Events - Player
39372
- * @since 1.2.3
39373
- *
39374
- * @example
39375
- * JavaScript
39376
- * ```js
39377
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39378
- * api.playbackRangeChanged.on((args) => {
39379
- * if (args.playbackRange) {
39380
- * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
39381
- * } else {
39382
- * clearHighlightInProgressBar();
39383
- * }
39384
- * });
39385
- * ```
39386
- *
39387
- * @example
39388
- * C#
39389
- * ```cs
39390
- * var api = new AlphaTabApi<MyControl>(...);
39391
- * api.PlaybackRangeChanged.On(args =>
39392
- * {
39393
- * if (args.PlaybackRange != null)
39394
- * {
39395
- * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
39396
- * }
39397
- * else
39398
- * {
39399
- * ClearHighlightInProgressBar();
39400
- * }
39401
- * });
39402
- * ```
39403
- *
39404
- * @example
39405
- * Android
39406
- * ```kotlin
39407
- * val api = AlphaTabApi<MyControl>(...)
39408
- * api.playbackRangeChanged.on { args ->
39409
- * val playbackRange = args.playbackRange
39410
- * if (playbackRange != null) {
39411
- * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
39412
- * } else {
39413
- * clearHighlightInProgressBar()
39414
- * }
39415
- * }
39416
- * ```
39417
- *
39418
- */
39419
- this.playbackRangeChanged = new EventEmitterOfT();
39420
39359
  /**
39421
39360
  * @internal
39422
39361
  */
@@ -39472,8 +39411,9 @@
39472
39411
  this.appendRenderResult(null); // marks last element
39473
39412
  });
39474
39413
  this.renderer.error.on(this.onError.bind(this));
39414
+ this.setupPlayerWrapper();
39475
39415
  if (this.settings.player.playerMode !== exports.PlayerMode.Disabled) {
39476
- this.setupPlayer();
39416
+ this.setupOrDestroyPlayer();
39477
39417
  }
39478
39418
  this.setupClickHandling();
39479
39419
  // delay rendering to allow ui to hook up with events first.
@@ -39481,6 +39421,36 @@
39481
39421
  this.uiFacade.initialRender();
39482
39422
  });
39483
39423
  }
39424
+ setupPlayerWrapper() {
39425
+ const player = new AlphaSynthWrapper();
39426
+ this._player = player;
39427
+ player.ready.on(() => {
39428
+ this.loadMidiForScore();
39429
+ });
39430
+ player.readyForPlayback.on(() => {
39431
+ this.onPlayerReady();
39432
+ if (this.tracks) {
39433
+ for (const track of this.tracks) {
39434
+ const volume = track.playbackInfo.volume / 16;
39435
+ player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
39436
+ player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
39437
+ }
39438
+ }
39439
+ });
39440
+ player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
39441
+ player.soundFontLoadFailed.on(e => {
39442
+ this.onError(e);
39443
+ });
39444
+ player.midiLoaded.on(this.onMidiLoaded.bind(this));
39445
+ player.midiLoadFailed.on(e => {
39446
+ this.onError(e);
39447
+ });
39448
+ player.stateChanged.on(this.onPlayerStateChanged.bind(this));
39449
+ player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
39450
+ player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
39451
+ player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
39452
+ player.finished.on(this.onPlayerFinished.bind(this));
39453
+ }
39484
39454
  /**
39485
39455
  * Destroys the alphaTab control and restores the initial state of the UI.
39486
39456
  * @remarks
@@ -39513,9 +39483,7 @@
39513
39483
  */
39514
39484
  destroy() {
39515
39485
  this._isDestroyed = true;
39516
- if (this.player) {
39517
- this.player.destroy();
39518
- }
39486
+ this._player.destroy();
39519
39487
  this.uiFacade.destroy();
39520
39488
  this.renderer.destroy();
39521
39489
  }
@@ -39563,14 +39531,8 @@
39563
39531
  ModelUtils.applyPitchOffsets(this.settings, score);
39564
39532
  }
39565
39533
  this.renderer.updateSettings(this.settings);
39566
- // enable/disable player if needed
39567
- if (this.settings.player.playerMode !== exports.PlayerMode.Disabled) {
39568
- if (this.setupPlayer() && score) {
39569
- this.loadMidiForScore();
39570
- }
39571
- }
39572
- else {
39573
- this.destroyPlayer();
39534
+ if (this.setupOrDestroyPlayer()) {
39535
+ this.loadMidiForScore();
39574
39536
  }
39575
39537
  this.onSettingsUpdated();
39576
39538
  }
@@ -39877,9 +39839,6 @@
39877
39839
  * ```
39878
39840
  */
39879
39841
  loadSoundFont(data, append = false) {
39880
- if (!this.player) {
39881
- return false;
39882
- }
39883
39842
  return this.uiFacade.loadSoundFont(data, append);
39884
39843
  }
39885
39844
  /**
@@ -39925,10 +39884,7 @@
39925
39884
  * ```
39926
39885
  */
39927
39886
  resetSoundFonts() {
39928
- if (!this.player) {
39929
- return;
39930
- }
39931
- this.player.resetSoundFonts();
39887
+ this._player.resetSoundFonts();
39932
39888
  }
39933
39889
  /**
39934
39890
  * Initiates a re-rendering of the current setup.
@@ -40042,6 +39998,37 @@
40042
39998
  get boundsLookup() {
40043
39999
  return this.renderer.boundsLookup;
40044
40000
  }
40001
+ /**
40002
+ * The alphaSynth player used for playback.
40003
+ * @remarks
40004
+ * This is the low-level API to the Midi synthesizer used for playback.
40005
+ * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
40006
+ * @category Properties - Player
40007
+ * @since 0.9.4
40008
+ * @example
40009
+ * JavaScript
40010
+ * ```js
40011
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
40012
+ * setupPlayerEvents(api.settings);
40013
+ * ```
40014
+ *
40015
+ * @example
40016
+ * C#
40017
+ * ```cs
40018
+ * var api = new AlphaTabApi<MyControl>(...);
40019
+ * SetupPlayerEvents(api.Player);
40020
+ * ```
40021
+ *
40022
+ * @example
40023
+ * Android
40024
+ * ```kotlin
40025
+ * val api = AlphaTabApi<MyControl>(...)
40026
+ * setupPlayerEvents(api.player)
40027
+ * ```
40028
+ */
40029
+ get player() {
40030
+ return this._player.instance ? this._player : null;
40031
+ }
40045
40032
  /**
40046
40033
  * Whether the player is ready for starting the playback.
40047
40034
  * @remarks
@@ -40071,10 +40058,7 @@
40071
40058
  * ```
40072
40059
  */
40073
40060
  get isReadyForPlayback() {
40074
- if (!this.player) {
40075
- return false;
40076
- }
40077
- return this.player.isReadyForPlayback;
40061
+ return this._player.isReadyForPlayback;
40078
40062
  }
40079
40063
  /**
40080
40064
  * The current player state.
@@ -40104,10 +40088,7 @@
40104
40088
  * ```
40105
40089
  */
40106
40090
  get playerState() {
40107
- if (!this.player) {
40108
- return PlayerState.Paused;
40109
- }
40110
- return this.player.state;
40091
+ return this._player.state;
40111
40092
  }
40112
40093
  /**
40113
40094
  * The current master volume as percentage (0-1).
@@ -40137,15 +40118,10 @@
40137
40118
  * ```
40138
40119
  */
40139
40120
  get masterVolume() {
40140
- if (!this.player) {
40141
- return 0;
40142
- }
40143
- return this.player.masterVolume;
40121
+ return this._player.masterVolume;
40144
40122
  }
40145
40123
  set masterVolume(value) {
40146
- if (this.player) {
40147
- this.player.masterVolume = value;
40148
- }
40124
+ this._player.masterVolume = value;
40149
40125
  }
40150
40126
  /**
40151
40127
  * The metronome volume as percentage (0-1).
@@ -40176,15 +40152,10 @@
40176
40152
  * ```
40177
40153
  */
40178
40154
  get metronomeVolume() {
40179
- if (!this.player) {
40180
- return 0;
40181
- }
40182
- return this.player.metronomeVolume;
40155
+ return this._player.metronomeVolume;
40183
40156
  }
40184
40157
  set metronomeVolume(value) {
40185
- if (this.player) {
40186
- this.player.metronomeVolume = value;
40187
- }
40158
+ this._player.metronomeVolume = value;
40188
40159
  }
40189
40160
  /**
40190
40161
  * The volume of the count-in metronome ticks.
@@ -40215,15 +40186,10 @@
40215
40186
  * ```
40216
40187
  */
40217
40188
  get countInVolume() {
40218
- if (!this.player) {
40219
- return 0;
40220
- }
40221
- return this.player.countInVolume;
40189
+ return this._player.countInVolume;
40222
40190
  }
40223
40191
  set countInVolume(value) {
40224
- if (this.player) {
40225
- this.player.countInVolume = value;
40226
- }
40192
+ this._player.countInVolume = value;
40227
40193
  }
40228
40194
  /**
40229
40195
  * The midi events which will trigger the `midiEventsPlayed` event
@@ -40282,15 +40248,10 @@
40282
40248
  * ```
40283
40249
  */
40284
40250
  get midiEventsPlayedFilter() {
40285
- if (!this.player) {
40286
- return [];
40287
- }
40288
- return this.player.midiEventsPlayedFilter;
40251
+ return this._player.midiEventsPlayedFilter;
40289
40252
  }
40290
40253
  set midiEventsPlayedFilter(value) {
40291
- if (this.player) {
40292
- this.player.midiEventsPlayedFilter = value;
40293
- }
40254
+ this._player.midiEventsPlayedFilter = value;
40294
40255
  }
40295
40256
  /**
40296
40257
  * The position within the song in midi ticks.
@@ -40318,15 +40279,10 @@
40318
40279
  * ```
40319
40280
  */
40320
40281
  get tickPosition() {
40321
- if (!this.player) {
40322
- return 0;
40323
- }
40324
- return this.player.tickPosition;
40282
+ return this._player.tickPosition;
40325
40283
  }
40326
40284
  set tickPosition(value) {
40327
- if (this.player) {
40328
- this.player.tickPosition = value;
40329
- }
40285
+ this._player.tickPosition = value;
40330
40286
  }
40331
40287
  /**
40332
40288
  * The position within the song in milliseconds
@@ -40354,15 +40310,10 @@
40354
40310
  * ```
40355
40311
  */
40356
40312
  get timePosition() {
40357
- if (!this.player) {
40358
- return 0;
40359
- }
40360
- return this.player.timePosition;
40313
+ return this._player.timePosition;
40361
40314
  }
40362
40315
  set timePosition(value) {
40363
- if (this.player) {
40364
- this.player.timePosition = value;
40365
- }
40316
+ this._player.timePosition = value;
40366
40317
  }
40367
40318
  /**
40368
40319
  * The range of the song that should be played.
@@ -40396,17 +40347,12 @@
40396
40347
  * ```
40397
40348
  */
40398
40349
  get playbackRange() {
40399
- if (!this.player) {
40400
- return null;
40401
- }
40402
- return this.player.playbackRange;
40350
+ return this._player.playbackRange;
40403
40351
  }
40404
40352
  set playbackRange(value) {
40405
- if (this.player) {
40406
- this.player.playbackRange = value;
40407
- if (this.settings.player.enableCursor) {
40408
- this.updateSelectionCursor(value);
40409
- }
40353
+ this._player.playbackRange = value;
40354
+ if (this.settings.player.enableCursor) {
40355
+ this.updateSelectionCursor(value);
40410
40356
  }
40411
40357
  }
40412
40358
  /**
@@ -40438,15 +40384,10 @@
40438
40384
  * ```
40439
40385
  */
40440
40386
  get playbackSpeed() {
40441
- if (!this.player) {
40442
- return 0;
40443
- }
40444
- return this.player.playbackSpeed;
40387
+ return this._player.playbackSpeed;
40445
40388
  }
40446
40389
  set playbackSpeed(value) {
40447
- if (this.player) {
40448
- this.player.playbackSpeed = value;
40449
- }
40390
+ this._player.playbackSpeed = value;
40450
40391
  }
40451
40392
  /**
40452
40393
  * Whether the playback should automatically restart after it finished.
@@ -40477,27 +40418,17 @@
40477
40418
  * ```
40478
40419
  */
40479
40420
  get isLooping() {
40480
- if (!this.player) {
40481
- return false;
40482
- }
40483
- return this.player.isLooping;
40421
+ return this._player.isLooping;
40484
40422
  }
40485
40423
  set isLooping(value) {
40486
- if (this.player) {
40487
- this.player.isLooping = value;
40488
- }
40424
+ this._player.isLooping = value;
40489
40425
  }
40490
40426
  destroyPlayer() {
40491
- if (!this.player) {
40492
- return;
40493
- }
40494
- this.player.destroy();
40495
- this.player = null;
40427
+ this._player.destroy();
40496
40428
  this._previousTick = 0;
40497
- this._playerState = PlayerState.Paused;
40498
40429
  this.destroyCursors();
40499
40430
  }
40500
- setupPlayer() {
40431
+ setupOrDestroyPlayer() {
40501
40432
  let mode = this.settings.player.playerMode;
40502
40433
  if (mode === exports.PlayerMode.EnabledAutomatic) {
40503
40434
  const score = this.score;
@@ -40511,66 +40442,34 @@
40511
40442
  mode = exports.PlayerMode.EnabledSynthesizer;
40512
40443
  }
40513
40444
  }
40445
+ let newPlayer = null;
40514
40446
  if (mode !== this._actualPlayerMode) {
40515
40447
  this.destroyPlayer();
40448
+ switch (mode) {
40449
+ case exports.PlayerMode.Disabled:
40450
+ newPlayer = null;
40451
+ break;
40452
+ case exports.PlayerMode.EnabledSynthesizer:
40453
+ newPlayer = this.uiFacade.createWorkerPlayer();
40454
+ break;
40455
+ case exports.PlayerMode.EnabledBackingTrack:
40456
+ newPlayer = this.uiFacade.createBackingTrackPlayer();
40457
+ break;
40458
+ case exports.PlayerMode.EnabledExternalMedia:
40459
+ newPlayer = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40460
+ break;
40461
+ }
40462
+ }
40463
+ else {
40464
+ // no change in player mode, just update song info if needed
40465
+ return true;
40516
40466
  }
40517
40467
  this.updateCursors();
40518
40468
  this._actualPlayerMode = mode;
40519
- switch (mode) {
40520
- case exports.PlayerMode.Disabled:
40521
- this.destroyPlayer();
40522
- return false;
40523
- case exports.PlayerMode.EnabledSynthesizer:
40524
- if (this.player) {
40525
- return true;
40526
- }
40527
- // new player needed
40528
- this.player = this.uiFacade.createWorkerPlayer();
40529
- break;
40530
- case exports.PlayerMode.EnabledBackingTrack:
40531
- if (this.player) {
40532
- return true;
40533
- }
40534
- // new player needed
40535
- this.player = this.uiFacade.createBackingTrackPlayer();
40536
- break;
40537
- case exports.PlayerMode.EnabledExternalMedia:
40538
- if (this.player) {
40539
- return true;
40540
- }
40541
- this.player = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40542
- break;
40543
- }
40544
- if (!this.player) {
40469
+ if (!newPlayer) {
40545
40470
  return false;
40546
40471
  }
40547
- this.player.ready.on(() => {
40548
- this.loadMidiForScore();
40549
- });
40550
- this.player.readyForPlayback.on(() => {
40551
- this.onPlayerReady();
40552
- if (this.tracks) {
40553
- for (const track of this.tracks) {
40554
- const volume = track.playbackInfo.volume / 16;
40555
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40556
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40557
- }
40558
- }
40559
- });
40560
- this.player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
40561
- this.player.soundFontLoadFailed.on(e => {
40562
- this.onError(e);
40563
- });
40564
- this.player.midiLoaded.on(this.onMidiLoaded.bind(this));
40565
- this.player.midiLoadFailed.on(e => {
40566
- this.onError(e);
40567
- });
40568
- this.player.stateChanged.on(this.onPlayerStateChanged.bind(this));
40569
- this.player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
40570
- this.player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
40571
- this.player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
40572
- this.player.finished.on(this.onPlayerFinished.bind(this));
40573
- this.setupPlayerEvents();
40472
+ this._player.instance = newPlayer;
40574
40473
  return false;
40575
40474
  }
40576
40475
  loadMidiForScore() {
@@ -40590,12 +40489,10 @@
40590
40489
  generator.generate();
40591
40490
  this._tickCache = generator.tickLookup;
40592
40491
  this.onMidiLoad(midiFile);
40593
- const player = this.player;
40594
- if (player) {
40595
- player.loadMidiFile(midiFile);
40596
- player.loadBackingTrack(score, generator.syncPoints);
40597
- player.applyTranspositionPitches(generator.transpositionPitches);
40598
- }
40492
+ const player = this._player;
40493
+ player.loadMidiFile(midiFile);
40494
+ player.loadBackingTrack(score, generator.syncPoints);
40495
+ player.applyTranspositionPitches(generator.transpositionPitches);
40599
40496
  }
40600
40497
  /**
40601
40498
  * Changes the volume of the given tracks.
@@ -40633,12 +40530,9 @@
40633
40530
  * ```
40634
40531
  */
40635
40532
  changeTrackVolume(tracks, volume) {
40636
- if (!this.player) {
40637
- return;
40638
- }
40639
40533
  for (const track of tracks) {
40640
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40641
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40534
+ this._player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40535
+ this._player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40642
40536
  }
40643
40537
  }
40644
40538
  /**
@@ -40675,12 +40569,9 @@
40675
40569
  * ```
40676
40570
  */
40677
40571
  changeTrackSolo(tracks, solo) {
40678
- if (!this.player) {
40679
- return;
40680
- }
40681
40572
  for (const track of tracks) {
40682
- this.player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40683
- this.player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40573
+ this._player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40574
+ this._player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40684
40575
  }
40685
40576
  }
40686
40577
  /**
@@ -40716,12 +40607,9 @@
40716
40607
  * ```
40717
40608
  */
40718
40609
  changeTrackMute(tracks, mute) {
40719
- if (!this.player) {
40720
- return;
40721
- }
40722
40610
  for (const track of tracks) {
40723
- this.player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40724
- this.player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40611
+ this._player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40612
+ this._player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40725
40613
  }
40726
40614
  }
40727
40615
  /**
@@ -40759,12 +40647,9 @@
40759
40647
  * ```
40760
40648
  */
40761
40649
  changeTrackTranspositionPitch(tracks, semitones) {
40762
- if (!this.player) {
40763
- return;
40764
- }
40765
40650
  for (const track of tracks) {
40766
- this.player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40767
- this.player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40651
+ this._player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40652
+ this._player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40768
40653
  }
40769
40654
  }
40770
40655
  /**
@@ -40795,10 +40680,7 @@
40795
40680
  * ```
40796
40681
  */
40797
40682
  play() {
40798
- if (!this.player) {
40799
- return false;
40800
- }
40801
- return this.player.play();
40683
+ return this._player.play();
40802
40684
  }
40803
40685
  /**
40804
40686
  * Pauses the playback of the current song.
@@ -40827,10 +40709,7 @@
40827
40709
  * ```
40828
40710
  */
40829
40711
  pause() {
40830
- if (!this.player) {
40831
- return;
40832
- }
40833
- this.player.pause();
40712
+ this._player.pause();
40834
40713
  }
40835
40714
  /**
40836
40715
  * Toggles between play/pause depending on the current player state.
@@ -40861,10 +40740,7 @@
40861
40740
  * ```
40862
40741
  */
40863
40742
  playPause() {
40864
- if (!this.player) {
40865
- return;
40866
- }
40867
- this.player.playPause();
40743
+ this._player.playPause();
40868
40744
  }
40869
40745
  /**
40870
40746
  * Stops the playback of the current song, and moves the playback position back to the start.
@@ -40895,10 +40771,7 @@
40895
40771
  * ```
40896
40772
  */
40897
40773
  stop() {
40898
- if (!this.player) {
40899
- return;
40900
- }
40901
- this.player.stop();
40774
+ this._player.stop();
40902
40775
  }
40903
40776
  /**
40904
40777
  * Triggers the play of the given beat.
@@ -40934,15 +40807,12 @@
40934
40807
  * ```
40935
40808
  */
40936
40809
  playBeat(beat) {
40937
- if (!this.player) {
40938
- return;
40939
- }
40940
40810
  // we generate a new midi file containing only the beat
40941
40811
  const midiFile = new MidiFile();
40942
40812
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40943
40813
  const generator = new MidiFileGenerator(beat.voice.bar.staff.track.score, this.settings, handler);
40944
40814
  generator.generateSingleBeat(beat);
40945
- this.player.playOneTimeMidiFile(midiFile);
40815
+ this._player.playOneTimeMidiFile(midiFile);
40946
40816
  }
40947
40817
  /**
40948
40818
  * Triggers the play of the given note.
@@ -40977,15 +40847,12 @@
40977
40847
  * ```
40978
40848
  */
40979
40849
  playNote(note) {
40980
- if (!this.player) {
40981
- return;
40982
- }
40983
40850
  // we generate a new midi file containing only the beat
40984
40851
  const midiFile = new MidiFile();
40985
40852
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40986
40853
  const generator = new MidiFileGenerator(note.beat.voice.bar.staff.track.score, this.settings, handler);
40987
40854
  generator.generateSingleNote(note);
40988
- this.player.playOneTimeMidiFile(midiFile);
40855
+ this._player.playOneTimeMidiFile(midiFile);
40989
40856
  }
40990
40857
  destroyCursors() {
40991
40858
  if (!this._cursorWrapper) {
@@ -41017,36 +40884,6 @@
41017
40884
  this.destroyCursors();
41018
40885
  }
41019
40886
  }
41020
- setupPlayerEvents() {
41021
- //
41022
- // Hook into events
41023
- this._previousTick = 0;
41024
- this._playerState = PlayerState.Paused;
41025
- // we need to update our position caches if we render a tablature
41026
- this.renderer.postRenderFinished.on(() => {
41027
- this._currentBeat = null;
41028
- this.cursorUpdateTick(this._previousTick, false, 1, this._previousTick > 10);
41029
- });
41030
- if (this.player) {
41031
- this.player.positionChanged.on(e => {
41032
- this._previousTick = e.currentTick;
41033
- this.uiFacade.beginInvoke(() => {
41034
- const cursorSpeed = e.modifiedTempo / e.originalTempo;
41035
- this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41036
- });
41037
- });
41038
- this.player.stateChanged.on(e => {
41039
- this._playerState = e.state;
41040
- if (!e.stopped && e.state === PlayerState.Paused) {
41041
- const currentBeat = this._currentBeat;
41042
- const tickCache = this._tickCache;
41043
- if (currentBeat && tickCache) {
41044
- this.player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41045
- }
41046
- }
41047
- });
41048
- }
41049
- }
41050
40887
  /**
41051
40888
  * updates the cursors to highlight the beat at the specified tick position
41052
40889
  * @param tick
@@ -41087,7 +40924,7 @@
41087
40924
  if (!forceUpdate &&
41088
40925
  beat === previousBeat?.beat &&
41089
40926
  cache === previousCache &&
41090
- previousState === this._playerState &&
40927
+ previousState === this._player.state &&
41091
40928
  previousBeat?.start === lookupResult.start) {
41092
40929
  return;
41093
40930
  }
@@ -41099,7 +40936,7 @@
41099
40936
  // actually show the cursor
41100
40937
  this._currentBeat = lookupResult;
41101
40938
  this._previousCursorCache = cache;
41102
- this._previousStateForCursor = this._playerState;
40939
+ this._previousStateForCursor = this._player.state;
41103
40940
  this.uiFacade.beginInvoke(() => {
41104
40941
  this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
41105
40942
  });
@@ -41110,9 +40947,9 @@
41110
40947
  * @category Methods - Player
41111
40948
  */
41112
40949
  scrollToCursor() {
41113
- const barBounds = this._currentBarBounds;
41114
- if (barBounds) {
41115
- this.internalScrollToCursor(barBounds);
40950
+ const beatBounds = this._currentBeatBounds;
40951
+ if (beatBounds) {
40952
+ this.internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
41116
40953
  }
41117
40954
  }
41118
40955
  internalScrollToCursor(barBoundings) {
@@ -41171,10 +41008,12 @@
41171
41008
  const beatCursor = this._beatCursor;
41172
41009
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
41173
41010
  const barBounds = barBoundings.visualBounds;
41174
- this._currentBarBounds = barBoundings;
41011
+ const previousBeatBounds = this._currentBeatBounds;
41012
+ this._currentBeatBounds = beatBoundings;
41175
41013
  if (barCursor) {
41176
41014
  barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
41177
41015
  }
41016
+ const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
41178
41017
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
41179
41018
  // get position of next beat on same system
41180
41019
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
@@ -41192,12 +41031,38 @@
41192
41031
  if (this.settings.player.enableAnimatedBeatCursor) {
41193
41032
  const animationWidth = nextBeatX - beatBoundings.onNotesX;
41194
41033
  const relativePosition = this._previousTick - this._currentBeat.start;
41195
- const ratioPosition = relativePosition / this._currentBeat.tickDuration;
41034
+ const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
41196
41035
  startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
41197
41036
  duration -= duration * ratioPosition;
41198
- beatCursor.transitionToX(0, startBeatX);
41037
+ if (isPlayingUpdate) {
41038
+ // we do not "reset" the cursor if we are smoothly moving from left to right.
41039
+ const jumpCursor = !previousBeatBounds ||
41040
+ barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
41041
+ startBeatX < previousBeatBounds.onNotesX ||
41042
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
41043
+ if (jumpCursor) {
41044
+ beatCursor.transitionToX(0, startBeatX);
41045
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41046
+ }
41047
+ // we need to put the transition to an own animation frame
41048
+ // otherwise the stop animation above is not applied.
41049
+ this.uiFacade.beginInvoke(() => {
41050
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
41051
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
41052
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
41053
+ const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * 2;
41054
+ beatCursor.transitionToX((duration / cursorSpeed) * 2, doubleEndBeatX);
41055
+ });
41056
+ }
41057
+ else {
41058
+ beatCursor.transitionToX(0, startBeatX);
41059
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41060
+ }
41061
+ }
41062
+ else {
41063
+ // ticking cursor
41064
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41199
41065
  }
41200
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41201
41066
  }
41202
41067
  // if playing, animate the cursor to the next beat
41203
41068
  if (this.settings.player.enableElementHighlighting) {
@@ -41205,7 +41070,6 @@
41205
41070
  }
41206
41071
  // actively playing? -> animate cursor and highlight items
41207
41072
  let shouldNotifyBeatChange = false;
41208
- const isPlayingUpdate = this._playerState === PlayerState.Playing && !stop;
41209
41073
  if (isPlayingUpdate) {
41210
41074
  if (this.settings.player.enableElementHighlighting) {
41211
41075
  for (const highlight of beatsToHighlight) {
@@ -41216,15 +41080,6 @@
41216
41080
  shouldScroll = !stop;
41217
41081
  shouldNotifyBeatChange = true;
41218
41082
  }
41219
- if (this.settings.player.enableAnimatedBeatCursor && beatCursor) {
41220
- if (isPlayingUpdate) {
41221
- // we need to put the transition to an own animation frame
41222
- // otherwise the stop animation above is not applied.
41223
- this.uiFacade.beginInvoke(() => {
41224
- beatCursor.transitionToX(duration / cursorSpeed, nextBeatX);
41225
- });
41226
- }
41227
- }
41228
41083
  if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== exports.ScrollMode.Off) {
41229
41084
  this.internalScrollToCursor(barBoundings);
41230
41085
  }
@@ -41314,7 +41169,7 @@
41314
41169
  const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
41315
41170
  // move to selection start
41316
41171
  this._currentBeat = null; // reset current beat so it is updating the cursor
41317
- if (this._playerState === PlayerState.Paused) {
41172
+ if (this._player.state === PlayerState.Paused) {
41318
41173
  this.cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
41319
41174
  }
41320
41175
  this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
@@ -41505,7 +41360,7 @@
41505
41360
  }
41506
41361
  this.scoreLoaded.trigger(score);
41507
41362
  this.uiFacade.triggerEvent(this.container, 'scoreLoaded', score);
41508
- if (this.setupPlayer()) {
41363
+ if (this.setupOrDestroyPlayer()) {
41509
41364
  this.loadMidiForScore();
41510
41365
  }
41511
41366
  }
@@ -41534,6 +41389,8 @@
41534
41389
  if (this._isDestroyed) {
41535
41390
  return;
41536
41391
  }
41392
+ this._currentBeat = null;
41393
+ this.cursorUpdateTick(this._previousTick, false, 1, this._previousTick > 10);
41537
41394
  this.postRenderFinished.trigger();
41538
41395
  this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null);
41539
41396
  }
@@ -41548,25 +41405,155 @@
41548
41405
  this.error.trigger(error);
41549
41406
  this.uiFacade.triggerEvent(this.container, 'error', error);
41550
41407
  }
41408
+ /**
41409
+ * This event is fired when all required data for playback is loaded and ready.
41410
+ * @remarks
41411
+ * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
41412
+ * 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.
41413
+ *
41414
+ * @eventProperty
41415
+ * @category Events - Player
41416
+ * @since 0.9.4
41417
+ *
41418
+ * @example
41419
+ * JavaScript
41420
+ * ```js
41421
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41422
+ * api.playerReady.on(() => {
41423
+ * enablePlayerControls();
41424
+ * });
41425
+ * ```
41426
+ *
41427
+ * @example
41428
+ * C#
41429
+ * ```cs
41430
+ * var api = new AlphaTabApi<MyControl>(...);
41431
+ * api.PlayerReady.On(() =>
41432
+ * {
41433
+ * EnablePlayerControls()
41434
+ * });
41435
+ * ```
41436
+ *
41437
+ * @example
41438
+ * Android
41439
+ * ```kotlin
41440
+ * val api = AlphaTabApi<MyControl>(...)
41441
+ * api.playerReady.on {
41442
+ * enablePlayerControls()
41443
+ * }
41444
+ * ```
41445
+ */
41446
+ get playerReady() {
41447
+ return this._player.readyForPlayback;
41448
+ }
41551
41449
  onPlayerReady() {
41552
41450
  if (this._isDestroyed) {
41553
41451
  return;
41554
41452
  }
41555
- this.playerReady.trigger();
41556
41453
  this.uiFacade.triggerEvent(this.container, 'playerReady', null);
41557
41454
  }
41455
+ /**
41456
+ * This event is fired when the playback of the whole song finished.
41457
+ * @remarks
41458
+ * This event is finished regardless on whether looping is enabled or not.
41459
+ *
41460
+ * @eventProperty
41461
+ * @category Events - Player
41462
+ * @since 0.9.4
41463
+ *
41464
+ * @example
41465
+ * JavaScript
41466
+ * ```js
41467
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41468
+ * api.playerFinished.on((args) => {
41469
+ * // speed trainer
41470
+ * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
41471
+ * });
41472
+ * api.isLooping = true;
41473
+ * api.playbackSpeed = 0.5;
41474
+ * api.play()
41475
+ * ```
41476
+ *
41477
+ * @example
41478
+ * C#
41479
+ * ```cs
41480
+ * var api = new AlphaTabApi<MyControl>(...);
41481
+ * api.PlayerFinished.On(() =>
41482
+ * {
41483
+ * // speed trainer
41484
+ * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
41485
+ * });
41486
+ * api.IsLooping = true;
41487
+ * api.PlaybackSpeed = 0.5;
41488
+ * api.Play();
41489
+ * ```
41490
+ *
41491
+ * @example
41492
+ * Android
41493
+ * ```kotlin
41494
+ * val api = AlphaTabApi<MyControl>(...)
41495
+ * api.playerFinished.on {
41496
+ * // speed trainer
41497
+ * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
41498
+ * }
41499
+ * api.isLooping = true
41500
+ * api.playbackSpeed = 0.5
41501
+ * api.play()
41502
+ * ```
41503
+ *
41504
+ */
41505
+ get playerFinished() {
41506
+ return this._player.finished;
41507
+ }
41558
41508
  onPlayerFinished() {
41559
41509
  if (this._isDestroyed) {
41560
41510
  return;
41561
41511
  }
41562
- this.playerFinished.trigger();
41563
41512
  this.uiFacade.triggerEvent(this.container, 'playerFinished', null);
41564
41513
  }
41514
+ /**
41515
+ * This event is fired when the SoundFont needed for playback was loaded.
41516
+ *
41517
+ * @eventProperty
41518
+ * @category Events - Player
41519
+ * @since 0.9.4
41520
+ *
41521
+ * @example
41522
+ * JavaScript
41523
+ * ```js
41524
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41525
+ * api.soundFontLoaded.on(() => {
41526
+ * hideSoundFontLoadingIndicator();
41527
+ * });
41528
+ * ```
41529
+ *
41530
+ * @example
41531
+ * C#
41532
+ * ```cs
41533
+ * var api = new AlphaTabApi<MyControl>(...);
41534
+ * api.SoundFontLoaded.On(() =>
41535
+ * {
41536
+ * HideSoundFontLoadingIndicator();
41537
+ * });
41538
+ * ```
41539
+ *
41540
+ * @example
41541
+ * Android
41542
+ * ```kotlin
41543
+ * val api = AlphaTabApi<MyControl>(...);
41544
+ * api.soundFontLoaded.on {
41545
+ * hideSoundFontLoadingIndicator();
41546
+ * }
41547
+ * ```
41548
+ *
41549
+ */
41550
+ get soundFontLoaded() {
41551
+ return this._player.soundFontLoaded;
41552
+ }
41565
41553
  onSoundFontLoaded() {
41566
41554
  if (this._isDestroyed) {
41567
41555
  return;
41568
41556
  }
41569
- this.soundFontLoaded.trigger();
41570
41557
  this.uiFacade.triggerEvent(this.container, 'soundFontLoaded', null);
41571
41558
  }
41572
41559
  onMidiLoad(e) {
@@ -41583,34 +41570,255 @@
41583
41570
  this.midiLoaded.trigger(e);
41584
41571
  this.uiFacade.triggerEvent(this.container, 'midiFileLoaded', e);
41585
41572
  }
41573
+ /**
41574
+ * This event is fired when the playback state changed.
41575
+ *
41576
+ * @eventProperty
41577
+ * @category Events - Player
41578
+ * @since 0.9.4
41579
+ *
41580
+ * @example
41581
+ * JavaScript
41582
+ * ```js
41583
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41584
+ * api.playerStateChanged.on((args) => {
41585
+ * updatePlayerControls(args.state, args.stopped);
41586
+ * });
41587
+ * ```
41588
+ *
41589
+ * @example
41590
+ * C#
41591
+ * ```cs
41592
+ * var api = new AlphaTabApi<MyControl>(...);
41593
+ * api.PlayerStateChanged.On(args =>
41594
+ * {
41595
+ * UpdatePlayerControls(args);
41596
+ * });
41597
+ * ```
41598
+ *
41599
+ * @example
41600
+ * Android
41601
+ * ```kotlin
41602
+ * val api = AlphaTabApi<MyControl>(...)
41603
+ * api.playerStateChanged.on { args ->
41604
+ * updatePlayerControls(args)
41605
+ * }
41606
+ * ```
41607
+ *
41608
+ */
41609
+ get playerStateChanged() {
41610
+ return this._player.stateChanged;
41611
+ }
41586
41612
  onPlayerStateChanged(e) {
41587
41613
  if (this._isDestroyed) {
41588
41614
  return;
41589
41615
  }
41590
- this.playerStateChanged.trigger(e);
41616
+ if (!e.stopped && e.state === PlayerState.Paused) {
41617
+ const currentBeat = this._currentBeat;
41618
+ const tickCache = this._tickCache;
41619
+ if (currentBeat && tickCache) {
41620
+ this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41621
+ }
41622
+ }
41591
41623
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
41592
41624
  }
41625
+ /**
41626
+ * This event is fired when the current playback position of the song changed.
41627
+ *
41628
+ * @eventProperty
41629
+ * @category Events - Player
41630
+ * @since 0.9.4
41631
+ *
41632
+ * @example
41633
+ * JavaScript
41634
+ * ```js
41635
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41636
+ * api.playerPositionChanged.on((args) => {
41637
+ * updatePlayerPosition(args);
41638
+ * });
41639
+ * ```
41640
+ *
41641
+ * @example
41642
+ * C#
41643
+ * ```cs
41644
+ * var api = new AlphaTabApi<MyControl>(...);
41645
+ * api.PlayerPositionChanged.On(args =>
41646
+ * {
41647
+ * UpdatePlayerPosition(args);
41648
+ * });
41649
+ * ```
41650
+ *
41651
+ * @example
41652
+ * Android
41653
+ * ```kotlin
41654
+ * val api = AlphaTabApi<MyControl>(...)
41655
+ * api.playerPositionChanged.on { args ->
41656
+ * updatePlayerPosition(args)
41657
+ * }
41658
+ * ```
41659
+ *
41660
+ */
41661
+ get playerPositionChanged() {
41662
+ return this._player.positionChanged;
41663
+ }
41593
41664
  onPlayerPositionChanged(e) {
41594
41665
  if (this._isDestroyed) {
41595
41666
  return;
41596
41667
  }
41597
- if (this.score !== null && this.tracks.length > 0) {
41598
- this.playerPositionChanged.trigger(e);
41599
- this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41600
- }
41668
+ this._previousTick = e.currentTick;
41669
+ this.uiFacade.beginInvoke(() => {
41670
+ const cursorSpeed = e.modifiedTempo / e.originalTempo;
41671
+ this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41672
+ });
41673
+ this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41674
+ }
41675
+ /**
41676
+ * This event is fired when the synthesizer played certain midi events.
41677
+ *
41678
+ * @remarks
41679
+ * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
41680
+ * audio playback elements like notes/rests played or metronome ticks.
41681
+ *
41682
+ * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
41683
+ *
41684
+ * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
41685
+ * until some extend but highly encourage changing to the new models in case of problems.
41686
+ *
41687
+ * @eventProperty
41688
+ * @category Events - Player
41689
+ * @since 1.2.0
41690
+ *
41691
+ * @example
41692
+ * JavaScript
41693
+ * ```js
41694
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41695
+ * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
41696
+ * api.midiEventsPlayed.on(function(e) {
41697
+ * for(const midi of e.events) {
41698
+ * if(midi.isMetronome) {
41699
+ * console.log('Metronome tick ' + midi.tick);
41700
+ * }
41701
+ * }
41702
+ * });
41703
+ * ```
41704
+ *
41705
+ * @example
41706
+ * C#
41707
+ * ```cs
41708
+ * var api = new AlphaTabApi<MyControl>(...);
41709
+ * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
41710
+ * api.MidiEventsPlayed.On(e =>
41711
+ * {
41712
+ * foreach(var midi of e.events)
41713
+ * {
41714
+ * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
41715
+ * {
41716
+ * Console.WriteLine("Metronome tick " + midi.Tick);
41717
+ * }
41718
+ * }
41719
+ * });
41720
+ * ```
41721
+ *
41722
+ * @example
41723
+ * Android
41724
+ * ```kotlin
41725
+ * val api = AlphaTabApi<MyControl>(...);
41726
+ * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
41727
+ * api.midiEventsPlayed.on { e ->
41728
+ * for (midi in e.events) {
41729
+ * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
41730
+ * println("Metronome tick " + midi.tick);
41731
+ * }
41732
+ * }
41733
+ * }
41734
+ * ```
41735
+ * @see {@link MidiEvent}
41736
+ * @see {@link TimeSignatureEvent}
41737
+ * @see {@link AlphaTabMetronomeEvent}
41738
+ * @see {@link AlphaTabRestEvent}
41739
+ * @see {@link NoteOnEvent}
41740
+ * @see {@link NoteOffEvent}
41741
+ * @see {@link ControlChangeEvent}
41742
+ * @see {@link ProgramChangeEvent}
41743
+ * @see {@link TempoChangeEvent}
41744
+ * @see {@link PitchBendEvent}
41745
+ * @see {@link NoteBendEvent}
41746
+ * @see {@link EndOfTrackEvent}
41747
+ * @see {@link MetaEvent}
41748
+ * @see {@link MetaDataEvent}
41749
+ * @see {@link MetaNumberEvent}
41750
+ * @see {@link Midi20PerNotePitchBendEvent}
41751
+ * @see {@link SystemCommonEvent}
41752
+ * @see {@link SystemExclusiveEvent}
41753
+ */
41754
+ get midiEventsPlayed() {
41755
+ return this._player.midiEventsPlayed;
41601
41756
  }
41602
41757
  onMidiEventsPlayed(e) {
41603
41758
  if (this._isDestroyed) {
41604
41759
  return;
41605
41760
  }
41606
- this.midiEventsPlayed.trigger(e);
41607
41761
  this.uiFacade.triggerEvent(this.container, 'midiEventsPlayed', e);
41608
41762
  }
41763
+ /**
41764
+ * This event is fired when the playback range changed.
41765
+ *
41766
+ * @eventProperty
41767
+ * @category Events - Player
41768
+ * @since 1.2.3
41769
+ *
41770
+ * @example
41771
+ * JavaScript
41772
+ * ```js
41773
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41774
+ * api.playbackRangeChanged.on((args) => {
41775
+ * if (args.playbackRange) {
41776
+ * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
41777
+ * } else {
41778
+ * clearHighlightInProgressBar();
41779
+ * }
41780
+ * });
41781
+ * ```
41782
+ *
41783
+ * @example
41784
+ * C#
41785
+ * ```cs
41786
+ * var api = new AlphaTabApi<MyControl>(...);
41787
+ * api.PlaybackRangeChanged.On(args =>
41788
+ * {
41789
+ * if (args.PlaybackRange != null)
41790
+ * {
41791
+ * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
41792
+ * }
41793
+ * else
41794
+ * {
41795
+ * ClearHighlightInProgressBar();
41796
+ * }
41797
+ * });
41798
+ * ```
41799
+ *
41800
+ * @example
41801
+ * Android
41802
+ * ```kotlin
41803
+ * val api = AlphaTabApi<MyControl>(...)
41804
+ * api.playbackRangeChanged.on { args ->
41805
+ * val playbackRange = args.playbackRange
41806
+ * if (playbackRange != null) {
41807
+ * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
41808
+ * } else {
41809
+ * clearHighlightInProgressBar()
41810
+ * }
41811
+ * }
41812
+ * ```
41813
+ *
41814
+ */
41815
+ get playbackRangeChanged() {
41816
+ return this._player.playbackRangeChanged;
41817
+ }
41609
41818
  onPlaybackRangeChanged(e) {
41610
41819
  if (this._isDestroyed) {
41611
41820
  return;
41612
41821
  }
41613
- this.playbackRangeChanged.trigger(e);
41614
41822
  this.uiFacade.triggerEvent(this.container, 'playbackRangeChanged', e);
41615
41823
  }
41616
41824
  onSettingsUpdated() {
@@ -41672,10 +41880,7 @@
41672
41880
  * ```
41673
41881
  */
41674
41882
  async enumerateOutputDevices() {
41675
- if (this.player) {
41676
- return await this.player.output.enumerateOutputDevices();
41677
- }
41678
- return [];
41883
+ return await this._player.output.enumerateOutputDevices();
41679
41884
  }
41680
41885
  /**
41681
41886
  * Changes the output device which should be used for playing the audio (player must be enabled).
@@ -41726,9 +41931,7 @@
41726
41931
  * ```
41727
41932
  */
41728
41933
  async setOutputDevice(device) {
41729
- if (this.player) {
41730
- await this.player.output.setOutputDevice(device);
41731
- }
41934
+ await this._player.output.setOutputDevice(device);
41732
41935
  }
41733
41936
  /**
41734
41937
  * The currently configured output device if changed via {@link setOutputDevice}.
@@ -41768,10 +41971,7 @@
41768
41971
  *
41769
41972
  */
41770
41973
  async getOutputDevice() {
41771
- if (this.player) {
41772
- return await this.player.output.getOutputDevice();
41773
- }
41774
- return null;
41974
+ return await this._player.output.getOutputDevice();
41775
41975
  }
41776
41976
  }
41777
41977
 
@@ -41948,38 +42148,52 @@
41948
42148
  this.element = element;
41949
42149
  this.mouseDown = {
41950
42150
  on: (value) => {
41951
- this.element.addEventListener('mousedown', e => {
42151
+ const nativeListener = e => {
41952
42152
  value(new BrowserMouseEventArgs(e));
41953
- }, true);
42153
+ };
42154
+ this.element.addEventListener('mousedown', nativeListener, true);
42155
+ return () => {
42156
+ this.element.removeEventListener('mousedown', nativeListener, true);
42157
+ };
41954
42158
  },
41955
42159
  off: (value) => {
41956
42160
  }
41957
42161
  };
41958
42162
  this.mouseUp = {
41959
42163
  on: (value) => {
41960
- this.element.addEventListener('mouseup', e => {
42164
+ const nativeListener = e => {
41961
42165
  value(new BrowserMouseEventArgs(e));
41962
- }, true);
42166
+ };
42167
+ this.element.addEventListener('mouseup', nativeListener, true);
42168
+ return () => {
42169
+ this.element.removeEventListener('mouseup', nativeListener, true);
42170
+ };
41963
42171
  },
41964
42172
  off: (value) => {
41965
42173
  }
41966
42174
  };
41967
42175
  this.mouseMove = {
41968
42176
  on: (value) => {
41969
- this.element.addEventListener('mousemove', e => {
42177
+ const nativeListener = e => {
41970
42178
  value(new BrowserMouseEventArgs(e));
41971
- }, true);
42179
+ };
42180
+ this.element.addEventListener('mousemove', nativeListener, true);
42181
+ return () => {
42182
+ this.element.removeEventListener('mousemove', nativeListener, true);
42183
+ };
41972
42184
  },
41973
42185
  off: (_) => {
41974
42186
  }
41975
42187
  };
42188
+ const container = this;
41976
42189
  this.resize = {
41977
- on: (value) => {
41978
- if (this._resizeListeners === 0) {
41979
- HtmlElementContainer.resizeObserver.value.observe(this.element);
42190
+ on: function (value) {
42191
+ if (container._resizeListeners === 0) {
42192
+ HtmlElementContainer.resizeObserver.value.observe(container.element);
41980
42193
  }
41981
- this.element.addEventListener('resize', value, true);
41982
- this._resizeListeners++;
42194
+ container.element.addEventListener('resize', value, true);
42195
+ container._resizeListeners++;
42196
+ return () => this.off(value);
41983
42197
  },
41984
42198
  off: (value) => {
41985
42199
  this.element.removeEventListener('resize', value, true);
@@ -42560,21 +42774,6 @@
42560
42774
  }
42561
42775
  }
42562
42776
 
42563
- /**
42564
- * Represents the progress of any data being loaded.
42565
- */
42566
- class ProgressEventArgs {
42567
- /**
42568
- * Initializes a new instance of the {@link ProgressEventArgs} class.
42569
- * @param loaded
42570
- * @param total
42571
- */
42572
- constructor(loaded, total) {
42573
- this.loaded = loaded;
42574
- this.total = total;
42575
- }
42576
- }
42577
-
42578
42777
  /**
42579
42778
  * a WebWorker based alphaSynth which uses the given player as output.
42580
42779
  * @target web
@@ -42811,25 +43010,6 @@
42811
43010
  append: append
42812
43011
  });
42813
43012
  }
42814
- loadSoundFontFromUrl(url, append, progress) {
42815
- Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
42816
- const request = new XMLHttpRequest();
42817
- request.open('GET', url, true, null, null);
42818
- request.responseType = 'arraybuffer';
42819
- request.onload = _ => {
42820
- const buffer = new Uint8Array(request.response);
42821
- this.loadSoundFont(buffer, append);
42822
- };
42823
- request.onerror = e => {
42824
- Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
42825
- this.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
42826
- };
42827
- request.onprogress = e => {
42828
- Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
42829
- progress(new ProgressEventArgs(e.loaded, e.total));
42830
- };
42831
- request.send();
42832
- }
42833
43013
  resetSoundFonts() {
42834
43014
  this._synth.postMessage({
42835
43015
  cmd: 'alphaSynth.resetSoundFonts'
@@ -43381,7 +43561,11 @@
43381
43561
  }
43382
43562
  this._padding = backingTrack.padding / 1000;
43383
43563
  const blob = new Blob([backingTrack.rawAudioFile]);
43564
+ // https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource
43565
+ // Step 8. resets the playbackRate, we need to remember and restore it.
43566
+ const playbackRate = this.audioElement.playbackRate;
43384
43567
  this.audioElement.src = URL.createObjectURL(blob);
43568
+ this.audioElement.playbackRate = playbackRate;
43385
43569
  }
43386
43570
  open(_bufferTimeInMilliseconds) {
43387
43571
  const audioElement = document.createElement('audio');
@@ -44100,6 +44284,21 @@
44100
44284
  }
44101
44285
  }
44102
44286
 
44287
+ /**
44288
+ * Represents the progress of any data being loaded.
44289
+ */
44290
+ class ProgressEventArgs {
44291
+ /**
44292
+ * Initializes a new instance of the {@link ProgressEventArgs} class.
44293
+ * @param loaded
44294
+ * @param total
44295
+ */
44296
+ constructor(loaded, total) {
44297
+ this.loaded = loaded;
44298
+ this.total = total;
44299
+ }
44300
+ }
44301
+
44103
44302
  /**
44104
44303
  * @target web
44105
44304
  */
@@ -44343,13 +44542,29 @@
44343
44542
  * @since 0.9.4
44344
44543
  */
44345
44544
  loadSoundFontFromUrl(url, append) {
44346
- if (!this.player) {
44545
+ const player = this.player;
44546
+ if (!player) {
44347
44547
  return;
44348
44548
  }
44349
- this.player.loadSoundFontFromUrl(url, append, e => {
44350
- this.soundFontLoad.trigger(e);
44351
- this.uiFacade.triggerEvent(this.container, 'soundFontLoad', e);
44352
- });
44549
+ Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
44550
+ const request = new XMLHttpRequest();
44551
+ request.open('GET', url, true, null, null);
44552
+ request.responseType = 'arraybuffer';
44553
+ request.onload = _ => {
44554
+ const buffer = new Uint8Array(request.response);
44555
+ this.loadSoundFont(buffer, append);
44556
+ };
44557
+ request.onerror = e => {
44558
+ Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
44559
+ player.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
44560
+ };
44561
+ request.onprogress = e => {
44562
+ Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
44563
+ const args = new ProgressEventArgs(e.loaded, e.total);
44564
+ this.soundFontLoad.trigger(args);
44565
+ this.uiFacade.triggerEvent(this.container, 'soundFontLoad', args);
44566
+ };
44567
+ request.send();
44353
44568
  }
44354
44569
  }
44355
44570
 
@@ -60316,9 +60531,9 @@
60316
60531
  print(`build date: ${VersionInfo.date}`);
60317
60532
  }
60318
60533
  }
60319
- VersionInfo.version = '1.6.0-alpha.1405';
60320
- VersionInfo.date = '2025-05-10T17:25:30.743Z';
60321
- VersionInfo.commit = 'a9f729a65e195d4fec684444cd2c2a259dc9729b';
60534
+ VersionInfo.version = '1.6.0-alpha.1409';
60535
+ VersionInfo.date = '2025-05-14T02:06:32.990Z';
60536
+ VersionInfo.commit = 'f91fed13b0a0946b3fa9306b480ddf8044b948e2';
60322
60537
 
60323
60538
  /**
60324
60539
  * A factory for custom layout engines.