@coderline/alphatab 1.6.0-alpha.1426 → 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.1426 (develop, build 1426)
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
  *
@@ -1418,13 +1418,7 @@
1418
1418
  */
1419
1419
  this.barOccurence = 0;
1420
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
- /**
1427
- * The uadio offset marking the position within the audio track in milliseconds.
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.
1429
1423
  */
1430
1424
  this.millisecondOffset = 0;
@@ -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
  }
@@ -7305,6 +7298,59 @@
7305
7298
  this.tracks[i].finish(settings, sharedDataBag);
7306
7299
  }
7307
7300
  }
7301
+ /**
7302
+ * Applies the given list of {@link FlatSyncPoint} to this song.
7303
+ * @param syncPoints The list of sync points to apply.
7304
+ * @since 1.6.0
7305
+ */
7306
+ applyFlatSyncPoints(syncPoints) {
7307
+ for (const b of this.masterBars) {
7308
+ b.syncPoints = undefined;
7309
+ }
7310
+ for (const syncPoint of syncPoints) {
7311
+ const automation = new Automation();
7312
+ automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
7313
+ automation.type = AutomationType.SyncPoint;
7314
+ automation.syncPointValue = new SyncPointData();
7315
+ automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
7316
+ automation.syncPointValue.barOccurence = syncPoint.barOccurence;
7317
+ if (syncPoint.barIndex < this.masterBars.length) {
7318
+ this.masterBars[syncPoint.barIndex].addSyncPoint(automation);
7319
+ }
7320
+ }
7321
+ for (const b of this.masterBars) {
7322
+ if (b.syncPoints) {
7323
+ b.syncPoints.sort((a, b) => {
7324
+ const occurence = a.syncPointValue.barOccurence - b.syncPointValue.barOccurence;
7325
+ if (occurence !== 0) {
7326
+ return occurence;
7327
+ }
7328
+ return a.ratioPosition - b.ratioPosition;
7329
+ });
7330
+ }
7331
+ }
7332
+ }
7333
+ /**
7334
+ * Exports all sync points in this song to a {@link FlatSyncPoint} list.
7335
+ * @since 1.6.0
7336
+ */
7337
+ exportFlatSyncPoints() {
7338
+ const syncPoints = [];
7339
+ for (const masterBar of this.masterBars) {
7340
+ const masterBarSyncPoints = masterBar.syncPoints;
7341
+ if (masterBarSyncPoints) {
7342
+ for (const syncPoint of masterBarSyncPoints) {
7343
+ syncPoints.push({
7344
+ barIndex: masterBar.index,
7345
+ barOccurence: syncPoint.syncPointValue.barOccurence,
7346
+ barPosition: syncPoint.ratioPosition,
7347
+ millisecondOffset: syncPoint.syncPointValue.millisecondOffset
7348
+ });
7349
+ }
7350
+ }
7351
+ }
7352
+ return syncPoints;
7353
+ }
7308
7354
  }
7309
7355
 
7310
7356
  /**
@@ -13613,6 +13659,7 @@
13613
13659
  XmlNodeType[XmlNodeType["CDATA"] = 3] = "CDATA";
13614
13660
  XmlNodeType[XmlNodeType["Document"] = 4] = "Document";
13615
13661
  XmlNodeType[XmlNodeType["DocumentType"] = 5] = "DocumentType";
13662
+ XmlNodeType[XmlNodeType["Comment"] = 6] = "Comment";
13616
13663
  })(XmlNodeType || (XmlNodeType = {}));
13617
13664
  class XmlNode {
13618
13665
  constructor() {
@@ -14189,7 +14236,7 @@
14189
14236
  this.indent();
14190
14237
  for (const child of xml.childNodes) {
14191
14238
  // skip text nodes in case of multiple children
14192
- if (child.nodeType === XmlNodeType.Element) {
14239
+ if (child.nodeType === XmlNodeType.Element || child.nodeType === XmlNodeType.Comment) {
14193
14240
  this.writeNode(child);
14194
14241
  }
14195
14242
  }
@@ -14220,6 +14267,9 @@
14220
14267
  case XmlNodeType.DocumentType:
14221
14268
  this.write(`<!DOCTYPE ${xml.value}>`);
14222
14269
  break;
14270
+ case XmlNodeType.Comment:
14271
+ this.write(`<!-- ${xml.value} -->`);
14272
+ break;
14223
14273
  }
14224
14274
  }
14225
14275
  unindend() {
@@ -14661,9 +14711,6 @@
14661
14711
  case 'BarOccurrence':
14662
14712
  syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
14663
14713
  break;
14664
- case 'ModifiedTempo':
14665
- syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
14666
- break;
14667
14714
  case 'FrameOffset':
14668
14715
  const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
14669
14716
  syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
@@ -16816,7 +16863,7 @@
16816
16863
  * Internal Range: 1 per quarter note
16817
16864
  */
