@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.
@@ -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
  *
@@ -203,9 +203,9 @@ class AlphaTabError extends Error {
203
203
  * @internal
204
204
  */
205
205
  class VersionInfo {
206
- static version = '1.8.0-alpha.1670';
207
- static date = '2026-01-09T02:24:51.076Z';
208
- static commit = 'c4cd0c6435ec1bed778365efd389cb1b52df7ff3';
206
+ static version = '1.8.0-alpha.1671';
207
+ static date = '2026-01-10T02:20:48.787Z';
208
+ static commit = '1edf83d58fe31a924adb1980bc4a529aed07dad4';
209
209
  static print(print) {
210
210
  print(`alphaTab ${VersionInfo.version}`);
211
211
  print(`commit: ${VersionInfo.commit}`);
@@ -2196,6 +2196,12 @@ class Bar {
2196
2196
  * If specified, overrides the value from the stylesheet on score level.
2197
2197
  */
2198
2198
  barNumberDisplay;
2199
+ /**
2200
+ * The shortest duration contained across beats in this bar.
2201
+ * @internal
2202
+ * @json_ignore
2203
+ */
2204
+ shortestDuration = Duration.DoubleWhole;
2199
2205
  /**
2200
2206
  * The bar line to draw on the left side of the bar with an "automatic" type resolved to the actual one.
2201
2207
  * @param isFirstOfSystem Whether the bar is the first one in the system.
@@ -2267,12 +2273,16 @@ class Bar {
2267
2273
  this._filledVoices.add(0);
2268
2274
  this._isEmpty = true;
2269
2275
  this._isRestOnly = true;
2276
+ this.shortestDuration = Duration.DoubleWhole;
2270
2277
  for (let i = 0, j = this.voices.length; i < j; i++) {
2271
2278
  const voice = this.voices[i];
2272
2279
  voice.finish(settings, sharedDataBag);
2273
2280
  if (!voice.isEmpty) {
2274
2281
  this._isEmpty = false;
2275
2282
  this._filledVoices.add(i);
2283
+ if (voice.shortestDuration > this.shortestDuration) {
2284
+ this.shortestDuration = voice.shortestDuration;
2285
+ }
2276
2286
  }
2277
2287
  if (!voice.isRestOnly) {
2278
2288
  this._isRestOnly = false;
@@ -2369,6 +2379,116 @@ var TripletFeel;
2369
2379
  TripletFeel[TripletFeel["Scottish8th"] = 6] = "Scottish8th";
2370
2380
  })(TripletFeel || (TripletFeel = {}));
2371
2381
 
2382
+ /**
2383
+ * Defines the custom beaming rules which define how beats are beamed together or split apart
2384
+ * during the automatic beaming when displayed.
2385
+ * @json
2386
+ * @json_strict
2387
+ * @public
2388
+ *
2389
+ * @remarks
2390
+ * The beaming logic works like this:
2391
+ *
2392
+ * The time axis of the bar is sliced into even chunks. The chunk-size is defined by the respective group definition.
2393
+ * Within these chunks groups can then be placed spanning 1 or more chunks.
2394
+ *
2395
+ * If beats start within the same "group" they are beamed together.
2396
+ */
2397
+ class BeamingRules {
2398
+ _singleGroupKey;
2399
+ /**
2400
+ * The the group for a given "longest duration" within the bar.
2401
+ * @remarks
2402
+ * The map key is the duration to which the bar will be sliced into.
2403
+ * The map value defines the "groups" placed within the sliced.
2404
+ */
2405
+ groups = new Map();
2406
+ /**
2407
+ * @internal
2408
+ * @json_ignore
2409
+ */
2410
+ uniqueId = '';
2411
+ /**
2412
+ * @internal
2413
+ * @json_ignore
2414
+ */
2415
+ timeSignatureNumerator = 0;
2416
+ /**
2417
+ * @internal
2418
+ * @json_ignore
2419
+ */
2420
+ timeSignatureDenominator = 0;
2421
+ /**
2422
+ * @internal
2423
+ */
2424
+ static createSimple(timeSignatureNumerator, timeSignatureDenominator, duration, groups) {
2425
+ const r = new BeamingRules();
2426
+ r.timeSignatureNumerator = timeSignatureNumerator;
2427
+ r.timeSignatureDenominator = timeSignatureDenominator;
2428
+ r.groups.set(duration, groups);
2429
+ r.finish();
2430
+ return r;
2431
+ }
2432
+ /**
2433
+ * @internal
2434
+ */
2435
+ findRule(shortestDuration) {
2436
+ // fast path: one rule -> take it
2437
+ const singleGroupKey = this._singleGroupKey;
2438
+ if (singleGroupKey) {
2439
+ return [singleGroupKey, this.groups.get(singleGroupKey)];
2440
+ }
2441
+ if (shortestDuration < Duration.Quarter) {
2442
+ return [shortestDuration, []];
2443
+ }
2444
+ // first search shorter
2445
+ let durationValue = shortestDuration;
2446
+ do {
2447
+ const duration = durationValue;
2448
+ if (this.groups.has(duration)) {
2449
+ return [duration, this.groups.get(duration)];
2450
+ }
2451
+ durationValue = durationValue * 2;
2452
+ } while (durationValue <= Duration.TwoHundredFiftySixth);
2453
+ // then longer
2454
+ durationValue = shortestDuration / 2;
2455
+ do {
2456
+ const duration = durationValue;
2457
+ if (this.groups.has(duration)) {
2458
+ return [duration, this.groups.get(duration)];
2459
+ }
2460
+ durationValue = durationValue / 2;
2461
+ } while (durationValue > Duration.Half);
2462
+ return [shortestDuration, []];
2463
+ }
2464
+ /**
2465
+ * @internal
2466
+ */
2467
+ finish() {
2468
+ let uniqueId = `${this.timeSignatureNumerator}_${this.timeSignatureDenominator}`;
2469
+ for (const [k, v] of this.groups) {
2470
+ uniqueId += `__${k}`;
2471
+ // trim of 0s at the end of the group
2472
+ let lastZero = v.length;
2473
+ for (let i = v.length - 1; i >= 0; i--) {
2474
+ if (v[i] === 0) {
2475
+ lastZero = i;
2476
+ }
2477
+ else {
2478
+ break;
2479
+ }
2480
+ }
2481
+ if (lastZero < v.length) {
2482
+ v.splice(lastZero, v.length - lastZero);
2483
+ }
2484
+ uniqueId += `_${v.join('_')}`;
2485
+ if (this.groups.size === 1) {
2486
+ this._singleGroupKey = k;
2487
+ }
2488
+ }
2489
+ this.uniqueId = uniqueId;
2490
+ }
2491
+ }
2372
2492
  /**
2373
2493
  * The MasterBar stores information about a bar which affects
2374
2494
  * all tracks.
@@ -2486,6 +2606,16 @@ class MasterBar {
2486
2606
  * Gets or sets whether this is bar has a common time signature.
2487
2607
  */
2488
2608
  timeSignatureCommon = false;
2609
+ /**
2610
+ * Defines the custom beaming rules which should be applied to this bar and all bars following.
2611
+ */
2612
+ beamingRules;
2613
+ /**
2614
+ * The actual (custom) beaming rules to use for this bar if any were specified.
2615
+ * @json_ignore
2616
+ * @internal
2617
+ */
2618
+ actualBeamingRules;
2489
2619
  /**
2490
2620
  * Gets or sets whether the bar indicates a free time playing.
2491
2621
  */
@@ -2618,6 +2748,32 @@ class MasterBar {
2618
2748
  }
2619
2749
  this.syncPoints.push(syncPoint);
2620
2750
  }
2751
+ finish(sharedDataBag) {
2752
+ let beamingRules = this.beamingRules;
2753
+ if (beamingRules) {
2754
+ beamingRules.timeSignatureNumerator = this.timeSignatureNumerator;
2755
+ beamingRules.timeSignatureDenominator = this.timeSignatureDenominator;
2756
+ beamingRules.finish();
2757
+ }
2758
+ if (this.index > 0) {
2759
+ this.start = this.previousMasterBar.start + this.previousMasterBar.calculateDuration();
2760
+ // clear out equal rules to reduce memory consumption.
2761
+ const previousRules = sharedDataBag.has('beamingRules')
2762
+ ? sharedDataBag.get('beamingRules')
2763
+ : undefined;
2764
+ if (previousRules && previousRules.uniqueId === beamingRules?.uniqueId) {
2765
+ this.beamingRules = undefined;
2766
+ beamingRules = previousRules;
2767
+ }
2768
+ else if (!beamingRules) {
2769
+ beamingRules = previousRules;
2770
+ }
2771
+ }
2772
+ this.actualBeamingRules = beamingRules;
2773
+ if (this.beamingRules) {
2774
+ sharedDataBag.set('beamingRules', beamingRules);
2775
+ }
2776
+ }
2621
2777
  }
2622
2778
 
2623
2779
  /**
@@ -2968,6 +3124,12 @@ let Voice$1 = class Voice {
2968
3124
  get isRestOnly() {
2969
3125
  return this._isRestOnly;
2970
3126
  }
3127
+ /**
3128
+ * The shortest duration contained across beats in this bar.
3129
+ * @internal
3130
+ * @json_ignore
3131
+ */
3132
+ shortestDuration = Duration.DoubleWhole;
2971
3133
  insertBeat(after, newBeat) {
2972
3134
  newBeat.nextBeat = after.nextBeat;
2973
3135
  if (newBeat.nextBeat) {
@@ -3061,6 +3223,7 @@ let Voice$1 = class Voice {
3061
3223
  }
3062
3224
  let currentDisplayTick = 0;
3063
3225
  let currentPlaybackTick = 0;
3226
+ this.shortestDuration = Duration.DoubleWhole;
3064
3227
  for (let i = 0; i < this.beats.length; i++) {
3065
3228
  const beat = this.beats[i];
3066
3229
  beat.index = i;
@@ -3069,6 +3232,9 @@ let Voice$1 = class Voice {
3069
3232
  // we need to first steal the duration from the right beat
3070
3233
  // and place the grace beats correctly
3071
3234
  if (beat.graceType === GraceType.None) {
3235
+ if (beat.duration > this.shortestDuration) {
3236
+ this.shortestDuration = beat.duration;
3237
+ }
3072
3238
  if (beat.graceGroup) {
3073
3239
  const firstGraceBeat = beat.graceGroup.beats[0];
3074
3240
  const lastGraceBeat = beat.graceGroup.beats[beat.graceGroup.beats.length - 1];
@@ -3529,7 +3695,7 @@ class Score {
3529
3695
  bar.previousMasterBar.nextMasterBar = bar;
3530
3696
  // NOTE: this will not work on anacrusis. Correct anacrusis durations are only working
3531
3697
  // when there are beats with playback positions already computed which requires full finish
3532
- // chicken-egg problem here. temporarily forcing anacrusis length here to 0,
3698
+ // chicken-egg problem here. temporarily forcing anacrusis length here to 0,
3533
3699
  // .finish() will correct these times
3534
3700
  bar.start =
3535
3701
  bar.previousMasterBar.start +
@@ -3593,9 +3759,7 @@ class Score {
3593
3759
  }
3594
3760
  // fixup masterbar starts to handle anacrusis lengths
3595
3761
  for (const mb of this.masterBars) {
3596
- if (mb.index > 0) {
3597
- mb.start = mb.previousMasterBar.start + mb.previousMasterBar.calculateDuration();
3598
- }
3762
+ mb.finish(sharedDataBag);
3599
3763
  }
3600
3764
  }
3601
3765
  /**
@@ -8964,7 +9128,16 @@ class AlphaTex1LanguageDefinitions {
8964
9128
  ['spu', [[[[16], 2]]]],
8965
9129
  ['db', null],
8966
9130
  ['voicemode', [[[[10, 17], 0, ['staffwise', 'barwise']]]]],
8967
- ['barnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]]
9131
+ ['barnumberdisplay', [[[[10, 17], 0, ['allbars', 'firstofsystem', 'hide']]]]],
9132
+ [
9133
+ 'beaming',
9134
+ [
9135
+ [
9136
+ [[16], 0],
9137
+ [[16], 5]
9138
+ ]
9139
+ ]
9140
+ ]
8968
9141
  ]);
8969
9142
  static metaDataProperties = AlphaTex1LanguageDefinitions._metaProps([
8970
9143
  [
@@ -9080,7 +9253,8 @@ class AlphaTex1LanguageDefinitions {
9080
9253
  ['spu', null],
9081
9254
  ['db', null],
9082
9255
  ['voicemode', null],
9083
- ['barnumberdisplay', null]
9256
+ ['barnumberdisplay', null],
9257
+ ['beaming', null]
9084
9258
  ]);
9085
9259
  static metaDataSignatures = [
9086
9260
  AlphaTex1LanguageDefinitions.scoreMetaDataSignatures,
@@ -12795,6 +12969,7 @@ var BeamDirection;
12795
12969
  */
12796
12970
  class AlphaTex1LanguageHandler {
12797
12971
  static instance = new AlphaTex1LanguageHandler();
12972
+ static _timeSignatureDenominators = new Set([1, 2, 4, 8, 16, 32, 64, 128]);
12798
12973
  applyScoreMetaData(importer, score, metaData) {
12799
12974
  const result = this._checkArgumentTypes(importer, [AlphaTex1LanguageDefinitions.scoreMetaDataSignatures], metaData, metaData.tag.tag.text.toLowerCase(), metaData.arguments);
12800
12975
  if (result !== undefined) {
@@ -13216,8 +13391,29 @@ class AlphaTex1LanguageHandler {
13216
13391
  case 'ts':
13217
13392
  switch (metaData.arguments.arguments[0].nodeType) {
13218
13393
  case AlphaTexNodeType.Number:
13219
- bar.masterBar.timeSignatureNumerator = metaData.arguments.arguments[0].value;
13220
- bar.masterBar.timeSignatureDenominator = metaData.arguments.arguments[1].value;
13394
+ bar.masterBar.timeSignatureNumerator =
13395
+ metaData.arguments.arguments[0].value | 0;
13396
+ if (bar.masterBar.timeSignatureNumerator < 1 || bar.masterBar.timeSignatureNumerator > 32) {
13397
+ importer.addSemanticDiagnostic({
13398
+ code: AlphaTexDiagnosticCode.AT211,
13399
+ message: `Value is out of valid range. Allowed range: 1-32, Actual Value: ${bar.masterBar.timeSignatureNumerator}`,
13400
+ start: metaData.arguments.arguments[0].start,
13401
+ end: metaData.arguments.arguments[0].end,
13402
+ severity: AlphaTexDiagnosticsSeverity.Error
13403
+ });
13404
+ }
13405
+ bar.masterBar.timeSignatureDenominator =
13406
+ metaData.arguments.arguments[1].value | 0;
13407
+ if (!AlphaTex1LanguageHandler._timeSignatureDenominators.has(bar.masterBar.timeSignatureDenominator)) {
13408
+ const valueList = Array.from(AlphaTex1LanguageHandler._timeSignatureDenominators).join(', ');
13409
+ importer.addSemanticDiagnostic({
13410
+ code: AlphaTexDiagnosticCode.AT211,
13411
+ message: `Value is out of valid range. Allowed range: ${valueList}, Actual Value: ${bar.masterBar.timeSignatureDenominator}`,
13412
+ start: metaData.arguments.arguments[0].start,
13413
+ end: metaData.arguments.arguments[0].end,
13414
+ severity: AlphaTexDiagnosticsSeverity.Error
13415
+ });
13416
+ }
13221
13417
  break;
13222
13418
  case AlphaTexNodeType.Ident:
13223
13419
  case AlphaTexNodeType.String:
@@ -13240,6 +13436,8 @@ class AlphaTex1LanguageHandler {
13240
13436
  break;
13241
13437
  }
13242
13438
  return ApplyNodeResult.Applied;
13439
+ case 'beaming':
13440
+ return this._parseBeamingRule(importer, metaData, bar.masterBar);
13243
13441
  case 'ks':
13244
13442
  const keySignature = AlphaTex1LanguageHandler._parseEnumValue(importer, metaData.arguments, 'key signature', AlphaTex1EnumMappings.keySignature);
13245
13443
  if (keySignature === undefined) {
@@ -13440,6 +13638,53 @@ class AlphaTex1LanguageHandler {
13440
13638
  return ApplyNodeResult.NotAppliedUnrecognizedMarker;
13441
13639
  }
13442
13640
  }
13641
+ _parseBeamingRule(importer, metaData, masterBar) {
13642
+ let duration = Duration.Eighth;
13643
+ const groupSizes = [];
13644
+ const durationValue = metaData.arguments.arguments[0].value;
13645
+ switch (durationValue) {
13646
+ case 4:
13647
+ duration = Duration.QuadrupleWhole;
13648
+ break;
13649
+ case 8:
13650
+ duration = Duration.Eighth;
13651
+ break;
13652
+ case 16:
13653
+ duration = Duration.Sixteenth;
13654
+ break;
13655
+ case 32:
13656
+ duration = Duration.ThirtySecond;
13657
+ break;
13658
+ default:
13659
+ importer.addSemanticDiagnostic({
13660
+ code: AlphaTexDiagnosticCode.AT209,
13661
+ message: `Value is out of valid range. Allowed range: 4,8,16 or 32, Actual Value: ${durationValue}`,
13662
+ severity: AlphaTexDiagnosticsSeverity.Error,
13663
+ start: metaData.arguments.arguments[0].start,
13664
+ end: metaData.arguments.arguments[0].end
13665
+ });
13666
+ return ApplyNodeResult.NotAppliedSemanticError;
13667
+ }
13668
+ for (let i = 1; i < metaData.arguments.arguments.length; i++) {
13669
+ const groupSize = metaData.arguments.arguments[i].value;
13670
+ if (groupSize < 1) {
13671
+ importer.addSemanticDiagnostic({
13672
+ code: AlphaTexDiagnosticCode.AT209,
13673
+ message: `Value is out of valid range. Allowed range: >0, Actual Value: ${durationValue}`,
13674
+ severity: AlphaTexDiagnosticsSeverity.Error,
13675
+ start: metaData.arguments.arguments[i].start,
13676
+ end: metaData.arguments.arguments[i].end
13677
+ });
13678
+ return ApplyNodeResult.NotAppliedSemanticError;
13679
+ }
13680
+ groupSizes.push(metaData.arguments.arguments[i].value);
13681
+ }
13682
+ if (!masterBar.beamingRules) {
13683
+ masterBar.beamingRules = new BeamingRules();
13684
+ }
13685
+ masterBar.beamingRules.groups.set(duration, groupSizes);
13686
+ return ApplyNodeResult.Applied;
13687
+ }
13443
13688
  static _handleAccidentalMode(importer, args) {
13444
13689
  const accidentalMode = AlphaTex1LanguageHandler._parseEnumValue(importer, args, 'accidental mode', AlphaTex1EnumMappings.alphaTexAccidentalMode);
13445
13690
  if (accidentalMode === undefined) {
@@ -15032,6 +15277,15 @@ class AlphaTex1LanguageHandler {
15032
15277
  ])));
15033
15278
  }
15034
15279
  }
15280
+ if (masterBar.beamingRules) {
15281
+ for (const [k, v] of masterBar.beamingRules.groups) {
15282
+ const args = Atnf.args([Atnf.number(k)], true);
15283
+ for (const i of v) {
15284
+ args.arguments.push(Atnf.number(i));
15285
+ }
15286
+ nodes.push(Atnf.meta('beaming', args));
15287
+ }
15288
+ }
15035
15289
  if ((masterBar.index > 0 && masterBar.tripletFeel !== masterBar.previousMasterBar?.tripletFeel) ||
15036
15290
  (masterBar.index === 0 && masterBar.tripletFeel !== TripletFeel.NoTripletFeel)) {
15037
15291
  nodes.push(Atnf.identMeta('tf', AlphaTex1EnumMappings.tripletFeelReversed.get(masterBar.tripletFeel)));
@@ -23032,6 +23286,8 @@ class GpifParser {
23032
23286
  }
23033
23287
  }
23034
23288
  _parseMasterBarXProperties(masterBar, node) {
23289
+ let beamingRuleDuration = Number.NaN;
23290
+ let beamingRuleGroups = undefined;
23035
23291
  for (const c of node.childElements()) {
23036
23292
  switch (c.localName) {
23037
23293
  case 'XProperty':
@@ -23040,10 +23296,32 @@ class GpifParser {
23040
23296
  case '1124073984':
23041
23297
  masterBar.displayScale = GpifParser._parseFloatSafe(c.findChildElement('Double')?.innerText, 1);
23042
23298
  break;
23299
+ case '1124139010':
23300
+ beamingRuleDuration = GpifParser._parseIntSafe(c.findChildElement('Int')?.innerText, Number.NaN);
23301
+ break;
23302
+ default:
23303
+ const idNumeric = GpifParser._parseIntSafe(id, 0);
23304
+ if (idNumeric >= 1124139264 && idNumeric <= 1124139295) {
23305
+ const groupIndex = idNumeric - 1124139264;
23306
+ const groupSize = GpifParser._parseIntSafe(c.findChildElement('Int')?.innerText, Number.NaN);
23307
+ if (beamingRuleGroups === undefined) {
23308
+ beamingRuleGroups = [];
23309
+ }
23310
+ while (beamingRuleGroups.length < groupIndex + 1) {
23311
+ beamingRuleGroups.push(0);
23312
+ }
23313
+ beamingRuleGroups[groupIndex] = groupSize;
23314
+ }
23315
+ break;
23043
23316
  }
23044
23317
  break;
23045
23318
  }
23046
23319
  }
23320
+ if (!Number.isNaN(beamingRuleDuration) && beamingRuleGroups) {
23321
+ const rules = new BeamingRules();
23322
+ rules.groups.set(beamingRuleDuration, beamingRuleGroups);
23323
+ masterBar.beamingRules = rules;
23324
+ }
23047
23325
  }
23048
23326
  _parseBeatProperties(node, beat) {
23049
23327
  let isWhammy = false;
@@ -36579,6 +36857,43 @@ class JsonHelper {
36579
36857
  }
36580
36858
  }
36581
36859
 
36860
+ /**
36861
+ * @internal
36862
+ */
36863
+ class BeamingRulesSerializer {
36864
+ static fromJson(obj, m) {
36865
+ if (!m) {
36866
+ return;
36867
+ }
36868
+ JsonHelper.forEach(m, (v, k) => BeamingRulesSerializer.setProperty(obj, k, v));
36869
+ }
36870
+ static toJson(obj) {
36871
+ if (!obj) {
36872
+ return null;
36873
+ }
36874
+ const o = new Map();
36875
+ {
36876
+ const m = new Map();
36877
+ o.set("groups", m);
36878
+ for (const [k, v] of obj.groups) {
36879
+ m.set(k.toString(), v);
36880
+ }
36881
+ }
36882
+ return o;
36883
+ }
36884
+ static setProperty(obj, property, v) {
36885
+ switch (property) {
36886
+ case "groups":
36887
+ obj.groups = new Map();
36888
+ JsonHelper.forEach(v, (v, k) => {
36889
+ obj.groups.set(JsonHelper.parseEnum(k, Duration), v);
36890
+ });
36891
+ return true;
36892
+ }
36893
+ return false;
36894
+ }
36895
+ }
36896
+
36582
36897
  /**
36583
36898
  * @internal
36584
36899
  */
@@ -36757,6 +37072,9 @@ class MasterBarSerializer {
36757
37072
  o.set("timesignaturenumerator", obj.timeSignatureNumerator);
36758
37073
  o.set("timesignaturedenominator", obj.timeSignatureDenominator);
36759
37074
  o.set("timesignaturecommon", obj.timeSignatureCommon);
37075
+ if (obj.beamingRules) {
37076
+ o.set("beamingrules", BeamingRulesSerializer.toJson(obj.beamingRules));
37077
+ }
36760
37078
  o.set("isfreetime", obj.isFreeTime);
36761
37079
  o.set("tripletfeel", obj.tripletFeel);
36762
37080
  if (obj.section) {
@@ -36809,6 +37127,15 @@ class MasterBarSerializer {
36809
37127
  case "timesignaturecommon":
36810
37128
  obj.timeSignatureCommon = v;
36811
37129
  return true;
37130
+ case "beamingrules":
37131
+ if (v) {
37132
+ obj.beamingRules = new BeamingRules();
37133
+ BeamingRulesSerializer.fromJson(obj.beamingRules, v);
37134
+ }
37135
+ else {
37136
+ obj.beamingRules = undefined;
37137
+ }
37138
+ return true;
36812
37139
  case "isfreetime":
36813
37140
  obj.isFreeTime = v;
36814
37141
  return true;
@@ -61467,6 +61794,7 @@ class BeamingHelperDrawInfo {
61467
61794
  class BeamingHelper {
61468
61795
  _staff;
61469
61796
  _renderer;
61797
+ _beamingRuleLookup;
61470
61798
  voice = null;
61471
61799
  beats = [];
61472
61800
  shortestDuration = Duration.QuadrupleWhole;
@@ -61500,10 +61828,11 @@ class BeamingHelper {
61500
61828
  static beatHasFlag(beat) {
61501
61829
  return (!beat.deadSlapped && !beat.isRest && (beat.duration > Duration.Quarter || beat.graceType !== GraceType.None));
61502
61830
  }
61503
- constructor(staff, renderer) {
61831
+ constructor(staff, renderer, beamingRuleLookup) {
61504
61832
  this._staff = staff;
61505
61833
  this._renderer = renderer;
61506
61834
  this.beats = [];
61835
+ this._beamingRuleLookup = beamingRuleLookup;
61507
61836
  }
61508
61837
  alignWithBeats() {
61509
61838
  for (const v of this.drawingInfos.values()) {
@@ -61558,7 +61887,7 @@ class BeamingHelper {
61558
61887
  switch (this.beats[this.beats.length - 1].beamingMode) {
61559
61888
  case BeatBeamingMode.Auto:
61560
61889
  case BeatBeamingMode.ForceSplitOnSecondaryToNext:
61561
- add = BeamingHelper._canJoin(this.beats[this.beats.length - 1], beat);
61890
+ add = this._canJoin(this.beats[this.beats.length - 1], beat);
61562
61891
  break;
61563
61892
  case BeatBeamingMode.ForceSplitToNext:
61564
61893
  add = false;
@@ -61630,8 +61959,7 @@ class BeamingHelper {
61630
61959
  this._highestNoteCompareValueInHelper = highestValueForNote;
61631
61960
  }
61632
61961
  }
61633
- // TODO: Check if this beaming is really correct, I'm not sure if we are connecting beats correctly
61634
- static _canJoin(b1, b2) {
61962
+ _canJoin(b1, b2) {
61635
61963
  // is this a voice we can join with?
61636
61964
  if (!b1 ||
61637
61965
  !b2 ||
@@ -61669,19 +61997,10 @@ class BeamingHelper {
61669
61997
  return true;
61670
61998
  }
61671
61999
  }
61672
- // TODO: create more rules for automatic beaming
61673
- let divisionLength = MidiUtils.QuarterTime;
61674
- switch (m1.masterBar.timeSignatureDenominator) {
61675
- case 8:
61676
- if (m1.masterBar.timeSignatureNumerator % 3 === 0) {
61677
- divisionLength += (MidiUtils.QuarterTime / 2) | 0;
61678
- }
61679
- break;
61680
- }
61681
- // check if they are on the same division
61682
- const division1 = ((divisionLength + start1) / divisionLength) | 0 | 0;
61683
- const division2 = ((divisionLength + start2) / divisionLength) | 0 | 0;
61684
- return division1 === division2;
62000
+ // check if they are on the same group as per rule definitions
62001
+ const groupId1 = this._beamingRuleLookup.calculateGroupIndex(start1);
62002
+ const groupId2 = this._beamingRuleLookup.calculateGroupIndex(start2);
62003
+ return groupId1 === groupId2;
61685
62004
  }
61686
62005
  static _canJoinDuration(d) {
61687
62006
  switch (d) {
@@ -61848,6 +62167,61 @@ class BarCollisionHelper {
61848
62167
  }
61849
62168
  }
61850
62169
 
62170
+ /**
62171
+ * @internal
62172
+ */
62173
+ class BeamingRuleLookup {
62174
+ _division = 0;
62175
+ _slots = [];
62176
+ _barDuration;
62177
+ constructor(barDuration, division, slots) {
62178
+ this._division = division;
62179
+ this._slots = slots;
62180
+ this._barDuration = barDuration;
62181
+ }
62182
+ calculateGroupIndex(beatStartTime) {
62183
+ // no slots -> all have their own group based (use the start time as index)
62184
+ if (this._slots.length === 0) {
62185
+ return beatStartTime;
62186
+ }
62187
+ // rollover within the bar.
62188
+ beatStartTime = beatStartTime % this._barDuration;
62189
+ const slotIndex = Math.floor(beatStartTime / this._division);
62190
+ return this._slots[slotIndex];
62191
+ }
62192
+ static build(masterBar, ruleDuration, ruleGroups) {
62193
+ const totalDuration = masterBar.calculateDuration(false);
62194
+ const division = MidiUtils.toTicks(ruleDuration);
62195
+ const slotCount = totalDuration / division;
62196
+ // should only happen in case of improper data.
62197
+ if (slotCount < 0 || ruleGroups.length === 0) {
62198
+ return new BeamingRuleLookup(0, 0, []);
62199
+ }
62200
+ let groupIndex = 0;
62201
+ let remainingSlots = ruleGroups[groupIndex];
62202
+ const slots = [];
62203
+ for (let i = 0; i < slotCount; i++) {
62204
+ if (groupIndex < ruleGroups.length) {
62205
+ slots.push(groupIndex);
62206
+ remainingSlots--;
62207
+ if (remainingSlots <= 0) {
62208
+ groupIndex++;
62209
+ if (groupIndex < ruleGroups.length) {
62210
+ remainingSlots = ruleGroups[groupIndex];
62211
+ }
62212
+ }
62213
+ }
62214
+ else {
62215
+ // no groups defined for the remaining slots: all slots are treated
62216
+ // as unjoined
62217
+ slots.push(groupIndex);
62218
+ groupIndex++;
62219
+ }
62220
+ }
62221
+ return new BeamingRuleLookup(totalDuration, division, slots);
62222
+ }
62223
+ }
62224
+
61851
62225
  /**
61852
62226
  * @internal
61853
62227
  */
@@ -61864,6 +62238,19 @@ class BarHelpers {
61864
62238
  initialize() {
61865
62239
  const barRenderer = this._renderer;
61866
62240
  const bar = this._renderer.bar;
62241
+ const masterBar = bar.masterBar;
62242
+ const beamingRules = masterBar.actualBeamingRules ?? BarHelpers._findOrBuildDefaultBeamingRules(masterBar);
62243
+ const rule = beamingRules.findRule(bar.shortestDuration);
62244
+ // NOTE: moste rules have only one group definition, so its better to reuse the unique id
62245
+ // than compute a potentially shorter id here.
62246
+ const key = `beaming_${beamingRules.uniqueId}_${rule[0]}`;
62247
+ let beamingRuleLookup = this._renderer.scoreRenderer.layout.beamingRuleLookups.has(key)
62248
+ ? this._renderer.scoreRenderer.layout.beamingRuleLookups.get(key)
62249
+ : undefined;
62250
+ if (!beamingRuleLookup) {
62251
+ beamingRuleLookup = BeamingRuleLookup.build(masterBar, rule[0], rule[1]);
62252
+ this._renderer.scoreRenderer.layout.beamingRuleLookups.set(key, beamingRuleLookup);
62253
+ }
61867
62254
  let currentBeamHelper = null;
61868
62255
  let currentGraceBeamHelper = null;
61869
62256
  for (let i = 0, j = bar.voices.length; i < j; i++) {
@@ -61889,7 +62276,7 @@ class BarHelpers {
61889
62276
  helperForBeat.finish();
61890
62277
  }
61891
62278
  // if not possible, create the next beaming helper
61892
- helperForBeat = new BeamingHelper(bar.staff, barRenderer);
62279
+ helperForBeat = new BeamingHelper(bar.staff, barRenderer, beamingRuleLookup);
61893
62280
  helperForBeat.preferredBeamDirection = this.preferredBeamDirection;
61894
62281
  helperForBeat.checkBeat(b);
61895
62282
  if (b.graceType !== GraceType.None) {
@@ -61912,6 +62299,69 @@ class BarHelpers {
61912
62299
  currentGraceBeamHelper = null;
61913
62300
  }
61914
62301
  }
62302
+ static _defaultBeamingRules;
62303
+ static _findOrBuildDefaultBeamingRules(masterBar) {
62304
+ let defaultBeamingRules = BarHelpers._defaultBeamingRules;
62305
+ if (!defaultBeamingRules) {
62306
+ defaultBeamingRules = new Map([
62307
+ BeamingRules.createSimple(2, 16, Duration.Sixteenth, [1, 1]),
62308
+ BeamingRules.createSimple(1, 8, Duration.Eighth, [1]),
62309
+ BeamingRules.createSimple(1, 4, Duration.Quarter, [1]),
62310
+ BeamingRules.createSimple(3, 16, Duration.Sixteenth, [3]),
62311
+ BeamingRules.createSimple(4, 16, Duration.Sixteenth, [2, 2]),
62312
+ BeamingRules.createSimple(2, 8, Duration.Eighth, [1, 1]),
62313
+ BeamingRules.createSimple(5, 16, Duration.Sixteenth, [3, 2]),
62314
+ BeamingRules.createSimple(6, 16, Duration.Sixteenth, [3, 3]),
62315
+ BeamingRules.createSimple(3, 8, Duration.Eighth, [3]),
62316
+ BeamingRules.createSimple(4, 8, Duration.Eighth, [2, 2]),
62317
+ BeamingRules.createSimple(2, 4, Duration.Quarter, [1, 1]),
62318
+ BeamingRules.createSimple(9, 16, Duration.Sixteenth, [3, 3, 3]),
62319
+ BeamingRules.createSimple(5, 8, Duration.Eighth, [3, 2]),
62320
+ BeamingRules.createSimple(12, 16, Duration.Sixteenth, [3, 3, 3, 3]),
62321
+ BeamingRules.createSimple(6, 8, Duration.Eighth, [3, 3, 3]),
62322
+ BeamingRules.createSimple(3, 4, Duration.Quarter, [1, 1, 1]),
62323
+ BeamingRules.createSimple(7, 8, Duration.Eighth, [4, 3]),
62324
+ BeamingRules.createSimple(8, 8, Duration.Eighth, [3, 3, 2]),
62325
+ BeamingRules.createSimple(4, 4, Duration.Quarter, [1, 1, 1, 1]),
62326
+ BeamingRules.createSimple(9, 8, Duration.Eighth, [3, 3, 3]),
62327
+ BeamingRules.createSimple(10, 8, Duration.Eighth, [4, 3, 3]),
62328
+ BeamingRules.createSimple(5, 4, Duration.Quarter, [1, 1, 1, 1, 1]),
62329
+ BeamingRules.createSimple(12, 8, Duration.Eighth, [3, 3, 3, 3]),
62330
+ BeamingRules.createSimple(6, 4, Duration.Quarter, [1, 1, 1, 1, 1, 1]),
62331
+ BeamingRules.createSimple(15, 8, Duration.Eighth, [3, 3, 3, 3, 3, 3]),
62332
+ BeamingRules.createSimple(8, 4, Duration.Quarter, [1, 1, 1, 1, 1, 1, 1, 1]),
62333
+ BeamingRules.createSimple(18, 8, Duration.Eighth, [3, 3, 3, 3, 3, 3])
62334
+ ].map(r => [`${r.timeSignatureNumerator}_${r.timeSignatureDenominator}`, r]));
62335
+ BarHelpers._defaultBeamingRules = defaultBeamingRules;
62336
+ }
62337
+ const key = `${masterBar.timeSignatureNumerator}_${masterBar.timeSignatureDenominator}`;
62338
+ if (defaultBeamingRules.has(key)) {
62339
+ return defaultBeamingRules.get(key);
62340
+ }
62341
+ // NOTE: this is the old alphaTab logic how we used to beamed bars.
62342
+ // we either group in quarters, or in 3x8ths depending on the key signature
62343
+ let divisionLength = MidiUtils.QuarterTime;
62344
+ switch (masterBar.timeSignatureDenominator) {
62345
+ case 8:
62346
+ if (masterBar.timeSignatureNumerator % 3 === 0) {
62347
+ divisionLength += (MidiUtils.QuarterTime / 2) | 0;
62348
+ }
62349
+ break;
62350
+ }
62351
+ const numberOfDivisions = Math.ceil(masterBar.calculateDuration(false) / divisionLength);
62352
+ const notesPerDivision = (divisionLength / MidiUtils.QuarterTime) * 2;
62353
+ const fallback = new BeamingRules();
62354
+ const groups = [];
62355
+ for (let i = 0; i < numberOfDivisions; i++) {
62356
+ groups.push(notesPerDivision);
62357
+ }
62358
+ fallback.groups.set(Duration.Eighth, groups);
62359
+ fallback.timeSignatureNumerator = masterBar.timeSignatureNumerator;
62360
+ fallback.timeSignatureDenominator = masterBar.timeSignatureDenominator;
62361
+ fallback.finish();
62362
+ defaultBeamingRules.set(key, fallback);
62363
+ return fallback;
62364
+ }
61915
62365
  getBeamingHelperForBeat(beat) {
61916
62366
  return this._beamHelperLookup.has(beat.id) ? this._beamHelperLookup.get(beat.id) : undefined;
61917
62367
  }
@@ -65844,6 +66294,7 @@ class ScoreLayout {
65844
66294
  this.renderer = renderer;
65845
66295
  }
65846
66296
  slurRegistry = new SlurRegistry();
66297
+ beamingRuleLookups = new Map();
65847
66298
  resize() {
65848
66299
  this._lazyPartials.clear();
65849
66300
  this.slurRegistry.clear();
@@ -65852,6 +66303,7 @@ class ScoreLayout {
65852
66303
  layoutAndRender() {
65853
66304
  this._lazyPartials.clear();
65854
66305
  this.slurRegistry.clear();
66306
+ this.beamingRuleLookups.clear();
65855
66307
  this._barRendererLookup.clear();
65856
66308
  this.profile = Environment.staveProfiles.get(this.renderer.settings.display.staveProfile);
65857
66309
  const score = this.renderer.score;
@@ -78039,6 +78491,9 @@ class GpifWriter {
78039
78491
  key.addElement('Sharps').innerText = 'Sharps';
78040
78492
  masterBarNode.addElement('Time').innerText =
78041
78493
  `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`;
78494
+ if (masterBar.actualBeamingRules) {
78495
+ this._writeBarXProperties(masterBarNode, masterBar);
78496
+ }
78042
78497
  if (masterBar.isFreeTime) {
78043
78498
  masterBarNode.addElement('FreeTime');
78044
78499
  }
@@ -78148,6 +78603,29 @@ class GpifWriter {
78148
78603
  }
78149
78604
  this._writeFermatas(masterBarNode, masterBar);
78150
78605
  }
78606
+ _writeBarXProperties(masterBarNode, masterBar) {
78607
+ const properties = masterBarNode.addElement('XProperties');
78608
+ const beamingRules = masterBar.actualBeamingRules;
78609
+ if (beamingRules) {
78610
+ // prefer 8th note rule (that's what GP mostly has)
78611
+ const rule = beamingRules.findRule(Duration.Eighth);
78612
+ // NOTE: it's not clear if guitar pro supports quarter rules
78613
+ // for that case we better convert this to an "8th" note rule.
78614
+ let durationProp = rule[0];
78615
+ let groupSizeFactor = 1;
78616
+ if (rule[0] === Duration.Quarter) {
78617
+ durationProp = 8;
78618
+ groupSizeFactor = 2;
78619
+ }
78620
+ this._writeSimpleXPropertyNode(properties, '1124139010', 'Int', durationProp.toString());
78621
+ const startGroupid = 1124139264;
78622
+ let i = 0;
78623
+ while (i < rule[1].length) {
78624
+ this._writeSimpleXPropertyNode(properties, (startGroupid + i).toString(), 'Int', (rule[1][i] * groupSizeFactor).toString());
78625
+ i++;
78626
+ }
78627
+ }
78628
+ }
78151
78629
  _writeFermatas(parent, masterBar) {
78152
78630
  const fermataCount = masterBar.fermata?.size ?? 0;
78153
78631
  if (fermataCount === 0) {