@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -209,9 +209,9 @@
209
209
  * @internal
210
210
  */
211
211
  class VersionInfo {
212
- static version = '1.8.0';
213
- static date = '2026-01-12T15:41:07.446Z';
214
- static commit = 'b3039f03e18398df334fd99c8ec92ea94c1ebea1';
212
+ static version = '1.8.1';
213
+ static date = '2026-02-01T19:53:46.853Z';
214
+ static commit = '8a2102fe7ee7dcf4b50c3a4198fbb8c0d5c0fda3';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -4525,135 +4525,6 @@
4525
4525
  return ModelUtils._keyTransposeTable[transpose][keySignature + 7];
4526
4526
  }
4527
4527
  }
4528
- /**
4529
- * a lookup list containing an info whether the notes within an octave
4530
- * need an accidental rendered. the accidental symbol is determined based on the type of key signature.
4531
- */
4532
- static _keySignatureLookup = [
4533
- // Flats (where the value is true, a flat accidental is required for the notes)
4534
- [true, true, true, true, true, true, true, true, true, true, true, true],
4535
- [true, true, true, true, true, false, true, true, true, true, true, true],
4536
- [false, true, true, true, true, false, true, true, true, true, true, true],
4537
- [false, true, true, true, true, false, false, false, true, true, true, true],
4538
- [false, false, false, true, true, false, false, false, true, true, true, true],
4539
- [false, false, false, true, true, false, false, false, false, false, true, true],
4540
- [false, false, false, false, false, false, false, false, false, false, true, true],
4541
- // natural
4542
- [false, false, false, false, false, false, false, false, false, false, false, false],
4543
- // sharps (where the value is true, a flat accidental is required for the notes)
4544
- [false, false, false, false, false, true, true, false, false, false, false, false],
4545
- [true, true, false, false, false, true, true, false, false, false, false, false],
4546
- [true, true, false, false, false, true, true, true, true, false, false, false],
4547
- [true, true, true, true, false, true, true, true, true, false, false, false],
4548
- [true, true, true, true, false, true, true, true, true, true, true, false],
4549
- [true, true, true, true, true, true, true, true, true, true, true, false],
4550
- [true, true, true, true, true, true, true, true, true, true, true, true]
4551
- ];
4552
- /**
4553
- * Contains the list of notes within an octave have accidentals set.
4554
- * @internal
4555
- */
4556
- static accidentalNotes = [
4557
- false,
4558
- true,
4559
- false,
4560
- true,
4561
- false,
4562
- false,
4563
- true,
4564
- false,
4565
- true,
4566
- false,
4567
- true,
4568
- false
4569
- ];
4570
- /**
4571
- * @internal
4572
- */
4573
- static computeAccidental(keySignature, accidentalMode, noteValue, quarterBend, currentAccidental = null) {
4574
- const ks = keySignature;
4575
- const ksi = ks + 7;
4576
- const index = noteValue % 12;
4577
- const accidentalForKeySignature = ksi < 7 ? AccidentalType.Flat : AccidentalType.Sharp;
4578
- const hasKeySignatureAccidentalSetForNote = ModelUtils._keySignatureLookup[ksi][index];
4579
- const hasNoteAccidentalWithinOctave = ModelUtils.accidentalNotes[index];
4580
- // the general logic is like this:
4581
- // - we check if the key signature has an accidental defined
4582
- // - we calculate which accidental a note needs according to its index in the octave
4583
- // - if the accidental is already placed at this line, nothing needs to be done, otherwise we place it
4584
- // - if there should not be an accidental, but there is one in the key signature, we clear it.
4585
- // the exceptions are:
4586
- // - for quarter bends we just place the corresponding accidental
4587
- // - the accidental mode can enforce the accidentals for the note
4588
- let accidentalToSet = AccidentalType.None;
4589
- if (quarterBend) {
4590
- accidentalToSet = hasNoteAccidentalWithinOctave ? accidentalForKeySignature : AccidentalType.Natural;
4591
- switch (accidentalToSet) {
4592
- case AccidentalType.Natural:
4593
- accidentalToSet = AccidentalType.NaturalQuarterNoteUp;
4594
- break;
4595
- case AccidentalType.Sharp:
4596
- accidentalToSet = AccidentalType.SharpQuarterNoteUp;
4597
- break;
4598
- case AccidentalType.Flat:
4599
- accidentalToSet = AccidentalType.FlatQuarterNoteUp;
4600
- break;
4601
- }
4602
- }
4603
- else {
4604
- // define which accidental should be shown ignoring what might be set on the KS already
4605
- switch (accidentalMode) {
4606
- case NoteAccidentalMode.ForceSharp:
4607
- accidentalToSet = AccidentalType.Sharp;
4608
- break;
4609
- case NoteAccidentalMode.ForceDoubleSharp:
4610
- accidentalToSet = AccidentalType.DoubleSharp;
4611
- break;
4612
- case NoteAccidentalMode.ForceFlat:
4613
- accidentalToSet = AccidentalType.Flat;
4614
- break;
4615
- case NoteAccidentalMode.ForceDoubleFlat:
4616
- accidentalToSet = AccidentalType.DoubleFlat;
4617
- break;
4618
- default:
4619
- // if note has an accidental in the octave, we place a symbol
4620
- // according to the Key Signature
4621
- if (hasNoteAccidentalWithinOctave) {
4622
- accidentalToSet = accidentalForKeySignature;
4623
- }
4624
- else if (hasKeySignatureAccidentalSetForNote) {
4625
- // note does not get an accidental, but KS defines one -> Naturalize
4626
- accidentalToSet = AccidentalType.Natural;
4627
- }
4628
- break;
4629
- }
4630
- // do we need an accidental on the note?
4631
- if (accidentalToSet !== AccidentalType.None) {
4632
- // if there is no accidental on the line, and the key signature has it set already, we clear it on the note
4633
- if (currentAccidental != null) {
4634
- if (currentAccidental === accidentalToSet) {
4635
- accidentalToSet = AccidentalType.None;
4636
- }
4637
- }
4638
- else if (hasKeySignatureAccidentalSetForNote && accidentalToSet === accidentalForKeySignature) {
4639
- accidentalToSet = AccidentalType.None;
4640
- }
4641
- }
4642
- else {
4643
- // if we don't want an accidental, but there is already one applied, we place a naturalize accidental
4644
- // and clear the registration
4645
- if (currentAccidental !== null) {
4646
- if (currentAccidental === AccidentalType.Natural) {
4647
- accidentalToSet = AccidentalType.None;
4648
- }
4649
- else {
4650
- accidentalToSet = AccidentalType.Natural;
4651
- }
4652
- }
4653
- }
4654
- }
4655
- return accidentalToSet;
4656
- }
4657
4528
  /**
4658
4529
  * @internal
4659
4530
  */
@@ -4692,6 +4563,239 @@
4692
4563
  }
4693
4564
  return systemIndex < systemsLayout.length ? systemsLayout[systemIndex] : defaultSystemsLayout;
4694
4565
  }
