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