@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -190,9 +190,9 @@
190
190
  * @internal
191
191
  */
192
192
  var VersionInfo = class VersionInfo {
193
- static version = "1.9.0-alpha.1883";
194
- static date = "2026-08-02T03:44:59.527Z";
195
- static commit = "a186437bb3263e5ae3f8fd373aef1fef5ebbc7e7";
193
+ static version = "1.9.0-alpha.1891";
194
+ static date = "2026-08-10T02:28:09.977Z";
195
+ static commit = "2a460d7c4b6d879465fe23382adc67bf6fd3196d";
196
196
  static print(print) {
197
197
  print(`alphaTab ${VersionInfo.version}`);
198
198
  print(`commit: ${VersionInfo.commit}`);
@@ -3138,7 +3138,7 @@
3138
3138
  this.beats.push(beat);
3139
3139
  }
3140
3140
  finish() {
3141
- if (this.beats.length > 0) this.id = `${this.beats[0].absoluteDisplayStart}_${this.beats[0].voice.index}`;
3141
+ if (this.beats.length > 0) this.id = `${this.beats[0].voice.bar.id}_${this.beats[0].voice.index}_${this.beats[0].absoluteDisplayStart}`;
3142
3142
  }
3143
3143
  };
3144
3144
  //#endregion
@@ -4143,6 +4143,20 @@
4143
4143
  }
4144
4144
  return headerFooterStyle;
4145
4145
  }
4146
+ static backfillStaffVoices(staff, targetVoiceCount) {
4147
+ for (let bi = 0; bi < staff.bars.length - 1; bi++) {
4148
+ const priorBar = staff.bars[bi];
4149
+ while (priorBar.voices.length < targetVoiceCount) ModelUtils.appendPlaceholderVoice(priorBar);
4150
+ }
4151
+ }
4152
+ static appendPlaceholderVoice(bar) {
4153
+ const voice = new Voice$1();
4154
+ bar.addVoice(voice);
4155
+ const beat = new Beat();
4156
+ beat.isEmpty = true;
4157
+ beat.duration = Duration.Quarter;
4158
+ voice.addBeat(beat);
4159
+ }
4146
4160
  /**
4147
4161
  * Performs some general consolidations of inconsistencies on the given score like
4148
4162
  * missing bars, beats, duplicated midi channels etc
@@ -4193,13 +4207,7 @@
4193
4207
  bar.keySignature = bar.previousBar.keySignature;
4194
4208
  bar.keySignatureType = bar.previousBar.keySignatureType;
4195
4209
  }
4196
- for (let i = 0; i < voiceCount; i++) {
4197
- const v = new Voice$1();
4198
- bar.addVoice(v);
4199
- const emptyBeat = new Beat();
4200
- emptyBeat.isEmpty = true;
4201
- v.addBeat(emptyBeat);
4202
- }
4210
+ for (let i = 0; i < voiceCount; i++) ModelUtils.appendPlaceholderVoice(bar);
4203
4211
  }
4204
4212
  }
4205
4213
  }
@@ -4946,6 +4954,11 @@
4946
4954
  const ksi = keySignature + 7;
4947
4955
  return keySignatureType === KeySignatureType.Minor ? ModelUtils._minorKeySignatureTonicDegrees[ksi] : ModelUtils._majorKeySignatureTonicDegrees[ksi];
4948
4956
  }
4957
+ /** True iff the staff's first note isn't stringed. Empty staves return false. */
4958
+ static staffNotesAreNotStringed(staff) {
4959
+ 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;
4960
+ return false;
4961
+ }
4949
4962
  };
4950
4963
  //#endregion
4951
4964
  //#region src/model/PickStroke.ts
@@ -6336,7 +6349,10 @@
6336
6349
  else realValue += this.harmonicPitch;
6337
6350
  return realValue;
6338
6351
  }
6339
- if (this.isPercussion) return this.percussionArticulation;
6352
+ if (this.isPercussion) {
6353
+ const art = PercussionMapper.getArticulation(this);
6354
+ return art !== null ? art.outputMidiNumber : this.percussionArticulation;
6355
+ }
6340
6356
  if (this.isStringed) return this.fret + this.stringTuning - transpositionPitch;
6341
6357
  if (this.isPiano) return this.octave * 12 + this.tone - transpositionPitch;
6342
6358
  return 0;
@@ -12859,6 +12875,18 @@
12859
12875
  staff.track = this;
12860
12876
  this.staves.push(staff);
12861
12877
  }