4566
+ // diatonic accidentals
4567
+ static _degreeSemitones = [0, 2, 4, 5, 7, 9, 11];
4568
+ static _sharpPreferredSpellings = [
4569
+ { degree: 0, accidentalOffset: 0 }, // C
4570
+ { degree: 0, accidentalOffset: 1 }, // C#
4571
+ { degree: 1, accidentalOffset: 0 }, // D
4572
+ { degree: 1, accidentalOffset: 1 }, // D#
4573
+ { degree: 2, accidentalOffset: 0 }, // E
4574
+ { degree: 3, accidentalOffset: 0 }, // F
4575
+ { degree: 3, accidentalOffset: 1 }, // F#
4576
+ { degree: 4, accidentalOffset: 0 }, // G
4577
+ { degree: 4, accidentalOffset: 1 }, // G#
4578
+ { degree: 5, accidentalOffset: 0 }, // A
4579
+ { degree: 5, accidentalOffset: 1 }, // A#
4580
+ { degree: 6, accidentalOffset: 0 } // B
4581
+ ];
4582
+ static _flatPreferredSpellings = [
4583
+ { degree: 0, accidentalOffset: 0 }, // C
4584
+ { degree: 1, accidentalOffset: -1 }, // Db
4585
+ { degree: 1, accidentalOffset: 0 }, // D
4586
+ { degree: 2, accidentalOffset: -1 }, // Eb
4587
+ { degree: 2, accidentalOffset: 0 }, // E
4588
+ { degree: 3, accidentalOffset: 0 }, // F
4589
+ { degree: 4, accidentalOffset: -1 }, // Gb
4590
+ { degree: 4, accidentalOffset: 0 }, // G
4591
+ { degree: 5, accidentalOffset: -1 }, // Ab
4592
+ { degree: 5, accidentalOffset: 0 }, // A
4593
+ { degree: 6, accidentalOffset: -1 }, // Bb
4594
+ { degree: 6, accidentalOffset: 0 } // B
4595
+ ];
4596
+ // 12 chromatic pitch classes with always 3 possible spellings in the
4597
+ // accidental range of bb..##
4598
+ static _spellingCandidates = [
4599
+ // 0: C
4600
+ [
4601
+ { degree: 0, accidentalOffset: 0 }, // C
4602
+ { degree: 1, accidentalOffset: -2 }, // Dbb
4603
+ { degree: 6, accidentalOffset: 1 } // B#
4604
+ ],
4605
+ // 1: C#/Db
4606
+ [
4607
+ { degree: 0, accidentalOffset: 1 }, // C#
4608
+ { degree: 1, accidentalOffset: -1 }, // Db
4609
+ { degree: 6, accidentalOffset: 2 } // B##
4610
+ ],
4611
+ // 2: D
4612
+ [
4613
+ { degree: 1, accidentalOffset: 0 }, // D
4614
+ { degree: 0, accidentalOffset: 2 }, // C##
4615
+ { degree: 2, accidentalOffset: -2 } // Ebb
4616
+ ],
4617
+ // 3: D#/Eb
4618
+ [
4619
+ { degree: 1, accidentalOffset: 1 }, // D#
4620
+ { degree: 2, accidentalOffset: -1 }, // Eb
4621
+ { degree: 3, accidentalOffset: -2 } // Fbb
4622
+ ],
4623
+ // 4: E
4624
+ [
4625
+ { degree: 2, accidentalOffset: 0 }, // E
4626
+ { degree: 1, accidentalOffset: 2 }, // D##
4627
+ { degree: 3, accidentalOffset: -1 } // Fb
4628
+ ],
4629
+ // 5: F
4630
+ [
4631
+ { degree: 3, accidentalOffset: 0 }, // F
4632
+ { degree: 2, accidentalOffset: 1 }, // E#
4633
+ { degree: 4, accidentalOffset: -2 } // Gbb
4634
+ ],
4635
+ // 6: F#/Gb
4636
+ [
4637
+ { degree: 3, accidentalOffset: 1 }, // F#
4638
+ { degree: 4, accidentalOffset: -1 }, // Gb
4639
+ { degree: 2, accidentalOffset: 2 } // E##
4640
+ ],
4641
+ // 7: G
4642
+ [
4643
+ { degree: 4, accidentalOffset: 0 }, // G
4644
+ { degree: 3, accidentalOffset: 2 }, // F##
4645
+ { degree: 5, accidentalOffset: -2 } // Abb
4646
+ ],
4647
+ // 8: G#/Ab
4648
+ [
4649
+ { degree: 4, accidentalOffset: 1 }, // G#
4650
+ { degree: 5, accidentalOffset: -1 } // Ab
4651
+ ],
4652
+ // 9: A
4653
+ [
4654
+ { degree: 5, accidentalOffset: 0 }, // A
4655
+ { degree: 4, accidentalOffset: 2 }, // G##
4656
+ { degree: 6, accidentalOffset: -2 } // Bbb
4657
+ ],
4658
+ // 10: A#/Bb
4659
+ [
4660
+ { degree: 5, accidentalOffset: 1 }, // A#
4661
+ { degree: 6, accidentalOffset: -1 }, // Bb
4662
+ { degree: 0, accidentalOffset: -2 } // Cbb
4663
+ ],
4664
+ // 11: B
4665
+ [
4666
+ { degree: 6, accidentalOffset: 0 }, // B
4667
+ { degree: 5, accidentalOffset: 2 }, // A##
4668
+ { degree: 0, accidentalOffset: -1 } // Cb
4669
+ ]
4670
+ ];
4671
+ static _sharpKeySignatureOrder = [3, 0, 4, 1, 5, 2, 6]; // F C G D A E B
4672
+ static _flatKeySignatureOrder = [6, 2, 5, 1, 4, 0, 3]; // B E A D G C F
4673
+ static _keySignatureAccidentalByDegree = ModelUtils._buildKeySignatureAccidentalByDegree();
4674
+ static _accidentalOffsetToType = new Map([
4675
+ [-2, AccidentalType.DoubleFlat],
4676
+ [-1, AccidentalType.Flat],
4677
+ [0, AccidentalType.Natural],
4678
+ [1, AccidentalType.Sharp],
4679
+ [2, AccidentalType.DoubleSharp]
4680
+ ]);
4681
+ static _forcedAccidentalOffsetByMode = new Map([
4682
+ [NoteAccidentalMode.ForceSharp, 1],
4683
+ [NoteAccidentalMode.ForceDoubleSharp, 2],
4684
+ [NoteAccidentalMode.ForceFlat, -1],
4685
+ [NoteAccidentalMode.ForceDoubleFlat, -2],
4686
+ [NoteAccidentalMode.ForceNatural, 0],
4687
+ [NoteAccidentalMode.ForceNone, 0],
4688
+ [NoteAccidentalMode.Default, Number.NaN]
4689
+ ]);
4690
+ static _buildKeySignatureAccidentalByDegree() {
4691
+ const lookup = [];
4692
+ for (let ks = -7; ks <= 7; ks++) {
4693
+ const row = [0, 0, 0, 0, 0, 0, 0];
4694
+ if (ks > 0) {
4695
+ for (let i = 0; i < ks; i++) {
4696
+ row[ModelUtils._sharpKeySignatureOrder[i]] = 1;
4697
+ }
4698
+ }
4699
+ else if (ks < 0) {
4700
+ for (let i = 0; i < -ks; i++) {
4701
+ row[ModelUtils._flatKeySignatureOrder[i]] = -1;
4702
+ }
4703
+ }
4704
+ lookup.push(row);
4705
+ }
4706
+ return lookup;
4707
+ }
4708
+ static getKeySignatureAccidentalOffset(keySignature, degree) {
4709
+ return ModelUtils._keySignatureAccidentalByDegree[keySignature + 7][degree];
4710
+ }
4711
+ static resolveSpelling(keySignature, noteValue, accidentalMode) {
4712
+ const chroma = ModelUtils.flooredDivision(noteValue, 12);
4713
+ const preferred = ModelUtils._getPreferredSpellingForKeySignature(keySignature, chroma);
4714
+ const desiredOffset = ModelUtils._forcedAccidentalOffsetByMode.has(accidentalMode)
4715
+ ? ModelUtils._forcedAccidentalOffsetByMode.get(accidentalMode)
4716
+ : Number.NaN;
4717
+ let spelling = preferred;
4718
+ if (!Number.isNaN(desiredOffset)) {
4719
+ const candidates = ModelUtils._spellingCandidates[chroma];
4720
+ const exact = candidates.find(c => c.accidentalOffset === desiredOffset);
4721
+ if (exact) {
4722
+ spelling = exact;
4723
+ }
4724
+ }
4725
+ const baseSemitone = ModelUtils._degreeSemitones[spelling.degree] + spelling.accidentalOffset;
4726
+ const octave = Math.floor((noteValue - baseSemitone) / 12) - 1;
4727
+ return {
4728
+ degree: spelling.degree,
4729
+ accidentalOffset: spelling.accidentalOffset,
4730
+ chroma,
4731
+ octave
4732
+ };
4733
+ }
4734
+ static computeAccidental(keySignature, accidentalMode, noteValue, quarterBend, currentAccidentalOffset = null) {
4735
+ const spelling = ModelUtils.resolveSpelling(keySignature, noteValue, accidentalMode);
4736
+ return ModelUtils.computeAccidentalForSpelling(keySignature, accidentalMode, spelling, quarterBend, currentAccidentalOffset);
4737
+ }
4738
+ static computeAccidentalForSpelling(keySignature, accidentalMode, spelling, quarterBend, currentAccidentalOffset = null) {
4739
+ if (accidentalMode === NoteAccidentalMode.ForceNone) {
4740
+ return AccidentalType.None;
4741
+ }
4742
+ if (quarterBend) {
4743
+ if (spelling.accidentalOffset > 0) {
4744
+ return AccidentalType.SharpQuarterNoteUp;
4745
+ }
4746
+ if (spelling.accidentalOffset < 0) {
4747
+ return AccidentalType.FlatQuarterNoteUp;
4748
+ }
4749
+ return AccidentalType.NaturalQuarterNoteUp;
4750
+ }
4751
+ const desiredOffset = spelling.accidentalOffset;
4752
+ const ksOffset = ModelUtils.getKeySignatureAccidentalOffset(keySignature, spelling.degree);
4753
+ // already active in bar -> no accidental needed
4754
+ if (currentAccidentalOffset === desiredOffset) {
4755
+ return AccidentalType.None;
4756
+ }
4757
+ // key signature already defines the accidental and no explicit accidental is active
4758
+ if (currentAccidentalOffset == null && desiredOffset === ksOffset) {
4759
+ return AccidentalType.None;
4760
+ }
4761
+ return ModelUtils.accidentalOffsetToType(desiredOffset);
4762
+ }
4763
+ static accidentalOffsetToType(offset) {
4764
+ return ModelUtils._accidentalOffsetToType.has(offset)
4765
+ ? ModelUtils._accidentalOffsetToType.get(offset)
4766
+ : AccidentalType.None;
4767
+ }
4768
+ static _getPreferredSpellingForKeySignature(keySignature, chroma) {
4769
+ const candidates = ModelUtils._spellingCandidates[chroma];
4770
+ const ksMatch = candidates.find(c => ModelUtils.getKeySignatureAccidentalOffset(keySignature, c.degree) === c.accidentalOffset);
4771
+ if (ksMatch) {
4772
+ return ksMatch;
4773
+ }
4774
+ const preferFlat = ModelUtils.keySignatureIsFlat(keySignature);
4775
+ return preferFlat ? ModelUtils._flatPreferredSpellings[chroma] : ModelUtils._sharpPreferredSpellings[chroma];
4776
+ }
4777
+ static _majorKeySignatureTonicDegrees = [
4778
+ // Flats: Cb, Gb, Db, Ab, Eb, Bb, F
4779
+ 0, 4, 1, 5, 2, 6, 3,
4780
+ // Natural: C
4781
+ 0,
4782
+ // Sharps: G, D, A, E, B, F#, C#
4783
+ 4, 1, 5, 2, 6, 3, 0
4784
+ ];
4785
+ static _minorKeySignatureTonicDegrees = [
4786
+ // Flats: Ab, Eb, Bb, F, C, G, D
4787
+ 5, 2, 6, 3, 0, 4, 1,
4788
+ // Natural: A
4789
+ 5,
4790
+ // Sharps: E, B, F#, C#, G#, D#, A#
4791
+ 2, 6, 3, 0, 4, 1, 5
4792
+ ];
4793
+ static getKeySignatureTonicDegree(keySignature, keySignatureType) {
4794
+ const ksi = keySignature + 7;
4795
+ return keySignatureType === KeySignatureType.Minor
4796
+ ? ModelUtils._minorKeySignatureTonicDegrees[ksi]
4797
+ : ModelUtils._majorKeySignatureTonicDegrees[ksi];
4798
+ }
4695
4799
  }
