musicxml-io 0.5.6 → 0.7.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.
@@ -171,6 +171,344 @@ function getMeasureEndPosition(measure) {
171
171
  return state.position;
172
172
  }
173
173
 
174
+ // src/query/playback-sequence.ts
175
+ function parseEndingNumbers(numberStr) {
176
+ const numbers = [];
177
+ const parts = numberStr.split(/[,\s]+/);
178
+ for (const part of parts) {
179
+ if (part.includes("-")) {
180
+ const [start, end] = part.split("-").map((s) => parseInt(s.trim(), 10));
181
+ if (!isNaN(start) && !isNaN(end)) {
182
+ for (let i = start; i <= end; i++) {
183
+ numbers.push(i);
184
+ }
185
+ }
186
+ } else {
187
+ const num = parseInt(part.trim(), 10);
188
+ if (!isNaN(num)) {
189
+ numbers.push(num);
190
+ }
191
+ }
192
+ }
193
+ return numbers.length > 0 ? numbers : [1];
194
+ }
195
+ function parseJumpText(text) {
196
+ const t = text.toLowerCase().replace(/\s+/g, " ").trim();
197
+ if (t.includes("d.c. al coda") || t.includes("da capo al coda")) return "dc_al_coda";
198
+ if (t.includes("d.c. al fine") || t.includes("da capo al fine")) return "dc_al_fine";
199
+ if (t.includes("d.c.") || t.includes("da capo")) return "dc";
200
+ if (t.includes("d.s. al coda") || t.includes("dal segno al coda")) return "ds_al_coda";
201
+ if (t.includes("d.s. al fine") || t.includes("dal segno al fine")) return "ds_al_fine";
202
+ if (t.includes("d.s.") || t.includes("dal segno")) return "ds";
203
+ if (t === "fine") return "fine";
204
+ if (t.includes("to coda") || t.includes("al coda")) return "to_coda";
205
+ if (t === "coda" || t === "\u{1D14C}") return "coda";
206
+ if (t === "segno" || t === "\u{1D10B}") return "segno";
207
+ return null;
208
+ }
209
+ function getFirstWordsText(entry) {
210
+ for (const dt of entry.directionTypes) {
211
+ if (dt.kind === "words" && dt.text) return dt.text;
212
+ }
213
+ return void 0;
214
+ }
215
+ function hasDirectionKind(entry, kind) {
216
+ return entry.directionTypes.some((dt) => dt.kind === kind);
217
+ }
218
+ function extractPlaybackControls(part) {
219
+ const controls = {
220
+ repeatStarts: [],
221
+ repeatEnds: [],
222
+ voltas: [],
223
+ jumps: [],
224
+ segnoIndex: null,
225
+ codaIndex: null,
226
+ fineIndex: null,
227
+ toCodaIndex: null
228
+ };
229
+ const soundJumps = [];
230
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
231
+ const measure = part.measures[measureIndex];
232
+ if (measure.barlines) {
233
+ for (const barline of measure.barlines) {
234
+ if (barline.repeat) {
235
+ if (barline.repeat.direction === "forward") {
236
+ controls.repeatStarts.push(measureIndex);
237
+ } else if (barline.repeat.direction === "backward") {
238
+ controls.repeatEnds.push({
239
+ measureIndex,
240
+ times: barline.repeat.times ?? 2
241
+ });
242
+ }
243
+ }
244
+ if (barline.ending) {
245
+ const numbers = parseEndingNumbers(barline.ending.number);
246
+ controls.voltas.push({
247
+ measureIndex,
248
+ numbers,
249
+ type: barline.ending.type
250
+ });
251
+ }
252
+ }
253
+ }
254
+ for (const entry of measure.entries) {
255
+ if (entry.type === "direction") {
256
+ const dir = entry;
257
+ if (hasDirectionKind(dir, "segno") && controls.segnoIndex === null) {
258
+ controls.segnoIndex = measureIndex;
259
+ }
260
+ if (hasDirectionKind(dir, "coda") && controls.codaIndex === null) {
261
+ controls.codaIndex = measureIndex;
262
+ }
263
+ const words = getFirstWordsText(dir);
264
+ if (words) {
265
+ const jt = parseJumpText(words);
266
+ if (jt === "fine") {
267
+ if (controls.fineIndex === null) controls.fineIndex = measureIndex;
268
+ } else if (jt === "to_coda") {
269
+ if (controls.toCodaIndex === null) controls.toCodaIndex = measureIndex;
270
+ } else if (jt === "coda") {
271
+ if (controls.codaIndex === null) controls.codaIndex = measureIndex;
272
+ } else if (jt === "segno") {
273
+ if (controls.segnoIndex === null) controls.segnoIndex = measureIndex;
274
+ } else if (jt) {
275
+ controls.jumps.push({ measureIndex, type: jt });
276
+ }
277
+ }
278
+ } else if (entry.type === "sound") {
279
+ const sound = entry;
280
+ if (sound.segno && controls.segnoIndex === null) controls.segnoIndex = measureIndex;
281
+ if (sound.coda && controls.codaIndex === null) controls.codaIndex = measureIndex;
282
+ if (sound.tocoda && controls.toCodaIndex === null) controls.toCodaIndex = measureIndex;
283
+ if (sound.fine && controls.fineIndex === null) controls.fineIndex = measureIndex;
284
+ if (sound.dacapo) soundJumps.push({ measureIndex, base: "dc" });
285
+ if (sound.dalsegno) soundJumps.push({ measureIndex, base: "ds" });
286
+ }
287
+ }
288
+ }
289
+ for (const sj of soundJumps) {
290
+ if (controls.jumps.some((j) => j.measureIndex === sj.measureIndex)) continue;
291
+ let type;
292
+ if (controls.fineIndex !== null) {
293
+ type = sj.base === "dc" ? "dc_al_fine" : "ds_al_fine";
294
+ } else if (controls.toCodaIndex !== null || controls.codaIndex !== null) {
295
+ type = sj.base === "dc" ? "dc_al_coda" : "ds_al_coda";
296
+ } else {
297
+ type = sj.base;
298
+ }
299
+ controls.jumps.push({ measureIndex: sj.measureIndex, type });
300
+ }
301
+ return controls;
302
+ }
303
+ function buildVoltaRanges(voltas, measureCount) {
304
+ const voltaRanges = /* @__PURE__ */ new Map();
305
+ let currentVoltaStart = null;
306
+ let currentVoltaNumbers = [];
307
+ for (const volta of voltas) {
308
+ if (volta.type === "start") {
309
+ currentVoltaStart = volta.measureIndex;
310
+ currentVoltaNumbers = volta.numbers;
311
+ } else if ((volta.type === "stop" || volta.type === "discontinue") && currentVoltaStart !== null) {
312
+ for (let i = currentVoltaStart; i <= volta.measureIndex; i++) {
313
+ voltaRanges.set(i, currentVoltaNumbers);
314
+ }
315
+ currentVoltaStart = null;
316
+ }
317
+ }
318
+ if (currentVoltaStart !== null) {
319
+ for (let i = currentVoltaStart; i < measureCount; i++) {
320
+ voltaRanges.set(i, currentVoltaNumbers);
321
+ }
322
+ }
323
+ return voltaRanges;
324
+ }
325
+ function generatePlaybackSequence(score, options) {
326
+ const partIndex = options?.partIndex ?? 0;
327
+ const part = score.parts[partIndex];
328
+ if (!part) return [];
329
+ const measureCount = part.measures.length;
330
+ const sequence = [];
331
+ if (measureCount === 0) return sequence;
332
+ const controls = extractPlaybackControls(part);
333
+ if (controls.repeatStarts.length === 0 && controls.repeatEnds.length === 0 && controls.voltas.length === 0 && controls.jumps.length === 0) {
334
+ for (let i = 0; i < measureCount; i++) {
335
+ sequence.push({ measureIndex: i, repeatIteration: 0 });
336
+ }
337
+ return sequence;
338
+ }
339
+ const voltaRanges = buildVoltaRanges(controls.voltas, measureCount);
340
+ const sortedRepeatStarts = [...controls.repeatStarts].sort((a, b) => a - b);
341
+ const sortedRepeatEnds = [...controls.repeatEnds].sort((a, b) => a.measureIndex - b.measureIndex);
342
+ const repeatSections = /* @__PURE__ */ new Map();
343
+ const voltaToRepeatEnd = /* @__PURE__ */ new Map();
344
+ const usedRepeatStarts = /* @__PURE__ */ new Set();
345
+ let lastRepeatEndMeasure = -1;
346
+ for (const repeatEnd of sortedRepeatEnds) {
347
+ let startIndex = null;
348
+ for (const startMeasure of sortedRepeatStarts) {
349
+ if (startMeasure <= repeatEnd.measureIndex && startMeasure > lastRepeatEndMeasure && !usedRepeatStarts.has(startMeasure)) {
350
+ startIndex = startMeasure;
351
+ } else if (startMeasure > repeatEnd.measureIndex) {
352
+ break;
353
+ }
354
+ }
355
+ if (startIndex === null) {
356
+ startIndex = lastRepeatEndMeasure + 1;
357
+ } else {
358
+ usedRepeatStarts.add(startIndex);
359
+ }
360
+ repeatSections.set(repeatEnd.measureIndex, {
361
+ startIndex,
362
+ times: repeatEnd.times
363
+ });
364
+ lastRepeatEndMeasure = repeatEnd.measureIndex;
365
+ voltaRanges.forEach((_iterations, voltaMeasure) => {
366
+ const isInsideRepeat = voltaMeasure >= startIndex && voltaMeasure <= repeatEnd.measureIndex;
367
+ const isImmediatelyAfter = voltaMeasure > repeatEnd.measureIndex && !voltaToRepeatEnd.has(voltaMeasure);
368
+ if (isInsideRepeat) {
369
+ voltaToRepeatEnd.set(voltaMeasure, repeatEnd.measureIndex);
370
+ } else if (isImmediatelyAfter) {
371
+ let belongsToLaterRepeat = false;
372
+ for (const otherEnd of sortedRepeatEnds) {
373
+ if (otherEnd.measureIndex > repeatEnd.measureIndex) {
374
+ const otherSection = repeatSections.get(otherEnd.measureIndex);
375
+ if (otherSection && voltaMeasure >= otherSection.startIndex && voltaMeasure <= otherEnd.measureIndex) {
376
+ belongsToLaterRepeat = true;
377
+ break;
378
+ }
379
+ }
380
+ }
381
+ if (!belongsToLaterRepeat) {
382
+ voltaToRepeatEnd.set(voltaMeasure, repeatEnd.measureIndex);
383
+ }
384
+ }
385
+ });
386
+ }
387
+ let currentMeasure = 0;
388
+ const repeatCounts = /* @__PURE__ */ new Map();
389
+ let lastRepeatEndIndex = null;
390
+ let lastRepeatIteration = 0;
391
+ let inRepeatJump = false;
392
+ let stopAtFine = false;
393
+ let jumpToCoda = false;
394
+ const maxIterations = measureCount * 10 + 10;
395
+ let iterations = 0;
396
+ while (currentMeasure < measureCount && iterations < maxIterations) {
397
+ iterations++;
398
+ const voltaIterations = voltaRanges.get(currentMeasure);
399
+ let repeatIteration = 0;
400
+ let inRepeatSection = false;
401
+ let currentRepeatEndIndex = null;
402
+ for (const [endIndex, section] of repeatSections) {
403
+ if (currentMeasure >= section.startIndex && currentMeasure <= endIndex) {
404
+ inRepeatSection = true;
405
+ currentRepeatEndIndex = endIndex;
406
+ repeatIteration = repeatCounts.get(endIndex) || 1;
407
+ break;
408
+ }
409
+ }
410
+ if (inRepeatSection && currentRepeatEndIndex !== null) {
411
+ lastRepeatEndIndex = currentRepeatEndIndex;
412
+ lastRepeatIteration = repeatIteration;
413
+ }
414
+ if (voltaIterations) {
415
+ let effectiveIteration = repeatIteration;
416
+ if (!inRepeatSection) {
417
+ const associatedRepeatEnd = voltaToRepeatEnd.get(currentMeasure);
418
+ if (associatedRepeatEnd !== void 0) {
419
+ effectiveIteration = repeatCounts.get(associatedRepeatEnd) || 1;
420
+ } else if (lastRepeatEndIndex !== null) {
421
+ effectiveIteration = lastRepeatIteration;
422
+ }
423
+ }
424
+ if (!voltaIterations.includes(effectiveIteration)) {
425
+ currentMeasure++;
426
+ continue;
427
+ }
428
+ }
429
+ sequence.push({
430
+ measureIndex: currentMeasure,
431
+ repeatIteration: inRepeatSection ? repeatIteration : 0
432
+ });
433
+ if (stopAtFine && controls.fineIndex === currentMeasure) {
434
+ break;
435
+ }
436
+ if (jumpToCoda && controls.toCodaIndex === currentMeasure && controls.codaIndex !== null) {
437
+ currentMeasure = controls.codaIndex;
438
+ jumpToCoda = false;
439
+ continue;
440
+ }
441
+ const repeatSection = repeatSections.get(currentMeasure);
442
+ if (repeatSection) {
443
+ const currentCount = repeatCounts.get(currentMeasure) || 1;
444
+ if (currentCount < repeatSection.times) {
445
+ repeatCounts.set(currentMeasure, currentCount + 1);
446
+ currentMeasure = repeatSection.startIndex;
447
+ continue;
448
+ }
449
+ }
450
+ if (voltaIterations && !inRepeatSection) {
451
+ const associatedRepeatEnd = voltaToRepeatEnd.get(currentMeasure);
452
+ if (associatedRepeatEnd !== void 0) {
453
+ const repeatSectionForVolta = repeatSections.get(associatedRepeatEnd);
454
+ if (repeatSectionForVolta) {
455
+ const currentCount = repeatCounts.get(associatedRepeatEnd) || 1;
456
+ if (currentCount < repeatSectionForVolta.times) {
457
+ repeatCounts.set(associatedRepeatEnd, currentCount + 1);
458
+ currentMeasure = repeatSectionForVolta.startIndex;
459
+ continue;
460
+ }
461
+ }
462
+ }
463
+ }
464
+ const jumpAtMeasure = controls.jumps.find((j) => j.measureIndex === currentMeasure);
465
+ if (jumpAtMeasure && !inRepeatJump) {
466
+ inRepeatJump = true;
467
+ switch (jumpAtMeasure.type) {
468
+ case "dc":
469
+ currentMeasure = 0;
470
+ continue;
471
+ case "dc_al_fine":
472
+ currentMeasure = 0;
473
+ stopAtFine = true;
474
+ continue;
475
+ case "dc_al_coda":
476
+ currentMeasure = 0;
477
+ jumpToCoda = true;
478
+ continue;
479
+ case "ds":
480
+ if (controls.segnoIndex !== null) {
481
+ currentMeasure = controls.segnoIndex;
482
+ continue;
483
+ }
484
+ break;
485
+ case "ds_al_fine":
486
+ if (controls.segnoIndex !== null) {
487
+ currentMeasure = controls.segnoIndex;
488
+ stopAtFine = true;
489
+ continue;
490
+ }
491
+ break;
492
+ case "ds_al_coda":
493
+ if (controls.segnoIndex !== null) {
494
+ currentMeasure = controls.segnoIndex;
495
+ jumpToCoda = true;
496
+ continue;
497
+ }
498
+ break;
499
+ }
500
+ }
501
+ currentMeasure++;
502
+ }
503
+ return sequence;
504
+ }
505
+ function hasPlaybackControls(score, options) {
506
+ const part = score.parts[options?.partIndex ?? 0];
507
+ if (!part) return false;
508
+ const controls = extractPlaybackControls(part);
509
+ return controls.repeatStarts.length > 0 || controls.repeatEnds.length > 0 || controls.voltas.length > 0 || controls.jumps.length > 0 || controls.segnoIndex !== null || controls.codaIndex !== null;
510
+ }
511
+
174
512
  // src/query/index.ts
175
513
  function getNotesForVoice(measure, filter) {
176
514
  return measure.entries.filter((entry) => {
@@ -1721,6 +2059,9 @@ function pitchesEqual(a, b) {
1721
2059
  }
1722
2060
 
1723
2061
  export {
2062
+ extractPlaybackControls,
2063
+ generatePlaybackSequence,
2064
+ hasPlaybackControls,
1724
2065
  STEPS,
1725
2066
  STEP_SEMITONES,
1726
2067
  pitchToSemitone,