@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
@@ -83,8 +83,8 @@ function diagnostic(error: unknown, project: string): Diagnostic {
83
83
  }
84
84
 
85
85
  /**
86
- * Adds `sha256` to sampler voices and project wavetable files that exist
87
- * and are within the size limit.
86
+ * Adds `sha256` to sampler voices, project wavetable files, audio clips and
87
+ * takes that exist and are within the size limit.
88
88
  */
89
89
  async function withSampleHashes(
90
90
  song: object,
@@ -106,6 +106,11 @@ async function withSampleHashes(
106
106
  refs.push(wavetable.table);
107
107
  const granular = track.granular;
108
108
  if (isObject(granular) && isObject(granular.src)) refs.push(granular.src);
109
+ // 0.7 audio clips and takes: a fresh `audio("samples/x.wav")` is pinned
110
+ // here; a written pin is kept and `dawg check` re-hashes it.
111
+ for (const key of ["clips", "takes"])
112
+ for (const item of Array.isArray(track[key]) ? track[key] : [])
113
+ if (isObject(item)) refs.push(item);
109
114
  }
110
115
  for (const ref of refs) {
111
116
  if (
package/core/sdk/eval.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  /**
2
- * Evaluates a project's `song.ts` in a sandboxed Bun subprocess and parses
2
+ * Evaluates a project's `song.ts` in an isolated Bun subprocess and parses
3
3
  * the result from `unknown` into a validated `TrackScore`.
4
4
  *
5
5
  * The child runs `eval-child.ts` with a scrubbed environment, the project as
6
6
  * its working directory, no native addons or auto-install, a 10 second
7
- * budget and a 1 MiB output bound. Anything it prints that is not the
7
+ * budget and a 32 MiB output bound. Anything it prints that is not the
8
8
  * expected JSON line is a diagnostic.
9
+ *
10
+ * Isolation is not a sandbox: the child runs project code as the user, with
11
+ * the user's filesystem and network access. It protects dawg's own process
12
+ * (a crash, hang or runaway output in song.ts never takes the window down),
13
+ * not the machine, so only evaluate projects you trust.
9
14
  */
10
15
 
11
16
  import { dirname, join } from "node:path";
@@ -15,7 +20,11 @@ import { refreshRhythm } from "../rhythm.ts";
15
20
  import { ScoreValidationError, TrackScore } from "../score.ts";
16
21
 
17
22
  export const EVAL_TIMEOUT_MS = 10_000;
18
- export const EVAL_MAX_OUTPUT_BYTES = 1024 * 1024;
23
+ /**
24
+ * Above the JSON of the largest score `SCORE_LIMITS` allows (64 tracks with
25
+ * every automation lane full plus 4096 notes stays well under 16 MiB).
26
+ */
27
+ export const EVAL_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
19
28
 
20
29
  /** A problem in a project file; `file` is project-relative when known. */
21
30
  export type Diagnostic = Readonly<{
@@ -39,7 +48,14 @@ export type EvalSpawn = (
39
48
  maxOutputBytes: number;
40
49
  }>,
41
50
  ) => Promise<
42
- Readonly<{ code: number; stdout: string; stderr: string; killed: boolean }>
51
+ Readonly<{
52
+ code: number;
53
+ stdout: string;
54
+ stderr: string;
55
+ killed: boolean;
56
+ /** Why the child was killed; absent means the time budget ran out. */
57
+ reason?: "timeout" | "output";
58
+ }>
43
59
  >;
44
60
 
45
61
  export type EvalOptions = Readonly<{
@@ -86,12 +102,21 @@ export async function evaluateProject(
86
102
  failure(started, [{ message: `could not start bun: ${text(error)}` }]),
87
103
  );
88
104
  }
105
+ if (run.killed && run.reason === "output")
106
+ return done(
107
+ failure(started, [
108
+ {
109
+ file: "song.ts",
110
+ message: `evaluation printed more than ${EVAL_MAX_OUTPUT_BYTES / (1024 * 1024)} MiB; song.ts must not log`,
111
+ },
112
+ ]),
113
+ );
89
114
  if (run.killed)
90
115
  return done(
91
116
  failure(started, [
92
117
  {
93
118
  file: "song.ts",
94
- message: "evaluation timed out (10 s); song.ts must be pure",
119
+ message: `evaluation timed out (${Math.round((options.timeoutMs ?? EVAL_TIMEOUT_MS) / 1000)} s); song.ts must be pure`,
95
120
  },
96
121
  ]),
97
122
  );
@@ -224,6 +249,7 @@ export const bunSpawn: EvalSpawn = async (args, options) => {
224
249
  stderr: "pipe",
225
250
  });
226
251
  let killed = false;
252
+ let reason: "timeout" | "output" = "timeout";
227
253
  const timer = setTimeout(() => {
228
254
  killed = true;
229
255
  child.kill();
@@ -237,6 +263,7 @@ export const bunSpawn: EvalSpawn = async (args, options) => {
237
263
  const { value, done } = await reader.read();
238
264
  if (done || !value) break;
239
265
  if (total + value.byteLength > options.maxOutputBytes) {
266
+ if (!killed) reason = "output";
240
267
  killed = true;
241
268
  child.kill();
242
269
  break;
@@ -253,7 +280,9 @@ export const bunSpawn: EvalSpawn = async (args, options) => {
253
280
  read(child.stderr),
254
281
  child.exited,
255
282
  ]);
256
- return { code, stdout, stderr, killed };
283
+ return killed
284
+ ? { code, stdout, stderr, killed, reason }
285
+ : { code, stdout, stderr, killed };
257
286
  } finally {
258
287
  clearTimeout(timer);
259
288
  }
package/core/sdk/print.ts CHANGED
@@ -21,12 +21,26 @@ import {
21
21
  type FxValues,
22
22
  } from "../fx.ts";
23
23
  import { MASTER_SPECS, MASTER_UNITS, type SongMaster } from "../master.ts";
24
+ import type { SongStyle } from "../style-provenance.ts";
24
25
  import { midiToPitch } from "../pitch.ts";
25
26
  import {
26
27
  MODAL_INSTRUMENT,
27
28
  MODAL_PARAMS,
28
29
  type TrackModal,
29
30
  } from "../resonators.ts";
31
+ import { WIND_INSTRUMENT, WIND_PARAMS, type TrackWind } from "../winds.ts";
32
+ import {
33
+ SING_INSTRUMENT,
34
+ SING_KEY_ORDER,
35
+ singNoteName,
36
+ type TrackSing,
37
+ } from "../sing.ts";
38
+ import {
39
+ VOCODER_INSTRUMENT,
40
+ VOCODER_PARAMS,
41
+ type TrackVocoder,
42
+ } from "../vocoder.ts";
43
+ import { AUTOTUNE_FIELDS, type TrackAutotune } from "../autotune.ts";
30
44
  import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
31
45
  import {
32
46
  BUILTIN_TABLE_PREFIX,
@@ -44,13 +58,15 @@ import {
44
58
  type SampleRef,
45
59
  type Sampler,
46
60
  type Section,
61
+ type AudioClip,
62
+ type Take,
47
63
  type Track,
48
64
  } from "../score.ts";
49
65
  import { trackSlug } from "../slug.ts";
50
- import { barStartTick } from "../tempo.ts";
66
+ import { barStartTick, timeWithinSong } from "../tempo.ts";
51
67
  import type { Tuning } from "../tuning.ts";
52
68
  import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
53
- import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
69
+ import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY, defaultClipId } from "./v1.ts";
54
70
 
55
71
  const WIDTH = 80;
56
72
  const INDENT = " ";
@@ -155,6 +171,14 @@ const RESERVED = new Set([
155
171
  "rig",
156
172
  "granular",
157
173
  "modal",
174
+ "wind",
175
+ "sing",
176
+ "audio",
177
+ "take",
178
+ "lyrics",
179
+ "repeatAudio",
180
+ "vocoder",
181
+ "autotune",
158
182
  "slices",
159
183
  "euclid",
160
184
  "euclidRot",
@@ -195,6 +219,7 @@ export function printSong(
195
219
  const helpers = [
196
220
  "song",
197
221
  ...TIME_HELPERS.filter((name) => time.used.has(name)),
222
+ ...(score.style ? ["style"] : []),
198
223
  ];
199
224
  const lines: string[] = [`import { ${helpers.join(", ")} } from "dawg";`];
200
225
  for (const track of score.tracks)
@@ -225,6 +250,7 @@ export function printSong(
225
250
  )}`,
226
251
  );
227
252
  if (score.master) entries.push(printMaster(score.master));
253
+ if (score.style) entries.push(`style: ${printStyle(score.style)}`);
228
254
  // Song sections and form (0.5): printed only when the song has them.
229
255
  if (score.sections.length > 0)
230
256
  entries.push(
@@ -239,6 +265,7 @@ export function printSong(
239
265
  entries.push(`form: ${printForm(score.form, INDENT)}`);
240
266
  if (score.loopSection !== undefined)
241
267
  entries.push(`loopSection: ${str(score.loopSection)}`);
268
+ if (score.calibration) entries.push(`calibration: ${num(score.calibration)}`);
242
269
  lines.push("export default song({");
243
270
  for (const entry of entries) lines.push(`${INDENT}${entry},`);
244
271
  lines.push("});", "");
@@ -255,7 +282,8 @@ function printTime(score: TrackScore): {
255
282
  } {
256
283
  const marks: string[] = [];
257
284
  const used = new Set<string>();
258
- const time = score.time;
285
+ // Marks past the final barline are never heard and the SDK refuses them.
286
+ const time = timeWithinSong(score);
259
287
  if (!time) return { marks, used };
260
288
  const beat = (tick: number) => num(tick / score.ticksPerBeat);
261
289
  type Mark = { tick: number; order: number; text: string };
@@ -414,6 +442,25 @@ export function printTrack(score: TrackScore, track: Track): string {
414
442
  ? printModal(track.modal, INDENT)
415
443
  : undefined;
416
444
  if (modalCall?.startsWith("modal(")) used.add("modal");
445
+ const windCall =
446
+ track.instrument === WIND_INSTRUMENT && track.wind
447
+ ? printWind(track.wind, INDENT)
448
+ : undefined;
449
+ if (windCall?.startsWith("wind(")) used.add("wind");
450
+ const singCall =
451
+ track.instrument === SING_INSTRUMENT && track.sing
452
+ ? printSing(track.sing, INDENT)
453
+ : undefined;
454
+ if (singCall?.startsWith("sing(")) used.add("sing");
455
+ const vocoderCall = track.vocoder
456
+ ? printVocoder(
457
+ track.vocoder,
458
+ vocoderSrcText(score, track.vocoder.src),
459
+ track.instrument === VOCODER_INSTRUMENT ? "instrument: " : "vocoder: ",
460
+ INDENT,
461
+ )
462
+ : undefined;
463
+ if (vocoderCall?.startsWith("vocoder(")) used.add("vocoder");
417
464
 
418
465
  const entries: string[] = [
419
466
  `id: ${str(track.id)}`,
@@ -432,8 +479,21 @@ export function printTrack(score: TrackScore, track: Track): string {
432
479
  entries.push(`instrument: ${printStringed(track.string!, INDENT)}`);
433
480
  } else if (modalCall) {
434
481
  entries.push(`instrument: ${modalCall}`);
482
+ } else if (windCall) {
483
+ entries.push(`instrument: ${windCall}`);
484
+ } else if (singCall) {
485
+ entries.push(`instrument: ${singCall}`);
486
+ } else if (vocoderCall && track.instrument === VOCODER_INSTRUMENT) {
487
+ entries.push(`instrument: ${vocoderCall}`);
435
488
  } else entries.push(`instrument: ${str(track.instrument)}`);
489
+ if (vocoderCall && track.instrument !== VOCODER_INSTRUMENT)
490
+ entries.push(`vocoder: ${vocoderCall}`);
436
491
  if (track.kit) entries.push(`kit: ${str(track.kit)}`);
492
+ // The table a track keeps after leaving the wavetable instrument.
493
+ if (track.wavetable && !wavetable) {
494
+ used.add("wavetable");
495
+ entries.push(`wavetable: ${printWavetable(track.wavetable, INDENT)}`);
496
+ }
437
497
  if (track.granular && !grained)
438
498
  entries.push(
439
499
  `granular: ${obj(
@@ -586,6 +646,11 @@ export function printTrack(score: TrackScore, track: Track): string {
586
646
  );
587
647
  }
588
648
  entries.push(...performanceEntries(score, track));
649
+ if (track.autotune) {
650
+ const call = printAutotune(track.autotune, INDENT);
651
+ if (call.startsWith("autotune(")) used.add("autotune");
652
+ entries.push(`autotune: ${call}`);
653
+ }
589
654
  const lanes: [string, readonly AutomationPoint[] | undefined][] = [
590
655
  ["volume", track.volumeAutomation],
591
656
  ["pan", track.panAutomation],
@@ -644,6 +709,20 @@ export function printTrack(score: TrackScore, track: Track): string {
644
709
  }
645
710
  if (rows.length === 0 || printed.length > 0)
646
711
  entries.push(`notes: ${list(printed, INDENT, "notes: ".length, 1)}`);
712
+ if (track.takes && track.takes.length > 0) {
713
+ used.add("take");
714
+ const items = track.takes.map((t) => printTake(score, t, INDENT + INDENT));
715
+ entries.push(`takes: ${list(items, INDENT, "takes: ".length, 1, true)}`);
716
+ }
717
+ if (track.clips && track.clips.length > 0) {
718
+ used.add("audio");
719
+ const items = track.clips.map((clip, index) =>
720
+ printClip(score, clip, index, INDENT + INDENT),
721
+ );
722
+ entries.push(
723
+ `clips: ${list(items, INDENT, "clips: ".length, 1, items.length > 1)}`,
724
+ );
725
+ }
647
726
 
648
727
  const names = [
649
728
  "track",
@@ -658,8 +737,14 @@ export function printTrack(score: TrackScore, track: Track): string {
658
737
  "rig",
659
738
  "granular",
660
739
  "modal",
740
+ "wind",
741
+ "sing",
742
+ "vocoder",
743
+ "autotune",
661
744
  "euclid",
662
745
  "grid",
746
+ "audio",
747
+ "take",
663
748
  ]
664
749
  .filter((name) => used.has(name))
665
750
  .join(", ");
@@ -768,6 +853,9 @@ function expressionEntries(
768
853
  ),
769
854
  ]);
770
855
  }
856
+ if (note.vowel) entries.push(["vowel", str(note.vowel)]);
857
+ if (note.lyric !== undefined) entries.push(["lyric", str(note.lyric)]);
858
+ if (note.drift !== undefined) entries.push(["drift", num(note.drift)]);
771
859
  return entries.length === 0 ? undefined : entries;
772
860
  }
773
861
 
@@ -791,6 +879,66 @@ function call(
791
879
  return `${name}(${[...args, "{"].join(", ")}\n${body}\n${indent}})`;
792
880
  }
793
881
 
882
+ /** Seconds at 4 decimals (sources.md 6). */
883
+ function secs(value: number): string {
884
+ return num(Math.round(value * 1e4) / 1e4);
885
+ }
886
+
887
+ /**
888
+ * `audio(src, { at, offset, dur, gain, fadeInTime, fadeTime, rev, take,
889
+ * mute, text, say, sha256 })` (SDK 1.32.0); defaults omitted, an id printed
890
+ * only when it differs from the positional default.
891
+ */
892
+ function printClip(
893
+ score: TrackScore,
894
+ clip: AudioClip,
895
+ index: number,
896
+ indent: string,
897
+ ): string {
898
+ const how: [string, string][] = [];
899
+ if (clip.id !== defaultClipId(index)) how.push(["id", str(clip.id)]);
900
+ if (clip.startTick !== 0)
901
+ how.push(["at", num(clip.startTick / score.ticksPerBeat)]);
902
+ if (clip.offset) how.push(["offset", secs(clip.offset)]);
903
+ if (clip.dur !== undefined) how.push(["dur", secs(clip.dur)]);
904
+ // Gain is linear in the file; a comment shows the dB the commands speak.
905
+ if (clip.gain !== undefined && clip.gain !== 1)
906
+ how.push([
907
+ "gain",
908
+ `${num(clip.gain)} /* ${
909
+ clip.gain > 0 ? (20 * Math.log10(clip.gain)).toFixed(1) : "-inf"
910
+ } dB */`,
911
+ ]);
912
+ if (clip.fadeInTime !== undefined)
913
+ how.push(["fadeInTime", secs(clip.fadeInTime)]);
914
+ if (clip.fadeTime !== undefined) how.push(["fadeTime", secs(clip.fadeTime)]);
915
+ if (clip.rev) how.push(["rev", "true"]);
916
+ if (clip.take !== undefined) how.push(["take", str(clip.take)]);
917
+ if (clip.mute) how.push(["mute", "true"]);
918
+ if (clip.text !== undefined) how.push(["text", str(clip.text)]);
919
+ if (clip.say !== undefined) how.push(["say", JSON.stringify(clip.say)]);
920
+ how.push(["sha256", str(clip.sha256)]);
921
+ return call("audio", [str(clip.src)], how, indent);
922
+ }
923
+
924
+ /** `take(name, src, { at, in, out, ... })` (SDK 1.32.0). */
925
+ function printTake(score: TrackScore, t: Take, indent: string): string {
926
+ const beatsAt = (tick: number) => num(tick / score.ticksPerBeat);
927
+ const how: [string, string][] = [];
928
+ if (t.startTick !== 0) how.push(["at", beatsAt(t.startTick)]);
929
+ how.push(["in", beatsAt(t.inTick)], ["out", beatsAt(t.outTick)]);
930
+ if (t.offset) how.push(["offset", secs(t.offset)]);
931
+ if (t.latency) how.push(["latency", secs(t.latency)]);
932
+ if (t.latencyAssumed) how.push(["latencyAssumed", "true"]);
933
+ if (t.ppm !== undefined)
934
+ how.push(["ppm", num(Math.round(t.ppm * 100) / 100)]);
935
+ if (t.fit !== undefined) how.push(["fit", num(t.fit)]);
936
+ if (t.warn !== undefined) how.push(["warn", str(t.warn)]);
937
+ if (t.nudge !== undefined) how.push(["nudge", num(t.nudge)]);
938
+ how.push(["sha256", str(t.sha256)]);
939
+ return call("take", [str(t.name), str(t.src)], how, indent);
940
+ }
941
+
794
942
  /** A track's performance fields (SDK 1.15.0) as `key: literal` entries. */
795
943
  function performanceEntries(score: TrackScore, track: Track): string[] {
796
944
  const entries: string[] = [];
@@ -808,13 +956,15 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
808
956
  1,
809
957
  )}`,
810
958
  );
811
- if (track.pedal) {
812
- const events = track.pedal.map(
959
+ for (const key of ["pedal", "softPedal", "sostenuto"] as const) {
960
+ const lane = track[key];
961
+ if (!lane) continue;
962
+ const events = lane.map(
813
963
  (event) =>
814
964
  `[${num(event.tick / score.ticksPerBeat)}, ${str(event.state)}]`,
815
965
  );
816
966
  entries.push(
817
- `pedal: ${list(events, INDENT, "pedal: ".length, 1, events.length > 1)}`,
967
+ `${key}: ${list(events, INDENT, `${key}: `.length, 1, events.length > 1)}`,
818
968
  );
819
969
  }
820
970
  if (track.velocityCurve)
@@ -851,6 +1001,20 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
851
1001
  )}`,
852
1002
  );
853
1003
  }
1004
+ if (track.guitar) {
1005
+ const { tune, capo, hand, ring, position } = track.guitar;
1006
+ const fields: [string, string][] = [];
1007
+ if (tune !== undefined)
1008
+ fields.push([
1009
+ "tune",
1010
+ typeof tune === "string" ? str(tune) : `[${tune.map(num).join(", ")}]`,
1011
+ ]);
1012
+ if (capo !== undefined) fields.push(["capo", num(capo)]);
1013
+ if (hand !== undefined) fields.push(["hand", num(hand)]);
1014
+ if (ring !== undefined) fields.push(["ring", num(ring)]);
1015
+ if (position !== undefined) fields.push(["position", num(position)]);
1016
+ entries.push(`guitar: ${obj(fields, INDENT, "guitar: ".length, 1)}`);
1017
+ }
854
1018
  return entries;
855
1019
  }
856
1020
 
@@ -1000,6 +1164,7 @@ function printModal(settings: TrackModal, indent: string): string {
1000
1164
  if (value === undefined) continue;
1001
1165
  params.push([key, typeof value === "number" ? num(value) : str(value)]);
1002
1166
  }
1167
+ if (settings.pair !== undefined) params.push(["pair", str(settings.pair)]);
1003
1168
  const preset = settings.preset;
1004
1169
  if (params.length === 0 && preset && preset !== "marimba") return str(preset);
1005
1170
  const head = preset ? str(preset) : "";
@@ -1012,6 +1177,164 @@ function printModal(settings: TrackModal, indent: string): string {
1012
1177
  return `modal(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1013
1178
  }
1014
1179
 
1180
+ /**
1181
+ * A wind track's instrument: the preset word alone (`"flute"`) when there
1182
+ * are no overrides, else `wind("sax", { breath: 0.8 })` with keys in
1183
+ * `WIND_PARAMS` order. Every wind preset word is also an instrument word.
1184
+ */
1185
+ function printWind(settings: TrackWind, indent: string): string {
1186
+ const params: [string, string][] = [];
1187
+ for (const key of Object.keys(WIND_PARAMS)) {
1188
+ const value = (settings as Record<string, unknown>)[key];
1189
+ if (value === undefined) continue;
1190
+ params.push([
1191
+ key,
1192
+ typeof value === "number"
1193
+ ? num(value)
1194
+ : typeof value === "boolean"
1195
+ ? String(value)
1196
+ : str(String(value)),
1197
+ ]);
1198
+ }
1199
+ const preset = settings.preset;
1200
+ if (params.length === 0 && preset) return str(preset);
1201
+ const head = preset ? str(preset) : "";
1202
+ if (params.length === 0) return `wind(${head})`;
1203
+ const lead = head ? `${head}, ` : "";
1204
+ const inline = `wind(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1205
+ if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
1206
+ return inline;
1207
+ const inner = indent + INDENT;
1208
+ return `wind(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1209
+ }
1210
+
1211
+ /**
1212
+ * A sing track's instrument: the preset word alone (`"choir"`) when there
1213
+ * are no overrides and the preset is an instrument word, else
1214
+ * `sing("khoomei", { drone: "D3" })` with keys in `SING_KEY_ORDER`; the
1215
+ * drone prints as a note name.
1216
+ */
1217
+ function printSing(settings: TrackSing, indent: string): string {
1218
+ const params: [string, string][] = [];
1219
+ for (const key of SING_KEY_ORDER) {
1220
+ const value = (settings as Record<string, unknown>)[key];
1221
+ if (value === undefined) continue;
1222
+ params.push([
1223
+ key,
1224
+ key === "drone"
1225
+ ? str(singNoteName(value as number))
1226
+ : Array.isArray(value)
1227
+ ? `[${value.map((v) => num(v as number)).join(", ")}]`
1228
+ : typeof value === "number"
1229
+ ? num(value)
1230
+ : str(String(value)),
1231
+ ]);
1232
+ }
1233
+ const preset = settings.preset;
1234
+ if (params.length === 0 && preset && SING_BARE_WORDS.has(preset))
1235
+ return str(preset);
1236
+ if (params.length === 0 && !preset) return str(SING_INSTRUMENT);
1237
+ const head = preset ? str(preset) : "";
1238
+ if (params.length === 0) return `sing(${head})`;
1239
+ const lead = head ? `${head}, ` : "";
1240
+ const inline = `sing(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1241
+ if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
1242
+ return inline;
1243
+ const inner = indent + INDENT;
1244
+ return `sing(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1245
+ }
1246
+
1247
+ /** Sing presets that are also instrument words (`"choir"`). */
1248
+ const SING_BARE_WORDS: ReadonlySet<string> = new Set([
1249
+ "aah",
1250
+ "ooh",
1251
+ "choir",
1252
+ "chorale",
1253
+ "khoomei",
1254
+ "sygyt",
1255
+ "kargyraa",
1256
+ ]);
1257
+
1258
+ /**
1259
+ * A vocoder's `src` as the SDK reads it back: the track's name slug when it
1260
+ * names exactly that track (no other slug or id collides), else the id.
1261
+ */
1262
+ function vocoderSrcText(
1263
+ score: TrackScore,
1264
+ src: string | undefined,
1265
+ ): string | undefined {
1266
+ if (src === undefined) return undefined;
1267
+ const target = score.tracks.find((track) => track.id === src);
1268
+ if (!target) return src;
1269
+ const slug = trackSlug(target.name);
1270
+ const clash = score.tracks.some(
1271
+ (track) =>
1272
+ track.id !== target.id &&
1273
+ (track.id === slug || trackSlug(track.name) === slug),
1274
+ );
1275
+ return clash ? src : slug;
1276
+ }
1277
+
1278
+ /**
1279
+ * A vocoder: the bare word `"vocoder"` for a built-in carrier with nothing
1280
+ * set, else `vocoder("talkbox", { src: "vox", formant: 2 })` with `src`
1281
+ * first and the rest in `VOCODER_PARAMS` order.
1282
+ */
1283
+ function printVocoder(
1284
+ settings: TrackVocoder,
1285
+ src: string | undefined,
1286
+ prefix: string,
1287
+ indent: string,
1288
+ ): string {
1289
+ const params: [string, string][] = [];
1290
+ if (src !== undefined) params.push(["src", str(src)]);
1291
+ for (const key of Object.keys(VOCODER_PARAMS)) {
1292
+ const value = (settings as Record<string, unknown>)[key];
1293
+ if (value === undefined) continue;
1294
+ params.push([
1295
+ key,
1296
+ typeof value === "number"
1297
+ ? num(value)
1298
+ : typeof value === "boolean"
1299
+ ? String(value)
1300
+ : str(String(value)),
1301
+ ]);
1302
+ }
1303
+ const preset = settings.preset;
1304
+ if (params.length === 0 && !preset && prefix === "instrument: ")
1305
+ return str(VOCODER_INSTRUMENT);
1306
+ const head = preset ? str(preset) : "";
1307
+ if (params.length === 0) return `vocoder(${head})`;
1308
+ const lead = head ? `${head}, ` : "";
1309
+ const inline = `vocoder(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1310
+ if (indent.length + prefix.length + inline.length + 1 <= WIDTH) return inline;
1311
+ const inner = indent + INDENT;
1312
+ return `vocoder(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1313
+ }
1314
+
1315
+ /**
1316
+ * A track's autotune: the preset word alone (`"hard"`) when nothing
1317
+ * overrides it, else `autotune("pop", { speed: 40 })` with keys in
1318
+ * `AUTOTUNE_FIELDS` order.
1319
+ */
1320
+ function printAutotune(settings: TrackAutotune, indent: string): string {
1321
+ const params: [string, string][] = [];
1322
+ for (const key of AUTOTUNE_FIELDS) {
1323
+ if (key === "preset") continue;
1324
+ const value = settings[key];
1325
+ if (value === undefined) continue;
1326
+ params.push([key, typeof value === "number" ? num(value) : str(value)]);
1327
+ }
1328
+ const preset = settings.preset;
1329
+ if (params.length === 0 && preset) return str(preset);
1330
+ const lead = preset ? `${str(preset)}, ` : "";
1331
+ const inline = `autotune(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1332
+ if (indent.length + "autotune: ".length + inline.length + 1 <= WIDTH)
1333
+ return inline;
1334
+ const inner = indent + INDENT;
1335
+ return `autotune(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1336
+ }
1337
+
1015
1338
  /** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
1016
1339
  function printWavetable(settings: TrackWavetable, indent: string): string {
1017
1340
  const src = settings.table.src;
@@ -1071,6 +1394,23 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
1071
1394
  if (ref.bpm !== undefined) entries.push(["bpm", num(ref.bpm)]);
1072
1395
  if (ref.fitmode !== undefined) entries.push(["fitmode", str(ref.fitmode)]);
1073
1396
  if (ref.len !== undefined) entries.push(["len", num(ref.len)]);
1397
+ if (ref.shift !== undefined) entries.push(["shift", num(ref.shift)]);
1398
+ if (ref.formant !== undefined) entries.push(["formant", num(ref.formant)]);
1399
+ if (ref.fadeInTime !== undefined)
1400
+ entries.push(["fadeInTime", num(ref.fadeInTime)]);
1401
+ if (ref.fadeTime !== undefined) entries.push(["fadeTime", num(ref.fadeTime)]);
1402
+ if (ref.from !== undefined) {
1403
+ const from = ref.from;
1404
+ const parts: [string, string][] = [["source", str(from.source)]];
1405
+ if (from.section !== undefined) parts.push(["section", str(from.section)]);
1406
+ if (from.bars !== undefined)
1407
+ parts.push(["bars", `[${num(from.bars[0])}, ${num(from.bars[1])}]`]);
1408
+ parts.push(["score", str(from.score)]);
1409
+ entries.push(["from", obj(parts, indent + INDENT, "from: ".length, 1)]);
1410
+ }
1411
+ if (ref.vel !== undefined)
1412
+ entries.push(["vel", `[${num(ref.vel[0])}, ${num(ref.vel[1])}]`]);
1413
+ if (ref.rr !== undefined) entries.push(["rr", str(ref.rr)]);
1074
1414
  if (entries.length === 1) return str(ref.src);
1075
1415
  return obj(entries, indent, prefix, 1);
1076
1416
  }
@@ -1138,6 +1478,14 @@ function fill(
1138
1478
  return `[\n${lines.join("\n")}\n${indent}]`;
1139
1479
  }
1140
1480
 
1481
+ /** `style("id", { seed, bars, blend })`: provenance, seed and bars always written. */
1482
+ function printStyle(value: SongStyle): string {
1483
+ const options = [`seed: ${num(value.seed)}`, `bars: ${num(value.bars)}`];
1484
+ if (value.blend)
1485
+ options.push(`blend: [${str(value.blend.id)}, ${num(value.blend.weight)}]`);
1486
+ return `style(${str(value.id)}, { ${options.join(", ")} })`;
1487
+ }
1488
+
1141
1489
  /** `master: { … }`, always expanded like `fx`; units in chain order. */
1142
1490
  function printMaster(master: SongMaster): string {
1143
1491
  const inner = INDENT + INDENT;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The SDK is one vendored file with no imports, so `core/sdk/v1.ts` carries
3
+ * a copy of the lyric grammar (`core/lyrics.ts`) between two marker lines.
4
+ * `bun core/sdk/sync-lyrics.ts` rewrites it and `sync-lyrics.test.ts` fails
5
+ * when it is stale. Same scheme as `sync-instruments.ts`.
6
+ */
7
+
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { format } from "prettier";
11
+
12
+ export const BEGIN_MARKER =
13
+ "// BEGIN lyrics: generated from core/lyrics.ts by core/sdk/sync-lyrics.ts";
14
+ export const END_MARKER = "// END lyrics";
15
+
16
+ const ROOT = join(import.meta.dir, "..", "..");
17
+ export const LYRICS_PATH = join(ROOT, "core", "lyrics.ts");
18
+ export const SDK_PATH = join(ROOT, "core", "sdk", "v1.ts");
19
+
20
+ /** The lyric block (markers included) for a `core/lyrics.ts` source. */
21
+ export function lyricBlock(source: string): string {
22
+ const start = source.indexOf("// ---- lyrics");
23
+ if (start < 0) throw new Error("core/lyrics.ts layout changed");
24
+ const body = source
25
+ .slice(source.indexOf("\n", start) + 1)
26
+ .replace(/^export /gm, "")
27
+ .trim();
28
+ return `${BEGIN_MARKER}\n${body}\n${END_MARKER}`;
29
+ }
30
+
31
+ /** `sdk` with its lyric block replaced, formatted like the repo. */
32
+ export async function syncedSdk(sdk: string, lyrics: string): Promise<string> {
33
+ const from = sdk.indexOf(BEGIN_MARKER);
34
+ const to = sdk.indexOf(END_MARKER);
35
+ if (from < 0 || to < 0) throw new Error("v1.ts has no lyric markers");
36
+ const next =
37
+ sdk.slice(0, from) + lyricBlock(lyrics) + sdk.slice(to + END_MARKER.length);
38
+ return format(next, { parser: "typescript", filepath: SDK_PATH });
39
+ }
40
+
41
+ if (import.meta.main) {
42
+ const [sdk, lyrics] = await Promise.all([
43
+ readFile(SDK_PATH, "utf8"),
44
+ readFile(LYRICS_PATH, "utf8"),
45
+ ]);
46
+ const next = await syncedSdk(sdk, lyrics);
47
+ if (next !== sdk) await writeFile(SDK_PATH, next);
48
+ console.log(next === sdk ? "v1.ts lyrics up to date" : "v1.ts updated");
49
+ }