abcjs 6.5.2 → 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.
@@ -0,0 +1,364 @@
1
+ // This takes a visual object and returns an object that can
2
+ // be rotely turned into a chord grid.
3
+ //
4
+ // 1) It will always be 8 measures on a line, unless it is a 12 bar blues, then it will be 4 measures.
5
+ // 2) If it is not in 4/4 it will return an error
6
+ // 3) If there are no chords it will return an error
7
+ // 4) It will be divided into parts with the part title and an array of measures
8
+ // 5) |: and :| will be included in a measure
9
+ // 6) If there are first and second endings and the chords are the same, then collapse them
10
+ // 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.
11
+ // 8) If there is one chord per measure and it is repeated in the next measure use a % for the second measure.
12
+ // 9) All lines are the same height, so they are tall enough to fit two lines if there lots of chords
13
+ // 10) Chords will be printed as large as they can without overlapping, so different chords will be smaller if they are long.
14
+ // 11) If there are two chords per measure then there is a slash between them.
15
+ // 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.
16
+ // 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.
17
+ // 14) Ignore pickup notes
18
+ // 15) if a part is not a multiple of 8 bars (and not 12 bars), the last line has
19
+ // 4 squares on left and not any grid on the right.
20
+ // 16) Annotations and some decorations get printed above the cells.
21
+
22
+ function chordGrid(visualObj) {
23
+ const meter = visualObj.getMeterFraction()
24
+ const isCommonTime = meter.num === 4 && meter.den === 4
25
+ const isCutTime = meter.num === 2 && meter.den === 2
26
+ if (!isCutTime && !isCommonTime)
27
+ throw new Error("notCommonTime")
28
+ const deline = visualObj.deline()
29
+
30
+ let chartLines = []
31
+
32
+ let nonSubtitle = false
33
+ deline.forEach(section => {
34
+ if (section.subtitle) {
35
+ if (nonSubtitle) {
36
+ // Don't do the subtitle if the first thing is the subtitle, but that is already printed on the top
37
+ chartLines.push({
38
+ type: "subtitle",
39
+ subtitle: section.subtitle.text
40
+ });
41
+ }
42
+ } else if (section.text) {
43
+ nonSubtitle = true
44
+ chartLines.push({
45
+ type: "text",
46
+ text: section.text.text
47
+ })
48
+ } else if (section.staff) {
49
+ nonSubtitle = true
50
+ // The first staff and the first voice in it drive everything.
51
+ // Only part designations there will count. However, look for
52
+ // chords in any other part. If there is not a chord defined in
53
+ // the first part, use a chord defined in another part.
54
+ const staves = section.staff
55
+ const parts = flattenVoices(staves)
56
+
57
+ chartLines = chartLines.concat(parts)
58
+ }
59
+ })
60
+ collapseIdenticalEndings(chartLines)
61
+ addLineBreaks(chartLines)
62
+ addPercents(chartLines)
63
+ return chartLines
64
+
65
+ }
66
+
67
+ const breakSynonyms = ['break', '(break)', 'no chord', 'n.c.', 'tacet'];
68
+
69
+ function flattenVoices(staves) {
70
+ const parts = []
71
+ let partName = ""
72
+ let measures = []
73
+ let currentBar = {chord: ['', '', '', '']}
74
+ let lastChord = ""
75
+ let nextBarEnding = ""
76
+ staves.forEach((staff, staffNum) => {
77
+ if (staff.voices) {
78
+ staff.voices.forEach((voice, voiceNum) => {
79
+ let currentPartNum = 0
80
+ let beatNum = 0
81
+ let measureNum = 0
82
+ voice.forEach(element => {
83
+ if (element.el_type === 'part') {
84
+ if (measures.length > 0) {
85
+ if (staffNum === 0 && voiceNum === 0) {
86
+ parts.push({
87
+ type: "part",
88
+ name: partName,
89
+ lines: [measures]
90
+ })
91
+ measures = []
92
+ // } else {
93
+ // currentPartNum++
94
+ // measureNum = 0
95
+ // measures = parts[currentPartNum].lines[0]
96
+ }
97
+ }
98
+ partName = element.title
99
+ } else if (element.el_type === 'note') {
100
+ addDecoration(element, currentBar)
101
+ const intBeat = Math.floor(beatNum)
102
+ if (element.chord && element.chord.length > 0) {
103
+ const chord = element.chord[0] // Use just the first chord specified - if there are multiple ones, then ignore them
104
+ const chordName = chord.position === 'default' || breakSynonyms.indexOf(chord.name.toLowerCase()) >= 0 ? chord.name : ''
105
+ if (chordName) {
106
+ if (intBeat > 0 && !currentBar.chord[0]) // Be sure there is a chord for the first beat in a measure
107
+ currentBar.chord[0] = lastChord
108
+ lastChord = chordName
109
+ if (currentBar.chord[intBeat]) {
110
+ // If there is already a chord on this beat put the next chord on the next beat, but don't overwrite anything.
111
+ // This handles the case were a chord is misplaced slightly, for instance it is on the 1/8 before the beat.
112
+ if (intBeat < 4 && !currentBar.chord[intBeat + 1])
113
+ currentBar.chord[intBeat + 1] = chordName
114
+ } else
115
+ currentBar.chord[intBeat] = chordName
116
+ }
117
+ element.chord.forEach(ch => {
118
+ if (ch.position !== 'default' && breakSynonyms.indexOf(chord.name.toLowerCase()) < 0){
119
+ if (!currentBar.annotations)
120
+ currentBar.annotations = []
121
+ currentBar.annotations.push(ch.name)
122
+ }
123
+ })
124
+ }
125
+ if (!element.rest || element.rest.type !== 'spacer') {
126
+ const thisDuration = Math.floor(element.duration * 4)
127
+ if (thisDuration > 4) {
128
+ measureNum += Math.floor(thisDuration / 4)
129
+ beatNum = 0
130
+ } else {
131
+ let thisBeat = element.duration * 4
132
+ if (element.tripletMultiplier)
133
+ thisBeat *= element.tripletMultiplier
134
+ beatNum += thisBeat
135
+ }
136
+ }
137
+ } else if (element.el_type === 'bar') {
138
+ if (nextBarEnding) {
139
+ currentBar.ending = nextBarEnding
140
+ nextBarEnding = ""
141
+ }
142
+ addDecoration(element, currentBar)
143
+ if (element.type === 'bar_dbl_repeat' || element.type === 'bar_left_repeat')
144
+ currentBar.hasStartRepeat = true
145
+ if (element.type === 'bar_dbl_repeat' || element.type === 'bar_right_repeat')
146
+ currentBar.hasEndRepeat = true
147
+ if (element.startEnding)
148
+ nextBarEnding = element.startEnding
149
+ if (beatNum >= 4) {
150
+ if (currentBar.chord[0] === '') {
151
+ // If there isn't a chord change at the beginning, repeat the last chord found
152
+ if (currentBar.chord[1] || currentBar.chord[2] || currentBar.chord[3]) {
153
+ currentBar.chord[0] = findLastChord(measures)
154
+ }
155
+ }
156
+ if (staffNum === 0 && voiceNum === 0)
157
+ measures.push(currentBar)
158
+ else {
159
+ // Add the found items of interest to the original array
160
+ // 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)
161
+ let index = measureNum
162
+ let partIndex = 0
163
+ while (index >= parts[partIndex].lines[0].length && partIndex < parts.length) {
164
+ index -= parts[partIndex].lines[0].length
165
+ partIndex++
166
+ }
167
+ if (partIndex < parts.length && index < parts[partIndex].lines[0].length) {
168
+ const bar = parts[partIndex].lines[0][index]
169
+ if (!bar.chord[0] && currentBar.chord[0])
170
+ bar.chord[0] = currentBar.chord[0]
171
+ if (!bar.chord[1] && currentBar.chord[1])
172
+ bar.chord[1] = currentBar.chord[1]
173
+ if (!bar.chord[2] && currentBar.chord[2])
174
+ bar.chord[2] = currentBar.chord[2]
175
+ if (!bar.chord[3] && currentBar.chord[3])
176
+ bar.chord[3] = currentBar.chord[3]
177
+ if (currentBar.annotations) {
178
+ if (!bar.annotations)
179
+ bar.annotations = currentBar.annotations
180
+ else
181
+ bar.annotations = bar.annotations.concat(currentBar.annotations)
182
+ }
183
+ }
184
+ measureNum++
185
+ }
186
+ currentBar = {chord: ['', '', '', '']}
187
+ } else
188
+ currentBar.chord = ['', '', '', '']
189
+ beatNum = 0
190
+ } else if (element.el_type === 'tempo') {
191
+ // TODO-PER: should probably report tempo, too
192
+ }
193
+ })
194
+ if (staffNum === 0 && voiceNum === 0) {
195
+ parts.push({
196
+ type: "part",
197
+ name: partName,
198
+ lines: [measures]
199
+ })
200
+ }
201
+ })
202
+ }
203
+ })
204
+ if (!lastChord)
205
+ throw new Error("noChords")
206
+ return parts
207
+ }
208
+
209
+ function findLastChord(measures) {
210
+ for (let m = measures.length-1; m >= 0; m--) {
211
+ for (let c = measures[m].chord.length-1; c >= 0; c--) {
212
+ if (measures[m].chord[c])
213
+ return measures[m].chord[c]
214
+ }
215
+ }
216
+ }
217
+
218
+ function collapseIdenticalEndings(chartLines) {
219
+ chartLines.forEach(line => {
220
+ if (line.type === "part") {
221
+ const partLine = line.lines[0]
222
+ const ending1 = partLine.findIndex(bar => {
223
+ return !!bar.ending
224
+ })
225
+ const ending2 = partLine.findIndex((bar, index) => {
226
+ return index > ending1 && !!bar.ending
227
+ })
228
+ if (ending1 >= 0 && ending2 >= 0) {
229
+ // If the endings are not the same length, don't collapse
230
+ if (ending2 - ending1 === partLine.length - ending2) {
231
+ let matches = true
232
+ for (let i = 0; i < ending2 - ending1 && matches; i++) {
233
+ const measureLhs = partLine[ending1+i]
234
+ const measureRhs = partLine[ending2+i]
235
+ if (measureLhs.chord[0] !== measureRhs.chord[0])
236
+ matches = false
237
+ if (measureLhs.chord[1] !== measureRhs.chord[1])
238
+ matches = false
239
+ if (measureLhs.chord[2] !== measureRhs.chord[2])
240
+ matches = false
241
+ if (measureLhs.chord[3] !== measureRhs.chord[3])
242
+ matches = false
243
+ if (measureLhs.annotations && !measureRhs.annotations)
244
+ matches = false
245
+ if (!measureLhs.annotations && measureRhs.annotations)
246
+ matches = false
247
+ if (measureLhs.annotations && measureRhs.annotations) {
248
+ if (measureLhs.annotations.length !== measureRhs.annotations.length)
249
+ matches = false
250
+ else {
251
+ for (let j = 0; j < measureLhs.annotations.length; j++) {
252
+ if (measureLhs.annotations[j] !== measureRhs.annotations[j])
253
+ matches = false
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (matches) {
259
+ delete partLine[ending1].ending
260
+ partLine.splice(ending2, partLine.length - ending2)
261
+ }
262
+ }
263
+ }
264
+ }
265
+ })
266
+ }
267
+
268
+ function addLineBreaks(chartLines) {
269
+ chartLines.forEach(line => {
270
+ if (line.type === "part") {
271
+ const newLines = []
272
+ const oldLines = line.lines[0]
273
+ let is12bar = false
274
+ const firstEndRepeat = oldLines.findIndex(l => {
275
+ return !!l.hasEndRepeat
276
+ })
277
+ const length = firstEndRepeat >= 0 ? Math.min(firstEndRepeat+1,oldLines.length) : oldLines.length
278
+ if (length === 12)
279
+ is12bar = true
280
+ const barsPerLine = is12bar ? 4 : 8 // Only do 4 bars per line for 12-bar blues
281
+ for (let i = 0; i < oldLines.length; i += barsPerLine) {
282
+ const newLine = oldLines.slice(i, i + barsPerLine)
283
+ const endRepeat = newLine.findIndex(l => {
284
+ return !!l.hasEndRepeat
285
+ })
286
+ if (endRepeat >= 0 && endRepeat < newLine.length-1) {
287
+ newLines.push(newLine.slice(0, endRepeat+1))
288
+ newLines.push(newLine.slice(endRepeat+1))
289
+ } else
290
+ newLines.push(newLine)
291
+ }
292
+ // TODO-PER: The following probably doesn't handle all cases. Rethink it.
293
+ for (let i = 0; i < newLines.length; i++) {
294
+ if (newLines[i][0].ending) {
295
+ const prevLine = Math.max(0, i-1)
296
+ const toAdd = newLines[prevLine].length - newLines[i].length
297
+ const thisLine = []
298
+ for (let j = 0; j < toAdd; j++)
299
+ thisLine.push({noBorder: true, chord: ['', '', '', '']})
300
+ newLines[i] = thisLine.concat(newLines[i])
301
+ }
302
+ }
303
+ line.lines = newLines
304
+ }
305
+ })
306
+ }
307
+
308
+ function addPercents(chartLines) {
309
+ chartLines.forEach(part => {
310
+ if (part.lines) {
311
+ let lastMeasureSingle = false
312
+ let lastChord = ""
313
+ part.lines.forEach(line => {
314
+ line.forEach(measure => {
315
+ if (!measure.noBorder) {
316
+ const chords = measure.chord
317
+ if (!chords[0] && !chords[1] && !chords[2] && !chords[3]) {
318
+ // if there are no chords specified for this measure
319
+ if (lastMeasureSingle) {
320
+ if (lastChord)
321
+ chords[0] = '%'
322
+ } else
323
+ chords[0] = lastChord
324
+ lastMeasureSingle = true
325
+ } else if (!chords[1] && !chords[2] && !chords[3]) {
326
+ // if there is a single chord for this measure
327
+ lastMeasureSingle = true
328
+ lastChord = chords[0]
329
+ } else {
330
+ // if the measure is complicated - in that case the next measure won't get %
331
+ lastMeasureSingle = false
332
+ lastChord = chords[3] || chords[2] || chords[1]
333
+ }
334
+ }
335
+ })
336
+ })
337
+ }
338
+ })
339
+ }
340
+
341
+ function addDecoration(element, currentBar) {
342
+ if (element.decoration) {
343
+ // Some decorations are interesting to rhythm players
344
+ for (let i = 0; i < element.decoration.length; i++) {
345
+ switch (element.decoration[i]) {
346
+ case 'fermata':
347
+ case 'segno':
348
+ case 'coda':
349
+ case "D.C.":
350
+ case "D.S.":
351
+ case "D.C.alcoda":
352
+ case "D.C.alfine":
353
+ case "D.S.alcoda":
354
+ case "D.S.alfine":
355
+ case "fine":
356
+ if (!currentBar.annotations)
357
+ currentBar.annotations = []
358
+ currentBar.annotations.push(element.decoration[i])
359
+ break;
360
+ }
361
+ }
362
+ }
363
+ }
364
+ module.exports = chordGrid
@@ -549,7 +549,7 @@ function resolveOverlays(tune) {
549
549
  } else if (event.el_type === "note") {
550
550
  if (inOverlay) {
551
551
  overlayVoice[k].voice.push(event);
552
- } else {
552
+ } else if (!event.rest || event.rest.type !== 'spacer') {
553
553
  durationThisBar += event.duration;
554
554
  durationsPerLines[i] += event.duration;
555
555
  }
package/src/str/output.js CHANGED
@@ -1,5 +1,5 @@
1
1
  var keyAccidentals = require("../const/key-accidentals");
2
- var { relativeMajor, transposeKey, relativeMode } = require("../const/relative-major");
2
+ var { relativeMajor, transposeKey, relativeMode, isLegalMode } = require("../const/relative-major");
3
3
  var transposeChordName = require("../parse/transpose-chord")
4
4
 
5
5
  var strTranspose;
@@ -61,8 +61,9 @@ var strTranspose;
61
61
  var match = segment.match(/^( *)([A-G])([#b]?)( ?)(\w*)/)
62
62
  if (match) {
63
63
  var start = count + 2 + match[1].length // move past the 'K:' and optional white space
64
- var key = match[2] + match[3] + match[4] + match[5] // key name, accidental, optional space, and mode
65
- var destinationKey = newKey({ root: match[2], acc: match[3], mode: match[5] }, steps)
64
+ var mode = isLegalMode(match[5]) ? match[5]: ''
65
+ var key = match[2] + match[3] + match[4] + mode // key name, accidental, optional space, and mode
66
+ var destinationKey = newKey({ root: match[2], acc: match[3], mode: mode }, steps)
66
67
  var dest = destinationKey.root + destinationKey.acc + match[4] + destinationKey.mode
67
68
  changes.push({ start: start, end: start + key.length, note: dest })
68
69
  }
@@ -136,14 +137,16 @@ var strTranspose;
136
137
  }
137
138
  }
138
139
  if (el.el_type === 'note' && el.pitches) {
139
- for (var j = 0; j < el.pitches.length; j++) {
140
- var note = parseNote(el.pitches[j].name, keyRoot, keyAccidentals, measureAccidentals)
140
+ var pitchArray = findNotes(abc,el.startChar, el.endChar)
141
+ //console.log(pitchArray)
142
+ for (var j = 0; j < pitchArray.length; j++) {
143
+ var note = parseNote(pitchArray[j].note, keyRoot, keyAccidentals, measureAccidentals)
141
144
  if (note.acc)
142
145
  measureAccidentals[note.name.toUpperCase()] = note.acc
143
146
  var newPitch = transposePitch(note, destinationKey, letterDistance, transposedMeasureAccidentals)
144
147
  if (newPitch.acc)
145
148
  transposedMeasureAccidentals[newPitch.upper] = newPitch.acc
146
- changes.push(replaceNote(abc, el.startChar, el.endChar, newPitch.acc + newPitch.name, j))
149
+ changes.push({note:newPitch.acc+newPitch.name, start: pitchArray[j].index, end: pitchArray[j].index+pitchArray[j].note.length})
147
150
  }
148
151
  if (el.gracenotes) {
149
152
  for (var g = 0; g < el.gracenotes.length; g++) {
@@ -216,6 +219,7 @@ var strTranspose;
216
219
  break;
217
220
  }
218
221
  }
222
+ var newNote
219
223
  switch (adj) {
220
224
  case -2: acc = "__"; break;
221
225
  case -1: acc = "_"; break;
@@ -224,7 +228,7 @@ var strTranspose;
224
228
  case 2: acc = "^^"; break;
225
229
  case -3:
226
230
  // This requires a triple flat, so bump down the pitch and try again
227
- var newNote = {}
231
+ newNote = {}
228
232
  newNote.pitch = note.pitch - 1
229
233
  newNote.oct = note.oct
230
234
  newNote.name = letters[letters.indexOf(note.name) - 1]
@@ -239,7 +243,7 @@ var strTranspose;
239
243
  return transposePitch(newNote, key, letterDistance + 1, measureAccidentals)
240
244
  case 3:
241
245
  // This requires a triple sharp, so bump up the pitch and try again
242
- var newNote = {}
246
+ newNote = {}
243
247
  newNote.pitch = note.pitch + 1
244
248
  newNote.oct = note.oct
245
249
  newNote.name = letters[letters.indexOf(note.name) + 1]
@@ -276,8 +280,8 @@ var strTranspose;
276
280
  var regPitch = /([_^=]*)([A-Ga-g])([,']*)/
277
281
  var regNote = /([_^=]*[A-Ga-g][,']*)(\d*\/*\d*)([\>\<\-\)\.\s\\]*)/
278
282
  var regOptionalNote = /([_^=]*[A-Ga-g][,']*)?(\d*\/*\d*)?([\>\<\-\)]*)?/
279
- var regSpace = /(\s*)$/
280
- var regOptionalSpace = /(\s*)/
283
+ //var regSpace = /(\s*)$/
284
+ //var regOptionalSpace = /(\s*)/
281
285
 
282
286
  // 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
283
287
  // 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.
@@ -298,63 +302,95 @@ var strTranspose;
298
302
  return { acc: reg[1], name: name, pitch: pos, oct: oct, adj: calcAdjustment(reg[1], keyAccidentals[name], measureAccidentals[name]), courtesy: reg[1] === currentAcc }
299
303
  }
300
304
 
301
- function replaceNote(abc, start, end, newPitch, index) {
305
+ function findNotes(abc, start, end) {
306
+ // 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
302
307
  var note = abc.substring(start, end);
303
- // Try single note first
304
- var match = note.match(new RegExp(regNote.source + regSpace.source));
305
- if (match) {
306
- var noteLen = match[1].length;
307
- var trailingLen = match[2].length + match[3].length + match[4].length;
308
- var leadingLen = end - start - noteLen - trailingLen;
309
- start += leadingLen;
310
- end -= trailingLen;
311
- } else {
312
- // Match chord
313
- var regPreBracket = /([^\[]*)/;
314
- var regOpenBracket = /\[/;
315
- var regCloseBracket = /\-?](\d*\/*\d*)?([\>\<\-\)]*)/;
316
- var regChord = new RegExp(
317
- regPreBracket.source +
318
- regOpenBracket.source +
319
- "(?:" + regOptionalNote.source + "\\s*){1,8}" +
320
- regCloseBracket.source +
321
- regSpace.source
322
- );
323
- match = note.match(regChord);
324
- if (match) {
325
- var beforeChordLen = match[1].length + 1; // text before + '['
326
- var chordBody = note.slice(match[1].length + 1, note.lastIndexOf("]"));
327
- // Collect notes inside chord
328
- var chordNotes = [];
329
- var regNoteWithSpace = new RegExp(regOptionalNote.source + "\\s*", "g");
330
- for (const m of chordBody.matchAll(regNoteWithSpace)) {
331
- let noteText = m[0].trim();
332
- if (noteText !== "") {
333
- chordNotes.push({ text: noteText, index: m.index });
334
- }
335
- }
336
- if (index >= chordNotes.length) {
337
- throw new Error("Chord index out of range for chord: " + note);
338
- }
339
- var chosen = chordNotes[index];
340
- // Preserve duration and tie
341
- let mDurTie = chosen.text.match(/^(.+?)(\d+\/?\d*)?(-)?$/);
342
- let pitchPart = mDurTie ? mDurTie[1] : chosen.text;
343
- let durationPart = mDurTie && mDurTie[2] ? mDurTie[2] : "";
344
- let tiePart = mDurTie && mDurTie[3] ? mDurTie[3] : "";
345
- // Replace note keeping duration and tie
346
- newPitch = newPitch + durationPart + tiePart;
347
- start += beforeChordLen + chosen.index;
348
- end = start + chosen.text.length;
308
+
309
+ // Since the regex will also find "c", "d", and "a" in `!coda!`, we need to filter them
310
+ var array
311
+ var ignoreBlocks = []
312
+ var regChord = /("[^"]+")+/g
313
+ while ((array = regChord.exec(note)) !== null) {
314
+ ignoreBlocks.push({start: regChord.lastIndex-array[0].length, end: regChord.lastIndex})
315
+ }
316
+ var regDec = /(![^!]+!)+/g
317
+ while ((array = regDec.exec(note)) !== null) {
318
+ ignoreBlocks.push({start: regDec.lastIndex-array[0].length, end: regDec.lastIndex})
319
+ }
320
+
321
+ var ret = []
322
+ // Define the regex each time because it is stateful
323
+ var regPitch = /([_^=]*)([A-Ga-g])([,']*)/g
324
+ while ((array = regPitch.exec(note)) !== null) {
325
+ var found = false
326
+ for (var i = 0; i < ignoreBlocks.length; i++) {
327
+ if (regPitch.lastIndex >= ignoreBlocks[i].start && regPitch.lastIndex <= ignoreBlocks[i].end)
328
+ found = true
349
329
  }
330
+ if (!found)
331
+ ret.push({note: array[0], index: start + regPitch.lastIndex-array[0].length})
350
332
  }
351
- return {
352
- start: start,
353
- end: end,
354
- note: newPitch
355
- };
333
+
334
+ return ret
356
335
  }
357
336
 
337
+ // function replaceNote(abc, start, end, newPitch, oldPitch, index) {
338
+ // var note = abc.substring(start, end);
339
+ // // Try single note first
340
+ // var match = note.match(new RegExp(regNote.source + regSpace.source));
341
+ // if (match) {
342
+ // var noteLen = match[1].length;
343
+ // var trailingLen = match[2].length + match[3].length + match[4].length;
344
+ // var leadingLen = end - start - noteLen - trailingLen;
345
+ // start += leadingLen;
346
+ // end -= trailingLen;
347
+ // } else {
348
+ // // Match chord
349
+ // var regPreBracket = /([^\[]*)/;
350
+ // var regOpenBracket = /\[/;
351
+ // var regCloseBracket = /\-?](\d*\/*\d*)?([\>\<\-\)]*)/;
352
+ // var regChord = new RegExp(
353
+ // regPreBracket.source +
354
+ // regOpenBracket.source +
355
+ // "(?:" + regOptionalNote.source + "\\s*){1,8}" +
356
+ // regCloseBracket.source +
357
+ // regSpace.source
358
+ // );
359
+ // match = note.match(regChord);
360
+ // if (match) {
361
+ // var beforeChordLen = match[1].length + 1; // text before + '['
362
+ // var chordBody = note.slice(match[1].length + 1, note.lastIndexOf("]"));
363
+ // // Collect notes inside chord
364
+ // var chordNotes = [];
365
+ // var regNoteWithSpace = new RegExp(regOptionalNote.source + "\\s*", "g");
366
+ // for (const m of chordBody.matchAll(regNoteWithSpace)) {
367
+ // let noteText = m[0].trim();
368
+ // if (noteText !== "") {
369
+ // chordNotes.push({ text: noteText, index: m.index });
370
+ // }
371
+ // }
372
+ // if (index >= chordNotes.length) {
373
+ // throw new Error("Chord index out of range for chord: " + note);
374
+ // }
375
+ // var chosen = chordNotes[index];
376
+ // // Preserve duration and tie
377
+ // let mDurTie = chosen.text.match(/^(.+?)(\d+\/?\d*)?(-)?$/);
378
+ // let pitchPart = mDurTie ? mDurTie[1] : chosen.text;
379
+ // let durationPart = mDurTie && mDurTie[2] ? mDurTie[2] : "";
380
+ // let tiePart = mDurTie && mDurTie[3] ? mDurTie[3] : "";
381
+ // // Replace note keeping duration and tie
382
+ // newPitch = newPitch + durationPart + tiePart;
383
+ // start += beforeChordLen + chosen.index;
384
+ // end = start + chosen.text.length;
385
+ // }
386
+ // }
387
+ // return {
388
+ // start: start,
389
+ // end: end,
390
+ // note: newPitch
391
+ // };
392
+ // }
393
+
358
394
  function replaceGrace(abc, start, end, newGrace, index) {
359
395
  var note = abc.substring(start, end)
360
396
  // 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.