@coderline/alphatab 1.8.0 → 1.8.1

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.8.0 (, build 26)
2
+ * alphaTab v1.8.1 (, build 30)
3
3
  *
4
4
  * Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -203,9 +203,9 @@ class AlphaTabError extends Error {
203
203
  * @internal
204
204
  */
205
205
  class VersionInfo {
206
- static version = '1.8.0';
207
- static date = '2026-01-12T15:41:07.446Z';
208
- static commit = 'b3039f03e18398df334fd99c8ec92ea94c1ebea1';
206
+ static version = '1.8.1';
207
+ static date = '2026-02-01T19:53:46.853Z';
208
+ static commit = '8a2102fe7ee7dcf4b50c3a4198fbb8c0d5c0fda3';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -4519,135 +4519,6 @@ class ModelUtils {
4519
4519
  return ModelUtils._keyTransposeTable[transpose][keySignature + 7];
4520
4520
  }
4521
4521
  }
4522
- /**
4523
- * a lookup list containing an info whether the notes within an octave
4524
- * need an accidental rendered. the accidental symbol is determined based on the type of key signature.
4525
- */
4526
- static _keySignatureLookup = [
4527
- // Flats (where the value is true, a flat accidental is required for the notes)
4528
- [true, true, true, true, true, true, true, true, true, true, true, true],
4529
- [true, true, true, true, true, false, true, true, true, true, true, true],
4530
- [false, true, true, true, true, false, true, true, true, true, true, true],
4531
- [false, true, true, true, true, false, false, false, true, true, true, true],
4532
- [false, false, false, true, true, false, false, false, true, true, true, true],
4533
- [false, false, false, true, true, false, false, false, false, false, true, true],
4534
- [false, false, false, false, false, false, false, false, false, false, true, true],
4535
- // natural
4536
- [false, false, false, false, false, false, false, false, false, false, false, false],
4537
- // sharps (where the value is true, a flat accidental is required for the notes)
4538
- [false, false, false, false, false, true, true, false, false, false, false, false],
4539
- [true, true, false, false, false, true, true, false, false, false, false, false],
4540
- [true, true, false, false, false, true, true, true, true, false, false, false],
4541
- [true, true, true, true, false, true, true, true, true, false, false, false],
4542
- [true, true, true, true, false, true, true, true, true, true, true, false],
4543
- [true, true, true, true, true, true, true, true, true, true, true, false],
4544
- [true, true, true, true, true, true, true, true, true, true, true, true]
4545
- ];
4546
- /**
4547
- * Contains the list of notes within an octave have accidentals set.
4548
- * @internal
4549
- */
4550
- static accidentalNotes = [
4551
- false,
4552
- true,
4553
- false,
4554
- true,
4555
- false,
4556
- false,
4557
- true,
4558
- false,
4559
- true,
4560
- false,
4561
- true,
4562
- false
4563
- ];
4564
- /**
4565
- * @internal
4566
- */
4567
- static computeAccidental(keySignature, accidentalMode, noteValue, quarterBend, currentAccidental = null) {
4568
- const ks = keySignature;
4569
- const ksi = ks + 7;
4570
- const index = noteValue % 12;
4571
- const accidentalForKeySignature = ksi < 7 ? AccidentalType.Flat : AccidentalType.Sharp;
4572
- const hasKeySignatureAccidentalSetForNote = ModelUtils._keySignatureLookup[ksi][index];
4573
- const hasNoteAccidentalWithinOctave = ModelUtils.accidentalNotes[index];
4574
- // the general logic is like this:
4575
- // - we check if the key signature has an accidental defined
4576
- // - we calculate which accidental a note needs according to its index in the octave
4577
- // - if the accidental is already placed at this line, nothing needs to be done, otherwise we place it
4578
- // - if there should not be an accidental, but there is one in the key signature, we clear it.
4579
- // the exceptions are:
4580
- // - for quarter bends we just place the corresponding accidental
4581
- // - the accidental mode can enforce the accidentals for the note
4582
- let accidentalToSet = AccidentalType.None;
4583
- if (quarterBend) {
4584
- accidentalToSet = hasNoteAccidentalWithinOctave ? accidentalForKeySignature : AccidentalType.Natural;
4585
- switch (accidentalToSet) {
4586
- case AccidentalType.Natural:
4587
- accidentalToSet = AccidentalType.NaturalQuarterNoteUp;
4588
- break;
4589
- case AccidentalType.Sharp:
4590
- accidentalToSet = AccidentalType.SharpQuarterNoteUp;
4591
- break;
4592
- case AccidentalType.Flat:
4593
- accidentalToSet = AccidentalType.FlatQuarterNoteUp;
4594
- break;
4595
- }
4596
- }
4597
- else {
4598
- // define which accidental should be shown ignoring what might be set on the KS already
4599
- switch (accidentalMode) {
4600
- case NoteAccidentalMode.ForceSharp:
4601
- accidentalToSet = AccidentalType.Sharp;
4602
- break;
4603
- case NoteAccidentalMode.ForceDoubleSharp:
4604
- accidentalToSet = AccidentalType.DoubleSharp;
4605
- break;
4606
- case NoteAccidentalMode.ForceFlat:
4607
- accidentalToSet = AccidentalType.Flat;
4608
- break;
4609
- case NoteAccidentalMode.ForceDoubleFlat:
4610
- accidentalToSet = AccidentalType.DoubleFlat;
4611
- break;
4612
- default:
4613
- // if note has an accidental in the octave, we place a symbol
4614
- // according to the Key Signature
4615
- if (hasNoteAccidentalWithinOctave) {
4616
- accidentalToSet = accidentalForKeySignature;
4617
- }
4618
- else if (hasKeySignatureAccidentalSetForNote) {
4619
- // note does not get an accidental, but KS defines one -> Naturalize
4620
- accidentalToSet = AccidentalType.Natural;
4621
- }
4622
- break;
4623
- }
4624
- // do we need an accidental on the note?
4625
- if (accidentalToSet !== AccidentalType.None) {
4626
- // if there is no accidental on the line, and the key signature has it set already, we clear it on the note
4627
- if (currentAccidental != null) {
4628
- if (currentAccidental === accidentalToSet) {
4629
- accidentalToSet = AccidentalType.None;
4630
- }
4631
- }
4632
- else if (hasKeySignatureAccidentalSetForNote && accidentalToSet === accidentalForKeySignature) {
4633
- accidentalToSet = AccidentalType.None;
4634
- }
4635
- }
4636
- else {
4637
- // if we don't want an accidental, but there is already one applied, we place a naturalize accidental
4638
- // and clear the registration
4639
- if (currentAccidental !== null) {
4640
- if (currentAccidental === AccidentalType.Natural) {
4641
- accidentalToSet = AccidentalType.None;
4642
- }
4643
- else {
4644
- accidentalToSet = AccidentalType.Natural;
4645
- }
4646
- }
4647
- }
4648
- }
4649
- return accidentalToSet;
4650
- }
4651
4522
  /**
4652
4523
  * @internal
4653
4524
  */
@@ -4686,6 +4557,239 @@ class ModelUtils {
4686
4557
  }
4687
4558
  return systemIndex < systemsLayout.length ? systemsLayout[systemIndex] : defaultSystemsLayout;
4688
4559
  }
