@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -1417,12 +1417,6 @@
1417
1417
  * Indicates for which repeat occurence this sync point is valid (e.g. 0 on the first time played, 1 on the second time played)
1418
1418
  */
1419
1419
  this.barOccurence = 0;
1420
- /**
1421
- * The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
1422
- * This information is used together with normal tempo changes to calculate how much faster/slower the
1423
- * cursor playback is performed to align with the audio track.
1424
- */
1425
- this.modifiedTempo = 0;
1426
1420
  /**
1427
1421
  * The audio offset marking the position within the audio track in milliseconds.
1428
1422
  * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
@@ -5849,7 +5843,6 @@
5849
5843
  static clone(original) {
5850
5844
  const clone = new SyncPointData();
5851
5845
  clone.barOccurence = original.barOccurence;
5852
- clone.modifiedTempo = original.modifiedTempo;
5853
5846
  clone.millisecondOffset = original.millisecondOffset;
5854
5847
  return clone;
5855
5848
  }
@@ -7319,7 +7312,6 @@
7319
7312
  automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
7320
7313
  automation.type = AutomationType.SyncPoint;
7321
7314
  automation.syncPointValue = new SyncPointData();
7322
- automation.syncPointValue.modifiedTempo = syncPoint.modifiedTempo;
7323
7315
  automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
7324
7316
  automation.syncPointValue.barOccurence = syncPoint.barOccurence;
7325
7317
  if (syncPoint.barIndex < this.masterBars.length) {
@@ -7352,8 +7344,7 @@
7352
7344
  barIndex: masterBar.index,
7353
7345
  barOccurence: syncPoint.syncPointValue.barOccurence,
7354
7346
  barPosition: syncPoint.ratioPosition,
7355
- millisecondOffset: syncPoint.syncPointValue.millisecondOffset,
7356
- modifiedTempo: syncPoint.syncPointValue.modifiedTempo
7347
+ millisecondOffset: syncPoint.syncPointValue.millisecondOffset
7357
7348
  });
7358
7349
  }
7359
7350
  }
@@ -14720,9 +14711,6 @@
14720
14711
  case 'BarOccurrence':
14721
14712
  syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
14722
14713
  break;
14723
- case 'ModifiedTempo':
14724
- syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
14725
- break;
14726
14714
  case 'FrameOffset':
14727
14715
  const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
14728
14716
  syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
@@ -22345,6 +22333,61 @@
22345
22333
  }
22346
22334
  }
22347
22335
 
22336
+ /**
22337
+ * Rerpresents a point to sync the alphaTab time axis with an external backing track.
22338
+ */
22339
+ class BackingTrackSyncPoint {
22340
+ constructor() {
22341
+ /**
22342
+ * The index of the masterbar to which this sync point belongs to.
22343
+ * @remarks
22344
+ * This property is purely informative for external use like in editors.
22345
+ * It has no impact to the synchronization itself.
22346
+ */
22347
+ this.masterBarIndex = 0;
22348
+ /**
22349
+ * The occurence of the masterbar to which this sync point belongs to. The occurence
22350
+ * is 0-based and increases with every repeated play of a masterbar (e.g. on repeats or jumps).
22351
+ * @remarks
22352
+ * This property is purely informative for external use like in editors.
22353
+ * It has no impact to the synchronization itself.
22354
+ */
22355
+ this.masterBarOccurence = 0;
22356
+ /**
22357
+ * The BPM the synthesizer has at the exact tick position of this sync point.
22358
+ */
22359
+ this.synthBpm = 0;
22360
+ /**
22361
+ * The millisecond time position of the synthesizer when this sync point is reached.
22362
+ */
22363
+ this.synthTime = 0;
22364
+ /**
22365
+ * The midi tick position of the synthesizer when this sync point is reached.
22366
+ */
22367
+ this.synthTick = 0;
22368
+ /**
22369
+ * The millisecond time in the external media marking the synchronization point.
22370
+ */
22371
+ this.syncTime = 0;
22372
+ /**
22373
+ * The BPM the song will have virtually after this sync point to align the external media time axis
22374
+ * with the one from the synthesizer.
22375
+ */
22376
+ this.syncBpm = 0;
22377
+ }
22378
+ /**
22379
+ * Updates the synchronization BPM that will apply after this sync point.
22380
+ * @param nextSyncPointSynthTime The synthesizer time of the next sync point after this one.
22381
+ * @param nextSyncPointSyncTime The synchronization time of the next sync point after this one.
22382
+ */
22383
+ updateSyncBpm(nextSyncPointSynthTime, nextSyncPointSyncTime) {
22384
+ const synthDuration = nextSyncPointSynthTime - this.synthTime;
22385
+ const syncedDuration = nextSyncPointSyncTime - this.syncTime;
22386
+ const modifiedTempo = (synthDuration / syncedDuration) * this.synthBpm;
22387
+ this.syncBpm = modifiedTempo;
22388
+ }
22389
+ }
22390
+
22348
22391
  class MidiFileSequencerTempoChange {
22349
22392
  constructor(bpm, ticks, time) {
22350
22393
  this.bpm = bpm;
@@ -22352,14 +22395,6 @@
22352
22395
  this.time = time;
22353
22396
  }
22354
22397
  }