16818
16865
  GpifParser.BendPointValueFactor = 1 / 25.0;
16819
- // test have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
16866
+ // tests have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
16820
16867
  // they are NOT using the sample rate of the input file.
16821
16868
  // Downsampling a 44100hz ogg to 8000hz and using it in as audio track resulted in the same frame offset when placing sync points.
16822
16869
  GpifParser.SampleRate = 44100;
@@ -22286,6 +22333,61 @@
22286
22333
  }
22287
22334
  }
22288
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
+
22289
22391
  class MidiFileSequencerTempoChange {
22290
22392
  constructor(bpm, ticks, time) {
22291
22393
  this.bpm = bpm;
@@ -22293,14 +22395,6 @@
22293
22395
  this.time = time;
22294
22396
  }
22295
22397
  }
22296
- class BackingTrackSyncPointWithTime {
22297
- constructor(tick, time, modifiedTempo, millisecondOffset) {
22298
- this.alphaTabTick = tick;
22299
- this.alphaTabTime = time;
22300
- this.modifiedTempo = modifiedTempo;
22301
- this.millisecondOffset = millisecondOffset;
22302
- }
22303
- }
22304
22398
  class MidiSequencerState {
22305
22399
  constructor() {
22306
22400
  this.tempoChanges = [];
@@ -22404,7 +22498,7 @@
22404
22498
  this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22405
22499
  this._mainState.modifiedTempo =
22406
22500
  this._mainState.syncPoints.length > 0
22407
- ? this._mainState.syncPoints[0].modifiedTempo
22501
+ ? this._mainState.syncPoints[0].syncBpm
22408
22502
  : this._mainState.currentTempo;
22409
22503
  if (this.isPlayingMain) {
22410
22504
  const metronomeVolume = this._synthesizer.metronomeVolume;
@@ -22575,7 +22669,7 @@
22575
22669
  }
22576
22670
  mainUpdateSyncPoints(syncPoints) {
22577
22671
  const state = this._mainState;
22578
- syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22672
+ syncPoints.sort((a, b) => a.synthTick - b.synthTick); // just in case
22579
22673
  state.syncPoints = [];
22580
22674
  if (syncPoints.length >= 0) {
22581
22675
  let bpm = 120;
@@ -22585,6 +22679,8 @@
22585
22679
  for (let i = 0; i < syncPoints.length; i++) {
22586
22680
  const p = syncPoints[i];
22587
22681
  let deltaTick = 0;
22682
+ // TODO: merge interpolation into MidiFileGenerator where we already play through
22683
+ // the time axis.
22588
22684
  // remember state from previous sync point (or start). to handle linear interpolation
22589
22685
  let previousModifiedTempo;
22590
22686
  let previousMillisecondOffset;
@@ -22596,9 +22692,9 @@
22596
22692
  }
22597
22693
  else {
22598
22694
  const previousSyncPoint = syncPoints[i - 1];
22599
- previousModifiedTempo = previousSyncPoint.data.modifiedTempo;
22600
- previousMillisecondOffset = previousSyncPoint.data.millisecondOffset;
22601
- previousTick = previousSyncPoint.tick;
22695
+ previousModifiedTempo = previousSyncPoint.syncBpm;
22696
+ previousMillisecondOffset = previousSyncPoint.syncTime;
22697
+ previousTick = previousSyncPoint.synthTick;
22602
22698
  }
22603
22699
  // process time until sync point
22604
22700
  // here it gets a bit tricky. if we have tempo changes on the synthesizer time axis (inbetween two sync points)
@@ -22606,25 +22702,31 @@
22606
22702
  // otherwise the linear interpolation later in the lookup will fail.
22607
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
22608
22704
  while (tempoChangeIndex < state.tempoChanges.length &&
22609
- state.tempoChanges[tempoChangeIndex].ticks <= p.tick) {
22705
+ state.tempoChanges[tempoChangeIndex].ticks <= p.synthTick) {
22610
22706
  deltaTick = state.tempoChanges[tempoChangeIndex].ticks - absTick;
22611
22707
  if (deltaTick > 0) {
22612
22708
  absTick += deltaTick;
22613
22709
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22614
- const millisPerTick = (p.data.millisecondOffset - previousMillisecondOffset) / (p.tick - previousTick);
22710
+ const millisPerTick = (p.syncTime - previousMillisecondOffset) / (p.synthTick - previousTick);
22615
22711
  const interpolatedMillisecondOffset = (absTick - previousTick) * millisPerTick + previousMillisecondOffset;
22616
- 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;
22617
22718
  }
22618
22719
  bpm = state.tempoChanges[tempoChangeIndex].bpm;
22619
22720
  tempoChangeIndex++;
22620
22721
  }
22621
- deltaTick = p.tick - absTick;
22722
+ deltaTick = p.synthTick - absTick;
22622
22723
  absTick += deltaTick;
22623
22724
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22624
- state.syncPoints.push(new BackingTrackSyncPointWithTime(p.tick, absTime, p.data.modifiedTempo, p.data.millisecondOffset));
22725
+ state.syncPoints.push(p);
22625
22726
  }
22626
22727
  }
22627
22728
  state.syncPointIndex = 0;
22729
+ state.modifiedTempo = state.syncPoints.length > 0 ? state.syncPoints[0].syncBpm : state.currentTempo;
22628
22730
  }
22629
22731
  currentTimePositionToTickPosition(timePosition) {
22630
22732
  const state = this._currentState;
@@ -22657,16 +22759,15 @@
22657
22759
  const syncPoints = state.syncPoints;
22658
22760
  if (syncPoints.length > 0) {
22659
22761
  let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22660
- if (timePosition < syncPoints[syncPointIndex].millisecondOffset) {
22762
+ if (timePosition < syncPoints[syncPointIndex].syncTime) {
22661
22763
  syncPointIndex = 0;
22662
22764
  }
22663
- while (syncPointIndex + 1 < syncPoints.length &&
22664
- syncPoints[syncPointIndex + 1].millisecondOffset <= timePosition) {
22765
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].syncTime <= timePosition) {
22665
22766
  syncPointIndex++;
22666
22767
  }
22667
22768
  if (syncPointIndex !== state.syncPointIndex) {
22668
22769
  state.syncPointIndex = syncPointIndex;
22669
- state.modifiedTempo = syncPoints[syncPointIndex].modifiedTempo;
22770
+ state.modifiedTempo = syncPoints[syncPointIndex].syncBpm;
22670
22771
  }
22671
22772
  }
22672
22773
  else {
@@ -22682,18 +22783,18 @@
22682
22783
  this.updateSyncPoints(this._mainState, timePosition);
22683
22784
  const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22684
22785
  const currentSyncPoint = syncPoints[syncPointIndex];
22685
- const timeDiff = timePosition - currentSyncPoint.millisecondOffset;
22786
+ const timeDiff = timePosition - currentSyncPoint.syncTime;
22686
22787
  let alphaTabTimeDiff;
22687
22788
  if (syncPointIndex + 1 < syncPoints.length) {
22688
22789
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22689
- const relativeTimeDiff = timeDiff / (nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset);
22690
- alphaTabTimeDiff = (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22790
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.syncTime - currentSyncPoint.syncTime);
22791
+ alphaTabTimeDiff = (nextSyncPoint.synthTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22691
22792
  }
22692
22793
  else {
22693
- const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.millisecondOffset);
22694
- alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22794
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.syncTime);
22795
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22695
22796
  }
22696
- return (currentSyncPoint.alphaTabTime + alphaTabTimeDiff) / this.playbackSpeed;
22797
+ return (currentSyncPoint.synthTime + alphaTabTimeDiff) / this.playbackSpeed;
22697
22798
  }
22698
22799
  mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22699
22800
  const mainState = this._mainState;
@@ -22703,27 +22804,27 @@
22703
22804
  }
