@coderline/alphatab 1.8.0-alpha.1670 → 1.8.0-alpha.1671

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/alphaTab.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * alphaTab v1.8.0-alpha.1670 (develop, build 1670)
2
+ * alphaTab v1.8.0-alpha.1671 (develop, build 1671)
3
3
  *
4
4
  * Copyright © 2026, Daniel Kuschny and Contributors, All rights reserved.
5
5
  *
@@ -209,9 +209,9 @@
209
209
  * @internal
210
210
  */
211
211
  class VersionInfo {
212
- static version = '1.8.0-alpha.1670';
213
- static date = '2026-01-09T02:24:51.076Z';
214
- static commit = 'c4cd0c6435ec1bed778365efd389cb1b52df7ff3';
212
+ static version = '1.8.0-alpha.1671';
213
+ static date = '2026-01-10T02:20:48.787Z';
214
+ static commit = '1edf83d58fe31a924adb1980bc4a529aed07dad4';
215
215
  static print(print) {
216
216
  print(`alphaTab ${VersionInfo.version}`);
217
217
  print(`commit: ${VersionInfo.commit}`);
@@ -2202,6 +2202,12 @@
2202
2202
  * If specified, overrides the value from the stylesheet on score level.
2203
2203
  */
2204
2204
  barNumberDisplay;
2205
+ /**
2206
+ * The shortest duration contained across beats in this bar.
2207
+ * @internal
2208
+ * @json_ignore
2209
+ */
2210
+ shortestDuration = Duration.DoubleWhole;
2205
2211
  /**
2206
2212
  * The bar line to draw on the left side of the bar with an "automatic" type resolved to the actual one.
2207
2213
  * @param isFirstOfSystem Whether the bar is the first one in the system.
@@ -2273,12 +2279,16 @@
2273
2279
  this._filledVoices.add(0);
2274
2280
  this._isEmpty = true;
2275
2281
  this._isRestOnly = true;
2282
+ this.shortestDuration = Duration.DoubleWhole;
2276
2283
  for (let i = 0, j = this.voices.length; i < j; i++) {
2277
2284
  const voice = this.voices[i];
2278
2285
  voice.finish(settings, sharedDataBag);
2279
2286
  if (!voice.isEmpty) {
2280
2287
  this._isEmpty = false;
2281
2288
  this._filledVoices.add(i);
2289
+ if (voice.shortestDuration > this.shortestDuration) {
2290
+ this.shortestDuration = voice.shortestDuration;
2291
+ }
2282
2292
  }
2283
2293
  if (!voice.isRestOnly) {
2284
2294
  this._isRestOnly = false;
@@ -2375,6 +2385,116 @@
2375
2385
  TripletFeel[TripletFeel["Scottish8th"] = 6] = "Scottish8th";
2376
2386
  })(TripletFeel || (TripletFeel = {}));
2377
2387
 
2388
+ /**
2389
+ * Defines the custom beaming rules which define how beats are beamed together or split apart
2390
+ * during the automatic beaming when displayed.
2391
+ * @json
2392
+ * @json_strict
2393
+ * @public
2394
+ *
2395
+ * @remarks
2396
+ * The beaming logic works like this:
2397
+ *
2398
+ * The time axis of the bar is sliced into even chunks. The chunk-size is defined by the respective group definition.
2399
+ * Within these chunks groups can then be placed spanning 1 or more chunks.
2400
+ *
2401
+ * If beats start within the same "group" they are beamed together.
2402
+ */
2403
+ class BeamingRules {
2404
+ _singleGroupKey;
2405
+ /**
2406
+ * The the group for a given "longest duration" within the bar.
2407
+ * @remarks
2408
+ * The map key is the duration to which the bar will be sliced into.
2409
+ * The map value defines the "groups" placed within the sliced.
2410
+ */
2411
+ groups = new Map();
2412
+ /**
2413
+ * @internal
2414
+ * @json_ignore
2415
+ */
2416
+ uniqueId = '';
2417
+ /**
2418
+ * @internal
2419
+ * @json_ignore
2420
+ */
2421
+ timeSignatureNumerator = 0;
2422
+ /**
2423
+ * @internal
2424
+ * @json_ignore
2425
+ */
2426
+ timeSignatureDenominator = 0;
2427
+ /**
2428
+ * @internal
2429
+ */
2430
+ static createSimple(timeSignatureNumerator, timeSignatureDenominator, duration, groups) {
2431
+ const r = new BeamingRules();
2432
+ r.timeSignatureNumerator = timeSignatureNumerator;
2433
+ r.timeSignatureDenominator = timeSignatureDenominator;
2434
+ r.groups.set(duration, groups);
2435
+ r.finish();
2436
+ return r;
2437
+ }
2438
+ /**
2439
+ * @internal
2440
+ */
2441
+ findRule(shortestDuration) {
2442
+ // fast path: one rule -> take it
2443
+ const singleGroupKey = this._singleGroupKey;
2444
+ if (singleGroupKey) {
2445
+ return [singleGroupKey, this.groups.get(singleGroupKey)];
2446
+ }
2447
+ if (shortestDuration < Duration.Quarter) {
2448
+ return [shortestDuration, []];
2449
+ }
2450
+ // first search shorter
2451
+ let durationValue = shortestDuration;
2452
+ do {
2453
+ const duration = durationValue;
2454
+ if (this.groups.has(duration)) {
2455
+ return [duration, this.groups.get(duration)];
2456
+ }
2457
+ durationValue = durationValue * 2;
2458
+ } while (durationValue <= Duration.TwoHundredFiftySixth);
2459
+ // then longer
2460
+ durationValue = shortestDuration / 2;
2461
+ do {
2462
+ const duration = durationValue;
2463
+ if (this.groups.has(duration)) {
2464
+ return [duration, this.groups.get(duration)];
2465
+ }
2466
+ durationValue = durationValue / 2;
2467
+ } while (durationValue > Duration.Half);
2468
+ return [shortestDuration, []];
2469
+ }
2470
+ /**
2471
+ * @internal
2472
+ */
2473
+ finish() {
2474
+ let uniqueId = `${this.timeSignatureNumerator}_${this.timeSignatureDenominator}`;
2475
+ for (const [k, v] of this.groups) {
2476
+ uniqueId += `__${k}`;
2477
+ // trim of 0s at the end of the group
2478
+ let lastZero = v.length;
2479
+ for (let i = v.length - 1; i >= 0; i--) {
2480
+ if (v[i] === 0) {
2481
+ lastZero = i;
2482
+ }
2483
+ else {
2484
+ break;
2485
+ }
2486
+ }
2487
+ if (lastZero < v.length) {
2488
+ v.splice(lastZero, v.length - lastZero);
2489
+ }
2490
+ uniqueId += `_${v.join('_')}`;
2491
+ if (this.groups.size === 1) {
2492
+ this._singleGroupKey = k;
2493
+ }
2494
+ }
2495
+ this.uniqueId = uniqueId;
2496
+ }
2497
+ }
2378
2498
  /**
2379
2499
  * The MasterBar stores information about a bar which affects
2380
2500
  * all tracks.
@@ -2492,6 +2612,16 @@
2492
2612
  * Gets or sets whether this is bar has a common time signature.
2493
2613
  */
2494
2614
  timeSignatureCommon = false;
2615
+ /**
2616
+ * Defines the custom beaming rules which should be applied to this bar and all bars following.
2617
+ */
2618
+ beamingRules;
2619
+ /**
2620
+ * The actual (custom) beaming rules to use for this bar if any were specified.
2621
+ * @json_ignore
2622
+ * @internal
2623
+ */
2624
+ actualBeamingRules;
2495
2625
  /**
2496
2626
  * Gets or sets whether the bar indicates a free time playing.
2497
2627
  */
@@ -2624,6 +2754,32 @@
2624
2754
  }
2625
2755
  this.syncPoints.push(syncPoint);
2626
2756
  }
