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

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.1432 (develop, build 1432)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -1012,6 +1012,26 @@
1012
1012
  get isEmpty() {
1013
1013
  return this._isEmpty;
1014
1014
  }
1015
+ /**
1016
+ * Whether this bar has any changes applied which are not related to the voices in it.
1017
+ * (e.g. new key signatures)
1018
+ */
1019
+ get hasChanges() {
1020
+ if (this.index === 0) {
1021
+ return true;
1022
+ }
1023
+ const hasChangesToPrevious = this.keySignature !== this.previousBar.keySignature ||
1024
+ this.keySignatureType !== this.previousBar.keySignatureType ||
1025
+ this.clef !== this.previousBar.clef ||
1026
+ this.clefOttava !== this.previousBar.clefOttava;
1027
+ if (hasChangesToPrevious) {
1028
+ return true;
1029
+ }
1030
+ return (this.simileMark !== SimileMark.None ||
1031
+ this.sustainPedals.length > 0 ||
1032
+ this.barLineLeft !== BarLineStyle.Automatic ||
1033
+ this.barLineRight !== BarLineStyle.Automatic);
1034
+ }
1015
1035
  /**
1016
1036
  * Whether this bar is empty or has only rests.
1017
1037
  */
@@ -1417,12 +1437,6 @@
1417
1437
  * 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
1438
  */
1419
1439
  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
1440
  /**
1427
1441
  * The audio offset marking the position within the audio track in milliseconds.
1428
1442
  * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
@@ -2631,6 +2645,33 @@
2631
2645
  */
2632
2646
  this.directions = null;
2633
2647
  }
2648
+ /**
2649
+ * Whether the masterbar is has any changes applied to it (e.g. tempo changes, time signature changes etc)
2650
+ * The first bar is always considered changed due to initial setup of values. It does not consider
2651
+ * elements like whether the tempo really changes to the previous bar.
2652
+ */
2653
+ get hasChanges() {
2654
+ if (this.index === 0) {
2655
+ return false;
2656
+ }
2657
+ const hasChangesToPrevious = this.timeSignatureCommon !== this.previousMasterBar.timeSignatureCommon ||
2658
+ this.timeSignatureNumerator !== this.previousMasterBar.timeSignatureNumerator ||
2659
+ this.timeSignatureDenominator !== this.previousMasterBar.timeSignatureDenominator ||
2660
+ this.tripletFeel !== this.previousMasterBar.tripletFeel;
2661
+ if (hasChangesToPrevious) {
2662
+ return true;
2663
+ }
2664
+ return (this.alternateEndings !== 0 ||
2665
+ this.isRepeatStart ||
2666
+ this.isRepeatEnd ||
2667
+ this.isFreeTime ||
2668
+ this.isSectionStart ||
2669
+ this.tempoAutomations.length > 0 ||
2670
+ this.syncPoints && this.syncPoints.length > 0 ||
2671
+ (this.fermata !== null && this.fermata.size > 0) ||
2672
+ (this.directions !== null && this.directions.size > 0) ||
2673
+ this.isAnacrusis);
2674
+ }
2634
2675
  /**
2635
2676
  * The key signature used on all bars.
2636
2677
  * @deprecated Use key signatures on bar level
@@ -3572,6 +3613,43 @@
3572
3613
  }
3573
3614
  }
3574
3615
  }
3616
+ /**
3617
+ * Trims any empty bars at the end of the song.
3618
+ * @param score
3619
+ */
3620
+ static trimEmptyBarsAtEnd(score) {
3621
+ while (score.masterBars.length > 1) {
3622
+ const barIndex = score.masterBars.length - 1;
3623
+ const masterBar = score.masterBars[barIndex];
3624
+ if (masterBar.hasChanges) {
3625
+ return;
3626
+ }
3627
+ for (const track of score.tracks) {
3628
+ for (const staff of track.staves) {
3629
+ if (barIndex < staff.bars.length) {
3630
+ const bar = staff.bars[barIndex];
3631
+ if (!bar.isEmpty || bar.hasChanges) {
3632
+ // found a non-empty bar, stop whole cleanup
3633
+ return;
3634
+ }
3635
+ }
3636
+ }
3637
+ }
3638
+ // if we reach here, all found bars are empty, remove the bar
3639
+ for (const track of score.tracks) {
3640
+ for (const staff of track.staves) {
3641
+ if (barIndex < staff.bars.length) {
3642
+ const bar = staff.bars[barIndex];
3643
+ staff.bars.pop();
3644
+ // unlink
3645
+ bar.previousBar.nextBar = null;
3646
+ }
3647
+ }
3648
+ }
3649
+ score.masterBars.pop();
3650
+ masterBar.previousMasterBar.nextMasterBar = null;
3651
+ }
3652
+ }
3575
3653
  }