12878
+ /**
12879
+ * Returns the index of {@link articulation} in {@link percussionArticulations},
12880
+ * appending it (deduplicated by `uniqueId`) when not yet present. Callers store the
12881
+ * returned index in {@link Note.percussionArticulation}.
12882
+ */
12883
+ getOrRegisterPercussionArticulation(articulation) {
12884
+ const uniqueId = articulation.uniqueId;
12885
+ for (let i = 0; i < this.percussionArticulations.length; i++) if (this.percussionArticulations[i].uniqueId === uniqueId) return i;
12886
+ const index = this.percussionArticulations.length;
12887
+ this.percussionArticulations.push(articulation);
12888
+ return index;
12889
+ }
12862
12890
  finish(settings, sharedDataBag = null) {
12863
12891
  if (!this.shortName) {
12864
12892
  this.shortName = this.name;
@@ -18354,11 +18382,38 @@
18354
18382
  readVoice(track, bar) {
18355
18383
  const beatCount = IOHelper.readInt32LE(this.data);
18356
18384
  if (beatCount === 0) return;
18385
+ const currentVoiceCount = bar.index === 0 ? 1 : bar.previousBar.voices.length;
18386
+ if (bar.voices.length === 0 || currentVoiceCount === 2) {
18387
+ this._readAndChainVoice(track, bar, beatCount);
18388
+ return;
18389
+ }
18390
+ if (beatCount === 1 && this._skipEmptyVoice()) return;
18391
+ ModelUtils.backfillStaffVoices(bar.staff, 2);
18392
+ this._readAndChainVoice(track, bar, beatCount);
18393
+ }
18394
+ _readAndChainVoice(track, bar, beatCount) {
18357
18395
  const newVoice = new Voice$1();
18358
18396
  bar.addVoice(newVoice);
18359
18397
  this._ensureLoopBoundary(beatCount, Gp3To5Importer._maxBeatCount, "beat count");
18360
18398
  for (let i = 0; i < beatCount; i++) this.readBeat(track, bar, newVoice);
18361
18399
  }
18400
+ /**
18401
+ * Attempts to skip a fully empty voice.
18402
+ * @returns true if we detected an empty voice, false if the beat was not empty and a full voice has to be read.
18403
+ */
18404
+ _skipEmptyVoice() {
18405
+ const startOfVoice = this.data.position;
18406
+ const flags = this.data.readByte();
18407
+ let isEmpty = false;
18408
+ if ((flags & 64) !== 0) isEmpty = (this.data.readByte() & 2) === 0;
18409
+ if (!isEmpty) {
18410
+ this.data.position = startOfVoice;
18411
+ return false;
18412
+ }
18413
+ this.data.skip(2);
18414
+ if (this._versionNumber >= 500) this.data.skip(2);
18415
+ return true;
18416
+ }
18362
18417
  readBeat(track, bar, voice) {
18363
18418
  const newBeat = new Beat();
18364
18419
  const flags = this.data.readByte();
@@ -18706,7 +18761,9 @@
18706
18761
  beat.addNote(newNote);
18707
18762
  if ((flags & 8) !== 0) this.readNoteEffects(track, voice, beat, newNote);
18708
18763
  if (bar.staff.isPercussion) {
18709
- newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret) ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret) : newNote.fret;
18764
+ const midi = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret) ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret) : newNote.fret;
18765
+ const knownArticulation = PercussionMapper.getArticulationById(midi);
18766
+ if (knownArticulation !== null) newNote.percussionArticulation = bar.staff.track.getOrRegisterPercussionArticulation(knownArticulation);
18710
18767
  newNote.fret = NaN;
18711
18768
  }
18712
18769
  if (swapAccidentals) {
@@ -19474,6 +19531,140 @@
19474
19531
  rawAudioFile;
19475
19532
  };
19476
19533
  //#endregion
19534
+ //#region src/model/FingeringAssigner.ts
19535
+ /**
19536
+ * Cost-function weights for {@link FingeringAssigner}. Defaults tuned for
19537
+ * six-string guitar.
19538
+ * @internal
19539
+ */
19540
+ var FingeringOptions = class {
19541
+ preferredHandPosition = 5;
19542
+ /** Negative = prefer open strings. */
19543
+ openStringBonus = -1;
19544
+ highFretPenaltyWeight = .5;
19545
+ negativeFretPenaltyWeight = 3;
19546
+ /** Soft; heavy weight prefers distinct strings but permits collisions
19547
+ * for chords with more notes than strings. */
19548
+ collisionPenalty = 100;
19549
+ /** Negative = cluster chord notes on neighbouring strings. */
19550
+ adjacentStringBonus = -1.5;
19551
+ /** Negative = repeated pitches stay on the same string across beats. */
19552
+ stringContinuityBonus = -.75;
19553
+ /** EWMA weight for the hand-position anchor:
19554
+ * `hand = α·hand + (1−α)·newHand`. */
19555
+ handPositionMomentum = .7;
19556
+ };
19557
+ /**
19558
+ * Assigns (string, fret) to a stream of beats via greedy hand-position
19559
+ * hysteresis. One instance per (staff, voice); mutates notes in place.
19560
+ * Not thread-safe.
19561
+ * @internal
19562
+ */
19563
+ var FingeringAssigner = class FingeringAssigner {
19564
+ static _maxStrings = 30;
19565
+ _tuning;
19566
+ _capo;
19567
+ _transpositionPitch;
19568
+ _options;
19569
+ _handPosition;
19570
+ _lastStringByMidi;
19571
+ _sortedIdx;
19572
+ /**
19573
+ * @param tuning High-to-low MIDI pitches (matches {@link Staff.tuning}). 1..30 entries.
19574
+ */
19575
+ constructor(tuning, capo, transpositionPitch, options) {
19576
+ if (tuning.length < 1 || tuning.length > FingeringAssigner._maxStrings) throw new Error(`FingeringAssigner requires 1..${FingeringAssigner._maxStrings} strings, got tuning.length=${tuning.length}`);
19577
+ this._tuning = tuning;
19578
+ this._capo = capo;
19579
+ this._transpositionPitch = transpositionPitch;
19580
+ this._options = options ?? new FingeringOptions();
19581
+ this._handPosition = this._options.preferredHandPosition;
19582
+ this._lastStringByMidi = /* @__PURE__ */ new Uint8Array(128);
19583
+ this._sortedIdx = /* @__PURE__ */ new Int32Array(16);
19584
+ }
19585
+ /** Reset the hand-position anchor and per-pitch continuity memory. */
19586
+ reset() {
19587
+ this._handPosition = this._options.preferredHandPosition;
19588
+ this._lastStringByMidi.fill(0);
19589
+ }
19590
+ /** Assigns `(string, fret)` to notes that don't already carry both. */
19591
+ assign(beat) {
19592
+ const notes = beat.notes;
19593
+ const K = notes.length;
19594
+ if (K === 0) return;
19595
+ if (this._sortedIdx.length < K) this._sortedIdx = new Int32Array(K);
19596
+ const sortedIdx = this._sortedIdx;
19597
+ let n = 0;
19598
+ for (let i = 0; i < K; i++) {
19599
+ const note = notes[i];
19600
+ if (note.isStringed) continue;
19601
+ if (note.isPercussion) {
19602
+ const art = PercussionMapper.getArticulation(note);
19603
+ if (art !== null) {
19604
+ if (Number.isNaN(note.string)) note.string = Math.max(1, Math.min(6, 7 - art.staffLine));
19605
+ if (Number.isNaN(note.fret)) note.fret = art.outputMidiNumber;
19606
+ }
19607
+ continue;
19608
+ }
19609
+ const tieOrigin = note.tieOrigin;
19610
+ if (note.isTieDestination && tieOrigin !== null && tieOrigin.isStringed) {
19611
+ note.string = tieOrigin.string;
19612
+ note.fret = tieOrigin.fret;
19613
+ continue;
19614
+ }
19615
+ let j = n;
19616
+ const noteValue = note.realValue;
19617
+ while (j > 0 && notes[sortedIdx[j - 1]].realValue > noteValue) {
19618
+ sortedIdx[j] = sortedIdx[j - 1];
19619
+ j--;
19620
+ }
19621
+ sortedIdx[j] = i;
19622
+ n++;
19623
+ }
19624
+ if (n === 0) return;
19625
+ const N = this._tuning.length;
19626
+ const opts = this._options;
19627
+ let usedStrings = 0;
19628
+ let newHand = -1;
19629
+ for (let k = 0; k < n; k++) {
19630
+ const note = notes[sortedIdx[k]];
19631
+ const realValue = note.realValue;
19632
+ const target = realValue + this._transpositionPitch;
19633
+ const continuityString = realValue >= 0 && realValue < 128 ? this._lastStringByMidi[realValue] : 0;
19634
+ let bestString = 1;
19635
+ let bestFret = 0;
19636
+ let bestCost = Number.POSITIVE_INFINITY;
19637
+ for (let s = 1; s <= N; s++) {
19638
+ const fret = target - (this._capo + this._tuning[N - s]);
19639
+ const distanceCost = Math.abs(fret - this._handPosition);
19640
+ const openBonus = fret === 0 ? opts.openStringBonus : 0;
19641
+ const negFretPenalty = fret < 0 ? -fret * opts.negativeFretPenaltyWeight : 0;
19642
+ const highFretPenalty = fret > 12 ? (fret - 12) * opts.highFretPenaltyWeight : 0;
19643
+ const collisionCost = (usedStrings & 1 << s - 1) !== 0 ? opts.collisionPenalty : 0;
19644
+ const leftUsed = s > 1 && (usedStrings & 1 << s - 2) !== 0;
19645
+ const rightUsed = s < N && (usedStrings & 1 << s) !== 0;
19646
+ const adjacencyBonus = leftUsed || rightUsed ? opts.adjacentStringBonus : 0;
19647
+ const continuityBonus = s === continuityString ? opts.stringContinuityBonus : 0;
19648
+ const cost = distanceCost + openBonus + negFretPenalty + highFretPenalty + collisionCost + adjacencyBonus + continuityBonus;
19649
+ if (cost < bestCost) {
19650
+ bestCost = cost;
19651
+ bestString = s;
19652
+ bestFret = fret;
19653
+ }
19654
+ }
19655
+ note.string = bestString;
19656
+ note.fret = bestFret;
19657
+ usedStrings |= 1 << bestString - 1;
19658
+ if (realValue >= 0 && realValue < 128) this._lastStringByMidi[realValue] = bestString;
19659
+ if (bestFret > 0 && (newHand < 0 || bestFret < newHand)) newHand = bestFret;
19660
+ }
19661
+ if (newHand >= 0) {
19662
+ const alpha = opts.handPositionMomentum;
19663
+ this._handPosition = alpha * this._handPosition + (1 - alpha) * newHand;
19664
+ }
19665
+ }
19666
+ };
19667
+ //#endregion
19477
19668
  //#region src/importer/GpifParser.ts
