@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.
@@ -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
  *
@@ -1412,13 +1412,7 @@ class SyncPointData {
1412
1412
  */
1413
1413
  this.barOccurence = 0;
1414
1414
  /**
1415
- * The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
1416
- * This information is used together with normal tempo changes to calculate how much faster/slower the
1417
- * cursor playback is performed to align with the audio track.
1418
- */
1419
- this.modifiedTempo = 0;
1420
- /**
1421
- * The uadio offset marking the position within the audio track in milliseconds.
1415
+ * The audio offset marking the position within the audio track in milliseconds.
1422
1416
  * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
1423
1417
  */
1424
1418
  this.millisecondOffset = 0;
@@ -5843,7 +5837,6 @@ class SyncPointDataCloner {
5843
5837
  static clone(original) {
5844
5838
  const clone = new SyncPointData();
5845
5839
  clone.barOccurence = original.barOccurence;
5846
- clone.modifiedTempo = original.modifiedTempo;
5847
5840
  clone.millisecondOffset = original.millisecondOffset;
5848
5841
  return clone;
5849
5842
  }
@@ -7299,6 +7292,59 @@ class Score {
7299
7292
  this.tracks[i].finish(settings, sharedDataBag);
7300
7293
  }
7301
7294
  }
7295
+ /**
7296
+ * Applies the given list of {@link FlatSyncPoint} to this song.
7297
+ * @param syncPoints The list of sync points to apply.
7298
+ * @since 1.6.0
7299
+ */
7300
+ applyFlatSyncPoints(syncPoints) {
7301
+ for (const b of this.masterBars) {
7302
+ b.syncPoints = undefined;
7303
+ }
7304
+ for (const syncPoint of syncPoints) {
7305
+ const automation = new Automation();
7306
+ automation.ratioPosition = Math.min(1, Math.max(0, syncPoint.barPosition));
7307
+ automation.type = AutomationType.SyncPoint;
7308
+ automation.syncPointValue = new SyncPointData();
7309
+ automation.syncPointValue.millisecondOffset = syncPoint.millisecondOffset;
7310
+ automation.syncPointValue.barOccurence = syncPoint.barOccurence;
7311
+ if (syncPoint.barIndex < this.masterBars.length) {
7312
+ this.masterBars[syncPoint.barIndex].addSyncPoint(automation);
7313
+ }
7314
+ }
7315
+ for (const b of this.masterBars) {
7316
+ if (b.syncPoints) {
7317
+ b.syncPoints.sort((a, b) => {
7318
+ const occurence = a.syncPointValue.barOccurence - b.syncPointValue.barOccurence;
7319
+ if (occurence !== 0) {
7320
+ return occurence;
7321
+ }
7322
+ return a.ratioPosition - b.ratioPosition;
7323
+ });
7324
+ }
7325
+ }
7326
+ }
7327
+ /**
7328
+ * Exports all sync points in this song to a {@link FlatSyncPoint} list.
7329
+ * @since 1.6.0
7330
+ */
7331
+ exportFlatSyncPoints() {
7332
+ const syncPoints = [];
7333
+ for (const masterBar of this.masterBars) {
7334
+ const masterBarSyncPoints = masterBar.syncPoints;
7335
+ if (masterBarSyncPoints) {
7336
+ for (const syncPoint of masterBarSyncPoints) {
7337
+ syncPoints.push({
7338
+ barIndex: masterBar.index,
7339
+ barOccurence: syncPoint.syncPointValue.barOccurence,
7340
+ barPosition: syncPoint.ratioPosition,
7341
+ millisecondOffset: syncPoint.syncPointValue.millisecondOffset
7342
+ });
7343
+ }
7344
+ }
7345
+ }
7346
+ return syncPoints;
7347
+ }
7302
7348
  }
7303
7349
 
7304
7350
  /**
@@ -13607,6 +13653,7 @@ var XmlNodeType;
13607
13653
  XmlNodeType[XmlNodeType["CDATA"] = 3] = "CDATA";
13608
13654
  XmlNodeType[XmlNodeType["Document"] = 4] = "Document";
13609
13655
  XmlNodeType[XmlNodeType["DocumentType"] = 5] = "DocumentType";
13656
+ XmlNodeType[XmlNodeType["Comment"] = 6] = "Comment";
13610
13657
  })(XmlNodeType || (XmlNodeType = {}));
13611
13658
  class XmlNode {
13612
13659
  constructor() {
@@ -14183,7 +14230,7 @@ class XmlWriter {
14183
14230
  this.indent();
14184
14231
  for (const child of xml.childNodes) {
14185
14232
  // skip text nodes in case of multiple children
14186
- if (child.nodeType === XmlNodeType.Element) {
14233
+ if (child.nodeType === XmlNodeType.Element || child.nodeType === XmlNodeType.Comment) {
14187
14234
  this.writeNode(child);
14188
14235
  }
14189
14236
  }
@@ -14214,6 +14261,9 @@ class XmlWriter {
14214
14261
  case XmlNodeType.DocumentType:
14215
14262
  this.write(`<!DOCTYPE ${xml.value}>`);
14216
14263
  break;
14264
+ case XmlNodeType.Comment:
14265
+ this.write(`<!-- ${xml.value} -->`);
14266
+ break;
14217
14267
  }
14218
14268
  }
14219
14269
  unindend() {
@@ -14655,9 +14705,6 @@ class GpifParser {
14655
14705
  case 'BarOccurrence':
14656
14706
  syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
14657
14707
  break;
14658
- case 'ModifiedTempo':
14659
- syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
14660
- break;
14661
14708
  case 'FrameOffset':
14662
14709
  const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
14663
14710
  syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
@@ -16810,7 +16857,7 @@ GpifParser.BendPointPositionFactor = BendPoint.MaxPosition / 100.0;
16810
16857
  * Internal Range: 1 per quarter note
16811
16858
  */