3576
3654
  ModelUtils.TuningLetters = new Set([
3577
3655
  0x43 /* C */, 0x44 /* D */, 0x45 /* E */, 0x46 /* F */, 0x47 /* G */, 0x41 /* A */, 0x42 /* B */, 0x63 /* c */,
@@ -5849,7 +5927,6 @@
5849
5927
  static clone(original) {
5850
5928
  const clone = new SyncPointData();
5851
5929
  clone.barOccurence = original.barOccurence;
5852
- clone.modifiedTempo = original.modifiedTempo;
5853
5930
  clone.millisecondOffset = original.millisecondOffset;
5854
5931
  return clone;
5855
5932
  }
@@ -7319,7 +7396,6 @@
7319
7396
  automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
7320
7397
  automation.type = AutomationType.SyncPoint;
7321
7398
  automation.syncPointValue = new SyncPointData();
7322
- automation.syncPointValue.modifiedTempo = syncPoint.modifiedTempo;
7323
7399
  automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
7324
7400
  automation.syncPointValue.barOccurence = syncPoint.barOccurence;
7325
7401
  if (syncPoint.barIndex < this.masterBars.length) {
@@ -7352,8 +7428,7 @@
7352
7428
  barIndex: masterBar.index,
7353
7429
  barOccurence: syncPoint.syncPointValue.barOccurence,
7354
7430
  barPosition: syncPoint.ratioPosition,
7355
- millisecondOffset: syncPoint.syncPointValue.millisecondOffset,
7356
- modifiedTempo: syncPoint.syncPointValue.modifiedTempo
7431
+ millisecondOffset: syncPoint.syncPointValue.millisecondOffset
7357
7432
  });
7358
7433
  }
7359
7434
  }
@@ -8692,6 +8767,7 @@
8692
8767
  this._slurs = new Map();
8693
8768
  this._articulationValueToIndex = new Map();
8694
8769
  this._accidentalMode = AlphaTexAccidentalMode.Explicit;
8770
+ this._syncPoints = [];
8695
8771
  this.logErrors = false;
8696
8772
  }
8697
8773
  get name() {
@@ -8732,10 +8808,16 @@
8732
8808
  if (!anyMetaRead && !anyBarsRead) {
8733
8809
  throw new UnsupportedFormatError('No alphaTex data found');
8734
8810
  }
8811
+ if (this._sy === AlphaTexSymbols.Dot) {
8812
+ this._sy = this.newSy();
8813
+ this.syncPoints();
8814
+ }
8735
8815
  }
8736
8816
  ModelUtils.consolidate(this._score);
8737
8817
  this._score.finish(this.settings);
8818
+ ModelUtils.trimEmptyBarsAtEnd(this._score);
8738
8819
  this._score.rebuildRepeatGroups();
8820
+ this._score.applyFlatSyncPoints(this._syncPoints);
8739
8821
  for (const [track, lyrics] of this._lyrics) {
8740
8822
  this._score.tracks[track].applyLyrics(lyrics);
8741
8823
  }
@@ -8752,6 +8834,47 @@
8752
8834
  throw e;
8753
8835
  }
8754
8836
  }
8837
+ syncPoints() {
8838
+ while (this._sy !== AlphaTexSymbols.Eof) {
8839
+ this.syncPoint();
8840
+ }
8841
+ }
8842
+ syncPoint() {
8843
+ // \sync BarIndex Occurence MillisecondOffset
8844
+ // \sync BarIndex Occurence MillisecondOffset RatioPosition
8845
+ if (this._sy !== AlphaTexSymbols.MetaCommand || this._syData !== 'sync') {
8846
+ this.error('syncPoint', AlphaTexSymbols.MetaCommand, true);
8847
+ }
8848
+ this._sy = this.newSy();
8849
+ if (this._sy !== AlphaTexSymbols.Number) {
8850
+ this.error('syncPointBarIndex', AlphaTexSymbols.Number, true);
8851
+ }
8852
+ const barIndex = this._syData;
8853
+ this._sy = this.newSy();
8854
+ if (this._sy !== AlphaTexSymbols.Number) {
8855
+ this.error('syncPointBarOccurence', AlphaTexSymbols.Number, true);
8856
+ }
8857
+ const barOccurence = this._syData;
8858
+ this._sy = this.newSy();
8859
+ if (this._sy !== AlphaTexSymbols.Number) {
8860
+ this.error('syncPointBarMillis', AlphaTexSymbols.Number, true);
8861
+ }
8862
+ const millisecondOffset = this._syData;
8863
+ this._allowFloat = true;
8864
+ this._sy = this.newSy();
8865
+ this._allowFloat = false;
8866
+ let barPosition = 0;
8867
+ if (this._sy === AlphaTexSymbols.Number) {
8868
+ barPosition = this._syData;
8869
+ this._sy = this.newSy();
8870
+ }
8871
+ this._syncPoints.push({
8872
+ barIndex,
8873
+ barOccurence,
8874
+ barPosition,
8875
+ millisecondOffset
8876
+ });
8877
+ }
8755
8878
  error(nonterm, expected, wrongSymbol = true) {
8756
8879
  let receivedSymbol;
8757
8880
  let showSyData = false;
@@ -14720,9 +14843,6 @@
14720
14843
  case 'BarOccurrence':
14721
14844
  syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
14722
14845
  break;
14723
- case 'ModifiedTempo':
14724
- syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
14725
- break;
14726
14846
  case 'FrameOffset':
14727
14847
  const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
14728
14848
  syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
@@ -22345,6 +22465,61 @@
22345
22465
  }
22346
22466
  }
22347
22467
 