22355
- class BackingTrackSyncPointWithTime {
22356
- constructor(tick, time, modifiedTempo, millisecondOffset) {
22357
- this.alphaTabTick = tick;
22358
- this.alphaTabTime = time;
22359
- this.modifiedTempo = modifiedTempo;
22360
- this.millisecondOffset = millisecondOffset;
22361
- }
22362
- }
22363
22398
  class MidiSequencerState {
22364
22399
  constructor() {
22365
22400
  this.tempoChanges = [];
@@ -22463,7 +22498,7 @@
22463
22498
  this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22464
22499
  this._mainState.modifiedTempo =
22465
22500
  this._mainState.syncPoints.length > 0
22466
- ? this._mainState.syncPoints[0].modifiedTempo
22501
+ ? this._mainState.syncPoints[0].syncBpm
22467
22502
  : this._mainState.currentTempo;
22468
22503
  if (this.isPlayingMain) {
22469
22504
  const metronomeVolume = this._synthesizer.metronomeVolume;
@@ -22634,7 +22669,7 @@
22634
22669
  }
22635
22670
  mainUpdateSyncPoints(syncPoints) {
22636
22671
  const state = this._mainState;
22637
- syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22672
+ syncPoints.sort((a, b) => a.synthTick - b.synthTick); // just in case
22638
22673
  state.syncPoints = [];
22639
22674
  if (syncPoints.length >= 0) {
22640
22675
  let bpm = 120;
@@ -22644,6 +22679,8 @@
22644
22679
  for (let i = 0; i < syncPoints.length; i++) {
22645
22680
  const p = syncPoints[i];
22646
22681
  let deltaTick = 0;
22682
+ // TODO: merge interpolation into MidiFileGenerator where we already play through
22683
+ // the time axis.
22647
22684
  // remember state from previous sync point (or start). to handle linear interpolation
22648
22685
  let previousModifiedTempo;
22649
22686
  let previousMillisecondOffset;
@@ -22655,9 +22692,9 @@
22655
22692
  }
22656
22693
  else {
22657
22694
  const previousSyncPoint = syncPoints[i - 1];
22658
- previousModifiedTempo = previousSyncPoint.data.modifiedTempo;
22659
- previousMillisecondOffset = previousSyncPoint.data.millisecondOffset;
22660
- previousTick = previousSyncPoint.tick;
22695
+ previousModifiedTempo = previousSyncPoint.syncBpm;
22696
+ previousMillisecondOffset = previousSyncPoint.syncTime;
22697
+ previousTick = previousSyncPoint.synthTick;
22661
22698
  }
22662
22699
  // process time until sync point
22663
22700
  // here it gets a bit tricky. if we have tempo changes on the synthesizer time axis (inbetween two sync points)
@@ -22665,27 +22702,31 @@
22665
22702
  // otherwise the linear interpolation later in the lookup will fail.
22666
22703
  // 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
22667
22704
  while (tempoChangeIndex < state.tempoChanges.length &&
22668
- state.tempoChanges[tempoChangeIndex].ticks <= p.tick) {
22705
+ state.tempoChanges[tempoChangeIndex].ticks <= p.synthTick) {
22669
22706
  deltaTick = state.tempoChanges[tempoChangeIndex].ticks - absTick;
22670
22707
  if (deltaTick > 0) {
22671
22708
  absTick += deltaTick;
22672
22709
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22673
- const millisPerTick = (p.data.millisecondOffset - previousMillisecondOffset) / (p.tick - previousTick);
22710
+ const millisPerTick = (p.syncTime - previousMillisecondOffset) / (p.synthTick - previousTick);
22674
22711
  const interpolatedMillisecondOffset = (absTick - previousTick) * millisPerTick + previousMillisecondOffset;
22675
- state.syncPoints.push(new BackingTrackSyncPointWithTime(absTick, absTime, previousModifiedTempo, interpolatedMillisecondOffset));
22712
+ const syncPoint = new BackingTrackSyncPoint();
22713
+ syncPoint.synthTick = absTick;
22714
+ syncPoint.synthBpm = bpm;
22715
+ syncPoint.synthTime = absTime;
22716
+ syncPoint.syncTime = interpolatedMillisecondOffset;
22717
+ syncPoint.syncBpm = previousModifiedTempo;
22676
22718
  }
22677
22719
  bpm = state.tempoChanges[tempoChangeIndex].bpm;
22678
22720
  tempoChangeIndex++;
22679
22721
  }
22680
- deltaTick = p.tick - absTick;
22722
+ deltaTick = p.synthTick - absTick;
22681
22723
  absTick += deltaTick;
22682
22724
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22683
- state.syncPoints.push(new BackingTrackSyncPointWithTime(p.tick, absTime, p.data.modifiedTempo, p.data.millisecondOffset));
22725
+ state.syncPoints.push(p);
22684
22726
  }
22685
22727
  }
22686
22728
  state.syncPointIndex = 0;
22687
- state.modifiedTempo =
22688
- state.syncPoints.length > 0 ? state.syncPoints[0].modifiedTempo : state.currentTempo;
22729
+ state.modifiedTempo = state.syncPoints.length > 0 ? state.syncPoints[0].syncBpm : state.currentTempo;
22689
22730
  }
22690
22731
  currentTimePositionToTickPosition(timePosition) {
22691
22732
  const state = this._currentState;
@@ -22718,16 +22759,15 @@
22718
22759
  const syncPoints = state.syncPoints;
22719
22760
  if (syncPoints.length > 0) {
22720
22761
  let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22721
- if (timePosition < syncPoints[syncPointIndex].millisecondOffset) {
22762
+ if (timePosition < syncPoints[syncPointIndex].syncTime) {
22722
22763
  syncPointIndex = 0;
22723
22764
  }
22724
- while (syncPointIndex + 1 < syncPoints.length &&
22725
- syncPoints[syncPointIndex + 1].millisecondOffset <= timePosition) {
22765
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].syncTime <= timePosition) {
22726
22766
  syncPointIndex++;
22727
22767
  }
22728
22768
  if (syncPointIndex !== state.syncPointIndex) {
22729
22769
  state.syncPointIndex = syncPointIndex;
22730
- state.modifiedTempo = syncPoints[syncPointIndex].modifiedTempo;
22770
+ state.modifiedTempo = syncPoints[syncPointIndex].syncBpm;
22731
22771
  }
22732
22772
  }
22733
22773
  else {
@@ -22743,18 +22783,18 @@
22743
22783
  this.updateSyncPoints(this._mainState, timePosition);
22744
22784
  const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22745
22785
  const currentSyncPoint = syncPoints[syncPointIndex];
22746
- const timeDiff = timePosition - currentSyncPoint.millisecondOffset;
22786
+ const timeDiff = timePosition - currentSyncPoint.syncTime;
22747
22787
  let alphaTabTimeDiff;
22748
22788
  if (syncPointIndex + 1 < syncPoints.length) {
22749
22789
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22750
- const relativeTimeDiff = timeDiff / (nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset);
22751
- alphaTabTimeDiff = (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22790
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.syncTime - currentSyncPoint.syncTime);
22791
+ alphaTabTimeDiff = (nextSyncPoint.synthTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22752
22792
  }
22753
22793
  else {
22754
- const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.millisecondOffset);
22755
- alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22794
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.syncTime);
22795
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22756
22796
  }
22757
- return (currentSyncPoint.alphaTabTime + alphaTabTimeDiff) / this.playbackSpeed;
22797
+ return (currentSyncPoint.synthTime + alphaTabTimeDiff) / this.playbackSpeed;
22758
22798
  }
22759
22799
  mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22760
22800
  const mainState = this._mainState;
@@ -22764,27 +22804,27 @@
22764
22804
  }