2757
+ finish(sharedDataBag) {
2758
+ let beamingRules = this.beamingRules;
2759
+ if (beamingRules) {
2760
+ beamingRules.timeSignatureNumerator = this.timeSignatureNumerator;
2761
+ beamingRules.timeSignatureDenominator = this.timeSignatureDenominator;
2762
+ beamingRules.finish();
2763
+ }
2764
+ if (this.index > 0) {
2765
+ this.start = this.previousMasterBar.start + this.previousMasterBar.calculateDuration();
2766
+ // clear out equal rules to reduce memory consumption.
2767
+ const previousRules = sharedDataBag.has('beamingRules')
2768
+ ? sharedDataBag.get('beamingRules')
2769
+ : undefined;
2770
+ if (previousRules && previousRules.uniqueId === beamingRules?.uniqueId) {
2771
+ this.beamingRules = undefined;
2772
+ beamingRules = previousRules;
2773
+ }
2774
+ else if (!beamingRules) {
2775
+ beamingRules = previousRules;
2776
+ }
2777
+ }
2778
+ this.actualBeamingRules = beamingRules;
2779
+ if (this.beamingRules) {
2780
+ sharedDataBag.set('beamingRules', beamingRules);
2781
+ }
2782
+ }
2627
2783
  }
2628
2784
 
