@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.
package/dist/alphaTab.js CHANGED
@@ -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
  *
@@ -209,9 +209,9 @@
209
209
  * @internal
210
210
  */
211
211
  class VersionInfo {
212
- static version = '1.9.0-alpha.1767';
213
- static date = '2026-04-09T03:22:29.010Z';
214
- static commit = '6c3194f5f54595c44b979b1fee6cf423b1c9ed15';
212
+ static version = '1.9.0-alpha.1785';
213
+ static date = '2026-04-27T03:55:02.662Z';
214
+ static commit = '760ed909a3d8dc36b159d23b4ff6780e95a3daf1';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -3832,7 +3832,6 @@
3832
3832
  */
3833
3833
  class SynthConstants {
3834
3834
  static DefaultChannelCount = 16 + 1;
3835
- static MetronomeChannel = SynthConstants.DefaultChannelCount - 1;
3836
3835
  static MetronomeKey = 33;
3837
3836
  static AudioChannels = 2;
3838
3837
  static MinVolume = 0;
@@ -3856,6 +3855,10 @@
3856
3855
  static DefaultPitchWheel = SynthConstants.MaxPitchWheel / 2;
3857
3856
  static MicroBufferCount = 32;
3858
3857
  static MicroBufferSize = 64;
3858
+ /**
3859
+ * approximately -60 dB, which is inaudible to humans
3860
+ */
3861
+ static AudibleLevelThreshold = 1e-3;
3859
3862
  }
3860
3863
 
3861
3864
  /**
@@ -23820,26 +23823,26 @@
23820
23823
  case 'HarmonicType':
23821
23824
  const htype = c.findChildElement('HType');
23822
23825
  if (htype) {
23823
- switch (htype.innerText) {
23824
- case 'NoHarmonic':
23826
+ switch (htype.innerText.toLowerCase()) {
23827
+ case 'noharmonic':
23825
23828
  note.harmonicType = HarmonicType.None;
23826
23829
  break;
23827
- case 'Natural':
23830
+ case 'natural':
23828
23831
  note.harmonicType = HarmonicType.Natural;
23829
23832
  break;
23830
- case 'Artificial':
23833
+ case 'artificial':
23831
23834
  note.harmonicType = HarmonicType.Artificial;
23832
23835
  break;
23833
- case 'Pinch':
23836
+ case 'pinch':
23834
23837
  note.harmonicType = HarmonicType.Pinch;
23835
23838
  break;
23836
- case 'Tap':
23839
+ case 'tap':
23837
23840
  note.harmonicType = HarmonicType.Tap;
23838
23841
  break;
23839
- case 'Semi':
23842
+ case 'semi':
23840
23843
  note.harmonicType = HarmonicType.Semi;
23841
23844
  break;
23842
- case 'Feedback':
23845
+ case 'feedback':
23843
23846
  note.harmonicType = HarmonicType.Feedback;
23844
23847
  break;
23845
23848
  }
@@ -32631,9 +32634,16 @@
32631
32634
  *
32632
32635
  * The page layout does not use `displayWidth`. The use of absolute widths would break the proper alignments needed for this kind of display.
32633
32636
  *
32634
- * 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.
32635
- * 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,
32636
- * alphaTab will match this layout quite close.
32637
+ * In both modes, prefix and postfix glyphs (clef, key signature, time signature, barlines) are treated as fixed overhead: they keep their
32638
+ * natural size and the remaining staff width is distributed across bars by a per-bar weight. This matches the convention used by
32639
+ * Guitar Pro, Dorico, Finale, Sibelius and MuseScore. Bars that carry a system-start prefix or a mid-line clef/key/time-signature change
32640
+ * are therefore visibly wider than plain bars with the same weight. The weight source depends on the mode:
32641
+ *
32642
+ * * `Automatic` (default for `page` layout): weights come from the built-in spacing engine (the natural content width of each bar).
32643
+ * `displayScale` on the model is ignored.
32644
+ * * `UseModelLayout` (and the `parchment` layout): weights come from `bar.displayScale` / `masterBar.displayScale`. An unset
32645
+ * `displayScale` defaults to `1` and behaves identically to an explicit `1`, matching Guitar Pro (which omits the value when the
32646
+ * author hasn't customized it).
32637
32647
  *
32638
32648
  * ### Horizontal Layout
32639
32649
  *
@@ -38832,14 +38842,25 @@
38832
38842
  */
38833
38843
  boundsLookup;
38834
38844
  /**
38835
- * Finished the lookup for optimized access.
38845
+ * Whether this system's bounds have already been scaled via `finish`. Prevents double-scaling
38846
+ * when the parent `BoundsLookup` is preserved across partial renders and `finish` is invoked
38847
+ * again on a mix of already-scaled (preserved) and newly-registered (natural-coordinate) systems.
38848
+ */
38849
+ isFinished = false;
38850
+ /**
38851
+ * Finished the lookup for optimized access. Idempotent: once finished, further calls are no-ops
38852
+ * so preserved systems survive partial renders without being re-scaled.
38836
38853
  */
38837
38854
  finish(scale = 1) {
38855
+ if (this.isFinished) {
38856
+ return;
38857
+ }
38838
38858
  this.realBounds.scaleWith(scale);
38839
38859
  this.visualBounds.scaleWith(scale);
38840
38860
  for (const t of this.bars) {
38841
38861
  t.finish(scale);
38842
38862
  }
38863
+ this.isFinished = true;
38843
38864
  }
38844
38865
  /**
38845
38866
  * Adds a new master bar to this lookup.
@@ -39012,6 +39033,58 @@
39012
39033
  }
39013
39034
  this.isFinished = true;
39014
39035
  }
39036
+ /**
39037
+ * Re-opens the lookup for registrations without discarding previously registered bounds.
39038
+ * Used by the renderer when it preserves this lookup across a partial render so that new
39039
+ * bounds for the re-layouted range can be added while preserved systems stay intact.
39040
+ * @internal
39041
+ */
39042
+ resetForPartialUpdate() {
39043
+ this.isFinished = false;
39044
+ }
39045
+ /**
39046
+ * Removes all entries belonging to the given master bar index and any bars after it.
39047
+ * Used before a partial render re-registers bounds for the re-layouted range, so the
39048
+ * preserved lookup ends up with only the unchanged entries when registration begins.
39049
+ *
39050
+ * Assumes the layout aligns its re-layouted range to system boundaries - i.e. the first
39051
+ * system to clear starts exactly at `masterBarIndex`. Caller is responsible for passing
39052
+ * the first master-bar-index of the first re-layouted system.
39053
+ * @internal
39054
+ */
39055
+ clearFromMasterBar(masterBarIndex) {
39056
+ // drop staff systems whose bars start at or after the cleared range.
39057
+ let firstRemovedSystem = -1;
39058
+ for (let i = 0; i < this.staffSystems.length; i++) {
39059
+ const systemBars = this.staffSystems[i].bars;
39060
+ if (systemBars.length > 0 && systemBars[0].index >= masterBarIndex) {
39061
+ firstRemovedSystem = i;
39062
+ break;
39063
+ }
39064
+ }
39065
+ if (firstRemovedSystem !== -1) {
39066
+ this.staffSystems.splice(firstRemovedSystem, this.staffSystems.length - firstRemovedSystem);
39067
+ }
39068
+ // drop master bar entries at or beyond the cleared range.
39069
+ for (const key of Array.from(this._masterBarLookup.keys())) {
39070
+ if (key >= masterBarIndex) {
39071
+ this._masterBarLookup.delete(key);
39072
+ }
39073
+ }
39074
+ // drop beat entries whose beats belong to cleared bars.
39075
+ for (const key of Array.from(this._beatLookup.keys())) {
39076
+ const list = this._beatLookup.get(key);
39077
+ const filtered = list.filter(b => b.beat.voice.bar.index < masterBarIndex);
39078
+ if (filtered.length === 0) {
39079
+ this._beatLookup.delete(key);
39080
+ }
39081
+ else if (filtered.length !== list.length) {
39082
+ this._beatLookup.set(key, filtered);
39083
+ }
39084
+ }
39085
+ // drop the in-progress pointer - the next addStaffSystem call will replace it.
39086
+ this._currentStaffSystem = null;
39087
+ }
39015
39088
  /**
39016
39089
  * Adds a new staff sytem to the lookup.
39017
39090
  * @param bounds The staff system bounds to add.
@@ -39229,8 +39302,11 @@
39229
39302
  this.renderFinished.trigger(data.result);
39230
39303
  break;
39231
39304
  case 'alphaTab.postRenderFinished':
39232
- this.boundsLookup = BoundsLookup.fromJson(data.boundsLookup, this._api.score);
39233
- this.boundsLookup?.finish();
39305
+ const score = this._api.score;
39306
+ if (score && data.boundsLookup) {
39307
+ this.boundsLookup = BoundsLookup.fromJson(data.boundsLookup, this._api.score);
39308
+ this.boundsLookup?.finish();
39309
+ }
39234
39310
  this.postRenderFinished.trigger();
39235
39311
  break;
39236
39312
  case 'alphaTab.error':
@@ -39487,6 +39563,164 @@
39487
39563
  }
39488
39564
  }
39489
39565
 
39566
+ /**
39567
+ * @internal
39568
+ */
39569
+ class QueueItem {
39570
+ value;
39571
+ next;
39572
+ constructor(value) {
39573
+ this.value = value;
39574
+ }
39575
+ }
39576
+ /**
39577
+ * @internal
39578
+ */
39579
+ class Queue {
39580
+ _head;
39581
+ _tail;
39582
+ get isEmpty() {
39583
+ return this._head === undefined;
39584
+ }
39585
+ clear() {
39586
+ this._head = undefined;
39587
+ this._tail = undefined;
39588
+ }
39589
+ enqueue(item) {
39590
+ const queueItem = new QueueItem(item);
39591
+ if (this._tail) {
39592
+ // not empty -> add after tail
39593
+ this._tail.next = queueItem;
39594
+ this._tail = queueItem;
39595
+ }
39596
+ else {
39597
+ // empty -> new item takes head and tail
39598
+ this._head = queueItem;
39599
+ this._tail = queueItem;
39600
+ }
39601
+ }
39602
+ enqueueFront(item) {
39603
+ const queueItem = new QueueItem(item);
39604
+ queueItem.next = this._head;
39605
+ if (this._head) {
39606
+ this._head = queueItem;
39607
+ }
39608
+ else {
39609
+ this._head = queueItem;
39610
+ this._tail = queueItem;
39611
+ }
39612
+ }
39613
+ peek() {
39614
+ const head = this._head;
39615
+ if (!head) {
39616
+ return undefined;
39617
+ }
39618
+ return head.value;
39619
+ }
39620
+ dequeue() {
39621
+ const head = this._head;
39622
+ if (!head) {
39623
+ return undefined;
39624
+ }
39625
+ const newHead = head.next;
39626
+ this._head = newHead;
39627
+ // last item removed?
39628
+ if (!newHead) {
39629
+ this._tail = undefined;
39630
+ }
39631
+ return head.value;
39632
+ }
39633
+ }
39634
+
39635
+ /**
39636
+ * The options controlling how to export the audio.
39637
+ * @public
39638
+ */
39639
+ class AudioExportOptions {
39640
+ /**
39641
+ * The soundfonts to load and use for generating the audio.
39642
+ * If not provided, the already loaded soundfonts of the synthesizer will be used.
39643
+ * If no existing synthesizer is initialized, the generated audio might not contain any hearable audio.
39644
+ */
39645
+ soundFonts;
39646
+ /**
39647
+ * The output sample rate.
39648
+ * @default `44100`
39649
+ */
39650
+ sampleRate = 44100;
39651
+ /**
39652
+ * Whether to respect sync point information during export.
39653
+ * @default `true`
39654
+ * @remarks
39655
+ * If the song contains sync point information for synchronization with an external media,
39656
+ * this option allows controlling whether the synthesized audio is aligned with these points.
39657
+ *
39658
+ * This is useful when mixing the exported audio together with external media, keeping the same timing.
39659
+ *
39660
+ * Disable this option if you want the original/exact timing as per music sheet in the exported audio.
39661
+ */
39662
+ useSyncPoints = false;
39663
+ /**
39664
+ * The current master volume as percentage. (range: 0.0-3.0, default 1.0)
39665
+ */
39666
+ masterVolume = 1;
39667
+ /**
39668
+ * The metronome volume. (range: 0.0-3.0, default 0.0)
39669
+ */
39670
+ metronomeVolume = 0;
39671
+ /**
39672
+ * The range of the song that should be exported. Set this to null
39673
+ * to play the whole song.
39674
+ */
39675
+ playbackRange;
39676
+ /**
39677
+ * The volume for individual tracks as percentage (range: 0.0-3.0).
39678
+ * @remarks
39679
+ * The key is the track index, and the value is the relative volume.
39680
+ * The configured volume (as per data model) still applies, this is an additional volume control.
39681
+ * If no custom value is set, 100% is used.
39682
+ * No values from the currently active synthesizer are applied.
39683
+ *
39684
+ * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
39685
+ */
39686
+ trackVolume = new Map();
39687
+ /**
39688
+ * The additional semitone pitch transpose to apply for individual tracks.
39689
+ * @remarks
39690
+ * The key is the track index, and the value is the number of semitones to apply.
39691
+ * No values from the currently active synthesizer are applied.
39692
+ *
39693
+ * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
39694
+ */
39695
+ trackTranspositionPitches = new Map();
39696
+ }
39697
+ /**
39698
+ * Represents a single chunk of audio produced.
39699
+ * @public
39700
+ */
39701
+ class AudioExportChunk {
39702
+ /**
39703
+ * The generated samples for the requested chunk.
39704
+ */
39705
+ samples;
39706
+ /**
39707
+ * The current time position within the song in milliseconds.
39708
+ */
39709
+ currentTime = 0;
39710
+ /**
39711
+ * The total length of the song in milliseconds.
39712
+ */
39713
+ endTime = 0;
39714
+ /**
39715
+ * The current time position within the song in midi ticks.
39716
+ */
39717
+ currentTick = 0;
39718
+ /**
39719
+ * The total length of the song in midi ticks.
39720
+ */
39721
+ endTick = 0;
39722
+ }
39723
+
39490
39724
  // The SoundFont loading and Audio Synthesis is based on TinySoundFont, licensed under MIT,
39491
39725
  // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont)
39492
39726
  // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny
@@ -39600,6 +39834,7 @@
39600
39834
  endTime = 0;
39601
39835
  currentTempo = 0;
39602
39836
  syncPointTempo = 0;
39837
+ metronomeChannel = SynthConstants.DefaultChannelCount - 1;
39603
39838
  }