4560
+ // diatonic accidentals
4561
+ static _degreeSemitones = [0, 2, 4, 5, 7, 9, 11];
4562
+ static _sharpPreferredSpellings = [
4563
+ { degree: 0, accidentalOffset: 0 }, // C
4564
+ { degree: 0, accidentalOffset: 1 }, // C#
4565
+ { degree: 1, accidentalOffset: 0 }, // D
4566
+ { degree: 1, accidentalOffset: 1 }, // D#
4567
+ { degree: 2, accidentalOffset: 0 }, // E
4568
+ { degree: 3, accidentalOffset: 0 }, // F
4569
+ { degree: 3, accidentalOffset: 1 }, // F#
4570
+ { degree: 4, accidentalOffset: 0 }, // G
4571
+ { degree: 4, accidentalOffset: 1 }, // G#
4572
+ { degree: 5, accidentalOffset: 0 }, // A
4573
+ { degree: 5, accidentalOffset: 1 }, // A#
4574
+ { degree: 6, accidentalOffset: 0 } // B
4575
+ ];
4576
+ static _flatPreferredSpellings = [
4577
+ { degree: 0, accidentalOffset: 0 }, // C
4578
+ { degree: 1, accidentalOffset: -1 }, // Db
4579
+ { degree: 1, accidentalOffset: 0 }, // D
4580
+ { degree: 2, accidentalOffset: -1 }, // Eb
4581
+ { degree: 2, accidentalOffset: 0 }, // E
4582
+ { degree: 3, accidentalOffset: 0 }, // F
4583
+ { degree: 4, accidentalOffset: -1 }, // Gb
4584
+ { degree: 4, accidentalOffset: 0 }, // G
4585
+ { degree: 5, accidentalOffset: -1 }, // Ab
4586
+ { degree: 5, accidentalOffset: 0 }, // A
4587
+ { degree: 6, accidentalOffset: -1 }, // Bb
4588
+ { degree: 6, accidentalOffset: 0 } // B
4589
+ ];
4590
+ // 12 chromatic pitch classes with always 3 possible spellings in the
4591
+ // accidental range of bb..##
4592
+ static _spellingCandidates = [
4593
+ // 0: C
4594
+ [
4595
+ { degree: 0, accidentalOffset: 0 }, // C
4596
+ { degree: 1, accidentalOffset: -2 }, // Dbb
4597
+ { degree: 6, accidentalOffset: 1 } // B#
4598
+ ],
4599
+ // 1: C#/Db
4600
+ [
4601
+ { degree: 0, accidentalOffset: 1 }, // C#
4602
+ { degree: 1, accidentalOffset: -1 }, // Db
4603
+ { degree: 6, accidentalOffset: 2 } // B##
4604
+ ],
4605
+ // 2: D
4606
+ [
4607
+ { degree: 1, accidentalOffset: 0 }, // D
4608
+ { degree: 0, accidentalOffset: 2 }, // C##
4609
+ { degree: 2, accidentalOffset: -2 } // Ebb
4610
+ ],
4611
+ // 3: D#/Eb
4612
+ [
4613
+ { degree: 1, accidentalOffset: 1 }, // D#
4614
+ { degree: 2, accidentalOffset: -1 }, // Eb
4615
+ { degree: 3, accidentalOffset: -2 } // Fbb
4616
+ ],
4617
+ // 4: E
4618
+ [
4619
+ { degree: 2, accidentalOffset: 0 }, // E
4620
+ { degree: 1, accidentalOffset: 2 }, // D##
4621
+ { degree: 3, accidentalOffset: -1 } // Fb
4622
+ ],
4623
+ // 5: F
4624
+ [
4625
+ { degree: 3, accidentalOffset: 0 }, // F
4626
+ { degree: 2, accidentalOffset: 1 }, // E#
4627
+ { degree: 4, accidentalOffset: -2 } // Gbb
4628
+ ],
4629
+ // 6: F#/Gb
4630
+ [
4631
+ { degree: 3, accidentalOffset: 1 }, // F#
4632
+ { degree: 4, accidentalOffset: -1 }, // Gb
4633
+ { degree: 2, accidentalOffset: 2 } // E##
4634
+ ],
4635
+ // 7: G
4636
+ [
4637
+ { degree: 4, accidentalOffset: 0 }, // G
4638
+ { degree: 3, accidentalOffset: 2 }, // F##
4639
+ { degree: 5, accidentalOffset: -2 } // Abb
4640
+ ],
4641
+ // 8: G#/Ab
4642
+ [
4643
+ { degree: 4, accidentalOffset: 1 }, // G#
4644
+ { degree: 5, accidentalOffset: -1 } // Ab
4645
+ ],
4646
+ // 9: A
4647
+ [
4648
+ { degree: 5, accidentalOffset: 0 }, // A
4649
+ { degree: 4, accidentalOffset: 2 }, // G##
4650
+ { degree: 6, accidentalOffset: -2 } // Bbb
4651
+ ],
4652
+ // 10: A#/Bb
4653
+ [
4654
+ { degree: 5, accidentalOffset: 1 }, // A#
4655
+ { degree: 6, accidentalOffset: -1 }, // Bb
4656
+ { degree: 0, accidentalOffset: -2 } // Cbb
4657
+ ],
4658
+ // 11: B
4659
+ [
4660
+ { degree: 6, accidentalOffset: 0 }, // B
4661
+ { degree: 5, accidentalOffset: 2 }, // A##
4662
+ { degree: 0, accidentalOffset: -1 } // Cb
4663
+ ]
4664
+ ];
4665
+ static _sharpKeySignatureOrder = [3, 0, 4, 1, 5, 2, 6]; // F C G D A E B
4666
+ static _flatKeySignatureOrder = [6, 2, 5, 1, 4, 0, 3]; // B E A D G C F
4667
+ static _keySignatureAccidentalByDegree = ModelUtils._buildKeySignatureAccidentalByDegree();
4668
+ static _accidentalOffsetToType = new Map([
4669
+ [-2, AccidentalType.DoubleFlat],
4670
+ [-1, AccidentalType.Flat],
4671
+ [0, AccidentalType.Natural],
4672
+ [1, AccidentalType.Sharp],
4673
+ [2, AccidentalType.DoubleSharp]
4674
+ ]);
4675
+ static _forcedAccidentalOffsetByMode = new Map([
4676
+ [NoteAccidentalMode.ForceSharp, 1],
4677
+ [NoteAccidentalMode.ForceDoubleSharp, 2],
4678
+ [NoteAccidentalMode.ForceFlat, -1],
4679
+ [NoteAccidentalMode.ForceDoubleFlat, -2],
4680
+ [NoteAccidentalMode.ForceNatural, 0],
4681
+ [NoteAccidentalMode.ForceNone, 0],
4682
+ [NoteAccidentalMode.Default, Number.NaN]
4683
+ ]);
4684
+ static _buildKeySignatureAccidentalByDegree() {
4685
+ const lookup = [];
4686
+ for (let ks = -7; ks <= 7; ks++) {
4687
+ const row = [0, 0, 0, 0, 0, 0, 0];
4688
+ if (ks > 0) {
4689
+ for (let i = 0; i < ks; i++) {
4690
+ row[ModelUtils._sharpKeySignatureOrder[i]] = 1;
4691
+ }
4692
+ }
4693
+ else if (ks < 0) {
4694
+ for (let i = 0; i < -ks; i++) {
4695
+ row[ModelUtils._flatKeySignatureOrder[i]] = -1;
4696
+ }
4697
+ }
4698
+ lookup.push(row);
4699
+ }
4700
+ return lookup;
4701
+ }
4702
+ static getKeySignatureAccidentalOffset(keySignature, degree) {
4703
+ return ModelUtils._keySignatureAccidentalByDegree[keySignature + 7][degree];
4704
+ }
4705
+ static resolveSpelling(keySignature, noteValue, accidentalMode) {
4706
+ const chroma = ModelUtils.flooredDivision(noteValue, 12);
4707
+ const preferred = ModelUtils._getPreferredSpellingForKeySignature(keySignature, chroma);
4708
+ const desiredOffset = ModelUtils._forcedAccidentalOffsetByMode.has(accidentalMode)
4709
+ ? ModelUtils._forcedAccidentalOffsetByMode.get(accidentalMode)
4710
+ : Number.NaN;
4711
+ let spelling = preferred;
4712
+ if (!Number.isNaN(desiredOffset)) {
4713
+ const candidates = ModelUtils._spellingCandidates[chroma];
4714
+ const exact = candidates.find(c => c.accidentalOffset === desiredOffset);
4715
+ if (exact) {
4716
+ spelling = exact;
4717
+ }
4718
+ }
4719
+ const baseSemitone = ModelUtils._degreeSemitones[spelling.degree] + spelling.accidentalOffset;
4720
+ const octave = Math.floor((noteValue - baseSemitone) / 12) - 1;
4721
+ return {
4722
+ degree: spelling.degree,
4723
+ accidentalOffset: spelling.accidentalOffset,
4724
+ chroma,
4725
+ octave
4726
+ };
4727
+ }
4728
+ static computeAccidental(keySignature, accidentalMode, noteValue, quarterBend, currentAccidentalOffset = null) {
4729
+ const spelling = ModelUtils.resolveSpelling(keySignature, noteValue, accidentalMode);
4730
+ return ModelUtils.computeAccidentalForSpelling(keySignature, accidentalMode, spelling, quarterBend, currentAccidentalOffset);
4731
+ }
4732
+ static computeAccidentalForSpelling(keySignature, accidentalMode, spelling, quarterBend, currentAccidentalOffset = null) {
4733
+ if (accidentalMode === NoteAccidentalMode.ForceNone) {
4734
+ return AccidentalType.None;
4735
+ }
4736
+ if (quarterBend) {
4737
+ if (spelling.accidentalOffset > 0) {
4738
+ return AccidentalType.SharpQuarterNoteUp;
4739
+ }
4740
+ if (spelling.accidentalOffset < 0) {
4741
+ return AccidentalType.FlatQuarterNoteUp;
4742
+ }
4743
+ return AccidentalType.NaturalQuarterNoteUp;
4744
+ }
4745
+ const desiredOffset = spelling.accidentalOffset;
4746
+ const ksOffset = ModelUtils.getKeySignatureAccidentalOffset(keySignature, spelling.degree);
4747
+ // already active in bar -> no accidental needed
4748
+ if (currentAccidentalOffset === desiredOffset) {
4749
+ return AccidentalType.None;
4750
+ }
4751
+ // key signature already defines the accidental and no explicit accidental is active
4752
+ if (currentAccidentalOffset == null && desiredOffset === ksOffset) {
4753
+ return AccidentalType.None;
4754
+ }
4755
+ return ModelUtils.accidentalOffsetToType(desiredOffset);
4756
+ }
4757
+ static accidentalOffsetToType(offset) {
4758
+ return ModelUtils._accidentalOffsetToType.has(offset)
4759
+ ? ModelUtils._accidentalOffsetToType.get(offset)
4760
+ : AccidentalType.None;
4761
+ }
4762
+ static _getPreferredSpellingForKeySignature(keySignature, chroma) {
4763
+ const candidates = ModelUtils._spellingCandidates[chroma];
4764
+ const ksMatch = candidates.find(c => ModelUtils.getKeySignatureAccidentalOffset(keySignature, c.degree) === c.accidentalOffset);
4765
+ if (ksMatch) {
4766
+ return ksMatch;
4767
+ }
4768
+ const preferFlat = ModelUtils.keySignatureIsFlat(keySignature);
4769
+ return preferFlat ? ModelUtils._flatPreferredSpellings[chroma] : ModelUtils._sharpPreferredSpellings[chroma];
4770
+ }
4771
+ static _majorKeySignatureTonicDegrees = [
4772
+ // Flats: Cb, Gb, Db, Ab, Eb, Bb, F
4773
+ 0, 4, 1, 5, 2, 6, 3,
4774
+ // Natural: C
4775
+ 0,
4776
+ // Sharps: G, D, A, E, B, F#, C#
4777
+ 4, 1, 5, 2, 6, 3, 0
4778
+ ];
4779
+ static _minorKeySignatureTonicDegrees = [
4780
+ // Flats: Ab, Eb, Bb, F, C, G, D
4781
+ 5, 2, 6, 3, 0, 4, 1,
4782
+ // Natural: A
4783
+ 5,
4784
+ // Sharps: E, B, F#, C#, G#, D#, A#
4785
+ 2, 6, 3, 0, 4, 1, 5
4786
+ ];
4787
+ static getKeySignatureTonicDegree(keySignature, keySignatureType) {
4788
+ const ksi = keySignature + 7;
4789
+ return keySignatureType === KeySignatureType.Minor
4790
+ ? ModelUtils._minorKeySignatureTonicDegrees[ksi]
4791
+ : ModelUtils._majorKeySignatureTonicDegrees[ksi];
4792
+ }
4689
4793
  }