22704
22805
  timePosition *= this.playbackSpeed;
22705
22806
  let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22706
- if (timePosition < syncPoints[syncPointIndex].alphaTabTime) {
22807
+ if (timePosition < syncPoints[syncPointIndex].synthTime) {
22707
22808
  syncPointIndex = 0;
22708
22809
  }
22709
- while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].alphaTabTime <= timePosition) {
22810
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].synthTime <= timePosition) {
22710
22811
  syncPointIndex++;
22711
22812
  }
22712
22813
  // NOTE: this logic heavily relies on the interpolation done in mainUpdateSyncPoints
22713
22814
  // we ensure that we have a linear increase between two points
22714
22815
  const currentSyncPoint = syncPoints[syncPointIndex];
22715
- const alphaTabTimeDiff = timePosition - currentSyncPoint.alphaTabTime;
22816
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.synthTime;
22716
22817
  let backingTrackPos;
22717
22818
  if (syncPointIndex + 1 < syncPoints.length) {
22718
22819
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22719
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime);
22720
- const backingTrackDiff = nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset;
22721
- 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;
22722
22823
  }
22723
22824
  else {
22724
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.alphaTabTime);
22725
- const frameDiff = backingTrackLength - currentSyncPoint.millisecondOffset;
22726
- 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;
22727
22828
  }
