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