@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.
@@ -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
  *
@@ -3371,18 +3371,26 @@ class ModelUtils {
3371
3371
  const lookup = new Map();
3372
3372
  const score = tracks[0].score;
3373
3373
  let currentIndex = startIndex;
3374
+ let tempo = score.tempo;
3374
3375
  while (currentIndex <= endIndexInclusive) {
3375
3376
  const currentGroupStartIndex = currentIndex;
3376
3377
  let currentGroup = null;
3377
3378
  while (currentIndex <= endIndexInclusive) {
3378
3379
  const masterBar = score.masterBars[currentIndex];
3380
+ let hasTempoChange = false;
3381
+ for (const a of masterBar.tempoAutomations) {
3382
+ if (a.value !== tempo) {
3383
+ hasTempoChange = true;
3384
+ }
3385
+ tempo = a.value;
3386
+ }
3379
3387
  // check if masterbar breaks multibar rests, it must be fully empty with no annotations
3380
3388
  if (masterBar.alternateEndings ||
3381
3389
  (masterBar.isRepeatStart && masterBar.index !== currentGroupStartIndex) ||
3382
3390
  masterBar.isFreeTime ||
3383
3391
  masterBar.isAnacrusis ||
3384
3392
  masterBar.section !== null ||
3385
- (masterBar.index !== currentGroupStartIndex && masterBar.tempoAutomations.length > 0) ||
3393
+ (masterBar.index !== currentGroupStartIndex && hasTempoChange) ||
3386
3394
  (masterBar.fermata !== null && masterBar.fermata.size > 0) ||
3387
3395
  (masterBar.directions !== null && masterBar.directions.size > 0)) {
3388
3396
  break;
@@ -20215,7 +20223,11 @@ class MusicXmlImporter extends ScoreImporter {
20215
20223
  for (const c of element.childElements()) {
20216
20224
  switch (c.localName) {
20217
20225
  case 'direction-type':
20218
- directionTypes.push(c.firstElement);
20226
+ // See https://github.com/CoderLine/alphaTab/issues/2102
20227
+ const type = c.firstElement;
20228
+ if (type) {
20229
+ directionTypes.push(type);
20230
+ }
20219
20231
  break;
20220
20232
  case 'offset':
20221
20233
  offset = Number.parseFloat(c.innerText);
@@ -22397,6 +22409,11 @@ class MidiFileSequencer {
22397
22409
  this._mainState.eventIndex = 0;
22398
22410
  this._mainState.syncPointIndex = 0;
22399
22411
  this._mainState.tempoChangeIndex = 0;
22412
+ this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22413
+ this._mainState.modifiedTempo =
22414
+ this._mainState.syncPoints.length > 0
22415
+ ? this._mainState.syncPoints[0].data.modifiedTempo
22416
+ : this._mainState.currentTempo;
22400
22417
  if (this.isPlayingMain) {
22401
22418
  const metronomeVolume = this._synthesizer.metronomeVolume;
22402
22419
  this._synthesizer.noteOffAll(true);
@@ -22535,18 +22552,6 @@ class MidiFileSequencer {
22535
22552
  this._currentState.synthData[this._currentState.eventIndex].time < this._currentState.currentTime) {
22536
22553
  const synthEvent = this._currentState.synthData[this._currentState.eventIndex];
22537
22554
  this._synthesizer.dispatchEvent(synthEvent);
22538
- while (this._currentState.syncPointIndex < this._currentState.syncPoints.length &&
22539
- this._currentState.syncPoints[this._currentState.syncPointIndex].tick < synthEvent.event.tick) {
22540
- this._currentState.modifiedTempo =
22541
- this._currentState.syncPoints[this._currentState.syncPointIndex].data.modifiedTempo;
22542
- this._currentState.syncPointIndex++;
22543
- }
22544
- while (this._currentState.tempoChangeIndex < this._currentState.tempoChanges.length &&
22545
- this._currentState.tempoChanges[this._currentState.tempoChangeIndex].time <= synthEvent.time) {
22546
- this._currentState.currentTempo =
22547
- this._currentState.tempoChanges[this._currentState.tempoChangeIndex].bpm;
22548
- this._currentState.tempoChangeIndex++;
22549
- }
22550
22555
  this._currentState.eventIndex++;
22551
22556
  anyEventsDispatched = true;
22552
22557
  }
@@ -22576,9 +22581,6 @@ class MidiFileSequencer {
22576
22581
  mainTickPositionToTimePosition(tickPosition) {
22577
22582
  return this.tickPositionToTimePositionWithSpeed(this._mainState, tickPosition, this.playbackSpeed);
22578
22583
  }
22579
- mainTimePositionToTickPosition(timePosition) {
22580
- return this.timePositionToTickPositionWithSpeed(this._mainState, timePosition, this.playbackSpeed);
22581
- }
22582
22584
  mainUpdateSyncPoints(syncPoints) {
22583
22585
  const state = this._mainState;
22584
22586
  syncPoints.sort((a, b) => a.tick - b.tick); // just in case
@@ -22606,7 +22608,49 @@ class MidiFileSequencer {
22606
22608
  state.syncPointIndex = 0;
22607
22609
  }
22608
22610
  currentTimePositionToTickPosition(timePosition) {
22609
- return this.timePositionToTickPositionWithSpeed(this._currentState, timePosition, this.playbackSpeed);
22611
+ const state = this._currentState;
22612
+ if (state.tempoChanges.length === 0) {
22613
+ return 0;
22614
+ }
22615
+ timePosition *= this.playbackSpeed;
22616
+ this.updateCurrentTempo(state, timePosition);
22617
+ const lastTempoChange = state.tempoChanges[state.tempoChangeIndex];
22618
+ const timeDiff = timePosition - lastTempoChange.time;
22619
+ const ticks = ((timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0);
22620
+ // we add 1 for possible rounding errors.(floating point issuses)
22621
+ return lastTempoChange.ticks + ticks + 1;
22622
+ }
22623
+ updateCurrentTempo(state, timePosition) {
22624
+ let tempoChangeIndex = state.tempoChangeIndex;
22625
+ if (timePosition < state.tempoChanges[tempoChangeIndex].time) {
22626
+ tempoChangeIndex = 0;
22627
+ }
22628
+ while (tempoChangeIndex + 1 < state.tempoChanges.length &&
22629
+ state.tempoChanges[tempoChangeIndex + 1].time <= timePosition) {
22630
+ tempoChangeIndex++;
22631
+ }
22632
+ if (tempoChangeIndex !== state.tempoChangeIndex) {
22633
+ state.tempoChangeIndex = tempoChangeIndex;
22634
+ state.currentTempo = state.tempoChanges[state.tempoChangeIndex].bpm;
22635
+ }
22636
+ const syncPoints = state.syncPoints;
22637
+ if (syncPoints.length > 0) {
22638
+ let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22639
+ if (timePosition < syncPoints[syncPointIndex].data.millisecondOffset) {
22640
+ syncPointIndex = 0;
22641
+ }
22642
+ while (syncPointIndex + 1 < syncPoints.length &&
22643
+ syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22644
+ syncPointIndex++;
22645
+ }
22646
+ if (syncPointIndex !== state.syncPointIndex) {
22647
+ state.syncPointIndex = syncPointIndex;
22648
+ state.modifiedTempo = syncPoints[syncPointIndex].data.modifiedTempo;
22649
+ }
22650
+ }
22651
+ else {
22652
+ state.modifiedTempo = state.currentTempo;
22653
+ }
22610
22654
  }
22611
22655
  mainTimePositionFromBackingTrack(timePosition, backingTrackLength) {
22612
22656
  const mainState = this._mainState;
@@ -22614,11 +22658,8 @@ class MidiFileSequencer {
22614
22658
  if (timePosition < 0 || syncPoints.length === 0) {
22615
22659
  return timePosition;
22616
22660
  }
22617
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].data.millisecondOffset ? mainState.syncPointIndex : 0;
22618
- while (syncPointIndex + 1 < syncPoints.length &&
22619
- syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22620
- syncPointIndex++;
22621
- }
22661
+ this.updateCurrentTempo(this._mainState, timePosition);
22662
+ const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22622
22663
  const currentSyncPoint = syncPoints[syncPointIndex];
22623
22664
  const timeDiff = timePosition - currentSyncPoint.data.millisecondOffset;
22624
22665
  let alphaTabTimeDiff;
@@ -22640,7 +22681,10 @@ class MidiFileSequencer {
22640
22681
  return timePosition;
22641
22682
  }
22642
22683
  timePosition *= this.playbackSpeed;
22643
- let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].time ? mainState.syncPointIndex : 0;
22684
+ let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22685
+ if (timePosition < syncPoints[syncPointIndex].time) {
22686
+ syncPointIndex = 0;
22687
+ }
22644
22688
  while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].time <= timePosition) {
22645
22689
  syncPointIndex++;
22646
22690
  }
@@ -22678,26 +22722,6 @@ class MidiFileSequencer {
22678
22722
  timePosition += tickPosition * (60000.0 / (bpm * state.division));
22679
22723
  return timePosition / playbackSpeed;
22680
22724
  }
22681
- timePositionToTickPositionWithSpeed(state, timePosition, playbackSpeed) {
22682
- timePosition *= playbackSpeed;
22683
- let ticks = 0;
22684
- let bpm = 120.0;
22685
- let lastChange = 0;
22686
- // find start and bpm of last tempo change before time
22687
- for (const c of state.tempoChanges) {
22688
- if (timePosition < c.time) {
22689
- break;
22690
- }
22691
- ticks = c.ticks;
22692
- bpm = c.bpm;
22693
- lastChange = c.time;
22694
- }
22695
- // add the missing ticks
22696
- timePosition -= lastChange;
22697
- ticks += (timePosition / (60000.0 / (bpm * state.division))) | 0;
22698
- // we add 1 for possible rounding errors.(floating point issuses)
22699
- return ticks + 1;
22700
- }
22701
22725
  get internalEndTime() {
22702
22726
  if (this.isPlayingMain) {
22703
22727
  return !this.mainPlaybackRange ? this._currentState.endTime : this._currentState.playbackRangeEndTime;
@@ -26916,7 +26940,7 @@ class TinySoundFont {
26916
26940
  return processedEvents;
26917
26941
  }
26918
26942
  processMidiMessage(e) {
26919
- Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26943
+ //Logger.debug('Midi', `Processing Midi message ${MidiEventType[e.type]}/${e.tick}`);
26920
26944
  const command = e.type;
26921
26945
  switch (command) {
26922
26946
  case MidiEventType.TimeSignature:
@@ -28037,6 +28061,9 @@ class EventEmitter {
28037
28061
  }
28038
28062
  on(value) {
28039
28063
  this._listeners.push(value);
28064
+ return () => {
28065
+ this.off(value);
28066
+ };
28040
28067
  }
28041
28068
  off(value) {
28042
28069
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28056,6 +28083,9 @@ class EventEmitterOfT {
28056
28083
  }
28057
28084
  on(value) {
28058
28085
  this._listeners.push(value);
28086
+ return () => {
28087
+ this.off(value);
28088
+ };
28059
28089
  }
28060
28090
  off(value) {
28061
28091
  this._listeners = this._listeners.filter(l => l !== value);
@@ -28452,7 +28482,7 @@ class AlphaSynthBase {
28452
28482
  endTick = this.sequencer.currentEndTick;
28453
28483
  }
28454
28484
  if (this._tickPosition >= endTick) {
28455
- // fully done with playback of remaining samples?
28485
+ // fully done with playback of remaining samples?
28456
28486
  if (this._notPlayedSamples <= 0) {
28457
28487
  this._notPlayedSamples = 0;
28458
28488
  if (this.sequencer.isPlayingCountIn) {
@@ -35802,6 +35832,27 @@ class MidiFileGenerator {
35802
35832
  controller.moveNext();
35803
35833
  previousMasterBar = bar;
35804
35834
  }
35835
+ // here we interpolate the sync point which marks the end of the sync.
35836
+ // Sync points define new tempos at certain positions.
35837
+ // looking from the last sync point to the end we do not assume the end where the audio ends,
35838
+ // but where it ends according to the BPM and the remaining ticks.
35839
+ if (this.syncPoints.length > 0) {
35840
+ const lastSyncPoint = this.syncPoints[this.syncPoints.length - 1];
35841
+ const endTick = controller.currentTick;
35842
+ const remainingTicks = endTick - lastSyncPoint.tick;
35843
+ if (remainingTicks > 0) {
35844
+ const syncPointData = new SyncPointData();
35845
+ // last occurence of the last bar
35846
+ syncPointData.barOccurence = barOccurence.get(this._score.masterBars.length - 1);
35847
+ // same tempo as last point
35848
+ syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35849
+ // interpolated end from last syncPoint
35850
+ syncPointData.millisecondOffset =
35851
+ lastSyncPoint.data.millisecondOffset +
35852
+ MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35853
+ this.syncPoints.push(new BackingTrackSyncPoint(endTick, syncPointData));
35854
+ }
35855
+ }
35805
35856
  for (const track of this._score.tracks) {
35806
35857
  this._handler.finishTrack(track.index, controller.currentTick);
35807
35858
  }
@@ -38293,6 +38344,357 @@ class ExternalMediaPlayer extends BackingTrackPlayer {
38293
38344
  }
38294
38345
  }
38295
38346
 
38347
+ /**
38348
+ * A {@link IAlphaSynth} implementation wrapping and underling other {@link IAlphaSynth}
38349
+ * allowing dynamic changing of the underlying instance without loosing aspects like the
38350
+ * main playback information and event listeners.
38351
+ *
38352
+ * @remarks
38353
+ * This wrapper is used when re-exposing the underlying player via {@link AlphaTabApiBase} to integrators.
38354
+ * Even with dynamic switching between synthesizer, backing tracks etc. aspects like volume, playbackspeed,
38355
+ * event listeners etc. should not be lost.
38356
+ */
38357
+ class AlphaSynthWrapper {
38358
+ constructor() {
38359
+ // relevant state information we want to remember when switching between player instances
38360
+ this._masterVolume = 1;
38361
+ this._metronomeVolume = 0;
38362
+ this._countInVolume = 0;
38363
+ this._playbackSpeed = 1;
38364
+ this._isLooping = false;
38365
+ this._midiEventsPlayedFilter = [];
38366
+ this.ready = new EventEmitter();
38367
+ this.readyForPlayback = new EventEmitter();
38368
+ this.finished = new EventEmitter();
38369
+ this.soundFontLoaded = new EventEmitter();
38370
+ this.soundFontLoadFailed = new EventEmitterOfT();
38371
+ this.midiLoaded = new EventEmitterOfT();
38372
+ this.midiLoadFailed = new EventEmitterOfT();
38373
+ this.stateChanged = new EventEmitterOfT();
38374
+ this.positionChanged = new EventEmitterOfT();
38375
+ this.midiEventsPlayed = new EventEmitterOfT();
38376
+ this.playbackRangeChanged = new EventEmitterOfT();
38377
+ }
38378
+ get instance() {
38379
+ return this._instance;
38380
+ }
38381
+ set instance(value) {
38382
+ this._instance = value;
38383
+ // unregister all events from previous instance
38384
+ const unregister = this._instanceEventUnregister;
38385
+ if (unregister) {
38386
+ for (const e of unregister) {
38387
+ e();
38388
+ }
38389
+ }
38390
+ if (value) {
38391
+ // regsiter to events of new player and forward them to existing listeners
38392
+ const newUnregister = [];
38393
+ newUnregister.push(value.ready.on(() => this.ready.trigger()));
38394
+ newUnregister.push(value.readyForPlayback.on(() => this.readyForPlayback.trigger()));
38395
+ newUnregister.push(value.finished.on(() => this.finished.trigger()));
38396
+ newUnregister.push(value.soundFontLoaded.on(() => this.soundFontLoaded.trigger()));
38397
+ newUnregister.push(value.soundFontLoadFailed.on(e => this.soundFontLoadFailed.trigger(e)));
38398
+ newUnregister.push(value.midiLoaded.on(e => this.midiLoaded.trigger(e)));
38399
+ newUnregister.push(value.midiLoadFailed.on(e => this.midiLoadFailed.trigger(e)));
38400
+ newUnregister.push(value.stateChanged.on(e => this.stateChanged.trigger(e)));
38401
+ newUnregister.push(value.positionChanged.on(e => this.positionChanged.trigger(e)));
38402
+ newUnregister.push(value.midiEventsPlayed.on(e => this.midiEventsPlayed.trigger(e)));
38403
+ newUnregister.push(value.playbackRangeChanged.on(e => this.playbackRangeChanged.trigger(e)));
38404
+ this._instanceEventUnregister = newUnregister;
38405
+ // restore state on new player
38406
+ if (this.isReady) {
38407
+ value.masterVolume = this._masterVolume;
38408
+ value.metronomeVolume = this._metronomeVolume;
38409
+ value.countInVolume = this._countInVolume;
38410
+ value.playbackSpeed = this._playbackSpeed;
38411
+ value.isLooping = this._isLooping;
38412
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38413
+ }
38414
+ else {
38415
+ newUnregister.push(value.ready.on(() => {
38416
+ value.masterVolume = this._masterVolume;
38417
+ value.metronomeVolume = this._metronomeVolume;
38418
+ value.countInVolume = this._countInVolume;
38419
+ value.playbackSpeed = this._playbackSpeed;
38420
+ value.isLooping = this._isLooping;
38421
+ value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38422
+ }));
38423
+ }
38424
+ }
38425
+ else {
38426
+ this._instanceEventUnregister = undefined;
38427
+ }
38428
+ }
38429
+ get output() {
38430
+ return this._instance.output;
38431
+ }
38432
+ get isReady() {
38433
+ return this._instance ? this._instance.isReady : false;
38434
+ }
38435
+ get isReadyForPlayback() {
38436
+ return this._instance ? this._instance.isReadyForPlayback : false;
38437
+ }
38438
+ get state() {
38439
+ return this._instance ? this._instance.state : PlayerState.Paused;
38440
+ }
38441
+ get logLevel() {
38442
+ return Logger.logLevel;
38443
+ }
38444
+ set logLevel(value) {
38445
+ Logger.logLevel = value;
38446
+ if (this._instance) {
38447
+ this._instance.logLevel = value;
38448
+ }
38449
+ }
38450
+ get masterVolume() {
38451
+ return this._masterVolume;
38452
+ }
38453
+ set masterVolume(value) {
38454
+ value = Math.max(value, SynthConstants.MinVolume);
38455
+ this._masterVolume = value;
38456
+ if (this._instance) {
38457
+ this._instance.masterVolume = value;
38458
+ }
38459
+ }
38460
+ get metronomeVolume() {
38461
+ return this._metronomeVolume;
38462
+ }
38463
+ set metronomeVolume(value) {
38464
+ value = Math.max(value, SynthConstants.MinVolume);
38465
+ this._metronomeVolume = value;
38466
+ if (this._instance) {
38467
+ this._instance.metronomeVolume = value;
38468
+ }
38469
+ }
38470
+ get playbackSpeed() {
38471
+ return this._playbackSpeed;
38472
+ }
38473
+ set playbackSpeed(value) {
38474
+ this._playbackSpeed = value;
38475
+ if (this._instance) {
38476
+ this._instance.playbackSpeed = value;
38477
+ }
38478
+ }
38479
+ get tickPosition() {
38480
+ return this._instance ? this._instance.tickPosition : 0;
38481
+ }
38482
+ set tickPosition(value) {
38483
+ if (this._instance) {
38484
+ this._instance.tickPosition = value;
38485
+ }
38486
+ }
38487
+ get timePosition() {
38488
+ return this._instance ? this._instance.timePosition : 0;
38489
+ }
38490
+ set timePosition(value) {
38491
+ if (this._instance) {
38492
+ this._instance.timePosition = value;
38493
+ }
38494
+ }
38495
+ get playbackRange() {
38496
+ return this._instance ? this._instance.playbackRange : null;
38497
+ }
38498
+ set playbackRange(value) {
38499
+ if (this._instance) {
38500
+ this._instance.playbackRange = value;
38501
+ }
38502
+ }
38503
+ get isLooping() {
38504
+ return this._isLooping;
38505
+ }
38506
+ set isLooping(value) {
38507
+ this._isLooping = value;
38508
+ if (this._instance) {
38509
+ this._instance.isLooping = value;
38510
+ }
38511
+ }
38512
+ get countInVolume() {
38513
+ return this._countInVolume;
38514
+ }
38515
+ set countInVolume(value) {
38516
+ this._countInVolume = value;
38517
+ if (this._instance) {
38518
+ this._instance.countInVolume = value;
38519
+ }
38520
+ }
38521
+ get midiEventsPlayedFilter() {
38522
+ return this._midiEventsPlayedFilter;
38523
+ }
38524
+ set midiEventsPlayedFilter(value) {
38525
+ this._midiEventsPlayedFilter = value;
38526
+ if (this._instance) {
38527
+ this._instance.midiEventsPlayedFilter = value;
38528
+ }
38529
+ }
38530
+ destroy() {
38531
+ if (this._instance) {
38532
+ this._instance.destroy();
38533
+ this._instance = undefined;
38534
+ }
38535
+ }
38536
+ play() {
38537
+ return this._instance ? this._instance.play() : false;
38538
+ }
38539
+ pause() {
38540
+ if (this._instance) {
38541
+ this._instance.pause();
38542
+ }
38543
+ }
38544
+ playPause() {
38545
+ if (this._instance) {
38546
+ this._instance.playPause();
38547
+ }
38548
+ }
38549
+ stop() {
38550
+ if (this._instance) {
38551
+ this._instance.stop();
38552
+ }
38553
+ }
38554
+ playOneTimeMidiFile(midi) {
38555
+ if (this._instance) {
38556
+ this._instance.playOneTimeMidiFile(midi);
38557
+ }
38558
+ }
38559
+ loadSoundFont(data, append) {
38560
+ if (this._instance) {
38561
+ this._instance.loadSoundFont(data, append);
38562
+ }
38563
+ }
38564
+ resetSoundFonts() {
38565
+ if (this._instance) {
38566
+ this._instance.resetSoundFonts();
38567
+ }
38568
+ }
38569
+ loadMidiFile(midi) {
38570
+ if (this._instance) {
38571
+ this._instance.loadMidiFile(midi);
38572
+ }
38573
+ }
38574
+ loadBackingTrack(score, syncPoints) {
38575
+ if (this._instance) {
38576
+ this._instance.loadBackingTrack(score, syncPoints);
38577
+ }
38578
+ }
38579
+ applyTranspositionPitches(transpositionPitches) {
38580
+ if (this._instance) {
38581
+ this._instance.applyTranspositionPitches(transpositionPitches);
38582
+ }
38583
+ }
38584
+ setChannelTranspositionPitch(channel, semitones) {
38585
+ if (this._instance) {
38586
+ this._instance.setChannelTranspositionPitch(channel, semitones);
38587
+ }
38588
+ }
38589
+ setChannelMute(channel, mute) {
38590
+ if (this._instance) {
38591
+ this._instance.setChannelMute(channel, mute);
38592
+ }
38593
+ }
38594
+ resetChannelStates() {
38595
+ if (this._instance) {
38596
+ this._instance.resetChannelStates();
38597
+ }
38598
+ }
38599
+ setChannelSolo(channel, solo) {
38600
+ if (this._instance) {
38601
+ this._instance.setChannelSolo(channel, solo);
38602
+ }
38603
+ }
38604
+ setChannelVolume(channel, volume) {
38605
+ if (this._instance) {
38606
+ this._instance.setChannelVolume(channel, volume);
38607
+ }
38608
+ }
38609
+ }
38610
+
38611
+ /**
38612
+ * A {@link IScoreRenderer} implementation wrapping and underling other {@link IScoreRenderer}
38613
+ * allowing dynamic changing of the underlying instance without loosing aspects like the
38614
+ * event listeners.
38615
+ */
38616
+ class ScoreRendererWrapper {
38617
+ constructor() {
38618
+ this._width = 0;
38619
+ this._score = null;
38620
+ this._trackIndexes = null;
38621
+ this.preRender = new EventEmitterOfT();
38622
+ this.renderFinished = new EventEmitterOfT();
38623
+ this.partialRenderFinished = new EventEmitterOfT();
38624
+ this.partialLayoutFinished = new EventEmitterOfT();
38625
+ this.postRenderFinished = new EventEmitter();
38626
+ this.error = new EventEmitterOfT();
38627
+ }
38628
+ get instance() {
38629
+ return this._instance;
38630
+ }
38631
+ set instance(value) {
38632
+ this._instance = value;
38633
+ // unregister all events from previous instance
38634
+ const unregister = this._instanceEventUnregister;
38635
+ if (unregister) {
38636
+ for (const e of unregister) {
38637
+ e();
38638
+ }
38639
+ }
38640
+ if (value) {
38641
+ // regsiter to events of new player and forward them to existing listeners
38642
+ const newUnregister = [];
38643
+ newUnregister.push(value.preRender.on(v => this.preRender.trigger(v)));
38644
+ newUnregister.push(value.renderFinished.on(v => this.renderFinished.trigger(v)));
38645
+ newUnregister.push(value.partialRenderFinished.on(v => this.partialRenderFinished.trigger(v)));
38646
+ newUnregister.push(value.partialLayoutFinished.on(v => this.partialLayoutFinished.trigger(v)));
38647
+ newUnregister.push(value.postRenderFinished.on(() => this.postRenderFinished.trigger()));
38648
+ newUnregister.push(value.error.on(v => this.error.trigger(v)));
38649
+ this._instanceEventUnregister = newUnregister;
38650
+ if (this._settings) {
38651
+ value.updateSettings(this._settings);
38652
+ }
38653
+ value.width = this._width;
38654
+ if (this._score !== null) {
38655
+ value.renderScore(this._score, this._trackIndexes);
38656
+ }
38657
+ }
38658
+ else {
38659
+ this._instanceEventUnregister = undefined;
38660
+ }
38661
+ }
38662
+ get boundsLookup() {
38663
+ return this._instance ? this._instance.boundsLookup : null;
38664
+ }
38665
+ get width() {
38666
+ return this._instance ? this._instance.width : 0;
38667
+ }
38668
+ set width(value) {
38669
+ this._width = value;
38670
+ if (this._instance) {
38671
+ this._instance.width = value;
38672
+ }
38673
+ }
38674
+ render() {
38675
+ this._instance?.render();
38676
+ }
38677
+ resizeRender() {
38678
+ this._instance?.resizeRender();
38679
+ }
38680
+ renderScore(score, trackIndexes) {
38681
+ this._score = score;
38682
+ this._trackIndexes = trackIndexes;
38683
+ this._instance?.renderScore(score, trackIndexes);
38684
+ }
38685
+ renderResult(resultId) {
38686
+ this._instance?.renderResult(resultId);
38687
+ }
38688
+ updateSettings(settings) {
38689
+ this._settings = settings;
38690
+ this._instance?.updateSettings(settings);
38691
+ }
38692
+ destroy() {
38693
+ this._instance?.destroy();
38694
+ this._instance = undefined;
38695
+ }
38696
+ }
38697
+
38296
38698
  class SelectionInfo {
38297
38699
  constructor(beat) {
38298
38700
  this.bounds = null;
@@ -38312,6 +38714,18 @@ class AlphaTabApiBase {
38312
38714
  get actualPlayerMode() {
38313
38715
  return this._actualPlayerMode;
38314
38716
  }
38717
+ /**
38718
+ * The score renderer used for rendering the music sheet.
38719
+ * @remarks
38720
+ * This is the low-level API responsible for the actual rendering engine.
38721
+ * Gets access to the underling {@link IScoreRenderer} that is used for the rendering.
38722
+ *
38723
+ * @category Properties - Core
38724
+ * @since 0.9.4
38725
+ */
38726
+ get renderer() {
38727
+ return this._renderer;
38728
+ }
38315
38729
  /**
38316
38730
  * The score holding all information about the song being rendered
38317
38731
  * @category Properties - Core
@@ -38383,43 +38797,14 @@ class AlphaTabApiBase {
38383
38797
  this._tracks = [];
38384
38798
  this._actualPlayerMode = PlayerMode.Disabled;
38385
38799
  this._tickCache = null;
38386
- /**
38387
- * The alphaSynth player used for playback.
38388
- * @remarks
38389
- * This is the low-level API to the Midi synthesizer used for playback.
38390
- * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
38391
- * @category Properties - Player
38392
- * @since 0.9.4
38393
- * @example
38394
- * JavaScript
38395
- * ```js
38396
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
38397
- * setupPlayerEvents(api.settings);
38398
- * ```
38399
- *
38400
- * @example
38401
- * C#
38402
- * ```cs
38403
- * var api = new AlphaTabApi<MyControl>(...);
38404
- * SetupPlayerEvents(api.Player);
38405
- * ```
38406
- *
38407
- * @example
38408
- * Android
38409
- * ```kotlin
38410
- * val api = AlphaTabApi<MyControl>(...)
38411
- * setupPlayerEvents(api.player)
38412
- * ```
38413
- */
38414
- this.player = null;
38415
38800
  this._cursorWrapper = null;
38416
38801
  this._barCursor = null;
38417
38802
  this._beatCursor = null;
38418
38803
  this._selectionWrapper = null;
38419
38804
  this._previousTick = 0;
38420
- this._playerState = PlayerState.Paused;
38421
38805
  this._currentBeat = null;
38422
- this._currentBarBounds = null;
38806
+ this._currentBeatBounds = null;
38807
+ this._isInitialBeatCursorUpdate = true;
38423
38808
  this._previousStateForCursor = PlayerState.Paused;
38424
38809
  this._previousCursorCache = null;
38425
38810
  this._lastScroll = 0;
@@ -38990,133 +39375,6 @@ class AlphaTabApiBase {
38990
39375
  *
38991
39376
  */
38992
39377
  this.error = new EventEmitterOfT();
38993
- /**
38994
- * This event is fired when all required data for playback is loaded and ready.
38995
- * @remarks
38996
- * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
38997
- * all background workers are started, the audio output is initialized, a soundfont is loaded, and a song was loaded into the player as midi file.
38998
- *
38999
- * @eventProperty
39000
- * @category Events - Player
39001
- * @since 0.9.4
39002
- *
39003
- * @example
39004
- * JavaScript
39005
- * ```js
39006
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39007
- * api.playerReady.on(() => {
39008
- * enablePlayerControls();
39009
- * });
39010
- * ```
39011
- *
39012
- * @example
39013
- * C#
39014
- * ```cs
39015
- * var api = new AlphaTabApi<MyControl>(...);
39016
- * api.PlayerReady.On(() =>
39017
- * {
39018
- * EnablePlayerControls()
39019
- * });
39020
- * ```
39021
- *
39022
- * @example
39023
- * Android
39024
- * ```kotlin
39025
- * val api = AlphaTabApi<MyControl>(...)
39026
- * api.playerReady.on {
39027
- * enablePlayerControls()
39028
- * }
39029
- * ```
39030
- */
39031
- this.playerReady = new EventEmitter();
39032
- /**
39033
- * This event is fired when the playback of the whole song finished.
39034
- * @remarks
39035
- * This event is finished regardless on whether looping is enabled or not.
39036
- *
39037
- * @eventProperty
39038
- * @category Events - Player
39039
- * @since 0.9.4
39040
- *
39041
- * @example
39042
- * JavaScript
39043
- * ```js
39044
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39045
- * api.playerFinished.on((args) => {
39046
- * // speed trainer
39047
- * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
39048
- * });
39049
- * api.isLooping = true;
39050
- * api.playbackSpeed = 0.5;
39051
- * api.play()
39052
- * ```
39053
- *
39054
- * @example
39055
- * C#
39056
- * ```cs
39057
- * var api = new AlphaTabApi<MyControl>(...);
39058
- * api.PlayerFinished.On(() =>
39059
- * {
39060
- * // speed trainer
39061
- * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
39062
- * });
39063
- * api.IsLooping = true;
39064
- * api.PlaybackSpeed = 0.5;
39065
- * api.Play();
39066
- * ```
39067
- *
39068
- * @example
39069
- * Android
39070
- * ```kotlin
39071
- * val api = AlphaTabApi<MyControl>(...)
39072
- * api.playerFinished.on {
39073
- * // speed trainer
39074
- * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
39075
- * }
39076
- * api.isLooping = true
39077
- * api.playbackSpeed = 0.5
39078
- * api.play()
39079
- * ```
39080
- *
39081
- */
39082
- this.playerFinished = new EventEmitter();
39083
- /**
39084
- * This event is fired when the SoundFont needed for playback was loaded.
39085
- *
39086
- * @eventProperty
39087
- * @category Events - Player
39088
- * @since 0.9.4
39089
- *
39090
- * @example
39091
- * JavaScript
39092
- * ```js
39093
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39094
- * api.soundFontLoaded.on(() => {
39095
- * hideSoundFontLoadingIndicator();
39096
- * });
39097
- * ```
39098
- *
39099
- * @example
39100
- * C#
39101
- * ```cs
39102
- * var api = new AlphaTabApi<MyControl>(...);
39103
- * api.SoundFontLoaded.On(() =>
39104
- * {
39105
- * HideSoundFontLoadingIndicator();
39106
- * });
39107
- * ```
39108
- *
39109
- * @example
39110
- * Android
39111
- * ```kotlin
39112
- * val api = AlphaTabApi<MyControl>(...);
39113
- * api.soundFontLoaded.on {
39114
- * hideSoundFontLoadingIndicator();
39115
- * }
39116
- * ```
39117
- *
39118
- */
39119
- this.soundFontLoaded = new EventEmitter();
39120
39378
  /**
39121
39379
  * This event is fired when a Midi file is being loaded.
39122
39380
  *
@@ -39205,176 +39463,18 @@ class AlphaTabApiBase {
39205
39463
  */
39206
39464
  this.midiLoaded = new EventEmitterOfT();
39207
39465
  /**
39208
- * This event is fired when the playback state changed.
39209
- *
39210
- * @eventProperty
39211
- * @category Events - Player
39212
- * @since 0.9.4
39213
- *
39214
- * @example
39215
- * JavaScript
39216
- * ```js
39217
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39218
- * api.playerStateChanged.on((args) => {
39219
- * updatePlayerControls(args.state, args.stopped);
39220
- * });
39221
- * ```
39222
- *
39223
- * @example
39224
- * C#
39225
- * ```cs
39226
- * var api = new AlphaTabApi<MyControl>(...);
39227
- * api.PlayerStateChanged.On(args =>
39228
- * {
39229
- * UpdatePlayerControls(args);
39230
- * });
39231
- * ```
39232
- *
39233
- * @example
39234
- * Android
39235
- * ```kotlin
39236
- * val api = AlphaTabApi<MyControl>(...)
39237
- * api.playerStateChanged.on { args ->
39238
- * updatePlayerControls(args)
39239
- * }
39240
- * ```
39241
- *
39242
- */
39243
- this.playerStateChanged = new EventEmitterOfT();
39244
- /**
39245
- * This event is fired when the current playback position of the song changed.
39246
- *
39247
- * @eventProperty
39248
- * @category Events - Player
39249
- * @since 0.9.4
39250
- *
39251
- * @example
39252
- * JavaScript
39253
- * ```js
39254
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39255
- * api.playerPositionChanged.on((args) => {
39256
- * updatePlayerPosition(args);
39257
- * });
39258
- * ```
39259
- *
39260
- * @example
39261
- * C#
39262
- * ```cs
39263
- * var api = new AlphaTabApi<MyControl>(...);
39264
- * api.PlayerPositionChanged.On(args =>
39265
- * {
39266
- * UpdatePlayerPosition(args);
39267
- * });
39268
- * ```
39269
- *
39270
- * @example
39271
- * Android
39272
- * ```kotlin
39273
- * val api = AlphaTabApi<MyControl>(...)
39274
- * api.playerPositionChanged.on { args ->
39275
- * updatePlayerPosition(args)
39276
- * }
39277
- * ```
39278
- *
39279
- */
39280
- this.playerPositionChanged = new EventEmitterOfT();
39281
- /**
39282
- * This event is fired when the synthesizer played certain midi events.
39283
- *
39284
- * @remarks
39285
- * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
39286
- * audio playback elements like notes/rests played or metronome ticks.
39287
- *
39288
- * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
39289
- *
39290
- * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
39291
- * until some extend but highly encourage changing to the new models in case of problems.
39466
+ * This event is fired when a settings update was requested.
39292
39467
  *
39293
39468
  * @eventProperty
39294
- * @category Events - Player
39295
- * @since 1.2.0
39296
- *
39297
- * @example
39298
- * JavaScript
39299
- * ```js
39300
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39301
- * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
39302
- * api.midiEventsPlayed.on(function(e) {
39303
- * for(const midi of e.events) {
39304
- * if(midi.isMetronome) {
39305
- * console.log('Metronome tick ' + midi.tick);
39306
- * }
39307
- * }
39308
- * });
39309
- * ```
39310
- *
39311
- * @example
39312
- * C#
39313
- * ```cs
39314
- * var api = new AlphaTabApi<MyControl>(...);
39315
- * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
39316
- * api.MidiEventsPlayed.On(e =>
39317
- * {
39318
- * foreach(var midi of e.events)
39319
- * {
39320
- * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
39321
- * {
39322
- * Console.WriteLine("Metronome tick " + midi.Tick);
39323
- * }
39324
- * }
39325
- * });
39326
- * ```
39327
- *
39328
- * @example
39329
- * Android
39330
- * ```kotlin
39331
- * val api = AlphaTabApi<MyControl>(...);
39332
- * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
39333
- * api.midiEventsPlayed.on { e ->
39334
- * for (midi in e.events) {
39335
- * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
39336
- * println("Metronome tick " + midi.tick);
39337
- * }
39338
- * }
39339
- * }
39340
- * ```
39341
- * @see {@link MidiEvent}
39342
- * @see {@link TimeSignatureEvent}
39343
- * @see {@link AlphaTabMetronomeEvent}
39344
- * @see {@link AlphaTabRestEvent}
39345
- * @see {@link NoteOnEvent}
39346
- * @see {@link NoteOffEvent}
39347
- * @see {@link ControlChangeEvent}
39348
- * @see {@link ProgramChangeEvent}
39349
- * @see {@link TempoChangeEvent}
39350
- * @see {@link PitchBendEvent}
39351
- * @see {@link NoteBendEvent}
39352
- * @see {@link EndOfTrackEvent}
39353
- * @see {@link MetaEvent}
39354
- * @see {@link MetaDataEvent}
39355
- * @see {@link MetaNumberEvent}
39356
- * @see {@link Midi20PerNotePitchBendEvent}
39357
- * @see {@link SystemCommonEvent}
39358
- * @see {@link SystemExclusiveEvent}
39359
- */
39360
- this.midiEventsPlayed = new EventEmitterOfT();
39361
- /**
39362
- * This event is fired when the playback range changed.
39363
- *
39364
- * @eventProperty
39365
- * @category Events - Player
39366
- * @since 1.2.3
39469
+ * @category Events - Core
39470
+ * @since 1.6.0
39367
39471
  *
39368
39472
  * @example
39369
39473
  * JavaScript
39370
39474
  * ```js
39371
39475
  * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
39372
- * api.playbackRangeChanged.on((args) => {
39373
- * if (args.playbackRange) {
39374
- * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
39375
- * } else {
39376
- * clearHighlightInProgressBar();
39377
- * }
39476
+ * api.settingsUpdated.on(() => {
39477
+ * updateSettingsUI(api.settings);
39378
39478
  * });
39379
39479
  * ```
39380
39480
  *
@@ -39382,16 +39482,9 @@ class AlphaTabApiBase {
39382
39482
  * C#
39383
39483
  * ```cs
39384
39484
  * var api = new AlphaTabApi<MyControl>(...);
39385
- * api.PlaybackRangeChanged.On(args =>
39485
+ * api.SettingsUpdated.On(() =>
39386
39486
  * {
39387
- * if (args.PlaybackRange != null)
39388
- * {
39389
- * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
39390
- * }
39391
- * else
39392
- * {
39393
- * ClearHighlightInProgressBar();
39394
- * }
39487
+ * UpdateSettingsUI(api.settings);
39395
39488
  * });
39396
39489
  * ```
39397
39490
  *
@@ -39399,21 +39492,12 @@ class AlphaTabApiBase {
39399
39492
  * Android
39400
39493
  * ```kotlin
39401
39494
  * val api = AlphaTabApi<MyControl>(...)
39402
- * api.playbackRangeChanged.on { args ->
39403
- * val playbackRange = args.playbackRange
39404
- * if (playbackRange != null) {
39405
- * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
39406
- * } else {
39407
- * clearHighlightInProgressBar()
39408
- * }
39495
+ * api.SettingsUpdated.on {
39496
+ * updateSettingsUI(api.settings)
39409
39497
  * }
39410
39498
  * ```
39411
39499
  *
39412
39500
  */
39413
- this.playbackRangeChanged = new EventEmitterOfT();
39414
- /**
39415
- * @internal
39416
- */
39417
39501
  this.settingsUpdated = new EventEmitter();
39418
39502
  this.uiFacade = uiFacade;
39419
39503
  this.container = uiFacade.rootContainer;
@@ -39426,48 +39510,49 @@ class AlphaTabApiBase {
39426
39510
  Environment.printEnvironmentInfo(false);
39427
39511
  this.canvasElement = uiFacade.createCanvasElement();
39428
39512
  this.container.appendChild(this.canvasElement);
39513
+ this._renderer = new ScoreRendererWrapper();
39429
39514
  if (this.settings.core.useWorkers &&
39430
39515
  this.uiFacade.areWorkersSupported &&
39431
39516
  Environment.getRenderEngineFactory(this.settings.core.engine).supportsWorkers) {
39432
- this.renderer = this.uiFacade.createWorkerRenderer();
39517
+ this._renderer.instance = this.uiFacade.createWorkerRenderer();
39433
39518
  }
39434
39519
  else {
39435
- this.renderer = new ScoreRenderer(this.settings);
39520
+ this._renderer.instance = new ScoreRenderer(this.settings);
39436
39521
  }
39437
39522
  this.container.resize.on(Environment.throttle(() => {
39438
39523
  if (this._isDestroyed) {
39439
39524
  return;
39440
39525
  }
39441
- if (this.container.width !== this.renderer.width) {
39526
+ if (this.container.width !== this._renderer.width) {
39442
39527
  this.triggerResize();
39443
39528
  }
39444
39529
  }, uiFacade.resizeThrottle));
39445
39530
  const initialResizeEventInfo = new ResizeEventArgs();
39446
- initialResizeEventInfo.oldWidth = this.renderer.width;
39531
+ initialResizeEventInfo.oldWidth = this._renderer.width;
39447
39532
  initialResizeEventInfo.newWidth = this.container.width | 0;
39448
39533
  initialResizeEventInfo.settings = this.settings;
39449
39534
  this.onResize(initialResizeEventInfo);
39450
- this.renderer.preRender.on(this.onRenderStarted.bind(this));
39451
- this.renderer.renderFinished.on(renderingResult => {
39535
+ this._renderer.preRender.on(this.onRenderStarted.bind(this));
39536
+ this._renderer.renderFinished.on(renderingResult => {
39452
39537
  this.onRenderFinished(renderingResult);
39453
39538
  });
39454
- this.renderer.postRenderFinished.on(() => {
39539
+ this._renderer.postRenderFinished.on(() => {
39455
39540
  const duration = Date.now() - this._startTime;
39456
39541
  Logger.debug('rendering', `Rendering completed in ${duration}ms`);
39457
39542
  this.onPostRenderFinished();
39458
39543
  });
39459
- this.renderer.preRender.on(_ => {
39544
+ this._renderer.preRender.on(_ => {
39460
39545
  this._startTime = Date.now();
39461
39546
  });
39462
- this.renderer.partialLayoutFinished.on(this.appendRenderResult.bind(this));
39463
- this.renderer.partialRenderFinished.on(this.updateRenderResult.bind(this));
39464
- this.renderer.renderFinished.on(r => {
39465
- this.appendRenderResult(r);
39466
- this.appendRenderResult(null); // marks last element
39547
+ this._renderer.partialLayoutFinished.on(r => this.appendRenderResult(r, false));
39548
+ this._renderer.partialRenderFinished.on(this.updateRenderResult.bind(this));
39549
+ this._renderer.renderFinished.on(r => {
39550
+ this.appendRenderResult(r, true);
39467
39551
  });
39468
- this.renderer.error.on(this.onError.bind(this));
39552
+ this._renderer.error.on(this.onError.bind(this));
39553
+ this.setupPlayerWrapper();
39469
39554
  if (this.settings.player.playerMode !== PlayerMode.Disabled) {
39470
- this.setupPlayer();
39555
+ this.setupOrDestroyPlayer();
39471
39556
  }
39472
39557
  this.setupClickHandling();
39473
39558
  // delay rendering to allow ui to hook up with events first.
@@ -39475,6 +39560,36 @@ class AlphaTabApiBase {
39475
39560
  this.uiFacade.initialRender();
39476
39561
  });
39477
39562
  }
39563
+ setupPlayerWrapper() {
39564
+ const player = new AlphaSynthWrapper();
39565
+ this._player = player;
39566
+ player.ready.on(() => {
39567
+ this.loadMidiForScore();
39568
+ });
39569
+ player.readyForPlayback.on(() => {
39570
+ this.onPlayerReady();
39571
+ if (this.tracks) {
39572
+ for (const track of this.tracks) {
39573
+ const volume = track.playbackInfo.volume / 16;
39574
+ player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
39575
+ player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
39576
+ }
39577
+ }
39578
+ });
39579
+ player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
39580
+ player.soundFontLoadFailed.on(e => {
39581
+ this.onError(e);
39582
+ });
39583
+ player.midiLoaded.on(this.onMidiLoaded.bind(this));
39584
+ player.midiLoadFailed.on(e => {
39585
+ this.onError(e);
39586
+ });
39587
+ player.stateChanged.on(this.onPlayerStateChanged.bind(this));
39588
+ player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
39589
+ player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
39590
+ player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
39591
+ player.finished.on(this.onPlayerFinished.bind(this));
39592
+ }
39478
39593
  /**
39479
39594
  * Destroys the alphaTab control and restores the initial state of the UI.
39480
39595
  * @remarks
@@ -39507,11 +39622,9 @@ class AlphaTabApiBase {
39507
39622
  */
39508
39623
  destroy() {
39509
39624
  this._isDestroyed = true;
39510
- if (this.player) {
39511
- this.player.destroy();
39512
- }
39625
+ this._player.destroy();
39513
39626
  this.uiFacade.destroy();
39514
- this.renderer.destroy();
39627
+ this._renderer.destroy();
39515
39628
  }
39516
39629
  /**
39517
39630
  * Applies any changes that were done to the settings object.
@@ -39556,17 +39669,29 @@ class AlphaTabApiBase {
39556
39669
  if (score) {
39557
39670
  ModelUtils.applyPitchOffsets(this.settings, score);
39558
39671
  }
39559
- this.renderer.updateSettings(this.settings);
39560
- // enable/disable player if needed
39561
- if (this.settings.player.playerMode !== PlayerMode.Disabled) {
39562
- if (this.setupPlayer() && score) {
39563
- this.loadMidiForScore();
39672
+ this.updateRenderer();
39673
+ this._renderer.updateSettings(this.settings);
39674
+ this.setupOrDestroyPlayer();
39675
+ this.onSettingsUpdated();
39676
+ }
39677
+ updateRenderer() {
39678
+ const renderer = this._renderer;
39679
+ if (this.settings.core.useWorkers &&
39680
+ this.uiFacade.areWorkersSupported &&
39681
+ Environment.getRenderEngineFactory(this.settings.core.engine).supportsWorkers) {
39682
+ // switch from non-worker to worker renderer
39683
+ if (renderer.instance instanceof ScoreRenderer) {
39684
+ renderer.destroy();
39685
+ renderer.instance = this.uiFacade.createWorkerRenderer();
39564
39686
  }
39565
39687
  }
39566
39688
  else {
39567
- this.destroyPlayer();
39689
+ // switch from worker to non-worker renderer
39690
+ if (!(renderer.instance instanceof ScoreRenderer)) {
39691
+ renderer.destroy();
39692
+ renderer.instance = new ScoreRenderer(this.settings);
39693
+ }
39568
39694
  }
39569
- this.onSettingsUpdated();
39570
39695
  }
39571
39696
  /**
39572
39697
  * Initiates a load of the score using the given data.
@@ -39762,30 +39887,33 @@ class AlphaTabApiBase {
39762
39887
  }
39763
39888
  else {
39764
39889
  const resizeEventInfo = new ResizeEventArgs();
39765
- resizeEventInfo.oldWidth = this.renderer.width;
39890
+ resizeEventInfo.oldWidth = this._renderer.width;
39766
39891
  resizeEventInfo.newWidth = this.container.width;
39767
39892
  resizeEventInfo.settings = this.settings;
39768
39893
  this.onResize(resizeEventInfo);
39769
- this.renderer.updateSettings(this.settings);
39770
- this.renderer.width = this.container.width;
39771
- this.renderer.resizeRender();
39894
+ this._renderer.updateSettings(this.settings);
39895
+ this._renderer.width = this.container.width;
39896
+ this._renderer.resizeRender();
39772
39897
  }
39773
39898
  }
39774
- appendRenderResult(result) {
39775
- if (result) {
39899
+ appendRenderResult(result, isLast) {
39900
+ // resizing the canvas and wrapper elements at the end is enough
39901
+ // it avoids flickering on resizes and re-renders.
39902
+ // the individual partials are anyhow sized correctly
39903
+ if (isLast) {
39776
39904
  this.canvasElement.width = result.totalWidth;
39777
39905
  this.canvasElement.height = result.totalHeight;
39778
39906
  if (this._cursorWrapper) {
39779
39907
  this._cursorWrapper.width = result.totalWidth;
39780
39908
  this._cursorWrapper.height = result.totalHeight;
39781
39909
  }
39782
- if (result.width > 0 || result.height > 0) {
39783
- this.uiFacade.beginAppendRenderResults(result);
39784
- }
39785
39910
  }
39786
- else {
39911
+ if (result.width > 0 || result.height > 0) {
39787
39912
  this.uiFacade.beginAppendRenderResults(result);
39788
39913
  }
39914
+ if (isLast) {
39915
+ this.uiFacade.beginAppendRenderResults(null);
39916
+ }
39789
39917
  }
39790
39918
  updateRenderResult(result) {
39791
39919
  if (result && result.renderResult) {
@@ -39871,9 +39999,6 @@ class AlphaTabApiBase {
39871
39999
  * ```
39872
40000
  */
39873
40001
  loadSoundFont(data, append = false) {
39874
- if (!this.player) {
39875
- return false;
39876
- }
39877
40002
  return this.uiFacade.loadSoundFont(data, append);
39878
40003
  }
39879
40004
  /**
@@ -39919,10 +40044,7 @@ class AlphaTabApiBase {
39919
40044
  * ```
39920
40045
  */
39921
40046
  resetSoundFonts() {
39922
- if (!this.player) {
39923
- return;
39924
- }
39925
- this.player.resetSoundFonts();
40047
+ this._player.resetSoundFonts();
39926
40048
  }
39927
40049
  /**
39928
40050
  * Initiates a re-rendering of the current setup.
@@ -39953,13 +40075,10 @@ class AlphaTabApiBase {
39953
40075
  * ```
39954
40076
  */
39955
40077
  render() {
39956
- if (!this.renderer) {
39957
- return;
39958
- }
39959
40078
  if (this.uiFacade.canRender) {
39960
40079
  // when font is finally loaded, start rendering
39961
- this.renderer.width = this.container.width;
39962
- this.renderer.renderScore(this.score, this._trackIndexes);
40080
+ this._renderer.width = this.container.width;
40081
+ this._renderer.renderScore(this.score, this._trackIndexes);
39963
40082
  }
39964
40083
  else {
39965
40084
  this.uiFacade.canRenderChanged.on(() => this.render());
@@ -40034,7 +40153,38 @@ class AlphaTabApiBase {
40034
40153
  * @since 1.5.0
40035
40154
  */
40036
40155
  get boundsLookup() {
40037
- return this.renderer.boundsLookup;
40156
+ return this._renderer.boundsLookup;
40157
+ }
40158
+ /**
40159
+ * The alphaSynth player used for playback.
40160
+ * @remarks
40161
+ * This is the low-level API to the Midi synthesizer used for playback.
40162
+ * Gets access to the underling {@link IAlphaSynth} that is used for the audio playback.
40163
+ * @category Properties - Player
40164
+ * @since 0.9.4
40165
+ * @example
40166
+ * JavaScript
40167
+ * ```js
40168
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
40169
+ * setupPlayerEvents(api.settings);
40170
+ * ```
40171
+ *
40172
+ * @example
40173
+ * C#
40174
+ * ```cs
40175
+ * var api = new AlphaTabApi<MyControl>(...);
40176
+ * SetupPlayerEvents(api.Player);
40177
+ * ```
40178
+ *
40179
+ * @example
40180
+ * Android
40181
+ * ```kotlin
40182
+ * val api = AlphaTabApi<MyControl>(...)
40183
+ * setupPlayerEvents(api.player)
40184
+ * ```
40185
+ */
40186
+ get player() {
40187
+ return this._player.instance ? this._player : null;
40038
40188
  }
40039
40189
  /**
40040
40190
  * Whether the player is ready for starting the playback.
@@ -40065,10 +40215,7 @@ class AlphaTabApiBase {
40065
40215
  * ```
40066
40216
  */
40067
40217
  get isReadyForPlayback() {
40068
- if (!this.player) {
40069
- return false;
40070
- }
40071
- return this.player.isReadyForPlayback;
40218
+ return this._player.isReadyForPlayback;
40072
40219
  }
40073
40220
  /**
40074
40221
  * The current player state.
@@ -40098,10 +40245,7 @@ class AlphaTabApiBase {
40098
40245
  * ```
40099
40246
  */
40100
40247
  get playerState() {
40101
- if (!this.player) {
40102
- return PlayerState.Paused;
40103
- }
40104
- return this.player.state;
40248
+ return this._player.state;
40105
40249
  }
40106
40250
  /**
40107
40251
  * The current master volume as percentage (0-1).
@@ -40131,15 +40275,10 @@ class AlphaTabApiBase {
40131
40275
  * ```
40132
40276
  */
40133
40277
  get masterVolume() {
40134
- if (!this.player) {
40135
- return 0;
40136
- }
40137
- return this.player.masterVolume;
40278
+ return this._player.masterVolume;
40138
40279
  }
40139
40280
  set masterVolume(value) {
40140
- if (this.player) {
40141
- this.player.masterVolume = value;
40142
- }
40281
+ this._player.masterVolume = value;
40143
40282
  }
40144
40283
  /**
40145
40284
  * The metronome volume as percentage (0-1).
@@ -40170,15 +40309,10 @@ class AlphaTabApiBase {
40170
40309
  * ```
40171
40310
  */
40172
40311
  get metronomeVolume() {
40173
- if (!this.player) {
40174
- return 0;
40175
- }
40176
- return this.player.metronomeVolume;
40312
+ return this._player.metronomeVolume;
40177
40313
  }
40178
40314
  set metronomeVolume(value) {
40179
- if (this.player) {
40180
- this.player.metronomeVolume = value;
40181
- }
40315
+ this._player.metronomeVolume = value;
40182
40316
  }
40183
40317
  /**
40184
40318
  * The volume of the count-in metronome ticks.
@@ -40209,15 +40343,10 @@ class AlphaTabApiBase {
40209
40343
  * ```
40210
40344
  */
40211
40345
  get countInVolume() {
40212
- if (!this.player) {
40213
- return 0;
40214
- }
40215
- return this.player.countInVolume;
40346
+ return this._player.countInVolume;
40216
40347
  }
40217
40348
  set countInVolume(value) {
40218
- if (this.player) {
40219
- this.player.countInVolume = value;
40220
- }
40349
+ this._player.countInVolume = value;
40221
40350
  }
40222
40351
  /**
40223
40352
  * The midi events which will trigger the `midiEventsPlayed` event
@@ -40276,15 +40405,10 @@ class AlphaTabApiBase {
40276
40405
  * ```
40277
40406
  */
40278
40407
  get midiEventsPlayedFilter() {
40279
- if (!this.player) {
40280
- return [];
40281
- }
40282
- return this.player.midiEventsPlayedFilter;
40408
+ return this._player.midiEventsPlayedFilter;
40283
40409
  }
40284
40410
  set midiEventsPlayedFilter(value) {
40285
- if (this.player) {
40286
- this.player.midiEventsPlayedFilter = value;
40287
- }
40411
+ this._player.midiEventsPlayedFilter = value;
40288
40412
  }
40289
40413
  /**
40290
40414
  * The position within the song in midi ticks.
@@ -40312,15 +40436,10 @@ class AlphaTabApiBase {
40312
40436
  * ```
40313
40437
  */
40314
40438
  get tickPosition() {
40315
- if (!this.player) {
40316
- return 0;
40317
- }
40318
- return this.player.tickPosition;
40439
+ return this._player.tickPosition;
40319
40440
  }
40320
40441
  set tickPosition(value) {
40321
- if (this.player) {
40322
- this.player.tickPosition = value;
40323
- }
40442
+ this._player.tickPosition = value;
40324
40443
  }
40325
40444
  /**
40326
40445
  * The position within the song in milliseconds
@@ -40348,15 +40467,10 @@ class AlphaTabApiBase {
40348
40467
  * ```
40349
40468
  */
40350
40469
  get timePosition() {
40351
- if (!this.player) {
40352
- return 0;
40353
- }
40354
- return this.player.timePosition;
40470
+ return this._player.timePosition;
40355
40471
  }
40356
40472
  set timePosition(value) {
40357
- if (this.player) {
40358
- this.player.timePosition = value;
40359
- }
40473
+ this._player.timePosition = value;
40360
40474
  }
40361
40475
  /**
40362
40476
  * The range of the song that should be played.
@@ -40390,17 +40504,12 @@ class AlphaTabApiBase {
40390
40504
  * ```
40391
40505
  */
40392
40506
  get playbackRange() {
40393
- if (!this.player) {
40394
- return null;
40395
- }
40396
- return this.player.playbackRange;
40507
+ return this._player.playbackRange;
40397
40508
  }
40398
40509
  set playbackRange(value) {
40399
- if (this.player) {
40400
- this.player.playbackRange = value;
40401
- if (this.settings.player.enableCursor) {
40402
- this.updateSelectionCursor(value);
40403
- }
40510
+ this._player.playbackRange = value;
40511
+ if (this.settings.player.enableCursor) {
40512
+ this.updateSelectionCursor(value);
40404
40513
  }
40405
40514
  }
40406
40515
  /**
@@ -40432,15 +40541,10 @@ class AlphaTabApiBase {
40432
40541
  * ```
40433
40542
  */
40434
40543
  get playbackSpeed() {
40435
- if (!this.player) {
40436
- return 0;
40437
- }
40438
- return this.player.playbackSpeed;
40544
+ return this._player.playbackSpeed;
40439
40545
  }
40440
40546
  set playbackSpeed(value) {
40441
- if (this.player) {
40442
- this.player.playbackSpeed = value;
40443
- }
40547
+ this._player.playbackSpeed = value;
40444
40548
  }
40445
40549
  /**
40446
40550
  * Whether the playback should automatically restart after it finished.
@@ -40471,27 +40575,21 @@ class AlphaTabApiBase {
40471
40575
  * ```
40472
40576
  */
40473
40577
  get isLooping() {
40474
- if (!this.player) {
40475
- return false;
40476
- }
40477
- return this.player.isLooping;
40578
+ return this._player.isLooping;
40478
40579
  }
40479
40580
  set isLooping(value) {
40480
- if (this.player) {
40481
- this.player.isLooping = value;
40482
- }
40581
+ this._player.isLooping = value;
40483
40582
  }
40484
40583
  destroyPlayer() {
40485
- if (!this.player) {
40486
- return;
40487
- }
40488
- this.player.destroy();
40489
- this.player = null;
40584
+ this._player.destroy();
40490
40585
  this._previousTick = 0;
40491
- this._playerState = PlayerState.Paused;
40492
40586
  this.destroyCursors();
40493
40587
  }
40494
- setupPlayer() {
40588
+ /**
40589
+ *
40590
+ * @returns true if a new player was created, false if no player was created (includes destroy & reuse of the current one)
40591
+ */
40592
+ setupOrDestroyPlayer() {
40495
40593
  let mode = this.settings.player.playerMode;
40496
40594
  if (mode === PlayerMode.EnabledAutomatic) {
40497
40595
  const score = this.score;
@@ -40505,68 +40603,44 @@ class AlphaTabApiBase {
40505
40603
  mode = PlayerMode.EnabledSynthesizer;
40506
40604
  }
40507
40605
  }
40606
+ let newPlayer = null;
40508
40607
  if (mode !== this._actualPlayerMode) {
40509
40608
  this.destroyPlayer();
40609
+ this.updateCursors();
40610
+ switch (mode) {
40611
+ case PlayerMode.Disabled:
40612
+ newPlayer = null;
40613
+ break;
40614
+ case PlayerMode.EnabledSynthesizer:
40615
+ newPlayer = this.uiFacade.createWorkerPlayer();
40616
+ break;
40617
+ case PlayerMode.EnabledBackingTrack:
40618
+ newPlayer = this.uiFacade.createBackingTrackPlayer();
40619
+ break;
40620
+ case PlayerMode.EnabledExternalMedia:
40621
+ newPlayer = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40622
+ break;
40623
+ }
40510
40624
  }
40511
- this.updateCursors();
40512
- this._actualPlayerMode = mode;
40513
- switch (mode) {
40514
- case PlayerMode.Disabled:
40515
- this.destroyPlayer();
40516
- return false;
40517
- case PlayerMode.EnabledSynthesizer:
40518
- if (this.player) {
40519
- return true;
40520
- }
40521
- // new player needed
40522
- this.player = this.uiFacade.createWorkerPlayer();
40523
- break;
40524
- case PlayerMode.EnabledBackingTrack:
40525
- if (this.player) {
40526
- return true;
40527
- }
40528
- // new player needed
40529
- this.player = this.uiFacade.createBackingTrackPlayer();
40530
- break;
40531
- case PlayerMode.EnabledExternalMedia:
40532
- if (this.player) {
40533
- return true;
40534
- }
40535
- this.player = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40536
- break;
40625
+ else {
40626
+ // no change in player mode, just update song info if needed
40627
+ this.updateCursors();
40628
+ return false;
40537
40629
  }
40538
- if (!this.player) {
40630
+ this._actualPlayerMode = mode;
40631
+ if (!newPlayer) {
40539
40632
  return false;
40540
40633
  }
40541
- this.player.ready.on(() => {
40542
- this.loadMidiForScore();
40543
- });
40544
- this.player.readyForPlayback.on(() => {
40545
- this.onPlayerReady();
40546
- if (this.tracks) {
40547
- for (const track of this.tracks) {
40548
- const volume = track.playbackInfo.volume / 16;
40549
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40550
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40551
- }
40552
- }
40553
- });
40554
- this.player.soundFontLoaded.on(this.onSoundFontLoaded.bind(this));
40555
- this.player.soundFontLoadFailed.on(e => {
40556
- this.onError(e);
40557
- });
40558
- this.player.midiLoaded.on(this.onMidiLoaded.bind(this));
40559
- this.player.midiLoadFailed.on(e => {
40560
- this.onError(e);
40561
- });
40562
- this.player.stateChanged.on(this.onPlayerStateChanged.bind(this));
40563
- this.player.positionChanged.on(this.onPlayerPositionChanged.bind(this));
40564
- this.player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this));
40565
- this.player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
40566
- this.player.finished.on(this.onPlayerFinished.bind(this));
40567
- this.setupPlayerEvents();
40634
+ this._player.instance = newPlayer;
40568
40635
  return false;
40569
40636
  }
40637
+ /**
40638
+ * Re-creates the midi for the current score and loads it.
40639
+ * @remarks
40640
+ * This will result in the player to stop playback. Some setting changes require re-genration of the midi song.
40641
+ * @category Methods - Player
40642
+ * @since 1.6.0
40643
+ */
40570
40644
  loadMidiForScore() {
40571
40645
  if (!this.score) {
40572
40646
  return;
@@ -40584,12 +40658,10 @@ class AlphaTabApiBase {
40584
40658
  generator.generate();
40585
40659
  this._tickCache = generator.tickLookup;
40586
40660
  this.onMidiLoad(midiFile);
40587
- const player = this.player;
40588
- if (player) {
40589
- player.loadMidiFile(midiFile);
40590
- player.loadBackingTrack(score, generator.syncPoints);
40591
- player.applyTranspositionPitches(generator.transpositionPitches);
40592
- }
40661
+ const player = this._player;
40662
+ player.loadMidiFile(midiFile);
40663
+ player.loadBackingTrack(score, generator.syncPoints);
40664
+ player.applyTranspositionPitches(generator.transpositionPitches);
40593
40665
  }
40594
40666
  /**
40595
40667
  * Changes the volume of the given tracks.
@@ -40627,12 +40699,9 @@ class AlphaTabApiBase {
40627
40699
  * ```
40628
40700
  */
40629
40701
  changeTrackVolume(tracks, volume) {
40630
- if (!this.player) {
40631
- return;
40632
- }
40633
40702
  for (const track of tracks) {
40634
- this.player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40635
- this.player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40703
+ this._player.setChannelVolume(track.playbackInfo.primaryChannel, volume);
40704
+ this._player.setChannelVolume(track.playbackInfo.secondaryChannel, volume);
40636
40705
  }
40637
40706
  }
40638
40707
  /**
@@ -40669,12 +40738,9 @@ class AlphaTabApiBase {
40669
40738
  * ```
40670
40739
  */
40671
40740
  changeTrackSolo(tracks, solo) {
40672
- if (!this.player) {
40673
- return;
40674
- }
40675
40741
  for (const track of tracks) {
40676
- this.player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40677
- this.player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40742
+ this._player.setChannelSolo(track.playbackInfo.primaryChannel, solo);
40743
+ this._player.setChannelSolo(track.playbackInfo.secondaryChannel, solo);
40678
40744
  }
40679
40745
  }
40680
40746
  /**
@@ -40710,12 +40776,9 @@ class AlphaTabApiBase {
40710
40776
  * ```
40711
40777
  */
40712
40778
  changeTrackMute(tracks, mute) {
40713
- if (!this.player) {
40714
- return;
40715
- }
40716
40779
  for (const track of tracks) {
40717
- this.player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40718
- this.player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40780
+ this._player.setChannelMute(track.playbackInfo.primaryChannel, mute);
40781
+ this._player.setChannelMute(track.playbackInfo.secondaryChannel, mute);
40719
40782
  }
40720
40783
  }
40721
40784
  /**
@@ -40753,12 +40816,9 @@ class AlphaTabApiBase {
40753
40816
  * ```
40754
40817
  */
40755
40818
  changeTrackTranspositionPitch(tracks, semitones) {
40756
- if (!this.player) {
40757
- return;
40758
- }
40759
40819
  for (const track of tracks) {
40760
- this.player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40761
- this.player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40820
+ this._player.setChannelTranspositionPitch(track.playbackInfo.primaryChannel, semitones);
40821
+ this._player.setChannelTranspositionPitch(track.playbackInfo.secondaryChannel, semitones);
40762
40822
  }
40763
40823
  }
40764
40824
  /**
@@ -40789,10 +40849,7 @@ class AlphaTabApiBase {
40789
40849
  * ```
40790
40850
  */
40791
40851
  play() {
40792
- if (!this.player) {
40793
- return false;
40794
- }
40795
- return this.player.play();
40852
+ return this._player.play();
40796
40853
  }
40797
40854
  /**
40798
40855
  * Pauses the playback of the current song.
@@ -40821,10 +40878,7 @@ class AlphaTabApiBase {
40821
40878
  * ```
40822
40879
  */
40823
40880
  pause() {
40824
- if (!this.player) {
40825
- return;
40826
- }
40827
- this.player.pause();
40881
+ this._player.pause();
40828
40882
  }
40829
40883
  /**
40830
40884
  * Toggles between play/pause depending on the current player state.
@@ -40855,10 +40909,7 @@ class AlphaTabApiBase {
40855
40909
  * ```
40856
40910
  */
40857
40911
  playPause() {
40858
- if (!this.player) {
40859
- return;
40860
- }
40861
- this.player.playPause();
40912
+ this._player.playPause();
40862
40913
  }
40863
40914
  /**
40864
40915
  * Stops the playback of the current song, and moves the playback position back to the start.
@@ -40889,10 +40940,7 @@ class AlphaTabApiBase {
40889
40940
  * ```
40890
40941
  */
40891
40942
  stop() {
40892
- if (!this.player) {
40893
- return;
40894
- }
40895
- this.player.stop();
40943
+ this._player.stop();
40896
40944
  }
40897
40945
  /**
40898
40946
  * Triggers the play of the given beat.
@@ -40928,15 +40976,12 @@ class AlphaTabApiBase {
40928
40976
  * ```
40929
40977
  */
40930
40978
  playBeat(beat) {
40931
- if (!this.player) {
40932
- return;
40933
- }
40934
40979
  // we generate a new midi file containing only the beat
40935
40980
  const midiFile = new MidiFile();
40936
40981
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40937
40982
  const generator = new MidiFileGenerator(beat.voice.bar.staff.track.score, this.settings, handler);
40938
40983
  generator.generateSingleBeat(beat);
40939
- this.player.playOneTimeMidiFile(midiFile);
40984
+ this._player.playOneTimeMidiFile(midiFile);
40940
40985
  }
40941
40986
  /**
40942
40987
  * Triggers the play of the given note.
@@ -40971,15 +41016,12 @@ class AlphaTabApiBase {
40971
41016
  * ```
40972
41017
  */
40973
41018
  playNote(note) {
40974
- if (!this.player) {
40975
- return;
40976
- }
40977
41019
  // we generate a new midi file containing only the beat
40978
41020
  const midiFile = new MidiFile();
40979
41021
  const handler = new AlphaSynthMidiFileHandler(midiFile);
40980
41022
  const generator = new MidiFileGenerator(note.beat.voice.bar.staff.track.score, this.settings, handler);
40981
41023
  generator.generateSingleNote(note);
40982
- this.player.playOneTimeMidiFile(midiFile);
41024
+ this._player.playOneTimeMidiFile(midiFile);
40983
41025
  }
40984
41026
  destroyCursors() {
40985
41027
  if (!this._cursorWrapper) {
@@ -41002,6 +41044,7 @@ class AlphaTabApiBase {
41002
41044
  this._barCursor = cursors.barCursor;
41003
41045
  this._beatCursor = cursors.beatCursor;
41004
41046
  this._selectionWrapper = cursors.selectionWrapper;
41047
+ this._isInitialBeatCursorUpdate = true;
41005
41048
  }
41006
41049
  if (this._currentBeat !== null) {
41007
41050
  this.cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
@@ -41011,36 +41054,6 @@ class AlphaTabApiBase {
41011
41054
  this.destroyCursors();
41012
41055
  }
41013
41056
  }
41014
- setupPlayerEvents() {
41015
- //
41016
- // Hook into events
41017
- this._previousTick = 0;
41018
- this._playerState = PlayerState.Paused;
41019
- // we need to update our position caches if we render a tablature
41020
- this.renderer.postRenderFinished.on(() => {
41021
- this._currentBeat = null;
41022
- this.cursorUpdateTick(this._previousTick, false, 1, this._previousTick > 10);
41023
- });
41024
- if (this.player) {
41025
- this.player.positionChanged.on(e => {
41026
- this._previousTick = e.currentTick;
41027
- this.uiFacade.beginInvoke(() => {
41028
- const cursorSpeed = e.modifiedTempo / e.originalTempo;
41029
- this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41030
- });
41031
- });
41032
- this.player.stateChanged.on(e => {
41033
- this._playerState = e.state;
41034
- if (!e.stopped && e.state === PlayerState.Paused) {
41035
- const currentBeat = this._currentBeat;
41036
- const tickCache = this._tickCache;
41037
- if (currentBeat && tickCache) {
41038
- this.player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41039
- }
41040
- }
41041
- });
41042
- }
41043
- }
41044
41057
  /**
41045
41058
  * updates the cursors to highlight the beat at the specified tick position
41046
41059
  * @param tick
@@ -41071,7 +41084,7 @@ class AlphaTabApiBase {
41071
41084
  if (!beat) {
41072
41085
  return;
41073
41086
  }
41074
- const cache = this.renderer.boundsLookup;
41087
+ const cache = this._renderer.boundsLookup;
41075
41088
  if (!cache) {
41076
41089
  return;
41077
41090
  }
@@ -41081,7 +41094,7 @@ class AlphaTabApiBase {
41081
41094
  if (!forceUpdate &&
41082
41095
  beat === previousBeat?.beat &&
41083
41096
  cache === previousCache &&
41084
- previousState === this._playerState &&
41097
+ previousState === this._player.state &&
41085
41098
  previousBeat?.start === lookupResult.start) {
41086
41099
  return;
41087
41100
  }
@@ -41093,7 +41106,7 @@ class AlphaTabApiBase {
41093
41106
  // actually show the cursor
41094
41107
  this._currentBeat = lookupResult;
41095
41108
  this._previousCursorCache = cache;
41096
- this._previousStateForCursor = this._playerState;
41109
+ this._previousStateForCursor = this._player.state;
41097
41110
  this.uiFacade.beginInvoke(() => {
41098
41111
  this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
41099
41112
  });
@@ -41104,9 +41117,9 @@ class AlphaTabApiBase {
41104
41117
  * @category Methods - Player
41105
41118
  */
41106
41119
  scrollToCursor() {
41107
- const barBounds = this._currentBarBounds;
41108
- if (barBounds) {
41109
- this.internalScrollToCursor(barBounds);
41120
+ const beatBounds = this._currentBeatBounds;
41121
+ if (beatBounds) {
41122
+ this.internalScrollToCursor(beatBounds.barBounds.masterBarBounds);
41110
41123
  }
41111
41124
  }
41112
41125
  internalScrollToCursor(barBoundings) {
@@ -41165,10 +41178,12 @@ class AlphaTabApiBase {
41165
41178
  const beatCursor = this._beatCursor;
41166
41179
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
41167
41180
  const barBounds = barBoundings.visualBounds;
41168
- this._currentBarBounds = barBoundings;
41181
+ const previousBeatBounds = this._currentBeatBounds;
41182
+ this._currentBeatBounds = beatBoundings;
41169
41183
  if (barCursor) {
41170
41184
  barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
41171
41185
  }
41186
+ const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
41172
41187
  let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
41173
41188
  // get position of next beat on same system
41174
41189
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
@@ -41186,20 +41201,49 @@ class AlphaTabApiBase {
41186
41201
  if (this.settings.player.enableAnimatedBeatCursor) {
41187
41202
  const animationWidth = nextBeatX - beatBoundings.onNotesX;
41188
41203
  const relativePosition = this._previousTick - this._currentBeat.start;
41189
- const ratioPosition = relativePosition / this._currentBeat.tickDuration;
41204
+ const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
41190
41205
  startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
41191
41206
  duration -= duration * ratioPosition;
41207
+ if (isPlayingUpdate) {
41208
+ // we do not "reset" the cursor if we are smoothly moving from left to right.
41209
+ const jumpCursor = !previousBeatBounds ||
41210
+ this._isInitialBeatCursorUpdate ||
41211
+ barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
41212
+ startBeatX < previousBeatBounds.onNotesX ||
41213
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
41214
+ if (jumpCursor) {
41215
+ beatCursor.transitionToX(0, startBeatX);
41216
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41217
+ }
41218
+ // we need to put the transition to an own animation frame
41219
+ // otherwise the stop animation above is not applied.
41220
+ this.uiFacade.beginInvoke(() => {
41221
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
41222
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
41223
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
41224
+ const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * 2;
41225
+ beatCursor.transitionToX((duration / cursorSpeed) * 2, doubleEndBeatX);
41226
+ });
41227
+ }
41228
+ else {
41229
+ beatCursor.transitionToX(0, startBeatX);
41230
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41231
+ }
41232
+ }
41233
+ else {
41234
+ // ticking cursor
41192
41235
  beatCursor.transitionToX(0, startBeatX);
41236
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41193
41237
  }
41194
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
41238
+ this._isInitialBeatCursorUpdate = false;
41195
41239
  }
41196
- // if playing, animate the cursor to the next beat
41197
- if (this.settings.player.enableElementHighlighting) {
41198
- this.uiFacade.removeHighlights();
41240
+ else {
41241
+ this._isInitialBeatCursorUpdate = true;
41199
41242
  }
41243
+ // if playing, animate the cursor to the next beat
41244
+ this.uiFacade.removeHighlights();
41200
41245
  // actively playing? -> animate cursor and highlight items
41201
41246
  let shouldNotifyBeatChange = false;
41202
- const isPlayingUpdate = this._playerState === PlayerState.Playing && !stop;
41203
41247
  if (isPlayingUpdate) {
41204
41248
  if (this.settings.player.enableElementHighlighting) {
41205
41249
  for (const highlight of beatsToHighlight) {
@@ -41210,15 +41254,6 @@ class AlphaTabApiBase {
41210
41254
  shouldScroll = !stop;
41211
41255
  shouldNotifyBeatChange = true;
41212
41256
  }
41213
- if (this.settings.player.enableAnimatedBeatCursor && beatCursor) {
41214
- if (isPlayingUpdate) {
41215
- // we need to put the transition to an own animation frame
41216
- // otherwise the stop animation above is not applied.
41217
- this.uiFacade.beginInvoke(() => {
41218
- beatCursor.transitionToX(duration / cursorSpeed, nextBeatX);
41219
- });
41220
- }
41221
- }
41222
41257
  if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) {
41223
41258
  this.internalScrollToCursor(barBoundings);
41224
41259
  }
@@ -41308,7 +41343,7 @@ class AlphaTabApiBase {
41308
41343
  const realMasterBarStart = tickCache.getMasterBarStart(this._selectionStart.beat.voice.bar.masterBar);
41309
41344
  // move to selection start
41310
41345
  this._currentBeat = null; // reset current beat so it is updating the cursor
41311
- if (this._playerState === PlayerState.Paused) {
41346
+ if (this._player.state === PlayerState.Paused) {
41312
41347
  this.cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
41313
41348
  }
41314
41349
  this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
@@ -41370,11 +41405,11 @@ class AlphaTabApiBase {
41370
41405
  }
41371
41406
  const relX = e.getX(this.canvasElement);
41372
41407
  const relY = e.getY(this.canvasElement);
41373
- const beat = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41408
+ const beat = this._renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41374
41409
  if (beat) {
41375
41410
  this.onBeatMouseDown(e, beat);
41376
41411
  if (this.settings.core.includeNoteBounds) {
41377
- const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41412
+ const note = this._renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41378
41413
  if (note) {
41379
41414
  this.onNoteMouseDown(e, note);
41380
41415
  }
@@ -41387,11 +41422,11 @@ class AlphaTabApiBase {
41387
41422
  }
41388
41423
  const relX = e.getX(this.canvasElement);
41389
41424
  const relY = e.getY(this.canvasElement);
41390
- const beat = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41425
+ const beat = this._renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41391
41426
  if (beat) {
41392
41427
  this.onBeatMouseMove(e, beat);
41393
41428
  if (this._noteMouseDown) {
41394
- const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41429
+ const note = this._renderer.boundsLookup?.getNoteAtPos(beat, relX, relY);
41395
41430
  if (note) {
41396
41431
  this.onNoteMouseMove(e, note);
41397
41432
  }
@@ -41407,11 +41442,11 @@ class AlphaTabApiBase {
41407
41442
  }
41408
41443
  const relX = e.getX(this.canvasElement);
41409
41444
  const relY = e.getY(this.canvasElement);
41410
- const beat = this.renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41445
+ const beat = this._renderer.boundsLookup?.getBeatAtPos(relX, relY) ?? null;
41411
41446
  this.onBeatMouseUp(e, beat);
41412
41447
  if (this._noteMouseDown) {
41413
41448
  if (beat) {
41414
- const note = this.renderer.boundsLookup?.getNoteAtPos(beat, relX, relY) ?? null;
41449
+ const note = this._renderer.boundsLookup?.getNoteAtPos(beat, relX, relY) ?? null;
41415
41450
  this.onNoteMouseUp(e, note);
41416
41451
  }
41417
41452
  else {
@@ -41419,7 +41454,7 @@ class AlphaTabApiBase {
41419
41454
  }
41420
41455
  }
41421
41456
  });
41422
- this.renderer.postRenderFinished.on(() => {
41457
+ this._renderer.postRenderFinished.on(() => {
41423
41458
  if (!this._selectionStart ||
41424
41459
  this.settings.player.playerMode === PlayerMode.Disabled ||
41425
41460
  !this.settings.player.enableCursor ||
@@ -41430,7 +41465,7 @@ class AlphaTabApiBase {
41430
41465
  });
41431
41466
  }
41432
41467
  cursorSelectRange(startBeat, endBeat) {
41433
- const cache = this.renderer.boundsLookup;
41468
+ const cache = this._renderer.boundsLookup;
41434
41469
  if (!cache) {
41435
41470
  return;
41436
41471
  }
@@ -41499,7 +41534,8 @@ class AlphaTabApiBase {
41499
41534
  }
41500
41535
  this.scoreLoaded.trigger(score);
41501
41536
  this.uiFacade.triggerEvent(this.container, 'scoreLoaded', score);
41502
- if (this.setupPlayer()) {
41537
+ if (!this.setupOrDestroyPlayer()) {
41538
+ // feed midi into current player (a new player will trigger a midi generation once the player is ready)
41503
41539
  this.loadMidiForScore();
41504
41540
  }
41505
41541
  }
@@ -41528,6 +41564,8 @@ class AlphaTabApiBase {
41528
41564
  if (this._isDestroyed) {
41529
41565
  return;
41530
41566
  }
41567
+ this._currentBeat = null;
41568
+ this.cursorUpdateTick(this._previousTick, false, 1, true, true);
41531
41569
  this.postRenderFinished.trigger();
41532
41570
  this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null);
41533
41571
  }
@@ -41542,25 +41580,155 @@ class AlphaTabApiBase {
41542
41580
  this.error.trigger(error);
41543
41581
  this.uiFacade.triggerEvent(this.container, 'error', error);
41544
41582
  }
41583
+ /**
41584
+ * This event is fired when all required data for playback is loaded and ready.
41585
+ * @remarks
41586
+ * This event is fired when all required data for playback is loaded and ready. The player is ready for playback when
41587
+ * 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.
41588
+ *
41589
+ * @eventProperty
41590
+ * @category Events - Player
41591
+ * @since 0.9.4
41592
+ *
41593
+ * @example
41594
+ * JavaScript
41595
+ * ```js
41596
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41597
+ * api.playerReady.on(() => {
41598
+ * enablePlayerControls();
41599
+ * });
41600
+ * ```
41601
+ *
41602
+ * @example
41603
+ * C#
41604
+ * ```cs
41605
+ * var api = new AlphaTabApi<MyControl>(...);
41606
+ * api.PlayerReady.On(() =>
41607
+ * {
41608
+ * EnablePlayerControls()
41609
+ * });
41610
+ * ```
41611
+ *
41612
+ * @example
41613
+ * Android
41614
+ * ```kotlin
41615
+ * val api = AlphaTabApi<MyControl>(...)
41616
+ * api.playerReady.on {
41617
+ * enablePlayerControls()
41618
+ * }
41619
+ * ```
41620
+ */
41621
+ get playerReady() {
41622
+ return this._player.readyForPlayback;
41623
+ }
41545
41624
  onPlayerReady() {
41546
41625
  if (this._isDestroyed) {
41547
41626
  return;
41548
41627
  }
41549
- this.playerReady.trigger();
41550
41628
  this.uiFacade.triggerEvent(this.container, 'playerReady', null);
41551
41629
  }
41630
+ /**
41631
+ * This event is fired when the playback of the whole song finished.
41632
+ * @remarks
41633
+ * This event is finished regardless on whether looping is enabled or not.
41634
+ *
41635
+ * @eventProperty
41636
+ * @category Events - Player
41637
+ * @since 0.9.4
41638
+ *
41639
+ * @example
41640
+ * JavaScript
41641
+ * ```js
41642
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41643
+ * api.playerFinished.on((args) => {
41644
+ * // speed trainer
41645
+ * api.playbackSpeed = Math.min(1.0, api.playbackSpeed + 0.1);
41646
+ * });
41647
+ * api.isLooping = true;
41648
+ * api.playbackSpeed = 0.5;
41649
+ * api.play()
41650
+ * ```
41651
+ *
41652
+ * @example
41653
+ * C#
41654
+ * ```cs
41655
+ * var api = new AlphaTabApi<MyControl>(...);
41656
+ * api.PlayerFinished.On(() =>
41657
+ * {
41658
+ * // speed trainer
41659
+ * api.PlaybackSpeed = Math.Min(1.0, api.PlaybackSpeed + 0.1);
41660
+ * });
41661
+ * api.IsLooping = true;
41662
+ * api.PlaybackSpeed = 0.5;
41663
+ * api.Play();
41664
+ * ```
41665
+ *
41666
+ * @example
41667
+ * Android
41668
+ * ```kotlin
41669
+ * val api = AlphaTabApi<MyControl>(...)
41670
+ * api.playerFinished.on {
41671
+ * // speed trainer
41672
+ * api.playbackSpeed = min(1.0, api.playbackSpeed + 0.1);
41673
+ * }
41674
+ * api.isLooping = true
41675
+ * api.playbackSpeed = 0.5
41676
+ * api.play()
41677
+ * ```
41678
+ *
41679
+ */
41680
+ get playerFinished() {
41681
+ return this._player.finished;
41682
+ }
41552
41683
  onPlayerFinished() {
41553
41684
  if (this._isDestroyed) {
41554
41685
  return;
41555
41686
  }
41556
- this.playerFinished.trigger();
41557
41687
  this.uiFacade.triggerEvent(this.container, 'playerFinished', null);
41558
41688
  }
41689
+ /**
41690
+ * This event is fired when the SoundFont needed for playback was loaded.
41691
+ *
41692
+ * @eventProperty
41693
+ * @category Events - Player
41694
+ * @since 0.9.4
41695
+ *
41696
+ * @example
41697
+ * JavaScript
41698
+ * ```js
41699
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41700
+ * api.soundFontLoaded.on(() => {
41701
+ * hideSoundFontLoadingIndicator();
41702
+ * });
41703
+ * ```
41704
+ *
41705
+ * @example
41706
+ * C#
41707
+ * ```cs
41708
+ * var api = new AlphaTabApi<MyControl>(...);
41709
+ * api.SoundFontLoaded.On(() =>
41710
+ * {
41711
+ * HideSoundFontLoadingIndicator();
41712
+ * });
41713
+ * ```
41714
+ *
41715
+ * @example
41716
+ * Android
41717
+ * ```kotlin
41718
+ * val api = AlphaTabApi<MyControl>(...);
41719
+ * api.soundFontLoaded.on {
41720
+ * hideSoundFontLoadingIndicator();
41721
+ * }
41722
+ * ```
41723
+ *
41724
+ */
41725
+ get soundFontLoaded() {
41726
+ return this._player.soundFontLoaded;
41727
+ }
41559
41728
  onSoundFontLoaded() {
41560
41729
  if (this._isDestroyed) {
41561
41730
  return;
41562
41731
  }
41563
- this.soundFontLoaded.trigger();
41564
41732
  this.uiFacade.triggerEvent(this.container, 'soundFontLoaded', null);
41565
41733
  }
41566
41734
  onMidiLoad(e) {
@@ -41577,34 +41745,255 @@ class AlphaTabApiBase {
41577
41745
  this.midiLoaded.trigger(e);
41578
41746
  this.uiFacade.triggerEvent(this.container, 'midiFileLoaded', e);
41579
41747
  }
41748
+ /**
41749
+ * This event is fired when the playback state changed.
41750
+ *
41751
+ * @eventProperty
41752
+ * @category Events - Player
41753
+ * @since 0.9.4
41754
+ *
41755
+ * @example
41756
+ * JavaScript
41757
+ * ```js
41758
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41759
+ * api.playerStateChanged.on((args) => {
41760
+ * updatePlayerControls(args.state, args.stopped);
41761
+ * });
41762
+ * ```
41763
+ *
41764
+ * @example
41765
+ * C#
41766
+ * ```cs
41767
+ * var api = new AlphaTabApi<MyControl>(...);
41768
+ * api.PlayerStateChanged.On(args =>
41769
+ * {
41770
+ * UpdatePlayerControls(args);
41771
+ * });
41772
+ * ```
41773
+ *
41774
+ * @example
41775
+ * Android
41776
+ * ```kotlin
41777
+ * val api = AlphaTabApi<MyControl>(...)
41778
+ * api.playerStateChanged.on { args ->
41779
+ * updatePlayerControls(args)
41780
+ * }
41781
+ * ```
41782
+ *
41783
+ */
41784
+ get playerStateChanged() {
41785
+ return this._player.stateChanged;
41786
+ }
41580
41787
  onPlayerStateChanged(e) {
41581
41788
  if (this._isDestroyed) {
41582
41789
  return;
41583
41790
  }
41584
- this.playerStateChanged.trigger(e);
41791
+ if (!e.stopped && e.state === PlayerState.Paused) {
41792
+ const currentBeat = this._currentBeat;
41793
+ const tickCache = this._tickCache;
41794
+ if (currentBeat && tickCache) {
41795
+ this._player.tickPosition = tickCache.getBeatStart(currentBeat.beat);
41796
+ }
41797
+ }
41585
41798
  this.uiFacade.triggerEvent(this.container, 'playerStateChanged', e);
41586
41799
  }
41800
+ /**
41801
+ * This event is fired when the current playback position of the song changed.
41802
+ *
41803
+ * @eventProperty
41804
+ * @category Events - Player
41805
+ * @since 0.9.4
41806
+ *
41807
+ * @example
41808
+ * JavaScript
41809
+ * ```js
41810
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41811
+ * api.playerPositionChanged.on((args) => {
41812
+ * updatePlayerPosition(args);
41813
+ * });
41814
+ * ```
41815
+ *
41816
+ * @example
41817
+ * C#
41818
+ * ```cs
41819
+ * var api = new AlphaTabApi<MyControl>(...);
41820
+ * api.PlayerPositionChanged.On(args =>
41821
+ * {
41822
+ * UpdatePlayerPosition(args);
41823
+ * });
41824
+ * ```
41825
+ *
41826
+ * @example
41827
+ * Android
41828
+ * ```kotlin
41829
+ * val api = AlphaTabApi<MyControl>(...)
41830
+ * api.playerPositionChanged.on { args ->
41831
+ * updatePlayerPosition(args)
41832
+ * }
41833
+ * ```
41834
+ *
41835
+ */
41836
+ get playerPositionChanged() {
41837
+ return this._player.positionChanged;
41838
+ }
41587
41839
  onPlayerPositionChanged(e) {
41588
41840
  if (this._isDestroyed) {
41589
41841
  return;
41590
41842
  }
41591
- if (this.score !== null && this.tracks.length > 0) {
41592
- this.playerPositionChanged.trigger(e);
41593
- this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41594
- }
41843
+ this._previousTick = e.currentTick;
41844
+ this.uiFacade.beginInvoke(() => {
41845
+ const cursorSpeed = e.modifiedTempo / e.originalTempo;
41846
+ this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
41847
+ });
41848
+ this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e);
41849
+ }
41850
+ /**
41851
+ * This event is fired when the synthesizer played certain midi events.
41852
+ *
41853
+ * @remarks
41854
+ * This event is fired when the synthesizer played certain midi events. This allows reacing on various low level
41855
+ * audio playback elements like notes/rests played or metronome ticks.
41856
+ *
41857
+ * Refer to the [related guide](https://www.alphatab.net/docs/guides/handling-midi-events) to learn more about this feature.
41858
+ *
41859
+ * Also note that the provided data models changed significantly in {@version 1.3.0}. We try to provide backwards compatibility
41860
+ * until some extend but highly encourage changing to the new models in case of problems.
41861
+ *
41862
+ * @eventProperty
41863
+ * @category Events - Player
41864
+ * @since 1.2.0
41865
+ *
41866
+ * @example
41867
+ * JavaScript
41868
+ * ```js
41869
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41870
+ * api.midiEventsPlayedFilter = [alphaTab.midi.MidiEventType.AlphaTabMetronome];
41871
+ * api.midiEventsPlayed.on(function(e) {
41872
+ * for(const midi of e.events) {
41873
+ * if(midi.isMetronome) {
41874
+ * console.log('Metronome tick ' + midi.tick);
41875
+ * }
41876
+ * }
41877
+ * });
41878
+ * ```
41879
+ *
41880
+ * @example
41881
+ * C#
41882
+ * ```cs
41883
+ * var api = new AlphaTabApi<MyControl>(...);
41884
+ * api.MidiEventsPlayedFilter = new MidiEventType[] { AlphaTab.Midi.MidiEventType.AlphaTabMetronome };
41885
+ * api.MidiEventsPlayed.On(e =>
41886
+ * {
41887
+ * foreach(var midi of e.events)
41888
+ * {
41889
+ * if(midi is AlphaTab.Midi.AlphaTabMetronomeEvent sysex && sysex.IsMetronome)
41890
+ * {
41891
+ * Console.WriteLine("Metronome tick " + midi.Tick);
41892
+ * }
41893
+ * }
41894
+ * });
41895
+ * ```
41896
+ *
41897
+ * @example
41898
+ * Android
41899
+ * ```kotlin
41900
+ * val api = AlphaTabApi<MyControl>(...);
41901
+ * api.midiEventsPlayedFilter = alphaTab.collections.List<alphaTab.midi.MidiEventType>( alphaTab.midi.MidiEventType.AlphaTabMetronome )
41902
+ * api.midiEventsPlayed.on { e ->
41903
+ * for (midi in e.events) {
41904
+ * if(midi instanceof alphaTab.midi.AlphaTabMetronomeEvent && midi.isMetronome) {
41905
+ * println("Metronome tick " + midi.tick);
41906
+ * }
41907
+ * }
41908
+ * }
41909
+ * ```
41910
+ * @see {@link MidiEvent}
41911
+ * @see {@link TimeSignatureEvent}
41912
+ * @see {@link AlphaTabMetronomeEvent}
41913
+ * @see {@link AlphaTabRestEvent}
41914
+ * @see {@link NoteOnEvent}
41915
+ * @see {@link NoteOffEvent}
41916
+ * @see {@link ControlChangeEvent}
41917
+ * @see {@link ProgramChangeEvent}
41918
+ * @see {@link TempoChangeEvent}
41919
+ * @see {@link PitchBendEvent}
41920
+ * @see {@link NoteBendEvent}
41921
+ * @see {@link EndOfTrackEvent}
41922
+ * @see {@link MetaEvent}
41923
+ * @see {@link MetaDataEvent}
41924
+ * @see {@link MetaNumberEvent}
41925
+ * @see {@link Midi20PerNotePitchBendEvent}
41926
+ * @see {@link SystemCommonEvent}
41927
+ * @see {@link SystemExclusiveEvent}
41928
+ */
41929
+ get midiEventsPlayed() {
41930
+ return this._player.midiEventsPlayed;
41595
41931
  }
41596
41932
  onMidiEventsPlayed(e) {
41597
41933
  if (this._isDestroyed) {
41598
41934
  return;
41599
41935
  }
41600
- this.midiEventsPlayed.trigger(e);
41601
41936
  this.uiFacade.triggerEvent(this.container, 'midiEventsPlayed', e);
41602
41937
  }
41938
+ /**
41939
+ * This event is fired when the playback range changed.
41940
+ *
41941
+ * @eventProperty
41942
+ * @category Events - Player
41943
+ * @since 1.2.3
41944
+ *
41945
+ * @example
41946
+ * JavaScript
41947
+ * ```js
41948
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
41949
+ * api.playbackRangeChanged.on((args) => {
41950
+ * if (args.playbackRange) {
41951
+ * highlightRangeInProgressBar(args.playbackRange.startTick, args.playbackRange.endTick);
41952
+ * } else {
41953
+ * clearHighlightInProgressBar();
41954
+ * }
41955
+ * });
41956
+ * ```
41957
+ *
41958
+ * @example
41959
+ * C#
41960
+ * ```cs
41961
+ * var api = new AlphaTabApi<MyControl>(...);
41962
+ * api.PlaybackRangeChanged.On(args =>
41963
+ * {
41964
+ * if (args.PlaybackRange != null)
41965
+ * {
41966
+ * HighlightRangeInProgressBar(args.PlaybackRange.StartTick, args.PlaybackRange.EndTick);
41967
+ * }
41968
+ * else
41969
+ * {
41970
+ * ClearHighlightInProgressBar();
41971
+ * }
41972
+ * });
41973
+ * ```
41974
+ *
41975
+ * @example
41976
+ * Android
41977
+ * ```kotlin
41978
+ * val api = AlphaTabApi<MyControl>(...)
41979
+ * api.playbackRangeChanged.on { args ->
41980
+ * val playbackRange = args.playbackRange
41981
+ * if (playbackRange != null) {
41982
+ * highlightRangeInProgressBar(playbackRange.startTick, playbackRange.endTick)
41983
+ * } else {
41984
+ * clearHighlightInProgressBar()
41985
+ * }
41986
+ * }
41987
+ * ```
41988
+ *
41989
+ */
41990
+ get playbackRangeChanged() {
41991
+ return this._player.playbackRangeChanged;
41992
+ }
41603
41993
  onPlaybackRangeChanged(e) {
41604
41994
  if (this._isDestroyed) {
41605
41995
  return;
41606
41996
  }
41607
- this.playbackRangeChanged.trigger(e);
41608
41997
  this.uiFacade.triggerEvent(this.container, 'playbackRangeChanged', e);
41609
41998
  }
41610
41999
  onSettingsUpdated() {
@@ -41666,10 +42055,7 @@ class AlphaTabApiBase {
41666
42055
  * ```
41667
42056
  */
41668
42057
  async enumerateOutputDevices() {
41669
- if (this.player) {
41670
- return await this.player.output.enumerateOutputDevices();
41671
- }
41672
- return [];
42058
+ return await this._player.output.enumerateOutputDevices();
41673
42059
  }
41674
42060
  /**
41675
42061
  * Changes the output device which should be used for playing the audio (player must be enabled).
@@ -41720,9 +42106,7 @@ class AlphaTabApiBase {
41720
42106
  * ```
41721
42107
  */
41722
42108
  async setOutputDevice(device) {
41723
- if (this.player) {
41724
- await this.player.output.setOutputDevice(device);
41725
- }
42109
+ await this._player.output.setOutputDevice(device);
41726
42110
  }
41727
42111
  /**
41728
42112
  * The currently configured output device if changed via {@link setOutputDevice}.
@@ -41762,10 +42146,7 @@ class AlphaTabApiBase {
41762
42146
  *
41763
42147
  */
41764
42148
  async getOutputDevice() {
41765
- if (this.player) {
41766
- return await this.player.output.getOutputDevice();
41767
- }
41768
- return null;
42149
+ return await this._player.output.getOutputDevice();
41769
42150
  }
41770
42151
  }
41771
42152
 
@@ -41942,38 +42323,52 @@ class HtmlElementContainer {
41942
42323
  this.element = element;
41943
42324
  this.mouseDown = {
41944
42325
  on: (value) => {
41945
- this.element.addEventListener('mousedown', e => {
42326
+ const nativeListener = e => {
41946
42327
  value(new BrowserMouseEventArgs(e));
41947
- }, true);
42328
+ };
42329
+ this.element.addEventListener('mousedown', nativeListener, true);
42330
+ return () => {
42331
+ this.element.removeEventListener('mousedown', nativeListener, true);
42332
+ };
41948
42333
  },
41949
42334
  off: (value) => {
41950
42335
  }
41951
42336
  };
41952
42337
  this.mouseUp = {
41953
42338
  on: (value) => {
41954
- this.element.addEventListener('mouseup', e => {
42339
+ const nativeListener = e => {
41955
42340
  value(new BrowserMouseEventArgs(e));
41956
- }, true);
42341
+ };
42342
+ this.element.addEventListener('mouseup', nativeListener, true);
42343
+ return () => {
42344
+ this.element.removeEventListener('mouseup', nativeListener, true);
42345
+ };
41957
42346
  },
41958
42347
  off: (value) => {
41959
42348
  }
41960
42349
  };
41961
42350
  this.mouseMove = {
41962
42351
  on: (value) => {
41963
- this.element.addEventListener('mousemove', e => {
42352
+ const nativeListener = e => {
41964
42353
  value(new BrowserMouseEventArgs(e));
41965
- }, true);
42354
+ };
42355
+ this.element.addEventListener('mousemove', nativeListener, true);
42356
+ return () => {
42357
+ this.element.removeEventListener('mousemove', nativeListener, true);
42358
+ };
41966
42359
  },
41967
42360
  off: (_) => {
41968
42361
  }
41969
42362
  };
42363
+ const container = this;
41970
42364
  this.resize = {
41971
- on: (value) => {
41972
- if (this._resizeListeners === 0) {
41973
- HtmlElementContainer.resizeObserver.value.observe(this.element);
42365
+ on: function (value) {
42366
+ if (container._resizeListeners === 0) {
42367
+ HtmlElementContainer.resizeObserver.value.observe(container.element);
41974
42368
  }
41975
- this.element.addEventListener('resize', value, true);
41976
- this._resizeListeners++;
42369
+ container.element.addEventListener('resize', value, true);
42370
+ container._resizeListeners++;
42371
+ return () => this.off(value);
41977
42372
  },
41978
42373
  off: (value) => {
41979
42374
  this.element.removeEventListener('resize', value, true);
@@ -42554,21 +42949,6 @@ class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBase {
42554
42949
  }
42555
42950
  }
42556
42951
 
42557
- /**
42558
- * Represents the progress of any data being loaded.
42559
- */
42560
- class ProgressEventArgs {
42561
- /**
42562
- * Initializes a new instance of the {@link ProgressEventArgs} class.
42563
- * @param loaded
42564
- * @param total
42565
- */
42566
- constructor(loaded, total) {
42567
- this.loaded = loaded;
42568
- this.total = total;
42569
- }
42570
- }
42571
-
42572
42952
  /**
42573
42953
  * a WebWorker based alphaSynth which uses the given player as output.
42574
42954
  * @target web
@@ -42805,25 +43185,6 @@ class AlphaSynthWebWorkerApi {
42805
43185
  append: append
42806
43186
  });
42807
43187
  }
42808
- loadSoundFontFromUrl(url, append, progress) {
42809
- Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
42810
- const request = new XMLHttpRequest();
42811
- request.open('GET', url, true, null, null);
42812
- request.responseType = 'arraybuffer';
42813
- request.onload = _ => {
42814
- const buffer = new Uint8Array(request.response);
42815
- this.loadSoundFont(buffer, append);
42816
- };
42817
- request.onerror = e => {
42818
- Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
42819
- this.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
42820
- };
42821
- request.onprogress = e => {
42822
- Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
42823
- progress(new ProgressEventArgs(e.loaded, e.total));
42824
- };
42825
- request.send();
42826
- }
42827
43188
  resetSoundFonts() {
42828
43189
  this._synth.postMessage({
42829
43190
  cmd: 'alphaSynth.resetSoundFonts'
@@ -43375,7 +43736,11 @@ class AudioElementBackingTrackSynthOutput {
43375
43736
  }
43376
43737
  this._padding = backingTrack.padding / 1000;
43377
43738
  const blob = new Blob([backingTrack.rawAudioFile]);
43739
+ // https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource
43740
+ // Step 8. resets the playbackRate, we need to remember and restore it.
43741
+ const playbackRate = this.audioElement.playbackRate;
43378
43742
  this.audioElement.src = URL.createObjectURL(blob);
43743
+ this.audioElement.playbackRate = playbackRate;
43379
43744
  }
43380
43745
  open(_bufferTimeInMilliseconds) {
43381
43746
  const audioElement = document.createElement('audio');
@@ -44094,6 +44459,21 @@ class BrowserUiFacade {
44094
44459
  }
44095
44460
  }
44096
44461
 
44462
+ /**
44463
+ * Represents the progress of any data being loaded.
44464
+ */
44465
+ class ProgressEventArgs {
44466
+ /**
44467
+ * Initializes a new instance of the {@link ProgressEventArgs} class.
44468
+ * @param loaded
44469
+ * @param total
44470
+ */
44471
+ constructor(loaded, total) {
44472
+ this.loaded = loaded;
44473
+ this.total = total;
44474
+ }
44475
+ }
44476
+
44097
44477
  /**
44098
44478
  * @target web
44099
44479
  */
@@ -44337,13 +44717,29 @@ class AlphaTabApi extends AlphaTabApiBase {
44337
44717
  * @since 0.9.4
44338
44718
  */
44339
44719
  loadSoundFontFromUrl(url, append) {
44340
- if (!this.player) {
44720
+ const player = this.player;
44721
+ if (!player) {
44341
44722
  return;
44342
44723
  }
44343
- this.player.loadSoundFontFromUrl(url, append, e => {
44344
- this.soundFontLoad.trigger(e);
44345
- this.uiFacade.triggerEvent(this.container, 'soundFontLoad', e);
44346
- });
44724
+ Logger.debug('AlphaSynth', `Start loading Soundfont from url ${url}`);
44725
+ const request = new XMLHttpRequest();
44726
+ request.open('GET', url, true, null, null);
44727
+ request.responseType = 'arraybuffer';
44728
+ request.onload = _ => {
44729
+ const buffer = new Uint8Array(request.response);
44730
+ this.loadSoundFont(buffer, append);
44731
+ };
44732
+ request.onerror = e => {
44733
+ Logger.error('AlphaSynth', `Loading failed: ${e.message}`);
44734
+ player.soundFontLoadFailed.trigger(new FileLoadError(e.message, request));
44735
+ };
44736
+ request.onprogress = e => {
44737
+ Logger.debug('AlphaSynth', `Soundfont downloading: ${e.loaded}/${e.total} bytes`);
44738
+ const args = new ProgressEventArgs(e.loaded, e.total);
44739
+ this.soundFontLoad.trigger(args);
44740
+ this.uiFacade.triggerEvent(this.container, 'soundFontLoad', args);
44741
+ };
44742
+ request.send();
44347
44743
  }
44348
44744
  }
44349
44745
 
@@ -46725,7 +47121,7 @@ class StaffSystem {
46725
47121
  return this.masterBarsRenderers[0].masterBar.index;
46726
47122
  }
46727
47123
  get lastBarIndex() {
46728
- return this.masterBarsRenderers[this.masterBarsRenderers.length - 1].masterBar.index;
47124
+ return this.masterBarsRenderers[this.masterBarsRenderers.length - 1].lastMasterBarIndex;
46729
47125
  }
46730
47126
  addMasterBarRenderers(tracks, renderers) {
46731
47127
  if (tracks.length === 0) {
@@ -47539,6 +47935,9 @@ class ScoreLayout {
47539
47935
  }
47540
47936
  }
47541
47937
  }
47938
+ else {
47939
+ this.tuningGlyph = null;
47940
+ }
47542
47941
  }
47543
47942
  // chord diagram glyphs
47544
47943
  if (notation.isNotationElementVisible(NotationElement.ChordDiagrams)) {
@@ -47567,6 +47966,12 @@ class ScoreLayout {
47567
47966
  }
47568
47967
  }
47569
47968
  }
47969
+ if (this.chordDiagrams.isEmpty) {
47970
+ this.chordDiagrams = null;
47971
+ }
47972
+ }
47973
+ else {
47974
+ this.chordDiagrams = null;
47570
47975
  }
47571
47976
  }
47572
47977
  createEmptyStaffSystem() {
@@ -60310,9 +60715,9 @@ class VersionInfo {
60310
60715
  print(`build date: ${VersionInfo.date}`);
60311
60716
  }
60312
60717
  }
60313
- VersionInfo.version = '1.6.0-alpha.1408';
60314
- VersionInfo.date = '2025-05-13T02:08:06.970Z';
60315
- VersionInfo.commit = '37b936b4f5eb8bf759bc78bdb4dd1812d7b2a072';
60718
+ VersionInfo.version = '1.6.0-alpha.1415';
60719
+ VersionInfo.date = '2025-05-19T16:08:22.342Z';
60720
+ VersionInfo.commit = '459db69f8896a2ea8822ce5d49dcc824edd36521';
60316
60721
 
60317
60722
  /**
60318
60723
  * A factory for custom layout engines.