@coderline/alphatab 1.6.0-alpha.1401 → 1.6.0-alpha.1403

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.1401 (develop, build 1401)
2
+ * alphaTab v1.6.0-alpha.1403 (develop, build 1403)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -49,7 +49,127 @@
49
49
  * @license
50
50
  */
51
51
 
52
- if(typeof Symbol.dispose==='undefined'){Symbol.dispose = Symbol('Symbol.dispose')}
52
+ /**
53
+ * A very basic polyfill of the ResizeObserver which triggers
54
+ * a the callback on window resize for all registered targets.
55
+ * @target web
56
+ */
57
+ class ResizeObserverPolyfill {
58
+ constructor(callback) {
59
+ this._targets = new Set();
60
+ this._callback = callback;
61
+ window.addEventListener('resize', this.onWindowResize.bind(this), false);
62
+ }
63
+ observe(target) {
64
+ this._targets.add(target);
65
+ }
66
+ unobserve(target) {
67
+ this._targets.delete(target);
68
+ }
69
+ disconnect() {
70
+ this._targets.clear();
71
+ }
72
+ onWindowResize() {
73
+ const entries = [];
74
+ for (const t of this._targets) {
75
+ entries.push({
76
+ target: t,
77
+ // not used by alphaTab
78
+ contentRect: undefined,
79
+ borderBoxSize: undefined,
80
+ contentBoxSize: [],
81
+ devicePixelContentBoxSize: []
82
+ });
83
+ }
84
+ this._callback(entries, this);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * A polyfill of the InsersectionObserver
90
+ * @target web
91
+ */
92
+ class IntersectionObserverPolyfill {
93
+ constructor(callback) {
94
+ this._elements = [];
95
+ let timer = null;
96
+ const oldCheck = this.check.bind(this);
97
+ this.check = () => {
98
+ if (!timer) {
99
+ timer = setTimeout(() => {
100
+ oldCheck();
101
+ timer = null;
102
+ }, 100);
103
+ }
104
+ };
105
+ this._callback = callback;
106
+ window.addEventListener('resize', this.check, true);
107
+ document.addEventListener('scroll', this.check, true);
108
+ }
109
+ observe(target) {
110
+ if (this._elements.indexOf(target) >= 0) {
111
+ return;
112
+ }
113
+ this._elements.push(target);
114
+ this.check();
115
+ }
116
+ unobserve(target) {
117
+ this._elements = this._elements.filter(item => {
118
+ return item !== target;
119
+ });
120
+ }
121
+ check() {
122
+ const entries = [];
123
+ for (const element of this._elements) {
124
+ const rect = element.getBoundingClientRect();
125
+ const isVisible = rect.top + rect.height >= 0 &&
126
+ rect.top <= window.innerHeight &&
127
+ rect.left + rect.width >= 0 &&
128
+ rect.left <= window.innerWidth;
129
+ if (isVisible) {
130
+ entries.push({
131
+ target: element,
132
+ isIntersecting: true
133
+ });
134
+ }
135
+ }
136
+ if (entries.length) {
137
+ this._callback(entries, this);
138
+ }
139
+ }
140
+ }
141
+
142
+ /*@target web*/
143
+ (() => {
144
+ if (typeof Symbol.dispose === 'undefined') {
145
+ Symbol.dispose = Symbol('Symbol.dispose');
146
+ }
147
+ if (typeof window !== 'undefined') {
148
+ // ResizeObserver API does not yet exist so long on Safari (only start 2020 with iOS Safari 13.7 and Desktop 13.1)
149
+ // so we better add a polyfill for it
150
+ if (!('ResizeObserver' in globalThis)) {
151
+ globalThis.ResizeObserver = ResizeObserverPolyfill;
152
+ }
153
+ // IntersectionObserver API does not on older iOS versions
154
+ // so we better add a polyfill for it
155
+ if (!('IntersectionObserver' in globalThis)) {
156
+ globalThis.IntersectionObserver = IntersectionObserverPolyfill;
157
+ }
158
+ if (!('replaceChildren' in Element.prototype)) {
159
+ Element.prototype.replaceChildren = function (...nodes) {
160
+ this.innerHTML = '';
161
+ this.append(...nodes);
162
+ };
163
+ Document.prototype.replaceChildren = Element.prototype.replaceChildren;
164
+ DocumentFragment.prototype.replaceChildren = Element.prototype.replaceChildren;
165
+ }
166
+ }
167
+ if (!('replaceAll' in String.prototype)) {
168
+ String.prototype.replaceAll = function (str, newStr) {
169
+ return this.replace(new RegExp(str, 'g'), newStr);
170
+ };
171
+ }
172
+ })();
53
173
 
54
174
  /**
55
175
  * Lists all layout modes that are supported.
@@ -1273,7 +1393,37 @@ var AutomationType;
1273
1393
  * Balance change.
1274
1394
  */
1275
1395
  AutomationType[AutomationType["Balance"] = 3] = "Balance";
1396
+ /**
1397
+ * A sync point for synchronizing the internal time axis with an external audio track.
1398
+ */
1399
+ AutomationType[AutomationType["SyncPoint"] = 4] = "SyncPoint";
1276
1400
  })(AutomationType || (AutomationType = {}));
1401
+ /**
1402
+ * Represents the data of a sync point for synchronizing the internal time axis with
1403
+ * an external audio file.
1404
+ * @cloneable
1405
+ * @json
1406
+ * @json_strict
1407
+ */
1408
+ class SyncPointData {
1409
+ constructor() {
1410
+ /**
1411
+ * Indicates for which repeat occurence this sync point is valid (e.g. 0 on the first time played, 1 on the second time played)
1412
+ */
1413
+ this.barOccurence = 0;
1414
+ /**
1415
+ * The modified tempo at which the cursor should move (aka. the tempo played within the external audio track).
1416
+ * This information is used together with the {@link originalTempo} 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.
1422
+ * This information is used to regularly sync (or on seeking) to match a given external audio time axis with the internal time axis.
1423
+ */
1424
+ this.millisecondOffset = 0;
1425
+ }
1426
+ }
1277
1427
  /**
1278
1428
  * Automations are used to change the behaviour of a song.
1279
1429
  * @cloneable
@@ -2572,6 +2722,16 @@ class MasterBar {
2572
2722
  }
2573
2723
  return null;
2574
2724
  }
2725
+ /**
2726
+ * Adds the given sync point to the list of sync points for this bar.
2727
+ * @param syncPoint The sync point to add.
2728
+ */
2729
+ addSyncPoint(syncPoint) {
2730
+ if (!this.syncPoints) {
2731
+ this.syncPoints = [];
2732
+ }
2733
+ this.syncPoints.push(syncPoint);
2734
+ }
2575
2735
  }
2576
2736
  MasterBar.MaxAlternateEndings = 8;
2577
2737
 
@@ -5666,6 +5826,21 @@ class NoteCloner {
5666
5826
  }
5667
5827
  }
5668
5828
 
5829
+ // <auto-generated>
5830
+ // This code was auto-generated.
5831
+ // Changes to this file may cause incorrect behavior and will be lost if
5832
+ // the code is regenerated.
5833
+ // </auto-generated>
5834
+ class SyncPointDataCloner {
5835
+ static clone(original) {
5836
+ const clone = new SyncPointData();
5837
+ clone.barOccurence = original.barOccurence;
5838
+ clone.modifiedTempo = original.modifiedTempo;
5839
+ clone.millisecondOffset = original.millisecondOffset;
5840
+ return clone;
5841
+ }
5842
+ }
5843
+
5669
5844
  // <auto-generated>
5670
5845
  // This code was auto-generated.
5671
5846
  // Changes to this file may cause incorrect behavior and will be lost if
@@ -5677,6 +5852,7 @@ class AutomationCloner {
5677
5852
  clone.isLinear = original.isLinear;
5678
5853
  clone.type = original.type;
5679
5854
  clone.value = original.value;
5855
+ clone.syncPointValue = original.syncPointValue ? SyncPointDataCloner.clone(original.syncPointValue) : undefined;
5680
5856
  clone.ratioPosition = original.ratioPosition;
5681
5857
  clone.text = original.text;
5682
5858
  return clone;
@@ -14122,6 +14298,21 @@ class XmlDocument extends XmlNode {
14122
14298
  }
14123
14299
  }
14124
14300
 
14301
+ /**
14302
+ * Holds information about the backing track which can be played instead of synthesized audio.
14303
+ * @json
14304
+ * @json_strict
14305
+ */
14306
+ class BackingTrack {
14307
+ constructor() {
14308
+ /**
14309
+ * The number of milliseconds the audio should be shifted to align with the song.
14310
+ * (e.g. negative values allow skipping potential silent parts at the start of the file and directly start with the first note).
14311
+ */
14312
+ this.padding = 0;
14313
+ }
14314
+ }
14315
+
14125
14316
  /**
14126
14317
  * This structure represents a duration within a gpif
14127
14318
  */
@@ -14214,6 +14405,9 @@ class GpifParser {
14214
14405
  case 'MasterTrack':
14215
14406
  this.parseMasterTrackNode(n);
14216
14407
  break;
14408
+ case 'BackingTrack':
14409
+ this.parseBackingTrackNode(n);
14410
+ break;
14217
14411
  case 'Tracks':
14218
14412
  this.parseTracksNode(n);
14219
14413
  break;
@@ -14235,6 +14429,9 @@ class GpifParser {
14235
14429
  case 'Rhythms':
14236
14430
  this.parseRhythms(n);
14237
14431
  break;
14432
+ case 'Assets':
14433
+ this.parseAssets(n);
14434
+ break;
14238
14435
  }
14239
14436
  }
14240
14437
  }
@@ -14242,6 +14439,37 @@ class GpifParser {
14242
14439
  throw new UnsupportedFormatError('Root node of XML was not GPIF');
14243
14440
  }
14244
14441
  }
14442
+ parseAssets(element) {
14443
+ for (const c of element.childElements()) {
14444
+ switch (c.localName) {
14445
+ case 'Asset':
14446
+ if (c.getAttribute('id') === this._backingTrackAssetId) {
14447
+ this.parseBackingTrackAsset(c);
14448
+ }
14449
+ break;
14450
+ }
14451
+ }
14452
+ }
14453
+ parseBackingTrackAsset(element) {
14454
+ let embeddedFilePath = '';
14455
+ for (const c of element.childElements()) {
14456
+ switch (c.localName) {
14457
+ case 'EmbeddedFilePath':
14458
+ embeddedFilePath = c.innerText;
14459
+ break;
14460
+ }
14461
+ }
14462
+ const loadAsset = this.loadAsset;
14463
+ if (loadAsset) {
14464
+ const assetData = loadAsset(embeddedFilePath);
14465
+ if (assetData) {
14466
+ this.score.backingTrack.rawAudioFile = assetData;
14467
+ }
14468
+ else {
14469
+ this.score.backingTrack = undefined;
14470
+ }
14471
+ }
14472
+ }
14245
14473
  //
14246
14474
  // <Score>...</Score>
14247
14475
  //
@@ -14322,7 +14550,41 @@ class GpifParser {
14322
14550
  if (!text) {
14323
14551
  return [];
14324
14552
  }
14325
- return text.split(separator).map(t => t.trim()).filter(t => t.length > 0);
14553
+ return text
14554
+ .split(separator)
14555
+ .map(t => t.trim())
14556
+ .filter(t => t.length > 0);
14557
+ }
14558
+ //
14559
+ // <BackingTrack>...</BackingTrack>
14560
+ //
14561
+ parseBackingTrackNode(node) {
14562
+ const backingTrack = new BackingTrack();
14563
+ let enabled = false;
14564
+ let source = '';
14565
+ let assetId = '';
14566
+ for (const c of node.childElements()) {
14567
+ switch (c.localName) {
14568
+ case 'Enabled':
14569
+ enabled = c.innerText === 'true';
14570
+ break;
14571
+ case 'Source':
14572
+ source = c.innerText;
14573
+ break;
14574
+ case 'AssetId':
14575
+ assetId = c.innerText;
14576
+ break;
14577
+ case 'FramePadding':
14578
+ backingTrack.padding = GpifParser.parseIntSafe(c.innerText, 0) / GpifParser.SampleRate * 1000;
14579
+ break;
14580
+ }
14581
+ }
14582
+ // only local (contained backing tracks are supported)
14583
+ // remote / youtube links seem to come in future releases according to the gpif tags.
14584
+ if (enabled && source === 'Local') {
14585
+ this.score.backingTrack = backingTrack;
14586
+ this._backingTrackAssetId = assetId; // when the Asset tag is parsed this ID is used to load the raw data
14587
+ }
14326
14588
  }
14327
14589
  //
14328
14590
  // <MasterTrack>...</MasterTrack>
