abcjs 6.4.3 → 6.5.0

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.
@@ -617,6 +617,7 @@ var tunebook = {};
617
617
  }
618
618
  }
619
619
  }
620
+
620
621
  return staves;
621
622
  }
622
623
  function measuresParser(staff, tune) {
@@ -1340,6 +1341,7 @@ var Tune = function Tune() {
1340
1341
  };
1341
1342
  return this.meter; // TODO-PER: is this saved value used anywhere? A get function shouldn't change state.
1342
1343
  };
1344
+
1343
1345
  this.getKeySignature = function () {
1344
1346
  for (var i = 0; i < this.lines.length; i++) {
1345
1347
  var line = this.lines[i];
@@ -1494,6 +1496,7 @@ var Tune = function Tune() {
1494
1496
  // isTiedState = voiceTimeMilliseconds;
1495
1497
  }
1496
1498
  }
1499
+
1497
1500
  return {
1498
1501
  isTiedState: isTiedState,
1499
1502
  duration: realDuration / timeDivider,
@@ -3119,6 +3122,7 @@ var Parse = function Parse() {
3119
3122
  multilineVars.lineBreaks = switches.lineBreaks;
3120
3123
  //multilineVars.continueall = true;
3121
3124
  }
3125
+
3122
3126
  header.reset(tokenizer, warn, multilineVars, tune);
3123
3127
  try {
3124
3128
  if (switches.format) {
@@ -3208,6 +3212,7 @@ var bookParser = function bookParser(book) {
3208
3212
  });
3209
3213
  pos += tune.length + 1; // We also lost a newline when splitting, so count that.
3210
3214
  });
3215
+
3211
3216
  if (tunes.length > 1 && !parseCommon.startsWith(tunes[0].abc, 'X:')) {
3212
3217
  // If there is only one tune, the X: might be missing, otherwise assume the top of the file is "intertune"
3213
3218
  // There could be file-wide directives in this, if so, we need to insert it into each tune. We can probably get away with
@@ -4314,11 +4319,24 @@ var parseDirective = {};
4314
4319
  }
4315
4320
  multilineVars.currBarNumber = tuneBuilder.setBarNumberImmediate(tokens[0].intt);
4316
4321
  break;
4322
+ case "keywarn":
4323
+ if (tokens.length !== 1 || tokens[0].type !== 'number' || tokens[0].intt !== 1 && tokens[0].intt !== 0) {
4324
+ return 'Directive ' + cmd + ' requires 0 or 1 as a parameter.';
4325
+ }
4326
+ multilineVars[cmd] = tokens[0].intt === 1;
4327
+ break;
4317
4328
  case "begintext":
4318
4329
  var textBlock = '';
4319
4330
  line = tokenizer.nextLine();
4320
4331
  while (line && line.indexOf('%%endtext') !== 0) {
4321
- if (parseCommon.startsWith(line, "%%")) textBlock += line.substring(2) + "\n";else textBlock += line + "\n";
4332
+ // MAE 9 May 2025 - for text blocks with just white space
4333
+ if (parseCommon.startsWith(line, "%%")) {
4334
+ var theLine = line.substring(2);
4335
+ theLine = theLine.trim() + "\n";
4336
+ textBlock += theLine;
4337
+ } else {
4338
+ textBlock += line.trim() + "\n";
4339
+ }
4322
4340
  line = tokenizer.nextLine();
4323
4341
  }
4324
4342
  tuneBuilder.addText(textBlock, {
@@ -4503,6 +4521,14 @@ var parseDirective = {};
4503
4521
  }
4504
4522
  }
4505
4523
  break;
4524
+ case "maxstaves":
4525
+ var nStaves = tokenizer.getInt(restOfString);
4526
+ if (nStaves.digits === 0) warn("Expected number of staves in maxstaves");else {
4527
+ if (nStaves.value > 0) {
4528
+ tune.formatting.maxStaves = nStaves.value;
4529
+ }
4530
+ }
4531
+ break;
4506
4532
  case "newpage":
4507
4533
  var pgNum = tokenizer.getInt(restOfString);
4508
4534
  tuneBuilder.addNewPage(pgNum.digits === 0 ? -1 : pgNum.value);
@@ -4563,6 +4589,10 @@ var parseDirective = {};
4563
4589
  tune.formatting.percmap[percmap.key] = percmap.value;
4564
4590
  }
4565
4591
  break;
4592
+ case "visualtranspose":
4593
+ var halfSteps = tokenizer.getInt(restOfString);
4594
+ if (halfSteps.digits === 0) warn("Expected number of half steps in visualTranspose");else multilineVars.globalTranspose = halfSteps.value;
4595
+ break;
4566
4596
  case "map":
4567
4597
  case "playtempo":
4568
4598
  case "auquality":
@@ -4780,6 +4810,7 @@ var ParseHeader = function ParseHeader(tokenizer, warn, multilineVars, tune, tun
4780
4810
  }
4781
4811
  return ret; // just to suppress warning
4782
4812
  };
4813
+
4783
4814
  var parseFraction = function parseFraction() {
4784
4815
  // handles this much: parseNum slash decimal
4785
4816
  var ret = parseNum();
@@ -4810,6 +4841,7 @@ var ParseHeader = function ParseHeader(tokenizer, warn, multilineVars, tune, tun
4810
4841
  //var tok = tokens.shift();
4811
4842
  //if (tok.token !== '+') throw "Extra characters in M: line";
4812
4843
  }
4844
+
4813
4845
  if (multilineVars.havent_set_length === true) {
4814
4846
  multilineVars.default_length = totalLength < 0.75 ? 0.0625 : 0.125;
4815
4847
  multilineVars.havent_set_length = false;
@@ -5085,6 +5117,7 @@ var ParseHeader = function ParseHeader(tokenizer, warn, multilineVars, tune, tun
5085
5117
  // TODO: complain about unhandled header
5086
5118
  }
5087
5119
  }
5120
+
5088
5121
  return [0];
5089
5122
  };