39604
39839
  /**
39605
39840
  * This sequencer dispatches midi events to the synthesizer based on the current
@@ -39612,6 +39847,9 @@
39612
39847
  _mainState;
39613
39848
  _oneTimeState = null;
39614
39849
  _countInState = null;
39850
+ get metronomeChannel() {
39851
+ return this._mainState.metronomeChannel;
39852
+ }
39615
39853
  get isPlayingMain() {
39616
39854
  return this._currentState === this._mainState;
39617
39855
  }
@@ -39695,7 +39933,7 @@
39695
39933
  const metronomeVolume = this._synthesizer.metronomeVolume;
39696
39934
  this._synthesizer.noteOffAll(true);
39697
39935
  this._synthesizer.resetSoft();
39698
- this._synthesizer.setupMetronomeChannel(metronomeVolume);
39936
+ this._synthesizer.setupMetronomeChannel(this.metronomeChannel, metronomeVolume);
39699
39937
  }
39700
39938
  this._mainSilentProcess(timePosition);
39701
39939
  }
@@ -39747,6 +39985,7 @@
39747
39985
  let metronomeLengthInMillis = 0;
39748
39986
  let metronomeTick = midiFile.tickShift; // shift metronome to content
39749
39987
  let metronomeTime = 0.0;
39988
+ let maxChannel = 0;
39750
39989
  let previousTick = 0;
39751
39990
  for (const mEvent of midiFile.events) {
39752
39991
  const synthData = new SynthEvent(state.synthData.length, mEvent);
@@ -39788,6 +40027,9 @@
39788
40027
  if (!state.firstProgramEventPerChannel.has(channel)) {
39789
40028
  state.firstProgramEventPerChannel.set(channel, synthData);
39790
40029
  }
40030
+ if (channel > maxChannel) {
40031
+ maxChannel = channel;
40032
+ }
39791
40033
  const isPercussion = channel === SynthConstants.PercussionChannel;
39792
40034
  if (!isPercussion) {
39793
40035
  this.instrumentPrograms.add(programChange.program);
@@ -39799,6 +40041,9 @@
39799
40041
  if (isPercussion) {
39800
40042
  this.percussionKeys.add(noteOn.noteKey);
39801
40043
  }
40044
+ if (noteOn.channel > maxChannel) {
40045
+ maxChannel = noteOn.channel;
40046
+ }
39802
40047
  }
39803
40048
  }
39804
40049
  state.currentTempo = state.tempoChanges.length > 0 ? state.tempoChanges[0].bpm : bpm;
@@ -39814,6 +40059,7 @@
39814
40059
  });
39815
40060
  state.endTime = absTime;
39816
40061
  state.endTick = absTick;
40062
+ state.metronomeChannel = maxChannel + 1;
39817
40063
  return state;
39818
40064
  }
39819
40065
  fillMidiEventQueue() {
@@ -44019,6 +44265,11 @@
44019
44265
  if (dynamicGain) {
44020
44266
  noteGain = SynthHelper.decibelsToGain(this.noteGainDb + this.modLfo.level * tmpModLfoToVolume);
44021
44267
  }
44268
+ // Update EG.
44269
+ this.ampEnv.process(blockSamples, f.outSampleRate);
44270
+ if (updateModEnv) {
44271
+ this.modEnv.process(blockSamples, f.outSampleRate);
44272
+ }
44022
44273
  gainMono = noteGain * this.ampEnv.level;
44023
44274
  if (isMuted) {
44024
44275
  gainMono = 0;
@@ -44026,11 +44277,6 @@
44026
44277
  else {
44027
44278
  gainMono *= this.mixVolume;
44028
44279
  }
44029
- // Update EG.
44030
- this.ampEnv.process(blockSamples, f.outSampleRate);
44031
- if (updateModEnv) {
44032
- this.modEnv.process(blockSamples, f.outSampleRate);
44033
- }
44034
44280
  // Update LFOs.
44035
44281
  if (updateModLFO) {
44036
44282
  this.modLfo.process(blockSamples);
@@ -44109,7 +44355,12 @@
44109
44355
  }
44110
44356
  break;
44111
44357
  }
44112
- if (tmpSourceSamplePosition >= tmpSampleEndDbl || this.ampEnv.segment === VoiceEnvelopeSegment.Done) {
44358
+ const inaudible = this.ampEnv.segment === VoiceEnvelopeSegment.Release &&
44359
+ Math.abs(gainMono) < SynthConstants.AudibleLevelThreshold;
44360
+ if (tmpSourceSamplePosition >= tmpSampleEndDbl ||
44361
+ this.ampEnv.segment === VoiceEnvelopeSegment.Done ||
44362
+ // Check if voice is inaudible during release to terminate early
44363
+ inaudible) {
44113
44364
  this.kill();
44114
44365
  return;
44115
44366
  }
@@ -44124,75 +44375,6 @@
44124
44375
  }
44125
44376
  }
44126
44377
 
44127
- /**
44128
- * @internal
44129
- */
44130
- class QueueItem {
44131
- value;
44132
- next;
44133
- constructor(value) {
44134
- this.value = value;
44135
- }
44136
- }
44137
- /**
44138
- * @internal
44139
- */
44140
- class Queue {
44141
- _head;
44142
- _tail;
44143
- get isEmpty() {
44144
- return this._head === undefined;
44145
- }
44146
- clear() {
44147
- this._head = undefined;
44148
- this._tail = undefined;
44149
- }
44150
- enqueue(item) {
44151
- const queueItem = new QueueItem(item);
44152
- if (this._tail) {
44153
- // not empty -> add after tail
44154
- this._tail.next = queueItem;
44155
- this._tail = queueItem;
44156
- }
44157
- else {
44158
- // empty -> new item takes head and tail
44159
- this._head = queueItem;
44160
- this._tail = queueItem;
44161
- }
44162
- }
44163
- enqueueFront(item) {
44164
- const queueItem = new QueueItem(item);
44165
- queueItem.next = this._head;
44166
- if (this._head) {
44167
- this._head = queueItem;
44168
- }
44169
- else {
44170
- this._head = queueItem;
44171
- this._tail = queueItem;
44172
- }
44173
- }
44174
- peek() {
44175
- const head = this._head;
44176
- if (!head) {
44177
- return undefined;
44178
- }
44179
- return head.value;
44180
- }
44181
- dequeue() {
44182
- const head = this._head;
44183
- if (!head) {
44184
- return undefined;
44185
- }
44186
- const newHead = head.next;
44187
- this._head = newHead;
44188
- // last item removed?
44189
- if (!newHead) {
44190
- this._tail = undefined;
44191
- }
44192
- return head.value;
44193
- }
44194
- }
44195
-
44196
44378
  /**
44197
44379
  * Lists all midi controllers.
44198
44380
  * @public
@@ -44349,6 +44531,7 @@
44349
44531
  currentTempo = 0;
44350
44532
  timeSignatureNumerator = 0;
44351
44533
  timeSignatureDenominator = 0;
44534
+ _metronomeChannel = SynthConstants.DefaultChannelCount - 1;
44352
44535
  constructor(sampleRate) {
44353
44536
  this.outSampleRate = sampleRate;
44354
44537
  }
@@ -44453,8 +44636,8 @@
44453
44636
  while (!this._midiEventQueue.isEmpty) {
44454
44637
  const m = this._midiEventQueue.dequeue();
44455
44638
  if (m.isMetronome && this.metronomeVolume > 0) {
44456
- this.channelNoteOff(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey);
44457
- this.channelNoteOn(SynthConstants.MetronomeChannel, SynthConstants.MetronomeKey, 95 / 127);
44639
+ this.channelNoteOff(this._metronomeChannel, SynthConstants.MetronomeKey);
44640
+ this.channelNoteOn(this._metronomeChannel, SynthConstants.MetronomeKey, 95 / 127);
44458
44641
  }
44459
44642
  else if (m.event) {
44460
44643
  this.processMidiMessage(m.event);
@@ -44468,7 +44651,7 @@
44468
44651
  // channel is muted if it is either explicitley muted, or another channel is set to solo but not this one.
44469
44652
  // exception. metronome is implicitly added in solo
44470
44653
  const isChannelMuted = this._mutedChannels.has(channel) ||
44471
- (anySolo && channel !== SynthConstants.MetronomeChannel && !this._soloChannels.has(channel));
44654
+ (anySolo && channel !== this._metronomeChannel && !this._soloChannels.has(channel));
44472
44655
  if (!buffer) {
44473
44656
  voice.kill();
44474
44657
  }
@@ -44522,16 +44705,17 @@
44522
44705
  }
44523
44706
  }
44524
44707
  get metronomeVolume() {
44525
- return this.channelGetMixVolume(SynthConstants.MetronomeChannel);
44708
+ return this.channelGetMixVolume(this._metronomeChannel);
44526
44709
  }
44527
44710
  set metronomeVolume(value) {
44528
- this.setupMetronomeChannel(value);
44711
+ this.setupMetronomeChannel(this._metronomeChannel, value);
44529
44712
  }
44530
- setupMetronomeChannel(volume) {
44531
- this.channelSetMixVolume(SynthConstants.MetronomeChannel, volume);
44713
+ setupMetronomeChannel(channel, volume) {
44714
+ this._metronomeChannel = channel;
44715
+ this.channelSetMixVolume(channel, volume);
44532
44716
  if (volume > 0) {
44533
- this.channelSetVolume(SynthConstants.MetronomeChannel, 1);
44534
- this.channelSetPresetNumber(SynthConstants.MetronomeChannel, 0, true);
44717
+ this.channelSetVolume(channel, 1);
44718
+ this.channelSetPresetNumber(channel, 0, true);
44535
44719
  }
44536
44720
  }
44537
44721
  get masterVolume() {
@@ -45611,95 +45795,6 @@
45611
45795
  }
45612
45796
  }
45613
45797
 
45614
- /**
45615
- * The options controlling how to export the audio.
45616
- * @public
45617
- */
45618
- class AudioExportOptions {
45619
- /**
45620
- * The soundfonts to load and use for generating the audio.
45621
- * If not provided, the already loaded soundfonts of the synthesizer will be used.
45622
- * If no existing synthesizer is initialized, the generated audio might not contain any hearable audio.
45623
- */
45624
- soundFonts;
45625
- /**
45626
- * The output sample rate.
45627
- * @default `44100`
45628
- */
45629
- sampleRate = 44100;
45630
- /**
45631
- * Whether to respect sync point information during export.
45632
- * @default `true`
45633
- * @remarks
45634
- * If the song contains sync point information for synchronization with an external media,
45635
- * this option allows controlling whether the synthesized audio is aligned with these points.
45636
- *
45637
- * This is useful when mixing the exported audio together with external media, keeping the same timing.
45638
- *
45639
- * Disable this option if you want the original/exact timing as per music sheet in the exported audio.
45640
- */
45641
- useSyncPoints = false;
45642
- /**
45643
- * The current master volume as percentage. (range: 0.0-3.0, default 1.0)
45644
- */
45645
- masterVolume = 1;
45646
- /**
45647
- * The metronome volume. (range: 0.0-3.0, default 0.0)
45648
- */
45649
- metronomeVolume = 0;
45650
- /**
45651
- * The range of the song that should be exported. Set this to null
45652
- * to play the whole song.
45653
- */
45654
- playbackRange;
45655
- /**
45656
- * The volume for individual tracks as percentage (range: 0.0-3.0).
45657
- * @remarks
45658
- * The key is the track index, and the value is the relative volume.
45659
- * The configured volume (as per data model) still applies, this is an additional volume control.
45660
- * If no custom value is set, 100% is used.
45661
- * No values from the currently active synthesizer are applied.
45662
- *
45663
- * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
45664
- */
45665
- trackVolume = new Map();
45666
- /**
45667
- * The additional semitone pitch transpose to apply for individual tracks.
45668
- * @remarks
45669
- * The key is the track index, and the value is the number of semitones to apply.
45670
- * No values from the currently active synthesizer are applied.
45671
- *
45672
- * The meaning of the key changes when used with AlphaSynth directly, in this case the key is the midi channel .
45673
- */
45674
- trackTranspositionPitches = new Map();
45675
- }
45676
- /**
45677
- * Represents a single chunk of audio produced.
45678
- * @public
45679
- */
45680
- class AudioExportChunk {
45681
- /**
45682
- * The generated samples for the requested chunk.
45683
- */
45684
- samples;
45685
- /**
45686
- * The current time position within the song in milliseconds.
45687
- */
45688
- currentTime = 0;
45689
- /**
45690
- * The total length of the song in milliseconds.
45691
- */
45692
- endTime = 0;
45693
- /**
45694
- * The current time position within the song in midi ticks.
45695
- */
45696
- currentTick = 0;
45697
- /**
45698
- * The total length of the song in midi ticks.
45699
- */
45700
- endTick = 0;
45701
- }
45702
-
45703
45798
  /**
45704
45799
  * This is the base class for synthesizer components which can be used to
45705
45800
  * play a {@link MidiFile} via a {@link ISynthOutput}.
@@ -45908,6 +46003,17 @@
45908
46003
  }
45909
46004
  this._notPlayedSamples += samples.length;
45910
46005
  this.output.addSamples(samples);
46006
+ // if the sequencer finished, we instantly force a noteOff on all
46007
+ // voices to complete playback and stop voices fast.
46008
+ // Doing this in the samplePlayed callback is too late as we might
46009
+ // continue generating audio for long-release notes (especially percussion like cymbals)
46010
+ // we still have checkForFinish which takes care of the counterpart
46011
+ // on the sample played area to ensure we seek back.
46012
+ // but thanks to this code we ensure the output will complete fast as we won't
46013
+ // be adding more samples beside a 0.1s ramp-down
46014
+ if (this.sequencer.isFinished) {
46015
+ this.synthesizer.noteOffAll(true);
46016
+ }
45911
46017
  }
45912
46018
  else {
45913
46019
  // Tell output that there is no data left for it.
@@ -45924,7 +46030,7 @@
45924
46030
  if (this._countInVolume > 0) {
45925
46031
  Logger.debug('AlphaSynth', 'Starting countin');
45926
46032
  this.sequencer.startCountIn();
45927
- this.synthesizer.setupMetronomeChannel(this._countInVolume);
46033
+ this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this._countInVolume);
45928
46034
  this.updateTimePosition(0, true);
45929
46035
  }
45930
46036
  this.output.play();
@@ -45936,7 +46042,7 @@
45936
46042
  this._stopOneTimeMidi();
45937
46043
  }
45938
46044
  Logger.debug('AlphaSynth', 'Starting playback');
45939
- this.synthesizer.setupMetronomeChannel(this.metronomeVolume);
46045
+ this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume);
45940
46046
  this._synthStopping = false;
45941
46047
  this.state = PlayerState.Playing;
45942
46048
  this.stateChanged.trigger(new PlayerStateChangedEventArgs(this.state, false));
@@ -46020,7 +46126,7 @@
46020
46126
  }
46021
46127
  _checkReadyForPlayback() {
46022
46128
  if (this.isReadyForPlayback) {
46023
- this.synthesizer.setupMetronomeChannel(this.metronomeVolume);
46129
+ this.synthesizer.setupMetronomeChannel(this.sequencer.metronomeChannel, this.metronomeVolume);
46024
46130
  const programs = this.sequencer.instrumentPrograms;
46025
46131
  const percussionKeys = this.sequencer.percussionKeys;
46026
46132
  let append = false;
@@ -46361,7 +46467,7 @@
46361
46467
  _generatedAudioCurrentTime = 0;
46362
46468
  _generatedAudioEndTime = 0;
46363
46469
  setup() {
46364
- this._synth.setupMetronomeChannel(this._synth.metronomeVolume);
46470
+ this._synth.setupMetronomeChannel(this._sequencer.metronomeChannel, this._synth.metronomeVolume);
46365
46471
  const syncPoints = this._sequencer.currentSyncPoints;
46366
46472
  const alphaTabEndTime = this._sequencer.currentEndTime;
46367
46473
  if (syncPoints.length === 0) {
@@ -46443,7 +46549,7 @@
46443
46549
  }
46444
46550
  loadPresets(_hydra, _instrumentPrograms, _percussionKeys, _append) {
46445
46551
  }
46446
- setupMetronomeChannel(_metronomeVolume) {
46552
+ setupMetronomeChannel(_metronomeChannel, _metronomeVolume) {
46447
46553
  }
46448
46554
  synthesizeSilent(_sampleCount) {
46449
46555
  this.fakeSynthesize();
@@ -46810,7 +46916,15 @@
46810
46916
  Logger.warning('Rendering', 'AlphaTab skipped rendering because of width=0 (element invisible)', null);
46811
46917
  return;
46812
46918
  }
46813
- this.boundsLookup = new BoundsLookup();
46919
+ // For partial renders we preserve the existing lookup so bars outside the re-layouted
46920
+ // range keep their already-scaled bounds - the layout will clear the changed range
46921
+ // before the paint pass re-registers fresh entries for it.
46922
+ if (renderHints?.firstChangedMasterBar !== undefined && this.boundsLookup) {
46923
+ this.boundsLookup.resetForPartialUpdate();
46924
+ }
46925
+ else {
46926
+ this.boundsLookup = new BoundsLookup();
46927
+ }
46814
46928
  this._recreateCanvas();
46815
46929
  this.canvas.lineWidth = 1;
46816
46930
  this.canvas.settings = this.settings;
@@ -47877,6 +47991,7 @@
47877
47991
  _bufferTimeInMilliseconds = 0;
47878
47992
  _settings;
47879
47993
  _boundHandleMessage;
47994
+ _pendingEvents;
47880
47995
  constructor(settings) {
47881
47996
  super();
47882
47997
  this._settings = settings;
@@ -47904,6 +48019,13 @@
47904
48019
  this.source.connect(this._worklet);
47905
48020
  this.source.start(0);
47906
48021
  this._worklet.connect(ctx.destination);
48022
+ const pending = this._pendingEvents;
48023
+ if (pending) {
48024
+ for (const e of pending) {
48025
+ this._worklet.port.postMessage(e);
48026
+ }
48027
+ this._pendingEvents = undefined;
48028
+ }
47907
48029
  }, (reason) => {
47908
48030
  Logger.error('WebAudio', `Audio Worklet creation failed: reason=${reason}`);
47909
48031
  });
@@ -47930,15 +48052,26 @@
47930
48052
  this._worklet.disconnect();
47931
48053
  }
47932
48054
  this._worklet = null;
48055
+ this._pendingEvents = undefined;
48056
+ }
48057
+ _postWorkerMessage(message) {
48058
+ const worklet = this._worklet;
48059
+ if (worklet) {
48060
+ worklet.port.postMessage(message);
48061
+ }
48062
+ else {
48063
+ this._pendingEvents ??= [];
48064
+ this._pendingEvents.push(message);
48065
+ }
47933
48066
  }
47934
48067
  addSamples(f) {
47935
- this._worklet?.port.postMessage({
48068
+ this._postWorkerMessage({
47936
48069
  cmd: 'alphaSynth.output.addSamples',
47937
48070
  samples: Environment.prepareForPostMessage(f)
47938
48071
  });
47939
48072
  }
47940
48073
  resetSamples() {
47941
- this._worklet?.port.postMessage({
48074
+ this._postWorkerMessage({
47942
48075
  cmd: 'alphaSynth.output.resetSamples'
47943
48076
  });
47944
48077
  }
@@ -48665,9 +48798,6 @@
48665
48798
  * @param beat The beat to add.
48666
48799
  */
