@sudobility/music_types 0.11.29 → 0.11.31

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 (229) hide show
  1. package/dist/domain/commands/edit-commands.d.ts +3 -3
  2. package/dist/domain/commands/edit-commands.d.ts.map +1 -1
  3. package/dist/domain/commands/edit-commands.js +25 -25
  4. package/dist/domain/commands/edit-commands.js.map +1 -1
  5. package/dist/domain/commands/history.d.ts +32 -0
  6. package/dist/domain/commands/history.d.ts.map +1 -0
  7. package/dist/domain/commands/history.js +63 -0
  8. package/dist/domain/commands/history.js.map +1 -0
  9. package/dist/domain/commands/note-commands.d.ts +147 -3
  10. package/dist/domain/commands/note-commands.d.ts.map +1 -1
  11. package/dist/domain/commands/note-commands.js +438 -46
  12. package/dist/domain/commands/note-commands.js.map +1 -1
  13. package/dist/domain/commands/reflow.d.ts +1 -1
  14. package/dist/domain/commands/reflow.js +17 -17
  15. package/dist/domain/commands/reflow.js.map +1 -1
  16. package/dist/domain/commands/region-commands.d.ts +4 -4
  17. package/dist/domain/commands/region-commands.d.ts.map +1 -1
  18. package/dist/domain/commands/region-commands.js +7 -7
  19. package/dist/domain/commands/region-commands.js.map +1 -1
  20. package/dist/domain/commands/relocate-commands.d.ts +3 -3
  21. package/dist/domain/commands/relocate-commands.js +15 -15
  22. package/dist/domain/commands/relocate-commands.js.map +1 -1
  23. package/dist/domain/commands/ripple-commands.d.ts +2 -2
  24. package/dist/domain/commands/ripple-commands.js +13 -13
  25. package/dist/domain/commands/ripple-commands.js.map +1 -1
  26. package/dist/domain/commands/snapshot.d.ts +3 -3
  27. package/dist/domain/commands/snapshot.js +5 -5
  28. package/dist/domain/commands/snapshot.js.map +1 -1
  29. package/dist/domain/commands/structure-commands.d.ts +86 -5
  30. package/dist/domain/commands/structure-commands.d.ts.map +1 -1
  31. package/dist/domain/commands/structure-commands.js +272 -36
  32. package/dist/domain/commands/structure-commands.js.map +1 -1
  33. package/dist/domain/commands/track-commands.d.ts +2 -2
  34. package/dist/domain/commands/track-commands.js +7 -7
  35. package/dist/domain/commands/track-commands.js.map +1 -1
  36. package/dist/domain/commands/types.d.ts +2 -2
  37. package/dist/domain/generation/replacement-region.d.ts +32 -0
  38. package/dist/domain/generation/replacement-region.d.ts.map +1 -0
  39. package/dist/domain/generation/replacement-region.js +79 -0
  40. package/dist/domain/generation/replacement-region.js.map +1 -0
  41. package/dist/domain/instruments/gm-icon.d.ts +12 -0
  42. package/dist/domain/instruments/gm-icon.d.ts.map +1 -0
  43. package/dist/domain/instruments/gm-icon.js +327 -0
  44. package/dist/domain/instruments/gm-icon.js.map +1 -0
  45. package/dist/domain/instruments/gm-kit.js +9 -9
  46. package/dist/domain/instruments/gm-kit.js.map +1 -1
  47. package/dist/domain/instruments/gm-percussion.js +48 -48
  48. package/dist/domain/instruments/gm-percussion.js.map +1 -1
  49. package/dist/domain/instruments/gm-polyphony.js +6 -6
  50. package/dist/domain/instruments/gm-range.js +6 -6
  51. package/dist/domain/instruments/gm-transposition.js +1 -1
  52. package/dist/domain/instruments/gm.d.ts +1 -1
  53. package/dist/domain/instruments/gm.js +161 -161
  54. package/dist/domain/instruments/gm.js.map +1 -1
  55. package/dist/domain/instruments/icon-art.d.ts +75 -0
  56. package/dist/domain/instruments/icon-art.d.ts.map +1 -0
  57. package/dist/domain/instruments/icon-art.js +93 -0
  58. package/dist/domain/instruments/icon-art.js.map +1 -0
  59. package/dist/domain/instruments/instrument-fit.d.ts +31 -0
  60. package/dist/domain/instruments/instrument-fit.d.ts.map +1 -0
  61. package/dist/domain/instruments/instrument-fit.js +117 -0
  62. package/dist/domain/instruments/instrument-fit.js.map +1 -0
  63. package/dist/domain/instruments/instrument-options.d.ts +62 -0
  64. package/dist/domain/instruments/instrument-options.d.ts.map +1 -0
  65. package/dist/domain/instruments/instrument-options.js +69 -0
  66. package/dist/domain/instruments/instrument-options.js.map +1 -0
  67. package/dist/domain/instruments/track-instrument.d.ts +80 -0
  68. package/dist/domain/instruments/track-instrument.d.ts.map +1 -0
  69. package/dist/domain/instruments/track-instrument.js +102 -0
  70. package/dist/domain/instruments/track-instrument.js.map +1 -0
  71. package/dist/domain/notation/chord-symbol.d.ts +26 -0
  72. package/dist/domain/notation/chord-symbol.d.ts.map +1 -0
  73. package/dist/domain/notation/chord-symbol.js +32 -0
  74. package/dist/domain/notation/chord-symbol.js.map +1 -0
  75. package/dist/domain/notation/lyric-syllables.d.ts +15 -0
  76. package/dist/domain/notation/lyric-syllables.d.ts.map +1 -0
  77. package/dist/domain/notation/lyric-syllables.js +15 -0
  78. package/dist/domain/notation/lyric-syllables.js.map +1 -0
  79. package/dist/domain/notation/music-vocabulary.d.ts +59 -0
  80. package/dist/domain/notation/music-vocabulary.d.ts.map +1 -0
  81. package/dist/domain/notation/music-vocabulary.js +164 -0
  82. package/dist/domain/notation/music-vocabulary.js.map +1 -0
  83. package/dist/domain/pitch/pitch.d.ts +1 -1
  84. package/dist/domain/pitch/pitch.js +29 -29
  85. package/dist/domain/pitch/transpose.d.ts +1 -1
  86. package/dist/domain/pitch/transpose.js +2 -2
  87. package/dist/domain/quantization/quantize.d.ts +2 -2
  88. package/dist/domain/quantization/quantize.js +9 -9
  89. package/dist/domain/quantization/quantize.js.map +1 -1
  90. package/dist/domain/score/articulation.d.ts +56 -0
  91. package/dist/domain/score/articulation.d.ts.map +1 -0
  92. package/dist/domain/score/articulation.js +56 -0
  93. package/dist/domain/score/articulation.js.map +1 -0
  94. package/dist/domain/score/bar-numbers.d.ts +38 -0
  95. package/dist/domain/score/bar-numbers.d.ts.map +1 -0
  96. package/dist/domain/score/bar-numbers.js +49 -0
  97. package/dist/domain/score/bar-numbers.js.map +1 -0
  98. package/dist/domain/score/collapse-rests.d.ts +23 -0
  99. package/dist/domain/score/collapse-rests.d.ts.map +1 -0
  100. package/dist/domain/score/collapse-rests.js +78 -0
  101. package/dist/domain/score/collapse-rests.js.map +1 -0
  102. package/dist/domain/score/cue-notes.d.ts +24 -0
  103. package/dist/domain/score/cue-notes.d.ts.map +1 -0
  104. package/dist/domain/score/cue-notes.js +105 -0
  105. package/dist/domain/score/cue-notes.js.map +1 -0
  106. package/dist/domain/score/dynamics.d.ts +62 -0
  107. package/dist/domain/score/dynamics.d.ts.map +1 -0
  108. package/dist/domain/score/dynamics.js +142 -0
  109. package/dist/domain/score/dynamics.js.map +1 -0
  110. package/dist/domain/score/effective-clef.d.ts +42 -0
  111. package/dist/domain/score/effective-clef.d.ts.map +1 -0
  112. package/dist/domain/score/effective-clef.js +39 -0
  113. package/dist/domain/score/effective-clef.js.map +1 -0
  114. package/dist/domain/score/extract-part.d.ts +10 -0
  115. package/dist/domain/score/extract-part.d.ts.map +1 -0
  116. package/dist/domain/score/extract-part.js +44 -0
  117. package/dist/domain/score/extract-part.js.map +1 -0
  118. package/dist/domain/score/factory.d.ts +1 -1
  119. package/dist/domain/score/factory.js +12 -12
  120. package/dist/domain/score/factory.js.map +1 -1
  121. package/dist/domain/score/fermata-tempo.d.ts +40 -0
  122. package/dist/domain/score/fermata-tempo.d.ts.map +1 -0
  123. package/dist/domain/score/fermata-tempo.js +95 -0
  124. package/dist/domain/score/fermata-tempo.js.map +1 -0
  125. package/dist/domain/score/flatten.d.ts +32 -0
  126. package/dist/domain/score/flatten.d.ts.map +1 -0
  127. package/dist/domain/score/flatten.js +132 -0
  128. package/dist/domain/score/flatten.js.map +1 -0
  129. package/dist/domain/score/fragment.d.ts +3 -3
  130. package/dist/domain/score/fragment.js +8 -8
  131. package/dist/domain/score/fragment.js.map +1 -1
  132. package/dist/domain/score/ids.d.ts +1 -1
  133. package/dist/domain/score/ids.js +6 -6
  134. package/dist/domain/score/ottava.d.ts +41 -0
  135. package/dist/domain/score/ottava.d.ts.map +1 -0
  136. package/dist/domain/score/ottava.js +98 -0
  137. package/dist/domain/score/ottava.js.map +1 -0
  138. package/dist/domain/score/performance-timeline.d.ts +42 -0
  139. package/dist/domain/score/performance-timeline.d.ts.map +1 -0
  140. package/dist/domain/score/performance-timeline.js +78 -0
  141. package/dist/domain/score/performance-timeline.js.map +1 -0
  142. package/dist/domain/score/queries.d.ts +2 -2
  143. package/dist/domain/score/queries.js +7 -7
  144. package/dist/domain/score/queries.js.map +1 -1
  145. package/dist/domain/score/rehearsal-marks.d.ts +30 -0
  146. package/dist/domain/score/rehearsal-marks.d.ts.map +1 -0
  147. package/dist/domain/score/rehearsal-marks.js +126 -0
  148. package/dist/domain/score/rehearsal-marks.js.map +1 -0
  149. package/dist/domain/score/repeat-order.d.ts +21 -0
  150. package/dist/domain/score/repeat-order.d.ts.map +1 -0
  151. package/dist/domain/score/repeat-order.js +123 -0
  152. package/dist/domain/score/repeat-order.js.map +1 -0
  153. package/dist/domain/score/snapshot-tree.d.ts +23 -0
  154. package/dist/domain/score/snapshot-tree.d.ts.map +1 -0
  155. package/dist/domain/score/snapshot-tree.js +45 -0
  156. package/dist/domain/score/snapshot-tree.js.map +1 -0
  157. package/dist/domain/score/ties.d.ts +1 -1
  158. package/dist/domain/score/ties.js +7 -7
  159. package/dist/domain/score/ties.js.map +1 -1
  160. package/dist/domain/score/written-pitch.d.ts +33 -0
  161. package/dist/domain/score/written-pitch.d.ts.map +1 -0
  162. package/dist/domain/score/written-pitch.js +110 -0
  163. package/dist/domain/score/written-pitch.js.map +1 -0
  164. package/dist/domain/selection/range-select.d.ts +13 -0
  165. package/dist/domain/selection/range-select.d.ts.map +1 -0
  166. package/dist/domain/selection/range-select.js +40 -0
  167. package/dist/domain/selection/range-select.js.map +1 -0
  168. package/dist/domain/selection/selection-editing.d.ts +30 -0
  169. package/dist/domain/selection/selection-editing.d.ts.map +1 -0
  170. package/dist/domain/selection/selection-editing.js +31 -0
  171. package/dist/domain/selection/selection-editing.js.map +1 -0
  172. package/dist/domain/selection/selection.d.ts +2 -2
  173. package/dist/domain/selection/selection.js +7 -7
  174. package/dist/domain/selection/selection.js.map +1 -1
  175. package/dist/domain/selection/types.d.ts +2 -2
  176. package/dist/domain/time/duration-modifiers.d.ts +40 -0
  177. package/dist/domain/time/duration-modifiers.d.ts.map +1 -0
  178. package/dist/domain/time/duration-modifiers.js +50 -0
  179. package/dist/domain/time/duration-modifiers.js.map +1 -0
  180. package/dist/domain/time/duration-selection.d.ts +23 -0
  181. package/dist/domain/time/duration-selection.d.ts.map +1 -0
  182. package/dist/domain/time/duration-selection.js +50 -0
  183. package/dist/domain/time/duration-selection.js.map +1 -0
  184. package/dist/domain/time/durations.js +6 -6
  185. package/dist/domain/time/durations.js.map +1 -1
  186. package/dist/domain/time/fraction.d.ts +1 -1
  187. package/dist/domain/time/fraction.js +1 -1
  188. package/dist/domain/time/tap-to-note.d.ts +21 -0
  189. package/dist/domain/time/tap-to-note.d.ts.map +1 -0
  190. package/dist/domain/time/tap-to-note.js +43 -0
  191. package/dist/domain/time/tap-to-note.js.map +1 -0
  192. package/dist/domain/time/tempo-map.d.ts +1 -1
  193. package/dist/domain/time/ticks.d.ts +1 -1
  194. package/dist/domain/time/ticks.d.ts.map +1 -1
  195. package/dist/domain/time/ticks.js +14 -14
  196. package/dist/domain/time/ticks.js.map +1 -1
  197. package/dist/domain/time/tuplets.d.ts +42 -0
  198. package/dist/domain/time/tuplets.d.ts.map +1 -0
  199. package/dist/domain/time/tuplets.js +53 -0
  200. package/dist/domain/time/tuplets.js.map +1 -0
  201. package/dist/domain/validation/issues.d.ts +1 -1
  202. package/dist/domain/validation/issues.js +20 -20
  203. package/dist/domain/validation/validator.d.ts +2 -2
  204. package/dist/domain/validation/validator.d.ts.map +1 -1
  205. package/dist/domain/validation/validator.js +31 -31
  206. package/dist/domain/validation/validator.js.map +1 -1
  207. package/dist/domain/voicing/allocate.d.ts +2 -2
  208. package/dist/domain/voicing/allocate.js +6 -6
  209. package/dist/domain/voicing/allocate.js.map +1 -1
  210. package/dist/index.d.ts +756 -56
  211. package/dist/index.d.ts.map +1 -1
  212. package/dist/index.js +164 -75
  213. package/dist/index.js.map +1 -1
  214. package/dist/platform/audio.d.ts.map +1 -1
  215. package/dist/platform/file.d.ts.map +1 -1
  216. package/dist/platform/index.d.ts +7 -7
  217. package/dist/platform/index.js +7 -7
  218. package/dist/platform/midi-input.d.ts +2 -2
  219. package/dist/platform/midi.d.ts.map +1 -1
  220. package/dist/platform/mod.d.ts +2 -2
  221. package/dist/platform/playback.d.ts +7 -7
  222. package/dist/platform/xml.js +1 -1
  223. package/dist/services/regeneration/controller.d.ts +6 -6
  224. package/dist/services/regeneration/controller.js +12 -12
  225. package/dist/services/regeneration/controller.js.map +1 -1
  226. package/dist/test/fixtures.d.ts +1 -1
  227. package/dist/test/fixtures.js +103 -103
  228. package/dist/test/fixtures.js.map +1 -1
  229. package/package.json +1 -1