16812
16859
  GpifParser.BendPointValueFactor = 1 / 25.0;
16813
- // test have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
16860
+ // tests have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
16814
16861
  // they are NOT using the sample rate of the input file.
16815
16862
  // Downsampling a 44100hz ogg to 8000hz and using it in as audio track resulted in the same frame offset when placing sync points.
16816
16863
  GpifParser.SampleRate = 44100;
@@ -22280,6 +22327,61 @@ class SynthEvent {
22280
22327
  }
22281
22328
  }
22282
22329
 
22330
+ /**
22331
+ * Rerpresents a point to sync the alphaTab time axis with an external backing track.
22332
+ */
22333
+ class BackingTrackSyncPoint {
22334
+ constructor() {
22335
+ /**
22336
+ * The index of the masterbar to which this sync point belongs to.
22337
+ * @remarks
22338
+ * This property is purely informative for external use like in editors.
22339
+ * It has no impact to the synchronization itself.
22340
+ */
22341
+ this.masterBarIndex = 0;
22342
+ /**
22343
+ * The occurence of the masterbar to which this sync point belongs to. The occurence
22344
+ * is 0-based and increases with every repeated play of a masterbar (e.g. on repeats or jumps).
22345
+ * @remarks
22346
+ * This property is purely informative for external use like in editors.
22347
+ * It has no impact to the synchronization itself.
22348
+ */
22349
+ this.masterBarOccurence = 0;
22350
+ /**
22351
+ * The BPM the synthesizer has at the exact tick position of this sync point.
22352
+ */
22353
+ this.synthBpm = 0;
22354
+ /**
22355
+ * The millisecond time position of the synthesizer when this sync point is reached.
22356
+ */
22357
+ this.synthTime = 0;
22358
+ /**
22359
+ * The midi tick position of the synthesizer when this sync point is reached.
22360
+ */
22361
+ this.synthTick = 0;
22362
+ /**
22363
+ * The millisecond time in the external media marking the synchronization point.
22364
+ */
22365
+ this.syncTime = 0;
22366
+ /**
22367
+ * The BPM the song will have virtually after this sync point to align the external media time axis
22368
+ * with the one from the synthesizer.
22369
+ */
22370
+ this.syncBpm = 0;
22371
+ }
22372
+ /**
22373
+ * Updates the synchronization BPM that will apply after this sync point.
22374
+ * @param nextSyncPointSynthTime The synthesizer time of the next sync point after this one.
22375
+ * @param nextSyncPointSyncTime The synchronization time of the next sync point after this one.
22376
+ */
22377
+ updateSyncBpm(nextSyncPointSynthTime, nextSyncPointSyncTime) {
22378
+ const synthDuration = nextSyncPointSynthTime - this.synthTime;
22379
+ const syncedDuration = nextSyncPointSyncTime - this.syncTime;
22380
+ const modifiedTempo = (synthDuration / syncedDuration) * this.synthBpm;
22381
+ this.syncBpm = modifiedTempo;
22382
+ }
22383
+ }
22384
+
22283
22385
  class MidiFileSequencerTempoChange {
22284
22386
  constructor(bpm, ticks, time) {
22285
22387
  this.bpm = bpm;
@@ -22287,14 +22389,6 @@ class MidiFileSequencerTempoChange {
22287
22389
  this.time = time;
22288
22390
  }
22289
22391
  }
22290
- class BackingTrackSyncPointWithTime {
22291
- constructor(tick, time, modifiedTempo, millisecondOffset) {
22292
- this.alphaTabTick = tick;
22293
- this.alphaTabTime = time;
22294
- this.modifiedTempo = modifiedTempo;
22295
- this.millisecondOffset = millisecondOffset;
22296
- }
22297
- }
22298
22392
  class MidiSequencerState {
22299
22393
  constructor() {
22300
22394
  this.tempoChanges = [];
@@ -22398,7 +22492,7 @@ class MidiFileSequencer {
22398
22492
  this._mainState.currentTempo = this._mainState.tempoChanges[0].bpm;
22399
22493
  this._mainState.modifiedTempo =
22400
22494
  this._mainState.syncPoints.length > 0
22401
- ? this._mainState.syncPoints[0].modifiedTempo
22495
+ ? this._mainState.syncPoints[0].syncBpm
22402
22496
  : this._mainState.currentTempo;
22403
22497
  if (this.isPlayingMain) {
22404
22498
  const metronomeVolume = this._synthesizer.metronomeVolume;
@@ -22569,7 +22663,7 @@ class MidiFileSequencer {
22569
22663
  }
22570
22664
  mainUpdateSyncPoints(syncPoints) {
22571
22665
  const state = this._mainState;
22572
- syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22666
+ syncPoints.sort((a, b) => a.synthTick - b.synthTick); // just in case
22573
22667
  state.syncPoints = [];
22574
22668
  if (syncPoints.length >= 0) {
22575
22669
  let bpm = 120;
@@ -22579,6 +22673,8 @@ class MidiFileSequencer {
22579
22673
  for (let i = 0; i < syncPoints.length; i++) {
22580
22674
  const p = syncPoints[i];
22581
22675
  let deltaTick = 0;
22676
+ // TODO: merge interpolation into MidiFileGenerator where we already play through
22677
+ // the time axis.
22582
22678
  // remember state from previous sync point (or start). to handle linear interpolation
22583
22679
  let previousModifiedTempo;
22584
22680
  let previousMillisecondOffset;
@@ -22590,9 +22686,9 @@ class MidiFileSequencer {
22590
22686
  }
22591
22687
  else {
22592
22688
  const previousSyncPoint = syncPoints[i - 1];
22593
- previousModifiedTempo = previousSyncPoint.data.modifiedTempo;
22594
- previousMillisecondOffset = previousSyncPoint.data.millisecondOffset;
22595
- previousTick = previousSyncPoint.tick;
22689
+ previousModifiedTempo = previousSyncPoint.syncBpm;
22690
+ previousMillisecondOffset = previousSyncPoint.syncTime;
22691
+ previousTick = previousSyncPoint.synthTick;
22596
22692
  }
22597
22693
  // process time until sync point
22598
22694
  // here it gets a bit tricky. if we have tempo changes on the synthesizer time axis (inbetween two sync points)
@@ -22600,25 +22696,31 @@ class MidiFileSequencer {
22600
22696
  // otherwise the linear interpolation later in the lookup will fail.
22601
22697
  // goal is to have always a linear increase between two points, no matter if the time axis is sliced by tempo changes or sync points
22602
22698
  while (tempoChangeIndex < state.tempoChanges.length &&
22603
- state.tempoChanges[tempoChangeIndex].ticks <= p.tick) {
22699
+ state.tempoChanges[tempoChangeIndex].ticks <= p.synthTick) {
22604
22700
  deltaTick = state.tempoChanges[tempoChangeIndex].ticks - absTick;
22605
22701
  if (deltaTick > 0) {
22606
22702
  absTick += deltaTick;
22607
22703
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22608
- const millisPerTick = (p.data.millisecondOffset - previousMillisecondOffset) / (p.tick - previousTick);
22704
+ const millisPerTick = (p.syncTime - previousMillisecondOffset) / (p.synthTick - previousTick);
22609
22705
  const interpolatedMillisecondOffset = (absTick - previousTick) * millisPerTick + previousMillisecondOffset;
22610
- state.syncPoints.push(new BackingTrackSyncPointWithTime(absTick, absTime, previousModifiedTempo, interpolatedMillisecondOffset));
22706
+ const syncPoint = new BackingTrackSyncPoint();
22707
+ syncPoint.synthTick = absTick;
22708
+ syncPoint.synthBpm = bpm;
22709
+ syncPoint.synthTime = absTime;
22710
+ syncPoint.syncTime = interpolatedMillisecondOffset;
22711
+ syncPoint.syncBpm = previousModifiedTempo;
22611
22712
  }
22612
22713
  bpm = state.tempoChanges[tempoChangeIndex].bpm;
22613
22714
  tempoChangeIndex++;
22614
22715
  }
22615
- deltaTick = p.tick - absTick;
22716
+ deltaTick = p.synthTick - absTick;
22616
22717
  absTick += deltaTick;
22617
22718
  absTime += deltaTick * (60000.0 / (bpm * state.division));
22618
- state.syncPoints.push(new BackingTrackSyncPointWithTime(p.tick, absTime, p.data.modifiedTempo, p.data.millisecondOffset));
22719
+ state.syncPoints.push(p);
22619
22720
  }
22620
22721
  }
22621
22722
  state.syncPointIndex = 0;
22723
+ state.modifiedTempo = state.syncPoints.length > 0 ? state.syncPoints[0].syncBpm : state.currentTempo;
22622
22724
  }
22623
22725
  currentTimePositionToTickPosition(timePosition) {
22624
22726
  const state = this._currentState;
@@ -22651,16 +22753,15 @@ class MidiFileSequencer {
22651
22753
  const syncPoints = state.syncPoints;
22652
22754
  if (syncPoints.length > 0) {
22653
22755
  let syncPointIndex = Math.min(state.syncPointIndex, syncPoints.length - 1);
22654
- if (timePosition < syncPoints[syncPointIndex].millisecondOffset) {
22756
+ if (timePosition < syncPoints[syncPointIndex].syncTime) {
22655
22757
  syncPointIndex = 0;
22656
22758
  }
22657
- while (syncPointIndex + 1 < syncPoints.length &&
22658
- syncPoints[syncPointIndex + 1].millisecondOffset <= timePosition) {
22759
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].syncTime <= timePosition) {
22659
22760
  syncPointIndex++;
22660
22761
  }
22661
22762
  if (syncPointIndex !== state.syncPointIndex) {
22662
22763
  state.syncPointIndex = syncPointIndex;
22663
- state.modifiedTempo = syncPoints[syncPointIndex].modifiedTempo;
22764
+ state.modifiedTempo = syncPoints[syncPointIndex].syncBpm;
22664
22765
  }
22665
22766
  }
22666
22767
  else {
@@ -22676,18 +22777,18 @@ class MidiFileSequencer {
22676
22777
  this.updateSyncPoints(this._mainState, timePosition);
22677
22778
  const syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22678
22779
  const currentSyncPoint = syncPoints[syncPointIndex];
22679
- const timeDiff = timePosition - currentSyncPoint.millisecondOffset;
22780
+ const timeDiff = timePosition - currentSyncPoint.syncTime;
22680
22781
  let alphaTabTimeDiff;
22681
22782
  if (syncPointIndex + 1 < syncPoints.length) {
22682
22783
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22683
- const relativeTimeDiff = timeDiff / (nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset);
22684
- alphaTabTimeDiff = (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22784
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.syncTime - currentSyncPoint.syncTime);
22785
+ alphaTabTimeDiff = (nextSyncPoint.synthTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22685
22786
  }
22686
22787
  else {
22687
- const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.millisecondOffset);
22688
- alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.alphaTabTime) * relativeTimeDiff;
22788
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.syncTime);
22789
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.synthTime) * relativeTimeDiff;
22689
22790
  }
22690
- return (currentSyncPoint.alphaTabTime + alphaTabTimeDiff) / this.playbackSpeed;
22791
+ return (currentSyncPoint.synthTime + alphaTabTimeDiff) / this.playbackSpeed;
22691
22792
  }
22692
22793
  mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22693
22794
  const mainState = this._mainState;
@@ -22697,27 +22798,27 @@ class MidiFileSequencer {
22697
22798
  }
22698
22799
  timePosition *= this.playbackSpeed;
22699
22800
  let syncPointIndex = Math.min(mainState.syncPointIndex, syncPoints.length - 1);
22700
- if (timePosition < syncPoints[syncPointIndex].alphaTabTime) {
22801
+ if (timePosition < syncPoints[syncPointIndex].synthTime) {
22701
22802
  syncPointIndex = 0;
22702
22803
  }
22703
- while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].alphaTabTime <= timePosition) {
22804
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].synthTime <= timePosition) {
22704
22805
  syncPointIndex++;
22705
22806
  }
22706
22807
  // NOTE: this logic heavily relies on the interpolation done in mainUpdateSyncPoints
22707
22808
  // we ensure that we have a linear increase between two points
22708
22809
  const currentSyncPoint = syncPoints[syncPointIndex];
22709
- const alphaTabTimeDiff = timePosition - currentSyncPoint.alphaTabTime;
22810
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.synthTime;
22710
22811
  let backingTrackPos;
22711
22812
  if (syncPointIndex + 1 < syncPoints.length) {
22712
22813
  const nextSyncPoint = syncPoints[syncPointIndex + 1];
22713
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.alphaTabTime - currentSyncPoint.alphaTabTime);
22714
- const backingTrackDiff = nextSyncPoint.millisecondOffset - currentSyncPoint.millisecondOffset;
22715
- backingTrackPos = currentSyncPoint.millisecondOffset + backingTrackDiff * relativeAlphaTabTimeDiff;
22814
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.synthTime - currentSyncPoint.synthTime);
22815
+ const backingTrackDiff = nextSyncPoint.syncTime - currentSyncPoint.syncTime;
22816
+ backingTrackPos = currentSyncPoint.syncTime + backingTrackDiff * relativeAlphaTabTimeDiff;
22716
22817
  }
22717
22818
  else {
22718
- const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.alphaTabTime);
22719
- const frameDiff = backingTrackLength - currentSyncPoint.millisecondOffset;
22720
- backingTrackPos = currentSyncPoint.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22819
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.synthTime);
22820
+ const frameDiff = backingTrackLength - currentSyncPoint.syncTime;
22821
+ backingTrackPos = currentSyncPoint.syncTime + frameDiff * relativeAlphaTabTimeDiff;
22721
22822
  }
22722
22823
  return backingTrackPos;
22723
22824
  }