22765
22805
  timePosition *= this.playbackSpeed;
22766
22806
  let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22767
- if (timePosition < syncPoints[syncPointIndex].alphaTabTime) {
22807
+ if (timePosition < syncPoints[syncPointIndex].synthTime) {
22768
22808
  syncPointIndex = 0;
22769
22809
  }
22770
- while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].alphaTabTime <= timePosition) {
22810
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].synthTime <= timePosition) {
22771
22811
  syncPointIndex++;
22772
22812
  }
22773
22813
  // NOTE: this logic heavily relies on the interpolation done in mainUpdateSyncPoints
22774
22814
  // we ensure that we have a linear increase between two points
22775
22815
  const currentSyncPoint = syncPoints[syncPointIndex];
22776
- const alphaTabTimeDiff = timePosition - currentSyncPoint.alphaTabTime;
22816
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.synthTime;
22777
22817
  let backingTrackPos;
22778
22818
  if (syncPointIndex + 1 < syncPoints.length) {
22779
22819
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22780
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime);
22781
- const backingTrackDiff = nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset;
22782
- backingTrackPos = currentSyncPoint.millisecondOffset + backingTrackDiff * relativeAlphaTabTimeDiff;
22820
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.synthTime - currentSyncPoint.synthTime);
22821
+ const backingTrackDiff = nextSyncPoint.syncTime - currentSyncPoint.syncTime;
22822
+ backingTrackPos = currentSyncPoint.syncTime + backingTrackDiff * relativeAlphaTabTimeDiff;
22783
22823
  }
