@sudobility/music_types 0.11.20 → 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 (149) 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/services/regeneration/controller.d.ts +60 -0
  142. package/dist/services/regeneration/controller.d.ts.map +1 -0
  143. package/dist/services/regeneration/controller.js +202 -0
  144. package/dist/services/regeneration/controller.js.map +1 -0
  145. package/dist/test/fixtures.d.ts +45 -0
  146. package/dist/test/fixtures.d.ts.map +1 -0
  147. package/dist/test/fixtures.js +420 -0
  148. package/dist/test/fixtures.js.map +1 -0
  149. package/package.json +2 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Moving notes to another track, another tick, or both.
3
+ *
4
+ * `moveNotesCommand` already relocates notes *within* a track. This is the
5
+ * cross-track case, and it takes the collision rule as a parameter so that a
6
+ * drag is **one** undo step: composing "clear the span" and "put them there"
7
+ * from two dispatches would make undo step back through the middle of a
8
+ * gesture the user experienced as one.
9
+ */
10
+ import { isNoteEvent } from '../../index.js';
11
+ import { transformCommand } from './snapshot.js';
12
+ import { clearDanglingTies, insertNoteIntoTrack, removeNotesFromTrack, withTracks, } from './reflow.js';
13
+ import { makeRoom } from './ripple-commands.js';
14
+ /** Every note in `score` carrying one of `ids`, with the voice it sat in. */
15
+ function collect(score, ids) {
16
+ const found = [];
17
+ for (const track of score.tracks) {
18
+ for (const measure of track.measures) {
19
+ measure.voices.forEach((voice, voiceIndex) => {
20
+ for (const event of voice.events) {
21
+ if (isNoteEvent(event) && ids.has(event.id))
22
+ found.push({ note: event, voiceIndex });
23
+ }
24
+ });
25
+ }
26
+ }
27
+ return found;
28
+ }
29
+ /** The last tick a note of `durationTicks` can start at and still fit in `track`. */
30
+ function lastStart(track, durationTicks) {
31
+ const last = track.measures[track.measures.length - 1];
32
+ const end = last ? last.startTick + last.durationTicks : 0;
33
+ return Math.max(0, end - durationTicks);
34
+ }
35
+ /** Ids of notes on `track` sounding inside `[from, to)` in any of `voices`. */
36
+ function occupantsOf(track, from, to, voices) {
37
+ const ids = [];
38
+ for (const measure of track.measures) {
39
+ measure.voices.forEach((voice, voiceIndex) => {
40
+ // Voice-scoped: clearing the span across *every* voice would delete the
41
+ // other line, which is a bug this codebase has already had once.
42
+ if (!voices.has(voiceIndex))
43
+ return;
44
+ for (const event of voice.events) {
45
+ if (!isNoteEvent(event))
46
+ continue;
47
+ if (event.startTick < to &&
48
+ event.startTick + event.durationTicks > from)
49
+ ids.push(event.id);
50
+ }
51
+ });
52
+ }
53
+ return ids;
54
+ }
55
+ function relocateNotes(score, eventIds, params) {
56
+ const ids = new Set(eventIds);
57
+ const moving = collect(score, ids);
58
+ if (moving.length === 0)
59
+ return score;
60
+ if (!score.tracks.some(t => t.id === params.targetTrackId))
61
+ return score;
62
+ // Partners left behind must not keep a tie to a note that has gone.
63
+ const detied = clearDanglingTies(score, ids);
64
+ let working = withTracks(detied, detied.tracks.map(track => removeNotesFromTrack(track, ids)));
65
+ const target = working.tracks.find(t => t.id === params.targetTrackId);
66
+ const placed = moving.map(({ note, voiceIndex }) => ({
67
+ note,
68
+ voiceIndex,
69
+ startTick: Math.max(0, Math.min(note.startTick + params.deltaTicks, lastStart(target, note.durationTicks))),
70
+ }));
71
+ const from = Math.min(...placed.map(p => p.startTick));
72
+ const to = Math.max(...placed.map(p => p.startTick + p.note.durationTicks));
73
+ if (params.collision === 'replace') {
74
+ const voices = new Set(placed.map(p => p.voiceIndex));
75
+ const doomed = new Set(occupantsOf(target, from, to, voices));
76
+ if (doomed.size > 0) {
77
+ working = withTracks(working, working.tracks.map(t => t.id === params.targetTrackId ? removeNotesFromTrack(t, doomed) : t));
78
+ }
79
+ }
80
+ else if (params.collision === 'ripple') {
81
+ working = makeRoom(working, params.targetTrackId, from, to - from);
82
+ }
83
+ for (const { note, voiceIndex, startTick } of placed) {
84
+ working = withTracks(working, working.tracks.map(t => t.id === params.targetTrackId
85
+ ? insertNoteIntoTrack(t, {
86
+ ...note,
87
+ trackId: params.targetTrackId,
88
+ startTick,
89
+ // The note's former neighbours are not necessarily adjacent
90
+ // any more, so its tie flags cannot survive the move.
91
+ tieStart: undefined,
92
+ tieStop: undefined,
93
+ }, voiceIndex)
94
+ : t));
95
+ }
96
+ return working;
97
+ }
98
+ /**
99
+ * Moves `eventIds` onto `params.targetTrackId`, shifted by `params.deltaTicks`.
100
+ *
101
+ * Pitch is untouched: a note dragged from flute to cello sounds identical, and
102
+ * only its player changes. Notes are clamped into the target track rather than
103
+ * dropped when the shift runs past its end.
104
+ */
105
+ export function relocateNotesCommand(eventIds, params, label) {
106
+ return transformCommand(label, score => relocateNotes(score, eventIds, params));
107
+ }
108
+ //# sourceMappingURL=relocate-commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relocate-commands.js","sourceRoot":"","sources":["../../../src/domain/commands/relocate-commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { ScoreCommand } from './types.js';
2
+ import type { Articulation, Pitch, Score, UUID } from '../../index.js';
3
+ export type RippleInsertParams = {
4
+ trackId: UUID;
5
+ measureId: UUID;
6
+ voiceIndex: number;
7
+ pitch: Pitch;
8
+ startTick: number;
9
+ durationTicks: number;
10
+ articulation?: Articulation;
11
+ };
12
+ /**
13
+ * `score` with `ticks` of empty time opened up at `atTick` on `trackId`.
14
+ *
15
+ * Shared by note entry and paste: both need the same "push this track's later
16
+ * music out of the way, growing the score if it runs off the end" behaviour,
17
+ * and duplicating it would let the two drift.
18
+ */
19
+ export declare function makeRoom(score: Score, trackId: UUID, atTick: number, ticks: number): Score;
20
+ /**
21
+ * `score` with `ticks` of time removed at `fromTick` on `trackId` — everything
22
+ * after the gap slides earlier to close it.
23
+ *
24
+ * The inverse of `makeRoom`, and what "move the notes behind up" means after a
25
+ * cut. Measures are deliberately NOT removed: the piece keeps its length and
26
+ * gains rests at the end, because dropping a bar is a structural edit the user
27
+ * did not ask for, and it would misalign every other track.
28
+ *
29
+ * Notes that start inside the removed span are left where they are rather than
30
+ * dragged backwards past `fromTick`. A cut removes them first, so in practice
31
+ * there are none; guarding anyway keeps this honest if it is ever called on a
32
+ * span that still has content.
33
+ */
34
+ export declare function closeGap(score: Score, trackId: UUID, fromTick: number, ticks: number): Score;
35
+ /**
36
+ * Writes a note at `startTick`, moving everything at or after it on the same
37
+ * track later by the note's own duration.
38
+ *
39
+ * Only that track moves. The edited part deliberately shifts against its
40
+ * accompaniment — that is what inserting into one part means, and it is the
41
+ * reason the mode exists.
42
+ */
43
+ export declare function insertWithRippleCommand(params: RippleInsertParams, label: string): ScoreCommand;
44
+ //# sourceMappingURL=ripple-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ripple-commands.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/ripple-commands.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEvE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AA4BF;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,IAAI,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,KAAK,CAkBP;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,KAAK,CAkBP;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,GACZ,YAAY,CAsBd"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Insert that pushes existing music out of the way, rather than overwriting it.
3
+ *
4
+ * Separate from `addNoteCommand`, which is measure-local by design ("an add
5
+ * always targets one named measure"): a ripple spans measures and can lengthen
6
+ * the score, so folding it into that command would break the contract its
7
+ * callers rely on.
8
+ */
9
+ import { appendMeasure } from '../score/factory.js';
10
+ import { allNotes, scoreEndTick } from '../score/queries.js';
11
+ import { addNoteCommand, moveNotesCommand } from './note-commands.js';
12
+ import { transformCommand } from './snapshot.js';
13
+ /**
14
+ * Room for `neededTicks` more music on `trackId`, adding measures to EVERY
15
+ * track when the shifted tail would pass the final barline.
16
+ *
17
+ * All tracks, not just the edited one: measures are per-track, but the layout
18
+ * takes each measure's width as the maximum density across tracks at that
19
+ * index, so a track with more measures than its neighbours would misalign
20
+ * every barline beneath it.
21
+ */
22
+ function growToFit(score, trackId, neededTicks) {
23
+ const lastNoteEnd = allNotes(score)
24
+ .filter(note => note.trackId === trackId)
25
+ .reduce((end, note) => Math.max(end, note.startTick + note.durationTicks), 0);
26
+ let next = score;
27
+ // Bounded by the arithmetic, not by trust: each pass adds a whole measure,
28
+ // so the shortfall strictly shrinks.
29
+ while (lastNoteEnd + neededTicks > scoreEndTick(next)) {
30
+ next = appendMeasure(next);
31
+ }
32
+ return next;
33
+ }
34
+ /**
35
+ * `score` with `ticks` of empty time opened up at `atTick` on `trackId`.
36
+ *
37
+ * Shared by note entry and paste: both need the same "push this track's later
38
+ * music out of the way, growing the score if it runs off the end" behaviour,
39
+ * and duplicating it would let the two drift.
40
+ */
41
+ export function makeRoom(score, trackId, atTick, ticks) {
42
+ const grown = growToFit(score, trackId, ticks);
43
+ // Gathered by id rather than by span: `moveNotesCommand` is the primitive
44
+ // that already knows how to re-place notes and reflow their measures.
45
+ const displaced = allNotes(grown)
46
+ .filter(note => note.trackId === trackId && note.startTick >= atTick)
47
+ .map(note => note.id);
48
+ if (displaced.length === 0)
49
+ return grown;
50
+ return moveNotesCommand(displaced, {
51
+ deltaTicks: ticks,
52
+ deltaSemitones: 0,
53
+ }, 'Move notes').execute(grown);
54
+ }
55
+ /**
56
+ * `score` with `ticks` of time removed at `fromTick` on `trackId` — everything
57
+ * after the gap slides earlier to close it.
58
+ *
59
+ * The inverse of `makeRoom`, and what "move the notes behind up" means after a
60
+ * cut. Measures are deliberately NOT removed: the piece keeps its length and
61
+ * gains rests at the end, because dropping a bar is a structural edit the user
62
+ * did not ask for, and it would misalign every other track.
63
+ *
64
+ * Notes that start inside the removed span are left where they are rather than
65
+ * dragged backwards past `fromTick`. A cut removes them first, so in practice
66
+ * there are none; guarding anyway keeps this honest if it is ever called on a
67
+ * span that still has content.
68
+ */
69
+ export function closeGap(score, trackId, fromTick, ticks) {
70
+ if (ticks <= 0)
71
+ return score;
72
+ const displaced = allNotes(score)
73
+ .filter(note => note.trackId === trackId && note.startTick >= fromTick + ticks)
74
+ .map(note => note.id);
75
+ if (displaced.length === 0)
76
+ return score;
77
+ return moveNotesCommand(displaced, {
78
+ deltaTicks: -ticks,
79
+ deltaSemitones: 0,
80
+ }, 'Move notes').execute(score);
81
+ }
82
+ /**
83
+ * Writes a note at `startTick`, moving everything at or after it on the same
84
+ * track later by the note's own duration.
85
+ *
86
+ * Only that track moves. The edited part deliberately shifts against its
87
+ * accompaniment — that is what inserting into one part means, and it is the
88
+ * reason the mode exists.
89
+ */
90
+ export function insertWithRippleCommand(params, label) {
91
+ return transformCommand(label, score => {
92
+ const shifted = makeRoom(score, params.trackId, params.startTick, params.durationTicks);
93
+ return addNoteCommand({
94
+ trackId: params.trackId,
95
+ measureId: params.measureId,
96
+ voiceIndex: params.voiceIndex,
97
+ pitch: params.pitch,
98
+ startTick: params.startTick,
99
+ durationTicks: params.durationTicks,
100
+ ...(params.articulation ? { articulation: params.articulation } : {}),
101
+ }, 'Add note').execute(shifted);
102
+ });
103
+ }
104
+ //# sourceMappingURL=ripple-commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ripple-commands.js","sourceRoot":"","sources":["../../../src/domain/commands/ripple-commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Generic `ScoreCommand` builders backed by Immer patches (Task 5 brief).
3
+ * `snapshotCommand` records, at each `execute` call, the forward/inverse
4
+ * patches between the score it's given and the result of running `mutate`
5
+ * over an Immer draft of it; `undo` replays the inverse patches. Because
6
+ * Immer's patches are derived mechanically from the actual before/after
7
+ * trees, `execute` then `undo` is guaranteed to reproduce a deep-equal
8
+ * score for *any* `mutate`, however structurally involved — command
9
+ * factories don't need to hand-write inverse logic.
10
+ *
11
+ * `execute` is re-run (recomputing patches against whatever score it's
12
+ * given) every time it's called, so a command replayed via
13
+ * `HistoryManager.redo` after an intervening `undo` still produces correct
14
+ * patches for a subsequent `undo`, as long as `undo` is always called with
15
+ * the exact score `execute` most recently returned (the standard linear
16
+ * undo-stack contract `HistoryManager` relies on).
17
+ */
18
+ import type { Draft } from 'immer';
19
+ import type { Score } from '../../index.js';
20
+ import type { CommandKind, ScoreCommand } from './types.js';
21
+ /**
22
+ * Builds a `ScoreCommand` whose forward/inverse changes are computed by
23
+ * running `mutate` over an Immer draft of the score. `label` is fixed at
24
+ * construction time (per `ScoreCommand`); `mutate` may close over whatever
25
+ * parameters the command needs (event ids, new values, etc.), captured at
26
+ * construction.
27
+ */
28
+ export declare function snapshotCommand(label: string, mutate: (draft: Draft<Score>) => void, kind?: CommandKind): ScoreCommand;
29
+ /**
30
+ * Convenience wrapper over `snapshotCommand` for the common case where a
31
+ * command's actual logic is easiest to write as an ordinary pure
32
+ * `(Score) => Score` function (most factories in this module): `transform`
33
+ * runs against a plain snapshot of the current draft (via Immer's
34
+ * `current`), and its result is adopted wholesale as the new draft state,
35
+ * so `mutate`'s Immer-facing bookkeeping stays out of each factory.
36
+ */
37
+ export declare function transformCommand(label: string, transform: (score: Score) => Score, kind?: CommandKind): ScoreCommand;
38
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,KAAK,EAAS,MAAM,OAAO,CAAC;AAQ1C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI5D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,IAAI,EACrC,IAAI,GAAE,WAAuB,GAC5B,YAAY,CAkBd;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,EAClC,IAAI,GAAE,WAAuB,GAC5B,YAAY,CASd"}
@@ -0,0 +1,44 @@
1
+ import { applyPatches, current, enablePatches, produceWithPatches, } from 'immer';
2
+ import { createId } from '../score/ids.js';
3
+ enablePatches();
4
+ /**
5
+ * Builds a `ScoreCommand` whose forward/inverse changes are computed by
6
+ * running `mutate` over an Immer draft of the score. `label` is fixed at
7
+ * construction time (per `ScoreCommand`); `mutate` may close over whatever
8
+ * parameters the command needs (event ids, new values, etc.), captured at
9
+ * construction.
10
+ */
11
+ export function snapshotCommand(label, mutate, kind = 'content') {
12
+ let inversePatches = null;
13
+ return {
14
+ id: createId(),
15
+ label,
16
+ timestamp: Date.now(),
17
+ kind,
18
+ execute(score) {
19
+ const [next, , inverse] = produceWithPatches(score, mutate);
20
+ inversePatches = inverse;
21
+ return next;
22
+ },
23
+ undo(score) {
24
+ if (!inversePatches)
25
+ return score;
26
+ return applyPatches(score, inversePatches);
27
+ },
28
+ };
29
+ }
30
+ /**
31
+ * Convenience wrapper over `snapshotCommand` for the common case where a
32
+ * command's actual logic is easiest to write as an ordinary pure
33
+ * `(Score) => Score` function (most factories in this module): `transform`
34
+ * runs against a plain snapshot of the current draft (via Immer's
35
+ * `current`), and its result is adopted wholesale as the new draft state,
36
+ * so `mutate`'s Immer-facing bookkeeping stays out of each factory.
37
+ */
38
+ export function transformCommand(label, transform, kind = 'content') {
39
+ return snapshotCommand(label, draft => {
40
+ const next = transform(current(draft));
41
+ Object.assign(draft, next);
42
+ }, kind);
43
+ }
44
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/domain/commands/snapshot.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,YAAY,EACZ,OAAO,EACP,aAAa,EACb,kBAAkB,GACnB,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAI3C,aAAa,EAAE,CAAC;AAEhB;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,MAAqC,EACrC,OAAoB,SAAS;IAE7B,IAAI,cAAc,GAAmB,IAAI,CAAC;IAE1C,OAAO;QACL,EAAE,EAAE,QAAQ,EAAE;QACd,KAAK;QACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,IAAI;QACJ,OAAO,CAAC,KAAY;YAClB,MAAM,CAAC,IAAI,EAAE,AAAD,EAAG,OAAO,CAAC,GAAG,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC5D,cAAc,GAAG,OAAO,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,KAAY;YACf,IAAI,CAAC,cAAc;gBAAE,OAAO,KAAK,CAAC;YAClC,OAAO,YAAY,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAa,EACb,SAAkC,EAClC,OAAoB,SAAS;IAE7B,OAAO,eAAe,CACpB,KAAK,EACL,KAAK,CAAC,EAAE;QACN,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,CAAU,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC,EACD,IAAI,CACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { CreateTrackOptions } from '../score/factory.js';
2
+ import type { Clef, KeySignature, Measure, ScoreMetadata, TimeSignature, Track, UUID } from '../../index.js';
3
+ import type { ScoreCommand } from './types.js';
4
+ /** Appends one fully-rested measure to every track (reuses Task 3's `appendMeasure`). */
5
+ export declare function addMeasureCommand(label: string): ScoreCommand;
6
+ /** Removes the measure at `measureIndex` from every track, retracking subsequent measures' ticks. */
7
+ export declare function deleteMeasureCommand(measureIndex: number, label: string): ScoreCommand;
8
+ /** Builds a fully-rested measure matching an existing measure's position/signatures, for a newly added track. */
9
+ export declare function restMeasureLike(reference: Measure, trackId: UUID): Measure;
10
+ /** Adds a new track, fully-rested with the same measure layout (count/signatures) as the score's first existing track. */
11
+ export declare function addTrackCommand(options: CreateTrackOptions, label: string): ScoreCommand;
12
+ /** Removes the track with id `trackId`. */
13
+ export declare function deleteTrackCommand(trackId: UUID, label: string): ScoreCommand;
14
+ /**
15
+ * Changes one measure's time signature (on every track that has a measure
16
+ * with that id — normally exactly one, but the search covers every track
17
+ * defensively). Recomputes the measure's `durationTicks`, reflows its
18
+ * voices to fit the new length, and retracks every subsequent measure via
19
+ * `rebuildMeasureTicks`.
20
+ */
21
+ export declare function changeTimeSignatureCommand(measureId: UUID, timeSignature: TimeSignature, label: string): ScoreCommand;
22
+ /** Changes one measure's key signature. Existing note spellings are left as-is. */
23
+ export declare function changeKeySignatureCommand(measureId: UUID, keySignature: KeySignature, label: string): ScoreCommand;
24
+ /** Changes a track's clef, reinterpreting its program if it crosses into or out of percussion. */
25
+ export declare function changeClefCommand(trackId: UUID, clef: Clef, label: string): ScoreCommand;
26
+ export type ChangeTempoParams = {
27
+ tempoEventId?: UUID;
28
+ tick: number;
29
+ bpm: number;
30
+ };
31
+ /** Updates an existing tempo event (`tempoEventId` given) or inserts a new one, keeping `tempoMap` sorted by tick. */
32
+ export declare function changeTempoCommand(params: ChangeTempoParams, label: string): ScoreCommand;
33
+ /** Patches score metadata (title/composer/description); `createdAt` is immutable, `updatedAt` is always refreshed. */
34
+ export declare function changeMetadataCommand(patch: Partial<Omit<ScoreMetadata, 'createdAt'>>, label: string): ScoreCommand;
35
+ export type TrackPropsPatch = Partial<Omit<Track, 'id' | 'measures'>>;
36
+ /** Patches a track's non-structural properties (name, instrument, MIDI program/channel, clef, volume, pan, mute, solo). */
37
+ export declare function changeTrackPropsCommand(trackId: UUID, patch: TrackPropsPatch, label: string): ScoreCommand;
38
+ //# sourceMappingURL=structure-commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure-commands.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/structure-commands.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D,OAAO,KAAK,EACV,IAAI,EACJ,YAAY,EACZ,OAAO,EAEP,aAAa,EAEb,aAAa,EACb,KAAK,EACL,IAAI,EACL,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,YAAY,CAAC;AAM5D,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAE7D;AAYD,qGAAqG;AACrG,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAID,iHAAiH;AACjH,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,GAAG,OAAO,CAyB1E;AAeD,0HAA0H;AAC1H,wBAAgB,eAAe,CAC7B,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAOD,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAE7E;AA4BD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,IAAI,EACf,aAAa,EAAE,aAAa,EAC5B,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAiBD,mFAAmF;AACnF,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,IAAI,EACf,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAqCD,kGAAkG;AAClG,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,IAAI,EACb,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAID,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAsBF,sHAAsH;AACtH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAkBD,sHAAsH;AACtH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,EAChD,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAID,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC;AA+BtE,2HAA2H;AAC3H,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,IAAI,EACb,KAAK,EAAE,eAAe,EACtB,KAAK,EAAE,MAAM,GACZ,YAAY,CAMd"}
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Structural (measure/track/score-property) command factories (spec §7,
3
+ * §14). Each factory wraps a pure `(Score) => Score` transform via
4
+ * `transformCommand`, matching the note-commands.ts pattern.
5
+ */
6
+ import { appendMeasure, createTrack, rebuildMeasureTicks, } from '../score/factory.js';
7
+ import { gmKitAt } from '../instruments/gm-kit.js';
8
+ import { gmInstrument } from '../instruments/gm.js';
9
+ import { createId } from '../score/ids.js';
10
+ import { measureDurationTicks } from '../time/ticks.js';
11
+ import { transformCommand } from './snapshot.js';
12
+ import { reflowVoice, touchMetadata, withTracks } from './reflow.js';
13
+ // ---- addMeasureCommand / deleteMeasureCommand ----------------------------------
14
+ /** Appends one fully-rested measure to every track (reuses Task 3's `appendMeasure`). */
15
+ export function addMeasureCommand(label) {
16
+ return transformCommand(label, score => appendMeasure(score));
17
+ }
18
+ function deleteMeasure(score, measureIndex) {
19
+ const tracks = score.tracks.map(track => {
20
+ const filtered = track.measures.filter(m => m.index !== measureIndex);
21
+ return filtered.length === track.measures.length
22
+ ? track
23
+ : { ...track, measures: filtered };
24
+ });
25
+ return rebuildMeasureTicks(withTracks(score, tracks));
26
+ }
27
+ /** Removes the measure at `measureIndex` from every track, retracking subsequent measures' ticks. */
28
+ export function deleteMeasureCommand(measureIndex, label) {
29
+ return transformCommand(label, score => deleteMeasure(score, measureIndex));
30
+ }
31
+ // ---- addTrackCommand / deleteTrackCommand --------------------------------------
32
+ /** Builds a fully-rested measure matching an existing measure's position/signatures, for a newly added track. */
33
+ export function restMeasureLike(reference, trackId) {
34
+ const voiceId = createId();
35
+ return {
36
+ id: createId(),
37
+ index: reference.index,
38
+ startTick: reference.startTick,
39
+ durationTicks: reference.durationTicks,
40
+ timeSignature: reference.timeSignature,
41
+ keySignature: reference.keySignature,
42
+ voices: [
43
+ {
44
+ id: voiceId,
45
+ name: 'Voice 1',
46
+ events: [
47
+ {
48
+ id: createId(),
49
+ startTick: reference.startTick,
50
+ durationTicks: reference.durationTicks,
51
+ voiceId,
52
+ trackId,
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ function addTrack(score, options) {
60
+ const track = createTrack(options);
61
+ const referenceTrack = score.tracks[0];
62
+ const measures = referenceTrack
63
+ ? referenceTrack.measures.map(m => restMeasureLike(m, track.id))
64
+ : [];
65
+ return {
66
+ ...score,
67
+ tracks: [...score.tracks, { ...track, measures }],
68
+ metadata: touchMetadata(score.metadata),
69
+ };
70
+ }
71
+ /** Adds a new track, fully-rested with the same measure layout (count/signatures) as the score's first existing track. */
72
+ export function addTrackCommand(options, label) {
73
+ return transformCommand(label, score => addTrack(score, options));
74
+ }
75
+ function deleteTrack(score, trackId) {
76
+ const tracks = score.tracks.filter(t => t.id !== trackId);
77
+ return withTracks(score, tracks);
78
+ }
79
+ /** Removes the track with id `trackId`. */
80
+ export function deleteTrackCommand(trackId, label) {
81
+ return transformCommand(label, score => deleteTrack(score, trackId));
82
+ }
83
+ // ---- changeTimeSignatureCommand / changeKeySignatureCommand -------------------
84
+ function changeTimeSignature(score, measureId, timeSignature) {
85
+ const tracks = score.tracks.map(track => {
86
+ const index = track.measures.findIndex(m => m.id === measureId);
87
+ if (index === -1)
88
+ return track;
89
+ const measure = track.measures[index];
90
+ const durationTicks = measureDurationTicks(timeSignature, score.ppq);
91
+ let updated = { ...measure, timeSignature, durationTicks };
92
+ for (const voice of updated.voices) {
93
+ updated = reflowVoice(updated, voice.id, track.id);
94
+ }
95
+ const measures = track.measures.slice();
96
+ measures[index] = updated;
97
+ return { ...track, measures };
98
+ });
99
+ return rebuildMeasureTicks(withTracks(score, tracks));
100
+ }
101
+ /**
102
+ * Changes one measure's time signature (on every track that has a measure
103
+ * with that id — normally exactly one, but the search covers every track
104
+ * defensively). Recomputes the measure's `durationTicks`, reflows its
105
+ * voices to fit the new length, and retracks every subsequent measure via
106
+ * `rebuildMeasureTicks`.
107
+ */
108
+ export function changeTimeSignatureCommand(measureId, timeSignature, label) {
109
+ return transformCommand(label, score => changeTimeSignature(score, measureId, timeSignature));
110
+ }
111
+ function changeKeySignature(score, measureId, keySignature) {
112
+ const tracks = score.tracks.map(track => {
113
+ const index = track.measures.findIndex(m => m.id === measureId);
114
+ if (index === -1)
115
+ return track;
116
+ const measures = track.measures.slice();
117
+ measures[index] = { ...measures[index], keySignature };
118
+ return { ...track, measures };
119
+ });
120
+ return withTracks(score, tracks);
121
+ }
122
+ /** Changes one measure's key signature. Existing note spellings are left as-is. */
123
+ export function changeKeySignatureCommand(measureId, keySignature, label) {
124
+ return transformCommand(label, score => changeKeySignature(score, measureId, keySignature));
125
+ }
126
+ // ---- changeClefCommand -----------------------------------------------------------
127
+ function changeClef(score, trackId, clef) {
128
+ const tracks = score.tracks.map(t => t.id === trackId ? { ...t, clef, ...programForClef(t, clef) } : t);
129
+ return withTracks(score, tracks);
130
+ }
131
+ /**
132
+ * The program a track should carry once its clef is `clef`.
133
+ *
134
+ * `midiProgram` addresses a drum kit on a percussion track and an instrument
135
+ * anywhere else, so crossing that boundary reinterprets the number it already
136
+ * holds. Going in, it is snapped to the kit whose region it falls in; coming
137
+ * out, it is reset to Acoustic Grand, because a kit address as an instrument is
138
+ * whatever program happens to sit there — Brush becomes a violin. `name` is
139
+ * deliberately left alone: it is the user's, unlike `instrumentName`, which
140
+ * describes the sound and so has to follow it.
141
+ */
142
+ function programForClef(track, clef) {
143
+ const wasPercussion = track.clef === 'percussion';
144
+ const isPercussion = clef === 'percussion';
145
+ if (wasPercussion === isPercussion)
146
+ return {};
147
+ if (isPercussion) {
148
+ const kit = gmKitAt(track.midiProgram);
149
+ return { midiProgram: kit.program, instrumentName: kit.name };
150
+ }
151
+ const instrument = gmInstrument(0);
152
+ return {
153
+ midiProgram: 0,
154
+ instrumentName: instrument?.name ?? 'Acoustic Grand Piano',
155
+ };
156
+ }
157
+ /** Changes a track's clef, reinterpreting its program if it crosses into or out of percussion. */
158
+ export function changeClefCommand(trackId, clef, label) {
159
+ return transformCommand(label, score => changeClef(score, trackId, clef));
160
+ }
161
+ function changeTempo(score, params) {
162
+ const tempoMap = params.tempoEventId
163
+ ? score.tempoMap.map(e => e.id === params.tempoEventId
164
+ ? { ...e, tick: params.tick, bpm: params.bpm }
165
+ : e)
166
+ : [
167
+ ...score.tempoMap,
168
+ { id: createId(), tick: params.tick, bpm: params.bpm },
169
+ ];
170
+ const sorted = [...tempoMap].sort((a, b) => a.tick - b.tick);
171
+ return {
172
+ ...score,
173
+ tempoMap: sorted,
174
+ metadata: touchMetadata(score.metadata),
175
+ };
176
+ }
177
+ /** Updates an existing tempo event (`tempoEventId` given) or inserts a new one, keeping `tempoMap` sorted by tick. */
178
+ export function changeTempoCommand(params, label) {
179
+ return transformCommand(label, score => changeTempo(score, params));
180
+ }
181
+ // ---- changeMetadataCommand --------------------------------------------------------
182
+ function changeMetadata(score, patch) {
183
+ return {
184
+ ...score,
185
+ metadata: {
186
+ ...score.metadata,
187
+ ...patch,
188
+ updatedAt: new Date().toISOString(),
189
+ },
190
+ };
191
+ }
192
+ /** Patches score metadata (title/composer/description); `createdAt` is immutable, `updatedAt` is always refreshed. */
193
+ export function changeMetadataCommand(patch, label) {
194
+ return transformCommand(label, score => changeMetadata(score, patch));
195
+ }
196
+ function changeTrackProps(score, trackId, patch) {
197
+ const tracks = score.tracks.map(t => t.id === trackId ? { ...t, ...patch } : t);
198
+ return withTracks(score, tracks);
199
+ }
200
+ /**
201
+ * The track properties that are mixing rather than music.
202
+ *
203
+ * `changeTrackPropsCommand` carries a partial patch and serves both purposes —
204
+ * `{ muted }` is mixing, `{ name }` and `{ midiProgram }` are the score — so
205
+ * the content/mix classification cannot be made on the command's type. It is
206
+ * made here, where the patch is, rather than in a switch elsewhere that would
207
+ * have to be kept in step with this list.
208
+ */
209
+ const MIX_PROPS = new Set(['volume', 'pan', 'muted', 'solo']);
210
+ /** A patch is mix only if *every* key in it is. An empty patch changes nothing, so it counts as mix. */
211
+ function trackPatchKind(patch) {
212
+ return Object.keys(patch).every(key => MIX_PROPS.has(key))
213
+ ? 'mix'
214
+ : 'content';
215
+ }
216
+ /** Patches a track's non-structural properties (name, instrument, MIDI program/channel, clef, volume, pan, mute, solo). */
217
+ export function changeTrackPropsCommand(trackId, patch, label) {
218
+ return transformCommand(label, score => changeTrackProps(score, trackId, patch), trackPatchKind(patch));
219
+ }
220
+ //# sourceMappingURL=structure-commands.js.map