19478
19669
  /**
19479
19670
  * This structure represents a duration within a gpif
@@ -19541,6 +19732,8 @@
19541
19732
  _articulationByName;
19542
19733
  _skipApplyLyrics = false;
19543
19734
  _backingTrackPadding = 0;
19735
+ /** Marks the input as a Guitar Pro 6 file. Also auto-detected from the GPIF header. */
19736
+ isGp6 = false;
19544
19737
  _doubleBars = /* @__PURE__ */ new Set();
19545
19738
  _keySignatures = /* @__PURE__ */ new Map();
19546
19739
  loadAsset;
@@ -19561,7 +19754,7 @@
19561
19754
  this._rhythmById = /* @__PURE__ */ new Map();
19562
19755
  this._notesOfBeat = /* @__PURE__ */ new Map();
19563
19756
  this._noteById = /* @__PURE__ */ new Map();
19564
- this._tappedNotes = /* @__PURE__ */ new Map();
19757
+ this._tappedNotes = /* @__PURE__ */ new Set();
19565
19758
  this._lyricsByTrack = /* @__PURE__ */ new Map();
19566
19759
  this._soundsByTrack = /* @__PURE__ */ new Map();
19567
19760
  this._skipApplyLyrics = false;
@@ -19574,6 +19767,7 @@
19574
19767
  this._parseDom(dom);
19575
19768
  this._buildModel();
19576
19769
  ModelUtils.consolidate(this.score);
19770
+ if (this.isGp6) this._assignFingeringForGp6();
19577
19771
  this.score.finish(settings);
19578
19772
  if (!this._skipApplyLyrics && this._lyricsByTrack.size > 0) for (const [t, lyrics] of this._lyricsByTrack) this._tracksById.get(t).applyLyrics(lyrics);
19579
19773
  }
@@ -19583,6 +19777,12 @@
19583
19777
  if (root.localName === "GPIF") {
19584
19778
  this.score = new Score();
19585
19779
  for (const n of root.childElements()) switch (n.localName) {
19780
+ case "GPVersion":
19781
+ if (n.innerText === "6") this.isGp6 = true;
19782
+ break;
19783
+ case "Encoding":
19784
+ if (n.findChildElement("EncodingDescription")?.innerText === "GP6") this.isGp6 = true;
19785
+ break;
19586
19786
  case "Score":
19587
19787
  this._parseScoreNode(n);
19588
19788
  break;
@@ -21077,7 +21277,7 @@
21077
21277
  variation = GpifParser._parseIntSafe(c.findChildElement("Variation")?.innerText, 0);
21078
21278
  break;
21079
21279
  case "Tapped":
21080
- this._tappedNotes.set(noteId, true);
21280
+ this._tappedNotes.add(noteId);
21081
21281
  break;
21082
21282
  case "HarmonicType":
21083
21283
  const htype = c.findChildElement("HType");
@@ -21265,6 +21465,84 @@
21265
21465
  }
21266
21466
  this._rhythmById.set(rhythmId, rhythm);
21267
21467
  }
