@coderline/alphatab 1.7.0-alpha.1517 → 1.7.0-alpha.1522

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.7.0-alpha.1517 (develop, build 1517)
2
+ * alphaTab v1.7.0-alpha.1522 (develop, build 1522)
3
3
  *
4
4
  * Copyright © 2025, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -197,9 +197,9 @@ class VersionInfo {
197
197
  print(`build date: ${VersionInfo.date}`);
198
198
  }
199
199
  }
200
- VersionInfo.version = '1.7.0-alpha.1517';
201
- VersionInfo.date = '2025-08-19T02:07:05.813Z';
202
- VersionInfo.commit = 'a8c49344e3fef6f72a38d5493a2e58223e2c777b';
200
+ VersionInfo.version = '1.7.0-alpha.1522';
201
+ VersionInfo.date = '2025-08-24T02:21:16.522Z';
202
+ VersionInfo.commit = 'c80b4a66eae6b73cb2d7dacaaf74c8f9cce16318';
203
203
 
204
204
  /**
205
205
  * This public class provides names for all general midi instruments.
@@ -226,6 +226,14 @@ class GeneralMidi {
226
226
  static isGuitar(program) {
227
227
  return (program >= 24 && program <= 39) || program === 105 || program === 43;
228
228
  }
229
+ static isBass(program) {
230
+ return program >= 32 && program <= 39;
231
+ }
232
+ static bankToLsbMsb(bank) {
233
+ const lsb = bank & 0x7f;
234
+ const msb = (bank >> 7) & 0x7f;
235
+ return [lsb, msb];
236
+ }
229
237
  }
230
238
  GeneralMidi._values = new Map([
231
239
  ['acousticgrandpiano', 0],
@@ -1420,6 +1428,10 @@ var AutomationType;
1420
1428
  * A sync point for synchronizing the internal time axis with an external audio track.
1421
1429
  */
1422
1430
  AutomationType[AutomationType["SyncPoint"] = 4] = "SyncPoint";
1431
+ /**
1432
+ * Midi Bank change.
1433
+ */
1434
+ AutomationType[AutomationType["Bank"] = 4] = "Bank";
1423
1435
  })(AutomationType || (AutomationType = {}));
1424
1436
  /**
1425
1437
  * Represents the data of a sync point for synchronizing the internal time axis with
@@ -8161,6 +8173,10 @@ class PlaybackInformation {
8161
8173
  * Gets or sets the midi program to use.
8162
8174
  */
8163
8175
  this.program = 0;
8176
+ /**
8177
+ * The midi bank to use.
8178
+ */
8179
+ this.bank = 0;
8164
8180
  /**
8165
8181
  * Gets or sets the primary channel for all normal midi events.
8166
8182
  */
@@ -8555,6 +8571,12 @@ class Track {
8555
8571
  */
8556
8572
  this.percussionArticulations = [];
8557
8573
  }