@@ -10,13 +10,14 @@
10
10
  * note that crosses a measure boundary into tied segments via Task 3's
11
11
  * `splitNoteAcrossMeasures`, per the Task 5 brief.
12
12
  */
13
- import { createId } from '../score/ids.js';
14
- import { splitNoteAcrossMeasures } from '../score/ties.js';
15
- import { isNoteEvent } from '../../index.js';
16
- import { ticksFor } from '../time/ticks.js';
17
- import { transposePitch } from '../pitch/transpose.js';
18
- import { transformCommand } from './snapshot.js';
19
- import { clearDanglingTies, ensureVoiceAtIndex, insertNoteIntoTrack, removeNotesFromTrack, reflowVoice, withTracks, } from './reflow.js';
13
+ import { createId } from "../score/ids.js";
14
+ import { splitNoteAcrossMeasures } from "../score/ties.js";
15
+ import { isNoteEvent } from "../../index.js";
16
+ import { findEvent } from "../score/queries.js";
17
+ import { ticksFor } from "../time/ticks.js";
18
+ import { transposePitch } from "../pitch/transpose.js";
19
+ import { transformCommand } from "./snapshot.js";
20
+ import { clearDanglingTies, ensureVoiceAtIndex, insertNoteIntoTrack, removeNotesFromTrack, reflowVoice, withTracks, } from "./reflow.js";
20
21
  // ---- shared traversal helpers ------------------------------------------------
