musicxml-io 0.1.0 → 0.2.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.
@@ -41,10 +41,1353 @@ function getMeasureEndPosition(measure) {
41
41
  return state.position;
42
42
  }
43
43
 
44
+ // src/validator/index.ts
45
+ var DEFAULT_OPTIONS = {
46
+ checkDivisions: true,
47
+ checkMeasureDuration: true,
48
+ checkMeasureFullness: false,
49
+ // Piano Roll semantics - opt-in
50
+ checkPosition: true,
51
+ checkTies: true,
52
+ checkBeams: true,
53
+ checkSlurs: true,
54
+ checkTuplets: true,
55
+ checkPartReferences: true,
56
+ checkPartStructure: true,
57
+ checkVoiceStaff: true,
58
+ checkStaffStructure: true,
59
+ durationTolerance: 0
60
+ };
61
+ function validate(score, options = {}) {
62
+ const opts = { ...DEFAULT_OPTIONS, ...options };
63
+ const allErrors = [];
64
+ if (opts.checkPartReferences) {
65
+ allErrors.push(...validatePartReferences(score));
66
+ }
67
+ if (opts.checkPartStructure) {
68
+ allErrors.push(...validatePartStructure(score));
69
+ }
70
+ if (opts.checkDivisions) {
71
+ allErrors.push(...validateDivisions(score));
72
+ }
73
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
74
+ const part = score.parts[partIndex];
75
+ let currentDivisions = 1;
76
+ let currentTime;
77
+ let currentStaves = 1;
78
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
79
+ const measure = part.measures[measureIndex];
80
+ const location = {
81
+ partIndex,
82
+ partId: part.id,
83
+ measureIndex,
84
+ measureNumber: measure.number
85
+ };
86
+ if (measure.attributes) {
87
+ if (measure.attributes.divisions !== void 0) {
88
+ currentDivisions = measure.attributes.divisions;
89
+ }
90
+ if (measure.attributes.time !== void 0) {
91
+ currentTime = measure.attributes.time;
92
+ }
93
+ if (measure.attributes.staves !== void 0) {
94
+ currentStaves = measure.attributes.staves;
95
+ }
96
+ }
97
+ if (opts.checkMeasureDuration && currentTime) {
98
+ allErrors.push(...validateMeasureDuration(
99
+ measure,
100
+ currentDivisions,
101
+ currentTime,
102
+ location,
103
+ opts.durationTolerance
104
+ ));
105
+ }
106
+ if (opts.checkMeasureFullness && currentTime) {
107
+ allErrors.push(...validateMeasureFullness(
108
+ measure,
109
+ currentDivisions,
110
+ currentTime,
111
+ location
112
+ ));
113
+ }
114
+ if (opts.checkPosition) {
115
+ allErrors.push(...validateBackupForward(measure, location));
116
+ }
117
+ if (opts.checkTies) {
118
+ allErrors.push(...validateTies(measure, location));
119
+ }
120
+ if (opts.checkBeams) {
121
+ allErrors.push(...validateBeams(measure, location));
122
+ }
123
+ if (opts.checkSlurs) {
124
+ allErrors.push(...validateSlurs(measure, location));
125
+ }
126
+ if (opts.checkTuplets) {
127
+ allErrors.push(...validateTuplets(measure, location));
128
+ }
129
+ if (opts.checkVoiceStaff) {
130
+ allErrors.push(...validateVoiceStaff(measure, currentStaves, location));
131
+ }
132
+ }
133
+ if (opts.checkStaffStructure) {
134
+ allErrors.push(...validateStaffStructure(part, partIndex));
135
+ }
136
+ }
137
+ const errors = allErrors.filter((e) => e.level === "error");
138
+ const warnings = allErrors.filter((e) => e.level === "warning");
139
+ const infos = allErrors.filter((e) => e.level === "info");
140
+ return {
141
+ valid: errors.length === 0,
142
+ errors,
143
+ warnings,
144
+ infos
145
+ };
146
+ }
147
+ function validateDivisions(score) {
148
+ const errors = [];
149
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
150
+ const part = score.parts[partIndex];
151
+ let hasDivisions = false;
152
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
153
+ const measure = part.measures[measureIndex];
154
+ if (measure.attributes?.divisions !== void 0) {
155
+ hasDivisions = true;
156
+ if (measure.attributes.divisions <= 0) {
157
+ errors.push({
158
+ code: "INVALID_DIVISIONS",
159
+ level: "error",
160
+ message: `Invalid divisions value: ${measure.attributes.divisions}. Must be positive.`,
161
+ location: {
162
+ partIndex,
163
+ partId: part.id,
164
+ measureIndex,
165
+ measureNumber: measure.number
166
+ },
167
+ details: { divisions: measure.attributes.divisions }
168
+ });
169
+ }
170
+ }
171
+ if (!hasDivisions) {
172
+ const hasNotes = measure.entries.some((e) => e.type === "note");
173
+ if (hasNotes) {
174
+ errors.push({
175
+ code: "MISSING_DIVISIONS",
176
+ level: "error",
177
+ message: "Notes found before divisions are defined",
178
+ location: {
179
+ partIndex,
180
+ partId: part.id,
181
+ measureIndex,
182
+ measureNumber: measure.number
183
+ }
184
+ });
185
+ hasDivisions = true;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return errors;
191
+ }
192
+ function validateMeasureDuration(measure, divisions, time, location, tolerance = 0) {
193
+ const errors = [];
194
+ if (time.senzaMisura) {
195
+ return errors;
196
+ }
197
+ const beats = parseInt(time.beats, 10);
198
+ if (isNaN(beats)) {
199
+ return errors;
200
+ }
201
+ const expectedDuration = beats / time.beatType * 4 * divisions;
202
+ const voiceDurations = calculateVoiceDurations(measure);
203
+ for (const [voiceKey, actualDuration] of voiceDurations.entries()) {
204
+ const [staff, voice] = voiceKey.split("-").map(Number);
205
+ const diff = actualDuration - expectedDuration;
206
+ if (Math.abs(diff) > tolerance) {
207
+ if (diff > 0) {
208
+ errors.push({
209
+ code: "MEASURE_DURATION_OVERFLOW",
210
+ level: "error",
211
+ message: `Voice ${voice} (staff ${staff}) duration ${actualDuration} exceeds expected ${expectedDuration}`,
212
+ location: { ...location, voice, staff },
213
+ details: {
214
+ expected: expectedDuration,
215
+ actual: actualDuration,
216
+ difference: diff
217
+ }
218
+ });
219
+ } else {
220
+ errors.push({
221
+ code: "MEASURE_DURATION_UNDERFLOW",
222
+ level: "warning",
223
+ message: `Voice ${voice} (staff ${staff}) duration ${actualDuration} is less than expected ${expectedDuration}`,
224
+ location: { ...location, voice, staff },
225
+ details: {
226
+ expected: expectedDuration,
227
+ actual: actualDuration,
228
+ difference: diff
229
+ }
230
+ });
231
+ }
232
+ }
233
+ }
234
+ return errors;
235
+ }
236
+ function calculateVoiceDurations(measure) {
237
+ const voiceDurations = /* @__PURE__ */ new Map();
238
+ let currentPosition = 0;
239
+ const voiceMaxPositions = /* @__PURE__ */ new Map();
240
+ for (const entry of measure.entries) {
241
+ if (entry.type === "note") {
242
+ const staff = entry.staff ?? 1;
243
+ const voice = entry.voice;
244
+ const key = `${staff}-${voice}`;
245
+ if (!entry.chord) {
246
+ const endPosition = currentPosition + entry.duration;
247
+ const currentMax = voiceMaxPositions.get(key) ?? 0;
248
+ voiceMaxPositions.set(key, Math.max(currentMax, endPosition));
249
+ currentPosition = endPosition;
250
+ }
251
+ } else if (entry.type === "backup") {
252
+ currentPosition -= entry.duration;
253
+ } else if (entry.type === "forward") {
254
+ currentPosition += entry.duration;
255
+ }
256
+ }
257
+ for (const [key, maxPos] of voiceMaxPositions.entries()) {
258
+ voiceDurations.set(key, maxPos);
259
+ }
260
+ return voiceDurations;
261
+ }
262
+ function validateMeasureFullness(measure, divisions, time, location) {
263
+ const errors = [];
264
+ if (time.senzaMisura) {
265
+ return errors;
266
+ }
267
+ const beats = parseInt(time.beats, 10);
268
+ if (isNaN(beats)) {
269
+ return errors;
270
+ }
271
+ const expectedDuration = beats / time.beatType * 4 * divisions;
272
+ const voiceCoverage = /* @__PURE__ */ new Map();
273
+ let currentPosition = 0;
274
+ for (const entry of measure.entries) {
275
+ if (entry.type === "note") {
276
+ const staff = entry.staff ?? 1;
277
+ const voice = entry.voice;
278
+ const key = `${staff}-${voice}`;
279
+ if (!entry.chord) {
280
+ if (!voiceCoverage.has(key)) {
281
+ voiceCoverage.set(key, { segments: [] });
282
+ }
283
+ voiceCoverage.get(key).segments.push({
284
+ start: currentPosition,
285
+ end: currentPosition + entry.duration
286
+ });
287
+ currentPosition += entry.duration;
288
+ }
289
+ } else if (entry.type === "backup") {
290
+ currentPosition -= entry.duration;
291
+ } else if (entry.type === "forward") {
292
+ const staff = entry.staff ?? 1;
293
+ const voice = entry.voice ?? 1;
294
+ const key = `${staff}-${voice}`;
295
+ if (!voiceCoverage.has(key)) {
296
+ voiceCoverage.set(key, { segments: [] });
297
+ }
298
+ voiceCoverage.get(key).segments.push({
299
+ start: currentPosition,
300
+ end: currentPosition + entry.duration
301
+ });
302
+ currentPosition += entry.duration;
303
+ }
304
+ }
305
+ for (const [voiceKey, { segments }] of voiceCoverage.entries()) {
306
+ const [staff, voice] = voiceKey.split("-").map(Number);
307
+ const sorted = [...segments].sort((a, b) => a.start - b.start);
308
+ let lastEnd = 0;
309
+ const gaps = [];
310
+ for (const seg of sorted) {
311
+ if (seg.start > lastEnd) {
312
+ gaps.push({ start: lastEnd, end: seg.start });
313
+ }
314
+ lastEnd = Math.max(lastEnd, seg.end);
315
+ }
316
+ if (lastEnd < expectedDuration) {
317
+ gaps.push({ start: lastEnd, end: expectedDuration });
318
+ }
319
+ for (const gap of gaps) {
320
+ errors.push({
321
+ code: "VOICE_GAP",
322
+ level: "warning",
323
+ message: `Voice ${voice} (staff ${staff}) has gap from position ${gap.start} to ${gap.end}`,
324
+ location: { ...location, voice, staff },
325
+ details: {
326
+ gapStart: gap.start,
327
+ gapEnd: gap.end,
328
+ gapDuration: gap.end - gap.start
329
+ }
330
+ });
331
+ }
332
+ if (lastEnd < expectedDuration) {
333
+ errors.push({
334
+ code: "VOICE_INCOMPLETE",
335
+ level: "warning",
336
+ message: `Voice ${voice} (staff ${staff}) ends at ${lastEnd}, expected ${expectedDuration}`,
337
+ location: { ...location, voice, staff },
338
+ details: {
339
+ actualEnd: lastEnd,
340
+ expectedDuration,
341
+ missing: expectedDuration - lastEnd
342
+ }
343
+ });
344
+ }
345
+ }
346
+ return errors;
347
+ }
348
+ function validateBackupForward(measure, location) {
349
+ const errors = [];
350
+ let position = 0;
351
+ let minPosition = 0;
352
+ for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
353
+ const entry = measure.entries[entryIndex];
354
+ if (entry.type === "note") {
355
+ if (!entry.chord) {
356
+ position += entry.duration;
357
+ }
358
+ } else if (entry.type === "backup") {
359
+ const newPosition = position - entry.duration;
360
+ if (newPosition < 0) {
361
+ errors.push({
362
+ code: "BACKUP_EXCEEDS_POSITION",
363
+ level: "error",
364
+ message: `Backup of ${entry.duration} at position ${position} results in negative position ${newPosition}`,
365
+ location: { ...location, entryIndex },
366
+ details: {
367
+ backupDuration: entry.duration,
368
+ positionBefore: position,
369
+ positionAfter: newPosition
370
+ }
371
+ });
372
+ }
373
+ position = newPosition;
374
+ minPosition = Math.min(minPosition, position);
375
+ } else if (entry.type === "forward") {
376
+ position += entry.duration;
377
+ }
378
+ }
379
+ if (minPosition < 0) {
380
+ errors.push({
381
+ code: "NEGATIVE_POSITION",
382
+ level: "error",
383
+ message: `Position went negative (min: ${minPosition}) in measure`,
384
+ location,
385
+ details: { minPosition }
386
+ });
387
+ }
388
+ return errors;
389
+ }
390
+ function validateTies(measure, location) {
391
+ const errors = [];
392
+ const openTies = /* @__PURE__ */ new Map();
393
+ for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
394
+ const entry = measure.entries[entryIndex];
395
+ if (entry.type !== "note" || !entry.pitch) continue;
396
+ const pitchKey = `${entry.pitch.step}${entry.pitch.octave}${entry.pitch.alter ?? 0}-${entry.voice}-${entry.staff ?? 1}`;
397
+ const ties = entry.ties ?? (entry.tie ? [entry.tie] : []);
398
+ for (const tie of ties) {
399
+ if (tie.type === "start") {
400
+ if (openTies.has(pitchKey)) {
401
+ }
402
+ openTies.set(pitchKey, { entryIndex, pitch: entry.pitch });
403
+ } else if (tie.type === "stop") {
404
+ if (!openTies.has(pitchKey)) {
405
+ errors.push({
406
+ code: "TIE_STOP_WITHOUT_START",
407
+ level: "warning",
408
+ message: `Tie stop without matching start for ${entry.pitch.step}${entry.pitch.octave}`,
409
+ location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
410
+ details: { pitch: entry.pitch }
411
+ });
412
+ } else {
413
+ openTies.delete(pitchKey);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ return errors;
419
+ }
420
+ function validateBeams(measure, location) {
421
+ const errors = [];
422
+ const openBeams = /* @__PURE__ */ new Map();
423
+ for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
424
+ const entry = measure.entries[entryIndex];
425
+ if (entry.type !== "note" || !entry.beam) continue;
426
+ for (const beam of entry.beam) {
427
+ const beamKey = `${beam.number}-${entry.voice}-${entry.staff ?? 1}`;
428
+ if (beam.type === "begin") {
429
+ if (openBeams.has(beamKey)) {
430
+ errors.push({
431
+ code: "BEAM_BEGIN_WITHOUT_END",
432
+ level: "error",
433
+ message: `Beam ${beam.number} started again before previous beam ended`,
434
+ location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
435
+ details: { beamNumber: beam.number }
436
+ });
437
+ }
438
+ openBeams.set(beamKey, entryIndex);
439
+ } else if (beam.type === "end") {
440
+ if (!openBeams.has(beamKey)) {
441
+ errors.push({
442
+ code: "BEAM_END_WITHOUT_BEGIN",
443
+ level: "error",
444
+ message: `Beam ${beam.number} end without matching begin`,
445
+ location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
446
+ details: { beamNumber: beam.number }
447
+ });
448
+ } else {
449
+ openBeams.delete(beamKey);
450
+ }
451
+ }
452
+ }
453
+ }
454
+ for (const [beamKey, startIndex] of openBeams.entries()) {
455
+ const [beamNumber, voice, staff] = beamKey.split("-").map(Number);
456
+ errors.push({
457
+ code: "BEAM_BEGIN_WITHOUT_END",
458
+ level: "error",
459
+ message: `Beam ${beamNumber} started but never ended in measure`,
460
+ location: { ...location, entryIndex: startIndex, voice, staff },
461
+ details: { beamNumber }
462
+ });
463
+ }
464
+ return errors;
465
+ }
466
+ function validateSlurs(measure, _location) {
467
+ const errors = [];
468
+ const openSlurs = /* @__PURE__ */ new Map();
469
+ for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
470
+ const entry = measure.entries[entryIndex];
471
+ if (entry.type !== "note" || !entry.notations) continue;
472
+ for (const notation of entry.notations) {
473
+ if (notation.type !== "slur") continue;
474
+ const slurNumber = notation.number ?? 1;
475
+ const slurKey = `${slurNumber}-${entry.voice}-${entry.staff ?? 1}`;
476
+ if (notation.slurType === "start") {
477
+ if (openSlurs.has(slurKey)) {
478
+ }
479
+ openSlurs.set(slurKey, entryIndex);
480
+ } else if (notation.slurType === "stop") {
481
+ if (!openSlurs.has(slurKey)) {
482
+ } else {
483
+ openSlurs.delete(slurKey);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ return errors;
489
+ }
490
+ function validateTuplets(measure, location) {
491
+ const errors = [];
492
+ const openTuplets = /* @__PURE__ */ new Map();
493
+ for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
494
+ const entry = measure.entries[entryIndex];
495
+ if (entry.type !== "note" || !entry.notations) continue;
496
+ for (const notation of entry.notations) {
497
+ if (notation.type !== "tuplet") continue;
498
+ const tupletNumber = notation.number ?? 1;
499
+ const tupletKey = `${tupletNumber}-${entry.voice}-${entry.staff ?? 1}`;
500
+ if (notation.tupletType === "start") {
501
+ if (openTuplets.has(tupletKey)) {
502
+ errors.push({
503
+ code: "TUPLET_START_WITHOUT_STOP",
504
+ level: "error",
505
+ message: `Tuplet ${tupletNumber} started again before previous tuplet ended`,
506
+ location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
507
+ details: { tupletNumber }
508
+ });
509
+ }
510
+ openTuplets.set(tupletKey, entryIndex);
511
+ } else if (notation.tupletType === "stop") {
512
+ if (!openTuplets.has(tupletKey)) {
513
+ errors.push({
514
+ code: "TUPLET_STOP_WITHOUT_START",
515
+ level: "error",
516
+ message: `Tuplet ${tupletNumber} stop without matching start`,
517
+ location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
518
+ details: { tupletNumber }
519
+ });
520
+ } else {
521
+ openTuplets.delete(tupletKey);
522
+ }
523
+ }
524
+ }
525
+ }
526
+ for (const [tupletKey, startIndex] of openTuplets.entries()) {
527
+ const [tupletNumber, voice, staff] = tupletKey.split("-").map(Number);
528
+ errors.push({
529
+ code: "TUPLET_START_WITHOUT_STOP",
530
+ level: "error",
531
+ message: `Tuplet ${tupletNumber} started but never ended in measure`,
532
+ location: { ...location, entryIndex: startIndex, voice, staff },
533
+ details: { tupletNumber }
534
+ });
535
+ }
536
+ return errors;
537
+ }
538
+ function validatePartReferences(score) {
539
+ const errors = [];
540
+ const partListIds = /* @__PURE__ */ new Set();
541
+ for (const entry of score.partList) {
542
+ if (entry.type === "score-part") {
543
+ partListIds.add(entry.id);
544
+ }
545
+ }
546
+ const partIds = /* @__PURE__ */ new Set();
547
+ for (const part of score.parts) {
548
+ partIds.add(part.id);
549
+ }
550
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
551
+ const part = score.parts[partIndex];
552
+ if (!partListIds.has(part.id)) {
553
+ errors.push({
554
+ code: "PART_ID_NOT_IN_PART_LIST",
555
+ level: "error",
556
+ message: `Part "${part.id}" is not defined in partList`,
557
+ location: { partIndex, partId: part.id }
558
+ });
559
+ }
560
+ }
561
+ for (const entry of score.partList) {
562
+ if (entry.type === "score-part" && !partIds.has(entry.id)) {
563
+ errors.push({
564
+ code: "PART_LIST_ID_NOT_IN_PARTS",
565
+ level: "error",
566
+ message: `PartList entry "${entry.id}" has no corresponding part`,
567
+ location: { partId: entry.id }
568
+ });
569
+ }
570
+ }
571
+ return errors;
572
+ }
573
+ function validateVoiceStaff(measure, staves, location) {
574
+ const errors = [];
575
+ for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
576
+ const entry = measure.entries[entryIndex];
577
+ if (entry.type !== "note") continue;
578
+ if (entry.voice <= 0) {
579
+ errors.push({
580
+ code: "INVALID_VOICE_NUMBER",
581
+ level: "error",
582
+ message: `Invalid voice number: ${entry.voice}. Must be positive.`,
583
+ location: { ...location, entryIndex, voice: entry.voice }
584
+ });
585
+ }
586
+ const staff = entry.staff ?? 1;
587
+ if (staff <= 0) {
588
+ errors.push({
589
+ code: "INVALID_STAFF_NUMBER",
590
+ level: "error",
591
+ message: `Invalid staff number: ${staff}. Must be positive.`,
592
+ location: { ...location, entryIndex, staff }
593
+ });
594
+ } else if (staff > staves) {
595
+ errors.push({
596
+ code: "STAFF_EXCEEDS_STAVES",
597
+ level: "error",
598
+ message: `Staff number ${staff} exceeds declared staves count ${staves}`,
599
+ location: { ...location, entryIndex, staff },
600
+ details: { declaredStaves: staves }
601
+ });
602
+ }
603
+ if (entry.duration < 0) {
604
+ errors.push({
605
+ code: "INVALID_DURATION",
606
+ level: "error",
607
+ message: `Invalid duration: ${entry.duration}. Must be non-negative.`,
608
+ location: { ...location, entryIndex },
609
+ details: { duration: entry.duration }
610
+ });
611
+ }
612
+ }
613
+ return errors;
614
+ }
615
+ function validatePartStructure(score) {
616
+ const errors = [];
617
+ if (score.parts.length === 0) {
618
+ return errors;
619
+ }
620
+ const partIds = /* @__PURE__ */ new Map();
621
+ for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
622
+ const part = score.parts[partIndex];
623
+ if (partIds.has(part.id)) {
624
+ errors.push({
625
+ code: "DUPLICATE_PART_ID",
626
+ level: "error",
627
+ message: `Duplicate part ID "${part.id}" found at index ${partIndex} (first at index ${partIds.get(part.id)})`,
628
+ location: { partIndex, partId: part.id },
629
+ details: { firstIndex: partIds.get(part.id) }
630
+ });
631
+ } else {
632
+ partIds.set(part.id, partIndex);
633
+ }
634
+ }
635
+ const referencePart = score.parts[0];
636
+ const referenceMeasureCount = referencePart.measures.length;
637
+ for (let partIndex = 1; partIndex < score.parts.length; partIndex++) {
638
+ const part = score.parts[partIndex];
639
+ if (part.measures.length !== referenceMeasureCount) {
640
+ errors.push({
641
+ code: "PART_MEASURE_COUNT_MISMATCH",
642
+ level: "error",
643
+ message: `Part "${part.id}" has ${part.measures.length} measures, expected ${referenceMeasureCount} (same as first part)`,
644
+ location: { partIndex, partId: part.id },
645
+ details: {
646
+ expected: referenceMeasureCount,
647
+ actual: part.measures.length
648
+ }
649
+ });
650
+ }
651
+ const minLength = Math.min(part.measures.length, referenceMeasureCount);
652
+ for (let measureIndex = 0; measureIndex < minLength; measureIndex++) {
653
+ const refMeasure = referencePart.measures[measureIndex];
654
+ const partMeasure = part.measures[measureIndex];
655
+ if (refMeasure.number !== partMeasure.number) {
656
+ errors.push({
657
+ code: "PART_MEASURE_NUMBER_MISMATCH",
658
+ level: "warning",
659
+ message: `Part "${part.id}" measure at index ${measureIndex} has number "${partMeasure.number}", expected "${refMeasure.number}"`,
660
+ location: {
661
+ partIndex,
662
+ partId: part.id,
663
+ measureIndex,
664
+ measureNumber: partMeasure.number
665
+ },
666
+ details: {
667
+ expected: refMeasure.number,
668
+ actual: partMeasure.number
669
+ }
670
+ });
671
+ }
672
+ }
673
+ }
674
+ const openGroups = /* @__PURE__ */ new Map();
675
+ for (let i = 0; i < score.partList.length; i++) {
676
+ const entry = score.partList[i];
677
+ if (entry.type !== "part-group") continue;
678
+ const groupNumber = entry.number ?? 1;
679
+ if (entry.groupType === "start") {
680
+ if (openGroups.has(groupNumber)) {
681
+ errors.push({
682
+ code: "PART_GROUP_START_WITHOUT_STOP",
683
+ level: "error",
684
+ message: `Part group ${groupNumber} started again at index ${i} before previous group ended`,
685
+ location: {},
686
+ details: { groupNumber, partListIndex: i }
687
+ });
688
+ }
689
+ openGroups.set(groupNumber, i);
690
+ } else if (entry.groupType === "stop") {
691
+ if (!openGroups.has(groupNumber)) {
692
+ errors.push({
693
+ code: "PART_GROUP_STOP_WITHOUT_START",
694
+ level: "error",
695
+ message: `Part group ${groupNumber} stop at index ${i} without matching start`,
696
+ location: {},
697
+ details: { groupNumber, partListIndex: i }
698
+ });
699
+ } else {
700
+ openGroups.delete(groupNumber);
701
+ }
702
+ }
703
+ }
704
+ for (const [groupNumber, startIndex] of openGroups.entries()) {
705
+ errors.push({
706
+ code: "PART_GROUP_START_WITHOUT_STOP",
707
+ level: "error",
708
+ message: `Part group ${groupNumber} started at index ${startIndex} but never stopped`,
709
+ location: {},
710
+ details: { groupNumber, partListIndex: startIndex }
711
+ });
712
+ }
713
+ return errors;
714
+ }
715
+ function validateStaffStructure(part, partIndex) {
716
+ const errors = [];
717
+ let currentStaves = void 0;
718
+ let stavesDeclarationMeasure = void 0;
719
+ const clefsDeclaredForStaves = /* @__PURE__ */ new Set();
720
+ for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
721
+ const measure = part.measures[measureIndex];
722
+ const location = {
723
+ partIndex,
724
+ partId: part.id,
725
+ measureIndex,
726
+ measureNumber: measure.number
727
+ };
728
+ if (measure.attributes?.staves !== void 0) {
729
+ const newStaves = measure.attributes.staves;
730
+ if (currentStaves !== void 0 && newStaves !== currentStaves) {
731
+ errors.push({
732
+ code: "STAVES_DECLARATION_MISMATCH",
733
+ level: "info",
734
+ message: `Staves count changed from ${currentStaves} to ${newStaves}`,
735
+ location,
736
+ details: {
737
+ previous: currentStaves,
738
+ new: newStaves,
739
+ previousMeasure: stavesDeclarationMeasure
740
+ }
741
+ });
742
+ clefsDeclaredForStaves.clear();
743
+ }
744
+ currentStaves = newStaves;
745
+ stavesDeclarationMeasure = measure.number;
746
+ }
747
+ if (measure.attributes?.clef) {
748
+ for (const clef of measure.attributes.clef) {
749
+ const staffNum = clef.staff ?? 1;
750
+ clefsDeclaredForStaves.add(staffNum);
751
+ if (currentStaves !== void 0 && staffNum > currentStaves) {
752
+ errors.push({
753
+ code: "CLEF_STAFF_EXCEEDS_STAVES",
754
+ level: "error",
755
+ message: `Clef declared for staff ${staffNum}, but only ${currentStaves} staves declared`,
756
+ location,
757
+ details: {
758
+ clefStaff: staffNum,
759
+ declaredStaves: currentStaves
760
+ }
761
+ });
762
+ }
763
+ }
764
+ }
765
+ if (currentStaves !== void 0) {
766
+ const usedStaves = /* @__PURE__ */ new Set();
767
+ for (const entry of measure.entries) {
768
+ if (entry.type === "note") {
769
+ usedStaves.add(entry.staff ?? 1);
770
+ } else if (entry.type === "forward" && entry.staff) {
771
+ usedStaves.add(entry.staff);
772
+ }
773
+ }
774
+ for (const usedStaff of usedStaves) {
775
+ if (usedStaff > currentStaves) {
776
+ }
777
+ }
778
+ }
779
+ }
780
+ if (currentStaves !== void 0 && currentStaves > 1) {
781
+ for (let staff = 1; staff <= currentStaves; staff++) {
782
+ if (!clefsDeclaredForStaves.has(staff)) {
783
+ errors.push({
784
+ code: "MISSING_CLEF_FOR_STAFF",
785
+ level: "warning",
786
+ message: `No clef declared for staff ${staff} in part "${part.id}"`,
787
+ location: { partIndex, partId: part.id },
788
+ details: { staff, totalStaves: currentStaves }
789
+ });
790
+ }
791
+ }
792
+ }
793
+ const allUsedStaves = /* @__PURE__ */ new Set();
794
+ for (const measure of part.measures) {
795
+ for (const entry of measure.entries) {
796
+ if (entry.type === "note" && entry.staff !== void 0) {
797
+ allUsedStaves.add(entry.staff);
798
+ }
799
+ }
800
+ }
801
+ if (allUsedStaves.size > 1 && currentStaves === void 0) {
802
+ errors.push({
803
+ code: "MISSING_STAVES_DECLARATION",
804
+ level: "warning",
805
+ message: `Part "${part.id}" uses staff numbers ${Array.from(allUsedStaves).sort().join(", ")} but has no staves declaration`,
806
+ location: { partIndex, partId: part.id },
807
+ details: { usedStaves: Array.from(allUsedStaves).sort() }
808
+ });
809
+ }
810
+ return errors;
811
+ }
812
+ var DEFAULT_LOCAL_OPTIONS = {
813
+ checkMeasureDuration: true,
814
+ checkMeasureFullness: false,
815
+ // Piano Roll semantics - opt-in
816
+ checkPosition: true,
817
+ checkBeams: true,
818
+ checkTuplets: true,
819
+ checkVoiceStaff: true,
820
+ durationTolerance: 0
821
+ };
822
+ function validateMeasureLocal(measure, context, options = {}) {
823
+ const opts = { ...DEFAULT_LOCAL_OPTIONS, ...options };
824
+ const errors = [];
825
+ const location = {
826
+ partIndex: context.partIndex,
827
+ partId: context.partId,
828
+ measureIndex: context.measureIndex,
829
+ measureNumber: measure.number
830
+ };
831
+ if (opts.checkMeasureDuration && context.time) {
832
+ errors.push(...validateMeasureDuration(
833
+ measure,
834
+ context.divisions,
835
+ context.time,
836
+ location,
837
+ opts.durationTolerance
838
+ ));
839
+ }
840
+ if (opts.checkMeasureFullness && context.time) {
841
+ errors.push(...validateMeasureFullness(
842
+ measure,
843
+ context.divisions,
844
+ context.time,
845
+ location
846
+ ));
847
+ }
848
+ if (opts.checkPosition) {
849
+ errors.push(...validateBackupForward(measure, location));
850
+ }
851
+ if (opts.checkBeams) {
852
+ errors.push(...validateBeams(measure, location));
853
+ }
854
+ if (opts.checkTuplets) {
855
+ errors.push(...validateTuplets(measure, location));
856
+ }
857
+ if (opts.checkVoiceStaff) {
858
+ errors.push(...validateVoiceStaff(measure, context.staves, location));
859
+ }
860
+ return errors;
861
+ }
862
+ function getMeasureContext(score, partIndex, measureIndex) {
863
+ const part = score.parts[partIndex];
864
+ if (!part) {
865
+ throw new Error(`Part index ${partIndex} out of bounds`);
866
+ }
867
+ let divisions = 1;
868
+ let time;
869
+ let staves = 1;
870
+ for (let i = 0; i <= measureIndex && i < part.measures.length; i++) {
871
+ const measure = part.measures[i];
872
+ if (measure.attributes) {
873
+ if (measure.attributes.divisions !== void 0) {
874
+ divisions = measure.attributes.divisions;
875
+ }
876
+ if (measure.attributes.time !== void 0) {
877
+ time = measure.attributes.time;
878
+ }
879
+ if (measure.attributes.staves !== void 0) {
880
+ staves = measure.attributes.staves;
881
+ }
882
+ }
883
+ for (const entry of measure.entries) {
884
+ if (entry.type === "attributes") {
885
+ if (entry.attributes.divisions !== void 0) {
886
+ divisions = entry.attributes.divisions;
887
+ }
888
+ if (entry.attributes.time !== void 0) {
889
+ time = entry.attributes.time;
890
+ }
891
+ if (entry.attributes.staves !== void 0) {
892
+ staves = entry.attributes.staves;
893
+ }
894
+ }
895
+ }
896
+ }
897
+ return {
898
+ divisions,
899
+ time,
900
+ staves,
901
+ partIndex,
902
+ partId: part.id,
903
+ measureIndex
904
+ };
905
+ }
906
+
44
907
  // src/operations/index.ts
908
+ function success(data, warnings) {
909
+ return { success: true, data, warnings };
910
+ }
911
+ function failure(errors) {
912
+ return { success: false, errors };
913
+ }
914
+ function operationError(code, message, location = {}, details) {
915
+ return {
916
+ code,
917
+ level: "error",
918
+ message,
919
+ location,
920
+ details
921
+ };
922
+ }
45
923
  function cloneScore(score) {
46
924
  return JSON.parse(JSON.stringify(score));
47
925
  }
926
+ function getMeasureDuration(divisions, time) {
927
+ const beats = parseInt(time.beats, 10);
928
+ if (isNaN(beats)) return divisions * 4;
929
+ return beats / time.beatType * 4 * divisions;
930
+ }
931
+ function getVoiceEntries(measure, voice, staff) {
932
+ const result = [];
933
+ let position = 0;
934
+ for (let i = 0; i < measure.entries.length; i++) {
935
+ const entry = measure.entries[i];
936
+ if (entry.type === "note") {
937
+ const noteStaff = entry.staff ?? 1;
938
+ if (entry.voice === voice && (staff === void 0 || noteStaff === staff)) {
939
+ if (!entry.chord) {
940
+ result.push({
941
+ entry,
942
+ entryIndex: i,
943
+ position,
944
+ endPosition: position + entry.duration
945
+ });
946
+ position += entry.duration;
947
+ } else {
948
+ if (result.length > 0) {
949
+ const prev = result[result.length - 1];
950
+ result.push({
951
+ entry,
952
+ entryIndex: i,
953
+ position: prev.position,
954
+ endPosition: prev.endPosition
955
+ });
956
+ }
957
+ }
958
+ } else if (!entry.chord) {
959
+ position += entry.duration;
960
+ }
961
+ } else if (entry.type === "backup") {
962
+ position -= entry.duration;
963
+ } else if (entry.type === "forward") {
964
+ if (entry.voice === voice) {
965
+ result.push({
966
+ entry,
967
+ entryIndex: i,
968
+ position,
969
+ endPosition: position + entry.duration
970
+ });
971
+ }
972
+ position += entry.duration;
973
+ }
974
+ }
975
+ return result;
976
+ }
977
+ function hasNotesInRange(voiceEntries, startPos, endPos) {
978
+ const conflicting = voiceEntries.filter((e) => {
979
+ if (e.entry.type !== "note") return false;
980
+ const note = e.entry;
981
+ if (note.rest) return false;
982
+ return e.position < endPos && e.endPosition > startPos;
983
+ });
984
+ return { hasNotes: conflicting.length > 0, conflictingNotes: conflicting };
985
+ }
986
+ function createRest(duration, voice, staff) {
987
+ return {
988
+ type: "note",
989
+ rest: { displayStep: void 0, displayOctave: void 0 },
990
+ duration,
991
+ voice,
992
+ staff
993
+ };
994
+ }
995
+ function rebuildMeasureWithVoice(measure, voice, newEntries, measureDuration, staff) {
996
+ const otherEntries = [];
997
+ let position = 0;
998
+ for (const entry of measure.entries) {
999
+ if (entry.type === "note") {
1000
+ if (entry.voice !== voice || staff !== void 0 && (entry.staff ?? 1) !== staff) {
1001
+ if (!entry.chord) {
1002
+ otherEntries.push({ position, entry });
1003
+ position += entry.duration;
1004
+ } else {
1005
+ otherEntries.push({ position, entry });
1006
+ }
1007
+ } else if (!entry.chord) {
1008
+ position += entry.duration;
1009
+ }
1010
+ } else if (entry.type === "backup") {
1011
+ position -= entry.duration;
1012
+ } else if (entry.type === "forward") {
1013
+ if (entry.voice !== voice) {
1014
+ otherEntries.push({ position, entry });
1015
+ }
1016
+ position += entry.duration;
1017
+ } else {
1018
+ otherEntries.push({ position, entry });
1019
+ }
1020
+ }
1021
+ const filledNewEntries = [];
1022
+ let currentPos = 0;
1023
+ const sortedNew = [...newEntries].sort((a, b) => a.position - b.position);
1024
+ for (const { position: notePos, entry } of sortedNew) {
1025
+ if (notePos > currentPos) {
1026
+ filledNewEntries.push({
1027
+ position: currentPos,
1028
+ entry: createRest(notePos - currentPos, voice, staff)
1029
+ });
1030
+ }
1031
+ filledNewEntries.push({ position: notePos, entry });
1032
+ if (!entry.chord) {
1033
+ currentPos = notePos + entry.duration;
1034
+ }
1035
+ }
1036
+ if (currentPos < measureDuration) {
1037
+ filledNewEntries.push({
1038
+ position: currentPos,
1039
+ entry: createRest(measureDuration - currentPos, voice, staff)
1040
+ });
1041
+ }
1042
+ const allEntries = [...otherEntries, ...filledNewEntries];
1043
+ allEntries.sort((a, b) => a.position - b.position);
1044
+ const result = [];
1045
+ let currentPosition = 0;
1046
+ for (const { position: targetPos, entry } of allEntries) {
1047
+ const diff = targetPos - currentPosition;
1048
+ if (diff < 0) {
1049
+ result.push({ type: "backup", duration: -diff });
1050
+ currentPosition = targetPos;
1051
+ } else if (diff > 0) {
1052
+ result.push({
1053
+ type: "forward",
1054
+ duration: diff,
1055
+ voice: entry.type === "note" ? entry.voice : 1,
1056
+ staff: entry.type === "note" ? entry.staff : void 0
1057
+ });
1058
+ currentPosition = targetPos;
1059
+ }
1060
+ result.push(entry);
1061
+ if (entry.type === "note" && !entry.chord) {
1062
+ currentPosition += entry.duration;
1063
+ } else if (entry.type === "forward") {
1064
+ currentPosition += entry.duration;
1065
+ }
1066
+ }
1067
+ return result;
1068
+ }
1069
+ function insertNote(score, options) {
1070
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1071
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1072
+ }
1073
+ const part = score.parts[options.partIndex];
1074
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1075
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1076
+ }
1077
+ if (options.duration <= 0) {
1078
+ return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1079
+ }
1080
+ if (options.position < 0) {
1081
+ return failure([operationError("INVALID_POSITION", `Position cannot be negative`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1082
+ }
1083
+ const result = cloneScore(score);
1084
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1085
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1086
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
1087
+ const noteEnd = options.position + options.duration;
1088
+ if (noteEnd > measureDuration) {
1089
+ return failure([operationError(
1090
+ "EXCEEDS_MEASURE",
1091
+ `Note ending at ${noteEnd} exceeds measure duration ${measureDuration}`,
1092
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
1093
+ { noteEnd, measureDuration }
1094
+ )]);
1095
+ }
1096
+ const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
1097
+ const { hasNotes, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, noteEnd);
1098
+ if (hasNotes) {
1099
+ return failure([operationError(
1100
+ "NOTE_CONFLICT",
1101
+ `Position ${options.position}-${noteEnd} conflicts with existing note(s)`,
1102
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice },
1103
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
1104
+ )]);
1105
+ }
1106
+ const newNote = {
1107
+ type: "note",
1108
+ pitch: options.pitch,
1109
+ duration: options.duration,
1110
+ voice: options.voice,
1111
+ staff: options.staff,
1112
+ noteType: options.noteType,
1113
+ dots: options.dots
1114
+ };
1115
+ const existingNotes = voiceEntries.filter((e) => {
1116
+ if (e.entry.type !== "note") return true;
1117
+ const note = e.entry;
1118
+ if (note.rest) {
1119
+ return !(e.position < noteEnd && e.endPosition > options.position);
1120
+ }
1121
+ return true;
1122
+ }).map((e) => ({ position: e.position, entry: e.entry }));
1123
+ existingNotes.push({ position: options.position, entry: newNote });
1124
+ measure.entries = rebuildMeasureWithVoice(
1125
+ measure,
1126
+ options.voice,
1127
+ existingNotes,
1128
+ measureDuration,
1129
+ options.staff
1130
+ );
1131
+ const errors = validateMeasureLocal(measure, context, {
1132
+ checkMeasureDuration: true,
1133
+ checkPosition: true,
1134
+ checkVoiceStaff: true
1135
+ });
1136
+ const criticalErrors = errors.filter((e) => e.level === "error");
1137
+ if (criticalErrors.length > 0) {
1138
+ return failure(criticalErrors);
1139
+ }
1140
+ return success(result, errors.filter((e) => e.level !== "error"));
1141
+ }
1142
+ function removeNote(score, options) {
1143
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1144
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1145
+ }
1146
+ const part = score.parts[options.partIndex];
1147
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1148
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1149
+ }
1150
+ const result = cloneScore(score);
1151
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1152
+ let noteCount = 0;
1153
+ let targetEntry = null;
1154
+ let targetIndex = -1;
1155
+ for (let i2 = 0; i2 < measure.entries.length; i2++) {
1156
+ const entry = measure.entries[i2];
1157
+ if (entry.type === "note" && !entry.rest) {
1158
+ if (noteCount === options.noteIndex) {
1159
+ targetEntry = entry;
1160
+ targetIndex = i2;
1161
+ break;
1162
+ }
1163
+ noteCount++;
1164
+ }
1165
+ }
1166
+ if (!targetEntry || targetIndex === -1) {
1167
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1168
+ }
1169
+ measure.entries[targetIndex] = createRest(
1170
+ targetEntry.duration,
1171
+ targetEntry.voice,
1172
+ targetEntry.staff
1173
+ );
1174
+ let i = targetIndex + 1;
1175
+ while (i < measure.entries.length) {
1176
+ const entry = measure.entries[i];
1177
+ if (entry.type === "note" && entry.chord) {
1178
+ measure.entries.splice(i, 1);
1179
+ } else {
1180
+ break;
1181
+ }
1182
+ }
1183
+ return success(result);
1184
+ }
1185
+ function addChord(score, options) {
1186
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1187
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1188
+ }
1189
+ const part = score.parts[options.partIndex];
1190
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1191
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1192
+ }
1193
+ const result = cloneScore(score);
1194
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1195
+ let noteCount = 0;
1196
+ let targetEntry = null;
1197
+ let targetIndex = -1;
1198
+ for (let i = 0; i < measure.entries.length; i++) {
1199
+ const entry = measure.entries[i];
1200
+ if (entry.type === "note" && !entry.rest && !entry.chord) {
1201
+ if (noteCount === options.noteIndex) {
1202
+ targetEntry = entry;
1203
+ targetIndex = i;
1204
+ break;
1205
+ }
1206
+ noteCount++;
1207
+ }
1208
+ }
1209
+ if (!targetEntry || targetIndex === -1) {
1210
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1211
+ }
1212
+ const chordNote = {
1213
+ type: "note",
1214
+ pitch: options.pitch,
1215
+ duration: targetEntry.duration,
1216
+ voice: targetEntry.voice,
1217
+ staff: targetEntry.staff,
1218
+ chord: true,
1219
+ noteType: targetEntry.noteType,
1220
+ dots: targetEntry.dots
1221
+ };
1222
+ let insertIndex = targetIndex + 1;
1223
+ while (insertIndex < measure.entries.length) {
1224
+ const entry = measure.entries[insertIndex];
1225
+ if (entry.type === "note" && entry.chord) {
1226
+ insertIndex++;
1227
+ } else {
1228
+ break;
1229
+ }
1230
+ }
1231
+ measure.entries.splice(insertIndex, 0, chordNote);
1232
+ return success(result);
1233
+ }
1234
+ function changeNoteDuration(score, options) {
1235
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1236
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1237
+ }
1238
+ const part = score.parts[options.partIndex];
1239
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1240
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1241
+ }
1242
+ if (options.newDuration <= 0) {
1243
+ return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1244
+ }
1245
+ const result = cloneScore(score);
1246
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1247
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1248
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
1249
+ let noteCount = 0;
1250
+ let targetEntry = null;
1251
+ let targetPosition = 0;
1252
+ let position = 0;
1253
+ for (const entry of measure.entries) {
1254
+ if (entry.type === "note") {
1255
+ if (!entry.rest && !entry.chord) {
1256
+ if (noteCount === options.noteIndex) {
1257
+ targetEntry = entry;
1258
+ targetPosition = position;
1259
+ break;
1260
+ }
1261
+ noteCount++;
1262
+ }
1263
+ if (!entry.chord) {
1264
+ position += entry.duration;
1265
+ }
1266
+ } else if (entry.type === "backup") {
1267
+ position -= entry.duration;
1268
+ } else if (entry.type === "forward") {
1269
+ position += entry.duration;
1270
+ }
1271
+ }
1272
+ if (!targetEntry) {
1273
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1274
+ }
1275
+ const oldDuration = targetEntry.duration;
1276
+ const newEnd = targetPosition + options.newDuration;
1277
+ if (newEnd > measureDuration) {
1278
+ return failure([operationError(
1279
+ "EXCEEDS_MEASURE",
1280
+ `New duration would exceed measure (ends at ${newEnd}, measure is ${measureDuration})`,
1281
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
1282
+ { newEnd, measureDuration }
1283
+ )]);
1284
+ }
1285
+ const voiceEntries = getVoiceEntries(measure, targetEntry.voice, targetEntry.staff);
1286
+ if (options.newDuration > oldDuration) {
1287
+ const { hasNotes, conflictingNotes } = hasNotesInRange(
1288
+ voiceEntries.filter((e) => e.position !== targetPosition),
1289
+ // Exclude current note
1290
+ targetPosition + oldDuration,
1291
+ newEnd
1292
+ );
1293
+ if (hasNotes) {
1294
+ return failure([operationError(
1295
+ "NOTE_CONFLICT",
1296
+ `Cannot extend note: conflicts with existing note(s)`,
1297
+ { partIndex: options.partIndex, measureIndex: options.measureIndex },
1298
+ { conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
1299
+ )]);
1300
+ }
1301
+ }
1302
+ targetEntry.duration = options.newDuration;
1303
+ if (options.noteType !== void 0) {
1304
+ targetEntry.noteType = options.noteType;
1305
+ }
1306
+ if (options.dots !== void 0) {
1307
+ targetEntry.dots = options.dots;
1308
+ }
1309
+ const existingNotes = voiceEntries.filter((e) => {
1310
+ if (e.position === targetPosition) return true;
1311
+ const note = e.entry;
1312
+ if (note.rest) {
1313
+ if (options.newDuration > oldDuration) {
1314
+ return !(e.position >= targetPosition + oldDuration && e.position < newEnd);
1315
+ }
1316
+ }
1317
+ return true;
1318
+ }).map((e) => ({ position: e.position, entry: e.entry }));
1319
+ const modifiedIdx = existingNotes.findIndex((e) => e.position === targetPosition);
1320
+ if (modifiedIdx >= 0) {
1321
+ existingNotes[modifiedIdx].entry = targetEntry;
1322
+ }
1323
+ measure.entries = rebuildMeasureWithVoice(
1324
+ measure,
1325
+ targetEntry.voice,
1326
+ existingNotes,
1327
+ measureDuration,
1328
+ targetEntry.staff
1329
+ );
1330
+ const errors = validateMeasureLocal(measure, context, {
1331
+ checkMeasureDuration: true,
1332
+ checkPosition: true,
1333
+ checkVoiceStaff: true
1334
+ });
1335
+ const criticalErrors = errors.filter((e) => e.level === "error");
1336
+ if (criticalErrors.length > 0) {
1337
+ return failure(criticalErrors);
1338
+ }
1339
+ return success(result, errors.filter((e) => e.level !== "error"));
1340
+ }
1341
+ function setNotePitch(score, options) {
1342
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1343
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1344
+ }
1345
+ const part = score.parts[options.partIndex];
1346
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1347
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1348
+ }
1349
+ const result = cloneScore(score);
1350
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1351
+ let noteCount = 0;
1352
+ for (const entry of measure.entries) {
1353
+ if (entry.type === "note" && !entry.rest) {
1354
+ if (noteCount === options.noteIndex) {
1355
+ entry.pitch = options.pitch;
1356
+ return success(result);
1357
+ }
1358
+ noteCount++;
1359
+ }
1360
+ }
1361
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1362
+ }
1363
+ function addVoice(score, options) {
1364
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1365
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1366
+ }
1367
+ const part = score.parts[options.partIndex];
1368
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1369
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1370
+ }
1371
+ const result = cloneScore(score);
1372
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
1373
+ const existingVoiceEntries = getVoiceEntries(measure, options.voice, options.staff);
1374
+ if (existingVoiceEntries.length > 0) {
1375
+ return failure([operationError(
1376
+ "NOTE_CONFLICT",
1377
+ `Voice ${options.voice} already exists in this measure`,
1378
+ { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice }
1379
+ )]);
1380
+ }
1381
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1382
+ const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
1383
+ const rest = createRest(measureDuration, options.voice, options.staff);
1384
+ const currentEnd = getMeasureEndPosition(measure);
1385
+ if (currentEnd > 0) {
1386
+ measure.entries.push({ type: "backup", duration: currentEnd });
1387
+ }
1388
+ measure.entries.push(rest);
1389
+ return success(result);
1390
+ }
48
1391
  function transposePitch(pitch, semitones) {
49
1392
  const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
50
1393
  const targetSemitone = currentSemitone + semitones;
@@ -71,7 +1414,7 @@ function transposePitch(pitch, semitones) {
71
1414
  };
72
1415
  }
