musicxml-io 0.1.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.
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/query/index.ts
21
+ var query_exports = {};
22
+ __export(query_exports, {
23
+ countNotes: () => countNotes,
24
+ findNotes: () => findNotes,
25
+ getAttributesAtMeasure: () => getAttributesAtMeasure,
26
+ getDivisions: () => getDivisions,
27
+ getDuration: () => getDuration,
28
+ getMeasure: () => getMeasure,
29
+ getMeasureByIndex: () => getMeasureByIndex,
30
+ getMeasureCount: () => getMeasureCount,
31
+ getPartById: () => getPartById,
32
+ getPartIndex: () => getPartIndex,
33
+ getStaveCount: () => getStaveCount,
34
+ hasMultipleStaves: () => hasMultipleStaves,
35
+ measureRoundtrip: () => measureRoundtrip,
36
+ scoresEqual: () => scoresEqual
37
+ });
38
+ module.exports = __toCommonJS(query_exports);
39
+
40
+ // src/utils/index.ts
41
+ var STEP_SEMITONES = {
42
+ "C": 0,
43
+ "D": 2,
44
+ "E": 4,
45
+ "F": 5,
46
+ "G": 7,
47
+ "A": 9,
48
+ "B": 11
49
+ };
50
+ function pitchToSemitone(pitch) {
51
+ return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);
52
+ }
53
+
54
+ // src/query/index.ts
55
+ function getMeasure(score, options) {
56
+ const part = score.parts[options.part];
57
+ if (!part) return void 0;
58
+ const targetMeasure = String(options.measure);
59
+ return part.measures.find((m) => m.number === targetMeasure);
60
+ }
61
+ function getMeasureByIndex(score, options) {
62
+ const part = score.parts[options.part];
63
+ if (!part) return void 0;
64
+ return part.measures[options.measureIndex];
65
+ }
66
+ function getMeasureCount(score) {
67
+ if (score.parts.length === 0) return 0;
68
+ return score.parts[0].measures.length;
69
+ }
70
+ function getDivisions(score, options) {
71
+ const part = score.parts[options.part];
72
+ if (!part) return 1;
73
+ const targetMeasure = parseInt(String(options.measure), 10);
74
+ if (isNaN(targetMeasure)) return 1;
75
+ for (let i = 0; i < part.measures.length; i++) {
76
+ const m = part.measures[i];
77
+ const mNum = parseInt(m.number, 10);
78
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
79
+ if (m.attributes?.divisions !== void 0) {
80
+ }
81
+ }
82
+ let divisions = 1;
83
+ for (const m of part.measures) {
84
+ const mNum = parseInt(m.number, 10);
85
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
86
+ if (m.attributes?.divisions !== void 0) {
87
+ divisions = m.attributes.divisions;
88
+ }
89
+ }
90
+ return divisions;
91
+ }
92
+ function getAttributesAtMeasure(score, options) {
93
+ const part = score.parts[options.part];
94
+ if (!part) return {};
95
+ const targetMeasure = parseInt(String(options.measure), 10);
96
+ const result = {};
97
+ for (const m of part.measures) {
98
+ const mNum = parseInt(m.number, 10);
99
+ if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
100
+ if (m.attributes) {
101
+ if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
102
+ if (m.attributes.time !== void 0) result.time = m.attributes.time;
103
+ if (m.attributes.key !== void 0) result.key = m.attributes.key;
104
+ if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
105
+ if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
106
+ if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
107
+ }
108
+ }
109
+ return result;
110
+ }
111
+ function findNotes(score, filter) {
112
+ const results = [];
113
+ for (const part of score.parts) {
114
+ for (const measure of part.measures) {
115
+ for (const entry of measure.entries) {
116
+ if (entry.type !== "note") continue;
117
+ if (filter.pitchRange && entry.pitch) {
118
+ const noteValue = pitchToSemitone(entry.pitch);
119
+ if (filter.pitchRange.min) {
120
+ const minValue = pitchToSemitone(filter.pitchRange.min);
121
+ if (noteValue < minValue) continue;
122
+ }
123
+ if (filter.pitchRange.max) {
124
+ const maxValue = pitchToSemitone(filter.pitchRange.max);
125
+ if (noteValue > maxValue) continue;
126
+ }
127
+ }
128
+ if (filter.voice !== void 0 && entry.voice !== filter.voice) continue;
129
+ if (filter.staff !== void 0 && (entry.staff ?? 1) !== filter.staff) continue;
130
+ if (filter.noteType !== void 0 && entry.noteType !== filter.noteType) continue;
131
+ if (filter.hasTie !== void 0) {
132
+ const hasTie = entry.tie !== void 0;
133
+ if (filter.hasTie !== hasTie) continue;
134
+ }
135
+ results.push(entry);
136
+ }
137
+ }
138
+ }
139
+ return results;
140
+ }
141
+ function getDuration(score) {
142
+ if (score.parts.length === 0) return 0;
143
+ const part = score.parts[0];
144
+ let totalDuration = 0;
145
+ let divisions = 1;
146
+ for (const measure of part.measures) {
147
+ if (measure.attributes?.divisions !== void 0) {
148
+ divisions = measure.attributes.divisions;
149
+ }
150
+ let measureDuration = 0;
151
+ for (const entry of measure.entries) {
152
+ if (entry.type === "note" && !entry.chord) {
153
+ measureDuration = Math.max(measureDuration, entry.duration);
154
+ }
155
+ }
156
+ const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });
157
+ if (attrs.time) {
158
+ const beats = parseInt(attrs.time.beats, 10) || 4;
159
+ const expectedDuration = beats / attrs.time.beatType * 4 * divisions;
160
+ measureDuration = Math.max(measureDuration, expectedDuration);
161
+ }
162
+ totalDuration += measureDuration;
163
+ }
164
+ return totalDuration;
165
+ }
166
+ function getPartById(score, id) {
167
+ return score.parts.find((p) => p.id === id);
168
+ }
169
+ function getPartIndex(score, id) {
170
+ return score.parts.findIndex((p) => p.id === id);
171
+ }
172
+ function hasMultipleStaves(score, partIndex = 0) {
173
+ const part = score.parts[partIndex];
174
+ if (!part) return false;
175
+ for (const measure of part.measures) {
176
+ if (measure.attributes?.staves !== void 0 && measure.attributes.staves > 1) {
177
+ return true;
178
+ }
179
+ }
180
+ return false;
181
+ }
182
+ function getStaveCount(score, partIndex = 0) {
183
+ const part = score.parts[partIndex];
184
+ if (!part) return 1;
185
+ for (const measure of part.measures) {
186
+ if (measure.attributes?.staves !== void 0) {
187
+ return measure.attributes.staves;
188
+ }
189
+ }
190
+ return 1;
191
+ }
192
+ function measureRoundtrip(original, exported) {
193
+ const notesOriginal = countNotes(original);
194
+ const notesExported = countNotes(exported);
195
+ const notesPreserved = Math.min(notesOriginal, notesExported);
196
+ const measuresOriginal = getMeasureCount(original);
197
+ const measuresExported = getMeasureCount(exported);
198
+ const measuresPreserved = Math.min(measuresOriginal, measuresExported);
199
+ const partsOriginal = original.parts.length;
200
+ const partsExported = exported.parts.length;
201
+ const partsPreserved = Math.min(partsOriginal, partsExported);
202
+ const preservationRate = notesOriginal > 0 ? notesPreserved / notesOriginal : 1;
203
+ return {
204
+ notesOriginal,
205
+ notesPreserved,
206
+ measuresOriginal,
207
+ measuresPreserved,
208
+ partsOriginal,
209
+ partsPreserved,
210
+ preservationRate
211
+ };
212
+ }
213
+ function countNotes(score) {
214
+ let count = 0;
215
+ for (const part of score.parts) {
216
+ for (const measure of part.measures) {
217
+ for (const entry of measure.entries) {
218
+ if (entry.type === "note") {
219
+ count++;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ return count;
225
+ }
226
+ function scoresEqual(a, b) {
227
+ if (a.parts.length !== b.parts.length) return false;
228
+ for (let i = 0; i < a.parts.length; i++) {
229
+ const partA = a.parts[i];
230
+ const partB = b.parts[i];
231
+ if (partA.measures.length !== partB.measures.length) return false;
232
+ for (let j = 0; j < partA.measures.length; j++) {
233
+ const measureA = partA.measures[j];
234
+ const measureB = partB.measures[j];
235
+ if (measureA.number !== measureB.number) return false;
236
+ const notesA = measureA.entries.filter((e) => e.type === "note");
237
+ const notesB = measureB.entries.filter((e) => e.type === "note");
238
+ if (notesA.length !== notesB.length) return false;
239
+ for (let k = 0; k < notesA.length; k++) {
240
+ const noteA = notesA[k];
241
+ const noteB = notesB[k];
242
+ if (!pitchesEqual(noteA.pitch, noteB.pitch)) return false;
243
+ if (noteA.duration !== noteB.duration) return false;
244
+ if (noteA.voice !== noteB.voice) return false;
245
+ }
246
+ }
247
+ }
248
+ return true;
249
+ }
250
+ function pitchesEqual(a, b) {
251
+ if (a === void 0 && b === void 0) return true;
252
+ if (a === void 0 || b === void 0) return false;
253
+ return a.step === b.step && a.octave === b.octave && (a.alter ?? 0) === (b.alter ?? 0);
254
+ }
255
+ // Annotate the CommonJS export names for ESM import in node:
256
+ 0 && (module.exports = {
257
+ countNotes,
258
+ findNotes,
259
+ getAttributesAtMeasure,
260
+ getDivisions,
261
+ getDuration,
262
+ getMeasure,
263
+ getMeasureByIndex,
264
+ getMeasureCount,
265
+ getPartById,
266
+ getPartIndex,
267
+ getStaveCount,
268
+ hasMultipleStaves,
269
+ measureRoundtrip,
270
+ scoresEqual
271
+ });
272
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/query/index.ts","../../src/utils/index.ts"],"sourcesContent":["import type {\n Score,\n Part,\n Measure,\n MeasureAttributes,\n NoteEntry,\n Pitch,\n} from '../types';\nimport { pitchToSemitone } from '../utils';\n\n/**\n * Get a specific measure from the score\n */\nexport function getMeasure(score: Score, options: { part: number; measure: string | number }): Measure | undefined {\n const part = score.parts[options.part];\n if (!part) return undefined;\n const targetMeasure = String(options.measure);\n\n return part.measures.find((m) => m.number === targetMeasure);\n}\n\n/**\n * Get measure by index\n */\nexport function getMeasureByIndex(score: Score, options: { part: number; measureIndex: number }): Measure | undefined {\n const part = score.parts[options.part];\n if (!part) return undefined;\n\n return part.measures[options.measureIndex];\n}\n\n/**\n * Get the total number of measures in a score\n */\nexport function getMeasureCount(score: Score): number {\n if (score.parts.length === 0) return 0;\n return score.parts[0].measures.length;\n}\n\n/**\n * Get the divisions value at a specific measure\n * Searches backwards from the specified measure to find the most recent divisions\n */\nexport function getDivisions(score: Score, options: { part: number; measure: string | number }): number {\n const part = score.parts[options.part];\n if (!part) return 1;\n const targetMeasure = parseInt(String(options.measure), 10);\n if (isNaN(targetMeasure)) return 1;\n\n for (let i = 0; i < part.measures.length; i++) {\n const m = part.measures[i];\n const mNum = parseInt(m.number, 10);\n if (!isNaN(mNum) && mNum > targetMeasure) break;\n\n if (m.attributes?.divisions !== undefined) {\n // Continue searching for a more recent value\n }\n }\n\n // Search from the beginning up to the specified measure\n let divisions = 1;\n for (const m of part.measures) {\n const mNum = parseInt(m.number, 10);\n if (!isNaN(mNum) && mNum > targetMeasure) break;\n if (m.attributes?.divisions !== undefined) {\n divisions = m.attributes.divisions;\n }\n }\n\n return divisions;\n}\n\n/**\n * Get the current attributes at a specific measure\n * Merges all attribute changes from measure 1 to the specified measure\n */\nexport function getAttributesAtMeasure(score: Score, options: { part: number; measure: string | number }): MeasureAttributes {\n const part = score.parts[options.part];\n if (!part) return {};\n const targetMeasure = parseInt(String(options.measure), 10);\n\n const result: MeasureAttributes = {};\n\n for (const m of part.measures) {\n const mNum = parseInt(m.number, 10);\n if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;\n\n if (m.attributes) {\n if (m.attributes.divisions !== undefined) result.divisions = m.attributes.divisions;\n if (m.attributes.time !== undefined) result.time = m.attributes.time;\n if (m.attributes.key !== undefined) result.key = m.attributes.key;\n if (m.attributes.clef !== undefined) result.clef = m.attributes.clef;\n if (m.attributes.staves !== undefined) result.staves = m.attributes.staves;\n if (m.attributes.transpose !== undefined) result.transpose = m.attributes.transpose;\n }\n }\n\n return result;\n}\n\n/**\n * Pitch range filter\n */\nexport interface PitchRange {\n min?: Pitch;\n max?: Pitch;\n}\n\n/**\n * Find notes filter\n */\nexport interface FindNotesFilter {\n pitchRange?: PitchRange;\n voice?: number;\n staff?: number;\n noteType?: string;\n hasTie?: boolean;\n}\n\n/**\n * Find notes matching specific criteria\n */\nexport function findNotes(score: Score, filter: FindNotesFilter): NoteEntry[] {\n const results: NoteEntry[] = [];\n\n for (const part of score.parts) {\n for (const measure of part.measures) {\n for (const entry of measure.entries) {\n if (entry.type !== 'note') continue;\n\n // Filter by pitch range\n if (filter.pitchRange && entry.pitch) {\n const noteValue = pitchToSemitone(entry.pitch);\n\n if (filter.pitchRange.min) {\n const minValue = pitchToSemitone(filter.pitchRange.min);\n if (noteValue < minValue) continue;\n }\n\n if (filter.pitchRange.max) {\n const maxValue = pitchToSemitone(filter.pitchRange.max);\n if (noteValue > maxValue) continue;\n }\n }\n\n // Filter by voice\n if (filter.voice !== undefined && entry.voice !== filter.voice) continue;\n\n // Filter by staff\n if (filter.staff !== undefined && (entry.staff ?? 1) !== filter.staff) continue;\n\n // Filter by note type\n if (filter.noteType !== undefined && entry.noteType !== filter.noteType) continue;\n\n // Filter by tie\n if (filter.hasTie !== undefined) {\n const hasTie = entry.tie !== undefined;\n if (filter.hasTie !== hasTie) continue;\n }\n\n results.push(entry);\n }\n }\n }\n\n return results;\n}\n\n/**\n * Get the total duration of the score in divisions\n */\nexport function getDuration(score: Score): number {\n if (score.parts.length === 0) return 0;\n\n const part = score.parts[0];\n let totalDuration = 0;\n let divisions = 1;\n\n for (const measure of part.measures) {\n if (measure.attributes?.divisions !== undefined) {\n divisions = measure.attributes.divisions;\n }\n\n // Calculate measure duration based on time signature or actual notes\n let measureDuration = 0;\n\n for (const entry of measure.entries) {\n if (entry.type === 'note' && !entry.chord) {\n measureDuration = Math.max(measureDuration, entry.duration);\n }\n }\n\n // Use time signature to calculate expected duration if available\n const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });\n if (attrs.time) {\n const beats = parseInt(attrs.time.beats, 10) || 4;\n const expectedDuration = (beats / attrs.time.beatType) * 4 * divisions;\n measureDuration = Math.max(measureDuration, expectedDuration);\n }\n\n totalDuration += measureDuration;\n }\n\n return totalDuration;\n}\n\n/**\n * Get part by ID\n */\nexport function getPartById(score: Score, id: string): Part | undefined {\n return score.parts.find((p) => p.id === id);\n}\n\n/**\n * Get part index by ID\n */\nexport function getPartIndex(score: Score, id: string): number {\n return score.parts.findIndex((p) => p.id === id);\n}\n\n/**\n * Check if the score has multiple staves (e.g., piano grand staff)\n */\nexport function hasMultipleStaves(score: Score, partIndex: number = 0): boolean {\n const part = score.parts[partIndex];\n if (!part) return false;\n\n for (const measure of part.measures) {\n if (measure.attributes?.staves !== undefined && measure.attributes.staves > 1) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Get the number of staves for a part\n */\nexport function getStaveCount(score: Score, partIndex: number = 0): number {\n const part = score.parts[partIndex];\n if (!part) return 1;\n\n for (const measure of part.measures) {\n if (measure.attributes?.staves !== undefined) {\n return measure.attributes.staves;\n }\n }\n\n return 1;\n}\n\n/**\n * Round-trip metrics for measuring preservation fidelity\n */\nexport interface RoundtripMetrics {\n notesOriginal: number;\n notesPreserved: number;\n measuresOriginal: number;\n measuresPreserved: number;\n partsOriginal: number;\n partsPreserved: number;\n preservationRate: number;\n}\n\n/**\n * Compare two scores and calculate round-trip preservation metrics\n */\nexport function measureRoundtrip(original: Score, exported: Score): RoundtripMetrics {\n const notesOriginal = countNotes(original);\n const notesExported = countNotes(exported);\n const notesPreserved = Math.min(notesOriginal, notesExported);\n\n const measuresOriginal = getMeasureCount(original);\n const measuresExported = getMeasureCount(exported);\n const measuresPreserved = Math.min(measuresOriginal, measuresExported);\n\n const partsOriginal = original.parts.length;\n const partsExported = exported.parts.length;\n const partsPreserved = Math.min(partsOriginal, partsExported);\n\n // Calculate preservation rate based on notes (most granular)\n const preservationRate = notesOriginal > 0\n ? notesPreserved / notesOriginal\n : 1;\n\n return {\n notesOriginal,\n notesPreserved,\n measuresOriginal,\n measuresPreserved,\n partsOriginal,\n partsPreserved,\n preservationRate,\n };\n}\n\n/**\n * Count total notes in a score\n */\nexport function countNotes(score: Score): number {\n let count = 0;\n\n for (const part of score.parts) {\n for (const measure of part.measures) {\n for (const entry of measure.entries) {\n if (entry.type === 'note') {\n count++;\n }\n }\n }\n }\n\n return count;\n}\n\n/**\n * Compare two scores for structural equality\n */\nexport function scoresEqual(a: Score, b: Score): boolean {\n // Check parts count\n if (a.parts.length !== b.parts.length) return false;\n\n for (let i = 0; i < a.parts.length; i++) {\n const partA = a.parts[i];\n const partB = b.parts[i];\n\n if (partA.measures.length !== partB.measures.length) return false;\n\n for (let j = 0; j < partA.measures.length; j++) {\n const measureA = partA.measures[j];\n const measureB = partB.measures[j];\n\n if (measureA.number !== measureB.number) return false;\n\n // Compare entries count\n const notesA = measureA.entries.filter(e => e.type === 'note');\n const notesB = measureB.entries.filter(e => e.type === 'note');\n\n if (notesA.length !== notesB.length) return false;\n\n // Compare each note\n for (let k = 0; k < notesA.length; k++) {\n const noteA = notesA[k] as NoteEntry;\n const noteB = notesB[k] as NoteEntry;\n\n if (!pitchesEqual(noteA.pitch, noteB.pitch)) return false;\n if (noteA.duration !== noteB.duration) return false;\n if (noteA.voice !== noteB.voice) return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Compare two pitches for equality\n */\nfunction pitchesEqual(a: Pitch | undefined, b: Pitch | undefined): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n return a.step === b.step && a.octave === b.octave && (a.alter ?? 0) === (b.alter ?? 0);\n}\n","import type { Pitch, Measure, MeasureEntry, NoteEntry } from '../types';\n\n// Pitch constants\nexport const STEPS: Pitch['step'][] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];\nexport const STEP_SEMITONES: Record<Pitch['step'], number> = {\n 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11,\n};\n\n/** Convert pitch to semitone value (MIDI-like) */\nexport function pitchToSemitone(pitch: Pitch): number {\n return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);\n}\n\n// Position tracking for measure iteration\nexport interface PositionState {\n position: number;\n lastNonChordPosition: number;\n}\n\nexport function createPositionState(): PositionState {\n return { position: 0, lastNonChordPosition: 0 };\n}\n\n/** Update position state for entry, returns position before update */\nexport function updatePositionForEntry(state: PositionState, entry: MeasureEntry): number {\n const pos = state.position;\n switch (entry.type) {\n case 'note': {\n const note = entry as NoteEntry;\n if (!note.chord) {\n state.lastNonChordPosition = state.position;\n state.position += note.duration;\n }\n return note.chord ? state.lastNonChordPosition : pos;\n }\n case 'backup':\n state.position -= entry.duration;\n state.lastNonChordPosition = state.position;\n return pos;\n case 'forward':\n state.position += entry.duration;\n state.lastNonChordPosition = state.position;\n return pos;\n default:\n return pos;\n }\n}\n\n/** Get absolute position of a note within a measure */\nexport function getAbsolutePositionForNote(note: NoteEntry, measure: Measure): number {\n const state = createPositionState();\n for (const entry of measure.entries) {\n if (entry === note) return entry.chord ? state.lastNonChordPosition : state.position;\n updatePositionForEntry(state, entry);\n }\n return state.position;\n}\n\n/** Get position at end of measure */\nexport function getMeasureEndPosition(measure: Measure): number {\n const state = createPositionState();\n for (const entry of measure.entries) updatePositionForEntry(state, entry);\n return state.position;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,232 @@
1
+ // src/utils/index.ts
2
+ var STEP_SEMITONES = {
3
+ "C": 0,
4
+ "D": 2,
5
+ "E": 4,
6
+ "F": 5,
7
+ "G": 7,
8
+ "A": 9,
9
+ "B": 11
10
+ };
11
+ function pitchToSemitone(pitch) {
12
+ return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);
13
+ }
14
+
15
+ // src/query/index.ts
16
+ function getMeasure(score, options) {
17
+ const part = score.parts[options.part];
18
+ if (!part) return void 0;
19
+ const targetMeasure = String(options.measure);
20
+ return part.measures.find((m) => m.number === targetMeasure);
21
+ }
22
+ function getMeasureByIndex(score, options) {
23
+ const part = score.parts[options.part];
24
+ if (!part) return void 0;
25
+ return part.measures[options.measureIndex];
26
+ }
27
+ function getMeasureCount(score) {
28
+ if (score.parts.length === 0) return 0;
29
+ return score.parts[0].measures.length;
30
+ }
31
+ function getDivisions(score, options) {
32
+ const part = score.parts[options.part];
33
+ if (!part) return 1;
34
+ const targetMeasure = parseInt(String(options.measure), 10);
35
+ if (isNaN(targetMeasure)) return 1;
36
+ for (let i = 0; i < part.measures.length; i++) {
37
+ const m = part.measures[i];
38
+ const mNum = parseInt(m.number, 10);
39
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
40
+ if (m.attributes?.divisions !== void 0) {
41
+ }
42
+ }
43
+ let divisions = 1;
44
+ for (const m of part.measures) {
45
+ const mNum = parseInt(m.number, 10);
46
+ if (!isNaN(mNum) && mNum > targetMeasure) break;
47
+ if (m.attributes?.divisions !== void 0) {
48
+ divisions = m.attributes.divisions;
49
+ }
50
+ }
51
+ return divisions;
52
+ }
53
+ function getAttributesAtMeasure(score, options) {
54
+ const part = score.parts[options.part];
55
+ if (!part) return {};
56
+ const targetMeasure = parseInt(String(options.measure), 10);
57
+ const result = {};
58
+ for (const m of part.measures) {
59
+ const mNum = parseInt(m.number, 10);
60
+ if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;
61
+ if (m.attributes) {
62
+ if (m.attributes.divisions !== void 0) result.divisions = m.attributes.divisions;
63
+ if (m.attributes.time !== void 0) result.time = m.attributes.time;
64
+ if (m.attributes.key !== void 0) result.key = m.attributes.key;
65
+ if (m.attributes.clef !== void 0) result.clef = m.attributes.clef;
66
+ if (m.attributes.staves !== void 0) result.staves = m.attributes.staves;
67
+ if (m.attributes.transpose !== void 0) result.transpose = m.attributes.transpose;
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ function findNotes(score, filter) {
73
+ const results = [];
74
+ for (const part of score.parts) {
75
+ for (const measure of part.measures) {
76
+ for (const entry of measure.entries) {
77
+ if (entry.type !== "note") continue;
78
+ if (filter.pitchRange && entry.pitch) {
79
+ const noteValue = pitchToSemitone(entry.pitch);
80
+ if (filter.pitchRange.min) {
81
+ const minValue = pitchToSemitone(filter.pitchRange.min);
82
+ if (noteValue < minValue) continue;
83
+ }
84
+ if (filter.pitchRange.max) {
85
+ const maxValue = pitchToSemitone(filter.pitchRange.max);
86
+ if (noteValue > maxValue) continue;
87
+ }
88
+ }
89
+ if (filter.voice !== void 0 && entry.voice !== filter.voice) continue;
90
+ if (filter.staff !== void 0 && (entry.staff ?? 1) !== filter.staff) continue;
91
+ if (filter.noteType !== void 0 && entry.noteType !== filter.noteType) continue;
92
+ if (filter.hasTie !== void 0) {
93
+ const hasTie = entry.tie !== void 0;
94
+ if (filter.hasTie !== hasTie) continue;
95
+ }
96
+ results.push(entry);
97
+ }
98
+ }
99
+ }
100
+ return results;
101
+ }
102
+ function getDuration(score) {
103
+ if (score.parts.length === 0) return 0;
104
+ const part = score.parts[0];
105
+ let totalDuration = 0;
106
+ let divisions = 1;
107
+ for (const measure of part.measures) {
108
+ if (measure.attributes?.divisions !== void 0) {
109
+ divisions = measure.attributes.divisions;
110
+ }
111
+ let measureDuration = 0;
112
+ for (const entry of measure.entries) {
113
+ if (entry.type === "note" && !entry.chord) {
114
+ measureDuration = Math.max(measureDuration, entry.duration);
115
+ }
116
+ }
117
+ const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });
118
+ if (attrs.time) {
119
+ const beats = parseInt(attrs.time.beats, 10) || 4;
120
+ const expectedDuration = beats / attrs.time.beatType * 4 * divisions;
121
+ measureDuration = Math.max(measureDuration, expectedDuration);
122
+ }
123
+ totalDuration += measureDuration;
124
+ }
125
+ return totalDuration;
126
+ }
127
+ function getPartById(score, id) {
128
+ return score.parts.find((p) => p.id === id);
129
+ }
130
+ function getPartIndex(score, id) {
131
+ return score.parts.findIndex((p) => p.id === id);
132
+ }
133
+ function hasMultipleStaves(score, partIndex = 0) {
134
+ const part = score.parts[partIndex];
135
+ if (!part) return false;
136
+ for (const measure of part.measures) {
137
+ if (measure.attributes?.staves !== void 0 && measure.attributes.staves > 1) {
138
+ return true;
139
+ }
140
+ }
141
+ return false;
142
+ }
143
+ function getStaveCount(score, partIndex = 0) {
144
+ const part = score.parts[partIndex];
145
+ if (!part) return 1;
146
+ for (const measure of part.measures) {
147
+ if (measure.attributes?.staves !== void 0) {
148
+ return measure.attributes.staves;
149
+ }
150
+ }
151
+ return 1;
152
+ }
153
+ function measureRoundtrip(original, exported) {
154
+ const notesOriginal = countNotes(original);
155
+ const notesExported = countNotes(exported);
156
+ const notesPreserved = Math.min(notesOriginal, notesExported);
157
+ const measuresOriginal = getMeasureCount(original);
158
+ const measuresExported = getMeasureCount(exported);
159
+ const measuresPreserved = Math.min(measuresOriginal, measuresExported);
160
+ const partsOriginal = original.parts.length;
161
+ const partsExported = exported.parts.length;
162
+ const partsPreserved = Math.min(partsOriginal, partsExported);
163
+ const preservationRate = notesOriginal > 0 ? notesPreserved / notesOriginal : 1;
164
+ return {
165
+ notesOriginal,
166
+ notesPreserved,
167
+ measuresOriginal,
168
+ measuresPreserved,
169
+ partsOriginal,
170
+ partsPreserved,
171
+ preservationRate
172
+ };
173
+ }
174
+ function countNotes(score) {
175
+ let count = 0;
176
+ for (const part of score.parts) {
177
+ for (const measure of part.measures) {
178
+ for (const entry of measure.entries) {
179
+ if (entry.type === "note") {
180
+ count++;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return count;
186
+ }
187
+ function scoresEqual(a, b) {
188
+ if (a.parts.length !== b.parts.length) return false;
189
+ for (let i = 0; i < a.parts.length; i++) {
190
+ const partA = a.parts[i];
191
+ const partB = b.parts[i];
192
+ if (partA.measures.length !== partB.measures.length) return false;
193
+ for (let j = 0; j < partA.measures.length; j++) {
194
+ const measureA = partA.measures[j];
195
+ const measureB = partB.measures[j];
196
+ if (measureA.number !== measureB.number) return false;
197
+ const notesA = measureA.entries.filter((e) => e.type === "note");
198
+ const notesB = measureB.entries.filter((e) => e.type === "note");
199
+ if (notesA.length !== notesB.length) return false;
200
+ for (let k = 0; k < notesA.length; k++) {
201
+ const noteA = notesA[k];
202
+ const noteB = notesB[k];
203
+ if (!pitchesEqual(noteA.pitch, noteB.pitch)) return false;
204
+ if (noteA.duration !== noteB.duration) return false;
205
+ if (noteA.voice !== noteB.voice) return false;
206
+ }
207
+ }
208
+ }
209
+ return true;
210
+ }
211
+ function pitchesEqual(a, b) {
212
+ if (a === void 0 && b === void 0) return true;
213
+ if (a === void 0 || b === void 0) return false;
214
+ return a.step === b.step && a.octave === b.octave && (a.alter ?? 0) === (b.alter ?? 0);
215
+ }
216
+ export {
217
+ countNotes,
218
+ findNotes,
219
+ getAttributesAtMeasure,
220
+ getDivisions,
221
+ getDuration,
222
+ getMeasure,
223
+ getMeasureByIndex,
224
+ getMeasureCount,
225
+ getPartById,
226
+ getPartIndex,
227
+ getStaveCount,
228
+ hasMultipleStaves,
229
+ measureRoundtrip,
230
+ scoresEqual
231
+ };
232
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/index.ts","../../src/query/index.ts"],"sourcesContent":["import type { Pitch, Measure, MeasureEntry, NoteEntry } from '../types';\n\n// Pitch constants\nexport const STEPS: Pitch['step'][] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];\nexport const STEP_SEMITONES: Record<Pitch['step'], number> = {\n 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11,\n};\n\n/** Convert pitch to semitone value (MIDI-like) */\nexport function pitchToSemitone(pitch: Pitch): number {\n return pitch.octave * 12 + STEP_SEMITONES[pitch.step] + (pitch.alter ?? 0);\n}\n\n// Position tracking for measure iteration\nexport interface PositionState {\n position: number;\n lastNonChordPosition: number;\n}\n\nexport function createPositionState(): PositionState {\n return { position: 0, lastNonChordPosition: 0 };\n}\n\n/** Update position state for entry, returns position before update */\nexport function updatePositionForEntry(state: PositionState, entry: MeasureEntry): number {\n const pos = state.position;\n switch (entry.type) {\n case 'note': {\n const note = entry as NoteEntry;\n if (!note.chord) {\n state.lastNonChordPosition = state.position;\n state.position += note.duration;\n }\n return note.chord ? state.lastNonChordPosition : pos;\n }\n case 'backup':\n state.position -= entry.duration;\n state.lastNonChordPosition = state.position;\n return pos;\n case 'forward':\n state.position += entry.duration;\n state.lastNonChordPosition = state.position;\n return pos;\n default:\n return pos;\n }\n}\n\n/** Get absolute position of a note within a measure */\nexport function getAbsolutePositionForNote(note: NoteEntry, measure: Measure): number {\n const state = createPositionState();\n for (const entry of measure.entries) {\n if (entry === note) return entry.chord ? state.lastNonChordPosition : state.position;\n updatePositionForEntry(state, entry);\n }\n return state.position;\n}\n\n/** Get position at end of measure */\nexport function getMeasureEndPosition(measure: Measure): number {\n const state = createPositionState();\n for (const entry of measure.entries) updatePositionForEntry(state, entry);\n return state.position;\n}\n","import type {\n Score,\n Part,\n Measure,\n MeasureAttributes,\n NoteEntry,\n Pitch,\n} from '../types';\nimport { pitchToSemitone } from '../utils';\n\n/**\n * Get a specific measure from the score\n */\nexport function getMeasure(score: Score, options: { part: number; measure: string | number }): Measure | undefined {\n const part = score.parts[options.part];\n if (!part) return undefined;\n const targetMeasure = String(options.measure);\n\n return part.measures.find((m) => m.number === targetMeasure);\n}\n\n/**\n * Get measure by index\n */\nexport function getMeasureByIndex(score: Score, options: { part: number; measureIndex: number }): Measure | undefined {\n const part = score.parts[options.part];\n if (!part) return undefined;\n\n return part.measures[options.measureIndex];\n}\n\n/**\n * Get the total number of measures in a score\n */\nexport function getMeasureCount(score: Score): number {\n if (score.parts.length === 0) return 0;\n return score.parts[0].measures.length;\n}\n\n/**\n * Get the divisions value at a specific measure\n * Searches backwards from the specified measure to find the most recent divisions\n */\nexport function getDivisions(score: Score, options: { part: number; measure: string | number }): number {\n const part = score.parts[options.part];\n if (!part) return 1;\n const targetMeasure = parseInt(String(options.measure), 10);\n if (isNaN(targetMeasure)) return 1;\n\n for (let i = 0; i < part.measures.length; i++) {\n const m = part.measures[i];\n const mNum = parseInt(m.number, 10);\n if (!isNaN(mNum) && mNum > targetMeasure) break;\n\n if (m.attributes?.divisions !== undefined) {\n // Continue searching for a more recent value\n }\n }\n\n // Search from the beginning up to the specified measure\n let divisions = 1;\n for (const m of part.measures) {\n const mNum = parseInt(m.number, 10);\n if (!isNaN(mNum) && mNum > targetMeasure) break;\n if (m.attributes?.divisions !== undefined) {\n divisions = m.attributes.divisions;\n }\n }\n\n return divisions;\n}\n\n/**\n * Get the current attributes at a specific measure\n * Merges all attribute changes from measure 1 to the specified measure\n */\nexport function getAttributesAtMeasure(score: Score, options: { part: number; measure: string | number }): MeasureAttributes {\n const part = score.parts[options.part];\n if (!part) return {};\n const targetMeasure = parseInt(String(options.measure), 10);\n\n const result: MeasureAttributes = {};\n\n for (const m of part.measures) {\n const mNum = parseInt(m.number, 10);\n if (!isNaN(targetMeasure) && !isNaN(mNum) && mNum > targetMeasure) break;\n\n if (m.attributes) {\n if (m.attributes.divisions !== undefined) result.divisions = m.attributes.divisions;\n if (m.attributes.time !== undefined) result.time = m.attributes.time;\n if (m.attributes.key !== undefined) result.key = m.attributes.key;\n if (m.attributes.clef !== undefined) result.clef = m.attributes.clef;\n if (m.attributes.staves !== undefined) result.staves = m.attributes.staves;\n if (m.attributes.transpose !== undefined) result.transpose = m.attributes.transpose;\n }\n }\n\n return result;\n}\n\n/**\n * Pitch range filter\n */\nexport interface PitchRange {\n min?: Pitch;\n max?: Pitch;\n}\n\n/**\n * Find notes filter\n */\nexport interface FindNotesFilter {\n pitchRange?: PitchRange;\n voice?: number;\n staff?: number;\n noteType?: string;\n hasTie?: boolean;\n}\n\n/**\n * Find notes matching specific criteria\n */\nexport function findNotes(score: Score, filter: FindNotesFilter): NoteEntry[] {\n const results: NoteEntry[] = [];\n\n for (const part of score.parts) {\n for (const measure of part.measures) {\n for (const entry of measure.entries) {\n if (entry.type !== 'note') continue;\n\n // Filter by pitch range\n if (filter.pitchRange && entry.pitch) {\n const noteValue = pitchToSemitone(entry.pitch);\n\n if (filter.pitchRange.min) {\n const minValue = pitchToSemitone(filter.pitchRange.min);\n if (noteValue < minValue) continue;\n }\n\n if (filter.pitchRange.max) {\n const maxValue = pitchToSemitone(filter.pitchRange.max);\n if (noteValue > maxValue) continue;\n }\n }\n\n // Filter by voice\n if (filter.voice !== undefined && entry.voice !== filter.voice) continue;\n\n // Filter by staff\n if (filter.staff !== undefined && (entry.staff ?? 1) !== filter.staff) continue;\n\n // Filter by note type\n if (filter.noteType !== undefined && entry.noteType !== filter.noteType) continue;\n\n // Filter by tie\n if (filter.hasTie !== undefined) {\n const hasTie = entry.tie !== undefined;\n if (filter.hasTie !== hasTie) continue;\n }\n\n results.push(entry);\n }\n }\n }\n\n return results;\n}\n\n/**\n * Get the total duration of the score in divisions\n */\nexport function getDuration(score: Score): number {\n if (score.parts.length === 0) return 0;\n\n const part = score.parts[0];\n let totalDuration = 0;\n let divisions = 1;\n\n for (const measure of part.measures) {\n if (measure.attributes?.divisions !== undefined) {\n divisions = measure.attributes.divisions;\n }\n\n // Calculate measure duration based on time signature or actual notes\n let measureDuration = 0;\n\n for (const entry of measure.entries) {\n if (entry.type === 'note' && !entry.chord) {\n measureDuration = Math.max(measureDuration, entry.duration);\n }\n }\n\n // Use time signature to calculate expected duration if available\n const attrs = getAttributesAtMeasure(score, { part: 0, measure: measure.number });\n if (attrs.time) {\n const beats = parseInt(attrs.time.beats, 10) || 4;\n const expectedDuration = (beats / attrs.time.beatType) * 4 * divisions;\n measureDuration = Math.max(measureDuration, expectedDuration);\n }\n\n totalDuration += measureDuration;\n }\n\n return totalDuration;\n}\n\n/**\n * Get part by ID\n */\nexport function getPartById(score: Score, id: string): Part | undefined {\n return score.parts.find((p) => p.id === id);\n}\n\n/**\n * Get part index by ID\n */\nexport function getPartIndex(score: Score, id: string): number {\n return score.parts.findIndex((p) => p.id === id);\n}\n\n/**\n * Check if the score has multiple staves (e.g., piano grand staff)\n */\nexport function hasMultipleStaves(score: Score, partIndex: number = 0): boolean {\n const part = score.parts[partIndex];\n if (!part) return false;\n\n for (const measure of part.measures) {\n if (measure.attributes?.staves !== undefined && measure.attributes.staves > 1) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Get the number of staves for a part\n */\nexport function getStaveCount(score: Score, partIndex: number = 0): number {\n const part = score.parts[partIndex];\n if (!part) return 1;\n\n for (const measure of part.measures) {\n if (measure.attributes?.staves !== undefined) {\n return measure.attributes.staves;\n }\n }\n\n return 1;\n}\n\n/**\n * Round-trip metrics for measuring preservation fidelity\n */\nexport interface RoundtripMetrics {\n notesOriginal: number;\n notesPreserved: number;\n measuresOriginal: number;\n measuresPreserved: number;\n partsOriginal: number;\n partsPreserved: number;\n preservationRate: number;\n}\n\n/**\n * Compare two scores and calculate round-trip preservation metrics\n */\nexport function measureRoundtrip(original: Score, exported: Score): RoundtripMetrics {\n const notesOriginal = countNotes(original);\n const notesExported = countNotes(exported);\n const notesPreserved = Math.min(notesOriginal, notesExported);\n\n const measuresOriginal = getMeasureCount(original);\n const measuresExported = getMeasureCount(exported);\n const measuresPreserved = Math.min(measuresOriginal, measuresExported);\n\n const partsOriginal = original.parts.length;\n const partsExported = exported.parts.length;\n const partsPreserved = Math.min(partsOriginal, partsExported);\n\n // Calculate preservation rate based on notes (most granular)\n const preservationRate = notesOriginal > 0\n ? notesPreserved / notesOriginal\n : 1;\n\n return {\n notesOriginal,\n notesPreserved,\n measuresOriginal,\n measuresPreserved,\n partsOriginal,\n partsPreserved,\n preservationRate,\n };\n}\n\n/**\n * Count total notes in a score\n */\nexport function countNotes(score: Score): number {\n let count = 0;\n\n for (const part of score.parts) {\n for (const measure of part.measures) {\n for (const entry of measure.entries) {\n if (entry.type === 'note') {\n count++;\n }\n }\n }\n }\n\n return count;\n}\n\n/**\n * Compare two scores for structural equality\n */\nexport function scoresEqual(a: Score, b: Score): boolean {\n // Check parts count\n if (a.parts.length !== b.parts.length) return false;\n\n for (let i = 0; i < a.parts.length; i++) {\n const partA = a.parts[i];\n const partB = b.parts[i];\n\n if (partA.measures.length !== partB.measures.length) return false;\n\n for (let j = 0; j < partA.measures.length; j++) {\n const measureA = partA.measures[j];\n const measureB = partB.measures[j];\n\n if (measureA.number !== measureB.number) return false;\n\n // Compare entries count\n const notesA = measureA.entries.filter(e => e.type === 'note');\n const notesB = measureB.entries.filter(e => e.type === 'note');\n\n if (notesA.length !== notesB.length) return false;\n\n // Compare each note\n for (let k = 0; k < notesA.length; k++) {\n const noteA = notesA[k] as NoteEntry;\n const noteB = notesB[k] as NoteEntry;\n\n if (!pitchesEqual(noteA.pitch, noteB.pitch)) return false;\n if (noteA.duration !== noteB.duration) return false;\n if (noteA.voice !== noteB.voice) return false;\n }\n }\n }\n\n return true;\n}\n\n/**\n * Compare two pitches for equality\n */\nfunction pitchesEqual(a: Pitch | undefined, b: Pitch | undefined): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n return a.step === b.step && a.octave === b.octave && (a.alter ?? 0) === (b.alter ?? 0);\n}\n"],"mappings":"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","names":[]}