@coderline/alphatab 1.9.0-alpha.1883 → 1.9.0-alpha.1891

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.9.0-alpha.1883 (develop, build 1883)
2
+ * alphaTab v1.9.0-alpha.1891 (develop, build 1891)
3
3
  *
4
4
  * Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -186,9 +186,9 @@ class AlphaTabError extends Error {
186
186
  * @internal
187
187
  */
188
188
  class VersionInfo {
189
- static version = "1.9.0-alpha.1883";
190
- static date = "2026-08-02T03:44:59.527Z";
191
- static commit = "a186437bb3263e5ae3f8fd373aef1fef5ebbc7e7";
189
+ static version = "1.9.0-alpha.1891";
190
+ static date = "2026-08-10T02:28:09.977Z";
191
+ static commit = "2a460d7c4b6d879465fe23382adc67bf6fd3196d";
192
192
  static print(print) {
193
193
  print(`alphaTab ${VersionInfo.version}`);
194
194
  print(`commit: ${VersionInfo.commit}`);
@@ -3134,7 +3134,7 @@ class GraceGroup {
3134
3134
  this.beats.push(beat);
3135
3135
  }
3136
3136
  finish() {
3137
- if (this.beats.length > 0) this.id = `${this.beats[0].absoluteDisplayStart}_${this.beats[0].voice.index}`;
3137
+ if (this.beats.length > 0) this.id = `${this.beats[0].voice.bar.id}_${this.beats[0].voice.index}_${this.beats[0].absoluteDisplayStart}`;
3138
3138
  }
3139
3139
  }
3140
3140
  //#endregion
@@ -4139,6 +4139,20 @@ class ModelUtils {
4139
4139
  }
4140
4140
  return headerFooterStyle;
4141
4141
  }
4142
+ static backfillStaffVoices(staff, targetVoiceCount) {
4143
+ for (let bi = 0; bi < staff.bars.length - 1; bi++) {
4144
+ const priorBar = staff.bars[bi];
4145
+ while (priorBar.voices.length < targetVoiceCount) ModelUtils.appendPlaceholderVoice(priorBar);
4146
+ }
4147
+ }
4148
+ static appendPlaceholderVoice(bar) {
4149
+ const voice = new Voice$1();
4150
+ bar.addVoice(voice);
4151
+ const beat = new Beat();
4152
+ beat.isEmpty = true;
4153
+ beat.duration = Duration.Quarter;
4154
+ voice.addBeat(beat);
4155
+ }
4142
4156
  /**
4143
4157
  * Performs some general consolidations of inconsistencies on the given score like
4144
4158
  * missing bars, beats, duplicated midi channels etc
@@ -4189,13 +4203,7 @@ class ModelUtils {
4189
4203
  bar.keySignature = bar.previousBar.keySignature;
4190
4204
  bar.keySignatureType = bar.previousBar.keySignatureType;
4191
4205
  }
4192
- for (let i = 0; i < voiceCount; i++) {
4193
- const v = new Voice$1();
4194
- bar.addVoice(v);
4195
- const emptyBeat = new Beat();
4196
- emptyBeat.isEmpty = true;
4197
- v.addBeat(emptyBeat);
4198
- }
4206
+ for (let i = 0; i < voiceCount; i++) ModelUtils.appendPlaceholderVoice(bar);
4199
4207
  }
4200
4208
  }
4201
4209
  }
@@ -4942,6 +4950,11 @@ class ModelUtils {
4942
4950
  const ksi = keySignature + 7;
4943
4951
  return keySignatureType === KeySignatureType.Minor ? ModelUtils._minorKeySignatureTonicDegrees[ksi] : ModelUtils._majorKeySignatureTonicDegrees[ksi];
4944
4952
  }
4953
+ /** True iff the staff's first note isn't stringed. Empty staves return false. */
4954
+ static staffNotesAreNotStringed(staff) {
4955
+ for (const bar of staff.bars) for (const voice of bar.voices) for (const beat of voice.beats) for (const note of beat.notes) return !note.isStringed;
4956
+ return false;
4957
+ }
4945
4958
  }
4946
4959
  //#endregion
4947
4960
  //#region src/model/PickStroke.ts
@@ -6332,7 +6345,10 @@ class Note {
6332
6345
  else realValue += this.harmonicPitch;
6333
6346
  return realValue;
6334
6347
  }
6335
- if (this.isPercussion) return this.percussionArticulation;
6348
+ if (this.isPercussion) {
6349
+ const art = PercussionMapper.getArticulation(this);
6350
+ return art !== null ? art.outputMidiNumber : this.percussionArticulation;
6351
+ }
6336
6352
  if (this.isStringed) return this.fret + this.stringTuning - transpositionPitch;
6337
6353
  if (this.isPiano) return this.octave * 12 + this.tone - transpositionPitch;
6338
6354
  return 0;
@@ -12855,6 +12871,18 @@ class Track {
12855
12871
  staff.track = this;
12856
12872
  this.staves.push(staff);
12857
12873
  }
