musicxml-io 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -6
- package/dist/chunk-CANSNTYK.js +4944 -0
- package/dist/chunk-EFP2DAOK.mjs +4944 -0
- package/dist/chunk-TIFUKSTH.js +1809 -0
- package/dist/chunk-ZDAN74FN.mjs +1809 -0
- package/dist/index.js +687 -7273
- package/dist/index.mjs +918 -7230
- package/dist/operations/index.js +217 -5070
- package/dist/operations/index.mjs +109 -4830
- package/dist/query/index.js +158 -1778
- package/dist/query/index.mjs +79 -1596
- package/package.json +5 -3
|
@@ -1,4833 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const stepSemitone = STEP_SEMITONES[step];
|
|
111
|
-
const diff = (stepSemitone - pitchClass + 12) % 12;
|
|
112
|
-
if (diff === 2) {
|
|
113
|
-
return { step, octave, alter: -2 };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return { step: "C", octave, alter: pitchClass };
|
|
118
|
-
}
|
|
119
|
-
function determineAccidental(pitch, key, accidentalsInMeasure) {
|
|
120
|
-
const noteKey = `${pitch.step}${pitch.octave}`;
|
|
121
|
-
const alter = pitch.alter ?? 0;
|
|
122
|
-
const keyAlter = getAlterForStepInKey(pitch.step, key);
|
|
123
|
-
const previousAlter = accidentalsInMeasure.get(noteKey);
|
|
124
|
-
if (previousAlter !== void 0) {
|
|
125
|
-
if (alter === previousAlter) {
|
|
126
|
-
return void 0;
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
if (alter === keyAlter) {
|
|
130
|
-
return void 0;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (alter === 0) return "natural";
|
|
134
|
-
if (alter === 1) return "sharp";
|
|
135
|
-
if (alter === -1) return "flat";
|
|
136
|
-
if (alter === 2) return "double-sharp";
|
|
137
|
-
if (alter === -2) return "double-flat";
|
|
138
|
-
return void 0;
|
|
139
|
-
}
|
|
140
|
-
function createPositionState() {
|
|
141
|
-
return { position: 0, lastNonChordPosition: 0 };
|
|
142
|
-
}
|
|
143
|
-
function updatePositionForEntry(state, entry) {
|
|
144
|
-
const pos = state.position;
|
|
145
|
-
switch (entry.type) {
|
|
146
|
-
case "note": {
|
|
147
|
-
const note = entry;
|
|
148
|
-
if (!note.chord) {
|
|
149
|
-
state.lastNonChordPosition = state.position;
|
|
150
|
-
state.position += note.duration;
|
|
151
|
-
}
|
|
152
|
-
return note.chord ? state.lastNonChordPosition : pos;
|
|
153
|
-
}
|
|
154
|
-
case "backup":
|
|
155
|
-
state.position -= entry.duration;
|
|
156
|
-
state.lastNonChordPosition = state.position;
|
|
157
|
-
return pos;
|
|
158
|
-
case "forward":
|
|
159
|
-
state.position += entry.duration;
|
|
160
|
-
state.lastNonChordPosition = state.position;
|
|
161
|
-
return pos;
|
|
162
|
-
default:
|
|
163
|
-
return pos;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
function getAbsolutePositionForNote(note, measure) {
|
|
167
|
-
const state = createPositionState();
|
|
168
|
-
for (const entry of measure.entries) {
|
|
169
|
-
if (entry === note) return entry.chord ? state.lastNonChordPosition : state.position;
|
|
170
|
-
updatePositionForEntry(state, entry);
|
|
171
|
-
}
|
|
172
|
-
return state.position;
|
|
173
|
-
}
|
|
174
|
-
function getMeasureEndPosition(measure) {
|
|
175
|
-
const state = createPositionState();
|
|
176
|
-
for (const entry of measure.entries) updatePositionForEntry(state, entry);
|
|
177
|
-
return state.position;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// src/validator/index.ts
|
|
181
|
-
var DEFAULT_OPTIONS = {
|
|
182
|
-
checkDivisions: true,
|
|
183
|
-
checkMeasureDuration: true,
|
|
184
|
-
checkMeasureFullness: false,
|
|
185
|
-
// Piano Roll semantics - opt-in
|
|
186
|
-
checkPosition: true,
|
|
187
|
-
checkTies: true,
|
|
188
|
-
checkBeams: true,
|
|
189
|
-
checkSlurs: true,
|
|
190
|
-
checkTuplets: true,
|
|
191
|
-
checkPartReferences: true,
|
|
192
|
-
checkPartStructure: true,
|
|
193
|
-
checkVoiceStaff: true,
|
|
194
|
-
checkStaffStructure: true,
|
|
195
|
-
durationTolerance: 0
|
|
196
|
-
};
|
|
197
|
-
function validate(score, options = {}) {
|
|
198
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
199
|
-
const allErrors = [];
|
|
200
|
-
if (opts.checkPartReferences) {
|
|
201
|
-
allErrors.push(...validatePartReferences(score));
|
|
202
|
-
}
|
|
203
|
-
if (opts.checkPartStructure) {
|
|
204
|
-
allErrors.push(...validatePartStructure(score));
|
|
205
|
-
}
|
|
206
|
-
if (opts.checkDivisions) {
|
|
207
|
-
allErrors.push(...validateDivisions(score));
|
|
208
|
-
}
|
|
209
|
-
for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
|
|
210
|
-
const part = score.parts[partIndex];
|
|
211
|
-
let currentDivisions = 1;
|
|
212
|
-
let currentTime;
|
|
213
|
-
let currentStaves = 1;
|
|
214
|
-
for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
|
|
215
|
-
const measure = part.measures[measureIndex];
|
|
216
|
-
const location = {
|
|
217
|
-
partIndex,
|
|
218
|
-
partId: part.id,
|
|
219
|
-
measureIndex,
|
|
220
|
-
measureNumber: measure.number
|
|
221
|
-
};
|
|
222
|
-
if (measure.attributes) {
|
|
223
|
-
if (measure.attributes.divisions !== void 0) {
|
|
224
|
-
currentDivisions = measure.attributes.divisions;
|
|
225
|
-
}
|
|
226
|
-
if (measure.attributes.time !== void 0) {
|
|
227
|
-
currentTime = measure.attributes.time;
|
|
228
|
-
}
|
|
229
|
-
if (measure.attributes.staves !== void 0) {
|
|
230
|
-
currentStaves = measure.attributes.staves;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (opts.checkMeasureDuration && currentTime) {
|
|
234
|
-
allErrors.push(...validateMeasureDuration(
|
|
235
|
-
measure,
|
|
236
|
-
currentDivisions,
|
|
237
|
-
currentTime,
|
|
238
|
-
location,
|
|
239
|
-
opts.durationTolerance
|
|
240
|
-
));
|
|
241
|
-
}
|
|
242
|
-
if (opts.checkMeasureFullness && currentTime) {
|
|
243
|
-
allErrors.push(...validateMeasureFullness(
|
|
244
|
-
measure,
|
|
245
|
-
currentDivisions,
|
|
246
|
-
currentTime,
|
|
247
|
-
location
|
|
248
|
-
));
|
|
249
|
-
}
|
|
250
|
-
if (opts.checkPosition) {
|
|
251
|
-
allErrors.push(...validateBackupForward(measure, location));
|
|
252
|
-
}
|
|
253
|
-
if (opts.checkTies) {
|
|
254
|
-
allErrors.push(...validateTies(measure, location));
|
|
255
|
-
}
|
|
256
|
-
if (opts.checkBeams) {
|
|
257
|
-
allErrors.push(...validateBeams(measure, location));
|
|
258
|
-
}
|
|
259
|
-
if (opts.checkSlurs) {
|
|
260
|
-
allErrors.push(...validateSlurs(measure, location));
|
|
261
|
-
}
|
|
262
|
-
if (opts.checkTuplets) {
|
|
263
|
-
allErrors.push(...validateTuplets(measure, location));
|
|
264
|
-
}
|
|
265
|
-
if (opts.checkVoiceStaff) {
|
|
266
|
-
allErrors.push(...validateVoiceStaff(measure, currentStaves, location));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (opts.checkStaffStructure) {
|
|
270
|
-
allErrors.push(...validateStaffStructure(part, partIndex));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const errors = allErrors.filter((e) => e.level === "error");
|
|
274
|
-
const warnings = allErrors.filter((e) => e.level === "warning");
|
|
275
|
-
const infos = allErrors.filter((e) => e.level === "info");
|
|
276
|
-
return {
|
|
277
|
-
valid: errors.length === 0,
|
|
278
|
-
errors,
|
|
279
|
-
warnings,
|
|
280
|
-
infos
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function validateDivisions(score) {
|
|
284
|
-
const errors = [];
|
|
285
|
-
for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
|
|
286
|
-
const part = score.parts[partIndex];
|
|
287
|
-
let hasDivisions = false;
|
|
288
|
-
for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
|
|
289
|
-
const measure = part.measures[measureIndex];
|
|
290
|
-
if (measure.attributes?.divisions !== void 0) {
|
|
291
|
-
hasDivisions = true;
|
|
292
|
-
if (measure.attributes.divisions <= 0) {
|
|
293
|
-
errors.push({
|
|
294
|
-
code: "INVALID_DIVISIONS",
|
|
295
|
-
level: "error",
|
|
296
|
-
message: `Invalid divisions value: ${measure.attributes.divisions}. Must be positive.`,
|
|
297
|
-
location: {
|
|
298
|
-
partIndex,
|
|
299
|
-
partId: part.id,
|
|
300
|
-
measureIndex,
|
|
301
|
-
measureNumber: measure.number
|
|
302
|
-
},
|
|
303
|
-
details: { divisions: measure.attributes.divisions }
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (!hasDivisions) {
|
|
308
|
-
const hasNotes = measure.entries.some((e) => e.type === "note");
|
|
309
|
-
if (hasNotes) {
|
|
310
|
-
errors.push({
|
|
311
|
-
code: "MISSING_DIVISIONS",
|
|
312
|
-
level: "error",
|
|
313
|
-
message: "Notes found before divisions are defined",
|
|
314
|
-
location: {
|
|
315
|
-
partIndex,
|
|
316
|
-
partId: part.id,
|
|
317
|
-
measureIndex,
|
|
318
|
-
measureNumber: measure.number
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
hasDivisions = true;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return errors;
|
|
327
|
-
}
|
|
328
|
-
function validateMeasureDuration(measure, divisions, time, location, tolerance = 0) {
|
|
329
|
-
const errors = [];
|
|
330
|
-
if (time.senzaMisura) {
|
|
331
|
-
return errors;
|
|
332
|
-
}
|
|
333
|
-
const beats = parseInt(time.beats, 10);
|
|
334
|
-
if (isNaN(beats)) {
|
|
335
|
-
return errors;
|
|
336
|
-
}
|
|
337
|
-
const expectedDuration = beats / time.beatType * 4 * divisions;
|
|
338
|
-
const voiceDurations = calculateVoiceDurations(measure);
|
|
339
|
-
for (const [voiceKey, actualDuration] of voiceDurations.entries()) {
|
|
340
|
-
const [staff, voice] = voiceKey.split("-").map(Number);
|
|
341
|
-
const diff = actualDuration - expectedDuration;
|
|
342
|
-
if (Math.abs(diff) > tolerance) {
|
|
343
|
-
if (diff > 0) {
|
|
344
|
-
errors.push({
|
|
345
|
-
code: "MEASURE_DURATION_OVERFLOW",
|
|
346
|
-
level: "error",
|
|
347
|
-
message: `Voice ${voice} (staff ${staff}) duration ${actualDuration} exceeds expected ${expectedDuration}`,
|
|
348
|
-
location: { ...location, voice, staff },
|
|
349
|
-
details: {
|
|
350
|
-
expected: expectedDuration,
|
|
351
|
-
actual: actualDuration,
|
|
352
|
-
difference: diff
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
} else {
|
|
356
|
-
errors.push({
|
|
357
|
-
code: "MEASURE_DURATION_UNDERFLOW",
|
|
358
|
-
level: "warning",
|
|
359
|
-
message: `Voice ${voice} (staff ${staff}) duration ${actualDuration} is less than expected ${expectedDuration}`,
|
|
360
|
-
location: { ...location, voice, staff },
|
|
361
|
-
details: {
|
|
362
|
-
expected: expectedDuration,
|
|
363
|
-
actual: actualDuration,
|
|
364
|
-
difference: diff
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return errors;
|
|
371
|
-
}
|
|
372
|
-
function calculateVoiceDurations(measure) {
|
|
373
|
-
const voiceDurations = /* @__PURE__ */ new Map();
|
|
374
|
-
let currentPosition = 0;
|
|
375
|
-
const voiceMaxPositions = /* @__PURE__ */ new Map();
|
|
376
|
-
for (const entry of measure.entries) {
|
|
377
|
-
if (entry.type === "note") {
|
|
378
|
-
const staff = entry.staff ?? 1;
|
|
379
|
-
const voice = entry.voice;
|
|
380
|
-
const key = `${staff}-${voice}`;
|
|
381
|
-
if (!entry.chord) {
|
|
382
|
-
const endPosition = currentPosition + entry.duration;
|
|
383
|
-
const currentMax = voiceMaxPositions.get(key) ?? 0;
|
|
384
|
-
voiceMaxPositions.set(key, Math.max(currentMax, endPosition));
|
|
385
|
-
currentPosition = endPosition;
|
|
386
|
-
}
|
|
387
|
-
} else if (entry.type === "backup") {
|
|
388
|
-
currentPosition -= entry.duration;
|
|
389
|
-
} else if (entry.type === "forward") {
|
|
390
|
-
currentPosition += entry.duration;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
for (const [key, maxPos] of voiceMaxPositions.entries()) {
|
|
394
|
-
voiceDurations.set(key, maxPos);
|
|
395
|
-
}
|
|
396
|
-
return voiceDurations;
|
|
397
|
-
}
|
|
398
|
-
function validateMeasureFullness(measure, divisions, time, location) {
|
|
399
|
-
const errors = [];
|
|
400
|
-
if (time.senzaMisura) {
|
|
401
|
-
return errors;
|
|
402
|
-
}
|
|
403
|
-
const beats = parseInt(time.beats, 10);
|
|
404
|
-
if (isNaN(beats)) {
|
|
405
|
-
return errors;
|
|
406
|
-
}
|
|
407
|
-
const expectedDuration = beats / time.beatType * 4 * divisions;
|
|
408
|
-
const voiceCoverage = /* @__PURE__ */ new Map();
|
|
409
|
-
let currentPosition = 0;
|
|
410
|
-
for (const entry of measure.entries) {
|
|
411
|
-
if (entry.type === "note") {
|
|
412
|
-
const staff = entry.staff ?? 1;
|
|
413
|
-
const voice = entry.voice;
|
|
414
|
-
const key = `${staff}-${voice}`;
|
|
415
|
-
if (!entry.chord) {
|
|
416
|
-
if (!voiceCoverage.has(key)) {
|
|
417
|
-
voiceCoverage.set(key, { segments: [] });
|
|
418
|
-
}
|
|
419
|
-
voiceCoverage.get(key).segments.push({
|
|
420
|
-
start: currentPosition,
|
|
421
|
-
end: currentPosition + entry.duration
|
|
422
|
-
});
|
|
423
|
-
currentPosition += entry.duration;
|
|
424
|
-
}
|
|
425
|
-
} else if (entry.type === "backup") {
|
|
426
|
-
currentPosition -= entry.duration;
|
|
427
|
-
} else if (entry.type === "forward") {
|
|
428
|
-
const staff = entry.staff ?? 1;
|
|
429
|
-
const voice = entry.voice ?? 1;
|
|
430
|
-
const key = `${staff}-${voice}`;
|
|
431
|
-
if (!voiceCoverage.has(key)) {
|
|
432
|
-
voiceCoverage.set(key, { segments: [] });
|
|
433
|
-
}
|
|
434
|
-
voiceCoverage.get(key).segments.push({
|
|
435
|
-
start: currentPosition,
|
|
436
|
-
end: currentPosition + entry.duration
|
|
437
|
-
});
|
|
438
|
-
currentPosition += entry.duration;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
for (const [voiceKey, { segments }] of voiceCoverage.entries()) {
|
|
442
|
-
const [staff, voice] = voiceKey.split("-").map(Number);
|
|
443
|
-
const sorted = [...segments].sort((a, b) => a.start - b.start);
|
|
444
|
-
let lastEnd = 0;
|
|
445
|
-
const gaps = [];
|
|
446
|
-
for (const seg of sorted) {
|
|
447
|
-
if (seg.start > lastEnd) {
|
|
448
|
-
gaps.push({ start: lastEnd, end: seg.start });
|
|
449
|
-
}
|
|
450
|
-
lastEnd = Math.max(lastEnd, seg.end);
|
|
451
|
-
}
|
|
452
|
-
if (lastEnd < expectedDuration) {
|
|
453
|
-
gaps.push({ start: lastEnd, end: expectedDuration });
|
|
454
|
-
}
|
|
455
|
-
for (const gap of gaps) {
|
|
456
|
-
errors.push({
|
|
457
|
-
code: "VOICE_GAP",
|
|
458
|
-
level: "warning",
|
|
459
|
-
message: `Voice ${voice} (staff ${staff}) has gap from position ${gap.start} to ${gap.end}`,
|
|
460
|
-
location: { ...location, voice, staff },
|
|
461
|
-
details: {
|
|
462
|
-
gapStart: gap.start,
|
|
463
|
-
gapEnd: gap.end,
|
|
464
|
-
gapDuration: gap.end - gap.start
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
if (lastEnd < expectedDuration) {
|
|
469
|
-
errors.push({
|
|
470
|
-
code: "VOICE_INCOMPLETE",
|
|
471
|
-
level: "warning",
|
|
472
|
-
message: `Voice ${voice} (staff ${staff}) ends at ${lastEnd}, expected ${expectedDuration}`,
|
|
473
|
-
location: { ...location, voice, staff },
|
|
474
|
-
details: {
|
|
475
|
-
actualEnd: lastEnd,
|
|
476
|
-
expectedDuration,
|
|
477
|
-
missing: expectedDuration - lastEnd
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return errors;
|
|
483
|
-
}
|
|
484
|
-
function validateBackupForward(measure, location) {
|
|
485
|
-
const errors = [];
|
|
486
|
-
let position = 0;
|
|
487
|
-
let minPosition = 0;
|
|
488
|
-
for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
|
|
489
|
-
const entry = measure.entries[entryIndex];
|
|
490
|
-
if (entry.type === "note") {
|
|
491
|
-
if (!entry.chord) {
|
|
492
|
-
position += entry.duration;
|
|
493
|
-
}
|
|
494
|
-
} else if (entry.type === "backup") {
|
|
495
|
-
const newPosition = position - entry.duration;
|
|
496
|
-
if (newPosition < 0) {
|
|
497
|
-
errors.push({
|
|
498
|
-
code: "BACKUP_EXCEEDS_POSITION",
|
|
499
|
-
level: "error",
|
|
500
|
-
message: `Backup of ${entry.duration} at position ${position} results in negative position ${newPosition}`,
|
|
501
|
-
location: { ...location, entryIndex },
|
|
502
|
-
details: {
|
|
503
|
-
backupDuration: entry.duration,
|
|
504
|
-
positionBefore: position,
|
|
505
|
-
positionAfter: newPosition
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
position = newPosition;
|
|
510
|
-
minPosition = Math.min(minPosition, position);
|
|
511
|
-
} else if (entry.type === "forward") {
|
|
512
|
-
position += entry.duration;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (minPosition < 0) {
|
|
516
|
-
errors.push({
|
|
517
|
-
code: "NEGATIVE_POSITION",
|
|
518
|
-
level: "error",
|
|
519
|
-
message: `Position went negative (min: ${minPosition}) in measure`,
|
|
520
|
-
location,
|
|
521
|
-
details: { minPosition }
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
return errors;
|
|
525
|
-
}
|
|
526
|
-
function validateTies(measure, location) {
|
|
527
|
-
const errors = [];
|
|
528
|
-
const openTies = /* @__PURE__ */ new Map();
|
|
529
|
-
for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
|
|
530
|
-
const entry = measure.entries[entryIndex];
|
|
531
|
-
if (entry.type !== "note" || !entry.pitch) continue;
|
|
532
|
-
const pitchKey = `${entry.pitch.step}${entry.pitch.octave}${entry.pitch.alter ?? 0}-${entry.voice}-${entry.staff ?? 1}`;
|
|
533
|
-
const ties = entry.ties ?? (entry.tie ? [entry.tie] : []);
|
|
534
|
-
for (const tie of ties) {
|
|
535
|
-
if (tie.type === "start") {
|
|
536
|
-
if (openTies.has(pitchKey)) {
|
|
537
|
-
}
|
|
538
|
-
openTies.set(pitchKey, { entryIndex, pitch: entry.pitch });
|
|
539
|
-
} else if (tie.type === "stop") {
|
|
540
|
-
if (!openTies.has(pitchKey)) {
|
|
541
|
-
errors.push({
|
|
542
|
-
code: "TIE_STOP_WITHOUT_START",
|
|
543
|
-
level: "warning",
|
|
544
|
-
message: `Tie stop without matching start for ${entry.pitch.step}${entry.pitch.octave}`,
|
|
545
|
-
location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
|
|
546
|
-
details: { pitch: entry.pitch }
|
|
547
|
-
});
|
|
548
|
-
} else {
|
|
549
|
-
openTies.delete(pitchKey);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return errors;
|
|
555
|
-
}
|
|
556
|
-
function validateBeams(measure, location) {
|
|
557
|
-
const errors = [];
|
|
558
|
-
const openBeams = /* @__PURE__ */ new Map();
|
|
559
|
-
for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
|
|
560
|
-
const entry = measure.entries[entryIndex];
|
|
561
|
-
if (entry.type !== "note" || !entry.beam) continue;
|
|
562
|
-
for (const beam of entry.beam) {
|
|
563
|
-
const beamKey = `${beam.number}-${entry.voice}`;
|
|
564
|
-
if (beam.type === "begin") {
|
|
565
|
-
if (openBeams.has(beamKey)) {
|
|
566
|
-
errors.push({
|
|
567
|
-
code: "BEAM_BEGIN_WITHOUT_END",
|
|
568
|
-
level: "error",
|
|
569
|
-
message: `Beam ${beam.number} started again before previous beam ended`,
|
|
570
|
-
location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
|
|
571
|
-
details: { beamNumber: beam.number }
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
openBeams.set(beamKey, { entryIndex, staff: entry.staff ?? 1 });
|
|
575
|
-
} else if (beam.type === "end") {
|
|
576
|
-
if (!openBeams.has(beamKey)) {
|
|
577
|
-
errors.push({
|
|
578
|
-
code: "BEAM_END_WITHOUT_BEGIN",
|
|
579
|
-
level: "error",
|
|
580
|
-
message: `Beam ${beam.number} end without matching begin`,
|
|
581
|
-
location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
|
|
582
|
-
details: { beamNumber: beam.number }
|
|
583
|
-
});
|
|
584
|
-
} else {
|
|
585
|
-
openBeams.delete(beamKey);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
for (const [beamKey, { entryIndex: startIndex, staff }] of openBeams.entries()) {
|
|
591
|
-
const [beamNumber, voice] = beamKey.split("-").map(Number);
|
|
592
|
-
errors.push({
|
|
593
|
-
code: "BEAM_BEGIN_WITHOUT_END",
|
|
594
|
-
level: "error",
|
|
595
|
-
message: `Beam ${beamNumber} started but never ended in measure`,
|
|
596
|
-
location: { ...location, entryIndex: startIndex, voice, staff },
|
|
597
|
-
details: { beamNumber }
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
return errors;
|
|
601
|
-
}
|
|
602
|
-
function validateSlurs(measure, _location) {
|
|
603
|
-
const errors = [];
|
|
604
|
-
const openSlurs = /* @__PURE__ */ new Map();
|
|
605
|
-
for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
|
|
606
|
-
const entry = measure.entries[entryIndex];
|
|
607
|
-
if (entry.type !== "note" || !entry.notations) continue;
|
|
608
|
-
for (const notation of entry.notations) {
|
|
609
|
-
if (notation.type !== "slur") continue;
|
|
610
|
-
const slurNumber = notation.number ?? 1;
|
|
611
|
-
const slurKey = `${slurNumber}-${entry.voice}-${entry.staff ?? 1}`;
|
|
612
|
-
if (notation.slurType === "start") {
|
|
613
|
-
if (openSlurs.has(slurKey)) {
|
|
614
|
-
}
|
|
615
|
-
openSlurs.set(slurKey, entryIndex);
|
|
616
|
-
} else if (notation.slurType === "stop") {
|
|
617
|
-
if (!openSlurs.has(slurKey)) {
|
|
618
|
-
} else {
|
|
619
|
-
openSlurs.delete(slurKey);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return errors;
|
|
625
|
-
}
|
|
626
|
-
function validateTuplets(measure, location) {
|
|
627
|
-
const errors = [];
|
|
628
|
-
const openTuplets = /* @__PURE__ */ new Map();
|
|
629
|
-
for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
|
|
630
|
-
const entry = measure.entries[entryIndex];
|
|
631
|
-
if (entry.type !== "note" || !entry.notations) continue;
|
|
632
|
-
for (const notation of entry.notations) {
|
|
633
|
-
if (notation.type !== "tuplet") continue;
|
|
634
|
-
const tupletNumber = notation.number ?? 1;
|
|
635
|
-
const tupletKey = `${tupletNumber}-${entry.voice}-${entry.staff ?? 1}`;
|
|
636
|
-
if (notation.tupletType === "start") {
|
|
637
|
-
if (openTuplets.has(tupletKey)) {
|
|
638
|
-
errors.push({
|
|
639
|
-
code: "TUPLET_START_WITHOUT_STOP",
|
|
640
|
-
level: "error",
|
|
641
|
-
message: `Tuplet ${tupletNumber} started again before previous tuplet ended`,
|
|
642
|
-
location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
|
|
643
|
-
details: { tupletNumber }
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
openTuplets.set(tupletKey, entryIndex);
|
|
647
|
-
} else if (notation.tupletType === "stop") {
|
|
648
|
-
if (!openTuplets.has(tupletKey)) {
|
|
649
|
-
errors.push({
|
|
650
|
-
code: "TUPLET_STOP_WITHOUT_START",
|
|
651
|
-
level: "error",
|
|
652
|
-
message: `Tuplet ${tupletNumber} stop without matching start`,
|
|
653
|
-
location: { ...location, entryIndex, voice: entry.voice, staff: entry.staff ?? 1 },
|
|
654
|
-
details: { tupletNumber }
|
|
655
|
-
});
|
|
656
|
-
} else {
|
|
657
|
-
openTuplets.delete(tupletKey);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
for (const [tupletKey, startIndex] of openTuplets.entries()) {
|
|
663
|
-
const [tupletNumber, voice, staff] = tupletKey.split("-").map(Number);
|
|
664
|
-
errors.push({
|
|
665
|
-
code: "TUPLET_START_WITHOUT_STOP",
|
|
666
|
-
level: "error",
|
|
667
|
-
message: `Tuplet ${tupletNumber} started but never ended in measure`,
|
|
668
|
-
location: { ...location, entryIndex: startIndex, voice, staff },
|
|
669
|
-
details: { tupletNumber }
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
return errors;
|
|
673
|
-
}
|
|
674
|
-
function validatePartReferences(score) {
|
|
675
|
-
const errors = [];
|
|
676
|
-
const partListIds = /* @__PURE__ */ new Set();
|
|
677
|
-
for (const entry of score.partList) {
|
|
678
|
-
if (entry.type === "score-part") {
|
|
679
|
-
partListIds.add(entry.id);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const partIds = /* @__PURE__ */ new Set();
|
|
683
|
-
for (const part of score.parts) {
|
|
684
|
-
partIds.add(part.id);
|
|
685
|
-
}
|
|
686
|
-
for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
|
|
687
|
-
const part = score.parts[partIndex];
|
|
688
|
-
if (!partListIds.has(part.id)) {
|
|
689
|
-
errors.push({
|
|
690
|
-
code: "PART_ID_NOT_IN_PART_LIST",
|
|
691
|
-
level: "error",
|
|
692
|
-
message: `Part "${part.id}" is not defined in partList`,
|
|
693
|
-
location: { partIndex, partId: part.id }
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
for (const entry of score.partList) {
|
|
698
|
-
if (entry.type === "score-part" && !partIds.has(entry.id)) {
|
|
699
|
-
errors.push({
|
|
700
|
-
code: "PART_LIST_ID_NOT_IN_PARTS",
|
|
701
|
-
level: "error",
|
|
702
|
-
message: `PartList entry "${entry.id}" has no corresponding part`,
|
|
703
|
-
location: { partId: entry.id }
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return errors;
|
|
708
|
-
}
|
|
709
|
-
function validateVoiceStaff(measure, staves, location) {
|
|
710
|
-
const errors = [];
|
|
711
|
-
for (let entryIndex = 0; entryIndex < measure.entries.length; entryIndex++) {
|
|
712
|
-
const entry = measure.entries[entryIndex];
|
|
713
|
-
if (entry.type !== "note") continue;
|
|
714
|
-
if (entry.voice !== void 0 && entry.voice <= 0) {
|
|
715
|
-
errors.push({
|
|
716
|
-
code: "INVALID_VOICE_NUMBER",
|
|
717
|
-
level: "error",
|
|
718
|
-
message: `Invalid voice number: ${entry.voice}. Must be positive.`,
|
|
719
|
-
location: { ...location, entryIndex, voice: entry.voice }
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
const staff = entry.staff ?? 1;
|
|
723
|
-
if (staff <= 0) {
|
|
724
|
-
errors.push({
|
|
725
|
-
code: "INVALID_STAFF_NUMBER",
|
|
726
|
-
level: "error",
|
|
727
|
-
message: `Invalid staff number: ${staff}. Must be positive.`,
|
|
728
|
-
location: { ...location, entryIndex, staff }
|
|
729
|
-
});
|
|
730
|
-
} else if (staff > staves) {
|
|
731
|
-
errors.push({
|
|
732
|
-
code: "STAFF_EXCEEDS_STAVES",
|
|
733
|
-
level: "error",
|
|
734
|
-
message: `Staff number ${staff} exceeds declared staves count ${staves}`,
|
|
735
|
-
location: { ...location, entryIndex, staff },
|
|
736
|
-
details: { declaredStaves: staves }
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
if (entry.duration < 0) {
|
|
740
|
-
errors.push({
|
|
741
|
-
code: "INVALID_DURATION",
|
|
742
|
-
level: "error",
|
|
743
|
-
message: `Invalid duration: ${entry.duration}. Must be non-negative.`,
|
|
744
|
-
location: { ...location, entryIndex },
|
|
745
|
-
details: { duration: entry.duration }
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return errors;
|
|
750
|
-
}
|
|
751
|
-
function validatePartStructure(score) {
|
|
752
|
-
const errors = [];
|
|
753
|
-
if (score.parts.length === 0) {
|
|
754
|
-
return errors;
|
|
755
|
-
}
|
|
756
|
-
const partIds = /* @__PURE__ */ new Map();
|
|
757
|
-
for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {
|
|
758
|
-
const part = score.parts[partIndex];
|
|
759
|
-
if (partIds.has(part.id)) {
|
|
760
|
-
errors.push({
|
|
761
|
-
code: "DUPLICATE_PART_ID",
|
|
762
|
-
level: "error",
|
|
763
|
-
message: `Duplicate part ID "${part.id}" found at index ${partIndex} (first at index ${partIds.get(part.id)})`,
|
|
764
|
-
location: { partIndex, partId: part.id },
|
|
765
|
-
details: { firstIndex: partIds.get(part.id) }
|
|
766
|
-
});
|
|
767
|
-
} else {
|
|
768
|
-
partIds.set(part.id, partIndex);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
const referencePart = score.parts[0];
|
|
772
|
-
const referenceMeasureCount = referencePart.measures.length;
|
|
773
|
-
for (let partIndex = 1; partIndex < score.parts.length; partIndex++) {
|
|
774
|
-
const part = score.parts[partIndex];
|
|
775
|
-
if (part.measures.length !== referenceMeasureCount) {
|
|
776
|
-
errors.push({
|
|
777
|
-
code: "PART_MEASURE_COUNT_MISMATCH",
|
|
778
|
-
level: "error",
|
|
779
|
-
message: `Part "${part.id}" has ${part.measures.length} measures, expected ${referenceMeasureCount} (same as first part)`,
|
|
780
|
-
location: { partIndex, partId: part.id },
|
|
781
|
-
details: {
|
|
782
|
-
expected: referenceMeasureCount,
|
|
783
|
-
actual: part.measures.length
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
const minLength = Math.min(part.measures.length, referenceMeasureCount);
|
|
788
|
-
for (let measureIndex = 0; measureIndex < minLength; measureIndex++) {
|
|
789
|
-
const refMeasure = referencePart.measures[measureIndex];
|
|
790
|
-
const partMeasure = part.measures[measureIndex];
|
|
791
|
-
if (refMeasure.number !== partMeasure.number) {
|
|
792
|
-
errors.push({
|
|
793
|
-
code: "PART_MEASURE_NUMBER_MISMATCH",
|
|
794
|
-
level: "warning",
|
|
795
|
-
message: `Part "${part.id}" measure at index ${measureIndex} has number "${partMeasure.number}", expected "${refMeasure.number}"`,
|
|
796
|
-
location: {
|
|
797
|
-
partIndex,
|
|
798
|
-
partId: part.id,
|
|
799
|
-
measureIndex,
|
|
800
|
-
measureNumber: partMeasure.number
|
|
801
|
-
},
|
|
802
|
-
details: {
|
|
803
|
-
expected: refMeasure.number,
|
|
804
|
-
actual: partMeasure.number
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
const openGroups = /* @__PURE__ */ new Map();
|
|
811
|
-
for (let i = 0; i < score.partList.length; i++) {
|
|
812
|
-
const entry = score.partList[i];
|
|
813
|
-
if (entry.type !== "part-group") continue;
|
|
814
|
-
const groupNumber = entry.number ?? 1;
|
|
815
|
-
if (entry.groupType === "start") {
|
|
816
|
-
if (openGroups.has(groupNumber)) {
|
|
817
|
-
errors.push({
|
|
818
|
-
code: "PART_GROUP_START_WITHOUT_STOP",
|
|
819
|
-
level: "error",
|
|
820
|
-
message: `Part group ${groupNumber} started again at index ${i} before previous group ended`,
|
|
821
|
-
location: {},
|
|
822
|
-
details: { groupNumber, partListIndex: i }
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
openGroups.set(groupNumber, i);
|
|
826
|
-
} else if (entry.groupType === "stop") {
|
|
827
|
-
if (!openGroups.has(groupNumber)) {
|
|
828
|
-
errors.push({
|
|
829
|
-
code: "PART_GROUP_STOP_WITHOUT_START",
|
|
830
|
-
level: "error",
|
|
831
|
-
message: `Part group ${groupNumber} stop at index ${i} without matching start`,
|
|
832
|
-
location: {},
|
|
833
|
-
details: { groupNumber, partListIndex: i }
|
|
834
|
-
});
|
|
835
|
-
} else {
|
|
836
|
-
openGroups.delete(groupNumber);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
for (const [groupNumber, startIndex] of openGroups.entries()) {
|
|
841
|
-
errors.push({
|
|
842
|
-
code: "PART_GROUP_START_WITHOUT_STOP",
|
|
843
|
-
level: "error",
|
|
844
|
-
message: `Part group ${groupNumber} started at index ${startIndex} but never stopped`,
|
|
845
|
-
location: {},
|
|
846
|
-
details: { groupNumber, partListIndex: startIndex }
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
return errors;
|
|
850
|
-
}
|
|
851
|
-
function validateStaffStructure(part, partIndex) {
|
|
852
|
-
const errors = [];
|
|
853
|
-
let currentStaves = void 0;
|
|
854
|
-
let stavesDeclarationMeasure = void 0;
|
|
855
|
-
const clefsDeclaredForStaves = /* @__PURE__ */ new Set();
|
|
856
|
-
for (let measureIndex = 0; measureIndex < part.measures.length; measureIndex++) {
|
|
857
|
-
const measure = part.measures[measureIndex];
|
|
858
|
-
const location = {
|
|
859
|
-
partIndex,
|
|
860
|
-
partId: part.id,
|
|
861
|
-
measureIndex,
|
|
862
|
-
measureNumber: measure.number
|
|
863
|
-
};
|
|
864
|
-
if (measure.attributes?.staves !== void 0) {
|
|
865
|
-
const newStaves = measure.attributes.staves;
|
|
866
|
-
if (currentStaves !== void 0 && newStaves !== currentStaves) {
|
|
867
|
-
errors.push({
|
|
868
|
-
code: "STAVES_DECLARATION_MISMATCH",
|
|
869
|
-
level: "info",
|
|
870
|
-
message: `Staves count changed from ${currentStaves} to ${newStaves}`,
|
|
871
|
-
location,
|
|
872
|
-
details: {
|
|
873
|
-
previous: currentStaves,
|
|
874
|
-
new: newStaves,
|
|
875
|
-
previousMeasure: stavesDeclarationMeasure
|
|
876
|
-
}
|
|
877
|
-
});
|
|
878
|
-
clefsDeclaredForStaves.clear();
|
|
879
|
-
}
|
|
880
|
-
currentStaves = newStaves;
|
|
881
|
-
stavesDeclarationMeasure = measure.number;
|
|
882
|
-
}
|
|
883
|
-
if (measure.attributes?.clef) {
|
|
884
|
-
for (const clef of measure.attributes.clef) {
|
|
885
|
-
const staffNum = clef.staff ?? 1;
|
|
886
|
-
clefsDeclaredForStaves.add(staffNum);
|
|
887
|
-
if (currentStaves !== void 0 && staffNum > currentStaves) {
|
|
888
|
-
errors.push({
|
|
889
|
-
code: "CLEF_STAFF_EXCEEDS_STAVES",
|
|
890
|
-
level: "error",
|
|
891
|
-
message: `Clef declared for staff ${staffNum}, but only ${currentStaves} staves declared`,
|
|
892
|
-
location,
|
|
893
|
-
details: {
|
|
894
|
-
clefStaff: staffNum,
|
|
895
|
-
declaredStaves: currentStaves
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
if (currentStaves !== void 0) {
|
|
902
|
-
const usedStaves = /* @__PURE__ */ new Set();
|
|
903
|
-
for (const entry of measure.entries) {
|
|
904
|
-
if (entry.type === "note") {
|
|
905
|
-
usedStaves.add(entry.staff ?? 1);
|
|
906
|
-
} else if (entry.type === "forward" && entry.staff) {
|
|
907
|
-
usedStaves.add(entry.staff);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
for (const usedStaff of usedStaves) {
|
|
911
|
-
if (usedStaff > currentStaves) {
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
if (currentStaves !== void 0 && currentStaves > 1) {
|
|
917
|
-
for (let staff = 1; staff <= currentStaves; staff++) {
|
|
918
|
-
if (!clefsDeclaredForStaves.has(staff)) {
|
|
919
|
-
errors.push({
|
|
920
|
-
code: "MISSING_CLEF_FOR_STAFF",
|
|
921
|
-
level: "warning",
|
|
922
|
-
message: `No clef declared for staff ${staff} in part "${part.id}"`,
|
|
923
|
-
location: { partIndex, partId: part.id },
|
|
924
|
-
details: { staff, totalStaves: currentStaves }
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
const allUsedStaves = /* @__PURE__ */ new Set();
|
|
930
|
-
for (const measure of part.measures) {
|
|
931
|
-
for (const entry of measure.entries) {
|
|
932
|
-
if (entry.type === "note" && entry.staff !== void 0) {
|
|
933
|
-
allUsedStaves.add(entry.staff);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
if (allUsedStaves.size > 1 && currentStaves === void 0) {
|
|
938
|
-
errors.push({
|
|
939
|
-
code: "MISSING_STAVES_DECLARATION",
|
|
940
|
-
level: "warning",
|
|
941
|
-
message: `Part "${part.id}" uses staff numbers ${Array.from(allUsedStaves).sort().join(", ")} but has no staves declaration`,
|
|
942
|
-
location: { partIndex, partId: part.id },
|
|
943
|
-
details: { usedStaves: Array.from(allUsedStaves).sort() }
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
return errors;
|
|
947
|
-
}
|
|
948
|
-
var DEFAULT_LOCAL_OPTIONS = {
|
|
949
|
-
checkMeasureDuration: true,
|
|
950
|
-
checkMeasureFullness: false,
|
|
951
|
-
// Piano Roll semantics - opt-in
|
|
952
|
-
checkPosition: true,
|
|
953
|
-
checkBeams: true,
|
|
954
|
-
checkTuplets: true,
|
|
955
|
-
checkVoiceStaff: true,
|
|
956
|
-
durationTolerance: 0
|
|
957
|
-
};
|
|
958
|
-
function validateMeasureLocal(measure, context, options = {}) {
|
|
959
|
-
const opts = { ...DEFAULT_LOCAL_OPTIONS, ...options };
|
|
960
|
-
const errors = [];
|
|
961
|
-
const location = {
|
|
962
|
-
partIndex: context.partIndex,
|
|
963
|
-
partId: context.partId,
|
|
964
|
-
measureIndex: context.measureIndex,
|
|
965
|
-
measureNumber: measure.number
|
|
966
|
-
};
|
|
967
|
-
if (opts.checkMeasureDuration && context.time) {
|
|
968
|
-
errors.push(...validateMeasureDuration(
|
|
969
|
-
measure,
|
|
970
|
-
context.divisions,
|
|
971
|
-
context.time,
|
|
972
|
-
location,
|
|
973
|
-
opts.durationTolerance
|
|
974
|
-
));
|
|
975
|
-
}
|
|
976
|
-
if (opts.checkMeasureFullness && context.time) {
|
|
977
|
-
errors.push(...validateMeasureFullness(
|
|
978
|
-
measure,
|
|
979
|
-
context.divisions,
|
|
980
|
-
context.time,
|
|
981
|
-
location
|
|
982
|
-
));
|
|
983
|
-
}
|
|
984
|
-
if (opts.checkPosition) {
|
|
985
|
-
errors.push(...validateBackupForward(measure, location));
|
|
986
|
-
}
|
|
987
|
-
if (opts.checkBeams) {
|
|
988
|
-
errors.push(...validateBeams(measure, location));
|
|
989
|
-
}
|
|
990
|
-
if (opts.checkTuplets) {
|
|
991
|
-
errors.push(...validateTuplets(measure, location));
|
|
992
|
-
}
|
|
993
|
-
if (opts.checkVoiceStaff) {
|
|
994
|
-
errors.push(...validateVoiceStaff(measure, context.staves, location));
|
|
995
|
-
}
|
|
996
|
-
return errors;
|
|
997
|
-
}
|
|
998
|
-
function getMeasureContext(score, partIndex, measureIndex) {
|
|
999
|
-
const part = score.parts[partIndex];
|
|
1000
|
-
if (!part) {
|
|
1001
|
-
throw new Error(`Part index ${partIndex} out of bounds`);
|
|
1002
|
-
}
|
|
1003
|
-
let divisions = 1;
|
|
1004
|
-
let time;
|
|
1005
|
-
let staves = 1;
|
|
1006
|
-
for (let i = 0; i <= measureIndex && i < part.measures.length; i++) {
|
|
1007
|
-
const measure = part.measures[i];
|
|
1008
|
-
if (measure.attributes) {
|
|
1009
|
-
if (measure.attributes.divisions !== void 0) {
|
|
1010
|
-
divisions = measure.attributes.divisions;
|
|
1011
|
-
}
|
|
1012
|
-
if (measure.attributes.time !== void 0) {
|
|
1013
|
-
time = measure.attributes.time;
|
|
1014
|
-
}
|
|
1015
|
-
if (measure.attributes.staves !== void 0) {
|
|
1016
|
-
staves = measure.attributes.staves;
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
for (const entry of measure.entries) {
|
|
1020
|
-
if (entry.type === "attributes") {
|
|
1021
|
-
if (entry.attributes.divisions !== void 0) {
|
|
1022
|
-
divisions = entry.attributes.divisions;
|
|
1023
|
-
}
|
|
1024
|
-
if (entry.attributes.time !== void 0) {
|
|
1025
|
-
time = entry.attributes.time;
|
|
1026
|
-
}
|
|
1027
|
-
if (entry.attributes.staves !== void 0) {
|
|
1028
|
-
staves = entry.attributes.staves;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return {
|
|
1034
|
-
divisions,
|
|
1035
|
-
time,
|
|
1036
|
-
staves,
|
|
1037
|
-
partIndex,
|
|
1038
|
-
partId: part.id,
|
|
1039
|
-
measureIndex
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// src/query/index.ts
|
|
1044
|
-
function getAttributesAtMeasure(score, options) {
|
|
1045
|
-
const part = score.parts[options.part];
|
|
1046
|
-
if (!part) return {};
|
|
1047
|
-
const targetMeasure = parseInt(String(options.measure), 10);
|
|
1048
|
-
const result = {};
|
|
1049
|
-
for (const m of part.measures) {
|
|
1050
|
-
const mNum = parseInt(m.number, 10);
|
|
1051
|
-
if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
|
|
1052
|
-
if (m.attributes) {
|
|
1053
|
-
if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
|
|
1054
|
-
if (m.attributes.time !== void 0) result.time = m.attributes.time;
|
|
1055
|
-
if (m.attributes.key !== void 0) result.key = m.attributes.key;
|
|
1056
|
-
if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
|
|
1057
|
-
if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
|
|
1058
|
-
if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
return result;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// src/operations/index.ts
|
|
1065
|
-
function success(data, warnings) {
|
|
1066
|
-
return { success: true, data, warnings };
|
|
1067
|
-
}
|
|
1068
|
-
function failure(errors) {
|
|
1069
|
-
return { success: false, errors };
|
|
1070
|
-
}
|
|
1071
|
-
function operationError(code, message, location = {}, details) {
|
|
1072
|
-
return {
|
|
1073
|
-
code,
|
|
1074
|
-
level: "error",
|
|
1075
|
-
message,
|
|
1076
|
-
location,
|
|
1077
|
-
details
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
function cloneScore(score) {
|
|
1081
|
-
return JSON.parse(JSON.stringify(score));
|
|
1082
|
-
}
|
|
1083
|
-
function cloneNoteWithNewId(note) {
|
|
1084
|
-
const cloned = JSON.parse(JSON.stringify(note));
|
|
1085
|
-
cloned._id = generateId();
|
|
1086
|
-
return cloned;
|
|
1087
|
-
}
|
|
1088
|
-
function cloneEntryWithNewId(entry) {
|
|
1089
|
-
const cloned = JSON.parse(JSON.stringify(entry));
|
|
1090
|
-
cloned._id = generateId();
|
|
1091
|
-
return cloned;
|
|
1092
|
-
}
|
|
1093
|
-
function cloneMeasureWithNewIds(measure) {
|
|
1094
|
-
const cloned = JSON.parse(JSON.stringify(measure));
|
|
1095
|
-
cloned._id = generateId();
|
|
1096
|
-
cloned.entries = cloned.entries.map((entry) => cloneEntryWithNewId(entry));
|
|
1097
|
-
if (cloned.barlines) {
|
|
1098
|
-
cloned.barlines = cloned.barlines.map((barline) => ({
|
|
1099
|
-
...barline,
|
|
1100
|
-
_id: generateId()
|
|
1101
|
-
}));
|
|
1102
|
-
}
|
|
1103
|
-
return cloned;
|
|
1104
|
-
}
|
|
1105
|
-
function clonePartWithNewIds(part) {
|
|
1106
|
-
const cloned = JSON.parse(JSON.stringify(part));
|
|
1107
|
-
cloned._id = generateId();
|
|
1108
|
-
cloned.measures = cloned.measures.map((measure) => cloneMeasureWithNewIds(measure));
|
|
1109
|
-
return cloned;
|
|
1110
|
-
}
|
|
1111
|
-
function getMeasureDuration(divisions, time) {
|
|
1112
|
-
const beats = parseInt(time.beats, 10);
|
|
1113
|
-
if (isNaN(beats)) return divisions * 4;
|
|
1114
|
-
return beats / time.beatType * 4 * divisions;
|
|
1115
|
-
}
|
|
1116
|
-
function getVoiceEntries(measure, voice, staff) {
|
|
1117
|
-
const result = [];
|
|
1118
|
-
let position = 0;
|
|
1119
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
1120
|
-
const entry = measure.entries[i];
|
|
1121
|
-
if (entry.type === "note") {
|
|
1122
|
-
const noteStaff = entry.staff ?? 1;
|
|
1123
|
-
if (entry.voice === voice && (staff === void 0 || noteStaff === staff)) {
|
|
1124
|
-
if (!entry.chord) {
|
|
1125
|
-
result.push({
|
|
1126
|
-
entry,
|
|
1127
|
-
entryIndex: i,
|
|
1128
|
-
position,
|
|
1129
|
-
endPosition: position + entry.duration
|
|
1130
|
-
});
|
|
1131
|
-
position += entry.duration;
|
|
1132
|
-
} else {
|
|
1133
|
-
if (result.length > 0) {
|
|
1134
|
-
const prev = result[result.length - 1];
|
|
1135
|
-
result.push({
|
|
1136
|
-
entry,
|
|
1137
|
-
entryIndex: i,
|
|
1138
|
-
position: prev.position,
|
|
1139
|
-
endPosition: prev.endPosition
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
} else if (!entry.chord) {
|
|
1144
|
-
position += entry.duration;
|
|
1145
|
-
}
|
|
1146
|
-
} else if (entry.type === "backup") {
|
|
1147
|
-
position -= entry.duration;
|
|
1148
|
-
} else if (entry.type === "forward") {
|
|
1149
|
-
if (entry.voice === voice) {
|
|
1150
|
-
result.push({
|
|
1151
|
-
entry,
|
|
1152
|
-
entryIndex: i,
|
|
1153
|
-
position,
|
|
1154
|
-
endPosition: position + entry.duration
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
position += entry.duration;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
return result;
|
|
1161
|
-
}
|
|
1162
|
-
function hasNotesInRange(voiceEntries, startPos, endPos) {
|
|
1163
|
-
const conflicting = voiceEntries.filter((e) => {
|
|
1164
|
-
if (e.entry.type !== "note") return false;
|
|
1165
|
-
const note = e.entry;
|
|
1166
|
-
if (note.rest) return false;
|
|
1167
|
-
return e.position < endPos && e.endPosition > startPos;
|
|
1168
|
-
});
|
|
1169
|
-
return { hasNotes: conflicting.length > 0, conflictingNotes: conflicting };
|
|
1170
|
-
}
|
|
1171
|
-
function createRest(duration, voice, staff) {
|
|
1172
|
-
const note = {
|
|
1173
|
-
_id: generateId(),
|
|
1174
|
-
type: "note",
|
|
1175
|
-
rest: { displayStep: void 0, displayOctave: void 0 },
|
|
1176
|
-
duration,
|
|
1177
|
-
staff
|
|
1178
|
-
};
|
|
1179
|
-
if (voice !== void 0) {
|
|
1180
|
-
note.voice = voice;
|
|
1181
|
-
}
|
|
1182
|
-
return note;
|
|
1183
|
-
}
|
|
1184
|
-
function rebuildMeasureWithVoice(measure, voice, newEntries, measureDuration, staff) {
|
|
1185
|
-
const otherEntries = [];
|
|
1186
|
-
let position = 0;
|
|
1187
|
-
for (const entry of measure.entries) {
|
|
1188
|
-
if (entry.type === "note") {
|
|
1189
|
-
if (entry.voice !== voice || staff !== void 0 && (entry.staff ?? 1) !== staff) {
|
|
1190
|
-
if (!entry.chord) {
|
|
1191
|
-
otherEntries.push({ position, entry });
|
|
1192
|
-
position += entry.duration;
|
|
1193
|
-
} else {
|
|
1194
|
-
otherEntries.push({ position, entry });
|
|
1195
|
-
}
|
|
1196
|
-
} else if (!entry.chord) {
|
|
1197
|
-
position += entry.duration;
|
|
1198
|
-
}
|
|
1199
|
-
} else if (entry.type === "backup") {
|
|
1200
|
-
position -= entry.duration;
|
|
1201
|
-
} else if (entry.type === "forward") {
|
|
1202
|
-
if (entry.voice !== voice) {
|
|
1203
|
-
otherEntries.push({ position, entry });
|
|
1204
|
-
}
|
|
1205
|
-
position += entry.duration;
|
|
1206
|
-
} else {
|
|
1207
|
-
otherEntries.push({ position, entry });
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
const filledNewEntries = [];
|
|
1211
|
-
let currentPos = 0;
|
|
1212
|
-
const sortedNew = [...newEntries].sort((a, b) => a.position - b.position);
|
|
1213
|
-
for (const { position: notePos, entry } of sortedNew) {
|
|
1214
|
-
if (notePos > currentPos) {
|
|
1215
|
-
filledNewEntries.push({
|
|
1216
|
-
position: currentPos,
|
|
1217
|
-
entry: createRest(notePos - currentPos, voice, staff)
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
filledNewEntries.push({ position: notePos, entry });
|
|
1221
|
-
if (!entry.chord) {
|
|
1222
|
-
currentPos = notePos + entry.duration;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
if (currentPos < measureDuration) {
|
|
1226
|
-
filledNewEntries.push({
|
|
1227
|
-
position: currentPos,
|
|
1228
|
-
entry: createRest(measureDuration - currentPos, voice, staff)
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
const allEntries = [...otherEntries, ...filledNewEntries];
|
|
1232
|
-
allEntries.sort((a, b) => a.position - b.position);
|
|
1233
|
-
const result = [];
|
|
1234
|
-
let currentPosition = 0;
|
|
1235
|
-
for (const { position: targetPos, entry } of allEntries) {
|
|
1236
|
-
const diff = targetPos - currentPosition;
|
|
1237
|
-
if (diff < 0) {
|
|
1238
|
-
result.push({ _id: generateId(), type: "backup", duration: -diff });
|
|
1239
|
-
currentPosition = targetPos;
|
|
1240
|
-
} else if (diff > 0) {
|
|
1241
|
-
result.push({
|
|
1242
|
-
_id: generateId(),
|
|
1243
|
-
type: "forward",
|
|
1244
|
-
duration: diff,
|
|
1245
|
-
voice: entry.type === "note" ? entry.voice : 1,
|
|
1246
|
-
staff: entry.type === "note" ? entry.staff : void 0
|
|
1247
|
-
});
|
|
1248
|
-
currentPosition = targetPos;
|
|
1249
|
-
}
|
|
1250
|
-
result.push(entry);
|
|
1251
|
-
if (entry.type === "note" && !entry.chord) {
|
|
1252
|
-
currentPosition += entry.duration;
|
|
1253
|
-
} else if (entry.type === "forward") {
|
|
1254
|
-
currentPosition += entry.duration;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
return result;
|
|
1258
|
-
}
|
|
1259
|
-
function insertNote(score, options) {
|
|
1260
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1261
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1262
|
-
}
|
|
1263
|
-
const part = score.parts[options.partIndex];
|
|
1264
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1265
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1266
|
-
}
|
|
1267
|
-
if (options.duration <= 0) {
|
|
1268
|
-
return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1269
|
-
}
|
|
1270
|
-
if (options.position < 0) {
|
|
1271
|
-
return failure([operationError("INVALID_POSITION", `Position cannot be negative`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1272
|
-
}
|
|
1273
|
-
const result = cloneScore(score);
|
|
1274
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1275
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
1276
|
-
const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
|
|
1277
|
-
const noteEnd = options.position + options.duration;
|
|
1278
|
-
if (noteEnd > measureDuration) {
|
|
1279
|
-
return failure([operationError(
|
|
1280
|
-
"EXCEEDS_MEASURE",
|
|
1281
|
-
`Note ending at ${noteEnd} exceeds measure duration ${measureDuration}`,
|
|
1282
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex },
|
|
1283
|
-
{ noteEnd, measureDuration }
|
|
1284
|
-
)]);
|
|
1285
|
-
}
|
|
1286
|
-
const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
|
|
1287
|
-
const { hasNotes, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, noteEnd);
|
|
1288
|
-
if (hasNotes) {
|
|
1289
|
-
return failure([operationError(
|
|
1290
|
-
"NOTE_CONFLICT",
|
|
1291
|
-
`Position ${options.position}-${noteEnd} conflicts with existing note(s)`,
|
|
1292
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice },
|
|
1293
|
-
{ conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
|
|
1294
|
-
)]);
|
|
1295
|
-
}
|
|
1296
|
-
const newNote = {
|
|
1297
|
-
_id: generateId(),
|
|
1298
|
-
type: "note",
|
|
1299
|
-
pitch: options.pitch,
|
|
1300
|
-
duration: options.duration,
|
|
1301
|
-
voice: options.voice,
|
|
1302
|
-
staff: options.staff,
|
|
1303
|
-
noteType: options.noteType,
|
|
1304
|
-
dots: options.dots
|
|
1305
|
-
};
|
|
1306
|
-
const existingNotes = voiceEntries.filter((e) => {
|
|
1307
|
-
if (e.entry.type !== "note") return true;
|
|
1308
|
-
const note = e.entry;
|
|
1309
|
-
if (note.rest) {
|
|
1310
|
-
return !(e.position < noteEnd && e.endPosition > options.position);
|
|
1311
|
-
}
|
|
1312
|
-
return true;
|
|
1313
|
-
}).map((e) => ({ position: e.position, entry: e.entry }));
|
|
1314
|
-
existingNotes.push({ position: options.position, entry: newNote });
|
|
1315
|
-
measure.entries = rebuildMeasureWithVoice(
|
|
1316
|
-
measure,
|
|
1317
|
-
options.voice,
|
|
1318
|
-
existingNotes,
|
|
1319
|
-
measureDuration,
|
|
1320
|
-
options.staff
|
|
1321
|
-
);
|
|
1322
|
-
const errors = validateMeasureLocal(measure, context, {
|
|
1323
|
-
checkMeasureDuration: true,
|
|
1324
|
-
checkPosition: true,
|
|
1325
|
-
checkVoiceStaff: true
|
|
1326
|
-
});
|
|
1327
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
1328
|
-
if (criticalErrors.length > 0) {
|
|
1329
|
-
return failure(criticalErrors);
|
|
1330
|
-
}
|
|
1331
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
1332
|
-
}
|
|
1333
|
-
function removeNote(score, options) {
|
|
1334
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1335
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1336
|
-
}
|
|
1337
|
-
const part = score.parts[options.partIndex];
|
|
1338
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1339
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1340
|
-
}
|
|
1341
|
-
const result = cloneScore(score);
|
|
1342
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1343
|
-
let noteCount = 0;
|
|
1344
|
-
let targetEntry = null;
|
|
1345
|
-
let targetIndex = -1;
|
|
1346
|
-
for (let i2 = 0; i2 < measure.entries.length; i2++) {
|
|
1347
|
-
const entry = measure.entries[i2];
|
|
1348
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1349
|
-
if (noteCount === options.noteIndex) {
|
|
1350
|
-
targetEntry = entry;
|
|
1351
|
-
targetIndex = i2;
|
|
1352
|
-
break;
|
|
1353
|
-
}
|
|
1354
|
-
noteCount++;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
if (!targetEntry || targetIndex === -1) {
|
|
1358
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1359
|
-
}
|
|
1360
|
-
measure.entries[targetIndex] = createRest(
|
|
1361
|
-
targetEntry.duration,
|
|
1362
|
-
targetEntry.voice,
|
|
1363
|
-
targetEntry.staff
|
|
1364
|
-
);
|
|
1365
|
-
let i = targetIndex + 1;
|
|
1366
|
-
while (i < measure.entries.length) {
|
|
1367
|
-
const entry = measure.entries[i];
|
|
1368
|
-
if (entry.type === "note" && entry.chord) {
|
|
1369
|
-
measure.entries.splice(i, 1);
|
|
1370
|
-
} else {
|
|
1371
|
-
break;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
return success(result);
|
|
1375
|
-
}
|
|
1376
|
-
function addChord(score, options) {
|
|
1377
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1378
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1379
|
-
}
|
|
1380
|
-
const part = score.parts[options.partIndex];
|
|
1381
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1382
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1383
|
-
}
|
|
1384
|
-
const result = cloneScore(score);
|
|
1385
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1386
|
-
let noteCount = 0;
|
|
1387
|
-
let targetEntry = null;
|
|
1388
|
-
let targetIndex = -1;
|
|
1389
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
1390
|
-
const entry = measure.entries[i];
|
|
1391
|
-
if (entry.type === "note" && !entry.rest && !entry.chord) {
|
|
1392
|
-
if (noteCount === options.noteIndex) {
|
|
1393
|
-
targetEntry = entry;
|
|
1394
|
-
targetIndex = i;
|
|
1395
|
-
break;
|
|
1396
|
-
}
|
|
1397
|
-
noteCount++;
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
if (!targetEntry || targetIndex === -1) {
|
|
1401
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1402
|
-
}
|
|
1403
|
-
const chordNote = {
|
|
1404
|
-
_id: generateId(),
|
|
1405
|
-
type: "note",
|
|
1406
|
-
pitch: options.pitch,
|
|
1407
|
-
duration: targetEntry.duration,
|
|
1408
|
-
voice: targetEntry.voice,
|
|
1409
|
-
staff: targetEntry.staff,
|
|
1410
|
-
chord: true,
|
|
1411
|
-
noteType: targetEntry.noteType,
|
|
1412
|
-
dots: targetEntry.dots
|
|
1413
|
-
};
|
|
1414
|
-
let insertIndex = targetIndex + 1;
|
|
1415
|
-
while (insertIndex < measure.entries.length) {
|
|
1416
|
-
const entry = measure.entries[insertIndex];
|
|
1417
|
-
if (entry.type === "note" && entry.chord) {
|
|
1418
|
-
insertIndex++;
|
|
1419
|
-
} else {
|
|
1420
|
-
break;
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
measure.entries.splice(insertIndex, 0, chordNote);
|
|
1424
|
-
return success(result);
|
|
1425
|
-
}
|
|
1426
|
-
function changeNoteDuration(score, options) {
|
|
1427
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1428
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1429
|
-
}
|
|
1430
|
-
const part = score.parts[options.partIndex];
|
|
1431
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1432
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1433
|
-
}
|
|
1434
|
-
if (options.newDuration <= 0) {
|
|
1435
|
-
return failure([operationError("INVALID_DURATION", `Duration must be positive`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1436
|
-
}
|
|
1437
|
-
const result = cloneScore(score);
|
|
1438
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1439
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
1440
|
-
const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
|
|
1441
|
-
let noteCount = 0;
|
|
1442
|
-
let targetEntry = null;
|
|
1443
|
-
let targetPosition = 0;
|
|
1444
|
-
let position = 0;
|
|
1445
|
-
for (const entry of measure.entries) {
|
|
1446
|
-
if (entry.type === "note") {
|
|
1447
|
-
if (!entry.rest && !entry.chord) {
|
|
1448
|
-
if (noteCount === options.noteIndex) {
|
|
1449
|
-
targetEntry = entry;
|
|
1450
|
-
targetPosition = position;
|
|
1451
|
-
break;
|
|
1452
|
-
}
|
|
1453
|
-
noteCount++;
|
|
1454
|
-
}
|
|
1455
|
-
if (!entry.chord) {
|
|
1456
|
-
position += entry.duration;
|
|
1457
|
-
}
|
|
1458
|
-
} else if (entry.type === "backup") {
|
|
1459
|
-
position -= entry.duration;
|
|
1460
|
-
} else if (entry.type === "forward") {
|
|
1461
|
-
position += entry.duration;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
if (!targetEntry) {
|
|
1465
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1466
|
-
}
|
|
1467
|
-
const oldDuration = targetEntry.duration;
|
|
1468
|
-
const newEnd = targetPosition + options.newDuration;
|
|
1469
|
-
if (newEnd > measureDuration) {
|
|
1470
|
-
return failure([operationError(
|
|
1471
|
-
"EXCEEDS_MEASURE",
|
|
1472
|
-
`New duration would exceed measure (ends at ${newEnd}, measure is ${measureDuration})`,
|
|
1473
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex },
|
|
1474
|
-
{ newEnd, measureDuration }
|
|
1475
|
-
)]);
|
|
1476
|
-
}
|
|
1477
|
-
const voiceEntries = getVoiceEntries(measure, targetEntry.voice, targetEntry.staff);
|
|
1478
|
-
if (options.newDuration > oldDuration) {
|
|
1479
|
-
const { hasNotes, conflictingNotes } = hasNotesInRange(
|
|
1480
|
-
voiceEntries.filter((e) => e.position !== targetPosition),
|
|
1481
|
-
// Exclude current note
|
|
1482
|
-
targetPosition + oldDuration,
|
|
1483
|
-
newEnd
|
|
1484
|
-
);
|
|
1485
|
-
if (hasNotes) {
|
|
1486
|
-
return failure([operationError(
|
|
1487
|
-
"NOTE_CONFLICT",
|
|
1488
|
-
`Cannot extend note: conflicts with existing note(s)`,
|
|
1489
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex },
|
|
1490
|
-
{ conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
|
|
1491
|
-
)]);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
targetEntry.duration = options.newDuration;
|
|
1495
|
-
if (options.noteType !== void 0) {
|
|
1496
|
-
targetEntry.noteType = options.noteType;
|
|
1497
|
-
}
|
|
1498
|
-
if (options.dots !== void 0) {
|
|
1499
|
-
targetEntry.dots = options.dots;
|
|
1500
|
-
}
|
|
1501
|
-
const existingNotes = voiceEntries.filter((e) => {
|
|
1502
|
-
if (e.position === targetPosition) return true;
|
|
1503
|
-
const note = e.entry;
|
|
1504
|
-
if (note.rest) {
|
|
1505
|
-
if (options.newDuration > oldDuration) {
|
|
1506
|
-
return !(e.position >= targetPosition + oldDuration && e.position < newEnd);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
return true;
|
|
1510
|
-
}).map((e) => ({ position: e.position, entry: e.entry }));
|
|
1511
|
-
const modifiedIdx = existingNotes.findIndex((e) => e.position === targetPosition);
|
|
1512
|
-
if (modifiedIdx >= 0) {
|
|
1513
|
-
existingNotes[modifiedIdx].entry = targetEntry;
|
|
1514
|
-
}
|
|
1515
|
-
measure.entries = rebuildMeasureWithVoice(
|
|
1516
|
-
measure,
|
|
1517
|
-
targetEntry.voice,
|
|
1518
|
-
existingNotes,
|
|
1519
|
-
measureDuration,
|
|
1520
|
-
targetEntry.staff
|
|
1521
|
-
);
|
|
1522
|
-
const errors = validateMeasureLocal(measure, context, {
|
|
1523
|
-
checkMeasureDuration: true,
|
|
1524
|
-
checkPosition: true,
|
|
1525
|
-
checkVoiceStaff: true
|
|
1526
|
-
});
|
|
1527
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
1528
|
-
if (criticalErrors.length > 0) {
|
|
1529
|
-
return failure(criticalErrors);
|
|
1530
|
-
}
|
|
1531
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
1532
|
-
}
|
|
1533
|
-
function setNotePitch(score, options) {
|
|
1534
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1535
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1536
|
-
}
|
|
1537
|
-
const part = score.parts[options.partIndex];
|
|
1538
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1539
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1540
|
-
}
|
|
1541
|
-
const result = cloneScore(score);
|
|
1542
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1543
|
-
let noteCount = 0;
|
|
1544
|
-
for (const entry of measure.entries) {
|
|
1545
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1546
|
-
if (noteCount === options.noteIndex) {
|
|
1547
|
-
entry.pitch = options.pitch;
|
|
1548
|
-
return success(result);
|
|
1549
|
-
}
|
|
1550
|
-
noteCount++;
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1554
|
-
}
|
|
1555
|
-
function setNotePitchBySemitone(score, options) {
|
|
1556
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1557
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1558
|
-
}
|
|
1559
|
-
const part = score.parts[options.partIndex];
|
|
1560
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1561
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1562
|
-
}
|
|
1563
|
-
const result = cloneScore(score);
|
|
1564
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1565
|
-
const measureNumber = measure.number ?? String(options.measureIndex + 1);
|
|
1566
|
-
const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
|
|
1567
|
-
const keySignature = attrs.key ?? { fifths: 0 };
|
|
1568
|
-
let noteCount = 0;
|
|
1569
|
-
for (const entry of measure.entries) {
|
|
1570
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1571
|
-
if (noteCount === options.noteIndex) {
|
|
1572
|
-
const notePosition = getAbsolutePositionForNote(entry, measure);
|
|
1573
|
-
const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
|
|
1574
|
-
const newPitch = semitoneToKeyAwarePitch(options.semitone, keySignature, {
|
|
1575
|
-
preferSharp: options.preferSharp
|
|
1576
|
-
});
|
|
1577
|
-
const accidental = determineAccidental(newPitch, keySignature, accidentalsInMeasure);
|
|
1578
|
-
entry.pitch = newPitch;
|
|
1579
|
-
if (accidental) {
|
|
1580
|
-
entry.accidental = { value: accidental };
|
|
1581
|
-
} else {
|
|
1582
|
-
delete entry.accidental;
|
|
1583
|
-
}
|
|
1584
|
-
return success(result);
|
|
1585
|
-
}
|
|
1586
|
-
noteCount++;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1590
|
-
}
|
|
1591
|
-
function shiftNotePitch(score, options) {
|
|
1592
|
-
if (options.semitones === 0) {
|
|
1593
|
-
return success(score);
|
|
1594
|
-
}
|
|
1595
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1596
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1597
|
-
}
|
|
1598
|
-
const part = score.parts[options.partIndex];
|
|
1599
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1600
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1601
|
-
}
|
|
1602
|
-
const measure = part.measures[options.measureIndex];
|
|
1603
|
-
let noteCount = 0;
|
|
1604
|
-
let currentSemitone = null;
|
|
1605
|
-
for (const entry of measure.entries) {
|
|
1606
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1607
|
-
if (noteCount === options.noteIndex) {
|
|
1608
|
-
if (!entry.pitch) {
|
|
1609
|
-
return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1610
|
-
}
|
|
1611
|
-
currentSemitone = pitchToSemitone(entry.pitch);
|
|
1612
|
-
break;
|
|
1613
|
-
}
|
|
1614
|
-
noteCount++;
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
if (currentSemitone === null) {
|
|
1618
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1619
|
-
}
|
|
1620
|
-
return setNotePitchBySemitone(score, {
|
|
1621
|
-
partIndex: options.partIndex,
|
|
1622
|
-
measureIndex: options.measureIndex,
|
|
1623
|
-
noteIndex: options.noteIndex,
|
|
1624
|
-
semitone: currentSemitone + options.semitones,
|
|
1625
|
-
preferSharp: options.preferSharp
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
function raiseAccidental(score, options) {
|
|
1629
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1630
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1631
|
-
}
|
|
1632
|
-
const part = score.parts[options.partIndex];
|
|
1633
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1634
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1635
|
-
}
|
|
1636
|
-
const result = cloneScore(score);
|
|
1637
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1638
|
-
const measureNumber = measure.number ?? String(options.measureIndex + 1);
|
|
1639
|
-
const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
|
|
1640
|
-
const keySignature = attrs.key ?? { fifths: 0 };
|
|
1641
|
-
let noteCount = 0;
|
|
1642
|
-
for (const entry of measure.entries) {
|
|
1643
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1644
|
-
if (noteCount === options.noteIndex) {
|
|
1645
|
-
if (!entry.pitch) {
|
|
1646
|
-
return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1647
|
-
}
|
|
1648
|
-
const currentAlter = entry.pitch.alter ?? 0;
|
|
1649
|
-
const newAlter = currentAlter + 1;
|
|
1650
|
-
if (newAlter > 2) {
|
|
1651
|
-
return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot raise accidental beyond double-sharp (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1652
|
-
}
|
|
1653
|
-
entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
|
|
1654
|
-
const notePosition = getAbsolutePositionForNote(entry, measure);
|
|
1655
|
-
const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
|
|
1656
|
-
const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
|
|
1657
|
-
if (accidental) {
|
|
1658
|
-
entry.accidental = { value: accidental };
|
|
1659
|
-
} else {
|
|
1660
|
-
delete entry.accidental;
|
|
1661
|
-
}
|
|
1662
|
-
return success(result);
|
|
1663
|
-
}
|
|
1664
|
-
noteCount++;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1668
|
-
}
|
|
1669
|
-
function lowerAccidental(score, options) {
|
|
1670
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1671
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1672
|
-
}
|
|
1673
|
-
const part = score.parts[options.partIndex];
|
|
1674
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1675
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1676
|
-
}
|
|
1677
|
-
const result = cloneScore(score);
|
|
1678
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1679
|
-
const measureNumber = measure.number ?? String(options.measureIndex + 1);
|
|
1680
|
-
const attrs = getAttributesAtMeasure(result, { part: options.partIndex, measure: measureNumber });
|
|
1681
|
-
const keySignature = attrs.key ?? { fifths: 0 };
|
|
1682
|
-
let noteCount = 0;
|
|
1683
|
-
for (const entry of measure.entries) {
|
|
1684
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1685
|
-
if (noteCount === options.noteIndex) {
|
|
1686
|
-
if (!entry.pitch) {
|
|
1687
|
-
return failure([operationError("NOTE_NOT_FOUND", "Note has no pitch", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1688
|
-
}
|
|
1689
|
-
const currentAlter = entry.pitch.alter ?? 0;
|
|
1690
|
-
const newAlter = currentAlter - 1;
|
|
1691
|
-
if (newAlter < -2) {
|
|
1692
|
-
return failure([operationError("ACCIDENTAL_OUT_OF_BOUNDS", `Cannot lower accidental beyond double-flat (current: ${currentAlter})`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1693
|
-
}
|
|
1694
|
-
entry.pitch.alter = newAlter === 0 ? void 0 : newAlter;
|
|
1695
|
-
const notePosition = getAbsolutePositionForNote(entry, measure);
|
|
1696
|
-
const accidentalsInMeasure = getAccidentalsInMeasure(measure, notePosition, entry.voice);
|
|
1697
|
-
const accidental = determineAccidental(entry.pitch, keySignature, accidentalsInMeasure);
|
|
1698
|
-
if (accidental) {
|
|
1699
|
-
entry.accidental = { value: accidental };
|
|
1700
|
-
} else {
|
|
1701
|
-
delete entry.accidental;
|
|
1702
|
-
}
|
|
1703
|
-
return success(result);
|
|
1704
|
-
}
|
|
1705
|
-
noteCount++;
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1709
|
-
}
|
|
1710
|
-
function addVoice(score, options) {
|
|
1711
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1712
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1713
|
-
}
|
|
1714
|
-
const part = score.parts[options.partIndex];
|
|
1715
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1716
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1717
|
-
}
|
|
1718
|
-
const result = cloneScore(score);
|
|
1719
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1720
|
-
const existingVoiceEntries = getVoiceEntries(measure, options.voice, options.staff);
|
|
1721
|
-
if (existingVoiceEntries.length > 0) {
|
|
1722
|
-
return failure([operationError(
|
|
1723
|
-
"NOTE_CONFLICT",
|
|
1724
|
-
`Voice ${options.voice} already exists in this measure`,
|
|
1725
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice }
|
|
1726
|
-
)]);
|
|
1727
|
-
}
|
|
1728
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
1729
|
-
const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
|
|
1730
|
-
const rest = createRest(measureDuration, options.voice, options.staff);
|
|
1731
|
-
const currentEnd = getMeasureEndPosition(measure);
|
|
1732
|
-
if (currentEnd > 0) {
|
|
1733
|
-
measure.entries.push({ _id: generateId(), type: "backup", duration: currentEnd });
|
|
1734
|
-
}
|
|
1735
|
-
measure.entries.push(rest);
|
|
1736
|
-
return success(result);
|
|
1737
|
-
}
|
|
1738
|
-
function transposePitch(pitch, semitones) {
|
|
1739
|
-
const currentSemitone = STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0) + pitch.octave * 12;
|
|
1740
|
-
const targetSemitone = currentSemitone + semitones;
|
|
1741
|
-
const targetOctave = Math.floor(targetSemitone / 12);
|
|
1742
|
-
const targetPitchClass = (targetSemitone % 12 + 12) % 12;
|
|
1743
|
-
let bestStep = "C";
|
|
1744
|
-
let bestAlter = 99;
|
|
1745
|
-
for (const step of STEPS) {
|
|
1746
|
-
const stepSemitone = STEP_SEMITONES[step];
|
|
1747
|
-
let diff = targetPitchClass - stepSemitone;
|
|
1748
|
-
if (diff > 6) diff -= 12;
|
|
1749
|
-
if (diff < -6) diff += 12;
|
|
1750
|
-
if (diff >= -2 && diff <= 2) {
|
|
1751
|
-
if (Math.abs(diff) < Math.abs(bestAlter)) {
|
|
1752
|
-
bestStep = step;
|
|
1753
|
-
bestAlter = diff;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return {
|
|
1758
|
-
step: bestStep,
|
|
1759
|
-
octave: targetOctave,
|
|
1760
|
-
alter: bestAlter !== 0 ? bestAlter : void 0
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
function transpose(score, semitones) {
|
|
1764
|
-
if (semitones === 0) return success(score);
|
|
1765
|
-
const result = cloneScore(score);
|
|
1766
|
-
for (const part of result.parts) {
|
|
1767
|
-
for (const measure of part.measures) {
|
|
1768
|
-
for (const entry of measure.entries) {
|
|
1769
|
-
if (entry.type === "note" && entry.pitch) {
|
|
1770
|
-
entry.pitch = transposePitch(entry.pitch, semitones);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
return success(result);
|
|
1776
|
-
}
|
|
1777
|
-
function addPart(score, options) {
|
|
1778
|
-
if (score.parts.find((p) => p.id === options.id)) {
|
|
1779
|
-
return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.id}" already exists`, { partId: options.id })]);
|
|
1780
|
-
}
|
|
1781
|
-
const result = cloneScore(score);
|
|
1782
|
-
const insertIndex = options.insertIndex ?? result.parts.length;
|
|
1783
|
-
const partInfo = {
|
|
1784
|
-
_id: generateId(),
|
|
1785
|
-
type: "score-part",
|
|
1786
|
-
id: options.id,
|
|
1787
|
-
name: options.name,
|
|
1788
|
-
abbreviation: options.abbreviation
|
|
1789
|
-
};
|
|
1790
|
-
let partListInsertIndex = result.partList.length;
|
|
1791
|
-
let partCount = 0;
|
|
1792
|
-
for (let i = 0; i < result.partList.length; i++) {
|
|
1793
|
-
if (result.partList[i].type === "score-part") {
|
|
1794
|
-
if (partCount === insertIndex) {
|
|
1795
|
-
partListInsertIndex = i;
|
|
1796
|
-
break;
|
|
1797
|
-
}
|
|
1798
|
-
partCount++;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
result.partList.splice(partListInsertIndex, 0, partInfo);
|
|
1802
|
-
const measureCount = result.parts.length > 0 ? result.parts[0].measures.length : 1;
|
|
1803
|
-
const newPart = { _id: generateId(), id: options.id, measures: [] };
|
|
1804
|
-
for (let i = 0; i < measureCount; i++) {
|
|
1805
|
-
const measureNumber = result.parts.length > 0 ? result.parts[0].measures[i]?.number ?? String(i + 1) : String(i + 1);
|
|
1806
|
-
const measure = { _id: generateId(), number: measureNumber, entries: [] };
|
|
1807
|
-
if (i === 0) {
|
|
1808
|
-
measure.attributes = {
|
|
1809
|
-
divisions: options.divisions ?? 4,
|
|
1810
|
-
time: options.time ?? { beats: "4", beatType: 4 },
|
|
1811
|
-
key: options.key ?? { fifths: 0 },
|
|
1812
|
-
clef: options.clef ? [options.clef] : [{ sign: "G", line: 2 }]
|
|
1813
|
-
};
|
|
1814
|
-
}
|
|
1815
|
-
newPart.measures.push(measure);
|
|
1816
|
-
}
|
|
1817
|
-
result.parts.splice(insertIndex, 0, newPart);
|
|
1818
|
-
const validationResult = validate(result, { checkPartReferences: true, checkPartStructure: true });
|
|
1819
|
-
if (!validationResult.valid) {
|
|
1820
|
-
return failure(validationResult.errors);
|
|
1821
|
-
}
|
|
1822
|
-
return success(result, validationResult.warnings);
|
|
1823
|
-
}
|
|
1824
|
-
function removePart(score, partId) {
|
|
1825
|
-
const partIndex = score.parts.findIndex((p) => p.id === partId);
|
|
1826
|
-
if (partIndex === -1) {
|
|
1827
|
-
return failure([operationError("PART_NOT_FOUND", `Part "${partId}" not found`, { partId })]);
|
|
1828
|
-
}
|
|
1829
|
-
if (score.parts.length <= 1) {
|
|
1830
|
-
return failure([operationError("PART_NOT_FOUND", "Cannot remove the only remaining part", { partId })]);
|
|
1831
|
-
}
|
|
1832
|
-
const result = cloneScore(score);
|
|
1833
|
-
result.parts.splice(partIndex, 1);
|
|
1834
|
-
const partListIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === partId);
|
|
1835
|
-
if (partListIndex !== -1) {
|
|
1836
|
-
result.partList.splice(partListIndex, 1);
|
|
1837
|
-
}
|
|
1838
|
-
return success(result);
|
|
1839
|
-
}
|
|
1840
|
-
function duplicatePart(score, options) {
|
|
1841
|
-
const sourceIndex = score.parts.findIndex((p) => p.id === options.sourcePartId);
|
|
1842
|
-
if (sourceIndex === -1) {
|
|
1843
|
-
return failure([operationError("PART_NOT_FOUND", `Source part "${options.sourcePartId}" not found`, { partId: options.sourcePartId })]);
|
|
1844
|
-
}
|
|
1845
|
-
if (score.parts.find((p) => p.id === options.newPartId)) {
|
|
1846
|
-
return failure([operationError("DUPLICATE_PART_ID", `Part ID "${options.newPartId}" already exists`, { partId: options.newPartId })]);
|
|
1847
|
-
}
|
|
1848
|
-
const result = cloneScore(score);
|
|
1849
|
-
const sourcePart = result.parts[sourceIndex];
|
|
1850
|
-
const newPart = clonePartWithNewIds(sourcePart);
|
|
1851
|
-
newPart.id = options.newPartId;
|
|
1852
|
-
const sourcePartInfo = result.partList.find((e) => e.type === "score-part" && e.id === options.sourcePartId);
|
|
1853
|
-
const newPartInfo = {
|
|
1854
|
-
_id: generateId(),
|
|
1855
|
-
type: "score-part",
|
|
1856
|
-
id: options.newPartId,
|
|
1857
|
-
name: options.newPartName ?? sourcePartInfo?.name,
|
|
1858
|
-
abbreviation: sourcePartInfo?.abbreviation
|
|
1859
|
-
};
|
|
1860
|
-
result.parts.splice(sourceIndex + 1, 0, newPart);
|
|
1861
|
-
const partListSourceIndex = result.partList.findIndex((e) => e.type === "score-part" && e.id === options.sourcePartId);
|
|
1862
|
-
if (partListSourceIndex !== -1) {
|
|
1863
|
-
result.partList.splice(partListSourceIndex + 1, 0, newPartInfo);
|
|
1864
|
-
} else {
|
|
1865
|
-
result.partList.push(newPartInfo);
|
|
1866
|
-
}
|
|
1867
|
-
return success(result);
|
|
1868
|
-
}
|
|
1869
|
-
function setStaves(score, options) {
|
|
1870
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1871
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1872
|
-
}
|
|
1873
|
-
if (options.staves < 1) {
|
|
1874
|
-
return failure([operationError("INVALID_STAFF", `Staves count must be at least 1`, { partIndex: options.partIndex })]);
|
|
1875
|
-
}
|
|
1876
|
-
const result = cloneScore(score);
|
|
1877
|
-
const part = result.parts[options.partIndex];
|
|
1878
|
-
const fromMeasureIndex = options.fromMeasure ?? 0;
|
|
1879
|
-
const measure = part.measures[fromMeasureIndex];
|
|
1880
|
-
if (!measure) {
|
|
1881
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${fromMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: fromMeasureIndex })]);
|
|
1882
|
-
}
|
|
1883
|
-
if (!measure.attributes) {
|
|
1884
|
-
measure.attributes = {};
|
|
1885
|
-
}
|
|
1886
|
-
measure.attributes.staves = options.staves;
|
|
1887
|
-
if (options.clefs) {
|
|
1888
|
-
measure.attributes.clef = options.clefs;
|
|
1889
|
-
} else {
|
|
1890
|
-
const existingClefs = measure.attributes.clef ?? [];
|
|
1891
|
-
const newClefs = [...existingClefs];
|
|
1892
|
-
for (let staff = existingClefs.length + 1; staff <= options.staves; staff++) {
|
|
1893
|
-
newClefs.push(staff === 2 ? { sign: "F", line: 4, staff } : { sign: "G", line: 2, staff });
|
|
1894
|
-
}
|
|
1895
|
-
measure.attributes.clef = newClefs;
|
|
1896
|
-
}
|
|
1897
|
-
const validationResult = validate(result, { checkVoiceStaff: true, checkStaffStructure: true });
|
|
1898
|
-
if (!validationResult.valid) {
|
|
1899
|
-
return failure(validationResult.errors);
|
|
1900
|
-
}
|
|
1901
|
-
return success(result, validationResult.warnings);
|
|
1902
|
-
}
|
|
1903
|
-
function moveNoteToStaff(score, options) {
|
|
1904
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
1905
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
1906
|
-
}
|
|
1907
|
-
const part = score.parts[options.partIndex];
|
|
1908
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
1909
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1910
|
-
}
|
|
1911
|
-
if (options.targetStaff < 1) {
|
|
1912
|
-
return failure([operationError("INVALID_STAFF", `Target staff must be at least 1`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1913
|
-
}
|
|
1914
|
-
const result = cloneScore(score);
|
|
1915
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
1916
|
-
let noteCount = 0;
|
|
1917
|
-
for (const entry of measure.entries) {
|
|
1918
|
-
if (entry.type === "note" && !entry.rest) {
|
|
1919
|
-
if (noteCount === options.noteIndex) {
|
|
1920
|
-
entry.staff = options.targetStaff;
|
|
1921
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
1922
|
-
const errors = validateMeasureLocal(measure, context, { checkVoiceStaff: true });
|
|
1923
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
1924
|
-
if (criticalErrors.length > 0) {
|
|
1925
|
-
return failure(criticalErrors);
|
|
1926
|
-
}
|
|
1927
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
1928
|
-
}
|
|
1929
|
-
noteCount++;
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
1933
|
-
}
|
|
1934
|
-
function changeKey(score, key, options) {
|
|
1935
|
-
const result = cloneScore(score);
|
|
1936
|
-
const targetMeasure = String(options.fromMeasure);
|
|
1937
|
-
for (const part of result.parts) {
|
|
1938
|
-
for (const measure of part.measures) {
|
|
1939
|
-
if (measure.number === targetMeasure) {
|
|
1940
|
-
if (!measure.attributes) measure.attributes = {};
|
|
1941
|
-
measure.attributes.key = key;
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
return result;
|
|
1946
|
-
}
|
|
1947
|
-
function changeTime(score, time, options) {
|
|
1948
|
-
const result = cloneScore(score);
|
|
1949
|
-
const targetMeasure = String(options.fromMeasure);
|
|
1950
|
-
for (const part of result.parts) {
|
|
1951
|
-
for (const measure of part.measures) {
|
|
1952
|
-
if (measure.number === targetMeasure) {
|
|
1953
|
-
if (!measure.attributes) measure.attributes = {};
|
|
1954
|
-
measure.attributes.time = time;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
return result;
|
|
1959
|
-
}
|
|
1960
|
-
function insertMeasure(score, options) {
|
|
1961
|
-
const result = cloneScore(score);
|
|
1962
|
-
const targetMeasure = String(options.afterMeasure);
|
|
1963
|
-
for (const part of result.parts) {
|
|
1964
|
-
const insertIndex = part.measures.findIndex((m) => m.number === targetMeasure);
|
|
1965
|
-
if (insertIndex === -1) continue;
|
|
1966
|
-
const numericPart = parseInt(targetMeasure, 10);
|
|
1967
|
-
const newMeasureNumber = String(isNaN(numericPart) ? insertIndex + 2 : numericPart + 1);
|
|
1968
|
-
const newMeasure = { _id: generateId(), number: newMeasureNumber, entries: [] };
|
|
1969
|
-
if (options.copyAttributes && part.measures[insertIndex].attributes) {
|
|
1970
|
-
newMeasure.attributes = { ...part.measures[insertIndex].attributes };
|
|
1971
|
-
}
|
|
1972
|
-
part.measures.splice(insertIndex + 1, 0, newMeasure);
|
|
1973
|
-
for (let i = insertIndex + 2; i < part.measures.length; i++) {
|
|
1974
|
-
const currentNum = parseInt(part.measures[i].number, 10);
|
|
1975
|
-
if (!isNaN(currentNum)) {
|
|
1976
|
-
part.measures[i].number = String(currentNum + 1);
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
return result;
|
|
1981
|
-
}
|
|
1982
|
-
function deleteMeasure(score, measureNumber) {
|
|
1983
|
-
const result = cloneScore(score);
|
|
1984
|
-
const targetMeasure = String(measureNumber);
|
|
1985
|
-
for (const part of result.parts) {
|
|
1986
|
-
const deleteIndex = part.measures.findIndex((m) => m.number === targetMeasure);
|
|
1987
|
-
if (deleteIndex === -1) continue;
|
|
1988
|
-
part.measures.splice(deleteIndex, 1);
|
|
1989
|
-
for (let i = deleteIndex; i < part.measures.length; i++) {
|
|
1990
|
-
const currentNum = parseInt(part.measures[i].number, 10);
|
|
1991
|
-
if (!isNaN(currentNum)) {
|
|
1992
|
-
part.measures[i].number = String(currentNum - 1);
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
return result;
|
|
1997
|
-
}
|
|
1998
|
-
function findNoteByIndex(measure, noteIndex) {
|
|
1999
|
-
let noteCount = 0;
|
|
2000
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
2001
|
-
const entry = measure.entries[i];
|
|
2002
|
-
if (entry.type === "note" && !entry.rest) {
|
|
2003
|
-
if (noteCount === noteIndex) {
|
|
2004
|
-
return { note: entry, entryIndex: i };
|
|
2005
|
-
}
|
|
2006
|
-
noteCount++;
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
return null;
|
|
2010
|
-
}
|
|
2011
|
-
function pitchesEqual(p1, p2) {
|
|
2012
|
-
return p1.step === p2.step && p1.octave === p2.octave && (p1.alter ?? 0) === (p2.alter ?? 0);
|
|
2013
|
-
}
|
|
2014
|
-
function addTie(score, options) {
|
|
2015
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2016
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2017
|
-
}
|
|
2018
|
-
const part = score.parts[options.partIndex];
|
|
2019
|
-
if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
|
|
2020
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
2021
|
-
}
|
|
2022
|
-
if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
|
|
2023
|
-
return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
|
|
2024
|
-
}
|
|
2025
|
-
const result = cloneScore(score);
|
|
2026
|
-
const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
|
|
2027
|
-
const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
|
|
2028
|
-
const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
|
|
2029
|
-
if (!startResult) {
|
|
2030
|
-
return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
2031
|
-
}
|
|
2032
|
-
const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
|
|
2033
|
-
if (!endResult) {
|
|
2034
|
-
return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
|
|
2035
|
-
}
|
|
2036
|
-
const startNote = startResult.note;
|
|
2037
|
-
const endNote = endResult.note;
|
|
2038
|
-
if (!startNote.pitch || !endNote.pitch) {
|
|
2039
|
-
return failure([operationError("TIE_INVALID_TARGET", "Cannot tie notes without pitch", { partIndex: options.partIndex })]);
|
|
2040
|
-
}
|
|
2041
|
-
if (!pitchesEqual(startNote.pitch, endNote.pitch)) {
|
|
2042
|
-
return failure([operationError("TIE_PITCH_MISMATCH", "Tied notes must have the same pitch", { partIndex: options.partIndex }, { startPitch: startNote.pitch, endPitch: endNote.pitch })]);
|
|
2043
|
-
}
|
|
2044
|
-
if (startNote.tie?.type === "start" || startNote.tie?.type === "continue") {
|
|
2045
|
-
return failure([operationError("TIE_ALREADY_EXISTS", "Start note already has a tie start", { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
2046
|
-
}
|
|
2047
|
-
startNote.tie = { type: "start" };
|
|
2048
|
-
if (!startNote.notations) startNote.notations = [];
|
|
2049
|
-
startNote.notations.push({ type: "tied", tiedType: "start" });
|
|
2050
|
-
endNote.tie = { type: "stop" };
|
|
2051
|
-
if (!endNote.notations) endNote.notations = [];
|
|
2052
|
-
endNote.notations.push({ type: "tied", tiedType: "stop" });
|
|
2053
|
-
const validationResult = validate(result, { checkTies: true });
|
|
2054
|
-
const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
|
|
2055
|
-
if (criticalErrors.length > 0) {
|
|
2056
|
-
return failure(criticalErrors);
|
|
2057
|
-
}
|
|
2058
|
-
return success(result, validationResult.warnings);
|
|
2059
|
-
}
|
|
2060
|
-
function removeTie(score, options) {
|
|
2061
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2062
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2063
|
-
}
|
|
2064
|
-
const part = score.parts[options.partIndex];
|
|
2065
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2066
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2067
|
-
}
|
|
2068
|
-
const result = cloneScore(score);
|
|
2069
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2070
|
-
const noteResult = findNoteByIndex(measure, options.noteIndex);
|
|
2071
|
-
if (!noteResult) {
|
|
2072
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2073
|
-
}
|
|
2074
|
-
const note = noteResult.note;
|
|
2075
|
-
if (!note.tie) {
|
|
2076
|
-
return failure([operationError("TIE_NOT_FOUND", "Note does not have a tie", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2077
|
-
}
|
|
2078
|
-
delete note.tie;
|
|
2079
|
-
delete note.ties;
|
|
2080
|
-
if (note.notations) {
|
|
2081
|
-
note.notations = note.notations.filter((n) => n.type !== "tied");
|
|
2082
|
-
if (note.notations.length === 0) {
|
|
2083
|
-
delete note.notations;
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
return success(result);
|
|
2087
|
-
}
|
|
2088
|
-
function addSlur(score, options) {
|
|
2089
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2090
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2091
|
-
}
|
|
2092
|
-
const part = score.parts[options.partIndex];
|
|
2093
|
-
if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
|
|
2094
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
2095
|
-
}
|
|
2096
|
-
if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
|
|
2097
|
-
return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
|
|
2098
|
-
}
|
|
2099
|
-
const result = cloneScore(score);
|
|
2100
|
-
const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
|
|
2101
|
-
const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
|
|
2102
|
-
const startResult = findNoteByIndex(startMeasure, options.startNoteIndex);
|
|
2103
|
-
if (!startResult) {
|
|
2104
|
-
return failure([operationError("NOTE_NOT_FOUND", `Start note index ${options.startNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
2105
|
-
}
|
|
2106
|
-
const endResult = findNoteByIndex(endMeasure, options.endNoteIndex);
|
|
2107
|
-
if (!endResult) {
|
|
2108
|
-
return failure([operationError("NOTE_NOT_FOUND", `End note index ${options.endNoteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
|
|
2109
|
-
}
|
|
2110
|
-
const startNote = startResult.note;
|
|
2111
|
-
const endNote = endResult.note;
|
|
2112
|
-
const slurNumber = options.number ?? 1;
|
|
2113
|
-
if (startNote.notations?.some((n) => n.type === "slur" && n.slurType === "start" && (n.number ?? 1) === slurNumber)) {
|
|
2114
|
-
return failure([operationError("SLUR_ALREADY_EXISTS", `Slur ${slurNumber} already starts on this note`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
2115
|
-
}
|
|
2116
|
-
if (!startNote.notations) startNote.notations = [];
|
|
2117
|
-
startNote.notations.push({
|
|
2118
|
-
type: "slur",
|
|
2119
|
-
slurType: "start",
|
|
2120
|
-
number: slurNumber,
|
|
2121
|
-
placement: options.placement
|
|
2122
|
-
});
|
|
2123
|
-
if (!endNote.notations) endNote.notations = [];
|
|
2124
|
-
endNote.notations.push({
|
|
2125
|
-
type: "slur",
|
|
2126
|
-
slurType: "stop",
|
|
2127
|
-
number: slurNumber
|
|
2128
|
-
});
|
|
2129
|
-
const validationResult = validate(result, { checkSlurs: true });
|
|
2130
|
-
const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
|
|
2131
|
-
if (criticalErrors.length > 0) {
|
|
2132
|
-
return failure(criticalErrors);
|
|
2133
|
-
}
|
|
2134
|
-
return success(result, validationResult.warnings);
|
|
2135
|
-
}
|
|
2136
|
-
function removeSlur(score, options) {
|
|
2137
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2138
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2139
|
-
}
|
|
2140
|
-
const part = score.parts[options.partIndex];
|
|
2141
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2142
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2143
|
-
}
|
|
2144
|
-
const result = cloneScore(score);
|
|
2145
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2146
|
-
const noteResult = findNoteByIndex(measure, options.noteIndex);
|
|
2147
|
-
if (!noteResult) {
|
|
2148
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2149
|
-
}
|
|
2150
|
-
const note = noteResult.note;
|
|
2151
|
-
const slurNumber = options.number ?? 1;
|
|
2152
|
-
if (!note.notations?.some((n) => n.type === "slur" && (n.number ?? 1) === slurNumber)) {
|
|
2153
|
-
return failure([operationError("SLUR_NOT_FOUND", `Slur ${slurNumber} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2154
|
-
}
|
|
2155
|
-
note.notations = note.notations.filter((n) => !(n.type === "slur" && (n.number ?? 1) === slurNumber));
|
|
2156
|
-
if (note.notations.length === 0) {
|
|
2157
|
-
delete note.notations;
|
|
2158
|
-
}
|
|
2159
|
-
return success(result);
|
|
2160
|
-
}
|
|
2161
|
-
function addArticulation(score, options) {
|
|
2162
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2163
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2164
|
-
}
|
|
2165
|
-
const part = score.parts[options.partIndex];
|
|
2166
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2167
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2168
|
-
}
|
|
2169
|
-
const result = cloneScore(score);
|
|
2170
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2171
|
-
const noteResult = findNoteByIndex(measure, options.noteIndex);
|
|
2172
|
-
if (!noteResult) {
|
|
2173
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2174
|
-
}
|
|
2175
|
-
const note = noteResult.note;
|
|
2176
|
-
if (note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
|
|
2177
|
-
return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Articulation ${options.articulation} already exists on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2178
|
-
}
|
|
2179
|
-
if (!note.notations) note.notations = [];
|
|
2180
|
-
note.notations.push({
|
|
2181
|
-
type: "articulation",
|
|
2182
|
-
articulation: options.articulation,
|
|
2183
|
-
placement: options.placement
|
|
2184
|
-
});
|
|
2185
|
-
return success(result);
|
|
2186
|
-
}
|
|
2187
|
-
function removeArticulation(score, options) {
|
|
2188
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2189
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2190
|
-
}
|
|
2191
|
-
const part = score.parts[options.partIndex];
|
|
2192
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2193
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2194
|
-
}
|
|
2195
|
-
const result = cloneScore(score);
|
|
2196
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2197
|
-
const noteResult = findNoteByIndex(measure, options.noteIndex);
|
|
2198
|
-
if (!noteResult) {
|
|
2199
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2200
|
-
}
|
|
2201
|
-
const note = noteResult.note;
|
|
2202
|
-
if (!note.notations?.some((n) => n.type === "articulation" && n.articulation === options.articulation)) {
|
|
2203
|
-
return failure([operationError("ARTICULATION_NOT_FOUND", `Articulation ${options.articulation} not found on this note`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2204
|
-
}
|
|
2205
|
-
note.notations = note.notations.filter((n) => !(n.type === "articulation" && n.articulation === options.articulation));
|
|
2206
|
-
if (note.notations.length === 0) {
|
|
2207
|
-
delete note.notations;
|
|
2208
|
-
}
|
|
2209
|
-
return success(result);
|
|
2210
|
-
}
|
|
2211
|
-
function getInsertPositionForDirection(measure, targetPosition) {
|
|
2212
|
-
let position = 0;
|
|
2213
|
-
let insertIndex = 0;
|
|
2214
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
2215
|
-
const entry = measure.entries[i];
|
|
2216
|
-
if (position >= targetPosition) {
|
|
2217
|
-
return insertIndex;
|
|
2218
|
-
}
|
|
2219
|
-
if (entry.type === "note" && !entry.chord) {
|
|
2220
|
-
position += entry.duration;
|
|
2221
|
-
} else if (entry.type === "backup") {
|
|
2222
|
-
position -= entry.duration;
|
|
2223
|
-
} else if (entry.type === "forward") {
|
|
2224
|
-
position += entry.duration;
|
|
2225
|
-
}
|
|
2226
|
-
insertIndex = i + 1;
|
|
2227
|
-
}
|
|
2228
|
-
return insertIndex;
|
|
2229
|
-
}
|
|
2230
|
-
function addDynamics(score, options) {
|
|
2231
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2232
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2233
|
-
}
|
|
2234
|
-
const part = score.parts[options.partIndex];
|
|
2235
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2236
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2237
|
-
}
|
|
2238
|
-
if (options.position < 0) {
|
|
2239
|
-
return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2240
|
-
}
|
|
2241
|
-
const result = cloneScore(score);
|
|
2242
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2243
|
-
const directionEntry = {
|
|
2244
|
-
_id: generateId(),
|
|
2245
|
-
type: "direction",
|
|
2246
|
-
directionTypes: [{
|
|
2247
|
-
kind: "dynamics",
|
|
2248
|
-
value: options.dynamics
|
|
2249
|
-
}],
|
|
2250
|
-
placement: options.placement ?? "below",
|
|
2251
|
-
staff: options.staff
|
|
2252
|
-
};
|
|
2253
|
-
const insertIndex = getInsertPositionForDirection(measure, options.position);
|
|
2254
|
-
measure.entries.splice(insertIndex, 0, directionEntry);
|
|
2255
|
-
return success(result);
|
|
2256
|
-
}
|
|
2257
|
-
function removeDynamics(score, options) {
|
|
2258
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2259
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2260
|
-
}
|
|
2261
|
-
const part = score.parts[options.partIndex];
|
|
2262
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2263
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2264
|
-
}
|
|
2265
|
-
const result = cloneScore(score);
|
|
2266
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2267
|
-
let directionCount = 0;
|
|
2268
|
-
let targetIndex = -1;
|
|
2269
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
2270
|
-
const entry = measure.entries[i];
|
|
2271
|
-
if (entry.type === "direction") {
|
|
2272
|
-
const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
|
|
2273
|
-
if (hasDynamics) {
|
|
2274
|
-
if (directionCount === options.directionIndex) {
|
|
2275
|
-
targetIndex = i;
|
|
2276
|
-
break;
|
|
2277
|
-
}
|
|
2278
|
-
directionCount++;
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
if (targetIndex === -1) {
|
|
2283
|
-
return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2284
|
-
}
|
|
2285
|
-
measure.entries.splice(targetIndex, 1);
|
|
2286
|
-
return success(result);
|
|
2287
|
-
}
|
|
2288
|
-
function modifyDynamics(score, options) {
|
|
2289
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2290
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2291
|
-
}
|
|
2292
|
-
const part = score.parts[options.partIndex];
|
|
2293
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2294
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2295
|
-
}
|
|
2296
|
-
const result = cloneScore(score);
|
|
2297
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2298
|
-
let dynamicsCount = 0;
|
|
2299
|
-
let targetIndex = -1;
|
|
2300
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
2301
|
-
const entry = measure.entries[i];
|
|
2302
|
-
if (entry.type === "direction") {
|
|
2303
|
-
const hasDynamics = entry.directionTypes.some((dt) => dt.kind === "dynamics");
|
|
2304
|
-
if (hasDynamics) {
|
|
2305
|
-
if (dynamicsCount === options.directionIndex) {
|
|
2306
|
-
targetIndex = i;
|
|
2307
|
-
break;
|
|
2308
|
-
}
|
|
2309
|
-
dynamicsCount++;
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
if (targetIndex === -1) {
|
|
2314
|
-
return failure([operationError("DYNAMICS_NOT_FOUND", `Dynamics direction index ${options.directionIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2315
|
-
}
|
|
2316
|
-
const direction = measure.entries[targetIndex];
|
|
2317
|
-
const dynamicsType = direction.directionTypes.find((dt) => dt.kind === "dynamics");
|
|
2318
|
-
if (dynamicsType && dynamicsType.kind === "dynamics") {
|
|
2319
|
-
dynamicsType.value = options.dynamics;
|
|
2320
|
-
}
|
|
2321
|
-
if (options.placement !== void 0) {
|
|
2322
|
-
direction.placement = options.placement;
|
|
2323
|
-
}
|
|
2324
|
-
return success(result);
|
|
2325
|
-
}
|
|
2326
|
-
function insertClefChange(score, options) {
|
|
2327
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2328
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2329
|
-
}
|
|
2330
|
-
const part = score.parts[options.partIndex];
|
|
2331
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2332
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2333
|
-
}
|
|
2334
|
-
if (options.position < 0) {
|
|
2335
|
-
return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2336
|
-
}
|
|
2337
|
-
const validSigns = ["G", "F", "C", "percussion", "TAB"];
|
|
2338
|
-
if (!validSigns.includes(options.clef.sign)) {
|
|
2339
|
-
return failure([operationError("INVALID_CLEF", `Invalid clef sign: ${options.clef.sign}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2340
|
-
}
|
|
2341
|
-
const result = cloneScore(score);
|
|
2342
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2343
|
-
if (options.position === 0) {
|
|
2344
|
-
if (!measure.attributes) {
|
|
2345
|
-
measure.attributes = {};
|
|
2346
|
-
}
|
|
2347
|
-
const staff = options.clef.staff ?? 1;
|
|
2348
|
-
if (!measure.attributes.clef) {
|
|
2349
|
-
measure.attributes.clef = [];
|
|
2350
|
-
}
|
|
2351
|
-
const existingIndex = measure.attributes.clef.findIndex((c) => (c.staff ?? 1) === staff);
|
|
2352
|
-
if (existingIndex >= 0) {
|
|
2353
|
-
measure.attributes.clef[existingIndex] = options.clef;
|
|
2354
|
-
} else {
|
|
2355
|
-
measure.attributes.clef.push(options.clef);
|
|
2356
|
-
}
|
|
2357
|
-
} else {
|
|
2358
|
-
const attributesEntry = {
|
|
2359
|
-
_id: generateId(),
|
|
2360
|
-
type: "attributes",
|
|
2361
|
-
attributes: {
|
|
2362
|
-
clef: [options.clef]
|
|
2363
|
-
}
|
|
2364
|
-
};
|
|
2365
|
-
const insertIndex = getInsertPositionForDirection(measure, options.position);
|
|
2366
|
-
measure.entries.splice(insertIndex, 0, attributesEntry);
|
|
2367
|
-
}
|
|
2368
|
-
const validationResult = validate(result, { checkStaffStructure: true });
|
|
2369
|
-
const criticalErrors = validationResult.errors.filter((e) => e.level === "error");
|
|
2370
|
-
if (criticalErrors.length > 0) {
|
|
2371
|
-
return failure(criticalErrors);
|
|
2372
|
-
}
|
|
2373
|
-
return success(result, validationResult.warnings);
|
|
2374
|
-
}
|
|
2375
|
-
var addNote = (score, options) => {
|
|
2376
|
-
const result = insertNote(score, {
|
|
2377
|
-
partIndex: options.partIndex,
|
|
2378
|
-
measureIndex: options.measureIndex,
|
|
2379
|
-
voice: options.voice,
|
|
2380
|
-
staff: options.staff,
|
|
2381
|
-
position: options.position,
|
|
2382
|
-
pitch: options.note.pitch ?? { step: "C", octave: 4 },
|
|
2383
|
-
duration: options.note.duration,
|
|
2384
|
-
noteType: options.note.noteType,
|
|
2385
|
-
dots: options.note.dots
|
|
2386
|
-
});
|
|
2387
|
-
return result.success ? result.data : score;
|
|
2388
|
-
};
|
|
2389
|
-
var deleteNote = (score, options) => {
|
|
2390
|
-
const result = removeNote(score, options);
|
|
2391
|
-
return result.success ? result.data : score;
|
|
2392
|
-
};
|
|
2393
|
-
var addChordNote = (score, options) => {
|
|
2394
|
-
const result = addChord(score, { ...options, noteIndex: options.afterNoteIndex });
|
|
2395
|
-
return result.success ? result.data : score;
|
|
2396
|
-
};
|
|
2397
|
-
var modifyNotePitch = (score, options) => {
|
|
2398
|
-
const result = setNotePitch(score, options);
|
|
2399
|
-
return result.success ? result.data : score;
|
|
2400
|
-
};
|
|
2401
|
-
var modifyNoteDuration = (score, options) => {
|
|
2402
|
-
const result = changeNoteDuration(score, { ...options, newDuration: options.duration });
|
|
2403
|
-
return result.success ? result.data : score;
|
|
2404
|
-
};
|
|
2405
|
-
var addNoteChecked = (score, options) => {
|
|
2406
|
-
return insertNote(score, {
|
|
2407
|
-
partIndex: options.partIndex,
|
|
2408
|
-
measureIndex: options.measureIndex,
|
|
2409
|
-
voice: options.voice,
|
|
2410
|
-
staff: options.staff,
|
|
2411
|
-
position: options.position,
|
|
2412
|
-
pitch: options.note.pitch ?? { step: "C", octave: 4 },
|
|
2413
|
-
duration: options.note.duration,
|
|
2414
|
-
noteType: options.note.noteType,
|
|
2415
|
-
dots: options.note.dots
|
|
2416
|
-
});
|
|
2417
|
-
};
|
|
2418
|
-
var deleteNoteChecked = removeNote;
|
|
2419
|
-
var addChordNoteChecked = (score, options) => {
|
|
2420
|
-
return addChord(score, { ...options, noteIndex: options.afterNoteIndex });
|
|
2421
|
-
};
|
|
2422
|
-
var modifyNotePitchChecked = setNotePitch;
|
|
2423
|
-
var modifyNoteDurationChecked = (score, options) => {
|
|
2424
|
-
return changeNoteDuration(score, { ...options, newDuration: options.duration });
|
|
2425
|
-
};
|
|
2426
|
-
var transposeChecked = transpose;
|
|
2427
|
-
function createTuplet(score, options) {
|
|
2428
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2429
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2430
|
-
}
|
|
2431
|
-
const part = score.parts[options.partIndex];
|
|
2432
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2433
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2434
|
-
}
|
|
2435
|
-
if (options.noteCount < 2) {
|
|
2436
|
-
return failure([operationError("INVALID_DURATION", "Tuplet must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2437
|
-
}
|
|
2438
|
-
if (options.actualNotes < 2 || options.normalNotes < 1) {
|
|
2439
|
-
return failure([operationError("INVALID_DURATION", "Invalid tuplet ratio", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2440
|
-
}
|
|
2441
|
-
const result = cloneScore(score);
|
|
2442
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2443
|
-
const notes = [];
|
|
2444
|
-
let noteCount = 0;
|
|
2445
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
2446
|
-
const entry = measure.entries[i];
|
|
2447
|
-
if (entry.type === "note" && !entry.rest && !entry.chord) {
|
|
2448
|
-
if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
|
|
2449
|
-
notes.push({ note: entry, entryIndex: i });
|
|
2450
|
-
}
|
|
2451
|
-
noteCount++;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
if (notes.length !== options.noteCount) {
|
|
2455
|
-
return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2456
|
-
}
|
|
2457
|
-
const voice = notes[0].note.voice;
|
|
2458
|
-
const staff = notes[0].note.staff;
|
|
2459
|
-
if (!notes.every((n) => n.note.voice === voice)) {
|
|
2460
|
-
return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
|
|
2461
|
-
}
|
|
2462
|
-
if (!notes.every((n) => n.note.staff === staff)) {
|
|
2463
|
-
return failure([operationError("NOTE_CONFLICT", "All notes in a tuplet must be on the same staff", { partIndex: options.partIndex, measureIndex: options.measureIndex, staff })]);
|
|
2464
|
-
}
|
|
2465
|
-
const tupletNumber = 1;
|
|
2466
|
-
for (let i = 0; i < notes.length; i++) {
|
|
2467
|
-
const { note } = notes[i];
|
|
2468
|
-
note.timeModification = {
|
|
2469
|
-
actualNotes: options.actualNotes,
|
|
2470
|
-
normalNotes: options.normalNotes
|
|
2471
|
-
};
|
|
2472
|
-
if (!note.notations) note.notations = [];
|
|
2473
|
-
if (i === 0) {
|
|
2474
|
-
note.notations.push({
|
|
2475
|
-
type: "tuplet",
|
|
2476
|
-
tupletType: "start",
|
|
2477
|
-
number: tupletNumber,
|
|
2478
|
-
bracket: options.bracket ?? true,
|
|
2479
|
-
showNumber: options.showNumber ?? "actual",
|
|
2480
|
-
tupletActual: { tupletNumber: options.actualNotes },
|
|
2481
|
-
tupletNormal: { tupletNumber: options.normalNotes }
|
|
2482
|
-
});
|
|
2483
|
-
} else if (i === notes.length - 1) {
|
|
2484
|
-
note.notations.push({
|
|
2485
|
-
type: "tuplet",
|
|
2486
|
-
tupletType: "stop",
|
|
2487
|
-
number: tupletNumber
|
|
2488
|
-
});
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
2492
|
-
const errors = validateMeasureLocal(measure, context, {
|
|
2493
|
-
checkTuplets: true,
|
|
2494
|
-
checkMeasureDuration: true
|
|
2495
|
-
});
|
|
2496
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
2497
|
-
if (criticalErrors.length > 0) {
|
|
2498
|
-
return failure(criticalErrors);
|
|
2499
|
-
}
|
|
2500
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
2501
|
-
}
|
|
2502
|
-
function removeTuplet(score, options) {
|
|
2503
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2504
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2505
|
-
}
|
|
2506
|
-
const part = score.parts[options.partIndex];
|
|
2507
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2508
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2509
|
-
}
|
|
2510
|
-
const result = cloneScore(score);
|
|
2511
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2512
|
-
let noteCount = 0;
|
|
2513
|
-
let targetNote = null;
|
|
2514
|
-
let targetEntryIndex = -1;
|
|
2515
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
2516
|
-
const entry = measure.entries[i];
|
|
2517
|
-
if (entry.type === "note" && !entry.rest) {
|
|
2518
|
-
if (noteCount === options.noteIndex) {
|
|
2519
|
-
targetNote = entry;
|
|
2520
|
-
targetEntryIndex = i;
|
|
2521
|
-
break;
|
|
2522
|
-
}
|
|
2523
|
-
noteCount++;
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
if (!targetNote || targetEntryIndex === -1) {
|
|
2527
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2528
|
-
}
|
|
2529
|
-
if (!targetNote.timeModification) {
|
|
2530
|
-
return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a tuplet", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2531
|
-
}
|
|
2532
|
-
const voice = targetNote.voice;
|
|
2533
|
-
const staff = targetNote.staff;
|
|
2534
|
-
const actualNotes = targetNote.timeModification.actualNotes;
|
|
2535
|
-
const normalNotes = targetNote.timeModification.normalNotes;
|
|
2536
|
-
const tupletNotes = [];
|
|
2537
|
-
let inTuplet = false;
|
|
2538
|
-
let currentTupletNumber;
|
|
2539
|
-
for (const entry of measure.entries) {
|
|
2540
|
-
if (entry.type !== "note" || entry.rest) continue;
|
|
2541
|
-
if (entry.voice !== voice || entry.staff !== staff) continue;
|
|
2542
|
-
const hasSameTimeModification = entry.timeModification?.actualNotes === actualNotes && entry.timeModification?.normalNotes === normalNotes;
|
|
2543
|
-
const tupletStart = entry.notations?.find(
|
|
2544
|
-
(n) => n.type === "tuplet" && n.tupletType === "start"
|
|
2545
|
-
);
|
|
2546
|
-
const tupletStop = entry.notations?.find(
|
|
2547
|
-
(n) => n.type === "tuplet" && n.tupletType === "stop" && (currentTupletNumber === void 0 || n.number === currentTupletNumber)
|
|
2548
|
-
);
|
|
2549
|
-
if (tupletStart && tupletStart.type === "tuplet") {
|
|
2550
|
-
inTuplet = true;
|
|
2551
|
-
currentTupletNumber = tupletStart.number;
|
|
2552
|
-
}
|
|
2553
|
-
if (inTuplet && hasSameTimeModification) {
|
|
2554
|
-
tupletNotes.push(entry);
|
|
2555
|
-
}
|
|
2556
|
-
if (tupletStop && inTuplet) {
|
|
2557
|
-
if (tupletNotes.includes(targetNote)) {
|
|
2558
|
-
break;
|
|
2559
|
-
} else {
|
|
2560
|
-
tupletNotes.length = 0;
|
|
2561
|
-
inTuplet = false;
|
|
2562
|
-
currentTupletNumber = void 0;
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
if (tupletNotes.length === 0) {
|
|
2567
|
-
tupletNotes.push(targetNote);
|
|
2568
|
-
}
|
|
2569
|
-
for (const note of tupletNotes) {
|
|
2570
|
-
delete note.timeModification;
|
|
2571
|
-
if (note.notations) {
|
|
2572
|
-
note.notations = note.notations.filter((n) => n.type !== "tuplet");
|
|
2573
|
-
if (note.notations.length === 0) {
|
|
2574
|
-
delete note.notations;
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
return success(result);
|
|
2579
|
-
}
|
|
2580
|
-
function addBeam(score, options) {
|
|
2581
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2582
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2583
|
-
}
|
|
2584
|
-
const part = score.parts[options.partIndex];
|
|
2585
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2586
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2587
|
-
}
|
|
2588
|
-
if (options.noteCount < 2) {
|
|
2589
|
-
return failure([operationError("INVALID_DURATION", "Beam must contain at least 2 notes", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2590
|
-
}
|
|
2591
|
-
const result = cloneScore(score);
|
|
2592
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2593
|
-
const beamLevel = options.beamLevel ?? 1;
|
|
2594
|
-
const notes = [];
|
|
2595
|
-
let noteCount = 0;
|
|
2596
|
-
for (const entry of measure.entries) {
|
|
2597
|
-
if (entry.type === "note" && !entry.rest && !entry.chord) {
|
|
2598
|
-
if (noteCount >= options.startNoteIndex && noteCount < options.startNoteIndex + options.noteCount) {
|
|
2599
|
-
notes.push(entry);
|
|
2600
|
-
}
|
|
2601
|
-
noteCount++;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
if (notes.length !== options.noteCount) {
|
|
2605
|
-
return failure([operationError("NOTE_NOT_FOUND", `Could not find ${options.noteCount} notes starting at index ${options.startNoteIndex}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2606
|
-
}
|
|
2607
|
-
const voice = notes[0].voice;
|
|
2608
|
-
if (!notes.every((n) => n.voice === voice)) {
|
|
2609
|
-
return failure([operationError("NOTE_CONFLICT", "All beamed notes must be in the same voice", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice })]);
|
|
2610
|
-
}
|
|
2611
|
-
for (let i = 0; i < notes.length; i++) {
|
|
2612
|
-
const note = notes[i];
|
|
2613
|
-
if (!note.beam) {
|
|
2614
|
-
note.beam = [];
|
|
2615
|
-
}
|
|
2616
|
-
note.beam = note.beam.filter((b) => b.number !== beamLevel);
|
|
2617
|
-
let beamType;
|
|
2618
|
-
if (i === 0) {
|
|
2619
|
-
beamType = "begin";
|
|
2620
|
-
} else if (i === notes.length - 1) {
|
|
2621
|
-
beamType = "end";
|
|
2622
|
-
} else {
|
|
2623
|
-
beamType = "continue";
|
|
2624
|
-
}
|
|
2625
|
-
note.beam.push({
|
|
2626
|
-
number: beamLevel,
|
|
2627
|
-
type: beamType
|
|
2628
|
-
});
|
|
2629
|
-
}
|
|
2630
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
2631
|
-
const errors = validateMeasureLocal(measure, context, { checkBeams: true });
|
|
2632
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
2633
|
-
if (criticalErrors.length > 0) {
|
|
2634
|
-
return failure(criticalErrors);
|
|
2635
|
-
}
|
|
2636
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
2637
|
-
}
|
|
2638
|
-
function removeBeam(score, options) {
|
|
2639
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2640
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2641
|
-
}
|
|
2642
|
-
const part = score.parts[options.partIndex];
|
|
2643
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2644
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2645
|
-
}
|
|
2646
|
-
const result = cloneScore(score);
|
|
2647
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2648
|
-
let noteCount = 0;
|
|
2649
|
-
let targetNote = null;
|
|
2650
|
-
for (const entry of measure.entries) {
|
|
2651
|
-
if (entry.type === "note" && !entry.rest) {
|
|
2652
|
-
if (noteCount === options.noteIndex) {
|
|
2653
|
-
targetNote = entry;
|
|
2654
|
-
break;
|
|
2655
|
-
}
|
|
2656
|
-
noteCount++;
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
if (!targetNote) {
|
|
2660
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} not found`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2661
|
-
}
|
|
2662
|
-
if (!targetNote.beam || targetNote.beam.length === 0) {
|
|
2663
|
-
return failure([operationError("NOTE_NOT_FOUND", "Note is not part of a beam group", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2664
|
-
}
|
|
2665
|
-
const voice = targetNote.voice;
|
|
2666
|
-
const staff = targetNote.staff;
|
|
2667
|
-
const beamNotes = [];
|
|
2668
|
-
let inBeam = false;
|
|
2669
|
-
const targetBeamLevel = options.beamLevel ?? targetNote.beam[0]?.number ?? 1;
|
|
2670
|
-
for (const entry of measure.entries) {
|
|
2671
|
-
if (entry.type !== "note" || entry.rest) continue;
|
|
2672
|
-
if (entry.voice !== voice || entry.staff !== staff) continue;
|
|
2673
|
-
const beamInfo = entry.beam?.find((b) => b.number === targetBeamLevel);
|
|
2674
|
-
if (!beamInfo) {
|
|
2675
|
-
if (inBeam) {
|
|
2676
|
-
break;
|
|
2677
|
-
}
|
|
2678
|
-
continue;
|
|
2679
|
-
}
|
|
2680
|
-
if (beamInfo.type === "begin") {
|
|
2681
|
-
inBeam = true;
|
|
2682
|
-
beamNotes.push(entry);
|
|
2683
|
-
} else if (beamInfo.type === "continue") {
|
|
2684
|
-
if (inBeam) beamNotes.push(entry);
|
|
2685
|
-
} else if (beamInfo.type === "end") {
|
|
2686
|
-
beamNotes.push(entry);
|
|
2687
|
-
if (beamNotes.includes(targetNote)) {
|
|
2688
|
-
break;
|
|
2689
|
-
} else {
|
|
2690
|
-
beamNotes.length = 0;
|
|
2691
|
-
inBeam = false;
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
if (beamNotes.length === 0) {
|
|
2696
|
-
beamNotes.push(targetNote);
|
|
2697
|
-
}
|
|
2698
|
-
for (const note of beamNotes) {
|
|
2699
|
-
if (note.beam) {
|
|
2700
|
-
if (options.beamLevel !== void 0) {
|
|
2701
|
-
note.beam = note.beam.filter((b) => b.number !== options.beamLevel);
|
|
2702
|
-
} else {
|
|
2703
|
-
note.beam = [];
|
|
2704
|
-
}
|
|
2705
|
-
if (note.beam.length === 0) {
|
|
2706
|
-
delete note.beam;
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
return success(result);
|
|
2711
|
-
}
|
|
2712
|
-
function autoBeam(score, options) {
|
|
2713
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2714
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2715
|
-
}
|
|
2716
|
-
const part = score.parts[options.partIndex];
|
|
2717
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2718
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2719
|
-
}
|
|
2720
|
-
const result = cloneScore(score);
|
|
2721
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2722
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
2723
|
-
const divisions = context.divisions;
|
|
2724
|
-
const time = context.time ?? { beats: "4", beatType: 4 };
|
|
2725
|
-
const beatDuration = 4 / time.beatType * divisions;
|
|
2726
|
-
for (const entry of measure.entries) {
|
|
2727
|
-
if (entry.type === "note") {
|
|
2728
|
-
delete entry.beam;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
const notesByVoice = /* @__PURE__ */ new Map();
|
|
2732
|
-
let position = 0;
|
|
2733
|
-
for (const entry of measure.entries) {
|
|
2734
|
-
if (entry.type === "note") {
|
|
2735
|
-
if (!entry.chord && !entry.rest) {
|
|
2736
|
-
const voice = entry.voice ?? 1;
|
|
2737
|
-
if (options.voice === void 0 || voice === options.voice) {
|
|
2738
|
-
if (!notesByVoice.has(voice)) {
|
|
2739
|
-
notesByVoice.set(voice, []);
|
|
2740
|
-
}
|
|
2741
|
-
notesByVoice.get(voice).push({ note: entry, position });
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
if (!entry.chord) {
|
|
2745
|
-
position += entry.duration;
|
|
2746
|
-
}
|
|
2747
|
-
} else if (entry.type === "backup") {
|
|
2748
|
-
position -= entry.duration;
|
|
2749
|
-
} else if (entry.type === "forward") {
|
|
2750
|
-
position += entry.duration;
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
for (const [, notes] of notesByVoice) {
|
|
2754
|
-
const beatGroups = [];
|
|
2755
|
-
let currentBeat = -1;
|
|
2756
|
-
let currentGroup = [];
|
|
2757
|
-
for (const { note, position: notePos } of notes) {
|
|
2758
|
-
if (note.duration > beatDuration / 2) {
|
|
2759
|
-
if (currentGroup.length >= 2) {
|
|
2760
|
-
beatGroups.push(currentGroup);
|
|
2761
|
-
}
|
|
2762
|
-
currentGroup = [];
|
|
2763
|
-
currentBeat = -1;
|
|
2764
|
-
continue;
|
|
2765
|
-
}
|
|
2766
|
-
const beat = Math.floor(notePos / beatDuration);
|
|
2767
|
-
if (options.groupByBeat !== false && beat !== currentBeat) {
|
|
2768
|
-
if (currentGroup.length >= 2) {
|
|
2769
|
-
beatGroups.push(currentGroup);
|
|
2770
|
-
}
|
|
2771
|
-
currentGroup = [{ note, position: notePos }];
|
|
2772
|
-
currentBeat = beat;
|
|
2773
|
-
} else {
|
|
2774
|
-
currentGroup.push({ note, position: notePos });
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
if (currentGroup.length >= 2) {
|
|
2778
|
-
beatGroups.push(currentGroup);
|
|
2779
|
-
}
|
|
2780
|
-
for (const group of beatGroups) {
|
|
2781
|
-
for (let i = 0; i < group.length; i++) {
|
|
2782
|
-
const { note } = group[i];
|
|
2783
|
-
if (!note.beam) {
|
|
2784
|
-
note.beam = [];
|
|
2785
|
-
}
|
|
2786
|
-
let beamType;
|
|
2787
|
-
if (i === 0) {
|
|
2788
|
-
beamType = "begin";
|
|
2789
|
-
} else if (i === group.length - 1) {
|
|
2790
|
-
beamType = "end";
|
|
2791
|
-
} else {
|
|
2792
|
-
beamType = "continue";
|
|
2793
|
-
}
|
|
2794
|
-
note.beam.push({
|
|
2795
|
-
number: 1,
|
|
2796
|
-
type: beamType
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
const errors = validateMeasureLocal(measure, context, { checkBeams: true });
|
|
2802
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
2803
|
-
if (criticalErrors.length > 0) {
|
|
2804
|
-
return failure(criticalErrors);
|
|
2805
|
-
}
|
|
2806
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
2807
|
-
}
|
|
2808
|
-
function copyNotes(score, options) {
|
|
2809
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2810
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2811
|
-
}
|
|
2812
|
-
const part = score.parts[options.partIndex];
|
|
2813
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2814
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2815
|
-
}
|
|
2816
|
-
if (options.startPosition >= options.endPosition) {
|
|
2817
|
-
return failure([operationError("INVALID_POSITION", "Start position must be less than end position", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2818
|
-
}
|
|
2819
|
-
const measure = part.measures[options.measureIndex];
|
|
2820
|
-
const copiedNotes = [];
|
|
2821
|
-
let position = 0;
|
|
2822
|
-
for (const entry of measure.entries) {
|
|
2823
|
-
if (entry.type === "note") {
|
|
2824
|
-
if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
|
|
2825
|
-
if (!entry.chord) {
|
|
2826
|
-
const noteEnd = position + entry.duration;
|
|
2827
|
-
if (position < options.endPosition && noteEnd > options.startPosition) {
|
|
2828
|
-
const clonedNote = cloneNoteWithNewId(entry);
|
|
2829
|
-
if (clonedNote.tie) {
|
|
2830
|
-
}
|
|
2831
|
-
copiedNotes.push({
|
|
2832
|
-
relativePosition: position - options.startPosition,
|
|
2833
|
-
note: clonedNote
|
|
2834
|
-
});
|
|
2835
|
-
}
|
|
2836
|
-
position += entry.duration;
|
|
2837
|
-
} else {
|
|
2838
|
-
if (copiedNotes.length > 0) {
|
|
2839
|
-
const lastCopied = copiedNotes[copiedNotes.length - 1];
|
|
2840
|
-
if (lastCopied.note.voice === entry.voice && (options.staff === void 0 || (lastCopied.note.staff ?? 1) === (entry.staff ?? 1))) {
|
|
2841
|
-
const clonedNote = cloneNoteWithNewId(entry);
|
|
2842
|
-
copiedNotes.push({
|
|
2843
|
-
relativePosition: lastCopied.relativePosition,
|
|
2844
|
-
note: clonedNote
|
|
2845
|
-
});
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
} else if (!entry.chord) {
|
|
2850
|
-
position += entry.duration;
|
|
2851
|
-
}
|
|
2852
|
-
} else if (entry.type === "backup") {
|
|
2853
|
-
position -= entry.duration;
|
|
2854
|
-
} else if (entry.type === "forward") {
|
|
2855
|
-
position += entry.duration;
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
if (copiedNotes.length === 0) {
|
|
2859
|
-
return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, measureIndex: options.measureIndex, voice: options.voice })]);
|
|
2860
|
-
}
|
|
2861
|
-
const selection = {
|
|
2862
|
-
source: {
|
|
2863
|
-
partIndex: options.partIndex,
|
|
2864
|
-
measureIndex: options.measureIndex,
|
|
2865
|
-
startPosition: options.startPosition,
|
|
2866
|
-
endPosition: options.endPosition,
|
|
2867
|
-
voice: options.voice,
|
|
2868
|
-
staff: options.staff
|
|
2869
|
-
},
|
|
2870
|
-
notes: copiedNotes,
|
|
2871
|
-
duration: options.endPosition - options.startPosition
|
|
2872
|
-
};
|
|
2873
|
-
return success(selection);
|
|
2874
|
-
}
|
|
2875
|
-
function pasteNotes(score, options) {
|
|
2876
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
2877
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
2878
|
-
}
|
|
2879
|
-
const part = score.parts[options.partIndex];
|
|
2880
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
2881
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2882
|
-
}
|
|
2883
|
-
if (options.position < 0) {
|
|
2884
|
-
return failure([operationError("INVALID_POSITION", "Position cannot be negative", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
2885
|
-
}
|
|
2886
|
-
const result = cloneScore(score);
|
|
2887
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2888
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
2889
|
-
const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
|
|
2890
|
-
const targetVoice = options.voice ?? options.selection.source.voice;
|
|
2891
|
-
const targetStaff = options.staff ?? options.selection.source.staff;
|
|
2892
|
-
const pasteEnd = options.position + options.selection.duration;
|
|
2893
|
-
if (pasteEnd > measureDuration) {
|
|
2894
|
-
return failure([operationError(
|
|
2895
|
-
"EXCEEDS_MEASURE",
|
|
2896
|
-
`Paste would exceed measure duration (ends at ${pasteEnd}, measure is ${measureDuration})`,
|
|
2897
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex },
|
|
2898
|
-
{ pasteEnd, measureDuration }
|
|
2899
|
-
)]);
|
|
2900
|
-
}
|
|
2901
|
-
const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
|
|
2902
|
-
if (options.overwrite !== false) {
|
|
2903
|
-
const entriesToKeep = voiceEntries.filter((e) => {
|
|
2904
|
-
if (e.entry.type !== "note") return true;
|
|
2905
|
-
const note = e.entry;
|
|
2906
|
-
if (note.rest) return true;
|
|
2907
|
-
return e.endPosition <= options.position || e.position >= pasteEnd;
|
|
2908
|
-
});
|
|
2909
|
-
const newEntries = [];
|
|
2910
|
-
for (const { position, entry } of entriesToKeep) {
|
|
2911
|
-
if (entry.type === "note") {
|
|
2912
|
-
newEntries.push({ position, entry });
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
for (const { relativePosition, note } of options.selection.notes) {
|
|
2916
|
-
const pastePosition = options.position + Math.max(0, relativePosition);
|
|
2917
|
-
const newNote = cloneNoteWithNewId(note);
|
|
2918
|
-
newNote.voice = targetVoice;
|
|
2919
|
-
if (targetStaff !== void 0) {
|
|
2920
|
-
newNote.staff = targetStaff;
|
|
2921
|
-
}
|
|
2922
|
-
delete newNote.tie;
|
|
2923
|
-
delete newNote.ties;
|
|
2924
|
-
if (newNote.notations) {
|
|
2925
|
-
newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
|
|
2926
|
-
if (newNote.notations.length === 0) {
|
|
2927
|
-
delete newNote.notations;
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
newEntries.push({ position: pastePosition, entry: newNote });
|
|
2931
|
-
}
|
|
2932
|
-
measure.entries = rebuildMeasureWithVoice(
|
|
2933
|
-
measure,
|
|
2934
|
-
targetVoice,
|
|
2935
|
-
newEntries,
|
|
2936
|
-
measureDuration,
|
|
2937
|
-
targetStaff
|
|
2938
|
-
);
|
|
2939
|
-
} else {
|
|
2940
|
-
const { hasNotes, conflictingNotes } = hasNotesInRange(voiceEntries, options.position, pasteEnd);
|
|
2941
|
-
if (hasNotes) {
|
|
2942
|
-
return failure([operationError(
|
|
2943
|
-
"NOTE_CONFLICT",
|
|
2944
|
-
`Paste range ${options.position}-${pasteEnd} conflicts with existing notes`,
|
|
2945
|
-
{ partIndex: options.partIndex, measureIndex: options.measureIndex, voice: targetVoice },
|
|
2946
|
-
{ conflictingPositions: conflictingNotes.map((n) => ({ start: n.position, end: n.endPosition })) }
|
|
2947
|
-
)]);
|
|
2948
|
-
}
|
|
2949
|
-
const existingNotes = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
|
|
2950
|
-
for (const { relativePosition, note } of options.selection.notes) {
|
|
2951
|
-
const pastePosition = options.position + Math.max(0, relativePosition);
|
|
2952
|
-
const newNote = cloneNoteWithNewId(note);
|
|
2953
|
-
newNote.voice = targetVoice;
|
|
2954
|
-
if (targetStaff !== void 0) {
|
|
2955
|
-
newNote.staff = targetStaff;
|
|
2956
|
-
}
|
|
2957
|
-
delete newNote.tie;
|
|
2958
|
-
delete newNote.ties;
|
|
2959
|
-
if (newNote.notations) {
|
|
2960
|
-
newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
|
|
2961
|
-
if (newNote.notations.length === 0) {
|
|
2962
|
-
delete newNote.notations;
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
existingNotes.push({ position: pastePosition, entry: newNote });
|
|
2966
|
-
}
|
|
2967
|
-
measure.entries = rebuildMeasureWithVoice(
|
|
2968
|
-
measure,
|
|
2969
|
-
targetVoice,
|
|
2970
|
-
existingNotes,
|
|
2971
|
-
measureDuration,
|
|
2972
|
-
targetStaff
|
|
2973
|
-
);
|
|
2974
|
-
}
|
|
2975
|
-
const errors = validateMeasureLocal(measure, context, {
|
|
2976
|
-
checkMeasureDuration: true,
|
|
2977
|
-
checkPosition: true,
|
|
2978
|
-
checkVoiceStaff: true
|
|
2979
|
-
});
|
|
2980
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
2981
|
-
if (criticalErrors.length > 0) {
|
|
2982
|
-
return failure(criticalErrors);
|
|
2983
|
-
}
|
|
2984
|
-
return success(result, errors.filter((e) => e.level !== "error"));
|
|
2985
|
-
}
|
|
2986
|
-
function cutNotes(score, options) {
|
|
2987
|
-
const copyResult = copyNotes(score, options);
|
|
2988
|
-
if (!copyResult.success) {
|
|
2989
|
-
return failure(copyResult.errors);
|
|
2990
|
-
}
|
|
2991
|
-
const selection = copyResult.data;
|
|
2992
|
-
const result = cloneScore(score);
|
|
2993
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
2994
|
-
const context = getMeasureContext(result, options.partIndex, options.measureIndex);
|
|
2995
|
-
const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
|
|
2996
|
-
const voiceEntries = getVoiceEntries(measure, options.voice, options.staff);
|
|
2997
|
-
const entriesToKeep = voiceEntries.filter((e) => {
|
|
2998
|
-
if (e.entry.type !== "note") return true;
|
|
2999
|
-
const note = e.entry;
|
|
3000
|
-
if (note.rest) return true;
|
|
3001
|
-
return e.endPosition <= options.startPosition || e.position >= options.endPosition;
|
|
3002
|
-
});
|
|
3003
|
-
const newEntries = [];
|
|
3004
|
-
for (const { position, entry } of entriesToKeep) {
|
|
3005
|
-
if (entry.type === "note") {
|
|
3006
|
-
newEntries.push({ position, entry });
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
measure.entries = rebuildMeasureWithVoice(
|
|
3010
|
-
measure,
|
|
3011
|
-
options.voice,
|
|
3012
|
-
newEntries,
|
|
3013
|
-
measureDuration,
|
|
3014
|
-
options.staff
|
|
3015
|
-
);
|
|
3016
|
-
const errors = validateMeasureLocal(measure, context, {
|
|
3017
|
-
checkMeasureDuration: true,
|
|
3018
|
-
checkPosition: true,
|
|
3019
|
-
checkVoiceStaff: true
|
|
3020
|
-
});
|
|
3021
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
3022
|
-
if (criticalErrors.length > 0) {
|
|
3023
|
-
return failure(criticalErrors);
|
|
3024
|
-
}
|
|
3025
|
-
return success(
|
|
3026
|
-
{ score: result, selection },
|
|
3027
|
-
errors.filter((e) => e.level !== "error")
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
function copyNotesMultiMeasure(score, options) {
|
|
3031
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3032
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3033
|
-
}
|
|
3034
|
-
const part = score.parts[options.partIndex];
|
|
3035
|
-
if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
|
|
3036
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
3037
|
-
}
|
|
3038
|
-
if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex >= part.measures.length) {
|
|
3039
|
-
return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
|
|
3040
|
-
}
|
|
3041
|
-
const selection = {
|
|
3042
|
-
source: {
|
|
3043
|
-
partIndex: options.partIndex,
|
|
3044
|
-
startMeasureIndex: options.startMeasureIndex,
|
|
3045
|
-
endMeasureIndex: options.endMeasureIndex,
|
|
3046
|
-
voice: options.voice,
|
|
3047
|
-
staff: options.staff
|
|
3048
|
-
},
|
|
3049
|
-
measures: []
|
|
3050
|
-
};
|
|
3051
|
-
for (let measureIndex = options.startMeasureIndex; measureIndex <= options.endMeasureIndex; measureIndex++) {
|
|
3052
|
-
const measure = part.measures[measureIndex];
|
|
3053
|
-
const measureOffset = measureIndex - options.startMeasureIndex;
|
|
3054
|
-
const copiedNotes = [];
|
|
3055
|
-
let position = 0;
|
|
3056
|
-
for (const entry of measure.entries) {
|
|
3057
|
-
if (entry.type === "note") {
|
|
3058
|
-
if (entry.voice === options.voice && (options.staff === void 0 || (entry.staff ?? 1) === options.staff)) {
|
|
3059
|
-
if (!entry.chord && !entry.rest) {
|
|
3060
|
-
const clonedNote = cloneNoteWithNewId(entry);
|
|
3061
|
-
copiedNotes.push({
|
|
3062
|
-
relativePosition: position,
|
|
3063
|
-
note: clonedNote
|
|
3064
|
-
});
|
|
3065
|
-
position += entry.duration;
|
|
3066
|
-
} else if (entry.chord && copiedNotes.length > 0) {
|
|
3067
|
-
const clonedNote = cloneNoteWithNewId(entry);
|
|
3068
|
-
copiedNotes.push({
|
|
3069
|
-
relativePosition: copiedNotes[copiedNotes.length - 1].relativePosition,
|
|
3070
|
-
note: clonedNote
|
|
3071
|
-
});
|
|
3072
|
-
} else if (!entry.chord) {
|
|
3073
|
-
position += entry.duration;
|
|
3074
|
-
}
|
|
3075
|
-
} else if (!entry.chord) {
|
|
3076
|
-
position += entry.duration;
|
|
3077
|
-
}
|
|
3078
|
-
} else if (entry.type === "backup") {
|
|
3079
|
-
position -= entry.duration;
|
|
3080
|
-
} else if (entry.type === "forward") {
|
|
3081
|
-
position += entry.duration;
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
if (copiedNotes.length > 0) {
|
|
3085
|
-
selection.measures.push({
|
|
3086
|
-
measureOffset,
|
|
3087
|
-
notes: copiedNotes
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
if (selection.measures.length === 0) {
|
|
3092
|
-
return failure([operationError("NOTE_NOT_FOUND", "No notes found in the specified range", { partIndex: options.partIndex, voice: options.voice })]);
|
|
3093
|
-
}
|
|
3094
|
-
return success(selection);
|
|
3095
|
-
}
|
|
3096
|
-
function pasteNotesMultiMeasure(score, options) {
|
|
3097
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3098
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3099
|
-
}
|
|
3100
|
-
const part = score.parts[options.partIndex];
|
|
3101
|
-
const measureCount = options.selection.measures.length > 0 ? options.selection.measures[options.selection.measures.length - 1].measureOffset + 1 : 0;
|
|
3102
|
-
if (options.startMeasureIndex + measureCount > part.measures.length) {
|
|
3103
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Not enough measures to paste (need ${measureCount})`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
3104
|
-
}
|
|
3105
|
-
let result = cloneScore(score);
|
|
3106
|
-
const targetVoice = options.voice ?? options.selection.source.voice;
|
|
3107
|
-
const targetStaff = options.staff ?? options.selection.source.staff;
|
|
3108
|
-
for (const measureData of options.selection.measures) {
|
|
3109
|
-
const measureIndex = options.startMeasureIndex + measureData.measureOffset;
|
|
3110
|
-
const measure = result.parts[options.partIndex].measures[measureIndex];
|
|
3111
|
-
const context = getMeasureContext(result, options.partIndex, measureIndex);
|
|
3112
|
-
const measureDuration = context.time ? getMeasureDuration(context.divisions, context.time) : context.divisions * 4;
|
|
3113
|
-
const voiceEntries = getVoiceEntries(measure, targetVoice, targetStaff);
|
|
3114
|
-
let entriesToKeep;
|
|
3115
|
-
if (options.overwrite !== false) {
|
|
3116
|
-
entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note" && e.entry.rest).map((e) => ({ position: e.position, entry: e.entry }));
|
|
3117
|
-
} else {
|
|
3118
|
-
entriesToKeep = voiceEntries.filter((e) => e.entry.type === "note").map((e) => ({ position: e.position, entry: e.entry }));
|
|
3119
|
-
}
|
|
3120
|
-
for (const { relativePosition, note } of measureData.notes) {
|
|
3121
|
-
const newNote = cloneNoteWithNewId(note);
|
|
3122
|
-
newNote.voice = targetVoice;
|
|
3123
|
-
if (targetStaff !== void 0) {
|
|
3124
|
-
newNote.staff = targetStaff;
|
|
3125
|
-
}
|
|
3126
|
-
delete newNote.tie;
|
|
3127
|
-
delete newNote.ties;
|
|
3128
|
-
if (newNote.notations) {
|
|
3129
|
-
newNote.notations = newNote.notations.filter((n) => n.type !== "tied");
|
|
3130
|
-
if (newNote.notations.length === 0) {
|
|
3131
|
-
delete newNote.notations;
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
entriesToKeep.push({ position: relativePosition, entry: newNote });
|
|
3135
|
-
}
|
|
3136
|
-
measure.entries = rebuildMeasureWithVoice(
|
|
3137
|
-
measure,
|
|
3138
|
-
targetVoice,
|
|
3139
|
-
entriesToKeep,
|
|
3140
|
-
measureDuration,
|
|
3141
|
-
targetStaff
|
|
3142
|
-
);
|
|
3143
|
-
const errors = validateMeasureLocal(measure, context, {
|
|
3144
|
-
checkMeasureDuration: true,
|
|
3145
|
-
checkPosition: true,
|
|
3146
|
-
checkVoiceStaff: true
|
|
3147
|
-
});
|
|
3148
|
-
const criticalErrors = errors.filter((e) => e.level === "error");
|
|
3149
|
-
if (criticalErrors.length > 0) {
|
|
3150
|
-
return failure(criticalErrors);
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
return success(result);
|
|
3154
|
-
}
|
|
3155
|
-
function addTempo(score, options) {
|
|
3156
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3157
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3158
|
-
}
|
|
3159
|
-
const part = score.parts[options.partIndex];
|
|
3160
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3161
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3162
|
-
}
|
|
3163
|
-
if (options.bpm <= 0) {
|
|
3164
|
-
return failure([operationError("INVALID_DURATION", "BPM must be positive", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3165
|
-
}
|
|
3166
|
-
const result = cloneScore(score);
|
|
3167
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3168
|
-
const directionTypes = [];
|
|
3169
|
-
directionTypes.push({
|
|
3170
|
-
kind: "metronome",
|
|
3171
|
-
beatUnit: options.beatUnit ?? "quarter",
|
|
3172
|
-
beatUnitDot: options.beatUnitDot,
|
|
3173
|
-
perMinute: options.bpm
|
|
3174
|
-
});
|
|
3175
|
-
if (options.text) {
|
|
3176
|
-
directionTypes.push({
|
|
3177
|
-
kind: "words",
|
|
3178
|
-
text: options.text,
|
|
3179
|
-
fontWeight: "bold"
|
|
3180
|
-
});
|
|
3181
|
-
}
|
|
3182
|
-
const direction = {
|
|
3183
|
-
_id: generateId(),
|
|
3184
|
-
type: "direction",
|
|
3185
|
-
directionTypes,
|
|
3186
|
-
placement: options.placement ?? "above",
|
|
3187
|
-
sound: { tempo: options.bpm }
|
|
3188
|
-
};
|
|
3189
|
-
insertDirectionAtPosition(measure, direction, options.position);
|
|
3190
|
-
return success(result);
|
|
3191
|
-
}
|
|
3192
|
-
function removeTempo(score, options) {
|
|
3193
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3194
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3195
|
-
}
|
|
3196
|
-
const part = score.parts[options.partIndex];
|
|
3197
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3198
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3199
|
-
}
|
|
3200
|
-
const result = cloneScore(score);
|
|
3201
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3202
|
-
const tempoDirectionIndices = [];
|
|
3203
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
3204
|
-
const entry = measure.entries[i];
|
|
3205
|
-
if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
|
|
3206
|
-
tempoDirectionIndices.push(i);
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
if (tempoDirectionIndices.length === 0) {
|
|
3210
|
-
return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3211
|
-
}
|
|
3212
|
-
const targetIndex = options.directionIndex ?? 0;
|
|
3213
|
-
if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
|
|
3214
|
-
return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3215
|
-
}
|
|
3216
|
-
measure.entries.splice(tempoDirectionIndices[targetIndex], 1);
|
|
3217
|
-
return success(result);
|
|
3218
|
-
}
|
|
3219
|
-
function modifyTempo(score, options) {
|
|
3220
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3221
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3222
|
-
}
|
|
3223
|
-
const part = score.parts[options.partIndex];
|
|
3224
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3225
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3226
|
-
}
|
|
3227
|
-
const result = cloneScore(score);
|
|
3228
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3229
|
-
const tempoDirectionIndices = [];
|
|
3230
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
3231
|
-
const entry = measure.entries[i];
|
|
3232
|
-
if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "metronome")) {
|
|
3233
|
-
tempoDirectionIndices.push(i);
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
if (tempoDirectionIndices.length === 0) {
|
|
3237
|
-
return failure([operationError("TEMPO_NOT_FOUND", "No tempo marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3238
|
-
}
|
|
3239
|
-
const targetIndex = options.directionIndex ?? 0;
|
|
3240
|
-
if (targetIndex < 0 || targetIndex >= tempoDirectionIndices.length) {
|
|
3241
|
-
return failure([operationError("TEMPO_NOT_FOUND", `Tempo direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3242
|
-
}
|
|
3243
|
-
const direction = measure.entries[tempoDirectionIndices[targetIndex]];
|
|
3244
|
-
const metronome = direction.directionTypes.find((dt) => dt.kind === "metronome");
|
|
3245
|
-
if (metronome && metronome.kind === "metronome") {
|
|
3246
|
-
if (options.bpm !== void 0) {
|
|
3247
|
-
metronome.perMinute = options.bpm;
|
|
3248
|
-
}
|
|
3249
|
-
if (options.beatUnit !== void 0) {
|
|
3250
|
-
metronome.beatUnit = options.beatUnit;
|
|
3251
|
-
}
|
|
3252
|
-
if (options.beatUnitDot !== void 0) {
|
|
3253
|
-
metronome.beatUnitDot = options.beatUnitDot;
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
if (options.text !== void 0) {
|
|
3257
|
-
const wordsIndex = direction.directionTypes.findIndex((dt) => dt.kind === "words");
|
|
3258
|
-
if (wordsIndex >= 0) {
|
|
3259
|
-
const words = direction.directionTypes[wordsIndex];
|
|
3260
|
-
if (words.kind === "words") {
|
|
3261
|
-
words.text = options.text;
|
|
3262
|
-
}
|
|
3263
|
-
} else if (options.text) {
|
|
3264
|
-
direction.directionTypes.push({
|
|
3265
|
-
kind: "words",
|
|
3266
|
-
text: options.text,
|
|
3267
|
-
fontWeight: "bold"
|
|
3268
|
-
});
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
|
-
if (options.bpm !== void 0 && direction.sound) {
|
|
3272
|
-
direction.sound.tempo = options.bpm;
|
|
3273
|
-
}
|
|
3274
|
-
if (options.placement !== void 0) {
|
|
3275
|
-
direction.placement = options.placement;
|
|
3276
|
-
}
|
|
3277
|
-
return success(result);
|
|
3278
|
-
}
|
|
3279
|
-
function addWedge(score, options) {
|
|
3280
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3281
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3282
|
-
}
|
|
3283
|
-
const part = score.parts[options.partIndex];
|
|
3284
|
-
if (options.startMeasureIndex < 0 || options.startMeasureIndex >= part.measures.length) {
|
|
3285
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Start measure index ${options.startMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.startMeasureIndex })]);
|
|
3286
|
-
}
|
|
3287
|
-
if (options.endMeasureIndex < 0 || options.endMeasureIndex >= part.measures.length) {
|
|
3288
|
-
return failure([operationError("MEASURE_NOT_FOUND", `End measure index ${options.endMeasureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.endMeasureIndex })]);
|
|
3289
|
-
}
|
|
3290
|
-
if (options.endMeasureIndex < options.startMeasureIndex || options.endMeasureIndex === options.startMeasureIndex && options.endPosition <= options.startPosition) {
|
|
3291
|
-
return failure([operationError("INVALID_RANGE", "End position must be after start position", { partIndex: options.partIndex })]);
|
|
3292
|
-
}
|
|
3293
|
-
const result = cloneScore(score);
|
|
3294
|
-
const startMeasure = result.parts[options.partIndex].measures[options.startMeasureIndex];
|
|
3295
|
-
const startDirection = {
|
|
3296
|
-
_id: generateId(),
|
|
3297
|
-
type: "direction",
|
|
3298
|
-
directionTypes: [{
|
|
3299
|
-
kind: "wedge",
|
|
3300
|
-
type: options.type
|
|
3301
|
-
}],
|
|
3302
|
-
placement: options.placement ?? "below",
|
|
3303
|
-
staff: options.staff
|
|
3304
|
-
};
|
|
3305
|
-
insertDirectionAtPosition(startMeasure, startDirection, options.startPosition);
|
|
3306
|
-
const endMeasure = result.parts[options.partIndex].measures[options.endMeasureIndex];
|
|
3307
|
-
const endDirection = {
|
|
3308
|
-
_id: generateId(),
|
|
3309
|
-
type: "direction",
|
|
3310
|
-
directionTypes: [{
|
|
3311
|
-
kind: "wedge",
|
|
3312
|
-
type: "stop"
|
|
3313
|
-
}],
|
|
3314
|
-
placement: options.placement ?? "below",
|
|
3315
|
-
staff: options.staff
|
|
3316
|
-
};
|
|
3317
|
-
insertDirectionAtPosition(endMeasure, endDirection, options.endPosition);
|
|
3318
|
-
return success(result);
|
|
3319
|
-
}
|
|
3320
|
-
function removeWedge(score, options) {
|
|
3321
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3322
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3323
|
-
}
|
|
3324
|
-
const part = score.parts[options.partIndex];
|
|
3325
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3326
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3327
|
-
}
|
|
3328
|
-
const result = cloneScore(score);
|
|
3329
|
-
const wedgeStarts = [];
|
|
3330
|
-
for (let mi = options.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
|
|
3331
|
-
const measure = result.parts[options.partIndex].measures[mi];
|
|
3332
|
-
for (let ei = 0; ei < measure.entries.length; ei++) {
|
|
3333
|
-
const entry = measure.entries[ei];
|
|
3334
|
-
if (entry.type === "direction") {
|
|
3335
|
-
const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge");
|
|
3336
|
-
if (wedgeType && wedgeType.kind === "wedge" && (wedgeType.type === "crescendo" || wedgeType.type === "diminuendo")) {
|
|
3337
|
-
wedgeStarts.push({ measureIndex: mi, entryIndex: ei });
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
if (mi === options.measureIndex && wedgeStarts.length > 0) break;
|
|
3342
|
-
}
|
|
3343
|
-
if (wedgeStarts.length === 0) {
|
|
3344
|
-
return failure([operationError("WEDGE_NOT_FOUND", "No wedge found starting in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3345
|
-
}
|
|
3346
|
-
const targetIndex = options.directionIndex ?? 0;
|
|
3347
|
-
if (targetIndex >= wedgeStarts.length) {
|
|
3348
|
-
return failure([operationError("WEDGE_NOT_FOUND", `Wedge direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3349
|
-
}
|
|
3350
|
-
const startInfo = wedgeStarts[targetIndex];
|
|
3351
|
-
const startMeasure = result.parts[options.partIndex].measures[startInfo.measureIndex];
|
|
3352
|
-
startMeasure.entries.splice(startInfo.entryIndex, 1);
|
|
3353
|
-
for (let mi = startInfo.measureIndex; mi < result.parts[options.partIndex].measures.length; mi++) {
|
|
3354
|
-
const measure = result.parts[options.partIndex].measures[mi];
|
|
3355
|
-
for (let ei = 0; ei < measure.entries.length; ei++) {
|
|
3356
|
-
const entry = measure.entries[ei];
|
|
3357
|
-
if (entry.type === "direction") {
|
|
3358
|
-
const wedgeType = entry.directionTypes.find((dt) => dt.kind === "wedge" && dt.type === "stop");
|
|
3359
|
-
if (wedgeType) {
|
|
3360
|
-
measure.entries.splice(ei, 1);
|
|
3361
|
-
return success(result);
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
|
-
return success(result);
|
|
3367
|
-
}
|
|
3368
|
-
function addFermata(score, options) {
|
|
3369
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3370
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3371
|
-
}
|
|
3372
|
-
const part = score.parts[options.partIndex];
|
|
3373
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3374
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3375
|
-
}
|
|
3376
|
-
const result = cloneScore(score);
|
|
3377
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3378
|
-
const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
|
|
3379
|
-
if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
|
|
3380
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3381
|
-
}
|
|
3382
|
-
const note = notes[options.noteIndex];
|
|
3383
|
-
if (note.notations?.some((n) => n.type === "fermata")) {
|
|
3384
|
-
return failure([operationError("FERMATA_ALREADY_EXISTS", "Note already has a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3385
|
-
}
|
|
3386
|
-
if (!note.notations) {
|
|
3387
|
-
note.notations = [];
|
|
3388
|
-
}
|
|
3389
|
-
const fermataNotation = {
|
|
3390
|
-
type: "fermata",
|
|
3391
|
-
shape: options.shape ?? "normal",
|
|
3392
|
-
fermataType: options.fermataType ?? "upright",
|
|
3393
|
-
placement: options.placement ?? "above"
|
|
3394
|
-
};
|
|
3395
|
-
note.notations.push(fermataNotation);
|
|
3396
|
-
return success(result);
|
|
3397
|
-
}
|
|
3398
|
-
function removeFermata(score, options) {
|
|
3399
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3400
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3401
|
-
}
|
|
3402
|
-
const part = score.parts[options.partIndex];
|
|
3403
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3404
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3405
|
-
}
|
|
3406
|
-
const result = cloneScore(score);
|
|
3407
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3408
|
-
const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
|
|
3409
|
-
if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
|
|
3410
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3411
|
-
}
|
|
3412
|
-
const note = notes[options.noteIndex];
|
|
3413
|
-
const fermataIndex = note.notations?.findIndex((n) => n.type === "fermata");
|
|
3414
|
-
if (fermataIndex === void 0 || fermataIndex === -1) {
|
|
3415
|
-
return failure([operationError("FERMATA_NOT_FOUND", "Note does not have a fermata", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3416
|
-
}
|
|
3417
|
-
note.notations.splice(fermataIndex, 1);
|
|
3418
|
-
if (note.notations.length === 0) {
|
|
3419
|
-
delete note.notations;
|
|
3420
|
-
}
|
|
3421
|
-
return success(result);
|
|
3422
|
-
}
|
|
3423
|
-
function addOrnament(score, options) {
|
|
3424
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3425
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3426
|
-
}
|
|
3427
|
-
const part = score.parts[options.partIndex];
|
|
3428
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3429
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3430
|
-
}
|
|
3431
|
-
const result = cloneScore(score);
|
|
3432
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3433
|
-
const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
|
|
3434
|
-
if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
|
|
3435
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3436
|
-
}
|
|
3437
|
-
const note = notes[options.noteIndex];
|
|
3438
|
-
if (note.notations?.some((n) => n.type === "ornament" && n.ornament === options.ornament)) {
|
|
3439
|
-
return failure([operationError("ORNAMENT_ALREADY_EXISTS", `Note already has ornament: ${options.ornament}`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3440
|
-
}
|
|
3441
|
-
if (!note.notations) {
|
|
3442
|
-
note.notations = [];
|
|
3443
|
-
}
|
|
3444
|
-
const ornamentNotation = {
|
|
3445
|
-
type: "ornament",
|
|
3446
|
-
ornament: options.ornament,
|
|
3447
|
-
placement: options.placement,
|
|
3448
|
-
accidentalMark: options.accidentalMark
|
|
3449
|
-
};
|
|
3450
|
-
note.notations.push(ornamentNotation);
|
|
3451
|
-
return success(result);
|
|
3452
|
-
}
|
|
3453
|
-
function removeOrnament(score, options) {
|
|
3454
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3455
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3456
|
-
}
|
|
3457
|
-
const part = score.parts[options.partIndex];
|
|
3458
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3459
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3460
|
-
}
|
|
3461
|
-
const result = cloneScore(score);
|
|
3462
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3463
|
-
const notes = measure.entries.filter((e) => e.type === "note" && !e.rest);
|
|
3464
|
-
if (options.noteIndex < 0 || options.noteIndex >= notes.length) {
|
|
3465
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note index ${options.noteIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3466
|
-
}
|
|
3467
|
-
const note = notes[options.noteIndex];
|
|
3468
|
-
const ornamentIndex = options.ornament ? note.notations?.findIndex((n) => n.type === "ornament" && n.ornament === options.ornament) : note.notations?.findIndex((n) => n.type === "ornament");
|
|
3469
|
-
if (ornamentIndex === void 0 || ornamentIndex === -1) {
|
|
3470
|
-
return failure([operationError("ORNAMENT_NOT_FOUND", "Note does not have the specified ornament", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3471
|
-
}
|
|
3472
|
-
note.notations.splice(ornamentIndex, 1);
|
|
3473
|
-
if (note.notations.length === 0) {
|
|
3474
|
-
delete note.notations;
|
|
3475
|
-
}
|
|
3476
|
-
return success(result);
|
|
3477
|
-
}
|
|
3478
|
-
function addPedal(score, options) {
|
|
3479
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3480
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3481
|
-
}
|
|
3482
|
-
const part = score.parts[options.partIndex];
|
|
3483
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3484
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3485
|
-
}
|
|
3486
|
-
const result = cloneScore(score);
|
|
3487
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3488
|
-
const direction = {
|
|
3489
|
-
_id: generateId(),
|
|
3490
|
-
type: "direction",
|
|
3491
|
-
directionTypes: [{
|
|
3492
|
-
kind: "pedal",
|
|
3493
|
-
type: options.pedalType,
|
|
3494
|
-
line: options.line
|
|
3495
|
-
}],
|
|
3496
|
-
placement: options.placement ?? "below"
|
|
3497
|
-
};
|
|
3498
|
-
insertDirectionAtPosition(measure, direction, options.position);
|
|
3499
|
-
return success(result);
|
|
3500
|
-
}
|
|
3501
|
-
function removePedal(score, options) {
|
|
3502
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3503
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3504
|
-
}
|
|
3505
|
-
const part = score.parts[options.partIndex];
|
|
3506
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3507
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3508
|
-
}
|
|
3509
|
-
const result = cloneScore(score);
|
|
3510
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3511
|
-
const pedalIndices = [];
|
|
3512
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
3513
|
-
const entry = measure.entries[i];
|
|
3514
|
-
if (entry.type === "direction" && entry.directionTypes.some((dt) => dt.kind === "pedal")) {
|
|
3515
|
-
pedalIndices.push(i);
|
|
3516
|
-
}
|
|
3517
|
-
}
|
|
3518
|
-
if (pedalIndices.length === 0) {
|
|
3519
|
-
return failure([operationError("PEDAL_NOT_FOUND", "No pedal marking found in measure", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3520
|
-
}
|
|
3521
|
-
const targetIndex = options.directionIndex ?? 0;
|
|
3522
|
-
if (targetIndex < 0 || targetIndex >= pedalIndices.length) {
|
|
3523
|
-
return failure([operationError("PEDAL_NOT_FOUND", `Pedal direction index ${targetIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3524
|
-
}
|
|
3525
|
-
measure.entries.splice(pedalIndices[targetIndex], 1);
|
|
3526
|
-
return success(result);
|
|
3527
|
-
}
|
|
3528
|
-
function addTextDirection(score, options) {
|
|
3529
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3530
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3531
|
-
}
|
|
3532
|
-
const part = score.parts[options.partIndex];
|
|
3533
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3534
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3535
|
-
}
|
|
3536
|
-
if (!options.text.trim()) {
|
|
3537
|
-
return failure([operationError("INVALID_TEXT", "Text cannot be empty", { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3538
|
-
}
|
|
3539
|
-
const result = cloneScore(score);
|
|
3540
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3541
|
-
const direction = {
|
|
3542
|
-
_id: generateId(),
|
|
3543
|
-
type: "direction",
|
|
3544
|
-
directionTypes: [{
|
|
3545
|
-
kind: "words",
|
|
3546
|
-
text: options.text,
|
|
3547
|
-
fontStyle: options.fontStyle,
|
|
3548
|
-
fontWeight: options.fontWeight
|
|
3549
|
-
}],
|
|
3550
|
-
placement: options.placement ?? "above"
|
|
3551
|
-
};
|
|
3552
|
-
insertDirectionAtPosition(measure, direction, options.position);
|
|
3553
|
-
return success(result);
|
|
3554
|
-
}
|
|
3555
|
-
function addRehearsalMark(score, options) {
|
|
3556
|
-
if (options.partIndex < 0 || options.partIndex >= score.parts.length) {
|
|
3557
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${options.partIndex} out of bounds`, { partIndex: options.partIndex })]);
|
|
3558
|
-
}
|
|
3559
|
-
const part = score.parts[options.partIndex];
|
|
3560
|
-
if (options.measureIndex < 0 || options.measureIndex >= part.measures.length) {
|
|
3561
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${options.measureIndex} out of bounds`, { partIndex: options.partIndex, measureIndex: options.measureIndex })]);
|
|
3562
|
-
}
|
|
3563
|
-
const result = cloneScore(score);
|
|
3564
|
-
const measure = result.parts[options.partIndex].measures[options.measureIndex];
|
|
3565
|
-
const direction = {
|
|
3566
|
-
_id: generateId(),
|
|
3567
|
-
type: "direction",
|
|
3568
|
-
directionTypes: [{
|
|
3569
|
-
kind: "rehearsal",
|
|
3570
|
-
text: options.text,
|
|
3571
|
-
enclosure: options.enclosure ?? "square"
|
|
3572
|
-
}],
|
|
3573
|
-
placement: "above"
|
|
3574
|
-
};
|
|
3575
|
-
insertDirectionAtPosition(measure, direction, 0);
|
|
3576
|
-
return success(result);
|
|
3577
|
-
}
|
|
3578
|
-
function insertDirectionAtPosition(measure, direction, position) {
|
|
3579
|
-
let currentPosition = 0;
|
|
3580
|
-
let insertIndex = 0;
|
|
3581
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
3582
|
-
const entry = measure.entries[i];
|
|
3583
|
-
if (currentPosition >= position) {
|
|
3584
|
-
insertIndex = i;
|
|
3585
|
-
break;
|
|
3586
|
-
}
|
|
3587
|
-
if (entry.type === "note" && !entry.chord) {
|
|
3588
|
-
currentPosition += entry.duration;
|
|
3589
|
-
} else if (entry.type === "forward") {
|
|
3590
|
-
currentPosition += entry.duration;
|
|
3591
|
-
} else if (entry.type === "backup") {
|
|
3592
|
-
currentPosition -= entry.duration;
|
|
3593
|
-
}
|
|
3594
|
-
insertIndex = i + 1;
|
|
3595
|
-
}
|
|
3596
|
-
measure.entries.splice(insertIndex, 0, direction);
|
|
3597
|
-
}
|
|
3598
|
-
function addRepeatBarline(score, options) {
|
|
3599
|
-
const { partIndex, measureIndex, direction, times } = options;
|
|
3600
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3601
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3602
|
-
}
|
|
3603
|
-
const part = score.parts[partIndex];
|
|
3604
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3605
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3606
|
-
}
|
|
3607
|
-
const result = cloneScore(score);
|
|
3608
|
-
const location = direction === "forward" ? "left" : "right";
|
|
3609
|
-
const barStyle = direction === "forward" ? "heavy-light" : "light-heavy";
|
|
3610
|
-
for (const p of result.parts) {
|
|
3611
|
-
if (measureIndex >= p.measures.length) continue;
|
|
3612
|
-
const measure = p.measures[measureIndex];
|
|
3613
|
-
if (!measure.barlines) {
|
|
3614
|
-
measure.barlines = [];
|
|
3615
|
-
}
|
|
3616
|
-
const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
|
|
3617
|
-
if (existingIndex >= 0) {
|
|
3618
|
-
return failure([operationError("REPEAT_ALREADY_EXISTS", `Repeat barline already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
|
|
3619
|
-
}
|
|
3620
|
-
const nonRepeatIndex = measure.barlines.findIndex((b) => b.location === location && !b.repeat);
|
|
3621
|
-
if (nonRepeatIndex >= 0) {
|
|
3622
|
-
measure.barlines.splice(nonRepeatIndex, 1);
|
|
3623
|
-
}
|
|
3624
|
-
measure.barlines.push({
|
|
3625
|
-
_id: generateId(),
|
|
3626
|
-
location,
|
|
3627
|
-
barStyle,
|
|
3628
|
-
repeat: {
|
|
3629
|
-
direction,
|
|
3630
|
-
times
|
|
3631
|
-
}
|
|
3632
|
-
});
|
|
3633
|
-
}
|
|
3634
|
-
return success(result);
|
|
3635
|
-
}
|
|
3636
|
-
function removeRepeatBarline(score, options) {
|
|
3637
|
-
const { partIndex, measureIndex, location } = options;
|
|
3638
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3639
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3640
|
-
}
|
|
3641
|
-
const part = score.parts[partIndex];
|
|
3642
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3643
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3644
|
-
}
|
|
3645
|
-
const measure = part.measures[measureIndex];
|
|
3646
|
-
if (!measure.barlines) {
|
|
3647
|
-
return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
|
|
3648
|
-
}
|
|
3649
|
-
const existingIndex = measure.barlines.findIndex((b) => b.location === location && b.repeat);
|
|
3650
|
-
if (existingIndex < 0) {
|
|
3651
|
-
return failure([operationError("REPEAT_NOT_FOUND", `No repeat barline found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
|
|
3652
|
-
}
|
|
3653
|
-
const result = cloneScore(score);
|
|
3654
|
-
for (const p of result.parts) {
|
|
3655
|
-
if (measureIndex >= p.measures.length) continue;
|
|
3656
|
-
const m = p.measures[measureIndex];
|
|
3657
|
-
if (m.barlines) {
|
|
3658
|
-
const idx = m.barlines.findIndex((b) => b.location === location && b.repeat);
|
|
3659
|
-
if (idx >= 0) {
|
|
3660
|
-
m.barlines.splice(idx, 1);
|
|
3661
|
-
}
|
|
3662
|
-
if (m.barlines.length === 0) {
|
|
3663
|
-
delete m.barlines;
|
|
3664
|
-
}
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
return success(result);
|
|
3668
|
-
}
|
|
3669
|
-
function addEnding(score, options) {
|
|
3670
|
-
const { partIndex, measureIndex, number, type } = options;
|
|
3671
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3672
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3673
|
-
}
|
|
3674
|
-
const part = score.parts[partIndex];
|
|
3675
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3676
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3677
|
-
}
|
|
3678
|
-
const result = cloneScore(score);
|
|
3679
|
-
const location = type === "start" ? "left" : "right";
|
|
3680
|
-
for (const p of result.parts) {
|
|
3681
|
-
if (measureIndex >= p.measures.length) continue;
|
|
3682
|
-
const measure = p.measures[measureIndex];
|
|
3683
|
-
if (!measure.barlines) {
|
|
3684
|
-
measure.barlines = [];
|
|
3685
|
-
}
|
|
3686
|
-
let barline = measure.barlines.find((b) => b.location === location);
|
|
3687
|
-
if (!barline) {
|
|
3688
|
-
barline = { _id: generateId(), location };
|
|
3689
|
-
measure.barlines.push(barline);
|
|
3690
|
-
}
|
|
3691
|
-
if (barline.ending) {
|
|
3692
|
-
return failure([operationError("ENDING_ALREADY_EXISTS", `Ending already exists at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
|
|
3693
|
-
}
|
|
3694
|
-
barline.ending = { number, type };
|
|
3695
|
-
}
|
|
3696
|
-
return success(result);
|
|
3697
|
-
}
|
|
3698
|
-
function removeEnding(score, options) {
|
|
3699
|
-
const { partIndex, measureIndex, location } = options;
|
|
3700
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3701
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3702
|
-
}
|
|
3703
|
-
const part = score.parts[partIndex];
|
|
3704
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3705
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3706
|
-
}
|
|
3707
|
-
const measure = part.measures[measureIndex];
|
|
3708
|
-
const barline = measure.barlines?.find((b) => b.location === location && b.ending);
|
|
3709
|
-
if (!barline) {
|
|
3710
|
-
return failure([operationError("ENDING_NOT_FOUND", `No ending found at ${location} of measure ${measureIndex}`, { partIndex, measureIndex })]);
|
|
3711
|
-
}
|
|
3712
|
-
const result = cloneScore(score);
|
|
3713
|
-
for (const p of result.parts) {
|
|
3714
|
-
if (measureIndex >= p.measures.length) continue;
|
|
3715
|
-
const m = p.measures[measureIndex];
|
|
3716
|
-
if (m.barlines) {
|
|
3717
|
-
const bl = m.barlines.find((b) => b.location === location);
|
|
3718
|
-
if (bl) {
|
|
3719
|
-
delete bl.ending;
|
|
3720
|
-
if (!bl.barStyle && !bl.repeat && !bl.ending) {
|
|
3721
|
-
const idx = m.barlines.indexOf(bl);
|
|
3722
|
-
m.barlines.splice(idx, 1);
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
if (m.barlines.length === 0) {
|
|
3726
|
-
delete m.barlines;
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
return success(result);
|
|
3731
|
-
}
|
|
3732
|
-
function changeBarline(score, options) {
|
|
3733
|
-
const { partIndex, measureIndex, location, barStyle } = options;
|
|
3734
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3735
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3736
|
-
}
|
|
3737
|
-
const part = score.parts[partIndex];
|
|
3738
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3739
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3740
|
-
}
|
|
3741
|
-
const result = cloneScore(score);
|
|
3742
|
-
for (const p of result.parts) {
|
|
3743
|
-
if (measureIndex >= p.measures.length) continue;
|
|
3744
|
-
const measure = p.measures[measureIndex];
|
|
3745
|
-
if (!measure.barlines) {
|
|
3746
|
-
measure.barlines = [];
|
|
3747
|
-
}
|
|
3748
|
-
let barline = measure.barlines.find((b) => b.location === location);
|
|
3749
|
-
if (!barline) {
|
|
3750
|
-
barline = { _id: generateId(), location };
|
|
3751
|
-
measure.barlines.push(barline);
|
|
3752
|
-
}
|
|
3753
|
-
barline.barStyle = barStyle;
|
|
3754
|
-
}
|
|
3755
|
-
return success(result);
|
|
3756
|
-
}
|
|
3757
|
-
function addSegno(score, options) {
|
|
3758
|
-
const { partIndex, measureIndex, position = 0 } = options;
|
|
3759
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3760
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3761
|
-
}
|
|
3762
|
-
const part = score.parts[partIndex];
|
|
3763
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3764
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3765
|
-
}
|
|
3766
|
-
const result = cloneScore(score);
|
|
3767
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
3768
|
-
const direction = {
|
|
3769
|
-
_id: generateId(),
|
|
3770
|
-
type: "direction",
|
|
3771
|
-
directionTypes: [{ kind: "segno" }],
|
|
3772
|
-
placement: "above"
|
|
3773
|
-
};
|
|
3774
|
-
insertDirectionAtPosition(measure, direction, position);
|
|
3775
|
-
return success(result);
|
|
3776
|
-
}
|
|
3777
|
-
function addCoda(score, options) {
|
|
3778
|
-
const { partIndex, measureIndex, position = 0 } = options;
|
|
3779
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3780
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3781
|
-
}
|
|
3782
|
-
const part = score.parts[partIndex];
|
|
3783
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3784
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3785
|
-
}
|
|
3786
|
-
const result = cloneScore(score);
|
|
3787
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
3788
|
-
const direction = {
|
|
3789
|
-
_id: generateId(),
|
|
3790
|
-
type: "direction",
|
|
3791
|
-
directionTypes: [{ kind: "coda" }],
|
|
3792
|
-
placement: "above"
|
|
3793
|
-
};
|
|
3794
|
-
insertDirectionAtPosition(measure, direction, position);
|
|
3795
|
-
return success(result);
|
|
3796
|
-
}
|
|
3797
|
-
function addDaCapo(score, options) {
|
|
3798
|
-
const { partIndex, measureIndex, position } = options;
|
|
3799
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3800
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3801
|
-
}
|
|
3802
|
-
const part = score.parts[partIndex];
|
|
3803
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3804
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3805
|
-
}
|
|
3806
|
-
const result = cloneScore(score);
|
|
3807
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
3808
|
-
const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
|
|
3809
|
-
const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
|
|
3810
|
-
const insertPos = position ?? measureDuration;
|
|
3811
|
-
const direction = {
|
|
3812
|
-
_id: generateId(),
|
|
3813
|
-
type: "direction",
|
|
3814
|
-
directionTypes: [{ kind: "words", text: "D.C." }],
|
|
3815
|
-
placement: "above"
|
|
3816
|
-
};
|
|
3817
|
-
insertDirectionAtPosition(measure, direction, insertPos);
|
|
3818
|
-
const sound = {
|
|
3819
|
-
_id: generateId(),
|
|
3820
|
-
type: "sound",
|
|
3821
|
-
dacapo: true
|
|
3822
|
-
};
|
|
3823
|
-
measure.entries.push(sound);
|
|
3824
|
-
return success(result);
|
|
3825
|
-
}
|
|
3826
|
-
function addDalSegno(score, options) {
|
|
3827
|
-
const { partIndex, measureIndex, position } = options;
|
|
3828
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3829
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3830
|
-
}
|
|
3831
|
-
const part = score.parts[partIndex];
|
|
3832
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3833
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3834
|
-
}
|
|
3835
|
-
const result = cloneScore(score);
|
|
3836
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
3837
|
-
const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
|
|
3838
|
-
const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
|
|
3839
|
-
const insertPos = position ?? measureDuration;
|
|
3840
|
-
const direction = {
|
|
3841
|
-
_id: generateId(),
|
|
3842
|
-
type: "direction",
|
|
3843
|
-
directionTypes: [{ kind: "words", text: "D.S." }],
|
|
3844
|
-
placement: "above"
|
|
3845
|
-
};
|
|
3846
|
-
insertDirectionAtPosition(measure, direction, insertPos);
|
|
3847
|
-
const sound = {
|
|
3848
|
-
_id: generateId(),
|
|
3849
|
-
type: "sound",
|
|
3850
|
-
dalsegno: "segno"
|
|
3851
|
-
};
|
|
3852
|
-
measure.entries.push(sound);
|
|
3853
|
-
return success(result);
|
|
3854
|
-
}
|
|
3855
|
-
function addFine(score, options) {
|
|
3856
|
-
const { partIndex, measureIndex, position } = options;
|
|
3857
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3858
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3859
|
-
}
|
|
3860
|
-
const part = score.parts[partIndex];
|
|
3861
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3862
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3863
|
-
}
|
|
3864
|
-
const result = cloneScore(score);
|
|
3865
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
3866
|
-
const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
|
|
3867
|
-
const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
|
|
3868
|
-
const insertPos = position ?? measureDuration;
|
|
3869
|
-
const direction = {
|
|
3870
|
-
_id: generateId(),
|
|
3871
|
-
type: "direction",
|
|
3872
|
-
directionTypes: [{ kind: "words", text: "Fine" }],
|
|
3873
|
-
placement: "above"
|
|
3874
|
-
};
|
|
3875
|
-
insertDirectionAtPosition(measure, direction, insertPos);
|
|
3876
|
-
const sound = {
|
|
3877
|
-
_id: generateId(),
|
|
3878
|
-
type: "sound",
|
|
3879
|
-
fine: true
|
|
3880
|
-
};
|
|
3881
|
-
measure.entries.push(sound);
|
|
3882
|
-
return success(result);
|
|
3883
|
-
}
|
|
3884
|
-
function addToCoda(score, options) {
|
|
3885
|
-
const { partIndex, measureIndex, position } = options;
|
|
3886
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3887
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3888
|
-
}
|
|
3889
|
-
const part = score.parts[partIndex];
|
|
3890
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3891
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3892
|
-
}
|
|
3893
|
-
const result = cloneScore(score);
|
|
3894
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
3895
|
-
const attrs = getAttributesAtMeasure(result, { part: partIndex, measure: measureIndex });
|
|
3896
|
-
const measureDuration = getMeasureDuration(attrs.divisions ?? 1, attrs.time ?? { beats: "4", beatType: 4 });
|
|
3897
|
-
const insertPos = position ?? measureDuration;
|
|
3898
|
-
const direction = {
|
|
3899
|
-
_id: generateId(),
|
|
3900
|
-
type: "direction",
|
|
3901
|
-
directionTypes: [{ kind: "words", text: "To Coda" }],
|
|
3902
|
-
placement: "above"
|
|
3903
|
-
};
|
|
3904
|
-
insertDirectionAtPosition(measure, direction, insertPos);
|
|
3905
|
-
const sound = {
|
|
3906
|
-
_id: generateId(),
|
|
3907
|
-
type: "sound",
|
|
3908
|
-
tocoda: "coda"
|
|
3909
|
-
};
|
|
3910
|
-
measure.entries.push(sound);
|
|
3911
|
-
return success(result);
|
|
3912
|
-
}
|
|
3913
|
-
function addGraceNote(score, options) {
|
|
3914
|
-
const { partIndex, measureIndex, targetNoteIndex, pitch, noteType = "eighth", slash = true, voice, staff } = options;
|
|
3915
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3916
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3917
|
-
}
|
|
3918
|
-
const part = score.parts[partIndex];
|
|
3919
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3920
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3921
|
-
}
|
|
3922
|
-
const measure = part.measures[measureIndex];
|
|
3923
|
-
let noteCount = 0;
|
|
3924
|
-
let targetEntryIndex = -1;
|
|
3925
|
-
let targetNote = null;
|
|
3926
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
3927
|
-
const entry = measure.entries[i];
|
|
3928
|
-
if (entry.type === "note" && !entry.chord) {
|
|
3929
|
-
if (noteCount === targetNoteIndex) {
|
|
3930
|
-
targetEntryIndex = i;
|
|
3931
|
-
targetNote = entry;
|
|
3932
|
-
break;
|
|
3933
|
-
}
|
|
3934
|
-
noteCount++;
|
|
3935
|
-
}
|
|
3936
|
-
}
|
|
3937
|
-
if (targetEntryIndex < 0 || !targetNote) {
|
|
3938
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${targetNoteIndex} not found`, { partIndex, measureIndex })]);
|
|
3939
|
-
}
|
|
3940
|
-
const result = cloneScore(score);
|
|
3941
|
-
const resultMeasure = result.parts[partIndex].measures[measureIndex];
|
|
3942
|
-
const graceNote = {
|
|
3943
|
-
_id: generateId(),
|
|
3944
|
-
type: "note",
|
|
3945
|
-
pitch,
|
|
3946
|
-
duration: 0,
|
|
3947
|
-
// Grace notes have no duration
|
|
3948
|
-
voice: voice ?? targetNote.voice,
|
|
3949
|
-
staff: staff ?? targetNote.staff,
|
|
3950
|
-
noteType,
|
|
3951
|
-
grace: {
|
|
3952
|
-
slash
|
|
3953
|
-
}
|
|
3954
|
-
};
|
|
3955
|
-
resultMeasure.entries.splice(targetEntryIndex, 0, graceNote);
|
|
3956
|
-
return success(result);
|
|
3957
|
-
}
|
|
3958
|
-
function removeGraceNote(score, options) {
|
|
3959
|
-
const { partIndex, measureIndex, graceNoteIndex } = options;
|
|
3960
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3961
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3962
|
-
}
|
|
3963
|
-
const part = score.parts[partIndex];
|
|
3964
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3965
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3966
|
-
}
|
|
3967
|
-
const measure = part.measures[measureIndex];
|
|
3968
|
-
let graceCount = 0;
|
|
3969
|
-
let targetEntryIndex = -1;
|
|
3970
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
3971
|
-
const entry = measure.entries[i];
|
|
3972
|
-
if (entry.type === "note" && entry.grace) {
|
|
3973
|
-
if (graceCount === graceNoteIndex) {
|
|
3974
|
-
targetEntryIndex = i;
|
|
3975
|
-
break;
|
|
3976
|
-
}
|
|
3977
|
-
graceCount++;
|
|
3978
|
-
}
|
|
3979
|
-
}
|
|
3980
|
-
if (targetEntryIndex < 0) {
|
|
3981
|
-
return failure([operationError("GRACE_NOTE_NOT_FOUND", `Grace note at index ${graceNoteIndex} not found`, { partIndex, measureIndex })]);
|
|
3982
|
-
}
|
|
3983
|
-
const result = cloneScore(score);
|
|
3984
|
-
result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
|
|
3985
|
-
return success(result);
|
|
3986
|
-
}
|
|
3987
|
-
function convertToGrace(score, options) {
|
|
3988
|
-
const { partIndex, measureIndex, noteIndex, slash = true } = options;
|
|
3989
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
3990
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
3991
|
-
}
|
|
3992
|
-
const part = score.parts[partIndex];
|
|
3993
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
3994
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
3995
|
-
}
|
|
3996
|
-
const measure = part.measures[measureIndex];
|
|
3997
|
-
let noteCount = 0;
|
|
3998
|
-
let targetEntryIndex = -1;
|
|
3999
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4000
|
-
const entry = measure.entries[i];
|
|
4001
|
-
if (entry.type === "note" && !entry.chord) {
|
|
4002
|
-
if (noteCount === noteIndex) {
|
|
4003
|
-
targetEntryIndex = i;
|
|
4004
|
-
break;
|
|
4005
|
-
}
|
|
4006
|
-
noteCount++;
|
|
4007
|
-
}
|
|
4008
|
-
}
|
|
4009
|
-
if (targetEntryIndex < 0) {
|
|
4010
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4011
|
-
}
|
|
4012
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4013
|
-
if (targetEntry.type !== "note") {
|
|
4014
|
-
return failure([operationError("NOTE_NOT_FOUND", `Entry at index is not a note`, { partIndex, measureIndex })]);
|
|
4015
|
-
}
|
|
4016
|
-
if (targetEntry.grace) {
|
|
4017
|
-
return failure([operationError("INVALID_GRACE_NOTE", `Note is already a grace note`, { partIndex, measureIndex })]);
|
|
4018
|
-
}
|
|
4019
|
-
const result = cloneScore(score);
|
|
4020
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4021
|
-
resultNote.grace = { slash };
|
|
4022
|
-
resultNote.duration = 0;
|
|
4023
|
-
return success(result);
|
|
4024
|
-
}
|
|
4025
|
-
function addLyric(score, options) {
|
|
4026
|
-
const { partIndex, measureIndex, noteIndex, text, syllabic = "single", verse = 1, extend = false } = options;
|
|
4027
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4028
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4029
|
-
}
|
|
4030
|
-
const part = score.parts[partIndex];
|
|
4031
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4032
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4033
|
-
}
|
|
4034
|
-
const measure = part.measures[measureIndex];
|
|
4035
|
-
let noteCount = 0;
|
|
4036
|
-
let targetEntryIndex = -1;
|
|
4037
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4038
|
-
const entry = measure.entries[i];
|
|
4039
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4040
|
-
if (noteCount === noteIndex) {
|
|
4041
|
-
targetEntryIndex = i;
|
|
4042
|
-
break;
|
|
4043
|
-
}
|
|
4044
|
-
noteCount++;
|
|
4045
|
-
}
|
|
4046
|
-
}
|
|
4047
|
-
if (targetEntryIndex < 0) {
|
|
4048
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4049
|
-
}
|
|
4050
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4051
|
-
if (targetEntry.type !== "note") {
|
|
4052
|
-
return failure([operationError("NOTE_NOT_FOUND", `Entry is not a note`, { partIndex, measureIndex })]);
|
|
4053
|
-
}
|
|
4054
|
-
if (targetEntry.lyrics?.some((l) => l.number === verse)) {
|
|
4055
|
-
return failure([operationError("LYRIC_ALREADY_EXISTS", `Lyric for verse ${verse} already exists on this note`, { partIndex, measureIndex })]);
|
|
4056
|
-
}
|
|
4057
|
-
const result = cloneScore(score);
|
|
4058
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4059
|
-
if (!resultNote.lyrics) {
|
|
4060
|
-
resultNote.lyrics = [];
|
|
4061
|
-
}
|
|
4062
|
-
const lyric = {
|
|
4063
|
-
number: verse,
|
|
4064
|
-
syllabic,
|
|
4065
|
-
text,
|
|
4066
|
-
extend
|
|
4067
|
-
};
|
|
4068
|
-
resultNote.lyrics.push(lyric);
|
|
4069
|
-
return success(result);
|
|
4070
|
-
}
|
|
4071
|
-
function removeLyric(score, options) {
|
|
4072
|
-
const { partIndex, measureIndex, noteIndex, verse } = options;
|
|
4073
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4074
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4075
|
-
}
|
|
4076
|
-
const part = score.parts[partIndex];
|
|
4077
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4078
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4079
|
-
}
|
|
4080
|
-
const measure = part.measures[measureIndex];
|
|
4081
|
-
let noteCount = 0;
|
|
4082
|
-
let targetEntryIndex = -1;
|
|
4083
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4084
|
-
const entry = measure.entries[i];
|
|
4085
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4086
|
-
if (noteCount === noteIndex) {
|
|
4087
|
-
targetEntryIndex = i;
|
|
4088
|
-
break;
|
|
4089
|
-
}
|
|
4090
|
-
noteCount++;
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
if (targetEntryIndex < 0) {
|
|
4094
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4095
|
-
}
|
|
4096
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4097
|
-
if (targetEntry.type !== "note" || !targetEntry.lyrics || targetEntry.lyrics.length === 0) {
|
|
4098
|
-
return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
|
|
4099
|
-
}
|
|
4100
|
-
if (verse !== void 0) {
|
|
4101
|
-
const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
|
|
4102
|
-
if (lyricIndex < 0) {
|
|
4103
|
-
return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
const result = cloneScore(score);
|
|
4107
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4108
|
-
if (verse !== void 0) {
|
|
4109
|
-
resultNote.lyrics = resultNote.lyrics.filter((l) => l.number !== verse);
|
|
4110
|
-
} else {
|
|
4111
|
-
delete resultNote.lyrics;
|
|
4112
|
-
}
|
|
4113
|
-
if (resultNote.lyrics && resultNote.lyrics.length === 0) {
|
|
4114
|
-
delete resultNote.lyrics;
|
|
4115
|
-
}
|
|
4116
|
-
return success(result);
|
|
4117
|
-
}
|
|
4118
|
-
function updateLyric(score, options) {
|
|
4119
|
-
const { partIndex, measureIndex, noteIndex, verse = 1, text, syllabic, extend } = options;
|
|
4120
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4121
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4122
|
-
}
|
|
4123
|
-
const part = score.parts[partIndex];
|
|
4124
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4125
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4126
|
-
}
|
|
4127
|
-
const measure = part.measures[measureIndex];
|
|
4128
|
-
let noteCount = 0;
|
|
4129
|
-
let targetEntryIndex = -1;
|
|
4130
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4131
|
-
const entry = measure.entries[i];
|
|
4132
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4133
|
-
if (noteCount === noteIndex) {
|
|
4134
|
-
targetEntryIndex = i;
|
|
4135
|
-
break;
|
|
4136
|
-
}
|
|
4137
|
-
noteCount++;
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
if (targetEntryIndex < 0) {
|
|
4141
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4142
|
-
}
|
|
4143
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4144
|
-
if (targetEntry.type !== "note" || !targetEntry.lyrics) {
|
|
4145
|
-
return failure([operationError("LYRIC_NOT_FOUND", `No lyrics found on note`, { partIndex, measureIndex })]);
|
|
4146
|
-
}
|
|
4147
|
-
const lyricIndex = targetEntry.lyrics.findIndex((l) => l.number === verse);
|
|
4148
|
-
if (lyricIndex < 0) {
|
|
4149
|
-
return failure([operationError("LYRIC_NOT_FOUND", `Lyric for verse ${verse} not found on note`, { partIndex, measureIndex })]);
|
|
4150
|
-
}
|
|
4151
|
-
const result = cloneScore(score);
|
|
4152
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4153
|
-
const lyric = resultNote.lyrics[lyricIndex];
|
|
4154
|
-
if (text !== void 0) {
|
|
4155
|
-
lyric.text = text;
|
|
4156
|
-
}
|
|
4157
|
-
if (syllabic !== void 0) {
|
|
4158
|
-
lyric.syllabic = syllabic;
|
|
4159
|
-
}
|
|
4160
|
-
if (extend !== void 0) {
|
|
4161
|
-
lyric.extend = extend;
|
|
4162
|
-
}
|
|
4163
|
-
return success(result);
|
|
4164
|
-
}
|
|
4165
|
-
function addHarmony(score, options) {
|
|
4166
|
-
const { partIndex, measureIndex, position, root, kind, kindText, bass, degrees, staff, placement = "above" } = options;
|
|
4167
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4168
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4169
|
-
}
|
|
4170
|
-
const part = score.parts[partIndex];
|
|
4171
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4172
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4173
|
-
}
|
|
4174
|
-
const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
|
|
4175
|
-
if (!validSteps.includes(root.step.toUpperCase())) {
|
|
4176
|
-
return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
|
|
4177
|
-
}
|
|
4178
|
-
if (bass && !validSteps.includes(bass.step.toUpperCase())) {
|
|
4179
|
-
return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
|
|
4180
|
-
}
|
|
4181
|
-
const result = cloneScore(score);
|
|
4182
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
4183
|
-
const harmony = {
|
|
4184
|
-
_id: generateId(),
|
|
4185
|
-
type: "harmony",
|
|
4186
|
-
root: {
|
|
4187
|
-
rootStep: root.step.toUpperCase(),
|
|
4188
|
-
rootAlter: root.alter
|
|
4189
|
-
},
|
|
4190
|
-
kind,
|
|
4191
|
-
kindText,
|
|
4192
|
-
bass: bass ? {
|
|
4193
|
-
bassStep: bass.step.toUpperCase(),
|
|
4194
|
-
bassAlter: bass.alter
|
|
4195
|
-
} : void 0,
|
|
4196
|
-
degrees: degrees?.map((d) => ({
|
|
4197
|
-
degreeValue: d.value,
|
|
4198
|
-
degreeAlter: d.alter,
|
|
4199
|
-
degreeType: d.type
|
|
4200
|
-
})),
|
|
4201
|
-
staff,
|
|
4202
|
-
placement
|
|
4203
|
-
};
|
|
4204
|
-
let currentPosition = 0;
|
|
4205
|
-
let insertIndex = 0;
|
|
4206
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4207
|
-
const entry = measure.entries[i];
|
|
4208
|
-
if (currentPosition >= position) {
|
|
4209
|
-
insertIndex = i;
|
|
4210
|
-
break;
|
|
4211
|
-
}
|
|
4212
|
-
if (entry.type === "note" && !entry.chord) {
|
|
4213
|
-
currentPosition += entry.duration;
|
|
4214
|
-
} else if (entry.type === "forward") {
|
|
4215
|
-
currentPosition += entry.duration;
|
|
4216
|
-
} else if (entry.type === "backup") {
|
|
4217
|
-
currentPosition -= entry.duration;
|
|
4218
|
-
}
|
|
4219
|
-
insertIndex = i + 1;
|
|
4220
|
-
}
|
|
4221
|
-
measure.entries.splice(insertIndex, 0, harmony);
|
|
4222
|
-
return success(result);
|
|
4223
|
-
}
|
|
4224
|
-
function removeHarmony(score, options) {
|
|
4225
|
-
const { partIndex, measureIndex, harmonyIndex } = options;
|
|
4226
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4227
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4228
|
-
}
|
|
4229
|
-
const part = score.parts[partIndex];
|
|
4230
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4231
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4232
|
-
}
|
|
4233
|
-
const measure = part.measures[measureIndex];
|
|
4234
|
-
let harmonyCount = 0;
|
|
4235
|
-
let targetEntryIndex = -1;
|
|
4236
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4237
|
-
const entry = measure.entries[i];
|
|
4238
|
-
if (entry.type === "harmony") {
|
|
4239
|
-
if (harmonyCount === harmonyIndex) {
|
|
4240
|
-
targetEntryIndex = i;
|
|
4241
|
-
break;
|
|
4242
|
-
}
|
|
4243
|
-
harmonyCount++;
|
|
4244
|
-
}
|
|
4245
|
-
}
|
|
4246
|
-
if (targetEntryIndex < 0) {
|
|
4247
|
-
return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
|
|
4248
|
-
}
|
|
4249
|
-
const result = cloneScore(score);
|
|
4250
|
-
result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
|
|
4251
|
-
return success(result);
|
|
4252
|
-
}
|
|
4253
|
-
function updateHarmony(score, options) {
|
|
4254
|
-
const { partIndex, measureIndex, harmonyIndex, root, kind, kindText, bass, degrees } = options;
|
|
4255
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4256
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4257
|
-
}
|
|
4258
|
-
const part = score.parts[partIndex];
|
|
4259
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4260
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4261
|
-
}
|
|
4262
|
-
const measure = part.measures[measureIndex];
|
|
4263
|
-
let harmonyCount = 0;
|
|
4264
|
-
let targetEntryIndex = -1;
|
|
4265
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4266
|
-
const entry = measure.entries[i];
|
|
4267
|
-
if (entry.type === "harmony") {
|
|
4268
|
-
if (harmonyCount === harmonyIndex) {
|
|
4269
|
-
targetEntryIndex = i;
|
|
4270
|
-
break;
|
|
4271
|
-
}
|
|
4272
|
-
harmonyCount++;
|
|
4273
|
-
}
|
|
4274
|
-
}
|
|
4275
|
-
if (targetEntryIndex < 0) {
|
|
4276
|
-
return failure([operationError("HARMONY_NOT_FOUND", `Harmony at index ${harmonyIndex} not found`, { partIndex, measureIndex })]);
|
|
4277
|
-
}
|
|
4278
|
-
const validSteps = ["A", "B", "C", "D", "E", "F", "G"];
|
|
4279
|
-
if (root && !validSteps.includes(root.step.toUpperCase())) {
|
|
4280
|
-
return failure([operationError("INVALID_HARMONY", `Invalid root step: ${root.step}`, { partIndex, measureIndex })]);
|
|
4281
|
-
}
|
|
4282
|
-
if (bass && !validSteps.includes(bass.step.toUpperCase())) {
|
|
4283
|
-
return failure([operationError("INVALID_HARMONY", `Invalid bass step: ${bass.step}`, { partIndex, measureIndex })]);
|
|
4284
|
-
}
|
|
4285
|
-
const result = cloneScore(score);
|
|
4286
|
-
const harmony = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4287
|
-
if (root) {
|
|
4288
|
-
harmony.root = {
|
|
4289
|
-
rootStep: root.step.toUpperCase(),
|
|
4290
|
-
rootAlter: root.alter
|
|
4291
|
-
};
|
|
4292
|
-
}
|
|
4293
|
-
if (kind !== void 0) {
|
|
4294
|
-
harmony.kind = kind;
|
|
4295
|
-
}
|
|
4296
|
-
if (kindText !== void 0) {
|
|
4297
|
-
harmony.kindText = kindText;
|
|
4298
|
-
}
|
|
4299
|
-
if (bass !== void 0) {
|
|
4300
|
-
if (bass === null) {
|
|
4301
|
-
delete harmony.bass;
|
|
4302
|
-
} else {
|
|
4303
|
-
harmony.bass = {
|
|
4304
|
-
bassStep: bass.step.toUpperCase(),
|
|
4305
|
-
bassAlter: bass.alter
|
|
4306
|
-
};
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
if (degrees !== void 0) {
|
|
4310
|
-
if (degrees === null) {
|
|
4311
|
-
delete harmony.degrees;
|
|
4312
|
-
} else {
|
|
4313
|
-
harmony.degrees = degrees.map((d) => ({
|
|
4314
|
-
degreeValue: d.value,
|
|
4315
|
-
degreeAlter: d.alter,
|
|
4316
|
-
degreeType: d.type
|
|
4317
|
-
}));
|
|
4318
|
-
}
|
|
4319
|
-
}
|
|
4320
|
-
return success(result);
|
|
4321
|
-
}
|
|
4322
|
-
function addFingering(score, options) {
|
|
4323
|
-
const { partIndex, measureIndex, noteIndex, fingering, substitution = false, alternate = false, placement } = options;
|
|
4324
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4325
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4326
|
-
}
|
|
4327
|
-
const part = score.parts[partIndex];
|
|
4328
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4329
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4330
|
-
}
|
|
4331
|
-
const measure = part.measures[measureIndex];
|
|
4332
|
-
let noteCount = 0;
|
|
4333
|
-
let targetEntryIndex = -1;
|
|
4334
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4335
|
-
const entry = measure.entries[i];
|
|
4336
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4337
|
-
if (noteCount === noteIndex) {
|
|
4338
|
-
targetEntryIndex = i;
|
|
4339
|
-
break;
|
|
4340
|
-
}
|
|
4341
|
-
noteCount++;
|
|
4342
|
-
}
|
|
4343
|
-
}
|
|
4344
|
-
if (targetEntryIndex < 0) {
|
|
4345
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4346
|
-
}
|
|
4347
|
-
const result = cloneScore(score);
|
|
4348
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4349
|
-
if (!resultNote.notations) {
|
|
4350
|
-
resultNote.notations = [];
|
|
4351
|
-
}
|
|
4352
|
-
resultNote.notations.push({
|
|
4353
|
-
type: "technical",
|
|
4354
|
-
technical: "fingering",
|
|
4355
|
-
fingering,
|
|
4356
|
-
fingeringSubstitution: substitution || void 0,
|
|
4357
|
-
fingeringAlternate: alternate || void 0,
|
|
4358
|
-
placement
|
|
4359
|
-
});
|
|
4360
|
-
return success(result);
|
|
4361
|
-
}
|
|
4362
|
-
function removeFingering(score, options) {
|
|
4363
|
-
const { partIndex, measureIndex, noteIndex } = options;
|
|
4364
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4365
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4366
|
-
}
|
|
4367
|
-
const part = score.parts[partIndex];
|
|
4368
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4369
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4370
|
-
}
|
|
4371
|
-
const measure = part.measures[measureIndex];
|
|
4372
|
-
let noteCount = 0;
|
|
4373
|
-
let targetEntryIndex = -1;
|
|
4374
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4375
|
-
const entry = measure.entries[i];
|
|
4376
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4377
|
-
if (noteCount === noteIndex) {
|
|
4378
|
-
targetEntryIndex = i;
|
|
4379
|
-
break;
|
|
4380
|
-
}
|
|
4381
|
-
noteCount++;
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
if (targetEntryIndex < 0) {
|
|
4385
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4386
|
-
}
|
|
4387
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4388
|
-
if (targetEntry.type !== "note" || !targetEntry.notations) {
|
|
4389
|
-
return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
|
|
4390
|
-
}
|
|
4391
|
-
const fingeringIndex = targetEntry.notations.findIndex(
|
|
4392
|
-
(n) => n.type === "technical" && n.technical === "fingering"
|
|
4393
|
-
);
|
|
4394
|
-
if (fingeringIndex < 0) {
|
|
4395
|
-
return failure([operationError("NOTE_NOT_FOUND", `No fingering found on note`, { partIndex, measureIndex })]);
|
|
4396
|
-
}
|
|
4397
|
-
const result = cloneScore(score);
|
|
4398
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4399
|
-
resultNote.notations.splice(fingeringIndex, 1);
|
|
4400
|
-
if (resultNote.notations.length === 0) {
|
|
4401
|
-
delete resultNote.notations;
|
|
4402
|
-
}
|
|
4403
|
-
return success(result);
|
|
4404
|
-
}
|
|
4405
|
-
function addBowing(score, options) {
|
|
4406
|
-
const { partIndex, measureIndex, noteIndex, bowingType, placement } = options;
|
|
4407
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4408
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4409
|
-
}
|
|
4410
|
-
const part = score.parts[partIndex];
|
|
4411
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4412
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4413
|
-
}
|
|
4414
|
-
const measure = part.measures[measureIndex];
|
|
4415
|
-
let noteCount = 0;
|
|
4416
|
-
let targetEntryIndex = -1;
|
|
4417
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4418
|
-
const entry = measure.entries[i];
|
|
4419
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4420
|
-
if (noteCount === noteIndex) {
|
|
4421
|
-
targetEntryIndex = i;
|
|
4422
|
-
break;
|
|
4423
|
-
}
|
|
4424
|
-
noteCount++;
|
|
4425
|
-
}
|
|
4426
|
-
}
|
|
4427
|
-
if (targetEntryIndex < 0) {
|
|
4428
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4429
|
-
}
|
|
4430
|
-
const result = cloneScore(score);
|
|
4431
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4432
|
-
if (!resultNote.notations) {
|
|
4433
|
-
resultNote.notations = [];
|
|
4434
|
-
}
|
|
4435
|
-
resultNote.notations.push({
|
|
4436
|
-
type: "technical",
|
|
4437
|
-
technical: bowingType,
|
|
4438
|
-
placement
|
|
4439
|
-
});
|
|
4440
|
-
return success(result);
|
|
4441
|
-
}
|
|
4442
|
-
function removeBowing(score, options) {
|
|
4443
|
-
const { partIndex, measureIndex, noteIndex, bowingType } = options;
|
|
4444
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4445
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4446
|
-
}
|
|
4447
|
-
const part = score.parts[partIndex];
|
|
4448
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4449
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4450
|
-
}
|
|
4451
|
-
const measure = part.measures[measureIndex];
|
|
4452
|
-
let noteCount = 0;
|
|
4453
|
-
let targetEntryIndex = -1;
|
|
4454
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4455
|
-
const entry = measure.entries[i];
|
|
4456
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4457
|
-
if (noteCount === noteIndex) {
|
|
4458
|
-
targetEntryIndex = i;
|
|
4459
|
-
break;
|
|
4460
|
-
}
|
|
4461
|
-
noteCount++;
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
if (targetEntryIndex < 0) {
|
|
4465
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4466
|
-
}
|
|
4467
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4468
|
-
if (targetEntry.type !== "note" || !targetEntry.notations) {
|
|
4469
|
-
return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
|
|
4470
|
-
}
|
|
4471
|
-
const bowingIndex = targetEntry.notations.findIndex((n) => {
|
|
4472
|
-
if (n.type !== "technical") return false;
|
|
4473
|
-
if (bowingType) return n.technical === bowingType;
|
|
4474
|
-
return n.technical === "up-bow" || n.technical === "down-bow";
|
|
4475
|
-
});
|
|
4476
|
-
if (bowingIndex < 0) {
|
|
4477
|
-
return failure([operationError("NOTE_NOT_FOUND", `No bowing found on note`, { partIndex, measureIndex })]);
|
|
4478
|
-
}
|
|
4479
|
-
const result = cloneScore(score);
|
|
4480
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4481
|
-
resultNote.notations.splice(bowingIndex, 1);
|
|
4482
|
-
if (resultNote.notations.length === 0) {
|
|
4483
|
-
delete resultNote.notations;
|
|
4484
|
-
}
|
|
4485
|
-
return success(result);
|
|
4486
|
-
}
|
|
4487
|
-
function addStringNumber(score, options) {
|
|
4488
|
-
const { partIndex, measureIndex, noteIndex, stringNumber, placement } = options;
|
|
4489
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4490
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4491
|
-
}
|
|
4492
|
-
const part = score.parts[partIndex];
|
|
4493
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4494
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4495
|
-
}
|
|
4496
|
-
if (stringNumber < 1) {
|
|
4497
|
-
return failure([operationError("INVALID_POSITION", `String number must be positive`, { partIndex, measureIndex })]);
|
|
4498
|
-
}
|
|
4499
|
-
const measure = part.measures[measureIndex];
|
|
4500
|
-
let noteCount = 0;
|
|
4501
|
-
let targetEntryIndex = -1;
|
|
4502
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4503
|
-
const entry = measure.entries[i];
|
|
4504
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4505
|
-
if (noteCount === noteIndex) {
|
|
4506
|
-
targetEntryIndex = i;
|
|
4507
|
-
break;
|
|
4508
|
-
}
|
|
4509
|
-
noteCount++;
|
|
4510
|
-
}
|
|
4511
|
-
}
|
|
4512
|
-
if (targetEntryIndex < 0) {
|
|
4513
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4514
|
-
}
|
|
4515
|
-
const result = cloneScore(score);
|
|
4516
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4517
|
-
if (!resultNote.notations) {
|
|
4518
|
-
resultNote.notations = [];
|
|
4519
|
-
}
|
|
4520
|
-
resultNote.notations.push({
|
|
4521
|
-
type: "technical",
|
|
4522
|
-
technical: "string",
|
|
4523
|
-
string: stringNumber,
|
|
4524
|
-
placement
|
|
4525
|
-
});
|
|
4526
|
-
return success(result);
|
|
4527
|
-
}
|
|
4528
|
-
function removeStringNumber(score, options) {
|
|
4529
|
-
const { partIndex, measureIndex, noteIndex } = options;
|
|
4530
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4531
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4532
|
-
}
|
|
4533
|
-
const part = score.parts[partIndex];
|
|
4534
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4535
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4536
|
-
}
|
|
4537
|
-
const measure = part.measures[measureIndex];
|
|
4538
|
-
let noteCount = 0;
|
|
4539
|
-
let targetEntryIndex = -1;
|
|
4540
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4541
|
-
const entry = measure.entries[i];
|
|
4542
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4543
|
-
if (noteCount === noteIndex) {
|
|
4544
|
-
targetEntryIndex = i;
|
|
4545
|
-
break;
|
|
4546
|
-
}
|
|
4547
|
-
noteCount++;
|
|
4548
|
-
}
|
|
4549
|
-
}
|
|
4550
|
-
if (targetEntryIndex < 0) {
|
|
4551
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4552
|
-
}
|
|
4553
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4554
|
-
if (targetEntry.type !== "note" || !targetEntry.notations) {
|
|
4555
|
-
return failure([operationError("NOTE_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
|
|
4556
|
-
}
|
|
4557
|
-
const stringIndex = targetEntry.notations.findIndex(
|
|
4558
|
-
(n) => n.type === "technical" && n.technical === "string"
|
|
4559
|
-
);
|
|
4560
|
-
if (stringIndex < 0) {
|
|
4561
|
-
return failure([operationError("NOTE_NOT_FOUND", `No string number found on note`, { partIndex, measureIndex })]);
|
|
4562
|
-
}
|
|
4563
|
-
const result = cloneScore(score);
|
|
4564
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4565
|
-
resultNote.notations.splice(stringIndex, 1);
|
|
4566
|
-
if (resultNote.notations.length === 0) {
|
|
4567
|
-
delete resultNote.notations;
|
|
4568
|
-
}
|
|
4569
|
-
return success(result);
|
|
4570
|
-
}
|
|
4571
|
-
function addOctaveShift(score, options) {
|
|
4572
|
-
const { partIndex, measureIndex, position, shiftType, size = 8 } = options;
|
|
4573
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4574
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4575
|
-
}
|
|
4576
|
-
const part = score.parts[partIndex];
|
|
4577
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4578
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4579
|
-
}
|
|
4580
|
-
const result = cloneScore(score);
|
|
4581
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
4582
|
-
const direction = {
|
|
4583
|
-
_id: generateId(),
|
|
4584
|
-
type: "direction",
|
|
4585
|
-
directionTypes: [{
|
|
4586
|
-
kind: "octave-shift",
|
|
4587
|
-
type: shiftType,
|
|
4588
|
-
size
|
|
4589
|
-
}],
|
|
4590
|
-
placement: shiftType === "down" ? "above" : "below"
|
|
4591
|
-
};
|
|
4592
|
-
insertDirectionAtPosition(measure, direction, position);
|
|
4593
|
-
return success(result);
|
|
4594
|
-
}
|
|
4595
|
-
function stopOctaveShift(score, options) {
|
|
4596
|
-
const { partIndex, measureIndex, position, size = 8 } = options;
|
|
4597
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4598
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4599
|
-
}
|
|
4600
|
-
const part = score.parts[partIndex];
|
|
4601
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4602
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4603
|
-
}
|
|
4604
|
-
const result = cloneScore(score);
|
|
4605
|
-
const measure = result.parts[partIndex].measures[measureIndex];
|
|
4606
|
-
const direction = {
|
|
4607
|
-
_id: generateId(),
|
|
4608
|
-
type: "direction",
|
|
4609
|
-
directionTypes: [{
|
|
4610
|
-
kind: "octave-shift",
|
|
4611
|
-
type: "stop",
|
|
4612
|
-
size
|
|
4613
|
-
}]
|
|
4614
|
-
};
|
|
4615
|
-
insertDirectionAtPosition(measure, direction, position);
|
|
4616
|
-
return success(result);
|
|
4617
|
-
}
|
|
4618
|
-
function removeOctaveShift(score, options) {
|
|
4619
|
-
const { partIndex, measureIndex, octaveShiftIndex = 0 } = options;
|
|
4620
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4621
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4622
|
-
}
|
|
4623
|
-
const part = score.parts[partIndex];
|
|
4624
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4625
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4626
|
-
}
|
|
4627
|
-
const measure = part.measures[measureIndex];
|
|
4628
|
-
let shiftCount = 0;
|
|
4629
|
-
let targetEntryIndex = -1;
|
|
4630
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4631
|
-
const entry = measure.entries[i];
|
|
4632
|
-
if (entry.type === "direction") {
|
|
4633
|
-
const hasOctaveShift = entry.directionTypes.some((dt) => dt.kind === "octave-shift");
|
|
4634
|
-
if (hasOctaveShift) {
|
|
4635
|
-
if (shiftCount === octaveShiftIndex) {
|
|
4636
|
-
targetEntryIndex = i;
|
|
4637
|
-
break;
|
|
4638
|
-
}
|
|
4639
|
-
shiftCount++;
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
}
|
|
4643
|
-
if (targetEntryIndex < 0) {
|
|
4644
|
-
return failure([operationError("NOTE_NOT_FOUND", `Octave shift at index ${octaveShiftIndex} not found`, { partIndex, measureIndex })]);
|
|
4645
|
-
}
|
|
4646
|
-
const result = cloneScore(score);
|
|
4647
|
-
result.parts[partIndex].measures[measureIndex].entries.splice(targetEntryIndex, 1);
|
|
4648
|
-
return success(result);
|
|
4649
|
-
}
|
|
4650
|
-
function addBreathMark(score, options) {
|
|
4651
|
-
const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
|
|
4652
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4653
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4654
|
-
}
|
|
4655
|
-
const part = score.parts[partIndex];
|
|
4656
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4657
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4658
|
-
}
|
|
4659
|
-
const measure = part.measures[measureIndex];
|
|
4660
|
-
let noteCount = 0;
|
|
4661
|
-
let targetEntryIndex = -1;
|
|
4662
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4663
|
-
const entry = measure.entries[i];
|
|
4664
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4665
|
-
if (noteCount === noteIndex) {
|
|
4666
|
-
targetEntryIndex = i;
|
|
4667
|
-
break;
|
|
4668
|
-
}
|
|
4669
|
-
noteCount++;
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
if (targetEntryIndex < 0) {
|
|
4673
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4674
|
-
}
|
|
4675
|
-
const result = cloneScore(score);
|
|
4676
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4677
|
-
if (!resultNote.notations) {
|
|
4678
|
-
resultNote.notations = [];
|
|
4679
|
-
}
|
|
4680
|
-
const existingBreathMark = resultNote.notations.find(
|
|
4681
|
-
(n) => n.type === "articulation" && n.articulation === "breath-mark"
|
|
4682
|
-
);
|
|
4683
|
-
if (existingBreathMark) {
|
|
4684
|
-
return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Breath mark already exists on note`, { partIndex, measureIndex })]);
|
|
4685
|
-
}
|
|
4686
|
-
resultNote.notations.push({
|
|
4687
|
-
type: "articulation",
|
|
4688
|
-
articulation: "breath-mark",
|
|
4689
|
-
placement
|
|
4690
|
-
});
|
|
4691
|
-
return success(result);
|
|
4692
|
-
}
|
|
4693
|
-
function removeBreathMark(score, options) {
|
|
4694
|
-
const { partIndex, measureIndex, noteIndex } = options;
|
|
4695
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4696
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4697
|
-
}
|
|
4698
|
-
const part = score.parts[partIndex];
|
|
4699
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4700
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4701
|
-
}
|
|
4702
|
-
const measure = part.measures[measureIndex];
|
|
4703
|
-
let noteCount = 0;
|
|
4704
|
-
let targetEntryIndex = -1;
|
|
4705
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4706
|
-
const entry = measure.entries[i];
|
|
4707
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4708
|
-
if (noteCount === noteIndex) {
|
|
4709
|
-
targetEntryIndex = i;
|
|
4710
|
-
break;
|
|
4711
|
-
}
|
|
4712
|
-
noteCount++;
|
|
4713
|
-
}
|
|
4714
|
-
}
|
|
4715
|
-
if (targetEntryIndex < 0) {
|
|
4716
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4717
|
-
}
|
|
4718
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4719
|
-
if (targetEntry.type !== "note" || !targetEntry.notations) {
|
|
4720
|
-
return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
|
|
4721
|
-
}
|
|
4722
|
-
const breathMarkIndex = targetEntry.notations.findIndex(
|
|
4723
|
-
(n) => n.type === "articulation" && n.articulation === "breath-mark"
|
|
4724
|
-
);
|
|
4725
|
-
if (breathMarkIndex < 0) {
|
|
4726
|
-
return failure([operationError("ARTICULATION_NOT_FOUND", `No breath mark found on note`, { partIndex, measureIndex })]);
|
|
4727
|
-
}
|
|
4728
|
-
const result = cloneScore(score);
|
|
4729
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4730
|
-
resultNote.notations.splice(breathMarkIndex, 1);
|
|
4731
|
-
if (resultNote.notations.length === 0) {
|
|
4732
|
-
delete resultNote.notations;
|
|
4733
|
-
}
|
|
4734
|
-
return success(result);
|
|
4735
|
-
}
|
|
4736
|
-
function addCaesura(score, options) {
|
|
4737
|
-
const { partIndex, measureIndex, noteIndex, placement = "above" } = options;
|
|
4738
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4739
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4740
|
-
}
|
|
4741
|
-
const part = score.parts[partIndex];
|
|
4742
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4743
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4744
|
-
}
|
|
4745
|
-
const measure = part.measures[measureIndex];
|
|
4746
|
-
let noteCount = 0;
|
|
4747
|
-
let targetEntryIndex = -1;
|
|
4748
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4749
|
-
const entry = measure.entries[i];
|
|
4750
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4751
|
-
if (noteCount === noteIndex) {
|
|
4752
|
-
targetEntryIndex = i;
|
|
4753
|
-
break;
|
|
4754
|
-
}
|
|
4755
|
-
noteCount++;
|
|
4756
|
-
}
|
|
4757
|
-
}
|
|
4758
|
-
if (targetEntryIndex < 0) {
|
|
4759
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4760
|
-
}
|
|
4761
|
-
const result = cloneScore(score);
|
|
4762
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4763
|
-
if (!resultNote.notations) {
|
|
4764
|
-
resultNote.notations = [];
|
|
4765
|
-
}
|
|
4766
|
-
const existingCaesura = resultNote.notations.find(
|
|
4767
|
-
(n) => n.type === "articulation" && n.articulation === "caesura"
|
|
4768
|
-
);
|
|
4769
|
-
if (existingCaesura) {
|
|
4770
|
-
return failure([operationError("ARTICULATION_ALREADY_EXISTS", `Caesura already exists on note`, { partIndex, measureIndex })]);
|
|
4771
|
-
}
|
|
4772
|
-
resultNote.notations.push({
|
|
4773
|
-
type: "articulation",
|
|
4774
|
-
articulation: "caesura",
|
|
4775
|
-
placement
|
|
4776
|
-
});
|
|
4777
|
-
return success(result);
|
|
4778
|
-
}
|
|
4779
|
-
function removeCaesura(score, options) {
|
|
4780
|
-
const { partIndex, measureIndex, noteIndex } = options;
|
|
4781
|
-
if (partIndex < 0 || partIndex >= score.parts.length) {
|
|
4782
|
-
return failure([operationError("PART_NOT_FOUND", `Part index ${partIndex} out of bounds`, { partIndex })]);
|
|
4783
|
-
}
|
|
4784
|
-
const part = score.parts[partIndex];
|
|
4785
|
-
if (measureIndex < 0 || measureIndex >= part.measures.length) {
|
|
4786
|
-
return failure([operationError("MEASURE_NOT_FOUND", `Measure index ${measureIndex} out of bounds`, { partIndex, measureIndex })]);
|
|
4787
|
-
}
|
|
4788
|
-
const measure = part.measures[measureIndex];
|
|
4789
|
-
let noteCount = 0;
|
|
4790
|
-
let targetEntryIndex = -1;
|
|
4791
|
-
for (let i = 0; i < measure.entries.length; i++) {
|
|
4792
|
-
const entry = measure.entries[i];
|
|
4793
|
-
if (entry.type === "note" && !entry.chord && !entry.rest) {
|
|
4794
|
-
if (noteCount === noteIndex) {
|
|
4795
|
-
targetEntryIndex = i;
|
|
4796
|
-
break;
|
|
4797
|
-
}
|
|
4798
|
-
noteCount++;
|
|
4799
|
-
}
|
|
4800
|
-
}
|
|
4801
|
-
if (targetEntryIndex < 0) {
|
|
4802
|
-
return failure([operationError("NOTE_NOT_FOUND", `Note at index ${noteIndex} not found`, { partIndex, measureIndex })]);
|
|
4803
|
-
}
|
|
4804
|
-
const targetEntry = measure.entries[targetEntryIndex];
|
|
4805
|
-
if (targetEntry.type !== "note" || !targetEntry.notations) {
|
|
4806
|
-
return failure([operationError("ARTICULATION_NOT_FOUND", `No notations found on note`, { partIndex, measureIndex })]);
|
|
4807
|
-
}
|
|
4808
|
-
const caesuraIndex = targetEntry.notations.findIndex(
|
|
4809
|
-
(n) => n.type === "articulation" && n.articulation === "caesura"
|
|
4810
|
-
);
|
|
4811
|
-
if (caesuraIndex < 0) {
|
|
4812
|
-
return failure([operationError("ARTICULATION_NOT_FOUND", `No caesura found on note`, { partIndex, measureIndex })]);
|
|
4813
|
-
}
|
|
4814
|
-
const result = cloneScore(score);
|
|
4815
|
-
const resultNote = result.parts[partIndex].measures[measureIndex].entries[targetEntryIndex];
|
|
4816
|
-
resultNote.notations.splice(caesuraIndex, 1);
|
|
4817
|
-
if (resultNote.notations.length === 0) {
|
|
4818
|
-
delete resultNote.notations;
|
|
4819
|
-
}
|
|
4820
|
-
return success(result);
|
|
4821
|
-
}
|
|
4822
|
-
var addText = addTextDirection;
|
|
4823
|
-
var setBeaming = autoBeam;
|
|
4824
|
-
var addChordSymbol = addHarmony;
|
|
4825
|
-
var removeChordSymbol = removeHarmony;
|
|
4826
|
-
var updateChordSymbol = updateHarmony;
|
|
4827
|
-
var changeClef = insertClefChange;
|
|
4828
|
-
var setBarline = changeBarline;
|
|
4829
|
-
var addRepeat = addRepeatBarline;
|
|
4830
|
-
var removeRepeat = removeRepeatBarline;
|
|
1
|
+
import {
|
|
2
|
+
addArticulation,
|
|
3
|
+
addBeam,
|
|
4
|
+
addBowing,
|
|
5
|
+
addBreathMark,
|
|
6
|
+
addCaesura,
|
|
7
|
+
addChord,
|
|
8
|
+
addChordNote,
|
|
9
|
+
addChordNoteChecked,
|
|
10
|
+
addChordSymbol,
|
|
11
|
+
addCoda,
|
|
12
|
+
addDaCapo,
|
|
13
|
+
addDalSegno,
|
|
14
|
+
addDynamics,
|
|
15
|
+
addEnding,
|
|
16
|
+
addFermata,
|
|
17
|
+
addFine,
|
|
18
|
+
addFingering,
|
|
19
|
+
addGraceNote,
|
|
20
|
+
addHarmony,
|
|
21
|
+
addLyric,
|
|
22
|
+
addNote,
|
|
23
|
+
addNoteChecked,
|
|
24
|
+
addOctaveShift,
|
|
25
|
+
addOrnament,
|
|
26
|
+
addPart,
|
|
27
|
+
addPedal,
|
|
28
|
+
addRehearsalMark,
|
|
29
|
+
addRepeat,
|
|
30
|
+
addRepeatBarline,
|
|
31
|
+
addSegno,
|
|
32
|
+
addSlur,
|
|
33
|
+
addStringNumber,
|
|
34
|
+
addTempo,
|
|
35
|
+
addText,
|
|
36
|
+
addTextDirection,
|
|
37
|
+
addTie,
|
|
38
|
+
addToCoda,
|
|
39
|
+
addVoice,
|
|
40
|
+
addWedge,
|
|
41
|
+
autoBeam,
|
|
42
|
+
changeBarline,
|
|
43
|
+
changeClef,
|
|
44
|
+
changeKey,
|
|
45
|
+
changeNoteDuration,
|
|
46
|
+
changeTime,
|
|
47
|
+
convertToGrace,
|
|
48
|
+
copyNotes,
|
|
49
|
+
copyNotesMultiMeasure,
|
|
50
|
+
createTuplet,
|
|
51
|
+
cutNotes,
|
|
52
|
+
deleteMeasure,
|
|
53
|
+
deleteNote,
|
|
54
|
+
deleteNoteChecked,
|
|
55
|
+
duplicatePart,
|
|
56
|
+
insertClefChange,
|
|
57
|
+
insertMeasure,
|
|
58
|
+
insertNote,
|
|
59
|
+
lowerAccidental,
|
|
60
|
+
modifyDynamics,
|
|
61
|
+
modifyNoteDuration,
|
|
62
|
+
modifyNoteDurationChecked,
|
|
63
|
+
modifyNotePitch,
|
|
64
|
+
modifyNotePitchChecked,
|
|
65
|
+
modifyTempo,
|
|
66
|
+
moveNoteToStaff,
|
|
67
|
+
pasteNotes,
|
|
68
|
+
pasteNotesMultiMeasure,
|
|
69
|
+
raiseAccidental,
|
|
70
|
+
removeArticulation,
|
|
71
|
+
removeBeam,
|
|
72
|
+
removeBowing,
|
|
73
|
+
removeBreathMark,
|
|
74
|
+
removeCaesura,
|
|
75
|
+
removeChordSymbol,
|
|
76
|
+
removeDynamics,
|
|
77
|
+
removeEnding,
|
|
78
|
+
removeFermata,
|
|
79
|
+
removeFingering,
|
|
80
|
+
removeGraceNote,
|
|
81
|
+
removeHarmony,
|
|
82
|
+
removeLyric,
|
|
83
|
+
removeNote,
|
|
84
|
+
removeOctaveShift,
|
|
85
|
+
removeOrnament,
|
|
86
|
+
removePart,
|
|
87
|
+
removePedal,
|
|
88
|
+
removeRepeat,
|
|
89
|
+
removeRepeatBarline,
|
|
90
|
+
removeSlur,
|
|
91
|
+
removeStringNumber,
|
|
92
|
+
removeTempo,
|
|
93
|
+
removeTie,
|
|
94
|
+
removeTuplet,
|
|
95
|
+
removeWedge,
|
|
96
|
+
setBarline,
|
|
97
|
+
setBeaming,
|
|
98
|
+
setNotePitch,
|
|
99
|
+
setNotePitchBySemitone,
|
|
100
|
+
setStaves,
|
|
101
|
+
shiftNotePitch,
|
|
102
|
+
stopOctaveShift,
|
|
103
|
+
transpose,
|
|
104
|
+
transposeChecked,
|
|
105
|
+
updateChordSymbol,
|
|
106
|
+
updateHarmony,
|
|
107
|
+
updateLyric
|
|
108
|
+
} from "../chunk-EFP2DAOK.mjs";
|
|
109
|
+
import "../chunk-ZDAN74FN.mjs";
|
|
4831
110
|
export {
|
|
4832
111
|
addArticulation,
|
|
4833
112
|
addBeam,
|