48667
48800
  highlightBeat(beat, playbackStart) {
48668
- if (beat.isEmpty && !beat.voice.isEmpty) {
48669
- return;
48670
- }
48671
48801
  if (!this._highlightedBeats.has(beat.id)) {
48672
48802
  this._highlightedBeats.set(beat.id, true);
48673
48803
  this.highlightedBeats.push(new BeatTickLookupItem(beat, playbackStart));
@@ -50135,7 +50265,13 @@
50135
50265
  let beatStart = beat.playbackStart;
50136
50266
  let audioDuration = beat.playbackDuration;
50137
50267
  const masterBarDuration = beat.voice.bar.masterBar.calculateDuration();
50138
- if (beat.voice.bar.isEmpty) {
50268
+ // For a bar whose voice contains a single empty beat (the typical "whole-bar rest"
50269
+ // placeholder inserted during score.finish), extend the beat's audio duration to cover
50270
+ // the full bar so cursor navigation has a beat to follow across the whole bar. Don't
50271
+ // apply this when the voice has multiple beats: those represent explicit rhythmic
50272
+ // subdivisions even when each beat is empty (e.g. a recording grid of placeholder
50273
+ // slots), and overriding would make every beat overlap the whole bar.
50274
+ if (beat.voice.bar.isEmpty && beat.voice.beats.length === 1) {
50139
50275
  audioDuration = masterBarDuration;
50140
50276
  }
50141
50277
  else if (beat.voice.bar.masterBar.tripletFeel !== TripletFeel.NoTripletFeel &&
@@ -54756,7 +54892,8 @@
54756
54892
  this._isInitialBeatCursorUpdate ||
54757
54893
  barBounds.y !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.y ||
54758
54894
  startBeatX < previousBeatBounds.onNotesX ||
54759
- barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1;
54895
+ barBoundings.index > previousBeatBounds.barBounds.masterBarBounds.index + 1 ||
54896
+ barBounds.h !== previousBeatBounds.barBounds.masterBarBounds.visualBounds.h;
54760
54897
  if (jumpCursor) {
54761
54898
  cursorHandler.placeBeatCursor(beatCursor, beatBoundings, startBeatX);
54762
54899
  }
@@ -55811,6 +55948,9 @@
55811
55948
  this._beatVisibilityChecker.bounds = this.boundsLookup;
55812
55949
  this._currentBeat = null;
55813
55950
  this._cursorUpdateTick(this._previousTick, false, 1, true, true);
55951
+ if (this._selectionStart) {
55952
+ this.highlightPlaybackRange(this._selectionStart.beat, this._selectionEnd.beat);
55953
+ }
55814
55954
  this.postRenderFinished.trigger();
55815
55955
  this.uiFacade.triggerEvent(this.container, 'postRenderFinished', null);
55816
55956
  }
@@ -57993,8 +58133,9 @@
57993
58133
  break;
57994
58134
  case 'alphaTab.renderScore':
57995
58135
  this._updateFontSizes(data.fontSizes);
58136
+ const renderHints = data.renderHints;
57996
58137
  const score = data.score == null ? null : JsonConverter.jsObjectToScore(data.score, this._renderer.settings);
57997
- this._renderMultiple(score, data.trackIndexes);
58138
+ this._renderMultiple(score, data.trackIndexes, renderHints);
57998
58139
  break;
57999
58140
  case 'alphaTab.updateSettings':
58000
58141
  this._updateSettings(data.settings);
@@ -62867,6 +63008,15 @@
62867
63008
  }
62868
63009
  return false;
62869
63010
  }
63011
+ /**
63012
+ * The fixed-overhead width of this renderer: glyphs that do not stretch when
63013
+ * the bar is scaled (clef, key signature, time signature, barlines, courtesy
63014
+ * accidentals, etc). Treated as a fixed allocation by the system-level layout
63015
+ * before distributing remaining width across bars by {@link Bar.displayScale}.
63016
+ */
63017
+ get fixedOverhead() {
63018
+ return this._preBeatGlyphs.width + this._postBeatGlyphs.width;
63019
+ }
62870
63020
  scaleToWidth(width) {
62871
63021
  // preBeat and postBeat glyphs do not get resized
62872
63022
  const containerWidth = width - this._preBeatGlyphs.width - this._postBeatGlyphs.width;
@@ -65425,6 +65575,22 @@
65425
65575
  postBeatSize = 0;
65426
65576
  minStretchForce = 0;
65427
65577
  totalSpringConstant = 0;
65578
+ /**
65579
+ * The smallest note duration encountered within this bar's springs, used as the reference in
65580
+ * the Gourlay stretch formula. Read by the owning {@link StaffSystem} so that the system can
65581
+ * aggregate a shared minimum across all bars and trigger a reconcile if an added bar introduces
65582
+ * a shorter duration than previously seen.
65583
+ */
65584
+ get localMinDuration() {
65585
+ return this._minDuration;
65586
+ }
65587
+ /**
65588
+ * The minimum-duration reference against which the spring constants currently held by this info
65589
+ * were computed. Set by {@link finish} and {@link recomputeSpringConstants}. The owning
65590
+ * StaffSystem compares this against its system-wide minimum to decide whether spring constants
65591
+ * need re-derivation.
65592
+ */
65593
+ computedWithMinDuration = 0;
65428
65594
  _updateMinStretchForce(force) {
65429
65595
  if (this.minStretchForce < force) {
65430
65596
  this.minStretchForce = force;
@@ -65594,10 +65760,26 @@
65594
65760
  this._incompleteGraceRodsWidth += sp.preBeatWidth + sp.postSpringWidth;
65595
65761
  }
65596
65762
  }
65597
- this._calculateSpringConstants();
65763
+ this._calculateSpringConstants(this._minDuration);
65764
+ this.computedWithMinDuration = this._minDuration;
65598
65765
  this.version++;
65599
65766
  }
65600
- _calculateSpringConstants() {
65767
+ /**
65768
+ * Re-derives the spring constants (and {@link minStretchForce} / {@link totalSpringConstant})
65769
+ * using a caller-supplied minimum-duration reference rather than this bar's local minimum.
65770
+ *
65771
+ * Called by {@link StaffSystem.reconcileMinDurationIfDirty} when a bar added later to the
65772
+ * system introduced a shorter note than previously seen, invalidating this bar's spring
65773
+ * constants. Grace-rod data is not recomputed — it is independent of the minimum-duration
65774
+ * reference. The internal {@link version} is bumped so downstream consumers (e.g.
65775
+ * {@link BarRendererBase.applyLayoutingInfo}) pick up the refreshed positions.
65776
+ */
65777
+ recomputeSpringConstants(minDuration) {
65778
+ this._calculateSpringConstants(minDuration);
65779
+ this.computedWithMinDuration = minDuration;
65780
+ this.version++;
65781
+ }
65782
+ _calculateSpringConstants(minDuration) {
65601
65783
  let totalSpringConstant = 0;
65602
65784
  const sortedSprings = this._timeSortedSprings;
65603
65785
  if (sortedSprings.length === 0) {
@@ -65615,7 +65797,7 @@
65615
65797
  const nextSpring = sortedSprings[i + 1];
65616
65798
  duration = Math.abs(nextSpring.timePosition - currentSpring.timePosition);
65617
65799
  }
65618
- currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration);
65800
+ currentSpring.springConstant = this._calculateSpringConstant(currentSpring, duration, minDuration);
65619
65801
  totalSpringConstant += 1 / currentSpring.springConstant;
65620
65802
  }
65621
65803
  this.totalSpringConstant = 1 / totalSpringConstant;
@@ -65675,7 +65857,7 @@
65675
65857
  // springX += this.calculateWidth(force, spring.springConstant);
65676
65858
  // }
65677
65859
  // }
65678
- _calculateSpringConstant(spring, duration) {
65860
+ _calculateSpringConstant(spring, duration, minDuration) {
65679
65861
  if (duration <= 0) {
65680
65862
  duration = MidiUtils.toTicks(Duration.TwoHundredFiftySixth);
65681
65863
  }
@@ -65683,7 +65865,6 @@
65683
65865
  spring.smallestDuration = duration;
65684
65866
  }
65685
65867
  const smallestDuration = spring.smallestDuration;
65686
- const minDuration = this._minDuration;
65687
65868
  const minDurationWidth = BarLayoutingInfo._defaultMinDurationWidth;
65688
65869
  const phi = 1 + 0.85 * Math.log2(duration / minDuration);
65689
65870
  return (smallestDuration / duration) * (1 / (phi * minDurationWidth));
@@ -65746,6 +65927,18 @@
65746
65927
  canWrap = true;
65747
65928
  masterBar;
65748
65929
  additionalMultiBarRestIndexes = null;
65930
+ /**
65931
+ * Max fixed overhead (prefix + postfix glyph width) across all staves of this bar.
65932
+ * Used by the layout-mode horizontal scaling pass to carve out the fixed-overhead bucket
65933
+ * before distributing staff width across bars.
65934
+ */
65935
+ maxFixedOverhead = 0;
65936
+ /**
65937
+ * Max natural content width (computedWidth - fixedOverhead) across all staves of this bar.
65938
+ * Used as the bar weight when the layout ignores {@link MasterBar.displayScale} (e.g.
65939
+ * Page layout with `SystemsLayoutMode.Automatic`).
65940
+ */
65941
+ maxContentWidth = 0;
65749
65942
  get lastMasterBarIndex() {
65750
65943
  if (this.additionalMultiBarRestIndexes) {
65751
65944
  return this.additionalMultiBarRestIndexes[this.additionalMultiBarRestIndexes.length - 1];
@@ -65927,6 +66120,45 @@
65927
66120
  * This value is mainly used in the parchment style layout for correct scaling of the bars.
65928
66121
  */
65929
66122
  totalBarDisplayScale = 0;
66123
+ /**
66124
+ * Sum of per-bar {@link MasterBarsRenderers.maxFixedOverhead} across the system. The layout-mode
66125
+ * horizontal scaling pass subtracts this from the available staff width before distributing the
66126
+ * remainder across bars.
66127
+ */
66128
+ totalFixedOverhead = 0;
66129
+ /**
66130
+ * Sum of per-bar {@link MasterBarsRenderers.maxContentWidth} across the system. Used as the
66131
+ * denominator when distributing staff width in modes that weight bars by natural content width
66132
+ * (Page layout with `SystemsLayoutMode.Automatic`).
66133
+ */
66134
+ totalContentWidth = 0;
66135
+ /**
66136
+ * Shortest note duration (in ticks) across every bar that has been added to this system, used
66137
+ * as the common reference in the Gourlay stretch formula so that rhythmically-equivalent beats
66138
+ * in different bars of the same system align column-wise.
66139
+ *
66140
+ * `-1` means "no bar added yet". The value only moves downward during system assembly; when a
66141
+ * new bar introduces a shorter minimum, {@link isMinDurationDirty} is set so that
66142
+ * {@link reconcileMinDurationIfDirty} can re-derive spring constants on the previously-added
66143
+ * bars before layout distribution runs.
66144
+ */
66145
+ minDuration = -1;
66146
+ /**
66147
+ * Set when a bar added to this system introduced a shorter {@link minDuration} than previously
66148
+ * seen, leaving earlier bars' spring constants stale. Consumed by
66149
+ * {@link reconcileMinDurationIfDirty} which is called from `VerticalLayoutBase._fitSystem`
66150
+ * once the system is fully assembled.
66151
+ */
66152
+ isMinDurationDirty = false;
66153
+ /**
66154
+ * Whether this system coordinates a shared minimum-duration reference across its bars for the
66155
+ * Gourlay stretch formula. Defaults to `true` for page-style and parchment layouts where bars
66156
+ * of a system fight for a common staff width. Set to `false` for horizontal layouts where each
66157
+ * bar is sized independently (by `bar.displayWidth` or its intrinsic width) and there is no
66158
+ * column-alignment concern - each bar keeps its local minimum so pre-existing rendering is
66159
+ * preserved.
66160
+ */
66161
+ shareMinDurationAcrossBars = true;
65930
66162
  isLast = false;
65931
66163
  masterBarsRenderers = [];
65932
66164
  staves = [];
@@ -65988,6 +66220,9 @@
65988
66220
  }
65989
66221
  this.firstVisibleStaff = firstVisibleStaff;
65990
66222
  this._calculateAccoladeSpacing(tracks);
66223
+ // On the resize path the layoutingInfo was finalized in a previous layout pass, so we
66224
+ // only need to check whether its min-duration reference still matches the new system's.
66225
+ this._trackSystemMinDuration(renderers.layoutingInfo);
65991
66226
  this._applyLayoutAndUpdateWidth();
65992
66227
  return renderers;
65993
66228
  }
@@ -66043,10 +66278,89 @@
66043
66278
  this.firstVisibleStaff = firstVisibleStaff;
66044
66279
  this._calculateAccoladeSpacing(tracks);
66045
66280
  barLayoutingInfo.finish();
66281
+ // Reconcile against the system-wide minimum-duration reference now that springs are
66282
+ // finalized. If this bar introduced a shorter note, earlier bars become stale (flagged
66283
+ // for bulk reconcile at fit time). If the system already had a shorter min than this
66284
+ // bar's local one, this bar's spring constants are recomputed immediately so the width
66285
+ // we return below reflects the shared reference.
66286
+ this._trackSystemMinDuration(barLayoutingInfo);
66046
66287
  // ensure same widths of new renderer
66047
66288
  result.width = this._applyLayoutAndUpdateWidth();
66048
66289
  return result;
66049
66290
  }
66291
+ /**
66292
+ * Updates {@link minDuration} and {@link isMinDurationDirty} when a bar is added, and brings
66293
+ * the just-added bar's {@link BarLayoutingInfo} in line with the current system minimum if the
66294
+ * system already saw a shorter reference. The bulk reconcile over previously-added bars is
66295
+ * deferred to {@link reconcileMinDurationIfDirty} (called from `_fitSystem`) to avoid
66296
+ * re-iterating the system every time a bar is appended.
66297
+ */
66298
+ _trackSystemMinDuration(info) {
66299
+ if (!this.shareMinDurationAcrossBars) {
66300
+ return;
66301
+ }
66302
+ const localMin = info.localMinDuration;
66303
+ if (this.minDuration === -1 || localMin < this.minDuration) {
66304
+ // this bar shortens the system minimum; earlier bars (if any) are now stale
66305
+ if (this.masterBarsRenderers.length > 1 && localMin !== this.minDuration) {
66306
+ this.isMinDurationDirty = true;
66307
+ }
66308
+ this.minDuration = localMin;
66309
+ }
66310
+ if (info.computedWithMinDuration > this.minDuration) {
66311
+ // this bar was initialized against a larger (local) min than the system carries; pull
66312
+ // it down to the system reference so its computedWidth reflects the shared spacing.
66313
+ info.recomputeSpringConstants(this.minDuration);
66314
+ }
66315
+ }
66316
+ /**
66317
+ * Re-derives spring constants on bars whose {@link BarLayoutingInfo.computedWithMinDuration}
66318
+ * is out of sync with the current {@link minDuration}, and rebuilds the cached system totals
66319
+ * (widths, {@link totalFixedOverhead}, {@link totalContentWidth}) from the refreshed bar
66320
+ * widths. Called from `VerticalLayoutBase._fitSystem` after the system is fully assembled and
66321
+ * before distribution runs. No-op when {@link isMinDurationDirty} is false.
66322
+ */
66323
+ reconcileMinDurationIfDirty() {
66324
+ if (!this.isMinDurationDirty) {
66325
+ return;
66326
+ }
66327
+ let systemWidth = this.accoladeWidth;
66328
+ let totalFixedOverhead = 0;
66329
+ let totalContentWidth = 0;
66330
+ for (const mb of this.masterBarsRenderers) {
66331
+ if (mb.layoutingInfo.computedWithMinDuration > this.minDuration) {
66332
+ mb.layoutingInfo.recomputeSpringConstants(this.minDuration);
66333
+ }
66334
+ let maxPrefix = 0;
66335
+ let maxContent = 0;
66336
+ let realWidth = 0;
66337
+ for (const r of mb.renderers) {
66338
+ r.applyLayoutingInfo();
66339
+ if (r.computedWidth > realWidth) {
66340
+ realWidth = r.computedWidth;
66341
+ }
66342
+ const overhead = r.fixedOverhead;
66343
+ if (overhead > maxPrefix) {
66344
+ maxPrefix = overhead;
66345
+ }
66346
+ const content = Math.max(0, r.computedWidth - overhead);
66347
+ if (content > maxContent) {
66348
+ maxContent = content;
66349
+ }
66350
+ }
66351
+ mb.maxFixedOverhead = maxPrefix;
66352
+ mb.maxContentWidth = maxContent;
66353
+ mb.width = realWidth;
66354
+ systemWidth += realWidth;
66355
+ totalFixedOverhead += maxPrefix;
66356
+ totalContentWidth += maxContent;
66357
+ }
66358
+ this.width = systemWidth;
66359
+ this.computedWidth = systemWidth;
66360
+ this.totalFixedOverhead = totalFixedOverhead;
66361
+ this.totalContentWidth = totalContentWidth;
66362
+ this.isMinDurationDirty = false;
66363
+ }
66050
66364
  getBarDisplayScale(renderer) {
66051
66365
  return this.staves.length > 1 ? renderer.bar.masterBar.displayScale : renderer.bar.displayScale;
66052
66366
  }
@@ -66085,12 +66399,16 @@
66085
66399
  this.width -= width;
66086
66400
  this.computedWidth -= width;
66087
66401
  this.totalBarDisplayScale -= barDisplayScale;
66402
+ this.totalFixedOverhead -= toRemove.maxFixedOverhead;
66403
+ this.totalContentWidth -= toRemove.maxContentWidth;
66088
66404
  return toRemove;
66089
66405
  }
