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.
@@ -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
  }
@@ -761,11 +761,11 @@ function cleanUpSlursInLine(line, staffNum, voiceNum, currSlur) {
761
761
  }
762
762
 
763
763
  function wrapMusicLines(lines, barsperstaff) {
764
- for (i = 0; i < lines.length; i++) {
764
+ for (var i = 0; i < lines.length; i++) {
765
765
  if (lines[i].staff !== undefined) {
766
- for (s = 0; s < lines[i].staff.length; s++) {
766
+ for (var s = 0; s < lines[i].staff.length; s++) {
767
767
  var permanentItems = [];
768
- for (v = 0; v < lines[i].staff[s].voices.length; v++) {
768
+ for (var v = 0; v < lines[i].staff[s].voices.length; v++) {
769
769
  var voice = lines[i].staff[s].voices[v];
770
770
  var barNumThisLine = 0;
771
771
  for (var n = 0; n < voice.length; n++) {
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,7 +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*)$/
283
+ //var regSpace = /(\s*)$/
284
+ //var regOptionalSpace = /(\s*)/
280
285
 
281
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
282
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.
@@ -297,51 +302,95 @@ var strTranspose;
297
302
  return { acc: reg[1], name: name, pitch: pos, oct: oct, adj: calcAdjustment(reg[1], keyAccidentals[name], measureAccidentals[name]), courtesy: reg[1] === currentAcc }
298
303
  }
299
304
 
300
- function replaceNote(abc, start, end, newPitch, index) {
301
- // 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.
302
- // This could also be a part of a chord. If so, then the particular note needs to be teased out.
303
- var note = abc.substring(start, end)
304
- var match = note.match(new RegExp(regNote.source + regSpace.source), '')
305
- if (match) {
306
- // This will match a single note
307
- var noteLen = match[1].length
308
- var trailingLen = match[2].length + match[3].length + match[4].length
309
- var leadingLen = end - start - noteLen - trailingLen
310
- start += leadingLen
311
- end -= trailingLen
312
- } else {
313
- // 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.
314
- var regPreBracket = /([^\[]*)/
315
- var regOpenBracket = /\[/
316
- var regCloseBracket = /\-?](\d*\/*\d*)?([\>\<\-\)]*)/
317
- match = note.match(new RegExp(regPreBracket.source + regOpenBracket.source + regOptionalNote.source +
318
- regOptionalNote.source + regOptionalNote.source + regOptionalNote.source +
319
- regOptionalNote.source + regOptionalNote.source + regOptionalNote.source +
320
- regOptionalNote.source + regCloseBracket.source + regSpace.source))
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
307
+ var note = abc.substring(start, end);
321
308
 
322
- if (match) {
323
- // This will match a chord
324
- // Get the number of chars used by the previous notes in this chord
325
- var count = 1 + match[1].length // one character for the open bracket
326
- for (var i = 0; i < index; i++) { // index is the iteration through the chord. This function gets called for each one.
327
- if (match[i * 3 + 2])
328
- count += match[i * 3 + 2].length
329
- if (match[i * 3 + 3])
330
- count += match[i * 3 + 3].length
331
- if (match[i * 3 + 4])
332
- count += match[i * 3 + 4].length
333
- }
334
- start += count
335
- var endLen = match[index * 3 + 2] ? match[index * 3 + 2].length : 0
336
- // endLen += match[index * 3 + 3] ? match[index * 3 + 3].length : 0
337
- // endLen += match[index * 3 + 4] ? match[index * 3 + 4].length : 0
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
+ }
338
320
 
339
- end = start + endLen
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
340
329
  }
330
+ if (!found)
331
+ ret.push({note: array[0], index: start + regPitch.lastIndex-array[0].length})
341
332
  }
342
- return { start: start, end: end, note: newPitch }
333
+
334
+ return ret
343
335
  }
344
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
+
345
394
  function replaceGrace(abc, start, end, newGrace, index) {
346
395
  var note = abc.substring(start, end)
347
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.