@coderline/alphatab 1.9.0-alpha.1767 → 1.9.0-alpha.1785

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * alphaTab v1.9.0-alpha.1767 (develop, build 1767)
2
+ * alphaTab v1.9.0-alpha.1785 (develop, build 1785)
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.9.0-alpha.1767';
207
- static date = '2026-04-09T03:22:29.010Z';
208
- static commit = '6c3194f5f54595c44b979b1fee6cf423b1c9ed15';
206
+ static version = '1.9.0-alpha.1785';
207
+ static date = '2026-04-27T03:55:02.662Z';
208
+ static commit = '760ed909a3d8dc36b159d23b4ff6780e95a3daf1';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -3826,7 +3826,6 @@ class Score {
3826
3826
  */
3827
3827
  class SynthConstants {
3828
3828
  static DefaultChannelCount = 16 + 1;
3829
- static MetronomeChannel = SynthConstants.DefaultChannelCount - 1;
3830
3829
  static MetronomeKey = 33;
3831
3830
  static AudioChannels = 2;
3832
3831
  static MinVolume = 0;
@@ -3850,6 +3849,10 @@ class SynthConstants {
3850
3849
  static DefaultPitchWheel = SynthConstants.MaxPitchWheel / 2;
3851
3850
  static MicroBufferCount = 32;
3852
3851
  static MicroBufferSize = 64;
3852
+ /**
3853
+ * approximately -60 dB, which is inaudible to humans
3854
+ */
3855
+ static AudibleLevelThreshold = 1e-3;
3853
3856
  }
3854
3857
 
3855
3858
  /**
@@ -23814,26 +23817,26 @@ class GpifParser {
23814
23817
  case 'HarmonicType':
23815
23818
  const htype = c.findChildElement('HType');
23816
23819
  if (htype) {
23817
- switch (htype.innerText) {
23818
- case 'NoHarmonic':
23820
+ switch (htype.innerText.toLowerCase()) {
23821
+ case 'noharmonic':
23819
23822
  note.harmonicType = HarmonicType.None;
23820
23823
  break;
23821
- case 'Natural':
23824
+ case 'natural':
23822
23825
  note.harmonicType = HarmonicType.Natural;
23823
23826
  break;
23824
- case 'Artificial':
23827
+ case 'artificial':
23825
23828
  note.harmonicType = HarmonicType.Artificial;
23826
23829
  break;
23827
- case 'Pinch':
23830
+ case 'pinch':
23828
23831
  note.harmonicType = HarmonicType.Pinch;
23829
23832
  break;
23830
- case 'Tap':
23833
+ case 'tap':
23831
23834
  note.harmonicType = HarmonicType.Tap;
23832
23835
  break;
23833
- case 'Semi':
23836
+ case 'semi':
23834
23837
  note.harmonicType = HarmonicType.Semi;
23835
23838
  break;
23836
- case 'Feedback':
23839
+ case 'feedback':
23837
23840
  note.harmonicType = HarmonicType.Feedback;
23838
23841
  break;
23839
23842
  }
@@ -32625,9 +32628,16 @@ class DisplaySettings {
32625
32628
  *
32626
32629
  * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
32627
32630
  *
32628
- * Also note that the sizing is including any glyphs and notation elements within the bar. e.g. if there are clefs in the bar, they are still "squeezed" into the available size.
32629
- * It is not the case that the actual notes with their lengths are sized accordingly. This fits the sizing system of Guitar Pro and when files are customized there,
32630
- * alphaTab will match this layout quite close.
32631
+ * In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
32632
+ * natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
32633
+ * Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
32634
+ * are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
32635
+ *
32636
+ * * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
32637
+ * `displayScale` on the model is ignored.
32638
+ * * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
32639
+ * `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
32640
+ * author hasn't customized it).
32631
32641
  *
32632
32642
  * ### Horizontal Layout
32633
32643
  *
@@ -38826,14 +38836,25 @@ class StaffSystemBounds {
38826
38836
  */
38827
38837
  boundsLookup;
38828
38838
  /**
38829
- * Finished the lookup for optimized access.
38839
+ * Whether this system's bounds have already been scaled via `finish`. Prevents double-scaling
38840
+ * when the parent `BoundsLookup` is preserved across partial renders and `finish` is invoked
38841
+ * again on a mix of already-scaled (preserved) and newly-registered (natural-coordinate) systems.
38842
+ */
38843
+ isFinished = false;
38844
+ /**
38845
+ * Finished the lookup for optimized access. Idempotent: once finished, further calls are no-ops
38846
+ * so preserved systems survive partial renders without being re-scaled.
38830
38847
  */
38831
38848
  finish(scale = 1) {
38849
+ if (this.isFinished) {
38850
+ return;
38851
+ }
38832
38852
  this.realBounds.scaleWith(scale);
38833
38853
  this.visualBounds.scaleWith(scale);
38834
38854
  for (const t of this.bars) {
38835
38855
  t.finish(scale);
38836
38856
  }
38857
+ this.isFinished = true;
38837
38858
  }
38838
38859
  /**
38839
38860
  * Adds a new master bar to this lookup.
@@ -39006,6 +39027,58 @@ class BoundsLookup {
39006
39027
  }
39007
39028
  this.isFinished = true;
39008
39029
  }
39030
+ /**
39031
+ * Re-opens the lookup for registrations without discarding previously registered bounds.
39032
+ * Used by the renderer when it preserves this lookup across a partial render so that new
39033
+ * bounds for the re-layouted range can be added while preserved systems stay intact.
39034
+ * @internal
39035
+ */
39036
+ resetForPartialUpdate() {
39037
+ this.isFinished = false;
39038
+ }
39039
+ /**
39040
+ * Removes all entries belonging to the given master bar index and any bars after it.
39041
+ * Used before a partial render re-registers bounds for the re-layouted range, so the
39042
+ * preserved lookup ends up with only the unchanged entries when registration begins.
39043
+ *
39044
+ * Assumes the layout aligns its re-layouted range to system boundaries - i.e. the first
39045
+ * system to clear starts exactly at `masterBarIndex`. Caller is responsible for passing
39046
+ * the first master-bar-index of the first re-layouted system.
39047
+ * @internal
39048
+ */
39049
+ clearFromMasterBar(masterBarIndex) {
39050
+ // drop staff systems whose bars start at or after the cleared range.
39051
+ let firstRemovedSystem = -1;
39052
+ for (let i = 0; i < this.staffSystems.length; i++) {
39053
+ const systemBars = this.staffSystems[i].bars;
39054
+ if (systemBars.length > 0 && systemBars[0].index >= masterBarIndex) {
39055
+ firstRemovedSystem = i;
39056
+ break;
39057
+ }
39058
+ }
39059
+ if (firstRemovedSystem !== -1) {
39060
+ this.staffSystems.splice(firstRemovedSystem, this.staffSystems.length - firstRemovedSystem);
39061
+ }
39062
+ // drop master bar entries at or beyond the cleared range.
39063
+ for (const key of Array.from(this._masterBarLookup.keys())) {
39064
+ if (key >= masterBarIndex) {
39065
+ this._masterBarLookup.delete(key);
39066
+ }
39067
+ }
39068
+ // drop beat entries whose beats belong to cleared bars.
39069
+ for (const key of Array.from(this._beatLookup.keys())) {
39070
+ const list = this._beatLookup.get(key);
39071
+ const filtered = list.filter(b => b.beat.voice.bar.index < masterBarIndex);
39072
+ if (filtered.length === 0) {
39073
+ this._beatLookup.delete(key);
39074
+ }
39075
+ else if (filtered.length !== list.length) {
39076
+ this._beatLookup.set(key, filtered);
39077
+ }
39078
+ }
39079
+ // drop the in-progress pointer - the next addStaffSystem call will replace it.
39080
+ this._currentStaffSystem = null;
39081
+ }
39009
39082
  /**
39010
39083
  * Adds a new staff sytem to the lookup.
39011
39084
  * @param bounds The staff system bounds to add.
@@ -39223,8 +39296,11 @@ class AlphaTabWorkerScoreRenderer {
39223
39296
  this.renderFinished.trigger(data.result);
39224
39297
  break;
39225
39298
  case 'alphaTab.postRenderFinished':
39226
- this.boundsLookup = BoundsLookup.fromJson(data.boundsLookup, this._api.score);
39227
- this.boundsLookup?.finish();
39299
+ const score = this._api.score;
39300
+ if (score && data.boundsLookup) {
39301
+ this.boundsLookup = BoundsLookup.fromJson(data.boundsLookup, this._api.score);
39302
+ this.boundsLookup?.finish();
39303
+ }
39228
39304
  this.postRenderFinished.trigger();
39229
39305
  break;
39230
39306
  case 'alphaTab.error':
@@ -39481,6 +39557,164 @@ class AudioElementBackingTrackSynthOutput {
39481
39557
  }
39482
39558
  }
39483
39559
 
39560
+ /**
39561
+ * @internal
39562
+ */
39563
+ class QueueItem {
39564
+ value;
39565
+ next;
39566
+ constructor(value) {
39567
+ this.value = value;
39568
+ }
39569
+ }
39570
+ /**
39571
+ * @internal
39572
+ */
39573
+ class Queue {
39574
+ _head;
39575
+ _tail;
39576
+ get isEmpty() {
39577
+ return this._head === undefined;
39578
+ }
39579
+ clear() {
39580
+ this._head = undefined;
39581
+ this._tail = undefined;
39582
+ }
39583
+ enqueue(item) {
39584
+ const queueItem = new QueueItem(item);
39585
+ if (this._tail) {
39586
+ // not empty -> add after tail
39587
+ this._tail.next = queueItem;
39588
+ this._tail = queueItem;
39589
+ }
39590
+ else {
39591
+ // empty -> new item takes head and tail
39592
+ this._head = queueItem;
39593
+ this._tail = queueItem;
39594
+ }
39595
+ }
39596
+ enqueueFront(item) {
39597
+ const queueItem = new QueueItem(item);
39598
+ queueItem.next = this._head;
39599
+ if (this._head) {
39600
+ this._head = queueItem;
39601
+ }
39602
+ else {
39603
+ this._head = queueItem;
39604
+ this._tail = queueItem;
39605
+ }
39606
+ }
39607
+ peek() {
39608
+ const head = this._head;
39609
+ if (!head) {
39610
+ return undefined;
39611
+ }
39612
+ return head.value;
39613
+ }
39614
+ dequeue() {
39615
+ const head = this._head;
39616
+ if (!head) {
39617
+ return undefined;
39618
+ }
39619
+ const newHead = head.next;
39620
+ this._head = newHead;
39621
+ // last item removed?
39622
+ if (!newHead) {
39623
+ this._tail = undefined;
39624
+ }
39625
+ return head.value;
39626
+ }
39627
+ }
39628
+
39629
+ /**
39630
+ * The options controlling how to export the audio.
39631
+ * @public
39632
+ */
39633
+ class AudioExportOptions {
39634
+ /**
39635
+ * The soundfonts to load and use for generating the audio.
39636
+ * If not provided, the already loaded soundfonts of the synthesizer will be used.
39637
+ * If no existing synthesizer is initialized, the generated audio might not contain any hearable audio.
39638
+ */
39639
+ soundFonts;
39640
+ /**
39641
+ * The output sample rate.
39642
+ * @default `44100`
39643
+ */
39644
+ sampleRate = 44100;
39645
+ /**
39646
+ * Whether to respect sync point information during export.
39647
+ * @default `true`
39648
+ * @remarks
39649
+ * If the song contains sync point information for synchronization with an external media,
39650
+ * this option allows controlling whether the synthesized audio is aligned with these points.
39651
+ *
39652
+ * This is useful when mixing the exported audio together with external media, keeping the same timing.
39653
+ *
39654
+ * Disable this option if you want the original/exact timing as per music sheet in the exported audio.
39655
+ */
39656
+ useSyncPoints = false;
39657
+ /**
39658
+ * The current master volume as percentage. (range: 0.0-3.0, default 1.0)
39659
+ */
39660
+ masterVolume = 1;
39661
+ /**
39662
+ * The metronome volume. (range: 0.0-3.0, default 0.0)
39663
+ */
39664
+ metronomeVolume = 0;
39665
+ /**
39666
+ * The range of the song that should be exported. Set this to null
39667
+ * to play the whole song.
39668
+ */
39669
+ playbackRange;
39670
+ /**
39671
+ * The volume for individual tracks as percentage (range: 0.0-3.0).
39672
+ * @remarks
39673
+ * The key is the track index, and the value is the relative volume.
39674
+ * The configured volume (as per data model) still applies, this is an additional volume control.
39675
+ * If no custom value is set, 100% is used.
39676
+ * No values from the currently active synthesizer are applied.
39677
+ *
39678
+ * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
39679
+ */
39680
+ trackVolume = new Map();
39681
+ /**
39682
+ * The additional semitone pitch transpose to apply for individual tracks.
39683
+ * @remarks
39684
+ * The key is the track index, and the value is the number of semitones to apply.
39685
+ * No values from the currently active synthesizer are applied.
39686
+ *
39687
+ * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
39688
+ */
39689
+ trackTranspositionPitches = new Map();
39690
+ }
39691
+ /**
39692
+ * Represents a single chunk of audio produced.
39693
+ * @public
39694
+ */
39695
+ class AudioExportChunk {
39696
+ /**
39697
+ * The generated samples for the requested chunk.
39698
+ */
39699
+ samples;
39700
+ /**
39701
+ * The current time position within the song in milliseconds.
39702
+ */
39703
+ currentTime = 0;
39704
+ /**
39705
+ * The total length of the song in milliseconds.
39706
+ */
39707
+ endTime = 0;
39708
+ /**
39709
+ * The current time position within the song in midi ticks.
39710
+ */
39711
+ currentTick = 0;
39712
+ /**
39713
+ * The total length of the song in midi ticks.
39714
+ */
39715
+ endTick = 0;
39716
+ }
39717
+
39484
39718
  // The SoundFont loading and Audio Synthesis is based on TinySoundFont, licensed under MIT,
39485
39719
  // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont)
39486
39720
  // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny
@@ -39594,6 +39828,7 @@ class MidiSequencerState {
39594
39828
  endTime = 0;
39595
39829
  currentTempo = 0;
39596
39830
  syncPointTempo = 0;
39831
+ metronomeChannel = SynthConstants.DefaultChannelCount - 1;
39597
39832
  }
39598
39833
  /**
39599
39834
  * This sequencer dispatches midi events to the synthesizer based on the current
@@ -39606,6 +39841,9 @@ class MidiFileSequencer {
39606
39841
  _mainState;
39607
39842
  _oneTimeState = null;
39608
39843
  _countInState = null;
39844
+ get metronomeChannel() {
39845
+ return this._mainState.metronomeChannel;
39846
+ }
39609
39847
  get isPlayingMain() {
39610
39848
  return this._currentState === this._mainState;
39611
39849
  }
@@ -39689,7 +39927,7 @@ class MidiFileSequencer {
39689
39927
  const metronomeVolume = this._synthesizer.metronomeVolume;
39690
39928
  this._synthesizer.noteOffAll(true);
39691
39929
  this._synthesizer.resetSoft();
39692
- this._synthesizer.setupMetronomeChannel(metronomeVolume);
39930
+ this._synthesizer.setupMetronomeChannel(this.metronomeChannel, metronomeVolume);
39693
39931
  }
39694
39932
  this._mainSilentProcess(timePosition);
39695
39933
  }
@@ -39741,6 +39979,7 @@ class MidiFileSequencer {
39741
39979
  let metronomeLengthInMillis = 0;
39742
39980
  let metronomeTick = midiFile.tickShift; // shift metronome to content
39743
39981
  let metronomeTime = 0.0;
39982
+ let maxChannel = 0;
39744
39983
  let previousTick = 0;
39745
39984
  for (const mEvent of midiFile.events) {
39746
39985
  const synthData = new SynthEvent(state.synthData.length, mEvent);
@@ -39782,6 +40021,9 @@ class MidiFileSequencer {
39782
40021
  if (!state.firstProgramEventPerChannel.has(channel)) {
39783
40022
  state.firstProgramEventPerChannel.set(channel, synthData);
39784
40023
  }
40024
+ if (channel > maxChannel) {
40025
+ maxChannel = channel;
40026
+ }
39785
40027
  const isPercussion = channel === SynthConstants.PercussionChannel;
39786
40028
  if (!isPercussion) {
39787
40029
  this.instrumentPrograms.add(programChange.program);
@@ -39793,6 +40035,9 @@ class MidiFileSequencer {
39793
40035
  if (isPercussion) {
39794
40036
  this.percussionKeys.add(noteOn.noteKey);
39795
40037
  }
40038
+ if (noteOn.channel > maxChannel) {
40039
+ maxChannel = noteOn.channel;
40040
+ }
39796
40041
  }
39797
40042
  }
39798
40043
  state.currentTempo = state.tempoChanges.length > 0 ? state.tempoChanges[0].bpm : bpm;
@@ -39808,6 +40053,7 @@ class MidiFileSequencer {
39808
40053
  });
39809
40054
  state.endTime = absTime;
39810
40055
  state.endTick = absTick;
40056
+ state.metronomeChannel = maxChannel + 1;
39811
40057
  return state;
39812
40058
  }
39813
40059
  fillMidiEventQueue() {
@@ -44013,6 +44259,11 @@ class Voice {
44013
44259
  if (dynamicGain) {
44014
44260
  noteGain = SynthHelper.decibelsToGain(this.noteGainDb + this.modLfo.level * tmpModLfoToVolume);
44015
44261
  }
44262
+ // Update EG.
44263
+ this.ampEnv.process(blockSamples, f.outSampleRate);
44264
+ if (updateModEnv) {
44265
+ this.modEnv.process(blockSamples, f.outSampleRate);
44266
+ }
44016
44267
  gainMono = noteGain * this.ampEnv.level;
44017
44268
  if (isMuted) {
44018
44269
  gainMono = 0;
@@ -44020,11 +44271,6 @@ class Voice {
44020
44271
  else {
44021
44272
  gainMono *= this.mixVolume;
44022
44273
  }
44023
- // Update EG.
44024
- this.ampEnv.process(blockSamples, f.outSampleRate);
44025
- if (updateModEnv) {
44026
- this.modEnv.process(blockSamples, f.outSampleRate);
44027
- }
44028
44274
  // Update LFOs.
44029
44275
  if (updateModLFO) {
44030
44276
  this.modLfo.process(blockSamples);
@@ -44103,7 +44349,12 @@ class Voice {
44103
44349
  }
44104
44350
  break;
44105
44351
  }
44106
- if (tmpSourceSamplePosition >= tmpSampleEndDbl || this.ampEnv.segment === VoiceEnvelopeSegment.Done) {
44352
+ const inaudible = this.ampEnv.segment === VoiceEnvelopeSegment.Release &&
44353
+ Math.abs(gainMono) < SynthConstants.AudibleLevelThreshold;
44354
+ if (tmpSourceSamplePosition >= tmpSampleEndDbl ||
44355
+ this.ampEnv.segment === VoiceEnvelopeSegment.Done ||
44356
+ // Check if voice is inaudible during release to terminate early
44357
+ inaudible) {
44107
44358
  this.kill();
44108
44359
  return;
44109
44360
  }
@@ -44118,75 +44369,6 @@ class Voice {
44118
44369
  }
44119
44370
  }
44120
44371
 
44121
- /**
44122
- * @internal
44123
- */
44124
- class QueueItem {
44125
- value;
44126
- next;
44127
- constructor(value) {
44128
- this.value = value;
44129
- }
44130
- }
44131
- /**
44132
- * @internal
44133
- */
44134
- class Queue {
44135
- _head;
44136
- _tail;
44137
- get isEmpty() {
44138
- return this._head === undefined;
44139
- }
44140
- clear() {
44141
- this._head = undefined;
44142
- this._tail = undefined;
44143
- }
44144
- enqueue(item) {
44145
- const queueItem = new QueueItem(item);
44146
- if (this._tail) {
44147
- // not empty -> add after tail
44148
- this._tail.next = queueItem;
44149
- this._tail = queueItem;
44150
- }
44151
- else {
44152
- // empty -> new item takes head and tail
44153
- this._head = queueItem;
44154
- this._tail = queueItem;
44155
- }
44156
- }
44157
- enqueueFront(item) {
44158
- const queueItem = new QueueItem(item);
44159
- queueItem.next = this._head;
44160
- if (this._head) {
44161
- this._head = queueItem;
44162
- }
44163
- else {
44164
- this._head = queueItem;
44165
- this._tail = queueItem;
44166
- }
44167
- }
44168
- peek() {
44169
- const head = this._head;
44170
- if (!head) {
44171
- return undefined;
44172
- }
44173
- return head.value;
44174
- }
44175
- dequeue() {
44176
- const head = this._head;
44177
- if (!head) {
44178
- return undefined;
44179
- }
44180
- const newHead = head.next;
44181
- this._head = newHead;
44182
- // last item removed?
44183
- if (!newHead) {
44184
- this._tail = undefined;
44185
- }
44186
- return head.value;
44187
- }
44188
- }
44189
-
44190
44372
  /**
44191
44373
  * Lists all midi controllers.
44192
44374
  * @public
@@ -44343,6 +44525,7 @@ class TinySoundFont {
44343
44525
  currentTempo = 0;
44344
44526
  timeSignatureNumerator = 0;
44345
44527
  timeSignatureDenominator = 0;
44528
+ _metronomeChannel = SynthConstants.DefaultChannelCount - 1;
44346
44529
  constructor(sampleRate) {
44347
44530
  this.outSampleRate = sampleRate;
44348
44531
  }
@@ -44447,8 +44630,8 @@ class TinySoundFont {
44447
44630
  while (!this._midiEventQueue.isEmpty) {
44448
44631
  const m = this._midiEventQueue.dequeue();
44449
44632
  if (m.isMetronome && this.metronomeVolume > 0) {
44450
- this.channelNoteOff(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey);
44451
- this.channelNoteOn(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey, 95 / 127);
44633
+ this.channelNoteOff(this._metronomeChannel, SynthConstants.MetronomeKey);
44634
+ this.channelNoteOn(this._metronomeChannel, SynthConstants.MetronomeKey, 95 / 127);
44452
44635
  }
44453
44636
  else if (m.event) {
44454
44637
  this.processMidiMessage(m.event);
@@ -44462,7 +44645,7 @@ class TinySoundFont {
44462
44645
  // channel is muted if it is either explicitley muted, or another channel is set to solo but not this one.
44463
44646
  // exception. metronome is implicitly added in solo
44464
44647
  const isChannelMuted = this._mutedChannels.has(channel) ||
44465
- (anySolo && channel !== SynthConstants.MetronomeChannel && !this._soloChannels.has(channel));
44648
+ (anySolo && channel !== this._metronomeChannel && !this._soloChannels.has(channel));
44466
44649
  if (!buffer) {
44467
44650
  voice.kill();
44468
44651
  }
@@ -44516,16 +44699,17 @@ class TinySoundFont {
44516
44699
  }
44517
44700
  }
44518
44701
  get metronomeVolume() {
44519
- return this.channelGetMixVolume(SynthConstants.MetronomeChannel);
44702
+ return this.channelGetMixVolume(this._metronomeChannel);
44520
44703
  }
44521
44704
  set metronomeVolume(value) {
44522
- this.setupMetronomeChannel(value);
44705
+ this.setupMetronomeChannel(this._metronomeChannel, value);
44523
44706
  }
44524
- setupMetronomeChannel(volume) {
44525
- this.channelSetMixVolume(SynthConstants.MetronomeChannel, volume);
44707
+ setupMetronomeChannel(channel, volume) {
44708
+ this._metronomeChannel = channel;
44709
+ this.channelSetMixVolume(channel, volume);
44526
44710
  if (volume > 0) {
44527
- this.channelSetVolume(SynthConstants.MetronomeChannel, 1);
44528
- this.channelSetPresetNumber(SynthConstants.MetronomeChannel, 0, true);
44711
+ this.channelSetVolume(channel, 1);
44712
+ this.channelSetPresetNumber(channel, 0, true);
44529
44713
  }
44530
44714
  }
44531
44715
  get masterVolume() {
@@ -45605,95 +45789,6 @@ class TinySoundFont {
45605
45789
  }
45606
45790
  }
45607
45791
 
45608
- /**
45609
- * The options controlling how to export the audio.
45610
- * @public
45611
- */
45612
- class AudioExportOptions {
45613
- /**
45614
- * The soundfonts to load and use for generating the audio.
45615
- * If not provided, the already loaded soundfonts of the synthesizer will be used.
45616
- * If no existing synthesizer is initialized, the generated audio might not contain any hearable audio.
45617
- */
45618
- soundFonts;
45619
- /**
45620
- * The output sample rate.
45621
- * @default `44100`
45622
- */
45623
- sampleRate = 44100;
45624
- /**
45625
- * Whether to respect sync point information during export.
45626
- * @default `true`
45627
- * @remarks
45628
- * If the song contains sync point information for synchronization with an external media,
45629
- * this option allows controlling whether the synthesized audio is aligned with these points.
45630
- *
45631
- * This is useful when mixing the exported audio together with external media, keeping the same timing.
45632
- *
45633
- * Disable this option if you want the original/exact timing as per music sheet in the exported audio.
45634
- */
45635
- useSyncPoints = false;
45636
- /**
45637
- * The current master volume as percentage. (range: 0.0-3.0, default 1.0)
45638
- */
45639
- masterVolume = 1;
45640
- /**
45641
- * The metronome volume. (range: 0.0-3.0, default 0.0)
45642
- */
45643
- metronomeVolume = 0;
45644
- /**
45645
- * The range of the song that should be exported. Set this to null
45646
- * to play the whole song.
45647
- */
45648
- playbackRange;
45649
- /**
45650
- * The volume for individual tracks as percentage (range: 0.0-3.0).
45651
- * @remarks
45652
- * The key is the track index, and the value is the relative volume.
45653
- * The configured volume (as per data model) still applies, this is an additional volume control.
45654
- * If no custom value is set, 100% is used.
45655
- * No values from the currently active synthesizer are applied.
45656
- *
45657
- * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
45658
- */
45659
- trackVolume = new Map();
45660
- /**
45661
- * The additional semitone pitch transpose to apply for individual tracks.
45662
- * @remarks
45663
- * The key is the track index, and the value is the number of semitones to apply.
45664
- * No values from the currently active synthesizer are applied.
45665
- *
45666
- * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
45667
- */
45668
- trackTranspositionPitches = new Map();
45669
- }
45670
- /**
45671
- * Represents a single chunk of audio produced.
45672
- * @public
45673
- */
45674
- class AudioExportChunk {
45675
- /**
45676
- * The generated samples for the requested chunk.
45677
- */
45678
- samples;
45679
- /**
45680
- * The current time position within the song in milliseconds.
45681
- */
45682
- currentTime = 0;
45683
- /**
45684
- * The total length of the song in milliseconds.
45685
- */
45686
- endTime = 0;
45687
- /**
45688
- * The current time position within the song in midi ticks.
45689
- */
45690
- currentTick = 0;
45691
- /**
45692
- * The total length of the song in midi ticks.
45693
- */
45694
- endTick = 0;
45695
- }
45696
-
45697
45792
  /**
45698
45793
  * This is the base class for synthesizer components which can be used to
45699
45794
  * play a {@link MidiFile} via a {@link ISynthOutput}.
@@ -45902,6 +45997,17 @@ class AlphaSynthBase {
45902
45997
  }
45903
45998
  this._notPlayedSamples += samples.length;
45904
45999
  this.output.addSamples(samples);
46000
+ // if the sequencer finished, we instantly force a noteOff on all
46001
+ // voices to complete playback and stop voices fast.
46002
+ // Doing this in the samplePlayed callback is too late as we might
46003
+ // continue generating audio for long-release notes (especially percussion like cymbals)
46004
+ // we still have checkForFinish which takes care of the counterpart
46005
+ // on the sample played area to ensure we seek back.
46006
+ // but thanks to this code we ensure the output will complete fast as we won't
46007
+ // be adding more samples beside a 0.1s ramp-down
46008
+ if (this.sequencer.isFinished) {
46009
+ this.synthesizer.noteOffAll(true);
46010
+ }
45905
46011
  }
45906
46012
  else {
45907
46013
  // Tell output that there is no data left for it.
@@ -45918,7 +46024,7 @@ class AlphaSynthBase {
45918
46024
  if (this._countInVolume > 0) {
45919
46025
  Logger.debug('AlphaSynth', 'Starting countin');
45920
46026
  this.sequencer.startCountIn();
45921
- this.synthesizer.setupMetronomeChannel(this._countInVolume);
46027
+ this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this._countInVolume);
45922
46028
  this.updateTimePosition(0, true);
45923
46029
  }
45924
46030
  this.output.play();
@@ -45930,7 +46036,7 @@ class AlphaSynthBase {
45930
46036
  this._stopOneTimeMidi();
45931
46037
  }
45932
46038
  Logger.debug('AlphaSynth', 'Starting playback');
45933
- this.synthesizer.setupMetronomeChannel(this.metronomeVolume);
46039
+ this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume);
45934
46040
  this._synthStopping = false;
45935
46041
  this.state = PlayerState.Playing;
45936
46042
  this.stateChanged.trigger(new PlayerStateChangedEventArgs(this.state, false));
@@ -46014,7 +46120,7 @@ class AlphaSynthBase {
46014
46120
  }
46015
46121
  _checkReadyForPlayback() {
46016
46122
  if (this.isReadyForPlayback) {
46017
- this.synthesizer.setupMetronomeChannel(this.metronomeVolume);
46123
+ this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume);
46018
46124
  const programs = this.sequencer.instrumentPrograms;
46019
46125
  const percussionKeys = this.sequencer.percussionKeys;
46020
46126
  let append = false;
@@ -46355,7 +46461,7 @@ class AlphaSynthAudioExporter {
46355
46461
  _generatedAudioCurrentTime = 0;
46356
46462
  _generatedAudioEndTime = 0;
46357
46463
  setup() {
46358
- this._synth.setupMetronomeChannel(this._synth.metronomeVolume);
46464
+ this._synth.setupMetronomeChannel(this._sequencer.metronomeChannel, this._synth.metronomeVolume);
46359
46465
  const syncPoints = this._sequencer.currentSyncPoints;
46360
46466
  const alphaTabEndTime = this._sequencer.currentEndTime;
46361
46467
  if (syncPoints.length === 0) {
@@ -46437,7 +46543,7 @@ class BackingTrackAudioSynthesizer {
46437
46543
  }
46438
46544
  loadPresets(_hydra, _instrumentPrograms, _percussionKeys, _append) {
46439
46545
  }
46440
- setupMetronomeChannel(_metronomeVolume) {
46546
+ setupMetronomeChannel(_metronomeChannel, _metronomeVolume) {
46441
46547
  }
46442
46548
  synthesizeSilent(_sampleCount) {
46443
46549
  this.fakeSynthesize();
@@ -46804,7 +46910,15 @@ class ScoreRenderer {
46804
46910
  Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null);
46805
46911
  return;
46806
46912
  }
46807
- this.boundsLookup = new BoundsLookup();
46913
+ // For partial renders we preserve the existing lookup so bars outside the re-layouted
46914
+ // range keep their already-scaled bounds - the layout will clear the changed range
46915
+ // before the paint pass re-registers fresh entries for it.
46916
+ if (renderHints?.firstChangedMasterBar !== undefined && this.boundsLookup) {
46917
+ this.boundsLookup.resetForPartialUpdate();
46918
+ }
46919
+ else {
46920
+ this.boundsLookup = new BoundsLookup();
46921
+ }
46808
46922
  this._recreateCanvas();
46809
46923
  this.canvas.lineWidth = 1;
46810
46924
  this.canvas.settings = this.settings;
@@ -47871,6 +47985,7 @@ class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
47871
47985
  _bufferTimeInMilliseconds = 0;
47872
47986
  _settings;
47873
47987
  _boundHandleMessage;
47988
+ _pendingEvents;
47874
47989
  constructor(settings) {
47875
47990
  super();
47876
47991
  this._settings = settings;
@@ -47898,6 +48013,13 @@ class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
47898
48013
  this.source.connect(this._worklet);
47899
48014
  this.source.start(0);
47900
48015
  this._worklet.connect(ctx.destination);
48016
+ const pending = this._pendingEvents;
48017
+ if (pending) {
48018
+ for (const e of pending) {
48019
+ this._worklet.port.postMessage(e);
48020
+ }
48021
+ this._pendingEvents = undefined;
48022
+ }
47901
48023
  }, (reason) => {
47902
48024
  Logger.error('WebAudio', `Audio Worklet creation failed: reason=${reason}`);
47903
48025
  });
@@ -47924,15 +48046,26 @@ class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
47924
48046
  this._worklet.disconnect();
47925
48047
  }
47926
48048
  this._worklet = null;
48049
+ this._pendingEvents = undefined;
48050
+ }
48051
+ _postWorkerMessage(message) {
48052
+ const worklet = this._worklet;
48053
+ if (worklet) {
48054
+ worklet.port.postMessage(message);
48055
+ }
48056
+ else {
48057
+ this._pendingEvents ??= [];
48058
+ this._pendingEvents.push(message);
48059
+ }
47927
48060
  }
47928
48061
  addSamples(f) {
47929
- this._worklet?.port.postMessage({
48062
+ this._postWorkerMessage({
47930
48063
  cmd: 'alphaSynth.output.addSamples',
47931
48064
  samples: Environment.prepareForPostMessage(f)
47932
48065
  });
47933
48066
  }
47934
48067
  resetSamples() {
47935
- this._worklet?.port.postMessage({
48068
+ this._postWorkerMessage({
47936
48069
  cmd: 'alphaSynth.output.resetSamples'
47937
48070
  });
47938
48071
  }
@@ -48659,9 +48792,6 @@ class BeatTickLookup {
48659
48792
  * @param beat The beat to add.
48660
48793
  */
48661
48794
  highlightBeat(beat, playbackStart) {
48662
- if (beat.isEmpty && !beat.voice.isEmpty) {
48663
- return;
48664
- }
48665
48795
  if (!this._highlightedBeats.has(beat.id)) {
48666
48796
  this._highlightedBeats.set(beat.id, true);
48667
48797
  this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart));
@@ -50129,7 +50259,13 @@ class MidiFileGenerator {
50129
50259
  let beatStart = beat.playbackStart;
50130
50260
  let audioDuration = beat.playbackDuration;
50131
50261
  const masterBarDuration = beat.voice.bar.masterBar.calculateDuration();
50132
- if (beat.voice.bar.isEmpty) {
50262
+ // For a bar whose voice contains a single empty beat (the typical "whole-bar rest"
50263
+ // placeholder inserted during score.finish), extend the beat's audio duration to cover
50264
+ // the full bar so cursor navigation has a beat to follow across the whole bar. Don't
50265
+ // apply this when the voice has multiple beats: those represent explicit rhythmic
50266
+ // subdivisions even when each beat is empty (e.g. a recording grid of placeholder
50267
+ // slots), and overriding would make every beat overlap the whole bar.
50268
+ if (beat.voice.bar.isEmpty && beat.voice.beats.length === 1) {
50133
50269
  audioDuration = masterBarDuration;
50134
50270
  }
50135
50271
  else if (beat.voice.bar.masterBar.tripletFeel !== TripletFeel.NoTripletFeel &&
@@ -54750,7 +54886,8 @@ class AlphaTabApiBase {
54750
54886
  this._isInitialBeatCursorUpdate ||
54751
54887
  barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
54752
54888
  startBeatX < previousBeatBounds.onNotesX ||
54753
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
54889
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 ||
54890
+ barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h;
54754
54891
  if (jumpCursor) {
54755
54892
  cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
54756
54893
  }
@@ -55805,6 +55942,9 @@ class AlphaTabApiBase {
55805
55942
  this._beatVisibilityChecker.bounds = this.boundsLookup;
55806
55943
  this._currentBeat = null;
55807
55944
  this._cursorUpdateTick(this._previousTick, false, 1, true, true);
55945
+ if (this._selectionStart) {
55946
+ this.highlightPlaybackRange(this._selectionStart.beat, this._selectionEnd.beat);
55947
+ }
55808
55948
  this.postRenderFinished.trigger();
55809
55949
  this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null);
55810
55950
  }
@@ -57987,8 +58127,9 @@ class AlphaTabWebWorker {
57987
58127
  break;
57988
58128
  case 'alphaTab.renderScore':
57989
58129
  this._updateFontSizes(data.fontSizes);
58130
+ const renderHints = data.renderHints;
57990
58131
  const score = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings);
57991
- this._renderMultiple(score, data.trackIndexes);
58132
+ this._renderMultiple(score, data.trackIndexes, renderHints);
57992
58133
  break;
57993
58134
  case 'alphaTab.updateSettings':
57994
58135
  this._updateSettings(data.settings);
@@ -62861,6 +63002,15 @@ class BarRendererBase {
62861
63002
  }
62862
63003
  return false;
62863
63004
  }
63005
+ /**
63006
+ * The fixed-overhead width of this renderer: glyphs that do not stretch when
63007
+ * the bar is scaled (clef, key signature, time signature, barlines, courtesy
63008
+ * accidentals, etc). Treated as a fixed allocation by the system-level layout
63009
+ * before distributing remaining width across bars by {@link Bar.displayScale}.
63010
+ */
63011
+ get fixedOverhead() {
63012
+ return this._preBeatGlyphs.width + this._postBeatGlyphs.width;
63013
+ }
62864
63014
  scaleToWidth(width) {
62865
63015
  // preBeat and postBeat glyphs do not get resized
62866
63016
  const containerWidth = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width;
@@ -65419,6 +65569,22 @@ class BarLayoutingInfo {
65419
65569
  postBeatSize = 0;
65420
65570
  minStretchForce = 0;
65421
65571
  totalSpringConstant = 0;
65572
+ /**
65573
+ * The smallest note duration encountered within this bar's springs, used as the reference in
65574
+ * the Gourlay stretch formula. Read by the owning {@link StaffSystem} so that the system can
65575
+ * aggregate a shared minimum across all bars and trigger a reconcile if an added bar introduces
65576
+ * a shorter duration than previously seen.
65577
+ */
65578
+ get localMinDuration() {
65579
+ return this._minDuration;
65580
+ }
65581
+ /**
65582
+ * The minimum-duration reference against which the spring constants currently held by this info
65583
+ * were computed. Set by {@link finish} and {@link recomputeSpringConstants}. The owning
65584
+ * StaffSystem compares this against its system-wide minimum to decide whether spring constants
65585
+ * need re-derivation.
65586
+ */
65587
+ computedWithMinDuration = 0;
65422
65588
  _updateMinStretchForce(force) {
65423
65589
  if (this.minStretchForce < force) {
65424
65590
  this.minStretchForce = force;
@@ -65588,10 +65754,26 @@ class BarLayoutingInfo {
65588
65754
  this._incompleteGraceRodsWidth += sp.preBeatWidth + sp.postSpringWidth;
65589
65755
  }
65590
65756
  }
65591
- this._calculateSpringConstants();
65757
+ this._calculateSpringConstants(this._minDuration);
65758
+ this.computedWithMinDuration = this._minDuration;
65592
65759
  this.version++;
65593
65760
  }
65594
- _calculateSpringConstants() {
65761
+ /**
65762
+ * Re-derives the spring constants (and {@link minStretchForce} / {@link totalSpringConstant})
65763
+ * using a caller-supplied minimum-duration reference rather than this bar's local minimum.
65764
+ *
65765
+ * Called by {@link StaffSystem.reconcileMinDurationIfDirty} when a bar added later to the
65766
+ * system introduced a shorter note than previously seen, invalidating this bar's spring
65767
+ * constants. Grace-rod data is not recomputed — it is independent of the minimum-duration
65768
+ * reference. The internal {@link version} is bumped so downstream consumers (e.g.
65769
+ * {@link BarRendererBase.applyLayoutingInfo}) pick up the refreshed positions.
65770
+ */
65771
+ recomputeSpringConstants(minDuration) {
65772
+ this._calculateSpringConstants(minDuration);
65773
+ this.computedWithMinDuration = minDuration;
65774
+ this.version++;
65775
+ }
65776
+ _calculateSpringConstants(minDuration) {
65595
65777
  let totalSpringConstant = 0;
65596
65778
  const sortedSprings = this._timeSortedSprings;
65597
65779
  if (sortedSprings.length === 0) {
@@ -65609,7 +65791,7 @@ class BarLayoutingInfo {
65609
65791
  const nextSpring = sortedSprings[i + 1];
65610
65792
  duration = Math.abs(nextSpring.timePosition - currentSpring.timePosition);
65611
65793
  }
65612
- currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration);
65794
+ currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration, minDuration);
65613
65795
  totalSpringConstant += 1 / currentSpring.springConstant;
65614
65796
  }
65615
65797
  this.totalSpringConstant = 1 / totalSpringConstant;
@@ -65669,7 +65851,7 @@ class BarLayoutingInfo {
65669
65851
  // springX += this.calculateWidth(force, spring.springConstant);
65670
65852
  // }
65671
65853
  // }
65672
- _calculateSpringConstant(spring, duration) {
65854
+ _calculateSpringConstant(spring, duration, minDuration) {
65673
65855
  if (duration <= 0) {
65674
65856
  duration = MidiUtils.toTicks(Duration.TwoHundredFiftySixth);
65675
65857
  }
@@ -65677,7 +65859,6 @@ class BarLayoutingInfo {
65677
65859
  spring.smallestDuration = duration;
65678
65860
  }
65679
65861
  const smallestDuration = spring.smallestDuration;
65680
- const minDuration = this._minDuration;
65681
65862
  const minDurationWidth = BarLayoutingInfo._defaultMinDurationWidth;
65682
65863
  const phi = 1 + 0.85 * Math.log2(duration / minDuration);
65683
65864
  return (smallestDuration / duration) * (1 / (phi * minDurationWidth));
@@ -65740,6 +65921,18 @@ class MasterBarsRenderers {
65740
65921
  canWrap = true;
65741
65922
  masterBar;
65742
65923
  additionalMultiBarRestIndexes = null;
65924
+ /**
65925
+ * Max fixed overhead (prefix + postfix glyph width) across all staves of this bar.
65926
+ * Used by the layout-mode horizontal scaling pass to carve out the fixed-overhead bucket
65927
+ * before distributing staff width across bars.
65928
+ */
65929
+ maxFixedOverhead = 0;
65930
+ /**
65931
+ * Max natural content width (computedWidth - fixedOverhead) across all staves of this bar.
65932
+ * Used as the bar weight when the layout ignores {@link MasterBar.displayScale} (e.g.
65933
+ * Page layout with `SystemsLayoutMode.Automatic`).
65934
+ */
65935
+ maxContentWidth = 0;
65743
65936
  get lastMasterBarIndex() {
65744
65937
  if (this.additionalMultiBarRestIndexes) {
65745
65938
  return this.additionalMultiBarRestIndexes[this.additionalMultiBarRestIndexes.length - 1];
@@ -65921,6 +66114,45 @@ class StaffSystem {
65921
66114
  * This value is mainly used in the parchment style layout for correct scaling of the bars.
65922
66115
  */
65923
66116
  totalBarDisplayScale = 0;
66117
+ /**
66118
+ * Sum of per-bar {@link MasterBarsRenderers.maxFixedOverhead} across the system. The layout-mode
66119
+ * horizontal scaling pass subtracts this from the available staff width before distributing the
66120
+ * remainder across bars.
66121
+ */
66122
+ totalFixedOverhead = 0;
66123
+ /**
66124
+ * Sum of per-bar {@link MasterBarsRenderers.maxContentWidth} across the system. Used as the
66125
+ * denominator when distributing staff width in modes that weight bars by natural content width
66126
+ * (Page layout with `SystemsLayoutMode.Automatic`).
66127
+ */
66128
+ totalContentWidth = 0;
66129
+ /**
66130
+ * Shortest note duration (in ticks) across every bar that has been added to this system, used
66131
+ * as the common reference in the Gourlay stretch formula so that rhythmically-equivalent beats
66132
+ * in different bars of the same system align column-wise.
66133
+ *
66134
+ * `-1` means "no bar added yet". The value only moves downward during system assembly; when a
66135
+ * new bar introduces a shorter minimum, {@link isMinDurationDirty} is set so that
66136
+ * {@link reconcileMinDurationIfDirty} can re-derive spring constants on the previously-added
66137
+ * bars before layout distribution runs.
66138
+ */
66139
+ minDuration = -1;
66140
+ /**
66141
+ * Set when a bar added to this system introduced a shorter {@link minDuration} than previously
66142
+ * seen, leaving earlier bars' spring constants stale. Consumed by
66143
+ * {@link reconcileMinDurationIfDirty} which is called from `VerticalLayoutBase._fitSystem`
66144
+ * once the system is fully assembled.
66145
+ */
66146
+ isMinDurationDirty = false;
66147
+ /**
66148
+ * Whether this system coordinates a shared minimum-duration reference across its bars for the
66149
+ * Gourlay stretch formula. Defaults to `true` for page-style and parchment layouts where bars
66150
+ * of a system fight for a common staff width. Set to `false` for horizontal layouts where each
66151
+ * bar is sized independently (by `bar.displayWidth` or its intrinsic width) and there is no
66152
+ * column-alignment concern - each bar keeps its local minimum so pre-existing rendering is
66153
+ * preserved.
66154
+ */
66155
+ shareMinDurationAcrossBars = true;
65924
66156
  isLast = false;
65925
66157
  masterBarsRenderers = [];
65926
66158
  staves = [];
@@ -65982,6 +66214,9 @@ class StaffSystem {
65982
66214
  }
65983
66215
  this.firstVisibleStaff = firstVisibleStaff;
65984
66216
  this._calculateAccoladeSpacing(tracks);
66217
+ // On the resize path the layoutingInfo was finalized in a previous layout pass, so we
66218
+ // only need to check whether its min-duration reference still matches the new system's.
66219
+ this._trackSystemMinDuration(renderers.layoutingInfo);
65985
66220
  this._applyLayoutAndUpdateWidth();
65986
66221
  return renderers;
65987
66222
  }
@@ -66037,10 +66272,89 @@ class StaffSystem {
66037
66272
  this.firstVisibleStaff = firstVisibleStaff;
66038
66273
  this._calculateAccoladeSpacing(tracks);
66039
66274
  barLayoutingInfo.finish();
66275
+ // Reconcile against the system-wide minimum-duration reference now that springs are
66276
+ // finalized. If this bar introduced a shorter note, earlier bars become stale (flagged
66277
+ // for bulk reconcile at fit time). If the system already had a shorter min than this
66278
+ // bar's local one, this bar's spring constants are recomputed immediately so the width
66279
+ // we return below reflects the shared reference.
66280
+ this._trackSystemMinDuration(barLayoutingInfo);
66040
66281
  // ensure same widths of new renderer
66041
66282
  result.width = this._applyLayoutAndUpdateWidth();
66042
66283
  return result;
66043
66284
  }
66285
+ /**
66286
+ * Updates {@link minDuration} and {@link isMinDurationDirty} when a bar is added, and brings
66287
+ * the just-added bar's {@link BarLayoutingInfo} in line with the current system minimum if the
66288
+ * system already saw a shorter reference. The bulk reconcile over previously-added bars is
66289
+ * deferred to {@link reconcileMinDurationIfDirty} (called from `_fitSystem`) to avoid
66290
+ * re-iterating the system every time a bar is appended.
66291
+ */
66292
+ _trackSystemMinDuration(info) {
66293
+ if (!this.shareMinDurationAcrossBars) {
66294
+ return;
66295
+ }
66296
+ const localMin = info.localMinDuration;
66297
+ if (this.minDuration === -1 || localMin < this.minDuration) {
66298
+ // this bar shortens the system minimum; earlier bars (if any) are now stale
66299
+ if (this.masterBarsRenderers.length > 1 && localMin !== this.minDuration) {
66300
+ this.isMinDurationDirty = true;
66301
+ }
66302
+ this.minDuration = localMin;
66303
+ }
66304
+ if (info.computedWithMinDuration > this.minDuration) {
66305
+ // this bar was initialized against a larger (local) min than the system carries; pull
66306
+ // it down to the system reference so its computedWidth reflects the shared spacing.
66307
+ info.recomputeSpringConstants(this.minDuration);
66308
+ }
66309
+ }
66310
+ /**
66311
+ * Re-derives spring constants on bars whose {@link BarLayoutingInfo.computedWithMinDuration}
66312
+ * is out of sync with the current {@link minDuration}, and rebuilds the cached system totals
66313
+ * (widths, {@link totalFixedOverhead}, {@link totalContentWidth}) from the refreshed bar
66314
+ * widths. Called from `VerticalLayoutBase._fitSystem` after the system is fully assembled and
66315
+ * before distribution runs. No-op when {@link isMinDurationDirty} is false.
66316
+ */
66317
+ reconcileMinDurationIfDirty() {
66318
+ if (!this.isMinDurationDirty) {
66319
+ return;
66320
+ }
66321
+ let systemWidth = this.accoladeWidth;
66322
+ let totalFixedOverhead = 0;
66323
+ let totalContentWidth = 0;
66324
+ for (const mb of this.masterBarsRenderers) {
66325
+ if (mb.layoutingInfo.computedWithMinDuration > this.minDuration) {
66326
+ mb.layoutingInfo.recomputeSpringConstants(this.minDuration);
66327
+ }
66328
+ let maxPrefix = 0;
66329
+ let maxContent = 0;
66330
+ let realWidth = 0;
66331
+ for (const r of mb.renderers) {
66332
+ r.applyLayoutingInfo();
66333
+ if (r.computedWidth > realWidth) {
66334
+ realWidth = r.computedWidth;
66335
+ }
66336
+ const overhead = r.fixedOverhead;
66337
+ if (overhead > maxPrefix) {
66338
+ maxPrefix = overhead;
66339
+ }
66340
+ const content = Math.max(0, r.computedWidth - overhead);
66341
+ if (content > maxContent) {
66342
+ maxContent = content;
66343
+ }
66344
+ }
66345
+ mb.maxFixedOverhead = maxPrefix;
66346
+ mb.maxContentWidth = maxContent;
66347
+ mb.width = realWidth;
66348
+ systemWidth += realWidth;
66349
+ totalFixedOverhead += maxPrefix;
66350
+ totalContentWidth += maxContent;
66351
+ }
66352
+ this.width = systemWidth;
66353
+ this.computedWidth = systemWidth;
66354
+ this.totalFixedOverhead = totalFixedOverhead;
66355
+ this.totalContentWidth = totalContentWidth;
66356
+ this.isMinDurationDirty = false;
66357
+ }
66044
66358
  getBarDisplayScale(renderer) {
66045
66359
  return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale;
66046
66360
  }
@@ -66079,12 +66393,16 @@ class StaffSystem {
66079
66393
  this.width -= width;
66080
66394
  this.computedWidth -= width;
66081
66395
  this.totalBarDisplayScale -= barDisplayScale;
66396
+ this.totalFixedOverhead -= toRemove.maxFixedOverhead;
66397
+ this.totalContentWidth -= toRemove.maxContentWidth;
66082
66398
  return toRemove;
66083
66399
  }
66084
66400
  return null;
66085
66401
  }
66086
66402
  _applyLayoutAndUpdateWidth() {
66087
66403
  let realWidth = 0;
66404
+ let maxFixedOverhead = 0;
66405
+ let maxContentWidth = 0;
66088
66406
  let barDisplayScale = 0;
66089
66407
  for (const s of this.allStaves) {
66090
66408
  const last = s.barRenderers[s.barRenderers.length - 1];
@@ -66093,8 +66411,21 @@ class StaffSystem {
66093
66411
  if (last.computedWidth > realWidth) {
66094
66412
  realWidth = last.computedWidth;
66095
66413
  }
66414
+ const overhead = last.fixedOverhead;
66415
+ if (overhead > maxFixedOverhead) {
66416
+ maxFixedOverhead = overhead;
66417
+ }
66418
+ const content = Math.max(0, last.computedWidth - overhead);
66419
+ if (content > maxContentWidth) {
66420
+ maxContentWidth = content;
66421
+ }
66096
66422
  }
66423
+ const renderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1];
66424
+ renderers.maxFixedOverhead = maxFixedOverhead;
66425
+ renderers.maxContentWidth = maxContentWidth;
66097
66426
  this.totalBarDisplayScale += barDisplayScale;
66427
+ this.totalFixedOverhead += maxFixedOverhead;
66428
+ this.totalContentWidth += maxContentWidth;
66098
66429
  this.width += realWidth;
66099
66430
  this.computedWidth += realWidth;
66100
66431
  return realWidth;
@@ -66592,17 +66923,6 @@ class StaffSystem {
66592
66923
  }
66593
66924
  }
66594
66925
 
66595
- /**
66596
- * @internal
66597
- */
66598
- class LazyPartial {
66599
- args;
66600
- renderCallback;
66601
- constructor(args, renderCallback) {
66602
- this.args = args;
66603
- this.renderCallback = renderCallback;
66604
- }
66605
- }
66606
66926
  /**
66607
66927
  * This is the base class for creating new layouting engines for the score renderer.
66608
66928
  * @internal
@@ -66633,15 +66953,21 @@ class ScoreLayout {
66633
66953
  this.doResize();
66634
66954
  }
66635
66955
  layoutAndRender(renderHints) {
66636
- this._lazyPartials.clear();
66637
66956
  this.slurRegistry.clear();
66638
- this.beamingRuleLookups.clear();
66639
- this._barRendererLookup.clear();
66640
- this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66641
66957
  const score = this.renderer.score;
66642
66958
  this.firstBarIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.renderer.settings);
66643
66959
  this.lastBarIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.renderer.settings, this.firstBarIndex);
66644
66960
  this.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.renderer.tracks, this.firstBarIndex, this.lastBarIndex);
66961
+ const firstChangedMasterBar = renderHints?.firstChangedMasterBar;
66962
+ if (firstChangedMasterBar !== undefined) {
66963
+ if (this.doUpdateForBars(renderHints)) {
66964
+ return;
66965
+ }
66966
+ }
66967
+ this._lazyPartials.clear();
66968
+ this.beamingRuleLookups.clear();
66969
+ this._barRendererLookup.clear();
66970
+ this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66645
66971
  this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale);
66646
66972
  if (!this.pagePadding) {
66647
66973
  this.pagePadding = [0, 0, 0, 0];
@@ -66656,6 +66982,9 @@ class ScoreLayout {
66656
66982
  this.doLayoutAndRender(renderHints);
66657
66983
  }
66658
66984
  _lazyPartials = new Map();
66985
+ getExistingPartialArgs(id) {
66986
+ return this._lazyPartials.has(id) ? this._lazyPartials.get(id).args : undefined;
66987
+ }
66659
66988
  registerPartial(args, callback) {
66660
66989
  if (args.height === 0) {
66661
66990
  return;
@@ -66674,7 +67003,11 @@ class ScoreLayout {
66674
67003
  }
66675
67004
  else {
66676
67005
  // in case of lazy loading -> first register lazy, then notify
66677
- this._lazyPartials.set(args.id, new LazyPartial(args, callback));
67006
+ const partial = {
67007
+ args,
67008
+ renderCallback: callback
67009
+ };
67010
+ this._lazyPartials.set(args.id, partial);
66678
67011
  this.renderer.partialLayoutFinished.trigger(args);
66679
67012
  }
66680
67013
  }
@@ -66969,7 +67302,7 @@ class ScoreLayout {
66969
67302
  glyph.textAlign = TextAlign.Left;
66970
67303
  }
66971
67304
  }
66972
- layoutAndRenderAnnotation(y) {
67305
+ _layoutAndRenderAnnotation(y) {
66973
67306
  // attention, you are not allowed to remove change this notice within any version of this library without permission!
66974
67307
  const msg = 'rendered by alphaTab';
66975
67308
  const resources = this.renderer.settings.display.resources;
@@ -67033,6 +67366,12 @@ class HorizontalScreenLayout extends ScoreLayout {
67033
67366
  }
67034
67367
  doResize() {
67035
67368
  }
67369
+ doUpdateForBars(_renderHints) {
67370
+ // not supported yet, modifications likely cause anyhow full updates
67371
+ // as we do not optimize effect bands yet. with effect bands being more
67372
+ // isolated in bars we could try updating dynamically
67373
+ return false;
67374
+ }
67036
67375
  doLayoutAndRender(renderHints) {
67037
67376
  const score = this.renderer.score;
67038
67377
  let startIndex = this.renderer.settings.display.startBar;
@@ -67046,6 +67385,11 @@ class HorizontalScreenLayout extends ScoreLayout {
67046
67385
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
67047
67386
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
67048
67387
  this._system = this.createEmptyStaffSystem(0);
67388
+ // Each bar in horizontal layout is sized independently (by bar.displayWidth or the bar's
67389
+ // intrinsic width), so there is no shared staff width to distribute across bars. Keep each
67390
+ // bar's spring constants referenced against its own local minimum-duration so rendering
67391
+ // matches the historical per-bar behaviour.
67392
+ this._system.shareMinDurationAcrossBars = false;
67049
67393
  this._system.isLast = true;
67050
67394
  this._system.x = this.pagePadding[0];
67051
67395
  this._system.y = this.pagePadding[1];
@@ -67107,7 +67451,7 @@ class HorizontalScreenLayout extends ScoreLayout {
67107
67451
  currentBarIndex += partial.masterBars.length;
67108
67452
  }
67109
67453
  this.height = this.layoutAndRenderBottomScoreInfo(this.height);
67110
- this.height = this.layoutAndRenderAnnotation(this.height);
67454
+ this.height = this._layoutAndRenderAnnotation(this.height);
67111
67455
  this.height += this.pagePadding[3];
67112
67456
  this.height *= this.renderer.settings.display.scale;
67113
67457
  }
@@ -67174,11 +67518,16 @@ class VerticalLayoutBase extends ScoreLayout {
67174
67518
  _allMasterBarRenderers = [];
67175
67519
  _barsFromPreviousSystem = [];
67176
67520
  _reuseViewPort = false;
67521
+ _preSystemPartialIds = [];
67522
+ _systemPartialIds = [];
67177
67523
  doLayoutAndRender(renderHints) {
67178
67524
  let y = this.pagePadding[1];
67179
67525
  this.width = this.renderer.width;
67180
67526
  this._allMasterBarRenderers = [];
67527
+ this._preSystemPartialIds = [];
67528
+ this._systemPartialIds = [];
67181
67529
  this._reuseViewPort = renderHints?.reuseViewport ?? false;
67530
+ this._systems = [];
67182
67531
  //
67183
67532
  // 1. Score Info
67184
67533
  y = this._layoutAndRenderScoreInfo(y, -1);
@@ -67190,15 +67539,23 @@ class VerticalLayoutBase extends ScoreLayout {
67190
67539
  y = this._layoutAndRenderChordDiagrams(y, -1);
67191
67540
  //
67192
67541
  // 4. One result per StaffSystem
67193
- y = this._layoutAndRenderScore(y);
67542
+ y = this._layoutAndRenderScore(y, this.firstBarIndex);
67194
67543
  y = this.layoutAndRenderBottomScoreInfo(y);
67195
- y = this.layoutAndRenderAnnotation(y);
67544
+ y = this._layoutAndRenderAnnotation(y);
67196
67545
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67197
67546
  }
67198
67547
  registerPartial(args, callback) {
67199
67548
  args.reuseViewport = this._reuseViewPort;
67200
67549
  super.registerPartial(args, callback);
67201
67550
  }
67551
+ reregisterPartial(id) {
67552
+ const args = this.getExistingPartialArgs(id);
67553
+ if (!args) {
67554
+ return;
67555
+ }
67556
+ args.reuseViewport = this._reuseViewPort;
67557
+ this.renderer.partialLayoutFinished.trigger(args);
67558
+ }
67202
67559
  get supportsResize() {
67203
67560
  return true;
67204
67561
  }
@@ -67209,6 +67566,47 @@ class VerticalLayoutBase extends ScoreLayout {
67209
67566
  }
67210
67567
  return x;
67211
67568
  }
67569
+ doUpdateForBars(renderHints) {
67570
+ this._reuseViewPort = renderHints.reuseViewport ?? false;
67571
+ const firstModifiedMasterBar = renderHints.firstChangedMasterBar;
67572
+ // first update existing systems as needed
67573
+ const systemIndex = this._systems.findIndex(s => {
67574
+ const first = s.masterBarsRenderers[0].masterBar.index;
67575
+ const last = s.masterBarsRenderers[s.masterBarsRenderers.length - 1].masterBar.index;
67576
+ return first <= firstModifiedMasterBar && firstModifiedMasterBar <= last;
67577
+ });
67578
+ if (systemIndex === -1 || !this.renderer.settings.core.enableLazyLoading) {
67579
+ return false;
67580
+ }
67581
+ // Bars from the start of the re-layouted system onward will be re-registered during the
67582
+ // paint pass. Clear their old entries from the preserved BoundsLookup so registration
67583
+ // produces a clean, complete lookup after this render finishes.
67584
+ const firstRebuiltBarIndex = this._systems[systemIndex].masterBarsRenderers[0].masterBar.index;
67585
+ this.renderer.boundsLookup.clearFromMasterBar(firstRebuiltBarIndex);
67586
+ // for now we do a full relayout from the first modified masterbar
67587
+ // there is a lot of room for even more performant updates, but they come
67588
+ // at a risk that features break.
67589
+ // e.g. we could only shift systems where the content didn't change,
67590
+ // but we might still have ties/slurs which have to be updated.
67591
+ const removeSystems = this._systems.splice(systemIndex, this._systems.length - systemIndex);
67592
+ this._systemPartialIds.splice(systemIndex, this._systemPartialIds.length - systemIndex);
67593
+ const system = removeSystems[0];
67594
+ let y = system.y;
67595
+ const firstBarIndex = system.masterBarsRenderers[0].masterBar.index;
67596
+ // signal all partials which didn't change
67597
+ for (const preSystemPartial of this._preSystemPartialIds) {
67598
+ this.reregisterPartial(preSystemPartial);
67599
+ }
67600
+ for (let i = 0; i < systemIndex; i++) {
67601
+ this.reregisterPartial(this._systemPartialIds[i]);
67602
+ }
67603
+ // new partials for all other prats
67604
+ y = this._layoutAndRenderScore(y, firstBarIndex);
67605
+ y = this.layoutAndRenderBottomScoreInfo(y);
67606
+ y = this._layoutAndRenderAnnotation(y);
67607
+ this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67608
+ return true;
67609
+ }
67212
67610
  doResize() {
67213
67611
  let y = this.pagePadding[1];
67214
67612
  this.width = this.renderer.width;
@@ -67227,7 +67625,7 @@ class VerticalLayoutBase extends ScoreLayout {
67227
67625
  // 4. One result per StaffSystem
67228
67626
  y = this._resizeAndRenderScore(y, oldHeight);
67229
67627
  y = this.layoutAndRenderBottomScoreInfo(y);
67230
- y = this.layoutAndRenderAnnotation(y);
67628
+ y = this._layoutAndRenderAnnotation(y);
67231
67629
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67232
67630
  }
67233
67631
  _layoutAndRenderTunings(y, totalHeight = -1) {
@@ -67251,6 +67649,7 @@ class VerticalLayoutBase extends ScoreLayout {
67251
67649
  canvas.textAlign = TextAlign.Center;
67252
67650
  this.tuningGlyph.paint(0, 0, canvas);
67253
67651
  });
67652
+ this._preSystemPartialIds.push(e.id);
67254
67653
  return y + tuningHeight;
67255
67654
  }
67256
67655
  _layoutAndRenderChordDiagrams(y, totalHeight = -1) {
@@ -67274,6 +67673,7 @@ class VerticalLayoutBase extends ScoreLayout {
67274
67673
  canvas.textAlign = TextAlign.Center;
67275
67674
  this.chordDiagrams.paint(0, 0, canvas);
67276
67675
  });
67676
+ this._preSystemPartialIds.push(e.id);
67277
67677
  return y + diagramHeight;
67278
67678
  }
67279
67679
  _layoutAndRenderScoreInfo(y, totalHeight = -1) {
@@ -67316,12 +67716,14 @@ class VerticalLayoutBase extends ScoreLayout {
67316
67716
  g.paint(0, 0, canvas);
67317
67717
  }
67318
67718
  });
67719
+ this._preSystemPartialIds.push(e.id);
67319
67720
  }
67320
67721
  return y + infoHeight;
67321
67722
  }
67322
67723
  _resizeAndRenderScore(y, oldHeight) {
67323
67724
  // if we have a fixed number of bars per row, we only need to refit them.
67324
67725
  const barsPerRowActive = this.getBarsPerSystem(0) > 0;
67726
+ this._systemPartialIds = [];
67325
67727
  if (barsPerRowActive) {
67326
67728
  for (let i = 0; i < this._systems.length; i++) {
67327
67729
  const system = this._systems[i];
@@ -67384,11 +67786,9 @@ class VerticalLayoutBase extends ScoreLayout {
67384
67786
  }
67385
67787
  return y;
67386
67788
  }
67387
- _layoutAndRenderScore(y) {
67388
- const startIndex = this.firstBarIndex;
67789
+ _layoutAndRenderScore(y, startIndex) {
67389
67790
  let currentBarIndex = startIndex;
67390
67791
  const endBarIndex = this.lastBarIndex;
67391
- this._systems = [];
67392
67792
  while (currentBarIndex <= endBarIndex) {
67393
67793
  // create system and align set proper coordinates
67394
67794
  const system = this._createStaffSystem(currentBarIndex, endBarIndex);
@@ -67423,6 +67823,7 @@ class VerticalLayoutBase extends ScoreLayout {
67423
67823
  // since we use partial drawing
67424
67824
  system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas);
67425
67825
  });
67826
+ this._systemPartialIds.push(args.id);
67426
67827
  // calculate coordinates for next system
67427
67828
  return height;
67428
67829
  }
@@ -67430,6 +67831,10 @@ class VerticalLayoutBase extends ScoreLayout {
67430
67831
  * Realignes the bars in this line according to the available space
67431
67832
  */
67432
67833
  _fitSystem(system) {
67834
+ // If a bar added late in the assembly introduced a shorter note than earlier bars, the
67835
+ // earlier bars' spring constants (and the cached system widths / totals) are stale.
67836
+ // Reconcile now - it's a no-op when nothing changed.
67837
+ system.reconcileMinDurationIfDirty();
67433
67838
  if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) {
67434
67839
  this._scaleToWidth(system, this._maxWidth);
67435
67840
  }
@@ -67441,29 +67846,35 @@ class VerticalLayoutBase extends ScoreLayout {
67441
67846
  _scaleToWidth(system, width) {
67442
67847
  const staffWidth = width - system.accoladeWidth;
67443
67848
  const shouldApplyBarScale = this.shouldApplyBarScale;
67444
- const totalScale = system.totalBarDisplayScale;
67445
- // NOTE: it currently delivers best results if we evenly distribute the available space across bars
67446
- // scaling bars relatively to their computed width, rather causes distortions whenever bars have
67447
- // pre-beat glyphs.
67448
- // most precise scaling would come if we use the contents (voiceContainerGlyph) width as a calculation
67449
- // factor. but this would make the calculation additionally complex with not much gain.
67450
- const difference = width - system.computedWidth;
67451
- const spacePerBar = difference / system.masterBarsRenderers.length;
67849
+ // Industry fixed-overhead model (Behind Bars, Dorico, Finale, Sibelius, MuseScore, Guitar Pro):
67850
+ // prefix/postfix glyphs (clef, key sig, time sig, barlines) are treated as fixed overhead and the
67851
+ // remaining staff width is distributed across bars by a per-bar weight.
67852
+ //
67853
+ // distributable = staffWidth - totalFixedOverhead
67854
+ // contentShare = distributable / sum(weight)
67855
+ // bar.width = bar.maxFixedOverhead + weight * contentShare
67856
+ //
67857
+ // The weight depends on the layout mode:
67858
+ // - shouldApplyBarScale=true -> weight = bar.displayScale (model-driven, matches Guitar Pro)
67859
+ // displayScale defaults to 1, so an unset value behaves identically
67860
+ // to an explicit 1 (GP omits the property when not customized).
67861
+ // - shouldApplyBarScale=false -> weight = natural content width (automatic, ignores displayScale)
67862
+ //
67863
+ // Per-bar maxFixedOverhead / maxContentWidth and the system-wide totals are maintained incrementally
67864
+ // in StaffSystem._applyLayoutAndUpdateWidth / revertLastBar so this pass can apply directly.
67865
+ const weightTotal = shouldApplyBarScale ? system.totalBarDisplayScale : system.totalContentWidth;
67866
+ const distributable = Math.max(0, staffWidth - system.totalFixedOverhead);
67867
+ const contentShare = weightTotal > 0 ? distributable / weightTotal : 0;
67452
67868
  for (const s of system.allStaves) {
67453
67869
  s.resetSharedLayoutData();
67454
- // scale the bars by keeping their respective ratio size
67455
67870
  let w = 0;
67456
- for (const renderer of s.barRenderers) {
67871
+ for (let i = 0; i < s.barRenderers.length; i++) {
67872
+ const renderer = s.barRenderers[i];
67873
+ const mb = system.masterBarsRenderers[i];
67457
67874
  renderer.x = w;
67458
67875
  renderer.y = s.topPadding + s.topOverflow;
67459
- let actualBarWidth;
67460
- if (shouldApplyBarScale) {
67461
- const barDisplayScale = system.getBarDisplayScale(renderer);
67462
- actualBarWidth = (barDisplayScale * staffWidth) / totalScale;
67463
- }
67464
- else {
67465
- actualBarWidth = renderer.computedWidth + spacePerBar;
67466
- }
67876
+ const weight = shouldApplyBarScale ? system.getBarDisplayScale(renderer) : mb.maxContentWidth;
67877
+ const actualBarWidth = mb.maxFixedOverhead + weight * contentShare;
67467
67878
  renderer.scaleToWidth(actualBarWidth);
67468
67879
  w += renderer.width;
67469
67880
  }
@@ -69210,16 +69621,18 @@ class LineBarRenderer extends BarRendererBase {
69210
69621
  s = [];
69211
69622
  const zero = MusicFontSymbol.Tuplet0;
69212
69623
  if (num > 10) {
69213
- s.push((zero + Math.floor(num / 10)));
69214
- s.push((zero + (num - 10)));
69624
+ const tens = Math.floor(num / 10);
69625
+ s.push((zero + tens));
69626
+ s.push((zero + (num - 10 * tens)));
69215
69627
  }
69216
69628
  else {
69217
69629
  s.push((zero + num));
69218
69630
  }
69219
69631
  s.push(MusicFontSymbol.TupletColon);
69220
69632
  if (den > 10) {
69221
- s.push((zero + Math.floor(den / 10)));
69222
- s.push((zero + (den - 10)));
69633
+ const tens = Math.floor(den / 10);
69634
+ s.push((zero + tens));
69635
+ s.push((zero + (den - 10 * tens)));
69223
69636
  }
69224
69637
  else {
69225
69638
  s.push((zero + den));
@@ -69285,12 +69698,24 @@ class LineBarRenderer extends BarRendererBase {
69285
69698
  const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat);
69286
69699
  const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat);
69287
69700
  const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper);
69288
- let startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69289
- let endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69701
+ let startY;
69702
+ let endY;
69290
69703
  if (isRestOnly) {
69291
- startY = Math.max(startY, endY);
69704
+ // rests have no stems, so anchor to the actual rest glyph bounds
69705
+ // instead of a stem-adjusted flag position (which would place the bracket
69706
+ // a full quarter-stem length away from the rests).
69707
+ if (direction === BeamDirection.Up) {
69708
+ startY = Math.min(this.getRestY(firstNonRestBeat, NoteYPosition.Top), this.getRestY(lastNonRestBeat, NoteYPosition.Top));
69709
+ }
69710
+ else {
69711
+ startY = Math.max(this.getRestY(firstNonRestBeat, NoteYPosition.Bottom), this.getRestY(lastNonRestBeat, NoteYPosition.Bottom));
69712
+ }
69292
69713
  endY = startY;
69293
69714
  }
69715
+ else {
69716
+ startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69717
+ endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69718
+ }
69294
69719
  // align line centered in available space
69295
69720
  if (direction === BeamDirection.Down) {
69296
69721
  startY += shift;
@@ -69660,7 +70085,30 @@ class LineBarRenderer extends BarRendererBase {
69660
70085
  let minNoteY = 0;
69661
70086
  for (const v of this.helpers.beamHelpers) {
69662
70087
  for (const h of v) {
69663
- if (!this.shouldPaintBeamingHelper(h)) ;
70088
+ if (!this.shouldPaintBeamingHelper(h)) {
70089
+ // beam is not drawn, but a rest-only tuplet still draws a bracket
70090
+ // anchored to the rest glyph bounds and needs overflow reserved.
70091
+ if (h.hasTuplet && h.isRestBeamHelper) {
70092
+ const tupletGroup = h.beats[0].tupletGroup;
70093
+ const tupletFirst = tupletGroup.beats[0];
70094
+ const tupletLast = tupletGroup.beats[tupletGroup.beats.length - 1];
70095
+ const tupletDirection = this.getTupletBeamDirection(h);
70096
+ if (tupletDirection === BeamDirection.Up) {
70097
+ const restTop = Math.min(this.getRestY(tupletFirst, NoteYPosition.Top), this.getRestY(tupletLast, NoteYPosition.Top));
70098
+ const topY = restTop - this.tupletSize - this.tupletOffset;
70099
+ if (topY < maxNoteY) {
70100
+ maxNoteY = topY;
70101
+ }
70102
+ }
70103
+ else {
70104
+ const restBottom = Math.max(this.getRestY(tupletFirst, NoteYPosition.Bottom), this.getRestY(tupletLast, NoteYPosition.Bottom));
70105
+ const bottomY = restBottom + this.tupletSize + this.tupletOffset;
70106
+ if (bottomY > minNoteY) {
70107
+ minNoteY = bottomY;
70108
+ }
70109
+ }
70110
+ }
70111
+ }
69664
70112
  else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
69665
70113
  const tupletDirection = this.getTupletBeamDirection(h);
69666
70114
  const direction = this.getBeamDirection(h);
@@ -72198,8 +72646,10 @@ class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
72198
72646
  const group = new GlyphGroup(0, 0);
72199
72647
  group.renderer = this.renderer;
72200
72648
  for (const note of this.container.beat.notes) {
72201
- const g = this._createBeatDot(sr.getNoteSteps(note), group);
72202
- g.colorOverride = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note);
72649
+ if (note.isVisible) {
72650
+ const g = this._createBeatDot(sr.getNoteSteps(note), group);
72651
+ g.colorOverride = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note);
72652
+ }
72203
72653
  }
72204
72654
  this.addEffect(group);
72205
72655
  }