4696
4800
 
4697
4801
  /**
@@ -5402,7 +5506,8 @@
5402
5506
  }
5403
5507
  }
5404
5508
  }
5405
- return 'unknown';
5509
+ // unknown combination, should not happen, fallback to some default value (Snare hit)
5510
+ return 'Snare (hit)';
5406
5511
  }
5407
5512
  static getArticulation(n) {
5408
5513
  const articulationIndex = n.percussionArticulation;
@@ -15578,7 +15683,11 @@
15578
15683
  const slurId = `s${note.slurOrigin.id}`;
15579
15684
  Atnf.prop(properties, 'slur', Atnf.identValue(slurId));
15580
15685
  }
15581
- if (note.accidentalMode !== NoteAccidentalMode.Default) {
15686
+ // NOTE: it would be better to check via accidentalhelper what accidentals we really need to force
15687
+ const skipAccidental = note.accidentalMode === NoteAccidentalMode.Default ||
15688
+ (note.beat.voice.bar.keySignature === KeySignature.C &&
15689
+ note.accidentalMode === NoteAccidentalMode.ForceNatural);
15690
+ if (!skipAccidental) {
15582
15691
  Atnf.prop(properties, 'acc', Atnf.identValue(ModelUtils.reverseAccidentalModeMapping.get(note.accidentalMode)));
15583
15692
  }
15584
15693
  switch (note.ornament) {
@@ -19543,6 +19652,24 @@
19543
19652
  */
19544
19653
  class Gp3To5Importer extends ScoreImporter {
19545
19654
  static _versionString = 'FICHIER GUITAR PRO ';
19655
+ // NOTE: General Midi only defines percussion instruments from 35-81
19656
+ // Guitar Pro 5 allowed GS extensions (27-34 and 82-87)
19657
+ // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback
19658
+ // (even if they are not correct)
19659
+ // we can support this properly in future when we allow custom alphaTex articulation definitions
19660
+ // then we don't need to rely on GP specifics anymore but handle things on export/import
19661
+ static _gp5PercussionInstrumentMap = new Map([
19662
+ // High Q -> GS "High Q / Filter Snap"
19663
+ [27, 42],
19664
+ // Slap
19665
+ [28, 60],
19666
+ // Scratch Push
19667
+ [29, 29],
19668
+ // Scratch Pull
19669
+ [30, 30],
19670
+ // Square Click
19671
+ [32, 31]
19672
+ ]);
19546
19673
  _versionNumber = 0;
19547
19674
  _score;
19548
19675
  _globalTripletFeel = TripletFeel.NoTripletFeel;
@@ -19889,9 +20016,9 @@
19889
20016
  }
19890
20017
  }
19891
20018
  /**
19892
- * Guitar Pro 3-6 changes to a bass clef if any string tuning is below B2;
20019
+ * Guitar Pro 3-6 changes to a bass clef if any string tuning is below B1
19893
20020
  */
19894
- static _bassClefTuningThreshold = ModelUtils.parseTuning('B2').realValue;
20021
+ static _bassClefTuningThreshold = ModelUtils.parseTuning('B1').realValue;
19895
20022
  readTrack() {
19896
20023
  const newTrack = new Track();
19897
20024
  newTrack.ensureStaveCount(1);
@@ -20612,7 +20739,9 @@
20612
20739
  this.readNoteEffects(track, voice, beat, newNote);
20613
20740
  }
20614
20741
  if (bar.staff.isPercussion) {
20615
- newNote.percussionArticulation = newNote.fret;
20742
+ newNote.percussionArticulation = Gp3To5Importer._gp5PercussionInstrumentMap.has(newNote.fret)
20743
+ ? Gp3To5Importer._gp5PercussionInstrumentMap.get(newNote.fret)
20744
+ : newNote.fret;
20616
20745
  newNote.string = -1;
20617
20746
  newNote.fret = -1;
20618
20747
  }
@@ -23864,6 +23993,9 @@
23864
23993
  switch (c.localName) {
23865
23994
  case 'Accidental':
23866
23995
  switch (c.innerText) {
23996
+ case '':
23997
+ note.accidentalMode = NoteAccidentalMode.ForceNatural;
23998
+ break;
23867
23999
  case 'x':
23868
24000
  note.accidentalMode = NoteAccidentalMode.ForceDoubleSharp;
23869
24001
  break;
@@ -24902,13 +25034,9 @@
24902
25034
  */
24903
25035
  static _octaveSteps = [38, 32, 30, 26, 38];
24904
25036
  /**
24905
- * The step offsets of the notes within an octave in case of for sharp keysignatures
24906
- */
24907
- static sharpNoteSteps = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
24908
- /**
24909
- * The step offsets of the notes within an octave in case of for flat keysignatures
25037
+ * Diatonic step offsets within an octave.
24910
25038
  */
24911
- static flatNoteSteps = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6];
25039
+ static _diatonicSteps = [0, 1, 2, 3, 4, 5, 6];
24912
25040
  _registeredAccidentals = new Map();
24913
25041
  _appliedScoreSteps = new Map();
24914
25042
  _appliedScoreStepsByValue = new Map();
@@ -24940,23 +25068,7 @@
24940
25068
  return PercussionMapper.getArticulation(note)?.staffLine ?? 0;
24941
25069
  }
