@sudobility/music_types 0.11.19 → 0.11.21

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.
Files changed (153) hide show
  1. package/dist/domain/commands/edit-commands.d.ts +66 -0
  2. package/dist/domain/commands/edit-commands.d.ts.map +1 -0
  3. package/dist/domain/commands/edit-commands.js +185 -0
  4. package/dist/domain/commands/edit-commands.js.map +1 -0
  5. package/dist/domain/commands/note-commands.d.ts +73 -0
  6. package/dist/domain/commands/note-commands.d.ts.map +1 -0
  7. package/dist/domain/commands/note-commands.js +292 -0
  8. package/dist/domain/commands/note-commands.js.map +1 -0
  9. package/dist/domain/commands/reflow.d.ts +69 -0
  10. package/dist/domain/commands/reflow.d.ts.map +1 -0
  11. package/dist/domain/commands/reflow.js +395 -0
  12. package/dist/domain/commands/reflow.js.map +1 -0
  13. package/dist/domain/commands/region-commands.d.ts +21 -0
  14. package/dist/domain/commands/region-commands.d.ts.map +1 -0
  15. package/dist/domain/commands/region-commands.js +47 -0
  16. package/dist/domain/commands/region-commands.js.map +1 -0
  17. package/dist/domain/commands/relocate-commands.d.ts +18 -0
  18. package/dist/domain/commands/relocate-commands.d.ts.map +1 -0
  19. package/dist/domain/commands/relocate-commands.js +108 -0
  20. package/dist/domain/commands/relocate-commands.js.map +1 -0
  21. package/dist/domain/commands/ripple-commands.d.ts +44 -0
  22. package/dist/domain/commands/ripple-commands.d.ts.map +1 -0
  23. package/dist/domain/commands/ripple-commands.js +104 -0
  24. package/dist/domain/commands/ripple-commands.js.map +1 -0
  25. package/dist/domain/commands/snapshot.d.ts +38 -0
  26. package/dist/domain/commands/snapshot.d.ts.map +1 -0
  27. package/dist/domain/commands/snapshot.js +44 -0
  28. package/dist/domain/commands/snapshot.js.map +1 -0
  29. package/dist/domain/commands/structure-commands.d.ts +38 -0
  30. package/dist/domain/commands/structure-commands.d.ts.map +1 -0
  31. package/dist/domain/commands/structure-commands.js +220 -0
  32. package/dist/domain/commands/structure-commands.js.map +1 -0
  33. package/dist/domain/commands/track-commands.d.ts +15 -0
  34. package/dist/domain/commands/track-commands.d.ts.map +1 -0
  35. package/dist/domain/commands/track-commands.js +56 -0
  36. package/dist/domain/commands/track-commands.js.map +1 -0
  37. package/dist/domain/commands/types.d.ts +31 -0
  38. package/dist/domain/commands/types.d.ts.map +1 -0
  39. package/dist/domain/commands/types.js +2 -0
  40. package/dist/domain/commands/types.js.map +1 -0
  41. package/dist/domain/instruments/gm-kit.d.ts +45 -0
  42. package/dist/domain/instruments/gm-kit.d.ts.map +1 -0
  43. package/dist/domain/instruments/gm-kit.js +59 -0
  44. package/dist/domain/instruments/gm-kit.js.map +1 -0
  45. package/dist/domain/instruments/gm-percussion.d.ts +42 -0
  46. package/dist/domain/instruments/gm-percussion.d.ts.map +1 -0
  47. package/dist/domain/instruments/gm-percussion.js +85 -0
  48. package/dist/domain/instruments/gm-percussion.js.map +1 -0
  49. package/dist/domain/instruments/gm-polyphony.d.ts +13 -0
  50. package/dist/domain/instruments/gm-polyphony.d.ts.map +1 -0
  51. package/dist/domain/instruments/gm-polyphony.js +76 -0
  52. package/dist/domain/instruments/gm-polyphony.js.map +1 -0
  53. package/dist/domain/instruments/gm-range.d.ts +15 -0
  54. package/dist/domain/instruments/gm-range.d.ts.map +1 -0
  55. package/dist/domain/instruments/gm-range.js +97 -0
  56. package/dist/domain/instruments/gm-range.js.map +1 -0
  57. package/dist/domain/instruments/gm-transposition.d.ts +11 -0
  58. package/dist/domain/instruments/gm-transposition.d.ts.map +1 -0
  59. package/dist/domain/instruments/gm-transposition.js +69 -0
  60. package/dist/domain/instruments/gm-transposition.js.map +1 -0
  61. package/dist/domain/instruments/gm.d.ts +28 -0
  62. package/dist/domain/instruments/gm.d.ts.map +1 -0
  63. package/dist/domain/instruments/gm.js +217 -0
  64. package/dist/domain/instruments/gm.js.map +1 -0
  65. package/dist/domain/pitch/pitch.d.ts +15 -0
  66. package/dist/domain/pitch/pitch.d.ts.map +1 -0
  67. package/dist/domain/pitch/pitch.js +81 -0
  68. package/dist/domain/pitch/pitch.js.map +1 -0
  69. package/dist/domain/pitch/transpose.d.ts +37 -0
  70. package/dist/domain/pitch/transpose.d.ts.map +1 -0
  71. package/dist/domain/pitch/transpose.js +65 -0
  72. package/dist/domain/pitch/transpose.js.map +1 -0
  73. package/dist/domain/quantization/options.d.ts +55 -0
  74. package/dist/domain/quantization/options.d.ts.map +1 -0
  75. package/dist/domain/quantization/options.js +2 -0
  76. package/dist/domain/quantization/options.js.map +1 -0
  77. package/dist/domain/quantization/quantize.d.ts +13 -0
  78. package/dist/domain/quantization/quantize.d.ts.map +1 -0
  79. package/dist/domain/quantization/quantize.js +199 -0
  80. package/dist/domain/quantization/quantize.js.map +1 -0
  81. package/dist/domain/score/factory.d.ts +39 -0
  82. package/dist/domain/score/factory.d.ts.map +1 -0
  83. package/dist/domain/score/factory.js +148 -0
  84. package/dist/domain/score/factory.js.map +1 -0
  85. package/dist/domain/score/fragment.d.ts +28 -0
  86. package/dist/domain/score/fragment.d.ts.map +1 -0
  87. package/dist/domain/score/fragment.js +68 -0
  88. package/dist/domain/score/fragment.js.map +1 -0
  89. package/dist/domain/score/ids.d.ts +8 -0
  90. package/dist/domain/score/ids.d.ts.map +1 -0
  91. package/dist/domain/score/ids.js +38 -0
  92. package/dist/domain/score/ids.js.map +1 -0
  93. package/dist/domain/score/queries.d.ts +34 -0
  94. package/dist/domain/score/queries.d.ts.map +1 -0
  95. package/dist/domain/score/queries.js +132 -0
  96. package/dist/domain/score/queries.js.map +1 -0
  97. package/dist/domain/score/ties.d.ts +73 -0
  98. package/dist/domain/score/ties.d.ts.map +1 -0
  99. package/dist/domain/score/ties.js +200 -0
  100. package/dist/domain/score/ties.js.map +1 -0
  101. package/dist/domain/selection/selection.d.ts +63 -0
  102. package/dist/domain/selection/selection.d.ts.map +1 -0
  103. package/dist/domain/selection/selection.js +129 -0
  104. package/dist/domain/selection/selection.js.map +1 -0
  105. package/dist/domain/selection/types.d.ts +10 -0
  106. package/dist/domain/selection/types.d.ts.map +1 -0
  107. package/dist/domain/selection/types.js +5 -0
  108. package/dist/domain/selection/types.js.map +1 -0
  109. package/dist/domain/time/durations.d.ts +21 -0
  110. package/dist/domain/time/durations.d.ts.map +1 -0
  111. package/dist/domain/time/durations.js +53 -0
  112. package/dist/domain/time/durations.js.map +1 -0
  113. package/dist/domain/time/fraction.d.ts +19 -0
  114. package/dist/domain/time/fraction.d.ts.map +1 -0
  115. package/dist/domain/time/fraction.js +60 -0
  116. package/dist/domain/time/fraction.js.map +1 -0
  117. package/dist/domain/time/tempo-map.d.ts +24 -0
  118. package/dist/domain/time/tempo-map.d.ts.map +1 -0
  119. package/dist/domain/time/tempo-map.js +85 -0
  120. package/dist/domain/time/tempo-map.js.map +1 -0
  121. package/dist/domain/time/ticks.d.ts +34 -0
  122. package/dist/domain/time/ticks.d.ts.map +1 -0
  123. package/dist/domain/time/ticks.js +93 -0
  124. package/dist/domain/time/ticks.js.map +1 -0
  125. package/dist/domain/validation/issues.d.ts +44 -0
  126. package/dist/domain/validation/issues.d.ts.map +1 -0
  127. package/dist/domain/validation/issues.js +34 -0
  128. package/dist/domain/validation/issues.js.map +1 -0
  129. package/dist/domain/validation/validator.d.ts +11 -0
  130. package/dist/domain/validation/validator.d.ts.map +1 -0
  131. package/dist/domain/validation/validator.js +375 -0
  132. package/dist/domain/validation/validator.js.map +1 -0
  133. package/dist/domain/voicing/allocate.d.ts +19 -0
  134. package/dist/domain/voicing/allocate.d.ts.map +1 -0
  135. package/dist/domain/voicing/allocate.js +100 -0
  136. package/dist/domain/voicing/allocate.js.map +1 -0
  137. package/dist/index.d.ts +46 -4
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +50 -2
  140. package/dist/index.js.map +1 -1
  141. package/dist/platform/audio.d.ts +12 -0
  142. package/dist/platform/audio.d.ts.map +1 -1
  143. package/dist/platform/playback.d.ts +85 -15
  144. package/dist/platform/playback.d.ts.map +1 -1
  145. package/dist/services/regeneration/controller.d.ts +60 -0
  146. package/dist/services/regeneration/controller.d.ts.map +1 -0
  147. package/dist/services/regeneration/controller.js +202 -0
  148. package/dist/services/regeneration/controller.js.map +1 -0
  149. package/dist/test/fixtures.d.ts +45 -0
  150. package/dist/test/fixtures.d.ts.map +1 -0
  151. package/dist/test/fixtures.js +420 -0
  152. package/dist/test/fixtures.js.map +1 -0
  153. package/package.json +2 -1