22784
22824
  else {
22785
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.alphaTabTime);
22786
- const frameDiff = backingTrackLength - currentSyncPoint.millisecondOffset;
22787
- backingTrackPos = currentSyncPoint.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22825
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.synthTime);
22826
+ const frameDiff = backingTrackLength - currentSyncPoint.syncTime;
22827
+ backingTrackPos = currentSyncPoint.syncTime + frameDiff * relativeAlphaTabTimeDiff;
22788
22828
  }
22789
22829
  return backingTrackPos;
22790
22830
  }
@@ -31056,7 +31096,6 @@
31056
31096
  }
31057
31097
  const o = new Map();
31058
31098
  o.set("baroccurence", obj.barOccurence);
31059
- o.set("modifiedtempo", obj.modifiedTempo);
31060
31099
  o.set("millisecondoffset", obj.millisecondOffset);
31061
31100
  return o;
31062
31101
  }
@@ -31065,9 +31104,6 @@
31065
31104
  case "baroccurence":
31066
31105
  obj.barOccurence = v;
31067
31106
  return true;
31068
- case "modifiedtempo":
31069
- obj.modifiedTempo = v;
31070
- return true;
31071
31107
  case "millisecondoffset":
31072
31108
  obj.millisecondOffset = v;
31073
31109
  return true;
@@ -35830,17 +35866,6 @@
35830
35866
  }
35831
35867
  }
35832
35868
 