21468
+ _keyFor(trackId, base) {
21469
+ if (!this._transposeKeySignaturePerTrack.has(trackId)) return base;
21470
+ return [ModelUtils.transposeKey(base[0], this._transposeKeySignaturePerTrack.get(trackId)), base[1]];
21471
+ }
21472
+ _equalizeVoiceCount(staff, bar, staffVoiceLimits) {
21473
+ const previousMax = staffVoiceLimits.get(staff) ?? 0;
21474
+ const target = Math.max(previousMax, bar.voices.length, 1);
21475
+ if (target > previousMax) {
21476
+ staffVoiceLimits.set(staff, target);
21477
+ ModelUtils.backfillStaffVoices(staff, target);
21478
+ }
21479
+ while (bar.voices.length < target) ModelUtils.appendPlaceholderVoice(bar);
21480
+ }
21481
+ _attachVoiceToBar(bar, voiceId, staff) {
21482
+ const voice = this._voiceById.get(voiceId);
21483
+ bar.addVoice(voice);
21484
+ const beatIds = this._beatsOfVoice.get(voiceId);
21485
+ if (!beatIds) return;
21486
+ for (const beatId of beatIds) if (beatId !== GpifParser._invalidId) this._attachBeatToVoice(voice, beatId, staff);
21487
+ }
21488
+ _attachBeatToVoice(voice, beatId, staff) {
21489
+ const beat = BeatCloner.clone(this._beatById.get(beatId));
21490
+ voice.addBeat(beat);
21491
+ const rhythmId = this._rhythmOfBeat.get(beatId);
21492
+ const rhythm = this._rhythmById.get(rhythmId);
21493
+ beat.duration = rhythm.value;
21494
+ beat.dots = rhythm.dots;
21495
+ beat.tupletNumerator = rhythm.tupletNumerator;
21496
+ beat.tupletDenominator = rhythm.tupletDenominator;
21497
+ const noteIds = this._notesOfBeat.get(beatId);
21498
+ if (!noteIds) return;
21499
+ for (const noteId of noteIds) if (noteId !== GpifParser._invalidId) this._attachNoteToBeat(beat, noteId, staff);
21500
+ }
21501
+ _attachNoteToBeat(beat, noteId, staff) {
21502
+ const note = NoteCloner.clone(this._noteById.get(noteId));
21503
+ if (staff.isPercussion) note.fret = NaN;
21504
+ else note.percussionArticulation = NaN;
21505
+ beat.addNote(note);
21506
+ if (this._tappedNotes.has(noteId)) beat.tap = true;
21507
+ if (staff.isPercussion && note.percussionArticulation >= 0) {
21508
+ const trackArticulations = staff.track.percussionArticulations;
21509
+ let known = null;
21510
+ if (note.percussionArticulation < trackArticulations.length) known = trackArticulations[note.percussionArticulation];
21511
+ else known = PercussionMapper.getArticulationById(note.percussionArticulation);
21512
+ if (known !== null) note.percussionArticulation = staff.track.getOrRegisterPercussionArticulation(known);
21513
+ }
21514
+ }
21515
+ _assignFingeringForGp6() {
21516
+ for (const track of this.score.tracks) for (const staff of track.staves) {
21517
+ const isPercussion = staff.isPercussion;
21518
+ const isPitchedOnly = !isPercussion && staff.stringTuning.tunings.length === 0 && ModelUtils.staffNotesAreNotStringed(staff);
21519
+ if (!isPercussion && !isPitchedOnly) continue;
21520
+ if (isPercussion) staff.stringTuning.tunings = [
21521
+ 0,
21522
+ 0,
21523
+ 0,
21524
+ 0,
21525
+ 0,
21526
+ 0
21527
+ ];
21528
+ else {
21529
+ const fallback = staff.index === 0 ? Tuning.getDefaultTuningFor(6) : Tuning.getDefaultTuningFor(5);
21530
+ if (fallback !== null) {
21531
+ staff.stringTuning.tunings = fallback.tunings.slice();
21532
+ staff.stringTuning.name = fallback.name;
21533
+ }
21534
+ }
21535
+ const assignersByVoiceIndex = /* @__PURE__ */ new Map();
21536
+ for (const bar of staff.bars) for (const voice of bar.voices) {
21537
+ let assigner = assignersByVoiceIndex.get(voice.index);
21538
+ if (assigner === void 0) {
21539
+ assigner = new FingeringAssigner(staff.stringTuning.tunings, staff.capo, staff.transpositionPitch);
21540
+ assignersByVoiceIndex.set(voice.index, assigner);
21541
+ }
21542
+ for (const beat of voice.beats) assigner.assign(beat);
21543
+ }
21544
+ }
21545
+ }
21268
21546
  _buildModel() {
21269
21547
  for (let i = 0, j = this._masterBars.length; i < j; i++) {
21270
21548
  const masterBar = this._masterBars[i];
@@ -21282,66 +21560,46 @@
21282
21560
  this.score.addTrack(track);
21283
21561
  trackIndexToTrackId.push(trackId);
21284
21562
  }
21563
+ const staffVoiceLimits = /* @__PURE__ */ new Map();
21285
21564
  let keySignature;
21286
21565
  for (const barIds of this._barsOfMasterBar) {
21287
21566
  let staffIndex = 0;
21288
21567
  let trackIndex = 0;
21289
- keySignature = [KeySignature.C, KeySignatureType.Major];
21290
- if (this._transposeKeySignaturePerTrack.has(trackIndexToTrackId[0])) keySignature = [ModelUtils.transposeKey(keySignature[0], this._transposeKeySignaturePerTrack.get(trackIndexToTrackId[0])), keySignature[1]];
21568
+ keySignature = this._keyFor(trackIndexToTrackId[0], [KeySignature.C, KeySignatureType.Major]);
21291
21569
  for (let barIndex = 0; barIndex < barIds.length && trackIndex < this.score.tracks.length; barIndex++) {
21292
21570
  const barId = barIds[barIndex];
21293
- if (barId !== GpifParser._invalidId) {
21294
- const bar = this._barsById.get(barId);
21295
- const track = this.score.tracks[trackIndex];
21296
- const staff = track.staves[staffIndex];
21297
- staff.addBar(bar);
21298
- const masterBarIndex = staff.bars.length - 1;
21299
- if (this._keySignatures.has(masterBarIndex)) {
21300
- keySignature = this._keySignatures.get(masterBarIndex);
21301
- if (this._transposeKeySignaturePerTrack.has(trackIndexToTrackId[trackIndex])) keySignature = [ModelUtils.transposeKey(keySignature[0], this._transposeKeySignaturePerTrack.get(trackIndexToTrackId[trackIndex])), keySignature[1]];
21302
- }
21303
- bar.keySignature = keySignature[0];
21304
- bar.keySignatureType = keySignature[1];
21305
- if (this._doubleBars.has(bar.masterBar)) bar.barLineRight = BarLineStyle.LightLight;
21306
- if (this._voicesOfBar.has(barId)) for (const voiceId of this._voicesOfBar.get(barId)) if (voiceId !== GpifParser._invalidId) {
21307
- const voice = this._voiceById.get(voiceId);
21308
- bar.addVoice(voice);
21309
- if (this._beatsOfVoice.has(voiceId)) {
21310
- for (const beatId of this._beatsOfVoice.get(voiceId)) if (beatId !== GpifParser._invalidId) {
21311
- const beat = BeatCloner.clone(this._beatById.get(beatId));
21312
- voice.addBeat(beat);
21313
- const rhythmId = this._rhythmOfBeat.get(beatId);
21314
- const rhythm = this._rhythmById.get(rhythmId);
21315
- beat.duration = rhythm.value;
21316
- beat.dots = rhythm.dots;
21317
- beat.tupletNumerator = rhythm.tupletNumerator;
21318
- beat.tupletDenominator = rhythm.tupletDenominator;
21319
- if (this._notesOfBeat.has(beatId)) {
21320
- for (const noteId of this._notesOfBeat.get(beatId)) if (noteId !== GpifParser._invalidId) {
21321
- const note = NoteCloner.clone(this._noteById.get(noteId));
21322
- if (staff.isPercussion) note.fret = NaN;
21323
- else note.percussionArticulation = NaN;
21324
- beat.addNote(note);
21325
- if (this._tappedNotes.has(noteId)) beat.tap = true;
21326
- }
21327
- }
21328
- }
21571
+ if (barId === GpifParser._invalidId) {
21572
+ trackIndex++;
21573
+ continue;
21574
+ }
21575
+ const bar = this._barsById.get(barId);
21576
+ const track = this.score.tracks[trackIndex];
21577
+ const staff = track.staves[staffIndex];
21578
+ staff.addBar(bar);
21579
+ const masterBarIndex = staff.bars.length - 1;
21580
+ if (this._keySignatures.has(masterBarIndex)) keySignature = this._keyFor(trackIndexToTrackId[trackIndex], this._keySignatures.get(masterBarIndex));
21581
+ bar.keySignature = keySignature[0];
21582
+ bar.keySignatureType = keySignature[1];
21583
+ if (this._doubleBars.has(bar.masterBar)) bar.barLineRight = BarLineStyle.LightLight;
21584
+ const voiceIds = this._voicesOfBar.get(barId);
21585
+ if (voiceIds) {
21586
+ let pendingPlaceholders = 0;
21587
+ for (const voiceId of voiceIds) if (voiceId === GpifParser._invalidId) pendingPlaceholders++;
21588
+ else {
21589
+ while (pendingPlaceholders > 0) {
21590
+ ModelUtils.appendPlaceholderVoice(bar);
21591
+ pendingPlaceholders--;
21329
21592
  }
21330
- } else {
21331
- const voice = new Voice$1();
21332
- bar.addVoice(voice);
21333
- const beat = new Beat();
21334
- beat.isEmpty = true;
21335
- beat.duration = Duration.Quarter;
21336
- voice.addBeat(beat);
21593
+ this._attachVoiceToBar(bar, voiceId, staff);
21337
21594
  }
21338
- if (staffIndex === track.staves.length - 1) {
21339
- trackIndex++;
21340
- staffIndex = 0;
21341
- } else staffIndex++;
21342
- keySignature = [KeySignature.C, KeySignatureType.Major];
21343
- if (trackIndex < trackIndexToTrackId.length && this._transposeKeySignaturePerTrack.has(trackIndexToTrackId[trackIndex])) keySignature = [ModelUtils.transposeKey(keySignature[0], this._transposeKeySignaturePerTrack.get(trackIndexToTrackId[trackIndex])), keySignature[1]];
21344
- } else trackIndex++;
21595
+ }
21596
+ this._equalizeVoiceCount(staff, bar, staffVoiceLimits);
21597
+ staffIndex++;
21598
+ if (staffIndex >= track.staves.length) {
21599
+ trackIndex++;
21600
+ staffIndex = 0;
21601
+ }
21602
+ keySignature = trackIndex < trackIndexToTrackId.length ? this._keyFor(trackIndexToTrackId[trackIndex], [KeySignature.C, KeySignatureType.Major]) : [KeySignature.C, KeySignatureType.Major];
21345
21603
  }
21346
21604
  }
21347
21605
  for (const trackId of this._tracksMapping) {
@@ -21874,6 +22132,7 @@
21874
22132
  if (!xml) throw new UnsupportedFormatError("No score.gpif found in GPX");
21875
22133
  Logger.debug(this.name, "Start Parsing score.gpif");
21876
22134
  const gpifParser = new GpifParser();
22135
+ gpifParser.isGp6 = true;
21877
22136
  gpifParser.parseXml(xml, this.settings);
21878
22137
  Logger.debug(this.name, "score.gpif parsed");
21879
22138
  const score = gpifParser.score;
@@ -22211,6 +22470,16 @@
22211
22470
  }
22212
22471
  };
22213
22472
  /**
22473
+ * Tracks how raw MusicXML voice numbers on a staff map into alphaTab's local
22474
+ * (dense, 0-based) voice slots. Collapses MuseScore's sparse `staff*4+local`
22475
+ * convention (staff 2 → voices 5..8) into dense per-staff indices.
22476
+ * @internal
22477
+ */
22478
+ var StaffVoicePacking = class {
22479
+ mapping = /* @__PURE__ */ new Map();
22480
+ sortedRawVoices = [];
22481
+ };
22482
+ /**
22214
22483
  * @internal
22215
22484
  */
22216
22485
  var MusicXmlImporter = class MusicXmlImporter extends ScoreImporter {
@@ -22218,6 +22487,7 @@
22218
22487
  _idToTrackInfo = /* @__PURE__ */ new Map();
22219
22488
  _indexToTrackInfo = /* @__PURE__ */ new Map();
22220
22489
  _staffToContext = /* @__PURE__ */ new Map();
22490
+ _staffVoicePacking = /* @__PURE__ */ new Map();
22221
22491
  _currentBarNumberDisplayPart;
22222
22492
  _currentBarNumberDisplayBar;
22223
22493
  _divisionsPerQuarterNote = 1;
@@ -23594,14 +23864,38 @@
23594
23864
  }
23595
23865
  return staff.bars[masterBar.index];
23596
23866
  }
23597
- _getOrCreateVoice(bar, voiceIndex) {
23598
- let voicesCreated = false;
23599
- while (bar.voices.length <= voiceIndex) {
23600
- bar.addVoice(new Voice$1());
23601
- voicesCreated = true;
23867
+ _resolveAndPlaceVoice(staff, rawVoice, bar) {
23868
+ let packing;
23869
+ if (this._staffVoicePacking.has(staff)) packing = this._staffVoicePacking.get(staff);
23870
+ else {
23871
+ packing = new StaffVoicePacking();
23872
+ this._staffVoicePacking.set(staff, packing);
23602
23873
  }
23603
- if (voicesCreated) for (const b of bar.staff.bars) while (b.voices.length <= voiceIndex) b.addVoice(new Voice$1());
23604
- return bar.voices[voiceIndex];
23874
+ if (packing.mapping.has(rawVoice)) return bar.voices[packing.mapping.get(rawVoice)];
23875
+ let newVoiceNumber = Number.parseInt(rawVoice, 10);
23876
+ if (Number.isNaN(newVoiceNumber)) {
23877
+ Logger.warning("MusicXML", "Voices need to be specified as numbers");
23878
+ newVoiceNumber = 0;
23879
+ }
23880
+ let insertPos = packing.sortedRawVoices.length;
23881
+ for (let i = 0; i < packing.sortedRawVoices.length; i++) {
23882
+ const existing = Number.parseInt(packing.sortedRawVoices[i], 10);
23883
+ if ((Number.isNaN(existing) ? 0 : existing) > newVoiceNumber) {
23884
+ insertPos = i;
23885
+ break;
23886
+ }
23887
+ }
23888
+ packing.sortedRawVoices.splice(insertPos, 0, rawVoice);
23889
+ for (const b of staff.bars) {
23890
+ b.voices.splice(insertPos, 0, new Voice$1());
23891
+ for (let i = insertPos; i < b.voices.length; i++) {
23892
+ b.voices[i].index = i;
23893
+ b.voices[i].bar = b;
23894
+ }
23895
+ }
23896
+ packing.mapping.clear();
23897
+ for (let i = 0; i < packing.sortedRawVoices.length; i++) packing.mapping.set(packing.sortedRawVoices[i], i);
23898
+ return bar.voices[insertPos];
23605
23899
  }
23606
23900
  _parseNote(element, masterBar, track) {
23607
23901
  let beat = null;
@@ -23610,7 +23904,7 @@
23610
23904
  let beamMode = null;
23611
23905
  let isChord = false;
23612
23906
  let staffIndex = 0;
23613
- let voiceIndex = 0;
23907
+ let voiceRaw = "1";
23614
23908
  let durationInTicks = -1;
23615
23909
  let beatDuration = null;
23616
23910
  let dots = 0;
@@ -23638,7 +23932,7 @@
23638
23932
  return;
23639
23933
  }
23640
23934
  const bar = this._getOrCreateBar(staff, masterBar);
23641
- const voice = this._getOrCreateVoice(bar, voiceIndex);
23935
+ const voice = this._resolveAndPlaceVoice(staff, voiceRaw, bar);
23642
23936
  const actualMusicalPosition = voice.beats.length === 0 ? 0 : voice.beats[voice.beats.length - 1].displayEnd;
23643
23937
  let gap = this._musicalPosition - actualMusicalPosition;
23644
23938
  if (gap > 0) {
@@ -23740,13 +24034,11 @@
23740
24034
  case "instrument":
23741
24035
  instrumentId = c.getAttribute("id", "");
23742
24036
  break;
23743
- case "voice":
23744
- voiceIndex = Number.parseInt(c.innerText, 10);
23745
- if (Number.isNaN(voiceIndex)) {
23746
- Logger.warning("MusicXML", "Voices need to be specified as numbers");
23747
- voiceIndex = 0;
23748
- } else voiceIndex = voiceIndex - 1;
24037
+ case "voice": {
24038
+ const trimmed = c.innerText.trim();
24039
+ voiceRaw = trimmed.length > 0 ? trimmed : "1";
23749
24040
  break;
24041
+ }
23750
24042
  case "type":
23751
24043
  beatDuration = this._parseBeatDuration(c);
23752
24044
  break;
@@ -23832,7 +24124,7 @@
23832
24124
  if (instrumentId !== null && trackInfo.isUnpitchedInstrument(instrumentId)) note.percussionArticulation = trackInfo.getOrCreateArticulation(instrumentId, note);
23833
24125
  else if (note.beat.voice.bar.staff.isPercussion) {
23834
24126
  const knownArticulation = PercussionMapper.getArticulationById(note.displayValue);
23835
- if (knownArticulation) note.percussionArticulation = knownArticulation.id;
24127
+ if (knownArticulation) note.percussionArticulation = track.getOrRegisterPercussionArticulation(knownArticulation);
23836
24128
  }
23837
24129
  }
23838
24130
  _parsePlay(element, note) {
@@ -68847,9 +69139,11 @@
68847
69139
  var GpifWriter = class GpifWriter {
68848
69140
  static _sampleRate = 44100;
68849
69141
  _rhythmIdLookup = /* @__PURE__ */ new Map();
69142
+ _tuningByStaff = /* @__PURE__ */ new Map();
68850
69143
  writeXml(score) {
68851
69144
  const xmlDocument = new XmlDocument();
68852
69145
  this._rhythmIdLookup = /* @__PURE__ */ new Map();
69146
+ this._tuningByStaff = /* @__PURE__ */ new Map();
68853
69147
  this._writeDom(xmlDocument, score);
68854
69148
  return xmlDocument.toFormattedString("", true);
68855
69149
  }
@@ -68878,15 +69172,30 @@
68878
69172
  const beats = gpif.addElement("Beats");
68879
69173
  const notes = gpif.addElement("Notes");
68880
69174
  const rhythms = gpif.addElement("Rhythms");
68881
- for (const tracks of score.tracks) for (const staff of tracks.staves) for (const bar of staff.bars) {
68882
- this._writeBarNode(bars, bar);
68883
- for (const voice of bar.voices) {
68884
- this._writeVoiceNode(voices, voice);
68885
- for (const beat of voice.beats) {
68886
- this._writeBeatNode(beats, beat, rhythms);
68887
- for (const note of beat.notes) this._writeNoteNode(notes, note);
69175
+ for (const tracks of score.tracks) for (const staff of tracks.staves) {
69176
+ const needsFingering = ModelUtils.staffNotesAreNotStringed(staff);
69177
+ const assignersByVoiceIndex = needsFingering ? /* @__PURE__ */ new Map() : null;
69178
+ const stringedTuning = needsFingering ? this._tuningByStaff.get(staff) : null;
69179
+ const savedTunings = staff.tuning;
69180
+ if (needsFingering && stringedTuning !== null && savedTunings.length === 0) staff.stringTuning.tunings = stringedTuning.slice();
69181
+ for (const bar of staff.bars) {
69182
+ const activeVoices = this._writeBarNode(bars, bar);
69183
+ for (const voice of activeVoices) {
69184
+ let assigner = null;
69185
+ if (assignersByVoiceIndex !== null && stringedTuning !== null) if (assignersByVoiceIndex.has(voice.index)) assigner = assignersByVoiceIndex.get(voice.index);
69186
+ else {
69187
+ assigner = new FingeringAssigner(stringedTuning, staff.capo, staff.transpositionPitch);
69188
+ assignersByVoiceIndex.set(voice.index, assigner);
69189
+ }
69190
+ this._writeVoiceNode(voices, voice);
69191
+ for (const beat of voice.beats) {
69192
+ if (assigner !== null) assigner.assign(beat);
69193
+ this._writeBeatNode(beats, beat, rhythms);
69194
+ for (const note of beat.notes) this._writeNoteNode(notes, note);
69195
+ }
68888
69196
  }
68889
69197
  }
69198
+ staff.stringTuning.tunings = savedTunings;
68890
69199
  }
68891
69200
  }
68892
69201
  _writeAssets(parent, score) {
@@ -68992,13 +69301,18 @@
68992
69301
  const properties = parent.addElement("Properties");
68993
69302
  this._writeConcertPitch(properties, note);
68994
69303
  this._writeTransposedPitch(properties, note);
68995
- if (note.isStringed) {
69304
+ if (note.isPercussion) {
69305
+ const art = PercussionMapper.getArticulation(note);
69306
+ const midi = art !== null ? art.outputMidiNumber : 0;
69307
+ this._writeSimplePropertyNode(properties, "String", "String", (note.string - 1).toString());
69308
+ this._writeSimplePropertyNode(properties, "Fret", "Fret", midi.toString());
69309
+ this._writeSimplePropertyNode(properties, "Midi", "Number", midi.toString());
69310
+ } else if (note.isStringed) {
68996
69311
  this._writeSimplePropertyNode(properties, "String", "String", (note.string - 1).toString());
68997
69312
  this._writeSimplePropertyNode(properties, "Fret", "Fret", note.fret.toString());
68998
69313
  this._writeSimplePropertyNode(properties, "Midi", "Number", note.realValue.toString());
68999
69314
  if (note.showStringNumber) this._writeSimplePropertyNode(properties, "ShowStringNumber", "Enable", null);
69000
69315
  }
69001
- if (note.isPercussion) this._writeSimplePropertyNode(properties, "String", "String", (note.string - 1).toString());
69002
69316
  if (note.isPiano) {
69003
69317
  this._writeSimplePropertyNode(properties, "Octave", "Number", note.octave.toString());
69004
69318
  this._writeSimplePropertyNode(properties, "Tone", "Step", note.tone.toString());
@@ -69060,7 +69374,7 @@
69060
69374
  if (slideFlags > 0) this._writeSimplePropertyNode(properties, "Slide", "Flags", slideFlags.toString());
69061
69375
  }
69062
69376
  _writeTransposedPitch(properties, note) {
69063
- if (note.isPercussion) this._writePitch(properties, "ConcertPitch", "C", "-1", "");
69377
+ if (note.isPercussion) this._writePitch(properties, "TransposedPitch", "C", "-1", "");
69064
69378
  else this._writePitchForValue(properties, "TransposedPitch", note.displayValueWithoutBend, note.accidentalMode, note.beat.voice.bar.keySignature);
69065
69379
  }
69066
69380
  _writeConcertPitch(properties, note) {
@@ -69465,7 +69779,7 @@
69465
69779
  initialTempoAutomation.addElement("Bar").innerText = "0";
69466
69780
  initialTempoAutomation.addElement("Position").innerText = "0";
69467
69781
  initialTempoAutomation.addElement("Visible").innerText = "true";
69468
- initialTempoAutomation.addElement("Value").innerText = `${score.tempo} 2`;
69782
+ initialTempoAutomation.addElement("Value").innerText = `${score.tempo | 0} 2`;
69469
69783
  if (score.tempoLabel) initialTempoAutomation.addElement("Text").innerText = score.tempoLabel;
69470
69784
  }
69471
69785
  const initialSyncPoint = score.masterBars[0].syncPoints ? score.masterBars[0].syncPoints.find((p) => p.ratioPosition === 0 && p.syncPointValue.barOccurence === 0) : void 0;
@@ -69480,7 +69794,7 @@
69480
69794
  tempoAutomation.addElement("Bar").innerText = mb.index.toString();
69481
69795
  tempoAutomation.addElement("Position").innerText = automation.ratioPosition.toString();
69482
69796
  tempoAutomation.addElement("Visible").innerText = automation.isVisible ? "true" : "false";
69483
- tempoAutomation.addElement("Value").innerText = `${automation.value} 2`;
69797
+ tempoAutomation.addElement("Value").innerText = `${automation.value | 0} 2`;
69484
69798
  if (automation.text) tempoAutomation.addElement("Text").innerText = automation.text;
69485
69799
  }
69486
69800
  if (mb.syncPoints) for (const syncPoint of mb.syncPoints) {
@@ -69616,14 +69930,35 @@
69616
69930
  const properties = parent.addElement("Staff").addElement("Properties");
69617
69931
  this._writeSimplePropertyNode(properties, "CapoFret", "Fret", staff.capo.toString());
69618
69932
  this._writeSimplePropertyNode(properties, "FretCount", "Fret", "24");
69619
- if (staff.tuning.length > 0) {
69933
+ let tuning = staff.tuning;
69934
+ let tuningName = staff.tuningName;
69935
+ if (tuning.length === 0) {
69936
+ if (staff.isPercussion) {
69937
+ tuning = [
69938
+ 0,
69939
+ 0,
69940
+ 0,
69941
+ 0,
69942
+ 0,
69943
+ 0
69944
+ ];
69945
+ tuningName = "";
69946
+ } else if (ModelUtils.staffNotesAreNotStringed(staff)) {
69947
+ const staffTuning = staff.index === 0 ? Tuning.getDefaultTuningFor(6) : Tuning.getDefaultTuningFor(5);
69948
+ tuning = staffTuning.tunings;
69949
+ tuningName = staffTuning.name;
69950
+ }
69951
+ }
69952
+ this._tuningByStaff.set(staff, tuning);
69953
+ if (tuning.length > 0) {
69620
69954
  const tuningProperty = properties.addElement("Property");
69621
69955
  tuningProperty.attributes.set("name", "Tuning");
69622
- tuningProperty.addElement("Pitches").innerText = staff.tuning.slice().reverse().join(" ");
69623
- tuningProperty.addElement("Label").setCData(staff.tuningName);
69624
- tuningProperty.addElement("LabelVisible").innerText = staff.tuningName ? "true" : "false";
69956
+ tuningProperty.addElement("Pitches").innerText = tuning.slice().reverse().join(" ");
69957
+ tuningProperty.addElement("Label").setCData(tuningName);
69958
+ tuningProperty.addElement("LabelVisible").innerText = tuningName ? "true" : "false";
69625
69959
  tuningProperty.addElement("Flat");
69626
- switch (staff.tuning.length) {
69960
+ if (staff.isPercussion) tuningProperty.addElement("Instrument").innerText = "Undefined";
69961
+ else switch (tuning.length) {
69627
69962
  case 3:
69628
69963
  tuningProperty.addElement("Instrument").innerText = "Shamisen";
69629
69964
  break;
@@ -69996,10 +70331,37 @@
69996
70331
  _writeBarNode(parent, bar) {
69997
70332
  const barNode = parent.addElement("Bar");
69998
70333
  barNode.attributes.set("id", bar.id.toString());
69999
- barNode.addElement("Voices").innerText = bar.voices.map((v) => v.isEmpty ? "-1" : v.id.toString()).join(" ");
70334
+ const activeVoices = [];
70335
+ const slots = [
70336
+ "-1",
70337
+ "-1",
70338
+ "-1",
70339
+ "-1"
70340
+ ];
70341
+ const overflowVoices = [];
70342
+ for (let i = 0; i < bar.voices.length; i++) {
70343
+ const v = bar.voices[i];
70344
+ if (i < 4) {
70345
+ if (!v.isEmpty) {
70346
+ slots[i] = v.id.toString();
70347
+ activeVoices.push(v);
70348
+ }
70349
+ } else if (!v.isEmpty) overflowVoices.push(v);
70350
+ }
70351
+ let dropped = 0;
70352
+ for (const v of overflowVoices) {
70353
+ const freeSlot = slots.indexOf("-1");
70354
+ if (freeSlot >= 0) {
70355
+ slots[freeSlot] = v.id.toString();
70356
+ activeVoices.push(v);
70357
+ } else dropped++;
70358
+ }
70359
+ if (dropped > 0) Logger.warning("GpifWriter", `Bar ${bar.id} has ${activeVoices.length + dropped} non-empty voices; Guitar Pro supports max 4. Dropping ${dropped}.`);
70360
+ barNode.addElement("Voices").innerText = slots.join(" ");
70000
70361
  barNode.addElement("Clef").innerText = Clef[bar.clef];
70001
70362
  if (bar.clefOttava !== Ottavia.Regular) barNode.addElement("Ottavia").innerText = Ottavia[bar.clefOttava].substr(1);
70002
70363
  if (bar.simileMark !== SimileMark.None) barNode.addElement("SimileMark").innerText = SimileMark[bar.simileMark];
70364
+ return activeVoices;
70003
70365
  }
70004
70366
  _writeVoiceNode(parent, voice) {
70005
70367
  if (voice.isEmpty) return;