24942
25070
  static getNoteValue(note) {
24943
- let noteValue = note.displayValue;
24944
- // adjust note height according to accidentals enforced
24945
- switch (note.accidentalMode) {
24946
- case NoteAccidentalMode.ForceDoubleFlat:
24947
- noteValue += 2;
24948
- break;
24949
- case NoteAccidentalMode.ForceDoubleSharp:
24950
- noteValue -= 2;
24951
- break;
24952
- case NoteAccidentalMode.ForceFlat:
24953
- noteValue += 1;
24954
- break;
24955
- case NoteAccidentalMode.ForceSharp:
24956
- noteValue -= 1;
24957
- break;
24958
- }
24959
- return noteValue;
25071
+ return note.displayValue;
24960
25072
  }
24961
25073
  /**
24962
25074
  * Calculates the accidental for the given note and assignes the value to it.
@@ -24988,7 +25100,8 @@
24988
25100
  steps = AccidentalHelper.getPercussionSteps(note);
24989
25101
  }
24990
25102
  else {
24991
- steps = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue);
25103
+ const spelling = ModelUtils.resolveSpelling(bar.keySignature, noteValue, note.accidentalMode);
25104
+ steps = AccidentalHelper.calculateNoteSteps(bar.clef, spelling);
24992
25105
  }
24993
25106
  return steps;
24994
25107
  }
@@ -25001,11 +25114,12 @@
25001
25114
  }
25002
25115
  else {
25003
25116
  const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default;
25004
- steps = AccidentalHelper.calculateNoteSteps(this._bar.keySignature, this._bar.clef, noteValue);
25005
- const currentAccidental = this._registeredAccidentals.has(steps)
25117
+ const spelling = ModelUtils.resolveSpelling(this._bar.keySignature, noteValue, accidentalMode);
25118
+ steps = AccidentalHelper.calculateNoteSteps(this._bar.clef, spelling);
25119
+ const currentAccidentalOffset = this._registeredAccidentals.has(steps)
25006
25120
  ? this._registeredAccidentals.get(steps)
25007
25121
  : null;
25008
- accidentalToSet = ModelUtils.computeAccidental(this._bar.keySignature, accidentalMode, noteValue, quarterBend, currentAccidental);
25122
+ accidentalToSet = ModelUtils.computeAccidentalForSpelling(this._bar.keySignature, accidentalMode, spelling, quarterBend, currentAccidentalOffset);
25009
25123
  let skipAccidental = false;
25010
25124
  switch (accidentalToSet) {
25011
25125
  case AccidentalType.NaturalQuarterNoteUp:
@@ -25030,14 +25144,12 @@
25030
25144
  if (skipAccidental) {
25031
25145
  accidentalToSet = AccidentalType.None;
25032
25146
  }
25033
- else {
25034
- // do we need an accidental on the note?
25035
- if (accidentalToSet !== AccidentalType.None) {
25036
- this._registeredAccidentals.set(steps, accidentalToSet);
25037
- }
25038
- }
25039
25147
  break;
25040
25148
  }
25149
+ const shouldRegister = !quarterBend && accidentalToSet !== AccidentalType.None;
25150
+ if (shouldRegister) {
25151
+ this._registeredAccidentals.set(steps, spelling.accidentalOffset);
25152
+ }
25041
25153
  }
25042
25154
  if (note) {
25043
25155
  this._appliedScoreSteps.set(note.id, steps);
@@ -25089,21 +25201,14 @@
25089
25201
  getMinStepsNote(b) {
25090
25202
  return this._beatSteps.has(b.id) ? this._beatSteps.get(b.id).minStepsNote : null;
25091
25203
  }
25092
- static calculateNoteSteps(keySignature, clef, noteValue) {
25093
- const value = noteValue;
25094
- const ks = keySignature;
25204
+ static calculateNoteSteps(clef, spelling) {
25095
25205
  const clefValue = clef;
25096
- const index = value % 12;
25097
- const octave = ((value / 12) | 0) - 1;
25098
25206
  // Initial Position
25099
25207
  let steps = AccidentalHelper._octaveSteps[clefValue];
25100
25208
  // Move to Octave
25101
- steps -= octave * AccidentalHelper._stepsPerOctave;
25102
- // get the step list for the current keySignature
25103
- const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks)
25104
- ? AccidentalHelper.sharpNoteSteps
25105
- : AccidentalHelper.flatNoteSteps;
25106
- steps -= stepList[index];
25209
+ steps -= spelling.octave * AccidentalHelper._stepsPerOctave;
25210
+ // Move within octave
25211
+ steps -= AccidentalHelper._diatonicSteps[spelling.degree];
25107
25212
  return steps;
25108
25213
  }
25109
25214
  getNoteSteps(n) {
@@ -25208,7 +25313,8 @@
25208
25313
  musicXmlStaffSteps = 4; // middle of bar
25209
25314
  }
25210
25315
  else {
25211
- musicXmlStaffSteps = AccidentalHelper.calculateNoteSteps(bar.keySignature, bar.clef, noteValue);
25316
+ const spelling = ModelUtils.resolveSpelling(bar.keySignature, noteValue, NoteAccidentalMode.Default);
25317
+ musicXmlStaffSteps = AccidentalHelper.calculateNoteSteps(bar.clef, spelling);
25212
25318
  }
25213
25319
  // to translate this into the "staffLine" semantics we need to subtract additionally the steps "missing" from the absent lines
25214
25320
  const actualSteps = note.beat.voice.bar.staff.standardNotationLineCount * 2 - 1;
@@ -47062,9 +47168,14 @@
47062
47168
  */
47063
47169
  MidiTickLookupFindBeatResultCursorMode[MidiTickLookupFindBeatResultCursorMode["ToNextBext"] = 1] = "ToNextBext";
47064
47170
  /**
47065
- * The cursor should animate to the end of the bar (typically on repeats and jumps)
47171
+ * @deprecated replaced by {@link ToEndOfBeat}
47066
47172
  */
47067
47173
  MidiTickLookupFindBeatResultCursorMode[MidiTickLookupFindBeatResultCursorMode["ToEndOfBar"] = 2] = "ToEndOfBar";
47174
+ /**
47175
+ * The cursor should animate to the end of the **beat** (typically on repeats and jumps)
47176
+ * (this is named end of bar historically)
47177
+ */
47178
+ MidiTickLookupFindBeatResultCursorMode[MidiTickLookupFindBeatResultCursorMode["ToEndOfBeat"] = 3] = "ToEndOfBeat";
47068
47179
  })(MidiTickLookupFindBeatResultCursorMode || (MidiTickLookupFindBeatResultCursorMode = {}));
47069
47180
  /**
47070
47181
  * Represents the results of searching the currently played beat.
@@ -47212,6 +47323,11 @@
47212
47323
  * This info allows building the correct "next" beat and duration.
47213
47324
  */
47214
47325
  multiBarRestInfo = null;