22728
22829
  return backingTrackPos;
22729
22830
  }
@@ -30995,7 +31096,6 @@
30995
31096
  }
30996
31097
  const o = new Map();
30997
31098
  o.set("baroccurence", obj.barOccurence);
30998
- o.set("modifiedtempo", obj.modifiedTempo);
30999
31099
  o.set("millisecondoffset", obj.millisecondOffset);
31000
31100
  return o;
31001
31101
  }
@@ -31004,9 +31104,6 @@
31004
31104
  case "baroccurence":
31005
31105
  obj.barOccurence = v;
31006
31106
  return true;
31007
- case "modifiedtempo":
31008
- obj.modifiedTempo = v;
31009
- return true;
31010
31107
  case "millisecondoffset":
31011
31108
  obj.millisecondOffset = v;
31012
31109
  return true;
@@ -35769,17 +35866,6 @@
35769
35866
  }
35770
35867
  }
35771
35868
 
35772
- /**
35773
- * Rerpresents a point to sync the alphaTab time axis with an external backing track.
35774
- */
35775
- class BackingTrackSyncPoint {
35776
- constructor(tick, data) {
35777
- this.tick = 0;
35778
- this.tick = tick;
35779
- this.data = data;
35780
- }
35781
- }
35782
-
35783
35869
  class MidiNoteDuration {
35784
35870
  constructor() {
35785
35871
  this.noteOnly = 0;
@@ -35800,6 +35886,14 @@
35800
35886
  this.brushInfos = [];
35801
35887
  }
35802
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
+ }
35803
35897
  /**
35804
35898
  * This generator creates a midi file using a score.
35805
35899
  */
@@ -35926,9 +36020,22 @@
35926
36020
  });
35927
36021
  return syncPoints;
35928
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
+ }
35929
36034
  static playThroughSong(score, syncPoints, generateMasterBar, generateTracks, finish) {
35930
36035
  const controller = new MidiPlaybackController(score);
35931
- let currentTempo = score.tempo;
36036
+ const playContext = new PlayThroughContext();
36037
+ playContext.currentTempo = score.tempo;
36038
+ playContext.syncPoints = syncPoints;
35932
36039
  let previousMasterBar = null;
35933
36040
  // store the previous played bar for repeats
35934
36041
  const barOccurence = new Map();
@@ -35941,23 +36048,11 @@
35941
36048
  let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
35942
36049
  occurence++;
35943
36050
  barOccurence.set(index, occurence);
35944
- generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
35945
- const barSyncPoints = bar.syncPoints;
35946
- if (barSyncPoints) {
35947
- for (const syncPoint of barSyncPoints) {
35948
- if (syncPoint.syncPointValue.barOccurence === occurence) {
35949
- const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
35950
- syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
35951
- }
35952
- }
35953
- }
35954
- if (bar.tempoAutomations.length > 0) {
35955
- currentTempo = bar.tempoAutomations[0].value;
35956
- }
35957
- generateTracks(index, currentTick, currentTempo);
35958
- if (bar.tempoAutomations.length > 0) {
35959
- currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
35960
- }
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);
35961
36056
  }