21
22
  /**
22
23
  * Applies `updater` to every note event in `score` whose id is in
@@ -26,14 +27,14 @@ import { clearDanglingTies, ensureVoiceAtIndex, insertNoteIntoTrack, removeNotes
26
27
  */
27
28
  function mapNotes(score, eventIds, updater) {
28
29
  const idSet = new Set(eventIds);
29
- const tracks = score.tracks.map(track => {
30
- const measures = track.measures.map(measure => {
31
- const voices = measure.voices.map(voice => {
32
- if (!voice.events.some(e => idSet.has(e.id)))
30
+ const tracks = score.tracks.map((track) => {
31
+ const measures = track.measures.map((measure) => {
32
+ const voices = measure.voices.map((voice) => {
33
+ if (!voice.events.some((e) => idSet.has(e.id)))
33
34
  return voice;
34
35
  return {
35
36
  ...voice,
36
- events: voice.events.map(event => isNoteEvent(event) && idSet.has(event.id) ? updater(event) : event),
37
+ events: voice.events.map((event) => isNoteEvent(event) && idSet.has(event.id) ? updater(event) : event),
37
38
  };
38
39
  });
39
40
  const measureChanged = voices.some((v, i) => v !== measure.voices[i]);
@@ -45,10 +46,10 @@ function mapNotes(score, eventIds, updater) {
45
46
  return withTracks(score, tracks);
46
47
  }
47
48
  function addNote(score, params) {
48
- const tracks = score.tracks.map(track => {
49
+ const tracks = score.tracks.map((track) => {
49
50
  if (track.id !== params.trackId)
50
51
  return track;
51
- const measureExists = track.measures.some(m => m.id === params.measureId);
52
+ const measureExists = track.measures.some((m) => m.id === params.measureId);
52
53
  if (!measureExists)
53
54
  return track;
54
55
  const note = {
@@ -57,7 +58,7 @@ function addNote(score, params) {
57
58
  startTick: params.startTick,
58
59
  durationTicks: params.durationTicks,
59
60
  velocity: params.velocity ?? 80,
60
- voiceId: '', // renumbered by insertNoteIntoTrack to the destination voice's id
61
+ voiceId: "", // renumbered by insertNoteIntoTrack to the destination voice's id
61
62
  trackId: track.id,
62
63
  ...(params.articulation ? { articulation: params.articulation } : {}),
63
64
  };
@@ -84,7 +85,7 @@ function addNote(score, params) {
84
85
  * further, or split it into per-measure `addNoteCommand` calls themselves.
85
86
  */
86
87
  export function addNoteCommand(params, label) {
87
- return transformCommand(label, score => addNote(score, params));
88
+ return transformCommand(label, (score) => addNote(score, params));
88
89
  }
89
90
  // ---- deleteEventsCommand -------------------------------------------------------
90
91
  function deleteEvents(score, eventIds) {
@@ -93,7 +94,7 @@ function deleteEvents(score, eventIds) {
93
94
  // note *before* actually removing the notes (clearDanglingTies needs
94
95
  // the deleted notes still in place to find their chain partners).
95
96
  const detied = clearDanglingTies(score, idSet);
96
- const tracks = detied.tracks.map(track => removeNotesFromTrack(track, idSet));
97
+ const tracks = detied.tracks.map((track) => removeNotesFromTrack(track, idSet));
97
98
  return withTracks(detied, tracks);
98
99
  }
99
100
  /**
@@ -102,13 +103,13 @@ function deleteEvents(score, eventIds) {
102
103
  * also being deleted, that partner's now-dangling tie flag is cleared.
103
104
  */
104
105
  export function deleteEventsCommand(eventIds, label) {
105
- return transformCommand(label, score => deleteEvents(score, eventIds));
106
+ return transformCommand(label, (score) => deleteEvents(score, eventIds));
106
107
  }
107
108
  function moveNotesOnTrack(track, eventIds, params) {
108
109
  const toMove = [];
109
- track.measures.forEach(measure => {
110
+ track.measures.forEach((measure) => {
110
111
  measure.voices.forEach((voice, voiceIndex) => {
111
- voice.events.forEach(event => {
112
+ voice.events.forEach((event) => {
112
113
  if (isNoteEvent(event) && eventIds.has(event.id))
113
114
  toMove.push({ note: event, voiceIndex });
114
115
  });
@@ -116,8 +117,8 @@ function moveNotesOnTrack(track, eventIds, params) {
116
117
  });
117
118
  if (toMove.length === 0)
118
119
  return track;
119
- let working = removeNotesFromTrack(track, new Set(toMove.map(m => m.note.id)));
120
- const boundaries = working.measures.map(m => m.startTick);
120
+ let working = removeNotesFromTrack(track, new Set(toMove.map((m) => m.note.id)));
121
+ const boundaries = working.measures.map((m) => m.startTick);
121
122
  const lastMeasure = working.measures[working.measures.length - 1];
122
123
  const trackEnd = lastMeasure
123
124
  ? lastMeasure.startTick + lastMeasure.durationTicks
@@ -125,7 +126,7 @@ function moveNotesOnTrack(track, eventIds, params) {
125
126
  for (const { note, voiceIndex } of toMove) {
126
127
  const maxStart = Math.max(0, trackEnd - note.durationTicks);
127
128
  const clampedStart = Math.max(0, Math.min(note.startTick + params.deltaTicks, maxStart));
128
- const destMeasure = working.measures.find(m => clampedStart >= m.startTick &&
129
+ const destMeasure = working.measures.find((m) => clampedStart >= m.startTick &&
129
130
  clampedStart < m.startTick + m.durationTicks) ?? lastMeasure;
130
131
  const newPitch = params.deltaSemitones !== 0
131
132
  ? transposePitch(note.pitch, params.deltaSemitones, destMeasure?.keySignature)
@@ -149,7 +150,7 @@ function moveNotes(score, eventIds, params) {
149
150
  // As in deleteEvents: clear dangling tie flags on any surviving partner
150
151
  // of a moved note before moveNotesOnTrack relocates it away.
151
152
  const detied = clearDanglingTies(score, idSet);
152
- const tracks = detied.tracks.map(track => moveNotesOnTrack(track, idSet, params));
153
+ const tracks = detied.tracks.map((track) => moveNotesOnTrack(track, idSet, params));
153
154
  return withTracks(detied, tracks);
154
155
  }
155
156
  /**
@@ -163,21 +164,21 @@ function moveNotes(score, eventIds, params) {
163
164
  * partner's now-dangling tie flag is cleared too.
164
165
  */
165
166
  export function moveNotesCommand(eventIds, params, label) {
166
- return transformCommand(label, score => moveNotes(score, eventIds, params));
167
+ return transformCommand(label, (score) => moveNotes(score, eventIds, params));
167
168
  }
168
169
  // ---- resizeNotesCommand / changeDurationCommand ----------------------------------
169
170
  function resizeNotes(score, eventIds, durationTicks) {
170
171
  const idSet = new Set(eventIds);
171
- const tracks = score.tracks.map(track => {
172
- const measures = track.measures.map(measure => {
172
+ const tracks = score.tracks.map((track) => {
173
+ const measures = track.measures.map((measure) => {
173
174
  let nextMeasure = measure;
174
175
  let changed = false;
175
176
  for (const voice of measure.voices) {
176
- if (!voice.events.some(e => idSet.has(e.id)))
177
+ if (!voice.events.some((e) => idSet.has(e.id)))
177
178
  continue;
178
179
  changed = true;
179
180
  const measureEnd = measure.startTick + measure.durationTicks;
180
- const updatedNotes = voice.events.filter(isNoteEvent).map(e => {
181
+ const updatedNotes = voice.events.filter(isNoteEvent).map((e) => {
181
182
  if (!idSet.has(e.id))
182
183
  return e;
183
184
  const clamped = Math.max(1, Math.min(durationTicks, measureEnd - e.startTick));
@@ -185,7 +186,7 @@ function resizeNotes(score, eventIds, durationTicks) {
185
186
  });
186
187
  const withResized = {
187
188
  ...nextMeasure,
188
- voices: nextMeasure.voices.map(v => v.id === voice.id ? { ...v, events: updatedNotes } : v),
189
+ voices: nextMeasure.voices.map((v) => v.id === voice.id ? { ...v, events: updatedNotes } : v),
189
190
  };
190
191
  nextMeasure = reflowVoice(withResized, voice.id, track.id);
191
192
  }
@@ -202,24 +203,24 @@ function resizeNotes(score, eventIds, durationTicks) {
202
203
  * current measure (no cross-measure re-splitting), then reflows.
203
204
  */
204
205
  export function resizeNotesCommand(eventIds, durationTicks, label) {
205
- return transformCommand(label, score => resizeNotes(score, eventIds, durationTicks));
206
+ return transformCommand(label, (score) => resizeNotes(score, eventIds, durationTicks));
206
207
  }
207
208
  /** Sets the given notes' duration to a named value (e.g. from the inspector), converted to ticks via the score's ppq. */
208
209
  export function changeDurationCommand(eventIds, duration, label) {
209
- return transformCommand(label, score => resizeNotes(score, eventIds, ticksFor(duration, score.ppq)));
210
+ return transformCommand(label, (score) => resizeNotes(score, eventIds, ticksFor(duration, score.ppq)));
210
211
  }
211
212
  // ---- simple per-note field commands ------------------------------------------
212
213
  /** Sets the given notes' pitch to an absolute `Pitch`. */
213
214
  export function changePitchCommand(eventIds, pitch, label) {
214
- return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, pitch })));
215
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => ({ ...note, pitch })));
215
216
  }
216
217
  /** Sets the given notes' velocity (0-127). */
217
218
  export function changeVelocityCommand(eventIds, velocity, label) {
218
- return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, velocity })));
219
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => ({ ...note, velocity })));
219
220
  }
220
221
  /** Sets (or clears, when `articulation` is `undefined`) the given notes' articulation. */
221
222
  export function changeArticulationCommand(eventIds, articulation, label) {
222
- return transformCommand(label, score => mapNotes(score, eventIds, note => {
223
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => {
223
224
  if (articulation)
224
225
  return { ...note, articulation };
225
226
  const updated = { ...note };
@@ -227,25 +228,416 @@ export function changeArticulationCommand(eventIds, articulation, label) {
227
228
  return updated;
228
229
  }));
229
230
  }
231
+ /**
232
+ * Sets (or clears, when `ornament` is `undefined`) the given notes' ornament.
233
+ *
234
+ * Shaped like `changeArticulationCommand` rather than like the fermata toggle,
235
+ * because an ornament is a choice among several rather than an on/off: a menu
236
+ * sets the one it names, and "None" clears. A note carries at most one sign —
237
+ * a trill that is also a turn is not a marking anybody writes.
238
+ */
239
+ export function changeOrnamentCommand(eventIds, ornament, label) {
240
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => {
241
+ if (ornament)
242
+ return { ...note, ornament };
243
+ const updated = { ...note };
244
+ delete updated.ornament;
245
+ return updated;
246
+ }));
247
+ }
248
+ /**
249
+ * Puts a fermata on the given notes, or takes it off the ones that have one.
250
+ *
251
+ * Toggles on the *whole selection together* rather than per note: the state is
252
+ * read from whether every selected note already carries one, so a mixed
253
+ * selection gains fermatas rather than flipping each note independently. A
254
+ * control that did the latter would leave a selection half-marked and look
255
+ * broken.
256
+ *
257
+ * Unlike `toggleSlurCommand` this needs no endpoints and refuses nothing — a
258
+ * fermata is a property of a single note, so one note is a perfectly good
259
+ * selection and each marked note stands alone. Chords are marked note by note
260
+ * because that is what the selection contains; the renderer draws one fermata
261
+ * per notehead position, which is what a chord with a pause looks like.
262
+ */
263
+ export function toggleFermataCommand(eventIds, label) {
264
+ return transformCommand(label, (score) => {
265
+ const notes = eventIds
266
+ .map((id) => findEvent(score, id))
267
+ .filter((event) => event !== null && isNoteEvent(event));
268
+ if (notes.length === 0)
269
+ return score;
270
+ // Only remove when every selected note already has one, so a selection
271
+ // that is partly marked becomes fully marked rather than inverting.
272
+ const allMarked = notes.every((note) => note.fermata);
273
+ return mapNotes(score, eventIds, (note) => {
274
+ if (allMarked) {
275
+ const updated = { ...note };
276
+ delete updated.fermata;
277
+ return updated;
278
+ }
279
+ return { ...note, fermata: true };
280
+ });
281
+ });
282
+ }
283
+ /**
284
+ * Sets or clears the dynamic marking the given notes start.
285
+ *
286
+ * A dynamic is stored on the note it applies *from* and governs until the next
287
+ * one on that track, so marking one note `f` is how a passage becomes loud —
288
+ * there is nothing to apply to the notes in between, and applying it to each
289
+ * would print a marking under every notehead.
290
+ */
291
+ export function changeDynamicCommand(eventIds, dynamic, label) {
292
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => {
293
+ if (dynamic)
294
+ return { ...note, dynamic };
295
+ const updated = { ...note };
296
+ delete updated.dynamic;
297
+ return updated;
298
+ }));
299
+ }
230
300
  /** Sets the given notes' pitch accidental, keeping their step/octave. */
231
301
  export function changeAccidentalCommand(eventIds, accidental, label) {
232
- return transformCommand(label, score => mapNotes(score, eventIds, note => ({
302
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => ({
233
303
  ...note,
234
304
  pitch: { ...note.pitch, accidental },
235
305
  })));
236
306
  }
237
307
  /** Toggles `tieStart` or `tieStop` on the given notes. */
238
308
  export function toggleTieCommand(eventIds, which, label) {
239
- return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, [which]: !note[which] })));
309
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => ({ ...note, [which]: !note[which] })));
310
+ }
311
+ /**
312
+ * Slurs a selection, or removes the slur it already carries.
313
+ *
314
+ * Takes the whole selection rather than a flag per note, because that is what
315
+ * a slur is: one mark over a run of notes. The caller says "slur these" and
316
+ * this decides which of them is the start and which the stop — marking them
317
+ * individually would let a user create a start with no stop, which draws
318
+ * nothing and is invisible to fix.
319
+ *
320
+ * Endpoints are the earliest and latest note by tick, not the order the ids
321
+ * arrived in: a selection built by shift-clicking around a phrase is still
322
+ * that phrase.
323
+ *
324
+ * Fewer than two notes cannot be slurred — a phrase mark over one note means
325
+ * nothing — and toggling off is offered when the span is already slurred, so
326
+ * the same control removes what it made.
327
+ */
328
+ export function toggleSlurCommand(eventIds, label) {
329
+ return transformCommand(label, (score) => {
330
+ const notes = eventIds
331
+ .map((id) => findEvent(score, id))
332
+ .filter((event) => event !== null && isNoteEvent(event))
333
+ .sort((a, b) => a.startTick - b.startTick);
334
+ if (notes.length < 2)
335
+ return score;
336
+ const first = notes[0];
337
+ const last = notes[notes.length - 1];
338
+ const alreadySlurred = Boolean(first.slurStart && last.slurStop);
339
+ return mapNotes(score, eventIds, (note) => {
340
+ const updated = { ...note };
341
+ delete updated.slurStart;
342
+ delete updated.slurStop;
343
+ if (alreadySlurred)
344
+ return updated;
345
+ if (note.id === first.id)
346
+ updated.slurStart = true;
347
+ if (note.id === last.id)
348
+ updated.slurStop = true;
349
+ return updated;
350
+ });
351
+ });
352
+ }
353
+ /**
354
+ * Writes a hairpin across the selection, or removes the one it already has.
355
+ *
356
+ * Shaped like `toggleSlurCommand`, because a hairpin is the same kind of thing
357
+ * — one mark over a run of notes. The caller says "crescendo these" and this
358
+ * decides which of them opens it and which closes it; marking them one at a
359
+ * time would let a user create an opening with no close, which draws nothing
360
+ * and is invisible to fix.
361
+ *
362
+ * Endpoints are the earliest and latest note **by tick**, not the order the
363
+ * ids arrived in, so a selection built by shift-clicking around a phrase is
364
+ * still that phrase.
365
+ *
366
+ * Fewer than two notes is refused: a wedge over one note has nowhere to open
367
+ * to. Re-applying the **same** direction to a span that already carries it
368
+ * removes it, so one control undoes what it made; applying the **other**
369
+ * direction flips it, which is what reaching for the other button means.
370
+ */
371
+ export function toggleHairpinCommand(eventIds, hairpin, label) {
372
+ return transformCommand(label, (score) => {
373
+ const notes = eventIds
374
+ .map((id) => findEvent(score, id))
375
+ .filter((event) => event !== null && isNoteEvent(event))
376
+ .sort((a, b) => a.startTick - b.startTick);
377
+ if (notes.length < 2)
378
+ return score;
379
+ const first = notes[0];
380
+ const last = notes[notes.length - 1];
381
+ const sameAlready = first.hairpinStart === hairpin && Boolean(last.hairpinStop);
382
+ return mapNotes(score, eventIds, (note) => {
383
+ const updated = { ...note };
384
+ delete updated.hairpinStart;
385
+ delete updated.hairpinStop;
386
+ if (sameAlready)
387
+ return updated;
388
+ if (note.id === first.id)
389
+ updated.hairpinStart = hairpin;
390
+ if (note.id === last.id)
391
+ updated.hairpinStop = true;
392
+ return updated;
393
+ });
394
+ });
395
+ }
396
+ /**
397
+ * Rolls the selected chords, or stops rolling them.
398
+ *
399
+ * Toggles the whole selection together, like the fermata: the state is read
400
+ * from whether *every* selected note already carries the flag, so a partly
401
+ * marked selection becomes fully marked rather than inverting note by note.
402
+ *
403
+ * The mark belongs to a chord, but the selection is notes — so this simply
404
+ * sets the flag on what was selected and lets the renderer decide. A lone note
405
+ * keeps the flag harmlessly and draws nothing, which is better than refusing:
406
+ * selecting a bar and rolling its chords should not fail because one beat
407
+ * happens to be a single note.
408
+ */
409
+ export function toggleArpeggiateCommand(eventIds, label) {
410
+ return transformCommand(label, (score) => {
411
+ const notes = eventIds
412
+ .map((id) => findEvent(score, id))
413
+ .filter((event) => event !== null && isNoteEvent(event));
414
+ if (notes.length === 0)
415
+ return score;
416
+ const allMarked = notes.every((note) => note.arpeggiate);
417
+ return mapNotes(score, eventIds, (note) => {
418
+ if (allMarked) {
419
+ const updated = { ...note };
420
+ delete updated.arpeggiate;
421
+ return updated;
422
+ }
423
+ return { ...note, arpeggiate: true };
424
+ });
425
+ });
426
+ }
427
+ /**
428
+ * Brackets the selection at an octave, or removes the bracket it has.
429
+ *
430
+ * A span like the hairpin: endpoints by tick, two notes minimum, the same
431
+ * displacement twice removes it and a different one replaces it.
432
+ */
433
+ export function toggleOttavaCommand(eventIds, ottava, label) {
434
+ return transformCommand(label, (score) => {
435
+ const notes = eventIds
436
+ .map((id) => findEvent(score, id))
437
+ .filter((event) => event !== null && isNoteEvent(event))
438
+ .sort((a, b) => a.startTick - b.startTick);
439
+ if (notes.length < 2)
440
+ return score;
441
+ const first = notes[0];
442
+ const last = notes[notes.length - 1];
443
+ const sameAlready = first.ottavaStart === ottava && Boolean(last.ottavaStop);
444
+ return mapNotes(score, eventIds, (note) => {
445
+ const updated = { ...note };
446
+ delete updated.ottavaStart;
447
+ delete updated.ottavaStop;
448
+ if (sameAlready)
449
+ return updated;
450
+ if (note.id === first.id)
451
+ updated.ottavaStart = ottava;
452
+ if (note.id === last.id)
453
+ updated.ottavaStop = true;
454
+ return updated;
455
+ });
456
+ });
457
+ }
458
+ /**
459
+ * Slides between the selected notes, or removes the slide.
460
+ *
461
+ * Two notes exactly is the usual case and two is the minimum: a glissando is
462
+ * a line *between* noteheads, so one note has nothing to slide to. A wider
463
+ * selection slides from its first note to its last, which is what dragging
464
+ * across a run and asking for a slide means.
465
+ */
466
+ export function toggleGlissandoCommand(eventIds, label) {
467
+ return transformCommand(label, (score) => {
468
+ const notes = eventIds
469
+ .map((id) => findEvent(score, id))
470
+ .filter((event) => event !== null && isNoteEvent(event))
471
+ .sort((a, b) => a.startTick - b.startTick);
472
+ if (notes.length < 2)
473
+ return score;
474
+ const first = notes[0];
475
+ const last = notes[notes.length - 1];
476
+ const already = Boolean(first.glissandoStart && last.glissandoStop);
477
+ return mapNotes(score, eventIds, (note) => {
478
+ const updated = { ...note };
479
+ delete updated.glissandoStart;
480
+ delete updated.glissandoStop;
481
+ if (already)
482
+ return updated;
483
+ if (note.id === first.id)
484
+ updated.glissandoStart = true;
485
+ if (note.id === last.id)
486
+ updated.glissandoStop = true;
487
+ return updated;
488
+ });
489
+ });
490
+ }
491
+ /**
492
+ * Sets or clears the finger written on the given notes.
493
+ *
494
+ * Blank clears it rather than storing an empty string, which would reserve
495
+ * space beside the notehead and print nothing.
496
+ */
497
+ export function setFingeringCommand(eventIds, fingering, label) {
498
+ const trimmed = fingering?.trim();
499
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => {
500
+ if (trimmed)
501
+ return { ...note, fingering: trimmed };
502
+ const updated = { ...note };
503
+ delete updated.fingering;
504
+ return updated;
505
+ }));
506
+ }
507
+ /**
508
+ * Sets or clears the syllable sung on one note.
509
+ *
510
+ * One note at a time, unlike the other note commands: every syllable in a line
511
+ * is different, so applying one across a selection could only ever write the
512
+ * same word repeatedly.
513
+ *
514
+ * Blank text clears the lyric rather than storing an empty one — an empty
515
+ * syllable would reserve space under the note and print nothing.
516
+ */
517
+ export function setLyricCommand(eventId, lyric, label) {
518
+ return transformCommand(label, (score) => mapNotes(score, [eventId], (note) => {
519
+ const updated = { ...note };
520
+ if (!lyric || lyric.text.trim() === "") {
521
+ delete updated.lyric;
522
+ return updated;
523
+ }
524
+ updated.lyric = {
525
+ text: lyric.text.trim(),
526
+ // `single` is the default the model states, so storing it would only
527
+ // be noise in every saved score.
528
+ ...(lyric.syllabic && lyric.syllabic !== "single"
529
+ ? { syllabic: lyric.syllabic }
530
+ : {}),
531
+ };
532
+ return updated;
533
+ }));
534
+ }
535
+ /**
536
+ * Sets or clears the chord symbol printed from a note.
537
+ *
538
+ * One note at a time, like a lyric: every chord in a progression is different,
539
+ * so applying one across a selection could only write the same symbol
540
+ * repeatedly. Blank text clears it rather than storing an empty symbol, which
541
+ * would reserve space above the stave and print nothing.
542
+ */
543
+ export function setChordSymbolCommand(eventId, symbol, label) {
544
+ return transformCommand(label, (score) => mapNotes(score, [eventId], (note) => {
545
+ const updated = { ...note };
546
+ const trimmed = symbol?.trim();
547
+ if (!trimmed) {
548
+ delete updated.chordSymbol;
549
+ return updated;
550
+ }
551
+ updated.chordSymbol = trimmed;
552
+ return updated;
553
+ }));
554
+ }
555
+ /**
556
+ * Turns a written note into a grace note on the note that follows it.
557
+ *
558
+ * The note leaves the voice and reappears hanging off its neighbour, and the
559
+ * gap it leaves is filled with a rest — so the bar still adds up, which is the
560
+ * whole reason grace notes are stored on their principal rather than as events
561
+ * of their own.
562
+ *
563
+ * The principal is the next note *in the same voice*, since that is what the
564
+ * ornament leads into. A note with nothing after it cannot become one: an
565
+ * ornament with nothing to ornament would simply vanish from the page.
566
+ */
567
+ export function toGraceNoteCommand(eventId, label) {
568
+ return transformCommand(label, (score) => {
569
+ const tracks = score.tracks.map((track) => ({
570
+ ...track,
571
+ measures: track.measures.map((measure) => ({
572
+ ...measure,
573
+ voices: measure.voices.map((voice) => {
574
+ const index = voice.events.findIndex((e) => e.id === eventId);
575
+ if (index === -1)
576
+ return voice;
577
+ const source = voice.events[index];
578
+ if (!isNoteEvent(source))
579
+ return voice;
580
+ // The principal: the next *note* after it in this voice.
581
+ const principalIndex = voice.events.findIndex((e, i) => i > index && isNoteEvent(e));
582
+ if (principalIndex === -1)
583
+ return voice;
584
+ const principal = voice.events[principalIndex];
585
+ const grace = {
586
+ pitch: source.pitch,
587
+ durationTicks: source.durationTicks,
588
+ slashed: true,
589
+ };
590
+ /*
591
+ Ornaments the source itself carried travel with it, ahead of the
592
+ note it becomes: turning a decorated note into a grace note should
593
+ move the whole ornamental run to the new principal, not silently
594
+ drop everything in front of it.
595
+ */
596
+ const carried = [...(source.graceNotes ?? []), grace];
597
+ return {
598
+ ...voice,
599
+ events: voice.events.map((event, i) => {
600
+ // The time the note occupied stays occupied, by a rest: an
601
+ // ornament borrows from its principal, it does not shorten the
602
+ // bar.
603
+ if (i === index)
604
+ return {
605
+ id: source.id,
606
+ startTick: source.startTick,
607
+ durationTicks: source.durationTicks,
608
+ voiceId: source.voiceId,
609
+ trackId: source.trackId,
610
+ };
611
+ if (i === principalIndex)
612
+ return {
613
+ ...principal,
614
+ graceNotes: [...(principal.graceNotes ?? []), ...carried],
615
+ };
616
+ return event;
617
+ }),
618
+ };
619
+ }),
620
+ })),
621
+ }));
622
+ return withTracks(score, tracks);
623
+ });
624
+ }
625
+ /** Removes every ornament from the given notes. */
626
+ export function clearGraceNotesCommand(eventIds, label) {
627
+ return transformCommand(label, (score) => mapNotes(score, eventIds, (note) => {
628
+ const updated = { ...note };
629
+ delete updated.graceNotes;
630
+ return updated;
631
+ }));
240
632
  }
241
633
  // ---- changeVoiceCommand -----------------------------------------------------------
242
634
  function changeVoice(score, eventIds, targetVoiceIndex) {
243
635
  const idSet = new Set(eventIds);
244
- const tracks = score.tracks.map(track => {
245
- const measures = track.measures.map(measure => {
636
+ const tracks = score.tracks.map((track) => {
637
+ const measures = track.measures.map((measure) => {
246
638
  const matches = [];
247
- measure.voices.forEach(voice => {
248
- voice.events.forEach(e => {
639
+ measure.voices.forEach((voice) => {
640
+ voice.events.forEach((e) => {
249
641
  if (isNoteEvent(e) && idSet.has(e.id))
250
642
  matches.push(e);
251
643
  });
@@ -254,21 +646,21 @@ function changeVoice(score, eventIds, targetVoiceIndex) {
254
646
  return measure;
255
647
  let nextMeasure = measure;
256
648
  for (const voice of measure.voices) {
257
- if (!voice.events.some(e => idSet.has(e.id)))
649
+ if (!voice.events.some((e) => idSet.has(e.id)))
258
650
  continue;
259
651
  const remaining = voice.events
260
652
  .filter(isNoteEvent)
261
- .filter(e => !idSet.has(e.id));
653
+ .filter((e) => !idSet.has(e.id));
262
654
  const withRemoved = {
263
655
  ...nextMeasure,
264
- voices: nextMeasure.voices.map(v => v.id === voice.id ? { ...v, events: remaining } : v),
656
+ voices: nextMeasure.voices.map((v) => v.id === voice.id ? { ...v, events: remaining } : v),
265
657
  };
266
658
  nextMeasure = reflowVoice(withRemoved, voice.id, track.id);
267
659
  }
268
660
  nextMeasure = ensureVoiceAtIndex(nextMeasure, targetVoiceIndex, track.id);
269
661
  const targetVoice = nextMeasure.voices[targetVoiceIndex];
270
662
  const existingNotes = targetVoice.events.filter(isNoteEvent);
271
- const movedNotes = matches.map(n => ({
663
+ const movedNotes = matches.map((n) => ({
272
664
  ...n,
273
665
  voiceId: targetVoice.id,
274
666
  }));
@@ -287,6 +679,6 @@ function changeVoice(score, eventIds, targetVoiceIndex) {
287
679
  }
288
680
  /** Moves the given notes to the voice at ordinal position `targetVoiceIndex` within their current measure (created if absent). */
289
681
  export function changeVoiceCommand(eventIds, targetVoiceIndex, label) {
290
- return transformCommand(label, score => changeVoice(score, eventIds, targetVoiceIndex));
682
+ return transformCommand(label, (score) => changeVoice(score, eventIds, targetVoiceIndex));
291
683
  }
292
684
  //# sourceMappingURL=note-commands.js.map