@coderline/alphatab 1.6.0-alpha.1418 → 1.6.0-alpha.1421

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.1418 (develop, build 1418)
2
+ * alphaTab v1.6.0-alpha.1421 (develop, build 1421)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -14312,13 +14312,6 @@ class XmlDocument extends XmlNode {
14312
14312
  * @json_strict
14313
14313
  */
14314
14314
  class BackingTrack {
14315
- constructor() {
14316
- /**
14317
- * The number of milliseconds the audio should be shifted to align with the song.
14318
- * (e.g. negative values allow skipping potential silent parts at the start of the file and directly start with the first note).
14319
- */
14320
- this.padding = 0;
14321
- }
14322
14315
  }
14323
14316
 
14324
14317
  /**
@@ -14351,6 +14344,7 @@ class GpifParser {
14351
14344
  constructor() {
14352
14345
  this._hasAnacrusis = false;
14353
14346
  this._skipApplyLyrics = false;
14347
+ this._backingTrackPadding = 0;
14354
14348
  this._doubleBars = new Set();
14355
14349
  this._keySignatures = new Map();
14356
14350
  }
@@ -14583,7 +14577,7 @@ class GpifParser {
14583
14577
  assetId = c.innerText;
14584
14578
  break;
14585
14579
  case 'FramePadding':
14586
- backingTrack.padding = GpifParser.parseIntSafe(c.innerText, 0) / GpifParser.SampleRate * 1000;
14580
+ this._backingTrackPadding = GpifParser.parseIntSafe(c.innerText, 0) / GpifParser.SampleRate * 1000;
14587
14581
  break;
14588
14582
  }
14589
14583
  }
@@ -16797,6 +16791,7 @@ class GpifParser {
16797
16791
  masterBar.tempoAutomations.push(automation);
16798
16792
  break;
16799
16793
  case AutomationType.SyncPoint:
16794
+ automation.syncPointValue.millisecondOffset -= this._backingTrackPadding;
16800
16795
  masterBar.addSyncPoint(automation);
16801
16796
  break;
16802
16797
  }
@@ -22285,17 +22280,6 @@ class SynthEvent {
22285
22280
  }
22286
22281
  }
22287
22282
 
22288
- /**
22289
- * Rerpresents a point to sync the alphaTab time axis with an external backing track.
22290
- */
22291
- class BackingTrackSyncPoint {
22292
- constructor(tick, data) {
22293
- this.tick = 0;
22294
- this.tick = tick;
22295
- this.data = data;
22296
- }
22297
- }
22298
-
22299
22283
  class MidiFileSequencerTempoChange {
22300
22284
  constructor(bpm, ticks, time) {
22301
22285
  this.bpm = bpm;
@@ -22303,10 +22287,12 @@ class MidiFileSequencerTempoChange {
22303
22287
  this.time = time;
22304
22288
  }
22305
22289
  }
22306
- class BackingTrackSyncPointWithTime extends BackingTrackSyncPoint {
22307
- constructor(tick, data, time) {
22308
- super(tick, data);
22309
- this.time = time;
22290
+ class BackingTrackSyncPointWithTime {
22291
+ constructor(tick, time, modifiedTempo, millisecondOffset) {
22292
+ this.alphaTabTick = tick;
22293
+ this.alphaTabTime = time;
22294
+ this.modifiedTempo = modifiedTempo;
22295
+ this.millisecondOffset = millisecondOffset;
22310
22296
  }
22311
22297
  }
22312
22298
  class MidiSequencerState {
@@ -22412,7 +22398,7 @@ class MidiFileSequencer {
22412
22398
  this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22413
22399
  this._mainState.modifiedTempo =
22414
22400
  this._mainState.syncPoints.length > 0
22415
- ? this._mainState.syncPoints[0].data.modifiedTempo
22401
+ ? this._mainState.syncPoints[0].modifiedTempo
22416
22402
  : this._mainState.currentTempo;
22417
22403
  if (this.isPlayingMain) {
22418
22404
  const metronomeVolume = this._synthesizer.metronomeVolume;
@@ -22584,25 +22570,52 @@ class MidiFileSequencer {
22584
22570
  mainUpdateSyncPoints(syncPoints) {
22585
22571
  const state = this._mainState;
22586
22572
  syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22587
- state.syncPoints = new Array(syncPoints.length);
22573
+ state.syncPoints = [];
22588
22574
  if (syncPoints.length >= 0) {
22589
22575
  let bpm = 120;
22590
22576
  let absTick = 0;
22591
22577
  let absTime = 0.0;
22592
- let previousTick = 0;
22593
22578
  let tempoChangeIndex = 0;
22594
22579
  for (let i = 0; i < syncPoints.length; i++) {
22595
22580
  const p = syncPoints[i];
22596
- const deltaTick = p.tick - previousTick;
22597
- absTick += deltaTick;
22598
- absTime += deltaTick * (60000.0 / (bpm * state.division));
22599
- state.syncPoints[i] = new BackingTrackSyncPointWithTime(p.tick, p.data, absTime);
22600
- previousTick = p.tick;
22581
+ let deltaTick = 0;
22582
+ // remember state from previous sync point (or start). to handle linear interpolation
22583
+ let previousModifiedTempo;
22584
+ let previousMillisecondOffset;
22585
+ let previousTick;
22586
+ if (i === 0) {
22587
+ previousModifiedTempo = bpm;
22588
+ previousMillisecondOffset = 0;
22589
+ previousTick = 0;
22590
+ }
22591
+ else {
22592
+ const previousSyncPoint = syncPoints[i - 1];
22593
+ previousModifiedTempo = previousSyncPoint.data.modifiedTempo;
22594
+ previousMillisecondOffset = previousSyncPoint.data.millisecondOffset;
22595
+ previousTick = previousSyncPoint.tick;
22596
+ }
22597
+ // process time until sync point
22598
+ // here it gets a bit tricky. if we have tempo changes on the synthesizer time axis (inbetween two sync points)
22599
+ // we have to calculate a interpolated sync point on the alphaTab time axis.
22600
+ // otherwise the linear interpolation later in the lookup will fail.
22601
+ // goal is to have always a linear increase between two points, no matter if the time axis is sliced by tempo changes or sync points
22601
22602
  while (tempoChangeIndex < state.tempoChanges.length &&
22602
- state.tempoChanges[tempoChangeIndex].ticks <= absTick) {
22603
+ state.tempoChanges[tempoChangeIndex].ticks <= p.tick) {
22604
+ deltaTick = state.tempoChanges[tempoChangeIndex].ticks - absTick;
22605
+ if (deltaTick > 0) {
22606
+ absTick += deltaTick;
22607
+ absTime += deltaTick * (60000.0 / (bpm * state.division));
22608
+ const millisPerTick = (p.data.millisecondOffset - previousMillisecondOffset) / (p.tick - previousTick);
22609
+ const interpolatedMillisecondOffset = (absTick - previousTick) * millisPerTick + previousMillisecondOffset;
22610
+ state.syncPoints.push(new BackingTrackSyncPointWithTime(absTick, absTime, previousModifiedTempo, interpolatedMillisecondOffset));
22611
+ }
22603
22612
  bpm = state.tempoChanges[tempoChangeIndex].bpm;
22604
22613
  tempoChangeIndex++;
22605
22614
  }
22615
+ deltaTick = p.tick - absTick;
22616
+ absTick += deltaTick;
22617
+ absTime += deltaTick * (60000.0 / (bpm * state.division));
22618
+ state.syncPoints.push(new BackingTrackSyncPointWithTime(p.tick, absTime, p.data.modifiedTempo, p.data.millisecondOffset));
22606
22619
  }
22607
22620
  }
22608
22621
  state.syncPointIndex = 0;
@@ -22616,7 +22629,7 @@ class MidiFileSequencer {
22616
22629
  this.updateCurrentTempo(state, timePosition);
22617
22630
  const lastTempoChange = state.tempoChanges[state.tempoChangeIndex];
22618
22631
  const timeDiff = timePosition - lastTempoChange.time;
22619
- const ticks = ((timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0);
22632
+ const ticks = (timeDiff / (60000.0 / (lastTempoChange.bpm * state.division))) | 0;
22620
22633
  // we add 1 for possible rounding errors.(floating point issuses)
22621
22634
  return lastTempoChange.ticks + ticks + 1;
22622
22635
  }
@@ -22633,19 +22646,21 @@ class MidiFileSequencer {
22633
22646
  state.tempoChangeIndex = tempoChangeIndex;
22634
22647
  state.currentTempo = state.tempoChanges[state.tempoChangeIndex].bpm;
22635
22648
  }
22649
+ }
22650
+ updateSyncPoints(state, timePosition) {
22636
22651
  const syncPoints = state.syncPoints;
22637
22652
  if (syncPoints.length > 0) {
22638
22653
  let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22639
- if (timePosition < syncPoints[syncPointIndex].data.millisecondOffset) {
22654
+ if (timePosition < syncPoints[syncPointIndex].millisecondOffset) {
22640
22655
  syncPointIndex = 0;
22641
22656
  }
22642
22657
  while (syncPointIndex + 1 < syncPoints.length &&
22643
- syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22658
+ syncPoints[syncPointIndex + 1].millisecondOffset <= timePosition) {
22644
22659
  syncPointIndex++;
22645
22660
  }
22646
22661
  if (syncPointIndex !== state.syncPointIndex) {
22647
22662
  state.syncPointIndex = syncPointIndex;
22648
- state.modifiedTempo = syncPoints[syncPointIndex].data.modifiedTempo;
22663
+ state.modifiedTempo = syncPoints[syncPointIndex].modifiedTempo;
22649
22664
  }
22650
22665
  }
22651
22666
  else {
@@ -22658,21 +22673,21 @@ class MidiFileSequencer {
22658
22673
  if (timePosition < 0 || syncPoints.length === 0) {
22659
22674
  return timePosition;
22660
22675
  }
22661
- this.updateCurrentTempo(this._mainState, timePosition);
22676
+ this.updateSyncPoints(this._mainState, timePosition);
22662
22677
  const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22663
22678
  const currentSyncPoint = syncPoints[syncPointIndex];
22664
- const timeDiff = timePosition - currentSyncPoint.data.millisecondOffset;
22679
+ const timeDiff = timePosition - currentSyncPoint.millisecondOffset;
22665
22680
  let alphaTabTimeDiff;
22666
22681
  if (syncPointIndex + 1 < syncPoints.length) {
22667
22682
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22668
- const relativeTimeDiff = timeDiff / (nextSyncPoint.data.millisecondOffset - currentSyncPoint.data.millisecondOffset);
22669
- alphaTabTimeDiff = (nextSyncPoint.time - currentSyncPoint.time) * relativeTimeDiff;
22683
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset);
22684
+ alphaTabTimeDiff = (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22670
22685
  }
22671
22686
  else {
22672
- const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.data.millisecondOffset);
22673
- alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.time) * relativeTimeDiff;
22687
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.millisecondOffset);
22688
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22674
22689
  }
22675
- return (currentSyncPoint.time + alphaTabTimeDiff) / this.playbackSpeed;
22690
+ return (currentSyncPoint.alphaTabTime + alphaTabTimeDiff) / this.playbackSpeed;
22676
22691
  }
22677
22692
  mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22678
22693
  const mainState = this._mainState;
@@ -22682,25 +22697,27 @@ class MidiFileSequencer {
22682
22697
  }
22683
22698
  timePosition *= this.playbackSpeed;
22684
22699
  let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22685
- if (timePosition < syncPoints[syncPointIndex].time) {
22700
+ if (timePosition < syncPoints[syncPointIndex].alphaTabTime) {
22686
22701
  syncPointIndex = 0;
22687
22702
  }
22688
- while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].time <= timePosition) {
22703
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].alphaTabTime <= timePosition) {
22689
22704
  syncPointIndex++;
22690
22705
  }
22706
+ // NOTE: this logic heavily relies on the interpolation done in mainUpdateSyncPoints
22707
+ // we ensure that we have a linear increase between two points
22691
22708
  const currentSyncPoint = syncPoints[syncPointIndex];
22692
- const alphaTabTimeDiff = timePosition - currentSyncPoint.time;
22709
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.alphaTabTime;
22693
22710
  let backingTrackPos;
22694
22711
  if (syncPointIndex + 1 < syncPoints.length) {
22695
22712
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22696
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.time - currentSyncPoint.time);
22697
- const backingTrackDiff = nextSyncPoint.data.millisecondOffset - currentSyncPoint.data.millisecondOffset;
22698
- backingTrackPos = currentSyncPoint.data.millisecondOffset + backingTrackDiff * relativeAlphaTabTimeDiff;
22713
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime);
22714
+ const backingTrackDiff = nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset;
22715
+ backingTrackPos = currentSyncPoint.millisecondOffset + backingTrackDiff * relativeAlphaTabTimeDiff;
22699
22716
  }
22700
22717
  else {
22701
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.time);
22702
- const frameDiff = backingTrackLength - currentSyncPoint.data.millisecondOffset;
22703
- backingTrackPos = currentSyncPoint.data.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22718
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.alphaTabTime);
22719
+ const frameDiff = backingTrackLength - currentSyncPoint.millisecondOffset;
22720
+ backingTrackPos = currentSyncPoint.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22704
22721
  }
22705
22722
  return backingTrackPos;
22706
22723
  }
@@ -28582,7 +28599,9 @@ class AlphaSynthBase {
28582
28599
  hasSamplesForPercussion(key) {
28583
28600
  return this.synthesizer.hasSamplesForPercussion(key);
28584
28601
  }
28585
- loadBackingTrack(_score, _syncPoints) {
28602
+ loadBackingTrack(_score) {
28603
+ }
28604
+ updateSyncPoints(_syncPoints) {
28586
28605
  }
28587
28606
  }
28588
28607
  /**
@@ -32479,15 +32498,9 @@ class BackingTrackSerializer {
32479
32498
  return null;
32480
32499
  }
32481
32500
  const o = new Map();
32482
- o.set("padding", obj.padding);
32483
32501
  return o;
32484
32502
  }
32485
32503
  static setProperty(obj, property, v) {
32486
- switch (property) {
32487
- case "padding":
32488
- obj.padding = v;
32489
- return true;
32490
- }
32491
32504
  return false;
32492
32505
  }
32493
32506
  }
@@ -34950,7 +34963,13 @@ class MasterBarTickLookup {
34950
34963
  * Gets the list of tempo changes within the tick lookup.
34951
34964
  */
34952
34965
  this.tempoChanges = [];
34966
+ /**
34967
+ * The first beat in the bar.
34968
+ */
34953
34969
  this.firstBeat = null;
34970
+ /**
34971
+ * The last beat in the bar.
34972
+ */
34954
34973
  this.lastBeat = null;
34955
34974
  /**
34956
34975
  * Gets or sets the {@link MasterBarTickLookup} of the next masterbar in the {@link Score}
@@ -35364,14 +35383,13 @@ class MidiTickLookup {
35364
35383
  constructor() {
35365
35384
  this._currentMasterBar = null;
35366
35385
  /**
35367
- * Gets a dictionary of all master bars played. The index is the index equals to {@link MasterBar.index}.
35386
+ * A dictionary of all master bars played. The index is the index equals to {@link MasterBar.index}.
35368
35387
  * This lookup only contains the first time a MasterBar is played. For a whole sequence of the song refer to {@link MasterBars}.
35369
35388
  * @internal
35370
35389
  */
35371
35390
  this.masterBarLookup = new Map();
35372
35391
  /**
35373
- * Gets a list of all {@link MasterBarTickLookup} sorted by time.
35374
- * @internal
35392
+ * A list of all {@link MasterBarTickLookup} sorted by time.
35375
35393
  */
35376
35394
  this.masterBars = [];
35377
35395
  /**
@@ -35745,6 +35763,17 @@ class MidiTickLookup {
35745
35763
  }
35746
35764
  }
35747
35765
 
35766
+ /**
35767
+ * Rerpresents a point to sync the alphaTab time axis with an external backing track.
35768
+ */
35769
+ class BackingTrackSyncPoint {
35770
+ constructor(tick, data) {
35771
+ this.tick = 0;
35772
+ this.tick = tick;
35773
+ this.data = data;
35774
+ }
35775
+ }
35776
+
35748
35777
  class MidiNoteDuration {
35749
35778
  constructor() {
35750
35779
  this.noteOnly = 0;
@@ -35814,62 +35843,22 @@ class MidiFileGenerator {
35814
35843
  this.generateTrack(track);
35815
35844
  }
35816
35845
  Logger.debug('Midi', 'Begin midi generation');
35817
- const controller = new MidiPlaybackController(this._score);
35818
- let previousMasterBar = null;
35819
- let currentTempo = this._score.tempo;
35820
- // store the previous played bar for repeats
35821
- const barOccurence = new Map();
35822
- while (!controller.finished) {
35823
- const index = controller.index;
35824
- const bar = this._score.masterBars[index];
35825
- const currentTick = controller.currentTick;
35826
- controller.processCurrent();
35827
- if (controller.shouldPlay) {
35828
- let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
35829
- occurence++;
35830
- barOccurence.set(index, occurence);
35831
- this.generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
35832
- if (bar.tempoAutomations.length > 0) {
35833
- currentTempo = bar.tempoAutomations[0].value;
35834
- }
35835
- for (const track of this._score.tracks) {
35836
- for (const staff of track.staves) {
35837
- if (index < staff.bars.length) {
35838
- this.generateBar(staff.bars[index], currentTick, currentTempo);
35839
- }
35846
+ this.syncPoints = [];
35847
+ MidiFileGenerator.playThroughSong(this._score, this.syncPoints, (bar, previousMasterBar, currentTick, currentTempo, occurence) => {
35848
+ this.generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
35849
+ }, (index, currentTick, currentTempo) => {
35850
+ for (const track of this._score.tracks) {
35851
+ for (const staff of track.staves) {
35852
+ if (index < staff.bars.length) {
35853
+ this.generateBar(staff.bars[index], currentTick, currentTempo);
35840
35854
  }
35841
35855
  }
35842
- if (bar.tempoAutomations.length > 0) {
35843
- currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
35844
- }
35845
35856
  }
35846
- controller.moveNext();
35847
- previousMasterBar = bar;
35848
- }
35849
- // here we interpolate the sync point which marks the end of the sync.
35850
- // Sync points define new tempos at certain positions.
35851
- // looking from the last sync point to the end we do not assume the end where the audio ends,
35852
- // but where it ends according to the BPM and the remaining ticks.
35853
- if (this.syncPoints.length > 0) {
35854
- const lastSyncPoint = this.syncPoints[this.syncPoints.length - 1];
35855
- const endTick = controller.currentTick;
35856
- const remainingTicks = endTick - lastSyncPoint.tick;
35857
- if (remainingTicks > 0) {
35858
- const syncPointData = new SyncPointData();
35859
- // last occurence of the last bar
35860
- syncPointData.barOccurence = barOccurence.get(this._score.masterBars.length - 1);
35861
- // same tempo as last point
35862
- syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35863
- // interpolated end from last syncPoint
35864
- syncPointData.millisecondOffset =
35865
- lastSyncPoint.data.millisecondOffset +
35866
- MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35867
- this.syncPoints.push(new BackingTrackSyncPoint(endTick, syncPointData));
35857
+ }, endTick => {
35858
+ for (const track of this._score.tracks) {
35859
+ this._handler.finishTrack(track.index, endTick);
35868
35860
  }
35869
- }
35870
- for (const track of this._score.tracks) {
35871
- this._handler.finishTrack(track.index, controller.currentTick);
35872
- }
35861
+ });
35873
35862
  Logger.debug('Midi', 'Midi generation done');
35874
35863
  }
35875
35864
  generateTrack(track) {
@@ -35914,6 +35903,81 @@ class MidiFileGenerator {
35914
35903
  this._handler.addControlChange(track.index, 0, channel, ControllerType.DataEntryCoarse, MidiFileGenerator.PitchBendRangeInSemitones);
35915
35904
  this.addProgramChange(track, 0, channel, playbackInfo.program);
35916
35905
  }
35906
+ /**
35907
+ * Generates the sync points for the given score without re-generating the midi itself.
35908
+ * @remarks
35909
+ * Use this method if a re-generation of the sync points after modification is required.
35910
+ * It correctly handles repeats and places sync points accoridng to their absolute midi tick when they
35911
+ * need to be considered for synchronization.
35912
+ * @param score The song for which to regenerate the sync points.
35913
+ * @returns The generated sync points for usage in the backing track playback.
35914
+ */
35915
+ static generateSyncPoints(score) {
35916
+ const syncPoints = [];
35917
+ MidiFileGenerator.playThroughSong(score, syncPoints, (_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
35918
+ }, (_barIndex, _currentTick, _currentTempo) => {
35919
+ }, _endTick => {
35920
+ });
35921
+ return syncPoints;
35922
+ }
35923
+ static playThroughSong(score, syncPoints, generateMasterBar, generateTracks, finish) {
35924
+ const controller = new MidiPlaybackController(score);
35925
+ let currentTempo = score.tempo;
35926
+ let previousMasterBar = null;
35927
+ // store the previous played bar for repeats
35928
+ const barOccurence = new Map();
35929
+ while (!controller.finished) {
35930
+ const index = controller.index;
35931
+ const bar = score.masterBars[index];
35932
+ const currentTick = controller.currentTick;
35933
+ controller.processCurrent();
35934
+ if (controller.shouldPlay) {
35935
+ let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
35936
+ occurence++;
35937
+ barOccurence.set(index, occurence);
35938
+ generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
35939
+ const barSyncPoints = bar.syncPoints;
35940
+ if (barSyncPoints) {
35941
+ for (const syncPoint of barSyncPoints) {
35942
+ if (syncPoint.syncPointValue.barOccurence === occurence) {
35943
+ const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
35944
+ syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
35945
+ }
35946
+ }
35947
+ }
35948
+ if (bar.tempoAutomations.length > 0) {
35949
+ currentTempo = bar.tempoAutomations[0].value;
35950
+ }
35951
+ generateTracks(index, currentTick, currentTempo);
35952
+ if (bar.tempoAutomations.length > 0) {
35953
+ currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
35954
+ }
35955
+ }
35956
+ controller.moveNext();
35957
+ previousMasterBar = bar;
35958
+ }
35959
+ // here we interpolate the sync point which marks the end of the sync.
35960
+ // Sync points define new tempos at certain positions.
35961
+ // looking from the last sync point to the end we do not assume the end where the audio ends,
35962
+ // but where it ends according to the BPM and the remaining ticks.
35963
+ if (syncPoints.length > 0) {
35964
+ const lastSyncPoint = syncPoints[syncPoints.length - 1];
35965
+ const remainingTicks = controller.currentTick - lastSyncPoint.tick;
35966
+ if (remainingTicks > 0) {
35967
+ const syncPointData = new SyncPointData();
35968
+ // last occurence of the last bar
35969
+ syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1);
35970
+ // same tempo as last point
35971
+ syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35972
+ // interpolated end from last syncPoint
35973
+ syncPointData.millisecondOffset =
35974
+ lastSyncPoint.data.millisecondOffset +
35975
+ MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35976
+ syncPoints.push(new BackingTrackSyncPoint(controller.currentTick, syncPointData));
35977
+ }
35978
+ }
35979
+ finish(controller.currentTick);
35980
+ }
35917
35981
  static toChannelShort(data) {
35918
35982
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
35919
35983
  return Math.max(value, -1) + 1;
@@ -35945,15 +36009,6 @@ class MidiFileGenerator {
35945
36009
  else {
35946
36010
  masterBarLookup.tempoChanges.push(new MasterBarTickLookupTempoChange(currentTick, currentTempo));
35947
36011
  }
35948
- const syncPoints = masterBar.syncPoints;
35949
- if (syncPoints) {
35950
- for (const syncPoint of syncPoints) {
35951
- if (syncPoint.syncPointValue.barOccurence === barOccurence) {
35952
- const tick = currentTick + masterBarDuration * syncPoint.ratioPosition;
35953
- this.syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
35954
- }
35955
- }
35956
- }
35957
36012
  masterBarLookup.masterBar = masterBar;
35958
36013
  masterBarLookup.start = currentTick;
35959
36014
  masterBarLookup.end = masterBarLookup.start + masterBarDuration;
@@ -38250,21 +38305,23 @@ class BackingTrackPlayer extends AlphaSynthBase {
38250
38305
  this._backingTrackOutput.seekTo(this.sequencer.mainTimePositionToBackingTrack(timePosition, this._backingTrackOutput.backingTrackDuration));
38251
38306
  }
38252
38307
  }
38253
- loadBackingTrack(score, syncPoints) {
38308
+ loadBackingTrack(score) {
38254
38309
  const backingTrackInfo = score.backingTrack;
38255
38310
  if (backingTrackInfo) {
38256
38311
  this._backingTrackOutput.loadBackingTrack(backingTrackInfo);
38257
- this.sequencer.mainUpdateSyncPoints(syncPoints);
38258
38312
  this.timePosition = 0;
38259
38313
  }
38260
38314
  }
38315
+ updateSyncPoints(syncPoints) {
38316
+ this.sequencer.mainUpdateSyncPoints(syncPoints);
38317
+ this.tickPosition = this.tickPosition;
38318
+ }
38261
38319
  }
38262
38320
 
38263
38321
  class ExternalMediaSynthOutput {
38264
38322
  constructor() {
38265
38323
  // fake rate
38266
38324
  this.sampleRate = 44100;
38267
- this._padding = 0;
38268
38325
  this._seekPosition = 0;
38269
38326
  this.ready = new EventEmitter();
38270
38327
  this.samplesPlayed = new EventEmitterOfT();
@@ -38307,20 +38364,19 @@ class ExternalMediaSynthOutput {
38307
38364
  seekTo(time) {
38308
38365
  const handler = this.handler;
38309
38366
  if (handler) {
38310
- handler.seekTo(time - this._padding);
38367
+ handler.seekTo(time);
38311
38368
  }
38312
38369
  else {
38313
- this._seekPosition = time - this._padding;
38370
+ this._seekPosition = time;
38314
38371
  }
38315
38372
  }
38316
- loadBackingTrack(backingTrack) {
38317
- this._padding = backingTrack.padding;
38373
+ loadBackingTrack(_backingTrack) {
38318
38374
  }
38319
38375
  open(_bufferTimeInMilliseconds) {
38320
38376
  this.ready.trigger();
38321
38377
  }
38322
38378
  updatePosition(currentTime) {
38323
- this.timeUpdate.trigger(currentTime + this._padding);
38379
+ this.timeUpdate.trigger(currentTime);
38324
38380
  }
38325
38381
  play() {
38326
38382
  this.handler?.play();
@@ -38585,9 +38641,14 @@ class AlphaSynthWrapper {
38585
38641
  this._instance.loadMidiFile(midi);
38586
38642
  }
38587
38643
  }
38588
- loadBackingTrack(score, syncPoints) {
38644
+ loadBackingTrack(score) {
38589
38645
  if (this._instance) {
38590
- this._instance.loadBackingTrack(score, syncPoints);
38646
+ this._instance.loadBackingTrack(score);
38647
+ }
38648
+ }
38649
+ updateSyncPoints(syncPoints) {
38650
+ if (this._instance) {
38651
+ this._instance.updateSyncPoints(syncPoints);
38591
38652
  }
38592
38653
  }
38593
38654
  applyTranspositionPitches(transpositionPitches) {
@@ -39916,7 +39977,7 @@ class AlphaTabApiBase {
39916
39977
  }
39917
39978
  appendRenderResult(result, isLast) {
39918
39979
  // resizing the canvas and wrapper elements at the end is enough
39919
- // it avoids flickering on resizes and re-renders.
39980
+ // it avoids flickering on resizes and re-renders.
39920
39981
  // the individual partials are anyhow sized correctly
39921
39982
  if (isLast) {
39922
39983
  this.canvasElement.width = result.totalWidth;
@@ -40678,9 +40739,23 @@ class AlphaTabApiBase {
40678
40739
  this.onMidiLoad(midiFile);
40679
40740
  const player = this._player;
40680
40741
  player.loadMidiFile(midiFile);
40681
- player.loadBackingTrack(score, generator.syncPoints);
40742
+ player.loadBackingTrack(score);
40743
+ player.updateSyncPoints(generator.syncPoints);
40682
40744
  player.applyTranspositionPitches(generator.transpositionPitches);
40683
40745
  }
40746
+ /**
40747
+ * Triggers an update of the sync points for the current score after modification within the data model
40748
+ * @category Methods - Player
40749
+ * @since 1.6.0
40750
+ */
40751
+ updateSyncPoints() {
40752
+ if (!this.score) {
40753
+ return;
40754
+ }
40755
+ const score = this.score;
40756
+ const player = this._player;
40757
+ player.updateSyncPoints(MidiFileGenerator.generateSyncPoints(score));
40758
+ }
40684
40759
  /**
40685
40760
  * Changes the volume of the given tracks.
40686
40761
  * @param tracks The tracks for which the volume should be changed.
@@ -41239,8 +41314,9 @@ class AlphaTabApiBase {
41239
41314
  // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
41240
41315
  // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
41241
41316
  // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
41242
- const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * 2;
41243
- beatCursor.transitionToX((duration / cursorSpeed) * 2, doubleEndBeatX);
41317
+ const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
41318
+ const doubleEndBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
41319
+ beatCursor.transitionToX((duration / cursorSpeed) * factor, doubleEndBeatX);
41244
41320
  });
41245
41321
  }
41246
41322
  else {
@@ -43348,6 +43424,8 @@ class AlphaSynthWebWorkerApi {
43348
43424
  }
43349
43425
  loadBackingTrack(_score) {
43350
43426
  }
43427
+ updateSyncPoints(_syncPoints) {
43428
+ }
43351
43429
  }
43352
43430
 
43353
43431
  /**
@@ -43722,7 +43800,6 @@ class AudioElementBackingTrackSynthOutput {
43722
43800
  constructor() {
43723
43801
  // fake rate
43724
43802
  this.sampleRate = 44100;
43725
- this._padding = 0;
43726
43803
  this._updateInterval = 0;
43727
43804
  this.ready = new EventEmitter();
43728
43805
  this.samplesPlayed = new EventEmitterOfT();
@@ -43746,13 +43823,12 @@ class AudioElementBackingTrackSynthOutput {
43746
43823
  this.audioElement.volume = value;
43747
43824
  }
43748
43825
  seekTo(time) {
43749
- this.audioElement.currentTime = time / 1000 - this._padding;
43826
+ this.audioElement.currentTime = time / 1000;
43750
43827
  }
43751
43828
  loadBackingTrack(backingTrack) {
43752
43829
  if (this.audioElement?.src) {
43753
43830
  URL.revokeObjectURL(this.audioElement.src);
43754
43831
  }
43755
- this._padding = backingTrack.padding / 1000;
43756
43832
  const blob = new Blob([backingTrack.rawAudioFile]);
43757
43833
  // https://html.spec.whatwg.org/multipage/media.html#loading-the-media-resource
43758
43834
  // Step 8. resets the playbackRate, we need to remember and restore it.
@@ -43771,7 +43847,7 @@ class AudioElementBackingTrackSynthOutput {
43771
43847
  this.ready.trigger();
43772
43848
  }
43773
43849
  updatePosition() {
43774
- const timePos = (this.audioElement.currentTime + this._padding) * 1000;
43850
+ const timePos = this.audioElement.currentTime * 1000;
43775
43851
  this.timeUpdate.trigger(timePos);
43776
43852
  }
43777
43853
  play() {
@@ -43990,6 +44066,7 @@ class BrowserUiFacade {
43990
44066
  webFont.usages--;
43991
44067
  if (webFont.usages <= 0) {
43992
44068
  webFont.element.remove();
44069
+ BrowserUiFacade._registeredWebFonts.delete(webFont.hash);
43993
44070
  }
43994
44071
  }
43995
44072
  createCanvasElement() {
@@ -44146,6 +44223,7 @@ class BrowserUiFacade {
44146
44223
  checker.checkForFontAvailability();
44147
44224
  settings.display.resources.smuflFont = new Font(familyName, Environment.MusicFontSize, FontStyle.Plain, FontWeight.Regular);
44148
44225
  const webFont = {
44226
+ hash,
44149
44227
  element: styleElement,
44150
44228
  fontSuffix,
44151
44229
  usages: 1,
@@ -60857,9 +60935,9 @@ class VersionInfo {
60857
60935
  print(`build date: ${VersionInfo.date}`);
60858
60936
  }
60859
60937
  }
60860
- VersionInfo.version = '1.6.0-alpha.1418';
60861
- VersionInfo.date = '2025-05-21T02:08:19.492Z';
60862
- VersionInfo.commit = '1f6342282295a9544ebc36ddd38d73d220d2caab';
60938
+ VersionInfo.version = '1.6.0-alpha.1421';
60939
+ VersionInfo.date = '2025-05-22T19:50:36.984Z';
60940
+ VersionInfo.commit = '0b4cd7b2a2c0d244d65c0c0c5254fe4814976bc4';
60863
60941
 
60864
60942
  /**
60865
60943
  * A factory for custom layout engines.