@coderline/alphatab 1.6.0-alpha.1408 → 1.6.0-alpha.1415

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.1408 (develop, build 1408)
2
+ * alphaTab v1.6.0-alpha.1415 (develop, build 1415)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -3377,18 +3377,26 @@
3377
3377
  const lookup = new Map();
3378
3378
  const score = tracks[0].score;
3379
3379
  let currentIndex = startIndex;
3380
+ let tempo = score.tempo;
3380
3381
  while (currentIndex <= endIndexInclusive) {
3381
3382
  const currentGroupStartIndex = currentIndex;
3382
3383
  let currentGroup = null;
3383
3384
  while (currentIndex <= endIndexInclusive) {
3384
3385
  const masterBar = score.masterBars[currentIndex];
3386
+ let hasTempoChange = false;
3387
+ for (const a of masterBar.tempoAutomations) {
3388
+ if (a.value !== tempo) {
3389
+ hasTempoChange = true;
3390
+ }
3391
+ tempo = a.value;
3392
+ }
3385
3393
  // check if masterbar breaks multibar rests, it must be fully empty with no annotations
3386
3394
  if (masterBar.alternateEndings ||
3387
3395
  (masterBar.isRepeatStart && masterBar.index !== currentGroupStartIndex) ||
3388
3396
  masterBar.isFreeTime ||
3389
3397
  masterBar.isAnacrusis ||
3390
3398
  masterBar.section !== null ||
3391
- (masterBar.index !== currentGroupStartIndex && masterBar.tempoAutomations.length > 0) ||
3399
+ (masterBar.index !== currentGroupStartIndex && hasTempoChange) ||
3392
3400
  (masterBar.fermata !== null && masterBar.fermata.size > 0) ||
3393
3401
  (masterBar.directions !== null && masterBar.directions.size > 0)) {
3394
3402
  break;
@@ -20221,7 +20229,11 @@
20221
20229
  for (const c of element.childElements()) {
20222
20230
  switch (c.localName) {
20223
20231
  case 'direction-type':
20224
- directionTypes.push(c.firstElement);
20232
+ // See https://github.com/CoderLine/alphaTab/issues/2102
20233
+ const type = c.firstElement;
20234
+ if (type) {
20235
+ directionTypes.push(type);
20236
+ }
20225
20237
  break;
20226
20238
  case 'offset':
20227
20239
  offset = Number.parseFloat(c.innerText);
@@ -22403,6 +22415,11 @@
22403
22415
  this._mainState.eventIndex = 0;
22404
22416
  this._mainState.syncPointIndex = 0;
22405
22417
  this._mainState.tempoChangeIndex = 0;
22418
+ this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22419
+ this._mainState.modifiedTempo =
22420
+ this._mainState.syncPoints.length > 0
22421
+ ? this._mainState.syncPoints[0].data.modifiedTempo
22422
+ : this._mainState.currentTempo;
22406
22423
  if (this.isPlayingMain) {
22407
22424
  const metronomeVolume = this._synthesizer.metronomeVolume;
22408
22425
  this._synthesizer.noteOffAll(true);
@@ -22541,18 +22558,6 @@
22541
22558
  this._currentState.synthData[this._currentState.eventIndex].time < this._currentState.currentTime) {
22542
22559
  const synthEvent = this._currentState.synthData[this._currentState.eventIndex];
22543
22560
  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
22561
  this._currentState.eventIndex++;
22557
22562
  anyEventsDispatched = true;
22558
22563
  }
@@ -22582,9 +22587,6 @@
22582
22587
  mainTickPositionToTimePosition(tickPosition) {
22583
22588
  return this.tickPositionToTimePositionWithSpeed(this._mainState, tickPosition, this.playbackSpeed);
22584
22589
  }
22585
- mainTimePositionToTickPosition(timePosition) {
22586
- return this.timePositionToTickPositionWithSpeed(this._mainState, timePosition, this.playbackSpeed);
22587
- }
22588
22590
  mainUpdateSyncPoints(syncPoints) {
22589
22591
  const state = this._mainState;
22590
22592
  syncPoints.sort((a, b) => a.tick - b.tick); // just in case
@@ -22612,7 +22614,49 @@
22612
22614
  state.syncPointIndex = 0;
22613
22615
  }
22614
22616
  currentTimePositionToTickPosition(timePosition) {
22615
- return this.timePositionToTickPositionWithSpeed(this._currentState, timePosition, this.playbackSpeed);
22617
+ const state = this._currentState;
22618
+ if (state.tempoChanges.length === 0) {
22619
+ return 0;
22620
+ }
22621
+ timePosition *= this.playbackSpeed;
22622
+ this.updateCurrentTempo(state, timePosition);
22623
+ const lastTempoChange = state.tempoChanges[state.tempoChangeIndex];
22624
+ const timeDiff = timePosition - lastTempoChange.time;
22625
+ const ticks = ((timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0);
22626
+ // we add 1 for possible rounding errors.(floating point issuses)
22627
+ return lastTempoChange.ticks + ticks + 1;
22628
+ }
22629
+ updateCurrentTempo(state, timePosition) {
22630
+ let tempoChangeIndex = state.tempoChangeIndex;
22631
+ if (timePosition < state.tempoChanges[tempoChangeIndex].time) {
22632
+ tempoChangeIndex = 0;
22633
+ }
22634
+ while (tempoChangeIndex + 1 < state.tempoChanges.length &&
22635
+ state.tempoChanges[tempoChangeIndex + 1].time <= timePosition) {
22636
+ tempoChangeIndex++;
22637
+ }
22638
+ if (tempoChangeIndex !== state.tempoChangeIndex) {
22639
+ state.tempoChangeIndex = tempoChangeIndex;
22640
+ state.currentTempo = state.tempoChanges[state.tempoChangeIndex].bpm;
22641
+ }
22642
+ const syncPoints = state.syncPoints;
22643
+ if (syncPoints.length > 0) {
22644
+ let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22645
+ if (timePosition < syncPoints[syncPointIndex].data.millisecondOffset) {
22646
+ syncPointIndex = 0;
22647
+ }
22648
+ while (syncPointIndex + 1 < syncPoints.length &&
22649
+ syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22650
+ syncPointIndex++;
22651
+ }
22652
+ if (syncPointIndex !== state.syncPointIndex) {
22653
+ state.syncPointIndex = syncPointIndex;
22654
+ state.modifiedTempo = syncPoints[syncPointIndex].data.modifiedTempo;
22655
+ }
22656
+ }
22657
+ else {
22658
+ state.modifiedTempo = state.currentTempo;
22659
+ }
22616
22660
  }
22617
22661
  mainTimePositionFromBackingTrack(timePosition, backingTrackLength) {
22618
22662
  const mainState = this._mainState;
@@ -22620,11 +22664,8 @@
22620
22664
  if (timePosition < 0 || syncPoints.length === 0) {
22621
22665
  return timePosition;
22622
22666
  }
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
- }
22667
+ this.updateCurrentTempo(this._mainState, timePosition);
22668
+ const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22628
22669
  const currentSyncPoint = syncPoints[syncPointIndex];
22629
22670
  const timeDiff = timePosition - currentSyncPoint.data.millisecondOffset;
22630
22671
  let alphaTabTimeDiff;
@@ -22646,7 +22687,10 @@
22646
22687
  return timePosition;
22647
22688
  }
22648
22689
  timePosition *= this.playbackSpeed;
22649
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].time ? mainState.syncPointIndex : 0;
22690
+ let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22691
+ if (timePosition < syncPoints[syncPointIndex].time) {
22692
+ syncPointIndex = 0;
22693
+ }
22650
22694
  while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].time <= timePosition) {
22651
22695
  syncPointIndex++;
22652
22696
  }
@@ -22684,26 +22728,6 @@
22684
22728
  timePosition += tickPosition * (60000.0 / (bpm * state.division));
22685
22729
  return timePosition / playbackSpeed;
22686
22730
  }
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
22731
  get internalEndTime() {
22708
22732
  if (this.isPlayingMain) {
22709
22733
  return !this.mainPlaybackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime;
@@ -26922,7 +26946,7 @@
26922
26946
  return processedEvents;
26923
26947
  }
26924
26948
  processMidiMessage(e) {
26925
- Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26949
+ //Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26926
26950
  const command = e.type;
26927
26951
  switch (command) {
26928
26952
  case MidiEventType.TimeSignature:
@@ -28043,6 +28067,9 @@
28043
28067
  }
28044
28068
  on(value) {
28045
28069
  this._listeners.push(value);
28070
+ return () => {
28071
+ this.off(value);
28072
+ };
28046
28073
  }
28047
28074
  off(value) {
28048
28075
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28062,6 +28089,9 @@
28062
28089
  }
28063
28090
  on(value) {
28064
28091
  this._listeners.push(value);
28092
+ return () => {
28093
+ this.off(value);
28094
+ };
28065
28095
  }
28066
28096
  off(value) {
28067
28097
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28458,7 +28488,7 @@
28458
28488
  endTick = this.sequencer.currentEndTick;
28459
28489
  }
28460
28490
  if (this._tickPosition >= endTick) {
28461
- // fully done with playback of remaining samples?
28491
+ // fully done with playback of remaining samples?
28462
28492
  if (this._notPlayedSamples <= 0) {
28463
28493
  this._notPlayedSamples = 0;
28464
28494
  if (this.sequencer.isPlayingCountIn) {
@@ -35808,6 +35838,27 @@
35808
35838
  controller.moveNext();
35809
35839
  previousMasterBar = bar;
35810
35840
  }
35841
+ // here we interpolate the sync point which marks the end of the sync.
35842
+ // Sync points define new tempos at certain positions.
35843
+ // looking from the last sync point to the end we do not assume the end where the audio ends,
35844
+ // but where it ends according to the BPM and the remaining ticks.
35845
+ if (this.syncPoints.length > 0) {
35846
+ const lastSyncPoint = this.syncPoints[this.syncPoints.length - 1];
35847
+ const endTick = controller.currentTick;
35848
+ const remainingTicks = endTick - lastSyncPoint.tick;
35849
+ if (remainingTicks > 0) {
35850
+ const syncPointData = new SyncPointData();
35851
+ // last occurence of the last bar
35852
+ syncPointData.barOccurence = barOccurence.get(this._score.masterBars.length - 1);
35853
+ // same tempo as last point
35854
+ syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35855
+ // interpolated end from last syncPoint
35856
+ syncPointData.millisecondOffset =
35857
+ lastSyncPoint.data.millisecondOffset +
35858
+ MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35859
+ this.syncPoints.push(new BackingTrackSyncPoint(endTick, syncPointData));
35860
+ }
35861
+ }
35811
35862
  for (const track of this._score.tracks) {
35812
35863
  this._handler.finishTrack(track.index, controller.currentTick);
35813
35864
  }
@@ -38299,6 +38350,357 @@
38299
38350
  }
38300
38351
  }
38301
38352
 
38353
+ /**
38354
+ * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
38355
+ * allowing dynamic changing of the underlying instance without loosing aspects like the
38356
+ * main playback information and event listeners.
38357
+ *
38358
+ * @remarks
38359
+ * This wrapper is used when re-exposing the underlying player via {@link AlphaTabApiBase} to integrators.
38360
+ * Even with dynamic switching between synthesizer, backing tracks etc. aspects like volume, playbackspeed,
38361
+ * event listeners etc. should not be lost.
38362
+ */
38363
+ class AlphaSynthWrapper {
38364
+ constructor() {
38365
+ // relevant state information we want to remember when switching between player instances
38366
+ this._masterVolume = 1;
38367
+ this._metronomeVolume = 0;
38368
+ this._countInVolume = 0;
38369
+ this._playbackSpeed = 1;
38370
+ this._isLooping = false;
38371
+ this._midiEventsPlayedFilter = [];
38372
+ this.ready = new EventEmitter();
38373
+ this.readyForPlayback = new EventEmitter();
38374
+ this.finished = new EventEmitter();
38375
+ this.soundFontLoaded = new EventEmitter();
38376
+ this.soundFontLoadFailed = new EventEmitterOfT();
38377
+ this.midiLoaded = new EventEmitterOfT();
38378
+ this.midiLoadFailed = new EventEmitterOfT();
38379
+ this.stateChanged = new EventEmitterOfT();
38380
+ this.positionChanged = new EventEmitterOfT();
38381
+ this.midiEventsPlayed = new EventEmitterOfT();
38382
+ this.playbackRangeChanged = new EventEmitterOfT();
38383
+ }
38384
+ get instance() {
38385
+ return this._instance;
38386
+ }
38387
+ set instance(value) {
38388
+ this._instance = value;
38389
+ // unregister all events from previous instance
38390
+ const unregister = this._instanceEventUnregister;
38391
+ if (unregister) {
38392
+ for (const e of unregister) {
38393
+ e();
38394
+ }
38395
+ }
38396
+ if (value) {
38397
+ // regsiter to events of new player and forward them to existing listeners
38398
+ const newUnregister = [];
38399
+ newUnregister.push(value.ready.on(() => this.ready.trigger()));
38400
+ newUnregister.push(value.readyForPlayback.on(() => this.readyForPlayback.trigger()));
38401
+ newUnregister.push(value.finished.on(() => this.finished.trigger()));
38402
+ newUnregister.push(value.soundFontLoaded.on(() => this.soundFontLoaded.trigger()));
38403
+ newUnregister.push(value.soundFontLoadFailed.on(e => this.soundFontLoadFailed.trigger(e)));
38404
+ newUnregister.push(value.midiLoaded.on(e => this.midiLoaded.trigger(e)));
38405
+ newUnregister.push(value.midiLoadFailed.on(e => this.midiLoadFailed.trigger(e)));
38406
+ newUnregister.push(value.stateChanged.on(e => this.stateChanged.trigger(e)));
38407
+ newUnregister.push(value.positionChanged.on(e => this.positionChanged.trigger(e)));
38408
+ newUnregister.push(value.midiEventsPlayed.on(e => this.midiEventsPlayed.trigger(e)));
38409
+ newUnregister.push(value.playbackRangeChanged.on(e => this.playbackRangeChanged.trigger(e)));
38410
+ this._instanceEventUnregister = newUnregister;
38411
+ // restore state on new player
38412
+ if (this.isReady) {
38413
+ value.masterVolume = this._masterVolume;
38414
+ value.metronomeVolume = this._metronomeVolume;
38415
+ value.countInVolume = this._countInVolume;
38416
+ value.playbackSpeed = this._playbackSpeed;
38417
+ value.isLooping = this._isLooping;
38418
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38419
+ }
38420
+ else {
38421
+ newUnregister.push(value.ready.on(() => {
38422
+ value.masterVolume = this._masterVolume;
38423
+ value.metronomeVolume = this._metronomeVolume;
38424
+ value.countInVolume = this._countInVolume;
38425
+ value.playbackSpeed = this._playbackSpeed;
38426
+ value.isLooping = this._isLooping;
38427
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38428
+ }));
38429
+ }
38430
+ }
38431
+ else {
38432
+ this._instanceEventUnregister = undefined;
38433
+ }
38434
+ }
38435
+ get output() {
38436
+ return this._instance.output;
38437
+ }
38438
+ get isReady() {
38439
+ return this._instance ? this._instance.isReady : false;
38440
+ }
38441
+ get isReadyForPlayback() {
38442
+ return this._instance ? this._instance.isReadyForPlayback : false;
38443
+ }
38444
+ get state() {
38445
+ return this._instance ? this._instance.state : PlayerState.Paused;
38446
+ }
38447
+ get logLevel() {
38448
+ return Logger.logLevel;
38449
+ }
38450
+ set logLevel(value) {
38451
+ Logger.logLevel = value;
38452
+ if (this._instance) {
38453
+ this._instance.logLevel = value;
38454
+ }
38455
+ }
38456
+ get masterVolume() {
38457
+ return this._masterVolume;
38458
+ }
38459
+ set masterVolume(value) {
38460
+ value = Math.max(value, SynthConstants.MinVolume);
38461
+ this._masterVolume = value;
38462
+ if (this._instance) {
38463
+ this._instance.masterVolume = value;
38464
+ }
38465
+ }
38466
+ get metronomeVolume() {
38467
+ return this._metronomeVolume;
38468
+ }
38469
+ set metronomeVolume(value) {
38470
+ value = Math.max(value, SynthConstants.MinVolume);
38471
+ this._metronomeVolume = value;
38472
+ if (this._instance) {
38473
+ this._instance.metronomeVolume = value;
38474
+ }
38475
+ }
38476
+ get playbackSpeed() {
38477
+ return this._playbackSpeed;
38478
+ }
38479
+ set playbackSpeed(value) {
38480
+ this._playbackSpeed = value;
38481
+ if (this._instance) {
38482
+ this._instance.playbackSpeed = value;
38483
+ }
38484
+ }
38485
+ get tickPosition() {
38486
+ return this._instance ? this._instance.tickPosition : 0;
38487
+ }
38488
+ set tickPosition(value) {
38489
+ if (this._instance) {
38490
+ this._instance.tickPosition = value;
38491
+ }
38492
+ }
38493
+ get timePosition() {
38494
+ return this._instance ? this._instance.timePosition : 0;
38495
+ }
38496
+ set timePosition(value) {
38497
+ if (this._instance) {
38498
+ this._instance.timePosition = value;
38499
+ }
38500
+ }
38501
+ get playbackRange() {
38502
+ return this._instance ? this._instance.playbackRange : null;
38503
+ }
38504
+ set playbackRange(value) {
38505
+ if (this._instance) {
38506
+ this._instance.playbackRange = value;
38507
+ }
38508
+ }
38509
+ get isLooping() {
38510
+ return this._isLooping;
38511
+ }
38512
+ set isLooping(value) {
38513
+ this._isLooping = value;
38514
+ if (this._instance) {
38515
+ this._instance.isLooping = value;
38516
+ }
38517
+ }
38518
+ get countInVolume() {
38519
+ return this._countInVolume;
38520
+ }
38521
+ set countInVolume(value) {
38522
+ this._countInVolume = value;
38523
+ if (this._instance) {
38524
+ this._instance.countInVolume = value;
38525
+ }
38526
+ }
38527
+ get midiEventsPlayedFilter() {
38528
+ return this._midiEventsPlayedFilter;
38529
+ }
38530
+ set midiEventsPlayedFilter(value) {
38531
+ this._midiEventsPlayedFilter = value;
38532
+ if (this._instance) {
38533
+ this._instance.midiEventsPlayedFilter = value;
38534
+ }
38535
+ }
38536
+ destroy() {
38537
+ if (this._instance) {
38538
+ this._instance.destroy();
38539
+ this._instance = undefined;
38540
+ }
38541
+ }
38542
+ play() {
38543
+ return this._instance ? this._instance.play() : false;
38544
+ }
38545
+ pause() {
38546
+ if (this._instance) {
38547
+ this._instance.pause();
38548
+ }
38549
+ }
38550
+ playPause() {
38551
+ if (this._instance) {
38552
+ this._instance.playPause();
38553
+ }
38554
+ }
38555
+ stop() {
38556
+ if (this._instance) {
38557
+ this._instance.stop();
38558
+ }
38559
+ }
38560
+ playOneTimeMidiFile(midi) {
38561
+ if (this._instance) {
38562
+ this._instance.playOneTimeMidiFile(midi);
38563
+ }
38564
+ }
38565
+ loadSoundFont(data, append) {
38566
+ if (this._instance) {
38567
+ this._instance.loadSoundFont(data, append);
38568
+ }
38569
+ }
38570
+ resetSoundFonts() {
38571
+ if (this._instance) {
38572
+ this._instance.resetSoundFonts();
38573
+ }
38574
+ }
38575
+ loadMidiFile(midi) {
38576
+ if (this._instance) {
38577
+ this._instance.loadMidiFile(midi);
38578
+ }
38579
+ }
38580
+ loadBackingTrack(score, syncPoints) {
38581
+ if (this._instance) {
38582
+ this._instance.loadBackingTrack(score, syncPoints);
38583
+ }
38584
+ }
38585
+ applyTranspositionPitches(transpositionPitches) {
38586
+ if (this._instance) {
38587
+ this._instance.applyTranspositionPitches(transpositionPitches);
38588
+ }
38589
+ }
38590
+ setChannelTranspositionPitch(channel, semitones) {
38591
+ if (this._instance) {
38592
+ this._instance.setChannelTranspositionPitch(channel, semitones);
38593
+ }
38594
+ }
38595
+ setChannelMute(channel, mute) {
38596
+ if (this._instance) {
38597
+ this._instance.setChannelMute(channel, mute);
38598
+ }
38599
+ }
38600
+ resetChannelStates() {
38601
+ if (this._instance) {
38602
+ this._instance.resetChannelStates();
38603
+ }
38604
+ }
38605
+ setChannelSolo(channel, solo) {
38606
+ if (this._instance) {
38607
+ this._instance.setChannelSolo(channel, solo);
38608
+ }
38609
+ }
38610
+ setChannelVolume(channel, volume) {
38611
+ if (this._instance) {
38612
+ this._instance.setChannelVolume(channel, volume);
38613
+ }
38614
+ }
38615
+ }
38616
+
38617
+ /**
38618
+ * A {@link IScoreRenderer} implementation wrapping and underling other {@link IScoreRenderer}
38619
+ * allowing dynamic changing of the underlying instance without loosing aspects like the
38620
+ * event listeners.
38621
+ */
38622
+ class ScoreRendererWrapper {
38623
+ constructor() {
38624
+ this._width = 0;
38625
+ this._score = null;
38626
+ this._trackIndexes = null;
38627
+ this.preRender = new EventEmitterOfT();
38628
+ this.renderFinished = new EventEmitterOfT();
38629
+ this.partialRenderFinished = new EventEmitterOfT();
38630
+ this.partialLayoutFinished = new EventEmitterOfT();
38631
+ this.postRenderFinished = new EventEmitter();
38632
+ this.error = new EventEmitterOfT();
38633
+ }
38634
+ get instance() {
38635
+ return this._instance;
38636
+ }
38637
+ set instance(value) {
38638
+ this._instance = value;
38639
+ // unregister all events from previous instance
38640
+ const unregister = this._instanceEventUnregister;
38641
+ if (unregister) {
38642
+ for (const e of unregister) {
38643
+ e();
38644
+ }
38645
+ }
38646
+ if (value) {
38647
+ // regsiter to events of new player and forward them to existing listeners
38648
+ const newUnregister = [];
38649
+ newUnregister.push(value.preRender.on(v => this.preRender.trigger(v)));
38650
+ newUnregister.push(value.renderFinished.on(v => this.renderFinished.trigger(v)));
38651
+ newUnregister.push(value.partialRenderFinished.on(v => this.partialRenderFinished.trigger(v)));
38652
+ newUnregister.push(value.partialLayoutFinished.on(v => this.partialLayoutFinished.trigger(v)));
38653
+ newUnregister.push(value.postRenderFinished.on(() => this.postRenderFinished.trigger()));
38654
+ newUnregister.push(value.error.on(v => this.error.trigger(v)));
38655
+ this._instanceEventUnregister = newUnregister;
38656
+ if (this._settings) {
38657
+ value.updateSettings(this._settings);
38658
+ }
38659
+ value.width = this._width;
38660
+ if (this._score !== null) {
38661
+ value.renderScore(this._score, this._trackIndexes);
38662
+ }
38663
+ }
38664
+ else {
38665
+ this._instanceEventUnregister = undefined;
38666
+ }
38667
+ }
38668
+ get boundsLookup() {
38669
+ return this._instance ? this._instance.boundsLookup : null;
38670
+ }
38671
+ get width() {
38672
+ return this._instance ? this._instance.width : 0;
38673
+ }
38674
+ set width(value) {
38675
+ this._width = value;
38676
+ if (this._instance) {
38677
+ this._instance.width = value;
38678
+ }
38679
+ }
38680
+ render() {
38681
+ this._instance?.render();
38682
+ }
38683
+ resizeRender() {
38684
+ this._instance?.resizeRender();
38685
+ }
38686
+ renderScore(score, trackIndexes) {
38687
+ this._score = score;
38688
+ this._trackIndexes = trackIndexes;
38689
+ this._instance?.renderScore(score, trackIndexes);
38690
+ }
38691
+ renderResult(resultId) {
38692
+ this._instance?.renderResult(resultId);
38693
+ }
38694
+ updateSettings(settings) {
38695
+ this._settings = settings;
38696
+ this._instance?.updateSettings(settings);
38697
+ }
38698
+ destroy() {
38699
+ this._instance?.destroy();
38700
+ this._instance = undefined;
38701
+ }
38702
+ }
38703
+
38302
38704
  class SelectionInfo {
38303
38705
  constructor(beat) {
38304
38706
  this.bounds = null;
@@ -38318,6 +38720,18 @@
38318
38720
  get actualPlayerMode() {
38319
38721
  return this._actualPlayerMode;
38320
38722
  }
38723
+ /**
38724
+ * The score renderer used for rendering the music sheet.
38725
+ * @remarks
38726
+ * This is the low-level API responsible for the actual rendering engine.
38727
+ * Gets access to the underling {@link IScoreRenderer} that is used for the rendering.
38728
+ *
38729
+ * @category Properties - Core
38730
+ * @since 0.9.4
38731
+ */
38732
+ get renderer() {
38733
+ return this._renderer;
38734
+ }
38321
38735
  /**
38322
38736
  * The score holding all information about the song being rendered
38323
38737
  * @category Properties - Core
@@ -38389,43 +38803,14 @@
38389
38803
  this._tracks = [];
38390
38804
  this._actualPlayerMode = exports.PlayerMode.Disabled;
38391
38805
  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
38806
  this._cursorWrapper = null;
38422
38807
  this._barCursor = null;
38423
38808
  this._beatCursor = null;
38424
38809
  this._selectionWrapper = null;
38425
38810
  this._previousTick = 0;
38426
- this._playerState = PlayerState.Paused;
38427
38811
  this._currentBeat = null;
38428
- this._currentBarBounds = null;
38812
+ this._currentBeatBounds = null;
38813
+ this._isInitialBeatCursorUpdate = true;
38429
38814
  this._previousStateForCursor = PlayerState.Paused;
38430
38815
  this._previousCursorCache = null;
38431
38816
  this._lastScroll = 0;
@@ -38996,133 +39381,6 @@
38996
39381
  *
38997
39382
  */
38998
39383
  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
39384
  /**
39127
39385
  * This event is fired when a Midi file is being loaded.
39128
39386
  *
@@ -39211,176 +39469,18 @@
39211
39469
  */
39212
39470
  this.midiLoaded = new EventEmitterOfT();
39213
39471
  /**
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.
39472
+ * This event is fired when a settings update was requested.
39298
39473
  *
39299
39474
  * @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
39475
+ * @category Events - Core
39476
+ * @since 1.6.0
39373
39477
  *
39374
39478
  * @example
39375
39479
  * JavaScript
39376
39480
  * ```js
39377
39481
  * 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
- * }
39482
+ * api.settingsUpdated.on(() => {
39483
+ * updateSettingsUI(api.settings);
39384
39484
  * });
39385
39485
  * ```
39386
39486
  *
@@ -39388,16 +39488,9 @@
39388
39488
  * C#
39389
39489
  * ```cs
39390
39490
  * var api = new AlphaTabApi<MyControl>(...);
39391
- * api.PlaybackRangeChanged.On(args =>
39491
+ * api.SettingsUpdated.On(() =>
39392
39492
  * {
39393
- * if (args.PlaybackRange != null)
39394
- * {
39395
- * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
39396
- * }
39397
- * else
39398
- * {
39399
- * ClearHighlightInProgressBar();
39400
- * }
39493
+ * UpdateSettingsUI(api.settings);
39401
39494
  * });
39402
39495
  * ```
39403
39496
  *
@@ -39405,21 +39498,12 @@
39405
39498
  * Android
39406
39499
  * ```kotlin
39407
39500
  * 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
- * }
39501
+ * api.SettingsUpdated.on {
39502
+ * updateSettingsUI(api.settings)
39415
39503
  * }
39416
39504
  * ```
39417
39505
  *
39418
39506
  */
39419
- this.playbackRangeChanged = new EventEmitterOfT();
39420
- /**
39421
- * @internal
39422
- */
39423
39507
  this.settingsUpdated = new EventEmitter();
39424
39508
  this.uiFacade = uiFacade;
39425
39509
  this.container = uiFacade.rootContainer;
@@ -39432,48 +39516,49 @@
39432
39516
  Environment.printEnvironmentInfo(false);
39433
39517
  this.canvasElement = uiFacade.createCanvasElement();
39434
39518
  this.container.appendChild(this.canvasElement);
39519
+ this._renderer = new ScoreRendererWrapper();
39435
39520
  if (this.settings.core.useWorkers &&
39436
39521
  this.uiFacade.areWorkersSupported &&
39437
39522
  Environment.getRenderEngineFactory(this.settings.core.engine).supportsWorkers) {
39438
- this.renderer = this.uiFacade.createWorkerRenderer();
39523
+ this._renderer.instance = this.uiFacade.createWorkerRenderer();
39439
39524
  }
39440
39525
  else {
39441
- this.renderer = new ScoreRenderer(this.settings);
39526
+ this._renderer.instance = new ScoreRenderer(this.settings);
39442
39527
  }
39443
39528
  this.container.resize.on(Environment.throttle(() => {
39444
39529
  if (this._isDestroyed) {
39445
39530
  return;
39446
39531
  }
39447
- if (this.container.width !== this.renderer.width) {
39532
+ if (this.container.width !== this._renderer.width) {
39448
39533
  this.triggerResize();
39449
39534
  }
39450
39535
  }, uiFacade.resizeThrottle));
39451
39536
  const initialResizeEventInfo = new ResizeEventArgs();
39452
- initialResizeEventInfo.oldWidth = this.renderer.width;
39537
+ initialResizeEventInfo.oldWidth = this._renderer.width;
39453
39538
  initialResizeEventInfo.newWidth = this.container.width | 0;
39454
39539
  initialResizeEventInfo.settings = this.settings;
39455
39540
  this.onResize(initialResizeEventInfo);
39456
- this.renderer.preRender.on(this.onRenderStarted.bind(this));
39457
- this.renderer.renderFinished.on(renderingResult => {
39541
+ this._renderer.preRender.on(this.onRenderStarted.bind(this));
39542
+ this._renderer.renderFinished.on(renderingResult => {
39458
39543
  this.onRenderFinished(renderingResult);
39459
39544
  });
39460
- this.renderer.postRenderFinished.on(() => {
39545
+ this._renderer.postRenderFinished.on(() => {
39461
39546
  const duration = Date.now() - this._startTime;
39462
39547
  Logger.debug('rendering', `Rendering completed in ${duration}ms`);
39463
39548
  this.onPostRenderFinished();
39464
39549
  });
39465
- this.renderer.preRender.on(_ => {
39550
+ this._renderer.preRender.on(_ => {
39466
39551
  this._startTime = Date.now();
39467
39552
  });
39468
- this.renderer.partialLayoutFinished.on(this.appendRenderResult.bind(this));
39469
- this.renderer.partialRenderFinished.on(this.updateRenderResult.bind(this));
39470
- this.renderer.renderFinished.on(r => {
39471
- this.appendRenderResult(r);
39472
- this.appendRenderResult(null); // marks last element
39553
+ this._renderer.partialLayoutFinished.on(r => this.appendRenderResult(r, false));
39554
+ this._renderer.partialRenderFinished.on(this.updateRenderResult.bind(this));
39555
+ this._renderer.renderFinished.on(r => {
39556
+ this.appendRenderResult(r, true);
39473
39557
  });
39474
- this.renderer.error.on(this.onError.bind(this));
39558
+ this._renderer.error.on(this.onError.bind(this));
39559
+ this.setupPlayerWrapper();
39475
39560
  if (this.settings.player.playerMode !== exports.PlayerMode.Disabled) {
39476
- this.setupPlayer();
39561
+ this.setupOrDestroyPlayer();
39477
39562
  }
39478
39563
  this.setupClickHandling();
39479
39564
  // delay rendering to allow ui to hook up with events first.
@@ -39481,6 +39566,36 @@
39481
39566
  this.uiFacade.initialRender();
39482
39567
  });
39483
39568
  }
39569
+ setupPlayerWrapper() {
39570
+ const player = new AlphaSynthWrapper();
39571
+ this._player = player;
39572
+ player.ready.on(() => {
39573
+ this.loadMidiForScore();
39574
+ });
39575
+ player.readyForPlayback.on(() => {
39576
+ this.onPlayerReady();
39577
+ if (this.tracks) {
39578
+ for (const track of this.tracks) {
39579
+ const volume = track.playbackInfo.volume / 16;
39580
+ player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
39581
+ player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
39582
+ }
39583
+ }
39584
+ });
39585
+ player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
39586
+ player.soundFontLoadFailed.on(e => {
39587
+ this.onError(e);
39588
+ });
39589
+ player.midiLoaded.on(this.onMidiLoaded.bind(this));
39590
+ player.midiLoadFailed.on(e => {
39591
+ this.onError(e);
39592
+ });
39593
+ player.stateChanged.on(this.onPlayerStateChanged.bind(this));
39594
+ player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
39595
+ player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
39596
+ player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
39597
+ player.finished.on(this.onPlayerFinished.bind(this));
39598
+ }
39484
39599
  /**
39485
39600
  * Destroys the alphaTab control and restores the initial state of the UI.
39486
39601
  * @remarks
@@ -39513,11 +39628,9 @@
39513
39628
  */
39514
39629
  destroy() {
39515
39630
  this._isDestroyed = true;
39516
- if (this.player) {
39517
- this.player.destroy();
39518
- }
39631
+ this._player.destroy();
39519
39632
  this.uiFacade.destroy();
39520
- this.renderer.destroy();
39633
+ this._renderer.destroy();
39521
39634
  }
39522
39635
  /**
39523
39636
  * Applies any changes that were done to the settings object.
@@ -39562,17 +39675,29 @@
39562
39675
  if (score) {
39563
39676
  ModelUtils.applyPitchOffsets(this.settings, score);
39564
39677
  }
39565
- 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();
39678
+ this.updateRenderer();
39679
+ this._renderer.updateSettings(this.settings);
39680
+ this.setupOrDestroyPlayer();
39681
+ this.onSettingsUpdated();
39682
+ }
39683
+ updateRenderer() {
39684
+ const renderer = this._renderer;
39685
+ if (this.settings.core.useWorkers &&
39686
+ this.uiFacade.areWorkersSupported &&
39687
+ Environment.getRenderEngineFactory(this.settings.core.engine).supportsWorkers) {
39688
+ // switch from non-worker to worker renderer
39689
+ if (renderer.instance instanceof ScoreRenderer) {
39690
+ renderer.destroy();
39691
+ renderer.instance = this.uiFacade.createWorkerRenderer();
39570
39692
  }
39571
39693
  }
39572
39694
  else {
39573
- this.destroyPlayer();
39695
+ // switch from worker to non-worker renderer
39696
+ if (!(renderer.instance instanceof ScoreRenderer)) {
39697
+ renderer.destroy();
39698
+ renderer.instance = new ScoreRenderer(this.settings);
39699
+ }
39574
39700
  }
39575
- this.onSettingsUpdated();
39576
39701
  }
39577
39702
  /**
39578
39703
  * Initiates a load of the score using the given data.
@@ -39768,30 +39893,33 @@
39768
39893
  }
39769
39894
  else {
39770
39895
  const resizeEventInfo = new ResizeEventArgs();
39771
- resizeEventInfo.oldWidth = this.renderer.width;
39896
+ resizeEventInfo.oldWidth = this._renderer.width;
39772
39897
  resizeEventInfo.newWidth = this.container.width;
39773
39898
  resizeEventInfo.settings = this.settings;
39774
39899
  this.onResize(resizeEventInfo);
39775
- this.renderer.updateSettings(this.settings);
39776
- this.renderer.width = this.container.width;
39777
- this.renderer.resizeRender();
39900
+ this._renderer.updateSettings(this.settings);
39901
+ this._renderer.width = this.container.width;
39902
+ this._renderer.resizeRender();
39778
39903
  }
39779
39904
  }
39780
- appendRenderResult(result) {
39781
- if (result) {
39905
+ appendRenderResult(result, isLast) {
39906
+ // resizing the canvas and wrapper elements at the end is enough
39907
+ // it avoids flickering on resizes and re-renders.
39908
+ // the individual partials are anyhow sized correctly
39909
+ if (isLast) {
39782
39910
  this.canvasElement.width = result.totalWidth;
39783
39911
  this.canvasElement.height = result.totalHeight;
39784
39912
  if (this._cursorWrapper) {
39785
39913
  this._cursorWrapper.width = result.totalWidth;
39786
39914
  this._cursorWrapper.height = result.totalHeight;
39787
39915
  }
39788
- if (result.width > 0 || result.height > 0) {
39789
- this.uiFacade.beginAppendRenderResults(result);
39790
- }
39791
39916
  }
39792
- else {
39917
+ if (result.width > 0 || result.height > 0) {
39793
39918
  this.uiFacade.beginAppendRenderResults(result);
39794
39919
  }
39920
+ if (isLast) {
39921
+ this.uiFacade.beginAppendRenderResults(null);
39922
+ }
39795
39923
  }
39796
39924
  updateRenderResult(result) {
39797
39925
  if (result && result.renderResult) {
@@ -39877,9 +40005,6 @@
39877
40005
  * ```
39878
40006
  */
39879
40007
  loadSoundFont(data, append = false) {
39880
- if (!this.player) {
39881
- return false;
39882
- }
39883
40008
  return this.uiFacade.loadSoundFont(data, append);
39884
40009
  }
39885
40010
  /**
@@ -39925,10 +40050,7 @@
39925
40050
  * ```
39926
40051
  */
39927
40052
  resetSoundFonts() {
39928
- if (!this.player) {
39929
- return;
39930
- }
39931
- this.player.resetSoundFonts();
40053
+ this._player.resetSoundFonts();
39932
40054
  }
39933
40055
  /**
39934
40056
  * Initiates a re-rendering of the current setup.
@@ -39959,13 +40081,10 @@
39959
40081
  * ```
39960
40082
  */
39961
40083
  render() {
39962
- if (!this.renderer) {
39963
- return;
39964
- }
39965
40084
  if (this.uiFacade.canRender) {
39966
40085
  // when font is finally loaded, start rendering
39967
- this.renderer.width = this.container.width;
39968
- this.renderer.renderScore(this.score, this._trackIndexes);
40086
+ this._renderer.width = this.container.width;
40087
+ this._renderer.renderScore(this.score, this._trackIndexes);
39969
40088
  }
39970
40089
  else {
39971
40090
  this.uiFacade.canRenderChanged.on(() => this.render());
@@ -40040,7 +40159,38 @@
40040
40159
  * @since 1.5.0
40041
40160
  */
40042
40161
  get boundsLookup() {
40043
- return this.renderer.boundsLookup;
40162
+ return this._renderer.boundsLookup;
40163
+ }
40164
+ /**
40165
+ * The alphaSynth player used for playback.
40166
+ * @remarks
40167
+ * This is the low-level API to the Midi synthesizer used for playback.
40168
+ * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
40169
+ * @category Properties - Player
40170
+ * @since 0.9.4
40171
+ * @example
40172
+ * JavaScript
40173
+ * ```js
40174
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
40175
+ * setupPlayerEvents(api.settings);
40176
+ * ```
40177
+ *
40178
+ * @example
40179
+ * C#
40180
+ * ```cs
40181
+ * var api = new AlphaTabApi<MyControl>(...);
40182
+ * SetupPlayerEvents(api.Player);
40183
+ * ```
40184
+ *
40185
+ * @example
40186
+ * Android
40187
+ * ```kotlin
40188
+ * val api = AlphaTabApi<MyControl>(...)
40189
+ * setupPlayerEvents(api.player)
40190
+ * ```
40191
+ */
40192
+ get player() {
40193
+ return this._player.instance ? this._player : null;
40044
40194
  }
40045
40195
  /**
40046
40196
  * Whether the player is ready for starting the playback.
@@ -40071,10 +40221,7 @@
40071
40221
  * ```
40072
40222
  */
40073
40223
  get isReadyForPlayback() {
40074
- if (!this.player) {
40075
- return false;
40076
- }
40077
- return this.player.isReadyForPlayback;
40224
+ return this._player.isReadyForPlayback;
40078
40225
  }
40079
40226
  /**
40080
40227
  * The current player state.
@@ -40104,10 +40251,7 @@
40104
40251
  * ```
40105
40252
  */
40106
40253
  get playerState() {
40107
- if (!this.player) {
40108
- return PlayerState.Paused;
40109
- }
40110
- return this.player.state;
40254
+ return this._player.state;
40111
40255
  }
40112
40256
  /**
40113
40257
  * The current master volume as percentage (0-1).
@@ -40137,15 +40281,10 @@
40137
40281
  * ```
40138
40282
  */
40139
40283
  get masterVolume() {
40140
- if (!this.player) {
40141
- return 0;
40142
- }
40143
- return this.player.masterVolume;
40284
+ return this._player.masterVolume;
40144
40285
  }
40145
40286
  set masterVolume(value) {
40146
- if (this.player) {
40147
- this.player.masterVolume = value;
40148
- }
40287
+ this._player.masterVolume = value;
40149
40288
  }
40150
40289
  /**
40151
40290
  * The metronome volume as percentage (0-1).
@@ -40176,15 +40315,10 @@
40176
40315
  * ```
40177
40316
  */
40178
40317
  get metronomeVolume() {
40179
- if (!this.player) {
40180
- return 0;
40181
- }
40182
- return this.player.metronomeVolume;
40318
+ return this._player.metronomeVolume;
40183
40319
  }
40184
40320
  set metronomeVolume(value) {
40185
- if (this.player) {
40186
- this.player.metronomeVolume = value;
40187
- }
40321
+ this._player.metronomeVolume = value;
40188
40322
  }
40189
40323
  /**
40190
40324
  * The volume of the count-in metronome ticks.
@@ -40215,15 +40349,10 @@
40215
40349
  * ```
40216
40350
  */
40217
40351
  get countInVolume() {
40218
- if (!this.player) {
40219
- return 0;
40220
- }
40221
- return this.player.countInVolume;
40352
+ return this._player.countInVolume;
40222
40353
  }
40223
40354
  set countInVolume(value) {
40224
- if (this.player) {
40225
- this.player.countInVolume = value;
40226
- }
40355
+ this._player.countInVolume = value;
40227
40356
  }
40228
40357
  /**
40229
40358
  * The midi events which will trigger the `midiEventsPlayed` event
@@ -40282,15 +40411,10 @@
40282
40411
  * ```
40283
40412
  */
40284
40413
  get midiEventsPlayedFilter() {
40285
- if (!this.player) {
40286
- return [];
40287
- }
40288
- return this.player.midiEventsPlayedFilter;
40414
+ return this._player.midiEventsPlayedFilter;
40289
40415
  }
40290
40416
  set midiEventsPlayedFilter(value) {
40291
- if (this.player) {
40292
- this.player.midiEventsPlayedFilter = value;
40293
- }
40417
+ this._player.midiEventsPlayedFilter = value;
40294
40418
  }
40295
40419
  /**
40296
40420
  * The position within the song in midi ticks.
@@ -40318,15 +40442,10 @@
40318
40442
  * ```
40319
40443
  */
40320
40444
  get tickPosition() {
40321
- if (!this.player) {
40322
- return 0;
40323
- }
40324
- return this.player.tickPosition;
40445
+ return this._player.tickPosition;
40325
40446
  }
40326
40447
  set tickPosition(value) {
40327
- if (this.player) {
40328
- this.player.tickPosition = value;
40329
- }
40448
+ this._player.tickPosition = value;
40330
40449
  }
40331
40450
  /**
40332
40451
  * The position within the song in milliseconds
@@ -40354,15 +40473,10 @@
40354
40473
  * ```
40355
40474
  */
40356
40475
  get timePosition() {
40357
- if (!this.player) {
40358
- return 0;
40359
- }
40360
- return this.player.timePosition;
40476
+ return this._player.timePosition;
40361
40477
  }
40362
40478
  set timePosition(value) {
40363
- if (this.player) {
40364
- this.player.timePosition = value;
40365
- }
40479
+ this._player.timePosition = value;
40366
40480
  }
40367
40481
  /**
40368
40482
  * The range of the song that should be played.
@@ -40396,17 +40510,12 @@
40396
40510
  * ```
40397
40511
  */
40398
40512
  get playbackRange() {
40399
- if (!this.player) {
40400
- return null;
40401
- }
40402
- return this.player.playbackRange;
40513
+ return this._player.playbackRange;
40403
40514
  }
40404
40515
  set playbackRange(value) {
40405
- if (this.player) {
40406
- this.player.playbackRange = value;
40407
- if (this.settings.player.enableCursor) {
40408
- this.updateSelectionCursor(value);
40409
- }
40516
+ this._player.playbackRange = value;
40517
+ if (this.settings.player.enableCursor) {
40518
+ this.updateSelectionCursor(value);
40410
40519
  }
40411
40520
  }
40412
40521
  /**
@@ -40438,15 +40547,10 @@
40438
40547
  * ```
40439
40548
  */
40440
40549
  get playbackSpeed() {
40441
- if (!this.player) {
40442
- return 0;
40443
- }
40444
- return this.player.playbackSpeed;
40550
+ return this._player.playbackSpeed;
40445
40551
  }
40446
40552
  set playbackSpeed(value) {
40447
- if (this.player) {
40448
- this.player.playbackSpeed = value;
40449
- }
40553
+ this._player.playbackSpeed = value;
40450
40554
  }
40451
40555
  /**
40452
40556
  * Whether the playback should automatically restart after it finished.
@@ -40477,27 +40581,21 @@
40477
40581
  * ```
40478
40582
  */
40479
40583
  get isLooping() {
40480
- if (!this.player) {
40481
- return false;
40482
- }
40483
- return this.player.isLooping;
40584
+ return this._player.isLooping;
40484
40585
  }
40485
40586
  set isLooping(value) {
40486
- if (this.player) {
40487
- this.player.isLooping = value;
40488
- }
40587
+ this._player.isLooping = value;
40489
40588
  }
40490
40589
  destroyPlayer() {
40491
- if (!this.player) {
40492
- return;
40493
- }
40494
- this.player.destroy();
40495
- this.player = null;
40590
+ this._player.destroy();
40496
40591
  this._previousTick = 0;
40497
- this._playerState = PlayerState.Paused;
40498
40592
  this.destroyCursors();
40499
40593
  }
40500
- setupPlayer() {
40594
+ /**
40595
+ *
40596
+ * @returns true if a new player was created, false if no player was created (includes destroy & reuse of the current one)
40597
+ */
40598
+ setupOrDestroyPlayer() {
40501
40599
  let mode = this.settings.player.playerMode;
40502
40600
  if (mode === exports.PlayerMode.EnabledAutomatic) {
40503
40601
  const score = this.score;
@@ -40511,68 +40609,44 @@
40511
40609
  mode = exports.PlayerMode.EnabledSynthesizer;
40512
40610
  }
40513
40611
  }
40612
+ let newPlayer = null;
40514
40613
  if (mode !== this._actualPlayerMode) {
40515
40614
  this.destroyPlayer();
40615
+ this.updateCursors();
40616
+ switch (mode) {
40617
+ case exports.PlayerMode.Disabled:
40618
+ newPlayer = null;
40619
+ break;
40620
+ case exports.PlayerMode.EnabledSynthesizer:
40621
+ newPlayer = this.uiFacade.createWorkerPlayer();
40622
+ break;
40623
+ case exports.PlayerMode.EnabledBackingTrack:
40624
+ newPlayer = this.uiFacade.createBackingTrackPlayer();
40625
+ break;
40626
+ case exports.PlayerMode.EnabledExternalMedia:
40627
+ newPlayer = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40628
+ break;
40629
+ }
40516
40630
  }
40517
- this.updateCursors();
40518
- 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;
40631
+ else {
40632
+ // no change in player mode, just update song info if needed
40633
+ this.updateCursors();
40634
+ return false;
40543
40635
  }
40544
- if (!this.player) {
40636
+ this._actualPlayerMode = mode;
40637
+ if (!newPlayer) {
40545
40638
  return false;
40546
40639
  }
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();
40640
+ this._player.instance = newPlayer;
40574
40641
  return false;
40575
40642
  }
40643
+ /**
40644
+ * Re-creates the midi for the current score and loads it.
40645
+ * @remarks
40646
+ * This will result in the player to stop playback. Some setting changes require re-genration of the midi song.
40647
+ * @category Methods - Player
40648
+ * @since 1.6.0
40649
+ */
40576
40650
  loadMidiForScore() {
40577
40651
  if (!this.score) {
40578
40652
  return;
@@ -40590,12 +40664,10 @@
40590
40664
  generator.generate();
40591
40665
  this._tickCache = generator.tickLookup;
40592
40666
  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
- }
40667
+ const player = this._player;
40668
+ player.loadMidiFile(midiFile);
40669
+ player.loadBackingTrack(score, generator.syncPoints);
40670
+ player.applyTranspositionPitches(generator.transpositionPitches);
40599
40671
  }
40600
40672
  /**
40601
40673
  * Changes the volume of the given tracks.
@@ -40633,12 +40705,9 @@
40633
40705
  * ```
40634
40706
  */
40635
40707
  changeTrackVolume(tracks, volume) {
40636
- if (!this.player) {
40637
- return;
40638
- }
40639
40708
  for (const track of tracks) {
40640
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40641
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40709
+ this._player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40710
+ this._player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40642
40711
  }
40643
40712
  }
40644
40713
  /**
@@ -40675,12 +40744,9 @@
40675
40744
  * ```
40676
40745
  */
40677
40746
  changeTrackSolo(tracks, solo) {
40678
- if (!this.player) {
40679
- return;
40680
- }
40681
40747
  for (const track of tracks) {
40682
- this.player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40683
- this.player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40748
+ this._player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40749
+ this._player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40684
40750
  }
40685
40751
  }
40686
40752
  /**
@@ -40716,12 +40782,9 @@
40716
40782
  * ```
40717
40783
  */
40718
40784
  changeTrackMute(tracks, mute) {
40719
- if (!this.player) {
40720
- return;
40721
- }
40722
40785
  for (const track of tracks) {
40723
- this.player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40724
- this.player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40786
+ this._player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40787
+ this._player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40725
40788
  }
40726
40789
  }
40727
40790
  /**
@@ -40759,12 +40822,9 @@
40759
40822
  * ```
40760
40823
  */
40761
40824
  changeTrackTranspositionPitch(tracks, semitones) {
40762
- if (!this.player) {
40763
- return;
40764
- }
40765
40825
  for (const track of tracks) {
40766
- this.player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40767
- this.player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40826
+ this._player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40827
+ this._player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40768
40828
  }
40769
40829
  }
40770
40830
  /**
@@ -40795,10 +40855,7 @@
40795
40855
  * ```
40796
40856
  */
40797
40857
  play() {
40798
- if (!this.player) {
40799
- return false;
40800
- }
40801
- return this.player.play();
40858
+ return this._player.play();
40802
40859
  }
40803
40860
  /**
40804
40861
  * Pauses the playback of the current song.
@@ -40827,10 +40884,7 @@
40827
40884
  * ```
40828
40885
  */
40829
40886
  pause() {
40830
- if (!this.player) {
40831
- return;
40832
- }
40833
- this.player.pause();
40887
+ this._player.pause();
40834
40888
  }
40835
40889
  /**
40836
40890
  * Toggles between play/pause depending on the current player state.
@@ -40861,10 +40915,7 @@
40861
40915
  * ```
40862
40916
  */
40863
40917
  playPause() {
40864
- if (!this.player) {
40865
- return;
40866
- }
40867
- this.player.playPause();
40918
+ this._player.playPause();
40868
40919
  }
40869
40920
  /**
40870
40921
  * Stops the playback of the current song, and moves the playback position back to the start.
@@ -40895,10 +40946,7 @@
40895
40946
  * ```
40896
40947
  */
40897
40948
  stop() {
40898
- if (!this.player) {
40899
- return;
40900
- }
40901
- this.player.stop();
40949
+ this._player.stop();
40902
40950
  }
40903
40951
  /**
40904
40952
  * Triggers the play of the given beat.
@@ -40934,15 +40982,12 @@
40934
40982
  * ```
40935
40983
  */
40936
40984
  playBeat(beat) {
40937
- if (!this.player) {
40938
- return;
40939
- }
40940
40985
  // we generate a new midi file containing only the beat
40941
40986
  const midiFile = new MidiFile();
40942
40987
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40943
40988
  const generator = new MidiFileGenerator(beat.voice.bar.staff.track.score, this.settings, handler);
40944
40989
  generator.generateSingleBeat(beat);
40945
- this.player.playOneTimeMidiFile(midiFile);
40990
+ this._player.playOneTimeMidiFile(midiFile);
40946
40991
  }
40947
40992
  /**
40948
40993
  * Triggers the play of the given note.
@@ -40977,15 +41022,12 @@
40977
41022
  * ```
40978
41023
  */
40979
41024
  playNote(note) {
40980
- if (!this.player) {
40981
- return;
40982
- }
40983
41025
  // we generate a new midi file containing only the beat
40984
41026
  const midiFile = new MidiFile();
40985
41027
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40986
41028
  const generator = new MidiFileGenerator(note.beat.voice.bar.staff.track.score, this.settings, handler);
40987
41029
  generator.generateSingleNote(note);
40988
- this.player.playOneTimeMidiFile(midiFile);
41030
+ this._player.playOneTimeMidiFile(midiFile);
40989
41031
  }
40990
41032
  destroyCursors() {
40991
41033
  if (!this._cursorWrapper) {
@@ -41008,6 +41050,7 @@
41008
41050
  this._barCursor = cursors.barCursor;
41009
41051
  this._beatCursor = cursors.beatCursor;
41010
41052
  this._selectionWrapper = cursors.selectionWrapper;
41053
+ this._isInitialBeatCursorUpdate = true;
41011
41054
  }
41012
41055
  if (this._currentBeat !== null) {
41013
41056
  this.cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
@@ -41017,36 +41060,6 @@
41017
41060
  this.destroyCursors();
41018
41061
  }
41019
41062
  }
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
41063
  /**
41051
41064
  * updates the cursors to highlight the beat at the specified tick position
41052
41065
  * @param tick
@@ -41077,7 +41090,7 @@
41077
41090
  if (!beat) {
41078
41091
  return;
41079
41092
  }
41080
- const cache = this.renderer.boundsLookup;
41093
+ const cache = this._renderer.boundsLookup;
41081
41094
  if (!cache) {
41082
41095
  return;
41083
41096
  }
@@ -41087,7 +41100,7 @@
41087
41100
  if (!forceUpdate &&
41088
41101
  beat === previousBeat?.beat &&
41089
41102
  cache === previousCache &&
41090
- previousState === this._playerState &&
41103
+ previousState === this._player.state &&
41091
41104
  previousBeat?.start === lookupResult.start) {
41092
41105
  return;
41093
41106
  }
@@ -41099,7 +41112,7 @@
41099
41112
  // actually show the cursor
41100
41113
  this._currentBeat = lookupResult;
41101
41114
  this._previousCursorCache = cache;
41102
- this._previousStateForCursor = this._playerState;
41115
+ this._previousStateForCursor = this._player.state;
41103
41116
  this.uiFacade.beginInvoke(() => {
41104
41117
  this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
41105
41118
  });
@@ -41110,9 +41123,9 @@
41110
41123
  * @category Methods - Player
41111
41124
  */
41112
41125
  scrollToCursor() {
41113
- const barBounds = this._currentBarBounds;
41114
- if (barBounds) {
41115
- this.internalScrollToCursor(barBounds);
41126
+ const beatBounds = this._currentBeatBounds;
41127
+ if (beatBounds) {
41128
+ this.internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
41116
41129
  }
41117
41130
  }
41118
41131
  internalScrollToCursor(barBoundings) {
@@ -41171,10 +41184,12 @@
41171
41184
  const beatCursor = this._beatCursor;
41172
41185
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
41173
41186
  const barBounds = barBoundings.visualBounds;
41174
- this._currentBarBounds = barBoundings;
41187
+ const previousBeatBounds = this._currentBeatBounds;
41188
+ this._currentBeatBounds = beatBoundings;
41175
41189
  if (barCursor) {
41176
41190
  barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
41177
41191
  }
41192
+ const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
41178
41193
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
41179
41194
  // get position of next beat on same system
41180
41195
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
@@ -41192,20 +41207,49 @@
41192
41207
  if (this.settings.player.enableAnimatedBeatCursor) {
41193
41208
  const animationWidth = nextBeatX - beatBoundings.onNotesX;
41194
41209
  const relativePosition = this._previousTick - this._currentBeat.start;
41195
- const ratioPosition = relativePosition / this._currentBeat.tickDuration;
41210
+ const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
41196
41211
  startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
41197
41212
  duration -= duration * ratioPosition;
41213
+ if (isPlayingUpdate) {
41214
+ // we do not "reset" the cursor if we are smoothly moving from left to right.
41215
+ const jumpCursor = !previousBeatBounds ||
41216
+ this._isInitialBeatCursorUpdate ||
41217
+ barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
41218
+ startBeatX < previousBeatBounds.onNotesX ||
41219
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
41220
+ if (jumpCursor) {
41221
+ beatCursor.transitionToX(0, startBeatX);
41222
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41223
+ }
41224
+ // we need to put the transition to an own animation frame
41225
+ // otherwise the stop animation above is not applied.
41226
+ this.uiFacade.beginInvoke(() => {
41227
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
41228
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
41229
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
41230
+ const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * 2;
41231
+ beatCursor.transitionToX((duration / cursorSpeed) * 2, doubleEndBeatX);
41232
+ });
41233
+ }
41234
+ else {
41235
+ beatCursor.transitionToX(0, startBeatX);
41236
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41237
+ }
41238
+ }
41239
+ else {
41240
+ // ticking cursor
41198
41241
  beatCursor.transitionToX(0, startBeatX);
41242
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41199
41243
  }
41200
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41244
+ this._isInitialBeatCursorUpdate = false;
41201
41245
  }
41202
- // if playing, animate the cursor to the next beat
41203
- if (this.settings.player.enableElementHighlighting) {
41204
- this.uiFacade.removeHighlights();
41246
+ else {
41247
+ this._isInitialBeatCursorUpdate = true;
41205
41248
  }
41249
+ // if playing, animate the cursor to the next beat
41250
+ this.uiFacade.removeHighlights();
41206
41251
  // actively playing? -> animate cursor and highlight items
41207
41252
  let shouldNotifyBeatChange = false;
41208
- const isPlayingUpdate = this._playerState === PlayerState.Playing && !stop;
41209
41253
  if (isPlayingUpdate) {
41210
41254
  if (this.settings.player.enableElementHighlighting) {
41211
41255
  for (const highlight of beatsToHighlight) {
@@ -41216,15 +41260,6 @@
41216
41260
  shouldScroll = !stop;
41217
41261
  shouldNotifyBeatChange = true;
41218
41262
  }
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
41263
  if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== exports.ScrollMode.Off) {
41229
41264
  this.internalScrollToCursor(barBoundings);
41230
41265
  }
@@ -41314,7 +41349,7 @@
41314
41349
  const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
41315
41350
  // move to selection start
41316
41351
  this._currentBeat = null; // reset current beat so it is updating the cursor
41317
- if (this._playerState === PlayerState.Paused) {
41352
+ if (this._player.state === PlayerState.Paused) {
41318
41353
  this.cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
41319
41354
  }
41320
41355
  this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
@@ -41376,11 +41411,11 @@
41376
41411
  }
41377
41412
  const relX = e.getX(this.canvasElement);
41378
41413
  const relY = e.getY(this.canvasElement);
41379
- const beat = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41414
+ const beat = this._renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41380
41415
  if (beat) {
41381
41416
  this.onBeatMouseDown(e, beat);
41382
41417
  if (this.settings.core.includeNoteBounds) {
41383
- const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41418
+ const note = this._renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41384
41419
  if (note) {
41385
41420
  this.onNoteMouseDown(e, note);
41386
41421
  }
@@ -41393,11 +41428,11 @@
41393
41428
  }
41394
41429
  const relX = e.getX(this.canvasElement);
41395
41430
  const relY = e.getY(this.canvasElement);
41396
- const beat = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41431
+ const beat = this._renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41397
41432
  if (beat) {
41398
41433
  this.onBeatMouseMove(e, beat);
41399
41434
  if (this._noteMouseDown) {
41400
- const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41435
+ const note = this._renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41401
41436
  if (note) {
41402
41437
  this.onNoteMouseMove(e, note);
41403
41438
  }
@@ -41413,11 +41448,11 @@
41413
41448
  }
41414
41449
  const relX = e.getX(this.canvasElement);
41415
41450
  const relY = e.getY(this.canvasElement);
41416
- const beat = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41451
+ const beat = this._renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41417
41452
  this.onBeatMouseUp(e, beat);
41418
41453
  if (this._noteMouseDown) {
41419
41454
  if (beat) {
41420
- const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY) ?? null;
41455
+ const note = this._renderer.boundsLookup?.getNoteAtPos(beat, relX, relY) ?? null;
41421
41456
  this.onNoteMouseUp(e, note);
41422
41457
  }
41423
41458
  else {
@@ -41425,7 +41460,7 @@
41425
41460
  }
41426
41461
  }
41427
41462
  });
41428
- this.renderer.postRenderFinished.on(() => {
41463
+ this._renderer.postRenderFinished.on(() => {
41429
41464
  if (!this._selectionStart ||
41430
41465
  this.settings.player.playerMode === exports.PlayerMode.Disabled ||
41431
41466
  !this.settings.player.enableCursor ||
@@ -41436,7 +41471,7 @@
41436
41471
  });
41437
41472
  }
41438
41473
  cursorSelectRange(startBeat, endBeat) {
41439
- const cache = this.renderer.boundsLookup;
41474
+ const cache = this._renderer.boundsLookup;
41440
41475
  if (!cache) {
41441
41476
  return;
41442
41477
  }
@@ -41505,7 +41540,8 @@
41505
41540
  }
41506
41541
  this.scoreLoaded.trigger(score);
41507
41542
  this.uiFacade.triggerEvent(this.container, 'scoreLoaded', score);
41508
- if (this.setupPlayer()) {
41543
+ if (!this.setupOrDestroyPlayer()) {
41544
+ // feed midi into current player (a new player will trigger a midi generation once the player is ready)
41509
41545
  this.loadMidiForScore();
41510
41546
  }
41511
41547
  }
@@ -41534,6 +41570,8 @@
41534
41570
  if (this._isDestroyed) {
41535
41571
  return;
41536
41572
  }
41573
+ this._currentBeat = null;
41574
+ this.cursorUpdateTick(this._previousTick, false, 1, true, true);
41537
41575
  this.postRenderFinished.trigger();
41538
41576
  this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null);
41539
41577
  }
@@ -41548,25 +41586,155 @@
41548
41586
  this.error.trigger(error);
41549
41587
  this.uiFacade.triggerEvent(this.container, 'error', error);
41550
41588
  }
41589
+ /**
41590
+ * This event is fired when all required data for playback is loaded and ready.
41591
+ * @remarks
41592
+ * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
41593
+ * 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.
41594
+ *
41595
+ * @eventProperty
41596
+ * @category Events - Player
41597
+ * @since 0.9.4
41598
+ *
41599
+ * @example
41600
+ * JavaScript
41601
+ * ```js
41602
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41603
+ * api.playerReady.on(() => {
41604
+ * enablePlayerControls();
41605
+ * });
41606
+ * ```
41607
+ *
41608
+ * @example
41609
+ * C#
41610
+ * ```cs
41611
+ * var api = new AlphaTabApi<MyControl>(...);
41612
+ * api.PlayerReady.On(() =>
41613
+ * {
41614
+ * EnablePlayerControls()
41615
+ * });
41616
+ * ```
41617
+ *
41618
+ * @example
41619
+ * Android
41620
+ * ```kotlin
41621
+ * val api = AlphaTabApi<MyControl>(...)
41622
+ * api.playerReady.on {
41623
+ * enablePlayerControls()
41624
+ * }
41625
+ * ```
41626
+ */
41627
+ get playerReady() {
41628
+ return this._player.readyForPlayback;
41629
+ }
41551
41630
  onPlayerReady() {
41552
41631
  if (this._isDestroyed) {
41553
41632
  return;
41554
41633
  }
41555
- this.playerReady.trigger();
41556
41634
  this.uiFacade.triggerEvent(this.container, 'playerReady', null);
41557
41635
  }
41636
+ /**
41637
+ * This event is fired when the playback of the whole song finished.
41638
+ * @remarks
41639
+ * This event is finished regardless on whether looping is enabled or not.
41640
+ *
41641
+ * @eventProperty
41642
+ * @category Events - Player
41643
+ * @since 0.9.4
41644
+ *
41645
+ * @example
41646
+ * JavaScript
41647
+ * ```js
41648
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41649
+ * api.playerFinished.on((args) => {
41650
+ * // speed trainer
41651
+ * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
41652
+ * });
41653
+ * api.isLooping = true;
41654
+ * api.playbackSpeed = 0.5;
41655
+ * api.play()
41656
+ * ```
41657
+ *
41658
+ * @example
41659
+ * C#
41660
+ * ```cs
41661
+ * var api = new AlphaTabApi<MyControl>(...);
41662
+ * api.PlayerFinished.On(() =>
41663
+ * {
41664
+ * // speed trainer
41665
+ * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
41666
+ * });
41667
+ * api.IsLooping = true;
41668
+ * api.PlaybackSpeed = 0.5;
41669
+ * api.Play();
41670
+ * ```
41671
+ *
41672
+ * @example
41673
+ * Android
41674
+ * ```kotlin
41675
+ * val api = AlphaTabApi<MyControl>(...)
41676
+ * api.playerFinished.on {
41677
+ * // speed trainer
41678
+ * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
41679
+ * }
41680
+ * api.isLooping = true
41681
+ * api.playbackSpeed = 0.5
41682
+ * api.play()
41683
+ * ```
41684
+ *
41685
+ */
41686
+ get playerFinished() {
41687
+ return this._player.finished;
41688
+ }
41558
41689
  onPlayerFinished() {
41559
41690
  if (this._isDestroyed) {
41560
41691
  return;
41561
41692
  }
41562
- this.playerFinished.trigger();
41563
41693
  this.uiFacade.triggerEvent(this.container, 'playerFinished', null);
41564
41694
  }
41695
+ /**
41696
+ * This event is fired when the SoundFont needed for playback was loaded.
41697
+ *
41698
+ * @eventProperty
41699
+ * @category Events - Player
41700
+ * @since 0.9.4
41701
+ *
41702
+ * @example
41703
+ * JavaScript
41704
+ * ```js
41705
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41706
+ * api.soundFontLoaded.on(() => {
41707
+ * hideSoundFontLoadingIndicator();
41708
+ * });
41709
+ * ```
41710
+ *
41711
+ * @example
41712
+ * C#
41713
+ * ```cs
41714
+ * var api = new AlphaTabApi<MyControl>(...);
41715
+ * api.SoundFontLoaded.On(() =>
41716
+ * {
41717
+ * HideSoundFontLoadingIndicator();
41718
+ * });
41719
+ * ```
41720
+ *
41721
+ * @example
41722
+ * Android
41723
+ * ```kotlin
41724
+ * val api = AlphaTabApi<MyControl>(...);
41725
+ * api.soundFontLoaded.on {
41726
+ * hideSoundFontLoadingIndicator();
41727
+ * }
41728
+ * ```
41729
+ *
41730
+ */
41731
+ get soundFontLoaded() {
41732
+ return this._player.soundFontLoaded;
41733
+ }
41565
41734
  onSoundFontLoaded() {
41566
41735
  if (this._isDestroyed) {
41567
41736
  return;
41568
41737
  }
41569
- this.soundFontLoaded.trigger();
41570
41738
  this.uiFacade.triggerEvent(this.container, 'soundFontLoaded', null);
41571
41739
  }
41572
41740
  onMidiLoad(e) {
@@ -41583,34 +41751,255 @@
41583
41751
  this.midiLoaded.trigger(e);
41584
41752
  this.uiFacade.triggerEvent(this.container, 'midiFileLoaded', e);
41585
41753
  }
41754
+ /**
41755
+ * This event is fired when the playback state changed.
41756
+ *
41757
+ * @eventProperty
41758
+ * @category Events - Player
41759
+ * @since 0.9.4
41760
+ *
41761
+ * @example
41762
+ * JavaScript
41763
+ * ```js
41764
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41765
+ * api.playerStateChanged.on((args) => {
41766
+ * updatePlayerControls(args.state, args.stopped);
41767
+ * });
41768
+ * ```
41769
+ *
41770
+ * @example
41771
+ * C#
41772
+ * ```cs
41773
+ * var api = new AlphaTabApi<MyControl>(...);
41774
+ * api.PlayerStateChanged.On(args =>
41775
+ * {
41776
+ * UpdatePlayerControls(args);
41777
+ * });
41778
+ * ```
41779
+ *
41780
+ * @example
41781
+ * Android
41782
+ * ```kotlin
41783
+ * val api = AlphaTabApi<MyControl>(...)
41784
+ * api.playerStateChanged.on { args ->
41785
+ * updatePlayerControls(args)
41786
+ * }
41787
+ * ```
41788
+ *
41789
+ */
41790
+ get playerStateChanged() {
41791
+ return this._player.stateChanged;
41792
+ }
41586
41793
  onPlayerStateChanged(e) {
41587
41794
  if (this._isDestroyed) {
41588
41795
  return;
41589
41796
  }
41590
- this.playerStateChanged.trigger(e);
41797
+ if (!e.stopped && e.state === PlayerState.Paused) {
41798
+ const currentBeat = this._currentBeat;
41799
+ const tickCache = this._tickCache;
41800
+ if (currentBeat && tickCache) {
41801
+ this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41802
+ }
41803
+ }
41591
41804
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
41592
41805
  }
41806
+ /**
41807
+ * This event is fired when the current playback position of the song changed.
41808
+ *
41809
+ * @eventProperty
41810
+ * @category Events - Player
41811
+ * @since 0.9.4
41812
+ *
41813
+ * @example
41814
+ * JavaScript
41815
+ * ```js
41816
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41817
+ * api.playerPositionChanged.on((args) => {
41818
+ * updatePlayerPosition(args);
41819
+ * });
41820
+ * ```
41821
+ *
41822
+ * @example
41823
+ * C#
41824
+ * ```cs
41825
+ * var api = new AlphaTabApi<MyControl>(...);
41826
+ * api.PlayerPositionChanged.On(args =>
41827
+ * {
41828
+ * UpdatePlayerPosition(args);
41829
+ * });
41830
+ * ```
41831
+ *
41832
+ * @example
41833
+ * Android
41834
+ * ```kotlin
41835
+ * val api = AlphaTabApi<MyControl>(...)
41836
+ * api.playerPositionChanged.on { args ->
41837
+ * updatePlayerPosition(args)
41838
+ * }
41839
+ * ```
41840
+ *
41841
+ */
41842
+ get playerPositionChanged() {
41843
+ return this._player.positionChanged;
41844
+ }
41593
41845
  onPlayerPositionChanged(e) {
41594
41846
  if (this._isDestroyed) {
41595
41847
  return;
41596
41848
  }
41597
- if (this.score !== null && this.tracks.length > 0) {
41598
- this.playerPositionChanged.trigger(e);
41599
- this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41600
- }
41849
+ this._previousTick = e.currentTick;
41850
+ this.uiFacade.beginInvoke(() => {
41851
+ const cursorSpeed = e.modifiedTempo / e.originalTempo;
41852
+ this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41853
+ });
41854
+ this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41855
+ }
41856
+ /**
41857
+ * This event is fired when the synthesizer played certain midi events.
41858
+ *
41859
+ * @remarks
41860
+ * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
41861
+ * audio playback elements like notes/rests played or metronome ticks.
41862
+ *
41863
+ * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
41864
+ *
41865
+ * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
41866
+ * until some extend but highly encourage changing to the new models in case of problems.
41867
+ *
41868
+ * @eventProperty
41869
+ * @category Events - Player
41870
+ * @since 1.2.0
41871
+ *
41872
+ * @example
41873
+ * JavaScript
41874
+ * ```js
41875
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41876
+ * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
41877
+ * api.midiEventsPlayed.on(function(e) {
41878
+ * for(const midi of e.events) {
41879
+ * if(midi.isMetronome) {
41880
+ * console.log('Metronome tick ' + midi.tick);
41881
+ * }
41882
+ * }
41883
+ * });
41884
+ * ```
41885
+ *
41886
+ * @example
41887
+ * C#
41888
+ * ```cs
41889
+ * var api = new AlphaTabApi<MyControl>(...);
41890
+ * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
41891
+ * api.MidiEventsPlayed.On(e =>
41892
+ * {
41893
+ * foreach(var midi of e.events)
41894
+ * {
41895
+ * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
41896
+ * {
41897
+ * Console.WriteLine("Metronome tick " + midi.Tick);
41898
+ * }
41899
+ * }
41900
+ * });
41901
+ * ```
41902
+ *
41903
+ * @example
41904
+ * Android
41905
+ * ```kotlin
41906
+ * val api = AlphaTabApi<MyControl>(...);
41907
+ * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
41908
+ * api.midiEventsPlayed.on { e ->
41909
+ * for (midi in e.events) {
41910
+ * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
41911
+ * println("Metronome tick " + midi.tick);
41912
+ * }
41913
+ * }
41914
+ * }
41915
+ * ```
41916
+ * @see {@link MidiEvent}
41917
+ * @see {@link TimeSignatureEvent}
41918
+ * @see {@link AlphaTabMetronomeEvent}
41919
+ * @see {@link AlphaTabRestEvent}
41920
+ * @see {@link NoteOnEvent}
41921
+ * @see {@link NoteOffEvent}
41922
+ * @see {@link ControlChangeEvent}
41923
+ * @see {@link ProgramChangeEvent}
41924
+ * @see {@link TempoChangeEvent}
41925
+ * @see {@link PitchBendEvent}
41926
+ * @see {@link NoteBendEvent}
41927
+ * @see {@link EndOfTrackEvent}
41928
+ * @see {@link MetaEvent}
41929
+ * @see {@link MetaDataEvent}
41930
+ * @see {@link MetaNumberEvent}
41931
+ * @see {@link Midi20PerNotePitchBendEvent}
41932
+ * @see {@link SystemCommonEvent}
41933
+ * @see {@link SystemExclusiveEvent}
41934
+ */
41935
+ get midiEventsPlayed() {
41936
+ return this._player.midiEventsPlayed;
41601
41937
  }
41602
41938
  onMidiEventsPlayed(e) {
41603
41939
  if (this._isDestroyed) {
41604
41940
  return;
41605
41941
  }
41606
- this.midiEventsPlayed.trigger(e);
41607
41942
  this.uiFacade.triggerEvent(this.container, 'midiEventsPlayed', e);
41608
41943
  }
41944
+ /**
41945
+ * This event is fired when the playback range changed.
41946
+ *
41947
+ * @eventProperty
41948
+ * @category Events - Player
41949
+ * @since 1.2.3
41950
+ *
41951
+ * @example
41952
+ * JavaScript
41953
+ * ```js
41954
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41955
+ * api.playbackRangeChanged.on((args) => {
41956
+ * if (args.playbackRange) {
41957
+ * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
41958
+ * } else {
41959
+ * clearHighlightInProgressBar();
41960
+ * }
41961
+ * });
41962
+ * ```
41963
+ *
41964
+ * @example
41965
+ * C#
41966
+ * ```cs
41967
+ * var api = new AlphaTabApi<MyControl>(...);
41968
+ * api.PlaybackRangeChanged.On(args =>
41969
+ * {
41970
+ * if (args.PlaybackRange != null)
41971
+ * {
41972
+ * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
41973
+ * }
41974
+ * else
41975
+ * {
41976
+ * ClearHighlightInProgressBar();
41977
+ * }
41978
+ * });
41979
+ * ```
41980
+ *
41981
+ * @example
41982
+ * Android
41983
+ * ```kotlin
41984
+ * val api = AlphaTabApi<MyControl>(...)
41985
+ * api.playbackRangeChanged.on { args ->
41986
+ * val playbackRange = args.playbackRange
41987
+ * if (playbackRange != null) {
41988
+ * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
41989
+ * } else {
41990
+ * clearHighlightInProgressBar()
41991
+ * }
41992
+ * }
41993
+ * ```
41994
+ *
41995
+ */
41996
+ get playbackRangeChanged() {
41997
+ return this._player.playbackRangeChanged;
41998
+ }
41609
41999
  onPlaybackRangeChanged(e) {
41610
42000
  if (this._isDestroyed) {
41611
42001
  return;
41612
42002
  }
41613
- this.playbackRangeChanged.trigger(e);
41614
42003
  this.uiFacade.triggerEvent(this.container, 'playbackRangeChanged', e);
41615
42004
  }
41616
42005
  onSettingsUpdated() {
@@ -41672,10 +42061,7 @@
41672
42061
  * ```
41673
42062
  */
41674
42063
  async enumerateOutputDevices() {
41675
- if (this.player) {
41676
- return await this.player.output.enumerateOutputDevices();
41677
- }
41678
- return [];
42064
+ return await this._player.output.enumerateOutputDevices();
41679
42065
  }
41680
42066
  /**
41681
42067
  * Changes the output device which should be used for playing the audio (player must be enabled).
@@ -41726,9 +42112,7 @@
41726
42112
  * ```
41727
42113
  */
41728
42114
  async setOutputDevice(device) {
41729
- if (this.player) {
41730
- await this.player.output.setOutputDevice(device);
41731
- }
42115
+ await this._player.output.setOutputDevice(device);
41732
42116
  }
41733
42117
  /**
41734
42118
  * The currently configured output device if changed via {@link setOutputDevice}.
@@ -41768,10 +42152,7 @@
41768
42152
  *
41769
42153
  */
41770
42154
  async getOutputDevice() {
41771
- if (this.player) {
41772
- return await this.player.output.getOutputDevice();
41773
- }
41774
- return null;
42155
+ return await this._player.output.getOutputDevice();
41775
42156
  }
41776
42157
  }
41777
42158
 
@@ -41948,38 +42329,52 @@
41948
42329
  this.element = element;
41949
42330
  this.mouseDown = {
41950
42331
  on: (value) => {
41951
- this.element.addEventListener('mousedown', e => {
42332
+ const nativeListener = e => {
41952
42333
  value(new BrowserMouseEventArgs(e));
41953
- }, true);
42334
+ };
42335
+ this.element.addEventListener('mousedown', nativeListener, true);
42336
+ return () => {
42337
+ this.element.removeEventListener('mousedown', nativeListener, true);
42338
+ };
41954
42339
  },
41955
42340
  off: (value) => {
41956
42341
  }
41957
42342
  };
41958
42343
  this.mouseUp = {
41959
42344
  on: (value) => {
41960
- this.element.addEventListener('mouseup', e => {
42345
+ const nativeListener = e => {
41961
42346
  value(new BrowserMouseEventArgs(e));
41962
- }, true);
42347
+ };
42348
+ this.element.addEventListener('mouseup', nativeListener, true);
42349
+ return () => {
42350
+ this.element.removeEventListener('mouseup', nativeListener, true);
42351
+ };
41963
42352
  },
41964
42353
  off: (value) => {
41965
42354
  }
41966
42355
  };
41967
42356
  this.mouseMove = {
41968
42357
  on: (value) => {
41969
- this.element.addEventListener('mousemove', e => {
42358
+ const nativeListener = e => {
41970
42359
  value(new BrowserMouseEventArgs(e));
41971
- }, true);
42360
+ };
42361
+ this.element.addEventListener('mousemove', nativeListener, true);
42362
+ return () => {
42363
+ this.element.removeEventListener('mousemove', nativeListener, true);
42364
+ };
41972
42365
  },
41973
42366
  off: (_) => {
41974
42367
  }
41975
42368
  };
42369
+ const container = this;
41976
42370
  this.resize = {
41977
- on: (value) => {
41978
- if (this._resizeListeners === 0) {
41979
- HtmlElementContainer.resizeObserver.value.observe(this.element);
42371
+ on: function (value) {
42372
+ if (container._resizeListeners === 0) {
42373
+ HtmlElementContainer.resizeObserver.value.observe(container.element);
41980
42374
  }
41981
- this.element.addEventListener('resize', value, true);
41982
- this._resizeListeners++;
42375
+ container.element.addEventListener('resize', value, true);
42376
+ container._resizeListeners++;
42377
+ return () => this.off(value);
41983
42378
  },
41984
42379
  off: (value) => {
41985
42380
  this.element.removeEventListener('resize', value, true);
@@ -42560,21 +42955,6 @@
42560
42955
  }
42561
42956
  }
42562
42957
 
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
42958
  /**
42579
42959
  * a WebWorker based alphaSynth which uses the given player as output.
42580
42960
  * @target web
@@ -42811,25 +43191,6 @@
42811
43191
  append: append
42812
43192
  });
42813
43193
  }
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
43194
  resetSoundFonts() {
42834
43195
  this._synth.postMessage({
42835
43196
  cmd: 'alphaSynth.resetSoundFonts'
@@ -43381,7 +43742,11 @@
43381
43742
  }
43382
43743
  this._padding = backingTrack.padding / 1000;
43383
43744
  const blob = new Blob([backingTrack.rawAudioFile]);
43745
+ // https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource
43746
+ // Step 8. resets the playbackRate, we need to remember and restore it.
43747
+ const playbackRate = this.audioElement.playbackRate;
43384
43748
  this.audioElement.src = URL.createObjectURL(blob);
43749
+ this.audioElement.playbackRate = playbackRate;
43385
43750
  }
43386
43751
  open(_bufferTimeInMilliseconds) {
43387
43752
  const audioElement = document.createElement('audio');
@@ -44100,6 +44465,21 @@
44100
44465
  }
44101
44466
  }
44102
44467
 
44468
+ /**
44469
+ * Represents the progress of any data being loaded.
44470
+ */
44471
+ class ProgressEventArgs {
44472
+ /**
44473
+ * Initializes a new instance of the {@link ProgressEventArgs} class.
44474
+ * @param loaded
44475
+ * @param total
44476
+ */
44477
+ constructor(loaded, total) {
44478
+ this.loaded = loaded;
44479
+ this.total = total;
44480
+ }
44481
+ }
44482
+
44103
44483
  /**
44104
44484
  * @target web
44105
44485
  */
@@ -44343,13 +44723,29 @@
44343
44723
  * @since 0.9.4
44344
44724
  */
44345
44725
  loadSoundFontFromUrl(url, append) {
44346
- if (!this.player) {
44726
+ const player = this.player;
44727
+ if (!player) {
44347
44728
  return;
44348
44729
  }
44349
- this.player.loadSoundFontFromUrl(url, append, e => {
44350
- this.soundFontLoad.trigger(e);
44351
- this.uiFacade.triggerEvent(this.container, 'soundFontLoad', e);
44352
- });
44730
+ Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
44731
+ const request = new XMLHttpRequest();
44732
+ request.open('GET', url, true, null, null);
44733
+ request.responseType = 'arraybuffer';
44734
+ request.onload = _ => {
44735
+ const buffer = new Uint8Array(request.response);
44736
+ this.loadSoundFont(buffer, append);
44737
+ };
44738
+ request.onerror = e => {
44739
+ Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
44740
+ player.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
44741
+ };
44742
+ request.onprogress = e => {
44743
+ Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
44744
+ const args = new ProgressEventArgs(e.loaded, e.total);
44745
+ this.soundFontLoad.trigger(args);
44746
+ this.uiFacade.triggerEvent(this.container, 'soundFontLoad', args);
44747
+ };
44748
+ request.send();
44353
44749
  }
44354
44750
  }
44355
44751
 
@@ -46731,7 +47127,7 @@
46731
47127
  return this.masterBarsRenderers[0].masterBar.index;
46732
47128
  }
46733
47129
  get lastBarIndex() {
46734
- return this.masterBarsRenderers[this.masterBarsRenderers.length - 1].masterBar.index;
47130
+ return this.masterBarsRenderers[this.masterBarsRenderers.length - 1].lastMasterBarIndex;
46735
47131
  }
46736
47132
  addMasterBarRenderers(tracks, renderers) {
46737
47133
  if (tracks.length === 0) {
@@ -47545,6 +47941,9 @@
47545
47941
  }
47546
47942
  }
47547
47943
  }
47944
+ else {
47945
+ this.tuningGlyph = null;
47946
+ }
47548
47947
  }
47549
47948
  // chord diagram glyphs
47550
47949
  if (notation.isNotationElementVisible(exports.NotationElement.ChordDiagrams)) {
@@ -47573,6 +47972,12 @@
47573
47972
  }
47574
47973
  }
47575
47974
  }
47975
+ if (this.chordDiagrams.isEmpty) {
47976
+ this.chordDiagrams = null;
47977
+ }
47978
+ }
47979
+ else {
47980
+ this.chordDiagrams = null;
47576
47981
  }
47577
47982
  }
47578
47983
  createEmptyStaffSystem() {
@@ -60316,9 +60721,9 @@
60316
60721
  print(`build date: ${VersionInfo.date}`);
60317
60722
  }
60318
60723
  }
60319
- VersionInfo.version = '1.6.0-alpha.1408';
60320
- VersionInfo.date = '2025-05-13T02:08:06.970Z';
60321
- VersionInfo.commit = '37b936b4f5eb8bf759bc78bdb4dd1812d7b2a072';
60724
+ VersionInfo.version = '1.6.0-alpha.1415';
60725
+ VersionInfo.date = '2025-05-19T16:08:22.342Z';
60726
+ VersionInfo.commit = '459db69f8896a2ea8822ce5d49dcc824edd36521';
60322
60727
 
60323
60728
  /**
60324
60729
  * A factory for custom layout engines.