@@ -0,0 +1,375 @@
1
+ import { isNoteEvent } from '../../index.js';
2
+ import { pitchToMidi } from '../pitch/pitch.js';
3
+ import { voiceChannel } from '../score/ties.js';
4
+ import { ISSUE_CODES } from './issues.js';
5
+ const MIN_MIDI = 0;
6
+ const MAX_MIDI = 127;
7
+ const MIN_VELOCITY = 0;
8
+ const MAX_VELOCITY = 127;
9
+ const MIN_MIDI_PROGRAM = 0;
10
+ const MAX_MIDI_PROGRAM = 127;
11
+ const MIN_MIDI_CHANNEL = 0;
12
+ const MAX_MIDI_CHANNEL = 15;
13
+ const VALID_TIME_SIG_DENOMINATORS = new Set([1, 2, 4, 8, 16, 32]);
14
+ const MIN_FIFTHS = -7;
15
+ const MAX_FIFTHS = 7;
16
+ const MIN_BPM = 20;
17
+ const MAX_BPM = 400;
18
+ /**
19
+ * Maximum number of notes sounding at once (within a single track) before a
20
+ * "too many simultaneous notes" readability warning fires. Spec §23 names
21
+ * this rule but doesn't give a number; 10 is a deliberate implementer
22
+ * default (roughly as many notes as a two-hand piano voicing on one staff
23
+ * can render legibly), documented here so a later task can make it
24
+ * configurable if that turns out to be too strict/loose in practice.
25
+ */
26
+ const MAX_SIMULTANEOUS_NOTES = 10;
27
+ function samePitch(a, b) {
28
+ return (a.step === b.step && a.accidental === b.accidental && a.octave === b.octave);
29
+ }
30
+ function issue(severity, code, message, extra) {
31
+ return { severity, code, message, ...extra };
32
+ }
33
+ // ---- Whole-score: tempo map, unique ids -----------------------------------
34
+ /** Tempo-map tick validity/ordering and bpm range (20-400). */
35
+ function checkTempoMap(score) {
36
+ const issues = [];
37
+ let lastTick = -Infinity;
38
+ for (const event of score.tempoMap) {
39
+ if (event.tick < 0) {
40
+ issues.push(issue('error', ISSUE_CODES.NEGATIVE_TICK, `Tempo event ${event.id} has a negative tick (${event.tick}).`, {
41
+ objectId: event.id,
42
+ }));
43
+ }
44
+ if (event.tick < lastTick) {
45
+ issues.push(issue('error', ISSUE_CODES.TEMPO_MAP_UNSORTED, `Tempo map is not sorted by tick at event ${event.id} (tick ${event.tick} follows tick ${lastTick}).`, { objectId: event.id }));
46
+ }
47
+ lastTick = event.tick;
48
+ if (event.bpm < MIN_BPM || event.bpm > MAX_BPM) {
49
+ issues.push(issue('error', ISSUE_CODES.INVALID_TEMPO_BPM, `Tempo event ${event.id} has bpm ${event.bpm}, expected ${MIN_BPM}-${MAX_BPM}.`, { objectId: event.id }));
50
+ }
51
+ }
52
+ return issues;
53
+ }
54
+ /** Every id in the score (score/tempo/track/measure/voice/event) must be unique. */
55
+ function checkUniqueIds(score) {
56
+ const counts = new Map();
57
+ const record = (id) => {
58
+ counts.set(id, (counts.get(id) ?? 0) + 1);
59
+ };
60
+ record(score.id);
61
+ for (const tempoEvent of score.tempoMap)
62
+ record(tempoEvent.id);
63
+ for (const track of score.tracks) {
64
+ record(track.id);
65
+ for (const measure of track.measures) {
66
+ record(measure.id);
67
+ for (const voice of measure.voices) {
68
+ record(voice.id);
69
+ for (const event of voice.events)
70
+ record(event.id);
71
+ }
72
+ }
73
+ }
74
+ const issues = [];
75
+ for (const [id, count] of counts) {
76
+ if (count > 1) {
77
+ issues.push(issue('error', ISSUE_CODES.DUPLICATE_ID, `Id "${id}" is used ${count} times; ids must be unique.`, {
78
+ objectId: id,
79
+ }));
80
+ }
81
+ }
82
+ return issues;
83
+ }
84
+ // ---- Track-level ------------------------------------------------------------
85
+ /** midiProgram (0-127) and midiChannel (0-15) range checks. */
86
+ function checkTrack(track) {
87
+ const issues = [];
88
+ if (track.midiProgram < MIN_MIDI_PROGRAM ||
89
+ track.midiProgram > MAX_MIDI_PROGRAM) {
90
+ issues.push(issue('error', ISSUE_CODES.INVALID_MIDI_PROGRAM, `Track "${track.name}" has midiProgram ${track.midiProgram}, expected ${MIN_MIDI_PROGRAM}-${MAX_MIDI_PROGRAM}.`, { objectId: track.id, trackId: track.id }));
91
+ }
92
+ if (track.midiChannel < MIN_MIDI_CHANNEL ||
93
+ track.midiChannel > MAX_MIDI_CHANNEL) {
94
+ issues.push(issue('error', ISSUE_CODES.INVALID_MIDI_CHANNEL, `Track "${track.name}" has midiChannel ${track.midiChannel}, expected ${MIN_MIDI_CHANNEL}-${MAX_MIDI_CHANNEL}.`, { objectId: track.id, trackId: track.id }));
95
+ }
96
+ return issues;
97
+ }
98
+ /**
99
+ * A track's measures must be contiguous and sequentially indexed: measure
100
+ * at array position `i` must have `index === i` and `startTick` equal to
101
+ * the previous measure's `startTick + durationTicks` (0 for the first).
102
+ * Checked against each measure's *actual* preceding measure (not a
103
+ * recomputed ideal), so one bad measure doesn't cascade a flood of
104
+ * downstream false positives.
105
+ */
106
+ function checkMeasureOrdering(track) {
107
+ const issues = [];
108
+ let expectedStart = 0;
109
+ track.measures.forEach((measure, position) => {
110
+ if (measure.index !== position) {
111
+ issues.push(issue('error', ISSUE_CODES.MEASURE_ORDERING, `Measure at position ${position} of track "${track.name}" has index ${measure.index}, expected ${position}.`, { objectId: measure.id, trackId: track.id, measureId: measure.id }));
112
+ }
113
+ if (measure.startTick !== expectedStart) {
114
+ issues.push(issue('error', ISSUE_CODES.MEASURE_ORDERING, `Measure ${measure.index} of track "${track.name}" starts at tick ${measure.startTick}, expected ${expectedStart} (immediately after the previous measure).`, { objectId: measure.id, trackId: track.id, measureId: measure.id }));
115
+ }
116
+ expectedStart = measure.startTick + measure.durationTicks;
117
+ });
118
+ return issues;
119
+ }
120
+ /**
121
+ * Sweep-line max concurrent NoteEvent count across the whole track (all
122
+ * measures/voices). Note-off is treated as occurring strictly before a
123
+ * note-on at the same tick, so back-to-back (non-overlapping) notes don't
124
+ * count as simultaneous.
125
+ */
126
+ function checkTooManySimultaneousNotes(track) {
127
+ const points = [];
128
+ for (const measure of track.measures) {
129
+ for (const voice of measure.voices) {
130
+ for (const event of voice.events) {
131
+ if (!isNoteEvent(event))
132
+ continue;
133
+ points.push({ tick: event.startTick, delta: 1 });
134
+ points.push({ tick: event.startTick + event.durationTicks, delta: -1 });
135
+ }
136
+ }
137
+ }
138
+ points.sort((a, b) => a.tick - b.tick || a.delta - b.delta);
139
+ let current = 0;
140
+ let max = 0;
141
+ for (const point of points) {
142
+ current += point.delta;
143
+ max = Math.max(max, current);
144
+ }
145
+ if (max > MAX_SIMULTANEOUS_NOTES) {
146
+ return [
147
+ issue('warning', ISSUE_CODES.TOO_MANY_SIMULTANEOUS_NOTES, `Track "${track.name}" has up to ${max} notes sounding simultaneously (readability threshold ${MAX_SIMULTANEOUS_NOTES}).`, { trackId: track.id }),
148
+ ];
149
+ }
150
+ return [];
151
+ }
152
+ // ---- Measure-level ------------------------------------------------------------
153
+ /** Time signature (numerator >= 1, denominator in {1,2,4,8,16,32}) and key signature (fifths -7..7). */
154
+ function checkMeasureSignatures(track, measure) {
155
+ const issues = [];
156
+ const { timeSignature, keySignature } = measure;
157
+ if (timeSignature.numerator < 1 ||
158
+ !VALID_TIME_SIG_DENOMINATORS.has(timeSignature.denominator)) {
159
+ issues.push(issue('error', ISSUE_CODES.INVALID_TIME_SIGNATURE, `Measure ${measure.index} has invalid time signature ${timeSignature.numerator}/${timeSignature.denominator} (numerator must be >= 1; denominator must be one of 1, 2, 4, 8, 16, 32).`, { objectId: measure.id, trackId: track.id, measureId: measure.id }));
160
+ }
161
+ if (keySignature.fifths < MIN_FIFTHS || keySignature.fifths > MAX_FIFTHS) {
162
+ issues.push(issue('error', ISSUE_CODES.INVALID_KEY_SIGNATURE, `Measure ${measure.index} has key signature fifths=${keySignature.fifths}, expected ${MIN_FIFTHS}..${MAX_FIFTHS}.`, { objectId: measure.id, trackId: track.id, measureId: measure.id }));
163
+ }
164
+ return issues;
165
+ }
166
+ /** Same-pitch overlap (error) within one voice's note events. Different-pitch overlaps (chords/independent lines) are out of scope for this rule. */
167
+ function checkOverlappingSamePitch(track, measure, voice) {
168
+ const issues = [];
169
+ const notes = voice.events.filter(isNoteEvent);
170
+ for (let i = 0; i < notes.length; i += 1) {
171
+ for (let j = i + 1; j < notes.length; j += 1) {
172
+ const a = notes[i];
173
+ const b = notes[j];
174
+ const overlaps = a.startTick < b.startTick + b.durationTicks &&
175
+ b.startTick < a.startTick + a.durationTicks;
176
+ if (overlaps && samePitch(a.pitch, b.pitch)) {
177
+ issues.push(issue('error', ISSUE_CODES.OVERLAPPING_SAME_PITCH, `Notes ${a.id} and ${b.id} in voice "${voice.name}" (measure ${measure.index}) overlap in time at the same pitch.`, { objectId: b.id, trackId: track.id, measureId: measure.id }));
178
+ }
179
+ }
180
+ }
181
+ return issues;
182
+ }
183
+ /**
184
+ * Total *covered* ticks of a voice's events, merging overlapping/touching
185
+ * spans into disjoint intervals before summing their lengths. A plain
186
+ * "sum every event's durationTicks" would double-count block chords
187
+ * (multiple `NoteEvent`s deliberately sharing one startTick/duration, as
188
+ * built by `fixtures.ts`'s `buildChordMeasures`/spec §25 voice allocation)
189
+ * as if they were sequential content several times the measure's length.
190
+ * Merging first means a chord (or any simultaneous/overlapping content)
191
+ * counts once, at its own span, matching how the notated timeline actually
192
+ * reads.
193
+ */
194
+ function coveredTicks(events) {
195
+ const spans = events
196
+ .map(e => ({ start: e.startTick, end: e.startTick + e.durationTicks }))
197
+ .sort((a, b) => a.start - b.start);
198
+ let total = 0;
199
+ let i = 0;
200
+ while (i < spans.length) {
201
+ let end = spans[i].end;
202
+ let j = i + 1;
203
+ while (j < spans.length && spans[j].start <= end) {
204
+ end = Math.max(end, spans[j].end);
205
+ j += 1;
206
+ }
207
+ total += end - spans[i].start;
208
+ i = j;
209
+ }
210
+ return total;
211
+ }
212
+ /**
213
+ * Per-voice checks within a measure: positive duration, nonnegative
214
+ * startTick, containment within the measure, valid track/voice references,
215
+ * note-only velocity/pitch-range checks, same-pitch overlap, and the
216
+ * voice's covered-ticks total vs. the measure's duration (error if it
217
+ * covers more, warning if less).
218
+ */
219
+ function checkVoice(track, measure, voice) {
220
+ const issues = [];
221
+ for (const event of voice.events) {
222
+ if (event.durationTicks <= 0) {
223
+ issues.push(issue('error', ISSUE_CODES.NON_POSITIVE_DURATION, `Event ${event.id} has non-positive durationTicks (${event.durationTicks}).`, { objectId: event.id, trackId: track.id, measureId: measure.id }));
224
+ }
225
+ if (event.startTick < 0) {
226
+ issues.push(issue('error', ISSUE_CODES.NEGATIVE_TICK, `Event ${event.id} has a negative startTick (${event.startTick}).`, {
227
+ objectId: event.id,
228
+ trackId: track.id,
229
+ measureId: measure.id,
230
+ }));
231
+ }
232
+ if (event.startTick < measure.startTick ||
233
+ event.startTick + event.durationTicks >
234
+ measure.startTick + measure.durationTicks) {
235
+ issues.push(issue('error', ISSUE_CODES.EVENT_OUTSIDE_MEASURE, `Event ${event.id} [${event.startTick}, ${event.startTick + event.durationTicks}) falls outside measure ${measure.index}'s span [${measure.startTick}, ${measure.startTick + measure.durationTicks}).`, { objectId: event.id, trackId: track.id, measureId: measure.id }));
236
+ }
237
+ if (event.trackId !== track.id) {
238
+ issues.push(issue('error', ISSUE_CODES.INVALID_TRACK_REFERENCE, `Event ${event.id} references trackId "${event.trackId}" but lives on track "${track.id}".`, { objectId: event.id, trackId: track.id, measureId: measure.id }));
239
+ }
240
+ if (event.voiceId !== voice.id) {
241
+ issues.push(issue('error', ISSUE_CODES.INVALID_VOICE_REFERENCE, `Event ${event.id} references voiceId "${event.voiceId}" but lives in voice "${voice.id}".`, { objectId: event.id, trackId: track.id, measureId: measure.id }));
242
+ }
243
+ if (isNoteEvent(event)) {
244
+ if (event.velocity < MIN_VELOCITY || event.velocity > MAX_VELOCITY) {
245
+ issues.push(issue('error', ISSUE_CODES.INVALID_VELOCITY, `Note ${event.id} has velocity ${event.velocity}, expected ${MIN_VELOCITY}-${MAX_VELOCITY}.`, { objectId: event.id, trackId: track.id, measureId: measure.id }));
246
+ }
247
+ const midi = pitchToMidi(event.pitch);
248
+ if (midi < MIN_MIDI || midi > MAX_MIDI) {
249
+ issues.push(issue('error', ISSUE_CODES.INVALID_PITCH_RANGE, `Note ${event.id} has pitch MIDI ${midi}, expected ${MIN_MIDI}-${MAX_MIDI}.`, { objectId: event.id, trackId: track.id, measureId: measure.id }));
250
+ }
251
+ }
252
+ }
253
+ const covered = coveredTicks(voice.events);
254
+ if (covered > measure.durationTicks) {
255
+ issues.push(issue('error', ISSUE_CODES.MEASURE_OVERFULL, `Voice "${voice.name}" in measure ${measure.index} covers ${covered} ticks, exceeding the measure's ${measure.durationTicks}.`, { objectId: voice.id, trackId: track.id, measureId: measure.id }));
256
+ }
257
+ else if (covered < measure.durationTicks) {
258
+ issues.push(issue('warning', ISSUE_CODES.MEASURE_UNDERFULL, `Voice "${voice.name}" in measure ${measure.index} covers ${covered} ticks, short of the measure's ${measure.durationTicks}.`, { objectId: voice.id, trackId: track.id, measureId: measure.id }));
259
+ }
260
+ issues.push(...checkOverlappingSamePitch(track, measure, voice));
261
+ return issues;
262
+ }
263
+ // ---- Ties (whole-score, since a tie may cross a measure boundary) ---------
264
+ /** The most voices any single measure of `track` has (so every voice-ordinal channel is covered). */
265
+ function trackVoiceCount(track) {
266
+ return track.measures.reduce((max, measure) => Math.max(max, measure.voices.length), 0);
267
+ }
268
+ /**
269
+ * The first candidate in `index.get(tick)` (if any) that isn't `excludeId`
270
+ * itself, carries `requireFlag`, and shares `pitch` — the same (tick,
271
+ * pitch, tie-flag) matching `domain/score/ties.ts`'s `findForwardPartner`/
272
+ * `findBackwardPartner` use, just against a prebuilt index instead of a
273
+ * fresh linear scan.
274
+ */
275
+ function findIndexedPartner(index, tick, excludeId, pitch, requireFlag) {
276
+ const candidates = index.get(tick);
277
+ if (!candidates)
278
+ return undefined;
279
+ return candidates.find(c => c.event.id !== excludeId &&
280
+ requireFlag(c) &&
281
+ samePitch(c.event.pitch, pitch));
282
+ }
283
+ /**
284
+ * A tieStart note must have a matching tieStop note at the next position
285
+ * (same track/voice-channel, immediately following tick, same pitch) —
286
+ * warning otherwise. Symmetrically, a tieStop note must have a matching
287
+ * tieStart note immediately preceding it.
288
+ *
289
+ * Originally implemented via `tieChainFor` per tied note, which — for a
290
+ * track with many tied notes — rebuilds its voice-ordinal channel
291
+ * (`voiceChainFor`'s `locateVoiceIndex` + `voiceChannel`, each O(track
292
+ * size)) from scratch on *every* call and walks the full backward/forward
293
+ * chain by linear-scanning the channel at each hop: O(n) work per tied
294
+ * note, O(n^2) worst case across a track with O(n) tied notes (flagged in
295
+ * the Task 4 ledger as a risk against spec §29's 20k-note target).
296
+ *
297
+ * This only ever needs to know whether *one specific* note has a forward/
298
+ * backward partner (not the whole chain), so it builds each track/voice-
299
+ * ordinal channel exactly once via `voiceChannel` (same shared convention
300
+ * `tieChainFor` uses — see its doc comment), then indexes that channel by
301
+ * start tick and by end tick (`startTick + durationTicks`) for O(1)-average
302
+ * partner lookups. Net cost: O(n) per track instead of O(n^2). Matching
303
+ * semantics (same "does a same-pitch, correctly-tie-flagged event exist
304
+ * at the adjoining tick, within this voice-ordinal channel" question) are
305
+ * identical to the previous `tieChainFor`-based check, so the exact same
306
+ * set of issues is produced — see `checkTieTargets`'s tests, which cover
307
+ * both directions.
308
+ */
309
+ function checkTieTargets(score) {
310
+ const issues = [];
311
+ for (const track of score.tracks) {
312
+ const voiceCount = trackVoiceCount(track);
313
+ for (let voiceIndex = 0; voiceIndex < voiceCount; voiceIndex += 1) {
314
+ const channel = voiceChannel(track, voiceIndex);
315
+ if (channel.length === 0)
316
+ continue;
317
+ const byStartTick = new Map();
318
+ const byEndTick = new Map();
319
+ for (const candidate of channel) {
320
+ const start = candidate.event.startTick;
321
+ const end = start + candidate.event.durationTicks;
322
+ const startBucket = byStartTick.get(start);
323
+ if (startBucket)
324
+ startBucket.push(candidate);
325
+ else
326
+ byStartTick.set(start, [candidate]);
327
+ const endBucket = byEndTick.get(end);
328
+ if (endBucket)
329
+ endBucket.push(candidate);
330
+ else
331
+ byEndTick.set(end, [candidate]);
332
+ }
333
+ for (const { event, measureIndex } of channel) {
334
+ if (!event.tieStart && !event.tieStop)
335
+ continue;
336
+ const measureId = track.measures[measureIndex]?.id;
337
+ if (event.tieStart) {
338
+ const targetTick = event.startTick + event.durationTicks;
339
+ const partner = findIndexedPartner(byStartTick, targetTick, event.id, event.pitch, c => Boolean(c.event.tieStop));
340
+ if (!partner) {
341
+ issues.push(issue('warning', ISSUE_CODES.MISSING_TIE_TARGET, `Note ${event.id} has tieStart but no matching tieStop note at the next position with the same pitch.`, { objectId: event.id, trackId: track.id, measureId }));
342
+ }
343
+ }
344
+ if (event.tieStop) {
345
+ const partner = findIndexedPartner(byEndTick, event.startTick, event.id, event.pitch, c => Boolean(c.event.tieStart));
346
+ if (!partner) {
347
+ issues.push(issue('warning', ISSUE_CODES.MISSING_TIE_TARGET, `Note ${event.id} has tieStop but no matching tieStart note ending at its start tick with the same pitch.`, { objectId: event.id, trackId: track.id, measureId }));
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ return issues;
354
+ }
355
+ // ---- Entry point ------------------------------------------------------------
356
+ /** Validates a `Score` against every rule in spec §23 (Task 4 brief scope). Read-only; never mutates `score`. */
357
+ export function validateScore(score) {
358
+ const issues = [];
359
+ issues.push(...checkUniqueIds(score));
360
+ issues.push(...checkTempoMap(score));
361
+ for (const track of score.tracks) {
362
+ issues.push(...checkTrack(track));
363
+ issues.push(...checkMeasureOrdering(track));
364
+ issues.push(...checkTooManySimultaneousNotes(track));
365
+ for (const measure of track.measures) {
366
+ issues.push(...checkMeasureSignatures(track, measure));
367
+ for (const voice of measure.voices) {
368
+ issues.push(...checkVoice(track, measure, voice));
369
+ }
370
+ }
371
+ }
372
+ issues.push(...checkTieTargets(score));
373
+ return issues;
374
+ }
375
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../../src/domain/validation/validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { NoteEvent } from '../../index.js';
2
+ export type AllocateVoicesOptions = {
3
+ maxVoices: number;
4
+ splitPoint?: number;
5
+ };
6
+ export type VoiceGroup = {
7
+ voiceIndex: number;
8
+ staff: 'upper' | 'lower';
9
+ notes: NoteEvent[];
10
+ };
11
+ /**
12
+ * Allocates `notes` into voices (rhythm/overlap-based) and staves
13
+ * (pitch-based). A wide chord straddling `splitPoint` (or grand-staff
14
+ * tracks generally) can produce two output groups for the same
15
+ * `voiceIndex` — one per staff — since staff assignment is per-note while
16
+ * voice assignment is per-cluster.
17
+ */
18
+ export declare function allocateVoices(notes: NoteEvent[], opts: AllocateVoicesOptions): VoiceGroup[];
19
+ //# sourceMappingURL=allocate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocate.d.ts","sourceRoot":"","sources":["../../../src/domain/voicing/allocate.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAIhD,MAAM,MAAM,qBAAqB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,UAAU,GAAG;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IACzB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAuEF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,qBAAqB,GAC1B,UAAU,EAAE,CA6Bd"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Basic voice allocation (spec §25): groups simultaneous notes into
3
+ * chords, separates overlapping independent lines into separate voices
4
+ * (preferring minimal voice-hopping), respects a configurable maximum
5
+ * voice count, and assigns notes to a treble/bass staff by pitch for
6
+ * grand-staff tracks. Output need not match professional engraving, but
7
+ * must be readable; a caller may reassign manually afterward. Pure
8
+ * function: never mutates `notes`.
9
+ */
10
+ import { pitchToMidi } from '../pitch/pitch.js';
11
+ const DEFAULT_SPLIT_POINT = 60; // middle C
12
+ /**
13
+ * Groups `notes` into chronological clusters of notes sharing the exact
14
+ * same `startTick` *and* `durationTicks` — "simultaneous, equal-duration"
15
+ * per spec §25, since only such notes can be notated as one chord. Two
16
+ * simultaneous notes of differing duration are deliberately kept as
17
+ * separate one-note clusters (they can't share a single notated value).
18
+ * Clusters are returned in ascending `startTick` order.
19
+ */
20
+ function groupChordClusters(notes) {
21
+ const sorted = [...notes].sort((a, b) => a.startTick - b.startTick);
22
+ const order = [];
23
+ const byKey = new Map();
24
+ for (const note of sorted) {
25
+ const key = `${note.startTick}:${note.durationTicks}`;
26
+ let cluster = byKey.get(key);
27
+ if (!cluster) {
28
+ cluster = [];
29
+ byKey.set(key, cluster);
30
+ order.push(key);
31
+ }
32
+ cluster.push(note);
33
+ }
34
+ return order.map(key => byKey.get(key));
35
+ }
36
+ /**
37
+ * The index of the best voice slot for a cluster starting at `startTick`:
38
+ * among slots free at or before `startTick` (no overlap), the one whose
39
+ * `lastEnd` is largest — i.e. whose previous note ended closest before the
40
+ * new one, minimizing voice-hopping. Opens a fresh slot if none are free
41
+ * and the voice cap hasn't been reached (or none exist yet). Otherwise
42
+ * (at capacity, all busy) reuses the least-overlapping slot (largest
43
+ * `lastEnd`) rather than exceeding `maxVoices`.
44
+ */
45
+ function chooseVoiceIndex(voices, startTick, maxVoices) {
46
+ let bestFreeIndex = -1;
47
+ for (let i = 0; i < voices.length; i += 1) {
48
+ if (voices[i].lastEnd <= startTick) {
49
+ if (bestFreeIndex === -1 ||
50
+ voices[i].lastEnd > voices[bestFreeIndex].lastEnd) {
51
+ bestFreeIndex = i;
52
+ }
53
+ }
54
+ }
55
+ if (bestFreeIndex !== -1)
56
+ return bestFreeIndex;
57
+ if (voices.length === 0 || voices.length < maxVoices) {
58
+ return voices.length; // caller pushes a fresh slot at this index
59
+ }
60
+ let leastOverlapIndex = 0;
61
+ for (let i = 1; i < voices.length; i += 1) {
62
+ if (voices[i].lastEnd > voices[leastOverlapIndex].lastEnd)
63
+ leastOverlapIndex = i;
64
+ }
65
+ return leastOverlapIndex;
66
+ }
67
+ /**
68
+ * Allocates `notes` into voices (rhythm/overlap-based) and staves
69
+ * (pitch-based). A wide chord straddling `splitPoint` (or grand-staff
70
+ * tracks generally) can produce two output groups for the same
71
+ * `voiceIndex` — one per staff — since staff assignment is per-note while
72
+ * voice assignment is per-cluster.
73
+ */
74
+ export function allocateVoices(notes, opts) {
75
+ if (notes.length === 0)
76
+ return [];
77
+ const splitPoint = opts.splitPoint ?? DEFAULT_SPLIT_POINT;
78
+ const clusters = groupChordClusters(notes);
79
+ const voices = [];
80
+ for (const cluster of clusters) {
81
+ const startTick = cluster[0].startTick;
82
+ const endTick = cluster[0].startTick + cluster[0].durationTicks;
83
+ const index = chooseVoiceIndex(voices, startTick, opts.maxVoices);
84
+ if (!voices[index])
85
+ voices[index] = { lastEnd: -Infinity, notes: [] };
86
+ voices[index].notes.push(...cluster);
87
+ voices[index].lastEnd = Math.max(voices[index].lastEnd, endTick);
88
+ }
89
+ const result = [];
90
+ voices.forEach((voice, voiceIndex) => {
91
+ const upper = voice.notes.filter(n => pitchToMidi(n.pitch) >= splitPoint);
92
+ const lower = voice.notes.filter(n => pitchToMidi(n.pitch) < splitPoint);
93
+ if (upper.length > 0)
94
+ result.push({ voiceIndex, staff: 'upper', notes: upper });
95
+ if (lower.length > 0)
96
+ result.push({ voiceIndex, staff: 'lower', notes: lower });
97
+ });
98
+ return result;
99
+ }
100
+ //# sourceMappingURL=allocate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allocate.js","sourceRoot":"","sources":["../../../src/domain/voicing/allocate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,MAAM,mBAAmB,GAAG,EAAE,CAAC,CAAC,WAAW;AAU3C;;;;;;;GAOG;AACH,SAAS,kBAAkB,CAAC,KAAkB;IAC5C,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;IACpE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE7C,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACtD,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,EAAE,CAAC;YACb,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAgB,CAAC,CAAC;AACzD,CAAC;AAKD;;;;;;;;GAQG;AACH,SAAS,gBAAgB,CACvB,MAAmB,EACnB,SAAiB,EACjB,SAAiB;IAEjB,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC;YACnC,IACE,aAAa,KAAK,CAAC,CAAC;gBACpB,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,EACjD,CAAC;gBACD,aAAa,GAAG,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,aAAa,KAAK,CAAC,CAAC;QAAE,OAAO,aAAa,CAAC;IAE/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QACrD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,2CAA2C;IACnE,CAAC;IAED,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC,OAAO;YACvD,iBAAiB,GAAG,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAkB,EAClB,IAA2B;IAE3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAElC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,mBAAmB,CAAC;IAC1D,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;QAEhE,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAClE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAEtE,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACnC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,CAAC;QAC1E,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,CAAC;QACzE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -12,8 +12,15 @@
12
12
  * 8. Zod schemas for the project API
13
13
  * 9. Response envelope + error codes
14
14
  *
15
- * This package contains types, schemas, and pure helpers only no domain
16
- * logic (tick math, factories, commands live in @sudobility/music_lib).
15
+ * Contains the types and schemas, plus the pure domain primitives both sides
16
+ * of the system need: pitch and tick math, the score factory, quantization,
17
+ * ties and voice allocation.
18
+ *
19
+ * Those primitives used to live in `@sudobility/music_lib`, which put them out
20
+ * of reach of the backend — `music_api` must never depend on `music_lib`, and
21
+ * neither may depend on the other. Anything both sides need lives here or in
22
+ * `@sudobility/music_codecs`; `music_lib` keeps what is frontend-only (store,
23
+ * commands, rendering, playback).
17
24
  */
18
25
  import { z } from 'zod';
19
26
  export type UUID = string;
@@ -1697,9 +1704,9 @@ export declare const generationJobKindSchema: z.ZodEnum<{
1697
1704
  export type GenerationJobStatus = 'queued' | 'running' | 'done' | 'failed' | 'cancelled';
1698
1705
  export declare const generationJobStatusSchema: z.ZodEnum<{
1699
1706
  failed: "failed";
1707
+ done: "done";
1700
1708
  queued: "queued";
1701
1709
  running: "running";
1702
- done: "done";
1703
1710
  cancelled: "cancelled";
1704
1711
  }>;
1705
1712
  /**
@@ -1750,9 +1757,9 @@ export declare const generationJobSchema: z.ZodObject<{
1750
1757
  }>;
1751
1758
  status: z.ZodEnum<{
1752
1759
  failed: "failed";
1760
+ done: "done";
1753
1761
  queued: "queued";
1754
1762
  running: "running";
1755
- done: "done";
1756
1763
  cancelled: "cancelled";
1757
1764
  }>;
1758
1765
  createdAt: z.ZodString;
@@ -2714,4 +2721,39 @@ export declare function successResponse<T>(data: T): ApiResponse<T>;
2714
2721
  /** Wraps an error message (and optional typed code) in the standard error envelope. */
2715
2722
  export declare function errorResponse(message: string, code?: ApiErrorCode): ApiResponse<never>;
2716
2723
  export * from './platform/index.js';
2724
+ export * from './domain/pitch/pitch.js';
2725
+ export * from './domain/pitch/transpose.js';
2726
+ export * from './domain/quantization/options.js';
2727
+ export * from './domain/quantization/quantize.js';
2728
+ export * from './domain/score/factory.js';
2729
+ export * from './domain/score/fragment.js';
2730
+ export * from './domain/score/ids.js';
2731
+ export * from './domain/score/queries.js';
2732
+ export * from './domain/score/ties.js';
2733
+ export * from './domain/selection/types.js';
2734
+ export * from './domain/time/durations.js';
2735
+ export * from './domain/time/fraction.js';
2736
+ export * from './domain/time/tempo-map.js';
2737
+ export * from './domain/time/ticks.js';
2738
+ export * from './domain/voicing/allocate.js';
2739
+ export * from './domain/validation/issues.js';
2740
+ export * from './domain/validation/validator.js';
2741
+ export * from './domain/commands/types.js';
2742
+ export * from './domain/commands/reflow.js';
2743
+ export * from './domain/commands/snapshot.js';
2744
+ export * from './domain/commands/structure-commands.js';
2745
+ export * from './domain/commands/track-commands.js';
2746
+ export * from './domain/commands/region-commands.js';
2747
+ export * from './domain/commands/note-commands.js';
2748
+ export * from './domain/commands/edit-commands.js';
2749
+ export * from './domain/commands/relocate-commands.js';
2750
+ export * from './domain/commands/ripple-commands.js';
2751
+ export * from './domain/instruments/gm.js';
2752
+ export * from './domain/instruments/gm-kit.js';
2753
+ export * from './domain/instruments/gm-range.js';
2754
+ export * from './domain/instruments/gm-polyphony.js';
2755
+ export * from './domain/instruments/gm-transposition.js';
2756
+ export * from './domain/instruments/gm-percussion.js';
2757
+ export * from './domain/selection/selection.js';
2758
+ export * from './services/regeneration/controller.js';
2717
2759
  //# sourceMappingURL=index.d.ts.map