@hraness/dawg 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -27,6 +27,7 @@ import {
27
27
  type FxValues,
28
28
  type RigStage,
29
29
  type TrackFx,
30
+ SHOEGAZE_EFFECTS,
30
31
  } from "../../core/fx.ts";
31
32
  import {
32
33
  instrumentForWord,
@@ -44,6 +45,7 @@ import {
44
45
  type FxCommand,
45
46
  type FxResult,
46
47
  } from "./fx.ts";
48
+ import { nearest } from "./nearest.ts";
47
49
 
48
50
  export type RigCommand =
49
51
  | { type: "rig-show" }
@@ -66,33 +68,11 @@ function isStage(word: string): word is RigStage {
66
68
 
67
69
  /** `unknown rig crunh · did you mean crunch? · rigs clean crunch …` */
68
70
  function unknownRigMessage(name: string): string {
69
- let best: { name: string; distance: number } | undefined;
70
- for (const candidate of RIG_PRESET_NAMES) {
71
- const distance = editDistance(name, candidate);
72
- if (!best || distance < best.distance) best = { name: candidate, distance };
73
- }
74
- const near =
75
- best && best.distance <= Math.max(2, Math.floor(name.length / 3))
76
- ? ` · did you mean ${best.name}?`
77
- : "";
71
+ const match = nearest(name, RIG_PRESET_NAMES);
72
+ const near = match ? ` · did you mean ${match}?` : "";
78
73
  return `unknown rig ${name}${near} · rigs ${RIG_PRESET_NAMES.join(" ")} reset`;
79
74
  }
80
75
 
81
- function editDistance(a: string, b: string): number {
82
- let row = Array.from({ length: b.length + 1 }, (_, i) => i);
83
- for (let i = 1; i <= a.length; i++) {
84
- const next = [i];
85
- for (let j = 1; j <= b.length; j++)
86
- next[j] = Math.min(
87
- row[j]! + 1,
88
- next[j - 1]! + 1,
89
- row[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
90
- );
91
- row = next;
92
- }
93
- return row[b.length]!;
94
- }
95
-
96
76
  export function parseRigCommand(prompt: string): RigCommand | undefined {
97
77
  if (prompt.length > 1_024) return undefined;
98
78
  const words = prompt.trim().replace(/^\//, "").toLowerCase().split(/\s+/);
@@ -140,7 +120,7 @@ export function parseRigCommand(prompt: string): RigCommand | undefined {
140
120
  }
141
121
 
142
122
  function describeRig(values: Readonly<Record<string, FxValues | undefined>>) {
143
- return RIG_STAGES.filter((stage) => values[stage])
123
+ const stages = RIG_STAGES.filter((stage) => values[stage])
144
124
  .map((stage) => {
145
125
  const v = values[stage]!;
146
126
  const type = String(v.type);
@@ -148,6 +128,9 @@ function describeRig(values: Readonly<Record<string, FxValues | undefined>>) {
148
128
  return `${stage} ${type}${gain}`;
149
129
  })
150
130
  .join(" → ");
131
+ // 0.6.1 shoegaze stages follow the cab in the chain.
132
+ const gaze = SHOEGAZE_EFFECTS.filter((effect) => values[effect]);
133
+ return gaze.length ? `${stages} + ${gaze.join(" ")}` : stages;
151
134
  }
152
135
 
153
136
  export function applyRigCommand(
@@ -38,7 +38,8 @@ export type SampleCommand =
38
38
  }>;
39
39
 
40
40
  /** A sample control value: a number, a switch, a unit, or null to unset. */
41
- export type SampleControlValue = number | string | boolean | null;
41
+ export type SampleControlValue =
42
+ number | string | boolean | null | readonly number[];
42
43
 
43
44
  /**
44
45
  * Per-voice sample controls `/sample set` and `set_sample` change, by
@@ -62,6 +63,12 @@ export const SAMPLE_CONTROLS = Object.freeze({
62
63
  bpm: "the sample's own tempo: it follows the song's tempo map",
63
64
  fitmode: "how a fitted sample changes time: repitch, beats or tones",
64
65
  len: "the window lasts n beats of the song's tempo map",
66
+ shift: "pitch in semitones (−24..24), length unchanged",
67
+ formant: "formants in semitones with shift: 0 keeps the voice, off follows",
68
+ fadeTime: "release fade in seconds (0..2); alias fadeout",
69
+ fadeInTime: "attack fade in seconds (0..2); alias fadein",
70
+ vel: "velocity layer lo-hi (MIDI 0-127): plays only for those velocities",
71
+ rr: "round-robin group name: voices in a group take turns",
65
72
  });
66
73
  export type SampleControl = keyof typeof SAMPLE_CONTROLS;
67
74
 
@@ -70,6 +77,8 @@ const SAMPLE_ALIASES: Readonly<Record<string, string>> = {
70
77
  loope: "loopEnd",
71
78
  legato: "clip",
72
79
  choke: "cut",
80
+ fadeout: "fadeTime",
81
+ fadein: "fadeInTime",
73
82
  };
74
83
 
75
84
  function controlName(raw: string): SampleControl | undefined {
@@ -176,6 +185,18 @@ export function setSampleControls(
176
185
  continue;
177
186
  }
178
187
  if (name === "cut" && typeof value === "number") value = `cut${value}`;
188
+ if (name === "rr" && typeof value === "number") value = `rr${value}`;
189
+ if (name === "vel" && value !== null) {
190
+ const range = parseVelocityRange(value);
191
+ if (!range)
192
+ return {
193
+ ok: false,
194
+ message: "sample · vel needs lo-hi MIDI velocities, e.g. vel 0-63",
195
+ };
196
+ next.vel = range;
197
+ changed.push(`vel ${range[0]}-${range[1]}`);
198
+ continue;
199
+ }
179
200
  if (value === null) delete next[field];
180
201
  else next[field] = value;
181
202
  changed.push(`${raw} ${value === null ? "off" : String(value)}`);
@@ -203,10 +224,24 @@ export function setSampleControls(
203
224
  return {
204
225
  ok: true,
205
226
  next: updated,
206
- message: `sample · ${trackId}/${voice} · ${changed.join(" · ")}`,
227
+ message: `sample · ${trackId}/${voice} · ${changed.join(" · ")}${layerHint(updated, trackId, voice)}`,
207
228
  };
208
229
  }
209
230
 
231
+ /** `0-63`, `0..63`, `64` (one velocity) → [lo, hi]; else undefined. */
232
+ export function parseVelocityRange(
233
+ value: SampleControlValue | readonly number[],
234
+ ): [number, number] | undefined {
235
+ if (Array.isArray(value))
236
+ return value.length === 2 && value.every((v) => typeof v === "number")
237
+ ? [value[0] as number, value[1] as number]
238
+ : undefined;
239
+ if (typeof value === "number") return [value, value];
240
+ if (typeof value !== "string") return undefined;
241
+ const match = value.trim().match(/^(\d{1,3})\s*(?:-|\.\.|,)\s*(\d{1,3})$/);
242
+ return match ? [Number(match[1]), Number(match[2])] : undefined;
243
+ }
244
+
210
245
  function unquote(value: string): string {
211
246
  const quoted = value.match(/^(["'])(.*)\1$/);
212
247
  return quoted ? quoted[2]! : value;
@@ -372,6 +407,18 @@ export function freeVoiceName(
372
407
  }
373
408
  }
374
409
 
410
+ /**
411
+ * One-shot velocity layers swap only within a round-robin group: a kit
412
+ * voice with `vel` and no `rr` keeps its own pad, so say how to join them.
413
+ */
414
+ function layerHint(score: TrackScore, trackId: string, voice: string): string {
415
+ const sampler = score.tracks.find((item) => item.id === trackId)?.sampler;
416
+ const ref = sampler?.voices[voice];
417
+ if (sampler?.mode !== "oneshot" || !ref?.vel || ref.rr !== undefined)
418
+ return "";
419
+ return ` · one-shot layers share a pad only within an rr group: sample ${voice} rr <group> on each layer`;
420
+ }
421
+
375
422
  export class SamplePlacementError extends Error {}
376
423
 
377
424
  /**
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `shift <semitones> [formant keep|follow|<n>] [<voice>]` and
3
+ * `fade [in|out] <seconds> [<voice>]` (0.6.1): pitch a sampler voice
4
+ * without changing its length, and set its fade in and out. They act on
5
+ * the focused sampler track's voice (named, or its only one) through
6
+ * `setSampleControls`, so validation and printing stay the sampler's.
7
+ */
8
+ import type { TrackScore } from "../../core/score.ts";
9
+ import { fitVoice } from "./fit.ts";
10
+ import {
11
+ setSampleControls,
12
+ type SampleControlValue,
13
+ type SetSampleResult,
14
+ } from "./sample.ts";
15
+
16
+ export type ShiftCommand =
17
+ | Readonly<{
18
+ kind: "shift";
19
+ /** Semitones; null clears shift and formant. */
20
+ semitones: number | null;
21
+ /** 0 keeps the formants, null follows the pitch, undefined leaves it. */
22
+ formant?: number | null;
23
+ voice?: string;
24
+ }>
25
+ | Readonly<{
26
+ kind: "fade";
27
+ /** `in` is fadeInTime, `out` fadeTime, `both` sets each. */
28
+ edge: "in" | "out" | "both";
29
+ seconds: number | null;
30
+ voice?: string;
31
+ }>;
32
+
33
+ const VOICE = "([a-z][a-z0-9_]{0,31})";
34
+ const NUMBER = "([+-]?\\d+(?:\\.\\d+)?|off|none)";
35
+
36
+ /** Parse `shift ...` and `fade ...` (with or without the slash). */
37
+ export function parseShiftCommand(command: string): ShiftCommand | undefined {
38
+ const text = command.trim();
39
+ const shift = text.match(
40
+ new RegExp(
41
+ `^/?shift\\s+${NUMBER}(?:\\s+formant\\s+(keep|follow|[+-]?\\d+(?:\\.\\d+)?))?(?:\\s+(?:voice\\s+)?${VOICE})?$`,
42
+ "i",
43
+ ),
44
+ );
45
+ if (shift) {
46
+ const raw = shift[1]!.toLowerCase();
47
+ const formant = shift[2]?.toLowerCase();
48
+ return {
49
+ kind: "shift",
50
+ semitones: raw === "off" || raw === "none" ? null : Number(raw),
51
+ ...(formant === undefined
52
+ ? {}
53
+ : {
54
+ formant:
55
+ formant === "keep"
56
+ ? 0
57
+ : formant === "follow"
58
+ ? null
59
+ : Number(formant),
60
+ }),
61
+ ...(shift[3] ? { voice: shift[3].toLowerCase() } : {}),
62
+ };
63
+ }
64
+ const fade = text.match(
65
+ new RegExp(
66
+ `^/?fade(?:\\s+(in|out))?\\s+${NUMBER}(?:\\s+(?:voice\\s+)?${VOICE})?$`,
67
+ "i",
68
+ ),
69
+ );
70
+ if (!fade) return undefined;
71
+ const raw = fade[2]!.toLowerCase();
72
+ return {
73
+ kind: "fade",
74
+ edge: (fade[1]?.toLowerCase() as "in" | "out" | undefined) ?? "both",
75
+ seconds: raw === "off" || raw === "none" ? null : Number(raw),
76
+ ...(fade[3] ? { voice: fade[3].toLowerCase() } : {}),
77
+ };
78
+ }
79
+
80
+ /** Apply a parsed `shift` or `fade` to the focused track's voice. */
81
+ export function applyShiftCommand(
82
+ score: TrackScore,
83
+ trackId: string,
84
+ command: ShiftCommand,
85
+ ): SetSampleResult {
86
+ const target = fitVoice(score, trackId, command.voice);
87
+ if ("error" in target)
88
+ return {
89
+ ok: false,
90
+ message: target.error.replace(/^fit · /, `${command.kind} · `),
91
+ };
92
+ const values: Record<string, SampleControlValue> = {};
93
+ if (command.kind === "shift") {
94
+ // `shift off` clears the shift and its formant setting. `shift 0`
95
+ // clears the shift and a kept formant (0, a no-op without a shift) but
96
+ // keeps a formant move, so `shift 0 formant 3` is a formant-only move
97
+ // and stepping the shift through 0 keeps a stored formant.
98
+ if (command.semitones === null) {
99
+ values.shift = null;
100
+ values.formant = null;
101
+ } else {
102
+ values.shift = command.semitones === 0 ? null : command.semitones;
103
+ const formant =
104
+ command.formant !== undefined
105
+ ? command.formant
106
+ : score.tracks.find((track) => track.id === trackId)?.sampler?.voices[
107
+ target.voice
108
+ ]?.formant;
109
+ if (command.semitones === 0 && (formant === 0 || formant === null))
110
+ values.formant = null;
111
+ else if (command.formant !== undefined) values.formant = command.formant;
112
+ }
113
+ } else {
114
+ const seconds = command.seconds === 0 ? null : command.seconds;
115
+ if (command.edge !== "out") values.fadeInTime = seconds;
116
+ if (command.edge !== "in") values.fadeTime = seconds;
117
+ }
118
+ return setSampleControls(score, trackId, target.voice, values);
119
+ }