12874
+ /**
12875
+ * Returns the index of {@link articulation} in {@link percussionArticulations},
12876
+ * appending it (deduplicated by `uniqueId`) when not yet present. Callers store the
12877
+ * returned index in {@link Note.percussionArticulation}.
12878
+ */
12879
+ getOrRegisterPercussionArticulation(articulation) {
12880
+ const uniqueId = articulation.uniqueId;
12881
+ for (let i = 0; i < this.percussionArticulations.length; i++) if (this.percussionArticulations[i].uniqueId === uniqueId) return i;
12882
+ const index = this.percussionArticulations.length;
12883
+ this.percussionArticulations.push(articulation);
12884
+ return index;
12885
+ }
12858
12886
  finish(settings, sharedDataBag = null) {
12859
12887
  if (!this.shortName) {
12860
12888
  this.shortName = this.name;
@@ -18350,11 +18378,38 @@ class Gp3To5Importer extends ScoreImporter {
18350
18378
  readVoice(track, bar) {
18351
18379
  const beatCount = IOHelper.readInt32LE(this.data);
18352
18380
  if (beatCount === 0) return;
18381
+ const currentVoiceCount = bar.index === 0 ? 1 : bar.previousBar.voices.length;
18382
+ if (bar.voices.length === 0 || currentVoiceCount === 2) {
18383
+ this._readAndChainVoice(track, bar, beatCount);
18384
+ return;
18385
+ }
18386
+ if (beatCount === 1 && this._skipEmptyVoice()) return;
18387
+ ModelUtils.backfillStaffVoices(bar.staff, 2);
18388
+ this._readAndChainVoice(track, bar, beatCount);
18389
+ }
18390
+ _readAndChainVoice(track, bar, beatCount) {
18353
18391
  const newVoice = new Voice$1();
18354
18392
  bar.addVoice(newVoice);
18355
18393
  this._ensureLoopBoundary(beatCount, Gp3To5Importer._maxBeatCount, "beat count");
18356
18394
  for (let i = 0; i < beatCount; i++) this.readBeat(track, bar, newVoice);
18357
18395
  }
18396
+ /**
18397
+ * Attempts to skip a fully empty voice.
18398
+ * @returns true if we detected an empty voice, false if the beat was not empty and a full voice has to be read.
18399
+ */
18400
+ _skipEmptyVoice() {
18401
+ const startOfVoice = this.data.position;
18402
+ const flags = this.data.readByte();
18403
+ let isEmpty = false;
18404
+ if ((flags & 64) !== 0) isEmpty = (this.data.readByte() & 2) === 0;
18405
+ if (!isEmpty) {
18406
+ this.data.position = startOfVoice;
18407
+ return false;
18408
+ }
18409
+ this.data.skip(2);
18410
+ if (this._versionNumber >= 500) this.data.skip(2);
18411
+ return true;
18412
+ }
18358
18413
  readBeat(track, bar, voice) {
18359
18414
  const newBeat = new Beat();
18360
18415
  const flags = this.data.readByte();
@@ -18702,7 +18757,9 @@ class Gp3To5Importer extends ScoreImporter {
18702
18757
  beat.addNote(newNote);
18703
18758
  if ((flags & 8) !== 0) this.readNoteEffects(track, voice, beat, newNote);
18704
18759
  if (bar.staff.isPercussion) {
18705
- newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret) ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret) : newNote.fret;
18760
+ const midi = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret) ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret) : newNote.fret;
18761
+ const knownArticulation = PercussionMapper.getArticulationById(midi);
18762
+ if (knownArticulation !== null) newNote.percussionArticulation = bar.staff.track.getOrRegisterPercussionArticulation(knownArticulation);
18706
18763
  newNote.fret = NaN;
18707
18764
  }
18708
18765
  if (swapAccidentals) {
@@ -19470,6 +19527,140 @@ class BackingTrack {
19470
19527
  rawAudioFile;
19471
19528
  }
19472
19529
  //#endregion
19530
+ //#region src/model/FingeringAssigner.ts
19531
+ /**
19532
+ * Cost-function weights for {@link FingeringAssigner}. Defaults tuned for
19533
+ * six-string guitar.
19534
+ * @internal
19535
+ */
19536
+ class FingeringOptions {
19537
+ preferredHandPosition = 5;
19538
+ /** Negative = prefer open strings. */
19539
+ openStringBonus = -1;
19540
+ highFretPenaltyWeight = .5;
19541
+ negativeFretPenaltyWeight = 3;
19542
+ /** Soft; heavy weight prefers distinct strings but permits collisions
19543
+ * for chords with more notes than strings. */
19544
+ collisionPenalty = 100;
19545
+ /** Negative = cluster chord notes on neighbouring strings. */
19546
+ adjacentStringBonus = -1.5;
19547
+ /** Negative = repeated pitches stay on the same string across beats. */
19548
+ stringContinuityBonus = -.75;
19549
+ /** EWMA weight for the hand-position anchor:
19550
+ * `hand = α·hand + (1−α)·newHand`. */
19551
+ handPositionMomentum = .7;
19552
+ }
19553
+ /**
19554
+ * Assigns (string, fret) to a stream of beats via greedy hand-position
19555
+ * hysteresis. One instance per (staff, voice); mutates notes in place.
19556
+ * Not thread-safe.
19557
+ * @internal
19558
+ */
19559
+ class FingeringAssigner {
19560
+ static _maxStrings = 30;
19561
+ _tuning;
19562
+ _capo;
19563
+ _transpositionPitch;
19564
+ _options;
19565
+ _handPosition;
19566
+ _lastStringByMidi;
19567
+ _sortedIdx;
19568
+ /**
19569
+ * @param tuning High-to-low MIDI pitches (matches {@link Staff.tuning}). 1..30 entries.
19570
+ */
19571
+ constructor(tuning, capo, transpositionPitch, options) {
19572
+ if (tuning.length < 1 || tuning.length > FingeringAssigner._maxStrings) throw new Error(`FingeringAssigner requires 1..${FingeringAssigner._maxStrings} strings, got tuning.length=${tuning.length}`);
19573
+ this._tuning = tuning;
19574
+ this._capo = capo;
19575
+ this._transpositionPitch = transpositionPitch;
19576
+ this._options = options ?? new FingeringOptions();
19577
+ this._handPosition = this._options.preferredHandPosition;
19578
+ this._lastStringByMidi = /* @__PURE__ */ new Uint8Array(128);
19579
+ this._sortedIdx = /* @__PURE__ */ new Int32Array(16);
19580
+ }
19581
+ /** Reset the hand-position anchor and per-pitch continuity memory. */
19582
+ reset() {
19583
+ this._handPosition = this._options.preferredHandPosition;
19584
+ this._lastStringByMidi.fill(0);
19585
+ }
19586
+ /** Assigns `(string, fret)` to notes that don't already carry both. */
19587
+ assign(beat) {
19588
+ const notes = beat.notes;
19589
+ const K = notes.length;
19590
+ if (K === 0) return;
19591
+ if (this._sortedIdx.length < K) this._sortedIdx = new Int32Array(K);
19592
+ const sortedIdx = this._sortedIdx;
19593
+ let n = 0;
19594
+ for (let i = 0; i < K; i++) {
19595
+ const note = notes[i];
19596
+ if (note.isStringed) continue;
19597
+ if (note.isPercussion) {
19598
+ const art = PercussionMapper.getArticulation(note);
19599
+ if (art !== null) {
19600
+ if (Number.isNaN(note.string)) note.string = Math.max(1, Math.min(6, 7 - art.staffLine));
19601
+ if (Number.isNaN(note.fret)) note.fret = art.outputMidiNumber;
19602
+ }
19603
+ continue;
19604
+ }
19605
+ const tieOrigin = note.tieOrigin;
19606
+ if (note.isTieDestination && tieOrigin !== null && tieOrigin.isStringed) {
19607
+ note.string = tieOrigin.string;
19608
+ note.fret = tieOrigin.fret;
19609
+ continue;
19610
+ }
19611
+ let j = n;
19612
+ const noteValue = note.realValue;
19613
+ while (j > 0 && notes[sortedIdx[j - 1]].realValue > noteValue) {
19614
+ sortedIdx[j] = sortedIdx[j - 1];
19615
+ j--;
19616
+ }
19617
+ sortedIdx[j] = i;
19618
+ n++;
19619
+ }
19620
+ if (n === 0) return;
19621
+ const N = this._tuning.length;
19622
+ const opts = this._options;
19623
+ let usedStrings = 0;
19624
+ let newHand = -1;
19625
+ for (let k = 0; k < n; k++) {
19626
+ const note = notes[sortedIdx[k]];
19627
+ const realValue = note.realValue;
19628
+ const target = realValue + this._transpositionPitch;
19629
+ const continuityString = realValue >= 0 && realValue < 128 ? this._lastStringByMidi[realValue] : 0;
19630
+ let bestString = 1;
19631
+ let bestFret = 0;
19632
+ let bestCost = Number.POSITIVE_INFINITY;
19633
+ for (let s = 1; s <= N; s++) {
19634
+ const fret = target - (this._capo + this._tuning[N - s]);
19635
+ const distanceCost = Math.abs(fret - this._handPosition);
19636
+ const openBonus = fret === 0 ? opts.openStringBonus : 0;
19637
+ const negFretPenalty = fret < 0 ? -fret * opts.negativeFretPenaltyWeight : 0;
19638
+ const highFretPenalty = fret > 12 ? (fret - 12) * opts.highFretPenaltyWeight : 0;
19639
+ const collisionCost = (usedStrings & 1 << s - 1) !== 0 ? opts.collisionPenalty : 0;
19640
+ const leftUsed = s > 1 && (usedStrings & 1 << s - 2) !== 0;
19641
+ const rightUsed = s < N && (usedStrings & 1 << s) !== 0;
19642
+ const adjacencyBonus = leftUsed || rightUsed ? opts.adjacentStringBonus : 0;
19643
+ const continuityBonus = s === continuityString ? opts.stringContinuityBonus : 0;
19644
+ const cost = distanceCost + openBonus + negFretPenalty + highFretPenalty + collisionCost + adjacencyBonus + continuityBonus;
19645
+ if (cost < bestCost) {
19646
+ bestCost = cost;
19647
+ bestString = s;
19648
+ bestFret = fret;
19649
+ }
19650
+ }
19651
+ note.string = bestString;
19652
+ note.fret = bestFret;
19653
+ usedStrings |= 1 << bestString - 1;
19654
+ if (realValue >= 0 && realValue < 128) this._lastStringByMidi[realValue] = bestString;
19655
+ if (bestFret > 0 && (newHand < 0 || bestFret < newHand)) newHand = bestFret;
19656
+ }
19657
+ if (newHand >= 0) {
19658
+ const alpha = opts.handPositionMomentum;
19659
+ this._handPosition = alpha * this._handPosition + (1 - alpha) * newHand;
19660
+ }
19661
+ }
19662
+ }
19663
+ //#endregion
19473
19664
  //#region src/importer/GpifParser.ts
19474
19665
  /**
19475
19666
  * This structure represents a duration within a gpif
@@ -19537,6 +19728,8 @@ class GpifParser {
19537
19728
  _articulationByName;
19538
19729
  _skipApplyLyrics = false;
19539
19730
  _backingTrackPadding = 0;
19731
+ /** Marks the input as a Guitar Pro 6 file. Also auto-detected from the GPIF header. */
19732
+ isGp6 = false;
19540
19733
  _doubleBars = /* @__PURE__ */ new Set();
19541
19734
  _keySignatures = /* @__PURE__ */ new Map();
19542
19735
  loadAsset;
@@ -19557,7 +19750,7 @@ class GpifParser {
19557
19750
  this._rhythmById = /* @__PURE__ */ new Map();
19558
19751
  this._notesOfBeat = /* @__PURE__ */ new Map();
19559
19752
  this._noteById = /* @__PURE__ */ new Map();
19560
- this._tappedNotes = /* @__PURE__ */ new Map();
19753
+ this._tappedNotes = /* @__PURE__ */ new Set();
19561
19754
  this._lyricsByTrack = /* @__PURE__ */ new Map();
19562
19755
  this._soundsByTrack = /* @__PURE__ */ new Map();
19563
19756
  this._skipApplyLyrics = false;
@@ -19570,6 +19763,7 @@ class GpifParser {
19570
19763
  this._parseDom(dom);
19571
19764
  this._buildModel();
19572
19765
  ModelUtils.consolidate(this.score);
19766
+ if (this.isGp6) this._assignFingeringForGp6();
19573
19767
  this.score.finish(settings);
19574
19768
  if (!this._skipApplyLyrics && this._lyricsByTrack.size > 0) for (const [t, lyrics] of this._lyricsByTrack) this._tracksById.get(t).applyLyrics(lyrics);
19575
19769
  }
@@ -19579,6 +19773,12 @@ class GpifParser {
19579
19773
  if (root.localName === "GPIF") {
19580
19774
  this.score = new Score();
19581
19775
  for (const n of root.childElements()) switch (n.localName) {
19776
+ case "GPVersion":
19777
+ if (n.innerText === "6") this.isGp6 = true;
19778
+ break;
19779
+ case "Encoding":
19780
+ if (n.findChildElement("EncodingDescription")?.innerText === "GP6") this.isGp6 = true;
19781
+ break;
19582
19782
  case "Score":
19583
19783
  this._parseScoreNode(n);
19584
19784
  break;
@@ -21078,7 +21278,7 @@ class GpifParser {
21078
21278
  variation = GpifParser._parseIntSafe(c.findChildElement("Variation")?.innerText, 0);
21079
21279
  break;
21080
21280
  case "Tapped":
21081
- this._tappedNotes.set(noteId, true);
21281
+ this._tappedNotes.add(noteId);
21082
21282
  break;
21083
21283
  case "HarmonicType":
21084
21284
  const htype = c.findChildElement("HType");
@@ -21266,6 +21466,84 @@ class GpifParser {
21266
21466
  }
21267
21467
  this._rhythmById.set(rhythmId, rhythm);
21268
21468
  }
21469
+ _keyFor(trackId, base) {
21470
+ if (!this._transposeKeySignaturePerTrack.has(trackId)) return base;
21471
+ return [ModelUtils.transposeKey(base[0], this._transposeKeySignaturePerTrack.get(trackId)), base[1]];
21472
+ }
21473
+ _equalizeVoiceCount(staff, bar, staffVoiceLimits) {
21474
+ const previousMax = staffVoiceLimits.get(staff) ?? 0;
21475
+ const target = Math.max(previousMax, bar.voices.length, 1);
21476
+ if (target > previousMax) {
21477
+ staffVoiceLimits.set(staff, target);
21478
+ ModelUtils.backfillStaffVoices(staff, target);
21479
+ }
21480
+ while (bar.voices.length < target) ModelUtils.appendPlaceholderVoice(bar);
21481
+ }
21482
+ _attachVoiceToBar(bar, voiceId, staff) {
21483
+ const voice = this._voiceById.get(voiceId);
21484
+ bar.addVoice(voice);
21485
+ const beatIds = this._beatsOfVoice.get(voiceId);
21486
+ if (!beatIds) return;
21487
+ for (const beatId of beatIds) if (beatId !== GpifParser._invalidId) this._attachBeatToVoice(voice, beatId, staff);
21488
+ }
21489
+ _attachBeatToVoice(voice, beatId, staff) {
21490
+ const beat = BeatCloner.clone(this._beatById.get(beatId));
21491
+ voice.addBeat(beat);
21492
+ const rhythmId = this._rhythmOfBeat.get(beatId);
21493
+ const rhythm = this._rhythmById.get(rhythmId);
21494
+ beat.duration = rhythm.value;
21495
+ beat.dots = rhythm.dots;
21496
+ beat.tupletNumerator = rhythm.tupletNumerator;
21497
+ beat.tupletDenominator = rhythm.tupletDenominator;
21498
+ const noteIds = this._notesOfBeat.get(beatId);
21499
+ if (!noteIds) return;
21500
+ for (const noteId of noteIds) if (noteId !== GpifParser._invalidId) this._attachNoteToBeat(beat, noteId, staff);
21501
+ }
21502
+ _attachNoteToBeat(beat, noteId, staff) {
21503
+ const note = NoteCloner.clone(this._noteById.get(noteId));
21504
+ if (staff.isPercussion) note.fret = NaN;
21505
+ else note.percussionArticulation = NaN;
21506
+ beat.addNote(note);
21507
+ if (this._tappedNotes.has(noteId)) beat.tap = true;
21508
+ if (staff.isPercussion && note.percussionArticulation >= 0) {
21509
+ const trackArticulations = staff.track.percussionArticulations;
21510
+ let known = null;
21511
+ if (note.percussionArticulation < trackArticulations.length) known = trackArticulations[note.percussionArticulation];
21512
+ else known = PercussionMapper.getArticulationById(note.percussionArticulation);
21513
+ if (known !== null) note.percussionArticulation = staff.track.getOrRegisterPercussionArticulation(known);
21514
+ }
21515
+ }
21516
+ _assignFingeringForGp6() {
21517
+ for (const track of this.score.tracks) for (const staff of track.staves) {
21518
+ const isPercussion = staff.isPercussion;
21519
+ const isPitchedOnly = !isPercussion && staff.stringTuning.tunings.length === 0 && ModelUtils.staffNotesAreNotStringed(staff);
21520
+ if (!isPercussion && !isPitchedOnly) continue;
21521
+ if (isPercussion) staff.stringTuning.tunings = [
21522
+ 0,
21523
+ 0,
21524
+ 0,
21525
+ 0,
21526
+ 0,
21527
+ 0
21528
+ ];
21529
+ else {
21530
+ const fallback = staff.index === 0 ? Tuning.getDefaultTuningFor(6) : Tuning.getDefaultTuningFor(5);
21531
+ if (fallback !== null) {
21532
+ staff.stringTuning.tunings = fallback.tunings.slice();
21533
+ staff.stringTuning.name = fallback.name;
21534
+ }
21535
+ }
21536
+ const assignersByVoiceIndex = /* @__PURE__ */ new Map();
21537
+ for (const bar of staff.bars) for (const voice of bar.voices) {
21538
+ let assigner = assignersByVoiceIndex.get(voice.index);
21539
+ if (assigner === void 0) {
21540
+ assigner = new FingeringAssigner(staff.stringTuning.tunings, staff.capo, staff.transpositionPitch);
21541
+ assignersByVoiceIndex.set(voice.index, assigner);
21542
+ }
21543
+ for (const beat of voice.beats) assigner.assign(beat);
21544
+ }
21545
+ }
21546
+ }
21269
21547
  _buildModel() {
21270
21548
  for (let i = 0, j = this._masterBars.length; i < j; i++) {
21271
21549
  const masterBar = this._masterBars[i];
@@ -21283,66 +21561,46 @@ class GpifParser {
21283
21561
  this.score.addTrack(track);
21284
21562
  trackIndexToTrackId.push(trackId);
21285
21563
  }
21564
+ const staffVoiceLimits = /* @__PURE__ */ new Map();
21286
21565
  let keySignature;
21287
21566
  for (const barIds of this._barsOfMasterBar) {
21288
21567
  let staffIndex = 0;
21289
21568
  let trackIndex = 0;
21290
- keySignature = [KeySignature.C, KeySignatureType.Major];
21291
- if (this._transposeKeySignaturePerTrack.has(trackIndexToTrackId[0])) keySignature = [ModelUtils.transposeKey(keySignature[0], this._transposeKeySignaturePerTrack.get(trackIndexToTrackId[0])), keySignature[1]];
21569
+ keySignature = this._keyFor(trackIndexToTrackId[0], [KeySignature.C, KeySignatureType.Major]);
21292
21570
  for (let barIndex = 0; barIndex < barIds.length && trackIndex < this.score.tracks.length; barIndex++) {
21293
21571
  const barId = barIds[barIndex];
21294
- if (barId !== GpifParser._invalidId) {
21295
- const bar = this._barsById.get(barId);
21296
- const track = this.score.tracks[trackIndex];
21297
- const staff = track.staves[staffIndex];
21298
- staff.addBar(bar);
21299
- const masterBarIndex = staff.bars.length - 1;
21300
- if (this._keySignatures.has(masterBarIndex)) {
21301
- keySignature = this._keySignatures.get(masterBarIndex);
21302
- if (this._transposeKeySignaturePerTrack.has(trackIndexToTrackId[trackIndex])) keySignature = [ModelUtils.transposeKey(keySignature[0], this._transposeKeySignaturePerTrack.get(trackIndexToTrackId[trackIndex])), keySignature[1]];
21303
- }
21304
- bar.keySignature = keySignature[0];
21305
- bar.keySignatureType = keySignature[1];
21306
- if (this._doubleBars.has(bar.masterBar)) bar.barLineRight = BarLineStyle.LightLight;
21307
- if (this._voicesOfBar.has(barId)) for (const voiceId of this._voicesOfBar.get(barId)) if (voiceId !== GpifParser._invalidId) {
21308
- const voice = this._voiceById.get(voiceId);
21309
- bar.addVoice(voice);
21310
- if (this._beatsOfVoice.has(voiceId)) {
21311
- for (const beatId of this._beatsOfVoice.get(voiceId)) if (beatId !== GpifParser._invalidId) {
21312
- const beat = BeatCloner.clone(this._beatById.get(beatId));
21313
- voice.addBeat(beat);
21314
- const rhythmId = this._rhythmOfBeat.get(beatId);
21315
- const rhythm = this._rhythmById.get(rhythmId);
21316
- beat.duration = rhythm.value;
21317
- beat.dots = rhythm.dots;
21318
- beat.tupletNumerator = rhythm.tupletNumerator;
21319
- beat.tupletDenominator = rhythm.tupletDenominator;
21320
- if (this._notesOfBeat.has(beatId)) {
21321
- for (const noteId of this._notesOfBeat.get(beatId)) if (noteId !== GpifParser._invalidId) {
21322
- const note = NoteCloner.clone(this._noteById.get(noteId));
21323
- if (staff.isPercussion) note.fret = NaN;
21324
- else note.percussionArticulation = NaN;
21325
- beat.addNote(note);
21326
- if (this._tappedNotes.has(noteId)) beat.tap = true;
21327
- }
21328
- }
21329
- }
21572
+ if (barId === GpifParser._invalidId) {
21573
+ trackIndex++;
21574
+ continue;
21575
+ }
21576
+ const bar = this._barsById.get(barId);
21577
+ const track = this.score.tracks[trackIndex];
21578
+ const staff = track.staves[staffIndex];
21579
+ staff.addBar(bar);
21580
+ const masterBarIndex = staff.bars.length - 1;
21581
+ if (this._keySignatures.has(masterBarIndex)) keySignature = this._keyFor(trackIndexToTrackId[trackIndex], this._keySignatures.get(masterBarIndex));
21582
+ bar.keySignature = keySignature[0];
21583
+ bar.keySignatureType = keySignature[1];
21584
+ if (this._doubleBars.has(bar.masterBar)) bar.barLineRight = BarLineStyle.LightLight;
21585
+ const voiceIds = this._voicesOfBar.get(barId);
21586
+ if (voiceIds) {
21587
+ let pendingPlaceholders = 0;
21588
+ for (const voiceId of voiceIds) if (voiceId === GpifParser._invalidId) pendingPlaceholders++;
21589
+ else {
21590
+ while (pendingPlaceholders > 0) {
21591
+ ModelUtils.appendPlaceholderVoice(bar);
21592
+ pendingPlaceholders--;
21330
21593
  }
21331
- } else {
21332
- const voice = new Voice$1();
21333
- bar.addVoice(voice);
21334
- const beat = new Beat();
21335
- beat.isEmpty = true;
21336
- beat.duration = Duration.Quarter;
21337
- voice.addBeat(beat);
21594
+ this._attachVoiceToBar(bar, voiceId, staff);
21338
21595
  }
21339
- if (staffIndex === track.staves.length - 1) {
21340
- trackIndex++;
21341
- staffIndex = 0;
21342
- } else staffIndex++;
21343
- keySignature = [KeySignature.C, KeySignatureType.Major];
21344
- if (trackIndex < trackIndexToTrackId.length && this._transposeKeySignaturePerTrack.has(trackIndexToTrackId[trackIndex])) keySignature = [ModelUtils.transposeKey(keySignature[0], this._transposeKeySignaturePerTrack.get(trackIndexToTrackId[trackIndex])), keySignature[1]];
21345
- } else trackIndex++;
21596
+ }
21597
+ this._equalizeVoiceCount(staff, bar, staffVoiceLimits);
21598
+ staffIndex++;
21599
+ if (staffIndex >= track.staves.length) {
21600
+ trackIndex++;
21601
+ staffIndex = 0;
21602
+ }
21603
+ keySignature = trackIndex < trackIndexToTrackId.length ? this._keyFor(trackIndexToTrackId[trackIndex], [KeySignature.C, KeySignatureType.Major]) : [KeySignature.C, KeySignatureType.Major];
21346
21604
  }
21347
21605
  }
21348
21606
  for (const trackId of this._tracksMapping) {
@@ -21875,6 +22133,7 @@ class GpxImporter extends ScoreImporter {
21875
22133
  if (!xml) throw new UnsupportedFormatError("No score.gpif found in GPX");
21876
22134
  Logger.debug(this.name, "Start Parsing score.gpif");
21877
22135
  const gpifParser = new GpifParser();
22136
+ gpifParser.isGp6 = true;
21878
22137
  gpifParser.parseXml(xml, this.settings);
21879
22138
  Logger.debug(this.name, "score.gpif parsed");
21880
22139
  const score = gpifParser.score;
@@ -22212,6 +22471,16 @@ class TrackInfo {
22212
22471
  }
22213
22472
  }
22214
22473
  /**
22474
+ * Tracks how raw MusicXML voice numbers on a staff map into alphaTab's local
22475
+ * (dense, 0-based) voice slots. Collapses MuseScore's sparse `staff*4+local`
22476
+ * convention (staff 2 → voices 5..8) into dense per-staff indices.
22477
+ * @internal
22478
+ */
22479
+ class StaffVoicePacking {
22480
+ mapping = /* @__PURE__ */ new Map();
22481
+ sortedRawVoices = [];
22482
+ }
22483
+ /**
22215
22484
  * @internal
22216
22485
  */
22217
22486
  class MusicXmlImporter extends ScoreImporter {
@@ -22219,6 +22488,7 @@ class MusicXmlImporter extends ScoreImporter {
22219
22488
  _idToTrackInfo = /* @__PURE__ */ new Map();
22220
22489
  _indexToTrackInfo = /* @__PURE__ */ new Map();
22221
22490
  _staffToContext = /* @__PURE__ */ new Map();
22491
+ _staffVoicePacking = /* @__PURE__ */ new Map();
22222
22492
  _currentBarNumberDisplayPart;
22223
22493
  _currentBarNumberDisplayBar;
22224
22494
  _divisionsPerQuarterNote = 1;
@@ -23595,14 +23865,38 @@ class MusicXmlImporter extends ScoreImporter {
23595
23865
  }
23596
23866
  return staff.bars[masterBar.index];
23597
23867
  }
23598
- _getOrCreateVoice(bar, voiceIndex) {
23599
- let voicesCreated = false;
23600
- while (bar.voices.length <= voiceIndex) {
23601
- bar.addVoice(new Voice$1());
23602
- voicesCreated = true;
23868
+ _resolveAndPlaceVoice(staff, rawVoice, bar) {
23869
+ let packing;
23870
+ if (this._staffVoicePacking.has(staff)) packing = this._staffVoicePacking.get(staff);
23871
+ else {
23872
+ packing = new StaffVoicePacking();
23873
+ this._staffVoicePacking.set(staff, packing);
23603
23874
  }
23604
- if (voicesCreated) for (const b of bar.staff.bars) while (b.voices.length <= voiceIndex) b.addVoice(new Voice$1());
23605
- return bar.voices[voiceIndex];
23875
+ if (packing.mapping.has(rawVoice)) return bar.voices[packing.mapping.get(rawVoice)];
23876
+ let newVoiceNumber = Number.parseInt(rawVoice, 10);
23877
+ if (Number.isNaN(newVoiceNumber)) {
23878
+ Logger.warning("MusicXML", "Voices need to be specified as numbers");
23879
+ newVoiceNumber = 0;
23880
+ }
23881
+ let insertPos = packing.sortedRawVoices.length;
23882
+ for (let i = 0; i < packing.sortedRawVoices.length; i++) {
23883
+ const existing = Number.parseInt(packing.sortedRawVoices[i], 10);
23884
+ if ((Number.isNaN(existing) ? 0 : existing) > newVoiceNumber) {
23885
+ insertPos = i;
23886
+ break;
23887
+ }
23888
+ }
23889
+ packing.sortedRawVoices.splice(insertPos, 0, rawVoice);
23890
+ for (const b of staff.bars) {
23891
+ b.voices.splice(insertPos, 0, new Voice$1());
23892
+ for (let i = insertPos; i < b.voices.length; i++) {
23893
+ b.voices[i].index = i;
23894
+ b.voices[i].bar = b;
23895
+ }
23896
+ }
23897
+ packing.mapping.clear();
23898
+ for (let i = 0; i < packing.sortedRawVoices.length; i++) packing.mapping.set(packing.sortedRawVoices[i], i);
23899
+ return bar.voices[insertPos];
23606
23900
  }
23607
23901
  _parseNote(element, masterBar, track) {
23608
23902
  let beat = null;
@@ -23611,7 +23905,7 @@ class MusicXmlImporter extends ScoreImporter {
23611
23905
  let beamMode = null;
23612
23906
  let isChord = false;
23613
23907
  let staffIndex = 0;
23614
- let voiceIndex = 0;
23908
+ let voiceRaw = "1";
23615
23909
  let durationInTicks = -1;
23616
23910
  let beatDuration = null;
23617
23911
  let dots = 0;
@@ -23639,7 +23933,7 @@ class MusicXmlImporter extends ScoreImporter {
23639
23933
  return;
23640
23934
  }
23641
23935
  const bar = this._getOrCreateBar(staff, masterBar);
23642
- const voice = this._getOrCreateVoice(bar, voiceIndex);
23936
+ const voice = this._resolveAndPlaceVoice(staff, voiceRaw, bar);
23643
23937
  const actualMusicalPosition = voice.beats.length === 0 ? 0 : voice.beats[voice.beats.length - 1].displayEnd;
23644
23938
  let gap = this._musicalPosition - actualMusicalPosition;
23645
23939
  if (gap > 0) {
@@ -23741,13 +24035,11 @@ class MusicXmlImporter extends ScoreImporter {
23741
24035
  case "instrument":
23742
24036
  instrumentId = c.getAttribute("id", "");
23743
24037
  break;
23744
- case "voice":
23745
- voiceIndex = Number.parseInt(c.innerText, 10);
23746
- if (Number.isNaN(voiceIndex)) {
23747
- Logger.warning("MusicXML", "Voices need to be specified as numbers");
23748
- voiceIndex = 0;
23749
- } else voiceIndex = voiceIndex - 1;
24038
+ case "voice": {
24039
+ const trimmed = c.innerText.trim();
24040
+ voiceRaw = trimmed.length > 0 ? trimmed : "1";
23750
24041
  break;
24042
+ }
23751
24043
  case "type":
23752
24044
  beatDuration = this._parseBeatDuration(c);
23753
24045
  break;
@@ -23833,7 +24125,7 @@ class MusicXmlImporter extends ScoreImporter {
23833
24125
  if (instrumentId !== null && trackInfo.isUnpitchedInstrument(instrumentId)) note.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId, note);
23834
24126
  else if (note.beat.voice.bar.staff.isPercussion) {
23835
24127
  const knownArticulation = PercussionMapper.getArticulationById(note.displayValue);
23836
- if (knownArticulation) note.percussionArticulation = knownArticulation.id;
24128
+ if (knownArticulation) note.percussionArticulation = track.getOrRegisterPercussionArticulation(knownArticulation);
23837
24129
  }
23838
24130
  }
23839
24131
  _parsePlay(element, note) {
@@ -68852,9 +69144,11 @@ class GpifSoundMapper {
68852
69144
  class GpifWriter {
68853
69145
  static _sampleRate = 44100;
68854
69146
  _rhythmIdLookup = /* @__PURE__ */ new Map();
69147
+ _tuningByStaff = /* @__PURE__ */ new Map();
68855
69148
  writeXml(score) {
68856
69149
  const xmlDocument = new XmlDocument();
68857
69150
  this._rhythmIdLookup = /* @__PURE__ */ new Map();
69151
+ this._tuningByStaff = /* @__PURE__ */ new Map();
68858
69152
  this._writeDom(xmlDocument, score);
68859
69153
  return xmlDocument.toFormattedString("", true);
68860
69154
  }
@@ -68883,15 +69177,30 @@ class GpifWriter {
68883
69177
  const beats = gpif.addElement("Beats");
68884
69178
  const notes = gpif.addElement("Notes");
68885
69179
  const rhythms = gpif.addElement("Rhythms");
68886
- for (const tracks of score.tracks) for (const staff of tracks.staves) for (const bar of staff.bars) {
68887
- this._writeBarNode(bars, bar);
68888
- for (const voice of bar.voices) {
68889
- this._writeVoiceNode(voices, voice);
68890
- for (const beat of voice.beats) {
68891
- this._writeBeatNode(beats, beat, rhythms);
68892
- for (const note of beat.notes) this._writeNoteNode(notes, note);
69180
+ for (const tracks of score.tracks) for (const staff of tracks.staves) {
69181
+ const needsFingering = ModelUtils.staffNotesAreNotStringed(staff);
69182
+ const assignersByVoiceIndex = needsFingering ? /* @__PURE__ */ new Map() : null;
69183
+ const stringedTuning = needsFingering ? this._tuningByStaff.get(staff) : null;
69184
+ const savedTunings = staff.tuning;
69185
+ if (needsFingering && stringedTuning !== null && savedTunings.length === 0) staff.stringTuning.tunings = stringedTuning.slice();
69186
+ for (const bar of staff.bars) {
69187
+ const activeVoices = this._writeBarNode(bars, bar);
69188
+ for (const voice of activeVoices) {
69189
+ let assigner = null;
69190
+ if (assignersByVoiceIndex !== null && stringedTuning !== null) if (assignersByVoiceIndex.has(voice.index)) assigner = assignersByVoiceIndex.get(voice.index);
69191
+ else {
69192
+ assigner = new FingeringAssigner(stringedTuning, staff.capo, staff.transpositionPitch);
69193
+ assignersByVoiceIndex.set(voice.index, assigner);
69194
+ }
69195
+ this._writeVoiceNode(voices, voice);
69196
+ for (const beat of voice.beats) {
69197
+ if (assigner !== null) assigner.assign(beat);
69198
+ this._writeBeatNode(beats, beat, rhythms);
69199
+ for (const note of beat.notes) this._writeNoteNode(notes, note);
69200
+ }
68893
69201
  }
68894
69202
  }
69203
+ staff.stringTuning.tunings = savedTunings;
68895
69204
  }
68896
69205
  }
68897
69206
  _writeAssets(parent, score) {
@@ -68997,13 +69306,18 @@ class GpifWriter {
68997
69306
  const properties = parent.addElement("Properties");
68998
69307
  this._writeConcertPitch(properties, note);
68999
69308
  this._writeTransposedPitch(properties, note);
69000
- if (note.isStringed) {
69309
+ if (note.isPercussion) {
69310
+ const art = PercussionMapper.getArticulation(note);
69311
+ const midi = art !== null ? art.outputMidiNumber : 0;
69312
+ this._writeSimplePropertyNode(properties, "String", "String", (note.string - 1).toString());
69313
+ this._writeSimplePropertyNode(properties, "Fret", "Fret", midi.toString());
69314
+ this._writeSimplePropertyNode(properties, "Midi", "Number", midi.toString());
69315
+ } else if (note.isStringed) {
69001
69316
  this._writeSimplePropertyNode(properties, "String", "String", (note.string - 1).toString());
69002
69317
  this._writeSimplePropertyNode(properties, "Fret", "Fret", note.fret.toString());
69003
69318
  this._writeSimplePropertyNode(properties, "Midi", "Number", note.realValue.toString());
69004
69319
  if (note.showStringNumber) this._writeSimplePropertyNode(properties, "ShowStringNumber", "Enable", null);
69005
69320
  }
69006
- if (note.isPercussion) this._writeSimplePropertyNode(properties, "String", "String", (note.string - 1).toString());
69007
69321
  if (note.isPiano) {
69008
69322
  this._writeSimplePropertyNode(properties, "Octave", "Number", note.octave.toString());
69009
69323
  this._writeSimplePropertyNode(properties, "Tone", "Step", note.tone.toString());
@@ -69065,7 +69379,7 @@ class GpifWriter {
69065
69379
  if (slideFlags > 0) this._writeSimplePropertyNode(properties, "Slide", "Flags", slideFlags.toString());
69066
69380
  }
69067
69381
  _writeTransposedPitch(properties, note) {
69068
- if (note.isPercussion) this._writePitch(properties, "ConcertPitch", "C", "-1", "");
69382
+ if (note.isPercussion) this._writePitch(properties, "TransposedPitch", "C", "-1", "");
69069
69383
  else this._writePitchForValue(properties, "TransposedPitch", note.displayValueWithoutBend, note.accidentalMode, note.beat.voice.bar.keySignature);
69070
69384
  }
69071
69385
  _writeConcertPitch(properties, note) {
@@ -69470,7 +69784,7 @@ class GpifWriter {
69470
69784
  initialTempoAutomation.addElement("Bar").innerText = "0";
69471
69785
  initialTempoAutomation.addElement("Position").innerText = "0";
69472
69786
  initialTempoAutomation.addElement("Visible").innerText = "true";
69473
- initialTempoAutomation.addElement("Value").innerText = `${score.tempo} 2`;
69787
+ initialTempoAutomation.addElement("Value").innerText = `${score.tempo | 0} 2`;
69474
69788
  if (score.tempoLabel) initialTempoAutomation.addElement("Text").innerText = score.tempoLabel;
69475
69789
  }
69476
69790
  const initialSyncPoint = score.masterBars[0].syncPoints ? score.masterBars[0].syncPoints.find((p) => p.ratioPosition === 0 && p.syncPointValue.barOccurence === 0) : void 0;
@@ -69485,7 +69799,7 @@ class GpifWriter {
69485
69799
  tempoAutomation.addElement("Bar").innerText = mb.index.toString();
69486
69800
  tempoAutomation.addElement("Position").innerText = automation.ratioPosition.toString();
69487
69801
  tempoAutomation.addElement("Visible").innerText = automation.isVisible ? "true" : "false";
69488
- tempoAutomation.addElement("Value").innerText = `${automation.value} 2`;
69802
+ tempoAutomation.addElement("Value").innerText = `${automation.value | 0} 2`;
69489
69803
  if (automation.text) tempoAutomation.addElement("Text").innerText = automation.text;
69490
69804
  }
69491
69805
  if (mb.syncPoints) for (const syncPoint of mb.syncPoints) {
@@ -69621,14 +69935,35 @@ class GpifWriter {
69621
69935
  const properties = parent.addElement("Staff").addElement("Properties");
69622
69936
  this._writeSimplePropertyNode(properties, "CapoFret", "Fret", staff.capo.toString());
69623
69937
  this._writeSimplePropertyNode(properties, "FretCount", "Fret", "24");
69624
- if (staff.tuning.length > 0) {
69938
+ let tuning = staff.tuning;
69939
+ let tuningName = staff.tuningName;
69940
+ if (tuning.length === 0) {
69941
+ if (staff.isPercussion) {
69942
+ tuning = [
69943
+ 0,
69944
+ 0,
69945
+ 0,
69946
+ 0,
69947
+ 0,
69948
+ 0
69949
+ ];
69950
+ tuningName = "";
69951
+ } else if (ModelUtils.staffNotesAreNotStringed(staff)) {
69952
+ const staffTuning = staff.index === 0 ? Tuning.getDefaultTuningFor(6) : Tuning.getDefaultTuningFor(5);
69953
+ tuning = staffTuning.tunings;
69954
+ tuningName = staffTuning.name;
69955
+ }
69956
+ }
69957
+ this._tuningByStaff.set(staff, tuning);
69958
+ if (tuning.length > 0) {
69625
69959
  const tuningProperty = properties.addElement("Property");
69626
69960
  tuningProperty.attributes.set("name", "Tuning");
69627
- tuningProperty.addElement("Pitches").innerText = staff.tuning.slice().reverse().join(" ");
69628
- tuningProperty.addElement("Label").setCData(staff.tuningName);
69629
- tuningProperty.addElement("LabelVisible").innerText = staff.tuningName ? "true" : "false";
69961
+ tuningProperty.addElement("Pitches").innerText = tuning.slice().reverse().join(" ");
69962
+ tuningProperty.addElement("Label").setCData(tuningName);
69963
+ tuningProperty.addElement("LabelVisible").innerText = tuningName ? "true" : "false";
69630
69964
  tuningProperty.addElement("Flat");
69631
- switch (staff.tuning.length) {
69965
+ if (staff.isPercussion) tuningProperty.addElement("Instrument").innerText = "Undefined";
69966
+ else switch (tuning.length) {
69632
69967
  case 3:
69633
69968
  tuningProperty.addElement("Instrument").innerText = "Shamisen";
69634
69969
  break;
@@ -70001,10 +70336,37 @@ class GpifWriter {
70001
70336
  _writeBarNode(parent, bar) {
70002
70337
  const barNode = parent.addElement("Bar");
70003
70338
  barNode.attributes.set("id", bar.id.toString());
70004
- barNode.addElement("Voices").innerText = bar.voices.map((v) => v.isEmpty ? "-1" : v.id.toString()).join(" ");
70339
+ const activeVoices = [];
70340
+ const slots = [
70341
+ "-1",
70342
+ "-1",
70343
+ "-1",
70344
+ "-1"
70345
+ ];
70346
+ const overflowVoices = [];
70347
+ for (let i = 0; i < bar.voices.length; i++) {
70348
+ const v = bar.voices[i];
70349
+ if (i < 4) {
70350
+ if (!v.isEmpty) {
70351
+ slots[i] = v.id.toString();
70352
+ activeVoices.push(v);
70353
+ }
70354
+ } else if (!v.isEmpty) overflowVoices.push(v);
70355
+ }
70356
+ let dropped = 0;
70357
+ for (const v of overflowVoices) {
70358
+ const freeSlot = slots.indexOf("-1");
70359
+ if (freeSlot >= 0) {
70360
+ slots[freeSlot] = v.id.toString();
70361
+ activeVoices.push(v);
70362
+ } else dropped++;
70363
+ }
70364
+ if (dropped > 0) Logger.warning("GpifWriter", `Bar ${bar.id} has ${activeVoices.length + dropped} non-empty voices; Guitar Pro supports max 4. Dropping ${dropped}.`);
70365
+ barNode.addElement("Voices").innerText = slots.join(" ");
70005
70366
  barNode.addElement("Clef").innerText = Clef[bar.clef];
70006
70367
  if (bar.clefOttava !== Ottavia.Regular) barNode.addElement("Ottavia").innerText = Ottavia[bar.clefOttava].substr(1);
70007
70368
  if (bar.simileMark !== SimileMark.None) barNode.addElement("SimileMark").innerText = SimileMark[bar.simileMark];
70369
+ return activeVoices;
70008
70370
  }
70009
70371
  _writeVoiceNode(parent, voice) {
70010
70372
  if (voice.isEmpty) return;