47326
+ /**
47327
+ * An optional playback range to consider when performing lookups.
47328
+ * This will mainly influence the used {@link MidiTickLookupFindBeatResultCursorMode}
47329
+ */
47330
+ playbackRange = null;
47215
47331
  /**
47216
47332
  * Finds the currently played beat given a list of tracks and the current time.
47217
47333
  * @param trackLookup The tracks indices in which to search the played beat for.
@@ -47237,6 +47353,13 @@
47237
47353
  if (!result) {
47238
47354
  result = this._findBeatSlow(checker, currentBeatHint, tick, false);
47239
47355
  }
47356
+ if (result) {
47357
+ const playbackRange = this.playbackRange;
47358
+ const isBeyondRangeEnd = playbackRange !== null && result.start >= playbackRange.endTick;
47359
+ if (isBeyondRangeEnd) {
47360
+ return null;
47361
+ }
47362
+ }
47240
47363
  return result;
47241
47364
  }
47242
47365
  _findBeatFast(checker, currentBeatHint, tick) {
@@ -47276,20 +47399,24 @@
47276
47399
  if (current.nextBeat) {
47277
47400
  current.tickDuration = current.nextBeat.start - current.start;
47278
47401
  current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToNextBext;
47402
+ // jump back
47279
47403
  if (current.nextBeat.masterBar.masterBar.index !== endMasterBar.masterBar.index + 1 &&
47280
47404
  (current.nextBeat.masterBar.masterBar.index !== endMasterBar.masterBar.index ||
47281
47405
  current.nextBeat.beat.playbackStart <= current.beat.playbackStart)) {
47282
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47406
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47407
+ }
47408
+ else if (this.playbackRange !== null && this.playbackRange.endTick <= current.nextBeat.start) {
47409
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47283
47410
  }
47284
47411
  }
47285
47412
  else {
47286
47413
  current.tickDuration = endMasterBar.nextMasterBar.end - current.start;
47287
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47414
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47288
47415
  }
47289
47416
  }
47290
47417
  else {
47291
47418
  current.tickDuration = endMasterBar.end - current.start;
47292
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47419
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47293
47420
  }
47294
47421
  }
47295
47422
  else {
@@ -47297,7 +47424,7 @@
47297
47424
  // this is wierd, we have a masterbar without known tick?
47298
47425
  // make a best guess with the number of bars
47299
47426
  current.tickDuration = (current.masterBar.end - current.masterBar.start) * (group.length + 1);
47300
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47427
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47301
47428
  }
47302
47429
  current.calculateDuration();
47303
47430
  }
@@ -47323,16 +47450,20 @@
47323
47450
  }
47324
47451
  else {
47325
47452
  current.tickDuration = current.masterBar.end - current.start;
47326
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47453
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47327
47454
  current.calculateDuration();
47328
47455
  }
47329
- // if the next beat is not directly the next master bar (e.g. jumping back or forth)
47330
- // we report no next beat and animate to the end
47331
- if (current.nextBeat &&
47332
- current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index + 1 &&
47333
- (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index ||
47334
- current.nextBeat.beat.playbackStart <= current.beat.playbackStart)) {
47335
- current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBar;
47456
+ if (current.nextBeat) {
47457
+ // if the next beat is not directly the next master bar (e.g. jumping back or forth)
47458
+ // we report no next beat and animate to the end
47459
+ if (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index + 1 &&
47460
+ (current.nextBeat.masterBar.masterBar.index !== current.masterBar.masterBar.index ||
47461
+ current.nextBeat.beat.playbackStart <= current.beat.playbackStart)) {
47462
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47463
+ }
47464
+ else if (this.playbackRange !== null && this.playbackRange.endTick <= current.nextBeat.start) {
47465
+ current.cursorMode = MidiTickLookupFindBeatResultCursorMode.ToEndOfBeat;
47466
+ }
47336
47467
  }
47337
47468
  }
47338
47469
  _isMultiBarRestResult(current) {
@@ -49358,22 +49489,61 @@
49358
49489
  }
49359
49490
 
49360
49491
  /**
49361
- * Represents the information related to a resize event.
49362
- * @public
49492
+ * A cursor handler which animates the beat cursor to the next beat or end of the beat bounds
49493
+ * depending on the cursor mode.
49494
+ * @internal
49363
49495
  */