22468
+ /**
22469
+ * Rerpresents a point to sync the alphaTab time axis with an external backing track.
22470
+ */
22471
+ class BackingTrackSyncPoint {
22472
+ constructor() {
22473
+ /**
22474
+ * The index of the masterbar to which this sync point belongs to.
22475
+ * @remarks
22476
+ * This property is purely informative for external use like in editors.
22477
+ * It has no impact to the synchronization itself.
22478
+ */
22479
+ this.masterBarIndex = 0;
22480
+ /**
22481
+ * The occurence of the masterbar to which this sync point belongs to. The occurence
22482
+ * is 0-based and increases with every repeated play of a masterbar (e.g. on repeats or jumps).
22483
+ * @remarks
22484
+ * This property is purely informative for external use like in editors.
22485
+ * It has no impact to the synchronization itself.
22486
+ */
22487
+ this.masterBarOccurence = 0;
22488
+ /**
22489
+ * The BPM the synthesizer has at the exact tick position of this sync point.
22490
+ */
22491
+ this.synthBpm = 0;
22492
+ /**
22493
+ * The millisecond time position of the synthesizer when this sync point is reached.
22494
+ */
22495
+ this.synthTime = 0;
22496
+ /**
22497
+ * The midi tick position of the synthesizer when this sync point is reached.
22498
+ */
22499
+ this.synthTick = 0;
22500
+ /**
22501
+ * The millisecond time in the external media marking the synchronization point.
22502
+ */
22503
+ this.syncTime = 0;
22504
+ /**
22505
+ * The BPM the song will have virtually after this sync point to align the external media time axis
22506
+ * with the one from the synthesizer.
22507
+ */
22508
+ this.syncBpm = 0;
22509
+ }
22510
+ /**
22511
+ * Updates the synchronization BPM that will apply after this sync point.
22512
+ * @param nextSyncPointSynthTime The synthesizer time of the next sync point after this one.
22513
+ * @param nextSyncPointSyncTime The synchronization time of the next sync point after this one.
22514
+ */
22515
+ updateSyncBpm(nextSyncPointSynthTime, nextSyncPointSyncTime) {
22516
+ const synthDuration = nextSyncPointSynthTime - this.synthTime;
22517
+ const syncedDuration = nextSyncPointSyncTime - this.syncTime;
22518
+ const modifiedTempo = (synthDuration / syncedDuration) * this.synthBpm;
22519
+ this.syncBpm = modifiedTempo;
22520
+ }
22521
+ }
22522
+
22348
22523
  class MidiFileSequencerTempoChange {
22349
22524
  constructor(bpm, ticks, time) {
22350
22525
  this.bpm = bpm;
@@ -22352,14 +22527,6 @@
22352
22527
  this.time = time;
22353
22528
  }
22354
22529
  }
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
22530
  class MidiSequencerState {
22364
22531
  constructor() {
22365
22532
  this.tempoChanges = [];
@@ -22463,7 +22630,7 @@
22463
22630
  this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22464
22631
  this._mainState.modifiedTempo =
22465
22632
  this._mainState.syncPoints.length > 0
22466
- ? this._mainState.syncPoints[0].modifiedTempo
22633
+ ? this._mainState.syncPoints[0].syncBpm
22467
22634
  : this._mainState.currentTempo;
22468
22635
  if (this.isPlayingMain) {
22469
22636
  const metronomeVolume = this._synthesizer.metronomeVolume;
@@ -22634,7 +22801,7 @@
22634
22801
  }
22635
22802
  mainUpdateSyncPoints(syncPoints) {
22636
22803
  const state = this._mainState;
22637
- syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22804
+ syncPoints.sort((a, b) => a.synthTick - b.synthTick); // just in case
22638
22805
  state.syncPoints = [];
22639
22806
  if (syncPoints.length >= 0) {
22640
22807
  let bpm = 120;
@@ -22644,6 +22811,8 @@
22644
22811
  for (let i = 0; i < syncPoints.length; i++) {
22645
22812
  const p = syncPoints[i];
22646
22813
  let deltaTick = 0;
22814
+ // TODO: merge interpolation into MidiFileGenerator where we already play through
22815
+ // the time axis.
22647
22816
  // remember state from previous sync point (or start). to handle linear interpolation
22648
22817
  let previousModifiedTempo;
22649
22818
  let previousMillisecondOffset;
@@ -22655,9 +22824,9 @@
22655
22824
  }
22656
22825
  else {
22657
22826
  const previousSyncPoint = syncPoints[i - 1];
22658
- previousModifiedTempo = previousSyncPoint.data.modifiedTempo;
22659
- previousMillisecondOffset = previousSyncPoint.data.millisecondOffset;
22660
- previousTick = previousSyncPoint.tick;
22827
+ previousModifiedTempo = previousSyncPoint.syncBpm;
22828
+ previousMillisecondOffset = previousSyncPoint.syncTime;
22829
+ previousTick = previousSyncPoint.synthTick;
22661
22830
  }
22662
22831
  // process time until sync point
22663
22832
  // here it gets a bit tricky. if we have tempo changes on the synthesizer time axis (inbetween two sync points)
@@ -22665,27 +22834,31 @@
22665
22834
  // otherwise the linear interpolation later in the lookup will fail.
22666
22835
  // 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
22836
  while (tempoChangeIndex < state.tempoChanges.length &&
22668
- state.tempoChanges[tempoChangeIndex].ticks <= p.tick) {
22837
+ state.tempoChanges[tempoChangeIndex].ticks <= p.synthTick) {
22669
22838
  deltaTick = state.tempoChanges[tempoChangeIndex].ticks - absTick;
22670
22839
  if (deltaTick > 0) {
22671
22840
  absTick += deltaTick;
22672
22841
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22673
- const millisPerTick = (p.data.millisecondOffset - previousMillisecondOffset) / (p.tick - previousTick);
22842
+ const millisPerTick = (p.syncTime - previousMillisecondOffset) / (p.synthTick - previousTick);
22674
22843
  const interpolatedMillisecondOffset = (absTick - previousTick) * millisPerTick + previousMillisecondOffset;
22675
- state.syncPoints.push(new BackingTrackSyncPointWithTime(absTick, absTime, previousModifiedTempo, interpolatedMillisecondOffset));
22844
+ const syncPoint = new BackingTrackSyncPoint();
22845
+ syncPoint.synthTick = absTick;
22846
+ syncPoint.synthBpm = bpm;
22847
+ syncPoint.synthTime = absTime;
22848
+ syncPoint.syncTime = interpolatedMillisecondOffset;
22849
+ syncPoint.syncBpm = previousModifiedTempo;
22676
22850
  }
22677
22851
  bpm = state.tempoChanges[tempoChangeIndex].bpm;
22678
22852
  tempoChangeIndex++;
22679
22853
  }
22680
- deltaTick = p.tick - absTick;
22854
+ deltaTick = p.synthTick - absTick;
22681
22855
  absTick += deltaTick;
22682
22856
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22683
- state.syncPoints.push(new BackingTrackSyncPointWithTime(p.tick, absTime, p.data.modifiedTempo, p.data.millisecondOffset));
22857
+ state.syncPoints.push(p);
22684
22858
  }
22685
22859
  }
22686
22860
  state.syncPointIndex = 0;
22687
- state.modifiedTempo =
22688
- state.syncPoints.length > 0 ? state.syncPoints[0].modifiedTempo : state.currentTempo;
22861
+ state.modifiedTempo = state.syncPoints.length > 0 ? state.syncPoints[0].syncBpm : state.currentTempo;
22689
22862
  }
22690
22863
  currentTimePositionToTickPosition(timePosition) {
22691
22864
  const state = this._currentState;
@@ -22718,16 +22891,15 @@
22718
22891
  const syncPoints = state.syncPoints;
22719
22892
  if (syncPoints.length > 0) {
22720
22893
  let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22721
- if (timePosition < syncPoints[syncPointIndex].millisecondOffset) {
22894
+ if (timePosition < syncPoints[syncPointIndex].syncTime) {
22722
22895
  syncPointIndex = 0;
22723
22896
  }
22724
- while (syncPointIndex + 1 < syncPoints.length &&
22725
- syncPoints[syncPointIndex + 1].millisecondOffset <= timePosition) {
22897
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].syncTime <= timePosition) {
22726
22898
  syncPointIndex++;
22727
22899
  }
22728
22900
  if (syncPointIndex !== state.syncPointIndex) {
22729
22901
  state.syncPointIndex = syncPointIndex;
22730
- state.modifiedTempo = syncPoints[syncPointIndex].modifiedTempo;
22902
+ state.modifiedTempo = syncPoints[syncPointIndex].syncBpm;
22731
22903
  }
22732
22904
  }
22733
22905
  else {
@@ -22743,18 +22915,18 @@
22743
22915
  this.updateSyncPoints(this._mainState, timePosition);
22744
22916
  const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22745
22917
  const currentSyncPoint = syncPoints[syncPointIndex];
22746
- const timeDiff = timePosition - currentSyncPoint.millisecondOffset;
22918
+ const timeDiff = timePosition - currentSyncPoint.syncTime;
22747
22919
  let alphaTabTimeDiff;
22748
22920
  if (syncPointIndex + 1 < syncPoints.length) {
22749
22921
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22750
- const relativeTimeDiff = timeDiff / (nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset);
22751
- alphaTabTimeDiff = (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22922
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.syncTime - currentSyncPoint.syncTime);
22923
+ alphaTabTimeDiff = (nextSyncPoint.synthTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22752
22924
  }
22753
22925
  else {
22754
- const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.millisecondOffset);
22755
- alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22926
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.syncTime);
22927
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22756
22928
  }
22757
- return (currentSyncPoint.alphaTabTime + alphaTabTimeDiff) / this.playbackSpeed;
22929
+ return (currentSyncPoint.synthTime + alphaTabTimeDiff) / this.playbackSpeed;
22758
22930
  }
22759
22931
  mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22760
22932
  const mainState = this._mainState;
@@ -22764,27 +22936,27 @@
22764
22936
  }