66090
66406
  return null;
66091
66407
  }
66092
66408
  _applyLayoutAndUpdateWidth() {
66093
66409
  let realWidth = 0;
66410
+ let maxFixedOverhead = 0;
66411
+ let maxContentWidth = 0;
66094
66412
  let barDisplayScale = 0;
66095
66413
  for (const s of this.allStaves) {
66096
66414
  const last = s.barRenderers[s.barRenderers.length - 1];
@@ -66099,8 +66417,21 @@
66099
66417
  if (last.computedWidth > realWidth) {
66100
66418
  realWidth = last.computedWidth;
66101
66419
  }
66420
+ const overhead = last.fixedOverhead;
66421
+ if (overhead > maxFixedOverhead) {
66422
+ maxFixedOverhead = overhead;
66423
+ }
66424
+ const content = Math.max(0, last.computedWidth - overhead);
66425
+ if (content > maxContentWidth) {
66426
+ maxContentWidth = content;
66427
+ }
66102
66428
  }
66429
+ const renderers = this.masterBarsRenderers[this.masterBarsRenderers.length - 1];
66430
+ renderers.maxFixedOverhead = maxFixedOverhead;
66431
+ renderers.maxContentWidth = maxContentWidth;
66103
66432
  this.totalBarDisplayScale += barDisplayScale;
66433
+ this.totalFixedOverhead += maxFixedOverhead;
66434
+ this.totalContentWidth += maxContentWidth;
66104
66435
  this.width += realWidth;
66105
66436
  this.computedWidth += realWidth;
66106
66437
  return realWidth;
@@ -66598,17 +66929,6 @@
66598
66929
  }