49364
- class ResizeEventArgs {
49365
- /**
49366
- * Gets the size before the resizing happened.
49367
- */
49368
- oldWidth = 0;
49369
- /**
49370
- * Gets the size after the resize was complete.
49371
- */
49372
- newWidth = 0;
49373
- /**
49374
- * Gets the settings currently used for rendering.
49375
- */
49376
- settings = null;
49496
+ class ToNextBeatAnimatingCursorHandler {
49497
+ onAttach(_cursors) {
49498
+ }
49499
+ onDetach(_cursors) {
49500
+ }
49501
+ placeBeatCursor(beatCursor, beatBounds, startBeatX) {
49502
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49503
+ const barBounds = barBoundings.visualBounds;
49504
+ beatCursor.transitionToX(0, startBeatX);
49505
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
49506
+ }
49507
+ placeBarCursor(barCursor, beatBounds) {
49508
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49509
+ const barBounds = barBoundings.visualBounds;
49510
+ barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
49511
+ }
49512
+ transitionBeatCursor(beatCursor, _beatBounds, startBeatX, nextBeatX, duration, cursorMode) {
49513
+ // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
49514
+ // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
49515
+ // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
49516
+ const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
49517
+ nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
49518
+ duration = duration * factor;
49519
+ // we need to put the transition to an own animation frame
49520
+ // otherwise the stop animation above is not applied.
49521
+ beatCursor.transitionToX(duration, nextBeatX);
49522
+ }
49523
+ }
49524
+ /**
49525
+ * A cursor handler which just places the bar and beat cursor without any animations applied.
49526
+ * @internal
49527
+ */
49528
+ class NonAnimatingCursorHandler {
49529
+ onAttach(_cursors) {
49530
+ }
49531
+ onDetach(_cursors) {
49532
+ }
49533
+ placeBeatCursor(beatCursor, beatBounds, startBeatX) {
49534
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49535
+ const barBounds = barBoundings.visualBounds;
49536
+ beatCursor.transitionToX(0, startBeatX);
49537
+ beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
49538
+ }
49539
+ placeBarCursor(barCursor, beatBounds) {
49540
+ const barBoundings = beatBounds.barBounds.masterBarBounds;
49541
+ const barBounds = barBoundings.visualBounds;
49542
+ barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
49543
+ }
49544
+ transitionBeatCursor(beatCursor, beatBounds, startBeatX, _nextBeatX, _duration, _cursorMode) {
49545
+ this.placeBeatCursor(beatCursor, beatBounds, startBeatX);
49546
+ }
49377
49547
  }
49378
49548
 
49379
49549
  /**
@@ -49815,6 +49985,25 @@
49815
49985
  error = new EventEmitterOfT();
49816
49986
  }
49817
49987
 
49988
+ /**
49989
+ * Represents the information related to a resize event.
49990
+ * @public
49991
+ */
49992
+ class ResizeEventArgs {
49993
+ /**
49994
+ * Gets the size before the resizing happened.
49995
+ */
49996
+ oldWidth = 0;
49997
+ /**
49998
+ * Gets the size after the resize was complete.
49999
+ */
50000
+ newWidth = 0;
50001
+ /**
50002
+ * Gets the settings currently used for rendering.
50003
+ */
50004
+ settings = null;
50005
+ }
50006
+
49818
50007
  /**
49819
50008
  * Some basic scroll handler checking for changed offsets and scroll if changed.
49820
50009
  * @internal
@@ -50684,6 +50873,42 @@
50684
50873
  }
50685
50874
  }
50686
50875
 
50876
+ /**
50877
+ * This wrapper holds all cursor related elements.
50878
+ * @public
50879
+ */
50880
+ class Cursors {
50881
+ /**
50882
+ * Gets the element that spans across the whole music sheet and holds the other cursor elements.
50883
+ */
50884
+ cursorWrapper;
50885
+ /**
50886
+ * Gets the element that is positioned above the bar that is currently played.
50887
+ */
50888
+ barCursor;
50889
+ /**
50890
+ * Gets the element that is positioned above the beat that is currently played.
50891
+ */
50892
+ beatCursor;
50893
+ /**
50894
+ * Gets the element that spans across the whole music sheet and will hold any selection related elements.
50895
+ */
50896
+ selectionWrapper;
50897
+ /**
50898
+ * Initializes a new instance of the {@link Cursors} class.
50899
+ * @param cursorWrapper
50900
+ * @param barCursor
50901
+ * @param beatCursor
50902
+ * @param selectionWrapper
50903
+ */
50904
+ constructor(cursorWrapper, barCursor, beatCursor, selectionWrapper) {
50905
+ this.cursorWrapper = cursorWrapper;
50906
+ this.barCursor = barCursor;
50907
+ this.beatCursor = beatCursor;
50908
+ this.selectionWrapper = selectionWrapper;
50909
+ }
50910
+ }
50911
+
50687
50912
  /**
50688
50913
  * @internal
50689
50914
  */
@@ -50715,6 +50940,8 @@
50715
50940
  _player;
50716
50941
  _renderer;
50717
50942
  _defaultScrollHandler;
50943
+ _defaultCursorHandler;
50944
+ _customCursorHandler;
50718
50945
  /**
50719
50946
  * An indicator by how many midi-ticks the song contents are shifted.
50720
50947
  * Grace beats at start might require a shift for the first beat to start at 0.
@@ -51486,6 +51713,80 @@
51486
51713
  this.uiFacade.canRenderChanged.on(() => this.render(renderHints));
51487
51714
  }
51488
51715
  }
51716
+ /**
51717
+ * A custom cursor handler which will be used to update the cursor positions during playback.
51718
+ *
51719
+ * @category Properties - Player
51720
+ * @since 1.8.1
51721
+ * @example
51722
+ * JavaScript
51723
+ * ```js
51724
+ * const api = new alphaTab.AlphaTabApi(document.querySelector('#alphaTab'));
51725
+ * api.customCursorHandler = {
51726
+ * _customAdorner: undefined,
51727
+ * onAttach(cursors) {
51728
+ * this._customAdorner = document.createElement('div');
51729
+ * this._customAdorner.classList.add('cursor-adorner');
51730
+ * cursors.cursorWrapper.element.appendChild(this._customAdorner);
51731
+ * },
51732
+ * onDetach(cursors) { this._customAdorner.remove(); },
51733
+ * placeBarCursor(barCursor, beatBounds) {
51734
+ * const barBoundings = beatBounds.barBounds.masterBarBounds;
51735
+ * const barBounds = barBoundings.visualBounds;
51736
+ * barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
51737
+ * },
51738
+ * placeBeatCursor(beatCursor, beatBounds, startBeatX) {
51739
+ * const barBoundings = beatBounds.barBounds.masterBarBounds;
51740
+ * const barBounds = barBoundings.visualBounds;
51741
+ * beatCursor.transitionToX(0, startBeatX);
51742
+ * beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
51743
+ * this._customAdorner.style.left = startBeatX + 'px';
51744
+ * this._customAdorner.style.top = (barBounds.y - 10) + 'px';
51745
+ * this._customAdorner.style.width = '1px';
51746
+ * this._customAdorner.style.height = '10px';
51747
+ * this._customAdorner.style.transition = 'left 0ms linear'; // stop animation
51748
+ * },
51749
+ * transitionBeatCursor(beatCursor, beatBounds, startBeatX, endBeatX, duration, cursorMode) {
51750
+ * this._customAdorner.style.transition = `left ${duration}ms linear`; // start animation
51751
+ * this._customAdorner.style.left = endBeatX + 'px';
51752
+ * }
51753
+ * }
51754
+ * ```
51755
+ *
51756
+ * @example
51757
+ * C#
51758
+ * ```cs
51759
+ * var api = new AlphaTabApi<MyControl>(...);
51760
+ * api.CustomCursorHandler = new CustomCursorHandler();
51761
+ * ```
51762
+ *
51763
+ * @example
51764
+ * Android
51765
+ * ```kotlin
51766
+ * val api = AlphaTabApi<MyControl>(...)
51767
+ * api.customCursorHandler = CustomCursorHandler();
51768
+ * ```
51769
+ */
51770
+ get customCursorHandler() {
51771
+ return this._customCursorHandler;
51772
+ }
51773
+ set customCursorHandler(value) {
51774
+ if (this._customCursorHandler === value) {
51775
+ return;
51776
+ }
51777
+ const currentHandler = this._customCursorHandler ?? this._defaultCursorHandler;
51778
+ this._customCursorHandler = value;
51779
+ if (this._cursorWrapper) {
51780
+ const cursors = new Cursors(this._cursorWrapper, this._barCursor, this._beatCursor, this._selectionWrapper);
51781
+ currentHandler?.onDetach(cursors);
51782
+ if (value) {
51783
+ value?.onDetach(cursors);
51784
+ }
51785
+ else if (this._defaultCursorHandler) {
51786
+ this._defaultCursorHandler.onAttach(cursors);
51787
+ }
51788
+ }
51789
+ }
51489
51790
  _tickCache = null;
51490
51791
  /**
51491
51792
  * A custom scroll handler which will be used to handle scrolling operations during playback.
@@ -51963,6 +52264,9 @@
51963
52264
  }
51964
52265
  set playbackRange(value) {
51965
52266
  this._player.playbackRange = value;
52267
+ if (this._tickCache) {
52268
+ this._tickCache.playbackRange = value;
52269
+ }
51966
52270
  this._updateSelectionCursor(value);
51967
52271
  }
51968
52272
  /**
@@ -52110,6 +52414,7 @@
52110
52414
  generator.applyTranspositionPitches = false;
52111
52415
  generator.generate();
52112
52416
  this._tickCache = generator.tickLookup;
52417
+ this._tickCache.playbackRange = this.playbackRange;
52113
52418
  this._onMidiLoad(midiFile);
52114
52419
  const player = this._player;
52115
52420
  player.midiTickShift = handler.tickShift;
@@ -52505,6 +52810,8 @@
52505
52810
  if (!this._cursorWrapper) {
52506
52811
  return;
52507
52812
  }
52813
+ const cursorHandler = this.customCursorHandler ?? this._defaultCursorHandler;
52814
+ cursorHandler?.onDetach(new Cursors(this._cursorWrapper, this._barCursor, this._beatCursor, this._selectionWrapper));
52508
52815
  this.uiFacade.destroyCursors();
52509
52816
  this._cursorWrapper = null;
52510
52817
  this._barCursor = null;
@@ -52522,6 +52829,8 @@
52522
52829
  this._barCursor = cursors.barCursor;
52523
52830
  this._beatCursor = cursors.beatCursor;
52524
52831
  this._selectionWrapper = cursors.selectionWrapper;
52832
+ const cursorHandler = this.customCursorHandler ?? this._defaultCursorHandler;
52833
+ cursorHandler?.onAttach(cursors);
52525
52834
  this._isInitialBeatCursorUpdate = true;
52526
52835
  }
52527
52836
  if (this._currentBeat !== null) {
@@ -52529,6 +52838,7 @@
52529
52838
  }
52530
52839
  }
52531
52840
  _updateCursors() {
52841
+ this._updateCursorHandler();
52532
52842
  this._updateScrollHandler();
52533
52843
  const enable = this._hasCursor;
52534
52844
  if (enable) {
@@ -52538,6 +52848,21 @@
52538
52848
  this._destroyCursors();
52539
52849
  }
52540
52850
  }
52851
+ _cursorHandlerMode = false;
52852
+ _updateCursorHandler() {
52853
+ const currentHandler = this._defaultCursorHandler;
52854
+ const cursorHandlerMode = this.settings.player.enableAnimatedBeatCursor;
52855
+ // no change
52856
+ if (currentHandler !== undefined && this._cursorHandlerMode === cursorHandlerMode) {
52857
+ return;
52858
+ }
52859
+ if (cursorHandlerMode) {
52860
+ this._defaultCursorHandler = new ToNextBeatAnimatingCursorHandler();
52861
+ }
52862
+ else {
52863
+ this._defaultCursorHandler = new NonAnimatingCursorHandler();
52864
+ }
52865
+ }
52541
52866
  _scrollHandlerMode = exports.ScrollMode.Off;
52542
52867
  _scrollHandlerVertical = true;
52543
52868
  _updateScrollHandler() {
@@ -52605,9 +52930,6 @@
52605
52930
  */
52606
52931
  _cursorUpdateBeat(lookupResult, stop, shouldScroll, cursorSpeed, forceUpdate = false) {
52607
52932
  const beat = lookupResult.beat;
52608
- const nextBeat = lookupResult.nextBeat?.beat ?? null;
52609
- const duration = lookupResult.duration;
52610
- const beatsToHighlight = lookupResult.beatLookup.highlightedBeats;
52611
52933
  if (!beat) {
52612
52934
  return;
52613
52935
  }
@@ -52635,7 +52957,7 @@
52635
52957
  this._previousCursorCache = cache;
52636
52958
  this._previousStateForCursor = this._player.state;
52637
52959
  this.uiFacade.beginInvoke(() => {
52638
- this._internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, lookupResult.cursorMode, cursorSpeed);
52960
+ this._internalCursorUpdateBeat(lookupResult, stop, cache, beatBoundings, shouldScroll, cursorSpeed);
52639
52961
  });
52640
52962
  }