22765
22937
  timePosition *= this.playbackSpeed;
22766
22938
  let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22767
- if (timePosition < syncPoints[syncPointIndex].alphaTabTime) {
22939
+ if (timePosition < syncPoints[syncPointIndex].synthTime) {
22768
22940
  syncPointIndex = 0;
22769
22941
  }
22770
- while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].alphaTabTime <= timePosition) {
22942
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].synthTime <= timePosition) {
22771
22943
  syncPointIndex++;
22772
22944
  }
22773
22945
  // NOTE: this logic heavily relies on the interpolation done in mainUpdateSyncPoints
22774
22946
  // we ensure that we have a linear increase between two points
22775
22947
  const currentSyncPoint = syncPoints[syncPointIndex];
22776
- const alphaTabTimeDiff = timePosition - currentSyncPoint.alphaTabTime;
22948
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.synthTime;
22777
22949
  let backingTrackPos;
22778
22950
  if (syncPointIndex + 1 < syncPoints.length) {
22779
22951
  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;
22952
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.synthTime - currentSyncPoint.synthTime);
22953
+ const backingTrackDiff = nextSyncPoint.syncTime - currentSyncPoint.syncTime;
22954
+ backingTrackPos = currentSyncPoint.syncTime + backingTrackDiff * relativeAlphaTabTimeDiff;
22783
22955
  }
