abcjs 6.5.1 → 6.6.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.
@@ -213,6 +213,7 @@ var TimingCallbacks = function TimingCallbacks(target, params) {
213
213
  self.lineEndCallback = params.lineEndCallback; // This is called when the end of a line is approaching.
214
214
  self.lineEndAnticipation = params.lineEndAnticipation ? parseInt(params.lineEndAnticipation, 10) : 0; // How many milliseconds before the end should the call happen.
215
215
  self.beatSubdivisions = params.beatSubdivisions ? parseInt(params.beatSubdivisions, 10) : 1; // how many callbacks per beat is desired.
216
+ if (!self.beatSubdivisions) self.beatSubdivisions = 1;
216
217
  self.joggerTimer = null;
217
218
  self.replaceTarget = function (newTarget) {
218
219
  self.noteTimings = newTarget.setTiming(self.qpm, self.extraMeasuresAtBeginning);
@@ -235,18 +236,96 @@ var TimingCallbacks = function TimingCallbacks(target, params) {
235
236
  // noteTimings contains an array of events sorted by time. Events that happen at the same time are in the same element of the array.
236
237
  self.millisecondsPerBeat = 1000 / (self.qpm / 60) / self.beatSubdivisions;
237
238
  self.lastMoment = self.noteTimings[self.noteTimings.length - 1].milliseconds;
238
- self.totalBeats = Math.round(self.lastMoment / self.millisecondsPerBeat);
239
+
240
+ // For irregular time sigs that specify the beat divisions (that is, something like `M: 2+3/8`)
241
+ // Then the beat is not a regular pulse. To keep most of this logic easy, the beat will be specified
242
+ // as half as long, but the callback will happen according to the pattern. That is,
243
+ // for bpm=60 at the above time signature, internally the beat callback will happen at 120 bpm, but
244
+ // the callback function will be called at 2 sub beats, then 3 sub beats, etc.
245
+ // The beat number will be an integer for all of them and count up by one each time.
246
+ var meter = newTarget.getMeter();
247
+ var irregularMeter = '';
248
+ if (meter && meter.type === "specified" && meter.value && meter.value.length > 0 && meter.value[0].num.indexOf('+') > 0) irregularMeter = meter.value[0].num;
249
+ // for subdivisions = 1, then this should contain only whole numbers and the callbacks are irregular
250
+ // for subdivisions = 2, then this should be 1/8 notes. The beats should be something like: 0, 0.5, 1, 1.33, 1.66 2 (For M:2+3/8)
251
+ // etc for more subdivisions - they are just multiplied
252
+ self.beatStarts = [];
253
+ if (irregularMeter) {
254
+ var measureLength = self.noteTimings[self.noteTimings.length - 1].millisecondsPerMeasure;
255
+ var numMeasures = self.lastMoment / measureLength;
256
+ var parts = irregularMeter.split("+");
257
+ for (var i = 0; i < parts.length; i++) {
258
+ parts[i] = parseInt(parts[i], 10) / 2;
259
+ } // since we count a beat as a quarter note, but these numbers refer to 1/8 notes, we convert the beat length
260
+ var currentTs = 0;
261
+ var beatNumber = 0;
262
+ // For the input: parts = [ 1, 1.5 ] and beatSubdivisions = 2
263
+ // beatNumbers = 0 0.5 1 1.33 1.67 2 2.5 3 3.33 3.67 ...
264
+ // when part=1 then there is 1 extra sub beat and it is 0.5
265
+ // when part=1.5 then there are 2 extra sub beats at 0.33 and 0.67
266
+ //
267
+ // beatSubdivision | numFor1 | numFor1.5
268
+ // 2 | 2 | 3
269
+ // 3 | 3 | 4.5
270
+ // 4 | 4 | 6
271
+ for (var measureNumber = 0; measureNumber < numMeasures; measureNumber++) {
272
+ var measureStartTs = measureNumber * measureLength;
273
+ var subBeatCounter = 0;
274
+ for (var kk = 0; kk < parts.length; kk++) {
275
+ var beatLength = parts[kk]; // This is either 1 or 1.5 (how many quarter notes in this beat)
276
+ if (self.beatSubdivisions === 1) {
277
+ if (self.beatSubdivisions === 1) if (currentTs < self.lastMoment) {
278
+ self.beatStarts.push({
279
+ b: beatNumber,
280
+ ts: currentTs
281
+ });
282
+ }
283
+ currentTs += beatLength * self.millisecondsPerBeat;
284
+ } else {
285
+ var numDivisions = beatLength * self.beatSubdivisions;
286
+ for (var k = 0; k < Math.floor(numDivisions); k++) {
287
+ var subBeat = k / numDivisions;
288
+ var ts = Math.round(measureStartTs + subBeatCounter * self.millisecondsPerBeat);
289
+ if (ts < self.lastMoment) {
290
+ self.beatStarts.push({
291
+ b: beatNumber + subBeat,
292
+ ts: ts
293
+ });
294
+ }
295
+ subBeatCounter++;
296
+ }
297
+ }
298
+ beatNumber++;
299
+ }
300
+ }
301
+ self.beatStarts.push({
302
+ b: numMeasures * parts.length,
303
+ ts: self.lastMoment
304
+ });
305
+ self.totalBeats = self.beatStarts.length;
306
+ } else {
307
+ self.totalBeats = Math.round(self.lastMoment / self.millisecondsPerBeat);
308
+ // Add one so the last beat is the last moment
309
+ for (var j = 0; j < self.totalBeats + 1; j++) {
310
+ self.beatStarts.push({
311
+ b: j / self.beatSubdivisions,
312
+ ts: Math.round(j * self.millisecondsPerBeat)
313
+ });
314
+ }
315
+ }
316
+ //console.log({lastMoment: self.lastMoment, beatStarts: self.beatStarts})
239
317
  };
318
+
240
319
  self.replaceTarget(target);
241
320
  self.doTiming = function (timestamp) {
242
321
  // This is called 60 times a second, that is, every 16 msecs.
243
322
  //console.log("doTiming", timestamp, timestamp-self.lastTimestamp);
244
323
  if (self.lastTimestamp === timestamp) return; // If there are multiple seeks or other calls, then we can easily get multiple callbacks for the same instant.
245
324
  self.lastTimestamp = timestamp;
246
- if (!self.startTime) {
247
- self.startTime = timestamp;
248
- }
249
325
  if (!self.isPaused && self.isRunning) {
326
+ if (!self.startTime) {
327
+ self.startTime = timestamp;
328
+ }
250
329
  self.currentTime = timestamp - self.startTime;
251
330
  self.currentTime += 16; // Add a little slop because this function isn't called exactly.
252
331
  while (self.noteTimings.length > self.currentEvent && self.noteTimings[self.currentEvent].milliseconds < self.currentTime) {
@@ -270,14 +349,16 @@ var TimingCallbacks = function TimingCallbacks(target, params) {
270
349
  }
271
350
  if (self.currentTime < self.lastMoment) {
272
351
  requestAnimationFrame(self.doTiming);
273
- if (self.currentBeat * self.millisecondsPerBeat < self.currentTime) {
352
+ if (self.currentBeat < self.beatStarts.length && self.beatStarts[self.currentBeat].ts <= self.currentTime) {
274
353
  var ret = self.doBeatCallback(timestamp);
354
+ self.currentBeat++;
275
355
  if (ret !== null) self.currentTime = ret;
276
356
  }
277
357
  } else if (self.currentBeat <= self.totalBeats) {
278
358
  // Because of timing issues (for instance, if the browser tab isn't active), the beat callbacks might not have happened when they are supposed to. To keep the client programs from having to deal with that, this will keep calling the loop until all of them have been sent.
279
359
  if (self.beatCallback) {
280
360
  var ret2 = self.doBeatCallback(timestamp);
361
+ self.currentBeat++;
281
362
  if (ret2 !== null) self.currentTime = ret2;
282
363
  requestAnimationFrame(self.doTiming);
283
364
  }
@@ -362,11 +443,13 @@ var TimingCallbacks = function TimingCallbacks(target, params) {
362
443
  };
363
444
  }
364
445
  var thisStartTime = self.startTime; // the beat callback can call seek and change the position from beneath us.
365
- self.beatCallback(self.currentBeat / self.beatSubdivisions, self.totalBeats / self.beatSubdivisions, self.lastMoment, position, debugInfo);
446
+ self.beatCallback(self.beatStarts[self.currentBeat].b, self.totalBeats / self.beatSubdivisions, self.lastMoment, position, debugInfo);
366
447
  if (thisStartTime !== self.startTime) {
367
448
  return timestamp - self.startTime;
368
- } else self.currentBeat++;
449
+ } // else
450
+ // self.currentBeat++;
369
451
  }
452
+
370
453
  return null;
371
454
  };
372
455
 
@@ -457,7 +540,6 @@ var TimingCallbacks = function TimingCallbacks(target, params) {
457
540
  if (!self.isRunning) self.pausedPercent = percent;
458
541
  var now = performance.now();
459
542
  self.startTime = now - self.currentTime;
460
- var oldEvent = self.currentEvent;
461
543
  self.currentEvent = 0;
462
544
  while (self.noteTimings.length > self.currentEvent && self.noteTimings[self.currentEvent].milliseconds < self.currentTime) {
463
545
  self.currentEvent++;
@@ -468,11 +550,18 @@ var TimingCallbacks = function TimingCallbacks(target, params) {
468
550
  self.currentLine++;
469
551
  }
470
552
  }
553
+
554
+ //console.log({jump:self.currentTime})
471
555
  var oldBeat = self.currentBeat;
472
- self.currentBeat = Math.floor(self.currentTime / self.millisecondsPerBeat);
473
- if (self.beatCallback && oldBeat !== self.currentBeat)
556
+ for (self.currentBeat = 0; self.currentBeat < self.beatStarts.length; self.currentBeat++) {
557
+ if (self.beatStarts[self.currentBeat].ts > self.currentTime) break;
558
+ }
559
+ self.currentBeat--;
560
+ if (self.beatCallback && oldBeat !== self.currentBeat) {
474
561
  // If the movement caused the beat to change, then immediately report it to the client.
475
562
  self.doBeatCallback(self.startTime + self.currentTime);
563
+ self.currentBeat++;
564
+ }
476
565
  if (self.eventCallback && self.currentEvent >= 0 && self.noteTimings[self.currentEvent].type === 'event') self.eventCallback(self.noteTimings[self.currentEvent]);
477
566
  if (self.lineEndCallback) self.lineEndCallback(self.lineEndTimings[self.currentLine], self.noteTimings[self.currentEvent], {
478
567
  line: self.currentLine,
@@ -1024,67 +1113,71 @@ module.exports = keyAccidentals;
1024
1113
  // All these keys have the same number of accidentals
1025
1114
  var keys = {
1026
1115
  'C': {
1027
- modes: ['CMaj', 'Amin', 'Am', 'GMix', 'DDor', 'EPhr', 'FLyd', 'BLoc'],
1116
+ modes: ['CMaj', 'CIon', 'Amin', 'AAeo', 'Am', 'GMix', 'DDor', 'EPhr', 'FLyd', 'BLoc'],
1028
1117
  stepsFromC: 0
1029
1118
  },
1030
1119
  'Db': {
1031
- modes: ['DbMaj', 'Bbmin', 'Bbm', 'AbMix', 'EbDor', 'FPhr', 'GbLyd', 'CLoc'],
1120
+ modes: ['DbMaj', 'DbIon', 'Bbmin', 'BbAeo', 'Bbm', 'AbMix', 'EbDor', 'FPhr', 'GbLyd', 'CLoc'],
1032
1121
  stepsFromC: 1
1033
1122
  },
1034
1123
  'D': {
1035
- modes: ['DMaj', 'Bmin', 'Bm', 'AMix', 'EDor', 'F#Phr', 'GLyd', 'C#Loc'],
1124
+ modes: ['DMaj', 'DIon', 'Bmin', 'BAeo', 'Bm', 'AMix', 'EDor', 'F#Phr', 'GLyd', 'C#Loc'],
1036
1125
  stepsFromC: 2
1037
1126
  },
1038
1127
  'Eb': {
1039
- modes: ['EbMaj', 'Cmin', 'Cm', 'BbMix', 'FDor', 'GPhr', 'AbLyd', 'DLoc'],
1128
+ modes: ['EbMaj', 'EbIon', 'Cmin', 'CAeo', 'Cm', 'BbMix', 'FDor', 'GPhr', 'AbLyd', 'DLoc'],
1040
1129
  stepsFromC: 3
1041
1130
  },
1042
1131
  'E': {
1043
- modes: ['EMaj', 'C#min', 'C#m', 'BMix', 'F#Dor', 'G#Phr', 'ALyd', 'D#Loc'],
1132
+ modes: ['EMaj', 'EIon', 'C#min', 'C#Aeo', 'C#m', 'BMix', 'F#Dor', 'G#Phr', 'ALyd', 'D#Loc'],
1044
1133
  stepsFromC: 4
1045
1134
  },
1046
1135
  'F': {
1047
- modes: ['FMaj', 'Dmin', 'Dm', 'CMix', 'GDor', 'APhr', 'BbLyd', 'ELoc'],
1136
+ modes: ['FMaj', 'FIon', 'Dmin', 'DAeo', 'Dm', 'CMix', 'GDor', 'APhr', 'BbLyd', 'ELoc'],
1048
1137
  stepsFromC: 5
1049
1138
  },
1050
1139
  'Gb': {
1051
- modes: ['GbMaj', 'Ebmin', 'Ebm', 'DbMix', 'AbDor', 'BbPhr', 'CbLyd', 'FLoc'],
1140
+ modes: ['GbMaj', 'GbIon', 'Ebmin', 'EbAeo', 'Ebm', 'DbMix', 'AbDor', 'BbPhr', 'CbLyd', 'FLoc'],
1052
1141
  stepsFromC: 6
1053
1142
  },
1054
1143
  'G': {
1055
- modes: ['GMaj', 'Emin', 'Em', 'DMix', 'ADor', 'BPhr', 'CLyd', 'F#Loc'],
1144
+ modes: ['GMaj', 'GIon', 'Emin', 'EAeo', 'Em', 'DMix', 'ADor', 'BPhr', 'CLyd', 'F#Loc'],
1056
1145
  stepsFromC: 7
1057
1146
  },
1058
1147
  'Ab': {
1059
- modes: ['AbMaj', 'Fmin', 'Fm', 'EbMix', 'BbDor', 'CPhr', 'DbLyd', 'GLoc'],
1148
+ modes: ['AbMaj', 'AbIon', 'Fmin', 'FAeo', 'Fm', 'EbMix', 'BbDor', 'CPhr', 'DbLyd', 'GLoc'],
1060
1149
  stepsFromC: 8
1061
1150
  },
1062
1151
  'A': {
1063
- modes: ['AMaj', 'F#min', 'F#m', 'EMix', 'BDor', 'C#Phr', 'DLyd', 'G#Loc'],
1152
+ modes: ['AMaj', 'AIon', 'F#min', 'F#Aeo', 'F#m', 'EMix', 'BDor', 'C#Phr', 'DLyd', 'G#Loc'],
1064
1153
  stepsFromC: 9
1065
1154
  },
1066
1155
  'Bb': {
1067
- modes: ['BbMaj', 'Gmin', 'Gm', 'FMix', 'CDor', 'DPhr', 'EbLyd', 'ALoc'],
1156
+ modes: ['BbMaj', 'BbIon', 'Gmin', 'GAeo', 'Gm', 'FMix', 'CDor', 'DPhr', 'EbLyd', 'ALoc'],
1068
1157
  stepsFromC: 10
1069
1158
  },
1070
1159
  'B': {
1071
- modes: ['BMaj', 'G#min', 'G#m', 'F#Mix', 'C#Dor', 'D#Phr', 'ELyd', 'A#Loc'],
1160
+ modes: ['BMaj', 'BIon', 'G#min', 'G#Aeo', 'G#m', 'F#Mix', 'C#Dor', 'D#Phr', 'ELyd', 'A#Loc'],
1072
1161
  stepsFromC: 11
1073
1162
  },
1074
1163
  // Enharmonic keys
1075
1164
  'C#': {
1076
- modes: ['C#Maj', 'A#min', 'A#m', 'G#Mix', 'D#Dor', 'E#Phr', 'F#Lyd', 'B#Loc'],
1165
+ modes: ['C#Maj', 'C#Ion', 'A#min', 'A#Aeo', 'A#m', 'G#Mix', 'D#Dor', 'E#Phr', 'F#Lyd', 'B#Loc'],
1077
1166
  stepsFromC: 1
1078
1167
  },
1079
1168
  'F#': {
1080
- modes: ['F#Maj', 'D#min', 'D#m', 'C#Mix', 'G#Dor', 'A#Phr', 'BLyd', 'E#Loc'],
1169
+ modes: ['F#Maj', 'F#Ion', 'D#min', 'D#Aeo', 'D#m', 'C#Mix', 'G#Dor', 'A#Phr', 'BLyd', 'E#Loc'],
1081
1170
  stepsFromC: 6
1082
1171
  },
1083
1172
  'Cb': {
1084
- modes: ['CbMaj', 'Abmin', 'Abm', 'GbMix', 'DbDor', 'EbPhr', 'FbLyd', 'BbLoc'],
1173
+ modes: ['CbMaj', 'CbIon', 'Abmin', 'AbAeo', 'Abm', 'GbMix', 'DbDor', 'EbPhr', 'FbLyd', 'BbLoc'],
1085
1174
  stepsFromC: 11
1086
1175
  }
1087
1176
  };
1177
+ var modeNames = ['maj', 'ion', 'min', 'aeo', 'm', 'mix', 'dor', 'phr', 'lyd', 'loc'];
1178
+ function isLegalMode(mode) {
1179
+ return modeNames.indexOf(mode.toLowerCase()) >= 0;
1180
+ }
1088
1181
  var keyReverse = null;
1089
1182
  function createKeyReverse() {
1090
1183
  keyReverse = {};
@@ -1107,7 +1200,7 @@ function relativeMajor(key) {
1107
1200
  createKeyReverse();
1108
1201
  }
1109
1202
  // get the key portion itself - there might be other stuff, like extra sharps and flats, or the mode written out.
1110
- var mode = key.toLowerCase().match(/([a-g][b#]?)(maj|min|mix|dor|phr|lyd|loc|m)?/);
1203
+ var mode = key.toLowerCase().match(/([a-g][b#]?)(maj|ion|min|aeo|mix|dor|phr|lyd|loc|m)?/);
1111
1204
  if (!mode || !mode[2]) return key;
1112
1205
  mode = mode[1] + mode[2];
1113
1206
  var maj = keyReverse[mode];
@@ -1120,7 +1213,7 @@ function relativeMode(majorKey, mode) {
1120
1213
  var group = keys[majorKey];
1121
1214
  if (!group) return majorKey;
1122
1215
  if (mode === '') return majorKey;
1123
- var match = mode.toLowerCase().match(/^(maj|min|mix|dor|phr|lyd|loc|m)/);
1216
+ var match = mode.toLowerCase().match(/^(maj|ion|min|aeo|mix|dor|phr|lyd|loc|m)/);
1124
1217
  if (!match) return majorKey;
1125
1218
  var regMode = match[1];
1126
1219
  for (var i = 0; i < group.modes.length; i++) {
@@ -1148,7 +1241,8 @@ function transposeKey(key, steps) {
1148
1241
  module.exports = {
1149
1242
  relativeMajor: relativeMajor,
1150
1243
  relativeMode: relativeMode,
1151
- transposeKey: transposeKey
1244
+ transposeKey: transposeKey,
1245
+ isLegalMode: isLegalMode
1152
1246
  };
1153
1247
 
1154
1248
  /***/ }),
@@ -1239,9 +1333,12 @@ var Tune = function Tune() {
1239
1333
  this.getBeatLength = function () {
1240
1334
  // This returns a fraction: for instance 1/4 for a quarter
1241
1335
  // There are two types of meters: compound and regular. Compound meter has 3 beats counted as one.
1336
+
1337
+ // Irregular meters have the beat as an 1/8 note but the tempo as a 1/4.
1338
+ // That keeps it a similar tempo to 4/4 but that may or may not be generally intuitive, so that might need to change.
1242
1339
  var meter = this.getMeterFraction();
1243
1340
  var multiplier = 1;
1244
- if (meter.num === 6 || meter.num === 9 || meter.num === 12) multiplier = 3;else if (meter.num === 3 && meter.den === 8) multiplier = 3;
1341
+ if (meter.num === 6 || meter.num === 9 || meter.num === 12) multiplier = 3;else if (meter.num === 3 && meter.den === 8) multiplier = 3;else if (meter.den === 8 && (meter.num === 5 || meter.num === 7)) multiplier = 2;
1245
1342
  return multiplier / meter.den;
1246
1343
  };
1247
1344
  function computePickupLength(lines, barLength) {
@@ -1325,7 +1422,13 @@ var Tune = function Tune() {
1325
1422
  var den = 4;
1326
1423
  if (meter) {
1327
1424
  if (meter.type === 'specified') {
1328
- num = parseInt(meter.value[0].num, 10);
1425
+ if (meter.value && meter.value.length > 0 && meter.value[0].num.indexOf('+') > 0) {
1426
+ var parts = meter.value[0].num.split('+');
1427
+ num = 0;
1428
+ for (var i = 0; i < parts.length; i++) {
1429
+ num += parseInt(parts[i], 10);
1430
+ }
1431
+ } else num = parseInt(meter.value[0].num, 10);
1329
1432
  den = parseInt(meter.value[0].den, 10);
1330
1433
  } else if (meter.type === 'cut_time') {
1331
1434
  num = 2;
@@ -1757,7 +1860,7 @@ function delineTune(inputLines, options) {
1757
1860
  var currentTripletFont = [];
1758
1861
  var currentAnnotationFont = [];
1759
1862
  for (var i = 0; i < inputLines.length; i++) {
1760
- var inputLine = inputLines[i];
1863
+ var inputLine = cloneLine(inputLines[i]);
1761
1864
  if (inputLine.staff) {
1762
1865
  if (inMusicLine && !inputLine.vskip) {
1763
1866
  var outputLine = outputLines[outputLines.length - 1];
@@ -1988,7 +2091,11 @@ try {
1988
2091
  // if we aren't in a browser, this code will crash, but it is not needed then either.
1989
2092
  }
1990
2093
  var EditArea = function EditArea(textareaid) {
1991
- if (typeof textareaid === "string") this.textarea = document.getElementById(textareaid);else this.textarea = textareaid;
2094
+ this.isEditArea = true;
2095
+ if (typeof textareaid === "string") {
2096
+ this.textarea = document.getElementById(textareaid);
2097
+ if (!this.textarea) this.textarea = document.querySelector(textareaid);
2098
+ } else this.textarea = textareaid;
1992
2099
  this.initialText = this.textarea.value;
1993
2100
  this.isDragging = false;
1994
2101
  };
@@ -2156,10 +2263,17 @@ var Editor = function Editor(editarea, params) {
2156
2263
  // Copy all the options that will be passed through
2157
2264
  this.abcjsParams = gatherAbcParams(params);
2158
2265
  if (params.indicate_changed) this.indicate_changed = true;
2266
+
2267
+ // If a string is passed in then it could either be an element's ID or a selector
2268
+ // If an object is passed in then it could either be an EditArea or a textarea.
2159
2269
  if (typeof editarea === "string") {
2270
+ // EditArea handles both the ID and the selector
2160
2271
  this.editarea = new EditArea(editarea);
2161
2272
  } else {
2162
- this.editarea = editarea;
2273
+ // If an edit area was passed in, just use it
2274
+ if (editarea.isEditArea) this.editarea = editarea;else
2275
+ // Hopefully we were passed in a textarea or equivalent.
2276
+ this.editarea = new EditArea(editarea);
2163
2277
  }
2164
2278
  this.editarea.addSelectionListener(this);
2165
2279
  this.editarea.addChangeListener(this);
@@ -2209,6 +2323,7 @@ var Editor = function Editor(editarea, params) {
2209
2323
  this.div.parentNode.insertBefore(this.warningsdiv, this.div);
2210
2324
  }
2211
2325
  this.onchangeCallback = params.onchange;
2326
+ this.redrawCallback = params.redrawCallback;
2212
2327
  this.currentAbc = "";
2213
2328
  this.tunes = [];
2214
2329
  this.bReentry = false;
@@ -2265,12 +2380,14 @@ Editor.prototype.modelChanged = function () {
2265
2380
  this.bReentry = true;
2266
2381
  try {
2267
2382
  this.timerId = null;
2383
+ if (this.redrawCallback) this.redrawCallback(true);
2268
2384
  if (this.synth && this.synth.synthControl) this.synth.synthControl.disable(true);
2269
2385
  this.tunes = renderAbc(this.div, this.currentAbc, this.abcjsParams);
2270
2386
  if (this.tunes.length > 0) {
2271
2387
  this.warnings = this.tunes[0].warnings;
2272
2388
  }
2273
2389
  this.redrawMidi();
2390
+ if (this.redrawCallback) this.redrawCallback(false);
2274
2391
  } catch (error) {
2275
2392
  console.error("ABCJS error: ", error);
2276
2393
  if (!this.warnings) this.warnings = [];
@@ -2295,6 +2412,9 @@ Editor.prototype.paramChanged = function (engraverParams) {
2295
2412
  this.currentAbc = "";
2296
2413
  this.fireChanged();
2297
2414
  };
2415
+ Editor.prototype.getTunes = function () {
2416
+ return this.tunes;
2417
+ };
2298
2418
  Editor.prototype.synthParamChanged = function (options) {
2299
2419
  if (!this.synth) return;
2300
2420
  this.synth.options = {};
@@ -2434,8 +2554,8 @@ var create;
2434
2554
  var tempo = commands.tempo;
2435
2555
  var beatsPerSecond = tempo / 60;
2436
2556
 
2437
- // Fix tempo for */8 meters
2438
- if (time.den == 8) {
2557
+ // Fix tempo for compound meters
2558
+ if (time.den === 8 && time.num !== 5 && time.num !== 7) {
2439
2559
  // Compute the tempo based on the actual milliseconds per measure, scaled by the number of eight notes and halved to get tempo in bpm.
2440
2560
  var msPerMeasure = abcTune.millisecondsPerMeasure();
2441
2561
  tempo = 60000 / (msPerMeasure / time.num) / 2;
@@ -2585,6 +2705,7 @@ var ParseHeader = __webpack_require__(/*! ./abc_parse_header */ "./src/parse/abc
2585
2705
  var ParseMusic = __webpack_require__(/*! ./abc_parse_music */ "./src/parse/abc_parse_music.js");
2586
2706
  var Tokenizer = __webpack_require__(/*! ./abc_tokenizer */ "./src/parse/abc_tokenizer.js");
2587
2707
  var wrap = __webpack_require__(/*! ./wrap_lines */ "./src/parse/wrap_lines.js");
2708
+ var chordGrid = __webpack_require__(/*! ./chord-grid */ "./src/parse/chord-grid.js");
2588
2709
  var Tune = __webpack_require__(/*! ../data/abc_tune */ "./src/data/abc_tune.js");
2589
2710
  var TuneBuilder = __webpack_require__(/*! ../parse/tune-builder */ "./src/parse/tune-builder.js");
2590
2711
  var Parse = function Parse() {
@@ -2627,6 +2748,7 @@ var Parse = function Parse() {
2627
2748
  };
2628
2749
  if (tune.lineBreaks) t.lineBreaks = tune.lineBreaks;
2629
2750
  if (tune.visualTranspose) t.visualTranspose = tune.visualTranspose;
2751
+ if (tune.chordGrid) t.chordGrid = tune.chordGrid;
2630
2752
  return t;
2631
2753
  };
2632
2754
  function addPositioning(el, type, value) {
@@ -3176,6 +3298,22 @@ var Parse = function Parse() {
3176
3298
  addHintMeasures();
3177
3299
  }
3178
3300
  wrap.wrapLines(tune, multilineVars.lineBreaks, multilineVars.barNumbers);
3301
+ if (switches.chordGrid) {
3302
+ try {
3303
+ tune.chordGrid = chordGrid(tune);
3304
+ } catch (err) {
3305
+ switch (err.message) {
3306
+ case "notCommonTime":
3307
+ warn("Chord grid only works for 2/2 and 4/4 time.", 0, 0);
3308
+ break;
3309
+ case "noChords":
3310
+ warn("No chords are found in the tune.", 0, 0);
3311
+ break;
3312
+ default:
3313
+ warn(err.message, 0, 0);
3314
+ }
3315
+ }
3316
+ }
3179
3317
  };
3180
3318
  };
3181
3319
  module.exports = Parse;
@@ -9202,6 +9340,344 @@ module.exports = allNotes;
9202
9340
 
9203
9341
  /***/ }),
9204
9342
 
9343
+ /***/ "./src/parse/chord-grid.js":
9344
+ /*!*********************************!*\
9345
+ !*** ./src/parse/chord-grid.js ***!
9346
+ \*********************************/
9347
+ /***/ (function(module) {
9348
+
9349
+ // This takes a visual object and returns an object that can
9350
+ // be rotely turned into a chord grid.
9351
+ //
9352
+ // 1) It will always be 8 measures on a line, unless it is a 12 bar blues, then it will be 4 measures.
9353
+ // 2) If it is not in 4/4 it will return an error
9354
+ // 3) If there are no chords it will return an error
9355
+ // 4) It will be divided into parts with the part title and an array of measures
9356
+ // 5) |: and :| will be included in a measure
9357
+ // 6) If there are first and second endings and the chords are the same, then collapse them
9358
+ // 7) If there are first and second endings and the chords are different, use a separate line for the second ending and right justify it.
9359
+ // 8) If there is one chord per measure and it is repeated in the next measure use a % for the second measure.
9360
+ // 9) All lines are the same height, so they are tall enough to fit two lines if there lots of chords
9361
+ // 10) Chords will be printed as large as they can without overlapping, so different chords will be smaller if they are long.
9362
+ // 11) If there are two chords per measure then there is a slash between them.
9363
+ // 12) If there are three or four chords then there is a 2x2 grid with the chords reading right to left. For three chords, leave the repeated cell blank.
9364
+ // 13) Breaks are indicated by the word "break" or "N.C.". A break that extends to the next measure is indicated by three dots in the next measure.
9365
+ // 14) Ignore pickup notes
9366
+ // 15) if a part is not a multiple of 8 bars (and not 12 bars), the last line has
9367
+ // 4 squares on left and not any grid on the right.
9368
+ // 16) Annotations and some decorations get printed above the cells.
9369
+
9370
+ function chordGrid(visualObj) {
9371
+ var meter = visualObj.getMeterFraction();
9372
+ var isCommonTime = meter.num === 4 && meter.den === 4;
9373
+ var isCutTime = meter.num === 2 && meter.den === 2;
9374
+ if (!isCutTime && !isCommonTime) throw new Error("notCommonTime");
9375
+ var deline = visualObj.deline();
9376
+ var chartLines = [];
9377
+ var nonSubtitle = false;
9378
+ deline.forEach(function (section) {
9379
+ if (section.subtitle) {
9380
+ if (nonSubtitle) {
9381
+ // Don't do the subtitle if the first thing is the subtitle, but that is already printed on the top
9382
+ chartLines.push({
9383
+ type: "subtitle",
9384
+ subtitle: section.subtitle.text
9385
+ });
9386
+ }
9387
+ } else if (section.text) {
9388
+ nonSubtitle = true;
9389
+ chartLines.push({
9390
+ type: "text",
9391
+ text: section.text.text
9392
+ });
9393
+ } else if (section.staff) {
9394
+ nonSubtitle = true;
9395
+ // The first staff and the first voice in it drive everything.
9396
+ // Only part designations there will count. However, look for
9397
+ // chords in any other part. If there is not a chord defined in
9398
+ // the first part, use a chord defined in another part.
9399
+ var staves = section.staff;
9400
+ var parts = flattenVoices(staves);
9401
+ chartLines = chartLines.concat(parts);
9402
+ }
9403
+ });
9404
+ collapseIdenticalEndings(chartLines);
9405
+ addLineBreaks(chartLines);
9406
+ addPercents(chartLines);
9407
+ return chartLines;
9408
+ }
9409
+ var breakSynonyms = ['break', '(break)', 'no chord', 'n.c.', 'tacet'];
9410
+ function flattenVoices(staves) {
9411
+ var parts = [];
9412
+ var partName = "";
9413
+ var measures = [];
9414
+ var currentBar = {
9415
+ chord: ['', '', '', '']
9416
+ };
9417
+ var lastChord = "";
9418
+ var nextBarEnding = "";
9419
+ staves.forEach(function (staff, staffNum) {
9420
+ if (staff.voices) {
9421
+ staff.voices.forEach(function (voice, voiceNum) {
9422
+ var currentPartNum = 0;
9423
+ var beatNum = 0;
9424
+ var measureNum = 0;
9425
+ voice.forEach(function (element) {
9426
+ if (element.el_type === 'part') {
9427
+ if (measures.length > 0) {
9428
+ if (staffNum === 0 && voiceNum === 0) {
9429
+ parts.push({
9430
+ type: "part",
9431
+ name: partName,
9432
+ lines: [measures]
9433
+ });
9434
+ measures = [];
9435
+ // } else {
9436
+ // currentPartNum++
9437
+ // measureNum = 0
9438
+ // measures = parts[currentPartNum].lines[0]
9439
+ }
9440
+ }
9441
+
9442
+ partName = element.title;
9443
+ } else if (element.el_type === 'note') {
9444
+ addDecoration(element, currentBar);
9445
+ var intBeat = Math.floor(beatNum);
9446
+ if (element.chord && element.chord.length > 0) {
9447
+ var chord = element.chord[0]; // Use just the first chord specified - if there are multiple ones, then ignore them
9448
+ var chordName = chord.position === 'default' || breakSynonyms.indexOf(chord.name.toLowerCase()) >= 0 ? chord.name : '';
9449
+ if (chordName) {
9450
+ if (intBeat > 0 && !currentBar.chord[0])
9451
+ // Be sure there is a chord for the first beat in a measure
9452
+ currentBar.chord[0] = lastChord;
9453
+ lastChord = chordName;
9454
+ if (currentBar.chord[intBeat]) {
9455
+ // If there is already a chord on this beat put the next chord on the next beat, but don't overwrite anything.
9456
+ // This handles the case were a chord is misplaced slightly, for instance it is on the 1/8 before the beat.
9457
+ if (intBeat < 4 && !currentBar.chord[intBeat + 1]) currentBar.chord[intBeat + 1] = chordName;
9458
+ } else currentBar.chord[intBeat] = chordName;
9459
+ }
9460
+ element.chord.forEach(function (ch) {
9461
+ if (ch.position !== 'default' && breakSynonyms.indexOf(chord.name.toLowerCase()) < 0) {
9462
+ if (!currentBar.annotations) currentBar.annotations = [];
9463
+ currentBar.annotations.push(ch.name);
9464
+ }
9465
+ });
9466
+ }
9467
+ if (!element.rest || element.rest.type !== 'spacer') {
9468
+ var thisDuration = Math.floor(element.duration * 4);
9469
+ if (thisDuration > 4) {
9470
+ measureNum += Math.floor(thisDuration / 4);
9471
+ beatNum = 0;
9472
+ } else {
9473
+ var thisBeat = element.duration * 4;
9474
+ if (element.tripletMultiplier) thisBeat *= element.tripletMultiplier;
9475
+ beatNum += thisBeat;
9476
+ }
9477
+ }
9478
+ } else if (element.el_type === 'bar') {
9479
+ if (nextBarEnding) {
9480
+ currentBar.ending = nextBarEnding;
9481
+ nextBarEnding = "";
9482
+ }
9483
+ addDecoration(element, currentBar);
9484
+ if (element.type === 'bar_dbl_repeat' || element.type === 'bar_left_repeat') currentBar.hasStartRepeat = true;
9485
+ if (element.type === 'bar_dbl_repeat' || element.type === 'bar_right_repeat') currentBar.hasEndRepeat = true;
9486
+ if (element.startEnding) nextBarEnding = element.startEnding;
9487
+ if (beatNum >= 4) {
9488
+ if (currentBar.chord[0] === '') {
9489
+ // If there isn't a chord change at the beginning, repeat the last chord found
9490
+ if (currentBar.chord[1] || currentBar.chord[2] || currentBar.chord[3]) {
9491
+ currentBar.chord[0] = findLastChord(measures);
9492
+ }
9493
+ }
9494
+ if (staffNum === 0 && voiceNum === 0) measures.push(currentBar);else {
9495
+ // Add the found items of interest to the original array
9496
+ // We have the extra [0] in there because lines is an array of lines (but we just use the [0] for constructing, we split it apart at the end)
9497
+ var index = measureNum;
9498
+ var partIndex = 0;
9499
+ while (index >= parts[partIndex].lines[0].length && partIndex < parts.length) {
9500
+ index -= parts[partIndex].lines[0].length;
9501
+ partIndex++;
9502
+ }
9503
+ if (partIndex < parts.length && index < parts[partIndex].lines[0].length) {
9504
+ var bar = parts[partIndex].lines[0][index];
9505
+ if (!bar.chord[0] && currentBar.chord[0]) bar.chord[0] = currentBar.chord[0];
9506
+ if (!bar.chord[1] && currentBar.chord[1]) bar.chord[1] = currentBar.chord[1];
9507
+ if (!bar.chord[2] && currentBar.chord[2]) bar.chord[2] = currentBar.chord[2];
9508
+ if (!bar.chord[3] && currentBar.chord[3]) bar.chord[3] = currentBar.chord[3];
9509
+ if (currentBar.annotations) {
9510
+ if (!bar.annotations) bar.annotations = currentBar.annotations;else bar.annotations = bar.annotations.concat(currentBar.annotations);
9511
+ }
9512
+ }
9513
+ measureNum++;
9514
+ }
9515
+ currentBar = {
9516
+ chord: ['', '', '', '']
9517
+ };
9518
+ } else currentBar.chord = ['', '', '', ''];
9519
+ beatNum = 0;
9520
+ } else if (element.el_type === 'tempo') {
9521
+ // TODO-PER: should probably report tempo, too
9522
+ }
9523
+ });
9524
+ if (staffNum === 0 && voiceNum === 0) {
9525
+ parts.push({
9526
+ type: "part",
9527
+ name: partName,
9528
+ lines: [measures]
9529
+ });
9530
+ }
9531
+ });
9532
+ }
9533
+ });
9534
+ if (!lastChord) throw new Error("noChords");
9535
+ return parts;
9536
+ }
9537
+ function findLastChord(measures) {
9538
+ for (var m = measures.length - 1; m >= 0; m--) {
9539
+ for (var c = measures[m].chord.length - 1; c >= 0; c--) {
9540
+ if (measures[m].chord[c]) return measures[m].chord[c];
9541
+ }
9542
+ }
9543
+ }
9544
+ function collapseIdenticalEndings(chartLines) {
9545
+ chartLines.forEach(function (line) {
9546
+ if (line.type === "part") {
9547
+ var partLine = line.lines[0];
9548
+ var ending1 = partLine.findIndex(function (bar) {
9549
+ return !!bar.ending;
9550
+ });
9551
+ var ending2 = partLine.findIndex(function (bar, index) {
9552
+ return index > ending1 && !!bar.ending;
9553
+ });
9554
+ if (ending1 >= 0 && ending2 >= 0) {
9555
+ // If the endings are not the same length, don't collapse
9556
+ if (ending2 - ending1 === partLine.length - ending2) {
9557
+ var matches = true;
9558
+ for (var i = 0; i < ending2 - ending1 && matches; i++) {
9559
+ var measureLhs = partLine[ending1 + i];
9560
+ var measureRhs = partLine[ending2 + i];
9561
+ if (measureLhs.chord[0] !== measureRhs.chord[0]) matches = false;
9562
+ if (measureLhs.chord[1] !== measureRhs.chord[1]) matches = false;
9563
+ if (measureLhs.chord[2] !== measureRhs.chord[2]) matches = false;
9564
+ if (measureLhs.chord[3] !== measureRhs.chord[3]) matches = false;
9565
+ if (measureLhs.annotations && !measureRhs.annotations) matches = false;
9566
+ if (!measureLhs.annotations && measureRhs.annotations) matches = false;
9567
+ if (measureLhs.annotations && measureRhs.annotations) {
9568
+ if (measureLhs.annotations.length !== measureRhs.annotations.length) matches = false;else {
9569
+ for (var j = 0; j < measureLhs.annotations.length; j++) {
9570
+ if (measureLhs.annotations[j] !== measureRhs.annotations[j]) matches = false;
9571
+ }
9572
+ }
9573
+ }
9574
+ }
9575
+ if (matches) {
9576
+ delete partLine[ending1].ending;
9577
+ partLine.splice(ending2, partLine.length - ending2);
9578
+ }
9579
+ }
9580
+ }
9581
+ }
9582
+ });
9583
+ }
9584
+ function addLineBreaks(chartLines) {
9585
+ chartLines.forEach(function (line) {
9586
+ if (line.type === "part") {
9587
+ var newLines = [];
9588
+ var oldLines = line.lines[0];
9589
+ var is12bar = false;
9590
+ var firstEndRepeat = oldLines.findIndex(function (l) {
9591
+ return !!l.hasEndRepeat;
9592
+ });
9593
+ var length = firstEndRepeat >= 0 ? Math.min(firstEndRepeat + 1, oldLines.length) : oldLines.length;
9594
+ if (length === 12) is12bar = true;
9595
+ var barsPerLine = is12bar ? 4 : 8; // Only do 4 bars per line for 12-bar blues
9596
+ for (var i = 0; i < oldLines.length; i += barsPerLine) {
9597
+ var newLine = oldLines.slice(i, i + barsPerLine);
9598
+ var endRepeat = newLine.findIndex(function (l) {
9599
+ return !!l.hasEndRepeat;
9600
+ });
9601
+ if (endRepeat >= 0 && endRepeat < newLine.length - 1) {
9602
+ newLines.push(newLine.slice(0, endRepeat + 1));
9603
+ newLines.push(newLine.slice(endRepeat + 1));
9604
+ } else newLines.push(newLine);
9605
+ }
9606
+ // TODO-PER: The following probably doesn't handle all cases. Rethink it.
9607
+ for (var _i = 0; _i < newLines.length; _i++) {
9608
+ if (newLines[_i][0].ending) {
9609
+ var prevLine = Math.max(0, _i - 1);
9610
+ var toAdd = newLines[prevLine].length - newLines[_i].length;
9611
+ var thisLine = [];
9612
+ for (var j = 0; j < toAdd; j++) {
9613
+ thisLine.push({
9614
+ noBorder: true,
9615
+ chord: ['', '', '', '']
9616
+ });
9617
+ }
9618
+ newLines[_i] = thisLine.concat(newLines[_i]);
9619
+ }
9620
+ }
9621
+ line.lines = newLines;
9622
+ }
9623
+ });
9624
+ }
9625
+ function addPercents(chartLines) {
9626
+ chartLines.forEach(function (part) {
9627
+ if (part.lines) {
9628
+ var lastMeasureSingle = false;
9629
+ var lastChord = "";
9630
+ part.lines.forEach(function (line) {
9631
+ line.forEach(function (measure) {
9632
+ if (!measure.noBorder) {
9633
+ var chords = measure.chord;
9634
+ if (!chords[0] && !chords[1] && !chords[2] && !chords[3]) {
9635
+ // if there are no chords specified for this measure
9636
+ if (lastMeasureSingle) {
9637
+ if (lastChord) chords[0] = '%';
9638
+ } else chords[0] = lastChord;
9639
+ lastMeasureSingle = true;
9640
+ } else if (!chords[1] && !chords[2] && !chords[3]) {
9641
+ // if there is a single chord for this measure
9642
+ lastMeasureSingle = true;
9643
+ lastChord = chords[0];
9644
+ } else {
9645
+ // if the measure is complicated - in that case the next measure won't get %
9646
+ lastMeasureSingle = false;
9647
+ lastChord = chords[3] || chords[2] || chords[1];
9648
+ }
9649
+ }
9650
+ });
9651
+ });
9652
+ }
9653
+ });
9654
+ }
9655
+ function addDecoration(element, currentBar) {
9656
+ if (element.decoration) {
9657
+ // Some decorations are interesting to rhythm players
9658
+ for (var i = 0; i < element.decoration.length; i++) {
9659
+ switch (element.decoration[i]) {
9660
+ case 'fermata':
9661
+ case 'segno':
9662
+ case 'coda':
9663
+ case "D.C.":
9664
+ case "D.S.":
9665
+ case "D.C.alcoda":
9666
+ case "D.C.alfine":
9667
+ case "D.S.alcoda":
9668
+ case "D.S.alfine":
9669
+ case "fine":
9670
+ if (!currentBar.annotations) currentBar.annotations = [];
9671
+ currentBar.annotations.push(element.decoration[i]);
9672
+ break;
9673
+ }
9674
+ }
9675
+ }
9676
+ }
9677
+ module.exports = chordGrid;
9678
+
9679
+ /***/ }),
9680
+
9205
9681
  /***/ "./src/parse/transpose-chord.js":
9206
9682
  /*!**************************************!*\
9207
9683
  !*** ./src/parse/transpose-chord.js ***!
@@ -9840,7 +10316,7 @@ function resolveOverlays(tune) {
9840
10316
  } else if (event.el_type === "note") {
9841
10317
  if (inOverlay) {
9842
10318
  overlayVoice[k].voice.push(event);
9843
- } else {
10319
+ } else if (!event.rest || event.rest.type !== 'spacer') {
9844
10320
  durationThisBar += event.duration;
9845
10321
  durationsPerLines[i] += event.duration;
9846
10322
  }
@@ -10052,11 +10528,11 @@ function cleanUpSlursInLine(line, staffNum, voiceNum, currSlur) {
10052
10528
  }
10053
10529
  }
10054
10530
  function wrapMusicLines(lines, barsperstaff) {
10055
- for (i = 0; i < lines.length; i++) {
10531
+ for (var i = 0; i < lines.length; i++) {
10056
10532
  if (lines[i].staff !== undefined) {
10057
- for (s = 0; s < lines[i].staff.length; s++) {
10533
+ for (var s = 0; s < lines[i].staff.length; s++) {
10058
10534
  var permanentItems = [];
10059
- for (v = 0; v < lines[i].staff[s].voices.length; v++) {
10535
+ for (var v = 0; v < lines[i].staff[s].voices.length; v++) {
10060
10536
  var voice = lines[i].staff[s].voices[v];
10061
10537
  var barNumThisLine = 0;
10062
10538
  for (var n = 0; n < voice.length; n++) {
@@ -10781,7 +11257,8 @@ var keyAccidentals = __webpack_require__(/*! ../const/key-accidentals */ "./src/
10781
11257
  var _require = __webpack_require__(/*! ../const/relative-major */ "./src/const/relative-major.js"),
10782
11258
  relativeMajor = _require.relativeMajor,
10783
11259
  transposeKey = _require.transposeKey,
10784
- relativeMode = _require.relativeMode;
11260
+ relativeMode = _require.relativeMode,
11261
+ isLegalMode = _require.isLegalMode;
10785
11262
  var transposeChordName = __webpack_require__(/*! ../parse/transpose-chord */ "./src/parse/transpose-chord.js");
10786
11263
  var strTranspose;
10787
11264
  (function () {
@@ -10844,11 +11321,12 @@ var strTranspose;
10844
11321
  var match = segment.match(/^( *)([A-G])([#b]?)( ?)(\w*)/);
10845
11322
  if (match) {
10846
11323
  var start = count + 2 + match[1].length; // move past the 'K:' and optional white space
10847
- var key = match[2] + match[3] + match[4] + match[5]; // key name, accidental, optional space, and mode
11324
+ var mode = isLegalMode(match[5]) ? match[5] : '';
11325
+ var key = match[2] + match[3] + match[4] + mode; // key name, accidental, optional space, and mode
10848
11326
  var destinationKey = newKey({
10849
11327
  root: match[2],
10850
11328
  acc: match[3],
10851
- mode: match[5]
11329
+ mode: mode
10852
11330
  }, steps);
10853
11331
  var dest = destinationKey.root + destinationKey.acc + match[4] + destinationKey.mode;
10854
11332
  changes.push({
@@ -10912,12 +11390,18 @@ var strTranspose;
10912
11390
  }
10913
11391
  }
10914
11392
  if (el.el_type === 'note' && el.pitches) {
10915
- for (var j = 0; j < el.pitches.length; j++) {
10916
- var note = parseNote(el.pitches[j].name, keyRoot, keyAccidentals, measureAccidentals);
11393
+ var pitchArray = findNotes(abc, el.startChar, el.endChar);
11394
+ //console.log(pitchArray)
11395
+ for (var j = 0; j < pitchArray.length; j++) {
11396
+ var note = parseNote(pitchArray[j].note, keyRoot, keyAccidentals, measureAccidentals);
10917
11397
  if (note.acc) measureAccidentals[note.name.toUpperCase()] = note.acc;
10918
11398
  var newPitch = transposePitch(note, destinationKey, letterDistance, transposedMeasureAccidentals);
10919
11399
  if (newPitch.acc) transposedMeasureAccidentals[newPitch.upper] = newPitch.acc;
10920
- changes.push(replaceNote(abc, el.startChar, el.endChar, newPitch.acc + newPitch.name, j));
11400
+ changes.push({
11401
+ note: newPitch.acc + newPitch.name,
11402
+ start: pitchArray[j].index,
11403
+ end: pitchArray[j].index + pitchArray[j].note.length
11404
+ });
10921
11405
  }
10922
11406
  if (el.gracenotes) {
10923
11407
  for (var g = 0; g < el.gracenotes.length; g++) {
@@ -10993,6 +11477,7 @@ var strTranspose;
10993
11477
  break;
10994
11478
  }
10995
11479
  }
11480
+ var newNote;
10996
11481
  switch (adj) {
10997
11482
  case -2:
10998
11483
  acc = "__";
@@ -11011,7 +11496,7 @@ var strTranspose;
11011
11496
  break;
11012
11497
  case -3:
11013
11498
  // This requires a triple flat, so bump down the pitch and try again
11014
- var newNote = {};
11499
+ newNote = {};
11015
11500
  newNote.pitch = note.pitch - 1;
11016
11501
  newNote.oct = note.oct;
11017
11502
  newNote.name = letters[letters.indexOf(note.name) - 1];
@@ -11023,7 +11508,7 @@ var strTranspose;
11023
11508
  return transposePitch(newNote, key, letterDistance + 1, measureAccidentals);
11024
11509
  case 3:
11025
11510
  // This requires a triple sharp, so bump up the pitch and try again
11026
- var newNote = {};
11511
+ newNote = {};
11027
11512
  newNote.pitch = note.pitch + 1;
11028
11513
  newNote.oct = note.oct;
11029
11514
  newNote.name = letters[letters.indexOf(note.name) + 1];
@@ -11072,7 +11557,8 @@ var strTranspose;
11072
11557
  var regPitch = /([_^=]*)([A-Ga-g])([,']*)/;
11073
11558
  var regNote = /([_^=]*[A-Ga-g][,']*)(\d*\/*\d*)([\>\<\-\)\.\s\\]*)/;
11074
11559
  var regOptionalNote = /([_^=]*[A-Ga-g][,']*)?(\d*\/*\d*)?([\>\<\-\)]*)?/;
11075
- var regSpace = /(\s*)$/;
11560
+ //var regSpace = /(\s*)$/
11561
+ //var regOptionalSpace = /(\s*)/
11076
11562
 
11077
11563
  // This the relationship of the note to the tonic and an octave. So what is returned is a distance in steps from the tonic and the amount of adjustment from
11078
11564
  // a normal scale. That is - in the key of D an F# is two steps from the tonic and no adjustment. A G# is three steps from the tonic and one half-step higher.
@@ -11100,48 +11586,100 @@ var strTranspose;
11100
11586
  courtesy: reg[1] === currentAcc
11101
11587
  };
11102
11588
  }
11103
- function replaceNote(abc, start, end, newPitch, index) {
11104
- // There may be more than just the note between the start and end - there could be spaces, there could be a chord symbol, there could be a decoration.
11105
- // This could also be a part of a chord. If so, then the particular note needs to be teased out.
11589
+ function findNotes(abc, start, end) {
11590
+ // TODO-PER: I thought this regex should have found all the notes and ignored the chords and decorations but it didn't: /(?:"[^"]+")*(?:![^!]+!)*([_^=]*)([A-Ga-g])([,']*)/g
11106
11591
  var note = abc.substring(start, end);
11107
- var match = note.match(new RegExp(regNote.source + regSpace.source), '');
11108
- if (match) {
11109
- // This will match a single note
11110
- var noteLen = match[1].length;
11111
- var trailingLen = match[2].length + match[3].length + match[4].length;
11112
- var leadingLen = end - start - noteLen - trailingLen;
11113
- start += leadingLen;
11114
- end -= trailingLen;
11115
- } else {
11116
- // I don't know how to capture more than one note, so I'm separating them. There is a limit of the number of notes in a chord depending on the repeats I have here, but it is unlikely to happen in real music.
11117
- var regPreBracket = /([^\[]*)/;
11118
- var regOpenBracket = /\[/;
11119
- var regCloseBracket = /\-?](\d*\/*\d*)?([\>\<\-\)]*)/;
11120
- match = note.match(new RegExp(regPreBracket.source + regOpenBracket.source + regOptionalNote.source + regOptionalNote.source + regOptionalNote.source + regOptionalNote.source + regOptionalNote.source + regOptionalNote.source + regOptionalNote.source + regOptionalNote.source + regCloseBracket.source + regSpace.source));
11121
- if (match) {
11122
- // This will match a chord
11123
- // Get the number of chars used by the previous notes in this chord
11124
- var count = 1 + match[1].length; // one character for the open bracket
11125
- for (var i = 0; i < index; i++) {
11126
- // index is the iteration through the chord. This function gets called for each one.
11127
- if (match[i * 3 + 2]) count += match[i * 3 + 2].length;
11128
- if (match[i * 3 + 3]) count += match[i * 3 + 3].length;
11129
- if (match[i * 3 + 4]) count += match[i * 3 + 4].length;
11130
- }
11131
- start += count;
11132
- var endLen = match[index * 3 + 2] ? match[index * 3 + 2].length : 0;
11133
- // endLen += match[index * 3 + 3] ? match[index * 3 + 3].length : 0
11134
- // endLen += match[index * 3 + 4] ? match[index * 3 + 4].length : 0
11135
11592
 
11136
- end = start + endLen;
11593
+ // Since the regex will also find "c", "d", and "a" in `!coda!`, we need to filter them
11594
+ var array;
11595
+ var ignoreBlocks = [];
11596
+ var regChord = /("[^"]+")+/g;
11597
+ while ((array = regChord.exec(note)) !== null) {
11598
+ ignoreBlocks.push({
11599
+ start: regChord.lastIndex - array[0].length,
11600
+ end: regChord.lastIndex
11601
+ });
11602
+ }
11603
+ var regDec = /(![^!]+!)+/g;
11604
+ while ((array = regDec.exec(note)) !== null) {
11605
+ ignoreBlocks.push({
11606
+ start: regDec.lastIndex - array[0].length,
11607
+ end: regDec.lastIndex
11608
+ });
11609
+ }
11610
+ var ret = [];
11611
+ // Define the regex each time because it is stateful
11612
+ var regPitch = /([_^=]*)([A-Ga-g])([,']*)/g;
11613
+ while ((array = regPitch.exec(note)) !== null) {
11614
+ var found = false;
11615
+ for (var i = 0; i < ignoreBlocks.length; i++) {
11616
+ if (regPitch.lastIndex >= ignoreBlocks[i].start && regPitch.lastIndex <= ignoreBlocks[i].end) found = true;
11137
11617
  }
11618
+ if (!found) ret.push({
11619
+ note: array[0],
11620
+ index: start + regPitch.lastIndex - array[0].length
11621
+ });
11138
11622
  }
11139
- return {
11140
- start: start,
11141
- end: end,
11142
- note: newPitch
11143
- };
11623
+ return ret;
11144
11624
  }
11625
+
11626
+ // function replaceNote(abc, start, end, newPitch, oldPitch, index) {
11627
+ // var note = abc.substring(start, end);
11628
+ // // Try single note first
11629
+ // var match = note.match(new RegExp(regNote.source + regSpace.source));
11630
+ // if (match) {
11631
+ // var noteLen = match[1].length;
11632
+ // var trailingLen = match[2].length + match[3].length + match[4].length;
11633
+ // var leadingLen = end - start - noteLen - trailingLen;
11634
+ // start += leadingLen;
11635
+ // end -= trailingLen;
11636
+ // } else {
11637
+ // // Match chord
11638
+ // var regPreBracket = /([^\[]*)/;
11639
+ // var regOpenBracket = /\[/;
11640
+ // var regCloseBracket = /\-?](\d*\/*\d*)?([\>\<\-\)]*)/;
11641
+ // var regChord = new RegExp(
11642
+ // regPreBracket.source +
11643
+ // regOpenBracket.source +
11644
+ // "(?:" + regOptionalNote.source + "\\s*){1,8}" +
11645
+ // regCloseBracket.source +
11646
+ // regSpace.source
11647
+ // );
11648
+ // match = note.match(regChord);
11649
+ // if (match) {
11650
+ // var beforeChordLen = match[1].length + 1; // text before + '['
11651
+ // var chordBody = note.slice(match[1].length + 1, note.lastIndexOf("]"));
11652
+ // // Collect notes inside chord
11653
+ // var chordNotes = [];
11654
+ // var regNoteWithSpace = new RegExp(regOptionalNote.source + "\\s*", "g");
11655
+ // for (const m of chordBody.matchAll(regNoteWithSpace)) {
11656
+ // let noteText = m[0].trim();
11657
+ // if (noteText !== "") {
11658
+ // chordNotes.push({ text: noteText, index: m.index });
11659
+ // }
11660
+ // }
11661
+ // if (index >= chordNotes.length) {
11662
+ // throw new Error("Chord index out of range for chord: " + note);
11663
+ // }
11664
+ // var chosen = chordNotes[index];
11665
+ // // Preserve duration and tie
11666
+ // let mDurTie = chosen.text.match(/^(.+?)(\d+\/?\d*)?(-)?$/);
11667
+ // let pitchPart = mDurTie ? mDurTie[1] : chosen.text;
11668
+ // let durationPart = mDurTie && mDurTie[2] ? mDurTie[2] : "";
11669
+ // let tiePart = mDurTie && mDurTie[3] ? mDurTie[3] : "";
11670
+ // // Replace note keeping duration and tie
11671
+ // newPitch = newPitch + durationPart + tiePart;
11672
+ // start += beforeChordLen + chosen.index;
11673
+ // end = start + chosen.text.length;
11674
+ // }
11675
+ // }
11676
+ // return {
11677
+ // start: start,
11678
+ // end: end,
11679
+ // note: newPitch
11680
+ // };
11681
+ // }
11682
+
11145
11683
  function replaceGrace(abc, start, end, newGrace, index) {
11146
11684
  var note = abc.substring(start, end);
11147
11685
  // I don't know how to capture more than one note, so I'm separating them. There is a limit of the number of notes in a chord depending on the repeats I have here, but it is unlikely to happen in real music.
@@ -12550,11 +13088,12 @@ module.exports = rendererFactory;
12550
13088
 
12551
13089
  var sequence;
12552
13090
  var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/abc_common.js");
13091
+ var Repeats = __webpack_require__(/*! ./repeats */ "./src/synth/repeats.js");
12553
13092
  (function () {
12554
13093
  "use strict";
12555
13094
 
12556
13095
  var measureLength = 1; // This should be set by the meter, but just in case that is missing, we'll take a guess.
12557
- // The abc is provided to us line by line. It might have repeats in it. We want to re arrange the elements to
13096
+ // The abc is provided to us line by line. It might have repeats in it. We want to rearrange the elements to
12558
13097
  // be an array of voices with all the repeats embedded, and no lines. Then it is trivial to go through the events
12559
13098
  // one at a time and turn it into midi.
12560
13099
 
@@ -12691,8 +13230,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12691
13230
  timing: 0
12692
13231
  };
12693
13232
  var currentVolume;
12694
- var startRepeatPlaceholder = []; // There is a place holder for each voice.
12695
- var skipEndingPlaceholder = []; // This is the place where the first ending starts.
13233
+ var repeats = [];
12696
13234
  var startingDrumSet = false;
12697
13235
  var lines = abctune.lines; //abctune.deline(); TODO-PER: can switch to this, then simplify the loops below.
12698
13236
  for (var i = 0; i < lines.length; i++) {
@@ -12771,6 +13309,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12771
13309
  el_type: "name",
12772
13310
  trackName: voiceName
12773
13311
  });
13312
+ repeats[voiceNumber] = new Repeats(voices[voiceNumber]);
12774
13313
  }
12775
13314
  // Negate any transposition for the percussion staff.
12776
13315
  if (transpose && staff.clef.type === "perc") voices[voiceNumber].push({
@@ -12969,28 +13508,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
12969
13508
  }); // We need the bar marking to reset the accidentals.
12970
13509
  setDynamics(elem);
12971
13510
  noteEventsInBar = 0;
12972
- // figure out repeats and endings --
12973
- // The important part is where there is a start repeat, and end repeat, or a first ending.
12974
- var endRepeat = elem.type === "bar_right_repeat" || elem.type === "bar_dbl_repeat";
12975
- var startEnding = elem.startEnding === '1';
12976
- var startRepeat = elem.type === "bar_left_repeat" || elem.type === "bar_dbl_repeat" || elem.type === "bar_right_repeat";
12977
- if (endRepeat) {
12978
- var s = startRepeatPlaceholder[voiceNumber];
12979
- if (!s) s = 0; // If there wasn't a left repeat, then we repeat from the beginning.
12980
- var e = skipEndingPlaceholder[voiceNumber];
12981
- if (!e) e = voices[voiceNumber].length; // If there wasn't a first ending marker, then we copy everything.
12982
- // duplicate each of the elements - this has to be a deep copy.
12983
- for (var z = s; z < e; z++) {
12984
- var item = Object.assign({}, voices[voiceNumber][z]);
12985
- if (item.pitches) item.pitches = parseCommon.cloneArray(item.pitches);
12986
- voices[voiceNumber].push(item);
12987
- }
12988
- // reset these in case there is a second repeat later on.
12989
- skipEndingPlaceholder[voiceNumber] = undefined;
12990
- startRepeatPlaceholder[voiceNumber] = undefined;
12991
- }
12992
- if (startEnding) skipEndingPlaceholder[voiceNumber] = voices[voiceNumber].length;
12993
- if (startRepeat) startRepeatPlaceholder[voiceNumber] = voices[voiceNumber].length;
13511
+ repeats[voiceNumber].addBar(elem, voiceNumber);
12994
13512
  rhythmHeadThisBar = false;
12995
13513
  break;
12996
13514
  case 'style':
@@ -13140,6 +13658,10 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
13140
13658
  }
13141
13659
  }
13142
13660
  }
13661
+ for (var r = 0; r < repeats.length; r++) {
13662
+ voices[r] = repeats[r].resolveRepeats();
13663
+ }
13664
+
13143
13665
  // If there are tempo changes, make sure they are in all the voices. This must be done post process because all the elements in all the voices need to be created first.
13144
13666
  insertTempoChanges(voices, tempoChanges);
13145
13667
  if (drumIntro) {
@@ -13279,6 +13801,7 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
13279
13801
  num: 4,
13280
13802
  den: 4
13281
13803
  };
13804
+ measureLength = 4 / 4;
13282
13805
  break;
13283
13806
  case "cut_time":
13284
13807
  meter = {
@@ -13286,22 +13809,31 @@ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/ab
13286
13809
  num: 2,
13287
13810
  den: 2
13288
13811
  };
13812
+ measureLength = 2 / 2;
13289
13813
  break;
13290
13814
  case "specified":
13291
13815
  // TODO-PER: only taking the first meter, so the complex meters are not handled.
13816
+ var num = 0;
13817
+ if (element.value && element.value.length > 0 && element.value[0].num.indexOf('+') > 0) {
13818
+ var parts = element.value[0].num.split('+');
13819
+ for (var i = 0; i < parts.length; i++) {
13820
+ num += parseInt(parts[i], 10);
13821
+ }
13822
+ } else num = parseInt(element.value[0].num, 10);
13292
13823
  meter = {
13293
13824
  el_type: 'meter',
13294
- num: element.value[0].num,
13825
+ num: num,
13295
13826
  den: element.value[0].den
13296
13827
  };
13828
+ measureLength = num / parseInt(element.value[0].den, 10);
13297
13829
  break;
13298
13830
  default:
13299
13831
  // This should never happen.
13300
13832
  meter = {
13301
13833
  el_type: 'meter'
13302
13834
  };
13835
+ measureLength = 1;
13303
13836
  }
13304
- measureLength = meter.num / meter.den;
13305
13837
  return meter;
13306
13838
  }
13307
13839
  function removeNaturals(accidentals) {
@@ -14397,7 +14929,7 @@ function CreateSynth() {
14397
14929
  if (options.visualObj) {
14398
14930
  self.flattened = options.visualObj.setUpAudio(params);
14399
14931
  var meter = options.visualObj.getMeterFraction();
14400
- if (meter.den) self.meterSize = options.visualObj.getMeterFraction().num / options.visualObj.getMeterFraction().den;
14932
+ if (meter.den) self.meterSize = meter.num / meter.den;
14401
14933
  self.pickupLength = options.visualObj.getPickupLength();
14402
14934
  } else if (options.sequence) self.flattened = options.sequence;else return Promise.reject(new Error("Must pass in either a visualObj or a sequence"));
14403
14935
  self.millisecondsPerMeasure = options.millisecondsPerMeasure ? options.millisecondsPerMeasure : options.visualObj ? options.visualObj.millisecondsPerMeasure(self.flattened.tempo) : 1000;
@@ -15546,6 +16078,221 @@ module.exports = registerAudioContext;
15546
16078
 
15547
16079
  /***/ }),
15548
16080
 
16081
+ /***/ "./src/synth/repeats.js":
16082
+ /*!******************************!*\
16083
+ !*** ./src/synth/repeats.js ***!
16084
+ \******************************/
16085
+ /***/ (function(module, __unused_webpack_exports, __webpack_require__) {
16086
+
16087
+ var parseCommon = __webpack_require__(/*! ../parse/abc_common */ "./src/parse/abc_common.js");
16088
+ function Repeats(voice) {
16089
+ this.sections = [{
16090
+ type: 'startRepeat',
16091
+ index: -1
16092
+ }];
16093
+ this.addBar = function (elem) {
16094
+ // Record the "interesting" parts for analysis at the end.
16095
+ var thisIndex = voice.length - 1;
16096
+ var isStartRepeat = elem.type === "bar_left_repeat" || elem.type === "bar_dbl_repeat";
16097
+ var isEndRepeat = elem.type === "bar_right_repeat" || elem.type === "bar_dbl_repeat";
16098
+ var startEnding = elem.startEnding ? startEndingNumbers(elem.startEnding) : undefined;
16099
+ if (isEndRepeat) this.sections.push({
16100
+ type: "endRepeat",
16101
+ index: thisIndex
16102
+ });
16103
+ if (isStartRepeat) this.sections.push({
16104
+ type: "startRepeat",
16105
+ index: thisIndex
16106
+ });
16107
+ if (startEnding) this.sections.push({
16108
+ type: "startEnding",
16109
+ index: thisIndex,
16110
+ endings: startEnding
16111
+ });
16112
+ };
16113
+ this.resolveRepeats = function () {
16114
+ // this.sections contain all the interesting bars - start and end repeats.
16115
+ var e;
16116
+
16117
+ // There may be one last set of events after the last interesting bar, so capture that now.
16118
+ var lastSection = this.sections[this.sections.length - 1];
16119
+ var lastElement = voice.length - 1;
16120
+ if (lastSection.type === 'startRepeat') lastSection.end = lastElement;else if (lastSection.index + 1 < lastElement) this.sections.push({
16121
+ type: "startRepeat",
16122
+ index: lastSection.index + 1
16123
+ });
16124
+
16125
+ // console.log(voice.map((el,index) => {
16126
+ // return JSON.stringify({i: index, t: el.el_type, p: el.pitches ? el.pitches[0].name: undefined})
16127
+ // }).join("\n"))
16128
+
16129
+ // console.log(this.sections.map(s => JSON.stringify(s)).join("\n"))
16130
+ if (this.sections.length < 2) return voice; // If there are no repeats then don't bother copying anything
16131
+
16132
+ // Go through all the markers and turn that into an array of sets of sections in order.
16133
+ // The output is repeatInstructions. If "endings" is not present, then the common section should just
16134
+ // be copied once. If "endings" is present but is empty, that means it is a plain repeat without
16135
+ // endings so the common section is copied twice. If "endings" contains items, then copy the
16136
+ // common section followed by each ending in turn. If the last item in "endings" is -1, then
16137
+ // the common section should be copied one more time but there isn't a corresponding ending for it.
16138
+ var repeatInstructions = []; // { common: { start: number, end: number }, endings: Array<{start:number, end:number> }
16139
+ var currentRepeat = null;
16140
+ for (var i = 0; i < this.sections.length; i++) {
16141
+ var section = this.sections[i];
16142
+ //var end = i < this.sections.length-1 ? this.sections[i+1].index : lastElement
16143
+ switch (section.type) {
16144
+ case "startRepeat":
16145
+ if (currentRepeat) {
16146
+ if (!currentRepeat.common.end) currentRepeat.common.end = section.index;
16147
+ if (currentRepeat.endings) {
16148
+ for (e = 0; e < currentRepeat.endings.length; e++) {
16149
+ if (currentRepeat.endings[e] && !currentRepeat.endings[e].end && currentRepeat.endings[e].start !== section.index) currentRepeat.endings[e].end = section.index;
16150
+ }
16151
+ }
16152
+ // If the last event was an end repeat, then there is one more repeat of just the common area. (Only when there are ending markers - otherwise it is already taken care of.)
16153
+ if (this.sections[i - 1].type === 'endRepeat' && currentRepeat.endings && currentRepeat.endings.length) currentRepeat.endings[currentRepeat.endings.length] = {
16154
+ start: -1,
16155
+ end: -1
16156
+ };
16157
+ repeatInstructions.push(currentRepeat);
16158
+ }
16159
+
16160
+ // if there is a gap between the last event and this start, then
16161
+ // insert those items.
16162
+ if (currentRepeat) {
16163
+ var lastUsed = currentRepeat.common.end;
16164
+ if (currentRepeat.endings) {
16165
+ for (e = 0; e < currentRepeat.endings.length; e++) {
16166
+ if (currentRepeat.endings[e]) lastUsed = Math.max(lastUsed, currentRepeat.endings[e].end);
16167
+ }
16168
+ }
16169
+ if (lastUsed < section.index - 1) {
16170
+ console.log("gap", voice.slice(lastUsed + 1, section.index));
16171
+ repeatInstructions.push({
16172
+ common: {
16173
+ start: lastUsed + 1,
16174
+ end: section.index
16175
+ }
16176
+ });
16177
+ }
16178
+ }
16179
+ currentRepeat = {
16180
+ common: {
16181
+ start: section.index
16182
+ }
16183
+ };
16184
+ break;
16185
+ case "startEnding":
16186
+ {
16187
+ if (currentRepeat) {
16188
+ if (!currentRepeat.common.end) currentRepeat.common.end = section.index;
16189
+ if (!currentRepeat.endings) currentRepeat.endings = [];
16190
+ for (e = 0; e < section.endings.length; e++) {
16191
+ currentRepeat.endings[section.endings[e]] = {
16192
+ start: section.index + 1
16193
+ };
16194
+ }
16195
+ }
16196
+ break;
16197
+ }
16198
+ case "endRepeat":
16199
+ if (currentRepeat) {
16200
+ if (!currentRepeat.endings) currentRepeat.endings = [];
16201
+ if (currentRepeat.endings.length > 0) {
16202
+ for (e = 0; e < currentRepeat.endings.length; e++) {
16203
+ if (currentRepeat.endings[e] && !currentRepeat.endings[e].end) currentRepeat.endings[e].end = section.index;
16204
+ }
16205
+ }
16206
+ if (!currentRepeat.common.end)
16207
+ // This is a repeat that doesn't have first and second endings
16208
+ currentRepeat.common.end = section.index;
16209
+ }
16210
+ break;
16211
+ }
16212
+ }
16213
+ if (currentRepeat) {
16214
+ if (!currentRepeat.common.end) currentRepeat.common.end = lastElement;
16215
+ if (currentRepeat.endings) {
16216
+ for (e = 0; e < currentRepeat.endings.length; e++) {
16217
+ if (currentRepeat.endings[e] && !currentRepeat.endings[e].end) currentRepeat.endings[e].end = lastElement;
16218
+ }
16219
+ }
16220
+ repeatInstructions.push(currentRepeat);
16221
+ }
16222
+ // for (var x = 0; x < repeatInstructions.length; x++) {
16223
+ // console.log(JSON.stringify(repeatInstructions[x]))
16224
+ // }
16225
+
16226
+ var output = [];
16227
+ var lastEnd = -1;
16228
+ for (var r = 0; r < repeatInstructions.length; r++) {
16229
+ var instructions = repeatInstructions[r];
16230
+ if (!instructions.endings) {
16231
+ duplicateSpan(voice, output, instructions.common.start, instructions.common.end);
16232
+ } else if (instructions.endings.length === 0) {
16233
+ // this is when there is no endings specified - it is just a repeat
16234
+ duplicateSpan(voice, output, instructions.common.start, instructions.common.end);
16235
+ duplicateSpan(voice, output, instructions.common.start, instructions.common.end);
16236
+ } else {
16237
+ for (e = 0; e < instructions.endings.length; e++) {
16238
+ var ending = instructions.endings[e];
16239
+ if (ending) {
16240
+ // this is a sparse array so skip the empty ones
16241
+ duplicateSpan(voice, output, instructions.common.start, instructions.common.end);
16242
+ if (ending.start > 0) {
16243
+ duplicateSpan(voice, output, ending.start, ending.end);
16244
+ }
16245
+ lastEnd = Math.max(lastEnd, ending.end);
16246
+ }
16247
+ }
16248
+ }
16249
+ }
16250
+ return output;
16251
+ };
16252
+ }
16253
+ function duplicateSpan(input, output, start, end) {
16254
+ //console.log("dup", {start, end})
16255
+ for (var i = start; i <= end; i++) {
16256
+ output.push(duplicateItem(input[i]));
16257
+ }
16258
+ }
16259
+ function duplicateItem(src) {
16260
+ var item = Object.assign({}, src);
16261
+ if (item.pitches) item.pitches = parseCommon.cloneArray(item.pitches);
16262
+ return item;
16263
+ }
16264
+ function startEndingNumbers(startEnding) {
16265
+ // The ending can be in four different types: "random-string", "number", "number-number", "number,number"
16266
+ // If we don't get a number out of it then we will just skip the ending - we don't know what to do with it.
16267
+ var nums = [];
16268
+ var ending, endings, i;
16269
+ if (startEnding.indexOf(',') > 0) {
16270
+ endings = startEnding.split(',');
16271
+ for (i = 0; i < endings.length; i++) {
16272
+ ending = parseInt(endings[i], 10);
16273
+ if (ending > 0) {
16274
+ nums.push(ending);
16275
+ }
16276
+ }
16277
+ } else if (startEnding.indexOf('-') > 0) {
16278
+ endings = startEnding.split('-');
16279
+ var se = parseInt(endings[0], 10);
16280
+ var ee = parseInt(endings[1], 10);
16281
+ for (i = se; i <= ee; i++) {
16282
+ nums.push(i);
16283
+ }
16284
+ } else {
16285
+ ending = parseInt(startEnding, 10);
16286
+ if (ending > 0) {
16287
+ nums.push(ending);
16288
+ }
16289
+ }
16290
+ return nums;
16291
+ }
16292
+ module.exports = Repeats;
16293
+
16294
+ /***/ }),
16295
+
15549
16296
  /***/ "./src/synth/sounds-cache.js":
15550
16297
  /*!***********************************!*\
15551
16298
  !*** ./src/synth/sounds-cache.js ***!
@@ -17782,8 +18529,11 @@ AbstractEngraver.prototype.createABCElement = function (isFirstStaff, isSingleLi
17782
18529
  elemset[0] = abselem;
17783
18530
  break;
17784
18531
  case "tempo":
18532
+ // MAE 20 Nov 2025 For %%printtempo after initial header
17785
18533
  var abselem3 = new AbsoluteElement(elem, 0, 0, 'tempo', this.tuneNumber);
17786
- abselem3.addFixedX(new TempoElement(elem, this.tuneNumber, createNoteHead));
18534
+ if (!elem.suppress) {
18535
+ abselem3.addFixedX(new TempoElement(elem, this.tuneNumber, createNoteHead));
18536
+ }
17787
18537
  elemset[0] = abselem3;
17788
18538
  break;
17789
18539
  case "style":
@@ -22014,6 +22764,268 @@ module.exports = drawBrace;
22014
22764
 
22015
22765
  /***/ }),
22016
22766
 
22767
+ /***/ "./src/write/draw/chord-grid.js":
22768
+ /*!**************************************!*\
22769
+ !*** ./src/write/draw/chord-grid.js ***!
22770
+ \**************************************/
22771
+ /***/ (function(module, __unused_webpack_exports, __webpack_require__) {
22772
+
22773
+ var printSymbol = __webpack_require__(/*! ./print-symbol */ "./src/write/draw/print-symbol.js");
22774
+ var printStem = __webpack_require__(/*! ./print-stem */ "./src/write/draw/print-stem.js");
22775
+ function drawChordGrid(renderer, parts, leftMargin, pageWidth, fonts) {
22776
+ var chordFont = fonts.gchordfont;
22777
+ var partFont = fonts.partsfont;
22778
+ var annotationFont = fonts.annotationfont;
22779
+ var endingFont = fonts.repeatfont;
22780
+ var textFont = fonts.textfont;
22781
+ var subtitleFont = fonts.subtitlefont;
22782
+ var ROW_HEIGHT = 50;
22783
+ var ENDING_HEIGHT = 10;
22784
+ var ANNOTATION_HEIGHT = 14;
22785
+ var PART_MARGIN_TOP = 10;
22786
+ var PART_MARGIN_BOTTOM = 20;
22787
+ var TEXT_MARGIN = 16;
22788
+ parts.forEach(function (part) {
22789
+ switch (part.type) {
22790
+ case "text":
22791
+ {
22792
+ text(renderer, part.text, leftMargin, renderer.y, 16, textFont, null, null, false);
22793
+ renderer.moveY(TEXT_MARGIN);
22794
+ }
22795
+ break;
22796
+ case "subtitle":
22797
+ {
22798
+ text(renderer, part.subtitle, leftMargin, renderer.y + PART_MARGIN_TOP, 20, subtitleFont, null, "abcjs-subtitle", false);
22799
+ renderer.moveY(PART_MARGIN_BOTTOM);
22800
+ }
22801
+ break;
22802
+ case "part":
22803
+ if (part.lines.length > 0) {
22804
+ text(renderer, part.name, leftMargin, renderer.y + PART_MARGIN_TOP, 20, subtitleFont, part.name, "abcjs-part", false);
22805
+ renderer.moveY(PART_MARGIN_BOTTOM);
22806
+ var numCols = part.lines[0].length;
22807
+ var colWidth = pageWidth / numCols;
22808
+ part.lines.forEach(function (line, lineNum) {
22809
+ var hasEnding = false;
22810
+ var hasAnnotation = false;
22811
+ line.forEach(function (measure) {
22812
+ if (measure.ending) hasEnding = true;
22813
+ if (measure.annotations && measure.annotations.length > 0) hasAnnotation = true;
22814
+ });
22815
+ var extraTop = hasAnnotation ? ANNOTATION_HEIGHT : hasEnding ? ENDING_HEIGHT : 0;
22816
+ line.forEach(function (measure, barNum) {
22817
+ var RECT_WIDTH = 1;
22818
+ if (!measure.noBorder) {
22819
+ renderer.paper.rect({
22820
+ x: leftMargin + barNum * colWidth,
22821
+ y: renderer.y,
22822
+ width: colWidth,
22823
+ height: extraTop + ROW_HEIGHT
22824
+ });
22825
+ renderer.paper.rect({
22826
+ x: leftMargin + barNum * colWidth + RECT_WIDTH,
22827
+ y: renderer.y + RECT_WIDTH,
22828
+ width: colWidth - RECT_WIDTH * 2,
22829
+ height: extraTop + ROW_HEIGHT - RECT_WIDTH * 2
22830
+ });
22831
+ var repeatLeft = 0;
22832
+ var repeatRight = 0;
22833
+ var top = renderer.y;
22834
+ var left = leftMargin + colWidth * barNum;
22835
+ if (measure.hasStartRepeat) {
22836
+ drawRepeat(renderer, left, top, top + ROW_HEIGHT + extraTop, true, extraTop);
22837
+ repeatLeft = 12;
22838
+ }
22839
+ if (measure.hasEndRepeat) {
22840
+ drawRepeat(renderer, left + colWidth, top, top + ROW_HEIGHT + extraTop, false, extraTop);
22841
+ repeatRight = 12;
22842
+ }
22843
+ var endingWidth = 0;
22844
+ if (measure.ending) {
22845
+ var endingEl = text(renderer, measure.ending, leftMargin + barNum * colWidth + 4, top + 10, 12, endingFont, null, null, false);
22846
+ endingWidth = endingEl.getBBox().width + 4;
22847
+ }
22848
+ drawMeasure(renderer, top, leftMargin + repeatLeft, colWidth, lineNum, barNum, measure.chord, chordFont, repeatLeft + repeatRight, ROW_HEIGHT, extraTop);
22849
+ if (measure.annotations && measure.annotations.length > 0) {
22850
+ drawAnnotations(renderer, top, leftMargin + barNum * colWidth + endingWidth, measure.annotations, annotationFont);
22851
+ }
22852
+ if (extraTop) {
22853
+ renderer.paper.rectBeneath({
22854
+ x: leftMargin + barNum * colWidth,
22855
+ y: renderer.y,
22856
+ width: colWidth,
22857
+ height: extraTop,
22858
+ fill: '#e8e8e8',
22859
+ stroke: 'none'
22860
+ });
22861
+ }
22862
+ }
22863
+ });
22864
+ renderer.moveY(extraTop + ROW_HEIGHT);
22865
+ });
22866
+ renderer.moveY(PART_MARGIN_BOTTOM);
22867
+ }
22868
+ break;
22869
+ }
22870
+ });
22871
+ }
22872
+ function drawPercent(renderer, x, y, offset) {
22873
+ var lineX1 = x - 10;
22874
+ var lineX2 = x + 10;
22875
+ var lineY1 = y + 10;
22876
+ var lineY2 = y - 10;
22877
+ var leftDotX = x - 10;
22878
+ var leftDotY = -renderer.yToPitch(offset) + 2;
22879
+ var rightDotX = x + 6.5;
22880
+ var rightDotY = -renderer.yToPitch(offset) - 2.3;
22881
+ renderer.paper.lineToBack({
22882
+ x1: lineX1,
22883
+ x2: lineX2,
22884
+ y1: lineY1,
22885
+ y2: lineY2,
22886
+ 'stroke-width': '3px',
22887
+ 'stroke-linecap': "round"
22888
+ });
22889
+ printSymbol(renderer, leftDotX, leftDotY, "dots.dot", {
22890
+ scalex: 1,
22891
+ scaley: 1,
22892
+ klass: "",
22893
+ name: "dot"
22894
+ });
22895
+ printSymbol(renderer, rightDotX, rightDotY, "dots.dot", {
22896
+ scalex: 1,
22897
+ scaley: 1,
22898
+ klass: "",
22899
+ name: "dot"
22900
+ });
22901
+ }
22902
+ function drawRepeat(renderer, x, y1, y2, isStart, offset) {
22903
+ var lineX = isStart ? x + 2 : x - 4;
22904
+ var circleX = isStart ? x + 9 : x - 11;
22905
+ renderer.paper.openGroup({
22906
+ klass: 'abcjs-repeat'
22907
+ });
22908
+ printStem(renderer, lineX, 3 + renderer.lineThickness, y1, y2, null, "bar");
22909
+ printSymbol(renderer, circleX, -renderer.yToPitch(offset) - 4, "dots.dot", {
22910
+ scalex: 1,
22911
+ scaley: 1,
22912
+ klass: "",
22913
+ name: "dot"
22914
+ });
22915
+ printSymbol(renderer, circleX, -renderer.yToPitch(offset) - 8, "dots.dot", {
22916
+ scalex: 1,
22917
+ scaley: 1,
22918
+ klass: "",
22919
+ name: "dot"
22920
+ });
22921
+ renderer.paper.closeGroup();
22922
+ }
22923
+ var symbols = {
22924
+ 'segno': "scripts.segno",
22925
+ 'coda': "scripts.coda",
22926
+ "fermata": "scripts.ufermata"
22927
+ };
22928
+ function drawAnnotations(renderer, offset, left, annotations, annotationFont) {
22929
+ left += 3;
22930
+ var el;
22931
+ for (var a = 0; a < annotations.length; a++) {
22932
+ switch (annotations[a]) {
22933
+ case 'segno':
22934
+ case 'coda':
22935
+ case "fermata":
22936
+ {
22937
+ left += 12;
22938
+ el = printSymbol(renderer, left, -3, symbols[annotations[a]], {
22939
+ scalex: 1,
22940
+ scaley: 1,
22941
+ //klass: renderer.controller.classes.generate(klass),
22942
+ name: symbols[annotations[a]]
22943
+ });
22944
+ var box = el.getBBox();
22945
+ left += box.width;
22946
+ }
22947
+ break;
22948
+ default:
22949
+ text(renderer, annotations[a], left, offset + 12, 12, annotationFont, null, null, false);
22950
+ }
22951
+ }
22952
+ }
22953
+ function drawMeasure(renderer, offset, leftMargin, colWidth, lineNum, barNum, chords, chordFont, margin, height, extraTop) {
22954
+ var left = leftMargin + colWidth * barNum;
22955
+ if (!chords[1] && !chords[2] && !chords[3]) drawSingleChord(renderer, left, offset + extraTop, colWidth - margin, height, chords[0], chordFont, extraTop);else if (!chords[1] && !chords[3]) drawTwoChords(renderer, left, offset, colWidth - margin, height, chords[0], chords[2], chordFont, extraTop);else drawFourChords(renderer, left, offset, colWidth - margin, height, chords, chordFont, extraTop);
22956
+ }
22957
+ function renderChord(renderer, x, y, size, chord, font, maxWidth) {
22958
+ var el = text(renderer, chord, x, y, size, font, null, "abcjs-chord", true);
22959
+ var bb = el.getBBox();
22960
+ var fontSize = size;
22961
+ while (bb.width > maxWidth && fontSize >= 14) {
22962
+ fontSize -= 2;
22963
+ el.setAttribute('font-size', fontSize);
22964
+ bb = el.getBBox();
22965
+ }
22966
+ }
22967
+ var MAX_ONE_CHORD = 34;
22968
+ var MAX_TWO_CHORDS = 26;
22969
+ var MAX_FOUR_CHORDS = 20;
22970
+ var TOP_MARGIN = -3;
22971
+ function drawSingleChord(renderer, left, top, width, height, chord, font, extraTop) {
22972
+ if (chord === '%') drawPercent(renderer, left + width / 2, top + height / 2, extraTop + height / 2);else renderChord(renderer, left + width / 2, top + height / 2 + TOP_MARGIN, MAX_ONE_CHORD, chord, font, width);
22973
+ }
22974
+ function drawTwoChords(renderer, left, top, width, height, chord1, chord2, font, extraTop) {
22975
+ renderer.paper.lineToBack({
22976
+ x1: left,
22977
+ x2: left + width,
22978
+ y1: top + height + extraTop,
22979
+ y2: top + 2
22980
+ });
22981
+ renderChord(renderer, left + width / 4, top + height / 4 + 5 + extraTop + TOP_MARGIN, MAX_TWO_CHORDS, chord1, font, width / 2);
22982
+ renderChord(renderer, left + 3 * width / 4, top + 3 * height / 4 + extraTop + TOP_MARGIN, MAX_TWO_CHORDS, chord2, font, width / 2);
22983
+ }
22984
+ function drawFourChords(renderer, left, top, width, height, chords, font, extraTop) {
22985
+ var MARGIN = 3;
22986
+ renderer.paper.lineToBack({
22987
+ x1: left + MARGIN,
22988
+ x2: left + width - MARGIN,
22989
+ y1: top + height / 2 + extraTop,
22990
+ y2: top + height / 2 + extraTop
22991
+ });
22992
+ renderer.paper.lineToBack({
22993
+ x1: left + width / 2,
22994
+ x2: left + width / 2,
22995
+ y1: top + MARGIN + extraTop,
22996
+ y2: top + height - MARGIN + extraTop
22997
+ });
22998
+ if (chords[0]) renderChord(renderer, left + width / 4, top + height / 4 + 2 + extraTop + TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[0]), font, width / 2);
22999
+ if (chords[1]) renderChord(renderer, left + 3 * width / 4, top + height / 4 + 2 + extraTop + TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[1]), font, width / 2);
23000
+ if (chords[2]) renderChord(renderer, left + width / 4, top + 3 * height / 4 + extraTop + TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[2]), font, width / 2);
23001
+ if (chords[3]) renderChord(renderer, left + 3 * width / 4, top + 3 * height / 4 + extraTop + TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[3]), font, width / 2);
23002
+ }
23003
+ function shortenChord(chord) {
23004
+ if (chord === "No Chord") return "N.C.";
23005
+ return chord;
23006
+ }
23007
+ function text(renderer, str, x, y, size, font, dataName, klass, alignCenter) {
23008
+ var attr = {
23009
+ x: x,
23010
+ y: y,
23011
+ stroke: "none",
23012
+ 'font-size': size,
23013
+ 'font-style': font.style,
23014
+ 'font-family': font.face,
23015
+ 'font-weight': font.weight,
23016
+ 'text-decoration': font.decoration
23017
+ };
23018
+ if (dataName) attr['data-name'] = dataName;
23019
+ if (klass) attr['class'] = klass;
23020
+ attr["text-anchor"] = alignCenter ? "middle" : "start";
23021
+ return renderer.paper.text(str, attr, null, {
23022
+ "alignment-baseline": "middle"
23023
+ });
23024
+ }
23025
+ module.exports = drawChordGrid;
23026
+
23027
+ /***/ }),
23028
+
22017
23029
  /***/ "./src/write/draw/crescendo.js":
22018
23030
  /*!*************************************!*\
22019
23031
  !*** ./src/write/draw/crescendo.js ***!
@@ -22097,7 +23109,8 @@ var setPaperSize = __webpack_require__(/*! ./set-paper-size */ "./src/write/draw
22097
23109
  var nonMusic = __webpack_require__(/*! ./non-music */ "./src/write/draw/non-music.js");
22098
23110
  var spacing = __webpack_require__(/*! ../helpers/spacing */ "./src/write/helpers/spacing.js");
22099
23111
  var Selectables = __webpack_require__(/*! ./selectables */ "./src/write/draw/selectables.js");
22100
- function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, selectTypes, tuneNumber, lineOffset) {
23112
+ var drawChordGrid = __webpack_require__(/*! ./chord-grid */ "./src/write/draw/chord-grid.js");
23113
+ function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, selectTypes, tuneNumber, lineOffset, chordGrid) {
22101
23114
  var selectables = new Selectables(renderer.paper, selectTypes, tuneNumber);
22102
23115
  var groupClasses = {};
22103
23116
  if (classes.shouldAddClasses) groupClasses.klass = "abcjs-meta-top";
@@ -22106,43 +23119,52 @@ function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, se
22106
23119
  nonMusic(renderer, abcTune.topText, selectables);
22107
23120
  renderer.paper.closeGroup();
22108
23121
  renderer.moveY(renderer.spacing.music);
23122
+ var suppressMusic = false;
23123
+ if (chordGrid && abcTune.chordGrid) {
23124
+ drawChordGrid(renderer, abcTune.chordGrid, renderer.padding.left, width, abcTune.formatting);
23125
+ if (chordGrid === 'noMusic') suppressMusic = true;
23126
+ }
22109
23127
  var staffgroups = [];
22110
23128
  var nStaves = 0;
22111
- for (var line = 0; line < abcTune.lines.length; line++) {
22112
- classes.incrLine();
22113
- var abcLine = abcTune.lines[line];
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;
23129
+ if (!suppressMusic) {
23130
+ for (var line = 0; line < abcTune.lines.length; line++) {
23131
+ classes.incrLine();
23132
+ var abcLine = abcTune.lines[line];
23133
+ if (abcLine.staff) {
23134
+ // MAE 26 May 2025 - for incipits staff count limiting
23135
+ nStaves++;
23136
+ if (abcTune.formatting.maxStaves) {
23137
+ if (nStaves > abcTune.formatting.maxStaves) {
23138
+ break;
23139
+ }
22120
23140
  }
23141
+ if (classes.shouldAddClasses) groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber;
23142
+ renderer.paper.openGroup(groupClasses);
23143
+ if (abcLine.vskip) {
23144
+ renderer.moveY(abcLine.vskip);
23145
+ }
23146
+ if (staffgroups.length >= 1) addStaffPadding(renderer, renderer.spacing.staffSeparation, staffgroups[staffgroups.length - 1], abcLine.staffGroup);
23147
+ var staffgroup = engraveStaffLine(renderer, abcLine.staffGroup, selectables, line);
23148
+ staffgroup.line = lineOffset + line; // If there are non-music lines then the staffgroup array won't line up with the line array, so this keeps track.
23149
+ staffgroups.push(staffgroup);
23150
+ renderer.paper.closeGroup();
23151
+ } else if (abcLine.nonMusic) {
23152
+ if (classes.shouldAddClasses) groupClasses.klass = "abcjs-non-music";
23153
+ renderer.paper.openGroup(groupClasses);
23154
+ nonMusic(renderer, abcLine.nonMusic, selectables);
23155
+ renderer.paper.closeGroup();
22121
23156
  }
22122
- if (classes.shouldAddClasses) groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber;
22123
- renderer.paper.openGroup(groupClasses);
22124
- if (abcLine.vskip) {
22125
- renderer.moveY(abcLine.vskip);
22126
- }
22127
- if (staffgroups.length >= 1) addStaffPadding(renderer, renderer.spacing.staffSeparation, staffgroups[staffgroups.length - 1], abcLine.staffGroup);
22128
- var staffgroup = engraveStaffLine(renderer, abcLine.staffGroup, selectables, line);
22129
- staffgroup.line = lineOffset + line; // If there are non-music lines then the staffgroup array won't line up with the line array, so this keeps track.
22130
- staffgroups.push(staffgroup);
22131
- renderer.paper.closeGroup();
22132
- } else if (abcLine.nonMusic) {
22133
- if (classes.shouldAddClasses) groupClasses.klass = "abcjs-non-music";
22134
- renderer.paper.openGroup(groupClasses);
22135
- nonMusic(renderer, abcLine.nonMusic, selectables);
22136
- renderer.paper.closeGroup();
22137
23157
  }
22138
23158
  }
22139
23159
  classes.reset();
22140
- if (abcTune.bottomText && abcTune.bottomText.rows && abcTune.bottomText.rows.length > 0) {
22141
- if (classes.shouldAddClasses) groupClasses.klass = "abcjs-meta-bottom";
22142
- renderer.paper.openGroup(groupClasses);
22143
- renderer.moveY(24); // TODO-PER: Empirically discovered. What variable should this be?
22144
- nonMusic(renderer, abcTune.bottomText, selectables);
22145
- renderer.paper.closeGroup();
23160
+ if (!suppressMusic) {
23161
+ if (abcTune.bottomText && abcTune.bottomText.rows && abcTune.bottomText.rows.length > 0) {
23162
+ if (classes.shouldAddClasses) groupClasses.klass = "abcjs-meta-bottom";
23163
+ renderer.paper.openGroup(groupClasses);
23164
+ renderer.moveY(24); // TODO-PER: Empirically discovered. What variable should this be?
23165
+ nonMusic(renderer, abcTune.bottomText, selectables);
23166
+ renderer.paper.closeGroup();
23167
+ }
22146
23168
  }
22147
23169
  setPaperSize(renderer, maxWidth, scale, responsive);
22148
23170
  return {
@@ -23511,7 +24533,7 @@ function renderText(renderer, params, alreadyInGroup) {
23511
24533
 
23512
24534
  // MAE 9 May 2025 for free text blocks
23513
24535
  var text;
23514
- if (params.name == "free-text") {
24536
+ if (params.name === "free-text") {
23515
24537
  text = params.text.replace(/^[ \t]*\n/gm, ' \n');
23516
24538
  } else {
23517
24539
  text = params.text.replace(/\n\n/g, "\n \n");
@@ -23929,6 +24951,7 @@ var EngraverController = function EngraverController(paper, params) {
23929
24951
  if (params.accentAbove) this.accentAbove = params.accentAbove;
23930
24952
  if (params.germanAlphabet) this.germanAlphabet = params.germanAlphabet;
23931
24953
  if (params.lineThickness) this.lineThickness = params.lineThickness;
24954
+ if (params.chordGrid) this.chordGrid = params.chordGrid;
23932
24955
  this.renderer.controller = this; // TODO-GD needed for highlighting
23933
24956
  this.renderer.foregroundColor = params.foregroundColor ? params.foregroundColor : "currentColor";
23934
24957
  if (params.ariaLabel !== undefined) this.renderer.ariaLabel = params.ariaLabel;
@@ -24136,7 +25159,7 @@ EngraverController.prototype.engraveTune = function (abcTune, tuneNumber, lineOf
24136
25159
  }
24137
25160
 
24138
25161
  // Do all the writing to the SVG
24139
- var ret = draw(this.renderer, this.classes, abcTune, this.width, maxWidth, this.responsive, scale, this.selectTypes, tuneNumber, lineOffset);
25162
+ var ret = draw(this.renderer, this.classes, abcTune, this.width, maxWidth, this.responsive, scale, this.selectTypes, tuneNumber, lineOffset, this.chordGrid);
24140
25163
  this.staffgroups = ret.staffgroups;
24141
25164
  this.selectables = ret.selectables;
24142
25165
  if (this.oneSvgPerLine) {
@@ -25188,7 +26211,22 @@ function createAdditionalBeams(elems, asc, beam, isGrace, dy) {
25188
26211
  var auxBeamEndX = x;
25189
26212
  var auxBeamEndY = bary + sy * (j + 1);
25190
26213
  if (auxBeams[j].single) {
25191
- auxBeamEndX = i === 0 ? x + 5 : x - 5;
26214
+ if (i === 0) {
26215
+ // This is the first note in the group, always draw the beam to the right
26216
+ auxBeamEndX = x + 5;
26217
+ } else if (i === elems.length - 1) {
26218
+ // This is the last note in the group, always draw the beam to the left
26219
+ auxBeamEndX = x - 5;
26220
+ } else {
26221
+ // This is a middle note, check the note durations of the notes to the left and right
26222
+ if (elems[i - 1].duration === elems[i + 1].duration) {
26223
+ // The notes on either side are the same duration, alternate which side the beam goes to
26224
+ auxBeamEndX = i % 2 === 0 ? x + 5 : x - 5;
26225
+ } else {
26226
+ // The notes on either side are different durations, draw the beam to the shorter note
26227
+ auxBeamEndX = elems[i - 1].duration > elems[i + 1].duration ? x + 5 : x - 5;
26228
+ }
26229
+ }
25192
26230
  auxBeamEndY = getBarYAt(beam.startX, beam.startY, beam.endX, beam.endY, auxBeamEndX) + sy * (j + 1);
25193
26231
  }
25194
26232
  var b = {
@@ -26380,6 +27418,9 @@ Renderer.prototype.setVerticalSpace = function (formatting) {
26380
27418
  Renderer.prototype.calcY = function (ofs) {
26381
27419
  return this.y - ofs * spacing.STEP;
26382
27420
  };
27421
+ Renderer.prototype.yToPitch = function (ofs) {
27422
+ return ofs / spacing.STEP;
27423
+ };
26383
27424
  Renderer.prototype.moveY = function (em, numLines) {
26384
27425
  if (numLines === undefined) numLines = 1;
26385
27426
  this.y += em * numLines;
@@ -26540,7 +27581,7 @@ Svg.prototype.rectBeneath = function (attr) {
26540
27581
  if (attr['fill-opacity']) el.setAttribute("fill-opacity", attr['fill-opacity']);
26541
27582
  this.svg.insertBefore(el, this.svg.firstChild);
26542
27583
  };
26543
- Svg.prototype.text = function (text, attr, target) {
27584
+ Svg.prototype.text = function (text, attr, target, spanAttr) {
26544
27585
  var el = document.createElementNS(svgNS, 'text');
26545
27586
  el.setAttribute("stroke", "none");
26546
27587
  for (var key in attr) {
@@ -26556,6 +27597,13 @@ Svg.prototype.text = function (text, attr, target) {
26556
27597
  continue;
26557
27598
  }
26558
27599
  var line = document.createElementNS(svgNS, 'tspan');
27600
+ if (spanAttr) {
27601
+ for (var skey in spanAttr) {
27602
+ if (spanAttr.hasOwnProperty(skey)) {
27603
+ line.setAttribute(skey, spanAttr[skey]);
27604
+ }
27605
+ }
27606
+ }
26559
27607
  line.setAttribute("x", attr.x ? attr.x : 0);
26560
27608
  if (i !== 0) line.setAttribute("dy", "1.2em");
26561
27609
  if (lines[i].indexOf("\x03") !== -1) {
@@ -26767,7 +27815,7 @@ module.exports = Svg;
26767
27815
  \********************/
26768
27816
  /***/ (function(module) {
26769
27817
 
26770
- var version = '6.5.1';
27818
+ var version = '6.6.0';
26771
27819
  module.exports = version;
26772
27820
 
26773
27821
  /***/ })