66599
66930
  }
66600
66931
 
66601
- /**
66602
- * @internal
66603
- */
66604
- class LazyPartial {
66605
- args;
66606
- renderCallback;
66607
- constructor(args, renderCallback) {
66608
- this.args = args;
66609
- this.renderCallback = renderCallback;
66610
- }
66611
- }
66612
66932
  /**
66613
66933
  * This is the base class for creating new layouting engines for the score renderer.
66614
66934
  * @internal
@@ -66639,15 +66959,21 @@
66639
66959
  this.doResize();
66640
66960
  }
66641
66961
  layoutAndRender(renderHints) {
66642
- this._lazyPartials.clear();
66643
66962
  this.slurRegistry.clear();
66644
- this.beamingRuleLookups.clear();
66645
- this._barRendererLookup.clear();
66646
- this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66647
66963
  const score = this.renderer.score;
66648
66964
  this.firstBarIndex = ModelUtils.computeFirstDisplayedBarIndex(score, this.renderer.settings);
66649
66965
  this.lastBarIndex = ModelUtils.computeLastDisplayedBarIndex(score, this.renderer.settings, this.firstBarIndex);
66650
66966
  this.multiBarRestInfo = ModelUtils.buildMultiBarRestInfo(this.renderer.tracks, this.firstBarIndex, this.lastBarIndex);
66967
+ const firstChangedMasterBar = renderHints?.firstChangedMasterBar;
66968
+ if (firstChangedMasterBar !== undefined) {
66969
+ if (this.doUpdateForBars(renderHints)) {
66970
+ return;
66971
+ }
66972
+ }
66973
+ this._lazyPartials.clear();
66974
+ this.beamingRuleLookups.clear();
66975
+ this._barRendererLookup.clear();
66976
+ this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
66651
66977
  this.pagePadding = this.renderer.settings.display.padding.map(p => p / this.renderer.settings.display.scale);
66652
66978
  if (!this.pagePadding) {
66653
66979
  this.pagePadding = [0, 0, 0, 0];
@@ -66662,6 +66988,9 @@
66662
66988
  this.doLayoutAndRender(renderHints);
66663
66989
  }
66664
66990
  _lazyPartials = new Map();
66991
+ getExistingPartialArgs(id) {
66992
+ return this._lazyPartials.has(id) ? this._lazyPartials.get(id).args : undefined;
66993
+ }
66665
66994
  registerPartial(args, callback) {
66666
66995
  if (args.height === 0) {
66667
66996
  return;
@@ -66680,7 +67009,11 @@
66680
67009
  }
66681
67010
  else {
66682
67011
  // in case of lazy loading -> first register lazy, then notify
66683
- this._lazyPartials.set(args.id, new LazyPartial(args, callback));
67012
+ const partial = {
67013
+ args,
67014
+ renderCallback: callback
67015
+ };
67016
+ this._lazyPartials.set(args.id, partial);
66684
67017
  this.renderer.partialLayoutFinished.trigger(args);
66685
67018
  }
66686
67019
  }
@@ -66975,7 +67308,7 @@
66975
67308
  glyph.textAlign = TextAlign.Left;
66976
67309
  }
66977
67310
  }
66978
- layoutAndRenderAnnotation(y) {
67311
+ _layoutAndRenderAnnotation(y) {
66979
67312
  // attention, you are not allowed to remove change this notice within any version of this library without permission!
66980
67313
  const msg = 'rendered by alphaTab';
66981
67314
  const resources = this.renderer.settings.display.resources;
@@ -67039,6 +67372,12 @@
67039
67372
  }
67040
67373
  doResize() {
67041
67374
  }
67375
+ doUpdateForBars(_renderHints) {
67376
+ // not supported yet, modifications likely cause anyhow full updates
67377
+ // as we do not optimize effect bands yet. with effect bands being more
67378
+ // isolated in bars we could try updating dynamically
67379
+ return false;
67380
+ }
67042
67381
  doLayoutAndRender(renderHints) {
67043
67382
  const score = this.renderer.score;
67044
67383
  let startIndex = this.renderer.settings.display.startBar;
@@ -67052,6 +67391,11 @@
67052
67391
  endBarIndex = startIndex + endBarIndex - 1; // map count to array index
67053
67392
  endBarIndex = Math.min(score.masterBars.length - 1, Math.max(0, endBarIndex));
67054
67393
  this._system = this.createEmptyStaffSystem(0);
67394
+ // Each bar in horizontal layout is sized independently (by bar.displayWidth or the bar's
67395
+ // intrinsic width), so there is no shared staff width to distribute across bars. Keep each
67396
+ // bar's spring constants referenced against its own local minimum-duration so rendering
67397
+ // matches the historical per-bar behaviour.
67398
+ this._system.shareMinDurationAcrossBars = false;
67055
67399
  this._system.isLast = true;
67056
67400
  this._system.x = this.pagePadding[0];
67057
67401
  this._system.y = this.pagePadding[1];
@@ -67113,7 +67457,7 @@
67113
67457
  currentBarIndex += partial.masterBars.length;
67114
67458
  }
67115
67459
  this.height = this.layoutAndRenderBottomScoreInfo(this.height);
67116
- this.height = this.layoutAndRenderAnnotation(this.height);
67460
+ this.height = this._layoutAndRenderAnnotation(this.height);
67117
67461
  this.height += this.pagePadding[3];
67118
67462
  this.height *= this.renderer.settings.display.scale;
67119
67463
  }
@@ -67180,11 +67524,16 @@
67180
67524
  _allMasterBarRenderers = [];
67181
67525
  _barsFromPreviousSystem = [];
67182
67526
  _reuseViewPort = false;
67527
+ _preSystemPartialIds = [];
67528
+ _systemPartialIds = [];
67183
67529
  doLayoutAndRender(renderHints) {
67184
67530
  let y = this.pagePadding[1];
67185
67531
  this.width = this.renderer.width;
67186
67532
  this._allMasterBarRenderers = [];
67533
+ this._preSystemPartialIds = [];
67534
+ this._systemPartialIds = [];
67187
67535
  this._reuseViewPort = renderHints?.reuseViewport ?? false;
67536
+ this._systems = [];
67188
67537
  //
67189
67538
  // 1. Score Info
67190
67539
  y = this._layoutAndRenderScoreInfo(y, -1);
@@ -67196,15 +67545,23 @@
67196
67545
  y = this._layoutAndRenderChordDiagrams(y, -1);
67197
67546
  //
67198
67547
  // 4. One result per StaffSystem
67199
- y = this._layoutAndRenderScore(y);
67548
+ y = this._layoutAndRenderScore(y, this.firstBarIndex);
67200
67549
  y = this.layoutAndRenderBottomScoreInfo(y);
67201
- y = this.layoutAndRenderAnnotation(y);
67550
+ y = this._layoutAndRenderAnnotation(y);
67202
67551
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67203
67552
  }
67204
67553
  registerPartial(args, callback) {
67205
67554
  args.reuseViewport = this._reuseViewPort;
67206
67555
  super.registerPartial(args, callback);
67207
67556
  }
67557
+ reregisterPartial(id) {
67558
+ const args = this.getExistingPartialArgs(id);
67559
+ if (!args) {
67560
+ return;
67561
+ }
67562
+ args.reuseViewport = this._reuseViewPort;
67563
+ this.renderer.partialLayoutFinished.trigger(args);
67564
+ }
67208
67565
  get supportsResize() {
67209
67566
  return true;
67210
67567
  }
@@ -67215,6 +67572,47 @@
67215
67572
  }
67216
67573
  return x;
67217
67574
  }
67575
+ doUpdateForBars(renderHints) {
67576
+ this._reuseViewPort = renderHints.reuseViewport ?? false;
67577
+ const firstModifiedMasterBar = renderHints.firstChangedMasterBar;
67578
+ // first update existing systems as needed
67579
+ const systemIndex = this._systems.findIndex(s => {
67580
+ const first = s.masterBarsRenderers[0].masterBar.index;
67581
+ const last = s.masterBarsRenderers[s.masterBarsRenderers.length - 1].masterBar.index;
67582
+ return first <= firstModifiedMasterBar && firstModifiedMasterBar <= last;
67583
+ });
67584
+ if (systemIndex === -1 || !this.renderer.settings.core.enableLazyLoading) {
67585
+ return false;
67586
+ }
67587
+ // Bars from the start of the re-layouted system onward will be re-registered during the
67588
+ // paint pass. Clear their old entries from the preserved BoundsLookup so registration
67589
+ // produces a clean, complete lookup after this render finishes.
67590
+ const firstRebuiltBarIndex = this._systems[systemIndex].masterBarsRenderers[0].masterBar.index;
67591
+ this.renderer.boundsLookup.clearFromMasterBar(firstRebuiltBarIndex);
67592
+ // for now we do a full relayout from the first modified masterbar
67593
+ // there is a lot of room for even more performant updates, but they come
67594
+ // at a risk that features break.
67595
+ // e.g. we could only shift systems where the content didn't change,
67596
+ // but we might still have ties/slurs which have to be updated.
67597
+ const removeSystems = this._systems.splice(systemIndex, this._systems.length - systemIndex);
67598
+ this._systemPartialIds.splice(systemIndex, this._systemPartialIds.length - systemIndex);
67599
+ const system = removeSystems[0];
67600
+ let y = system.y;
67601
+ const firstBarIndex = system.masterBarsRenderers[0].masterBar.index;
67602
+ // signal all partials which didn't change
67603
+ for (const preSystemPartial of this._preSystemPartialIds) {
67604
+ this.reregisterPartial(preSystemPartial);
67605
+ }
67606
+ for (let i = 0; i < systemIndex; i++) {
67607
+ this.reregisterPartial(this._systemPartialIds[i]);
67608
+ }
67609
+ // new partials for all other prats
67610
+ y = this._layoutAndRenderScore(y, firstBarIndex);
67611
+ y = this.layoutAndRenderBottomScoreInfo(y);
67612
+ y = this._layoutAndRenderAnnotation(y);
67613
+ this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67614
+ return true;
67615
+ }
67218
67616
  doResize() {
67219
67617
  let y = this.pagePadding[1];
67220
67618
  this.width = this.renderer.width;
@@ -67233,7 +67631,7 @@
67233
67631
  // 4. One result per StaffSystem
67234
67632
  y = this._resizeAndRenderScore(y, oldHeight);
67235
67633
  y = this.layoutAndRenderBottomScoreInfo(y);
67236
- y = this.layoutAndRenderAnnotation(y);
67634
+ y = this._layoutAndRenderAnnotation(y);
67237
67635
  this.height = (y + this.pagePadding[3]) * this.renderer.settings.display.scale;
67238
67636
  }
67239
67637
  _layoutAndRenderTunings(y, totalHeight = -1) {
@@ -67257,6 +67655,7 @@
67257
67655
  canvas.textAlign = TextAlign.Center;
67258
67656
  this.tuningGlyph.paint(0, 0, canvas);
67259
67657
  });
67658
+ this._preSystemPartialIds.push(e.id);
67260
67659
  return y + tuningHeight;
67261
67660
  }
67262
67661
  _layoutAndRenderChordDiagrams(y, totalHeight = -1) {
@@ -67280,6 +67679,7 @@
67280
67679
  canvas.textAlign = TextAlign.Center;
67281
67680
  this.chordDiagrams.paint(0, 0, canvas);
67282
67681
  });
67682
+ this._preSystemPartialIds.push(e.id);
67283
67683
  return y + diagramHeight;
67284
67684
  }
67285
67685
  _layoutAndRenderScoreInfo(y, totalHeight = -1) {
@@ -67322,12 +67722,14 @@
67322
67722
  g.paint(0, 0, canvas);
67323
67723
  }
67324
67724
  });
67725
+ this._preSystemPartialIds.push(e.id);
67325
67726
  }
67326
67727
  return y + infoHeight;
67327
67728
  }
67328
67729
  _resizeAndRenderScore(y, oldHeight) {
67329
67730
  // if we have a fixed number of bars per row, we only need to refit them.
67330
67731
  const barsPerRowActive = this.getBarsPerSystem(0) > 0;
67732
+ this._systemPartialIds = [];
67331
67733
  if (barsPerRowActive) {
67332
67734
  for (let i = 0; i < this._systems.length; i++) {
67333
67735
  const system = this._systems[i];
@@ -67390,11 +67792,9 @@
67390
67792
  }
67391
67793
  return y;
67392
67794
  }
67393
- _layoutAndRenderScore(y) {
67394
- const startIndex = this.firstBarIndex;
67795
+ _layoutAndRenderScore(y, startIndex) {
67395
67796
  let currentBarIndex = startIndex;
67396
67797
  const endBarIndex = this.lastBarIndex;
67397
- this._systems = [];
67398
67798
  while (currentBarIndex <= endBarIndex) {
67399
67799
  // create system and align set proper coordinates
67400
67800
  const system = this._createStaffSystem(currentBarIndex, endBarIndex);
@@ -67429,6 +67829,7 @@
67429
67829
  // since we use partial drawing
67430
67830
  system.paint(0, -(args.y / this.renderer.settings.display.scale), canvas);
67431
67831
  });
67832
+ this._systemPartialIds.push(args.id);
67432
67833
  // calculate coordinates for next system
67433
67834
  return height;
67434
67835
  }
@@ -67436,6 +67837,10 @@
67436
67837
  * Realignes the bars in this line according to the available space
67437
67838
  */