22784
22956
  else {
22785
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.alphaTabTime);
22786
- const frameDiff = backingTrackLength - currentSyncPoint.millisecondOffset;
22787
- backingTrackPos = currentSyncPoint.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22957
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.synthTime);
22958
+ const frameDiff = backingTrackLength - currentSyncPoint.syncTime;
22959
+ backingTrackPos = currentSyncPoint.syncTime + frameDiff * relativeAlphaTabTimeDiff;
22788
22960
  }
22789
22961
  return backingTrackPos;
22790
22962
  }
@@ -31056,7 +31228,6 @@
31056
31228
  }
31057
31229
  const o = new Map();
31058
31230
  o.set("baroccurence", obj.barOccurence);
31059
- o.set("modifiedtempo", obj.modifiedTempo);
31060
31231
  o.set("millisecondoffset", obj.millisecondOffset);
31061
31232
  return o;
31062
31233
  }
@@ -31065,9 +31236,6 @@
31065
31236
  case "baroccurence":
31066
31237
  obj.barOccurence = v;
31067
31238
  return true;
31068
- case "modifiedtempo":
31069
- obj.modifiedTempo = v;
31070
- return true;
31071
31239
  case "millisecondoffset":
31072
31240
  obj.millisecondOffset = v;
31073
31241
  return true;
@@ -33072,7 +33240,7 @@
33072
33240
  addSamples(samples) {
33073
33241
  this._worker.postMessage({
33074
33242
  cmd: 'alphaSynth.output.addSamples',
33075
- samples: samples
33243
+ samples: Environment.prepareForPostMessage(samples)
33076
33244
  });
33077
33245
  }
33078
33246
  play() {
@@ -33268,7 +33436,7 @@
33268
33436
  onSoundFontLoadFailed(e) {
33269
33437
  this._main.postMessage({
33270
33438
  cmd: 'alphaSynth.soundFontLoadFailed',
33271
- error: this.serializeException(e)
33439
+ error: this.serializeException(Environment.prepareForPostMessage(e))
33272
33440
  });
33273
33441
  }
33274
33442
  serializeException(e) {
@@ -33299,7 +33467,7 @@
33299
33467
  onMidiLoadFailed(e) {
33300
33468
  this._main.postMessage({
33301
33469
  cmd: 'alphaSynth.midiLoaded',
33302
- error: this.serializeException(e)
33470
+ error: this.serializeException(Environment.prepareForPostMessage(e))
33303
33471
  });
33304
33472
  }
33305
33473
  onReadyForPlayback() {
@@ -35830,17 +35998,6 @@
35830
35998
  }
35831
35999
  }
35832
36000
 
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
36001
  class MidiNoteDuration {
35845
36002
  constructor() {
35846
36003
  this.noteOnly = 0;
@@ -35861,6 +36018,14 @@
35861
36018
  this.brushInfos = [];
35862
36019
  }
35863
36020
  }
36021
+ class PlayThroughContext {
36022
+ constructor() {
36023
+ this.synthTick = 0;
36024
+ this.synthTime = 0;
36025
+ this.currentTempo = 0;
36026
+ this.automationToSyncPoint = new Map();
36027
+ }
36028
+ }
35864
36029
  /**
35865
36030
  * This generator creates a midi file using a score.
35866
36031
  */
@@ -35987,9 +36152,22 @@
35987
36152
  });
35988
36153
  return syncPoints;
35989
36154
  }
36155
+ /**
36156
+ * @internal
36157
+ */
36158
+ static buildModifiedTempoLookup(score) {
36159
+ const syncPoints = [];
36160
+ const context = MidiFileGenerator.playThroughSong(score, syncPoints, (_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
36161
+ }, (_barIndex, _currentTick, _currentTempo) => {
36162
+ }, _endTick => {
36163
+ });
36164
+ return context.automationToSyncPoint;
36165
+ }
35990
36166
  static playThroughSong(score, syncPoints, generateMasterBar, generateTracks, finish) {
35991
36167
  const controller = new MidiPlaybackController(score);
35992
- let currentTempo = score.tempo;
36168
+ const playContext = new PlayThroughContext();
36169
+ playContext.currentTempo = score.tempo;
36170
+ playContext.syncPoints = syncPoints;
35993
36171
  let previousMasterBar = null;
35994
36172
  // store the previous played bar for repeats
35995
36173
  const barOccurence = new Map();
@@ -36002,23 +36180,11 @@
36002
36180
  let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
36003
36181
  occurence++;
36004
36182
  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
- }
36183
+ generateMasterBar(bar, previousMasterBar, currentTick, playContext.currentTempo, occurence);
36184
+ const trackTempo = bar.tempoAutomations.length > 0 ? bar.tempoAutomations[0].value : playContext.currentTempo;
36185
+ generateTracks(index, currentTick, trackTempo);
36186
+ playContext.synthTick = currentTick;
36187
+ MidiFileGenerator.processBarTime(bar, occurence, playContext);
36022
36188
  }
