@hraness/dawg 0.6.1 → 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 (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. 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,6 +21,7 @@ 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,
@@ -28,6 +29,18 @@ import {
28
29
  type TrackModal,
29
30
  } from "../resonators.ts";
30
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";
31
44
  import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
32
45
  import {
33
46
  BUILTIN_TABLE_PREFIX,
@@ -45,13 +58,15 @@ import {
45
58
  type SampleRef,
46
59
  type Sampler,
47
60
  type Section,
61
+ type AudioClip,
62
+ type Take,
48
63
  type Track,
49
64
  } from "../score.ts";
50
65
  import { trackSlug } from "../slug.ts";
51
- import { barStartTick } from "../tempo.ts";
66
+ import { barStartTick, timeWithinSong } from "../tempo.ts";
52
67
  import type { Tuning } from "../tuning.ts";
53
68
  import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
54
- import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
69
+ import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY, defaultClipId } from "./v1.ts";
55
70
 
56
71
  const WIDTH = 80;
57
72
  const INDENT = " ";
@@ -157,6 +172,13 @@ const RESERVED = new Set([
157
172
  "granular",
158
173
  "modal",
159
174
  "wind",
175
+ "sing",
176
+ "audio",
177
+ "take",
178
+ "lyrics",
179
+ "repeatAudio",
180
+ "vocoder",
181
+ "autotune",
160
182
  "slices",
161
183
  "euclid",
162
184
  "euclidRot",
@@ -197,6 +219,7 @@ export function printSong(
197
219
  const helpers = [
198
220
  "song",
199
221
  ...TIME_HELPERS.filter((name) => time.used.has(name)),
222
+ ...(score.style ? ["style"] : []),
200
223
  ];
201
224
  const lines: string[] = [`import { ${helpers.join(", ")} } from "dawg";`];
202
225
  for (const track of score.tracks)
@@ -227,6 +250,7 @@ export function printSong(
227
250
  )}`,
228
251
  );
229
252
  if (score.master) entries.push(printMaster(score.master));
253
+ if (score.style) entries.push(`style: ${printStyle(score.style)}`);
230
254
  // Song sections and form (0.5): printed only when the song has them.
231
255
  if (score.sections.length > 0)
232
256
  entries.push(
@@ -241,6 +265,7 @@ export function printSong(
241
265
  entries.push(`form: ${printForm(score.form, INDENT)}`);
242
266
  if (score.loopSection !== undefined)
243
267
  entries.push(`loopSection: ${str(score.loopSection)}`);
268
+ if (score.calibration) entries.push(`calibration: ${num(score.calibration)}`);
244
269
  lines.push("export default song({");
245
270
  for (const entry of entries) lines.push(`${INDENT}${entry},`);
246
271
  lines.push("});", "");
@@ -257,7 +282,8 @@ function printTime(score: TrackScore): {
257
282
  } {
258
283
  const marks: string[] = [];
259
284
  const used = new Set<string>();
260
- const time = score.time;
285
+ // Marks past the final barline are never heard and the SDK refuses them.
286
+ const time = timeWithinSong(score);
261
287
  if (!time) return { marks, used };
262
288
  const beat = (tick: number) => num(tick / score.ticksPerBeat);
263
289
  type Mark = { tick: number; order: number; text: string };
@@ -421,6 +447,20 @@ export function printTrack(score: TrackScore, track: Track): string {
421
447
  ? printWind(track.wind, INDENT)
422
448
  : undefined;
423
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");
424
464
 
425
465
  const entries: string[] = [
426
466
  `id: ${str(track.id)}`,
@@ -441,8 +481,19 @@ export function printTrack(score: TrackScore, track: Track): string {
441
481
  entries.push(`instrument: ${modalCall}`);
442
482
  } else if (windCall) {
443
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}`);
444
488
  } else entries.push(`instrument: ${str(track.instrument)}`);
489
+ if (vocoderCall && track.instrument !== VOCODER_INSTRUMENT)
490
+ entries.push(`vocoder: ${vocoderCall}`);
445
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
+ }
446
497
  if (track.granular && !grained)
447
498
  entries.push(
448
499
  `granular: ${obj(
@@ -595,6 +646,11 @@ export function printTrack(score: TrackScore, track: Track): string {
595
646
  );
596
647
  }
597
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
+ }
598
654
  const lanes: [string, readonly AutomationPoint[] | undefined][] = [
599
655
  ["volume", track.volumeAutomation],
600
656
  ["pan", track.panAutomation],
@@ -653,6 +709,20 @@ export function printTrack(score: TrackScore, track: Track): string {
653
709
  }
654
710
  if (rows.length === 0 || printed.length > 0)
655
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
+ }
656
726
 
657
727
  const names = [
658
728
  "track",
@@ -668,8 +738,13 @@ export function printTrack(score: TrackScore, track: Track): string {
668
738
  "granular",
669
739
  "modal",
670
740
  "wind",
741
+ "sing",
742
+ "vocoder",
743
+ "autotune",
671
744
  "euclid",
672
745
  "grid",
746
+ "audio",
747
+ "take",
673
748
  ]
674
749
  .filter((name) => used.has(name))
675
750
  .join(", ");
@@ -778,6 +853,9 @@ function expressionEntries(
778
853
  ),
779
854
  ]);
780
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)]);
781
859
  return entries.length === 0 ? undefined : entries;
782
860
  }
783
861
 
@@ -801,6 +879,66 @@ function call(
801
879
  return `${name}(${[...args, "{"].join(", ")}\n${body}\n${indent}})`;
802
880
  }
803
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
+
804
942
  /** A track's performance fields (SDK 1.15.0) as `key: literal` entries. */
805
943
  function performanceEntries(score: TrackScore, track: Track): string[] {
806
944
  const entries: string[] = [];
@@ -1070,6 +1208,133 @@ function printWind(settings: TrackWind, indent: string): string {
1070
1208
  return `wind(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1071
1209
  }
1072
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
+
1073
1338
  /** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
1074
1339
  function printWavetable(settings: TrackWavetable, indent: string): string {
1075
1340
  const src = settings.table.src;
@@ -1213,6 +1478,14 @@ function fill(
1213
1478
  return `[\n${lines.join("\n")}\n${indent}]`;
1214
1479
  }
1215
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
+
1216
1489
  /** `master: { … }`, always expanded like `fx`; units in chain order. */
1217
1490
  function printMaster(master: SongMaster): string {
1218
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
+ }