5090
5123
  this.letter_to_body_header = function (line, i) {
@@ -5101,8 +5134,8 @@ var ParseHeader = function ParseHeader(tokenizer, warn, multilineVars, tune, tun
5101
5134
  return [line.length];
5102
5135
  case "K:":
5103
5136
  var result = parseKeyVoice.parseKey(line.substring(i + 2), tuneBuilder.hasBeginMusic());
5104
- if (result.foundClef && tuneBuilder.hasBeginMusic()) tuneBuilder.appendStartingElement('clef', multilineVars.iChar + i, multilineVars.iChar + line.length, multilineVars.clef);
5105
- if (result.foundKey && tuneBuilder.hasBeginMusic()) tuneBuilder.appendStartingElement('key', multilineVars.iChar + i, multilineVars.iChar + line.length, parseKeyVoice.fixKey(multilineVars.clef, multilineVars.key));
5137
+ if (result.foundClef && tuneBuilder.hasBeginMusic() && multilineVars.keywarn !== false) tuneBuilder.appendStartingElement('clef', multilineVars.iChar + i, multilineVars.iChar + line.length, multilineVars.clef);
5138
+ if (result.foundKey && tuneBuilder.hasBeginMusic() && multilineVars.keywarn !== false) tuneBuilder.appendStartingElement('key', multilineVars.iChar + i, multilineVars.iChar + line.length, parseKeyVoice.fixKey(multilineVars.clef, multilineVars.key));
5106
5139
  return [line.length];
5107
5140
  case "P:":
5108
5141
  if (tuneBuilder.hasBeginMusic()) tuneBuilder.appendElement('part', multilineVars.iChar + i, multilineVars.iChar + line.length, {
@@ -5126,6 +5159,7 @@ var ParseHeader = function ParseHeader(tokenizer, warn, multilineVars, tune, tun
5126
5159
  // TODO: complain about unhandled header
5127
5160
  }
5128
5161
  }
5162
+
5129
5163
  return [0];
5130
5164
  };
5131
5165
  var metaTextHeaders = {
@@ -5183,7 +5217,7 @@ var ParseHeader = function ParseHeader(tokenizer, warn, multilineVars, tune, tun
5183
5217
  // since the key is the last thing that can happen in the header, we can resolve the tempo now
5184
5218
  this.resolveTempo();
5185
5219
  var result = parseKeyVoice.parseKey(line.substring(2), false);
5186
- if (!multilineVars.is_in_header && tuneBuilder.hasBeginMusic()) {
5220
+ if (!multilineVars.is_in_header && tuneBuilder.hasBeginMusic() && multilineVars.keywarn !== false) {
5187
5221
  if (result.foundClef) tuneBuilder.appendStartingElement('clef', startChar, endChar, multilineVars.clef);
5188
5222
  if (result.foundKey) tuneBuilder.appendStartingElement('key', startChar, endChar, parseKeyVoice.fixKey(multilineVars.clef, multilineVars.key));
5189
5223
  }
@@ -5592,6 +5626,7 @@ var parseKeyVoice = {};
5592
5626
  str: str.substring(i)
5593
5627
  }; // We get the note in the middle of the staff. We want the note that appears as the first ledger line below the staff.
5594
5628
  };
5629
+
5595
5630
  var normalizeAccidentals = function normalizeAccidentals(accs) {
5596
5631
  for (var i = 0; i < accs.length; i++) {
5597
5632
  if (accs[i].note === 'b') accs[i].note = 'B';else if (accs[i].note === 'a') accs[i].note = 'A';else if (accs[i].note === 'F') accs[i].note = 'f';else if (accs[i].note === 'E') accs[i].note = 'e';else if (accs[i].note === 'D') accs[i].note = 'd';else if (accs[i].note === 'C') accs[i].note = 'c';else if (accs[i].note === 'G' && accs[i].acc === 'sharp') accs[i].note = 'g';else if (accs[i].note === 'g' && accs[i].acc === 'flat') accs[i].note = 'G';
@@ -5714,7 +5749,7 @@ var parseKeyVoice = {};
5714
5749
  multilineVars.key = parseKeyVoice.deepCopyKey(parseKeyVoice.standardKey(key, retPitch.token, acc, keyCompensate));
5715
5750
  if (isInline) multilineVars.globalTransposeOrigKeySig = savedOrigKey;
5716
5751
  multilineVars.key.mode = mode;
5717
- if (oldKey) {
5752
+ if (oldKey && multilineVars.keywarn !== false) {
5718
5753
  // Add natural in all places that the old key had an accidental.
5719
5754
  var kk;
5720
5755
  for (var k = 0; k < multilineVars.key.accidentals.length; k++) {
@@ -6008,6 +6043,7 @@ var parseKeyVoice = {};
6008
6043
  if (multilineVars.currentVoice) {
6009
6044
  if (multilineVars.currentVoice.index === currentVoice.index && multilineVars.currentVoice.staffNum === currentVoice.staffNum) return; // there was no change so don't reset it.
6010
6045
  }
6046
+
6011
6047
  multilineVars.currentVoice = currentVoice;
6012
6048
  return tuneBuilder.setCurrentVoice(currentVoice.staffNum, currentVoice.index, id);
6013
6049
  };
@@ -6124,12 +6160,47 @@ var parseKeyVoice = {};
6124
6160
  case 'tenor,,':
6125
6161
  case 'alto,,':
6126
6162
  case 'none,,':
6163
+ // MAE 26 May 2025 Start of additional clefs
6164
+ case 'treble+8':
6165
+ case 'treble-8':
6166
+ case 'treble^8':
6167
+ case 'treble_8':
6168
+ case 'treble1':
6169
+ case 'treble2':
6170
+ case 'treble3':
6171
+ case 'treble4':
6172
+ case 'treble5':
6173
+ case 'bass+8':
6174
+ case 'bass-8':
6175
+ case 'bass^8':
6176
+ case 'bass_8':
6177
+ case 'bass+16':
6178
+ case 'bass-16':
6179
+ case 'bass^16':
6180
+ case 'bass_16':
6181
+ case 'bass1':
6182
+ case 'bass2':
6183
+ case 'bass3':
6184
+ case 'bass4':
6185
+ case 'bass5':
6186
+ case 'tenor1':
6187
+ case 'tenor2':
6188
+ case 'tenor3':
6189
+ case 'tenor4':
6190
+ case 'tenor5':
6191
+ case 'alto1':
6192
+ case 'alto2':
6193
+ case 'alto3':
6194
+ case 'alto4':
6195
+ case 'alto5':
6196
+ case 'alto+8':
6197
+ case 'alto-8':
6198
+ case 'alto^8':
6199
+ case 'alto_8':
6200
+ // MAE 26 May 2025 End of additional clefs
6201
+
6127
6202
  // TODO-PER: handle the octave indicators on the clef by changing the middle property
6128
6203
  var oct2 = 0;
6129
- // for (var iii = 0; iii < token.token.length; iii++) {
6130
- // if (token.token[iii] === ',') oct2 -= 7;
6131
- // else if (token.token[iii] === "'") oct2 += 7;
6132
- // }
6133
6204
  staffInfo.clef = token.token.replace(/[',]/g, ""); //'//comment for emacs formatting of regexp
6134
6205
  staffInfo.verticalPos = calcMiddle(staffInfo.clef, oct2);
6135
6206
  multilineVars.voices[id].clef = token.token;
@@ -6223,6 +6294,7 @@ var parseKeyVoice = {};
6223
6294
  // console.log("parse voice", token, tune.metaText.title);
6224
6295
  }
6225
6296
  }
6297
+
6226
6298
  start += tokenizer.eatWhiteSpace(line, start);
6227
6299
  }
6228
6300
 
@@ -6397,6 +6469,7 @@ MusicParser.prototype.parseMusic = function (line) {
6397
6469
  // delayStartNewLine = true;
6398
6470
  // TODO-PER: Handle inline headers
6399
6471
  }
6472
+
6400
6473
  var overlayLevel = 0;
6401
6474
  while (i < line.length) {
6402
6475
  var startI = i;
@@ -6535,8 +6608,7 @@ MusicParser.prototype.parseMusic = function (line) {
6535
6608
  if (el.chord !== undefined) bar.chord = el.chord;
6536
6609
  if (bar.startEnding && multilineVars.barFirstEndingNum === undefined) multilineVars.barFirstEndingNum = multilineVars.currBarNumber;else if (bar.startEnding && bar.endEnding && multilineVars.barFirstEndingNum) multilineVars.currBarNumber = multilineVars.barFirstEndingNum;else if (bar.endEnding) multilineVars.barFirstEndingNum = undefined;
6537
6610
  if (bar.type !== 'bar_invisible' && multilineVars.measureNotEmpty) {
6538
- var isFirstVoice = multilineVars.currentVoice === undefined || multilineVars.currentVoice.staffNum === 0 && multilineVars.currentVoice.index === 0;
6539
- if (isFirstVoice) {
6611
+ if (isFirstVoice()) {
6540
6612
  multilineVars.currBarNumber++;
6541
6613
  if (multilineVars.barNumbers && multilineVars.currBarNumber % multilineVars.barNumbers === 0) bar.barNumber = multilineVars.currBarNumber;
6542
6614
  }
@@ -6731,6 +6803,7 @@ MusicParser.prototype.parseMusic = function (line) {
6731
6803
  if (el.startTie !== undefined) el.pitches[0].startTie = el.startTie;
6732
6804
  } else {
6733
6805
  el.rest = core.rest;
6806
+ if (core.rest.type === 'multimeasure' && isFirstVoice()) multilineVars.currBarNumber += core.rest.text - 1; // The minus one is because the measure with the rest is already counted once normally.
6734
6807
  if (core.endSlur !== undefined) el.endSlur = core.endSlur;
6735
6808
  if (core.endTie !== undefined) el.rest.endTie = core.endTie;
6736
6809
  if (core.startSlur !== undefined) el.startSlur = core.startSlur;
@@ -7023,6 +7096,8 @@ var letter_to_accent = function letter_to_accent(line, i) {
7023
7096
  return [1, 'segno'];
7024
7097
  case 'T':
7025
7098
  return [1, 'trill'];
7099
+ case 't':
7100
+ return [1, 'trillh'];
7026
7101
  }
7027
7102
  return [0, 0];
7028
7103
  };
@@ -7191,8 +7266,7 @@ MusicParser.prototype.startNewLine = function () {
7191
7266
  if (params.currentVoice.staffNum === multilineVars.voices[voices[mv]].staffNum && params.currentVoice.index === multilineVars.voices[voices[mv]].index) params.currentVoiceName = voices[mv];
7192
7267
  }
7193
7268
  }
7194
- var isFirstVoice = multilineVars.currentVoice === undefined || multilineVars.currentVoice.staffNum === 0 && multilineVars.currentVoice.index === 0;
7195
- if (multilineVars.barNumbers === 0 && isFirstVoice && multilineVars.currBarNumber !== 1) params.barNumber = multilineVars.currBarNumber;
7269
+ if (multilineVars.barNumbers === 0 && isFirstVoice() && multilineVars.currBarNumber !== 1) params.barNumber = multilineVars.currBarNumber;
7196
7270
  tuneBuilder.startNewLine(params);
7197
7271
  if (multilineVars.key.impliedNaturals) delete multilineVars.key.impliedNaturals;
7198
7272
  multilineVars.partForNextLine = {};
@@ -7488,6 +7562,9 @@ var getBrokenRhythm = function getBrokenRhythm(line, index) {
7488
7562
  }
7489
7563
  return null;
7490
7564
  };
7565
+ function isFirstVoice() {
7566
+ return multilineVars.currentVoice === undefined || multilineVars.currentVoice.staffNum === 0 && multilineVars.currentVoice.index === 0;
7567
+ }
7491
7568
  module.exports = MusicParser;
7492
7569
 
7493
7570
  /***/ }),
@@ -7498,7 +7575,7 @@ module.exports = MusicParser;
7498
7575
  \*****************************************/
7499
7576
  /***/ (function(module) {
7500
7577
 
7501
- module.exports.legalAccents = ['trill', 'lowermordent', 'uppermordent', 'mordent', 'pralltriller', 'accent', 'fermata', 'invertedfermata', 'tenuto', '0', '1', '2', '3', '4', '5', '+', 'wedge', 'open', 'thumb', 'snap', 'turn', 'roll', 'breath', 'shortphrase', 'mediumphrase', 'longphrase', 'segno', 'coda', 'D.S.', 'D.C.', 'fine', 'beambr1', 'beambr2', 'slide', 'marcato', 'upbow', 'downbow', '/', '//', '///', '////', 'trem1', 'trem2', 'trem3', 'trem4', 'turnx', 'invertedturn', 'invertedturnx', 'trill(', 'trill)', 'arpeggio', 'xstem', 'mark', 'umarcato', 'style=normal', 'style=harmonic', 'style=rhythm', 'style=x', 'style=triangle', 'D.C.alcoda', 'D.C.alfine', 'D.S.alcoda', 'D.S.alfine', 'editorial', 'courtesy'];
7578
+ module.exports.legalAccents = ['trill', 'trillh', 'lowermordent', 'uppermordent', 'mordent', 'pralltriller', 'accent', 'fermata', 'invertedfermata', 'tenuto', '0', '1', '2', '3', '4', '5', '+', 'wedge', 'open', 'thumb', 'snap', 'turn', 'roll', 'breath', 'shortphrase', 'mediumphrase', 'longphrase', 'segno', 'coda', 'D.S.', 'D.C.', 'fine', 'beambr1', 'beambr2', 'slide', 'marcato', 'upbow', 'downbow', '/', '//', '///', '////', 'trem1', 'trem2', 'trem3', 'trem4', 'turnx', 'invertedturn', 'invertedturnx', 'trill(', 'trill)', 'arpeggio', 'xstem', 'mark', 'umarcato', 'style=normal', 'style=harmonic', 'style=rhythm', 'style=x', 'style=triangle', 'D.C.alcoda', 'D.C.alfine', 'D.S.alcoda', 'D.S.alfine', 'editorial', 'courtesy'];
7502
7579
  module.exports.volumeDecorations = ['p', 'pp', 'f', 'ff', 'mf', 'mp', 'ppp', 'pppp', 'fff', 'ffff', 'sfz'];
7503
7580
  module.exports.dynamicDecorations = ['crescendo(', 'crescendo)', 'diminuendo(', 'diminuendo)', 'glissando(', 'glissando)', '~(', '~)'];
7504
7581
  module.exports.accentPseudonyms = [['<', 'accent'], ['>', 'accent'], ['tr', 'trill'], ['plus', '+'], ['emphasis', 'accent'], ['^', 'umarcato'], ['marcato', 'umarcato']];
@@ -7577,6 +7654,7 @@ var Tokenizer = function Tokenizer(lines, multilineVars) {
7577
7654
  }
7578
7655
  return str.length; // It must have been all white space
7579
7656
  };
7657
+
7580
7658
  var finished = function finished(str, i) {
7581
7659
  return i >= str.length;
7582
7660
  };
@@ -7637,6 +7715,7 @@ var Tokenizer = function Tokenizer(lines, multilineVars) {
7637
7715
  // case 'f':return {len: i+1, token: 'F'};
7638
7716
  // case 'g':return {len: i+1, token: 'G'};
7639
7717
  }
7718
+
7640
7719
  return {
7641
7720
  len: 0
7642
7721
  };
@@ -8468,6 +8547,7 @@ var Tokenizer = function Tokenizer(lines, multilineVars) {
8468
8547
 
8469
8548
  // More chars: IJ ij Ď ď Đ đ Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š Ţ ţ Ť ť Ŧ ŧ Ŵ ŵ Ź ź Ż ż Ž
8470
8549
  };
8550
+
8471
8551
  var charMap1 = {
8472
8552
  "#": "♯",
8473
8553
  "b": "♭",
@@ -8713,14 +8793,38 @@ var Tokenizer = function Tokenizer(lines, multilineVars) {
8713
8793
  }
8714
8794
  }
8715
8795
  var thePatterns = [{
8716
- match: /,\s*[Tt]he$/,
8796
+ match: /,\s*The$/,
8717
8797
  replace: "The "
8718
8798
  }, {
8719
- match: /,\s*[Aa]$/,
8799
+ match: /,\s*the$/,
8800
+ replace: "the "
8801
+ }, {
8802
+ match: /,\s*A$/,
8720
8803
  replace: "A "
8721
8804
  }, {
8722
- match: /,\s*[Aa]n$/,
8805
+ match: /,\s*a$/,
8806
+ replace: "a "
8807
+ }, {
8808
+ match: /,\s*An$/,
8723
8809
  replace: "An "
8810
+ }, {
8811
+ match: /,\s*an$/,
8812
+ replace: "an "
8813
+ }, {
8814
+ match: /,\s*Da$/,
8815
+ replace: "Da "
8816
+ }, {
8817
+ match: /,\s*La$/,
8818
+ replace: "La "
8819
+ }, {
8820
+ match: /,\s*Le$/,
8821
+ replace: "Le "
8822
+ }, {
8823
+ match: /,\s*Les$/,
8824
+ replace: "Les "
8825
+ }, {
8826
+ match: /,\s*Ye$/,
8827
+ replace: "Ye "
8724
8828
  }];
8725
8829
  this.theReverser = function (str) {
8726
8830
  for (var i = 0; i < thePatterns.length; i++) {
@@ -8970,7 +9074,9 @@ transpose.keySignature = function (multilineVars, keyName, root, acc, localTrans
8970
9074
  var newKeyName = keyName[0] === 'm' ? newKeyMinor[index] : newKey[index];
8971
9075
  var transposedKey = newKeyName + keyName;
8972
9076
  var newKeySig = keyAccidentals(transposedKey);
8973
- if (newKeySig.length > 0 && newKeySig[0].acc === 'flat') multilineVars.localTransposePreferFlats = true;
9077
+ if (newKeySig.length === 0 || newKeySig[0].acc === 'flat')
9078
+ // key of C and all keys with flats should have chords with flats
9079
+ multilineVars.localTransposePreferFlats = true;
8974
9080
  var distance = transposedKey.charCodeAt(0) - baseKey.charCodeAt(0);
8975
9081
  if (multilineVars.localTranspose > 0) {
8976
9082
  if (distance < 0) distance += 7;else if (distance === 0) {
@@ -9142,6 +9248,13 @@ function transposeChordName(chord, steps, preferFlats, freeGCchord) {
9142
9248
  } else {
9143
9249
  if (freeGCchord) chord = sharpChordsFree[index];else chord = sharpChords[index];
9144
9250
  }
9251
+ var isDim = extra1 && (extra1.indexOf('dim') >= 0 || extra1.indexOf('°') >= 0);
9252
+ console.log(isDim, chord, extra1);
9253
+ // We never want A#dim or D#dim
9254
+ if (isDim && chord === 'A#') chord = 'Bb';
9255
+ if (isDim && chord === 'D#') chord = 'Eb';
9256
+ if (isDim && chord === 'A♯') chord = 'B♭';
9257
+ if (isDim && chord === 'D♯') chord = 'E♭';
9145
9258
  if (extra1) chord += extra1;
9146
9259
  if (bass) {
9147
9260
  var index = sharpChords.indexOf(bass);
@@ -9159,6 +9272,7 @@ function transposeChordName(chord, steps, preferFlats, freeGCchord) {
9159
9272
  }
9160
9273
  } else chord += bass; // Don't know what to do so do nothing
9161
9274
  }
9275
+
9162
9276
  if (extra2) chord += extra2;
9163
9277
  return chord;
9164
9278
  }
@@ -9182,7 +9296,7 @@ var TuneBuilder = function TuneBuilder(tune) {
9182
9296
  var currentVoiceName = '';
9183
9297
  tune.reset();
9184
9298
  this.setVisualTranspose = function (visualTranspose) {
9185
- if (visualTranspose) tune.visualTranspose = visualTranspose;
9299
+ if (visualTranspose !== undefined) tune.visualTranspose = visualTranspose;
9186
9300
  };
9187
9301
  this.cleanUp = function (barsperstaff, staffnonote, currSlur) {
9188
9302
  closeLine(tune); // Close the last line.
@@ -9369,6 +9483,7 @@ var TuneBuilder = function TuneBuilder(tune) {
9369
9483
  if (hashParams.rest && hashParams.rest.type === 'invisible') {
9370
9484
  delete hashParams.decoration; // the decorations on invisible rests should be invisible, too.
9371
9485
  }
9486
+
9372
9487
  if (tune.lines.length <= tune.lineNum || tune.lines[tune.lineNum].staff.length <= tune.staffNum) {
9373
9488
  //console.log("pushNote IGNORED", tune.lines[tune.lineNum])
9374
9489
  // TODO-PER: This prevents a crash, but it drops the element. Need to figure out how to start a new line, or delay adding this.
@@ -9556,7 +9671,7 @@ var TuneBuilder = function TuneBuilder(tune) {
9556
9671
  };
9557
9672
  this.getCurrentVoice = function () {
9558
9673
  //console.log("getCurrentVoice", tune.lineNum)
9559
- var currLine = tune.lines[tune.lineNum];
9674
+ var currLine = getPrevMusicLine(tune.lines, tune.lineNum);
9560
9675
  if (!currLine) return null;
9561
9676
  var currStaff = currLine.staff[tune.staffNum];
9562
9677
  if (!currStaff) return null;
@@ -9836,6 +9951,7 @@ function cleanUpSlursInLine(line, staffNum, voiceNum, currSlur) {
9836
9951
  obj.endSlur.push(slurNum);
9837
9952
  // lyr.syllable += '<' + slurNum; // TODO-PER: debugging
9838
9953
  }
9954
+
9839
9955
  if (currSlur[staffNum][voiceNum][chordPos].length === 0) delete currSlur[staffNum][voiceNum][chordPos];
9840
9956
  return slurNum;
9841
9957
  };
@@ -9978,6 +10094,15 @@ function wrapMusicLines(lines, barsperstaff) {
9978
10094
  }
9979
10095
  return false;
9980
10096
  }
10097
+ function getPrevMusicLine(lines, currentLine) {
10098
+ if (lines.length <= currentLine) return null;
10099
+ // If the current line doesn't have music, search backwards until one is found.
10100
+ while (currentLine >= 0) {
10101
+ if (lines[currentLine].staff) return lines[currentLine];
10102
+ currentLine--;
10103
+ }
10104
+ return null;
10105
+ }
9981
10106
  function getNextMusicLine(lines, currentLine) {
9982
10107
  currentLine++;
9983
10108
  while (lines.length > currentLine) {
@@ -10716,16 +10841,16 @@ var strTranspose;
10716
10841
  var count = arr[0].length;
10717
10842
  for (var i = 1; i < arr.length; i++) {
10718
10843
  var segment = arr[i];
10719
- var match = segment.match(/^( *)([A-G])([#b]?)(\w*)/);
10844
+ var match = segment.match(/^( *)([A-G])([#b]?)( ?)(\w*)/);
10720
10845
  if (match) {
10721
10846
  var start = count + 2 + match[1].length; // move past the 'K:' and optional white space
10722
- var key = match[2] + match[3] + match[4]; // key name, accidental, and mode
10847
+ var key = match[2] + match[3] + match[4] + match[5]; // key name, accidental, optional space, and mode
10723
10848
  var destinationKey = newKey({
10724
10849
  root: match[2],
10725
10850
  acc: match[3],
10726
- mode: match[4]
10851
+ mode: match[5]
10727
10852
  }, steps);
10728
- var dest = destinationKey.root + destinationKey.acc + destinationKey.mode;
10853
+ var dest = destinationKey.root + destinationKey.acc + match[4] + destinationKey.mode;
10729
10854
  changes.push({
10730
10855
  start: start,
10731
10856
  end: start + key.length,
@@ -11085,6 +11210,7 @@ var strTranspose;
11085
11210
  return 0;
11086
11211
  // this should never happen
11087
11212
  }
11213
+
11088
11214
  case '_':
11089
11215
  switch (thisAccidental) {
11090
11216
  case '__':
@@ -11101,6 +11227,7 @@ var strTranspose;
11101
11227
  return 0;
11102
11228
  // this should never happen
11103
11229
  }
11230
+
11104
11231
  case '^':
11105
11232
  switch (thisAccidental) {
11106
11233
  case '__':
@@ -11118,9 +11245,11 @@ var strTranspose;
11118
11245
  // this should never happen
11119
11246
  }
11120
11247
  }
11248
+
11121
11249
  return 0; // this should never happen
11122
11250
  }
11123
11251
  })();
11252
+
11124
11253
  module.exports = strTranspose;
11125
11254
 
11126
11255
  /***/ }),
@@ -11169,6 +11298,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11169
11298
  var stressBeat1 = 105;
11170
11299
  var stressBeatDown = 95;
11171
11300
  var stressBeatUp = 85;
11301
+ var volumesPerNotePitch = [[stressBeat1, stressBeatDown, stressBeatUp]];
11172
11302
  var beatFraction = 0.25;
11173
11303
  var nextVolume;
11174
11304
  var nextVolumeDelta;
@@ -11210,6 +11340,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11210
11340
  stressBeat1 = 105;
11211
11341
  stressBeatDown = 95;
11212
11342
  stressBeatUp = 85;
11343
+ volumesPerNotePitch = [];
11213
11344
  beatFraction = 0.25;
11214
11345
  nextVolume = undefined;
11215
11346
  nextVolumeDelta = undefined;
@@ -11320,6 +11451,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11320
11451
  stressBeat1 = element.beats[0];
11321
11452
  stressBeatDown = element.beats[1];
11322
11453
  stressBeatUp = element.beats[2];
11454
+ if (!element.volumesPerNotePitch) volumesPerNotePitch = [];else volumesPerNotePitch = element.volumesPerNotePitch;
11323
11455
  // TODO-PER: also use the last parameter - which changes which beats are strong.
11324
11456
  break;
11325
11457
  case "vol":
@@ -11445,6 +11577,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11445
11577
  // console.log(JSON.stringify(voices))
11446
11578
  }
11447
11579
  }
11580
+
11448
11581
  function getBeatFraction(meter) {
11449
11582
  switch (parseInt(meter.den, 10)) {
11450
11583
  case 2:
@@ -11462,21 +11595,29 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11462
11595
  var distanceFromStart = currTime - measureStart;
11463
11596
  return distanceFromStart / beatLength;
11464
11597
  }
11465
- function processVolume(beat, voiceOff) {
11598
+ function processVolume(beat, voiceOff, pitchIndexOfNote) {
11466
11599
  if (voiceOff) return 0;
11600
+ var pitchStressBeat1 = stressBeat1;
11601
+ var pitchStressBeatDown = stressBeatDown;
11602
+ var pitchStressBeatUp = stressBeatUp;
11603
+ if (pitchIndexOfNote !== undefined && volumesPerNotePitch.length >= pitchIndexOfNote + 1) {
11604
+ pitchStressBeat1 = volumesPerNotePitch[pitchIndexOfNote][0];
11605
+ pitchStressBeatDown = volumesPerNotePitch[pitchIndexOfNote][1];
11606
+ pitchStressBeatUp = volumesPerNotePitch[pitchIndexOfNote][2];
11607
+ }
11467
11608
  var volume;
11468
11609
  // MAE 21 Jun 2024 - This previously wasn't allowing zero volume to be applied
11469
- if (nextVolume != undefined) {
11610
+ if (nextVolume !== undefined) {
11470
11611
  volume = nextVolume;
11471
11612
  nextVolume = undefined;
11472
11613
  } else if (!doBeatAccents) {
11473
- volume = stressBeatDown;
11614
+ volume = pitchStressBeatDown;
11474
11615
  } else if (pickupLength > beat) {
11475
- volume = stressBeatUp;
11616
+ volume = pitchStressBeatUp;
11476
11617
  } else {
11477
11618
  //var barLength = meter.num / meter.den;
11478
11619
  var barBeat = calcBeat(lastBarTime, getBeatFraction(meter), beat);
11479
- if (barBeat === 0) volume = stressBeat1;else if (parseInt(barBeat, 10) === barBeat) volume = stressBeatDown;else volume = stressBeatUp;
11620
+ if (barBeat === 0) volume = pitchStressBeat1;else if (parseInt(barBeat, 10) === barBeat) volume = pitchStressBeatDown;else volume = pitchStressBeatUp;
11480
11621
  }
11481
11622
  if (nextVolumeDelta) {
11482
11623
  volume += nextVolumeDelta;
@@ -11490,7 +11631,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11490
11631
  var ret = {};
11491
11632
  if (elem.decoration) {
11492
11633
  for (var d = 0; d < elem.decoration.length; d++) {
11493
- if (elem.decoration[d] === 'staccato') ret.thisBreakBetweenNotes = 'staccato';else if (elem.decoration[d] === 'tenuto') ret.thisBreakBetweenNotes = 'tenuto';else if (elem.decoration[d] === 'accent') ret.velocity = Math.min(127, velocity * 1.5);else if (elem.decoration[d] === 'trill') ret.noteModification = "trill";else if (elem.decoration[d] === 'lowermordent') ret.noteModification = "lowermordent";else if (elem.decoration[d] === 'uppermordent') ret.noteModification = "mordent";else if (elem.decoration[d] === 'mordent') ret.noteModification = "mordent";else if (elem.decoration[d] === 'turn') ret.noteModification = "turn";else if (elem.decoration[d] === 'roll') ret.noteModification = "roll";
11634
+ if (elem.decoration[d] === 'staccato') ret.thisBreakBetweenNotes = 'staccato';else if (elem.decoration[d] === 'tenuto') ret.thisBreakBetweenNotes = 'tenuto';else if (elem.decoration[d] === 'accent') ret.velocity = Math.min(127, velocity * 1.5);else if (elem.decoration[d] === 'trill') ret.noteModification = "trill";else if (elem.decoration[d] === 'lowermordent') ret.noteModification = "lowermordent";else if (elem.decoration[d] === 'uppermordent') ret.noteModification = "pralltriller";else if (elem.decoration[d] === 'mordent') ret.noteModification = "mordent";else if (elem.decoration[d] === 'turn') ret.noteModification = "turn";else if (elem.decoration[d] === 'roll') ret.noteModification = "roll";else if (elem.decoration[d] === 'pralltriller') ret.noteModification = "pralltriller";else if (elem.decoration[d] === 'trillh') ret.noteModification = "trillh";
11494
11635
  }
11495
11636
  }
11496
11637
  return ret;
@@ -11504,6 +11645,24 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11504
11645
  var shortestNote = durationRounded(1.0 / 32);
11505
11646
  switch (noteModification) {
11506
11647
  case "trill":
11648
+ var note = 2;
11649
+ while (runningDuration > 0) {
11650
+ currentTrack.push({
11651
+ cmd: 'note',
11652
+ pitch: p.pitch + note,
11653
+ volume: p.volume,
11654
+ start: start,
11655
+ duration: shortestNote,
11656
+ gap: 0,
11657
+ instrument: currentInstrument,
11658
+ style: 'decoration'
11659
+ });
11660
+ note = note === 2 ? 0 : 2;
11661
+ runningDuration -= shortestNote;
11662
+ start += shortestNote;
11663
+ }
11664
+ break;
11665
+ case "trillh":
11507
11666
  var note = 1;
11508
11667
  while (runningDuration > 0) {
11509
11668
  currentTrack.push({
@@ -11521,7 +11680,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11521
11680
  start += shortestNote;
11522
11681
  }
11523
11682
  break;
11524
- case "mordent":
11683
+ case "pralltriller":
11525
11684
  currentTrack.push({
11526
11685
  cmd: 'note',
11527
11686
  pitch: p.pitch,
@@ -11536,7 +11695,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11536
11695
  start += shortestNote;
11537
11696
  currentTrack.push({
11538
11697
  cmd: 'note',
11539
- pitch: p.pitch + 1,
11698
+ pitch: p.pitch + 2,
11540
11699
  volume: p.volume,
11541
11700
  start: start,
11542
11701
  duration: shortestNote,
@@ -11556,6 +11715,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11556
11715
  instrument: currentInstrument
11557
11716
  });
11558
11717
  break;
11718
+ case "mordent":
11559
11719
  case "lowermordent":
11560
11720
  currentTrack.push({
11561
11721
  cmd: 'note',
@@ -11571,7 +11731,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11571
11731
  start += shortestNote;
11572
11732
  currentTrack.push({
11573
11733
  cmd: 'note',
11574
- pitch: p.pitch - 1,
11734
+ pitch: p.pitch - 2,
11575
11735
  volume: p.volume,
11576
11736
  start: start,
11577
11737
  duration: shortestNote,
@@ -11592,10 +11752,10 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11592
11752
  });
11593
11753
  break;
11594
11754
  case "turn":
11595
- shortestNote = p.duration / 5;
11755
+ shortestNote = p.duration / 4;
11596
11756
  currentTrack.push({
11597
11757
  cmd: 'note',
11598
- pitch: p.pitch,
11758
+ pitch: p.pitch + 2,
11599
11759
  volume: p.volume,
11600
11760
  start: start,
11601
11761
  duration: shortestNote,
@@ -11605,7 +11765,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11605
11765
  });
11606
11766
  currentTrack.push({
11607
11767
  cmd: 'note',
11608
- pitch: p.pitch + 1,
11768
+ pitch: p.pitch,
11609
11769
  volume: p.volume,
11610
11770
  start: start + shortestNote,
11611
11771
  duration: shortestNote,
@@ -11615,7 +11775,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11615
11775
  });
11616
11776
  currentTrack.push({
11617
11777
  cmd: 'note',
11618
- pitch: p.pitch,
11778
+ pitch: p.pitch - 1,
11619
11779
  volume: p.volume,
11620
11780
  start: start + shortestNote * 2,
11621
11781
  duration: shortestNote,
@@ -11625,7 +11785,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11625
11785
  });
11626
11786
  currentTrack.push({
11627
11787
  cmd: 'note',
11628
- pitch: p.pitch + 1,
11788
+ pitch: p.pitch,
11629
11789
  volume: p.volume,
11630
11790
  start: start + shortestNote * 3,
11631
11791
  duration: shortestNote,
@@ -11633,15 +11793,6 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11633
11793
  instrument: currentInstrument,
11634
11794
  style: 'decoration'
11635
11795
  });
11636
- currentTrack.push({
11637
- cmd: 'note',
11638
- pitch: p.pitch,
11639
- volume: p.volume,
11640
- start: start + shortestNote * 4,
11641
- duration: shortestNote,
11642
- gap: 0,
11643
- instrument: currentInstrument
11644
- });
11645
11796
  break;
11646
11797
  case "roll":
11647
11798
  while (runningDuration > 0) {
@@ -11729,6 +11880,11 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11729
11880
  }
11730
11881
  if (elem.elem) elem.elem.midiPitches = [];
11731
11882
  for (var i = 0; i < ePitches.length; i++) {
11883
+ //here we can set the volume for each note in a chord, if specified
11884
+ var pitchVelocity = velocity;
11885
+ if (!ret.velocity && Array.isArray(elem.decoration) && elem.decoration.length > i) {
11886
+ pitchVelocity = processVolume(timeToRealTime(elem.time), voiceOff, i);
11887
+ }
11732
11888
  var note = ePitches[i];
11733
11889
  if (!note) continue;
11734
11890
  if (note.startSlur) slurCount += note.startSlur.length;
@@ -11741,7 +11897,7 @@ var pitchesToPerc = __webpack_require__(/*! ./pitches-to-perc */ "./src/synth/pi
11741
11897
  var p = {
11742
11898
  cmd: 'note',
11743
11899
  pitch: actualPitch,
11744
- volume: velocity,
11900
+ volume: pitchVelocity,
11745
11901
  start: timeToRealTime(elem.time),
11746
11902
  duration: durationRounded(note.duration),
11747
11903
  instrument: currentInstrument,
@@ -12220,6 +12376,7 @@ var rendererFactory;
12220
12376
  this.track += this.noteOnAndChannel;
12221
12377
  this.track += "%" + pitch.toString(16) + toHex(loudness, 2); //note
12222
12378
  };
12379
+
12223
12380
  Midi.prototype.endNote = function (pitch) {
12224
12381
  this.track += toDurationHex(this.silencelength); // only need to shift by amount of silence (if there is any)
12225
12382
  this.silencelength = 0;
@@ -12233,6 +12390,7 @@ var rendererFactory;
12233
12390
  this.track += this.noteOffAndChannel;
12234
12391
  this.track += "%" + pitch.toString(16) + "%00"; //end note
12235
12392
  };
12393
+
12236
12394
  Midi.prototype.addRest = function (length) {
12237
12395
  this.silencelength += length;
12238
12396
  if (this.silencelength < 0) this.silencelength = 0;
@@ -12269,6 +12427,7 @@ var rendererFactory;
12269
12427
  }
12270
12428
  return "%00%FF" + cmdType + toHex(nameArray.length / 3, 2) + nameArray; // Each byte is represented by three chars "%XX", so divide by 3 to get the length.
12271
12429
  }
12430
+
12272
12431
  function keySignature(key) {
12273
12432
  //00 FF 5902 03 00 - key signature
12274
12433
  if (!key || !key.accidentals) return "";
@@ -12521,6 +12680,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12521
12680
 
12522
12681
  // visit each voice completely in turn
12523
12682
  var voices = [];
12683
+ var clefTransposeActive = [];
12524
12684
  var inCrescendo = [];
12525
12685
  var inDiminuendo = [];
12526
12686
  var durationCounter = [0];
@@ -12541,6 +12701,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12541
12701
  if (line.staff) {
12542
12702
  var setDynamics = function setDynamics(elem) {
12543
12703
  var volumes = {
12704
+ //stressBeat1, stressBeatDown, stressBeatUp
12544
12705
  'pppp': [15, 10, 5, 1],
12545
12706
  'ppp': [30, 20, 10, 1],
12546
12707
  'pp': [45, 35, 20, 1],
@@ -12557,9 +12718,17 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12557
12718
  if (elem.decoration.indexOf('pppp') >= 0) dynamicType = 'pppp';else if (elem.decoration.indexOf('ppp') >= 0) dynamicType = 'ppp';else if (elem.decoration.indexOf('pp') >= 0) dynamicType = 'pp';else if (elem.decoration.indexOf('p') >= 0) dynamicType = 'p';else if (elem.decoration.indexOf('mp') >= 0) dynamicType = 'mp';else if (elem.decoration.indexOf('mf') >= 0) dynamicType = 'mf';else if (elem.decoration.indexOf('f') >= 0) dynamicType = 'f';else if (elem.decoration.indexOf('ff') >= 0) dynamicType = 'ff';else if (elem.decoration.indexOf('fff') >= 0) dynamicType = 'fff';else if (elem.decoration.indexOf('ffff') >= 0) dynamicType = 'ffff';
12558
12719
  if (dynamicType) {
12559
12720
  currentVolume = volumes[dynamicType].slice(0);
12721
+ var volumesPerNotePitch = [currentVolume];
12722
+ if (Array.isArray(elem.decoration)) {
12723
+ volumesPerNotePitch = [];
12724
+ elem.decoration.forEach(function (d) {
12725
+ volumesPerNotePitch.push(volumes[d].slice(0));
12726
+ });
12727
+ }
12560
12728
  voices[voiceNumber].push({
12561
12729
  el_type: 'beat',
12562
- beats: currentVolume.slice(0)
12730
+ beats: currentVolume.slice(0),
12731
+ volumesPerNotePitch: volumesPerNotePitch
12563
12732
  });
12564
12733
  inCrescendo[k] = false;
12565
12734
  inDiminuendo[k] = false;
@@ -12637,15 +12806,31 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12637
12806
  el_type: 'transpose',
12638
12807
  transpose: staff.clef.transpose
12639
12808
  });
12809
+ clefTransposeActive[voiceNumber] = false;
12640
12810
  }
12641
12811
  if (staff.clef && staff.clef.type) {
12642
- if (staff.clef.type.indexOf("-8") >= 0) voices[voiceNumber].push({
12643
- el_type: 'transpose',
12644
- transpose: -12
12645
- });else if (staff.clef.type.indexOf("+8") >= 0) voices[voiceNumber].push({
12646
- el_type: 'transpose',
12647
- transpose: 12
12648
- });
12812
+ if (staff.clef.type.indexOf("-8") >= 0) {
12813
+ voices[voiceNumber].push({
12814
+ el_type: 'transpose',
12815
+ transpose: -12
12816
+ });
12817
+ clefTransposeActive[voiceNumber] = true;
12818
+ } else if (staff.clef.type.indexOf("+8") >= 0) {
12819
+ voices[voiceNumber].push({
12820
+ el_type: 'transpose',
12821
+ transpose: 12
12822
+ });
12823
+ clefTransposeActive[voiceNumber] = true;
12824
+ } else {
12825
+ // if we had a previous treble+8 and now have a regular clef, then cancel the transposition
12826
+ if (clefTransposeActive[voiceNumber]) {
12827
+ voices[voiceNumber].push({
12828
+ el_type: 'transpose',
12829
+ transpose: 0
12830
+ });
12831
+ clefTransposeActive[voiceNumber] = false;
12832
+ }
12833
+ }
12649
12834
  }
12650
12835
  if (abctune.formatting.midi && abctune.formatting.midi.drumoff) {
12651
12836
  // If there is a drum off command right at the beginning it is put in the metaText instead of the stream,
@@ -13058,6 +13243,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
13058
13243
  }
13059
13244
  }
13060
13245
  }
13246
+
13061
13247
  function chordVoiceOffThisBar(voices) {
13062
13248
  for (var i = 0; i < voices.length; i++) {
13063
13249
  var voice = voices[i];
@@ -13365,7 +13551,7 @@ ChordTrack.prototype.interpretChord = function (name) {
13365
13551
  };
13366
13552
  var root = name.substring(0, 1);
13367
13553
  if (root === '(') {
13368
- name = name.substring(1, name.length - 2);
13554
+ name = name.substring(1, name.length - 1);
13369
13555
  if (name.length === 0) return undefined;
13370
13556
  root = name.substring(0, 1);
13371
13557
  }
@@ -13529,6 +13715,7 @@ ChordTrack.prototype.resolveChords = function (startTime, endTime) {
13529
13715
  if (newBass) newBass = false;else isBoom = false; // only the first note in a chord is a bass note. This handles the case where bass and chord are played at the same time.
13530
13716
  }
13531
13717
  }
13718
+
13532
13719
  return;
13533
13720
  };
13534
13721
  ChordTrack.prototype.processChord = function (elem) {
@@ -14136,7 +14323,7 @@ function CreateSynth() {
14136
14323
  self.audioBuffers = []; // cache of the buffers so starting play can be fast.
14137
14324
  self.duration = undefined; // the duration of the tune in seconds.
14138
14325
  self.isRunning = false; // whether there is currently a sound buffer running.
14139
- self.options = undefined;
14326
+ self.options = {}; // Thx tomohirohiratsuka
14140
14327
  self.pickupLength = 0;
14141
14328
 
14142
14329
  // Load and cache all needed sounds
@@ -14364,87 +14551,93 @@ function CreateSynth() {
14364
14551
  self.isRunning = false;
14365
14552
  if (!self.audioBufferPossible) return Promise.reject(new Error(notSupportedMessage));
14366
14553
  if (self.debugCallback) self.debugCallback("prime called");
14367
- return new Promise(function (resolve) {
14368
- var startTime = activeAudioContext().currentTime;
14369
- var tempoMultiplier = self.millisecondsPerMeasure / 1000 / self.meterSize;
14370
- self.duration = self.flattened.totalDuration * tempoMultiplier;
14371
- if (self.duration <= 0) {
14372
- self.audioBuffers = [];
14373
- return resolve({
14374
- status: "empty",
14375
- seconds: 0
14376
- });
14377
- }
14378
- self.duration += fadeTimeSec;
14379
- var totalSamples = Math.floor(activeAudioContext().sampleRate * self.duration);
14380
-
14381
- // There might be a previous run that needs to be turned off.
14382
- self.stop();
14383
- var noteMapTracks = createNoteMap(self.flattened);
14384
- if (self.options.swing) addSwing(noteMapTracks, self.options.swing, self.meterFraction, self.pickupLength);
14385
- if (self.sequenceCallback) self.sequenceCallback(noteMapTracks, self.callbackContext);
14386
- var panDistances = setPan(noteMapTracks.length, self.pan);
14387
-
14388
- // Create a simple list of all the unique sounds in this music and where they should be placed.
14389
- // There appears to be a limit on how many audio buffers can be created at once so this technique limits the number needed.
14390
- var uniqueSounds = {};
14391
- noteMapTracks.forEach(function (noteMap, trackNumber) {
14392
- var panDistance = panDistances && panDistances.length > trackNumber ? panDistances[trackNumber] : 0;
14393
- noteMap.forEach(function (note) {
14394
- var key = note.instrument + ':' + note.pitch + ':' + note.volume + ':' + Math.round((note.end - note.start) * 1000) / 1000 + ':' + panDistance + ':' + tempoMultiplier + ':' + (note.cents ? note.cents : 0);
14395
- if (self.debugCallback) self.debugCallback("noteMapTrack " + key);
14396
- if (!uniqueSounds[key]) uniqueSounds[key] = [];
14397
- uniqueSounds[key].push(note.start);
14398
- });
14399
- });
14400
-
14401
- // Now that we know what we are trying to create, construct the audio buffer by creating each sound and placing it.
14402
- var allPromises = [];
14403
- var audioBuffer = activeAudioContext().createBuffer(2, totalSamples, activeAudioContext().sampleRate);
14404
- for (var key2 = 0; key2 < Object.keys(uniqueSounds).length; key2++) {
14405
- var k = Object.keys(uniqueSounds)[key2];
14406
- var parts = k.split(":");
14407
- var cents = parts[6] !== undefined ? parseFloat(parts[6]) : 0;
14408
- parts = {
14409
- instrument: parts[0],
14410
- pitch: parseInt(parts[1], 10),
14411
- volume: parseInt(parts[2], 10),
14412
- len: parseFloat(parts[3]),
14413
- pan: parseFloat(parts[4]),
14414
- tempoMultiplier: parseFloat(parts[5]),
14415
- cents: cents
14416
- };
14417
- allPromises.push(placeNote(audioBuffer, activeAudioContext().sampleRate, parts, uniqueSounds[k], self.soundFontVolumeMultiplier, self.programOffsets[parts.instrument], fadeTimeSec, self.noteEnd / 1000, self.debugCallback));
14418
- }
14419
- self.audioBuffers = [audioBuffer];
14420
- if (self.debugCallback) {
14421
- self.debugCallback("sampleRate = " + activeAudioContext().sampleRate);
14422
- self.debugCallback("totalSamples = " + totalSamples);
14423
- self.debugCallback("creationTime = " + Math.floor((activeAudioContext().currentTime - startTime) * 1000) + "ms");
14424
- }
14425
- function resolveData(me) {
14426
- var duration = me && me.audioBuffers && me.audioBuffers.length > 0 ? me.audioBuffers[0].duration : 0;
14427
- return {
14428
- status: activeAudioContext().state,
14429
- duration: duration
14554
+ return new Promise(function (resolve, reject) {
14555
+ try {
14556
+ var resolveData = function resolveData(me) {
14557
+ var duration = me && me.audioBuffers && me.audioBuffers.length > 0 ? me.audioBuffers[0].duration : 0;
14558
+ return {
14559
+ status: activeAudioContext().state,
14560
+ duration: duration
14561
+ };
14430
14562
  };
14431
- }
14432
- Promise.all(allPromises).then(function () {
14433
- // Safari iOS can mess with the audioContext state, so resume if needed.
14434
- if (activeAudioContext().state === "suspended") {
14435
- activeAudioContext().resume().then(function () {
14436
- resolve(resolveData(self));
14563
+ var startTime = activeAudioContext().currentTime;
14564
+ var tempoMultiplier = self.millisecondsPerMeasure / 1000 / self.meterSize;
14565
+ self.duration = self.flattened.totalDuration * tempoMultiplier;
14566
+ if (self.duration <= 0) {
14567
+ self.audioBuffers = [];
14568
+ return resolve({
14569
+ status: "empty",
14570
+ seconds: 0
14571
+ });
14572
+ }
14573
+ self.duration += fadeTimeSec;
14574
+ var totalSamples = Math.floor(activeAudioContext().sampleRate * self.duration);
14575
+
14576
+ // There might be a previous run that needs to be turned off.
14577
+ self.stop();
14578
+ var noteMapTracks = createNoteMap(self.flattened);
14579
+ if (self.options.swing) addSwing(noteMapTracks, self.options.swing, self.meterFraction, self.pickupLength);
14580
+ if (self.sequenceCallback) self.sequenceCallback(noteMapTracks, self.callbackContext);
14581
+ var panDistances = setPan(noteMapTracks.length, self.pan);
14582
+
14583
+ // Create a simple list of all the unique sounds in this music and where they should be placed.
14584
+ // There appears to be a limit on how many audio buffers can be created at once so this technique limits the number needed.
14585
+ var uniqueSounds = {};
14586
+ noteMapTracks.forEach(function (noteMap, trackNumber) {
14587
+ var panDistance = panDistances && panDistances.length > trackNumber ? panDistances[trackNumber] : 0;
14588
+ noteMap.forEach(function (note) {
14589
+ var key = note.instrument + ':' + note.pitch + ':' + note.volume + ':' + Math.round((note.end - note.start) * 1000) / 1000 + ':' + panDistance + ':' + tempoMultiplier + ':' + (note.cents ? note.cents : 0);
14590
+ if (self.debugCallback) self.debugCallback("noteMapTrack " + key);
14591
+ if (!uniqueSounds[key]) uniqueSounds[key] = [];
14592
+ uniqueSounds[key].push(note.start);
14437
14593
  });
14438
- } else if (activeAudioContext().state === "interrupted") {
14439
- activeAudioContext().suspend().then(function () {
14594
+ });
14595
+
14596
+ // Now that we know what we are trying to create, construct the audio buffer by creating each sound and placing it.
14597
+ var allPromises = [];
14598
+ var audioBuffer = activeAudioContext().createBuffer(2, totalSamples, activeAudioContext().sampleRate);
14599
+ for (var key2 = 0; key2 < Object.keys(uniqueSounds).length; key2++) {
14600
+ var k = Object.keys(uniqueSounds)[key2];
14601
+ var parts = k.split(":");
14602
+ var cents = parts[6] !== undefined ? parseFloat(parts[6]) : 0;
14603
+ parts = {
14604
+ instrument: parts[0],
14605
+ pitch: parseInt(parts[1], 10),
14606
+ volume: parseInt(parts[2], 10),
14607
+ len: parseFloat(parts[3]),
14608
+ pan: parseFloat(parts[4]),
14609
+ tempoMultiplier: parseFloat(parts[5]),
14610
+ cents: cents
14611
+ };
14612
+ allPromises.push(placeNote(audioBuffer, activeAudioContext().sampleRate, parts, uniqueSounds[k], self.soundFontVolumeMultiplier, self.programOffsets[parts.instrument], fadeTimeSec, self.noteEnd / 1000, self.debugCallback));
14613
+ }
14614
+ self.audioBuffers = [audioBuffer];
14615
+ if (self.debugCallback) {
14616
+ self.debugCallback("sampleRate = " + activeAudioContext().sampleRate);
14617
+ self.debugCallback("totalSamples = " + totalSamples);
14618
+ self.debugCallback("creationTime = " + Math.floor((activeAudioContext().currentTime - startTime) * 1000) + "ms");
14619
+ }
14620
+ Promise.all(allPromises).then(function () {
14621
+ // Safari iOS can mess with the audioContext state, so resume if needed.
14622
+ if (activeAudioContext().state === "suspended") {
14440
14623
  activeAudioContext().resume().then(function () {
14441
14624
  resolve(resolveData(self));
14442
14625
  });
14443
- });
14444
- } else {
14445
- resolve(resolveData(self));
14446
- }
14447
- });
14626
+ } else if (activeAudioContext().state === "interrupted") {
14627
+ activeAudioContext().suspend().then(function () {
14628
+ activeAudioContext().resume().then(function () {
14629
+ resolve(resolveData(self));
14630
+ });
14631
+ });
14632
+ } else {
14633
+ resolve(resolveData(self));
14634
+ }
14635
+ })["catch"](function (error) {
14636
+ reject(error);
14637
+ });
14638
+ } catch (error) {
14639
+ reject(error);
14640
+ }
14448
14641
  });
14449
14642
  };
14450
14643
  function setPan(numTracks, panParam) {
@@ -14584,6 +14777,7 @@ function CreateSynth() {
14584
14777
  self.directSource[trackNum].buffer = audioBuffer; // tell the source which sound to play
14585
14778
  self.directSource[trackNum].connect(activeAudioContext().destination); // connect the source to the context's destination (the speakers)
14586
14779
  });
14780
+
14587
14781
  self.directSource.forEach(function (source) {
14588
14782
  source.start(0, seconds);
14589
14783
  });
@@ -15257,7 +15451,7 @@ function placeNote(outputAudioBuffer, sampleRate, sound, startArray, volumeMulti
15257
15451
  });
15258
15452
  })["catch"](function (error) {
15259
15453
  if (debugCallback) debugCallback('placeNote catch: ' + error.message);
15260
- return Promise.resolve();
15454
+ return Promise.reject(error);
15261
15455
  });
15262
15456
  }
15263
15457
  var copyToChannel = function copyToChannel(toBuffer, fromBuffer, start) {
@@ -16769,6 +16963,7 @@ function convertToNumber(plugin, pitches, graceNotes) {
16769
16963
  plugin.setError(tabPos.error);
16770
16964
  return tabPos; // give up on error here
16771
16965
  }
16966
+
16772
16967
  if (tabPos.graces && tabPos.notes) {
16773
16968
  // add graces to last note in notes
16774
16969
  var posNote = tabPos.notes.length - 1;
@@ -16966,6 +17161,7 @@ function getLyricHeight(voice) {
16966
17161
  }
16967
17162
  return maxLyricHeight; // add spacing
16968
17163
  }
17164
+
16969
17165
  function buildTabName(plugin, renderer, dest) {
16970
17166
  var stringSemantics = plugin.semantics;
16971
17167
  var textSize = renderer.controller.getTextSize;
@@ -17044,6 +17240,7 @@ function getNextTabPos(tabIndex, staffGroup) {
17044
17240
  if (!staffGroup[startIndex].isTabStaff) {
17045
17241
  nbVoices = staffGroup[startIndex].voices.length; // get number of staff voices
17046
17242
  }
17243
+
17047
17244
  if (staffGroup[startIndex].isTabStaff) {
17048
17245
  handledVoices++;
17049
17246
  if (islastTabInStaff(startIndex, staffGroup)) {
@@ -17156,6 +17353,7 @@ function tabRenderer(plugin, renderer, line, staffIndex) {
17156
17353
  }
17157
17354
  linkStaffAndTabs(staffGroup.staffs); // crossreference tabs and staff
17158
17355
  }
17356
+
17159
17357
  module.exports = tabRenderer;
17160
17358
 
17161
17359
  /***/ }),
@@ -17375,6 +17573,7 @@ AbstractEngraver.prototype.createABCStaff = function (staffgroup, abcstaff, temp
17375
17573
  } else {
17376
17574
  voice.duplicate = true; // bar lines and other duplicate info need not be created
17377
17575
  }
17576
+
17378
17577
  if (abcstaff.title && abcstaff.title[v]) {
17379
17578
  voice.header = abcstaff.title[v].replace(/\\n/g, "\n");
17380
17579
  voice.headerPosition = 6 + staffgroup.getTextSize.baselineToCenter(voice.header, "voicefont", 'staff-extra voice-name', v, abcstaff.voices.length) / spacing.STEP;
@@ -17388,11 +17587,13 @@ AbstractEngraver.prototype.createABCStaff = function (staffgroup, abcstaff, temp
17388
17587
  voice.addChild(clef);
17389
17588
  this.startlimitelem = clef; // limit ties here
17390
17589
  }
17590
+
17391
17591
  var keySig = createKeySignature(abcstaff.key, this.tuneNumber);
17392
17592
  if (keySig) {
17393
17593
  voice.addChild(keySig);
17394
17594
  this.startlimitelem = keySig; // limit ties here
17395
17595
  }
17596
+
17396
17597
  if (abcstaff.meter) {
17397
17598
  if (abcstaff.meter.type === 'specified') {
17398
17599
  this.measureLength = abcstaff.meter.value[0].num / abcstaff.meter.value[0].den;
@@ -17401,6 +17602,7 @@ AbstractEngraver.prototype.createABCStaff = function (staffgroup, abcstaff, temp
17401
17602
  voice.addChild(ts);
17402
17603
  this.startlimitelem = ts; // limit ties here
17403
17604
  }
17605
+
17404
17606
  if (voice.duplicate) voice.children = []; // we shouldn't reprint the above if we're reusing the same staff. We just created them to get the right spacing.
17405
17607
  var staffLines = abcstaff.clef.stafflines || abcstaff.clef.stafflines === 0 ? abcstaff.clef.stafflines : 5;
17406
17608
  staffgroup.addVoice(voice, s, staffLines);
@@ -17564,6 +17766,7 @@ AbstractEngraver.prototype.createABCElement = function (isFirstStaff, isSingleLi
17564
17766
  elemset[0] = absKey;
17565
17767
  this.startlimitelem = elemset[0]; // limit ties here
17566
17768
  }
17769
+
17567
17770
  if (voice.duplicate && elemset.length > 0) elemset[0].invisible = true;
17568
17771
  break;
17569
17772
  case "stem":
@@ -17697,6 +17900,7 @@ AbstractEngraver.prototype.addGraceNotes = function (elem, voice, abselem, noteh
17697
17900
  if (hint) gracebeam.setHint();
17698
17901
  gracebeam.mainNote = abselem; // this gives us a reference back to the note this is attached to so that the stems can be attached somewhere.
17699
17902
  }
17903
+
17700
17904
  var i;
17701
17905
  var graceoffsets = [];
17702
17906
  for (i = elem.gracenotes.length - 1; i >= 0; i--) {
@@ -17796,6 +18000,7 @@ function addRestToAbsElement(abselem, elem, duration, dot, isMultiVoice, stemdir
17796
18000
  if (duration < 0.5) restpitch = 7;else if (duration < 1) restpitch = 7; // half rest
17797
18001
  else restpitch = 5; // whole rest
17798
18002
  }
18003
+
17799
18004
  switch (elem.rest.type) {
17800
18005
  case "whole":
17801
18006
  c = chartable.rest[0];
@@ -17934,6 +18139,7 @@ AbstractEngraver.prototype.addNoteToAbcElement = function (abselem, elem, dot, s
17934
18139
  elem.pitches[p].highestVert = elem.pitches[pp - 1].verticalPos;
17935
18140
  if (getDuration(elem) < 1 && (stemdir === "up" || dir === "up")) elem.pitches[p].highestVert += 6; // If the stem is up, then compensate for the length of the stem
17936
18141
  }
18142
+
17937
18143
  if (elem.startSlur) {
17938
18144
  if (!elem.pitches[p].startSlur) elem.pitches[p].startSlur = []; //TODO possibly redundant, provided array is not optional
17939
18145
  for (i = 0; i < elem.startSlur.length; i++) {
@@ -18103,6 +18309,7 @@ AbstractEngraver.prototype.createNote = function (elem, nostem, isSingleLineStaf
18103
18309
  flatBeams: this.flatBeams
18104
18310
  }); // above is opposite from case of slurs
18105
18311
  }
18312
+
18106
18313
  if (elem.endTriplet && this.triplet) {
18107
18314
  this.triplet.setCloseAnchor(notehead);
18108
18315
  }
@@ -18201,7 +18408,8 @@ AbstractEngraver.prototype.addMeasureNumber = function (number, abselem) {
18201
18408
  if (abselem.isClef)
18202
18409
  // If this is a clef rather than bar line, then the number shouldn't be centered because it could overlap the left side. This is an easy way to let it be centered but move it over, too.
18203
18410
  dx += measureNumDim.width / 2;
18204
- var vert = measureNumDim.width > 10 && abselem.abcelem.type === "treble" ? 13 : 11;
18411
+ // MAE 1 Oct 2024 - Change 13 to 13.5 since previously bar numbers were very slightly overlapping the top of the clef
18412
+ var vert = measureNumDim.width > 10 && abselem.abcelem.type === "treble" ? 13.5 : 11;
18205
18413
  abselem.addFixed(new RelativeElement(number, dx, measureNumDim.width, vert + measureNumDim.height / spacing.STEP, {
18206
18414
  type: "barNumber",
18207
18415
  dim: this.getTextSize.attr("measurefont", 'bar-number')
@@ -18236,6 +18444,7 @@ AbstractEngraver.prototype.createBarLine = function (voice, elem, isFirstStaff)
18236
18444
  abselem.addRight(new RelativeElement("dots.dot", dx, 1, 5));
18237
18445
  dx += 6; //2 hardcoded, twice;
18238
18446
  }
18447
+
18239
18448
  if (firstthin) {
18240
18449
  anchor = new RelativeElement(null, dx, 1, 2, {
18241
18450
  "type": "bar",
@@ -18284,6 +18493,7 @@ AbstractEngraver.prototype.createBarLine = function (voice, elem, isFirstStaff)
18284
18493
  });
18285
18494
  abselem.addRight(anchor); // 3 is hardcoded
18286
18495
  }
18496
+
18287
18497
  if (seconddots) {
18288
18498
  dx += 3; //3 hardcoded;
18289
18499
  abselem.addRight(new RelativeElement("dots.dot", dx, 1, 7));
@@ -18291,13 +18501,15 @@ AbstractEngraver.prototype.createBarLine = function (voice, elem, isFirstStaff)
18291
18501
  } // 2 is hardcoded
18292
18502
 
18293
18503
  if (elem.startEnding && isFirstStaff) {
18294
- // only put the first & second ending marks on the first staff
18295
- var textWidth = this.getTextSize.calc(elem.startEnding, "repeatfont", '').width;
18296
- abselem.minspacing += textWidth + 10; // Give plenty of room for the ending number.
18297
- this.partstartelem = new EndingElem(elem.startEnding, anchor, null);
18298
- voice.addOther(this.partstartelem);
18504
+ // MAE 17 May 2025 - Fixes drawing issue
18505
+ if (voice.voicenumber === 0) {
18506
+ // only put the first & second ending marks on the first staff
18507
+ var textWidth = this.getTextSize.calc(elem.startEnding, "repeatfont", '').width;
18508
+ abselem.minspacing += textWidth + 10; // Give plenty of room for the ending number.
18509
+ this.partstartelem = new EndingElem(elem.startEnding, anchor, null);
18510
+ voice.addOther(this.partstartelem);
18511
+ }
18299
18512
  }
18300
-
18301
18513
  // Add a little space to the left of the bar line so that nothing can crowd it.
18302
18514
  abselem.extraw -= 5;
18303
18515
  if (elem.chord !== undefined) {
@@ -18617,6 +18829,7 @@ var createClef = function createClef(elem, tuneNumber) {
18617
18829
  //abselem.top += 2;
18618
18830
  }
18619
18831
  }
18832
+
18620
18833
  return abselem;
18621
18834
  };
18622
18835
  function clefOffsets(clef) {
@@ -18808,6 +19021,7 @@ var createNoteHead = function createNoteHead(abselem, c, pitchelem, options) {
18808
19021
  }));
18809
19022
  extraLeft = glyphs.getSymbolWidth(symb) / 2; // TODO-PER: We need a little extra width if there is an accidental, but I'm not sure why it isn't the full width of the accidental.
18810
19023
  }
19024
+
18811
19025
  return {
18812
19026
  notehead: notehead,
18813
19027
  accidentalshiftx: accidentalshiftx,
@@ -19088,6 +19302,7 @@ var stackedDecoration = function stackedDecoration(decoration, width, abselem, y
19088
19302
  "mediumphrase": "scripts.mediumphrase",
19089
19303
  "longphrase": "scripts.longphrase",
19090
19304
  "trill": "scripts.trill",
19305
+ "trillh": "scripts.trill",
19091
19306
  "roll": "scripts.roll",
19092
19307
  "irishroll": "scripts.roll",
19093
19308
  "marcato": "scripts.umarcato",
@@ -19149,6 +19364,7 @@ var stackedDecoration = function stackedDecoration(decoration, width, abselem, y
19149
19364
  case "mediumphrase":
19150
19365
  case "longphrase":
19151
19366
  case "trill":
19367
+ case "trillh":
19152
19368
  case "roll":
19153
19369
  case "irishroll":
19154
19370
  case "marcato":
@@ -19462,10 +19678,18 @@ AbsoluteElement.prototype.setLimit = function (member, child) {
19462
19678
  if (!this.specialY[member]) this.specialY[member] = child[member];else this.specialY[member] = Math.max(this.specialY[member], child[member]);
19463
19679
  };
19464
19680
  AbsoluteElement.prototype._addChild = function (child) {
19465
- // console.log("Relative:",child);
19681
+ // console.log("Relative:",child);
19682
+
19683
+ // MAE 30 Sep 2024 - To avoid extra space for chords if there is only a bar number on the clef
19684
+ var okToPushTop = true;
19685
+ if (this.abcelem.el_type == "clef" && child.type == "barNumber") {
19686
+ okToPushTop = false;
19687
+ }
19466
19688
  child.parent = this;
19467
19689
  this.children[this.children.length] = child;
19468
- this.pushTop(child.top);
19690
+ if (okToPushTop) {
19691
+ this.pushTop(child.top);
19692
+ }
19469
19693
  this.pushBottom(child.bottom);
19470
19694
  this.setLimit('tempoHeightAbove', child);
19471
19695
  this.setLimit('partHeightAbove', child);
@@ -19598,6 +19822,7 @@ BeamElem.prototype.setStemDirection = function () {
19598
19822
  var middleLine = 6; // hardcoded 6 is B
19599
19823
  this.stemsUp = this.average < middleLine; // true is up, false is down;
19600
19824
  }
19825
+
19601
19826
  delete this.count;
19602
19827
  this.total = 0;
19603
19828
  };
@@ -19611,6 +19836,7 @@ BeamElem.prototype.calcDir = function () {
19611
19836
  var middleLine = 6; // hardcoded 6 is B
19612
19837
  this.stemsUp = this.average < middleLine; // true is up, false is down;
19613
19838
  }
19839
+
19614
19840
  var dir = this.stemsUp ? 'up' : 'down';
19615
19841
  for (var i = 0; i < this.elems.length; i++) {
19616
19842
  for (var j = 0; j < this.elems[i].heads.length; j++) {
@@ -19800,6 +20026,7 @@ BraceElem.prototype.continuing = function (voice) {
19800
20026
  BraceElem.prototype.getWidth = function () {
19801
20027
  return 10; // TODO-PER: right now the drawing function doesn't vary the width at all. If it does in the future then this will change.
19802
20028
  };
20029
+
19803
20030
  BraceElem.prototype.isStartVoice = function (voice) {
19804
20031
  if (this.startVoice && this.startVoice.staff && this.startVoice.staff.voices.length > 0 && this.startVoice.staff.voices[0] === voice) return true;
19805
20032
  return false;
@@ -19824,6 +20051,7 @@ var CrescendoElem = function CrescendoElem(anchor1, anchor2, dir, positioning) {
19824
20051
  if (positioning === 'above') this.dynamicHeightAbove = 6;else this.dynamicHeightBelow = 6;
19825
20052
  this.pitch = undefined; // This will be set later
19826
20053
  };
20054
+
19827
20055
  module.exports = CrescendoElem;
19828
20056
 
19829
20057
  /***/ }),
@@ -19843,6 +20071,7 @@ var DynamicDecoration = function DynamicDecoration(anchor, dec, position) {
19843
20071
  if (position === 'below') this.volumeHeightBelow = 6;else this.volumeHeightAbove = 6;
19844
20072
  this.pitch = undefined; // This will be set later
19845
20073
  };
20074
+
19846
20075
  module.exports = DynamicDecoration;
19847
20076
 
19848
20077
  /***/ }),
@@ -19863,6 +20092,7 @@ var EndingElem = function EndingElem(text, anchor1, anchor2) {
19863
20092
  this.endingHeightAbove = 5;
19864
20093
  this.pitch = undefined; // This will be set later
19865
20094
  };
20095
+
19866
20096
  module.exports = EndingElem;
19867
20097
 
19868
20098
  /***/ }),
@@ -19887,6 +20117,11 @@ function FreeText(info, vskip, getFontAndAttr, paddingLeft, width, getTextSize)
19887
20117
  move: hash.attr['font-size'] * 2
19888
20118
  }); // move the distance of the line, plus the distance of the margin, which is also one line.
19889
20119
  } else if (typeof text === 'string') {
20120
+ // MAE 9 May 2025 - Force blank text lines in a text block to have height
20121
+ var replaceStandaloneNewlinesForTextBlocks = function replaceStandaloneNewlinesForTextBlocks(input) {
20122
+ return input.replace(/^[ \t]*\n/gm, 'X\n');
20123
+ ;
20124
+ };
19890
20125
  this.rows.push({
19891
20126
  move: hash.attr['font-size'] / 2
19892
20127
  }); // TODO-PER: move down some - the y location should be the top of the text, but we output text specifying the center line.
@@ -19901,7 +20136,8 @@ function FreeText(info, vskip, getFontAndAttr, paddingLeft, width, getTextSize)
19901
20136
  absElemType: "freeText",
19902
20137
  name: "free-text"
19903
20138
  });
19904
- size = getTextSize.calc(text, 'textfont', 'defined-text');
20139
+ var textForSize = replaceStandaloneNewlinesForTextBlocks(text);
20140
+ size = getTextSize.calc(textForSize, 'textfont', 'defined-text'); // was text
19905
20141
  this.rows.push({
19906
20142
  move: size.height
19907
20143
  });
@@ -19968,6 +20204,7 @@ var GlissandoElem = function GlissandoElem(anchor1, anchor2) {
19968
20204
  this.anchor1 = anchor1; // must have a .x and a .parent property or be null (means starts at the "beginning" of the line - after keysig)
19969
20205
  this.anchor2 = anchor2; // must have a .x property or be null (means ends at the end of the line)
19970
20206
  };
20207
+
19971
20208
  module.exports = GlissandoElem;
19972
20209
 
19973
20210
  /***/ }),
@@ -20502,12 +20739,14 @@ TieElem.prototype.calcX = function (lineStartX, lineEndX) {
20502
20739
  if (this.anchor2) this.startX = this.anchor2.x - 20; // There is no element and no repeat mark: make a small arc
20503
20740
  else this.startX = lineStartX; // Don't have any guidance, so extend to beginning of line
20504
20741
  }
20742
+
20505
20743
  if (!this.anchor1 && this.dotted) this.startX -= 3; // The arc needs to be long enough to tell that it is dotted.
20506
20744
 
20507
20745
  if (this.anchor2) this.endX = this.anchor2.x; // The normal case where there is a starting element to attach to.
20508
20746
  else if (this.endLimitX) this.endX = this.endLimitX.x; // if there is no start element, but there is a repeat mark before the start of the line.
20509
20747
  else this.endX = lineEndX; // There is no element and no repeat mark: extend to the beginning of the line.
20510
20748
  };
20749
+
20511
20750
  TieElem.prototype.calcTieY = function () {
20512
20751
  // If the tie comes from another line, then one or both anchors will be missing.
20513
20752
  if (this.anchor1) this.startY = this.anchor1.pitch;else if (this.anchor2) this.startY = this.anchor2.pitch;else this.startY = this.above ? 14 : 0;
@@ -20644,6 +20883,7 @@ function TopText(metaText, metaTextInfo, formatting, lines, width, isPrint, padd
20644
20883
 
20645
20884
  // TopText.prototype.addTextIf = function (marginLeft, text, font, klass, marginTop, marginBottom, anchor, getTextSize, absElemType, noMove) {
20646
20885
  }
20886
+
20647
20887
  if (isPrint) this.rows.push({
20648
20888
  move: spacing.top
20649
20889
  });
@@ -21715,6 +21955,7 @@ function straightPath(renderer, xLeft, yTop, yBottom, type) {
21715
21955
  // right point
21716
21956
  -wCurve * 0.1, -hCurve * 0.3, -wCurve, -hCurve - spacing.STEP // left bottom
21717
21957
  );
21958
+
21718
21959
  return renderer.paper.path({
21719
21960
  path: pathString,
21720
21961
  stroke: renderer.foregroundColor,
@@ -21866,10 +22107,18 @@ function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, se
21866
22107
  renderer.paper.closeGroup();
21867
22108
  renderer.moveY(renderer.spacing.music);
21868
22109
  var staffgroups = [];
22110
+ var nStaves = 0;
21869
22111
  for (var line = 0; line < abcTune.lines.length; line++) {
21870
22112
  classes.incrLine();
21871
22113
  var abcLine = abcTune.lines[line];
21872
22114
  if (abcLine.staff) {
22115
+ // MAE 26 May 2025 - for incipits staff count limiting
22116
+ nStaves++;
22117
+ if (abcTune.formatting.maxStaves) {
22118
+ if (nStaves > abcTune.formatting.maxStaves) {
22119
+ break;
22120
+ }
22121
+ }
21873
22122
  if (classes.shouldAddClasses) groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber;
21874
22123
  renderer.paper.openGroup(groupClasses);
21875
22124
  if (abcLine.vskip) {
@@ -21976,6 +22225,8 @@ function drawEnding(renderer, params, linestartx, lineendx, selectables) {
21976
22225
  pathString += sprintf("M %f %f L %f %f ", linestartx, y, lineendx, y);
21977
22226
  renderer.paper.openGroup({
21978
22227
  klass: renderer.controller.classes.generate("ending"),
22228
+ // MAE 17 May 2025 - Ending numbers not being drawn in correct color
22229
+ fill: renderer.foregroundColor,
21979
22230
  "data-name": "ending"
21980
22231
  });
21981
22232
  printPath(renderer, {
@@ -22993,6 +23244,7 @@ function drawStaffGroup(renderer, params, selectables, lineNumber) {
22993
23244
  // renderer.moveY(spacing.STEP, -staff.bottom);
22994
23245
  }
22995
23246
  }
23247
+
22996
23248
  renderer.controller.classes.newMeasure();
22997
23249
 
22998
23250
  // connect all the staves together with a vertical line
@@ -23214,6 +23466,7 @@ function drawTempo(renderer, params) {
23214
23466
  // });
23215
23467
  //return [tempoGroup];
23216
23468
  }
23469
+
23217
23470
  module.exports = drawTempo;
23218
23471
 
23219
23472
  /***/ }),
@@ -23255,7 +23508,14 @@ function renderText(renderer, params, alreadyInGroup) {
23255
23508
  if (params.cursor) {
23256
23509
  hash.attr.cursor = params.cursor;
23257
23510
  }
23258
- var text = params.text.replace(/\n\n/g, "\n \n");
23511
+
23512
+ // MAE 9 May 2025 for free text blocks
23513
+ var text;
23514
+ if (params.name == "free-text") {
23515
+ text = params.text.replace(/^[ \t]*\n/gm, ' \n');
23516
+ } else {
23517
+ text = params.text.replace(/\n\n/g, "\n \n");
23518
+ }
23259
23519
  text = text.replace(/^\n/, "\xA0\n");
23260
23520
  if (hash.font.box) {
23261
23521
  if (!alreadyInGroup) renderer.paper.openGroup({
@@ -23550,6 +23810,7 @@ function drawVoice(renderer, params, bartop, selectables, staffPos) {
23550
23810
  renderer.controller.classes.incrMeasure();
23551
23811
  } else drawBeam(renderer, beam, selectables); // beams must be drawn first for proper printing of triplets, slurs and ties.
23552
23812
  }
23813
+
23553
23814
  renderer.controller.classes.startMeasure();
23554
23815
  for (i = 0; i < params.otherchildren.length; i++) {
23555
23816
  child = params.otherchildren[i];
@@ -23658,6 +23919,7 @@ var EngraverController = function EngraverController(paper, params) {
23658
23919
  this.staffwidthScreen = 740; // TODO-PER: Not sure where this number comes from, but this is how it's always been.
23659
23920
  this.staffwidthPrint = 680; // The number of pixels in 8.5", after 1cm of margin has been removed.
23660
23921
  }
23922
+
23661
23923
  this.listeners = [];
23662
23924
  if (params.clickListener) this.addSelectListener(params.clickListener);
23663
23925
  this.renderer = new Renderer(paper);
@@ -24476,8 +24738,13 @@ function keyboardSelection(ev) {
24476
24738
  }
24477
24739
  function findElementInHistory(selectables, el) {
24478
24740
  if (!el) return -1;
24741
+ // This should always exist, but it occasionally causes an exception, so check first.
24742
+ var dataset = el.dataset;
24743
+ if (!dataset) return -1;
24744
+ var index = dataset.index;
24479
24745
  for (var i = 0; i < selectables.length; i++) {
24480
- if (el.dataset.index === selectables[i].svgEl.dataset.index) return i;
24746
+ var svgDataset = selectables[i].svgEl.dataset;
24747
+ if (svgDataset && index === svgDataset.index) return i;
24481
24748
  }
24482
24749
  return -1;
24483
24750
  }
@@ -24567,6 +24834,7 @@ function getMousePosition(self, ev) {
24567
24834
  clickedOn = findElementByCoord(self, x, y);
24568
24835
  //console.log("clicked near", clickedOn, x, y, printEl(ev.target));
24569
24836
  }
24837
+
24570
24838
  return {
24571
24839
  x: x,
24572
24840
  y: y,
@@ -24804,6 +25072,7 @@ function minStem(element, stemsUp, referencePitch, minStemHeight) {
24804
25072
  var elem = element.children[i];
24805
25073
  if (stemsUp && elem.top !== undefined && elem.c === "flags.ugrace") minStemHeight = Math.max(minStemHeight, elem.top - referencePitch);else if (!stemsUp && elem.bottom !== undefined && elem.c === "flags.ugrace") minStemHeight = Math.max(minStemHeight, referencePitch - elem.bottom + 7); // The extra 7 is because we are measuring the slash from the top.
24806
25074
  }
25075
+
24807
25076
  return minStemHeight;
24808
25077
  }
24809
25078
  function calcSlant(leftAveragePitch, rightAveragePitch, numStems, isFlat) {
@@ -25208,6 +25477,7 @@ function calcHorizontalSpacing(isLastLine, stretchLast, targetWidth, lineWidth,
25208
25477
  if (!stretch) return null; // don't stretch last line too much
25209
25478
  }
25210
25479
  }
25480
+
25211
25481
  if (Math.abs(targetWidth - lineWidth) < 2) return null; // if we are already near the target width, we're done.
25212
25482
  var relSpace = spacingUnits * spacing;
25213
25483
  var constSpace = lineWidth - relSpace;
@@ -25272,6 +25542,7 @@ var setUpperAndLowerElements = function setUpperAndLowerElements(renderer, staff
25272
25542
  staff.originalTop = staff.top; // This is just being stored for debugging purposes.
25273
25543
  staff.originalBottom = staff.bottom; // This is just being stored for debugging purposes.
25274
25544
  }
25545
+
25275
25546
  incTop(staff, positionY, 'lyricHeightAbove');
25276
25547
  incTop(staff, positionY, 'chordHeightAbove', staff.specialY.chordLines.above);
25277
25548
  if (staff.specialY.endingHeightAbove) {
@@ -25334,6 +25605,7 @@ var setUpperAndLowerElements = function setUpperAndLowerElements(renderer, staff
25334
25605
  }
25335
25606
  //console.log("Staff Height: ",heightInPitches,this.height);
25336
25607
  };
25608
+
25337
25609
  var margin = 1;
25338
25610
  function incTop(staff, positionY, item, count) {
25339
25611
  if (staff.specialY[item]) {
@@ -25598,6 +25870,7 @@ function finished(voices) {
25598
25870
  function getDurationIndex(element) {
25599
25871
  return element.durationindex - (element.children[element.i] && element.children[element.i].duration > 0 ? 0 : 0.0000005); // if the ith element doesn't have a duration (is not a note), its duration index is fractionally before. This enables CLEF KEYSIG TIMESIG PART, etc. to be laid out before we get to the first note of other voices
25600
25872
  }
25873
+
25601
25874
  function isSameStaff(voice1, voice2) {
25602
25875
  if (!voice1 || !voice1.staff || !voice1.staff.voices || voice1.staff.voices.length === 0) return false;
25603
25876
  if (!voice2 || !voice2.staff || !voice2.staff.voices || voice2.staff.voices.length === 0) return false;
@@ -25695,6 +25968,7 @@ VoiceElement.beginLayout = function (startx, voice) {
25695
25968
  voice.nextx = startx; // x position where the next element of this voice should be placed assuming no other voices and no fixed width constraints
25696
25969
  voice.spacingduration = 0; // duration left to be laid out in current iteration (omitting additional spacing due to other aspects, such as bars, dots, sharps and flats)
25697
25970
  };
25971
+
25698
25972
  VoiceElement.layoutEnded = function (voice) {
25699
25973
  return voice.i >= voice.children.length;
25700
25974
  };
@@ -25760,6 +26034,7 @@ VoiceElement.layoutOneItem = function (x, spacing, voice, minPadding, firstVoice
25760
26034
 
25761
26035
  return x; // where we end up having placed the child
25762
26036
  };
26037
+
25763
26038
  VoiceElement.shiftRight = function (dx, voice) {
25764
26039
  var child = voice.children[voice.i];
25765
26040
  if (!child) return;
@@ -25922,6 +26197,7 @@ function setLane(absElems, numLanesAbove, numLanesBelow) {
25922
26197
  }
25923
26198
  }
25924
26199
  }
26200
+
25925
26201
  function yAtNote(element, beam) {
25926
26202
  beam = beam.beams[0];
25927
26203
  return getBarYAt(beam.startX, beam.startY, beam.endX, beam.endY, element.x);
@@ -26080,6 +26356,7 @@ Renderer.prototype.initVerticalSpace = function () {
26080
26356
  <float> range is 0.0 to 1.0.
26081
26357
  */
26082
26358
  };
26359
+
26083
26360
  Renderer.prototype.setVerticalSpace = function (formatting) {
26084
26361
  // conversion from pts to px 4/3
26085
26362
  if (formatting.staffsep !== undefined) this.spacing.staffSeparation = formatting.staffsep * 4 / 3;
@@ -26201,6 +26478,7 @@ Svg.prototype.insertStyles = function (styles) {
26201
26478
  this.svg.insertBefore(el, this.svg.firstChild); // prepend is not available on older browsers.
26202
26479
  // this.svg.prepend(el);
26203
26480
  };
26481
+
26204
26482
  Svg.prototype.setParentStyles = function (attr) {
26205
26483
  // This is needed to get the size right when there is scaling involved.
26206
26484
  for (var key in attr) {
@@ -26270,8 +26548,13 @@ Svg.prototype.text = function (text, attr, target) {
26270
26548
  el.setAttribute(key, attr[key]);
26271
26549
  }
26272
26550
  }
26551
+ var isFreeText = attr["data-name"] == "free-text";
26273
26552
  var lines = ("" + text).split("\n");
26274
26553
  for (var i = 0; i < lines.length; i++) {
26554
+ if (isFreeText && lines[i] == "") {
26555
+ // Don't draw empty lines
26556
+ continue;
26557
+ }
26275
26558
  var line = document.createElementNS(svgNS, 'tspan');
26276
26559
  line.setAttribute("x", attr.x ? attr.x : 0);
26277
26560
  if (i !== 0) line.setAttribute("dy", "1.2em");
@@ -26293,7 +26576,19 @@ Svg.prototype.text = function (text, attr, target) {
26293
26576
  ts3.textContent = parts[2];
26294
26577
  line.appendChild(ts3);
26295
26578
  }
26296
- } else line.textContent = lines[i];
26579
+ } else {
26580
+ // MAE 9 May 2025 - For improved block text
26581
+ if (isFreeText) {
26582
+ // Fixes issue where blank lines in text blocks didn't take up any vertical
26583
+ if (lines[i].trim() == "") {
26584
+ line.innerHTML = "&nbsp;";
26585
+ } else {
26586
+ line.textContent = lines[i];
26587
+ }
26588
+ } else {
26589
+ line.textContent = lines[i];
26590
+ }
26591
+ }
26297
26592
  el.appendChild(line);
26298
26593
  }
26299
26594
  if (target) target.appendChild(el);else this.append(el);
@@ -26343,6 +26638,7 @@ Svg.prototype.guessWidth = function (text, attr) {
26343
26638
  height: attr['font-size'] + 2
26344
26639
  }; // Just a wild guess.
26345
26640
  }
26641
+
26346
26642
  svg.removeChild(el);
26347
26643
  return size;
26348
26644
  };
@@ -26471,7 +26767,7 @@ module.exports = Svg;
26471
26767
  \********************/
26472
26768
  /***/ (function(module) {
26473
26769
 
26474
- var version = '6.4.3';
26770
+ var version = '6.5.0';
26475
26771
  module.exports = version;
26476
26772
 
26477
26773
  /***/ })