@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
package/core/keys.ts CHANGED
@@ -35,6 +35,70 @@ export function isPianoFamily(
35
35
  return (PIANO_FAMILIES as readonly string[]).includes(instrument ?? "");
36
36
  }
37
37
 
38
+ /**
39
+ * Electric keys (0.6.1, keys-electric): the Rhodes-style tine piano, the
40
+ * Wurlitzer-style reed piano and the clavinet. They share `Track.keys` and
41
+ * the `KEYS_PARAMS` table with the pianos; `KEYS_PARAM_FAMILIES` says which
42
+ * rows each family reads.
43
+ */
44
+ export const ELECTRIC_FAMILIES = Object.freeze([
45
+ "epiano",
46
+ "wurli",
47
+ "clav",
48
+ ] as const);
49
+
50
+ export type ElectricFamily = (typeof ELECTRIC_FAMILIES)[number];
51
+
52
+ export function isElectricFamily(
53
+ instrument: string | undefined,
54
+ ): instrument is ElectricFamily {
55
+ return (ELECTRIC_FAMILIES as readonly string[]).includes(instrument ?? "");
56
+ }
57
+
58
+ /**
59
+ * Why una corda would not sound on this track (0.6.1), or undefined when it
60
+ * does: only the modeled pianos with a `keys` object read `softPedal`.
61
+ */
62
+ export function softPedalNote(
63
+ track: Readonly<{ id: string; instrument: string; keys?: unknown }>,
64
+ ): string | undefined {
65
+ if (!isPianoFamily(track.instrument))
66
+ return `${track.id} is ${track.instrument}; una corda acts on the modeled pianos (${PIANO_FAMILIES.join(" ")})`;
67
+ if (track.keys === undefined)
68
+ return `${track.id} has no modeled piano yet; keys preset ${track.instrument} first`;
69
+ return undefined;
70
+ }
71
+
72
+ /** The organ families (f061-organ): tonewheel, combo and pipe organs. */
73
+ export const ORGAN_FAMILIES = Object.freeze([
74
+ "tonewheel",
75
+ "combo",
76
+ "pipe",
77
+ ] as const);
78
+
79
+ export type OrganFamily = (typeof ORGAN_FAMILIES)[number];
80
+
81
+ export function isOrganFamily(
82
+ instrument: string | undefined,
83
+ ): instrument is OrganFamily {
84
+ return (ORGAN_FAMILIES as readonly string[]).includes(instrument ?? "");
85
+ }
86
+
87
+ /** Every keys family (the instruments that read `Track.keys`). */
88
+ export const KEYS_FAMILIES = Object.freeze([
89
+ ...PIANO_FAMILIES,
90
+ ...ELECTRIC_FAMILIES,
91
+ ...ORGAN_FAMILIES,
92
+ ] as const);
93
+
94
+ export type KeysFamily = (typeof KEYS_FAMILIES)[number];
95
+
96
+ export function isKeysFamily(
97
+ instrument: string | undefined,
98
+ ): instrument is KeysFamily {
99
+ return (KEYS_FAMILIES as readonly string[]).includes(instrument ?? "");
100
+ }
101
+
38
102
  /** Body EQ voicings (soundboard and case colour). */