4690
4794
 
4691
4795
  /**
@@ -5396,7 +5500,8 @@ class PercussionMapper {
5396
5500
  }
5397
5501
  }
5398
5502
  }
5399
- return 'unknown';
5503
+ // unknown combination, should not happen, fallback to some default value (Snare hit)
5504
+ return 'Snare (hit)';
5400
5505
  }
5401
5506
  static getArticulation(n) {
5402
5507
  const articulationIndex = n.percussionArticulation;
@@ -15572,7 +15677,11 @@ class AlphaTex1LanguageHandler {
15572
15677
  const slurId = `s${note.slurOrigin.id}`;
15573
15678
  Atnf.prop(properties, 'slur', Atnf.identValue(slurId));
15574
15679
  }
15575
- if (note.accidentalMode !== NoteAccidentalMode.Default) {
15680
+ // NOTE: it would be better to check via accidentalhelper what accidentals we really need to force
15681
+ const skipAccidental = note.accidentalMode === NoteAccidentalMode.Default ||
15682
+ (note.beat.voice.bar.keySignature === KeySignature.C &&
15683
+ note.accidentalMode === NoteAccidentalMode.ForceNatural);
15684
+ if (!skipAccidental) {
15576
15685
  Atnf.prop(properties, 'acc', Atnf.identValue(ModelUtils.reverseAccidentalModeMapping.get(note.accidentalMode)));
15577
15686
  }
15578
15687
  switch (note.ornament) {
@@ -19537,6 +19646,24 @@ var Direction;
19537
19646
  */
19538
19647
  class Gp3To5Importer extends ScoreImporter {
19539
19648
  static _versionString = 'FICHIER GUITAR PRO ';
19649
+ // NOTE: General Midi only defines percussion instruments from 35-81
19650
+ // Guitar Pro 5 allowed GS extensions (27-34 and 82-87)
19651
+ // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
19652
+ // (even if they are not correct)
19653
+ // we can support this properly in future when we allow custom alphaTex articulation definitions
19654
+ // then we don't need to rely on GP specifics anymore but handle things on export/import
19655
+ static _gp5PercussionInstrumentMap = new Map([
19656
+ // High Q -> GS "High Q / Filter Snap"
19657
+ [27, 42],
19658
+ // Slap
19659
+ [28, 60],
19660
+ // Scratch Push
19661
+ [29, 29],
19662
+ // Scratch Pull
19663
+ [30, 30],
19664
+ // Square Click
19665
+ [32, 31]
19666
+ ]);
19540
19667
  _versionNumber = 0;
19541
19668
  _score;
19542
19669
  _globalTripletFeel = TripletFeel.NoTripletFeel;
@@ -19883,9 +20010,9 @@ class Gp3To5Importer extends ScoreImporter {
19883
20010
  }
19884
20011
  }
19885
20012
  /**
19886
- * Guitar Pro 3-6 changes to a bass clef if any string tuning is below B2;
20013
+ * Guitar Pro 3-6 changes to a bass clef if any string tuning is below B1
19887
20014
  */
19888
- static _bassClefTuningThreshold = ModelUtils.parseTuning('B2').realValue;
20015
+ static _bassClefTuningThreshold = ModelUtils.parseTuning('B1').realValue;
19889
20016
  readTrack() {
19890
20017
  const newTrack = new Track();
19891
20018
  newTrack.ensureStaveCount(1);
@@ -20606,7 +20733,9 @@ class Gp3To5Importer extends ScoreImporter {
20606
20733
  this.readNoteEffects(track, voice, beat, newNote);
20607
20734
  }
20608
20735
  if (bar.staff.isPercussion) {
20609
- newNote.percussionArticulation = newNote.fret;
20736
+ newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret)
20737
+ ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret)
20738
+ : newNote.fret;
20610
20739
  newNote.string = -1;
20611
20740
  newNote.fret = -1;
20612
20741
  }