8574
+ /**
8575
+ * Gets whether this track is a percussion track.
8576
+ */
8577
+ get isPercussion() {
8578
+ return this.staves.some(s => s.isPercussion);
8579
+ }
8558
8580
  /**
8559
8581
  * Adds a new line break.
8560
8582
  * @param index The index of the bar before which a line break should happen.
@@ -10219,33 +10241,15 @@ class AlphaTexImporter extends ScoreImporter {
10219
10241
  }
10220
10242
  return StaffMetaResult.KnownStaffMeta;
10221
10243
  case 'instrument':
10222
- this.sy = this.newSy();
10223
10244
  this._staffTuningApplied = false;
10224
- if (this.sy === AlphaTexSymbols.Number) {
10225
- const instrument = this.syData;
10226
- if (instrument >= 0 && instrument <= 127) {
10227
- this._currentTrack.playbackInfo.program = this.syData;
10228
- }
10229
- else {
10230
- this.error('instrument', AlphaTexSymbols.Number, false);
10231
- }
10232
- }
10233
- else if (this.sy === AlphaTexSymbols.String) {
10234
- const instrumentName = this.syData.toLowerCase();
10235
- if (instrumentName === 'percussion') {
10236
- for (const staff of this._currentTrack.staves) {
10237
- this.applyPercussionStaff(staff);
10238
- }
10239
- this._currentTrack.playbackInfo.primaryChannel = SynthConstants.PercussionChannel;
10240
- this._currentTrack.playbackInfo.secondaryChannel = SynthConstants.PercussionChannel;
10241
- }
10242
- else {
10243
- this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName);
10244
- }
10245
- }
10246
- else {
10247
- this.error('instrument', AlphaTexSymbols.Number, true);
10245
+ this.readTrackInstrument();
10246
+ return StaffMetaResult.KnownStaffMeta;
10247
+ case 'bank':
10248
+ this.sy = this.newSy();
10249
+ if (this.sy !== AlphaTexSymbols.Number) {
10250
+ this.error('bank', AlphaTexSymbols.Number, true);
10248
10251
  }
10252
+ this._currentTrack.playbackInfo.bank = this.syData;
10249
10253
  this.sy = this.newSy();
10250
10254
  return StaffMetaResult.KnownStaffMeta;
10251
10255
  case 'lyrics':
@@ -10356,6 +10360,35 @@ class AlphaTexImporter extends ScoreImporter {
10356
10360
  return StaffMetaResult.UnknownStaffMeta;
10357
10361
  }
10358
10362
  }
10363
+ readTrackInstrument() {
10364
+ this.sy = this.newSy();
10365
+ if (this.sy === AlphaTexSymbols.Number) {
10366
+ const instrument = this.syData;
10367
+ if (instrument >= 0 && instrument <= 127) {
10368
+ this._currentTrack.playbackInfo.program = this.syData;
10369
+ }
10370
+ else {
10371
+ this.error('instrument', AlphaTexSymbols.Number, false);
10372
+ }
10373
+ }
10374
+ else if (this.sy === AlphaTexSymbols.String) {
10375
+ const instrumentName = this.syData.toLowerCase();
10376
+ if (instrumentName === 'percussion') {
10377
+ for (const staff of this._currentTrack.staves) {
10378
+ this.applyPercussionStaff(staff);
10379
+ }
10380
+ this._currentTrack.playbackInfo.primaryChannel = SynthConstants.PercussionChannel;
10381
+ this._currentTrack.playbackInfo.secondaryChannel = SynthConstants.PercussionChannel;
10382
+ }
10383
+ else {
10384
+ this._currentTrack.playbackInfo.program = GeneralMidi.getValue(instrumentName);
10385
+ }
10386
+ }
10387
+ else {
10388
+ this.error('instrument', AlphaTexSymbols.Number, true);
10389
+ }
10390
+ this.sy = this.newSy();
10391
+ }
10359
10392
  handleAccidentalMode() {
10360
10393
  this.sy = this.newSy();
10361
10394
  if (this.sy !== AlphaTexSymbols.String) {
@@ -10628,6 +10661,17 @@ class AlphaTexImporter extends ScoreImporter {
10628
10661
  }
10629
10662
  this._score.stylesheet.perTrackMultiBarRest.add(this._currentTrack.index);
10630
10663
  break;
10664
+ case 'instrument':
10665
+ this.readTrackInstrument();
10666
+ break;
10667
+ case 'bank':
10668
+ this.sy = this.newSy();
10669
+ if (this.sy !== AlphaTexSymbols.Number) {
10670
+ this.error('bank', AlphaTexSymbols.Number, true);
10671
+ }
10672
+ this._currentTrack.playbackInfo.bank = this.syData;
10673
+ this.sy = this.newSy();
10674
+ break;
10631
10675
  default:
10632
10676
  this.error('track-properties', AlphaTexSymbols.String, false);
10633
10677
  break;
@@ -11428,6 +11472,17 @@ class AlphaTexImporter extends ScoreImporter {
11428
11472
  automation.value = program;
11429
11473
  beat.automations.push(automation);
11430
11474
  }
11475
+ else if (syData === 'bank') {
11476
+ this.sy = this.newSy();
11477
+ if (this.sy !== AlphaTexSymbols.Number) {
11478
+ this.error('bank-change', AlphaTexSymbols.Number, true);
11479
+ }
11480
+ const automation = new Automation();
11481
+ automation.isLinear = false;
11482
+ automation.type = AutomationType.Bank;
11483
+ automation.value = this.syData;
11484
+ beat.automations.push(automation);
11485
+ }
11431
11486
  else if (syData === 'fermata') {
11432
11487
  this.sy = this.newSy();
11433
11488
  if (this.sy !== AlphaTexSymbols.String) {
@@ -11438,7 +11493,7 @@ class AlphaTexImporter extends ScoreImporter {
11438
11493
  this.sy = this.newSy(true);
11439
11494
  if (this.sy === AlphaTexSymbols.Number) {
11440
11495
  fermata.length = this.syData;
11441
- this.sy = this.newSy(true);
11496
+ this.sy = this.newSy();
11442
11497
  }
11443
11498
  beat.fermata = fermata;
11444
11499
  return true;
@@ -11471,13 +11526,9 @@ class AlphaTexImporter extends ScoreImporter {
11471
11526
  beat.beamingMode = BeatBeamingMode.ForceSplitOnSecondaryToNext;
11472
11527
  break;
11473
11528
  }
11474
- this.sy = this.newSy();
11475
- return true;
11476
11529
  }
11477
11530
  else if (syData === 'timer') {
11478
11531
  beat.showTimer = true;
11479
- this.sy = this.newSy();
11480
- return true;
11481
11532
  }
11482
11533
  else {
11483
11534
  // string didn't match any beat effect syntax
@@ -15037,6 +15088,7 @@ class Gp3To5Importer extends ScoreImporter {
15037
15088
  this._trackCount = 0;
15038
15089
  this._playbackInfos = [];
15039
15090
  this._doubleBars = new Set();
15091
+ this._clefsPerTrack = new Map();
15040
15092
  this._keySignatures = new Map();
15041
15093
  this._beatTextChunksByTrack = new Map();
15042
15094
  this._directionLookup = new Map();
@@ -15421,26 +15473,74 @@ class Gp3To5Importer extends ScoreImporter {
15421
15473
  mainStaff.capo = IOHelper.readInt32LE(this.data);
15422
15474
  newTrack.color = GpBinaryHelpers.gpReadColor(this.data, false);
15423
15475
  if (this._versionNumber >= 500) {
15424
- const staveFlags = this.data.readByte();
15425
- mainStaff.showTablature = (staveFlags & 0x01) !== 0;
15426
- mainStaff.showStandardNotation = (staveFlags & 0x02) !== 0;
15427
- const showChordDiagramListOnTopOfScore = (staveFlags & 0x64) !== 0;
15476
+ const staffFlags = this.data.readByte();
15477
+ mainStaff.showTablature = (staffFlags & 0x01) !== 0;
15478
+ mainStaff.showStandardNotation = (staffFlags & 0x02) !== 0;
15479
+ const showChordDiagramListOnTopOfScore = (staffFlags & 0x64) !== 0;
15428
15480
  if (this._score.stylesheet.perTrackChordDiagramsOnTop === null) {
15429
15481
  this._score.stylesheet.perTrackChordDiagramsOnTop = new Map();
15430
15482
  }
15431
15483
  this._score.stylesheet.perTrackChordDiagramsOnTop.set(newTrack.index, showChordDiagramListOnTopOfScore);
15432
- // flags for
15484
+ // MIDI: Automatic
15485
+ // 0x01 -> always set (unknown)
15433
15486
  // 0x02 -> auto let ring
15434
15487
  // 0x04 -> auto brush
15435
15488
  this.data.readByte();
15436
- // unknown
15437
- this.data.skip(43);
15489
+ // RSE: Auto-Accentuation on the Beat
15490
+ // 0 - None
15491
+ // 1 - Very Soft
15492
+ // 2 - Soft
15493
+ // 3 - Medium
15494
+ // 4 - Strong
15495
+ // 5 - Very Strong
15496
+ this.data.readByte();
15497
+ newTrack.playbackInfo.bank = this.data.readByte();
15498
+ // RSE: Human Playing (1 byte, 0-100%)
15499
+ this.data.readByte();
15500
+ // `12` for all tunings which have bass clefs
15501
+ const clefMode = IOHelper.readInt32LE(this.data);
15502
+ if (clefMode === 12) {
15503
+ this._clefsPerTrack.set(index, Clef.F4);
15504
+ }
15505
+ else {
15506
+ this._clefsPerTrack.set(index, Clef.G2);
15507
+ }
15508
+ // Unknown, no UI setting seem to affect this
15509
+ IOHelper.readInt32LE(this.data);
15510
+ // Unknown, no UI setting seem to affect this
15511
+ // typically: 100
15512
+ IOHelper.readInt32LE(this.data);
15513
+ // Unknown, no UI setting seem to affect this
15514
+ // typically: 1 2 3 4 5 6 7 8 9 10
15515
+ this.data.skip(10);
15516
+ // Unknown, no UI setting seem to affect this
15517
+ // typically: 255
15518
+ this.data.readByte();
15519
+ // Unknown, no UI setting seem to affect this
15520
+ // typically: 3
15521
+ this.data.readByte();
15522
+ this.readRseBank();
15523
+ // this.data.skip(42);
15524
+ if (this._versionNumber >= 510) {
15525
+ // RSE: 3-band EQ
15526
+ // 1 byte Low
15527
+ // 1 byte Mid
15528
+ // 1 byte High
15529
+ // 1 byte PRE
15530
+ this.data.skip(4);
15531
+ // RSE: effect name
15532
+ GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding);
15533
+ // RSE: effect category
15534
+ GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding);
15535
+ }
15438
15536
  }
15439
- // unknown
15440
- if (this._versionNumber >= 510) {
15441
- this.data.skip(4);
15442
- GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding);
15443
- GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding);
15537
+ else {
15538
+ if (GeneralMidi.isBass(newTrack.playbackInfo.program)) {
15539
+ this._clefsPerTrack.set(index, Clef.F4);
15540
+ }
15541
+ else {
15542
+ this._clefsPerTrack.set(index, Clef.G2);
15543
+ }
15444
15544
  }
15445
15545
  }
15446
15546
  readBars() {
@@ -15456,6 +15556,9 @@ class Gp3To5Importer extends ScoreImporter {
15456
15556
  if (mainStaff.isPercussion) {
15457
15557
  newBar.clef = Clef.Neutral;
15458
15558
  }
15559
+ else if (this._clefsPerTrack.has(track.index)) {
15560
+ newBar.clef = this._clefsPerTrack.get(track.index);
15561
+ }
15459
15562
  mainStaff.addBar(newBar);
15460
15563
  if (this._keySignatures.has(newBar.index)) {
15461
15564
  const newKeySignature = this._keySignatures.get(newBar.index);
@@ -15877,11 +15980,29 @@ class Gp3To5Importer extends ScoreImporter {
15877
15980
  return 0;
15878
15981
  }
15879
15982
  }
15983
+ readRseBank() {
15984
+ // RSE Banks on disk are having filenames like this: 033_2_002.ini
15985
+ // 033 is the RSE instrument (selected in the instrument list)
15986
+ // 2 seem to be some sort of style variation. e.g. bass has multiple variations (1,2), guitars have only 1, percussion has only 0
15987
+ // likely for organizational purposes
15988
+ // 002 is the effective soundbank (selected in the soundbank dropdown)
15989
+ // RSE Instrument
15990
+ this.data.skip(4); // IOHelper.readInt32LE(this.data);
15991
+ // RSE Style/Variation
15992
+ this.data.skip(4); //IOHelper.readInt32LE(this.data);
15993
+ // RSE Soundbank
15994
+ this.data.skip(4); //IOHelper.readInt32LE(this.data);
15995
+ // Unknown, no UI setting seem to affect this
15996
+ // typically: -1
15997
+ this.data.skip(4); //IOHelper.readInt32LE(this.data);
15998
+ }
15880
15999
  readMixTableChange(beat) {
15881
16000
  const tableChange = new MixTableChange();
15882
16001
  tableChange.instrument = IOHelper.readSInt8(this.data);
16002
+ // NOTE: The UI shows a Midi Bank selection, but this information is not stored in the file
16003
+ // when reopening the file the bank is always 0
15883
16004
  if (this._versionNumber >= 500) {
15884
- this.data.skip(16); // Rse Info
16005
+ this.readRseBank();
15885
16006
  }
15886
16007
  tableChange.volume = IOHelper.readSInt8(this.data);
15887
16008
  tableChange.balance = IOHelper.readSInt8(this.data);
@@ -15893,7 +16014,7 @@ class Gp3To5Importer extends ScoreImporter {
15893
16014
  tableChange.tempoName = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding);
15894
16015
  }
15895
16016
  tableChange.tempo = IOHelper.readInt32LE(this.data);
15896
- // durations
16017
+ // durations (in number of beats)
15897
16018
  if (tableChange.volume >= 0) {
15898
16019
  this.data.readByte();
15899
16020
  }
@@ -16876,9 +16997,7 @@ class BinaryStylesheet {
16876
16997
  return writer.toArray();
16877
16998
  }
16878
16999
  static addHeaderAndFooter(binaryStylesheet, style, prefix, name) {
16879
- if (style.template !== undefined) {
16880
- binaryStylesheet.addValue(`${prefix}${name}`, style.template, DataType.String);
16881
- }
17000
+ binaryStylesheet.addValue(`${prefix}${name}`, style.template, DataType.String);
16882
17001
  binaryStylesheet.addValue(`${prefix}${name}Alignment`, style.textAlign, DataType.Integer);
16883
17002
  if (style.isVisible !== undefined) {
16884
17003
  binaryStylesheet.addValue(`${prefix}draw${name}`, style.isVisible, DataType.Boolean);
@@ -16912,6 +17031,7 @@ class GpifSound {
16912
17031
  this.path = '';
16913
17032
  this.role = '';
16914
17033
  this.program = 0;
17034
+ this.bank = 0;
16915
17035
  }
16916
17036
  get uniqueId() {
16917
17037
  return `${this.path};${this.name};${this.role}`;
@@ -17266,21 +17386,28 @@ class GpifParser {
17266
17386
  if (!type) {
17267
17387
  return;
17268
17388
  }
17269
- let automation = null;
17389
+ const newAutomations = [];
17270
17390
  switch (type) {
17271
17391
  case 'Tempo':
17272
- automation = Automation.buildTempoAutomation(isLinear, ratioPosition, numberValue, reference);
17392
+ newAutomations.push(Automation.buildTempoAutomation(isLinear, ratioPosition, numberValue, reference));
17273
17393
  break;
17274
17394
  case 'SyncPoint':
17275
- automation = new Automation();
17276
- automation.type = AutomationType.SyncPoint;
17277
- automation.isLinear = isLinear;
17278
- automation.ratioPosition = ratioPosition;
17279
- automation.syncPointValue = syncPointValue;
17395
+ const syncPoint = new Automation();
17396
+ syncPoint.type = AutomationType.SyncPoint;
17397
+ syncPoint.isLinear = isLinear;
17398
+ syncPoint.ratioPosition = ratioPosition;
17399
+ syncPoint.syncPointValue = syncPointValue;
17400
+ newAutomations.push(syncPoint);
17280
17401
  break;
17281
17402
  case 'Sound':
17282
17403
  if (textValue && sounds && sounds.has(textValue)) {
17283
- automation = Automation.buildInstrumentAutomation(isLinear, ratioPosition, sounds.get(textValue).program);
17404
+ const bankChange = new Automation();
17405
+ bankChange.type = AutomationType.Bank;
17406
+ bankChange.ratioPosition = ratioPosition;
17407
+ bankChange.value = sounds.get(textValue).bank;
17408
+ newAutomations.push(bankChange);
17409
+ const programChange = Automation.buildInstrumentAutomation(isLinear, ratioPosition, sounds.get(textValue).program);
17410
+ newAutomations.push(programChange);
17284
17411
  }
17285
17412
  break;
17286
17413
  case 'SustainPedal':
@@ -17308,15 +17435,19 @@ class GpifParser {
17308
17435
  }
17309
17436
  break;
17310
17437
  }
17311
- if (automation) {
17438
+ if (newAutomations.length) {
17312
17439
  if (text) {
17313
- automation.text = text;
17440
+ for (const a of newAutomations) {
17441
+ a.text = text;
17442
+ }
17314
17443
  }
17315
17444
  if (barIndex >= 0) {
17316
17445
  if (!automations.has(barIndex)) {
17317
17446
  automations.set(barIndex, []);
17318
17447
  }
17319
- automations.get(barIndex).push(automation);
17448
+ for (const a of newAutomations) {
17449
+ automations.get(barIndex).push(a);
17450
+ }
17320
17451
  }
17321
17452
  }
17322
17453
  }
@@ -17908,22 +18039,31 @@ class GpifParser {
17908
18039
  break;
17909
18040
  }
17910
18041
  }
17911
- if (sound.role === 'Factory' || track.playbackInfo.program === 0) {
17912
- track.playbackInfo.program = sound.program;
17913
- }
17914
18042
  if (!this._soundsByTrack.has(trackId)) {
17915
18043
  this._soundsByTrack.set(trackId, new Map());
18044
+ // apply first sound
18045
+ track.playbackInfo.program = sound.program;
18046
+ track.playbackInfo.bank = sound.bank;
17916
18047
  }
17917
18048
  this._soundsByTrack.get(trackId).set(sound.uniqueId, sound);
17918
18049
  }
17919
18050
  parseSoundMidi(sound, node) {
18051
+ let bankMsb = 0;
18052
+ let bankLsb = 0;
17920
18053
  for (const c of node.childElements()) {
17921
18054
  switch (c.localName) {
17922
18055
  case 'Program':
17923
18056
  sound.program = GpifParser.parseIntSafe(c.innerText, 0);
17924
18057
  break;
18058
+ case 'MSB': // coarse
18059
+ bankMsb = GpifParser.parseIntSafe(c.innerText, 0);
18060
+ break;
18061
+ case 'LSB': // Fine
18062
+ bankLsb = GpifParser.parseIntSafe(c.innerText, 0);
18063
+ break;
17925
18064
  }
17926
18065
  }
18066
+ sound.bank = ((bankMsb & 0x7f) << 7) | bankLsb;
17927
18067
  }
17928
18068
  parsePartSounding(trackId, track, node) {
17929
18069
  for (const c of node.childElements()) {
@@ -19377,7 +19517,12 @@ class GpifParser {
19377
19517
  for (const a of automations) {
19378
19518
  // NOTE: currently the automations of a bar are applied to the
19379
19519
  // first beat of a bar
19380
- beat.automations.push(a);
19520
+ const skip =
19521
+ // skip bank automations if they are 0 at start
19522
+ a.type === AutomationType.Bank && a.value === 0 && bar.index === 0;
19523
+ if (!skip) {
19524
+ beat.automations.push(a);
19525
+ }
19381
19526
  }
19382
19527
  }
19383
19528
  }
@@ -20397,6 +20542,10 @@ class InstrumentArticulationWithPlaybackInfo extends InstrumentArticulation {
20397
20542
  * The midi channel program to use when playing the note (-1 if using the default track program).
20398
20543
  */