@@ -30989,7 +31090,6 @@ class SyncPointDataSerializer {
30989
31090
  }
30990
31091
  const o = new Map();
30991
31092
  o.set("baroccurence", obj.barOccurence);
30992
- o.set("modifiedtempo", obj.modifiedTempo);
30993
31093
  o.set("millisecondoffset", obj.millisecondOffset);
30994
31094
  return o;
30995
31095
  }
@@ -30998,9 +31098,6 @@ class SyncPointDataSerializer {
30998
31098
  case "baroccurence":
30999
31099
  obj.barOccurence = v;
31000
31100
  return true;
31001
- case "modifiedtempo":
31002
- obj.modifiedTempo = v;
31003
- return true;
31004
31101
  case "millisecondoffset":
31005
31102
  obj.millisecondOffset = v;
31006
31103
  return true;
@@ -35763,17 +35860,6 @@ class MidiTickLookup {
35763
35860
  }
35764
35861
  }
35765
35862
 
35766
- /**
35767
- * Rerpresents a point to sync the alphaTab time axis with an external backing track.
35768
- */
35769
- class BackingTrackSyncPoint {
35770
- constructor(tick, data) {
35771
- this.tick = 0;
35772
- this.tick = tick;
35773
- this.data = data;
35774
- }
35775
- }
35776
-
35777
35863
  class MidiNoteDuration {
35778
35864
  constructor() {
35779
35865
  this.noteOnly = 0;
@@ -35794,6 +35880,14 @@ class RasgueadoInfo {
35794
35880
  this.brushInfos = [];
35795
35881
  }
35796
35882
  }
35883
+ class PlayThroughContext {
35884
+ constructor() {
35885
+ this.synthTick = 0;
35886
+ this.synthTime = 0;
35887
+ this.currentTempo = 0;
35888
+ this.automationToSyncPoint = new Map();
35889
+ }
35890
+ }
35797
35891
  /**
35798
35892
  * This generator creates a midi file using a score.
35799
35893
  */
@@ -35920,9 +36014,22 @@ class MidiFileGenerator {
35920
36014
  });