@@ -23858,6 +23987,9 @@ class GpifParser {
23858
23987
  switch (c.localName) {
23859
23988
  case 'Accidental':
23860
23989
  switch (c.innerText) {
23990
+ case '':
23991
+ note.accidentalMode = NoteAccidentalMode.ForceNatural;
23992
+ break;
23861
23993
  case 'x':
23862
23994
  note.accidentalMode = NoteAccidentalMode.ForceDoubleSharp;
23863
23995
  break;
@@ -24896,13 +25028,9 @@ class AccidentalHelper {
24896
25028
  */
24897
25029
  static _octaveSteps = [38, 32, 30, 26, 38];
24898
25030
  /**
24899
- * The step offsets of the notes within an octave in case of for sharp keysignatures
24900
- */
24901
- static sharpNoteSteps = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
24902
- /**
24903
- * The step offsets of the notes within an octave in case of for flat keysignatures
25031
+ * Diatonic step offsets within an octave.
24904
25032
  */
24905
- static flatNoteSteps = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6];
25033
+ static _diatonicSteps = [0, 1, 2, 3, 4, 5, 6];
24906
25034
  _registeredAccidentals = new Map();
24907
25035
  _appliedScoreSteps = new Map();
24908
25036
  _appliedScoreStepsByValue = new Map();
@@ -24934,23 +25062,7 @@ class AccidentalHelper {
24934
25062
  return PercussionMapper.getArticulation(note)?.staffLine ?? 0;
24935
25063
  }
24936
25064
  static getNoteValue(note) {
24937
- let noteValue = note.displayValue;
24938
- // adjust note height according to accidentals enforced
24939
- switch (note.accidentalMode) {
24940
- case NoteAccidentalMode.ForceDoubleFlat:
24941
- noteValue += 2;
24942
- break;
24943
- case NoteAccidentalMode.ForceDoubleSharp:
24944
- noteValue -= 2;
24945
- break;
24946
- case NoteAccidentalMode.ForceFlat:
24947
- noteValue += 1;
24948
- break;
24949
- case NoteAccidentalMode.ForceSharp:
24950
- noteValue -= 1;
24951
- break;
24952
- }
24953
- return noteValue;
25065
+ return note.displayValue;
24954
25066
  }
24955
25067
  /**
24956
25068
  * Calculates the accidental for the given note and assignes the value to it.
@@ -24982,7 +25094,8 @@ class AccidentalHelper {
24982
25094
  steps = AccidentalHelper.getPercussionSteps(note);
24983
25095
  }
24984
25096
  else {
24985
- steps = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue);
25097
+ const spelling = ModelUtils.resolveSpelling(bar.keySignature, noteValue, note.accidentalMode);
25098
+ steps = AccidentalHelper.calculateNoteSteps(bar.clef, spelling);
24986
25099
  }
24987
25100
  return steps;
24988
25101
  }
@@ -24995,11 +25108,12 @@ class AccidentalHelper {
24995
25108
  }
24996
25109
  else {
24997
25110
  const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default;
24998
- steps = AccidentalHelper.calculateNoteSteps(this._bar.keySignature, this._bar.clef, noteValue);
24999
- const currentAccidental = this._registeredAccidentals.has(steps)
25111
+ const spelling = ModelUtils.resolveSpelling(this._bar.keySignature, noteValue, accidentalMode);
25112
+ steps = AccidentalHelper.calculateNoteSteps(this._bar.clef, spelling);
25113
+ const currentAccidentalOffset = this._registeredAccidentals.has(steps)
25000
25114
  ? this._registeredAccidentals.get(steps)
25001
25115
  : null;
25002
- accidentalToSet = ModelUtils.computeAccidental(this._bar.keySignature, accidentalMode, noteValue, quarterBend, currentAccidental);
25116
+ accidentalToSet = ModelUtils.computeAccidentalForSpelling(this._bar.keySignature, accidentalMode, spelling, quarterBend, currentAccidentalOffset);
25003
25117
  let skipAccidental = false;
25004
25118
  switch (accidentalToSet) {
25005
25119
  case AccidentalType.NaturalQuarterNoteUp:
@@ -25024,14 +25138,12 @@ class AccidentalHelper {
25024
25138
  if (skipAccidental) {
25025
25139
  accidentalToSet = AccidentalType.None;
25026
25140
  }
25027
- else {
25028
- // do we need an accidental on the note?
25029
- if (accidentalToSet !== AccidentalType.None) {
25030
- this._registeredAccidentals.set(steps, accidentalToSet);
25031
- }
25032
- }
25033
25141
  break;
25034
25142
  }
25143
+ const shouldRegister = !quarterBend && accidentalToSet !== AccidentalType.None;
25144
+ if (shouldRegister) {
25145
+ this._registeredAccidentals.set(steps, spelling.accidentalOffset);
25146
+ }
25035
25147
  }
25036
25148
  if (note) {
25037
25149
  this._appliedScoreSteps.set(note.id, steps);
@@ -25083,21 +25195,14 @@ class AccidentalHelper {
25083
25195
  getMinStepsNote(b) {
25084
25196
  return this._beatSteps.has(b.id) ? this._beatSteps.get(b.id).minStepsNote : null;
25085
25197
  }
25086
- static calculateNoteSteps(keySignature, clef, noteValue) {
25087
- const value = noteValue;
25088
- const ks = keySignature;
25198
+ static calculateNoteSteps(clef, spelling) {
25089
25199
  const clefValue = clef;
25090
- const index = value % 12;
25091
- const octave = ((value / 12) | 0) - 1;
25092
25200
  // Initial Position
25093
25201
  let steps = AccidentalHelper._octaveSteps[clefValue];
25094
25202
  // Move to Octave
25095
- steps -= octave * AccidentalHelper._stepsPerOctave;
25096
- // get the step list for the current keySignature
25097
- const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks)
25098
- ? AccidentalHelper.sharpNoteSteps
25099
- : AccidentalHelper.flatNoteSteps;
25100
- steps -= stepList[index];
25203
+ steps -= spelling.octave * AccidentalHelper._stepsPerOctave;
25204
+ // Move within octave
25205
+ steps -= AccidentalHelper._diatonicSteps[spelling.degree];
25101
25206
  return steps;
25102
25207
  }
25103
25208
  getNoteSteps(n) {
@@ -25202,7 +25307,8 @@ class TrackInfo {
25202
25307
  musicXmlStaffSteps = 4; // middle of bar
25203
25308
  }
25204
25309
  else {
25205
- musicXmlStaffSteps = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue);
25310
+ const spelling = ModelUtils.resolveSpelling(bar.keySignature, noteValue, NoteAccidentalMode.Default);
25311
+ musicXmlStaffSteps = AccidentalHelper.calculateNoteSteps(bar.clef, spelling);
25206
25312
  }
25207
25313
  // to translate this into the "staffLine" semantics we need to subtract additionally the steps "missing" from the absent lines
25208
25314
  const actualSteps = note.beat.voice.bar.staff.standardNotationLineCount * 2 - 1;
@@ -47056,9 +47162,14 @@ var MidiTickLookupFindBeatResultCursorMode;
47056
47162
  */
47057
47163
  MidiTickLookupFindBeatResultCursorMode[MidiTickLookupFindBeatResultCursorMode["ToNextBext"] = 1] = "ToNextBext";
47058
47164
  /**
47059
- * The cursor should animate to the end of the bar (typically on repeats and jumps)
47165
+ * @deprecated replaced by {@link ToEndOfBeat}
47060
47166
  */
47061
47167
  MidiTickLookupFindBeatResultCursorMode[MidiTickLookupFindBeatResultCursorMode["ToEndOfBar"] = 2] = "ToEndOfBar";
47168
+ /**
47169
+ * The cursor should animate to the end of the **beat** (typically on repeats and jumps)
47170
+ * (this is named end of bar historically)
47171
+ */
47172
+ MidiTickLookupFindBeatResultCursorMode[MidiTickLookupFindBeatResultCursorMode["ToEndOfBeat"] = 3] = "ToEndOfBeat";
47062
47173
  })(MidiTickLookupFindBeatResultCursorMode || (MidiTickLookupFindBeatResultCursorMode = {}));
47063
47174
  /**
47064
47175
  * Represents the results of searching the currently played beat.
@@ -47206,6 +47317,11 @@ class MidiTickLookup {
47206
47317
  * This info allows building the correct "next" beat and duration.
47207
47318
  */
47208
47319
  multiBarRestInfo = null;
47320
+ /**
47321
+ * An optional playback range to consider when performing lookups.
47322
+ * This will mainly influence the used {@link MidiTickLookupFindBeatResultCursorMode}
47323
+ */
47324
+ playbackRange = null;
47209
47325
  /**
47210
47326
  * Finds the currently played beat given a list of tracks and the current time.
47211
47327
  * @param trackLookup The tracks indices in which to search the played beat for.
@@ -47231,6 +47347,13 @@ class MidiTickLookup {
47231
47347
  if (!result) {
47232
47348
  result = this._findBeatSlow(checker, currentBeatHint, tick, false);
47233
47349
  }
47350
+ if (result) {
47351
+ const playbackRange = this.playbackRange;
47352
+ const isBeyondRangeEnd = playbackRange !== null && result.start >= playbackRange.endTick;
47353
+ if (isBeyondRangeEnd) {
47354
+ return null;
47355
+ }
47356
+ }
47234
47357
  return result;
47235
47358
  }
47236
47359
  _findBeatFast(checker, currentBeatHint, tick) {
@@ -47270,20 +47393,24 @@ class MidiTickLookup {
47270
47393
  if (current.nextBeat) {
47271
47394
  current.tickDuration = current.nextBeat.start - current.start;
47272
47395
  current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToNextBext;
47396
+ // jump back
47273
47397
  if (current.nextBeat.masterBar.masterBar.index !== endMasterBar.masterBar.index + 1 &&
47274
47398
  (current.nextBeat.masterBar.masterBar.index !== endMasterBar.masterBar.index ||
47275
47399
  current.nextBeat.beat.playbackStart <= current.beat.playbackStart)) {
47276
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47400
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47401
+ }
47402
+ else if (this.playbackRange !== null && this.playbackRange.endTick <= current.nextBeat.start) {
47403
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47277
47404
  }
47278
47405
  }
47279
47406
  else {
47280
47407
  current.tickDuration = endMasterBar.nextMasterBar.end - current.start;
47281
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47408
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47282
47409
  }
47283
47410
  }
47284
47411
  else {
47285
47412
  current.tickDuration = endMasterBar.end - current.start;
47286
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47413
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47287
47414
  }
47288
47415
  }
47289
47416
  else {
@@ -47291,7 +47418,7 @@ class MidiTickLookup {
47291
47418
  // this is wierd, we have a masterbar without known tick?
47292
47419
  // make a best guess with the number of bars
47293
47420
  current.tickDuration = (current.masterBar.end - current.masterBar.start) * (group.length + 1);
47294
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47421
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47295
47422
  }
47296
47423
  current.calculateDuration();
47297
47424
  }
@@ -47317,16 +47444,20 @@ class MidiTickLookup {
47317
47444
  }
47318
47445
  else {
47319
47446
  current.tickDuration = current.masterBar.end - current.start;
47320
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47447
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47321
47448
  current.calculateDuration();
47322
47449
  }
47323
- // if the next beat is not directly the next master bar (e.g. jumping back or forth)
47324
- // we report no next beat and animate to the end
47325
- if (current.nextBeat &&
47326
- current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index + 1 &&
47327
- (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index ||
47328
- current.nextBeat.beat.playbackStart <= current.beat.playbackStart)) {
47329
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47450
+ if (current.nextBeat) {
47451
+ // if the next beat is not directly the next master bar (e.g. jumping back or forth)
47452
+ // we report no next beat and animate to the end
47453
+ if (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index + 1 &&
47454
+ (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index ||
47455
+ current.nextBeat.beat.playbackStart <= current.beat.playbackStart)) {
47456
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47457
+ }
47458
+ else if (this.playbackRange !== null && this.playbackRange.endTick <= current.nextBeat.start) {
47459
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47460
+ }
47330
47461
  }
47331
47462
  }
47332
47463
  _isMultiBarRestResult(current) {
@@ -49352,22 +49483,61 @@ class MidiFileGenerator {
49352
49483
  }
49353
49484
 
49354
49485
  /**
49355
- * Represents the information related to a resize event.
49356
- * @public
49486
+ * A cursor handler which animates the beat cursor to the next beat or end of the beat bounds
49487
+ * depending on the cursor mode.
49488
+ * @internal
49357
49489
  */
49358
- class ResizeEventArgs {
49359
- /**
49360
- * Gets the size before the resizing happened.
49361
- */
49362
- oldWidth = 0;
49363
- /**
49364
- * Gets the size after the resize was complete.
49365
- */
49366
- newWidth = 0;
49367
- /**
49368
- * Gets the settings currently used for rendering.
49369
- */
49370
- settings = null;
49490
+ class ToNextBeatAnimatingCursorHandler {
49491
+ onAttach(_cursors) {
49492
+ }
49493
+ onDetach(_cursors) {
49494
+ }
49495
+ placeBeatCursor(beatCursor, beatBounds, startBeatX) {
49496
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49497
+ const barBounds = barBoundings.visualBounds;
49498
+ beatCursor.transitionToX(0, startBeatX);
49499
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
49500
+ }
49501
+ placeBarCursor(barCursor, beatBounds) {
49502
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49503
+ const barBounds = barBoundings.visualBounds;
49504
+ barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
49505
+ }
49506
+ transitionBeatCursor(beatCursor, _beatBounds, startBeatX, nextBeatX, duration, cursorMode) {
49507
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
49508
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
49509
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
49510
+ const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
49511
+ nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
49512
+ duration = duration * factor;
49513
+ // we need to put the transition to an own animation frame
49514
+ // otherwise the stop animation above is not applied.
49515
+ beatCursor.transitionToX(duration, nextBeatX);
49516
+ }
49517
+ }
49518
+ /**
49519
+ * A cursor handler which just places the bar and beat cursor without any animations applied.
49520
+ * @internal
49521
+ */
49522
+ class NonAnimatingCursorHandler {
49523
+ onAttach(_cursors) {
49524
+ }
49525
+ onDetach(_cursors) {
49526
+ }
49527
+ placeBeatCursor(beatCursor, beatBounds, startBeatX) {
49528
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49529
+ const barBounds = barBoundings.visualBounds;
49530
+ beatCursor.transitionToX(0, startBeatX);
49531
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
49532
+ }
49533
+ placeBarCursor(barCursor, beatBounds) {
49534
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49535
+ const barBounds = barBoundings.visualBounds;
49536
+ barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
49537
+ }
49538
+ transitionBeatCursor(beatCursor, beatBounds, startBeatX, _nextBeatX, _duration, _cursorMode) {
49539
+ this.placeBeatCursor(beatCursor, beatBounds, startBeatX);
49540
+ }
49371
49541
  }
49372
49542
 
49373
49543
  /**
@@ -49809,6 +49979,25 @@ class ScoreRendererWrapper {
49809
49979
  error = new EventEmitterOfT();
49810
49980
  }
49811
49981
 
49982
+ /**
49983
+ * Represents the information related to a resize event.
49984
+ * @public
49985
+ */
49986
+ class ResizeEventArgs {
49987
+ /**
49988
+ * Gets the size before the resizing happened.
49989
+ */
49990
+ oldWidth = 0;
49991
+ /**
49992
+ * Gets the size after the resize was complete.
49993
+ */
49994
+ newWidth = 0;
49995
+ /**
49996
+ * Gets the settings currently used for rendering.
49997
+ */
49998
+ settings = null;
49999
+ }
50000
+
49812
50001
  /**
49813
50002
  * Some basic scroll handler checking for changed offsets and scroll if changed.
49814
50003
  * @internal
@@ -50678,6 +50867,42 @@ class ExternalMediaPlayer extends BackingTrackPlayer {
50678
50867
  }
50679
50868
  }
50680
50869
 
50870
+ /**
50871
+ * This wrapper holds all cursor related elements.
50872
+ * @public
50873
+ */
50874
+ class Cursors {
50875
+ /**
50876
+ * Gets the element that spans across the whole music sheet and holds the other cursor elements.
50877
+ */
50878
+ cursorWrapper;
50879
+ /**
50880
+ * Gets the element that is positioned above the bar that is currently played.
50881
+ */
50882
+ barCursor;
50883
+ /**
50884
+ * Gets the element that is positioned above the beat that is currently played.
50885
+ */
50886
+ beatCursor;
50887
+ /**
50888
+ * Gets the element that spans across the whole music sheet and will hold any selection related elements.
50889
+ */
50890
+ selectionWrapper;
50891
+ /**
50892
+ * Initializes a new instance of the {@link Cursors} class.
50893
+ * @param cursorWrapper
50894
+ * @param barCursor
50895
+ * @param beatCursor
50896
+ * @param selectionWrapper
50897
+ */
50898
+ constructor(cursorWrapper, barCursor, beatCursor, selectionWrapper) {
50899
+ this.cursorWrapper = cursorWrapper;
50900
+ this.barCursor = barCursor;
50901
+ this.beatCursor = beatCursor;
50902
+ this.selectionWrapper = selectionWrapper;
50903
+ }
50904
+ }
50905
+
50681
50906
  /**
50682
50907
  * @internal
50683
50908
  */
@@ -50709,6 +50934,8 @@ class AlphaTabApiBase {
50709
50934
  _player;
50710
50935
  _renderer;
50711
50936
  _defaultScrollHandler;
50937
+ _defaultCursorHandler;
50938
+ _customCursorHandler;
50712
50939
  /**
50713
50940
  * An indicator by how many midi-ticks the song contents are shifted.
50714
50941
  * Grace beats at start might require a shift for the first beat to start at 0.
@@ -51480,6 +51707,80 @@ class AlphaTabApiBase {
51480
51707
  this.uiFacade.canRenderChanged.on(() => this.render(renderHints));
51481
51708
  }
51482
51709
  }
51710
+ /**
51711
+ * A custom cursor handler which will be used to update the cursor positions during playback.
51712
+ *
51713
+ * @category Properties - Player
51714
+ * @since 1.8.1
51715
+ * @example
51716
+ * JavaScript
51717
+ * ```js
51718
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
51719
+ * api.customCursorHandler = {
51720
+ * _customAdorner: undefined,
51721
+ * onAttach(cursors) {
51722
+ * this._customAdorner = document.createElement('div');
51723
+ * this._customAdorner.classList.add('cursor-adorner');
51724
+ * cursors.cursorWrapper.element.appendChild(this._customAdorner);
51725
+ * },
51726
+ * onDetach(cursors) { this._customAdorner.remove(); },
51727
+ * placeBarCursor(barCursor, beatBounds) {
51728
+ * const barBoundings = beatBounds.barBounds.masterBarBounds;
51729
+ * const barBounds = barBoundings.visualBounds;
51730
+ * barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
51731
+ * },
51732
+ * placeBeatCursor(beatCursor, beatBounds, startBeatX) {
51733
+ * const barBoundings = beatBounds.barBounds.masterBarBounds;
51734
+ * const barBounds = barBoundings.visualBounds;
51735
+ * beatCursor.transitionToX(0, startBeatX);
51736
+ * beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51737
+ * this._customAdorner.style.left = startBeatX + 'px';
51738
+ * this._customAdorner.style.top = (barBounds.y - 10) + 'px';
51739
+ * this._customAdorner.style.width = '1px';
51740
+ * this._customAdorner.style.height = '10px';
51741
+ * this._customAdorner.style.transition = 'left 0ms linear'; // stop animation
51742
+ * },
51743
+ * transitionBeatCursor(beatCursor, beatBounds, startBeatX, endBeatX, duration, cursorMode) {
51744
+ * this._customAdorner.style.transition = `left ${duration}ms linear`; // start animation
51745
+ * this._customAdorner.style.left = endBeatX + 'px';
51746
+ * }
51747
+ * }
51748
+ * ```
51749
+ *
51750
+ * @example
51751
+ * C#
51752
+ * ```cs
51753
+ * var api = new AlphaTabApi<MyControl>(...);
51754
+ * api.CustomCursorHandler = new CustomCursorHandler();
51755
+ * ```
51756
+ *
51757
+ * @example
51758
+ * Android
51759
+ * ```kotlin
51760
+ * val api = AlphaTabApi<MyControl>(...)
51761
+ * api.customCursorHandler = CustomCursorHandler();
51762
+ * ```
51763
+ */
51764
+ get customCursorHandler() {
51765
+ return this._customCursorHandler;
51766
+ }
51767
+ set customCursorHandler(value) {
51768
+ if (this._customCursorHandler === value) {
51769
+ return;
51770
+ }
51771
+ const currentHandler = this._customCursorHandler ?? this._defaultCursorHandler;
51772
+ this._customCursorHandler = value;
51773
+ if (this._cursorWrapper) {
51774
+ const cursors = new Cursors(this._cursorWrapper, this._barCursor, this._beatCursor, this._selectionWrapper);
51775
+ currentHandler?.onDetach(cursors);
51776
+ if (value) {
51777
+ value?.onDetach(cursors);
51778
+ }
51779
+ else if (this._defaultCursorHandler) {
51780
+ this._defaultCursorHandler.onAttach(cursors);
51781
+ }
51782
+ }
51783
+ }
51483
51784
  _tickCache = null;
51484
51785
  /**
51485
51786
  * A custom scroll handler which will be used to handle scrolling operations during playback.
@@ -51957,6 +52258,9 @@ class AlphaTabApiBase {
51957
52258
  }
51958
52259
  set playbackRange(value) {
51959
52260
  this._player.playbackRange = value;
52261
+ if (this._tickCache) {
52262
+ this._tickCache.playbackRange = value;
52263
+ }
51960
52264
  this._updateSelectionCursor(value);
51961
52265
  }
51962
52266
  /**
@@ -52104,6 +52408,7 @@ class AlphaTabApiBase {
52104
52408
  generator.applyTranspositionPitches = false;
52105
52409
  generator.generate();
52106
52410
  this._tickCache = generator.tickLookup;
52411
+ this._tickCache.playbackRange = this.playbackRange;
52107
52412
  this._onMidiLoad(midiFile);
52108
52413
  const player = this._player;
52109
52414
  player.midiTickShift = handler.tickShift;
@@ -52499,6 +52804,8 @@ class AlphaTabApiBase {
52499
52804
  if (!this._cursorWrapper) {
52500
52805
  return;
52501
52806
  }
52807
+ const cursorHandler = this.customCursorHandler ?? this._defaultCursorHandler;
52808
+ cursorHandler?.onDetach(new Cursors(this._cursorWrapper, this._barCursor, this._beatCursor, this._selectionWrapper));
52502
52809
  this.uiFacade.destroyCursors();
52503
52810
  this._cursorWrapper = null;
52504
52811
  this._barCursor = null;
@@ -52516,6 +52823,8 @@ class AlphaTabApiBase {
52516
52823
  this._barCursor = cursors.barCursor;
52517
52824
  this._beatCursor = cursors.beatCursor;
52518
52825
  this._selectionWrapper = cursors.selectionWrapper;
52826
+ const cursorHandler = this.customCursorHandler ?? this._defaultCursorHandler;
52827
+ cursorHandler?.onAttach(cursors);
52519
52828
  this._isInitialBeatCursorUpdate = true;
52520
52829
  }
52521
52830
  if (this._currentBeat !== null) {
@@ -52523,6 +52832,7 @@ class AlphaTabApiBase {
52523
52832
  }
52524
52833
  }
52525
52834
  _updateCursors() {
52835
+ this._updateCursorHandler();
52526
52836
  this._updateScrollHandler();
52527
52837
  const enable = this._hasCursor;
52528
52838
  if (enable) {
@@ -52532,6 +52842,21 @@ class AlphaTabApiBase {
52532
52842
  this._destroyCursors();
52533
52843
  }
52534
52844
  }
52845
+ _cursorHandlerMode = false;
52846
+ _updateCursorHandler() {
52847
+ const currentHandler = this._defaultCursorHandler;
52848
+ const cursorHandlerMode = this.settings.player.enableAnimatedBeatCursor;
52849
+ // no change
52850
+ if (currentHandler !== undefined && this._cursorHandlerMode === cursorHandlerMode) {
52851
+ return;
52852
+ }
52853
+ if (cursorHandlerMode) {
52854
+ this._defaultCursorHandler = new ToNextBeatAnimatingCursorHandler();
52855
+ }
52856
+ else {
52857
+ this._defaultCursorHandler = new NonAnimatingCursorHandler();
52858
+ }
52859
+ }
52535
52860
  _scrollHandlerMode = ScrollMode.Off;
52536
52861
  _scrollHandlerVertical = true;
52537
52862
  _updateScrollHandler() {
@@ -52599,9 +52924,6 @@ class AlphaTabApiBase {
52599
52924
  */
52600
52925
  _cursorUpdateBeat(lookupResult, stop, shouldScroll, cursorSpeed, forceUpdate = false) {
52601
52926
  const beat = lookupResult.beat;
52602
- const nextBeat = lookupResult.nextBeat?.beat ?? null;
52603
- const duration = lookupResult.duration;
52604
- const beatsToHighlight = lookupResult.beatLookup.highlightedBeats;
52605
52927
  if (!beat) {
52606
52928
  return;
52607
52929
  }
@@ -52629,7 +52951,7 @@ class AlphaTabApiBase {
52629
52951
  this._previousCursorCache = cache;
52630
52952
  this._previousStateForCursor = this._player.state;
52631
52953
  this.uiFacade.beginInvoke(() => {
52632
- this._internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
52954
+ this._internalCursorUpdateBeat(lookupResult, stop, cache, beatBoundings, shouldScroll, cursorSpeed);
52633
52955
  });
52634
52956
  }
52635
52957
  /**
@@ -52646,24 +52968,30 @@ class AlphaTabApiBase {
52646
52968
  }
52647
52969
  }
52648
52970
  }
52649
- _internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, cursorMode, cursorSpeed) {
52650
- const barCursor = this._barCursor;
52971
+ _internalCursorUpdateBeat(lookupResult, stop, boundsLookup, beatBoundings, shouldScroll, cursorSpeed) {
52972
+ const beat = lookupResult.beat;
52973
+ const nextBeat = lookupResult.nextBeat?.beat;
52974
+ let duration = lookupResult.duration;
52975
+ const beatsToHighlight = lookupResult.beatLookup.highlightedBeats;
52976
+ const cursorMode = lookupResult.cursorMode;
52977
+ const cursorHandler = this.customCursorHandler ?? this._defaultCursorHandler;
52651
52978
  const beatCursor = this._beatCursor;
52979
+ const barCursor = this._barCursor;
52652
52980
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
52653
52981
  const barBounds = barBoundings.visualBounds;
52654
52982
  const previousBeatBounds = this._currentBeatBounds;
52655
52983
  this._currentBeatBounds = beatBoundings;
52656
52984
  if (barCursor) {
52657
- barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
52985
+ cursorHandler.placeBarCursor(barCursor, beatBoundings);
52658
52986
  }
52659
52987
  const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
52660
- let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
52988
+ let nextBeatX = beatBoundings.realBounds.x + beatBoundings.realBounds.w;
52661
52989
  let nextBeatBoundings = null;
52662
52990
  // get position of next beat on same system
52663
52991
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
52664
52992
  // if we are moving within the same bar or to the next bar
52665
52993
  // transition to the next beat, otherwise transition to the end of the bar.
52666
- nextBeatBoundings = cache.findBeat(nextBeat);
52994
+ nextBeatBoundings = boundsLookup.findBeat(nextBeat);
52667
52995
  if (nextBeatBoundings &&
52668
52996
  nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
52669
52997
  nextBeatX = nextBeatBoundings.onNotesX;
@@ -52671,48 +52999,30 @@ class AlphaTabApiBase {
52671
52999
  }
52672
53000
  let startBeatX = beatBoundings.onNotesX;
52673
53001
  if (beatCursor) {
52674
- // relative positioning of the cursor
52675
- if (this.settings.player.enableAnimatedBeatCursor) {
52676
- const animationWidth = nextBeatX - beatBoundings.onNotesX;
52677
- const relativePosition = this._previousTick - this._currentBeat.start;
52678
- const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
52679
- startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
52680
- duration -= duration * ratioPosition;
52681
- if (isPlayingUpdate) {
52682
- // we do not "reset" the cursor if we are smoothly moving from left to right.
52683
- const jumpCursor = !previousBeatBounds ||
52684
- this._isInitialBeatCursorUpdate ||
52685
- barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
52686
- startBeatX < previousBeatBounds.onNotesX ||
52687
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
52688
- if (jumpCursor) {
52689
- beatCursor.transitionToX(0, startBeatX);
52690
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
52691
- }
52692
- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
52693
- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
52694
- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
52695
- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
52696
- nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
52697
- duration = (duration / cursorSpeed) * factor;
52698
- // we need to put the transition to an own animation frame
52699
- // otherwise the stop animation above is not applied.
52700
- this.uiFacade.beginInvoke(() => {
52701
- beatCursor.transitionToX(duration, nextBeatX);
52702
- });
52703
- }
52704
- else {
52705
- duration = 0;
52706
- beatCursor.transitionToX(duration, nextBeatX);
52707
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
52708
- }
53002
+ const animationWidth = nextBeatX - beatBoundings.onNotesX;
53003
+ const relativePosition = this._previousTick - this._currentBeat.start;
53004
+ const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
53005
+ startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
53006
+ duration -= duration * ratioPosition;
53007
+ // respect speed
53008
+ duration = duration / cursorSpeed;
53009
+ if (isPlayingUpdate) {
53010
+ // we do not "reset" the cursor if we are smoothly moving from left to right.
53011
+ const jumpCursor = !previousBeatBounds ||
53012
+ this._isInitialBeatCursorUpdate ||
53013
+ barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
53014
+ startBeatX < previousBeatBounds.onNotesX ||
53015
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
53016
+ if (jumpCursor) {
53017
+ cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
53018
+ }
53019
+ this.uiFacade.beginInvoke(() => {
53020
+ cursorHandler.transitionBeatCursor(beatCursor, beatBoundings, startBeatX, nextBeatX, duration, cursorMode);
53021
+ });
52709
53022
  }
52710
53023
  else {
52711
- // ticking cursor
52712
53024
  duration = 0;
52713
- nextBeatX = startBeatX;
52714
- beatCursor.transitionToX(duration, nextBeatX);
52715
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
53025
+ cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
52716
53026
  }
52717
53027
  this._isInitialBeatCursorUpdate = false;
52718
53028
  }
@@ -55589,42 +55899,6 @@ class AlphaTabWorkerScoreRenderer {
55589
55899
  error = new EventEmitterOfT();
55590
55900
  }
55591
55901
 
55592
- /**
55593
- * This wrapper holds all cursor related elements.
55594
- * @public
55595
- */
55596
- class Cursors {
55597
- /**
55598
- * Gets the element that spans across the whole music sheet and holds the other cursor elements.
55599
- */
55600
- cursorWrapper;
55601
- /**
55602
- * Gets the element that is positioned above the bar that is currently played.
55603
- */
55604
- barCursor;
55605
- /**
55606
- * Gets the element that is positioned above the beat that is currently played.
55607
- */
55608
- beatCursor;
55609
- /**
55610
- * Gets the element that spans across the whole music sheet and will hold any selection related elements.
55611
- */
55612
- selectionWrapper;
55613
- /**
55614
- * Initializes a new instance of the {@link Cursors} class.
55615
- * @param cursorWrapper
55616
- * @param barCursor
55617
- * @param beatCursor
55618
- * @param selectionWrapper
55619
- */
55620
- constructor(cursorWrapper, barCursor, beatCursor, selectionWrapper) {
55621
- this.cursorWrapper = cursorWrapper;
55622
- this.barCursor = barCursor;
55623
- this.beatCursor = beatCursor;
55624
- this.selectionWrapper = selectionWrapper;
55625
- }
55626
- }
55627
-
55628
55902
  /**
55629
55903
  * An IContainer implementation which can be used for cursors and select ranges
55630
55904
  * where browser scaling is relevant.
@@ -59928,11 +60202,12 @@ class NumberedKeySignatureGlyph extends EffectGlyph {
59928
60202
  }
59929
60203
  doLayout() {
59930
60204
  super.doLayout();
59931
- const text = '1 = ';
60205
+ let text = '';
59932
60206
  let text2 = '';
59933
60207
  let accidental = AccidentalType.None;
59934
60208
  switch (this._keySignatureType) {
59935
60209
  case KeySignatureType.Major:
60210
+ text = '1 = ';
59936
60211
  switch (this._keySignature) {
59937
60212
  case KeySignature.Cb:
59938
60213
  text2 = ' C';
@@ -59996,6 +60271,7 @@ class NumberedKeySignatureGlyph extends EffectGlyph {
59996
60271
  }
59997
60272
  break;
59998
60273
  case KeySignatureType.Minor:
60274
+ text = '6 = ';
59999
60275
  switch (this._keySignature) {
60000
60276
  case KeySignature.Cb:
60001
60277
  text2 = ' a';
@@ -67975,7 +68251,6 @@ class SpacingGlyph extends Glyph {
67975
68251
  * @internal
67976
68252
  */
67977
68253
  class NumberedBeatPreNotesGlyph extends BeatGlyphBase {
67978
- isNaturalizeAccidental = false;
67979
68254
  accidental = AccidentalType.None;
67980
68255
  skipLayout = false;
67981
68256
  get effectElement() {
@@ -67990,25 +68265,25 @@ class NumberedBeatPreNotesGlyph extends BeatGlyphBase {
67990
68265
  accidentals.renderer = this.renderer;
67991
68266
  if (this.container.beat.notes.length > 0) {
67992
68267
  const note = this.container.beat.notes[0];
67993
- // Notes
67994
- // - Compared to standard notation accidentals:
67995
- // - Flat keysigs: When there is a naturalize symbol (against key signature, not naturalizing same line) we have a # in Numbered notation
67996
- // - Flat keysigs: When there is a flat symbol standard notation we also have a flat in Numbered notation
67997
- // - C keysig: A sharp on standard notation is a sharp on numbered notation
67998
- // - # keysigs: When there is a # symbol on standard notation we also a sharp in numbered notation
67999
- // - # keysigs: When there is a naturalize symbol (against key signature, not naturalizing same line) we have a flat in Numbered notation
68000
- // Or generally:
68001
- // - numbered notation has the same accidentals as standard notation if applied
68002
- // - when the standard notation naturalizes the accidental from the key signature, the numbered notation has the reversed accidental
68003
- const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default;
68004
- const noteValue = AccidentalHelper.getNoteValue(note);
68005
- let accidentalToSet = ModelUtils.computeAccidental(this.renderer.bar.keySignature, accidentalMode, noteValue, note.hasQuarterToneOffset);
68006
- if (accidentalToSet === AccidentalType.Natural) {
68007
- const ks = this.renderer.bar.keySignature;
68008
- const ksi = ks + 7;
68009
- const naturalizeAccidentalForKeySignature = ksi < 7 ? AccidentalType.Sharp : AccidentalType.Flat;
68010
- accidentalToSet = naturalizeAccidentalForKeySignature;
68011
- this.isNaturalizeAccidental = true;
68268
+ const spelling = ModelUtils.resolveSpelling(this.renderer.bar.keySignature, note.displayValue, note.accidentalMode);
68269
+ const ksOffset = ModelUtils.getKeySignatureAccidentalOffset(this.renderer.bar.keySignature, spelling.degree);
68270
+ const requiredOffset = spelling.accidentalOffset - ksOffset;
68271
+ let accidentalToSet = AccidentalType.None;
68272
+ if (note.accidentalMode !== NoteAccidentalMode.ForceNone) {
68273
+ if (note.hasQuarterToneOffset) {
68274
+ if (requiredOffset > 0) {
68275
+ accidentalToSet = AccidentalType.SharpQuarterNoteUp;
68276
+ }
68277
+ else if (requiredOffset < 0) {
68278
+ accidentalToSet = AccidentalType.FlatQuarterNoteUp;
68279
+ }
68280
+ else {
68281
+ accidentalToSet = AccidentalType.NaturalQuarterNoteUp;
68282
+ }
68283
+ }
68284
+ else if (requiredOffset !== 0) {
68285
+ accidentalToSet = ModelUtils.accidentalOffsetToType(requiredOffset);
68286
+ }
68012
68287
  }
68013
68288
  // do we need an accidental on the note?
68014
68289
  if (accidentalToSet !== AccidentalType.None) {
@@ -68115,21 +68390,21 @@ class NumberedBeatGlyph extends BeatOnNoteGlyphBase {
68115
68390
  }
68116
68391
  return 0;
68117
68392
  }
68118
- static majorKeySignatureOneValues = [
68119
- // Flats
68120
- 59, 66, 61, 68, 63, 58, 65,
68121
- // natural
68393
+ static _majorKeySignatureOneValues = [
68394
+ // Flats: Cb, Gb, Db, Ab, Eb, Bb, F
68395
+ 59, 66, 61, 68, 63, 70, 65,
68396
+ // natural: C
68122
68397
  60,
68123
- // sharps (where the value is true, a flat accidental is required for the notes)
68398
+ // sharps: G, D, A, E, B, F#, C#
68124
68399
  67, 62, 69, 64, 71, 66, 61
68125
68400
  ];
68126
- static minorKeySignatureOneValues = [
68127
- // Flats
68128
- 71, 66, 73, 68, 63, 70, 65,
68129
- // natural
68130
- 72,
68131
- // sharps (where the value is true, a flat accidental is required for the notes)
68132
- 67, 74, 69, 64, 71, 66, 73
68401
+ static _minorKeySignatureOneValues = [
68402
+ // Flats: Ab, Eb, Bb, F, C, G, D
68403
+ 68, 63, 70, 65, 60, 67, 62,
68404
+ // natural: A
68405
+ 69,
68406
+ // sharps: E, B, F#, C#, G#, D#, A#
68407
+ 64, 71, 66, 61, 68, 63, 70
68133
68408
  ];
68134
68409
  doLayout() {
68135
68410
  // create glyphs
@@ -68143,38 +68418,26 @@ class NumberedBeatGlyph extends BeatOnNoteGlyphBase {
68143
68418
  let numberWithinOctave = '0';
68144
68419
  if (this.container.beat.notes.length > 0) {
68145
68420
  const note = this.container.beat.notes[0];
68146
- const kst = this.renderer.bar.keySignatureType;
68147
- const ks = this.renderer.bar.keySignature;
68148
- const ksi = ks + 7;
68149
- const oneNoteValues = kst === KeySignatureType.Minor
68150
- ? NumberedBeatGlyph.minorKeySignatureOneValues
68151
- : NumberedBeatGlyph.majorKeySignatureOneValues;
68152
- const oneNoteValue = oneNoteValues[ksi];
68153
68421
  if (note.isDead) {
68154
68422
  numberWithinOctave = 'X';
68155
68423
  }
68156
68424
  else {
68425
+ const ks = this.renderer.bar.keySignature;
68426
+ const kst = this.renderer.bar.keySignatureType;
68427
+ const ksi = ks + 7;
68428
+ const oneNoteValues = kst === KeySignatureType.Minor
68429
+ ? NumberedBeatGlyph._minorKeySignatureOneValues
68430
+ : NumberedBeatGlyph._majorKeySignatureOneValues;
68431
+ const oneNoteValue = oneNoteValues[ksi];
68432
+ const spelling = ModelUtils.resolveSpelling(ks, note.displayValue, note.accidentalMode);
68433
+ const tonicDegree = ModelUtils.getKeySignatureTonicDegree(ks, kst);
68434
+ const effectiveTonic = kst === KeySignatureType.Minor
68435
+ ? (tonicDegree + 2) % 7 // relative major
68436
+ : tonicDegree;
68437
+ const degreeDistance = (spelling.degree - effectiveTonic + 7) % 7;
68438
+ numberWithinOctave = (degreeDistance + 1).toString();
68157
68439
  const noteValue = note.displayValue - oneNoteValue;
68158
- const index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12;
68159
- octaveDots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0;
68160
- if (noteValue < 0) {
68161
- octaveDots *= -1;
68162
- }
68163
- const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks)
68164
- ? AccidentalHelper.flatNoteSteps
68165
- : AccidentalHelper.sharpNoteSteps;
68166
- let steps = stepList[index] + 1;
68167
- const hasAccidental = ModelUtils.accidentalNotes[index];
68168
- if (hasAccidental &&
68169
- !this.container.preNotes.isNaturalizeAccidental) {
68170
- if (ksi < 7) {
68171
- steps++;
68172
- }
68173
- else {
68174
- steps--;
68175
- }
68176
- }
68177
- numberWithinOctave = steps.toString();
68440
+ octaveDots = Math.floor(noteValue / 12);
68178
68441
  }
68179
68442
  }
68180
68443
  if (this.container.beat.deadSlapped) {