36023
36189
  controller.moveNext();
36024
36190
  previousMasterBar = bar;
@@ -36029,21 +36195,119 @@
36029
36195
  // but where it ends according to the BPM and the remaining ticks.
36030
36196
  if (syncPoints.length > 0) {
36031
36197
  const lastSyncPoint = syncPoints[syncPoints.length - 1];
36032
- const remainingTicks = controller.currentTick - lastSyncPoint.tick;
36198
+ const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
36033
36199
  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));
36200
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36201
+ backingTrackSyncPoint.masterBarIndex = previousMasterBar.index;
36202
+ backingTrackSyncPoint.masterBarOccurence = barOccurence.get(previousMasterBar.index) - 1;
36203
+ backingTrackSyncPoint.synthTick = controller.currentTick;
36204
+ backingTrackSyncPoint.synthBpm = playContext.currentTempo;
36205
+ // we need to assume some BPM for the last interpolated point.
36206
+ // if we have more than just a start point, we keep the BPM before the last manual sync point
36207
+ // otherwise we have no customized sync BPM known and keep the synthesizer one.
36208
+ backingTrackSyncPoint.syncBpm =
36209
+ syncPoints.length > 1 ? syncPoints[syncPoints.length - 2].syncBpm : lastSyncPoint.synthBpm;
36210
+ backingTrackSyncPoint.synthTime =
36211
+ lastSyncPoint.synthTime + MidiUtils.ticksToMillis(remainingTicks, lastSyncPoint.synthBpm);
36212
+ backingTrackSyncPoint.syncTime =
36213
+ lastSyncPoint.syncTime + MidiUtils.ticksToMillis(remainingTicks, backingTrackSyncPoint.syncBpm);
36214
+ // update the previous sync point according to the new time
36215
+ lastSyncPoint.updateSyncBpm(backingTrackSyncPoint.synthTime, backingTrackSyncPoint.syncTime);
36216
+ syncPoints.push(backingTrackSyncPoint);
36044
36217
  }
36045
36218
  }
36046
36219
  finish(controller.currentTick);
36220
+ return playContext;
36221
+ }
36222
+ static processBarTime(bar, occurence, context) {
36223
+ const duration = bar.calculateDuration();
36224
+ const barSyncPoints = bar.syncPoints;
36225
+ const barStartTick = context.synthTick;
36226
+ if (barSyncPoints) {
36227
+ MidiFileGenerator.processBarTimeWithSyncPoints(bar, occurence, context);
36228
+ }
36229
+ else {
36230
+ MidiFileGenerator.processBarTimeNoSyncPoints(bar, context);
36231
+ }
36232
+ // don't forget the part after the last tempo change
36233
+ const endTick = barStartTick + duration;
36234
+ const tickOffset = endTick - context.synthTick;
36235
+ if (tickOffset > 0) {
36236
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36237
+ context.synthTick = endTick;
36238
+ }
36239
+ }
36240
+ static processBarTimeWithSyncPoints(bar, occurence, context) {
36241
+ const barStartTick = context.synthTick;
36242
+ const duration = bar.calculateDuration();
36243
+ let tempoChangeIndex = 0;
36244
+ let tickOffset;
36245
+ for (const syncPoint of bar.syncPoints) {
36246
+ if (syncPoint.syncPointValue.barOccurence !== occurence) {
36247
+ continue;
36248
+ }
36249
+ const syncPointTick = barStartTick + syncPoint.ratioPosition * duration;
36250
+ // first process all tempo changes until this sync point
36251
+ while (tempoChangeIndex < bar.tempoAutomations.length &&
36252
+ bar.tempoAutomations[tempoChangeIndex].ratioPosition <= syncPoint.ratioPosition) {
36253
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36254
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36255
+ tickOffset = absoluteTick - context.synthTick;
36256
+ if (tickOffset > 0) {
36257
+ context.synthTick = absoluteTick;
36258
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36259
+ }
36260
+ context.currentTempo = tempoChange.value;
36261
+ tempoChangeIndex++;
36262
+ }
36263
+ // process time until sync point
36264
+ tickOffset = syncPointTick - context.synthTick;
36265
+ if (tickOffset > 0) {
36266
+ context.synthTick = syncPointTick;
36267
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36268
+ }
36269
+ // update the previous sync point according to the new time
36270
+ if (context.syncPoints.length > 0) {
36271
+ context.syncPoints[context.syncPoints.length - 1].updateSyncBpm(context.synthTime, syncPoint.syncPointValue.millisecondOffset);
36272
+ }
36273
+ // create the new sync point
36274
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36275
+ backingTrackSyncPoint.masterBarIndex = bar.index;
36276
+ backingTrackSyncPoint.masterBarOccurence = occurence;
36277
+ backingTrackSyncPoint.synthTick = syncPointTick;
36278
+ backingTrackSyncPoint.synthBpm = context.currentTempo;
36279
+ backingTrackSyncPoint.synthTime = context.synthTime;
36280
+ backingTrackSyncPoint.syncTime = syncPoint.syncPointValue.millisecondOffset;
36281
+ backingTrackSyncPoint.syncBpm = 0 /* calculated by next sync point */;
36282
+ context.syncPoints.push(backingTrackSyncPoint);
36283
+ context.automationToSyncPoint.set(syncPoint, backingTrackSyncPoint);
36284
+ }
36285
+ // process remaining tempo changes after all sync points
36286
+ while (tempoChangeIndex < bar.tempoAutomations.length) {
36287
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36288
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36289
+ tickOffset = absoluteTick - context.synthTick;
36290
+ if (tickOffset > 0) {
36291
+ context.synthTick = absoluteTick;
36292
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36293
+ }
36294
+ context.currentTempo = tempoChange.value;
36295
+ tempoChangeIndex++;
36296
+ }
36297
+ }
36298
+ static processBarTimeNoSyncPoints(bar, context) {
36299
+ // walk through the tempo changes
36300
+ const barStartTick = context.synthTick;
36301
+ const duration = bar.calculateDuration();
36302
+ for (const changes of bar.tempoAutomations) {
36303
+ const absoluteTick = barStartTick + changes.ratioPosition * duration;
36304
+ const tickOffset = absoluteTick - context.synthTick;
36305
+ if (tickOffset > 0) {
36306
+ context.synthTick = absoluteTick;
36307
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36308
+ }
36309
+ context.currentTempo = changes.value;
36310
+ }
36047
36311
  }