73
1416
  function transpose(score, semitones) {
74
- if (semitones === 0) return score;
1417
+ if (semitones === 0) return success(score);
75
1418
  const result = cloneScore(score);
76
1419
  for (const part of result.parts) {
77
1420
  for (const measure of part.measures) {
@@ -82,59 +1425,162 @@ function transpose(score, semitones) {
82
1425
  }
83
1426
  }
84
1427
  }
85
- return result;
1428
+ return success(result);
86
1429
  }
87
- function addNote(score, options) {
1430
+ function addPart(score, options) {
1431
+ if (score.parts.find((p) => p.id === options.id)) {
1432
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.id}" already exists`, { partId: options.id })]);
1433
+ }
88
1434
  const result = cloneScore(score);
89
- const part = result.parts[options.partIndex];
90
- if (!part) return result;
91
- const measure = part.measures[options.measureIndex];
92
- if (!measure) return result;
93
- const newNote = {
94
- type: "note",
95
- voice: options.voice,
96
- staff: options.staff,
97
- ...options.note
1435
+ const insertIndex = options.insertIndex ?? result.parts.length;
1436
+ const partInfo = {
1437
+ type: "score-part",
1438
+ id: options.id,
1439
+ name: options.name,
1440
+ abbreviation: options.abbreviation
98
1441
  };
99
- const currentPosition = getMeasureEndPosition(measure);
100
- const positionDiff = options.position - currentPosition;
101
- if (positionDiff < 0) {
102
- measure.entries.push({
103
- type: "backup",
104
- duration: -positionDiff
105
- });
106
- } else if (positionDiff > 0) {
107
- measure.entries.push({
108
- type: "forward",
109
- duration: positionDiff,
110
- voice: options.voice,
111
- staff: options.staff
112
- });
1442
+ let partListInsertIndex = result.partList.length;
1443
+ let partCount = 0;
1444
+ for (let i = 0; i < result.partList.length; i++) {
1445
+ if (result.partList[i].type === "score-part") {
1446
+ if (partCount === insertIndex) {
1447
+ partListInsertIndex = i;
1448
+ break;
1449
+ }
1450
+ partCount++;
1451
+ }
113
1452
  }
114
- measure.entries.push(newNote);
115
- return result;
1453
+ result.partList.splice(partListInsertIndex, 0, partInfo);
1454
+ const measureCount = result.parts.length > 0 ? result.parts[0].measures.length : 1;
1455
+ const newPart = { id: options.id, measures: [] };
1456
+ for (let i = 0; i < measureCount; i++) {
1457
+ const measureNumber = result.parts.length > 0 ? result.parts[0].measures[i]?.number ?? String(i + 1) : String(i + 1);
1458
+ const measure = { number: measureNumber, entries: [] };
1459
+ if (i === 0) {
1460
+ measure.attributes = {
1461
+ divisions: options.divisions ?? 4,
1462
+ time: options.time ?? { beats: "4", beatType: 4 },
1463
+ key: options.key ?? { fifths: 0 },
1464
+ clef: options.clef ? [options.clef] : [{ sign: "G", line: 2 }]
1465
+ };
1466
+ }
1467
+ newPart.measures.push(measure);
1468
+ }
1469
+ result.parts.splice(insertIndex, 0, newPart);
1470
+ const validationResult = validate(result, { checkPartReferences: true, checkPartStructure: true });
1471
+ if (!validationResult.valid) {
1472
+ return failure(validationResult.errors);
1473
+ }
1474
+ return success(result, validationResult.warnings);
1475
+ }
1476
+ function removePart(score, partId) {
1477
+ const partIndex = score.parts.findIndex((p) => p.id === partId);
1478
+ if (partIndex === -1) {
1479
+ return failure([operationError("PART_NOT_FOUND", `Part "${partId}" not found`, { partId })]);
1480
+ }
1481
+ if (score.parts.length <= 1) {
1482
+ return failure([operationError("PART_NOT_FOUND", "Cannot remove the only remaining part", { partId })]);
1483
+ }
1484
+ const result = cloneScore(score);
1485
+ result.parts.splice(partIndex, 1);
1486
+ const partListIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === partId);
1487
+ if (partListIndex !== -1) {
1488
+ result.partList.splice(partListIndex, 1);
1489
+ }
1490
+ return success(result);
1491
+ }
1492
+ function duplicatePart(score, options) {
1493
+ const sourceIndex = score.parts.findIndex((p) => p.id === options.sourcePartId);
1494
+ if (sourceIndex === -1) {
1495
+ return failure([operationError("PART_NOT_FOUND", `Source part "${options.sourcePartId}" not found`, { partId: options.sourcePartId })]);
1496
+ }
1497
+ if (score.parts.find((p) => p.id === options.newPartId)) {
1498
+ return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.newPartId}" already exists`, { partId: options.newPartId })]);
1499
+ }
1500
+ const result = cloneScore(score);
1501
+ const sourcePart = result.parts[sourceIndex];
1502
+ const newPart = JSON.parse(JSON.stringify(sourcePart));
1503
+ newPart.id = options.newPartId;
1504
+ const sourcePartInfo = result.partList.find((e) => e.type === "score-part" && e.id === options.sourcePartId);
1505
+ const newPartInfo = {
1506
+ type: "score-part",
1507
+ id: options.newPartId,
1508
+ name: options.newPartName ?? sourcePartInfo?.name,
1509
+ abbreviation: sourcePartInfo?.abbreviation
1510
+ };
1511
+ result.parts.splice(sourceIndex + 1, 0, newPart);
1512
+ const partListSourceIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === options.sourcePartId);
1513
+ if (partListSourceIndex !== -1) {
1514
+ result.partList.splice(partListSourceIndex + 1, 0, newPartInfo);
1515
+ } else {
1516
+ result.partList.push(newPartInfo);
1517
+ }
1518
+ return success(result);
116
1519
  }