35833
- /**
35834
- * Rerpresents a point to sync the alphaTab time axis with an external backing track.
35835
- */
35836
- class BackingTrackSyncPoint {
35837
- constructor(tick, data) {
35838
- this.tick = 0;
35839
- this.tick = tick;
35840
- this.data = data;
35841
- }
35842
- }
35843
-
35844
35869
  class MidiNoteDuration {
35845
35870
  constructor() {
35846
35871
  this.noteOnly = 0;
@@ -35861,6 +35886,14 @@
35861
35886
  this.brushInfos = [];
35862
35887
  }
35863
35888
  }
35889
+ class PlayThroughContext {
35890
+ constructor() {
35891
+ this.synthTick = 0;
35892
+ this.synthTime = 0;
35893
+ this.currentTempo = 0;
35894
+ this.automationToSyncPoint = new Map();
35895
+ }
35896
+ }
35864
35897
  /**
35865
35898
  * This generator creates a midi file using a score.
35866
35899
  */
@@ -35987,9 +36020,22 @@
35987
36020
  });
35988
36021
  return syncPoints;
35989
36022
  }
36023
+ /**
36024
+ * @internal
36025
+ */
36026
+ static buildModifiedTempoLookup(score) {
36027
+ const syncPoints = [];
36028
+ const context = MidiFileGenerator.playThroughSong(score, syncPoints, (_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
36029
+ }, (_barIndex, _currentTick, _currentTempo) => {
36030
+ }, _endTick => {
36031
+ });
36032
+ return context.automationToSyncPoint;
36033
+ }
35990
36034
  static playThroughSong(score, syncPoints, generateMasterBar, generateTracks, finish) {
35991
36035
  const controller = new MidiPlaybackController(score);
35992
- let currentTempo = score.tempo;
36036
+ const playContext = new PlayThroughContext();
36037
+ playContext.currentTempo = score.tempo;
36038
+ playContext.syncPoints = syncPoints;
35993
36039
  let previousMasterBar = null;
35994
36040
  // store the previous played bar for repeats
35995
36041
  const barOccurence = new Map();
@@ -36002,23 +36048,11 @@
36002
36048
  let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
36003
36049
  occurence++;
36004
36050
  barOccurence.set(index, occurence);
36005
- generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
36006
- const barSyncPoints = bar.syncPoints;
36007
- if (barSyncPoints) {
36008
- for (const syncPoint of barSyncPoints) {
36009
- if (syncPoint.syncPointValue.barOccurence === occurence) {
36010
- const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
36011
- syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
36012
- }
36013
- }
36014
- }
36015
- if (bar.tempoAutomations.length > 0) {
36016
- currentTempo = bar.tempoAutomations[0].value;
36017
- }
36018
- generateTracks(index, currentTick, currentTempo);
36019
- if (bar.tempoAutomations.length > 0) {
36020
- currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
36021
- }
36051
+ generateMasterBar(bar, previousMasterBar, currentTick, playContext.currentTempo, occurence);
36052
+ const trackTempo = bar.tempoAutomations.length > 0 ? bar.tempoAutomations[0].value : playContext.currentTempo;
36053
+ generateTracks(index, currentTick, trackTempo);
36054
+ playContext.synthTick = currentTick;
36055
+ MidiFileGenerator.processBarTime(bar, occurence, playContext);
36022
36056
  }
36023
36057
  controller.moveNext();
36024
36058
  previousMasterBar = bar;
@@ -36029,21 +36063,119 @@
36029
36063
  // but where it ends according to the BPM and the remaining ticks.