2629
2785
  /**
@@ -2974,6 +3130,12 @@
2974
3130
  get isRestOnly() {
2975
3131
  return this._isRestOnly;
2976
3132
  }
3133
+ /**
3134
+ * The shortest duration contained across beats in this bar.
3135
+ * @internal
3136
+ * @json_ignore
3137
+ */
3138
+ shortestDuration = Duration.DoubleWhole;
2977
3139
  insertBeat(after, newBeat) {
2978
3140
  newBeat.nextBeat = after.nextBeat;
2979
3141
  if (newBeat.nextBeat) {
@@ -3067,6 +3229,7 @@
3067
3229
  }
3068
3230
  let currentDisplayTick = 0;
3069
3231
  let currentPlaybackTick = 0;
3232
+ this.shortestDuration = Duration.DoubleWhole;
3070
3233
  for (let i = 0; i < this.beats.length; i++) {
3071
3234
  const beat = this.beats[i];
3072
3235
  beat.index = i;
@@ -3075,6 +3238,9 @@
3075
3238
  // we need to first steal the duration from the right beat
3076
3239
  // and place the grace beats correctly
3077
3240
  if (beat.graceType === GraceType.None) {
3241
+ if (beat.duration > this.shortestDuration) {
3242
+ this.shortestDuration = beat.duration;
3243
+ }
3078
3244
  if (beat.graceGroup) {
3079
3245
  const firstGraceBeat = beat.graceGroup.beats[0];
3080
3246
  const lastGraceBeat = beat.graceGroup.beats[beat.graceGroup.beats.length - 1];
@@ -3535,7 +3701,7 @@
3535
3701
  bar.previousMasterBar.nextMasterBar = bar;
3536
3702
  // NOTE: this will not work on anacrusis. Correct anacrusis durations are only working
3537
3703
  // when there are beats with playback positions already computed which requires full finish
3538
- // chicken-egg problem here. temporarily forcing anacrusis length here to 0,
3704
+ // chicken-egg problem here. temporarily forcing anacrusis length here to 0,
3539
3705
  // .finish() will correct these times
3540
3706
  bar.start =
3541
3707
  bar.previousMasterBar.start +
@@ -3599,9 +3765,7 @@
3599
3765
  }
3600
3766
  // fixup masterbar starts to handle anacrusis lengths
3601
3767
  for (const mb of this.masterBars) {
3602
- if (mb.index > 0) {
3603
- mb.start = mb.previousMasterBar.start + mb.previousMasterBar.calculateDuration();
3604
- }
3768
+ mb.finish(sharedDataBag);
3605
3769
  }
3606
3770
  }
3607
3771
  /**
@@ -8970,7 +9134,16 @@
8970
9134
  ['spu', [[[[16], 2]]]],
8971
9135
  ['db', null],
8972
9136
  ['voicemode', [[[[10, 17], 0, ['staffwise', 'barwise']]]]],
8973
- ['barnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]]
9137
+ ['barnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]],
9138
+ [
9139
+ 'beaming',
9140
+ [
9141
+ [
9142
+ [[16], 0],
9143
+ [[16], 5]
9144
+ ]
9145
+ ]
9146
+ ]
8974
9147
  ]);
8975
9148
  static metaDataProperties = AlphaTex1LanguageDefinitions._metaProps([
8976
9149
  [
@@ -9086,7 +9259,8 @@
9086
9259
  ['spu', null],
9087
9260
  ['db', null],
9088
9261
  ['voicemode', null],
9089
- ['barnumberdisplay', null]
9262
+ ['barnumberdisplay', null],
9263
+ ['beaming', null]
9090
9264
  ]);
9091
9265
  static metaDataSignatures = [
9092
9266
  AlphaTex1LanguageDefinitions.scoreMetaDataSignatures,
@@ -12801,6 +12975,7 @@
12801
12975
  */
12802
12976
  class AlphaTex1LanguageHandler {
12803
12977
  static instance = new AlphaTex1LanguageHandler();
12978
+ static _timeSignatureDenominators = new Set([1, 2, 4, 8, 16, 32, 64, 128]);
12804
12979
  applyScoreMetaData(importer, score, metaData) {
12805
12980
  const result = this._checkArgumentTypes(importer, [AlphaTex1LanguageDefinitions.scoreMetaDataSignatures], metaData, metaData.tag.tag.text.toLowerCase(), metaData.arguments);
12806
12981
  if (result !== undefined) {
@@ -13222,8 +13397,29 @@
13222
13397
  case 'ts':
13223
13398
  switch (metaData.arguments.arguments[0].nodeType) {
13224
13399
  case AlphaTexNodeType.Number:
13225
- bar.masterBar.timeSignatureNumerator = metaData.arguments.arguments[0].value;
13226
- bar.masterBar.timeSignatureDenominator = metaData.arguments.arguments[1].value;
13400
+ bar.masterBar.timeSignatureNumerator =
13401
+ metaData.arguments.arguments[0].value | 0;
13402
+ if (bar.masterBar.timeSignatureNumerator < 1 || bar.masterBar.timeSignatureNumerator > 32) {
13403
+ importer.addSemanticDiagnostic({
13404
+ code: AlphaTexDiagnosticCode.AT211,
13405
+ message: `Value is out of valid range. Allowed range: 1-32, Actual Value: ${bar.masterBar.timeSignatureNumerator}`,
13406
+ start: metaData.arguments.arguments[0].start,
13407
+ end: metaData.arguments.arguments[0].end,
13408
+ severity: AlphaTexDiagnosticsSeverity.Error
13409
+ });
13410
+ }
13411
+ bar.masterBar.timeSignatureDenominator =
13412
+ metaData.arguments.arguments[1].value | 0;
13413
+ if (!AlphaTex1LanguageHandler._timeSignatureDenominators.has(bar.masterBar.timeSignatureDenominator)) {
13414
+ const valueList = Array.from(AlphaTex1LanguageHandler._timeSignatureDenominators).join(', ');
13415
+ importer.addSemanticDiagnostic({
13416
+ code: AlphaTexDiagnosticCode.AT211,
13417
+ message: `Value is out of valid range. Allowed range: ${valueList}, Actual Value: ${bar.masterBar.timeSignatureDenominator}`,
13418
+ start: metaData.arguments.arguments[0].start,
13419
+ end: metaData.arguments.arguments[0].end,
13420
+ severity: AlphaTexDiagnosticsSeverity.Error
13421
+ });
13422
+ }
13227
13423
  break;
13228
13424
  case AlphaTexNodeType.Ident:
13229
13425
  case AlphaTexNodeType.String:
@@ -13246,6 +13442,8 @@
13246
13442
  break;
13247
13443
  }
13248
13444
  return ApplyNodeResult.Applied;
13445
+ case 'beaming':
13446
+ return this._parseBeamingRule(importer, metaData, bar.masterBar);
13249
13447
  case 'ks':
13250
13448
  const keySignature = AlphaTex1LanguageHandler._parseEnumValue(importer, metaData.arguments, 'key signature', AlphaTex1EnumMappings.keySignature);
13251
13449
  if (keySignature === undefined) {
@@ -13446,6 +13644,53 @@
13446
13644
  return ApplyNodeResult.NotAppliedUnrecognizedMarker;
13447
13645
  }
13448
13646
  }
13647
+ _parseBeamingRule(importer, metaData, masterBar) {
13648
+ let duration = Duration.Eighth;
13649
+ const groupSizes = [];
13650
+ const durationValue = metaData.arguments.arguments[0].value;
13651
+ switch (durationValue) {
13652
+ case 4:
13653
+ duration = Duration.QuadrupleWhole;
13654
+ break;
13655
+ case 8:
13656
+ duration = Duration.Eighth;
13657
+ break;
13658
+ case 16:
13659
+ duration = Duration.Sixteenth;
13660
+ break;
13661
+ case 32:
13662
+ duration = Duration.ThirtySecond;
13663
+ break;
13664
+ default:
13665
+ importer.addSemanticDiagnostic({
13666
+ code: AlphaTexDiagnosticCode.AT209,
13667
+ message: `Value is out of valid range. Allowed range: 4,8,16 or 32, Actual Value: ${durationValue}`,
13668
+ severity: AlphaTexDiagnosticsSeverity.Error,
13669
+ start: metaData.arguments.arguments[0].start,
13670
+ end: metaData.arguments.arguments[0].end
13671
+ });
13672
+ return ApplyNodeResult.NotAppliedSemanticError;
13673
+ }
13674
+ for (let i = 1; i < metaData.arguments.arguments.length; i++) {
13675
+ const groupSize = metaData.arguments.arguments[i].value;
13676
+ if (groupSize < 1) {
13677
+ importer.addSemanticDiagnostic({
13678
+ code: AlphaTexDiagnosticCode.AT209,
13679
+ message: `Value is out of valid range. Allowed range: >0, Actual Value: ${durationValue}`,
13680
+ severity: AlphaTexDiagnosticsSeverity.Error,
13681
+ start: metaData.arguments.arguments[i].start,
13682
+ end: metaData.arguments.arguments[i].end
13683
+ });
13684
+ return ApplyNodeResult.NotAppliedSemanticError;
13685
+ }
13686
+ groupSizes.push(metaData.arguments.arguments[i].value);
13687
+ }
13688
+ if (!masterBar.beamingRules) {
13689
+ masterBar.beamingRules = new BeamingRules();
13690
+ }
13691
+ masterBar.beamingRules.groups.set(duration, groupSizes);
13692
+ return ApplyNodeResult.Applied;
13693
+ }
13449
13694
  static _handleAccidentalMode(importer, args) {
13450
13695
  const accidentalMode = AlphaTex1LanguageHandler._parseEnumValue(importer, args, 'accidental mode', AlphaTex1EnumMappings.alphaTexAccidentalMode);
13451
13696
  if (accidentalMode === undefined) {
@@ -15038,6 +15283,15 @@
15038
15283
  ])));
15039
15284
  }
15040
15285
  }