117
- function deleteNote(score, options) {
1520
+ function setStaves(score, options) {
1521
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1522
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1523
+ }
1524
+ if (options.staves < 1) {
1525
+ return failure([operationError("INVALID_STAFF", `Staves count must be at least 1`, { partIndex: options.partIndex })]);
1526
+ }
118
1527
  const result = cloneScore(score);
119
1528
  const part = result.parts[options.partIndex];
120
- if (!part) return result;
121
- const measure = part.measures[options.measureIndex];
122
- if (!measure) return result;
1529
+ const fromMeasureIndex = options.fromMeasure ?? 0;
1530
+ const measure = part.measures[fromMeasureIndex];
1531
+ if (!measure) {
1532
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${fromMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: fromMeasureIndex })]);
1533
+ }
1534
+ if (!measure.attributes) {
1535
+ measure.attributes = {};
1536
+ }
1537
+ measure.attributes.staves = options.staves;
1538
+ if (options.clefs) {
1539
+ measure.attributes.clef = options.clefs;
1540
+ } else {
1541
+ const existingClefs = measure.attributes.clef ?? [];
1542
+ const newClefs = [...existingClefs];
1543
+ for (let staff = existingClefs.length + 1; staff <= options.staves; staff++) {
1544
+ newClefs.push(staff === 2 ? { sign: "F", line: 4, staff } : { sign: "G", line: 2, staff });
1545
+ }
1546
+ measure.attributes.clef = newClefs;
1547
+ }
1548
+ const validationResult = validate(result, { checkVoiceStaff: true, checkStaffStructure: true });
1549
+ if (!validationResult.valid) {
1550
+ return failure(validationResult.errors);
1551
+ }
1552
+ return success(result, validationResult.warnings);
1553
+ }
1554
+ function moveNoteToStaff(score, options) {
1555
+ if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
1556
+ return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
1557
+ }
1558
+ const part = score.parts[options.partIndex];
1559
+ if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
1560
+ return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1561
+ }
1562
+ if (options.targetStaff < 1) {
1563
+ return failure([operationError("INVALID_STAFF", `Target staff must be at least 1`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
1564
+ }
1565
+ const result = cloneScore(score);
1566
+ const measure = result.parts[options.partIndex].measures[options.measureIndex];
123
1567
  let noteCount = 0;
124
- let entryIndex = -1;
125
- for (let i = 0; i < measure.entries.length; i++) {
126
- if (measure.entries[i].type === "note") {
1568
+ for (const entry of measure.entries) {
1569
+ if (entry.type === "note" && !entry.rest) {
127
1570
  if (noteCount === options.noteIndex) {
128
- entryIndex = i;
129
- break;
1571
+ entry.staff = options.targetStaff;
1572
+ const context = getMeasureContext(result, options.partIndex, options.measureIndex);
1573
+ const errors = validateMeasureLocal(measure, context, { checkVoiceStaff: true });
1574
+ const criticalErrors = errors.filter((e) => e.level === "error");
1575
+ if (criticalErrors.length > 0) {
1576
+ return failure(criticalErrors);
1577
+ }
1578
+ return success(result, errors.filter((e) => e.level !== "error"));
130
1579
  }
131
1580
  noteCount++;
132
1581
  }
133
1582
  }
134
- if (entryIndex !== -1) {
135
- measure.entries.splice(entryIndex, 1);
136
- }
137
- return result;
1583
+ return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
138
1584
  }
139
1585
  function changeKey(score, key, options) {
140
1586
  const result = cloneScore(score);
@@ -142,9 +1588,7 @@ function changeKey(score, key, options) {
142
1588
  for (const part of result.parts) {
143
1589
  for (const measure of part.measures) {
144
1590
  if (measure.number === targetMeasure) {
145
- if (!measure.attributes) {
146
- measure.attributes = {};
147
- }
1591
+ if (!measure.attributes) measure.attributes = {};
148
1592
  measure.attributes.key = key;
149
1593
  }
150
1594
  }
@@ -157,9 +1601,7 @@ function changeTime(score, time, options) {
157
1601
  for (const part of result.parts) {
158
1602
  for (const measure of part.measures) {
159
1603
  if (measure.number === targetMeasure) {
160
- if (!measure.attributes) {
161
- measure.attributes = {};
162
- }
1604
+ if (!measure.attributes) measure.attributes = {};
163
1605
  measure.attributes.time = time;
164
1606
  }
165
1607
  }
@@ -174,15 +1616,9 @@ function insertMeasure(score, options) {
174
1616
  if (insertIndex === -1) continue;
175
1617
  const numericPart = parseInt(targetMeasure, 10);
176
1618
  const newMeasureNumber = String(isNaN(numericPart) ? insertIndex + 2 : numericPart + 1);
177
- const newMeasure = {
178
- number: newMeasureNumber,
179
- entries: []
180
- };
181
- if (options.copyAttributes) {
182
- const sourceMeasure = part.measures[insertIndex];
183
- if (sourceMeasure.attributes) {
184
- newMeasure.attributes = { ...sourceMeasure.attributes };
185
- }
1619
+ const newMeasure = { number: newMeasureNumber, entries: [] };
1620
+ if (options.copyAttributes && part.measures[insertIndex].attributes) {
1621
+ newMeasure.attributes = { ...part.measures[insertIndex].attributes };
186
1622
  }
187
1623
  part.measures.splice(insertIndex + 1, 0, newMeasure);
188
1624
  for (let i = insertIndex + 2; i < part.measures.length; i++) {
@@ -210,106 +1646,84 @@ function deleteMeasure(score, measureNumber) {
210
1646
  }
211
1647
  return result;
212
1648
  }
213
- function setDivisions(score, options) {
214
- const result = cloneScore(score);
215
- const part = result.parts[options.partIndex];
216
- if (!part) return result;
217
- const measure = part.measures[options.measureIndex];
218
- if (!measure) return result;
219
- if (!measure.attributes) {
220
- measure.attributes = {};
221
- }
222
- measure.attributes.divisions = options.divisions;
223
- return result;
224
- }
225
- function addChordNote(score, options) {
226
- const result = cloneScore(score);
227
- const part = result.parts[options.partIndex];
228
- if (!part) return result;
229
- const measure = part.measures[options.measureIndex];
230
- if (!measure) return result;
231
- let noteCount = 0;
232
- let entryIndex = -1;
233
- let targetNote = null;
234
- for (let i = 0; i < measure.entries.length; i++) {
235
- const entry = measure.entries[i];
236
- if (entry.type === "note") {
237
- if (noteCount === options.afterNoteIndex) {
238
- entryIndex = i;
239
- targetNote = entry;
240
- break;
241
- }
242
- noteCount++;
243
- }
244
- }
245
- if (entryIndex !== -1 && targetNote) {
246
- const chordNote = {
247
- type: "note",
248
- pitch: options.pitch,
249
- duration: targetNote.duration,
250
- voice: targetNote.voice,
251
- staff: targetNote.staff,
252
- chord: true,
253
- noteType: targetNote.noteType,
254
- dots: targetNote.dots
255
- };
256
- measure.entries.splice(entryIndex + 1, 0, chordNote);
257
- }
258
- return result;
259
- }
260
- function modifyNotePitch(score, options) {
261
- const result = cloneScore(score);
262
- const part = result.parts[options.partIndex];
263
- if (!part) return result;
264
- const measure = part.measures[options.measureIndex];
265
- if (!measure) return result;
266
- let noteCount = 0;
267
- for (const entry of measure.entries) {
268
- if (entry.type === "note") {
269
- if (noteCount === options.noteIndex) {
270
- entry.pitch = options.pitch;
271
- break;
272
- }
273
- noteCount++;
274
- }
275
- }
276
- return result;
277
- }
278
- function modifyNoteDuration(score, options) {
279
- const result = cloneScore(score);
280
- const part = result.parts[options.partIndex];
281
- if (!part) return result;
282
- const measure = part.measures[options.measureIndex];
283
- if (!measure) return result;
284
- let noteCount = 0;
285
- for (const entry of measure.entries) {
286
- if (entry.type === "note") {
287
- if (noteCount === options.noteIndex) {
288
- entry.duration = options.duration;
289
- if (options.noteType !== void 0) {
290
- entry.noteType = options.noteType;
291
- }
292
- if (options.dots !== void 0) {
293
- entry.dots = options.dots;
294
- }
295
- break;
296
- }
297
- noteCount++;
298
- }
299
- }
300
- return result;
301
- }
1649
+ var addNote = (score, options) => {
1650
+ const result = insertNote(score, {
1651
+ partIndex: options.partIndex,
1652
+ measureIndex: options.measureIndex,
1653
+ voice: options.voice,
1654
+ staff: options.staff,
1655
+ position: options.position,
1656
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
1657
+ duration: options.note.duration,
1658
+ noteType: options.note.noteType,
1659
+ dots: options.note.dots
1660
+ });
1661
+ return result.success ? result.data : score;
1662
+ };
1663
+ var deleteNote = (score, options) => {
1664
+ const result = removeNote(score, options);
1665
+ return result.success ? result.data : score;
1666
+ };
1667
+ var addChordNote = (score, options) => {
1668
+ const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
1669
+ return result.success ? result.data : score;
1670
+ };
1671
+ var modifyNotePitch = (score, options) => {
1672
+ const result = setNotePitch(score, options);
1673
+ return result.success ? result.data : score;
1674
+ };
1675
+ var modifyNoteDuration = (score, options) => {
1676
+ const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
1677
+ return result.success ? result.data : score;
1678
+ };
1679
+ var addNoteChecked = (score, options) => {
1680
+ return insertNote(score, {
1681
+ partIndex: options.partIndex,
1682
+ measureIndex: options.measureIndex,
1683
+ voice: options.voice,
1684
+ staff: options.staff,
1685
+ position: options.position,
1686
+ pitch: options.note.pitch ?? { step: "C", octave: 4 },
1687
+ duration: options.note.duration,
1688
+ noteType: options.note.noteType,
1689
+ dots: options.note.dots
1690
+ });
1691
+ };
1692
+ var deleteNoteChecked = removeNote;
1693
+ var addChordNoteChecked = (score, options) => {
1694
+ return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
1695
+ };
1696
+ var modifyNotePitchChecked = setNotePitch;
1697
+ var modifyNoteDurationChecked = (score, options) => {
1698
+ return changeNoteDuration(score, { ...options, newDuration: options.duration });
1699
+ };
1700
+ var transposeChecked = transpose;
302
1701
  export {
1702
+ addChord,
303
1703
  addChordNote,
1704
+ addChordNoteChecked,
304
1705
  addNote,
1706
+ addNoteChecked,
1707
+ addPart,
1708
+ addVoice,
305
1709
  changeKey,
1710
+ changeNoteDuration,
306
1711
  changeTime,
307
1712
  deleteMeasure,
308
1713
  deleteNote,
1714
+ deleteNoteChecked,
1715
+ duplicatePart,
309
1716
  insertMeasure,
1717
+ insertNote,
310
1718
  modifyNoteDuration,
1719
+ modifyNoteDurationChecked,
311
1720
  modifyNotePitch,
312
- setDivisions,
313
- transpose
1721
+ modifyNotePitchChecked,
1722
+ moveNoteToStaff,
1723
+ removeNote,
1724
+ removePart,
1725
+ setNotePitch,
1726
+ setStaves,
1727
+ transpose,
1728
+ transposeChecked
314
1729
  };
315
- //# sourceMappingURL=index.mjs.map