36030
36064
  if (syncPoints.length > 0) {
36031
36065
  const lastSyncPoint = syncPoints[syncPoints.length - 1];
36032
- const remainingTicks = controller.currentTick - lastSyncPoint.tick;
36066
+ const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
36033
36067
  if (remainingTicks > 0) {
36034
- const syncPointData = new SyncPointData();
36035
- // last occurence of the last bar
36036
- syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1);
36037
- // same tempo as last point
36038
- syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
36039
- // interpolated end from last syncPoint
36040
- syncPointData.millisecondOffset =
36041
- lastSyncPoint.data.millisecondOffset +
36042
- MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
36043
- syncPoints.push(new BackingTrackSyncPoint(controller.currentTick, syncPointData));
36068
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36069
+ backingTrackSyncPoint.masterBarIndex = previousMasterBar.index;
36070
+ backingTrackSyncPoint.masterBarOccurence = barOccurence.get(previousMasterBar.index) - 1;
36071
+ backingTrackSyncPoint.synthTick = controller.currentTick;
36072
+ backingTrackSyncPoint.synthBpm = playContext.currentTempo;
36073
+ // we need to assume some BPM for the last interpolated point.
36074
+ // if we have more than just a start point, we keep the BPM before the last manual sync point
36075
+ // otherwise we have no customized sync BPM known and keep the synthesizer one.
36076
+ backingTrackSyncPoint.syncBpm =
36077
+ syncPoints.length > 1 ? syncPoints[syncPoints.length - 2].syncBpm : lastSyncPoint.synthBpm;
36078
+ backingTrackSyncPoint.synthTime =
36079
+ lastSyncPoint.synthTime + MidiUtils.ticksToMillis(remainingTicks, lastSyncPoint.synthBpm);
36080
+ backingTrackSyncPoint.syncTime =
36081
+ lastSyncPoint.syncTime + MidiUtils.ticksToMillis(remainingTicks, backingTrackSyncPoint.syncBpm);
36082
+ // update the previous sync point according to the new time
36083
+ lastSyncPoint.updateSyncBpm(backingTrackSyncPoint.synthTime, backingTrackSyncPoint.syncTime);
36084
+ syncPoints.push(backingTrackSyncPoint);
36044
36085
  }
36045
36086
  }
36046
36087
  finish(controller.currentTick);
36088
+ return playContext;
36089
+ }
36090
+ static processBarTime(bar, occurence, context) {
36091
+ const duration = bar.calculateDuration();
36092
+ const barSyncPoints = bar.syncPoints;
36093
+ const barStartTick = context.synthTick;
36094
+ if (barSyncPoints) {
36095
+ MidiFileGenerator.processBarTimeWithSyncPoints(bar, occurence, context);
36096
+ }
36097
+ else {
36098
+ MidiFileGenerator.processBarTimeNoSyncPoints(bar, context);
36099
+ }
36100
+ // don't forget the part after the last tempo change
36101
+ const endTick = barStartTick + duration;
36102
+ const tickOffset = endTick - context.synthTick;
36103
+ if (tickOffset > 0) {
36104
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36105
+ context.synthTick = endTick;
36106
+ }
36107
+ }
36108
+ static processBarTimeWithSyncPoints(bar, occurence, context) {
36109
+ const barStartTick = context.synthTick;
36110
+ const duration = bar.calculateDuration();
36111
+ let tempoChangeIndex = 0;
36112
+ let tickOffset;
36113
+ for (const syncPoint of bar.syncPoints) {
36114
+ if (syncPoint.syncPointValue.barOccurence !== occurence) {
36115
+ continue;
36116
+ }
36117
+ const syncPointTick = barStartTick + syncPoint.ratioPosition * duration;
36118
+ // first process all tempo changes until this sync point
36119
+ while (tempoChangeIndex < bar.tempoAutomations.length &&
36120
+ bar.tempoAutomations[tempoChangeIndex].ratioPosition <= syncPoint.ratioPosition) {
36121
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36122
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36123
+ tickOffset = absoluteTick - context.synthTick;
36124
+ if (tickOffset > 0) {
36125
+ context.synthTick = absoluteTick;
36126
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36127
+ }
36128
+ context.currentTempo = tempoChange.value;
36129
+ tempoChangeIndex++;
36130
+ }
36131
+ // process time until sync point
36132
+ tickOffset = syncPointTick - context.synthTick;
36133
+ if (tickOffset > 0) {
36134
+ context.synthTick = syncPointTick;
36135
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36136
+ }
36137
+ // update the previous sync point according to the new time
36138
+ if (context.syncPoints.length > 0) {
36139
+ context.syncPoints[context.syncPoints.length - 1].updateSyncBpm(context.synthTime, syncPoint.syncPointValue.millisecondOffset);
36140
+ }
36141
+ // create the new sync point
36142
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36143
+ backingTrackSyncPoint.masterBarIndex = bar.index;
36144
+ backingTrackSyncPoint.masterBarOccurence = occurence;
36145
+ backingTrackSyncPoint.synthTick = syncPointTick;
36146
+ backingTrackSyncPoint.synthBpm = context.currentTempo;
36147
+ backingTrackSyncPoint.synthTime = context.synthTime;
36148
+ backingTrackSyncPoint.syncTime = syncPoint.syncPointValue.millisecondOffset;
36149
+ backingTrackSyncPoint.syncBpm = 0 /* calculated by next sync point */;
36150
+ context.syncPoints.push(backingTrackSyncPoint);
36151
+ context.automationToSyncPoint.set(syncPoint, backingTrackSyncPoint);
36152
+ }
36153
+ // process remaining tempo changes after all sync points
36154
+ while (tempoChangeIndex < bar.tempoAutomations.length) {
36155
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36156
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36157
+ tickOffset = absoluteTick - context.synthTick;
36158
+ if (tickOffset > 0) {
36159
+ context.synthTick = absoluteTick;
36160
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36161
+ }
36162
+ context.currentTempo = tempoChange.value;
36163
+ tempoChangeIndex++;
36164
+ }
36165
+ }
36166
+ static processBarTimeNoSyncPoints(bar, context) {
36167
+ // walk through the tempo changes
36168
+ const barStartTick = context.synthTick;
36169
+ const duration = bar.calculateDuration();
36170
+ for (const changes of bar.tempoAutomations) {
36171
+ const absoluteTick = barStartTick + changes.ratioPosition * duration;
36172
+ const tickOffset = absoluteTick - context.synthTick;
36173
+ if (tickOffset > 0) {
36174
+ context.synthTick = absoluteTick;
36175
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36176
+ }
36177
+ context.currentTempo = changes.value;
36178
+ }
36047
36179
  }