@@ -14360,6 +14622,7 @@ class GpifParser {
14360
14622
  let textValue = null;
14361
14623
  let reference = 0;
14362
14624
  let text = null;
14625
+ let syncPointValue = undefined;
14363
14626
  for (const c of node.childElements()) {
14364
14627
  switch (c.localName) {
14365
14628
  case 'Type':
@@ -14378,6 +14641,28 @@ class GpifParser {
14378
14641
  if (c.firstElement && c.firstElement.nodeType === XmlNodeType.CDATA) {
14379
14642
  textValue = c.innerText;
14380
14643
  }
14644
+ else if (c.firstElement &&
14645
+ c.firstElement.nodeType === XmlNodeType.Element &&
14646
+ type === 'SyncPoint') {
14647
+ syncPointValue = new SyncPointData();
14648
+ for (const vc of c.childElements()) {
14649
+ switch (vc.localName) {
14650
+ case 'BarIndex':
14651
+ barIndex = GpifParser.parseIntSafe(vc.innerText, 0);
14652
+ break;
14653
+ case 'BarOccurrence':
14654
+ syncPointValue.barOccurence = GpifParser.parseIntSafe(vc.innerText, 0);
14655
+ break;
14656
+ case 'ModifiedTempo':
14657
+ syncPointValue.modifiedTempo = GpifParser.parseFloatSafe(vc.innerText, 0);
14658
+ break;
14659
+ case 'FrameOffset':
14660
+ const frameOffset = GpifParser.parseFloatSafe(vc.innerText, 0);
14661
+ syncPointValue.millisecondOffset = (frameOffset / GpifParser.SampleRate) * 1000;
14662
+ break;
14663
+ }
14664
+ }
14665
+ }
14381
14666
  else {
14382
14667
  const parts = GpifParser.splitSafe(c.innerText);
14383
14668
  // Issue 391: Some GPX files might have
@@ -14405,6 +14690,13 @@ class GpifParser {
14405
14690
  case 'Tempo':
14406
14691
  automation = Automation.buildTempoAutomation(isLinear, ratioPosition, numberValue, reference);
14407
14692
  break;
14693
+ case 'SyncPoint':
14694
+ automation = new Automation();
14695
+ automation.type = AutomationType.SyncPoint;
14696
+ automation.isLinear = isLinear;
14697
+ automation.ratioPosition = ratioPosition;
14698
+ automation.syncPointValue = syncPointValue;
14699
+ break;
14408
14700
  case 'Sound':
14409
14701
  if (textValue && sounds && sounds.has(textValue)) {
14410
14702
  automation = Automation.buildInstrumentAutomation(isLinear, ratioPosition, sounds.get(textValue).program);
@@ -16486,14 +16778,19 @@ class GpifParser {
16486
16778
  const masterBar = this.score.masterBars[barNumber];
16487
16779
  for (let i = 0, j = automations.length; i < j; i++) {
16488
16780
  const automation = automations[i];
16489
- if (automation.type === AutomationType.Tempo) {
16490
- if (barNumber === 0) {
16491
- this.score.tempo = automation.value | 0;
16492
- if (automation.text) {
16493
- this.score.tempoLabel = automation.text;
16781
+ switch (automation.type) {
16782
+ case AutomationType.Tempo:
16783
+ if (barNumber === 0) {
16784
+ this.score.tempo = automation.value | 0;
16785
+ if (automation.text) {
16786
+ this.score.tempoLabel = automation.text;
16787
+ }
16494
16788
  }
16495
- }
16496
- masterBar.tempoAutomations.push(automation);
16789
+ masterBar.tempoAutomations.push(automation);
16790
+ break;
16791
+ case AutomationType.SyncPoint:
16792
+ masterBar.addSyncPoint(automation);
16793
+ break;
16497
16794
  }
16498
16795
  }
16499
16796
  }
@@ -16510,6 +16807,10 @@ GpifParser.BendPointPositionFactor = BendPoint.MaxPosition / 100.0;
16510
16807
  * Internal Range: 1 per quarter note
16511
16808
  */
16512
16809
  GpifParser.BendPointValueFactor = 1 / 25.0;
16810
+ // test have shown that Guitar Pro seem to always work with 44100hz for the frame offsets,
16811
+ // they are NOT using the sample rate of the input file.
16812
+ // Downsampling a 44100hz ogg to 8000hz and using it in as audio track resulted in the same frame offset when placing sync points.
16813
+ GpifParser.SampleRate = 44100;
16513
16814
 
16514
16815
  // PartConfiguration File Format Notes.
16515
16816
  // Based off Guitar Pro 8
@@ -17376,7 +17677,9 @@ class Gp7To8Importer extends ScoreImporter {
17376
17677
  let binaryStylesheetData = null;
17377
17678
  let partConfigurationData = null;
17378
17679
  let layoutConfigurationData = null;
17680
+ const entryLookup = new Map();
17379
17681
  for (const entry of entries) {
17682
+ entryLookup.set(entry.fullName, entry);
17380
17683
  switch (entry.fileName) {
17381
17684
  case 'score.gpif':
17382
17685
  xml = IOHelper.toString(entry.data, this.settings.importer.encoding);
@@ -17399,6 +17702,12 @@ class Gp7To8Importer extends ScoreImporter {
17399
17702
  // the score information as XML we need to parse.
17400
17703
  Logger.debug(this.name, 'Start Parsing score.gpif');
17401
17704
  const gpifParser = new GpifParser();
17705
+ gpifParser.loadAsset = (fileName) => {
17706
+ if (entryLookup.has(fileName)) {
17707
+ return entryLookup.get(fileName).data;
17708
+ }
17709
+ return undefined;
17710
+ };
17402
17711
  gpifParser.parseXml(xml, this.settings);
17403
17712
  Logger.debug(this.name, 'score.gpif parsed');
17404
17713
  const score = gpifParser.score;
@@ -21864,8 +22173,24 @@ class ProgramChangeEvent extends MidiEvent {
21864
22173
  * Represents a change of the tempo in the song.
21865
22174
  */
21866
22175
  class TempoChangeEvent extends MidiEvent {
22176
+ /**
22177
+ * The tempo in microseconds per quarter note (aka USQ). A time format typically for midi.
22178
+ */
22179
+ get microSecondsPerQuarterNote() {
22180
+ return 60000000 / this.beatsPerMinute;
22181
+ }
22182
+ /**
22183
+ * The tempo in microseconds per quarter note (aka USQ). A time format typically for midi.
22184
+ */
22185
+ set microSecondsPerQuarterNote(value) {
22186
+ this.beatsPerMinute = 60000000 / value;
22187
+ }
21867
22188
  constructor(tick, microSecondsPerQuarterNote) {
21868
22189
  super(0, tick, MidiEventType.TempoChange);
22190
+ /**
22191
+ * The tempo in beats per minute
22192
+ */
22193
+ this.beatsPerMinute = 0;
21869
22194
  this.microSecondsPerQuarterNote = microSecondsPerQuarterNote;
21870
22195
  }
21871
22196
  writeTo(s) {
@@ -21948,6 +22273,17 @@ class SynthEvent {
21948
22273
  }
21949
22274
  }
21950
22275
 
22276
+ /**
22277
+ * Rerpresents a point to sync the alphaTab time axis with an external backing track.
22278
+ */
22279
+ class BackingTrackSyncPoint {
22280
+ constructor(tick, data) {
22281
+ this.tick = 0;
22282
+ this.tick = tick;
22283
+ this.data = data;
22284
+ }
22285
+ }
22286
+
21951
22287
  class MidiFileSequencerTempoChange {
21952
22288
  constructor(bpm, ticks, time) {
21953
22289
  this.bpm = bpm;
@@ -21955,9 +22291,17 @@ class MidiFileSequencerTempoChange {
21955
22291
  this.time = time;
21956
22292
  }
21957
22293
  }
22294
+ class BackingTrackSyncPointWithTime extends BackingTrackSyncPoint {
22295
+ constructor(tick, data, time) {
22296
+ super(tick, data);
22297
+ this.time = time;
22298
+ }
22299
+ }
21958
22300
  class MidiSequencerState {
21959
22301
  constructor() {
21960
22302
  this.tempoChanges = [];
22303
+ this.tempoChangeIndex = 0;
22304
+ this.syncPoints = [];
21961
22305
  this.firstProgramEventPerChannel = new Map();
21962
22306
  this.firstTimeSignatureNumerator = 0;
21963
22307
  this.firstTimeSignatureDenominator = 0;
@@ -21965,11 +22309,15 @@ class MidiSequencerState {
21965
22309
  this.division = MidiUtils.QuarterTime;
21966
22310
  this.eventIndex = 0;
21967
22311
  this.currentTime = 0;
22312
+ this.currentTick = 0;
22313
+ this.syncPointIndex = 0;
21968
22314
  this.playbackRange = null;
21969
22315
  this.playbackRangeStartTime = 0;
21970
22316
  this.playbackRangeEndTime = 0;
21971
22317
  this.endTick = 0;
21972
22318
  this.endTime = 0;
22319
+ this.currentTempo = 0;
22320
+ this.modifiedTempo = 0;
21973
22321
  }
21974
22322
  }
21975
22323
  /**
@@ -22022,6 +22370,12 @@ class MidiFileSequencer {
22022
22370
  get currentEndTime() {
22023
22371
  return this._currentState.endTime / this.playbackSpeed;
22024
22372
  }
22373
+ get currentTempo() {
22374
+ return this._currentState.currentTempo;
22375
+ }
22376
+ get modifiedTempo() {
22377
+ return this._currentState.modifiedTempo * this.playbackSpeed;
22378
+ }
22025
22379
  mainSeek(timePosition) {
22026
22380
  // map to speed=1
22027
22381
  timePosition *= this.playbackSpeed;
@@ -22041,6 +22395,8 @@ class MidiFileSequencer {
22041
22395
  // we have to restart the midi to make sure we get the right state: instruments, volume, pan, etc
22042
22396
  this._mainState.currentTime = 0;
22043
22397
  this._mainState.eventIndex = 0;
22398
+ this._mainState.syncPointIndex = 0;
22399
+ this._mainState.tempoChangeIndex = 0;
22044
22400
  if (this.isPlayingMain) {
22045
22401
  const metronomeVolume = this._synthesizer.metronomeVolume;
22046
22402
  this._synthesizer.noteOffAll(true);
@@ -22115,7 +22471,7 @@ class MidiFileSequencer {
22115
22471
  }
22116
22472
  if (mEvent.type === MidiEventType.TempoChange) {
22117
22473
  const meta = mEvent;
22118
- bpm = 60000000 / meta.microSecondsPerQuarterNote;
22474
+ bpm = meta.beatsPerMinute;
22119
22475
  state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime));
22120
22476
  metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division));
22121
22477
  }
@@ -22149,6 +22505,8 @@ class MidiFileSequencer {
22149
22505
  }
22150
22506
  }
22151
22507
  }
22508
+ state.currentTempo = state.tempoChanges.length > 0 ? state.tempoChanges[0].bpm : bpm;
22509
+ state.modifiedTempo = state.currentTempo;
22152
22510
  state.synthData.sort((a, b) => {
22153
22511
  if (a.time > b.time) {
22154
22512
  return 1;
@@ -22165,6 +22523,35 @@ class MidiFileSequencer {
22165
22523
  fillMidiEventQueue() {
22166
22524
  return this.fillMidiEventQueueLimited(-1);
22167
22525
  }
22526
+ fillMidiEventQueueToEndTime(endTime) {
22527
+ while (this._mainState.currentTime < endTime) {
22528
+ if (this.fillMidiEventQueueLimited(endTime - this._mainState.currentTime)) {
22529
+ this._synthesizer.synthesizeSilent(SynthConstants.MicroBufferSize);
22530
+ }
22531
+ }
22532
+ let anyEventsDispatched = false;
22533
+ this._currentState.currentTime = endTime;
22534
+ while (this._currentState.eventIndex < this._currentState.synthData.length &&
22535
+ this._currentState.synthData[this._currentState.eventIndex].time < this._currentState.currentTime) {
22536
+ const synthEvent = this._currentState.synthData[this._currentState.eventIndex];
22537
+ this._synthesizer.dispatchEvent(synthEvent);
22538
+ while (this._currentState.syncPointIndex < this._currentState.syncPoints.length &&
22539
+ this._currentState.syncPoints[this._currentState.syncPointIndex].tick < synthEvent.event.tick) {
22540
+ this._currentState.modifiedTempo =
22541
+ this._currentState.syncPoints[this._currentState.syncPointIndex].data.modifiedTempo;
22542
+ this._currentState.syncPointIndex++;
22543
+ }
22544
+ while (this._currentState.tempoChangeIndex < this._currentState.tempoChanges.length &&
22545
+ this._currentState.tempoChanges[this._currentState.tempoChangeIndex].time <= synthEvent.time) {
22546
+ this._currentState.currentTempo =
22547
+ this._currentState.tempoChanges[this._currentState.tempoChangeIndex].bpm;
22548
+ this._currentState.tempoChangeIndex++;
22549
+ }
22550
+ this._currentState.eventIndex++;
22551
+ anyEventsDispatched = true;
22552
+ }
22553
+ return anyEventsDispatched;
22554
+ }
22168
22555
  fillMidiEventQueueLimited(maxMilliseconds) {
22169
22556
  let millisecondsPerBuffer = (SynthConstants.MicroBufferSize / this._synthesizer.outSampleRate) * 1000 * this.playbackSpeed;
22170
22557
  let endTime = this.internalEndTime;
@@ -22192,9 +22579,87 @@ class MidiFileSequencer {
22192
22579
  mainTimePositionToTickPosition(timePosition) {
22193
22580
  return this.timePositionToTickPositionWithSpeed(this._mainState, timePosition, this.playbackSpeed);
22194
22581
  }
22582
+ mainUpdateSyncPoints(syncPoints) {
22583
+ const state = this._mainState;
22584
+ syncPoints.sort((a, b) => a.tick - b.tick); // just in case
22585
+ state.syncPoints = new Array(syncPoints.length);
22586
+ if (syncPoints.length >= 0) {
22587
+ let bpm = 120;
22588
+ let absTick = 0;
22589
+ let absTime = 0.0;
22590
+ let previousTick = 0;
22591
+ let tempoChangeIndex = 0;
22592
+ for (let i = 0; i < syncPoints.length; i++) {
22593
+ const p = syncPoints[i];
22594
+ const deltaTick = p.tick - previousTick;
22595
+ absTick += deltaTick;
22596
+ absTime += deltaTick * (60000.0 / (bpm * state.division));
22597
+ state.syncPoints[i] = new BackingTrackSyncPointWithTime(p.tick, p.data, absTime);
22598
+ previousTick = p.tick;
22599
+ while (tempoChangeIndex < state.tempoChanges.length &&
22600
+ state.tempoChanges[tempoChangeIndex].ticks <= absTick) {
22601
+ bpm = state.tempoChanges[tempoChangeIndex].bpm;
22602
+ tempoChangeIndex++;
22603
+ }
22604
+ }
22605
+ }
22606
+ state.syncPointIndex = 0;
22607
+ }
22195
22608
  currentTimePositionToTickPosition(timePosition) {
22196
22609
  return this.timePositionToTickPositionWithSpeed(this._currentState, timePosition, this.playbackSpeed);
22197
22610
  }
22611
+ mainTimePositionFromBackingTrack(timePosition, backingTrackLength) {
22612
+ const mainState = this._mainState;
22613
+ const syncPoints = mainState.syncPoints;
22614
+ if (timePosition < 0 || syncPoints.length === 0) {
22615
+ return timePosition;
22616
+ }
22617
+ let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].data.millisecondOffset ? mainState.syncPointIndex : 0;
22618
+ while (syncPointIndex + 1 < syncPoints.length &&
22619
+ syncPoints[syncPointIndex + 1].data.millisecondOffset <= timePosition) {
22620
+ syncPointIndex++;
22621
+ }
22622
+ const currentSyncPoint = syncPoints[syncPointIndex];
22623
+ const timeDiff = timePosition - currentSyncPoint.data.millisecondOffset;
22624
+ let alphaTabTimeDiff;
22625
+ if (syncPointIndex + 1 < syncPoints.length) {
22626
+ const nextSyncPoint = syncPoints[syncPointIndex + 1];
22627
+ const relativeTimeDiff = timeDiff / (nextSyncPoint.data.millisecondOffset - currentSyncPoint.data.millisecondOffset);
22628
+ alphaTabTimeDiff = (nextSyncPoint.time - currentSyncPoint.time) * relativeTimeDiff;
22629
+ }
22630
+ else {
22631
+ const relativeTimeDiff = timeDiff / (backingTrackLength - currentSyncPoint.data.millisecondOffset);
22632
+ alphaTabTimeDiff = (mainState.endTime - currentSyncPoint.time) * relativeTimeDiff;
22633
+ }
22634
+ return (currentSyncPoint.time + alphaTabTimeDiff) / this.playbackSpeed;
22635
+ }
22636
+ mainTimePositionToBackingTrack(timePosition, backingTrackLength) {
22637
+ const mainState = this._mainState;
22638
+ const syncPoints = mainState.syncPoints;
22639
+ if (timePosition < 0 || syncPoints.length === 0) {
22640
+ return timePosition;
22641
+ }
22642
+ timePosition *= this.playbackSpeed;
22643
+ let syncPointIndex = timePosition >= syncPoints[mainState.syncPointIndex].time ? mainState.syncPointIndex : 0;
22644
+ while (syncPointIndex + 1 < syncPoints.length && syncPoints[syncPointIndex + 1].time <= timePosition) {
22645
+ syncPointIndex++;
22646
+ }
22647
+ const currentSyncPoint = syncPoints[syncPointIndex];
22648
+ const alphaTabTimeDiff = timePosition - currentSyncPoint.time;
22649
+ let backingTrackPos;
22650
+ if (syncPointIndex + 1 < syncPoints.length) {
22651
+ const nextSyncPoint = syncPoints[syncPointIndex + 1];
22652
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (nextSyncPoint.time - currentSyncPoint.time);
22653
+ const backingTrackDiff = nextSyncPoint.data.millisecondOffset - currentSyncPoint.data.millisecondOffset;
22654
+ backingTrackPos = currentSyncPoint.data.millisecondOffset + backingTrackDiff * relativeAlphaTabTimeDiff;
22655
+ }
22656
+ else {
22657
+ const relativeAlphaTabTimeDiff = alphaTabTimeDiff / (mainState.endTime - currentSyncPoint.time);
22658
+ const frameDiff = backingTrackLength - currentSyncPoint.data.millisecondOffset;
22659
+ backingTrackPos = currentSyncPoint.data.millisecondOffset + frameDiff * relativeAlphaTabTimeDiff;
22660
+ }
22661
+ return backingTrackPos;
22662
+ }
22198
22663
  tickPositionToTimePositionWithSpeed(state, tickPosition, playbackSpeed) {
22199
22664
  let timePosition = 0.0;
22200
22665
  let bpm = 120.0;
@@ -22304,6 +22769,8 @@ class MidiFileSequencer {
22304
22769
  });
22305
22770
  state.endTime = metronomeTime;
22306
22771
  state.endTick = metronomeTick;
22772
+ state.currentTempo = bpm;
22773
+ state.modifiedTempo = bpm;
22307
22774
  this._countInState = state;
22308
22775
  }
22309
22776
  }
@@ -22349,12 +22816,22 @@ class PositionChangedEventArgs {
22349
22816
  * @param endTick The end tick.
22350
22817
  * @param isSeek Whether the time was seeked.
22351
22818
  */
22352
- constructor(currentTime, endTime, currentTick, endTick, isSeek) {
22819
+ constructor(currentTime, endTime, currentTick, endTick, isSeek, originalTempo, modifiedTempo) {
22820
+ /**
22821
+ * The original tempo in which alphaTab internally would be playing right now.
22822
+ */
22823
+ this.originalTempo = 0;
22824
+ /**
22825
+ * The modified tempo in which the actual playback is happening (e.g. due to playback speed or external audio synchronization)
22826
+ */
22827
+ this.modifiedTempo = 0;
22353
22828
  this.currentTime = currentTime;
22354
22829
  this.endTime = endTime;
22355
22830
  this.currentTick = currentTick;
22356
22831
  this.endTick = endTick;
22357
22832
  this.isSeek = isSeek;
22833
+ this.originalTempo = originalTempo;
22834
+ this.modifiedTempo = modifiedTempo;
22358
22835
  }
22359
22836
  }
22360
22837
 
@@ -26452,7 +26929,7 @@ class TinySoundFont {
26452
26929
  break;
26453
26930
  case MidiEventType.TempoChange:
26454
26931
  const tempoChange = e;
26455
- this.currentTempo = 60000000 / tempoChange.microSecondsPerQuarterNote;
26932
+ this.currentTempo = tempoChange.beatsPerMinute;
26456
26933
  break;
26457
26934
  case MidiEventType.PitchBend:
26458
26935
  const pitchBend = e;
@@ -27604,15 +28081,15 @@ class PlaybackRangeChangedEventArgs {
27604
28081
  }
27605
28082
 
27606
28083
  /**
27607
- * This is the main synthesizer component which can be used to
28084
+ * This is the base class for synthesizer components which can be used to
27608
28085
  * play a {@link MidiFile} via a {@link ISynthOutput}.
27609
28086
  */
27610
- class AlphaSynth {
28087
+ class AlphaSynthBase {
27611
28088
  get output() {
27612
28089
  return this._output;
27613
28090
  }
27614
28091
  get isReadyForPlayback() {
27615
- return this.isReady && this._isSoundFontLoaded && this._isMidiLoaded;
28092
+ return this.isReady && this.isSoundFontLoaded && this._isMidiLoaded;
27616
28093
  }
27617
28094
  get logLevel() {
27618
28095
  return Logger.logLevel;
@@ -27621,11 +28098,14 @@ class AlphaSynth {
27621
28098
  Logger.logLevel = value;
27622
28099
  }
27623
28100
  get masterVolume() {
27624
- return this._synthesizer.masterVolume;
28101
+ return this.synthesizer.masterVolume;
27625
28102
  }
27626
28103
  set masterVolume(value) {
27627
28104
  value = Math.max(value, SynthConstants.MinVolume);
27628
- this._synthesizer.masterVolume = value;
28105
+ this.updateMasterVolume(value);
28106
+ }
28107
+ updateMasterVolume(value) {
28108
+ this.synthesizer.masterVolume = value;
27629
28109
  }
27630
28110
  get metronomeVolume() {
27631
28111
  return this._metronomeVolume;
@@ -27633,7 +28113,7 @@ class AlphaSynth {
27633
28113
  set metronomeVolume(value) {
27634
28114
  value = Math.max(value, SynthConstants.MinVolume);
27635
28115
  this._metronomeVolume = value;
27636
- this._synthesizer.metronomeVolume = value;
28116
+ this.synthesizer.metronomeVolume = value;
27637
28117
  }
27638
28118
  get countInVolume() {
27639
28119
  return this._countInVolume;
@@ -27649,19 +28129,22 @@ class AlphaSynth {
27649
28129
  this._midiEventsPlayedFilter = new Set(value);
27650
28130
  }
27651
28131
  get playbackSpeed() {
27652
- return this._sequencer.playbackSpeed;
28132
+ return this.sequencer.playbackSpeed;
27653
28133
  }
27654
28134
  set playbackSpeed(value) {
27655
28135
  value = ModelUtils.clamp(value, SynthConstants.MinPlaybackSpeed, SynthConstants.MaxPlaybackSpeed);
27656
- const oldSpeed = this._sequencer.playbackSpeed;
27657
- this._sequencer.playbackSpeed = value;
28136
+ this.updatePlaybackSpeed(value);
28137
+ }
28138
+ updatePlaybackSpeed(value) {
28139
+ const oldSpeed = this.sequencer.playbackSpeed;
28140
+ this.sequencer.playbackSpeed = value;
27658
28141
  this.timePosition = this.timePosition * (oldSpeed / value);
27659
28142
  }
27660
28143
  get tickPosition() {
27661
28144
  return this._tickPosition;
27662
28145
  }
27663
28146
  set tickPosition(value) {
27664
- this.timePosition = this._sequencer.mainTickPositionToTimePosition(value);
28147
+ this.timePosition = this.sequencer.mainTickPositionToTimePosition(value);
27665
28148
  }
27666
28149
  get timePosition() {
27667
28150
  return this._timePosition;
@@ -27669,30 +28152,30 @@ class AlphaSynth {
27669
28152
  set timePosition(value) {
27670
28153
  Logger.debug('AlphaSynth', `Seeking to position ${value}ms (main)`);
27671
28154
  // tell the sequencer to jump to the given position
27672
- this._sequencer.mainSeek(value);
28155
+ this.sequencer.mainSeek(value);
27673
28156
  // update the internal position
27674
28157
  this.updateTimePosition(value, true);
27675
28158
  // tell the output to reset the already synthesized buffers and request data again
27676
- if (this._sequencer.isPlayingMain) {
28159
+ if (this.sequencer.isPlayingMain) {
27677
28160
  this._notPlayedSamples = 0;
27678
28161
  this.output.resetSamples();
27679
28162
  }
27680
28163
  }
27681
28164
  get playbackRange() {
27682
- return this._sequencer.mainPlaybackRange;
28165
+ return this.sequencer.mainPlaybackRange;
27683
28166
  }
27684
28167
  set playbackRange(value) {
27685
- this._sequencer.mainPlaybackRange = value;
28168
+ this.sequencer.mainPlaybackRange = value;
27686
28169
  if (value) {
27687
28170
  this.tickPosition = value.startTick;
27688
28171
  }
27689
28172
  this.playbackRangeChanged.trigger(new PlaybackRangeChangedEventArgs(value));
27690
28173
  }
27691
28174
  get isLooping() {
27692
- return this._sequencer.isLooping;
28175
+ return this.sequencer.isLooping;
27693
28176
  }
27694
28177
  set isLooping(value) {
27695
- this._sequencer.isLooping = value;
28178
+ this.sequencer.isLooping = value;
27696
28179
  }
27697
28180
  destroy() {
27698
28181
  Logger.debug('AlphaSynth', 'Destroying player');
@@ -27700,11 +28183,11 @@ class AlphaSynth {
27700
28183
  this.output.destroy();
27701
28184
  }
27702
28185
  /**
27703
- * Initializes a new instance of the {@link AlphaSynth} class.
28186
+ * Initializes a new instance of the {@link AlphaSynthBase} class.
27704
28187
  * @param output The output to use for playing the generated samples.
27705
28188
  */
27706
- constructor(output, bufferTimeInMilliseconds) {
27707
- this._isSoundFontLoaded = false;
28189
+ constructor(output, synthesizer, bufferTimeInMilliseconds) {
28190
+ this.isSoundFontLoaded = false;
27708
28191
  this._isMidiLoaded = false;
27709
28192
  this._tickPosition = 0;
27710
28193
  this._timePosition = 0;
@@ -27733,8 +28216,8 @@ class AlphaSynth {
27733
28216
  Logger.debug('AlphaSynth', 'Creating output');
27734
28217
  this._output = output;
27735
28218
  Logger.debug('AlphaSynth', 'Creating synthesizer');
27736
- this._synthesizer = new TinySoundFont(this.output.sampleRate);
27737
- this._sequencer = new MidiFileSequencer(this._synthesizer);
28219
+ this.synthesizer = synthesizer;
28220
+ this.sequencer = new MidiFileSequencer(this.synthesizer);
27738
28221
  Logger.debug('AlphaSynth', 'Opening output');
27739
28222
  this.output.ready.on(() => {
27740
28223
  this.isReady = true;
@@ -27742,42 +28225,45 @@ class AlphaSynth {
27742
28225
  this.checkReadyForPlayback();
27743
28226
  });
27744
28227
  this.output.sampleRequest.on(() => {
27745
- if (this.state === PlayerState.Playing &&
27746
- (!this._sequencer.isFinished || this._synthesizer.activeVoiceCount > 0)) {
27747
- let samples = new Float32Array(SynthConstants.MicroBufferSize * SynthConstants.MicroBufferCount * SynthConstants.AudioChannels);
27748
- let bufferPos = 0;
27749
- for (let i = 0; i < SynthConstants.MicroBufferCount; i++) {
27750
- // synthesize buffer
27751
- this._sequencer.fillMidiEventQueue();
27752
- const synthesizedEvents = this._synthesizer.synthesize(samples, bufferPos, SynthConstants.MicroBufferSize);
27753
- bufferPos += SynthConstants.MicroBufferSize * SynthConstants.AudioChannels;
27754
- // push all processed events into the queue
27755
- // for informing users about played events
27756
- for (const e of synthesizedEvents) {
27757
- if (this._midiEventsPlayedFilter.has(e.event.type)) {
27758
- this._playedEventsQueue.enqueue(e);
27759
- }
27760
- }
27761
- // tell sequencer to check whether its work is done
27762
- if (this._sequencer.isFinished) {
27763
- break;
28228
+ this.onSampleRequest();
28229
+ });
28230
+ this.output.samplesPlayed.on(this.onSamplesPlayed.bind(this));
28231
+ this.output.open(bufferTimeInMilliseconds);
28232
+ }
28233
+ onSampleRequest() {
28234
+ if (this.state === PlayerState.Playing &&
28235
+ (!this.sequencer.isFinished || this.synthesizer.activeVoiceCount > 0)) {
28236
+ let samples = new Float32Array(SynthConstants.MicroBufferSize * SynthConstants.MicroBufferCount * SynthConstants.AudioChannels);
28237
+ let bufferPos = 0;
28238
+ for (let i = 0; i < SynthConstants.MicroBufferCount; i++) {
28239
+ // synthesize buffer
28240
+ this.sequencer.fillMidiEventQueue();
28241
+ const synthesizedEvents = this.synthesizer.synthesize(samples, bufferPos, SynthConstants.MicroBufferSize);
28242
+ bufferPos += SynthConstants.MicroBufferSize * SynthConstants.AudioChannels;
28243
+ // push all processed events into the queue
28244
+ // for informing users about played events
28245
+ for (const e of synthesizedEvents) {
28246
+ if (this._midiEventsPlayedFilter.has(e.event.type)) {
28247
+ this._playedEventsQueue.enqueue(e);
27764
28248
  }
27765
28249
  }
27766
- // send it to output
27767
- if (bufferPos < samples.length) {
27768
- samples = samples.subarray(0, bufferPos);
28250
+ // tell sequencer to check whether its work is done
28251
+ if (this.sequencer.isFinished) {
28252
+ break;
27769
28253
  }
27770
- this._notPlayedSamples += samples.length;
27771
- this.output.addSamples(samples);
27772
28254
  }
27773
- else {
27774
- // Tell output that there is no data left for it.
27775
- const samples = new Float32Array(0);
27776
- this.output.addSamples(samples);
28255
+ // send it to output
28256
+ if (bufferPos < samples.length) {
28257
+ samples = samples.subarray(0, bufferPos);
27777
28258
  }
27778
- });
27779
- this.output.samplesPlayed.on(this.onSamplesPlayed.bind(this));
27780
- this.output.open(bufferTimeInMilliseconds);
28259
+ this._notPlayedSamples += samples.length;
28260
+ this.output.addSamples(samples);
28261
+ }
28262
+ else {
28263
+ // Tell output that there is no data left for it.
28264
+ const samples = new Float32Array(0);
28265
+ this.output.addSamples(samples);
28266
+ }
27781
28267
  }
27782
28268
  play() {
27783
28269
  if (this.state !== PlayerState.Paused || !this._isMidiLoaded) {
@@ -27787,20 +28273,20 @@ class AlphaSynth {
27787
28273
  this.playInternal();
27788
28274
  if (this._countInVolume > 0) {
27789
28275
  Logger.debug('AlphaSynth', 'Starting countin');
27790
- this._sequencer.startCountIn();
27791
- this._synthesizer.setupMetronomeChannel(this._countInVolume);
28276
+ this.sequencer.startCountIn();
28277
+ this.synthesizer.setupMetronomeChannel(this._countInVolume);
27792
28278
  this.updateTimePosition(0, true);
27793
28279
  }
27794
28280
  this.output.play();
27795
28281
  return true;
27796
28282
  }
27797
28283
  playInternal() {
27798
- if (this._sequencer.isPlayingOneTimeMidi) {
28284
+ if (this.sequencer.isPlayingOneTimeMidi) {
27799
28285
  Logger.debug('AlphaSynth', 'Cancelling one time midi');
27800
28286
  this.stopOneTimeMidi();
27801
28287
  }
27802
28288
  Logger.debug('AlphaSynth', 'Starting playback');
27803
- this._synthesizer.setupMetronomeChannel(this.metronomeVolume);
28289
+ this.synthesizer.setupMetronomeChannel(this.metronomeVolume);
27804
28290
  this._synthStopping = false;
27805
28291
  this.state = PlayerState.Playing;
27806
28292
  this.stateChanged.trigger(new PlayerStateChangedEventArgs(this.state, false));
@@ -27813,7 +28299,7 @@ class AlphaSynth {
27813
28299
  this.state = PlayerState.Paused;
27814
28300
  this.stateChanged.trigger(new PlayerStateChangedEventArgs(this.state, false));
27815
28301
  this.output.pause();
27816
- this._synthesizer.noteOffAll(false);
28302
+ this.synthesizer.noteOffAll(false);
27817
28303
  }
27818
28304
  playPause() {
27819
28305
  if (this.state !== PlayerState.Paused || !this._isMidiLoaded) {
@@ -27831,21 +28317,21 @@ class AlphaSynth {
27831
28317
  this.state = PlayerState.Paused;
27832
28318
  this.output.pause();
27833
28319
  this._notPlayedSamples = 0;
27834
- this._sequencer.stop();
27835
- this._synthesizer.noteOffAll(true);
27836
- this.tickPosition = this._sequencer.mainPlaybackRange ? this._sequencer.mainPlaybackRange.startTick : 0;
28320
+ this.sequencer.stop();
28321
+ this.synthesizer.noteOffAll(true);
28322
+ this.tickPosition = this.sequencer.mainPlaybackRange ? this.sequencer.mainPlaybackRange.startTick : 0;
27837
28323
  this.stateChanged.trigger(new PlayerStateChangedEventArgs(this.state, true));
27838
28324
  }
27839
28325
  playOneTimeMidiFile(midi) {
27840
- if (this._sequencer.isPlayingOneTimeMidi) {
28326
+ if (this.sequencer.isPlayingOneTimeMidi) {
27841
28327
  this.stopOneTimeMidi();
27842
28328
  }
27843
28329
  else {
27844
28330
  // pause current playback.
27845
28331
  this.pause();
27846
28332
  }
27847
- this._sequencer.loadOneTimeMidi(midi);
27848
- this._synthesizer.noteOffAll(true);
28333
+ this.sequencer.loadOneTimeMidi(midi);
28334
+ this.synthesizer.noteOffAll(true);
27849
28335
  // update the internal position
27850
28336
  this.updateTimePosition(0, true);
27851
28337
  // tell the output to reset the already synthesized buffers and request data again
@@ -27855,9 +28341,9 @@ class AlphaSynth {
27855
28341
  }
27856
28342
  resetSoundFonts() {
27857
28343
  this.stop();
27858
- this._synthesizer.resetPresets();
28344
+ this.synthesizer.resetPresets();
27859
28345
  this._loadedSoundFonts = [];
27860
- this._isSoundFontLoaded = false;
28346
+ this.isSoundFontLoaded = false;
27861
28347
  this.soundFontLoaded.trigger();
27862
28348
  }
27863
28349
  loadSoundFont(data, append) {
@@ -27871,7 +28357,7 @@ class AlphaSynth {
27871
28357
  this._loadedSoundFonts = [];
27872
28358
  }
27873
28359
  this._loadedSoundFonts.push(soundFont);
27874
- this._isSoundFontLoaded = true;
28360
+ this.isSoundFontLoaded = true;
27875
28361
  this.soundFontLoaded.trigger();
27876
28362
  Logger.debug('AlphaSynth', 'soundFont successfully loaded');
27877
28363
  this.checkReadyForPlayback();
@@ -27883,12 +28369,12 @@ class AlphaSynth {
27883
28369
  }
27884
28370
  checkReadyForPlayback() {
27885
28371
  if (this.isReadyForPlayback) {
27886
- this._synthesizer.setupMetronomeChannel(this.metronomeVolume);
27887
- const programs = this._sequencer.instrumentPrograms;
27888
- const percussionKeys = this._sequencer.percussionKeys;
28372
+ this.synthesizer.setupMetronomeChannel(this.metronomeVolume);
28373
+ const programs = this.sequencer.instrumentPrograms;
28374
+ const percussionKeys = this.sequencer.percussionKeys;
27889
28375
  let append = false;
27890
28376
  for (const soundFont of this._loadedSoundFonts) {
27891
- this._synthesizer.loadPresets(soundFont, programs, percussionKeys, append);
28377
+ this.synthesizer.loadPresets(soundFont, programs, percussionKeys, append);
27892
28378
  append = true;
27893
28379
  }
27894
28380
  this.readyForPlayback.trigger();
@@ -27902,9 +28388,9 @@ class AlphaSynth {
27902
28388
  this.stop();
27903
28389
  try {
27904
28390
  Logger.debug('AlphaSynth', 'Loading midi from model');
27905
- this._sequencer.loadMidi(midi);
28391
+ this.sequencer.loadMidi(midi);
27906
28392
  this._isMidiLoaded = true;
27907
- this.midiLoaded.trigger(new PositionChangedEventArgs(0, this._sequencer.currentEndTime, 0, this._sequencer.currentEndTick, false));
28393
+ this.midiLoaded.trigger(new PositionChangedEventArgs(0, this.sequencer.currentEndTime, 0, this.sequencer.currentEndTick, false, this.sequencer.currentTempo, this.sequencer.modifiedTempo));
27908
28394
  Logger.debug('AlphaSynth', 'Midi successfully loaded');
27909
28395
  this.checkReadyForPlayback();
27910
28396
  this.tickPosition = 0;
@@ -27915,29 +28401,29 @@ class AlphaSynth {
27915
28401
  }
27916
28402
  }
27917
28403
  applyTranspositionPitches(transpositionPitches) {
27918
- this._synthesizer.applyTranspositionPitches(transpositionPitches);
28404
+ this.synthesizer.applyTranspositionPitches(transpositionPitches);
27919
28405
  }
27920
28406
  setChannelTranspositionPitch(channel, semitones) {
27921
- this._synthesizer.setChannelTranspositionPitch(channel, semitones);
28407
+ this.synthesizer.setChannelTranspositionPitch(channel, semitones);
27922
28408
  }
27923
28409
  setChannelMute(channel, mute) {
27924
- this._synthesizer.channelSetMute(channel, mute);
28410
+ this.synthesizer.channelSetMute(channel, mute);
27925
28411
  }
27926
28412
  resetChannelStates() {
27927
- this._synthesizer.resetChannelStates();
28413
+ this.synthesizer.resetChannelStates();
27928
28414
  }
27929
28415
  setChannelSolo(channel, solo) {
27930
- this._synthesizer.channelSetSolo(channel, solo);
28416
+ this.synthesizer.channelSetSolo(channel, solo);
27931
28417
  }
27932
28418
  setChannelVolume(channel, volume) {
27933
28419
  volume = Math.max(volume, SynthConstants.MinVolume);
27934
- this._synthesizer.channelSetMixVolume(channel, volume);
28420
+ this.synthesizer.channelSetMixVolume(channel, volume);
27935
28421
  }
27936
28422
  onSamplesPlayed(sampleCount) {
27937
28423
  if (sampleCount === 0) {
27938
28424
  return;
27939
28425
  }
27940
- const playedMillis = (sampleCount / this._synthesizer.outSampleRate) * 1000;
28426
+ const playedMillis = (sampleCount / this.synthesizer.outSampleRate) * 1000;
27941
28427
  this._notPlayedSamples -= sampleCount * SynthConstants.AudioChannels;
27942
28428
  this.updateTimePosition(this._timePosition + playedMillis, false);
27943
28429
  this.checkForFinish();
@@ -27945,25 +28431,25 @@ class AlphaSynth {
27945
28431
  checkForFinish() {
27946
28432
  let startTick = 0;
27947
28433
  let endTick = 0;
27948
- if (this.playbackRange && this._sequencer.isPlayingMain) {
28434
+ if (this.playbackRange && this.sequencer.isPlayingMain) {
27949
28435
  startTick = this.playbackRange.startTick;
27950
28436
  endTick = this.playbackRange.endTick;
27951
28437
  }
27952
28438
  else {
27953
- endTick = this._sequencer.currentEndTick;
28439
+ endTick = this.sequencer.currentEndTick;
27954
28440
  }
27955
28441
  if (this._tickPosition >= endTick) {
27956
28442
  // fully done with playback of remaining samples?
27957
28443
  if (this._notPlayedSamples <= 0) {
27958
28444
  this._notPlayedSamples = 0;
27959
- if (this._sequencer.isPlayingCountIn) {
28445
+ if (this.sequencer.isPlayingCountIn) {
27960
28446
  Logger.debug('AlphaSynth', 'Finished playback (count-in)');
27961
- this._sequencer.resetCountIn();
27962
- this.timePosition = this._sequencer.currentTime;
28447
+ this.sequencer.resetCountIn();
28448
+ this.timePosition = this.sequencer.currentTime;
27963
28449
  this.playInternal();
27964
28450
  this.output.resetSamples();
27965
28451
  }
27966
- else if (this._sequencer.isPlayingOneTimeMidi) {
28452
+ else if (this.sequencer.isPlayingOneTimeMidi) {
27967
28453
  Logger.debug('AlphaSynth', 'Finished playback (one time)');
27968
28454
  this.output.resetSamples();
27969
28455
  this.state = PlayerState.Paused;
@@ -27975,11 +28461,11 @@ class AlphaSynth {
27975
28461
  this.tickPosition = startTick;
27976
28462
  this._synthStopping = false;
27977
28463
  }
27978
- else if (this._synthesizer.activeVoiceCount > 0) {
28464
+ else if (this.synthesizer.activeVoiceCount > 0) {
27979
28465
  // smooth stop
27980
28466
  if (!this._synthStopping) {
27981
28467
  Logger.debug('AlphaSynth', 'Signaling synth to stop all voices (all samples played)');
27982
- this._synthesizer.noteOffAll(true);
28468
+ this.synthesizer.noteOffAll(true);
27983
28469
  this._synthStopping = true;
27984
28470
  }
27985
28471
  }
@@ -27995,7 +28481,7 @@ class AlphaSynth {
27995
28481
  // to eventually bring the voices down to 0 and stop playing
27996
28482
  if (!this._synthStopping) {
27997
28483
  Logger.debug('AlphaSynth', 'Signaling synth to stop all voices (not all samples played)');
27998
- this._synthesizer.noteOffAll(true);
28484
+ this.synthesizer.noteOffAll(true);
27999
28485
  this._synthStopping = true;
28000
28486
  }
28001
28487
  }
@@ -28003,31 +28489,27 @@ class AlphaSynth {
28003
28489
  }
28004
28490
  stopOneTimeMidi() {
28005
28491
  this.output.pause();
28006
- this._synthesizer.noteOffAll(true);
28007
- this._sequencer.resetOneTimeMidi();
28008
- this.timePosition = this._sequencer.currentTime;
28492
+ this.synthesizer.noteOffAll(true);
28493
+ this.sequencer.resetOneTimeMidi();
28494
+ this.timePosition = this.sequencer.currentTime;
28009
28495
  }
28010
28496
  updateTimePosition(timePosition, isSeek) {
28011
28497
  // update the real positions
28012
28498
  let currentTime = timePosition;
28013
28499
  this._timePosition = currentTime;
28014
- let currentTick = this._sequencer.currentTimePositionToTickPosition(currentTime);
28500
+ let currentTick = this.sequencer.currentTimePositionToTickPosition(currentTime);
28015
28501
  this._tickPosition = currentTick;
28016
- const endTime = this._sequencer.currentEndTime;
28017
- const endTick = this._sequencer.currentEndTick;
28502
+ const endTime = this.sequencer.currentEndTime;
28503
+ const endTick = this.sequencer.currentEndTick;
28018
28504
  // on fade outs we can have some milliseconds longer, ensure we don't report this
28019
28505
  if (currentTime > endTime) {
28020
28506
  currentTime = endTime;
28021
28507
  currentTick = endTick;
28022
28508
  }
28023
- const mode = this._sequencer.isPlayingMain
28024
- ? 'main'
28025
- : this._sequencer.isPlayingCountIn
28026
- ? 'count-in'
28027
- : 'one-time';
28028
- Logger.debug('AlphaSynth', `Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this._synthesizer.activeVoiceCount} (${mode})`);
28029
- if (this._sequencer.isPlayingMain) {
28030
- this.positionChanged.trigger(new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick, isSeek));
28509
+ const mode = this.sequencer.isPlayingMain ? 'main' : this.sequencer.isPlayingCountIn ? 'count-in' : 'one-time';
28510
+ Logger.debug('AlphaSynth', `Position changed: (time: ${currentTime}/${endTime}, tick: ${currentTick}/${endTick}, Active Voices: ${this.synthesizer.activeVoiceCount} (${mode}), Tempo original: ${this.sequencer.currentTempo}, Tempo modified: ${this.sequencer.modifiedTempo})`);
28511
+ if (this.sequencer.isPlayingMain) {
28512
+ this.positionChanged.trigger(new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick, isSeek, this.sequencer.currentTempo, this.sequencer.modifiedTempo));
28031
28513
  }
28032
28514
  // build events which were actually played
28033
28515
  if (isSeek) {
@@ -28048,13 +28530,28 @@ class AlphaSynth {
28048
28530
  * @internal
28049
28531
  */
28050
28532
  hasSamplesForProgram(program) {
28051
- return this._synthesizer.hasSamplesForProgram(program);
28533
+ return this.synthesizer.hasSamplesForProgram(program);
28052
28534
  }
28053
28535
  /**
28054
28536
  * @internal
28055
28537
  */
28056
28538
  hasSamplesForPercussion(key) {
28057
- return this._synthesizer.hasSamplesForPercussion(key);
28539
+ return this.synthesizer.hasSamplesForPercussion(key);
28540
+ }
28541
+ loadBackingTrack(_score, _syncPoints) {
28542
+ }
28543
+ }
28544
+ /**
28545
+ * This is the main synthesizer component which can be used to
28546
+ * play a {@link MidiFile} via a {@link ISynthOutput}.
28547
+ */
28548
+ class AlphaSynth extends AlphaSynthBase {
28549
+ /**
28550
+ * Initializes a new instance of the {@link AlphaSynth} class.
28551
+ * @param output The output to use for playing the generated samples.
28552
+ */
28553
+ constructor(output, bufferTimeInMilliseconds) {
28554
+ super(output, new TinySoundFont(output.sampleRate), bufferTimeInMilliseconds);
28058
28555
  }
28059
28556
  }
28060
28557
 
@@ -29301,6 +29798,35 @@ var PlayerOutputMode;
29301
29798
  */
29302
29799
  PlayerOutputMode[PlayerOutputMode["WebAudioScriptProcessor"] = 1] = "WebAudioScriptProcessor";
29303
29800
  })(PlayerOutputMode || (PlayerOutputMode = {}));
29801
+ /**
29802
+ * Lists the different modes how the internal alphaTab player (and related cursor behavior) is working.
29803
+ */
29804
+ var PlayerMode;
29805
+ (function (PlayerMode) {
29806
+ /**
29807
+ * The player functionality is fully disabled.
29808
+ */
29809
+ PlayerMode[PlayerMode["Disabled"] = 0] = "Disabled";
29810
+ /**
29811
+ * The player functionality is enabled.
29812
+ * If the loaded file provides a backing track, it is used for playback.
29813
+ * If no backing track is provided, the midi synthesizer is used.
29814
+ */
29815
+ PlayerMode[PlayerMode["EnabledAutomatic"] = 1] = "EnabledAutomatic";
29816
+ /**
29817
+ * The player functionality is enabled and the synthesizer is used (even if a backing track is embedded in the file).
29818
+ */
29819
+ PlayerMode[PlayerMode["EnabledSynthesizer"] = 2] = "EnabledSynthesizer";
29820
+ /**
29821
+ * The player functionality is enabled. If the input data model has no backing track configured, the player might not work as expected (as playback completes instantly).
29822
+ */
29823
+ PlayerMode[PlayerMode["EnabledBackingTrack"] = 3] = "EnabledBackingTrack";
29824
+ /**
29825
+ * The player functionality is enabled and an external audio/video source is used as time axis.
29826
+ * The related player APIs need to be used to update the current position of the external audio source within alphaTab.
29827
+ */
29828
+ PlayerMode[PlayerMode["EnabledExternalMedia"] = 4] = "EnabledExternalMedia";
29829
+ })(PlayerMode || (PlayerMode = {}));
29304
29830
  /**
29305
29831
  * The player settings control how the audio playback and UI is behaving.
29306
29832
  * @json
@@ -29347,6 +29873,7 @@ class PlayerSettings {
29347
29873
  * @since 0.9.6
29348
29874
  * @defaultValue `false`
29349
29875
  * @category Player
29876
+ * @deprecated Use {@link playerMode} instead.
29350
29877
  * @remarks
29351
29878
  * This setting configures whether the player feature is enabled or not. Depending on the platform enabling the player needs some additional actions of the developer.
29352
29879
  * For the JavaScript version the [player.soundFont](/docs/reference/settings/player/soundfont) property must be set to the URL of the sound font that should be used or it must be loaded manually via API.
@@ -29355,6 +29882,37 @@ class PlayerSettings {
29355
29882
  * AlphaTab does not ship a default UI for the player. The API must be hooked up to some UI controls to allow the user to interact with the player.
29356
29883
  */
29357
29884
  this.enablePlayer = false;
29885
+ /**
29886
+ * Whether the player should be enabled and which mode it should use.
29887
+ * @since 1.6.0
29888
+ * @defaultValue `PlayerMode.Disabled`
29889
+ * @category Player
29890
+ * @remarks
29891
+ * This setting configures whether the player feature is enabled or not. Depending on the platform enabling the player needs some additional actions of the developer.
29892
+ *
29893
+ * **Synthesizer**
29894
+ *
29895
+ * If the synthesizer is used (via {@link PlayerMode.EnabledAutomatic} or {@link PlayerMode.EnabledSynthesizer}) a sound font is needed so that the midi synthesizer can produce the audio samples.
29896
+ *
29897
+ * For the JavaScript version the [player.soundFont](/docs/reference/settings/player/soundfont) property must be set to the URL of the sound font that should be used or it must be loaded manually via API.
29898
+ * For .net manually the soundfont must be loaded.
29899
+ *
29900
+ * **Backing Track**
29901
+ *
29902
+ * For a built-in backing track of the input file no additional data needs to be loaded (assuming everything is filled via the input file).
29903
+ * Otherwise the `score.backingTrack` needs to be filled before loading and the related sync points need to be configured.
29904
+ *
29905
+ * **External Media**
29906
+ *
29907
+ * For synchronizing alphaTab with an external media no data needs to be loaded into alphaTab. The configured sync points on the MasterBars are used
29908
+ * as reference to synchronize the external media with the internal time axis. Then the related APIs on the AlphaTabApi object need to be used
29909
+ * to update the playback state and exterrnal audio position during playback.
29910
+ *
29911
+ * **User Interface**
29912
+ *
29913
+ * AlphaTab does not ship a default UI for the player. The API must be hooked up to some UI controls to allow the user to interact with the player.
29914
+ */
29915
+ this.playerMode = PlayerMode.Disabled;
29358
29916
  /**
29359
29917
  * Whether playback cursors should be displayed.
29360
29918
  * @since 0.9.6
@@ -30060,6 +30618,7 @@ class PlayerSettingsSerializer {
30060
30618
  /*@target web*/
30061
30619
  o.set("outputmode", obj.outputMode);
30062
30620
  o.set("enableplayer", obj.enablePlayer);
30621
+ o.set("playermode", obj.playerMode);
30063
30622
  o.set("enablecursor", obj.enableCursor);
30064
30623
  o.set("enableanimatedbeatcursor", obj.enableAnimatedBeatCursor);
30065
30624
  o.set("enableelementhighlighting", obj.enableElementHighlighting);
@@ -30095,6 +30654,9 @@ class PlayerSettingsSerializer {
30095
30654
  case "enableplayer":
30096
30655
  obj.enablePlayer = v;
30097
30656
  return true;
30657
+ case "playermode":
30658
+ obj.playerMode = JsonHelper.parseEnum(v, PlayerMode);
30659
+ return true;
30098
30660
  case "enablecursor":
30099
30661
  obj.enableCursor = v;
30100
30662
  return true;
@@ -30329,6 +30891,39 @@ class SectionSerializer {
30329
30891
  }
30330
30892
  }
30331
30893
 
30894
+ class SyncPointDataSerializer {
30895
+ static fromJson(obj, m) {
30896
+ if (!m) {
30897
+ return;
30898
+ }
30899
+ JsonHelper.forEach(m, (v, k) => SyncPointDataSerializer.setProperty(obj, k, v));
30900
+ }
30901
+ static toJson(obj) {
30902
+ if (!obj) {
30903
+ return null;
30904
+ }
30905
+ const o = new Map();
30906
+ o.set("baroccurence", obj.barOccurence);
30907
+ o.set("modifiedtempo", obj.modifiedTempo);
30908
+ o.set("millisecondoffset", obj.millisecondOffset);
30909
+ return o;
30910
+ }
30911
+ static setProperty(obj, property, v) {
30912
+ switch (property) {
30913
+ case "baroccurence":
30914
+ obj.barOccurence = v;
30915
+ return true;
30916
+ case "modifiedtempo":
30917
+ obj.modifiedTempo = v;
30918
+ return true;
30919
+ case "millisecondoffset":
30920
+ obj.millisecondOffset = v;
30921
+ return true;
30922
+ }
30923
+ return false;
30924
+ }
30925
+ }
30926
+
30332
30927
  class AutomationSerializer {
30333
30928
  static fromJson(obj, m) {
30334
30929
  if (!m) {
@@ -30344,6 +30939,9 @@ class AutomationSerializer {
30344
30939
  o.set("islinear", obj.isLinear);
30345
30940
  o.set("type", obj.type);
30346
30941
  o.set("value", obj.value);
30942
+ if (obj.syncPointValue) {
30943
+ o.set("syncpointvalue", SyncPointDataSerializer.toJson(obj.syncPointValue));
30944
+ }
30347
30945
  o.set("ratioposition", obj.ratioPosition);
30348
30946
  o.set("text", obj.text);
30349
30947
  return o;
@@ -30359,6 +30957,15 @@ class AutomationSerializer {
30359
30957
  case "value":
30360
30958
  obj.value = v;
30361
30959
  return true;
30960
+ case "syncpointvalue":
30961
+ if (v) {
30962
+ obj.syncPointValue = new SyncPointData();
30963
+ SyncPointDataSerializer.fromJson(obj.syncPointValue, v);
30964
+ }
30965
+ else {
30966
+ obj.syncPointValue = undefined;
30967
+ }
30968
+ return true;
30362
30969
  case "ratioposition":
30363
30970
  obj.ratioPosition = v;
30364
30971
  return true;
@@ -30424,6 +31031,9 @@ class MasterBarSerializer {
30424
31031
  o.set("section", SectionSerializer.toJson(obj.section));
30425
31032
  }
30426
31033
  o.set("tempoautomations", obj.tempoAutomations.map(i => AutomationSerializer.toJson(i)));
31034
+ if (obj.syncPoints !== undefined) {
31035
+ o.set("syncpoints", obj.syncPoints?.map(i => AutomationSerializer.toJson(i)));
31036
+ }
30427
31037
  if (obj.fermata !== null) {
30428
31038
  const m = new Map();
30429
31039
  o.set("fermata", m);
@@ -30490,6 +31100,16 @@ class MasterBarSerializer {
30490
31100
  obj.tempoAutomations.push(i);
30491
31101
  }
30492
31102
  return true;
31103
+ case "syncpoints":
31104
+ if (v) {
31105
+ obj.syncPoints = [];
31106
+ for (const o of v) {
31107
+ const i = new Automation();
31108
+ AutomationSerializer.fromJson(i, o);
31109
+ obj.addSyncPoint(i);
31110
+ }
31111
+ }
31112
+ return true;
30493
31113
  case "fermata":
30494
31114
  obj.fermata = new Map();
30495
31115
  JsonHelper.forEach(v, (v, k) => {
@@ -31781,6 +32401,31 @@ class RenderStylesheetSerializer {
31781
32401
  }
31782
32402
  }
31783
32403
 
32404
+ class BackingTrackSerializer {
32405
+ static fromJson(obj, m) {
32406
+ if (!m) {
32407
+ return;
32408
+ }
32409
+ JsonHelper.forEach(m, (v, k) => BackingTrackSerializer.setProperty(obj, k, v));
32410
+ }
32411
+ static toJson(obj) {
32412
+ if (!obj) {
32413
+ return null;
32414
+ }
32415
+ const o = new Map();
32416
+ o.set("padding", obj.padding);
32417
+ return o;
32418
+ }
32419
+ static setProperty(obj, property, v) {
32420
+ switch (property) {
32421
+ case "padding":
32422
+ obj.padding = v;
32423
+ return true;
32424
+ }
32425
+ return false;
32426
+ }
32427
+ }
32428
+
31784
32429
  class HeaderFooterStyleSerializer {
31785
32430
  static fromJson(obj, m) {
31786
32431
  if (!m) {
@@ -31892,6 +32537,9 @@ class ScoreSerializer {
31892
32537
  o.set("defaultsystemslayout", obj.defaultSystemsLayout);
31893
32538
  o.set("systemslayout", obj.systemsLayout);
31894
32539
  o.set("stylesheet", RenderStylesheetSerializer.toJson(obj.stylesheet));
32540
+ if (obj.backingTrack) {
32541
+ o.set("backingtrack", BackingTrackSerializer.toJson(obj.backingTrack));
32542
+ }
31895
32543
  if (obj.style) {
31896
32544
  o.set("style", ScoreStyleSerializer.toJson(obj.style));
31897
32545
  }
@@ -31960,6 +32608,15 @@ class ScoreSerializer {
31960
32608
  case "stylesheet":
31961
32609
  RenderStylesheetSerializer.fromJson(obj.stylesheet, v);
31962
32610
  return true;
32611
+ case "backingtrack":
32612
+ if (v) {
32613
+ obj.backingTrack = new BackingTrack();
32614
+ BackingTrackSerializer.fromJson(obj.backingTrack, v);
32615
+ }
32616
+ else {
32617
+ obj.backingTrack = undefined;
32618
+ }
32619
+ return true;
31963
32620
  case "style":
31964
32621
  if (v) {
31965
32622
  obj.style = new ScoreStyle();
@@ -32136,7 +32793,9 @@ class JsonConverter {
32136
32793
  case MidiEventType.ProgramChange:
32137
32794
  return new ProgramChangeEvent(track, tick, JsonHelper.getValue(midiEvent, 'channel'), JsonHelper.getValue(midiEvent, 'program'));
32138
32795
  case MidiEventType.TempoChange:
32139
- return new TempoChangeEvent(tick, JsonHelper.getValue(midiEvent, 'microSecondsPerQuarterNote'));
32796
+ const tempo = new TempoChangeEvent(tick, 0);
32797
+ tempo.beatsPerMinute = JsonHelper.getValue(midiEvent, 'beatsPerMinute');
32798
+ return tempo;
32140
32799
  case MidiEventType.PitchBend:
32141
32800
  return new PitchBendEvent(track, tick, JsonHelper.getValue(midiEvent, 'channel'), JsonHelper.getValue(midiEvent, 'value'));
32142
32801
  case MidiEventType.PerNotePitchBend:
@@ -32211,7 +32870,7 @@ class JsonConverter {
32211
32870
  o.set('program', midiEvent.program);
32212
32871
  break;
32213
32872
  case MidiEventType.TempoChange:
32214
- o.set('microSecondsPerQuarterNote', midiEvent.microSecondsPerQuarterNote);
32873
+ o.set('beatsPerMinute', midiEvent.beatsPerMinute);
32215
32874
  break;
32216
32875
  case MidiEventType.PitchBend:
32217
32876
  o.set('channel', midiEvent.channel);
@@ -32438,7 +33097,9 @@ class AlphaSynthWebWorker {
32438
33097
  endTime: e.endTime,
32439
33098
  currentTick: e.currentTick,
32440
33099
  endTick: e.endTick,
32441
- isSeek: e.isSeek
33100
+ isSeek: e.isSeek,
33101
+ originalTempo: e.originalTempo,
33102
+ modifiedTempo: e.modifiedTempo
32442
33103
  });
32443
33104
  }
32444
33105
  onPlayerStateChanged(e) {
@@ -32484,7 +33145,9 @@ class AlphaSynthWebWorker {
32484
33145
  endTime: e.endTime,
32485
33146
  currentTick: e.currentTick,
32486
33147
  endTick: e.endTick,
32487
- isSeek: e.isSeek
33148
+ isSeek: e.isSeek,
33149
+ originalTempo: e.originalTempo,
33150
+ modifiedTempo: e.modifiedTempo
32488
33151
  });
32489
33152
  }
32490
33153
  onMidiLoadFailed(e) {
@@ -33793,8 +34456,9 @@ class AlphaSynthMidiFileHandler {
33793
34456
  }
33794
34457
  addTempo(tick, tempo) {
33795
34458
  // bpm -> microsecond per quarter note
33796
- const tempoInUsq = (60000000 / tempo) | 0;
33797
- this._midiFile.addEvent(new TempoChangeEvent(tick, tempoInUsq));
34459
+ const tempoEvent = new TempoChangeEvent(tick, 0);
34460
+ tempoEvent.beatsPerMinute = tempo;
34461
+ this._midiFile.addEvent(tempoEvent);
33798
34462
  }
33799
34463
  addBend(track, tick, channel, value) {
33800
34464
  if (value >= SynthConstants.MaxPitchWheel) {
@@ -35066,6 +35730,10 @@ class MidiFileGenerator {
35066
35730
  * Gets or sets whether transposition pitches should be applied to the individual midi events or not.
35067
35731
  */
35068
35732
  this.applyTranspositionPitches = true;
35733
+ /**
35734
+ * The computed sync points for synchronizing the midi file with an external backing track.
35735
+ */
35736
+ this.syncPoints = [];
35069
35737
  /**
35070
35738
  * Gets the transposition pitches for the individual midi channels.
35071
35739
  */
@@ -35092,13 +35760,17 @@ class MidiFileGenerator {
35092
35760
  let previousMasterBar = null;
35093
35761
  let currentTempo = this._score.tempo;
35094
35762
  // store the previous played bar for repeats
35763
+ const barOccurence = new Map();
35095
35764
  while (!controller.finished) {
35096
35765
  const index = controller.index;
35097
35766
  const bar = this._score.masterBars[index];
35098
35767
  const currentTick = controller.currentTick;
35099
35768
  controller.processCurrent();
35100
35769
  if (controller.shouldPlay) {
35101
- this.generateMasterBar(bar, previousMasterBar, currentTick, currentTempo);
35770
+ let occurence = barOccurence.has(index) ? barOccurence.get(index) : -1;
35771
+ occurence++;
35772
+ barOccurence.set(index, occurence);
35773
+ this.generateMasterBar(bar, previousMasterBar, currentTick, currentTempo, occurence);
35102
35774
  if (bar.tempoAutomations.length > 0) {
35103
35775
  currentTempo = bar.tempoAutomations[0].value;
35104
35776
  }
@@ -35167,7 +35839,7 @@ class MidiFileGenerator {
35167
35839
  const value = Math.max(-32768, Math.min(32767, data * 8 - 1));
35168
35840
  return Math.max(value, -1) + 1;
35169
35841
  }
35170
- generateMasterBar(masterBar, previousMasterBar, currentTick, currentTempo) {
35842
+ generateMasterBar(masterBar, previousMasterBar, currentTick, currentTempo, barOccurence) {
35171
35843
  // time signature
35172
35844
  if (!previousMasterBar ||
35173
35845
  previousMasterBar.timeSignatureDenominator !== masterBar.timeSignatureDenominator ||
@@ -35194,6 +35866,15 @@ class MidiFileGenerator {
35194
35866
  else {
35195
35867
  masterBarLookup.tempoChanges.push(new MasterBarTickLookupTempoChange(currentTick, currentTempo));
35196
35868
  }
35869
+ const syncPoints = masterBar.syncPoints;
35870
+ if (syncPoints) {
35871
+ for (const syncPoint of syncPoints) {
35872
+ if (syncPoint.syncPointValue.barOccurence === barOccurence) {
35873
+ const tick = currentTick + masterBarDuration * syncPoint.ratioPosition;
35874
+ this.syncPoints.push(new BackingTrackSyncPoint(tick, syncPoint.syncPointValue));
35875
+ }
35876
+ }
35877
+ }
35197
35878
  masterBarLookup.masterBar = masterBar;
35198
35879
  masterBarLookup.start = currentTick;
35199
35880
  masterBarLookup.end = masterBarLookup.start + masterBarDuration;
@@ -37391,6 +38072,213 @@ class ActiveBeatsChangedEventArgs {
37391
38072
  }
37392
38073
  }
37393
38074
 
38075
+ class BackingTrackAudioSynthesizer {
38076
+ constructor() {
38077
+ this._midiEventQueue = new Queue();
38078
+ this.masterVolume = 1;
38079
+ this.metronomeVolume = 0;
38080
+ this.outSampleRate = 44100;
38081
+ this.currentTempo = 120;
38082
+ this.timeSignatureNumerator = 4;
38083
+ this.timeSignatureDenominator = 4;
38084
+ this.activeVoiceCount = 0;
38085
+ }
38086
+ noteOffAll(_immediate) {
38087
+ }
38088
+ resetSoft() {
38089
+ }
38090
+ resetPresets() {
38091
+ }
38092
+ loadPresets(_hydra, _instrumentPrograms, _percussionKeys, _append) {
38093
+ }
38094
+ setupMetronomeChannel(_metronomeVolume) {
38095
+ }
38096
+ synthesizeSilent(_sampleCount) {
38097
+ this.fakeSynthesize();
38098
+ }
38099
+ processMidiMessage(e) {
38100
+ }
38101
+ dispatchEvent(synthEvent) {
38102
+ this._midiEventQueue.enqueue(synthEvent);
38103
+ }
38104
+ synthesize(_buffer, _bufferPos, _sampleCount) {
38105
+ return this.fakeSynthesize();
38106
+ }
38107
+ fakeSynthesize() {
38108
+ const processedEvents = [];
38109
+ while (!this._midiEventQueue.isEmpty) {
38110
+ const m = this._midiEventQueue.dequeue();
38111
+ if (m.isMetronome && this.metronomeVolume > 0) ;
38112
+ else if (m.event) {
38113
+ this.processMidiMessage(m.event);
38114
+ }
38115
+ processedEvents.push(m);
38116
+ }
38117
+ return processedEvents;
38118
+ }
38119
+ applyTranspositionPitches(transpositionPitches) {
38120
+ }
38121
+ setChannelTranspositionPitch(channel, semitones) {
38122
+ }
38123
+ channelSetMute(channel, mute) {
38124
+ }
38125
+ channelSetSolo(channel, solo) {
38126
+ }
38127
+ resetChannelStates() {
38128
+ }
38129
+ channelSetMixVolume(channel, volume) {
38130
+ }
38131
+ hasSamplesForProgram(program) {
38132
+ return true;
38133
+ }
38134
+ hasSamplesForPercussion(key) {
38135
+ return true;
38136
+ }
38137
+ }
38138
+ class BackingTrackPlayer extends AlphaSynthBase {
38139
+ constructor(backingTrackOutput, bufferTimeInMilliseconds) {
38140
+ super(backingTrackOutput, new BackingTrackAudioSynthesizer(), bufferTimeInMilliseconds);
38141
+ this.synthesizer.output = backingTrackOutput;
38142
+ this._backingTrackOutput = backingTrackOutput;
38143
+ backingTrackOutput.timeUpdate.on(timePosition => {
38144
+ const alphaTabTimePosition = this.sequencer.mainTimePositionFromBackingTrack(timePosition, backingTrackOutput.backingTrackDuration);
38145
+ this.sequencer.fillMidiEventQueueToEndTime(alphaTabTimePosition);
38146
+ this.synthesizer.fakeSynthesize();
38147
+ this.updateTimePosition(alphaTabTimePosition, false);
38148
+ this.checkForFinish();
38149
+ });
38150
+ }
38151
+ updateMasterVolume(value) {
38152
+ super.updateMasterVolume(value);
38153
+ this._backingTrackOutput.masterVolume = value;
38154
+ }
38155
+ updatePlaybackSpeed(value) {
38156
+ super.updatePlaybackSpeed(value);
38157
+ this._backingTrackOutput.playbackRate = value;
38158
+ }
38159
+ onSampleRequest() {
38160
+ }
38161
+ loadMidiFile(midi) {
38162
+ if (!this.isSoundFontLoaded) {
38163
+ this.isSoundFontLoaded = true;
38164
+ this.soundFontLoaded.trigger();
38165
+ }
38166
+ super.loadMidiFile(midi);
38167
+ }
38168
+ updateTimePosition(timePosition, isSeek) {
38169
+ super.updateTimePosition(timePosition, isSeek);
38170
+ if (isSeek) {
38171
+ this._backingTrackOutput.seekTo(this.sequencer.mainTimePositionToBackingTrack(timePosition, this._backingTrackOutput.backingTrackDuration));
38172
+ }
38173
+ }
38174
+ loadBackingTrack(score, syncPoints) {
38175
+ const backingTrackInfo = score.backingTrack;
38176
+ if (backingTrackInfo) {
38177
+ this._backingTrackOutput.loadBackingTrack(backingTrackInfo);
38178
+ this.sequencer.mainUpdateSyncPoints(syncPoints);
38179
+ this.timePosition = 0;
38180
+ }
38181
+ }
38182
+ }
38183
+
38184
+ class ExternalMediaSynthOutput {
38185
+ constructor() {
38186
+ // fake rate
38187
+ this.sampleRate = 44100;
38188
+ this._padding = 0;
38189
+ this._seekPosition = 0;
38190
+ this.ready = new EventEmitter();
38191
+ this.samplesPlayed = new EventEmitterOfT();
38192
+ this.timeUpdate = new EventEmitterOfT();
38193
+ this.sampleRequest = new EventEmitter();
38194
+ }
38195
+ get handler() {
38196
+ return this._handler;
38197
+ }
38198
+ set handler(value) {
38199
+ if (value) {
38200
+ if (this._seekPosition !== 0) {
38201
+ value.seekTo(this._seekPosition);
38202
+ this._seekPosition = 0;
38203
+ }
38204
+ }
38205
+ this._handler = value;
38206
+ }
38207
+ get backingTrackDuration() {
38208
+ return this.handler?.backingTrackDuration ?? 0;
38209
+ }
38210
+ get playbackRate() {
38211
+ return this.handler?.playbackRate ?? 1;
38212
+ }
38213
+ set playbackRate(value) {
38214
+ const handler = this.handler;
38215
+ if (handler) {
38216
+ handler.playbackRate = value;
38217
+ }
38218
+ }
38219
+ get masterVolume() {
38220
+ return this.handler?.masterVolume ?? 1;
38221
+ }
38222
+ set masterVolume(value) {
38223
+ const handler = this.handler;
38224
+ if (handler) {
38225
+ handler.masterVolume = value;
38226
+ }
38227
+ }
38228
+ seekTo(time) {
38229
+ const handler = this.handler;
38230
+ if (handler) {
38231
+ handler.seekTo(time - this._padding);
38232
+ }
38233
+ else {
38234
+ this._seekPosition = time - this._padding;
38235
+ }
38236
+ }
38237
+ loadBackingTrack(backingTrack) {
38238
+ this._padding = backingTrack.padding;
38239
+ }
38240
+ open(_bufferTimeInMilliseconds) {
38241
+ this.ready.trigger();
38242
+ }
38243
+ updatePosition(currentTime) {
38244
+ this.timeUpdate.trigger(currentTime + this._padding);
38245
+ }
38246
+ play() {
38247
+ this.handler?.play();
38248
+ }
38249
+ destroy() {
38250
+ }
38251
+ pause() {
38252
+ this.handler?.pause();
38253
+ }
38254
+ addSamples(_samples) {
38255
+ }
38256
+ resetSamples() {
38257
+ }
38258
+ activate() {
38259
+ }
38260
+ async enumerateOutputDevices() {
38261
+ const empty = [];
38262
+ return empty;
38263
+ }
38264
+ async setOutputDevice(_device) {
38265
+ }
38266
+ async getOutputDevice() {
38267
+ return null;
38268
+ }
38269
+ }
38270
+ class ExternalMediaPlayer extends BackingTrackPlayer {
38271
+ get handler() {
38272
+ return this.output.handler;
38273
+ }
38274
+ set handler(value) {
38275
+ this.output.handler = value;
38276
+ }
38277
+ constructor(bufferTimeInMilliseconds) {
38278
+ super(new ExternalMediaSynthOutput(), bufferTimeInMilliseconds);
38279
+ }
38280
+ }
38281
+
37394
38282
  class SelectionInfo {
37395
38283
  constructor(beat) {
37396
38284
  this.bounds = null;
@@ -37404,6 +38292,12 @@ class SelectionInfo {
37404
38292
  * @csharp_public
37405
38293
  */
37406
38294
  class AlphaTabApiBase {
38295
+ /**
38296
+ * The actual player mode which is currently active (e.g. allows determining whether a backing track or the synthesizer is active).
38297
+ */
38298
+ get actualPlayerMode() {
38299
+ return this._actualPlayerMode;
38300
+ }
37407
38301
  /**
37408
38302
  * The score holding all information about the song being rendered
37409
38303
  * @category Properties - Core
@@ -37473,10 +38367,8 @@ class AlphaTabApiBase {
37473
38367
  this._isDestroyed = false;
37474
38368
  this._score = null;
37475
38369
  this._tracks = [];
38370
+ this._actualPlayerMode = PlayerMode.Disabled;
37476
38371
  this._tickCache = null;
37477
- /**
37478
- * Gets the alphaSynth player used for playback. This is the low-level API to the Midi synthesizer used for playback.
37479
- */
37480
38372
  /**
37481
38373
  * The alphaSynth player used for playback.
37482
38374
  * @remarks
@@ -38513,6 +39405,10 @@ class AlphaTabApiBase {
38513
39405
  this.container = uiFacade.rootContainer;
38514
39406
  uiFacade.initialize(this, settings);
38515
39407
  Logger.logLevel = this.settings.core.logLevel;
39408
+ // backwards compatibility: remove in 2.0
39409
+ if (this.settings.player.playerMode === PlayerMode.Disabled && this.settings.player.enablePlayer) {
39410
+ this.settings.player.playerMode = PlayerMode.EnabledAutomatic;
39411
+ }
38516
39412
  Environment.printEnvironmentInfo(false);
38517
39413
  this.canvasElement = uiFacade.createCanvasElement();
38518
39414
  this.container.appendChild(this.canvasElement);
@@ -38556,7 +39452,7 @@ class AlphaTabApiBase {
38556
39452
  this.appendRenderResult(null); // marks last element
38557
39453
  });
38558
39454
  this.renderer.error.on(this.onError.bind(this));
38559
- if (this.settings.player.enablePlayer) {
39455
+ if (this.settings.player.playerMode !== PlayerMode.Disabled) {
38560
39456
  this.setupPlayer();
38561
39457
  }
38562
39458
  this.setupClickHandling();
@@ -38648,10 +39544,9 @@ class AlphaTabApiBase {
38648
39544
  }
38649
39545
  this.renderer.updateSettings(this.settings);
38650
39546
  // enable/disable player if needed
38651
- if (this.settings.player.enablePlayer) {
38652
- this.setupPlayer();
38653
- if (score) {
38654
- this.player?.applyTranspositionPitches(MidiFileGenerator.buildTranspositionPitches(score, this.settings));
39547
+ if (this.settings.player.playerMode !== PlayerMode.Disabled) {
39548
+ if (this.setupPlayer() && score) {
39549
+ this.loadMidiForScore();
38655
39550
  }
38656
39551
  }
38657
39552
  else {
@@ -39583,13 +40478,51 @@ class AlphaTabApiBase {
39583
40478
  this.destroyCursors();
39584
40479
  }
39585
40480
  setupPlayer() {
40481
+ let mode = this.settings.player.playerMode;
40482
+ if (mode === PlayerMode.EnabledAutomatic) {
40483
+ const score = this.score;
40484
+ if (!score) {
40485
+ return false;
40486
+ }
40487
+ if (score?.backingTrack?.rawAudioFile) {
40488
+ mode = PlayerMode.EnabledBackingTrack;
40489
+ }
40490
+ else {
40491
+ mode = PlayerMode.EnabledSynthesizer;
40492
+ }
40493
+ }
40494
+ if (mode !== this._actualPlayerMode) {
40495
+ this.destroyPlayer();
40496
+ }
39586
40497
  this.updateCursors();
39587
- if (this.player) {
39588
- return;
40498
+ this._actualPlayerMode = mode;
40499
+ switch (mode) {
40500
+ case PlayerMode.Disabled:
40501
+ this.destroyPlayer();
40502
+ return false;
40503
+ case PlayerMode.EnabledSynthesizer:
40504
+ if (this.player) {
40505
+ return true;
40506
+ }
40507
+ // new player needed
40508
+ this.player = this.uiFacade.createWorkerPlayer();
40509
+ break;
40510
+ case PlayerMode.EnabledBackingTrack:
40511
+ if (this.player) {
40512
+ return true;
40513
+ }
40514
+ // new player needed
40515
+ this.player = this.uiFacade.createBackingTrackPlayer();
40516
+ break;
40517
+ case PlayerMode.EnabledExternalMedia:
40518
+ if (this.player) {
40519
+ return true;
40520
+ }
40521
+ this.player = new ExternalMediaPlayer(this.settings.player.bufferTimeInMilliseconds);
40522
+ break;
39589
40523
  }
39590
- this.player = this.uiFacade.createWorkerPlayer();
39591
40524
  if (!this.player) {
39592
- return;
40525
+ return false;
39593
40526
  }
39594
40527
  this.player.ready.on(() => {
39595
40528
  this.loadMidiForScore();
@@ -39618,6 +40551,7 @@ class AlphaTabApiBase {
39618
40551
  this.player.playbackRangeChanged.on(this.onPlaybackRangeChanged.bind(this));
39619
40552
  this.player.finished.on(this.onPlayerFinished.bind(this));
39620
40553
  this.setupPlayerEvents();
40554
+ return false;
39621
40555
  }
39622
40556
  loadMidiForScore() {
39623
40557
  if (!this.score) {
@@ -39639,6 +40573,7 @@ class AlphaTabApiBase {
39639
40573
  const player = this.player;
39640
40574
  if (player) {
39641
40575
  player.loadMidiFile(midiFile);
40576
+ player.loadBackingTrack(score, generator.syncPoints);
39642
40577
  player.applyTranspositionPitches(generator.transpositionPitches);
39643
40578
  }
39644
40579
  }
@@ -40055,7 +40990,7 @@ class AlphaTabApiBase {
40055
40990
  this._selectionWrapper = cursors.selectionWrapper;
40056
40991
  }
40057
40992
  if (this._currentBeat !== null) {
40058
- this.cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, true);
40993
+ this.cursorUpdateBeat(this._currentBeat, false, this._previousTick > 10, 1, true);
40059
40994
  }
40060
40995
  }
40061
40996
  else if (!this.settings.player.enableCursor && this._cursorWrapper) {
@@ -40070,13 +41005,14 @@ class AlphaTabApiBase {
40070
41005
  // we need to update our position caches if we render a tablature
40071
41006
  this.renderer.postRenderFinished.on(() => {
40072
41007
  this._currentBeat = null;
40073
- this.cursorUpdateTick(this._previousTick, false, this._previousTick > 10);
41008
+ this.cursorUpdateTick(this._previousTick, false, 1, this._previousTick > 10);
40074
41009
  });
40075
41010
  if (this.player) {
40076
41011
  this.player.positionChanged.on(e => {
40077
41012
  this._previousTick = e.currentTick;
40078
41013
  this.uiFacade.beginInvoke(() => {
40079
- this.cursorUpdateTick(e.currentTick, false, false, e.isSeek);
41014
+ const cursorSpeed = e.modifiedTempo / e.originalTempo;
41015
+ this.cursorUpdateTick(e.currentTick, false, cursorSpeed, false, e.isSeek);
40080
41016
  });
40081
41017
  });
40082
41018
  this.player.stateChanged.on(e => {
@@ -40097,14 +41033,15 @@ class AlphaTabApiBase {
40097
41033
  * @param stop
40098
41034
  * @param shouldScroll whether we should scroll to the bar (if scrolling is active)
40099
41035
  */
40100
- cursorUpdateTick(tick, stop, shouldScroll = false, forceUpdate = false) {
41036
+ cursorUpdateTick(tick, stop, cursorSpeed, shouldScroll = false, forceUpdate = false) {
41037
+ this._previousTick = tick;
40101
41038
  const cache = this._tickCache;
40102
41039
  if (cache) {
40103
41040
  const tracks = this._trackIndexLookup;
40104
41041
  if (tracks != null && tracks.size > 0) {
40105
41042
  const beat = cache.findBeat(tracks, tick, this._currentBeat);
40106
41043
  if (beat) {
40107
- this.cursorUpdateBeat(beat, stop, shouldScroll, forceUpdate || this.playerState === PlayerState.Paused);
41044
+ this.cursorUpdateBeat(beat, stop, shouldScroll, cursorSpeed, forceUpdate || this.playerState === PlayerState.Paused);
40108
41045
  }
40109
41046
  }
40110
41047
  }
@@ -40112,7 +41049,7 @@ class AlphaTabApiBase {
40112
41049
  /**
40113
41050
  * updates the cursors to highlight the specified beat
40114
41051
  */
40115
- cursorUpdateBeat(lookupResult, stop, shouldScroll, forceUpdate = false) {
41052
+ cursorUpdateBeat(lookupResult, stop, shouldScroll, cursorSpeed, forceUpdate = false) {
40116
41053
  const beat = lookupResult.beat;
40117
41054
  const nextBeat = lookupResult.nextBeat?.beat ?? null;
40118
41055
  const duration = lookupResult.duration;
@@ -40144,7 +41081,7 @@ class AlphaTabApiBase {
40144
41081
  this._previousCursorCache = cache;
40145
41082
  this._previousStateForCursor = this._playerState;
40146
41083
  this.uiFacade.beginInvoke(() => {
40147
- this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode);
41084
+ this.internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
40148
41085
  });
40149
41086
  }
40150
41087
  /**
@@ -40209,7 +41146,7 @@ class AlphaTabApiBase {
40209
41146
  }
40210
41147
  }
40211
41148
  }
40212
- internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, cursorMode) {
41149
+ internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, cursorMode, cursorSpeed) {
40213
41150
  const barCursor = this._barCursor;
40214
41151
  const beatCursor = this._beatCursor;
40215
41152
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
@@ -40218,12 +41155,29 @@ class AlphaTabApiBase {
40218
41155
  if (barCursor) {
40219
41156
  barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
40220
41157
  }
41158
+ let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
41159
+ // get position of next beat on same system
41160
+ if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
41161
+ // if we are moving within the same bar or to the next bar
41162
+ // transition to the next beat, otherwise transition to the end of the bar.
41163
+ const nextBeatBoundings = cache.findBeat(nextBeat);
41164
+ if (nextBeatBoundings &&
41165
+ nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
41166
+ nextBeatX = nextBeatBoundings.onNotesX;
41167
+ }
41168
+ }
41169
+ let startBeatX = beatBoundings.onNotesX;
40221
41170
  if (beatCursor) {
40222
- // move beat to start position immediately
41171
+ // relative positioning of the cursor
40223
41172
  if (this.settings.player.enableAnimatedBeatCursor) {
40224
- beatCursor.stopAnimation();
41173
+ const animationWidth = nextBeatX - beatBoundings.onNotesX;
41174
+ const relativePosition = this._previousTick - this._currentBeat.start;
41175
+ const ratioPosition = relativePosition / this._currentBeat.tickDuration;
41176
+ startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
41177
+ duration -= duration * ratioPosition;
41178
+ beatCursor.transitionToX(0, startBeatX);
40225
41179
  }
40226
- beatCursor.setBounds(beatBoundings.onNotesX, barBounds.y, 1, barBounds.h);
41180
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
40227
41181
  }
40228
41182
  // if playing, animate the cursor to the next beat
40229
41183
  if (this.settings.player.enableElementHighlighting) {
@@ -40243,22 +41197,11 @@ class AlphaTabApiBase {
40243
41197
  shouldNotifyBeatChange = true;
40244
41198
  }
40245
41199
  if (this.settings.player.enableAnimatedBeatCursor && beatCursor) {
40246
- let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
40247
- // get position of next beat on same system
40248
- if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
40249
- // if we are moving within the same bar or to the next bar
40250
- // transition to the next beat, otherwise transition to the end of the bar.
40251
- const nextBeatBoundings = cache.findBeat(nextBeat);
40252
- if (nextBeatBoundings &&
40253
- nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
40254
- nextBeatX = nextBeatBoundings.onNotesX;
40255
- }
40256
- }
40257
41200
  if (isPlayingUpdate) {
40258
41201
  // we need to put the transition to an own animation frame
40259
41202
  // otherwise the stop animation above is not applied.
40260
41203
  this.uiFacade.beginInvoke(() => {
40261
- beatCursor.transitionToX(duration / this.playbackSpeed, nextBeatX);
41204
+ beatCursor.transitionToX(duration / cursorSpeed, nextBeatX);
40262
41205
  });
40263
41206
  }
40264
41207
  }
@@ -40289,7 +41232,7 @@ class AlphaTabApiBase {
40289
41232
  if (this._isDestroyed) {
40290
41233
  return;
40291
41234
  }
40292
- if (this.settings.player.enablePlayer &&
41235
+ if (this.settings.player.playerMode !== PlayerMode.Disabled &&
40293
41236
  this.settings.player.enableCursor &&
40294
41237
  this.settings.player.enableUserInteraction) {
40295
41238
  this._selectionStart = new SelectionInfo(beat);
@@ -40331,7 +41274,7 @@ class AlphaTabApiBase {
40331
41274
  if (this._isDestroyed) {
40332
41275
  return;
40333
41276
  }
40334
- if (this.settings.player.enablePlayer &&
41277
+ if (this.settings.player.playerMode !== PlayerMode.Disabled &&
40335
41278
  this.settings.player.enableCursor &&
40336
41279
  this.settings.player.enableUserInteraction) {
40337
41280
  if (this._selectionEnd) {
@@ -40352,7 +41295,7 @@ class AlphaTabApiBase {
40352
41295
  // move to selection start
40353
41296
  this._currentBeat = null; // reset current beat so it is updating the cursor
40354
41297
  if (this._playerState === PlayerState.Paused) {
40355
- this.cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false);
41298
+ this.cursorUpdateTick(this._tickCache.getBeatStart(this._selectionStart.beat), false, 1);
40356
41299
  }
40357
41300
  this.tickPosition = realMasterBarStart + this._selectionStart.beat.playbackStart;
40358
41301
  // set playback range
@@ -40464,7 +41407,7 @@ class AlphaTabApiBase {
40464
41407
  });
40465
41408
  this.renderer.postRenderFinished.on(() => {
40466
41409
  if (!this._selectionStart ||
40467
- !this.settings.player.enablePlayer ||
41410
+ this.settings.player.playerMode === PlayerMode.Disabled ||
40468
41411
  !this.settings.player.enableCursor ||
40469
41412
  !this.settings.player.enableUserInteraction) {
40470
41413
  return;
@@ -40542,6 +41485,9 @@ class AlphaTabApiBase {
40542
41485
  }
40543
41486
  this.scoreLoaded.trigger(score);
40544
41487
  this.uiFacade.triggerEvent(this.container, 'scoreLoaded', score);
41488
+ if (this.setupPlayer()) {
41489
+ this.loadMidiForScore();
41490
+ }
40545
41491
  }
40546
41492
  onResize(e) {
40547
41493
  if (this._isDestroyed) {
@@ -41281,6 +42227,85 @@ class AlphaSynthWebAudioSynthOutputDevice {
41281
42227
  return this.device.label;
41282
42228
  }
41283
42229
  }
42230
+ /**
42231
+ * Some shared web audio stuff.
42232
+ * @target web
42233
+ */
42234
+ class WebAudioHelper {
42235
+ static findKnownDevice(sinkId) {
42236
+ return WebAudioHelper._knownDevices.find(d => d.deviceId === sinkId);
42237
+ }
42238
+ static createAudioContext() {
42239
+ if ('AudioContext' in Environment.globalThis) {
42240
+ return new AudioContext();
42241
+ }
42242
+ if ('webkitAudioContext' in Environment.globalThis) {
42243
+ return new webkitAudioContext();
42244
+ }
42245
+ throw new AlphaTabError(AlphaTabErrorType.General, 'AudioContext not found');
42246
+ }
42247
+ static async checkSinkIdSupport() {
42248
+ // https://caniuse.com/mdn-api_audiocontext_sinkid
42249
+ const context = WebAudioHelper.createAudioContext();
42250
+ if (!('setSinkId' in context)) {
42251
+ Logger.warning('WebAudio', 'Browser does not support changing the output device');
42252
+ return false;
42253
+ }
42254
+ return true;
42255
+ }
42256
+ static async enumerateOutputDevices() {
42257
+ try {
42258
+ if (!(await WebAudioHelper.checkSinkIdSupport())) {
42259
+ return [];
42260
+ }
42261
+ // Request permissions
42262
+ try {
42263
+ await navigator.mediaDevices.getUserMedia({ audio: true });
42264
+ }
42265
+ catch (e) {
42266
+ // sometimes we get an error but can still enumerate, e.g. if microphone access is denied,
42267
+ // we can still load the output devices in some cases.
42268
+ Logger.warning('WebAudio', 'Output device permission rejected', e);
42269
+ }
42270
+ // load devices
42271
+ const devices = await navigator.mediaDevices.enumerateDevices();
42272
+ // default device candidates
42273
+ let defaultDeviceGroupId = '';
42274
+ let defaultDeviceId = '';
42275
+ const realDevices = new Map();
42276
+ for (const device of devices) {
42277
+ if (device.kind === 'audiooutput') {
42278
+ realDevices.set(device.groupId, new AlphaSynthWebAudioSynthOutputDevice(device));
42279
+ // chromium has the default device as deviceID: 'default'
42280
+ // the standard defines empty-string as default
42281
+ if (device.deviceId === 'default' || device.deviceId === '') {
42282
+ defaultDeviceGroupId = device.groupId;
42283
+ defaultDeviceId = device.deviceId;
42284
+ }
42285
+ }
42286
+ }
42287
+ const final = Array.from(realDevices.values());
42288
+ // flag default device
42289
+ let defaultDevice = final.find(d => d.deviceId === defaultDeviceId);
42290
+ if (!defaultDevice) {
42291
+ defaultDevice = final.find(d => d.device.groupId === defaultDeviceGroupId);
42292
+ }
42293
+ if (!defaultDevice && final.length > 0) {
42294
+ defaultDevice = final[0];
42295
+ }
42296
+ if (defaultDevice) {
42297
+ defaultDevice.isDefault = true;
42298
+ }
42299
+ WebAudioHelper._knownDevices = final;
42300
+ return final;
42301
+ }
42302
+ catch (e) {
42303
+ Logger.error('WebAudio', 'Failed to enumerate output devices', e);
42304
+ return [];
42305
+ }
42306
+ }
42307
+ }
42308
+ WebAudioHelper._knownDevices = [];
41284
42309
  /**
41285
42310
  * @target web
41286
42311
  */
@@ -41292,14 +42317,13 @@ class AlphaSynthWebAudioOutputBase {
41292
42317
  this.ready = new EventEmitter();
41293
42318
  this.samplesPlayed = new EventEmitterOfT();
41294
42319
  this.sampleRequest = new EventEmitter();
41295
- this._knownDevices = [];
41296
42320
  }
41297
42321
  get sampleRate() {
41298
42322
  return this._context ? this._context.sampleRate : AlphaSynthWebAudioOutputBase.PreferredSampleRate;
41299
42323
  }
41300
42324
  activate(resumedCallback) {
41301
42325
  if (!this._context) {
41302
- this._context = this.createAudioContext();
42326
+ this._context = WebAudioHelper.createAudioContext();
41303
42327
  }
41304
42328
  if (this._context.state === 'suspended' || this._context.state === 'interrupted') {
41305
42329
  Logger.debug('WebAudio', 'Audio Context is suspended, trying resume');
@@ -41316,7 +42340,7 @@ class AlphaSynthWebAudioOutputBase {
41316
42340
  patchIosSampleRate() {
41317
42341
  const ua = navigator.userAgent;
41318
42342
  if (ua.indexOf('iPhone') !== -1 || ua.indexOf('iPad') !== -1) {
41319
- const context = this.createAudioContext();
42343
+ const context = WebAudioHelper.createAudioContext();
41320
42344
  const buffer = context.createBuffer(1, 1, AlphaSynthWebAudioOutputBase.PreferredSampleRate);
41321
42345
  const dummy = context.createBufferSource();
41322
42346
  dummy.buffer = buffer;
@@ -41327,18 +42351,9 @@ class AlphaSynthWebAudioOutputBase {
41327
42351
  context.close();
41328
42352
  }
41329
42353
  }
41330
- createAudioContext() {
41331
- if ('AudioContext' in Environment.globalThis) {
41332
- return new AudioContext();
41333
- }
41334
- if ('webkitAudioContext' in Environment.globalThis) {
41335
- return new webkitAudioContext();
41336
- }
41337
- throw new AlphaTabError(AlphaTabErrorType.General, 'AudioContext not found');
41338
- }
41339
42354
  open(bufferTimeInMilliseconds) {
41340
42355
  this.patchIosSampleRate();
41341
- this._context = this.createAudioContext();
42356
+ this._context = WebAudioHelper.createAudioContext();
41342
42357
  const ctx = this._context;
41343
42358
  if (ctx.state === 'suspended') {
41344
42359
  this.registerResumeHandler();
@@ -41391,68 +42406,11 @@ class AlphaSynthWebAudioOutputBase {
41391
42406
  onReady() {
41392
42407
  this.ready.trigger();
41393
42408
  }
41394
- async checkSinkIdSupport() {
41395
- // https://caniuse.com/mdn-api_audiocontext_sinkid
41396
- const context = this._context ?? this.createAudioContext();
41397
- if (!('setSinkId' in context)) {
41398
- Logger.warning('WebAudio', 'Browser does not support changing the output device');
41399
- return false;
41400
- }
41401
- return true;
41402
- }
41403
- async enumerateOutputDevices() {
41404
- try {
41405
- if (!(await this.checkSinkIdSupport())) {
41406
- return [];
41407
- }
41408
- // Request permissions
41409
- try {
41410
- await navigator.mediaDevices.getUserMedia({ audio: true });
41411
- }
41412
- catch (e) {
41413
- // sometimes we get an error but can still enumerate, e.g. if microphone access is denied,
41414
- // we can still load the output devices in some cases.
41415
- Logger.warning('WebAudio', 'Output device permission rejected', e);
41416
- }
41417
- // load devices
41418
- const devices = await navigator.mediaDevices.enumerateDevices();
41419
- // default device candidates
41420
- let defaultDeviceGroupId = '';
41421
- let defaultDeviceId = '';
41422
- const realDevices = new Map();
41423
- for (const device of devices) {
41424
- if (device.kind === 'audiooutput') {
41425
- realDevices.set(device.groupId, new AlphaSynthWebAudioSynthOutputDevice(device));
41426
- // chromium has the default device as deviceID: 'default'
41427
- // the standard defines empty-string as default
41428
- if (device.deviceId === 'default' || device.deviceId === '') {
41429
- defaultDeviceGroupId = device.groupId;
41430
- defaultDeviceId = device.deviceId;
41431
- }
41432
- }
41433
- }
41434
- const final = Array.from(realDevices.values());
41435
- // flag default device
41436
- let defaultDevice = final.find(d => d.deviceId === defaultDeviceId);
41437
- if (!defaultDevice) {
41438
- defaultDevice = final.find(d => d.device.groupId === defaultDeviceGroupId);
41439
- }
41440
- if (!defaultDevice && final.length > 0) {
41441
- defaultDevice = final[0];
41442
- }
41443
- if (defaultDevice) {
41444
- defaultDevice.isDefault = true;
41445
- }
41446
- this._knownDevices = final;
41447
- return final;
41448
- }
41449
- catch (e) {
41450
- Logger.error('WebAudio', 'Failed to enumerate output devices', e);
41451
- return [];
41452
- }
42409
+ enumerateOutputDevices() {
42410
+ return WebAudioHelper.enumerateOutputDevices();
41453
42411
  }
41454
42412
  async setOutputDevice(device) {
41455
- if (!(await this.checkSinkIdSupport())) {
42413
+ if (!(await WebAudioHelper.checkSinkIdSupport())) {
41456
42414
  return;
41457
42415
  }
41458
42416
  // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId
@@ -41464,7 +42422,7 @@ class AlphaSynthWebAudioOutputBase {
41464
42422
  }
41465
42423
  }
41466
42424
  async getOutputDevice() {
41467
- if (!(await this.checkSinkIdSupport())) {
42425
+ if (!(await WebAudioHelper.checkSinkIdSupport())) {
41468
42426
  return null;
41469
42427
  }
41470
42428
  // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/sinkId
@@ -41473,7 +42431,7 @@ class AlphaSynthWebAudioOutputBase {
41473
42431
  return null;
41474
42432
  }
41475
42433
  // fast path -> cached devices list
41476
- let device = this._knownDevices.find(d => d.deviceId === sinkId);
42434
+ let device = WebAudioHelper.findKnownDevice(sinkId);
41477
42435
  if (device) {
41478
42436
  return device;
41479
42437
  }
@@ -41921,7 +42879,7 @@ class AlphaSynthWebWorkerApi {
41921
42879
  case 'alphaSynth.positionChanged':
41922
42880
  this._timePosition = data.currentTime;
41923
42881
  this._tickPosition = data.currentTick;
41924
- this.positionChanged.trigger(new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek));
42882
+ this.positionChanged.trigger(new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek, data.originalTempo, data.modifiedTempo));
41925
42883
  break;
41926
42884
  case 'alphaSynth.midiEventsPlayed':
41927
42885
  this.midiEventsPlayed.trigger(new MidiEventsPlayedEventArgs(data.events.map(JsonConverter.jsObjectToMidiEvent)));
@@ -41945,7 +42903,7 @@ class AlphaSynthWebWorkerApi {
41945
42903
  break;
41946
42904
  case 'alphaSynth.midiLoaded':
41947
42905
  this.checkReadyForPlayback();
41948
- this.midiLoaded.trigger(new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek));
42906
+ this.midiLoaded.trigger(new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek, data.originalTempo, data.modifiedTempo));
41949
42907
  break;
41950
42908
  case 'alphaSynth.midiLoadFailed':
41951
42909
  this.checkReadyForPlayback();
@@ -41995,6 +42953,8 @@ class AlphaSynthWebWorkerApi {
41995
42953
  this._outputIsReady = true;
41996
42954
  this.checkReady();
41997
42955
  }
42956
+ loadBackingTrack(_score) {
42957
+ }
41998
42958
  }
41999
42959
 
42000
42960
  /**
@@ -42362,6 +43322,123 @@ class ScalableHtmlElementContainer extends HtmlElementContainer {
42362
43322
  }
42363
43323
  }
42364
43324
 
43325
+ /**
43326
+ * @target web
43327
+ */
43328
+ class AudioElementBackingTrackSynthOutput {
43329
+ constructor() {
43330
+ // fake rate
43331
+ this.sampleRate = 44100;
43332
+ this._padding = 0;
43333
+ this._updateInterval = 0;
43334
+ this.ready = new EventEmitter();
43335
+ this.samplesPlayed = new EventEmitterOfT();
43336
+ this.timeUpdate = new EventEmitterOfT();
43337
+ this.sampleRequest = new EventEmitter();
43338
+ }
43339
+ get backingTrackDuration() {
43340
+ const duration = this.audioElement.duration ?? 0;
43341
+ return Number.isFinite(duration) ? duration * 1000 : 0;
43342
+ }
43343
+ get playbackRate() {
43344
+ return this.audioElement.playbackRate;
43345
+ }
43346
+ set playbackRate(value) {
43347
+ this.audioElement.playbackRate = value;
43348
+ }
43349
+ get masterVolume() {
43350
+ return this.audioElement.volume;
43351
+ }
43352
+ set masterVolume(value) {
43353
+ this.audioElement.volume = value;
43354
+ }
43355
+ seekTo(time) {
43356
+ this.audioElement.currentTime = time / 1000 - this._padding;
43357
+ }
43358
+ loadBackingTrack(backingTrack) {
43359
+ if (this.audioElement?.src) {
43360
+ URL.revokeObjectURL(this.audioElement.src);
43361
+ }
43362
+ this._padding = backingTrack.padding / 1000;
43363
+ const blob = new Blob([backingTrack.rawAudioFile]);
43364
+ this.audioElement.src = URL.createObjectURL(blob);
43365
+ }
43366
+ open(_bufferTimeInMilliseconds) {
43367
+ const audioElement = document.createElement('audio');
43368
+ audioElement.style.display = 'none';
43369
+ document.body.appendChild(audioElement);
43370
+ audioElement.addEventListener('timeupdate', () => {
43371
+ this.updatePosition();
43372
+ });
43373
+ this.audioElement = audioElement;
43374
+ this.ready.trigger();
43375
+ }
43376
+ updatePosition() {
43377
+ const timePos = (this.audioElement.currentTime + this._padding) * 1000;
43378
+ this.timeUpdate.trigger(timePos);
43379
+ }
43380
+ play() {
43381
+ this.audioElement.play();
43382
+ this._updateInterval = window.setInterval(() => {
43383
+ this.updatePosition();
43384
+ }, 50);
43385
+ }
43386
+ destroy() {
43387
+ const audioElement = this.audioElement;
43388
+ if (audioElement) {
43389
+ document.body.removeChild(audioElement);
43390
+ }
43391
+ }
43392
+ pause() {
43393
+ this.audioElement.pause();
43394
+ window.clearInterval(this._updateInterval);
43395
+ }
43396
+ addSamples(_samples) {
43397
+ }
43398
+ resetSamples() {
43399
+ }
43400
+ activate() {
43401
+ }
43402
+ async enumerateOutputDevices() {
43403
+ return WebAudioHelper.enumerateOutputDevices();
43404
+ }
43405
+ async setOutputDevice(device) {
43406
+ if (!(await WebAudioHelper.checkSinkIdSupport())) {
43407
+ return;
43408
+ }
43409
+ // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId
43410
+ if (!device) {
43411
+ await this.audioElement.setSinkId('');
43412
+ }
43413
+ else {
43414
+ await this.audioElement.setSinkId(device.deviceId);
43415
+ }
43416
+ }
43417
+ async getOutputDevice() {
43418
+ if (!(await WebAudioHelper.checkSinkIdSupport())) {
43419
+ return null;
43420
+ }
43421
+ // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/sinkId
43422
+ const sinkId = this.audioElement.sinkId;
43423
+ if (typeof sinkId !== 'string' || sinkId === '' || sinkId === 'default') {
43424
+ return null;
43425
+ }
43426
+ // fast path -> cached devices list
43427
+ let device = WebAudioHelper.findKnownDevice(sinkId);
43428
+ if (device) {
43429
+ return device;
43430
+ }
43431
+ // slow path -> enumerate devices
43432
+ const allDevices = await this.enumerateOutputDevices();
43433
+ device = allDevices.find(d => d.deviceId === sinkId);
43434
+ if (device) {
43435
+ return device;
43436
+ }
43437
+ Logger.warning('WebAudio', 'Could not find output device in device list', sinkId, allDevices);
43438
+ return null;
43439
+ }
43440
+ }
43441
+
42365
43442
  /**
42366
43443
  * @target web
42367
43444
  */
@@ -42998,6 +44075,9 @@ class BrowserUiFacade {
42998
44075
  window.requestAnimationFrame(step);
42999
44076
  }
43000
44077
  }
44078
+ createBackingTrackPlayer() {
44079
+ return new BackingTrackPlayer(new AudioElementBackingTrackSynthOutput(), this._api.settings.player.bufferTimeInMilliseconds);
44080
+ }
43001
44081
  }
43002
44082
 
43003
44083
  /**
@@ -43140,7 +44220,7 @@ class AlphaTabApi extends AlphaTabApiBase {
43140
44220
  settings.core.file = null;
43141
44221
  settings.core.tracks = null;
43142
44222
  settings.player.enableCursor = false;
43143
- settings.player.enablePlayer = false;
44223
+ settings.player.playerMode = PlayerMode.Disabled;
43144
44224
  settings.player.enableElementHighlighting = false;
43145
44225
  settings.player.enableUserInteraction = false;
43146
44226
  settings.player.soundFont = null;
@@ -57101,96 +58181,6 @@ class CapellaImporter extends ScoreImporter {
57101
58181
  }
57102
58182
  }
57103
58183
 
57104
- /**
57105
- * A very basic polyfill of the ResizeObserver which triggers
57106
- * a the callback on window resize for all registered targets.
57107
- * @target web
57108
- */
57109
- class ResizeObserverPolyfill {
57110
- constructor(callback) {
57111
- this._targets = new Set();
57112
- this._callback = callback;
57113
- window.addEventListener('resize', this.onWindowResize.bind(this), false);
57114
- }
57115
- observe(target) {
57116
- this._targets.add(target);
57117
- }
57118
- unobserve(target) {
57119
- this._targets.delete(target);
57120
- }
57121
- disconnect() {
57122
- this._targets.clear();
57123
- }
57124
- onWindowResize() {
57125
- const entries = [];
57126
- for (const t of this._targets) {
57127
- entries.push({
57128
- target: t,
57129
- // not used by alphaTab
57130
- contentRect: undefined,
57131
- borderBoxSize: undefined,
57132
- contentBoxSize: [],
57133
- devicePixelContentBoxSize: []
57134
- });
57135
- }
57136
- this._callback(entries, this);
57137
- }
57138
- }
57139
-
57140
- /**
57141
- * A polyfill of the InsersectionObserver
57142
- * @target web
57143
- */
57144
- class IntersectionObserverPolyfill {
57145
- constructor(callback) {
57146
- this._elements = [];
57147
- let timer = null;
57148
- const oldCheck = this.check.bind(this);
57149
- this.check = () => {
57150
- if (!timer) {
57151
- timer = setTimeout(() => {
57152
- oldCheck();
57153
- timer = null;
57154
- }, 100);
57155
- }
57156
- };
57157
- this._callback = callback;
57158
- window.addEventListener('resize', this.check, true);
57159
- document.addEventListener('scroll', this.check, true);
57160
- }
57161
- observe(target) {
57162
- if (this._elements.indexOf(target) >= 0) {
57163
- return;
57164
- }
57165
- this._elements.push(target);
57166
- this.check();
57167
- }
57168
- unobserve(target) {
57169
- this._elements = this._elements.filter(item => {
57170
- return item !== target;
57171
- });
57172
- }
57173
- check() {
57174
- const entries = [];
57175
- for (const element of this._elements) {
57176
- const rect = element.getBoundingClientRect();
57177
- const isVisible = rect.top + rect.height >= 0 &&
57178
- rect.top <= window.innerHeight &&
57179
- rect.left + rect.width >= 0 &&
57180
- rect.left <= window.innerWidth;
57181
- if (isVisible) {
57182
- entries.push({
57183
- target: element,
57184
- isIntersecting: true
57185
- });
57186
- }
57187
- }
57188
- if (entries.length) {
57189
- this._callback(entries, this);
57190
- }
57191
- }
57192
- }
57193
-
57194
58184
  /******************************************************************************
57195
58185
  Copyright (c) Microsoft Corporation.
57196
58186
 
@@ -59306,9 +60296,9 @@ class VersionInfo {
59306
60296
  print(`build date: ${VersionInfo.date}`);
59307
60297
  }
59308
60298
  }
59309
- VersionInfo.version = '1.6.0-alpha.1401';
59310
- VersionInfo.date = '2025-05-07T12:40:48.955Z';
59311
- VersionInfo.commit = 'e58a9704e560b3344b8fe39a2b2f46a2ee3bb5b1';
60299
+ VersionInfo.version = '1.6.0-alpha.1403';
60300
+ VersionInfo.date = '2025-05-09T02:06:22.101Z';
60301
+ VersionInfo.commit = '3644a11f557063573413de459c607a1f9c302a6a';
59312
60302
 
59313
60303
  /**
59314
60304
  * A factory for custom layout engines.
@@ -59779,29 +60769,6 @@ class Environment {
59779
60769
  if (Environment.webPlatform === WebPlatform.Browser || Environment.webPlatform === WebPlatform.BrowserModule) {
59780
60770
  Environment.registerJQueryPlugin();
59781
60771
  Environment.HighDpiFactor = window.devicePixelRatio;
59782
- // ResizeObserver API does not yet exist so long on Safari (only start 2020 with iOS Safari 13.7 and Desktop 13.1)
59783
- // so we better add a polyfill for it
59784
- if (!('ResizeObserver' in Environment.globalThis)) {
59785
- Environment.globalThis.ResizeObserver = ResizeObserverPolyfill;
59786
- }
59787
- // IntersectionObserver API does not on older iOS versions
59788
- // so we better add a polyfill for it
59789
- if (!('IntersectionObserver' in Environment.globalThis)) {
59790
- Environment.globalThis.IntersectionObserver = IntersectionObserverPolyfill;
59791
- }
59792
- if (!('replaceChildren' in Element.prototype)) {
59793
- Element.prototype.replaceChildren = function (...nodes) {
59794
- this.innerHTML = '';
59795
- this.append(...nodes);
59796
- };
59797
- Document.prototype.replaceChildren = Element.prototype.replaceChildren;
59798
- DocumentFragment.prototype.replaceChildren = Element.prototype.replaceChildren;
59799
- }
59800
- if (!('replaceAll' in String.prototype)) {
59801
- String.prototype.replaceAll = function (str, newStr) {
59802
- return this.replace(new RegExp(str, 'g'), newStr);
59803
- };
59804
- }
59805
60772
  }
59806
60773
  Environment.createWebWorker = createWebWorker;
59807
60774
  Environment.createAudioWorklet = createAudioWorklet;
@@ -63533,6 +64500,7 @@ const _barrel$3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty(
63533
64500
  get AccidentalType () { return AccidentalType; },
63534
64501
  Automation,
63535
64502
  get AutomationType () { return AutomationType; },
64503
+ BackingTrack,
63536
64504
  Bar,
63537
64505
  get BarLineStyle () { return BarLineStyle; },
63538
64506
  BarStyle,
@@ -63595,6 +64563,7 @@ const _barrel$3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty(
63595
64563
  Staff,
63596
64564
  SustainPedalMarker,
63597
64565
  get SustainPedalMarkerType () { return SustainPedalMarkerType; },
64566
+ SyncPointData,
63598
64567
  Track,
63599
64568
  get TrackNameMode () { return TrackNameMode; },
63600
64569
  get TrackNameOrientation () { return TrackNameOrientation; },
@@ -63659,4 +64628,4 @@ const _jsonbarrel = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.definePropert
63659
64628
  __proto__: null
63660
64629
  }, Symbol.toStringTag, { value: 'Module' }));
63661
64630
 
63662
- export { AlphaTabApi, AlphaTabApiBase, AlphaTabError, AlphaTabErrorType, ConsoleLogger, CoreSettings, DisplaySettings, Environment, FileLoadError, FingeringMode, FormatError, ImporterSettings, LayoutMode, LogLevel, Logger, NotationElement, NotationMode, NotationSettings, PlayerOutputMode, PlayerSettings, ProgressEventArgs, RenderEngineFactory, RenderingResources, ResizeEventArgs, ScrollMode, Settings, SlidePlaybackSettings, StaveProfile, SystemsLayoutMode, TabRhythmMode, VibratoPlaybackSettings, WebPlatform, _barrel$5 as exporter, _barrel$7 as importer, _barrel$6 as io, _jsonbarrel as json, VersionInfo as meta, _barrel$4 as midi, _barrel$3 as model, _barrel$1 as platform, _barrel$2 as rendering, _barrel as synth };
64631
+ export { AlphaTabApi, AlphaTabApiBase, AlphaTabError, AlphaTabErrorType, ConsoleLogger, CoreSettings, DisplaySettings, Environment, FileLoadError, FingeringMode, FormatError, ImporterSettings, LayoutMode, LogLevel, Logger, NotationElement, NotationMode, NotationSettings, PlayerMode, PlayerOutputMode, PlayerSettings, ProgressEventArgs, RenderEngineFactory, RenderingResources, ResizeEventArgs, ScrollMode, Settings, SlidePlaybackSettings, StaveProfile, SystemsLayoutMode, TabRhythmMode, VibratoPlaybackSettings, WebPlatform, _barrel$5 as exporter, _barrel$7 as importer, _barrel$6 as io, _jsonbarrel as json, VersionInfo as meta, _barrel$4 as midi, _barrel$3 as model, _barrel$1 as platform, _barrel$2 as rendering, _barrel as synth };