@goplayerjuggler/abc-tools 1.0.9 → 1.0.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplayerjuggler/abc-tools",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "sorting algorithm and implementation for ABC tunes; plus other tools for parsing and manipulating ABC tunes",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/incipit.js CHANGED
@@ -3,8 +3,7 @@ const { Fraction } = require("./math.js");
3
3
  const { getFirstBars } = require("./manipulator.js");
4
4
 
5
5
  const { getUnitLength, getMeter } = require("./parse/parser.js");
6
-
7
- const { getContour } = require("./sort/contour-sort.js");
6
+ const { getContour } = require("./sort/get-contour.js");
8
7
 
9
8
  //this file has code that's a fork of some code in Michael Eskin's abctools
10
9
 
@@ -307,20 +306,33 @@ function getIncipit(data) {
307
306
  return getFirstBars(abc, numBars, withAnacrucis, false, { all: true });
308
307
  }
309
308
 
310
- function getIncipitForContourGeneration(abc) {
309
+ function getIncipitForContourGeneration(
310
+ abc,
311
+ { numBars = new Fraction(3, 2) } = {}
312
+ ) {
311
313
  return getIncipit({
312
314
  abc,
313
315
  withAnacrucis: false,
314
- numBars: 1,
316
+ numBars,
315
317
  });
316
318
  }
317
319
 
318
- function getContourFromFullAbc(abc) {
320
+ function getContourFromFullAbc(
321
+ abc,
322
+ {
323
+ withSvg = true,
324
+ withSwingTransform = false,
325
+ numBars = new Fraction(3, 2),
326
+ } = {}
327
+ ) {
319
328
  if (Array.isArray(abc)) {
320
329
  if (abc.length === 0) return null;
321
330
  abc = abc[0];
322
331
  }
323
- return getContour(getIncipitForContourGeneration(abc), { withSvg: true });
332
+ return getContour(getIncipitForContourGeneration(abc, { numBars }), {
333
+ withSvg,
334
+ withSwingTransform,
335
+ });
324
336
  }
325
337
 
326
338
  module.exports = {
package/src/index.js CHANGED
@@ -11,6 +11,7 @@ const displayContour = require("./sort/display-contour.js");
11
11
 
12
12
  const incipit = require("./incipit.js");
13
13
  const javascriptify = require("./javascriptify.js");
14
+ const getContour = require("./sort/get-contour.js");
14
15
 
15
16
  module.exports = {
16
17
  // Parser functions
@@ -23,6 +24,7 @@ module.exports = {
23
24
  ...sort,
24
25
  ...displayContour,
25
26
  ...contourToSvg,
27
+ ...getContour,
26
28
 
27
29
  // Incipit functions
28
30
  ...incipit,
@@ -5,6 +5,8 @@ const {
5
5
  calculateBarDurations,
6
6
  } = require("./parse/parser.js");
7
7
 
8
+ const { getBarInfo } = require("./parse/getBarInfo.js");
9
+
8
10
  // ============================================================================
9
11
  // ABC manipulation functions
10
12
  // ============================================================================
@@ -43,14 +45,14 @@ function normaliseKey(keyHeader) {
43
45
  major: "major",
44
46
  ion: "major",
45
47
  ionian: "major",
48
+ mix: "mixolydian",
49
+ mixo: "mixolydian",
50
+ mixolydian: "mixolydian",
46
51
  m: "minor",
47
52
  min: "minor",
48
53
  minor: "minor",
49
54
  aeo: "minor",
50
55
  aeolian: "minor",
51
- mix: "mixolydian",
52
- mixo: "mixolydian",
53
- mixolydian: "mixolydian",
54
56
  dor: "dorian",
55
57
  dorian: "dorian",
56
58
  phr: "phrygian",
@@ -116,58 +118,31 @@ function hasAnacrucis(abc) {
116
118
  const parsed = parseAbc(abc, { maxBars: 2 });
117
119
  return hasAnacrucisFromParsed(parsed);
118
120
  }
119
- /**
120
- * Inserts a specified character at multiple positions within a string.
121
- * Optimised for performance with long strings and repeated usage.
122
- *
123
- * @param {string} originalString - The original string to modify.
124
- * @param {string} charToInsert - The character to insert at the specified positions.
125
- * @param {number[]} indexes - An array of positions (zero-based) where the character should be inserted.
126
- * @returns {string} The modified string with characters inserted at the specified positions.
127
- *
128
- * // Example usage:
129
- * const originalString = "hello world";
130
- * const charToInsert = "!";
131
- * const indexes = [2, 5, 8, 2, 15];
132
- * const result = insertCharsAtIndexes(originalString, charToInsert, indexes);
133
- * console.log(result); // Output: "he!l!lo! world!"
134
- *
135
- */
136
- function insertCharsAtIndexes(originalString, charToInsert, indexes) {
137
- // Filter and sort indexes only once: remove duplicates and invalid positions
138
- const validIndexes = [...new Set(indexes)]
139
- .filter((index) => index >= 0 && index <= originalString.length)
140
- .sort((a, b) => a - b);
141
-
142
- const result = [];
143
- let prevIndex = 0;
144
-
145
- for (const index of validIndexes) {
146
- // Push the substring up to the current index
147
- result.push(originalString.slice(prevIndex, index));
148
- // Push the character to insert
149
- result.push(charToInsert);
150
- // Update the previous index
151
- prevIndex = index;
152
- }
153
-
154
- // Push the remaining part of the string
155
- result.push(originalString.slice(prevIndex));
156
-
157
- return result.join("");
158
- }
159
121
 
160
122
  /**
161
123
  * Toggle meter by doubling or halving bar length
162
124
  * Supports 4/4↔4/2 and 6/8↔12/8 transformations
163
- * This is nearly a true inverse operation - going there and back preserves the ABC except for some
164
- * edge cases involving spaces around the bar lines. No need to handle them.
165
- * Handles anacrusis correctly and preserves line breaks
166
125
  *
167
- * @param {string} abc - ABC notation
168
- * @param {Array<number>} smallMeter - The smaller meter signature [num, den]
169
- * @param {Array<number>} largeMeter - The larger meter signature [num, den]
170
- * @returns {string} - ABC with toggled meter
126
+ * When going from small to large meters (e.g., 4/4→4/2):
127
+ * - Removes alternate bar lines to combine pairs of bars
128
+ * - Converts variant ending markers from |1, |2 to [1, [2 format
129
+ * - Respects section breaks (||, :|, etc.) and resets pairing after them
130
+ * - Handles bars starting with variant endings by keeping the bar line after them
131
+ *
132
+ * When going from large to small meters (e.g., 4/2→4/4):
133
+ * - Inserts bar lines at halfway points within each bar
134
+ * - Preserves variant ending markers in [1, [2 format (does not convert back to |1, |2)
135
+ * - Inserts bar lines before variant endings when they occur at the split point
136
+ *
137
+ * This is nearly a true inverse operation - going there and back preserves musical content
138
+ * but may change spacing around bar lines and normalises variant ending syntax to [1, [2 format.
139
+ * Correctly handles anacrusis (pickup bars), multi-bar variant endings, partial bars, and preserves line breaks.
140
+ *
141
+ * @param {string} abc - ABC notation string
142
+ * @param {Array<number>} smallMeter - The smaller meter signature [numerator, denominator]
143
+ * @param {Array<number>} largeMeter - The larger meter signature [numerator, denominator]
144
+ * @returns {string} ABC notation with toggled meter
145
+ * @throws {Error} If the current meter doesn't match either smallMeter or largeMeter
171
146
  */
172
147
  function toggleMeterDoubling(abc, smallMeter, largeMeter) {
173
148
  const currentMeter = getMeter(abc);
@@ -183,14 +158,8 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
183
158
  );
184
159
  }
185
160
 
186
- if (isSmall) {
187
- // We're going to remove some bars, so ensure every bar line (pipe / `|`) has a space preceding it
188
- // Regex handles bars like :| and [|]
189
- abc = abc.replaceAll(/([^\s])([[:]?\|)/g, "$1 $2");
190
- }
191
-
192
161
  const parsed = parseAbc(abc);
193
- const { headerLines, barLines, musicText } = parsed;
162
+ const { headerLines, barLines, musicText, bars, meter } = parsed;
194
163
 
195
164
  // Change meter in headers
196
165
  const newHeaders = headerLines.map((line) => {
@@ -202,80 +171,174 @@ function toggleMeterDoubling(abc, smallMeter, largeMeter) {
202
171
  return line;
203
172
  });
204
173
 
205
- const hasPickup = hasAnacrucisFromParsed(parsed);
206
-
207
174
  if (isSmall) {
208
- // Going from small to large: remove every other bar line (except final)
209
- const barLinesToRemove = new Set();
210
- const startIndex = hasPickup ? 1 : 0;
211
-
212
- for (let i = startIndex; i < barLines.length - 1; i += 2) {
213
- barLinesToRemove.add(barLines[i].sourceIndex);
175
+ // Going from small to large: remove every other bar line
176
+ // Get bar info with barNumbers to understand the musical structure
177
+ getBarInfo(bars, barLines, meter, {
178
+ barNumbers: true,
179
+ isPartial: true,
180
+ });
181
+
182
+ // Build a map of which bars start with variant endings
183
+ const barStartsWithVariant = new Map();
184
+ for (let i = 0; i < bars.length; i++) {
185
+ if (bars[i].length > 0 && bars[i][0].isVariantEnding) {
186
+ barStartsWithVariant.set(i, bars[i][0]);
187
+ }
214
188
  }
215
189
 
216
- // Reconstruct music by removing marked bar lines
217
- let newMusic = "";
218
- let lastPos = 0;
190
+ // Determine which bar lines to keep or remove
191
+ const barLineDecisions = new Map(); // barLineIndex -> {action, variantToken?}
192
+ let barPosition = 0; // Position within current pairing (0 or 1)
193
+ let barNumberOfStartOfSection = 0;
219
194
 
220
195
  for (let i = 0; i < barLines.length; i++) {
221
196
  const barLine = barLines[i];
222
- newMusic += musicText.substring(lastPos, barLine.sourceIndex);
223
197
 
224
- if (!barLinesToRemove.has(barLine.sourceIndex)) {
225
- lastPos = barLine.sourceIndex;
198
+ // Initial bar line is always kept
199
+ if (barLine.barNumber === null) {
200
+ barLineDecisions.set(i, { action: "keep" });
201
+ continue;
202
+ }
203
+
204
+ // initial anacrucis bar line of section is always kept
205
+ if (
206
+ barLine.barNumber === barNumberOfStartOfSection &&
207
+ barLine.isPartial
208
+ ) {
209
+ barLineDecisions.set(i, { action: "keep" });
210
+ continue;
211
+ }
212
+
213
+ // Final bar line is always kept
214
+ if (i === barLines.length - 1) {
215
+ barLineDecisions.set(i, { action: "keep" });
216
+ continue;
217
+ }
218
+
219
+ // Section breaks are always kept and reset pairing
220
+ if (barLine.isSectionBreak) {
221
+ barLineDecisions.set(i, { action: "keep" });
222
+ barPosition = 0;
223
+ barNumberOfStartOfSection = barLine.barNumber;
224
+ continue;
225
+ }
226
+
227
+ // barLines[i] comes after bars[i], so the NEXT bar is bars[i+1]
228
+ const nextBarIdx = i + 1;
229
+ const nextBarVariant =
230
+ nextBarIdx < bars.length ? barStartsWithVariant.get(nextBarIdx) : null;
231
+
232
+ // If the current bar (bars[i]) starts with a variant, keep its bar line and reset position
233
+ const currentBarVariant = barStartsWithVariant.get(i);
234
+ if (currentBarVariant) {
235
+ barLineDecisions.set(i, { action: "keep" });
236
+ barPosition = 0;
237
+ continue;
238
+ }
239
+
240
+ // Normal pairing logic
241
+ if (barPosition === 0) {
242
+ // First of pair - remove
243
+ if (nextBarVariant) {
244
+ barLineDecisions.set(i, {
245
+ action: "remove",
246
+ variantToken: nextBarVariant,
247
+ });
248
+ } else {
249
+ barLineDecisions.set(i, { action: "remove" });
250
+ }
251
+ barPosition = 1;
226
252
  } else {
227
- // Remove the bar line - skip over it
228
- lastPos = barLine.sourceIndex + barLine.sourceLength;
253
+ // Second of pair - keep
254
+ barLineDecisions.set(i, { action: "keep" });
255
+ barPosition = 0;
229
256
  }
230
257
  }
231
- newMusic += musicText.substring(lastPos);
232
258
 
233
- return `${newHeaders.join("\n")}\n${newMusic}`;
234
- } else {
235
- // Going from large to small: add bar line in middle of each bar
236
- const halfBarDuration = new Fraction(smallMeter[0], smallMeter[1]);
237
- const insertionPoints = [];
238
- const startBarIndex = hasPickup ? 1 : 0;
239
-
240
- for (let barIdx = startBarIndex; barIdx < parsed.bars.length; barIdx++) {
241
- const bar = parsed.bars[barIdx];
242
- let barDuration = new Fraction(0, 1);
243
- let insertPos = null;
244
-
245
- // Find position where we've accumulated half a bar
246
- for (let noteIdx = 0; noteIdx < bar.length; noteIdx++) {
247
- const token = bar[noteIdx];
248
-
249
- // Skip tokens with no duration
250
- if (!token.duration) {
251
- continue;
252
- }
259
+ // Track variant replacements
260
+ const variantReplacements = new Map();
261
+ for (const [, decision] of barLineDecisions) {
262
+ if (decision.action === "remove" && decision.variantToken) {
263
+ const token = decision.variantToken;
264
+ const newToken = token.token.replace(/^\|/, "[");
265
+ variantReplacements.set(token.sourceIndex, {
266
+ oldLength: token.sourceLength,
267
+ newText: " " + newToken,
268
+ });
269
+ }
270
+ }
271
+
272
+ // Reconstruct music
273
+ let newMusic = "";
274
+ let pos = 0;
275
+
276
+ while (pos < musicText.length) {
277
+ // Check for variant replacement
278
+ if (variantReplacements.has(pos)) {
279
+ const replacement = variantReplacements.get(pos);
280
+ newMusic += replacement.newText;
281
+ pos += replacement.oldLength;
282
+ continue;
283
+ }
253
284
 
254
- const prevDuration = barDuration.clone();
255
- barDuration = barDuration.add(token.duration);
285
+ // Check for bar line
286
+ const barLineIdx = barLines.findIndex((bl) => bl.sourceIndex === pos);
287
+ if (barLineIdx >= 0) {
288
+ const decision = barLineDecisions.get(barLineIdx);
289
+ const barLine = barLines[barLineIdx];
256
290
 
257
- // Check if we've just crossed the halfway point
258
291
  if (
259
- prevDuration.compare(halfBarDuration) < 0 &&
260
- barDuration.compare(halfBarDuration) >= 0
292
+ decision &&
293
+ decision.action === "remove" &&
294
+ !decision.variantToken
261
295
  ) {
262
- // Insert bar line after this note
263
- insertPos = token.sourceIndex + token.sourceLength;
264
- // Skip any trailing space that's part of this note
265
- if (token.spacing && token.spacing.whitespace) {
266
- insertPos += token.spacing.whitespace.length;
296
+ // Remove bar line and ensure there's a space
297
+ // Check if we already added a space (last char in newMusic)
298
+ const needsSpace =
299
+ newMusic.length === 0 || newMusic[newMusic.length - 1] !== " ";
300
+ if (needsSpace) {
301
+ newMusic += " ";
267
302
  }
268
- break;
303
+ let skipLength = barLine.sourceLength;
304
+ // Skip any trailing space after the bar line to avoid double spaces
305
+ if (
306
+ pos + skipLength < musicText.length &&
307
+ musicText[pos + skipLength] === " "
308
+ ) {
309
+ skipLength++;
310
+ }
311
+ pos += skipLength;
312
+ continue;
313
+ } else if (decision && decision.action === "keep") {
314
+ // Keep this bar line
315
+ newMusic += musicText.substring(pos, pos + barLine.sourceLength);
316
+ pos += barLine.sourceLength;
317
+ continue;
269
318
  }
270
319
  }
271
320
 
272
- if (insertPos !== null) {
273
- insertionPoints.push(insertPos);
274
- }
321
+ // Regular character
322
+ newMusic += musicText[pos];
323
+ pos++;
275
324
  }
276
325
 
326
+ return `${newHeaders.join("\n")}\n${newMusic}`;
327
+ } else {
328
+ // Going from large to small: add bar lines at midpoints
329
+ const barInfo = getBarInfo(bars, barLines, meter, {
330
+ divideBarsBy: 2,
331
+ });
332
+
333
+ const { midpoints } = barInfo;
334
+
277
335
  // Insert bar lines at calculated positions
278
- const newMusic = insertCharsAtIndexes(musicText, "| ", insertionPoints);
336
+ const insertionPoints = [...midpoints].sort((a, b) => b - a);
337
+ let newMusic = musicText;
338
+
339
+ for (const pos of insertionPoints) {
340
+ newMusic = newMusic.substring(0, pos) + "| " + newMusic.substring(pos);
341
+ }
279
342
 
280
343
  return `${newHeaders.join("\n")}\n${newMusic}`;
281
344
  }
@@ -318,9 +381,7 @@ function getFirstBars(
318
381
  ) {
319
382
  // Convert numBars to Fraction if it's a number
320
383
  const numBarsFraction =
321
- typeof numBars === "number"
322
- ? new Fraction(Math.round(numBars * 1000), 1000)
323
- : numBars;
384
+ typeof numBars === "number" ? new Fraction(numBars) : numBars;
324
385
 
325
386
  // Estimate maxBars needed - simple ceiling with buffer
326
387
  const estimatedMaxBars =
@@ -344,6 +405,7 @@ function getFirstBars(
344
405
  }
345
406
  }
346
407
 
408
+ //todo
347
409
  if (firstCompleteBarIdx === -1) {
348
410
  throw new Error("No complete bars found");
349
411
  }
@@ -426,10 +488,14 @@ function getFirstBars(
426
488
  accumulatedDuration = newAccumulated;
427
489
  }
428
490
 
491
+ // if (endPos === startPos) {
492
+ // throw new Error(
493
+ // `Not enough bars to satisfy request. Requested ${numBars} bars.`
494
+ // );
495
+ // }
496
+
429
497
  if (endPos === startPos) {
430
- throw new Error(
431
- `Not enough bars to satisfy request. Requested ${numBars} bars.`
432
- );
498
+ endPos = musicText.length - 1;
433
499
  }
434
500
 
435
501
  // Reconstruct ABC
package/src/math.js CHANGED
@@ -12,6 +12,10 @@ class Fraction {
12
12
  ) {
13
13
  throw new Error("invalid argument");
14
14
  }
15
+ // if (!Number.isInteger(numerator)) {
16
+ // numerator = Math.round(numerator * 1000);
17
+ // denominator = denominator * 1000;
18
+ // }
15
19
 
16
20
  const g = gcd(Math.abs(numerator), Math.abs(denominator));
17
21
  this.num = numerator / g;
@@ -23,11 +27,16 @@ class Fraction {
23
27
  this.den = -this.den;
24
28
  }
25
29
  }
26
-
27
30
  clone() {
28
31
  return new Fraction(this.num, this.den);
29
32
  }
30
33
 
34
+ static min(x, y) {
35
+ if (!x) return y;
36
+ if (!y) return x;
37
+ return x.compare(y) < 0 ? x : y;
38
+ }
39
+
31
40
  multiply(n) {
32
41
  if (typeof n === "number") {
33
42
  return new Fraction(this.num * n, this.den);
@@ -59,7 +68,7 @@ class Fraction {
59
68
  compare(other) {
60
69
  // Returns -1 if this < other, 0 if equal, 1 if this > other
61
70
  const diff =
62
- typeof n === "number"
71
+ typeof other === "number"
63
72
  ? this.num - other * this.den
64
73
  : this.num * other.den - other.num * this.den;
65
74
  return diff < 0 ? -1 : diff > 0 ? 1 : 0;
@@ -0,0 +1,202 @@
1
+ const { Fraction } = require("../math.js");
2
+
3
+ /**
4
+ * Enriches parsed ABC bar data with musical bar information
5
+ *
6
+ * Analyzes bars and bar lines to add:
7
+ * - barNumber: Index of the musical bar (null for initial bar lines)
8
+ * - isPartial: Flag for bar lines that occur mid-musical-bar
9
+ * - cumulativeDuration: Duration tracking for bar segments
10
+ * - midpoints: Positions where bars should be split (when divideBarsBy specified)
11
+ *
12
+ * A "musical bar" is defined by the meter (e.g., 4/4 means 4 quarter notes).
13
+ * Bar lines can create "partial bars" when repeats or variant endings split a musical bar.
14
+ * Consecutive partial bars within the same musical bar will have the same barNumber.
15
+ * Variant endings create alternative paths, so duration tracking follows one path at a time.
16
+ *
17
+ * @param {Array<Array<Object>>} bars - Array of bar arrays from parseAbc
18
+ * @param {Array<Object>} barLines - Array of barLine objects from parseAbc
19
+ * @param {Array<number>} meter - [numerator, denominator] from parseAbc
20
+ * @param {Object} options - Configuration options
21
+ * @param {boolean} options.barNumbers - Add barNumber to each barLine. Default value: true.
22
+ * @param {boolean} options.isPartial - Add isPartial flag to partial barLines. Default value: true.
23
+ * @param {boolean} options.cumulativeDuration - Add duration tracking to barLines. Default value: true.
24
+ * @param {number|null} options.divideBarsBy - Find midpoints for splitting (only 2 supported)
25
+ * @returns {Object} - { barLines: enriched array, midpoints: insertion positions }
26
+ */
27
+ function getBarInfo(bars, barLines, meter, options = {}) {
28
+ const {
29
+ barNumbers = true,
30
+ isPartial = true,
31
+ cumulativeDuration = true,
32
+ divideBarsBy = null,
33
+ } = options;
34
+
35
+ if (divideBarsBy !== null && divideBarsBy !== 2) {
36
+ throw new Error("divideBarsBy currently only supports value 2");
37
+ }
38
+ if (!barLines || barLines.length < bars.length) {
39
+ throw new Error(
40
+ "currently not handling bars without a bar line at the end"
41
+ );
42
+ }
43
+
44
+ const fullBarDuration = new Fraction(meter[0], meter[1]);
45
+ const midpoints = [];
46
+
47
+ let currentBarNumber = 0;
48
+ let durationSinceLastComplete = new Fraction(0, 1);
49
+ let lastCompleteBarLineIdx = -1;
50
+ let barLineOffset = 0;
51
+
52
+ // Check for initial bar line (before any music)
53
+ if (
54
+ bars.length > 0 &&
55
+ bars[0].length > 0 &&
56
+ barLines.length > 0 &&
57
+ barLines[0].sourceIndex < bars[0][0].sourceIndex
58
+ ) {
59
+ // Initial bar line exists
60
+ if (barNumbers) {
61
+ barLines[0].barNumber = null;
62
+ }
63
+ barLineOffset = 1;
64
+ }
65
+
66
+ // Process each bar and its following bar line
67
+ for (let barIdx = 0; barIdx < bars.length; barIdx++) {
68
+ const bar = bars[barIdx];
69
+ const barLineIdx = barIdx + barLineOffset;
70
+
71
+ // Check if this bar starts with a variant ending
72
+ const startsWithVariant = bar.length > 0 && bar[0].isVariantEnding;
73
+
74
+ // If this bar starts with a variant, reset duration tracking
75
+ // (variant endings create alternative paths)
76
+ if (startsWithVariant && lastCompleteBarLineIdx >= 0) {
77
+ durationSinceLastComplete = new Fraction(0, 1);
78
+ }
79
+
80
+ // Calculate duration of this bar segment
81
+ let barDuration = new Fraction(0, 1);
82
+ for (const token of bar) {
83
+ if (token.duration) {
84
+ barDuration = barDuration.add(token.duration);
85
+ }
86
+ }
87
+
88
+ durationSinceLastComplete = durationSinceLastComplete.add(barDuration);
89
+
90
+ // Get the bar line that follows this bar
91
+ if (barLineIdx < barLines.length) {
92
+ const barLine = barLines[barLineIdx];
93
+
94
+ // Determine if this bar line is partial
95
+ // A bar line is partial if THIS bar segment is less than full bar duration
96
+ const isPartialBar = barDuration.compare(fullBarDuration) < 0;
97
+
98
+ // Add barNumber
99
+ if (barNumbers) {
100
+ if (isPartialBar) {
101
+ // Partial bar line: barNumber is lastComplete + 1
102
+ barLine.barNumber =
103
+ lastCompleteBarLineIdx >= 0
104
+ ? barLines[lastCompleteBarLineIdx].barNumber + 1
105
+ : 0;
106
+
107
+ // If this is the initial anacrusis (barNumber 0), mark it as complete for numbering purposes
108
+ if (barLine.barNumber === 0) {
109
+ lastCompleteBarLineIdx = barLineIdx;
110
+ currentBarNumber = 1; // Next complete bar will be bar 1
111
+ }
112
+
113
+ // If partial bars have accumulated to a full bar, increment currentBarNumber
114
+ if (durationSinceLastComplete.compare(fullBarDuration) >= 0) {
115
+ currentBarNumber = barLine.barNumber + 1;
116
+ lastCompleteBarLineIdx = barLineIdx;
117
+ }
118
+ } else {
119
+ // Complete bar line
120
+ barLine.barNumber = currentBarNumber;
121
+ currentBarNumber++;
122
+ lastCompleteBarLineIdx = barLineIdx;
123
+ }
124
+ }
125
+
126
+ // Add isPartial flag (only when true)
127
+ if (isPartial && isPartialBar) {
128
+ barLine.isPartial = true;
129
+ }
130
+
131
+ // Add cumulative duration
132
+ if (cumulativeDuration) {
133
+ barLine.cumulativeDuration = {
134
+ sinceLastBarLine: barDuration.clone(),
135
+ sinceLastComplete: durationSinceLastComplete.clone(),
136
+ };
137
+ }
138
+
139
+ // Reset duration tracking if this completes a musical bar
140
+ // Also reset after initial anacrusis (barNumber 0)
141
+ if (lastCompleteBarLineIdx === barLineIdx || barLine.barNumber === 0) {
142
+ durationSinceLastComplete = new Fraction(0, 1);
143
+ }
144
+ }
145
+ }
146
+
147
+ // Calculate midpoints if requested
148
+ if (divideBarsBy === 2) {
149
+ const halfBarDuration = fullBarDuration.divide(new Fraction(2, 1));
150
+
151
+ for (let barIdx = 0; barIdx < bars.length; barIdx++) {
152
+ const bar = bars[barIdx];
153
+ let accumulated = new Fraction(0, 1);
154
+
155
+ // Check if this bar starts with a variant - if so, skip it
156
+ // (variant endings already create splits)
157
+ if (bar.length > 0 && bar[0].isVariantEnding) {
158
+ continue;
159
+ }
160
+
161
+ // Find the halfway point in this bar
162
+ for (let tokenIdx = 0; tokenIdx < bar.length; tokenIdx++) {
163
+ const token = bar[tokenIdx];
164
+
165
+ // If we hit a variant ending before halfway, don't split
166
+ if (token.isVariantEnding) {
167
+ break;
168
+ }
169
+
170
+ if (!token.duration) {
171
+ continue;
172
+ }
173
+
174
+ const prevAccumulated = accumulated.clone();
175
+ accumulated = accumulated.add(token.duration);
176
+
177
+ // Check if we just crossed the halfway point
178
+ if (
179
+ prevAccumulated.compare(halfBarDuration) < 0 &&
180
+ accumulated.compare(halfBarDuration) >= 0
181
+ ) {
182
+ // Insert after this token
183
+ let insertPos = token.sourceIndex + token.sourceLength;
184
+ if (token.spacing && token.spacing.whitespace) {
185
+ insertPos += token.spacing.whitespace.length;
186
+ }
187
+ midpoints.push(insertPos);
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ return {
195
+ barLines,
196
+ midpoints,
197
+ };
198
+ }
199
+
200
+ module.exports = {
201
+ getBarInfo,
202
+ };
@@ -13,7 +13,7 @@ const { Fraction } = require("../math.js");
13
13
  // ============================================================================
14
14
 
15
15
  /**
16
- * Extract key signature from ABC header
16
+ * Extract base note of key signature from ABC header
17
17
  *
18
18
  * @param {string} abc - ABC notation string
19
19
  * @returns {string} - Tonic note (e.g., 'C', 'D', 'G')
@@ -27,6 +27,20 @@ function getTonalBase(abc) {
27
27
  return keyMatch[1].toUpperCase();
28
28
  }
29
29
 
30
+ /**
31
+ * Extract key signature from ABC header
32
+ *
33
+ * @param {string} abc - ABC notation string
34
+ * @returns {string} - Tonic note (e.g., 'C', 'D', 'G')
35
+ * @throws {Error} - If no key signature found
36
+ */
37
+ function getKey(abc) {
38
+ const keyMatch = abc.match(/^K:\s*([A-G].*)/m);
39
+ if (!keyMatch) {
40
+ throw new Error("No key signature found in ABC");
41
+ }
42
+ return keyMatch[1];
43
+ }
30
44
  /**
31
45
  * Extract meter/time signature from ABC header
32
46
  *
@@ -151,4 +165,5 @@ module.exports = {
151
165
  getUnitLength,
152
166
  getMusicLines,
153
167
  getTitles,
168
+ getKey,
154
169
  };
@@ -2,6 +2,7 @@ const { Fraction } = require("../math.js");
2
2
  const {
3
3
  getTonalBase,
4
4
  getMeter,
5
+ getKey,
5
6
  getUnitLength,
6
7
  getMusicLines,
7
8
  } = require("./header-parser.js");
@@ -70,7 +71,7 @@ const {
70
71
  * Returns object with:
71
72
  * {
72
73
  * bars: Array<Array<ScoreObject>>, // Array of bars, each bar is array of ScoreObjects
73
- * // A Score object is almost anything that isn’t a bar line: note/chord/field/broken rhythm/tuplet/1st or 2nd repeat or variant ending
74
+ * // A ScoreObject is almost anything that isn’t a bar line: note/chord/field/broken rhythm/tuplet/1st or 2nd repeat or variant ending
74
75
  * barLines: Array<BarLineObject>, // Array of bar line information
75
76
  * unitLength: Fraction, // The L: field value (default 1/8)
76
77
  * meter: [number, number], // The M: field value (default [4,4])
@@ -484,7 +485,7 @@ function parseAbc(abc, options = {}) {
484
485
  ...barLineInfo,
485
486
  sourceIndex: barLinePos,
486
487
  sourceLength: barLineText.length,
487
- barNumber: barCount,
488
+ //barNumber: barCount,
488
489
  hasLineBreak: hasLineBreakAfterBar,
489
490
  });
490
491
 
@@ -602,6 +603,7 @@ function getTunes(text) {
602
603
 
603
604
  module.exports = {
604
605
  getTunes,
606
+ getKey,
605
607
  parseAbc,
606
608
  calculateBarDurations,
607
609
  // Re-export utilities for convenience
@@ -1,173 +1,13 @@
1
1
  const { Fraction } = require("../math.js");
2
- const {
3
- getTonalBase,
4
- getUnitLength,
5
- parseAbc,
6
- getMeter,
7
- } = require("../parse/parser.js");
8
2
 
9
- const { contourToSvg } = require("./contour-svg.js");
10
-
11
- const {
12
- calculateModalPosition,
13
- decodeChar,
14
- encodeToChar,
15
- silenceChar,
16
- } = require("./encode.js");
3
+ const { decodeChar, encodeToChar, silenceChar } = require("./encode.js");
4
+ const { getContour } = require("./get-contour.js");
17
5
 
18
6
  /**
19
7
  * Tune Contour Sort - Modal melody sorting algorithm
20
8
  * Sorts tunes by their modal contour, independent of key and mode
21
9
  */
22
10
 
23
- // ============================================================================
24
- // Contour (compare object) generation
25
- // ============================================================================
26
-
27
- /**
28
- * Generate contour (compare object) from ABC notation
29
- * @returns { sortKey: string, durations: Array, version: string, part: string }
30
- *
31
- * todo: complete this header. options.withSvg; options.maxNbUnitLengths
32
- */
33
- function getContour(
34
- abc,
35
- { withSvg = false, maxNbUnitLengths = 10, svgConfig = {} } = {}
36
- ) {
37
- const tonalBase = getTonalBase(abc);
38
- const unitLength = getUnitLength(abc);
39
- const maxDuration = unitLength.multiply(maxNbUnitLengths);
40
- const meter = getMeter(abc);
41
- const maxNbBars = meter
42
- ? maxDuration.divide(new Fraction(meter[0], meter[1]))
43
- : new Fraction(2, 1); //default 2 bars when no meter (free meter)
44
- const { bars } = parseAbc(abc, {
45
- maxBars: Math.ceil(maxNbBars.toNumber()),
46
- });
47
- let cumulatedDuration = new Fraction(0, 1);
48
- const sortKey = [];
49
- const durations = [];
50
- // const debugPositions = [];
51
- let index = 0;
52
- // get the parsed notes - notes are tokens with a duration
53
- const notes = [];
54
- let tied = false,
55
- previousPosition = null;
56
- for (let i = 0; i < bars.length; i++) {
57
- const bar = bars[i];
58
- for (let j = 0; j < bar.length; j++) {
59
- const token = bar[j];
60
- if (token.duration && token.duration.num > 0) {
61
- cumulatedDuration = cumulatedDuration.add(token.duration);
62
- if (cumulatedDuration.isGreaterThan(maxDuration)) break;
63
- notes.push(token);
64
- }
65
- }
66
- }
67
-
68
- notes.forEach((note) => {
69
- const { duration, isSilence } = note;
70
- const comparison = duration.compare(unitLength);
71
- const { encoded, encodedHeld, position } = isSilence
72
- ? { encoded: silenceChar, encodedHeld: silenceChar, position: 0 }
73
- : getEncodedFromNote(note, tonalBase, tied, previousPosition);
74
-
75
- if (note.tied) {
76
- tied = true;
77
- previousPosition = position;
78
- } else {
79
- tied = false;
80
- previousPosition = null;
81
- }
82
-
83
- if (comparison > 0) {
84
- // Held note: duration > unitLength
85
- const ratio = duration.divide(unitLength);
86
- const nbUnitLengths = Math.floor(ratio.num / ratio.den);
87
- const remainingDuration = duration.subtract(
88
- unitLength.multiply(nbUnitLengths)
89
- );
90
-
91
- // const durationRatio = Math.round(ratio.num / ratio.den);
92
-
93
- // First note is played
94
- sortKey.push(encoded);
95
- //debugPositions.push(position);
96
-
97
- // Subsequent notes are held
98
- for (let i = 1; i < nbUnitLengths; i++) {
99
- sortKey.push(encodedHeld);
100
- //debugPositions.push(position);
101
- }
102
-
103
- index += nbUnitLengths;
104
- if (remainingDuration.num !== 0) {
105
- pushShortNote(
106
- encodedHeld,
107
- unitLength,
108
- duration,
109
- index,
110
- durations,
111
- sortKey
112
- );
113
- //debugPositions.push(position);
114
- index++;
115
- }
116
- } else if (comparison < 0) {
117
- pushShortNote(encoded, unitLength, duration, index, durations, sortKey);
118
- //debugPositions.push(position);
119
- index++;
120
- } else {
121
- // Normal note: duration === unitLength
122
- sortKey.push(encoded);
123
- //debugPositions.push(position);
124
- index++;
125
- }
126
- });
127
-
128
- const result = {
129
- sortKey: sortKey.join(""),
130
- //debugPositions: debugPositions.join(","),
131
- };
132
- if (durations.length > 0) {
133
- result.durations = durations;
134
- }
135
- if (withSvg) {
136
- result.svg = contourToSvg(result, svgConfig);
137
- }
138
- return result;
139
- }
140
-
141
- /**
142
- * Adds a short note (duration < unitLength) to the contour
143
- * @param {string} encoded - the encoded representation of the note’s modal degree information (MDI)
144
- * @param {Fraction} unitLength - the unit length
145
- * @param {Fraction} duration - the duration of the note
146
- * @param {number} index - the index of the note
147
- * @param {Array<object>} durations - the durations array
148
- * @param {Array<string>} sortKey - array of MDIs
149
- */
150
- function pushShortNote(
151
- encoded,
152
- unitLength,
153
- duration,
154
- index,
155
- durations,
156
- sortKey
157
- ) {
158
- const relativeDuration = duration.divide(unitLength),
159
- d = {
160
- i: index,
161
- d: relativeDuration.den,
162
- };
163
- if (relativeDuration.num !== 1) {
164
- d.n = relativeDuration.num;
165
- }
166
-
167
- durations.push(d);
168
- sortKey.push(encoded);
169
- }
170
-
171
11
  // ============================================================================
172
12
  // COMPARISON FUNCTIONS
173
13
  // ============================================================================
@@ -352,26 +192,11 @@ function sortArray(arr) {
352
192
  return arr;
353
193
  }
354
194
 
355
- function getEncodedFromNote(note, tonalBase, tied, previousPosition) {
356
- // Handle pitched note
357
- const { pitch, octave } = note;
358
- const position = calculateModalPosition(tonalBase, pitch, octave);
359
- const encodedHeld = encodeToChar(position, true);
360
- const encoded = encodeToChar(position, false);
361
-
362
- return {
363
- encoded: tied && position === previousPosition ? encodedHeld : encoded,
364
- encodedHeld,
365
- position,
366
- };
367
- }
368
-
369
195
  // ============================================================================
370
196
  // EXPORTS
371
197
  // ============================================================================
372
198
 
373
199
  module.exports = {
374
- getContour,
375
200
  compare,
376
201
  sortArray,
377
202
  decodeChar,
@@ -0,0 +1,333 @@
1
+ const { Fraction } = require("../math.js");
2
+
3
+ const {
4
+ calculateModalPosition,
5
+ encodeToChar,
6
+ silenceChar,
7
+ } = require("./encode.js");
8
+
9
+ const {
10
+ getTonalBase,
11
+ getUnitLength,
12
+ parseAbc,
13
+ getMeter,
14
+ } = require("../parse/parser.js");
15
+
16
+ const { contourToSvg } = require("./contour-svg.js");
17
+
18
+ // ============================================================================
19
+ // Contour (compare object) generation
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Generate contour (compare object) from ABC notation
24
+ * @returns { sortKey: string, durations: Array, version: string, part: string }
25
+ *
26
+ * todo: complete this header. options.withSvg; options.maxNbUnitLengths
27
+ */
28
+ function getContour(
29
+ abc,
30
+ {
31
+ withSvg = false,
32
+ withSwingTransform = false,
33
+ maxNbBars = new Fraction(3, 2),
34
+ maxNbUnitLengths = 12,
35
+ svgConfig = {},
36
+ } = {}
37
+ ) {
38
+ const tonalBase = getTonalBase(abc);
39
+
40
+ const unitLength = getUnitLength(abc); //todo: could add as an argument; default null
41
+ if (typeof maxNbBars === "number") maxNbBars = new Fraction(maxNbBars);
42
+ let meter = getMeter(abc); //todo: could add as an argument; default null
43
+ if (!meter) meter = [4, 4]; //temp
44
+ const meterFraction = new Fraction(meter[0], meter[1]);
45
+ if (maxNbUnitLengths) {
46
+ const maxNbBarsFromMaxUnitLength = unitLength
47
+ .multiply(maxNbUnitLengths)
48
+ .divide(meterFraction);
49
+
50
+ maxNbBars = Fraction.min(maxNbBarsFromMaxUnitLength, maxNbBars);
51
+ }
52
+ const maxDuration = maxNbBars * meterFraction;
53
+
54
+ const {
55
+ bars,
56
+ } = //todo: could add as an argument; default null
57
+ parseAbc(abc, {
58
+ maxBars: Math.ceil(maxNbBars.toNumber()),
59
+ });
60
+
61
+ let cumulatedDuration = new Fraction(0, 1);
62
+ const sortKey = [];
63
+ const durations = [];
64
+ // const debugPositions = [];
65
+ let index = 0;
66
+ // get the parsed notes - notes are tokens with a duration
67
+ const notes = [];
68
+ let tied = false,
69
+ previousPosition = null;
70
+ for (let i = 0; i < bars.length; i++) {
71
+ const bar = bars[i];
72
+ for (let j = 0; j < bar.length; j++) {
73
+ const token = bar[j];
74
+ if (token.duration && token.duration.num > 0) {
75
+ cumulatedDuration = cumulatedDuration.add(token.duration);
76
+ if (cumulatedDuration.isGreaterThan(maxDuration)) break;
77
+ notes.push(token);
78
+ }
79
+ }
80
+ }
81
+
82
+ if (withSwingTransform) {
83
+ swingTransform(notes, unitLength, meter);
84
+ }
85
+
86
+ notes.forEach((note) => {
87
+ const { duration, isSilence } = note;
88
+ const comparison = duration.compare(unitLength);
89
+ const { encoded, encodedHeld, position } = isSilence
90
+ ? { encoded: silenceChar, encodedHeld: silenceChar, position: 0 }
91
+ : getEncodedFromNote(note, tonalBase, tied, previousPosition);
92
+
93
+ if (note.tied) {
94
+ tied = true;
95
+ previousPosition = position;
96
+ } else {
97
+ tied = false;
98
+ previousPosition = null;
99
+ }
100
+
101
+ if (comparison > 0) {
102
+ // Held note: duration > unitLength
103
+ const { nbUnitLengths, remainingDuration } = divideDuration(
104
+ duration,
105
+ unitLength
106
+ );
107
+
108
+ // First note is played
109
+ sortKey.push(encoded);
110
+ //debugPositions.push(position);
111
+
112
+ // Subsequent notes are held
113
+ for (let i = 1; i < nbUnitLengths; i++) {
114
+ sortKey.push(encodedHeld);
115
+ //debugPositions.push(position);
116
+ }
117
+
118
+ index += nbUnitLengths;
119
+ if (remainingDuration.num !== 0) {
120
+ pushShortNote(
121
+ encodedHeld,
122
+ unitLength,
123
+ duration,
124
+ index,
125
+ durations,
126
+ sortKey
127
+ );
128
+ //debugPositions.push(position);
129
+ index++;
130
+ }
131
+ } else if (comparison < 0) {
132
+ pushShortNote(encoded, unitLength, duration, index, durations, sortKey);
133
+ //debugPositions.push(position);
134
+ index++;
135
+ } else {
136
+ // Normal note: duration === unitLength
137
+ sortKey.push(encoded);
138
+ //debugPositions.push(position);
139
+ index++;
140
+ }
141
+ });
142
+
143
+ const result = {
144
+ sortKey: sortKey.join(""),
145
+ //debugPositions: debugPositions.join(","),
146
+ };
147
+ if (durations.length > 0) {
148
+ result.durations = durations;
149
+ }
150
+ if (withSvg) {
151
+ result.svg = contourToSvg(result, svgConfig);
152
+ }
153
+ return result;
154
+ }
155
+
156
+ function divideDuration(duration, unitLength) {
157
+ const ratio = duration.divide(unitLength);
158
+ const nbUnitLengths = Math.floor(ratio.num / ratio.den);
159
+ const remainingDuration = duration.subtract(
160
+ unitLength.multiply(nbUnitLengths)
161
+ );
162
+ return { nbUnitLengths, remainingDuration };
163
+ }
164
+
165
+ function swingTransform(notes, unitLength, meter) {
166
+ // if (meter[0] % 2 !== 0) {
167
+ //check meter is an even multiple of the unit length
168
+ {
169
+ const nbUnitLengths = new Fraction(meter[0], meter[1]).divide(unitLength);
170
+ if (nbUnitLengths.den !== 1 || nbUnitLengths.num % 2 !== 0)
171
+ throw new Error("invalid meter for swing transform");
172
+ }
173
+ // modify notes to ensure all are of duration <= 2*unitLength
174
+ {
175
+ const twoUnits = unitLength.multiply(2);
176
+ let tooLong = notes
177
+ .map((n, i) => {
178
+ return { n, i };
179
+ })
180
+ .filter((n) => n.n.duration.compare(twoUnits) > 0),
181
+ safety = 0;
182
+
183
+ while (tooLong.length > 0) {
184
+ if (safety > 1000) throw new Error("swingTransform safety check failed");
185
+
186
+ const noteToSplit = tooLong[0].n;
187
+ const { nbUnitLengths, remainingDuration } = divideDuration(
188
+ noteToSplit.duration,
189
+ twoUnits
190
+ );
191
+ noteToSplit.duration = twoUnits;
192
+ if (!tooLong.isSilence) noteToSplit.tied = true;
193
+ const toAdd = [];
194
+ for (let i = 1; i < nbUnitLengths; i++) {
195
+ toAdd.push({ ...noteToSplit });
196
+ }
197
+ const lastNote = { ...noteToSplit };
198
+ lastNote.duration = remainingDuration;
199
+ toAdd.push(lastNote);
200
+ notes.splice(tooLong[0].i + 1, 0, ...toAdd);
201
+ /*
202
+ myArray.splice(index, 0, ...itemsToInsert): The splice method takes three arguments:
203
+ The first argument (index) is the starting index at which to modify the array.
204
+ The second argument (0) indicates that no elements should be removed from the array.
205
+ The third argument uses the spread operator (...itemsToInsert) to insert the elements of itemsToInsert into myArray at the specified index.
206
+ */
207
+ safety++;
208
+ tooLong = notes
209
+ .map((n, i) => {
210
+ return { n, i };
211
+ })
212
+ .filter((n) => n.n.duration.compare(twoUnits) > 0);
213
+ }
214
+ }
215
+
216
+ const dotted = unitLength.multiply(3).divide(2), // dotted quaver, if L:1/8; long part of broken rhythm
217
+ semi = unitLength.divide(2), // semiquaver, if L:1/8; short part of broken rhythm
218
+ triplet = unitLength.multiply(2).divide(3),
219
+ multiplier = new Fraction(3, 2);
220
+
221
+ let i = 0;
222
+ while (true) {
223
+ if (i >= notes.length) break;
224
+ const n1 = notes[i],
225
+ n2 = i + 1 < notes.length ? notes[i + 1] : null,
226
+ n3 = i + 2 < notes.length ? notes[i + 2] : null;
227
+
228
+ //basic: change AB to A2B
229
+ if (
230
+ n2 &&
231
+ n1.duration.equals(unitLength) &&
232
+ n2.duration.equals(unitLength)
233
+ ) {
234
+ n1.duration = unitLength.multiply(2);
235
+ i += 2;
236
+ continue;
237
+ }
238
+ //broken
239
+ if (n2 && n1.duration.equals(dotted) && n2.duration.equals(semi)) {
240
+ n1.duration = unitLength.multiply(2);
241
+ n2.duration = unitLength;
242
+ i += 2;
243
+ continue;
244
+ }
245
+ //reverse broken
246
+ if (n2 && n2.duration.equals(dotted) && n1.duration.equals(semi)) {
247
+ n2.duration = unitLength.multiply(2);
248
+ n1.duration = unitLength;
249
+ i += 2;
250
+ continue;
251
+ }
252
+
253
+ //triplets
254
+ if (
255
+ n2 &&
256
+ n3 &&
257
+ n1.duration.equals(triplet) &&
258
+ n2.duration.equals(triplet) &&
259
+ n3.duration.equals(triplet)
260
+ ) {
261
+ n1.duration = unitLength;
262
+ n2.duration = unitLength;
263
+ n3.duration = unitLength;
264
+ i += 3;
265
+ continue;
266
+ }
267
+ //two short + long, eg d/c/B
268
+ if (
269
+ n2 &&
270
+ n3 &&
271
+ n1.duration.equals(semi) &&
272
+ n2.duration.equals(semi) &&
273
+ n3.duration.equals(unitLength)
274
+ ) {
275
+ n1.duration = unitLength.divide(2);
276
+ n2.duration = unitLength.divide(2);
277
+ n3.duration = unitLength.multiply(2);
278
+ i += 3;
279
+ continue;
280
+ }
281
+ // other
282
+ n1.duration = n1.duration.multiply(multiplier);
283
+ i++;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Adds a short note (duration < unitLength) to the contour
289
+ * @param {string} encoded - the encoded representation of the note’s modal degree information (MDI)
290
+ * @param {Fraction} unitLength - the unit length
291
+ * @param {Fraction} duration - the duration of the note
292
+ * @param {number} index - the index of the note
293
+ * @param {Array<object>} durations - the durations array
294
+ * @param {Array<string>} sortKey - array of MDIs
295
+ */
296
+ function pushShortNote(
297
+ encoded,
298
+ unitLength,
299
+ duration,
300
+ index,
301
+ durations,
302
+ sortKey
303
+ ) {
304
+ const relativeDuration = duration.divide(unitLength),
305
+ d = {
306
+ i: index,
307
+ d: relativeDuration.den,
308
+ };
309
+ if (relativeDuration.num !== 1) {
310
+ d.n = relativeDuration.num;
311
+ }
312
+
313
+ durations.push(d);
314
+ sortKey.push(encoded);
315
+ }
316
+
317
+ function getEncodedFromNote(note, tonalBase, tied, previousPosition) {
318
+ // Handle pitched note
319
+ const { pitch, octave } = note;
320
+ const position = calculateModalPosition(tonalBase, pitch, octave);
321
+ const encodedHeld = encodeToChar(position, true);
322
+ const encoded = encodeToChar(position, false);
323
+
324
+ return {
325
+ encoded: tied && position === previousPosition ? encodedHeld : encoded,
326
+ encodedHeld,
327
+ position,
328
+ };
329
+ }
330
+
331
+ module.exports = {
332
+ getContour,
333
+ };
package/testoutput.txt ADDED
Binary file