@coderline/alphatab 1.6.0-alpha.1428 → 1.6.0-alpha.1430

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.1428 (develop, build 1428)
2
+ * alphaTab v1.6.0-alpha.1430 (develop, build 1430)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -1411,12 +1411,6 @@ class SyncPointData {
1411
1411
  * Indicates for which repeat occurence this sync point is valid (e.g. 0 on the first time played, 1 on the second time played)
1412
1412
  */
1413
1413
  this.barOccurence = 0;
1414
- /**
1415
- * The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
1416
- * This information is used together with normal tempo changes to calculate how much faster/slower the
1417
- * cursor playback is performed to align with the audio track.
1418
- */
1419
- this.modifiedTempo = 0;
1420
1414
  /**
1421
1415
  * The audio offset marking the position within the audio track in milliseconds.
1422
1416
  * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
@@ -5843,7 +5837,6 @@ class SyncPointDataCloner {
5843
5837
  static clone(original) {
5844
5838
  const clone = new SyncPointData();
5845
5839
  clone.barOccurence = original.barOccurence;
5846
- clone.modifiedTempo = original.modifiedTempo;
5847
5840
  clone.millisecondOffset = original.millisecondOffset;
5848
5841
  return clone;
5849
5842
  }
@@ -7313,7 +7306,6 @@ class Score {
7313
7306
  automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
7314
7307
  automation.type = AutomationType.SyncPoint;
7315
7308
  automation.syncPointValue = new SyncPointData();
7316
- automation.syncPointValue.modifiedTempo = syncPoint.modifiedTempo;
7317
7309
  automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
7318
7310
  automation.syncPointValue.barOccurence = syncPoint.barOccurence;
7319
7311
  if (syncPoint.barIndex < this.masterBars.length) {
@@ -7346,8 +7338,7 @@ class Score {
7346
7338
  barIndex: masterBar.index,
7347
7339
  barOccurence: syncPoint.syncPointValue.barOccurence,
7348
7340
  barPosition: syncPoint.ratioPosition,
7349
- millisecondOffset: syncPoint.syncPointValue.millisecondOffset,
7350
- modifiedTempo: syncPoint.syncPointValue.modifiedTempo
7341
+ millisecondOffset: syncPoint.syncPointValue.millisecondOffset
7351
7342
  });
7352
7343
  }
7353
7344
  }
@@ -14714,9 +14705,6 @@ class GpifParser {
14714
14705
  case 'BarOccurrence':
14715
14706
  syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
14716
14707
  break;
14717
- case 'ModifiedTempo':
14718
- syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
14719
- break;
14720
14708
  case 'FrameOffset':
14721
14709
  const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
14722
14710
  syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
@@ -22339,6 +22327,61 @@ class SynthEvent {
22339
22327
  }
22340
22328
  }
22341
22329
 
22330
+ /**
22331
+ * Rerpresents a point to sync the alphaTab time axis with an external backing track.
22332
+ */
22333
+ class BackingTrackSyncPoint {
22334
+ constructor() {
22335
+ /**
22336
+ * The index of the masterbar to which this sync point belongs to.
22337
+ * @remarks
22338
+ * This property is purely informative for external use like in editors.
22339
+ * It has no impact to the synchronization itself.
22340
+ */
22341
+ this.masterBarIndex = 0;
22342
+ /**
22343
+ * The occurence of the masterbar to which this sync point belongs to. The occurence
22344
+ * is 0-based and increases with every repeated play of a masterbar (e.g. on repeats or jumps).
22345
+ * @remarks
22346
+ * This property is purely informative for external use like in editors.
22347
+ * It has no impact to the synchronization itself.
22348
+ */
22349
+ this.masterBarOccurence = 0;
22350
+ /**
22351
+ * The BPM the synthesizer has at the exact tick position of this sync point.
22352
+ */
22353
+ this.synthBpm = 0;
22354
+ /**
22355
+ * The millisecond time position of the synthesizer when this sync point is reached.
22356
+ */
22357
+ this.synthTime = 0;
22358
+ /**
22359
+ * The midi tick position of the synthesizer when this sync point is reached.
22360
+ */
22361
+ this.synthTick = 0;
22362
+ /**
22363
+ * The millisecond time in the external media marking the synchronization point.
22364
+ */
22365
+ this.syncTime = 0;
22366
+ /**
22367
+ * The BPM the song will have virtually after this sync point to align the external media time axis
22368
+ * with the one from the synthesizer.
22369
+ */
22370
+ this.syncBpm = 0;
22371
+ }
22372
+ /**
22373
+ * Updates the synchronization BPM that will apply after this sync point.
22374
+ * @param nextSyncPointSynthTime The synthesizer time of the next sync point after this one.
22375
+ * @param nextSyncPointSyncTime The synchronization time of the next sync point after this one.
22376
+ */
22377
+ updateSyncBpm(nextSyncPointSynthTime, nextSyncPointSyncTime) {
22378
+ const synthDuration = nextSyncPointSynthTime - this.synthTime;
22379
+ const syncedDuration = nextSyncPointSyncTime - this.syncTime;
22380
+ const modifiedTempo = (synthDuration / syncedDuration) * this.synthBpm;
22381
+ this.syncBpm = modifiedTempo;
22382
+ }
22383
+ }
22384
+
22342
22385
  class MidiFileSequencerTempoChange {
22343
22386
  constructor(bpm, ticks, time) {
22344
22387
  this.bpm = bpm;
@@ -22346,14 +22389,6 @@ class MidiFileSequencerTempoChange {
22346
22389
  this.time = time;
22347
22390
  }
22348
22391
  }
22349
- class BackingTrackSyncPointWithTime {
22350
- constructor(tick, time, modifiedTempo, millisecondOffset) {
22351
- this.alphaTabTick = tick;
22352
- this.alphaTabTime = time;
22353
- this.modifiedTempo = modifiedTempo;
22354
- this.millisecondOffset = millisecondOffset;
22355
- }
22356
- }
22357
22392
  class MidiSequencerState {
22358
22393
  constructor() {
22359
22394
  this.tempoChanges = [];
@@ -22457,7 +22492,7 @@ class MidiFileSequencer {
22457
22492
  this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22458
22493
  this._mainState.modifiedTempo =
22459
22494
  this._mainState.syncPoints.length > 0
22460
- ? this._mainState.syncPoints[0].modifiedTempo
22495
+ ? this._mainState.syncPoints[0].syncBpm
22461
22496
  : this._mainState.currentTempo;
22462
22497
  if (this.isPlayingMain) {
22463
22498
  const metronomeVolume = this._synthesizer.metronomeVolume;
@@ -22628,7 +22663,7 @@ class MidiFileSequencer {
22628
22663
  }
22629
22664
  mainUpdateSyncPoints(syncPoints) {
22630
22665
  const state = this._mainState;
22631
- syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22666
+ syncPoints.sort((a, b) => a.synthTick - b.synthTick); // just in case
22632
22667
  state.syncPoints = [];
22633
22668
  if (syncPoints.length >= 0) {
22634
22669
  let bpm = 120;
@@ -22638,6 +22673,8 @@ class MidiFileSequencer {
22638
22673
  for (let i = 0; i < syncPoints.length; i++) {
22639
22674
  const p = syncPoints[i];
22640
22675
  let deltaTick = 0;
22676
+ // TODO: merge interpolation into MidiFileGenerator where we already play through
22677
+ // the time axis.
22641
22678
  // remember state from previous sync point (or start). to handle linear interpolation
22642
22679
  let previousModifiedTempo;
22643
22680
  let previousMillisecondOffset;
@@ -22649,9 +22686,9 @@ class MidiFileSequencer {
22649
22686
  }
22650
22687
  else {
22651
22688
  const previousSyncPoint = syncPoints[i - 1];
22652
- previousModifiedTempo = previousSyncPoint.data.modifiedTempo;
22653
- previousMillisecondOffset = previousSyncPoint.data.millisecondOffset;
22654
- previousTick = previousSyncPoint.tick;
22689
+ previousModifiedTempo = previousSyncPoint.syncBpm;
22690
+ previousMillisecondOffset = previousSyncPoint.syncTime;
22691
+ previousTick = previousSyncPoint.synthTick;
22655
22692
  }
22656
22693
  // process time until sync point
22657
22694
  // here it gets a bit tricky. if we have tempo changes on the synthesizer time axis (inbetween two sync points)
@@ -22659,27 +22696,31 @@ class MidiFileSequencer {
22659
22696
  // otherwise the linear interpolation later in the lookup will fail.
22660
22697
  // 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
22661
22698
  while (tempoChangeIndex < state.tempoChanges.length &&
22662
- state.tempoChanges[tempoChangeIndex].ticks <= p.tick) {
22699
+ state.tempoChanges[tempoChangeIndex].ticks <= p.synthTick) {
22663
22700
  deltaTick = state.tempoChanges[tempoChangeIndex].ticks - absTick;
22664
22701
  if (deltaTick > 0) {
22665
22702
  absTick += deltaTick;
22666
22703
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22667
- const millisPerTick = (p.data.millisecondOffset - previousMillisecondOffset) / (p.tick - previousTick);
22704
+ const millisPerTick = (p.syncTime - previousMillisecondOffset) / (p.synthTick - previousTick);
22668
22705
  const interpolatedMillisecondOffset = (absTick - previousTick) * millisPerTick + previousMillisecondOffset;
22669
- state.syncPoints.push(new BackingTrackSyncPointWithTime(absTick, absTime, previousModifiedTempo, interpolatedMillisecondOffset));
22706
+ const syncPoint = new BackingTrackSyncPoint();
22707
+ syncPoint.synthTick = absTick;
22708
+ syncPoint.synthBpm = bpm;
22709
+ syncPoint.synthTime = absTime;
22710
+ syncPoint.syncTime = interpolatedMillisecondOffset;
22711
+ syncPoint.syncBpm = previousModifiedTempo;
22670
22712
  }
22671
22713
  bpm = state.tempoChanges[tempoChangeIndex].bpm;
22672
22714
  tempoChangeIndex++;
22673
22715
  }
22674
- deltaTick = p.tick - absTick;
22716
+ deltaTick = p.synthTick - absTick;
22675
22717
  absTick += deltaTick;
22676
22718
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22677
- state.syncPoints.push(new BackingTrackSyncPointWithTime(p.tick, absTime, p.data.modifiedTempo, p.data.millisecondOffset));
22719
+ state.syncPoints.push(p);
22678
22720
  }
22679
22721
  }
22680
22722
  state.syncPointIndex = 0;
22681
- state.modifiedTempo =
22682
- state.syncPoints.length > 0 ? state.syncPoints[0].modifiedTempo : state.currentTempo;
22723
+ state.modifiedTempo = state.syncPoints.length > 0 ? state.syncPoints[0].syncBpm : state.currentTempo;
22683
22724
  }
22684
22725
  currentTimePositionToTickPosition(timePosition) {
22685
22726
  const state = this._currentState;
@@ -22712,16 +22753,15 @@ class MidiFileSequencer {
22712
22753
  const syncPoints = state.syncPoints;
22713
22754
  if (syncPoints.length > 0) {
22714
22755
  let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22715
- if (timePosition < syncPoints[syncPointIndex].millisecondOffset) {
22756
+ if (timePosition < syncPoints[syncPointIndex].syncTime) {
22716
22757
  syncPointIndex = 0;
22717
22758
  }
22718
- while (syncPointIndex + 1 < syncPoints.length &&
22719
- syncPoints[syncPointIndex + 1].millisecondOffset <= timePosition) {
22759
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].syncTime <= timePosition) {
22720
22760
  syncPointIndex++;
22721
22761
  }
22722
22762
  if (syncPointIndex !== state.syncPointIndex) {
22723
22763
  state.syncPointIndex = syncPointIndex;
22724
- state.modifiedTempo = syncPoints[syncPointIndex].modifiedTempo;
22764
+ state.modifiedTempo = syncPoints[syncPointIndex].syncBpm;
22725
22765
  }
22726
22766
  }
22727
22767
  else {
@@ -22737,18 +22777,18 @@ class MidiFileSequencer {
22737
22777
  this.updateSyncPoints(this._mainState, timePosition);
22738
22778
  const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22739
22779
  const currentSyncPoint = syncPoints[syncPointIndex];
22740
- const timeDiff = timePosition - currentSyncPoint.millisecondOffset;
22780
+ const timeDiff = timePosition - currentSyncPoint.syncTime;
22741
22781
  let alphaTabTimeDiff;
22742
22782
  if (syncPointIndex + 1 < syncPoints.length) {
22743
22783
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22744
- const relativeTimeDiff = timeDiff / (nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset);
22745
- alphaTabTimeDiff = (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22784
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.syncTime - currentSyncPoint.syncTime);
22785
+ alphaTabTimeDiff = (nextSyncPoint.synthTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22746
22786
  }
22747
22787
  else {
22748
- const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.millisecondOffset);
22749
- alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22788
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.syncTime);
22789
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22750
22790
  }
22751
- return (currentSyncPoint.alphaTabTime + alphaTabTimeDiff) / this.playbackSpeed;
22791
+ return (currentSyncPoint.synthTime + alphaTabTimeDiff) / this.playbackSpeed;
22752
22792
  }
22753
22793
  mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22754
22794
  const mainState = this._mainState;
@@ -22758,27 +22798,27 @@ class MidiFileSequencer {
22758
22798
  }
22759
22799
  timePosition *= this.playbackSpeed;
22760
22800
  let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22761
- if (timePosition < syncPoints[syncPointIndex].alphaTabTime) {
22801
+ if (timePosition < syncPoints[syncPointIndex].synthTime) {
22762
22802
  syncPointIndex = 0;
22763
22803
  }
22764
- while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].alphaTabTime <= timePosition) {
22804
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].synthTime <= timePosition) {
22765
22805
  syncPointIndex++;
22766
22806
  }
22767
22807
  // NOTE: this logic heavily relies on the interpolation done in mainUpdateSyncPoints
22768
22808
  // we ensure that we have a linear increase between two points
22769
22809
  const currentSyncPoint = syncPoints[syncPointIndex];
22770
- const alphaTabTimeDiff = timePosition - currentSyncPoint.alphaTabTime;
22810
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.synthTime;
22771
22811
  let backingTrackPos;
22772
22812
  if (syncPointIndex + 1 < syncPoints.length) {
22773
22813
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22774
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime);
22775
- const backingTrackDiff = nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset;
22776
- backingTrackPos = currentSyncPoint.millisecondOffset + backingTrackDiff * relativeAlphaTabTimeDiff;
22814
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.synthTime - currentSyncPoint.synthTime);
22815
+ const backingTrackDiff = nextSyncPoint.syncTime - currentSyncPoint.syncTime;
22816
+ backingTrackPos = currentSyncPoint.syncTime + backingTrackDiff * relativeAlphaTabTimeDiff;
22777
22817
  }
22778
22818
  else {
22779
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.alphaTabTime);
22780
- const frameDiff = backingTrackLength - currentSyncPoint.millisecondOffset;
22781
- backingTrackPos = currentSyncPoint.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22819
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.synthTime);
22820
+ const frameDiff = backingTrackLength - currentSyncPoint.syncTime;
22821
+ backingTrackPos = currentSyncPoint.syncTime + frameDiff * relativeAlphaTabTimeDiff;
22782
22822
  }
22783
22823
  return backingTrackPos;
22784
22824
  }
@@ -31050,7 +31090,6 @@ class SyncPointDataSerializer {
31050
31090
  }
31051
31091
  const o = new Map();
31052
31092
  o.set("baroccurence", obj.barOccurence);
31053
- o.set("modifiedtempo", obj.modifiedTempo);
31054
31093
  o.set("millisecondoffset", obj.millisecondOffset);
31055
31094
  return o;
31056
31095
  }
@@ -31059,9 +31098,6 @@ class SyncPointDataSerializer {
31059
31098
  case "baroccurence":
31060
31099
  obj.barOccurence = v;
31061
31100
  return true;
31062
- case "modifiedtempo":
31063
- obj.modifiedTempo = v;
31064
- return true;
31065
31101
  case "millisecondoffset":
31066
31102
  obj.millisecondOffset = v;
31067
31103
  return true;
@@ -35824,17 +35860,6 @@ class MidiTickLookup {
35824
35860
  }
35825
35861
  }
35826
35862
 
35827
- /**
35828
- * Rerpresents a point to sync the alphaTab time axis with an external backing track.
35829
- */
35830
- class BackingTrackSyncPoint {
35831
- constructor(tick, data) {
35832
- this.tick = 0;
35833
- this.tick = tick;
35834
- this.data = data;
35835
- }
35836
- }
35837
-
35838
35863
  class MidiNoteDuration {
35839
35864
  constructor() {
35840
35865
  this.noteOnly = 0;
@@ -35855,6 +35880,14 @@ class RasgueadoInfo {
35855
35880
  this.brushInfos = [];
35856
35881
  }
35857
35882
  }
35883
+ class PlayThroughContext {
35884
+ constructor() {
35885
+ this.synthTick = 0;
35886
+ this.synthTime = 0;
35887
+ this.currentTempo = 0;
35888
+ this.automationToSyncPoint = new Map();
35889
+ }
35890
+ }
35858
35891
  /**
35859
35892
  * This generator creates a midi file using a score.
35860
35893
  */
@@ -35981,9 +36014,22 @@ class MidiFileGenerator {
35981
36014
  });
35982
36015
  return syncPoints;
35983
36016
  }
36017
+ /**
36018
+ * @internal
36019
+ */
36020
+ static buildModifiedTempoLookup(score) {
36021
+ const syncPoints = [];
36022
+ const context = MidiFileGenerator.playThroughSong(score, syncPoints, (_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
36023
+ }, (_barIndex, _currentTick, _currentTempo) => {
36024
+ }, _endTick => {
36025
+ });
36026
+ return context.automationToSyncPoint;
36027
+ }
35984
36028
  static playThroughSong(score, syncPoints, generateMasterBar, generateTracks, finish) {
35985
36029
  const controller = new MidiPlaybackController(score);
35986
- let currentTempo = score.tempo;
36030
+ const playContext = new PlayThroughContext();
36031
+ playContext.currentTempo = score.tempo;
36032
+ playContext.syncPoints = syncPoints;
35987
36033
  let previousMasterBar = null;
35988
36034
  // store the previous played bar for repeats
35989
36035
  const barOccurence = new Map();
@@ -35996,23 +36042,11 @@ class MidiFileGenerator {
35996
36042
  let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
35997
36043
  occurence++;
35998
36044
  barOccurence.set(index, occurence);
35999
- generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
36000
- const barSyncPoints = bar.syncPoints;
36001
- if (barSyncPoints) {
36002
- for (const syncPoint of barSyncPoints) {
36003
- if (syncPoint.syncPointValue.barOccurence === occurence) {
36004
- const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
36005
- syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
36006
- }
36007
- }
36008
- }
36009
- if (bar.tempoAutomations.length > 0) {
36010
- currentTempo = bar.tempoAutomations[0].value;
36011
- }
36012
- generateTracks(index, currentTick, currentTempo);
36013
- if (bar.tempoAutomations.length > 0) {
36014
- currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
36015
- }
36045
+ generateMasterBar(bar, previousMasterBar, currentTick, playContext.currentTempo, occurence);
36046
+ const trackTempo = bar.tempoAutomations.length > 0 ? bar.tempoAutomations[0].value : playContext.currentTempo;
36047
+ generateTracks(index, currentTick, trackTempo);
36048
+ playContext.synthTick = currentTick;
36049
+ MidiFileGenerator.processBarTime(bar, occurence, playContext);
36016
36050
  }
36017
36051
  controller.moveNext();
36018
36052
  previousMasterBar = bar;
@@ -36023,21 +36057,119 @@ class MidiFileGenerator {
36023
36057
  // but where it ends according to the BPM and the remaining ticks.
36024
36058
  if (syncPoints.length > 0) {
36025
36059
  const lastSyncPoint = syncPoints[syncPoints.length - 1];
36026
- const remainingTicks = controller.currentTick - lastSyncPoint.tick;
36060
+ const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
36027
36061
  if (remainingTicks > 0) {
36028
- const syncPointData = new SyncPointData();
36029
- // last occurence of the last bar
36030
- syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1);
36031
- // same tempo as last point
36032
- syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
36033
- // interpolated end from last syncPoint
36034
- syncPointData.millisecondOffset =
36035
- lastSyncPoint.data.millisecondOffset +
36036
- MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
36037
- syncPoints.push(new BackingTrackSyncPoint(controller.currentTick, syncPointData));
36062
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36063
+ backingTrackSyncPoint.masterBarIndex = previousMasterBar.index;
36064
+ backingTrackSyncPoint.masterBarOccurence = barOccurence.get(previousMasterBar.index) - 1;
36065
+ backingTrackSyncPoint.synthTick = controller.currentTick;
36066
+ backingTrackSyncPoint.synthBpm = playContext.currentTempo;
36067
+ // we need to assume some BPM for the last interpolated point.
36068
+ // if we have more than just a start point, we keep the BPM before the last manual sync point
36069
+ // otherwise we have no customized sync BPM known and keep the synthesizer one.
36070
+ backingTrackSyncPoint.syncBpm =
36071
+ syncPoints.length > 1 ? syncPoints[syncPoints.length - 2].syncBpm : lastSyncPoint.synthBpm;
36072
+ backingTrackSyncPoint.synthTime =
36073
+ lastSyncPoint.synthTime + MidiUtils.ticksToMillis(remainingTicks, lastSyncPoint.synthBpm);
36074
+ backingTrackSyncPoint.syncTime =
36075
+ lastSyncPoint.syncTime + MidiUtils.ticksToMillis(remainingTicks, backingTrackSyncPoint.syncBpm);
36076
+ // update the previous sync point according to the new time
36077
+ lastSyncPoint.updateSyncBpm(backingTrackSyncPoint.synthTime, backingTrackSyncPoint.syncTime);
36078
+ syncPoints.push(backingTrackSyncPoint);
36038
36079
  }
36039
36080
  }
36040
36081
  finish(controller.currentTick);
36082
+ return playContext;
36083
+ }
36084
+ static processBarTime(bar, occurence, context) {
36085
+ const duration = bar.calculateDuration();
36086
+ const barSyncPoints = bar.syncPoints;
36087
+ const barStartTick = context.synthTick;
36088
+ if (barSyncPoints) {
36089
+ MidiFileGenerator.processBarTimeWithSyncPoints(bar, occurence, context);
36090
+ }
36091
+ else {
36092
+ MidiFileGenerator.processBarTimeNoSyncPoints(bar, context);
36093
+ }
36094
+ // don't forget the part after the last tempo change
36095
+ const endTick = barStartTick + duration;
36096
+ const tickOffset = endTick - context.synthTick;
36097
+ if (tickOffset > 0) {
36098
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36099
+ context.synthTick = endTick;
36100
+ }
36101
+ }
36102
+ static processBarTimeWithSyncPoints(bar, occurence, context) {
36103
+ const barStartTick = context.synthTick;
36104
+ const duration = bar.calculateDuration();
36105
+ let tempoChangeIndex = 0;
36106
+ let tickOffset;
36107
+ for (const syncPoint of bar.syncPoints) {
36108
+ if (syncPoint.syncPointValue.barOccurence !== occurence) {
36109
+ continue;
36110
+ }
36111
+ const syncPointTick = barStartTick + syncPoint.ratioPosition * duration;
36112
+ // first process all tempo changes until this sync point
36113
+ while (tempoChangeIndex < bar.tempoAutomations.length &&
36114
+ bar.tempoAutomations[tempoChangeIndex].ratioPosition <= syncPoint.ratioPosition) {
36115
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36116
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36117
+ tickOffset = absoluteTick - context.synthTick;
36118
+ if (tickOffset > 0) {
36119
+ context.synthTick = absoluteTick;
36120
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36121
+ }
36122
+ context.currentTempo = tempoChange.value;
36123
+ tempoChangeIndex++;
36124
+ }
36125
+ // process time until sync point
36126
+ tickOffset = syncPointTick - context.synthTick;
36127
+ if (tickOffset > 0) {
36128
+ context.synthTick = syncPointTick;
36129
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36130
+ }
36131
+ // update the previous sync point according to the new time
36132
+ if (context.syncPoints.length > 0) {
36133
+ context.syncPoints[context.syncPoints.length - 1].updateSyncBpm(context.synthTime, syncPoint.syncPointValue.millisecondOffset);
36134
+ }
36135
+ // create the new sync point
36136
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36137
+ backingTrackSyncPoint.masterBarIndex = bar.index;
36138
+ backingTrackSyncPoint.masterBarOccurence = occurence;
36139
+ backingTrackSyncPoint.synthTick = syncPointTick;
36140
+ backingTrackSyncPoint.synthBpm = context.currentTempo;
36141
+ backingTrackSyncPoint.synthTime = context.synthTime;
36142
+ backingTrackSyncPoint.syncTime = syncPoint.syncPointValue.millisecondOffset;
36143
+ backingTrackSyncPoint.syncBpm = 0 /* calculated by next sync point */;
36144
+ context.syncPoints.push(backingTrackSyncPoint);
36145
+ context.automationToSyncPoint.set(syncPoint, backingTrackSyncPoint);
36146
+ }
36147
+ // process remaining tempo changes after all sync points
36148
+ while (tempoChangeIndex < bar.tempoAutomations.length) {
36149
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36150
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36151
+ tickOffset = absoluteTick - context.synthTick;
36152
+ if (tickOffset > 0) {
36153
+ context.synthTick = absoluteTick;
36154
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36155
+ }
36156
+ context.currentTempo = tempoChange.value;
36157
+ tempoChangeIndex++;
36158
+ }
36159
+ }
36160
+ static processBarTimeNoSyncPoints(bar, context) {
36161
+ // walk through the tempo changes
36162
+ const barStartTick = context.synthTick;
36163
+ const duration = bar.calculateDuration();
36164
+ for (const changes of bar.tempoAutomations) {
36165
+ const absoluteTick = barStartTick + changes.ratioPosition * duration;
36166
+ const tickOffset = absoluteTick - context.synthTick;
36167
+ if (tickOffset > 0) {
36168
+ context.synthTick = absoluteTick;
36169
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36170
+ }
36171
+ context.currentTempo = changes.value;
36172
+ }
36041
36173
  }
36042
36174
  static toChannelShort(data) {
36043
36175
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
@@ -61000,9 +61132,9 @@ class VersionInfo {
61000
61132
  print(`build date: ${VersionInfo.date}`);
61001
61133
  }
61002
61134
  }
61003
- VersionInfo.version = '1.6.0-alpha.1428';
61004
- VersionInfo.date = '2025-05-29T00:44:50.954Z';
61005
- VersionInfo.commit = 'fa81a14da248f229511867324cee663ea70a72b3';
61135
+ VersionInfo.version = '1.6.0-alpha.1430';
61136
+ VersionInfo.date = '2025-05-29T22:21:21.689Z';
61137
+ VersionInfo.commit = '98a4c2bec8d71f2645008118d3b77fb40973e7fe';
61006
61138
 
61007
61139
  /**
61008
61140
  * A factory for custom layout engines.
@@ -62652,6 +62784,7 @@ class GpifWriter {
62652
62784
  : undefined;
62653
62785
  const millisecondPadding = initialSyncPoint ? initialSyncPoint.syncPointValue.millisecondOffset : 0;
62654
62786
  this.backingTrackFramePadding = (-1 * ((millisecondPadding / 1000) * GpifWriter.SampleRate)) | 0;
62787
+ const modifiedTempoLookup = new Lazy(() => MidiFileGenerator.buildModifiedTempoLookup(score));
62655
62788
  for (const mb of score.masterBars) {
62656
62789
  for (const automation of mb.tempoAutomations) {
62657
62790
  const tempoAutomation = automations.addElement('Automation');
@@ -62676,7 +62809,7 @@ class GpifWriter {
62676
62809
  const value = syncPointAutomation.addElement('Value');
62677
62810
  value.addElement('BarIndex').innerText = mb.index.toString();
62678
62811
  value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue.barOccurence.toString();
62679
- value.addElement('ModifiedTempo').innerText = syncPoint.syncPointValue.modifiedTempo.toString();
62812
+ value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint).syncBpm.toString();
62680
62813
  value.addElement('OriginalTempo').innerText = score.tempo.toString();
62681
62814
  const frameOffset = (((syncPoint.syncPointValue.millisecondOffset - millisecondPadding) / 1000) *
62682
62815
  GpifWriter.SampleRate) |
@@ -3092,17 +3092,48 @@ declare class BackingTrack {
3092
3092
  * Rerpresents a point to sync the alphaTab time axis with an external backing track.
3093
3093
  */
3094
3094
  declare class BackingTrackSyncPoint {
3095
- tick: number;
3096
- data: SyncPointData;
3097
- constructor(tick: number, data: SyncPointData);
3098
- }
3099
-
3100
- declare class BackingTrackSyncPointWithTime {
3101
- alphaTabTick: number;
3102
- alphaTabTime: number;
3103
- modifiedTempo: number;
3104
- millisecondOffset: number;
3105
- constructor(tick: number, time: number, modifiedTempo: number, millisecondOffset: number);
3095
+ /**
3096
+ * The index of the masterbar to which this sync point belongs to.
3097
+ * @remarks
3098
+ * This property is purely informative for external use like in editors.
3099
+ * It has no impact to the synchronization itself.
3100
+ */
3101
+ masterBarIndex: number;
3102
+ /**
3103
+ * The occurence of the masterbar to which this sync point belongs to. The occurence
3104
+ * is 0-based and increases with every repeated play of a masterbar (e.g. on repeats or jumps).
3105
+ * @remarks
3106
+ * This property is purely informative for external use like in editors.
3107
+ * It has no impact to the synchronization itself.
3108
+ */
3109
+ masterBarOccurence: number;
3110
+ /**
3111
+ * The BPM the synthesizer has at the exact tick position of this sync point.
3112
+ */
3113
+ synthBpm: number;
3114
+ /**
3115
+ * The millisecond time position of the synthesizer when this sync point is reached.
3116
+ */
3117
+ synthTime: number;
3118
+ /**
3119
+ * The midi tick position of the synthesizer when this sync point is reached.
3120
+ */
3121
+ synthTick: number;
3122
+ /**
3123
+ * The millisecond time in the external media marking the synchronization point.
3124
+ */
3125
+ syncTime: number;
3126
+ /**
3127
+ * The BPM the song will have virtually after this sync point to align the external media time axis
3128
+ * with the one from the synthesizer.
3129
+ */
3130
+ syncBpm: number;
3131
+ /**
3132
+ * Updates the synchronization BPM that will apply after this sync point.
3133
+ * @param nextSyncPointSynthTime The synthesizer time of the next sync point after this one.
3134
+ * @param nextSyncPointSyncTime The synchronization time of the next sync point after this one.
3135
+ */
3136
+ updateSyncBpm(nextSyncPointSynthTime: number, nextSyncPointSyncTime: number): void;
3106
3137
  }
3107
3138
 
3108
3139
  /**
@@ -6492,13 +6523,7 @@ declare interface FlatSyncPoint {
6492
6523
  */
6493
6524
  barOccurence: number;
6494
6525
  /**
6495
- * The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
6496
- * This information is used together with normal tempo changes to calculate how much faster/slower the
6497
- * cursor playback is performed to align with the audio track.
6498
- */
6499
- modifiedTempo: number;
6500
- /**
6501
- * The uadio offset marking the position within the audio track in milliseconds.
6526
+ * The audio offset marking the position within the audio track in milliseconds.
6502
6527
  * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
6503
6528
  */
6504
6529
  millisecondOffset: number;
@@ -9271,7 +9296,11 @@ declare class MidiFileGenerator {
9271
9296
  * @returns The generated sync points for usage in the backing track playback.
9272
9297
  */
9273
9298
  static generateSyncPoints(score: Score): BackingTrackSyncPoint[];
9299
+ /* Excluded from this release type: buildModifiedTempoLookup */
9274
9300
  private static playThroughSong;
9301
+ private static processBarTime;
9302
+ private static processBarTimeWithSyncPoints;
9303
+ private static processBarTimeNoSyncPoints;
9275
9304
  private static toChannelShort;
9276
9305
  private generateMasterBar;
9277
9306
  private generateBar;
@@ -9415,7 +9444,7 @@ declare class MidiFileSequencerTempoChange {
9415
9444
  declare class MidiSequencerState {
9416
9445
  tempoChanges: MidiFileSequencerTempoChange[];
9417
9446
  tempoChangeIndex: number;
9418
- syncPoints: BackingTrackSyncPointWithTime[];
9447
+ syncPoints: BackingTrackSyncPoint[];
9419
9448
  firstProgramEventPerChannel: Map<number, SynthEvent>;
9420
9449
  firstTimeSignatureNumerator: number;
9421
9450
  firstTimeSignatureDenominator: number;
@@ -13656,12 +13685,6 @@ declare class SyncPointData {
13656
13685
  * Indicates for which repeat occurence this sync point is valid (e.g. 0 on the first time played, 1 on the second time played)
13657
13686
  */
13658
13687
  barOccurence: number;
13659
- /**
13660
- * The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
13661
- * This information is used together with normal tempo changes to calculate how much faster/slower the
13662
- * cursor playback is performed to align with the audio track.
13663
- */
13664
- modifiedTempo: number;
13665
13688
  /**
13666
13689
  * The audio offset marking the position within the audio track in milliseconds.
13667
13690
  * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.