15286
+ if (masterBar.beamingRules) {
15287
+ for (const [k, v] of masterBar.beamingRules.groups) {
15288
+ const args = Atnf.args([Atnf.number(k)], true);
15289
+ for (const i of v) {
15290
+ args.arguments.push(Atnf.number(i));
15291
+ }
15292
+ nodes.push(Atnf.meta('beaming', args));
15293
+ }
15294
+ }
15041
15295
  if ((masterBar.index > 0 && masterBar.tripletFeel !== masterBar.previousMasterBar?.tripletFeel) ||
15042
15296
  (masterBar.index === 0 && masterBar.tripletFeel !== TripletFeel.NoTripletFeel)) {
15043
15297
  nodes.push(Atnf.identMeta('tf', AlphaTex1EnumMappings.tripletFeelReversed.get(masterBar.tripletFeel)));
@@ -23038,6 +23292,8 @@
23038
23292
  }
23039
23293
  }
23040
23294
  _parseMasterBarXProperties(masterBar, node) {
23295
+ let beamingRuleDuration = Number.NaN;
23296
+ let beamingRuleGroups = undefined;
23041
23297
  for (const c of node.childElements()) {
23042
23298
  switch (c.localName) {
23043
23299
  case 'XProperty':
@@ -23046,10 +23302,32 @@
23046
23302
  case '1124073984':
23047
23303
  masterBar.displayScale = GpifParser._parseFloatSafe(c.findChildElement('Double')?.innerText, 1);
23048
23304
  break;
23305
+ case '1124139010':
23306
+ beamingRuleDuration = GpifParser._parseIntSafe(c.findChildElement('Int')?.innerText, Number.NaN);
23307
+ break;
23308
+ default:
23309
+ const idNumeric = GpifParser._parseIntSafe(id, 0);
23310
+ if (idNumeric >= 1124139264 && idNumeric <= 1124139295) {
23311
+ const groupIndex = idNumeric - 1124139264;
23312
+ const groupSize = GpifParser._parseIntSafe(c.findChildElement('Int')?.innerText, Number.NaN);
23313
+ if (beamingRuleGroups === undefined) {
23314
+ beamingRuleGroups = [];
23315
+ }
23316
+ while (beamingRuleGroups.length < groupIndex + 1) {
23317
+ beamingRuleGroups.push(0);
23318
+ }
23319
+ beamingRuleGroups[groupIndex] = groupSize;
23320
+ }
23321
+ break;
23049
23322
  }
23050
23323
  break;
23051
23324
  }
23052
23325
  }