36048
36180
  static toChannelShort(data) {
36049
36181
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
@@ -61006,9 +61138,9 @@
61006
61138
  print(`build date: ${VersionInfo.date}`);
61007
61139
  }
61008
61140
  }
61009
- VersionInfo.version = '1.6.0-alpha.1428';
61010
- VersionInfo.date = '2025-05-29T00:44:50.954Z';
61011
- VersionInfo.commit = 'fa81a14da248f229511867324cee663ea70a72b3';
61141
+ VersionInfo.version = '1.6.0-alpha.1430';
61142
+ VersionInfo.date = '2025-05-29T22:21:21.689Z';
61143
+ VersionInfo.commit = '98a4c2bec8d71f2645008118d3b77fb40973e7fe';
61012
61144
 
61013
61145
  /**
61014
61146
  * A factory for custom layout engines.
@@ -62658,6 +62790,7 @@
62658
62790
  : undefined;
62659
62791
  const millisecondPadding = initialSyncPoint ? initialSyncPoint.syncPointValue.millisecondOffset : 0;
62660
62792
  this.backingTrackFramePadding = (-1 * ((millisecondPadding / 1000) * GpifWriter.SampleRate)) | 0;
62793
+ const modifiedTempoLookup = new Lazy(() => MidiFileGenerator.buildModifiedTempoLookup(score));
62661
62794
  for (const mb of score.masterBars) {
62662
62795
  for (const automation of mb.tempoAutomations) {
62663
62796
  const tempoAutomation = automations.addElement('Automation');
@@ -62682,7 +62815,7 @@
62682
62815
  const value = syncPointAutomation.addElement('Value');
62683
62816
  value.addElement('BarIndex').innerText = mb.index.toString();
62684
62817
  value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue.barOccurence.toString();
62685
- value.addElement('ModifiedTempo').innerText = syncPoint.syncPointValue.modifiedTempo.toString();
62818
+ value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint).syncBpm.toString();
62686
62819
  value.addElement('OriginalTempo').innerText = score.tempo.toString();
62687
62820
  const frameOffset = (((syncPoint.syncPointValue.millisecondOffset - millisecondPadding) / 1000) *
62688
62821
  GpifWriter.SampleRate) |