36048
36312
  static toChannelShort(data) {
36049
36313
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
@@ -42328,6 +42592,18 @@
42328
42592
  * available importers
42329
42593
  */
42330
42594
  class ScoreLoader {
42595
+ /**
42596
+ * Loads the given alphaTex string.
42597
+ * @param tex The alphaTex string.
42598
+ * @param settings The settings to use for parsing.
42599
+ * @returns The parsed {@see Score}.
42600
+ */
42601
+ static loadAlphaTex(tex, settings) {
42602
+ const parser = new AlphaTexImporter();
42603
+ parser.logErrors = true;
42604
+ parser.initFromString(tex, settings ?? new Settings());
42605
+ return parser.readScore();
42606
+ }
42331
42607
  /**
42332
42608
  * Loads a score asynchronously from the given datasource
42333
42609
  * @param path the source path to load the binary file from
@@ -43178,7 +43454,7 @@
43178
43454
  this._midiEventsPlayedFilter = value;
43179
43455
  this._synth.postMessage({
43180
43456
  cmd: 'alphaSynth.setMidiEventsPlayedFilter',
43181
- value: value
43457
+ value: Environment.prepareForPostMessage(value)
43182
43458
  });
43183
43459
  }
43184
43460
  get playbackSpeed() {
@@ -43243,7 +43519,7 @@
43243
43519
  this._playbackRange = value;
43244
43520
  this._synth.postMessage({
43245
43521
  cmd: 'alphaSynth.setPlaybackRange',
43246
- value: value
43522
+ value: Environment.prepareForPostMessage(value)
43247
43523
  });
43248
43524
  }
43249
43525
  constructor(player, settings) {
@@ -43337,13 +43613,13 @@
43337
43613
  playOneTimeMidiFile(midi) {
43338
43614
  this._synth.postMessage({
43339
43615
  cmd: 'alphaSynth.playOneTimeMidiFile',
43340
- midi: JsonConverter.midiFileToJsObject(midi)
43616
+ midi: JsonConverter.midiFileToJsObject(Environment.prepareForPostMessage(midi))
43341
43617
  });
43342
43618
  }
43343
43619
  loadSoundFont(data, append) {
43344
43620
  this._synth.postMessage({
43345
43621
  cmd: 'alphaSynth.loadSoundFontBytes',
43346
- data: data,
43622
+ data: Environment.prepareForPostMessage(data),
43347
43623
  append: append
43348
43624
  });
43349
43625
  }
@@ -43355,13 +43631,13 @@
43355
43631
  loadMidiFile(midi) {
43356
43632
  this._synth.postMessage({
43357
43633
  cmd: 'alphaSynth.loadMidi',
43358
- midi: JsonConverter.midiFileToJsObject(midi)
43634
+ midi: JsonConverter.midiFileToJsObject(Environment.prepareForPostMessage(midi))
43359
43635
  });
43360
43636
  }
43361
43637
  applyTranspositionPitches(transpositionPitches) {
43362
43638
  this._synth.postMessage({
43363
43639
  cmd: 'alphaSynth.applyTranspositionPitches',
43364
- transpositionPitches: JSON.stringify(Array.from(transpositionPitches.entries()))
43640
+ transpositionPitches: JSON.stringify(Array.from(Environment.prepareForPostMessage(transpositionPitches).entries()))
43365
43641
  });
43366
43642
  }
43367
43643
  setChannelTranspositionPitch(channel, semitones) {
@@ -43533,7 +43809,7 @@
43533
43809
  });
43534
43810
  }
43535
43811
  serializeSettingsForWorker(settings) {
43536
- const jsObject = JsonConverter.settingsToJsObject(settings);
43812
+ const jsObject = JsonConverter.settingsToJsObject(Environment.prepareForPostMessage(settings));
43537
43813
  // cut out player settings, they are only needed on UI thread side
43538
43814
  jsObject.delete('player');
43539
43815
  return jsObject;
@@ -43591,11 +43867,11 @@
43591
43867
  }
43592
43868
  }
43593
43869
  renderScore(score, trackIndexes) {
43594
- const jsObject = score == null ? null : JsonConverter.scoreToJsObject(score);
43870
+ const jsObject = score == null ? null : JsonConverter.scoreToJsObject(Environment.prepareForPostMessage(score));
43595
43871
  this._worker.postMessage({
43596
43872
  cmd: 'alphaTab.renderScore',
43597
43873
  score: jsObject,
43598
- trackIndexes: trackIndexes,
43874
+ trackIndexes: Environment.prepareForPostMessage(trackIndexes),
43599
43875
  fontSizes: FontSizes.FontSizeLookupTables
43600
43876
  });
43601
43877
  }
@@ -43786,7 +44062,7 @@
43786
44062
  addSamples(f) {
43787
44063
  this._worklet?.port.postMessage({
43788
44064
  cmd: AlphaSynthWorkerSynthOutput.CmdOutputAddSamples,
43789
- samples: f
44065
+ samples: Environment.prepareForPostMessage(f)
43790
44066
  });
43791
44067
  }
43792
44068
  resetSamples() {
@@ -61006,9 +61282,9 @@
61006
61282
  print(`build date: ${VersionInfo.date}`);
61007
61283
  }
61008
61284
  }
61009
- VersionInfo.version = '1.6.0-alpha.1428';
61010
- VersionInfo.date = '2025-05-29T00:44:50.954Z';
61011
- VersionInfo.commit = 'fa81a14da248f229511867324cee663ea70a72b3';
61285
+ VersionInfo.version = '1.6.0-alpha.1432';
61286
+ VersionInfo.date = '2025-05-31T02:06:00.256Z';
61287
+ VersionInfo.commit = '91e41f1b2cf0b786a1ca2053f51cf3f370da4bc6';
61012
61288
 
61013
61289
  /**
61014
61290
  * A factory for custom layout engines.
@@ -61553,6 +61829,29 @@
61553
61829
  print(`Screen Size: ${window.screen.width}x${window.screen.height}`);
61554
61830
  }
61555
61831
  }
61832
+ /**
61833
+ * Prepares the given object to be sent to workers. Web Frameworks like Vue might
61834
+ * create proxy objects for all objects used. This code handles the necessary unwrapping.
61835
+ * @internal
61836
+ * @target web
61837
+ */
61838
+ static prepareForPostMessage(object) {
61839
+ if (!object) {
61840
+ return object;
61841
+ }
61842
+ // Vue toRaw:
61843
+ // https://github.com/vuejs/core/blob/e7381761cc7971c0d40ae0a0a72687a500fd8db3/packages/reactivity/src/reactive.ts#L378-L381
61844
+ if (typeof object === 'object') {
61845
+ const unwrapped = object.__v_raw;
61846
+ if (unwrapped) {
61847
+ return Environment.prepareForPostMessage(unwrapped);
61848
+ }
61849
+ }
61850
+ // Solidjs unwrap: the symbol required to access the raw object is unfortunately hidden and we cannot unwrap it without importing
61851
+ // import { unwrap } from "solid-js/store"
61852
+ // alternative for users is to replace this method during runtime.
61853
+ return object;
61854
+ }
61556
61855
  }
61557
61856
  Environment.StaffIdBeforeSlashAlways = 'before-slash-always';
61558
61857
  Environment.StaffIdBeforeScoreAlways = 'before-score-always';
@@ -61824,6 +62123,7 @@
61824
62123
 
61825
62124
  const _barrel$7 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
61826
62125
  __proto__: null,
62126
+ AlphaTexImporter,
61827
62127
  ScoreImporter,
61828
62128
  ScoreLoader,
61829
62129
  UnsupportedFormatError
@@ -62658,6 +62958,7 @@
62658
62958
  : undefined;
62659
62959
  const millisecondPadding = initialSyncPoint ? initialSyncPoint.syncPointValue.millisecondOffset : 0;
62660
62960
  this.backingTrackFramePadding = (-1 * ((millisecondPadding / 1000) * GpifWriter.SampleRate)) | 0;
62961
+ const modifiedTempoLookup = new Lazy(() => MidiFileGenerator.buildModifiedTempoLookup(score));
62661
62962
  for (const mb of score.masterBars) {
62662
62963
  for (const automation of mb.tempoAutomations) {
62663
62964
  const tempoAutomation = automations.addElement('Automation');
@@ -62682,7 +62983,7 @@
62682
62983
  const value = syncPointAutomation.addElement('Value');
62683
62984
  value.addElement('BarIndex').innerText = mb.index.toString();
62684
62985
  value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue.barOccurence.toString();
62685
- value.addElement('ModifiedTempo').innerText = syncPoint.syncPointValue.modifiedTempo.toString();
62986
+ value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint).syncBpm.toString();
62686
62987
  value.addElement('OriginalTempo').innerText = score.tempo.toString();
62687
62988
  const frameOffset = (((syncPoint.syncPointValue.millisecondOffset - millisecondPadding) / 1000) *
62688
62989
  GpifWriter.SampleRate) |