52641
52963
  /**
@@ -52652,24 +52974,30 @@
52652
52974
  }
52653
52975
  }
52654
52976
  }
52655
- _internalCursorUpdateBeat(beat, nextBeat, duration, stop, beatsToHighlight, cache, beatBoundings, shouldScroll, cursorMode, cursorSpeed) {
52656
- const barCursor = this._barCursor;
52977
+ _internalCursorUpdateBeat(lookupResult, stop, boundsLookup, beatBoundings, shouldScroll, cursorSpeed) {
52978
+ const beat = lookupResult.beat;
52979
+ const nextBeat = lookupResult.nextBeat?.beat;
52980
+ let duration = lookupResult.duration;
52981
+ const beatsToHighlight = lookupResult.beatLookup.highlightedBeats;
52982
+ const cursorMode = lookupResult.cursorMode;
52983
+ const cursorHandler = this.customCursorHandler ?? this._defaultCursorHandler;
52657
52984
  const beatCursor = this._beatCursor;
52985
+ const barCursor = this._barCursor;
52658
52986
  const barBoundings = beatBoundings.barBounds.masterBarBounds;
52659
52987
  const barBounds = barBoundings.visualBounds;
52660
52988
  const previousBeatBounds = this._currentBeatBounds;
52661
52989
  this._currentBeatBounds = beatBoundings;
52662
52990
  if (barCursor) {
52663
- barCursor.setBounds(barBounds.x, barBounds.y, barBounds.w, barBounds.h);
52991
+ cursorHandler.placeBarCursor(barCursor, beatBoundings);
52664
52992
  }
52665
52993
  const isPlayingUpdate = this._player.state === PlayerState.Playing && !stop;
52666
- let nextBeatX = barBoundings.visualBounds.x + barBoundings.visualBounds.w;
52994
+ let nextBeatX = beatBoundings.realBounds.x + beatBoundings.realBounds.w;
52667
52995
  let nextBeatBoundings = null;
52668
52996
  // get position of next beat on same system
52669
52997
  if (nextBeat && cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext) {
52670
52998
  // if we are moving within the same bar or to the next bar
52671
52999
  // transition to the next beat, otherwise transition to the end of the bar.
52672
- nextBeatBoundings = cache.findBeat(nextBeat);
53000
+ nextBeatBoundings = boundsLookup.findBeat(nextBeat);
52673
53001
  if (nextBeatBoundings &&
52674
53002
  nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds) {
52675
53003
  nextBeatX = nextBeatBoundings.onNotesX;
@@ -52677,48 +53005,30 @@
52677
53005
  }
52678
53006
  let startBeatX = beatBoundings.onNotesX;
52679
53007
  if (beatCursor) {
52680
- // relative positioning of the cursor
52681
- if (this.settings.player.enableAnimatedBeatCursor) {
52682
- const animationWidth = nextBeatX - beatBoundings.onNotesX;
52683
- const relativePosition = this._previousTick - this._currentBeat.start;
52684
- const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
52685
- startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
52686
- duration -= duration * ratioPosition;
52687
- if (isPlayingUpdate) {
52688
- // we do not "reset" the cursor if we are smoothly moving from left to right.
52689
- const jumpCursor = !previousBeatBounds ||
52690
- this._isInitialBeatCursorUpdate ||
52691
- barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
52692
- startBeatX < previousBeatBounds.onNotesX ||
52693
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
52694
- if (jumpCursor) {
52695
- beatCursor.transitionToX(0, startBeatX);
52696
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
52697
- }
52698
- // it can happen that the cursor reaches the target position slightly too early (especially on backing tracks)
52699
- // to avoid the cursor stopping, causing a wierd look, we animate the cursor to the double position in double time.
52700
- // beatCursor!.transitionToX((duration / cursorSpeed), nextBeatX);
52701
- const factor = cursorMode === MidiTickLookupFindBeatResultCursorMode.ToNextBext ? 2 : 1;
52702
- nextBeatX = startBeatX + (nextBeatX - startBeatX) * factor;
52703
- duration = (duration / cursorSpeed) * factor;
52704
- // we need to put the transition to an own animation frame
52705
- // otherwise the stop animation above is not applied.
52706
- this.uiFacade.beginInvoke(() => {
52707
- beatCursor.transitionToX(duration, nextBeatX);
52708
- });
52709
- }
52710
- else {
52711
- duration = 0;
52712
- beatCursor.transitionToX(duration, nextBeatX);
52713
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
52714
- }
53008
+ const animationWidth = nextBeatX - beatBoundings.onNotesX;
53009
+ const relativePosition = this._previousTick - this._currentBeat.start;
53010
+ const ratioPosition = this._currentBeat.tickDuration > 0 ? relativePosition / this._currentBeat.tickDuration : 0;
53011
+ startBeatX = beatBoundings.onNotesX + animationWidth * ratioPosition;
53012
+ duration -= duration * ratioPosition;
53013
+ // respect speed
53014
+ duration = duration / cursorSpeed;
53015
+ if (isPlayingUpdate) {
53016
+ // we do not "reset" the cursor if we are smoothly moving from left to right.
53017
+ const jumpCursor = !previousBeatBounds ||
53018
+ this._isInitialBeatCursorUpdate ||
53019
+ barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
53020
+ startBeatX < previousBeatBounds.onNotesX ||
53021
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
53022
+ if (jumpCursor) {
53023
+ cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
53024
+ }
53025
+ this.uiFacade.beginInvoke(() => {
53026
+ cursorHandler.transitionBeatCursor(beatCursor, beatBoundings, startBeatX, nextBeatX, duration, cursorMode);
53027
+ });
52715
53028
  }
52716
53029
  else {
52717
- // ticking cursor
52718
53030
  duration = 0;
52719
- nextBeatX = startBeatX;
52720
- beatCursor.transitionToX(duration, nextBeatX);
52721
- beatCursor.setBounds(startBeatX, barBounds.y, 1, barBounds.h);
53031
+ cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
52722
53032
  }
52723
53033
  this._isInitialBeatCursorUpdate = false;
52724
53034
  }
@@ -55595,42 +55905,6 @@
55595
55905
  error = new EventEmitterOfT();
55596
55906
  }
55597
55907
 
55598
- /**
55599
- * This wrapper holds all cursor related elements.
55600
- * @public
55601
- */
55602
- class Cursors {
55603
- /**
55604
- * Gets the element that spans across the whole music sheet and holds the other cursor elements.
55605
- */
55606
- cursorWrapper;
55607
- /**
55608
- * Gets the element that is positioned above the bar that is currently played.
55609
- */
55610
- barCursor;
55611
- /**
55612
- * Gets the element that is positioned above the beat that is currently played.
55613
- */
55614
- beatCursor;
55615
- /**
55616
- * Gets the element that spans across the whole music sheet and will hold any selection related elements.
55617
- */
55618
- selectionWrapper;
55619
- /**
55620
- * Initializes a new instance of the {@link Cursors} class.
55621
- * @param cursorWrapper
55622
- * @param barCursor
55623
- * @param beatCursor
55624
- * @param selectionWrapper
55625
- */
55626
- constructor(cursorWrapper, barCursor, beatCursor, selectionWrapper) {
55627
- this.cursorWrapper = cursorWrapper;
55628
- this.barCursor = barCursor;
55629
- this.beatCursor = beatCursor;
55630
- this.selectionWrapper = selectionWrapper;
55631
- }
55632
- }
55633
-
55634
55908
  /**
55635
55909
  * An IContainer implementation which can be used for cursors and select ranges
55636
55910
  * where browser scaling is relevant.
@@ -59934,11 +60208,12 @@
59934
60208
  }
59935
60209
  doLayout() {
59936
60210
  super.doLayout();
59937
- const text = '1 = ';
60211
+ let text = '';
59938
60212
  let text2 = '';
59939
60213
  let accidental = AccidentalType.None;
59940
60214
  switch (this._keySignatureType) {
59941
60215
  case KeySignatureType.Major:
60216
+ text = '1 = ';
59942
60217
  switch (this._keySignature) {
59943
60218
  case KeySignature.Cb:
59944
60219
  text2 = ' C';
@@ -60002,6 +60277,7 @@
60002
60277
  }
60003
60278
  break;
60004
60279
  case KeySignatureType.Minor:
60280
+ text = '6 = ';
60005
60281
  switch (this._keySignature) {
60006
60282
  case KeySignature.Cb:
60007
60283
  text2 = ' a';
@@ -67981,7 +68257,6 @@
67981
68257
  * @internal
67982
68258
  */