35962
36057
  controller.moveNext();
35963
36058
  previousMasterBar = bar;
@@ -35968,21 +36063,119 @@
35968
36063
  // but where it ends according to the BPM and the remaining ticks.
35969
36064
  if (syncPoints.length > 0) {
35970
36065
  const lastSyncPoint = syncPoints[syncPoints.length - 1];
35971
- const remainingTicks = controller.currentTick - lastSyncPoint.tick;
36066
+ const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
35972
36067
  if (remainingTicks > 0) {
35973
- const syncPointData = new SyncPointData();
35974
- // last occurence of the last bar
35975
- syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1);
35976
- // same tempo as last point
35977
- syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35978
- // interpolated end from last syncPoint
35979
- syncPointData.millisecondOffset =
35980
- lastSyncPoint.data.millisecondOffset +
35981
- MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35982
- 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);
35983
36085
  }
35984
36086
  }
35985
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
+ }
35986
36179
  }
35987
36180
  static toChannelShort(data) {
35988
36181
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
@@ -38486,6 +38679,7 @@
38486
38679
  value.playbackSpeed = this._playbackSpeed;
38487
38680
  value.isLooping = this._isLooping;
38488
38681
  value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38682
+ this.ready.trigger();
38489
38683
  }
38490
38684
  else {
38491
38685
  newUnregister.push(value.ready.on(() => {
@@ -43846,6 +44040,9 @@
43846
44040
  const audioElement = document.createElement('audio');
43847
44041
  audioElement.style.display = 'none';
43848
44042
  document.body.appendChild(audioElement);
44043
+ audioElement.addEventListener('seeked', () => {
44044
+ this.updatePosition();
44045
+ });
43849
44046
  audioElement.addEventListener('timeupdate', () => {
43850
44047
  this.updatePosition();
43851
44048
  });
@@ -60941,9 +61138,9 @@
60941
61138
  print(`build date: ${VersionInfo.date}`);
60942
61139
  }
60943
61140
  }
60944
- VersionInfo.version = '1.6.0-alpha.1426';
60945
- VersionInfo.date = '2025-05-27T02:07:22.361Z';
60946
- VersionInfo.commit = '6f8b50015eb3eaebd605045bc9f5aaf6fbf2882d';
61141
+ VersionInfo.version = '1.6.0-alpha.1430';
61142
+ VersionInfo.date = '2025-05-29T22:21:21.689Z';
61143
+ VersionInfo.commit = '98a4c2bec8d71f2645008118d3b77fb40973e7fe';
60947
61144
 
60948
61145
  /**
60949
61146
  * A factory for custom layout engines.
@@ -61856,18 +62053,24 @@
61856
62053
  writeDom(parent, score) {
61857
62054
  const gpif = parent.addElement('GPIF');
61858
62055
  // just some values at the time this was implemented,
61859
- gpif.addElement('GPVersion').innerText = '7';
62056
+ gpif.addElement('GPVersion').innerText = '8.1.3';
61860
62057
  const gpRevision = gpif.addElement('GPRevision');
61861
- gpRevision.innerText = '7';
61862
- gpRevision.attributes.set('required', '12021');
61863
- gpRevision.attributes.set('recommended', '12023');
61864
- gpRevision.innerText = '12025';
61865
- gpif.addElement('Encoding').addElement('EncodingDescription').innerText = 'GP7';
62058
+ gpRevision.attributes.set('required', '12024');
62059
+ gpRevision.attributes.set('recommended', '13000');
62060
+ gpRevision.innerText = '13007';
62061
+ const encoding = gpif.addElement('Encoding');
62062
+ encoding.addElement('EncodingDescription').innerText = 'GP8';
62063
+ const alphaTabComment = new XmlNode();
62064
+ alphaTabComment.nodeType = XmlNodeType.Comment;
62065
+ alphaTabComment.value = `Written by alphaTab ${VersionInfo.version} (${VersionInfo.commit})`;
62066
+ encoding.addChild(alphaTabComment);
61866
62067
  this.writeScoreNode(gpif, score);
61867
62068
  this.writeMasterTrackNode(gpif, score);
62069
+ this.writeBackingTrackNode(gpif, score);
61868
62070
  this.writeAudioTracksNode(gpif, score);
61869
62071
  this.writeTracksNode(gpif, score);
61870
62072
  this.writeMasterBarsNode(gpif, score);
62073
+ this.writeAssets(gpif, score);
61871
62074
  const bars = gpif.addElement('Bars');
61872
62075
  const voices = gpif.addElement('Voices');
61873
62076
  const beats = gpif.addElement('Beats');
@@ -61890,6 +62093,42 @@
61890
62093
  }
61891
62094
  }
61892
62095
  }
62096
+ writeAssets(parent, score) {
62097
+ if (!score.backingTrack?.rawAudioFile) {
62098
+ return;
62099
+ }
62100
+ const assets = parent.addElement('Assets');
62101
+ const asset = assets.addElement('Asset');
62102
+ asset.attributes.set('id', this.backingTrackAssetId);
62103
+ this.backingTrackAssetFileName = 'Content/Assets/backing-track';
62104
+ asset.addElement('EmbeddedFilePath').setCData(this.backingTrackAssetFileName);
62105
+ }
62106
+ writeBackingTrackNode(parent, score) {
62107
+ if (!score.backingTrack?.rawAudioFile) {
62108
+ return;
62109
+ }
62110
+ const backingTrackNode = parent.addElement('BackingTrack');
62111
+ const backingTrackAssetId = '0';
62112
+ this.backingTrackAssetId = backingTrackAssetId;
62113
+ backingTrackNode.addElement('IconId').innerText = '21';
62114
+ backingTrackNode.addElement('Color').innerText = '0 0 0';
62115
+ backingTrackNode.addElement('Name').setCData('Audio Track');
62116
+ backingTrackNode.addElement('ShortName').setCData('a.track');
62117
+ backingTrackNode.addElement('PlaybackState').innerText = 'Default';
62118
+ backingTrackNode.addElement('Enabled').innerText = 'true';
62119
+ backingTrackNode.addElement('Source').innerText = 'Local';
62120
+ backingTrackNode.addElement('AssetId').innerText = backingTrackAssetId;
62121
+ const channelStrip = backingTrackNode.addElement('ChannelStrip');
62122
+ channelStrip.addElement('Parameters').innerText =
62123
+ '0.500000 0.500000 0.500000 0.500000 0.500000 0.500000 0.500000 0.500000 0.500000 0.000000 0.500000 0.500000 0.800000 0.500000 0.500000 0.500000';
62124
+ channelStrip.addElement('YouTubeVideoUrl').innerText = '';
62125
+ channelStrip.addElement('Filter').innerText = '6';
62126
+ channelStrip.addElement('FramesPerPixel').innerText = '400';
62127
+ const framePadding = this.backingTrackFramePadding !== undefined ? this.backingTrackFramePadding : 0;
62128
+ backingTrackNode.addElement('FramePadding').innerText = `${framePadding}`;
62129
+ backingTrackNode.addElement('Semitones').innerText = '0';
62130
+ backingTrackNode.addElement('Cents').innerText = '0';
62131
+ }
61893
62132
  writeNoteNode(parent, note) {
61894
62133
  const noteNode = parent.addElement('Note');
61895
62134
  noteNode.attributes.set('id', note.id.toString());
@@ -62546,6 +62785,12 @@
62546
62785
  initialTempoAutomation.addElement('Text').innerText = score.tempoLabel;
62547
62786
  }
62548
62787
  }
62788
+ const initialSyncPoint = score.masterBars[0].syncPoints
62789
+ ? score.masterBars[0].syncPoints.find(p => p.ratioPosition === 0 && p.syncPointValue.barOccurence === 0)
62790
+ : undefined;
62791
+ const millisecondPadding = initialSyncPoint ? initialSyncPoint.syncPointValue.millisecondOffset : 0;
62792
+ this.backingTrackFramePadding = (-1 * ((millisecondPadding / 1000) * GpifWriter.SampleRate)) | 0;
62793
+ const modifiedTempoLookup = new Lazy(() => MidiFileGenerator.buildModifiedTempoLookup(score));
62549
62794
  for (const mb of score.masterBars) {
62550
62795
  for (const automation of mb.tempoAutomations) {
62551
62796
  const tempoAutomation = automations.addElement('Automation');
@@ -62559,6 +62804,25 @@
62559
62804
  tempoAutomation.addElement('Text').innerText = automation.text;
62560
62805
  }
62561
62806
  }
62807
+ if (mb.syncPoints) {
62808
+ for (const syncPoint of mb.syncPoints) {
62809
+ const syncPointAutomation = automations.addElement('Automation');
62810
+ syncPointAutomation.addElement('Type').innerText = 'SyncPoint';
62811
+ syncPointAutomation.addElement('Linear').innerText = 'false';
62812
+ syncPointAutomation.addElement('Bar').innerText = mb.index.toString();
62813
+ syncPointAutomation.addElement('Position').innerText = syncPoint.ratioPosition.toString();
62814
+ syncPointAutomation.addElement('Visible').innerText = 'true';
62815
+ const value = syncPointAutomation.addElement('Value');
62816
+ value.addElement('BarIndex').innerText = mb.index.toString();
62817
+ value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue.barOccurence.toString();
62818
+ value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint).syncBpm.toString();
62819
+ value.addElement('OriginalTempo').innerText = score.tempo.toString();
62820
+ const frameOffset = (((syncPoint.syncPointValue.millisecondOffset - millisecondPadding) / 1000) *
62821
+ GpifWriter.SampleRate) |
62822
+ 0;
62823
+ value.addElement('FrameOffset').innerText = frameOffset.toString();
62824
+ }
62825
+ }
62562
62826
  }
62563
62827
  }
62564
62828
  writeAudioTracksNode(parent, score) {
@@ -63209,6 +63473,10 @@
63209
63473
  voiceNode.addElement('Beats').innerText = voice.beats.map(v => v.id).join(' ');
63210
63474
  }
63211
63475
  }
63476
+ // tests have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
63477
+ // they are NOT using the sample rate of the input file.
63478
+ // Downsampling a 44100hz ogg to 8000hz and using it in as audio track resulted in the same frame offset when placing sync points.
63479
+ GpifWriter.SampleRate = 44100;
63212
63480
  GpifWriter.MidiProgramInfoLookup = new Map([
63213
63481
  [0, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')],
63214
63482
  [1, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')],
@@ -64946,11 +65214,11 @@
64946
65214
  }
64947
65215
 
64948
65216
  /**
64949
- * This ScoreExporter can write Guitar Pro 7 (gp) files.
65217
+ * This ScoreExporter can write Guitar Pro 7+ (gp) files.
64950
65218
  */
64951
65219
  class Gp7Exporter extends ScoreExporter {
64952
65220
  get name() {
64953
- return 'Guitar Pro 7';
65221
+ return 'Guitar Pro 7-8';
64954
65222
  }
64955
65223
  writeScore(score) {
64956
65224
  Logger.debug(this.name, 'Writing data entries');
@@ -64967,6 +65235,9 @@
64967
65235
  fileSystem.writeEntry(new ZipEntry('Content/PartConfiguration', partConfiguration));
64968
65236
  fileSystem.writeEntry(new ZipEntry('Content/LayoutConfiguration', layoutConfiguration));
64969
65237
  fileSystem.writeEntry(new ZipEntry('Content/score.gpif', IOHelper.stringToBytes(gpifXml)));
65238
+ if (gpifWriter.backingTrackAssetFileName) {
65239
+ fileSystem.writeEntry(new ZipEntry(gpifWriter.backingTrackAssetFileName, score.backingTrack.rawAudioFile));
65240
+ }
64970
65241
  fileSystem.end();
64971
65242
  }
64972
65243
  }