35921
36015
  return syncPoints;
35922
36016
  }
36017
+ /**
36018
+ * @internal
36019
+ */
36020
+ static buildModifiedTempoLookup(score) {
36021
+ const syncPoints = [];
36022
+ const context = MidiFileGenerator.playThroughSong(score, syncPoints, (_masterBar, _previousMasterBar, _currentTick, _currentTempo, _barOccurence) => {
36023
+ }, (_barIndex, _currentTick, _currentTempo) => {
36024
+ }, _endTick => {
36025
+ });
36026
+ return context.automationToSyncPoint;
36027
+ }
35923
36028
  static playThroughSong(score, syncPoints, generateMasterBar, generateTracks, finish) {
35924
36029
  const controller = new MidiPlaybackController(score);
35925
- let currentTempo = score.tempo;
36030
+ const playContext = new PlayThroughContext();
36031
+ playContext.currentTempo = score.tempo;
36032
+ playContext.syncPoints = syncPoints;
35926
36033
  let previousMasterBar = null;
35927
36034
  // store the previous played bar for repeats
35928
36035
  const barOccurence = new Map();
@@ -35935,23 +36042,11 @@ class MidiFileGenerator {
35935
36042
  let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
35936
36043
  occurence++;
35937
36044
  barOccurence.set(index, occurence);
35938
- generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
35939
- const barSyncPoints = bar.syncPoints;
35940
- if (barSyncPoints) {
35941
- for (const syncPoint of barSyncPoints) {
35942
- if (syncPoint.syncPointValue.barOccurence === occurence) {
35943
- const tick = currentTick + bar.calculateDuration() * syncPoint.ratioPosition;
35944
- syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
35945
- }
35946
- }
35947
- }
35948
- if (bar.tempoAutomations.length > 0) {
35949
- currentTempo = bar.tempoAutomations[0].value;
35950
- }
35951
- generateTracks(index, currentTick, currentTempo);
35952
- if (bar.tempoAutomations.length > 0) {
35953
- currentTempo = bar.tempoAutomations[bar.tempoAutomations.length - 1].value;
35954
- }
36045
+ generateMasterBar(bar, previousMasterBar, currentTick, playContext.currentTempo, occurence);
36046
+ const trackTempo = bar.tempoAutomations.length > 0 ? bar.tempoAutomations[0].value : playContext.currentTempo;
36047
+ generateTracks(index, currentTick, trackTempo);
36048
+ playContext.synthTick = currentTick;
36049
+ MidiFileGenerator.processBarTime(bar, occurence, playContext);
35955
36050
  }
35956
36051
  controller.moveNext();
35957
36052
  previousMasterBar = bar;
@@ -35962,21 +36057,119 @@ class MidiFileGenerator {
35962
36057
  // but where it ends according to the BPM and the remaining ticks.
35963
36058
  if (syncPoints.length > 0) {
35964
36059
  const lastSyncPoint = syncPoints[syncPoints.length - 1];
35965
- const remainingTicks = controller.currentTick - lastSyncPoint.tick;
36060
+ const remainingTicks = controller.currentTick - lastSyncPoint.synthTick;
35966
36061
  if (remainingTicks > 0) {
35967
- const syncPointData = new SyncPointData();
35968
- // last occurence of the last bar
35969
- syncPointData.barOccurence = barOccurence.get(score.masterBars.length - 1);
35970
- // same tempo as last point
35971
- syncPointData.modifiedTempo = lastSyncPoint.data.modifiedTempo;
35972
- // interpolated end from last syncPoint
35973
- syncPointData.millisecondOffset =
35974
- lastSyncPoint.data.millisecondOffset +
35975
- MidiUtils.ticksToMillis(remainingTicks, syncPointData.modifiedTempo);
35976
- syncPoints.push(new BackingTrackSyncPoint(controller.currentTick, syncPointData));
36062
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36063
+ backingTrackSyncPoint.masterBarIndex = previousMasterBar.index;
36064
+ backingTrackSyncPoint.masterBarOccurence = barOccurence.get(previousMasterBar.index) - 1;
36065
+ backingTrackSyncPoint.synthTick = controller.currentTick;
36066
+ backingTrackSyncPoint.synthBpm = playContext.currentTempo;
36067
+ // we need to assume some BPM for the last interpolated point.
36068
+ // if we have more than just a start point, we keep the BPM before the last manual sync point
36069
+ // otherwise we have no customized sync BPM known and keep the synthesizer one.
36070
+ backingTrackSyncPoint.syncBpm =
36071
+ syncPoints.length > 1 ? syncPoints[syncPoints.length - 2].syncBpm : lastSyncPoint.synthBpm;
36072
+ backingTrackSyncPoint.synthTime =
36073
+ lastSyncPoint.synthTime + MidiUtils.ticksToMillis(remainingTicks, lastSyncPoint.synthBpm);
36074
+ backingTrackSyncPoint.syncTime =
36075
+ lastSyncPoint.syncTime + MidiUtils.ticksToMillis(remainingTicks, backingTrackSyncPoint.syncBpm);
36076
+ // update the previous sync point according to the new time
36077
+ lastSyncPoint.updateSyncBpm(backingTrackSyncPoint.synthTime, backingTrackSyncPoint.syncTime);
36078
+ syncPoints.push(backingTrackSyncPoint);
35977
36079
  }
35978
36080
  }
35979
36081
  finish(controller.currentTick);
36082
+ return playContext;
36083
+ }
36084
+ static processBarTime(bar, occurence, context) {
36085
+ const duration = bar.calculateDuration();
36086
+ const barSyncPoints = bar.syncPoints;
36087
+ const barStartTick = context.synthTick;
36088
+ if (barSyncPoints) {
36089
+ MidiFileGenerator.processBarTimeWithSyncPoints(bar, occurence, context);
36090
+ }
36091
+ else {
36092
+ MidiFileGenerator.processBarTimeNoSyncPoints(bar, context);
36093
+ }
36094
+ // don't forget the part after the last tempo change
36095
+ const endTick = barStartTick + duration;
36096
+ const tickOffset = endTick - context.synthTick;
36097
+ if (tickOffset > 0) {
36098
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36099
+ context.synthTick = endTick;
36100
+ }
36101
+ }
36102
+ static processBarTimeWithSyncPoints(bar, occurence, context) {
36103
+ const barStartTick = context.synthTick;
36104
+ const duration = bar.calculateDuration();
36105
+ let tempoChangeIndex = 0;
36106
+ let tickOffset;
36107
+ for (const syncPoint of bar.syncPoints) {
36108
+ if (syncPoint.syncPointValue.barOccurence !== occurence) {
36109
+ continue;
36110
+ }
36111
+ const syncPointTick = barStartTick + syncPoint.ratioPosition * duration;
36112
+ // first process all tempo changes until this sync point
36113
+ while (tempoChangeIndex < bar.tempoAutomations.length &&
36114
+ bar.tempoAutomations[tempoChangeIndex].ratioPosition <= syncPoint.ratioPosition) {
36115
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36116
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36117
+ tickOffset = absoluteTick - context.synthTick;
36118
+ if (tickOffset > 0) {
36119
+ context.synthTick = absoluteTick;
36120
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36121
+ }
36122
+ context.currentTempo = tempoChange.value;
36123
+ tempoChangeIndex++;
36124
+ }
36125
+ // process time until sync point
36126
+ tickOffset = syncPointTick - context.synthTick;
36127
+ if (tickOffset > 0) {
36128
+ context.synthTick = syncPointTick;
36129
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36130
+ }
36131
+ // update the previous sync point according to the new time
36132
+ if (context.syncPoints.length > 0) {
36133
+ context.syncPoints[context.syncPoints.length - 1].updateSyncBpm(context.synthTime, syncPoint.syncPointValue.millisecondOffset);
36134
+ }
36135
+ // create the new sync point
36136
+ const backingTrackSyncPoint = new BackingTrackSyncPoint();
36137
+ backingTrackSyncPoint.masterBarIndex = bar.index;
36138
+ backingTrackSyncPoint.masterBarOccurence = occurence;
36139
+ backingTrackSyncPoint.synthTick = syncPointTick;
36140
+ backingTrackSyncPoint.synthBpm = context.currentTempo;
36141
+ backingTrackSyncPoint.synthTime = context.synthTime;
36142
+ backingTrackSyncPoint.syncTime = syncPoint.syncPointValue.millisecondOffset;
36143
+ backingTrackSyncPoint.syncBpm = 0 /* calculated by next sync point */;
36144
+ context.syncPoints.push(backingTrackSyncPoint);
36145
+ context.automationToSyncPoint.set(syncPoint, backingTrackSyncPoint);
36146
+ }
36147
+ // process remaining tempo changes after all sync points
36148
+ while (tempoChangeIndex < bar.tempoAutomations.length) {
36149
+ const tempoChange = bar.tempoAutomations[tempoChangeIndex];
36150
+ const absoluteTick = barStartTick + tempoChange.ratioPosition * duration;
36151
+ tickOffset = absoluteTick - context.synthTick;
36152
+ if (tickOffset > 0) {
36153
+ context.synthTick = absoluteTick;
36154
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36155
+ }
36156
+ context.currentTempo = tempoChange.value;
36157
+ tempoChangeIndex++;
36158
+ }
36159
+ }
36160
+ static processBarTimeNoSyncPoints(bar, context) {
36161
+ // walk through the tempo changes
36162
+ const barStartTick = context.synthTick;
36163
+ const duration = bar.calculateDuration();
36164
+ for (const changes of bar.tempoAutomations) {
36165
+ const absoluteTick = barStartTick + changes.ratioPosition * duration;
36166
+ const tickOffset = absoluteTick - context.synthTick;
36167
+ if (tickOffset > 0) {
36168
+ context.synthTick = absoluteTick;
36169
+ context.synthTime += MidiUtils.ticksToMillis(tickOffset, context.currentTempo);
36170
+ }
36171
+ context.currentTempo = changes.value;
36172
+ }
35980
36173
  }
35981
36174
  static toChannelShort(data) {
35982
36175
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
@@ -38480,6 +38673,7 @@ class AlphaSynthWrapper {
38480
38673
  value.playbackSpeed = this._playbackSpeed;
38481
38674
  value.isLooping = this._isLooping;
38482
38675
  value.midiEventsPlayedFilter = this._midiEventsPlayedFilter;
38676
+ this.ready.trigger();
38483
38677
  }
38484
38678
  else {
38485
38679
  newUnregister.push(value.ready.on(() => {
@@ -43840,6 +44034,9 @@ class AudioElementBackingTrackSynthOutput {
43840
44034
  const audioElement = document.createElement('audio');
43841
44035
  audioElement.style.display = 'none';
43842
44036
  document.body.appendChild(audioElement);
44037
+ audioElement.addEventListener('seeked', () => {
44038
+ this.updatePosition();
44039
+ });
43843
44040
  audioElement.addEventListener('timeupdate', () => {
43844
44041
  this.updatePosition();
43845
44042
  });
@@ -60935,9 +61132,9 @@ class VersionInfo {
60935
61132
  print(`build date: ${VersionInfo.date}`);
60936
61133
  }
60937
61134
  }
60938
- VersionInfo.version = '1.6.0-alpha.1426';
60939
- VersionInfo.date = '2025-05-27T02:07:22.361Z';
60940
- VersionInfo.commit = '6f8b50015eb3eaebd605045bc9f5aaf6fbf2882d';
61135
+ VersionInfo.version = '1.6.0-alpha.1430';
61136
+ VersionInfo.date = '2025-05-29T22:21:21.689Z';
61137
+ VersionInfo.commit = '98a4c2bec8d71f2645008118d3b77fb40973e7fe';
60941
61138
 
60942
61139
  /**
60943
61140
  * A factory for custom layout engines.
@@ -61850,18 +62047,24 @@ class GpifWriter {
61850
62047
  writeDom(parent, score) {
61851
62048
  const gpif = parent.addElement('GPIF');
61852
62049
  // just some values at the time this was implemented,
61853
- gpif.addElement('GPVersion').innerText = '7';
62050
+ gpif.addElement('GPVersion').innerText = '8.1.3';
61854
62051
  const gpRevision = gpif.addElement('GPRevision');
61855
- gpRevision.innerText = '7';
61856
- gpRevision.attributes.set('required', '12021');
61857
- gpRevision.attributes.set('recommended', '12023');
61858
- gpRevision.innerText = '12025';
61859
- gpif.addElement('Encoding').addElement('EncodingDescription').innerText = 'GP7';
62052
+ gpRevision.attributes.set('required', '12024');
62053
+ gpRevision.attributes.set('recommended', '13000');
62054
+ gpRevision.innerText = '13007';
62055
+ const encoding = gpif.addElement('Encoding');
62056
+ encoding.addElement('EncodingDescription').innerText = 'GP8';
62057
+ const alphaTabComment = new XmlNode();
62058
+ alphaTabComment.nodeType = XmlNodeType.Comment;
62059
+ alphaTabComment.value = `Written by alphaTab ${VersionInfo.version} (${VersionInfo.commit})`;
62060
+ encoding.addChild(alphaTabComment);
61860
62061
  this.writeScoreNode(gpif, score);
61861
62062
  this.writeMasterTrackNode(gpif, score);
62063
+ this.writeBackingTrackNode(gpif, score);
61862
62064
  this.writeAudioTracksNode(gpif, score);
61863
62065
  this.writeTracksNode(gpif, score);
61864
62066
  this.writeMasterBarsNode(gpif, score);
62067
+ this.writeAssets(gpif, score);
61865
62068
  const bars = gpif.addElement('Bars');
61866
62069
  const voices = gpif.addElement('Voices');
61867
62070
  const beats = gpif.addElement('Beats');
@@ -61884,6 +62087,42 @@ class GpifWriter {
61884
62087
  }
61885
62088
  }
61886
62089
  }
62090
+ writeAssets(parent, score) {
62091
+ if (!score.backingTrack?.rawAudioFile) {
62092
+ return;
62093
+ }
62094
+ const assets = parent.addElement('Assets');
62095
+ const asset = assets.addElement('Asset');
62096
+ asset.attributes.set('id', this.backingTrackAssetId);
62097
+ this.backingTrackAssetFileName = 'Content/Assets/backing-track';
62098
+ asset.addElement('EmbeddedFilePath').setCData(this.backingTrackAssetFileName);
62099
+ }
62100
+ writeBackingTrackNode(parent, score) {
62101
+ if (!score.backingTrack?.rawAudioFile) {
62102
+ return;
62103
+ }
62104
+ const backingTrackNode = parent.addElement('BackingTrack');
62105
+ const backingTrackAssetId = '0';
62106
+ this.backingTrackAssetId = backingTrackAssetId;
62107
+ backingTrackNode.addElement('IconId').innerText = '21';
62108
+ backingTrackNode.addElement('Color').innerText = '0 0 0';
62109
+ backingTrackNode.addElement('Name').setCData('Audio Track');
62110
+ backingTrackNode.addElement('ShortName').setCData('a.track');
62111
+ backingTrackNode.addElement('PlaybackState').innerText = 'Default';
62112
+ backingTrackNode.addElement('Enabled').innerText = 'true';
62113
+ backingTrackNode.addElement('Source').innerText = 'Local';
62114
+ backingTrackNode.addElement('AssetId').innerText = backingTrackAssetId;
62115
+ const channelStrip = backingTrackNode.addElement('ChannelStrip');
62116
+ channelStrip.addElement('Parameters').innerText =
62117
+ '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';
62118
+ channelStrip.addElement('YouTubeVideoUrl').innerText = '';
62119
+ channelStrip.addElement('Filter').innerText = '6';
62120
+ channelStrip.addElement('FramesPerPixel').innerText = '400';
62121
+ const framePadding = this.backingTrackFramePadding !== undefined ? this.backingTrackFramePadding : 0;
62122
+ backingTrackNode.addElement('FramePadding').innerText = `${framePadding}`;
62123
+ backingTrackNode.addElement('Semitones').innerText = '0';
62124
+ backingTrackNode.addElement('Cents').innerText = '0';
62125
+ }
61887
62126
  writeNoteNode(parent, note) {
61888
62127
  const noteNode = parent.addElement('Note');
61889
62128
  noteNode.attributes.set('id', note.id.toString());
@@ -62540,6 +62779,12 @@ class GpifWriter {
62540
62779
  initialTempoAutomation.addElement('Text').innerText = score.tempoLabel;
62541
62780
  }
62542
62781
  }
62782
+ const initialSyncPoint = score.masterBars[0].syncPoints
62783
+ ? score.masterBars[0].syncPoints.find(p => p.ratioPosition === 0 && p.syncPointValue.barOccurence === 0)
62784
+ : undefined;
62785
+ const millisecondPadding = initialSyncPoint ? initialSyncPoint.syncPointValue.millisecondOffset : 0;
62786
+ this.backingTrackFramePadding = (-1 * ((millisecondPadding / 1000) * GpifWriter.SampleRate)) | 0;
62787
+ const modifiedTempoLookup = new Lazy(() => MidiFileGenerator.buildModifiedTempoLookup(score));
62543
62788
  for (const mb of score.masterBars) {
62544
62789
  for (const automation of mb.tempoAutomations) {
62545
62790
  const tempoAutomation = automations.addElement('Automation');
@@ -62553,6 +62798,25 @@ class GpifWriter {
62553
62798
  tempoAutomation.addElement('Text').innerText = automation.text;
62554
62799
  }
62555
62800
  }
62801
+ if (mb.syncPoints) {
62802
+ for (const syncPoint of mb.syncPoints) {
62803
+ const syncPointAutomation = automations.addElement('Automation');
62804
+ syncPointAutomation.addElement('Type').innerText = 'SyncPoint';
62805
+ syncPointAutomation.addElement('Linear').innerText = 'false';
62806
+ syncPointAutomation.addElement('Bar').innerText = mb.index.toString();
62807
+ syncPointAutomation.addElement('Position').innerText = syncPoint.ratioPosition.toString();
62808
+ syncPointAutomation.addElement('Visible').innerText = 'true';
62809
+ const value = syncPointAutomation.addElement('Value');
62810
+ value.addElement('BarIndex').innerText = mb.index.toString();
62811
+ value.addElement('BarOccurrence').innerText = syncPoint.syncPointValue.barOccurence.toString();
62812
+ value.addElement('ModifiedTempo').innerText = modifiedTempoLookup.value.get(syncPoint).syncBpm.toString();
62813
+ value.addElement('OriginalTempo').innerText = score.tempo.toString();
62814
+ const frameOffset = (((syncPoint.syncPointValue.millisecondOffset - millisecondPadding) / 1000) *
62815
+ GpifWriter.SampleRate) |
62816
+ 0;
62817
+ value.addElement('FrameOffset').innerText = frameOffset.toString();
62818
+ }
62819
+ }
62556
62820
  }
62557
62821
  }
62558
62822
  writeAudioTracksNode(parent, score) {
@@ -63203,6 +63467,10 @@ class GpifWriter {
63203
63467
  voiceNode.addElement('Beats').innerText = voice.beats.map(v => v.id).join(' ');
63204
63468
  }
63205
63469
  }
63470
+ // tests have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
63471
+ // they are NOT using the sample rate of the input file.
63472
+ // Downsampling a 44100hz ogg to 8000hz and using it in as audio track resulted in the same frame offset when placing sync points.
63473
+ GpifWriter.SampleRate = 44100;
63206
63474
  GpifWriter.MidiProgramInfoLookup = new Map([
63207
63475
  [0, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')],
63208
63476
  [1, new GpifMidiProgramInfo(GpifIconIds.Piano, 'Acoustic Piano')],
@@ -64940,11 +65208,11 @@ class ZipWriter {
64940
65208
  }
64941
65209
 
64942
65210
  /**
64943
- * This ScoreExporter can write Guitar Pro 7 (gp) files.
65211
+ * This ScoreExporter can write Guitar Pro 7+ (gp) files.
64944
65212
  */
64945
65213
  class Gp7Exporter extends ScoreExporter {
64946
65214
  get name() {
64947
- return 'Guitar Pro 7';
65215
+ return 'Guitar Pro 7-8';
64948
65216
  }
64949
65217
  writeScore(score) {
64950
65218
  Logger.debug(this.name, 'Writing data entries');
@@ -64961,6 +65229,9 @@ class Gp7Exporter extends ScoreExporter {
64961
65229
  fileSystem.writeEntry(new ZipEntry('Content/PartConfiguration', partConfiguration));
64962
65230
  fileSystem.writeEntry(new ZipEntry('Content/LayoutConfiguration', layoutConfiguration));
64963
65231
  fileSystem.writeEntry(new ZipEntry('Content/score.gpif', IOHelper.stringToBytes(gpifXml)));
65232
+ if (gpifWriter.backingTrackAssetFileName) {
65233
+ fileSystem.writeEntry(new ZipEntry(gpifWriter.backingTrackAssetFileName, score.backingTrack.rawAudioFile));
65234
+ }
64964
65235
  fileSystem.end();
64965
65236
  }
64966
65237
  }