67983
68259
  class NumberedBeatPreNotesGlyph extends BeatGlyphBase {
67984
- isNaturalizeAccidental = false;
67985
68260
  accidental = AccidentalType.None;
67986
68261
  skipLayout = false;
67987
68262
  get effectElement() {
@@ -67996,25 +68271,25 @@
67996
68271
  accidentals.renderer = this.renderer;
67997
68272
  if (this.container.beat.notes.length > 0) {
67998
68273
  const note = this.container.beat.notes[0];
67999
- // Notes
68000
- // - Compared to standard notation accidentals:
68001
- // - Flat keysigs: When there is a naturalize symbol (against key signature, not naturalizing same line) we have a # in Numbered notation
68002
- // - Flat keysigs: When there is a flat symbol standard notation we also have a flat in Numbered notation
68003
- // - C keysig: A sharp on standard notation is a sharp on numbered notation
68004
- // - # keysigs: When there is a # symbol on standard notation we also a sharp in numbered notation
68005
- // - # keysigs: When there is a naturalize symbol (against key signature, not naturalizing same line) we have a flat in Numbered notation
68006
- // Or generally:
68007
- // - numbered notation has the same accidentals as standard notation if applied
68008
- // - when the standard notation naturalizes the accidental from the key signature, the numbered notation has the reversed accidental
68009
- const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default;
68010
- const noteValue = AccidentalHelper.getNoteValue(note);
68011
- let accidentalToSet = ModelUtils.computeAccidental(this.renderer.bar.keySignature, accidentalMode, noteValue, note.hasQuarterToneOffset);
68012
- if (accidentalToSet === AccidentalType.Natural) {
68013
- const ks = this.renderer.bar.keySignature;
68014
- const ksi = ks + 7;
68015
- const naturalizeAccidentalForKeySignature = ksi < 7 ? AccidentalType.Sharp : AccidentalType.Flat;
68016
- accidentalToSet = naturalizeAccidentalForKeySignature;
68017
- this.isNaturalizeAccidental = true;
68274
+ const spelling = ModelUtils.resolveSpelling(this.renderer.bar.keySignature, note.displayValue, note.accidentalMode);
68275
+ const ksOffset = ModelUtils.getKeySignatureAccidentalOffset(this.renderer.bar.keySignature, spelling.degree);
68276
+ const requiredOffset = spelling.accidentalOffset - ksOffset;
68277
+ let accidentalToSet = AccidentalType.None;
68278
+ if (note.accidentalMode !== NoteAccidentalMode.ForceNone) {
68279
+ if (note.hasQuarterToneOffset) {
68280
+ if (requiredOffset > 0) {
68281
+ accidentalToSet = AccidentalType.SharpQuarterNoteUp;
68282
+ }
68283
+ else if (requiredOffset < 0) {
68284
+ accidentalToSet = AccidentalType.FlatQuarterNoteUp;
68285
+ }
68286
+ else {
68287
+ accidentalToSet = AccidentalType.NaturalQuarterNoteUp;
68288
+ }
68289
+ }
68290
+ else if (requiredOffset !== 0) {
68291
+ accidentalToSet = ModelUtils.accidentalOffsetToType(requiredOffset);
68292
+ }
68018
68293
  }
68019
68294
  // do we need an accidental on the note?
68020
68295
  if (accidentalToSet !== AccidentalType.None) {
@@ -68121,21 +68396,21 @@
68121
68396
  }
68122
68397
  return 0;
68123
68398
  }
68124
- static majorKeySignatureOneValues = [
68125
- // Flats
68126
- 59, 66, 61, 68, 63, 58, 65,
68127
- // natural
68399
+ static _majorKeySignatureOneValues = [
68400
+ // Flats: Cb, Gb, Db, Ab, Eb, Bb, F
68401
+ 59, 66, 61, 68, 63, 70, 65,
68402
+ // natural: C
68128
68403
  60,
68129
- // sharps (where the value is true, a flat accidental is required for the notes)
68404
+ // sharps: G, D, A, E, B, F#, C#
68130
68405
  67, 62, 69, 64, 71, 66, 61
68131
68406
  ];
68132
- static minorKeySignatureOneValues = [
68133
- // Flats
68134
- 71, 66, 73, 68, 63, 70, 65,
68135
- // natural
68136
- 72,
68137
- // sharps (where the value is true, a flat accidental is required for the notes)
68138
- 67, 74, 69, 64, 71, 66, 73
68407
+ static _minorKeySignatureOneValues = [
68408
+ // Flats: Ab, Eb, Bb, F, C, G, D
68409
+ 68, 63, 70, 65, 60, 67, 62,
68410
+ // natural: A
68411
+ 69,
68412
+ // sharps: E, B, F#, C#, G#, D#, A#
68413
+ 64, 71, 66, 61, 68, 63, 70
68139
68414
  ];
68140
68415
  doLayout() {
68141
68416
  // create glyphs
@@ -68149,38 +68424,26 @@
68149
68424
  let numberWithinOctave = '0';
68150
68425
  if (this.container.beat.notes.length > 0) {
68151
68426
  const note = this.container.beat.notes[0];
68152
- const kst = this.renderer.bar.keySignatureType;
68153
- const ks = this.renderer.bar.keySignature;
68154
- const ksi = ks + 7;
68155
- const oneNoteValues = kst === KeySignatureType.Minor
68156
- ? NumberedBeatGlyph.minorKeySignatureOneValues
68157
- : NumberedBeatGlyph.majorKeySignatureOneValues;
68158
- const oneNoteValue = oneNoteValues[ksi];
68159
68427
  if (note.isDead) {
68160
68428
  numberWithinOctave = 'X';
68161
68429
  }
68162
68430
  else {
68431
+ const ks = this.renderer.bar.keySignature;
68432
+ const kst = this.renderer.bar.keySignatureType;
68433
+ const ksi = ks + 7;
68434
+ const oneNoteValues = kst === KeySignatureType.Minor
68435
+ ? NumberedBeatGlyph._minorKeySignatureOneValues
68436
+ : NumberedBeatGlyph._majorKeySignatureOneValues;
68437
+ const oneNoteValue = oneNoteValues[ksi];
68438
+ const spelling = ModelUtils.resolveSpelling(ks, note.displayValue, note.accidentalMode);
68439
+ const tonicDegree = ModelUtils.getKeySignatureTonicDegree(ks, kst);
68440
+ const effectiveTonic = kst === KeySignatureType.Minor
68441
+ ? (tonicDegree + 2) % 7 // relative major
68442
+ : tonicDegree;
68443
+ const degreeDistance = (spelling.degree - effectiveTonic + 7) % 7;
68444
+ numberWithinOctave = (degreeDistance + 1).toString();
68163
68445
  const noteValue = note.displayValue - oneNoteValue;
68164
- const index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12;
68165
- octaveDots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0;
68166
- if (noteValue < 0) {
68167
- octaveDots *= -1;
68168
- }
68169
- const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks)
68170
- ? AccidentalHelper.flatNoteSteps
68171
- : AccidentalHelper.sharpNoteSteps;
68172
- let steps = stepList[index] + 1;
68173
- const hasAccidental = ModelUtils.accidentalNotes[index];
68174
- if (hasAccidental &&
68175
- !this.container.preNotes.isNaturalizeAccidental) {
68176
- if (ksi < 7) {
68177
- steps++;
68178
- }
68179
- else {
68180
- steps--;
68181
- }
68182
- }
68183
- numberWithinOctave = steps.toString();
68446
+ octaveDots = Math.floor(noteValue / 12);
68184
68447
  }
68185
68448
  }
68186
68449
  if (this.container.beat.deadSlapped) {