39
103
  export const PIANO_BODIES = Object.freeze([
40
104
  "grand",
@@ -157,8 +221,316 @@ export const KEYS_PARAMS: Readonly<Record<string, ParamSpec>> = Object.freeze({
157
221
  doc: "pitch wobble depth in semitones",
158
222
  strudel: ["vibmod", "vmod"],
159
223
  },
224
+ // keys-electric (0.6.1): epiano, wurli and clav rows.
225
+ bark: unit(0.35, "pickup drive: growl when played hard (epiano, wurli)"),
226
+ bell: unit(0.5, "tine or reed bell ping (epiano, wurli)"),
227
+ tone: {
228
+ kind: "number",
229
+ min: 0,
230
+ max: 12000,
231
+ default: 0,
232
+ step: 100,
233
+ unit: "Hz",
234
+ automate: true,
235
+ doc: "output low-pass of the electric keys; 0 is off",
236
+ },
237
+ pickup: {
238
+ kind: "enum",
239
+ values: ["neck", "bridge", "both", "out"],
240
+ default: "both",
241
+ doc: "clav pickup switch; out is both pickups out of phase",
242
+ },
243
+ mute: unit(0, "clav mute slider: damps the upper partials"),
244
+ vibe: unit(
245
+ 0,
246
+ "suitcase stereo vibrato depth: antiphase left/right pan (epiano)",
247
+ true,
248
+ ),
249
+ vibehz: {
250
+ kind: "number",
251
+ min: 0.5,
252
+ max: 12,
253
+ default: 4,
254
+ step: 0.25,
255
+ unit: "Hz",
256
+ doc: "suitcase vibrato rate (epiano)",
257
+ },
258
+ trem: unit(
259
+ 0,
260
+ "tremolo depth: reed piano at 5.6 Hz (wurli), pipe tremulant (pipe)",
261
+ true,
262
+ ),
263
+ // keys-electric (0.6.1): piano sympathetic resonance (pianos only; 0 keeps
264
+ // 0.6.0 renders byte-identical).
265
+ sym: unit(
266
+ 0,
267
+ "sympathetic string resonance while the sustain pedal is down; 0 is off",
268
+ ),
269
+ // organ (f061-organ): tonewheel, combo and pipe rows (ORGAN_ROWS says
270
+ // which family reads each). The drawbars, registers and stops strings
271
+ // live in ORGAN_TEXT (ParamSpec has no text kind).
272
+ perc: {
273
+ kind: "enum",
274
+ values: ["off", "2nd", "3rd"],
275
+ default: "off",
276
+ doc: "tonewheel percussion harmonic, single-trigger; on mutes the 1' bar",
277
+ },
278
+ percdecay: {
279
+ kind: "enum",
280
+ values: ["fast", "slow"],
281
+ default: "fast",
282
+ doc: "tonewheel percussion decay (fast 0.6 s, slow 1.8 s)",
283
+ },
284
+ click: unit(0.5, "tonewheel key click (contact bounce)"),
285
+ scanner: {
286
+ kind: "enum",
287
+ values: ["off", "v1", "v2", "v3", "c1", "c2", "c3"],
288
+ default: "c3",
289
+ doc: "tonewheel scanner vibrato (v1-v3) or chorus (c1-c3)",
290
+ },
291
+ drive: unit(0.15, "organ preamp overdrive", true),
292
+ rotary: {
293
+ kind: "enum",
294
+ values: ["slow", "fast", "stop"],
295
+ default: "slow",
296
+ doc: "rotary speaker at the start (lane keys-rotary: 0 stop, 1 slow, 2 fast)",
297
+ },
298
+ voice: {
299
+ kind: "enum",
300
+ values: ["flute", "reed", "bright"],
301
+ default: "reed",
302
+ doc: "combo register timbre: filtered square, reed pulse, bright saw",
303
+ },
304
+ chiff: unit(0.4, "pipe speech: the noise at each flue pipe's attack"),
305
+ wind: unit(0.3, "pipe wind instability (slow pitch and level drift)"),
306
+ percvol: {
307
+ kind: "enum",
308
+ values: ["normal", "soft"],
309
+ default: "normal",
310
+ doc: "tonewheel percussion volume: soft is about 6 dB down and keeps the drawbars at full level",
311
+ },
160
312
  });
161
313
 
314
+ /**
315
+ * The families each keys parameter applies to (menus and listings show a
316
+ * track only its family's rows). A parameter missing here is a piano row.
317
+ */
318
+ export const KEYS_PARAM_FAMILIES: Readonly<
319
+ Record<string, readonly KeysFamily[]>
320
+ > = Object.freeze({
321
+ hardness: KEYS_FAMILIES,
322
+ touch: KEYS_FAMILIES,
323
+ decay: KEYS_FAMILIES,
324
+ release: KEYS_FAMILIES,
325
+ width: KEYS_FAMILIES,
326
+ vib: KEYS_FAMILIES,
327
+ vibmod: KEYS_FAMILIES,
328
+ bark: ["epiano", "wurli"],
329
+ bell: ["epiano", "wurli"],
330
+ tone: ELECTRIC_FAMILIES,
331
+ pickup: ["clav"],
332
+ mute: ["clav"],
333
+ vibe: ["epiano"],
334
+ vibehz: ["epiano"],
335
+ trem: ["wurli", "pipe"],
336
+ // organ (f061-organ): the organ rows; organs list theirs in ORGAN_ROWS.
337
+ perc: ["tonewheel"],
338
+ percdecay: ["tonewheel"],
339
+ percvol: ["tonewheel"],
340
+ click: ["tonewheel"],
341
+ scanner: ["tonewheel"],
342
+ drive: ["tonewheel", "combo"],
343
+ rotary: ["tonewheel", "combo"],
344
+ voice: ["combo"],
345
+ chiff: ["pipe"],
346
+ wind: ["pipe"],
347
+ });
348
+
349
+ /** The keys parameters a family reads, in `KEYS_PARAMS` order. */
350
+ export function keysParamsFor(instrument: string | undefined): string[] {
351
+ if (isOrganFamily(instrument)) return [...ORGAN_ROWS[instrument]];
352
+ const family = isKeysFamily(instrument) ? instrument : "grand";
353
+ return Object.keys(KEYS_PARAMS).filter((name) => {
354
+ const families = KEYS_PARAM_FAMILIES[name];
355
+ return families ? families.includes(family) : isPianoFamily(family);
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Organ text rows (f061-organ), stored as strings after the KEYS_PARAMS
361
+ * values: `drawbars` nine digits 0-8 (16' 5⅓' 8' 4' 2⅔' 2' 1⅗' 1⅓' 1'),
362
+ * `registers` five digits 0-8 (combo 16' 8' 4' 2⅔' 2'), `stops` pipe stop
363
+ * names and registrations separated by spaces.
364
+ */
365
+ export const ORGAN_TEXT = Object.freeze({
366
+ drawbars: Object.freeze({
367
+ digits: 9,
368
+ default: "888000000",
369
+ doc: "tonewheel drawbars, nine digits 0-8: 16' 5⅓' 8' 4' 2⅔' 2' 1⅗' 1⅓' 1'",
370
+ }),
371
+ registers: Object.freeze({
372
+ digits: 5,
373
+ default: "08800",
374
+ doc: "combo registers, five digits 0-8: 16' 8' 4' 2⅔' 2'",
375
+ }),
376
+ stops: Object.freeze({
377
+ digits: 0,
378
+ default: "principal8 octave4 fifteenth2 mixture",
379
+ doc: "pipe stops (or a registration: plenum flutes cornet reeds strings full)",
380
+ }),
381
+ } as const);
382
+
383
+ export type OrganTextParam = keyof typeof ORGAN_TEXT;
384
+
385
+ export function isOrganText(name: string): name is OrganTextParam {
386
+ return Object.prototype.hasOwnProperty.call(ORGAN_TEXT, name);
387
+ }
388
+
389
+ /** Drawbar footages in drawbar order (labels for the menu and docs). */
390
+ export const DRAWBAR_FEET = Object.freeze([
391
+ "16'",
392
+ "5⅓'",
393
+ "8'",
394
+ "4'",
395
+ "2⅔'",
396
+ "2'",
397
+ "1⅗'",
398
+ "1⅓'",
399
+ "1'",
400
+ ]);
401
+
402
+ /** Combo register footages in register order. */
403
+ export const REGISTER_FEET = Object.freeze(["16'", "8'", "4'", "2⅔'", "2'"]);
404
+
405
+ /** Pipe stops, low to high pitch with the reeds last. */
406
+ export const PIPE_STOPS = Object.freeze([
407
+ "subbass16",
408
+ "bourdon16",
409
+ "principal8",
410
+ "flute8",
411
+ "gedackt8",
412
+ "gamba8",
413
+ "celeste8",
414
+ "octave4",
415
+ "flute4",
416
+ "nazard",
417
+ "fifteenth2",
418
+ "piccolo2",
419
+ "tierce",
420
+ "larigot",
421
+ "mixture",
422
+ "trombone16",
423
+ "trumpet8",
424
+ "oboe8",
425
+ "krummhorn8",
426
+ ] as const);
427
+
428
+ /** Named registrations: a word that stands for a set of stops. */
429
+ export const PIPE_REGISTRATIONS: Readonly<Record<string, readonly string[]>> =
430
+ Object.freeze({
431
+ plenum: ["subbass16", "principal8", "octave4", "fifteenth2", "mixture"],
432
+ flutes: ["gedackt8", "flute4"],
433
+ cornet: ["flute8", "flute4", "nazard", "piccolo2", "tierce"],
434
+ reeds: ["trumpet8", "oboe8", "nazard", "tierce", "larigot"],
435
+ strings: ["gamba8", "celeste8"],
436
+ full: PIPE_STOPS.filter((stop) => stop !== "celeste8"),
437
+ });
438
+
439
+ /** The stop names a `stops` value sounds (registrations expanded, deduped). */
440
+ export function pipeStops(text: string): string[] {
441
+ const out: string[] = [];
442
+ for (const word of text.trim().split(/\s+/)) {
443
+ const names = PIPE_REGISTRATIONS[word] ?? [word];
444
+ for (const name of names) if (!out.includes(name)) out.push(name);
445
+ }
446
+ return out.filter((name) => (PIPE_STOPS as readonly string[]).includes(name));
447
+ }
448
+
449
+ /** Validates one organ text value; returns the stored string. */
450
+ export function normalizeOrganText(
451
+ name: OrganTextParam,
452
+ value: unknown,
453
+ ): string {
454
+ if (typeof value !== "string")
455
+ throw new FxValidationError(`keys ${name} must be a string`);
456
+ const spec = ORGAN_TEXT[name];
457
+ if (spec.digits > 0) {
458
+ if (!new RegExp(`^[0-8]{${spec.digits}}$`).test(value))
459
+ throw new FxValidationError(
460
+ `keys ${name} must be ${spec.digits} digits 0-8 (like ${spec.default})`,
461
+ );
462
+ return value;
463
+ }
464
+ const words = value.trim().toLowerCase().split(/\s+/).filter(Boolean);
465
+ if (words.length === 0)
466
+ throw new FxValidationError("keys stops must name at least one stop");
467
+ for (const word of words)
468
+ if (
469
+ !(PIPE_STOPS as readonly string[]).includes(word) &&
470
+ !Object.prototype.hasOwnProperty.call(PIPE_REGISTRATIONS, word)
471
+ )
472
+ throw new FxValidationError(
473
+ `keys stops: unknown stop "${word}" (stops ${PIPE_STOPS.join(" ")}; registrations ${Object.keys(PIPE_REGISTRATIONS).join(" ")})`,
474
+ );
475
+ return words.join(" ");
476
+ }
477
+
478
+ /** The KEYS_PARAMS rows and text rows each organ family reads. */
479
+ export const ORGAN_ROWS: Readonly<Record<OrganFamily, readonly string[]>> =
480
+ Object.freeze({
481
+ tonewheel: [
482
+ "drawbars",
483
+ "perc",
484
+ "percdecay",
485
+ "percvol",
486
+ "click",
487
+ "scanner",
488
+ "drive",
489
+ "rotary",
490
+ ],
491
+ combo: ["registers", "voice", "vib", "vibmod", "drive", "rotary"],
492
+ pipe: ["stops", "chiff", "wind", "trem"],
493
+ });
494
+
495
+ /** Every organ-only row name (KEYS_PARAMS rows and text rows). */
496
+ export const ORGAN_PARAM_NAMES: readonly string[] = Object.freeze([
497
+ "perc",
498
+ "percdecay",
499
+ "click",
500
+ "scanner",
501
+ "drive",
502
+ "rotary",
503
+ "voice",
504
+ "chiff",
505
+ "wind",
506
+ "trem",
507
+ "percvol",
508
+ ...Object.keys(ORGAN_TEXT),
509
+ ]);
510
+
511
+ /** Each organ family's own defaults over KEYS_PARAMS. */
512
+ export const ORGAN_FAMILY_DEFAULTS: Readonly<
513
+ Record<OrganFamily, Readonly<Record<string, number | string>>>
514
+ > = Object.freeze({
515
+ tonewheel: Object.freeze({ drawbars: ORGAN_TEXT.drawbars.default }),
516
+ combo: Object.freeze({
517
+ registers: ORGAN_TEXT.registers.default,
518
+ vib: 6,
519
+ vibmod: 0.12,
520
+ drive: 0,
521
+ rotary: "stop",
522
+ }),
523
+ pipe: Object.freeze({ stops: ORGAN_TEXT.stops.default }),
524
+ });
525
+
526
+ /**
527
+ * The rows a keys family shows and reads, in display order (an organ's
528
+ * ORGAN_ROWS, else its KEYS_PARAM_FAMILIES rows).
529
+ */
530
+ export function keysRowsFor(instrument: string | undefined): string[] {
531
+ return keysParamsFor(instrument);
532
+ }
533
+
162
534
  /** The parameters shown first in the menu and `keys` listing. */
163
535
  export const KEYS_SIMPLE = Object.freeze([
164
536
  "hardness",
@@ -167,6 +539,25 @@ export const KEYS_SIMPLE = Object.freeze([
167
539
  "felt",
168
540
  ]);
169
541
 
542
+ /** The electric families' first rows. */
543
+ export const ELECTRIC_SIMPLE: Readonly<
544
+ Record<ElectricFamily, readonly string[]>
545
+ > = Object.freeze({
546
+ epiano: Object.freeze(["bark", "bell", "tone", "vibe"]),
547
+ wurli: Object.freeze(["bark", "bell", "tone", "trem"]),
548
+ clav: Object.freeze(["pickup", "mute", "tone", "decay"]),
549
+ });
550
+
551
+ /** The first rows for a keys family. */
552
+ export function keysSimpleFor(
553
+ instrument: string | undefined,
554
+ ): readonly string[] {
555
+ if (isOrganFamily(instrument)) return ORGAN_ROWS[instrument];
556
+ return isElectricFamily(instrument)
557
+ ? ELECTRIC_SIMPLE[instrument]
558
+ : KEYS_SIMPLE;
559
+ }
560
+
170
561
  export type TrackKeys = Readonly<
171
562
  Record<string, number | string> & { preset?: string }
172
563
  >;
@@ -187,6 +578,15 @@ export const PIANO_FAMILY_DEFAULTS: Readonly<
187
578
  prepared: Object.freeze({ prep: 0.6, body: "prepared" }),
188
579
  });
189
580
 
581
+ /** Each electric family's own defaults over `KEYS_PARAMS`. */
582
+ export const ELECTRIC_FAMILY_DEFAULTS: Readonly<
583
+ Record<ElectricFamily, Readonly<Record<string, number | string>>>
584
+ > = Object.freeze({
585
+ epiano: Object.freeze({ width: 0.3, tone: 6000 }),
586
+ wurli: Object.freeze({ width: 0.3, bark: 0.5, bell: 0.2, tone: 5000 }),
587
+ clav: Object.freeze({ width: 0.3 }),
588
+ });
589
+
190
590
  /** Effects a preset sets with the voice (applied over the effect defaults). */
191
591
  export type KeysPresetFx = Readonly<{
192
592
  /** The track filter (`Track.filter`). */
@@ -197,7 +597,7 @@ export type KeysPresetFx = Readonly<{
197
597
  }>;
198
598
 
199
599
  export type KeysPreset = Readonly<{
200
- instrument: PianoFamily;
600
+ instrument: KeysFamily;
201
601
  /** The preset's overrides over the family defaults. */
202
602
  keys: Readonly<Record<string, number | string>>;
203
603
  doc: string;
@@ -259,9 +659,132 @@ export const KEYS_PRESETS: Readonly<Record<string, KeysPreset>> = Object.freeze(
259
659
  doc: "bolts, rubber and screws on 60% of keys (seeded per key)",
260
660
  styles: "Cage, Aphex-style, Oneohtrix Point Never",
261
661
  },
662
+ // keys-electric (0.6.1)
663
+ epiano: {
664
+ instrument: "epiano",
665
+ keys: {},
666
+ doc: "Rhodes-style stage tine piano: bell and bark with velocity",
667
+ styles: "jazz, soul, Flying Lotus, Radiohead",
668
+ },
669
+ suitcase: {
670
+ instrument: "epiano",
671
+ keys: { vibe: 0.7, vibehz: 4, bark: 0.3 },
672
+ doc: "tine piano with the suitcase stereo vibrato, left and right in turn",
673
+ styles: "70s soul, Stevie Wonder ballads, neo-soul",
674
+ },
675
+ dyno: {
676
+ instrument: "epiano",
677
+ keys: { bell: 0.85, tone: 9000, hardness: 0.7 },
678
+ doc: "bright, bell-heavy tine piano",
679
+ styles: "80s ballads, city pop",
680
+ },
681
+ wurli: {
682
+ instrument: "wurli",
683
+ keys: {},
684
+ doc: "Wurlitzer-style reed piano, nasal and growly",
685
+ styles: "soul, funk, Radiohead, Supertramp",
686
+ },
687
+ clav: {
688
+ instrument: "clav",
689
+ keys: {},
690
+ doc: "clavinet, both pickups",
691
+ styles: "funk, disco, reggae",
692
+ },
693
+ funkclav: {
694
+ instrument: "clav",
695
+ keys: { pickup: "out", mute: 0.3 },
696
+ doc: "clavinet with both pickups out of phase and the mute slider up",
697
+ styles: "Stevie Wonder funk, Bill Withers",
698
+ },
699
+ // organ (f061-organ)
700
+ tonewheel: {
701
+ instrument: "tonewheel",
702
+ keys: {},
703
+ doc: "tonewheel organ 888000000, scanner C3, slow rotary",
704
+ styles: "jazz, soul, gospel comping, Beach House",
705
+ },
706
+ gospel: {
707
+ instrument: "tonewheel",
708
+ keys: {
709
+ drawbars: "888800008",
710
+ perc: "3rd",
711
+ rotary: "fast",
712
+ drive: 0.35,
713
+ },
714
+ doc: "888800008, 3rd percussion (mutes the 1'), fast rotary, driven",
715
+ styles: "gospel, Motown, rock",
716
+ },
717
+ jazzorgan: {
718
+ instrument: "tonewheel",
719
+ keys: {
720
+ drawbars: "888000000",
721
+ perc: "3rd",
722
+ percvol: "soft",
723
+ scanner: "c3",
724
+ },
725
+ doc: "888000000 with soft fast 3rd percussion, slow rotary",
726
+ styles: "Jimmy Smith jazz, soul jazz",
727
+ },
728
+ combo: {
729
+ instrument: "combo",
730
+ keys: {},
731
+ doc: "Farfisa-style reed registers 08800 with vibrato",
732
+ styles: "Beach House, 60s garage, Glass, Reich",
733
+ },
734
+ vox: {
735
+ instrument: "combo",
736
+ keys: { registers: "08880", voice: "bright", vib: 5.5, vibmod: 0.1 },
737
+ doc: "Vox-style bright saw registers 08880",
738
+ styles: "60s, Doors-ish, shoegaze drones",
739
+ },
740
+ pipe: {
741
+ instrument: "pipe",
742
+ keys: { stops: "plenum" },
743
+ reverb: { mix: 0.35, size: 0.9 },
744
+ doc: "plenum: principal chorus, mixture and 16' in a church",
745
+ styles: "Bach fugue, Bach cantata",
746
+ },
747
+ flutes: {
748
+ instrument: "pipe",
749
+ keys: { stops: "flutes", trem: 0.4, chiff: 0.5 },
750
+ reverb: { mix: 0.3, size: 0.8 },
751
+ doc: "gedackt 8' and flute 4' with tremulant",
752
+ styles: "Bach chorale preludes, Eno, ambient",
753
+ },
754
+ cornet: {
755
+ instrument: "pipe",
756
+ keys: { stops: "cornet" },
757
+ reverb: { mix: 0.3, size: 0.8 },
758
+ doc: "the cornet solo combination (8' 4' 2⅔' 2' 1⅗')",
759
+ styles: "Bach, Buxtehude, French classical",
760
+ },
761
+ reeds: {
762
+ instrument: "pipe",
763
+ keys: { stops: "reeds" },
764
+ reverb: { mix: 0.3, size: 0.85 },
765
+ doc: "trumpet and oboe with nazard, tierce and larigot",
766
+ styles: "Messiaen",
767
+ },
768
+ celeste: {
769
+ instrument: "pipe",
770
+ keys: { stops: "strings", wind: 0.4 },
771
+ reverb: { mix: 0.35, size: 0.9 },
772
+ doc: "gamba and celeste: slow beating strings",
773
+ styles: "Messiaen, ambient, La Monte Young drones",
774
+ },
262
775
  },
263
776
  );
264
777
 
778
+ /** Organ words that name a keys preset (aliases of the presets). */
779
+ export const ORGAN_ALIASES: Readonly<Record<string, string>> = Object.freeze({
780
+ hammond: "tonewheel",
781
+ b3: "tonewheel",
782
+ farfisa: "combo",
783
+ church: "pipe",
784
+ pipeorgan: "pipe",
785
+ churchorgan: "pipe",
786
+ });
787
+
265
788
  export function isKeysPreset(name: string): boolean {
266
789
  return Object.prototype.hasOwnProperty.call(KEYS_PRESETS, name);
267
790
  }
@@ -274,6 +797,10 @@ export function isKeysParam(name: string): boolean {
274
797
  export function keysParamName(name: string): string | undefined {
275
798
  const lower = name.toLowerCase();
276
799
  if (isKeysParam(lower)) return lower;
800
+ if (isOrganText(lower)) return lower;
801
+ if (lower === "reg" || lower === "registration" || lower === "stop")
802
+ return "stops";
803
+ if (lower === "drawbar") return "drawbars";
277
804
  if (lower === "aftersound") return "after";
278
805
  if (lower === "prepared") return "prep";
279
806
  for (const [key, spec] of Object.entries(KEYS_PARAMS))
@@ -292,9 +819,30 @@ export const KEYS_LANE_PARAMS: readonly Readonly<{
292
819
  (entry): entry is [string, NumberParam] =>
293
820
  entry[1].kind === "number" && entry[1].automate === true,
294
821
  )
295
- .map(([param, spec]) => Object.freeze({ param, spec })),
822
+ .map(([param, spec]) => Object.freeze({ param, spec }))
823
+ .concat(
824
+ // f061-organ: the rotary speed as a lane (0 stop, 1 slow, 2 fast);
825
+ // the rotors glide between speeds with their own inertia.
826
+ Object.freeze({
827
+ param: "rotary",
828
+ spec: Object.freeze({
829
+ kind: "number",
830
+ min: 0,
831
+ max: 2,
832
+ default: 1,
833
+ step: 1,
834
+ integer: true,
835
+ doc: "rotary speed: 0 stop, 1 slow, 2 fast",
836
+ }) as NumberParam,
837
+ }),
838
+ ),
296
839
  );
297
840
 
841
+ /** The `rotary` enum as its lane number (0 stop, 1 slow, 2 fast). */
842
+ export function rotarySpeed(value: unknown): number {
843
+ return value === "fast" ? 2 : value === "stop" ? 0 : 1;
844
+ }
845
+
298
846
  /**
299
847
  * Validates `Track.keys`: overrides in `KEYS_PARAMS` order, then `preset`.
300
848
  * `{}` stays `{}` (its presence turns the modelled piano on); null and
@@ -305,6 +853,7 @@ export function normalizeKeys(input: unknown): TrackKeys | undefined {
305
853
  if (!isRecord(input))
306
854
  throw new FxValidationError("track keys must be an object or null");
307
855
  const params: Record<string, unknown> = {};
856
+ const text: Partial<Record<OrganTextParam, string>> = {};
308
857
  let preset: string | undefined;
309
858
  for (const [key, value] of Object.entries(input)) {
310
859
  if (value === undefined || value === null) continue;
@@ -316,6 +865,10 @@ export function normalizeKeys(input: unknown): TrackKeys | undefined {
316
865
  preset = value;
317
866
  continue;
318
867
  }
868
+ if (isOrganText(key)) {
869
+ text[key] = normalizeOrganText(key, value);
870
+ continue;
871
+ }
319
872
  if (!isKeysParam(key)) {
320
873
  const name = keysParamName(key);
321
874
  throw new FxValidationError(
@@ -327,6 +880,11 @@ export function normalizeKeys(input: unknown): TrackKeys | undefined {
327
880
  const values = normalizeParams(KEYS_PARAMS, params, "keys", false);
328
881
  return Object.freeze({
329
882
  ...(values as Record<string, number | string>),
883
+ ...Object.fromEntries(
884
+ Object.keys(ORGAN_TEXT)
885
+ .filter((key) => text[key as OrganTextParam] !== undefined)
886
+ .map((key) => [key, text[key as OrganTextParam]!]),
887
+ ),
330
888
  ...(preset ? { preset } : {}),
331
889
  });
332
890
  }
@@ -344,6 +902,10 @@ export function resolvedKeys(
344
902
  out[key] = spec.default as number | string;
345
903
  if (isPianoFamily(instrument))
346
904
  Object.assign(out, PIANO_FAMILY_DEFAULTS[instrument]);
905
+ if (isElectricFamily(instrument))
906
+ Object.assign(out, ELECTRIC_FAMILY_DEFAULTS[instrument]);
907
+ if (isOrganFamily(instrument))
908
+ Object.assign(out, ORGAN_FAMILY_DEFAULTS[instrument]);
347
909
  const preset = keys?.preset ? KEYS_PRESETS[keys.preset] : undefined;
348
910
  if (preset) Object.assign(out, preset.keys);
349
911
  for (const [key, value] of Object.entries(keys ?? {}))
@@ -351,6 +913,13 @@ export function resolvedKeys(
351
913
  return out;
352
914
  }
353
915
 
916
+ /** Other names for the electric keys presets (keys-electric, 0.6.1). */
917
+ const KEYS_ALIASES: Readonly<Record<string, string>> = Object.freeze({
918
+ rhodes: "epiano",
919
+ wurlitzer: "wurli",
920
+ clavinet: "clav",
921
+ });
922
+
354
923
  /**
355
924
  * What a new write of a keys word stores: `piano` and `grand` become the
356
925
  * modelled grand, a preset name its instrument and overrides. Undefined
@@ -358,7 +927,7 @@ export function resolvedKeys(
358
927
  */
359
928
  export function pianoWrite(word: string):
360
929
  | (Readonly<{
361
- instrument: PianoFamily;
930
+ instrument: KeysFamily;
362
931
  keys: TrackKeys;
363
932
  preset?: string;
364
933
  }> &
@@ -372,7 +941,7 @@ export function pianoWrite(word: string):
372
941
  : "upright"
373
942
  : lower === "feltpiano"
374
943
  ? "felt"
375
- : lower;
944
+ : (KEYS_ALIASES[lower] ?? ORGAN_ALIASES[lower] ?? lower);
376
945
  if (!isKeysPreset(name)) return undefined;
377
946
  const preset = KEYS_PRESETS[name]!;
378
947
  return Object.freeze({