67438
67839
  _fitSystem(system) {
67840
+ // If a bar added late in the assembly introduced a shorter note than earlier bars, the
67841
+ // earlier bars' spring constants (and the cached system widths / totals) are stale.
67842
+ // Reconcile now - it's a no-op when nothing changed.
67843
+ system.reconcileMinDurationIfDirty();
67439
67844
  if (system.isFull || system.width > this._maxWidth || this.renderer.settings.display.justifyLastSystem) {
67440
67845
  this._scaleToWidth(system, this._maxWidth);
67441
67846
  }
@@ -67447,29 +67852,35 @@
67447
67852
  _scaleToWidth(system, width) {
67448
67853
  const staffWidth = width - system.accoladeWidth;
67449
67854
  const shouldApplyBarScale = this.shouldApplyBarScale;
67450
- const totalScale = system.totalBarDisplayScale;
67451
- // NOTE: it currently delivers best results if we evenly distribute the available space across bars
67452
- // scaling bars relatively to their computed width, rather causes distortions whenever bars have
67453
- // pre-beat glyphs.
67454
- // most precise scaling would come if we use the contents (voiceContainerGlyph) width as a calculation
67455
- // factor. but this would make the calculation additionally complex with not much gain.
67456
- const difference = width - system.computedWidth;
67457
- const spacePerBar = difference / system.masterBarsRenderers.length;
67855
+ // Industry fixed-overhead model (Behind Bars, Dorico, Finale, Sibelius, MuseScore, Guitar Pro):
67856
+ // prefix/postfix glyphs (clef, key sig, time sig, barlines) are treated as fixed overhead and the
67857
+ // remaining staff width is distributed across bars by a per-bar weight.
67858
+ //
67859
+ // distributable = staffWidth - totalFixedOverhead
67860
+ // contentShare = distributable / sum(weight)
67861
+ // bar.width = bar.maxFixedOverhead + weight * contentShare
67862
+ //
67863
+ // The weight depends on the layout mode:
67864
+ // - shouldApplyBarScale=true -> weight = bar.displayScale (model-driven, matches Guitar Pro)
67865
+ // displayScale defaults to 1, so an unset value behaves identically
67866
+ // to an explicit 1 (GP omits the property when not customized).
67867
+ // - shouldApplyBarScale=false -> weight = natural content width (automatic, ignores displayScale)
67868
+ //
67869
+ // Per-bar maxFixedOverhead / maxContentWidth and the system-wide totals are maintained incrementally
67870
+ // in StaffSystem._applyLayoutAndUpdateWidth / revertLastBar so this pass can apply directly.
67871
+ const weightTotal = shouldApplyBarScale ? system.totalBarDisplayScale : system.totalContentWidth;
67872
+ const distributable = Math.max(0, staffWidth - system.totalFixedOverhead);
67873
+ const contentShare = weightTotal > 0 ? distributable / weightTotal : 0;
67458
67874
  for (const s of system.allStaves) {
67459
67875
  s.resetSharedLayoutData();
67460
- // scale the bars by keeping their respective ratio size
67461
67876
  let w = 0;
67462
- for (const renderer of s.barRenderers) {
67877
+ for (let i = 0; i < s.barRenderers.length; i++) {
67878
+ const renderer = s.barRenderers[i];
67879
+ const mb = system.masterBarsRenderers[i];
67463
67880
  renderer.x = w;
67464
67881
  renderer.y = s.topPadding + s.topOverflow;
67465
- let actualBarWidth;
67466
- if (shouldApplyBarScale) {
67467
- const barDisplayScale = system.getBarDisplayScale(renderer);
67468
- actualBarWidth = (barDisplayScale * staffWidth) / totalScale;
67469
- }
67470
- else {
67471
- actualBarWidth = renderer.computedWidth + spacePerBar;
67472
- }
67882
+ const weight = shouldApplyBarScale ? system.getBarDisplayScale(renderer) : mb.maxContentWidth;
67883
+ const actualBarWidth = mb.maxFixedOverhead + weight * contentShare;
67473
67884
  renderer.scaleToWidth(actualBarWidth);
67474
67885
  w += renderer.width;
67475
67886
  }
@@ -69216,16 +69627,18 @@
69216
69627
  s = [];
69217
69628
  const zero = MusicFontSymbol.Tuplet0;
69218
69629
  if (num > 10) {
69219
- s.push((zero + Math.floor(num / 10)));
69220
- s.push((zero + (num - 10)));
69630
+ const tens = Math.floor(num / 10);
69631
+ s.push((zero + tens));
69632
+ s.push((zero + (num - 10 * tens)));
69221
69633
  }
69222
69634
  else {
69223
69635
  s.push((zero + num));
69224
69636
  }
69225
69637
  s.push(MusicFontSymbol.TupletColon);
69226
69638
  if (den > 10) {
69227
- s.push((zero + Math.floor(den / 10)));
69228
- s.push((zero + (den - 10)));
69639
+ const tens = Math.floor(den / 10);
69640
+ s.push((zero + tens));
69641
+ s.push((zero + (den - 10 * tens)));
69229
69642
  }
69230
69643
  else {
69231
69644
  s.push((zero + den));
@@ -69291,12 +69704,24 @@
69291
69704
  const firstNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(firstNonRestBeat);
69292
69705
  const lastNonRestBeamingHelper = this.helpers.getBeamingHelperForBeat(lastNonRestBeat);
69293
69706
  const direction = this.getTupletBeamDirection(firstNonRestBeamingHelper);
69294
- let startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69295
- let endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69707
+ let startY;
69708
+ let endY;
69296
69709
  if (isRestOnly) {
69297
- startY = Math.max(startY, endY);
69710
+ // rests have no stems, so anchor to the actual rest glyph bounds
69711
+ // instead of a stem-adjusted flag position (which would place the bracket
69712
+ // a full quarter-stem length away from the rests).
69713
+ if (direction === BeamDirection.Up) {
69714
+ startY = Math.min(this.getRestY(firstNonRestBeat, NoteYPosition.Top), this.getRestY(lastNonRestBeat, NoteYPosition.Top));
69715
+ }
69716
+ else {
69717
+ startY = Math.max(this.getRestY(firstNonRestBeat, NoteYPosition.Bottom), this.getRestY(lastNonRestBeat, NoteYPosition.Bottom));
69718
+ }
69298
69719
  endY = startY;
69299
69720
  }
69721
+ else {
69722
+ startY = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction);
69723
+ endY = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction);
69724
+ }
69300
69725
  // align line centered in available space
69301
69726
  if (direction === BeamDirection.Down) {
69302
69727
  startY += shift;
@@ -69666,7 +70091,30 @@
69666
70091
  let minNoteY = 0;
69667
70092
  for (const v of this.helpers.beamHelpers) {
69668
70093
  for (const h of v) {
69669
- if (!this.shouldPaintBeamingHelper(h)) ;
70094
+ if (!this.shouldPaintBeamingHelper(h)) {
70095
+ // beam is not drawn, but a rest-only tuplet still draws a bracket
70096
+ // anchored to the rest glyph bounds and needs overflow reserved.
70097
+ if (h.hasTuplet && h.isRestBeamHelper) {
70098
+ const tupletGroup = h.beats[0].tupletGroup;
70099
+ const tupletFirst = tupletGroup.beats[0];
70100
+ const tupletLast = tupletGroup.beats[tupletGroup.beats.length - 1];
70101
+ const tupletDirection = this.getTupletBeamDirection(h);
70102
+ if (tupletDirection === BeamDirection.Up) {
70103
+ const restTop = Math.min(this.getRestY(tupletFirst, NoteYPosition.Top), this.getRestY(tupletLast, NoteYPosition.Top));
70104
+ const topY = restTop - this.tupletSize - this.tupletOffset;
70105
+ if (topY < maxNoteY) {
70106
+ maxNoteY = topY;
70107
+ }
70108
+ }
70109
+ else {
70110
+ const restBottom = Math.max(this.getRestY(tupletFirst, NoteYPosition.Bottom), this.getRestY(tupletLast, NoteYPosition.Bottom));
70111
+ const bottomY = restBottom + this.tupletSize + this.tupletOffset;
70112
+ if (bottomY > minNoteY) {
70113
+ minNoteY = bottomY;
70114
+ }
70115
+ }
70116
+ }
70117
+ }
69670
70118
  else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) {
69671
70119
  const tupletDirection = this.getTupletBeamDirection(h);
69672
70120
  const direction = this.getBeamDirection(h);
@@ -72204,8 +72652,10 @@
72204
72652
  const group = new GlyphGroup(0, 0);
72205
72653
  group.renderer = this.renderer;
72206
72654
  for (const note of this.container.beat.notes) {
72207
- const g = this._createBeatDot(sr.getNoteSteps(note), group);
72208
- g.colorOverride = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note);
72655
+ if (note.isVisible) {
72656
+ const g = this._createBeatDot(sr.getNoteSteps(note), group);
72657
+ g.colorOverride = ElementStyleHelper.noteColor(sr.resources, NoteSubElement.StandardNotationEffects, note);
72658
+ }
72209
72659
  }
72210
72660
  this.addEffect(group);
72211
72661
  }