23326
+ if (!Number.isNaN(beamingRuleDuration) && beamingRuleGroups) {
23327
+ const rules = new BeamingRules();
23328
+ rules.groups.set(beamingRuleDuration, beamingRuleGroups);
23329
+ masterBar.beamingRules = rules;
23330
+ }
23053
23331
  }
23054
23332
  _parseBeatProperties(node, beat) {
23055
23333
  let isWhammy = false;
@@ -36585,6 +36863,43 @@
36585
36863
  }
36586
36864
  }
36587
36865
 
36866
+ /**
36867
+ * @internal
36868
+ */
36869
+ class BeamingRulesSerializer {
36870
+ static fromJson(obj, m) {
36871
+ if (!m) {
36872
+ return;
36873
+ }
36874
+ JsonHelper.forEach(m, (v, k) => BeamingRulesSerializer.setProperty(obj, k, v));
36875
+ }
36876
+ static toJson(obj) {
36877
+ if (!obj) {
36878
+ return null;
36879
+ }
36880
+ const o = new Map();
36881
+ {
36882
+ const m = new Map();
36883
+ o.set("groups", m);
36884
+ for (const [k, v] of obj.groups) {
36885
+ m.set(k.toString(), v);
36886
+ }
36887
+ }
36888
+ return o;
36889
+ }
36890
+ static setProperty(obj, property, v) {
36891
+ switch (property) {
36892
+ case "groups":
36893
+ obj.groups = new Map();
36894
+ JsonHelper.forEach(v, (v, k) => {
36895
+ obj.groups.set(JsonHelper.parseEnum(k, Duration), v);
36896
+ });
36897
+ return true;
36898
+ }
36899
+ return false;
36900
+ }
36901
+ }
36902
+
36588
36903
  /**
36589
36904
  * @internal
36590
36905
  */
@@ -36763,6 +37078,9 @@
36763
37078
  o.set("timesignaturenumerator", obj.timeSignatureNumerator);
36764
37079
  o.set("timesignaturedenominator", obj.timeSignatureDenominator);
36765
37080
  o.set("timesignaturecommon", obj.timeSignatureCommon);
37081
+ if (obj.beamingRules) {
37082
+ o.set("beamingrules", BeamingRulesSerializer.toJson(obj.beamingRules));
37083
+ }
36766
37084
  o.set("isfreetime", obj.isFreeTime);
36767
37085
  o.set("tripletfeel", obj.tripletFeel);