20399
20544
  this.outputMidiProgram = -1;
20545
+ /**
20546
+ * The midi bank to use when playing the note (-1 if using the default track bank).
20547
+ */
20548
+ this.outputMidiBank = -1;
20400
20549
  /**
20401
20550
  * The volume to use when playing the note (-1 if using the default track volume).
20402
20551
  */
@@ -20895,6 +21044,9 @@ class MusicXmlImporter extends ScoreImporter {
20895
21044
  if (trackInfo.firstArticulation.outputMidiProgram >= 0) {
20896
21045
  track.playbackInfo.program = trackInfo.firstArticulation.outputMidiProgram;
20897
21046
  }
21047
+ if (trackInfo.firstArticulation.outputMidiBank >= 0) {
21048
+ track.playbackInfo.bank = trackInfo.firstArticulation.outputMidiBank;
21049
+ }
20898
21050
  if (trackInfo.firstArticulation.outputBalance >= 0) {
20899
21051
  track.playbackInfo.balance = trackInfo.firstArticulation.outputBalance;
20900
21052
  }
@@ -20926,7 +21078,9 @@ class MusicXmlImporter extends ScoreImporter {
20926
21078
  articulation.outputMidiChannel = Number.parseInt(c.innerText) - 1;
20927
21079
  break;
20928
21080
  // case 'midi-name': Ignored
20929
- // case 'midi-bank': Not supported (https://github.com/CoderLine/alphaTab/issues/1986)
21081
+ case 'midi-bank':
21082
+ articulation.outputMidiBank = Number.parseInt(c.innerText) - 1;
21083
+ break;
20930
21084
  case 'midi-program':
20931
21085
  articulation.outputMidiProgram = Number.parseInt(c.innerText) - 1;
20932
21086
  break;
@@ -21456,7 +21610,15 @@ class MusicXmlImporter extends ScoreImporter {
21456
21610
  switch (c.localName) {
21457
21611
  // case 'midi-channel': Ignored
21458
21612
  // case 'midi-name': Ignored
21459
- // case 'midi-bank': Ignored
21613
+ case 'midi-bank':
21614
+ if (!this._nextBeatAutomations) {
21615
+ this._nextBeatAutomations = [];
21616
+ }
21617
+ automation = new Automation();
21618
+ automation.type = AutomationType.Bank;
21619
+ automation.value = Number.parseInt(c.innerText) - 1;
21620
+ this._nextBeatAutomations.push(automation);
21621
+ break;
21460
21622
  case 'midi-program':
21461
21623
  if (!this._nextBeatAutomations) {
21462
21624
  this._nextBeatAutomations = [];
@@ -23727,11 +23889,15 @@ class CircularSampleBuffer {
23727
23889
  }
23728
23890
 
23729
23891
  class EventEmitter {
23730
- constructor() {
23892
+ constructor(fireOnRegister = undefined) {
23731
23893
  this._listeners = [];
23894
+ this._fireOnRegister = fireOnRegister;
23732
23895
  }
23733
23896
  on(value) {
23734
23897
  this._listeners.push(value);
23898
+ if (this._fireOnRegister?.()) {
23899
+ value();
23900
+ }
23735
23901
  return () => {
23736
23902
  this.off(value);
23737
23903
  };
@@ -23749,11 +23915,18 @@ class EventEmitter {
23749
23915
  * @partial
23750
23916
  */
23751
23917
  class EventEmitterOfT {
23752
- constructor() {
23918
+ constructor(fireOnRegister = undefined) {
23753
23919
  this._listeners = [];
23920
+ this._fireOnRegister = fireOnRegister;
23754
23921
  }
23755
23922
  on(value) {
23756
23923
  this._listeners.push(value);
23924
+ if (this._fireOnRegister) {
23925
+ const arg = this._fireOnRegister();
23926
+ if (arg !== null) {
23927
+ value(arg);
23928
+ }
23929
+ }
23757
23930
  return () => {
23758
23931
  this.off(value);
23759
23932
  };
@@ -30794,6 +30967,12 @@ class AlphaSynthBase {
30794
30967
  this.sequencer.playbackSpeed = value;
30795
30968
  this.timePosition = this.timePosition * (oldSpeed / value);
30796
30969
  }
30970
+ get loadedMidiInfo() {
30971
+ return this._loadedMidiInfo;
30972
+ }
30973
+ get currentPosition() {
30974
+ return this._currentPosition;
30975
+ }
30797
30976
  get tickPosition() {
30798
30977
  return this._tickPosition;
30799
30978
  }
@@ -30851,22 +31030,38 @@ class AlphaSynthBase {
30851
31030
  this._midiEventsPlayedFilter = new Set();
30852
31031
  this._notPlayedSamples = 0;
30853
31032
  this._synthStopping = false;
31033
+ this._currentPosition = new PositionChangedEventArgs(0, 0, 0, 0, false, 120, 120);
30854
31034
  this.isReady = false;
30855
31035
  this.state = PlayerState.Paused;
30856
31036
  this._loadedSoundFonts = [];
30857
- this.ready = new EventEmitter();
30858
31037
  this.readyForPlayback = new EventEmitter();
30859
31038
  this.finished = new EventEmitter();
30860
31039
  this.soundFontLoaded = new EventEmitter();
30861
31040
  this.soundFontLoadFailed = new EventEmitterOfT();
30862
- this.midiLoaded = new EventEmitterOfT();
30863
31041
  this.midiLoadFailed = new EventEmitterOfT();
30864
- this.stateChanged = new EventEmitterOfT();
30865
- this.positionChanged = new EventEmitterOfT();
30866
31042
  this.midiEventsPlayed = new EventEmitterOfT();
30867
- this.playbackRangeChanged = new EventEmitterOfT();
30868
31043
  Logger.debug('AlphaSynth', 'Initializing player');
30869
31044
  this.state = PlayerState.Paused;
31045
+ this.ready = new EventEmitter(() => this.isReady);
31046
+ this.readyForPlayback = new EventEmitter(() => this.isReadyForPlayback);
31047
+ this.midiLoaded = new EventEmitterOfT(() => {
31048
+ if (this._loadedMidiInfo) {
31049
+ return this._loadedMidiInfo;
31050
+ }
31051
+ return null;
31052
+ });
31053
+ this.stateChanged = new EventEmitterOfT(() => {
31054
+ return new PlayerStateChangedEventArgs(this.state, false);
31055
+ });
31056
+ this.positionChanged = new EventEmitterOfT(() => {
31057
+ return this._currentPosition;
31058
+ });
31059
+ this.playbackRangeChanged = new EventEmitterOfT(() => {
31060
+ if (this.playbackRange) {
31061
+ return new PlaybackRangeChangedEventArgs(this.playbackRange);
31062
+ }
31063
+ return null;
31064
+ });
30870
31065
  Logger.debug('AlphaSynth', 'Creating output');
30871
31066
  this._output = output;
30872
31067
  Logger.debug('AlphaSynth', 'Creating synthesizer');
@@ -31044,7 +31239,8 @@ class AlphaSynthBase {
31044
31239
  Logger.debug('AlphaSynth', 'Loading midi from model');
31045
31240
  this.sequencer.loadMidi(midi);
31046
31241
  this._isMidiLoaded = true;
31047
- this.midiLoaded.trigger(new PositionChangedEventArgs(0, this.sequencer.currentEndTime, 0, this.sequencer.currentEndTick, false, this.sequencer.currentTempo, this.sequencer.modifiedTempo));
31242
+ this._loadedMidiInfo = new PositionChangedEventArgs(0, this.sequencer.currentEndTime, 0, this.sequencer.currentEndTick, false, this.sequencer.currentTempo, this.sequencer.modifiedTempo);
31243
+ this.midiLoaded.trigger(this._loadedMidiInfo);
31048
31244
  Logger.debug('AlphaSynth', 'Midi successfully loaded');
31049
31245
  this.checkReadyForPlayback();
31050
31246
  this.tickPosition = 0;
@@ -31147,23 +31343,28 @@ class AlphaSynthBase {
31147
31343
  this.sequencer.resetOneTimeMidi();
31148
31344
  this.timePosition = this.sequencer.currentTime;
31149
31345
  }
31150
- updateTimePosition(timePosition, isSeek) {
31151
- // update the real positions
31152
- let currentTime = timePosition;
31153
- this._timePosition = currentTime;
31346
+ createPositionChangedEventArgs(isSeek) {
31347
+ // on fade outs we can have some milliseconds longer, ensure we don't report this
31348
+ let currentTime = this._timePosition;
31154
31349
  let currentTick = this.sequencer.currentTimePositionToTickPosition(currentTime);
31155
- this._tickPosition = currentTick;
31156
31350
  const endTime = this.sequencer.currentEndTime;
31157
31351
  const endTick = this.sequencer.currentEndTick;
31158
- // on fade outs we can have some milliseconds longer, ensure we don't report this
31159
31352
  if (currentTime > endTime) {
31160
31353
  currentTime = endTime;
31161
31354
  currentTick = endTick;
31162
31355
  }
31356
+ return new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick, isSeek, this.sequencer.currentTempo, this.sequencer.modifiedTempo);
31357
+ }
31358
+ updateTimePosition(timePosition, isSeek) {
31359
+ // update the real positions
31360
+ this._timePosition = timePosition;
31361
+ const args = this.createPositionChangedEventArgs(isSeek);
31362
+ this._tickPosition = args.currentTick;
31163
31363
  const mode = this.sequencer.isPlayingMain ? 'main' : this.sequencer.isPlayingCountIn ? 'count-in' : 'one-time';
31164
- 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})`);
31364
+ Logger.debug('AlphaSynth', `Position changed: (time: ${args.currentTime}/${args.endTime}, tick: ${args.currentTick}/${args.endTick}, Active Voices: ${this.synthesizer.activeVoiceCount} (${mode}), Tempo original: ${this.sequencer.currentTempo}, Tempo modified: ${this.sequencer.modifiedTempo})`);
31165
31365
  if (this.sequencer.isPlayingMain) {
31166
- this.positionChanged.trigger(new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick, isSeek, this.sequencer.currentTempo, this.sequencer.modifiedTempo));
31366
+ this._currentPosition = args;
31367
+ this.positionChanged.trigger(args);
31167
31368
  }
31168
31369
  // build events which were actually played
31169
31370
  if (isSeek) {
@@ -31171,7 +31372,7 @@ class AlphaSynthBase {
31171
31372
  }
31172
31373
  else {
31173
31374
  const playedEvents = [];
31174
- while (!this._playedEventsQueue.isEmpty && this._playedEventsQueue.peek().time < currentTime) {
31375
+ while (!this._playedEventsQueue.isEmpty && this._playedEventsQueue.peek().time < args.currentTime) {
31175
31376
  const synthEvent = this._playedEventsQueue.dequeue();
31176
31377
  playedEvents.push(synthEvent.event);
31177
31378
  }
@@ -36622,6 +36823,15 @@ class Settings {
36622
36823
  fillFromJson(json) {
36623
36824
  SettingsSerializer.fromJson(this, json);
36624
36825
  }
36826
+ /**
36827
+ * handles backwards compatibility aspects on the settings, removed in 2.0
36828
+ * @internal
36829
+ */
36830
+ handleBackwardsCompatibility() {
36831
+ if (this.player.playerMode === PlayerMode.Disabled && this.player.enablePlayer) {
36832
+ this.player.playerMode = PlayerMode.EnabledAutomatic;
36833
+ }
36834
+ }
36625
36835
  }
36626
36836
 
36627
36837
  class SectionSerializer {
@@ -37839,6 +38049,7 @@ class PlaybackInformationSerializer {
37839
38049
  o.set("balance", obj.balance);
37840
38050
  o.set("port", obj.port);
37841
38051
  o.set("program", obj.program);
38052
+ o.set("bank", obj.bank);
37842
38053
  o.set("primarychannel", obj.primaryChannel);
37843
38054
  o.set("secondarychannel", obj.secondaryChannel);
37844
38055
  o.set("ismute", obj.isMute);
@@ -37859,6 +38070,9 @@ class PlaybackInformationSerializer {
37859
38070
  case "program":
37860
38071
  obj.program = v;
37861
38072
  return true;
38073
+ case "bank":
38074
+ obj.bank = v;
38075
+ return true;
37862
38076
  case "primarychannel":
37863
38077
  obj.primaryChannel = v;
37864
38078
  return true;
@@ -41517,6 +41731,11 @@ class MidiFileGenerator {
41517
41731
  this._programsPerChannel.set(channel, program);
41518
41732
  }
41519
41733
  }
41734
+ addBankChange(track, tick, channel, bank) {
41735
+ const lsbMsb = GeneralMidi.bankToLsbMsb(bank);
41736
+ this._handler.addControlChange(track.index, tick, channel, ControllerType.BankSelectCoarse, lsbMsb[1]);
41737
+ this._handler.addControlChange(track.index, tick, channel, ControllerType.BankSelectFine, lsbMsb[0]);
41738
+ }
41520
41739
  static buildTranspositionPitches(score, settings) {
41521
41740
  const transpositionPitches = new Map();
41522
41741
  for (const track of score.tracks) {
@@ -41544,6 +41763,7 @@ class MidiFileGenerator {
41544
41763
  // Set PitchBendRangeCoarse to 12
41545
41764
  this._handler.addControlChange(track.index, 0, channel, ControllerType.DataEntryFine, 0);
41546
41765
  this._handler.addControlChange(track.index, 0, channel, ControllerType.DataEntryCoarse, MidiFileGenerator.PitchBendRangeInSemitones);
41766
+ this.addBankChange(track, 0, channel, playbackInfo.bank);
41547
41767
  this.addProgramChange(track, 0, channel, playbackInfo.program);
41548
41768
  }
41549
41769
  /**
@@ -42839,6 +43059,10 @@ class MidiFileGenerator {
42839
43059
  this.addProgramChange(beat.voice.bar.staff.track, beat.playbackStart + startMove, beat.voice.bar.staff.track.playbackInfo.primaryChannel, (automation.value | 0) & 0xff);
42840
43060
  this.addProgramChange(beat.voice.bar.staff.track, beat.playbackStart + startMove, beat.voice.bar.staff.track.playbackInfo.secondaryChannel, (automation.value | 0) & 0xff);
42841
43061
  break;
43062
+ case AutomationType.Bank:
43063
+ this.addBankChange(beat.voice.bar.staff.track, beat.playbackStart + startMove, beat.voice.bar.staff.track.playbackInfo.primaryChannel, automation.value);
43064
+ this.addBankChange(beat.voice.bar.staff.track, beat.playbackStart + startMove, beat.voice.bar.staff.track.playbackInfo.secondaryChannel, automation.value);
43065
+ break;
42842
43066
  case AutomationType.Balance:
42843
43067
  const balance = MidiFileGenerator.toChannelShort(automation.value);
42844
43068
  this._handler.addControlChange(beat.voice.bar.staff.track.index, beat.playbackStart + startMove, beat.voice.bar.staff.track.playbackInfo.primaryChannel, ControllerType.PanCoarse, balance);
@@ -43788,17 +44012,29 @@ class AlphaSynthWrapper {
43788
44012
  this._playbackSpeed = 1;
43789
44013
  this._isLooping = false;
43790
44014
  this._midiEventsPlayedFilter = [];
43791
- this.ready = new EventEmitter();
43792
- this.readyForPlayback = new EventEmitter();
43793
44015
  this.finished = new EventEmitter();
43794
44016
  this.soundFontLoaded = new EventEmitter();
43795
44017
  this.soundFontLoadFailed = new EventEmitterOfT();
43796
- this.midiLoaded = new EventEmitterOfT();
43797
44018
  this.midiLoadFailed = new EventEmitterOfT();
43798
- this.stateChanged = new EventEmitterOfT();
43799
- this.positionChanged = new EventEmitterOfT();
43800
44019
  this.midiEventsPlayed = new EventEmitterOfT();
43801
- this.playbackRangeChanged = new EventEmitterOfT();
44020
+ this.ready = new EventEmitter(() => this.isReady);
44021
+ this.readyForPlayback = new EventEmitter(() => this.isReadyForPlayback);
44022
+ this.midiLoaded = new EventEmitterOfT(() => {
44023
+ return this._instance?.loadedMidiInfo ?? null;
44024
+ });
44025
+ this.stateChanged = new EventEmitterOfT(() => {
44026
+ return new PlayerStateChangedEventArgs(this.state, false);
44027
+ });
44028
+ this.positionChanged = new EventEmitterOfT(() => {
44029
+ return this.currentPosition;
44030
+ });
44031
+ this.playbackRangeChanged = new EventEmitterOfT(() => {
44032
+ const range = this.playbackRange;
44033
+ if (range) {
44034
+ return new PlaybackRangeChangedEventArgs(range);
44035
+ }
44036
+ return null;
44037
+ });
43802
44038
  }
43803
44039
  get instance() {
43804
44040
  return this._instance;
@@ -43820,10 +44056,14 @@ class AlphaSynthWrapper {
43820
44056
  newUnregister.push(value.finished.on(() => this.finished.trigger()));
43821
44057
  newUnregister.push(value.soundFontLoaded.on(() => this.soundFontLoaded.trigger()));
43822
44058
  newUnregister.push(value.soundFontLoadFailed.on(e => this.soundFontLoadFailed.trigger(e)));
43823
- newUnregister.push(value.midiLoaded.on(e => this.midiLoaded.trigger(e)));
44059
+ newUnregister.push(value.midiLoaded.on(e => {
44060
+ this.midiLoaded.trigger(e);
44061
+ }));
43824
44062
  newUnregister.push(value.midiLoadFailed.on(e => this.midiLoadFailed.trigger(e)));
43825
44063
  newUnregister.push(value.stateChanged.on(e => this.stateChanged.trigger(e)));
43826
- newUnregister.push(value.positionChanged.on(e => this.positionChanged.trigger(e)));
44064
+ newUnregister.push(value.positionChanged.on(e => {
44065
+ this.positionChanged.trigger(e);
44066
+ }));
43827
44067
  newUnregister.push(value.midiEventsPlayed.on(e => this.midiEventsPlayed.trigger(e)));
43828
44068
  newUnregister.push(value.playbackRangeChanged.on(e => this.playbackRangeChanged.trigger(e)));
43829
44069
  this._instanceEventUnregister = newUnregister;
@@ -43902,6 +44142,14 @@ class AlphaSynthWrapper {
43902
44142
  this._instance.playbackSpeed = value;
43903
44143
  }
43904
44144
  }
44145
+ get loadedMidiInfo() {
44146
+ return this._instance ? this._instance.loadedMidiInfo : undefined;
44147
+ }
44148
+ get currentPosition() {
44149
+ return this._instance
44150
+ ? this._instance.currentPosition
44151
+ : new PositionChangedEventArgs(0, 0, 0, 0, false, 120, 120);
44152
+ }
43905
44153
  get tickPosition() {
43906
44154
  return this._instance ? this._instance.tickPosition : 0;
43907
44155
  }
@@ -44243,83 +44491,6 @@ class AlphaTabApiBase {
44243
44491
  this._previousStateForCursor = PlayerState.Paused;
44244
44492
  this._previousCursorCache = null;
44245
44493
  this._lastScroll = 0;
44246
- /**
44247
- * This event is fired when the played beat changed.
44248
- *
44249
- * @eventProperty
44250
- * @category Events - Player
44251
- * @since 0.9.4
44252
- *
44253
- * @example
44254
- * JavaScript
44255
- * ```js
44256
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
44257
- * api.playedBeatChanged.on((beat) => {
44258
- * updateFretboard(beat);
44259
- * });
44260
- * ```
44261
- *
44262
- * @example
44263
- * C#
44264
- * ```cs
44265
- * var api = new AlphaTabApi<MyControl>(...);
44266
- * api.PlayedBeatChanged.On(beat =>
44267
- * {
44268
- * UpdateFretboard(beat);
44269
- * });
44270
- * ```
44271
- *
44272
- * @example
44273
- * Android
44274
- * ```kotlin
44275
- * val api = AlphaTabApi<MyControl>(...)
44276
- * api.playedBeatChanged.on { beat ->
44277
- * updateFretboard(beat)
44278
- * }
44279
- * ```
44280
- *
44281
- */
44282
- this.playedBeatChanged = new EventEmitterOfT();
44283
- /**
44284
- * This event is fired when the currently active beats across all tracks change.
44285
- *
44286
- * @remarks
44287
- * Unlike the {@link playedBeatChanged} event this event contains the beats of all tracks and voices independent of them being rendered.
44288
- *
44289
- * @eventProperty
44290
- * @category Events - Player
44291
- * @since 1.2.3
44292
- *
44293
- * @example
44294
- * JavaScript
44295
- * ```js
44296
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
44297
- * api.activeBeatsChanged.on(args => {
44298
- * updateHighlights(args.activeBeats);
44299
- * });
44300
- * ```
44301
- *
44302
- * @example
44303
- * C#
44304
- * ```cs
44305
- * var api = new AlphaTabApi<MyControl>(...);
44306
- * api.ActiveBeatsChanged.On(args =>
44307
- * {
44308
- * UpdateHighlights(args.ActiveBeats);
44309
- * });
44310
- * ```
44311
- *
44312
- * @example
44313
- * Android
44314
- * ```kotlin
44315
- * val api = AlphaTabApi<MyControl>(...)
44316
- * api.activeBeatsChanged.on { args ->
44317
- * updateHighlights(args.activeBeats)
44318
- * }
44319
- * ```
44320
- *
44321
- */
44322
- this.activeBeatsChanged = new EventEmitterOfT();
44323
44494
  this._beatMouseDown = false;
44324
44495
  this._noteMouseDown = false;
44325
44496
  this._selectionStart = null;
@@ -44558,47 +44729,6 @@ class AlphaTabApiBase {
44558
44729
  *
44559
44730
  */
44560
44731
  this.noteMouseUp = new EventEmitterOfT();
44561
- /**
44562
- * This event is fired whenever a new song is loaded.
44563
- * @remarks
44564
- * This event is fired whenever a new song is loaded or changing due to {@link renderScore} or {@link renderTracks} calls.
44565
- * It is fired after the transposition midi pitches from the settings were applied, but before any midi is generated or rendering is started.
44566
- * This allows any modification of the score before further processing.
44567
- *
44568
- * @eventProperty
44569
- * @category Events - Core
44570
- * @since 0.9.4
44571
- *
44572
- * @example
44573
- * JavaScript
44574
- * ```js
44575
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
44576
- * api.scoreLoaded.on((score) => {
44577
- * updateSongInformationInUi(score);
44578
- * });
44579
- * ```
44580
- *
44581
- * @example
44582
- * C#
44583
- * ```cs
44584
- * var api = new AlphaTabApi<MyControl>(...);
44585
- * api.ScoreLoaded.On(score =>
44586
- * {
44587
- * UpdateSongInformationInUi(score);
44588
- * });
44589
- * ```
44590
- *
44591
- * @example
44592
- * Android
44593
- * ```kotlin
44594
- * val api = AlphaTabApi<MyControl>(...)
44595
- * api.scoreLoaded.on { score ->
44596
- * updateSongInformationInUi(score)
44597
- * }
44598
- * ```
44599
- *
44600
- */
44601
- this.scoreLoaded = new EventEmitterOfT();
44602
44732
  /**
44603
44733
  * This event is fired when alphaTab was resized and is about to rerender the music notation.
44604
44734
  * @remarks
@@ -44857,46 +44987,6 @@ class AlphaTabApiBase {
44857
44987
  *
44858
44988
  */
44859
44989
  this.midiLoad = new EventEmitterOfT();
44860
- /**
44861
- * This event is fired when the Midi file needed for playback was loaded.
44862
- *
44863
- * @eventProperty
44864
- * @category Events - Player
44865
- * @since 0.9.4
44866
- *
44867
- * @example
44868
- * JavaScript
44869
- * ```js
44870
- * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
44871
- * api.midiLoaded.on(e => {
44872
- * hideGeneratingAudioIndicator();
44873
- * updateSongDuration(e.endTime);
44874
- * });
44875
- * ```
44876
- *
44877
- * @example
44878
- * C#
44879
- * ```cs
44880
- * var api = new AlphaTabApi<MyControl>(...);
44881
- * api.MidiLoaded.On(e =>
44882
- * {
44883
- * HideGeneratingAudioIndicator();
44884
- * UpdateSongDuration(e.EndTime);
44885
- * });
44886
- * ```
44887
- *
44888
- * @example
44889
- * Android
44890
- * ```kotlin
44891
- * val api = AlphaTabApi<MyControl>(...)
44892
- * api.midiLoaded.on { e ->
44893
- * hideGeneratingAudioIndicator()
44894
- * updateSongDuration(e.endTime)
44895
- * }
44896
- * ```
44897
- *
44898
- */
44899
- this.midiLoaded = new EventEmitterOfT();
44900
44990
  /**
44901
44991
  * This event is fired when a settings update was requested.
44902
44992
  *
@@ -44936,12 +45026,30 @@ class AlphaTabApiBase {
44936
45026
  this.settingsUpdated = new EventEmitter();
44937
45027
  this.uiFacade = uiFacade;
44938
45028
  this.container = uiFacade.rootContainer;
45029
+ this.activeBeatsChanged = new EventEmitterOfT(() => {
45030
+ if (this._player.state === PlayerState.Playing && this._currentBeat) {
45031
+ return new ActiveBeatsChangedEventArgs(this._currentBeat.beatLookup.highlightedBeats.map(h => h.beat));
45032
+ }
45033
+ return null;
45034
+ });
45035
+ this.playedBeatChanged = new EventEmitterOfT(() => {
45036
+ if (this._player.state === PlayerState.Playing && this._currentBeat) {
45037
+ return this._currentBeat.beat;
45038
+ }
45039
+ return null;
45040
+ });
45041
+ this.scoreLoaded = new EventEmitterOfT(() => {
45042
+ if (this._score) {
45043
+ return this._score;
45044
+ }
45045
+ return null;
45046
+ });
45047
+ this.midiLoaded = new EventEmitterOfT(() => {
45048
+ return this._player.loadedMidiInfo ?? null;
45049
+ });
44939
45050
  uiFacade.initialize(this, settings);
44940
45051
  Logger.logLevel = this.settings.core.logLevel;
44941
- // backwards compatibility: remove in 2.0
44942
- if (this.settings.player.playerMode === PlayerMode.Disabled && this.settings.player.enablePlayer) {
44943
- this.settings.player.playerMode = PlayerMode.EnabledAutomatic;
44944
- }
45052
+ this.settings.handleBackwardsCompatibility();
44945
45053
  Environment.printEnvironmentInfo(false);
44946
45054
  this.canvasElement = uiFacade.createCanvasElement();
44947
45055
  this.container.appendChild(this.canvasElement);
@@ -45100,6 +45208,7 @@ class AlphaTabApiBase {
45100
45208
  * ```
45101
45209
  */
45102
45210
  updateSettings() {
45211
+ this.settings.handleBackwardsCompatibility();
45103
45212
  const score = this.score;
45104
45213
  if (score) {
45105
45214
  ModelUtils.applyPitchOffsets(this.settings, score);
@@ -45907,6 +46016,22 @@ class AlphaTabApiBase {
45907
46016
  set timePosition(value) {
45908
46017
  this._player.timePosition = value;
45909
46018
  }
46019
+ /**
46020
+ * The total length of the song in midi ticks.
46021
+ * @category Properties - Player
46022
+ * @since 1.6.2
46023
+ */
46024
+ get endTick() {
46025
+ return this._player.currentPosition.endTick;
46026
+ }
46027
+ /**
46028
+ * The total length of the song in milliseconds.
46029
+ * @category Properties - Player
46030
+ * @since 1.6.2
46031
+ */
46032
+ get endTime() {
46033
+ return this._player.currentPosition.endTime;
46034
+ }
45910
46035
  /**
45911
46036
  * The range of the song that should be played.
45912
46037
  * @remarks
@@ -48231,27 +48356,33 @@ class AlphaSynthWebWorkerApi {
48231
48356
  value: value
48232
48357
  });
48233
48358
  }
48359
+ get loadedMidiInfo() {
48360
+ return this.loadedMidiInfo;
48361
+ }
48362
+ get currentPosition() {
48363
+ return this._currentPosition;
48364
+ }
48234
48365
  get tickPosition() {
48235
- return this._tickPosition;
48366
+ return this._currentPosition.currentTick;
48236
48367
  }
48237
48368
  set tickPosition(value) {
48238
48369
  if (value < 0) {
48239
48370
  value = 0;
48240
48371
  }
48241
- this._tickPosition = value;
48372
+ this._currentPosition = new PositionChangedEventArgs(this._currentPosition.currentTime, this._currentPosition.endTime, value, this._currentPosition.endTick, true, this._currentPosition.originalTempo, this._currentPosition.modifiedTempo);
48242
48373
  this._synth.postMessage({
48243
48374
  cmd: 'alphaSynth.setTickPosition',
48244
48375
  value: value
48245
48376
  });
48246
48377
  }
48247
48378
  get timePosition() {
48248
- return this._timePosition;
48379
+ return this._currentPosition.currentTime;
48249
48380
  }
48250
48381
  set timePosition(value) {
48251
48382
  if (value < 0) {
48252
48383
  value = 0;
48253
48384
  }
48254
- this._timePosition = value;
48385
+ this._currentPosition = new PositionChangedEventArgs(value, this._currentPosition.endTime, this._currentPosition.currentTick, this._currentPosition.endTick, true, this._currentPosition.originalTempo, this._currentPosition.modifiedTempo);
48255
48386
  this._synth.postMessage({
48256
48387
  cmd: 'alphaSynth.setTimePosition',
48257
48388
  value: value
@@ -48294,11 +48425,10 @@ class AlphaSynthWebWorkerApi {
48294
48425
  this._metronomeVolume = 0;
48295
48426
  this._countInVolume = 0;
48296
48427
  this._playbackSpeed = 0;
48297
- this._tickPosition = 0;
48298
- this._timePosition = 0;
48299
48428
  this._isLooping = false;
48300
48429
  this._playbackRange = null;
48301
48430
  this._midiEventsPlayedFilter = [];
48431
+ this._currentPosition = new PositionChangedEventArgs(0, 0, 0, 0, false, 120, 120);
48302
48432
  this.ready = new EventEmitter();
48303
48433
  this.readyForPlayback = new EventEmitter();
48304
48434
  this.finished = new EventEmitter();
@@ -48317,8 +48447,6 @@ class AlphaSynthWebWorkerApi {
48317
48447
  this._masterVolume = 0.0;
48318
48448
  this._metronomeVolume = 0.0;
48319
48449
  this._playbackSpeed = 0.0;
48320
- this._tickPosition = 0;
48321
- this._timePosition = 0.0;
48322
48450
  this._isLooping = false;
48323
48451
  this._playbackRange = null;
48324
48452
  this._output = player;
@@ -48453,9 +48581,8 @@ class AlphaSynthWebWorkerApi {
48453
48581
  this.checkReadyForPlayback();
48454
48582
  break;
48455
48583
  case 'alphaSynth.positionChanged':
48456
- this._timePosition = data.currentTime;
48457
- this._tickPosition = data.currentTick;
48458
- this.positionChanged.trigger(new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek, data.originalTempo, data.modifiedTempo));
48584
+ this._currentPosition = new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek, data.originalTempo, data.modifiedTempo);
48585
+ this.positionChanged.trigger(this._currentPosition);
48459
48586
  break;
48460
48587
  case 'alphaSynth.midiEventsPlayed':
48461
48588
  this.midiEventsPlayed.trigger(new MidiEventsPlayedEventArgs(data.events.map(JsonConverter.jsObjectToMidiEvent)));
@@ -48479,7 +48606,8 @@ class AlphaSynthWebWorkerApi {
48479
48606
  break;
48480
48607
  case 'alphaSynth.midiLoaded':
48481
48608
  this.checkReadyForPlayback();
48482
- this.midiLoaded.trigger(new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek, data.originalTempo, data.modifiedTempo));
48609
+ this._loadedMidiInfo = new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek, data.originalTempo, data.modifiedTempo);
48610
+ this.midiLoaded.trigger(this._loadedMidiInfo);
48483
48611
  break;
48484
48612
  case 'alphaSynth.midiLoadFailed':
48485
48613
  this.checkReadyForPlayback();
@@ -66622,6 +66750,10 @@ class AlphaTexExporter extends ScoreExporter {
66622
66750
  track.score.stylesheet.perTrackMultiBarRest.has(track.index)) {
66623
66751
  writer.writeLine(` multibarrest`);
66624
66752
  }
66753
+ writer.writeLine(` instrument ${track.isPercussion ? 'percussion' : GeneralMidi.getName(track.playbackInfo.program)}`);
66754
+ if (track.playbackInfo.bank > 0) {
66755
+ writer.writeLine(` bank ${track.playbackInfo.bank}`);
66756
+ }
66625
66757
  writer.outdent();
66626
66758
  writer.writeLine('}');
66627
66759
  writer.indent();
@@ -66680,13 +66812,7 @@ class AlphaTexExporter extends ScoreExporter {
66680
66812
  writer.writeLine('|');
66681
66813
  }
66682
66814
  if (voiceIndex === 0) {
66683
- // Track meta on first bar
66684
66815
  let anyWritten = false;
66685
- if (bar.index === 0 && bar.staff.index === 0) {
66686
- const l = writer.tex.length;
66687
- this.writeTrackMetaTo(writer, bar.staff.track);
66688
- anyWritten = writer.tex.length > l;
66689
- }
66690
66816
  // Staff meta on first bar
66691
66817
  if (bar.index === 0) {
66692
66818
  const l = writer.tex.length;
@@ -66716,14 +66842,6 @@ class AlphaTexExporter extends ScoreExporter {
66716
66842
  }
66717
66843
  writer.outdent();
66718
66844
  }
66719
- writeTrackMetaTo(writer, track) {
66720
- writer.writeSingleLineComment(`Track ${track.index + 1} Metadata`);
66721
- this.writePlaybackInfoTo(writer, track);
66722
- }
66723
- writePlaybackInfoTo(writer, track) {
66724
- const isPercussion = track.staves.some(s => s.isPercussion);
66725
- writer.writeMeta('instrument', isPercussion ? 'percussion' : GeneralMidi.getName(track.playbackInfo.program));
66726
- }
66727
66845
  writeStaffMetaTo(writer, staff) {
66728
66846
  writer.writeSingleLineComment(`Staff ${staff.index + 1} Metadata`);
66729
66847
  if (staff.capo !== 0) {
@@ -68346,15 +68464,16 @@ class GpifWriter {
68346
68464
  }
68347
68465
  return GpifIconIds.SteelGuitar;
68348
68466
  }
68349
- writeSoundAndAutomation(soundsNode, automationsNode, name, path, role, barIndex, program, ratioPosition = 0) {
68467
+ writeSoundAndAutomation(soundsNode, automationsNode, name, path, role, barIndex, program, bank, ratioPosition = 0) {
68350
68468
  const soundNode = soundsNode.addElement('Sound');
68351
68469
  soundNode.addElement('Name').setCData(name);
68352
68470
  soundNode.addElement('Label').setCData(name);
68353
68471
  soundNode.addElement('Path').setCData(path);
68354
68472
  soundNode.addElement('Role').setCData(role);
68355
68473
  const midi = soundNode.addElement('MIDI');
68356
- midi.addElement('LSB').innerText = '0';
68357
- midi.addElement('MSB').innerText = '0';
68474
+ const lsbMsb = GeneralMidi.bankToLsbMsb(bank);
68475
+ midi.addElement('LSB').innerText = lsbMsb[0].toString();
68476
+ midi.addElement('MSB').innerText = lsbMsb[1].toString();
68358
68477
  midi.addElement('Program').innerText = program.toString();
68359
68478
  const automationNode = automationsNode.addElement('Automation');
68360
68479
  automationNode.addElement('Type').innerText = 'Sound';
@@ -68372,21 +68491,26 @@ class GpifWriter {
68372
68491
  const trackSoundPath = `Midi/${track.playbackInfo.program}`;
68373
68492
  const trackSoundRole = 'Factory';
68374
68493
  let trackSoundWritten = false;
68494
+ let bank = track.playbackInfo.bank;
68375
68495
  for (const staff of track.staves) {
68376
68496
  for (const bar of staff.bars) {
68377
68497
  for (const voice of bar.voices) {
68378
68498
  for (const beat of voice.beats) {
68379
68499
  const soundAutomation = beat.getAutomation(AutomationType.Instrument);
68380
68500
  const isTrackSound = bar.index === 0 && beat.index === 0;
68501
+ const bankAutomation = beat.getAutomation(AutomationType.Bank);
68502
+ if (bankAutomation) {
68503
+ bank = bankAutomation.value;
68504
+ }
68381
68505
  if (soundAutomation) {
68382
68506
  const name = isTrackSound ? trackSoundName : `ProgramChange_${beat.id}`;
68383
68507
  const path = isTrackSound ? trackSoundPath : `Midi/${soundAutomation.value}`;
68384
68508
  const role = isTrackSound ? trackSoundRole : 'User';
68385
68509
  if (!isTrackSound && !trackSoundWritten) {
68386
- this.writeSoundAndAutomation(soundsNode, automationsNode, trackSoundName, trackSoundPath, trackSoundRole, track.staves[0].bars[0].index, track.playbackInfo.program);
68510
+ this.writeSoundAndAutomation(soundsNode, automationsNode, trackSoundName, trackSoundPath, trackSoundRole, track.staves[0].bars[0].index, track.playbackInfo.program, track.playbackInfo.bank);
68387
68511
  trackSoundWritten = true;
68388
68512
  }
68389
- this.writeSoundAndAutomation(soundsNode, automationsNode, name, path, role, bar.index, soundAutomation.value, soundAutomation.ratioPosition);
68513
+ this.writeSoundAndAutomation(soundsNode, automationsNode, name, path, role, bar.index, soundAutomation.value, bank, soundAutomation.ratioPosition);
68390
68514
  if (isTrackSound) {
68391
68515
  trackSoundWritten = true;
68392
68516
  }