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