36768
37086
  if (obj.section) {
@@ -36815,6 +37133,15 @@
36815
37133
  case "timesignaturecommon":
36816
37134
  obj.timeSignatureCommon = v;
36817
37135
  return true;
37136
+ case "beamingrules":
37137
+ if (v) {
37138
+ obj.beamingRules = new BeamingRules();
37139
+ BeamingRulesSerializer.fromJson(obj.beamingRules, v);
37140
+ }
37141
+ else {
37142
+ obj.beamingRules = undefined;
37143
+ }
37144
+ return true;
36818
37145
  case "isfreetime":
36819
37146
  obj.isFreeTime = v;
36820
37147
  return true;
@@ -61473,6 +61800,7 @@
61473
61800
  class BeamingHelper {
61474
61801
  _staff;
61475
61802
  _renderer;
61803
+ _beamingRuleLookup;
61476
61804
  voice = null;
61477
61805
  beats = [];
61478
61806
  shortestDuration = Duration.QuadrupleWhole;
@@ -61506,10 +61834,11 @@
61506
61834
  static beatHasFlag(beat) {
61507
61835
  return (!beat.deadSlapped && !beat.isRest && (beat.duration > Duration.Quarter || beat.graceType !== GraceType.None));
61508
61836
  }
61509
- constructor(staff, renderer) {
61837
+ constructor(staff, renderer, beamingRuleLookup) {
61510
61838
  this._staff = staff;
61511
61839
  this._renderer = renderer;
61512
61840
  this.beats = [];
61841
+ this._beamingRuleLookup = beamingRuleLookup;
61513
61842
  }
61514
61843
  alignWithBeats() {
61515
61844
  for (const v of this.drawingInfos.values()) {
@@ -61564,7 +61893,7 @@
61564
61893
  switch (this.beats[this.beats.length - 1].beamingMode) {
61565
61894
  case BeatBeamingMode.Auto:
61566
61895
  case BeatBeamingMode.ForceSplitOnSecondaryToNext:
61567
- add = BeamingHelper._canJoin(this.beats[this.beats.length - 1], beat);
61896
+ add = this._canJoin(this.beats[this.beats.length - 1], beat);
61568
61897
  break;
61569
61898
  case BeatBeamingMode.ForceSplitToNext:
61570
61899
  add = false;
@@ -61636,8 +61965,7 @@
61636
61965
  this._highestNoteCompareValueInHelper = highestValueForNote;
61637
61966
  }
61638
61967
  }
61639
- // TODO: Check if this beaming is really correct, I'm not sure if we are connecting beats correctly
61640
- static _canJoin(b1, b2) {
61968
+ _canJoin(b1, b2) {
61641
61969
  // is this a voice we can join with?
61642
61970
  if (!b1 ||
61643
61971
  !b2 ||
@@ -61675,19 +62003,10 @@
61675
62003
  return true;
61676
62004
  }
61677
62005
  }
61678
- // TODO: create more rules for automatic beaming
61679
- let divisionLength = MidiUtils.QuarterTime;
61680
- switch (m1.masterBar.timeSignatureDenominator) {
61681
- case 8:
61682
- if (m1.masterBar.timeSignatureNumerator % 3 === 0) {
61683
- divisionLength += (MidiUtils.QuarterTime / 2) | 0;
61684
- }
61685
- break;
61686
- }
61687
- // check if they are on the same division
61688
- const division1 = ((divisionLength + start1) / divisionLength) | 0 | 0;
61689
- const division2 = ((divisionLength + start2) / divisionLength) | 0 | 0;
61690
- return division1 === division2;
62006
+ // check if they are on the same group as per rule definitions
62007
+ const groupId1 = this._beamingRuleLookup.calculateGroupIndex(start1);
62008
+ const groupId2 = this._beamingRuleLookup.calculateGroupIndex(start2);
62009
+ return groupId1 === groupId2;
61691
62010
  }
61692
62011
  static _canJoinDuration(d) {
61693
62012
  switch (d) {
@@ -61854,6 +62173,61 @@
61854
62173
  }
61855
62174
  }
61856
62175
 
62176
+ /**
62177
+ * @internal
62178
+ */
62179
+ class BeamingRuleLookup {
62180
+ _division = 0;
62181
+ _slots = [];
62182
+ _barDuration;
62183
+ constructor(barDuration, division, slots) {
62184
+ this._division = division;
62185
+ this._slots = slots;
62186
+ this._barDuration = barDuration;
62187
+ }
62188
+ calculateGroupIndex(beatStartTime) {
62189
+ // no slots -> all have their own group based (use the start time as index)
62190
+ if (this._slots.length === 0) {
62191
+ return beatStartTime;
62192
+ }
62193
+ // rollover within the bar.
62194
+ beatStartTime = beatStartTime % this._barDuration;
62195
+ const slotIndex = Math.floor(beatStartTime / this._division);
62196
+ return this._slots[slotIndex];
62197
+ }
62198
+ static build(masterBar, ruleDuration, ruleGroups) {
62199
+ const totalDuration = masterBar.calculateDuration(false);
62200
+ const division = MidiUtils.toTicks(ruleDuration);
62201
+ const slotCount = totalDuration / division;
62202
+ // should only happen in case of improper data.
62203
+ if (slotCount < 0 || ruleGroups.length === 0) {
62204
+ return new BeamingRuleLookup(0, 0, []);
62205
+ }
62206
+ let groupIndex = 0;
62207
+ let remainingSlots = ruleGroups[groupIndex];
62208
+ const slots = [];
62209
+ for (let i = 0; i < slotCount; i++) {
62210
+ if (groupIndex < ruleGroups.length) {
62211
+ slots.push(groupIndex);
62212
+ remainingSlots--;
62213
+ if (remainingSlots <= 0) {
62214
+ groupIndex++;
62215
+ if (groupIndex < ruleGroups.length) {
62216
+ remainingSlots = ruleGroups[groupIndex];
62217
+ }
62218
+ }
62219
+ }
62220
+ else {
62221
+ // no groups defined for the remaining slots: all slots are treated
62222
+ // as unjoined
62223
+ slots.push(groupIndex);
62224
+ groupIndex++;
62225
+ }
62226
+ }
62227
+ return new BeamingRuleLookup(totalDuration, division, slots);
62228
+ }
62229
+ }
62230
+
61857
62231
  /**
61858
62232
  * @internal
61859
62233
  */
@@ -61870,6 +62244,19 @@
61870
62244
  initialize() {
61871
62245
  const barRenderer = this._renderer;
61872
62246
  const bar = this._renderer.bar;
62247
+ const masterBar = bar.masterBar;
62248
+ const beamingRules = masterBar.actualBeamingRules ?? BarHelpers._findOrBuildDefaultBeamingRules(masterBar);
62249
+ const rule = beamingRules.findRule(bar.shortestDuration);
62250
+ // NOTE: moste rules have only one group definition, so its better to reuse the unique id
62251
+ // than compute a potentially shorter id here.
62252
+ const key = `beaming_${beamingRules.uniqueId}_${rule[0]}`;
62253
+ let beamingRuleLookup = this._renderer.scoreRenderer.layout.beamingRuleLookups.has(key)
62254
+ ? this._renderer.scoreRenderer.layout.beamingRuleLookups.get(key)
62255
+ : undefined;
62256
+ if (!beamingRuleLookup) {
62257
+ beamingRuleLookup = BeamingRuleLookup.build(masterBar, rule[0], rule[1]);
62258
+ this._renderer.scoreRenderer.layout.beamingRuleLookups.set(key, beamingRuleLookup);
62259
+ }
61873
62260
  let currentBeamHelper = null;
61874
62261
  let currentGraceBeamHelper = null;
61875
62262
  for (let i = 0, j = bar.voices.length; i < j; i++) {
@@ -61895,7 +62282,7 @@
61895
62282
  helperForBeat.finish();
61896
62283
  }
61897
62284
  // if not possible, create the next beaming helper
61898
- helperForBeat = new BeamingHelper(bar.staff, barRenderer);
62285
+ helperForBeat = new BeamingHelper(bar.staff, barRenderer, beamingRuleLookup);
61899
62286
  helperForBeat.preferredBeamDirection = this.preferredBeamDirection;
61900
62287
  helperForBeat.checkBeat(b);
61901
62288
  if (b.graceType !== GraceType.None) {
@@ -61918,6 +62305,69 @@
61918
62305
  currentGraceBeamHelper = null;
61919
62306
  }
61920
62307
  }
62308
+ static _defaultBeamingRules;
62309
+ static _findOrBuildDefaultBeamingRules(masterBar) {
62310
+ let defaultBeamingRules = BarHelpers._defaultBeamingRules;
62311
+ if (!defaultBeamingRules) {
62312
+ defaultBeamingRules = new Map([
62313
+ BeamingRules.createSimple(2, 16, Duration.Sixteenth, [1, 1]),
62314
+ BeamingRules.createSimple(1, 8, Duration.Eighth, [1]),
62315
+ BeamingRules.createSimple(1, 4, Duration.Quarter, [1]),
62316
+ BeamingRules.createSimple(3, 16, Duration.Sixteenth, [3]),
62317
+ BeamingRules.createSimple(4, 16, Duration.Sixteenth, [2, 2]),
62318
+ BeamingRules.createSimple(2, 8, Duration.Eighth, [1, 1]),
62319
+ BeamingRules.createSimple(5, 16, Duration.Sixteenth, [3, 2]),
62320
+ BeamingRules.createSimple(6, 16, Duration.Sixteenth, [3, 3]),
62321
+ BeamingRules.createSimple(3, 8, Duration.Eighth, [3]),
62322
+ BeamingRules.createSimple(4, 8, Duration.Eighth, [2, 2]),
62323
+ BeamingRules.createSimple(2, 4, Duration.Quarter, [1, 1]),
62324
+ BeamingRules.createSimple(9, 16, Duration.Sixteenth, [3, 3, 3]),
62325
+ BeamingRules.createSimple(5, 8, Duration.Eighth, [3, 2]),
62326
+ BeamingRules.createSimple(12, 16, Duration.Sixteenth, [3, 3, 3, 3]),
62327
+ BeamingRules.createSimple(6, 8, Duration.Eighth, [3, 3, 3]),
62328
+ BeamingRules.createSimple(3, 4, Duration.Quarter, [1, 1, 1]),
62329
+ BeamingRules.createSimple(7, 8, Duration.Eighth, [4, 3]),
62330
+ BeamingRules.createSimple(8, 8, Duration.Eighth, [3, 3, 2]),
62331
+ BeamingRules.createSimple(4, 4, Duration.Quarter, [1, 1, 1, 1]),
62332
+ BeamingRules.createSimple(9, 8, Duration.Eighth, [3, 3, 3]),
62333
+ BeamingRules.createSimple(10, 8, Duration.Eighth, [4, 3, 3]),
62334
+ BeamingRules.createSimple(5, 4, Duration.Quarter, [1, 1, 1, 1, 1]),
62335
+ BeamingRules.createSimple(12, 8, Duration.Eighth, [3, 3, 3, 3]),
62336
+ BeamingRules.createSimple(6, 4, Duration.Quarter, [1, 1, 1, 1, 1, 1]),
62337
+ BeamingRules.createSimple(15, 8, Duration.Eighth, [3, 3, 3, 3, 3, 3]),
62338
+ BeamingRules.createSimple(8, 4, Duration.Quarter, [1, 1, 1, 1, 1, 1, 1, 1]),
62339
+ BeamingRules.createSimple(18, 8, Duration.Eighth, [3, 3, 3, 3, 3, 3])
62340
+ ].map(r => [`${r.timeSignatureNumerator}_${r.timeSignatureDenominator}`, r]));
62341
+ BarHelpers._defaultBeamingRules = defaultBeamingRules;
62342
+ }
62343
+ const key = `${masterBar.timeSignatureNumerator}_${masterBar.timeSignatureDenominator}`;
62344
+ if (defaultBeamingRules.has(key)) {
62345
+ return defaultBeamingRules.get(key);
62346
+ }
62347
+ // NOTE: this is the old alphaTab logic how we used to beamed bars.
62348
+ // we either group in quarters, or in 3x8ths depending on the key signature
62349
+ let divisionLength = MidiUtils.QuarterTime;
62350
+ switch (masterBar.timeSignatureDenominator) {
62351
+ case 8:
62352
+ if (masterBar.timeSignatureNumerator % 3 === 0) {
62353
+ divisionLength += (MidiUtils.QuarterTime / 2) | 0;
62354
+ }
62355
+ break;
62356
+ }
62357
+ const numberOfDivisions = Math.ceil(masterBar.calculateDuration(false) / divisionLength);
62358
+ const notesPerDivision = (divisionLength / MidiUtils.QuarterTime) * 2;
62359
+ const fallback = new BeamingRules();
62360
+ const groups = [];
62361
+ for (let i = 0; i < numberOfDivisions; i++) {
62362
+ groups.push(notesPerDivision);
62363
+ }
62364
+ fallback.groups.set(Duration.Eighth, groups);
62365
+ fallback.timeSignatureNumerator = masterBar.timeSignatureNumerator;
62366
+ fallback.timeSignatureDenominator = masterBar.timeSignatureDenominator;
62367
+ fallback.finish();
62368
+ defaultBeamingRules.set(key, fallback);
62369
+ return fallback;
62370
+ }
61921
62371
  getBeamingHelperForBeat(beat) {
61922
62372
  return this._beamHelperLookup.has(beat.id) ? this._beamHelperLookup.get(beat.id) : undefined;
61923
62373
  }
@@ -65850,6 +66300,7 @@
65850
66300
  this.renderer = renderer;
65851
66301
  }
65852
66302
  slurRegistry = new SlurRegistry();
66303
+ beamingRuleLookups = new Map();
65853
66304
  resize() {
65854
66305
  this._lazyPartials.clear();
65855
66306
  this.slurRegistry.clear();
@@ -65858,6 +66309,7 @@
65858
66309
  layoutAndRender() {
65859
66310
  this._lazyPartials.clear();
65860
66311
  this.slurRegistry.clear();
66312
+ this.beamingRuleLookups.clear();
65861
66313
  this._barRendererLookup.clear();
65862
66314
  this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
65863
66315
  const score = this.renderer.score;
@@ -78045,6 +78497,9 @@
78045
78497
  key.addElement('Sharps').innerText = 'Sharps';
78046
78498
  masterBarNode.addElement('Time').innerText =
78047
78499
  `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`;
78500
+ if (masterBar.actualBeamingRules) {
78501
+ this._writeBarXProperties(masterBarNode, masterBar);
78502
+ }
78048
78503
  if (masterBar.isFreeTime) {
78049
78504
  masterBarNode.addElement('FreeTime');
78050
78505
  }
@@ -78154,6 +78609,29 @@
78154
78609
  }
78155
78610
  this._writeFermatas(masterBarNode, masterBar);
78156
78611
  }
78612
+ _writeBarXProperties(masterBarNode, masterBar) {
78613
+ const properties = masterBarNode.addElement('XProperties');
78614
+ const beamingRules = masterBar.actualBeamingRules;
78615
+ if (beamingRules) {
78616
+ // prefer 8th note rule (that's what GP mostly has)
78617
+ const rule = beamingRules.findRule(Duration.Eighth);
78618
+ // NOTE: it's not clear if guitar pro supports quarter rules
78619
+ // for that case we better convert this to an "8th" note rule.
78620
+ let durationProp = rule[0];
78621
+ let groupSizeFactor = 1;
78622
+ if (rule[0] === Duration.Quarter) {
78623
+ durationProp = 8;
78624
+ groupSizeFactor = 2;
78625
+ }
78626
+ this._writeSimpleXPropertyNode(properties, '1124139010', 'Int', durationProp.toString());
78627
+ const startGroupid = 1124139264;
78628
+ let i = 0;
78629
+ while (i < rule[1].length) {
78630
+ this._writeSimpleXPropertyNode(properties, (startGroupid + i).toString(), 'Int', (rule[1][i] * groupSizeFactor).toString());
78631
+ i++;
78632
+ }
78633
+ }
78634
+ }
78157
78635
  _writeFermatas(parent, masterBar) {
78158
78636
  const fermataCount = masterBar.fermata?.size ?? 0;
78159
78637
  if (fermataCount === 0) {