@hraness/dawg 0.5.0 → 0.6.1

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 (124) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/DAWG.md +592 -49
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +9 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +689 -2
  8. package/core/granular.ts +619 -0
  9. package/core/instruments.ts +442 -0
  10. package/core/keys.ts +955 -0
  11. package/core/resonators.ts +862 -0
  12. package/core/score.ts +529 -8
  13. package/core/sdk/eval-child.ts +2 -0
  14. package/core/sdk/print.ts +247 -16
  15. package/core/sdk/sync-instruments.ts +58 -0
  16. package/core/sdk/v1.ts +2417 -40
  17. package/core/sections.ts +44 -10
  18. package/core/strings.ts +1080 -0
  19. package/core/winds.ts +652 -0
  20. package/guides/automation.md +1 -0
  21. package/guides/chords.md +3 -1
  22. package/guides/effects.md +5 -2
  23. package/guides/media.md +1 -1
  24. package/guides/performance.md +3 -0
  25. package/guides/resample.md +26 -0
  26. package/guides/sounds.md +7 -2
  27. package/guides/tempo.md +1 -0
  28. package/package.json +1 -1
  29. package/src/agent/agent.ts +13 -0
  30. package/src/agent/chord-tools.ts +153 -1
  31. package/src/agent/expression-tools.ts +91 -0
  32. package/src/agent/granular-tools.ts +138 -0
  33. package/src/agent/models.ts +4 -4
  34. package/src/agent/ops.ts +42 -2
  35. package/src/agent/preview-tool.ts +7 -1
  36. package/src/agent/resample-tool.ts +131 -0
  37. package/src/agent/tools.ts +726 -17
  38. package/src/agent/xcb-agent.ts +13 -0
  39. package/src/audio/arrange.ts +42 -3
  40. package/src/audio/dsp/bank.ts +233 -0
  41. package/src/audio/dsp/envelope.ts +161 -0
  42. package/src/audio/dsp/fft.ts +6 -0
  43. package/src/audio/dsp/filters.ts +57 -0
  44. package/src/audio/dsp/interp.ts +112 -0
  45. package/src/audio/dsp/modal.ts +684 -0
  46. package/src/audio/dsp/onset.ts +197 -0
  47. package/src/audio/dsp/oversample.ts +202 -0
  48. package/src/audio/dsp/rng.ts +37 -0
  49. package/src/audio/dsp/shape.ts +81 -0
  50. package/src/audio/dsp/shift.ts +256 -0
  51. package/src/audio/dsp/stft.ts +73 -0
  52. package/src/audio/dsp/window.ts +77 -0
  53. package/src/audio/effects/chain.ts +23 -3
  54. package/src/audio/effects/common.ts +14 -0
  55. package/src/audio/effects/convolution.ts +106 -1
  56. package/src/audio/effects/gaze.ts +354 -0
  57. package/src/audio/effects/rig/cab.ts +99 -0
  58. package/src/audio/effects/rig/filters.ts +152 -0
  59. package/src/audio/effects/rig/gate.ts +39 -0
  60. package/src/audio/effects/rig/head.ts +487 -0
  61. package/src/audio/effects/rig/index.ts +170 -0
  62. package/src/audio/effects/rig/section.ts +78 -0
  63. package/src/audio/effects/rig/stomp.ts +238 -0
  64. package/src/audio/engine.ts +56 -6
  65. package/src/audio/fit.ts +447 -0
  66. package/src/audio/granular.ts +995 -0
  67. package/src/audio/instrument-check.ts +137 -0
  68. package/src/audio/instruments.ts +140 -0
  69. package/src/audio/keys/dsp.ts +323 -0
  70. package/src/audio/keys/electric.ts +420 -0
  71. package/src/audio/keys/engine.ts +485 -0
  72. package/src/audio/keys/organ.ts +1335 -0
  73. package/src/audio/keys/piano.ts +476 -0
  74. package/src/audio/keys/sympathetic.ts +127 -0
  75. package/src/audio/live-worker.ts +56 -0
  76. package/src/audio/live.ts +361 -15
  77. package/src/audio/loudness.ts +65 -20
  78. package/src/audio/preview.ts +17 -1
  79. package/src/audio/resample.ts +285 -0
  80. package/src/audio/resonators.ts +295 -0
  81. package/src/audio/sampler.ts +328 -35
  82. package/src/audio/samples.ts +56 -8
  83. package/src/audio/strings/body.ts +263 -0
  84. package/src/audio/strings/bow.ts +656 -0
  85. package/src/audio/strings/engine.ts +615 -0
  86. package/src/audio/strings/loop.ts +119 -0
  87. package/src/audio/strings/measure.test-helpers.ts +200 -0
  88. package/src/audio/strings/pluck.ts +354 -0
  89. package/src/audio/warp.ts +61 -0
  90. package/src/audio/wav.ts +191 -19
  91. package/src/audio/winds/engine.ts +302 -0
  92. package/src/audio/winds/filters.ts +153 -0
  93. package/src/audio/winds/pitch.ts +104 -0
  94. package/src/audio/winds/trim.ts +80 -0
  95. package/src/audio/winds/trims.ts +917 -0
  96. package/src/audio/winds/voice.ts +479 -0
  97. package/src/commands/expression.ts +111 -23
  98. package/src/commands/fit.ts +135 -0
  99. package/src/commands/fx.ts +35 -1
  100. package/src/commands/granular.ts +432 -0
  101. package/src/commands/help.ts +159 -5
  102. package/src/commands/keys.ts +629 -0
  103. package/src/commands/modal.ts +310 -0
  104. package/src/commands/resample.ts +281 -0
  105. package/src/commands/rig.ts +264 -0
  106. package/src/commands/sample.ts +52 -2
  107. package/src/commands/shift.ts +119 -0
  108. package/src/commands/string.ts +201 -0
  109. package/src/commands/strum.ts +473 -0
  110. package/src/commands/time.ts +4 -1
  111. package/src/commands/wind.ts +244 -0
  112. package/src/main.ts +339 -15
  113. package/src/project/check.ts +17 -0
  114. package/src/render.ts +49 -4
  115. package/src/session/presence.ts +16 -3
  116. package/src/tui/audition.ts +2 -2
  117. package/src/tui/granular-menu.ts +278 -0
  118. package/src/tui/menu.ts +934 -9
  119. package/src/tui/modal-menu.ts +145 -0
  120. package/src/tui/performance-menu.ts +42 -0
  121. package/src/tui/play-chords.ts +104 -3
  122. package/src/tui/play-mode.ts +1 -0
  123. package/src/tui/play-session.ts +70 -2
  124. package/src/tui/wind-menu.ts +144 -0
package/core/winds.ts ADDED
@@ -0,0 +1,652 @@
1
+ /**
2
+ * Blown instruments (0.6.1 "Instruments wave 2", lane f061-gamelan-winds):
3
+ * flutes, reeds, saxes and brass played by one waveguide engine
4
+ * (`src/audio/winds/`). Pure and import-light, like `core/resonators.ts`
5
+ * (which re-exports this file): the parameter table, the 21 presets and the
6
+ * validator that the score, the prompt, the menu, the agent and the SDK
7
+ * printer share.
8
+ *
9
+ * A track plays this engine only when its `instrument` is `"wind"` AND it
10
+ * carries a `wind` object (`{ preset, ...overrides }`). A bare `"wind"`
11
+ * instrument without the field keeps the legacy wind tone byte for byte.
12
+ *
13
+ * Models (STK's waveguides, Cook and Scavone, corrected for exact pitch by
14
+ * per-rate trim tables): `jet` flutes, `reed` clarinets, `sax` conical reeds
15
+ * (saxes, and oboe and bassoon until a true double-reed table lands) and
16
+ * `lips` brass.
17
+ */
18
+ import {
19
+ FxValidationError,
20
+ isRecord,
21
+ normalizeParams,
22
+ type EnumParam,
23
+ type NumberParam,
24
+ type ParamSpec,
25
+ } from "./params.ts";
26
+
27
+ /** Instrument value that selects the wind engine. */
28
+ export const WIND_INSTRUMENT = "wind" as const;
29
+
30
+ export const WIND_MODELS = Object.freeze(["jet", "reed", "sax", "lips"]);
31
+ export type WindModel = "jet" | "reed" | "sax" | "lips";
32
+
33
+ export const WIND_MUTES = Object.freeze([
34
+ "open",
35
+ "straight",
36
+ "cup",
37
+ "harmon",
38
+ "plunger",
39
+ ]);
40
+ export type WindMute = "open" | "straight" | "cup" | "harmon" | "plunger";
41
+
42
+ export const WIND_PRESET_NAMES = Object.freeze([
43
+ "flute",
44
+ "recorder",
45
+ "whistle",
46
+ "ney",
47
+ "shakuhachi",
48
+ "panpipe",
49
+ "suling",
50
+ "bansuri",
51
+ "clarinet",
52
+ "bassclarinet",
53
+ "oboe",
54
+ "bassoon",
55
+ "sax",
56
+ "altosax",
57
+ "barisax",
58
+ "trumpet",
59
+ "harmon",
60
+ "plunger",
61
+ "trombone",
62
+ "tuba",
63
+ "horn",
64
+ ] as const);
65
+ export type WindPresetName = (typeof WIND_PRESET_NAMES)[number];
66
+
67
+ /** Extra words that pick a preset (`wind preset saxophone`). */
68
+ export const WIND_ALIASES: Readonly<Record<string, WindPresetName>> =
69
+ Object.freeze({
70
+ tinwhistle: "whistle",
71
+ pennywhistle: "whistle",
72
+ nay: "ney",
73
+ panflute: "panpipe",
74
+ panpipes: "panpipe",
75
+ saxophone: "sax",
76
+ tenorsax: "sax",
77
+ tenor: "sax",
78
+ alto: "altosax",
79
+ bari: "barisax",
80
+ baritonesax: "barisax",
81
+ frenchhorn: "horn",
82
+ mutedtrumpet: "harmon",
83
+ wahtrumpet: "plunger",
84
+ });
85
+
86
+ /** The browse group "Winds and brass", in menu order. */
87
+ export const WIND_GROUPS: readonly Readonly<{
88
+ label: string;
89
+ presets: readonly WindPresetName[];
90
+ }>[] = Object.freeze([
91
+ {
92
+ label: "Flutes",
93
+ presets: [
94
+ "flute",
95
+ "recorder",
96
+ "whistle",
97
+ "ney",
98
+ "shakuhachi",
99
+ "panpipe",
100
+ "suling",
101
+ "bansuri",
102
+ ],
103
+ },
104
+ {
105
+ label: "Saxophones",
106
+ presets: ["sax", "altosax", "barisax"],
107
+ },
108
+ {
109
+ label: "Reeds",
110
+ presets: ["clarinet", "bassclarinet", "oboe", "bassoon"],
111
+ },
112
+ {
113
+ label: "Brass",
114
+ presets: ["trumpet", "harmon", "plunger", "trombone", "tuba", "horn"],
115
+ },
116
+ ]);
117
+
118
+ const num = (
119
+ min: number,
120
+ max: number,
121
+ fallback: number,
122
+ step: number | "log",
123
+ doc: string,
124
+ extra: Partial<NumberParam> = {},
125
+ ): NumberParam =>
126
+ Object.freeze({
127
+ kind: "number",
128
+ min,
129
+ max,
130
+ default: fallback,
131
+ step,
132
+ optional: true,
133
+ doc,
134
+ ...extra,
135
+ });
136
+
137
+ /**
138
+ * Every wind parameter. All are optional: an absent one takes the preset's
139
+ * value, then the default here. `automate` ones have a `wind-<param>` lane
140
+ * the voice reads every 32-sample control tick.
141
+ */
142
+ export const WIND_PARAMS: Readonly<Record<string, ParamSpec>> = Object.freeze({
143
+ model: Object.freeze({
144
+ kind: "enum",
145
+ values: WIND_MODELS,
146
+ default: "jet",
147
+ optional: true,
148
+ doc: "exciter and bore: jet (flute), reed (clarinet), sax (conical reed), lips (brass)",
149
+ }) as EnumParam,
150
+ breath: num(0, 1, 0.6, 0.05, "blowing pressure: swells, louder and fuller", {
151
+ automate: true,
152
+ }),
153
+ noise: num(0, 1, 0.08, 0.05, "breath noise", { automate: true }),
154
+ attack: num(0.001, 2, 0.04, "log", "breath rise", {
155
+ unit: "s",
156
+ strudel: ["att"],
157
+ }),
158
+ release: num(0.005, 2, 0.08, "log", "breath fall", {
159
+ unit: "s",
160
+ strudel: ["rel"],
161
+ }),
162
+ vib: num(0, 12, 5, 0.25, "vibrato rate", {
163
+ unit: "Hz",
164
+ strudel: ["vibrato"],
165
+ }),
166
+ vibmod: num(0, 1, 0, 0.02, "vibrato depth", {
167
+ unit: "st",
168
+ strudel: ["vmod", "vibdepth", "depth"],
169
+ }),
170
+ reed: num(0, 1, 0.5, 0.05, "reed stiffness, jet offset or lip tension"),
171
+ bright: num(0, 1, 0.5, 0.05, "bore loss: dark to bright"),
172
+ stopped: Object.freeze({
173
+ kind: "boolean",
174
+ default: false,
175
+ optional: true,
176
+ doc: "stopped pipe, odd harmonics (jet only)",
177
+ }),
178
+ mute: Object.freeze({
179
+ kind: "enum",
180
+ values: WIND_MUTES,
181
+ default: "open",
182
+ optional: true,
183
+ doc: "brass mute: open, straight, cup, harmon or plunger",
184
+ }) as EnumParam,
185
+ wah: num(0, 1, 1, 0.05, "plunger opening: 0 closed, 1 open", {
186
+ automate: true,
187
+ }),
188
+ wahenv: num(0, 1, 0, 0.05, "plunger opens with each note (doo-wah)"),
189
+ growl: num(0, 1, 0, 0.05, "hum into the horn: rough, raspy", {
190
+ automate: true,
191
+ }),
192
+ flutter: num(0, 1, 0, 0.05, "flutter tongue (rolled r)", { automate: true }),
193
+ players: num(1, 8, 1, 1, "section size, spread over chord tones", {
194
+ integer: true,
195
+ }),
196
+ gain: num(0, 2, 0.8, 0.05, "level"),
197
+ });
198
+
199
+ /** Menu rows most people reach for. */
200
+ export const WIND_SIMPLE = Object.freeze([
201
+ "breath",
202
+ "bright",
203
+ "mute",
204
+ "players",
205
+ "growl",
206
+ ]);
207
+
208
+ /** Resolved settings the engine plays. */
209
+ export type WindSettings = Readonly<{
210
+ model: WindModel;
211
+ breath: number;
212
+ noise: number;
213
+ attack: number;
214
+ release: number;
215
+ vib: number;
216
+ vibmod: number;
217
+ reed: number;
218
+ bright: number;
219
+ stopped: boolean;
220
+ mute: WindMute;
221
+ wah: number;
222
+ wahenv: number;
223
+ growl: number;
224
+ flutter: number;
225
+ players: number;
226
+ gain: number;
227
+ }>;
228
+
229
+ export const WIND_DEFAULTS: WindSettings = Object.freeze({
230
+ model: "jet",
231
+ breath: 0.6,
232
+ noise: 0.08,
233
+ attack: 0.04,
234
+ release: 0.08,
235
+ vib: 5,
236
+ vibmod: 0,
237
+ reed: 0.5,
238
+ bright: 0.5,
239
+ stopped: false,
240
+ mute: "open",
241
+ wah: 1,
242
+ wahenv: 0,
243
+ growl: 0,
244
+ flutter: 0,
245
+ players: 1,
246
+ gain: 0.8,
247
+ });
248
+
249
+ export type WindPreset = Readonly<{
250
+ settings: WindSettings;
251
+ /** MIDI range the instrument is built for (sounding pitch). */
252
+ range: readonly [number, number];
253
+ doc: string;
254
+ styles: string;
255
+ }>;
256
+
257
+ const preset = (
258
+ settings: Partial<WindSettings>,
259
+ range: [number, number],
260
+ doc: string,
261
+ styles: string,
262
+ ): WindPreset =>
263
+ Object.freeze({
264
+ settings: Object.freeze({ ...WIND_DEFAULTS, ...settings }),
265
+ range: Object.freeze(range) as readonly [number, number],
266
+ doc,
267
+ styles,
268
+ });
269
+
270
+ /**
271
+ * Presets, ported from the reviewed prototype (proto/resonators/presets.ts)
272
+ * and extended to the 21 of the revised brief. Ranges are sounding pitch
273
+ * (written ranges of the transposing instruments, transposed).
274
+ */
275
+ export const WIND_PRESETS: Readonly<Record<WindPresetName, WindPreset>> =
276
+ Object.freeze({
277
+ flute: preset(
278
+ {
279
+ model: "jet",
280
+ breath: 0.6,
281
+ noise: 0.12,
282
+ attack: 0.06,
283
+ vib: 5,
284
+ vibmod: 0.12,
285
+ bright: 0.5,
286
+ },
287
+ [60, 96],
288
+ "concert flute, airy with gentle vibrato",
289
+ "Debussy, Bach cantata, jazz flute",
290
+ ),
291
+ recorder: preset(
292
+ { model: "jet", breath: 0.5, noise: 0.06, attack: 0.02, bright: 0.6 },
293
+ [65, 93],
294
+ "alto recorder, pure and straight",
295
+ "Bach cantata, early music",
296
+ ),
297
+ whistle: preset(
298
+ { model: "jet", breath: 0.6, noise: 0.08, attack: 0.015, bright: 0.7 },
299
+ [74, 98],
300
+ "tin whistle, bright and reedy",
301
+ "celtic",
302
+ ),
303
+ ney: preset(
304
+ {
305
+ model: "jet",
306
+ breath: 0.45,
307
+ noise: 0.35,
308
+ attack: 0.12,
309
+ vib: 5.5,
310
+ vibmod: 0.08,
311
+ bright: 0.3,
312
+ },
313
+ [55, 86],
314
+ "Persian reed flute, very breathy",
315
+ "Persian, Sufi, ambient",
316
+ ),
317
+ shakuhachi: preset(
318
+ {
319
+ model: "jet",
320
+ breath: 0.55,
321
+ noise: 0.3,
322
+ attack: 0.08,
323
+ vib: 4,
324
+ vibmod: 0.2,
325
+ bright: 0.35,
326
+ },
327
+ [62, 86],
328
+ "bamboo end-blown flute, breath accents",
329
+ "Sakamoto, OPN, ambient",
330
+ ),
331
+ panpipe: preset(
332
+ {
333
+ model: "jet",
334
+ stopped: true,
335
+ breath: 0.6,
336
+ noise: 0.2,
337
+ attack: 0.03,
338
+ bright: 0.5,
339
+ },
340
+ [60, 96],
341
+ "stopped cane pipes, hollow odd harmonics",
342
+ "ambient, Andean, Eno",
343
+ ),
344
+ suling: preset(
345
+ {
346
+ model: "jet",
347
+ breath: 0.5,
348
+ noise: 0.25,
349
+ attack: 0.05,
350
+ vib: 5.5,
351
+ vibmod: 0.1,
352
+ bright: 0.45,
353
+ },
354
+ [67, 93],
355
+ "Sundanese and Balinese ring flute, soft and breathy",
356
+ "gamelan, degung, ambient",
357
+ ),
358
+ bansuri: preset(
359
+ {
360
+ model: "jet",
361
+ breath: 0.55,
362
+ noise: 0.22,
363
+ attack: 0.07,
364
+ vib: 5,
365
+ vibmod: 0.15,
366
+ bright: 0.4,
367
+ },
368
+ [55, 84],
369
+ "Indian bamboo flute, warm with deep vibrato",
370
+ "Hindustani, Bollywood, ambient",
371
+ ),
372
+ clarinet: preset(
373
+ { model: "reed", breath: 0.6, noise: 0.05, attack: 0.04, reed: 0.5 },
374
+ [50, 91],
375
+ "cylindrical bore, woody odd harmonics",
376
+ "Brahms, jazz, klezmer, Motown",
377
+ ),
378
+ bassclarinet: preset(
379
+ {
380
+ model: "reed",
381
+ breath: 0.6,
382
+ noise: 0.06,
383
+ attack: 0.05,
384
+ reed: 0.4,
385
+ bright: 0.4,
386
+ },
387
+ [37, 72],
388
+ "bass clarinet, dark and hollow",
389
+ "Bitches Brew, Messiaen, film",
390
+ ),
391
+ oboe: preset(
392
+ {
393
+ model: "sax",
394
+ breath: 0.6,
395
+ noise: 0.04,
396
+ attack: 0.03,
397
+ vib: 5.5,
398
+ vibmod: 0.08,
399
+ reed: 0.8,
400
+ bright: 0.75,
401
+ },
402
+ [58, 91],
403
+ "double reed, nasal and singing",
404
+ "Bach, Ravel, Morricone",
405
+ ),
406
+ bassoon: preset(
407
+ {
408
+ model: "sax",
409
+ breath: 0.6,
410
+ noise: 0.04,
411
+ attack: 0.04,
412
+ vib: 5,
413
+ vibmod: 0.06,
414
+ reed: 0.7,
415
+ bright: 0.35,
416
+ },
417
+ [34, 72],
418
+ "bassoon, reedy and woody low voice",
419
+ "Stravinsky, Prokofiev, orchestral",
420
+ ),
421
+ sax: preset(
422
+ {
423
+ model: "sax",
424
+ breath: 0.65,
425
+ noise: 0.1,
426
+ attack: 0.03,
427
+ vib: 5.2,
428
+ vibmod: 0.15,
429
+ },
430
+ [44, 79],
431
+ "tenor sax, warm to raspy with breath",
432
+ "jazz, Motown, soul, funk",
433
+ ),
434
+ altosax: preset(
435
+ {
436
+ model: "sax",
437
+ breath: 0.65,
438
+ noise: 0.09,
439
+ attack: 0.03,
440
+ vib: 5.4,
441
+ vibmod: 0.12,
442
+ bright: 0.55,
443
+ },
444
+ [49, 81],
445
+ "alto sax, bright and vocal",
446
+ "bebop, Motown, soul",
447
+ ),
448
+ barisax: preset(
449
+ {
450
+ model: "sax",
451
+ breath: 0.68,
452
+ noise: 0.1,
453
+ attack: 0.04,
454
+ vib: 5,
455
+ vibmod: 0.08,
456
+ bright: 0.45,
457
+ },
458
+ [36, 69],
459
+ "baritone sax, honking low end",
460
+ "Motown, funk, ska",
461
+ ),
462
+ trumpet: preset(
463
+ {
464
+ model: "lips",
465
+ breath: 0.7,
466
+ noise: 0.03,
467
+ attack: 0.03,
468
+ bright: 0.6,
469
+ vib: 5.5,
470
+ },
471
+ [52, 82],
472
+ "open trumpet, brightens as it gets louder",
473
+ "Louis Armstrong, Motown, salsa, jazz",
474
+ ),
475
+ harmon: preset(
476
+ {
477
+ model: "lips",
478
+ breath: 0.7,
479
+ noise: 0.03,
480
+ attack: 0.03,
481
+ bright: 0.6,
482
+ mute: "harmon",
483
+ },
484
+ [52, 82],
485
+ "trumpet with a harmon mute, buzzy and close",
486
+ "jazz, Radiohead, Sakamoto",
487
+ ),
488
+ plunger: preset(
489
+ {
490
+ model: "lips",
491
+ breath: 0.75,
492
+ noise: 0.04,
493
+ attack: 0.03,
494
+ bright: 0.6,
495
+ mute: "plunger",
496
+ wah: 0.3,
497
+ },
498
+ [52, 82],
499
+ "trumpet with a plunger: automate wah for ya-ya",
500
+ "Louis Armstrong, Duke Ellington, funk",
501
+ ),
502
+ trombone: preset(
503
+ { model: "lips", breath: 0.7, noise: 0.03, attack: 0.04, bright: 0.45 },
504
+ [40, 72],
505
+ "slide trombone, broad",
506
+ "salsa, Motown, jazz",
507
+ ),
508
+ tuba: preset(
509
+ { model: "lips", breath: 0.7, noise: 0.02, attack: 0.06, bright: 0.3 },
510
+ [28, 58],
511
+ "tuba, round and deep",
512
+ "New Orleans brass band, oompah, orchestral",
513
+ ),
514
+ horn: preset(
515
+ { model: "lips", breath: 0.6, noise: 0.02, attack: 0.06, bright: 0.25 },
516
+ [41, 77],
517
+ "French horn, dark and round",
518
+ "Brahms, Beethoven, orchestral",
519
+ ),
520
+ });
521
+
522
+ /** Default preset of a `wind` field that names none. */
523
+ export const DEFAULT_WIND_PRESET: WindPresetName = "flute";
524
+
525
+ /** A track's stored wind settings: a preset plus overrides, all optional. */
526
+ export type TrackWind = Readonly<{
527
+ preset?: WindPresetName;
528
+ model?: WindModel;
529
+ breath?: number;
530
+ noise?: number;
531
+ attack?: number;
532
+ release?: number;
533
+ vib?: number;
534
+ vibmod?: number;
535
+ reed?: number;
536
+ bright?: number;
537
+ stopped?: boolean;
538
+ mute?: WindMute;
539
+ wah?: number;
540
+ wahenv?: number;
541
+ growl?: number;
542
+ flutter?: number;
543
+ players?: number;
544
+ gain?: number;
545
+ }>;
546
+
547
+ export function isWindPreset(name: string): name is WindPresetName {
548
+ return (WIND_PRESET_NAMES as readonly string[]).includes(name);
549
+ }
550
+
551
+ /** A preset word or alias (`saxophone`) to its preset; undefined if neither. */
552
+ export function windPresetFor(word: string): WindPresetName | undefined {
553
+ const lower = word.trim().toLowerCase();
554
+ if (isWindPreset(lower)) return lower;
555
+ return WIND_ALIASES[lower];
556
+ }
557
+
558
+ export function isWindParam(name: string): boolean {
559
+ return Object.prototype.hasOwnProperty.call(WIND_PARAMS, name);
560
+ }
561
+
562
+ /** Strudel spelling (`att`, `rel`) to a wind parameter name. */
563
+ export function windParamName(name: string): string | undefined {
564
+ if (isWindParam(name)) return name;
565
+ for (const [key, spec] of Object.entries(WIND_PARAMS))
566
+ if (spec.strudel?.includes(name)) return key;
567
+ return undefined;
568
+ }
569
+
570
+ /**
571
+ * Validates a `wind` field: `{ preset?, ...overrides }`, null or absent
572
+ * meaning none. Keys come out in canonical order (preset, then
573
+ * `WIND_PARAMS` order). `{}` is kept: it means the default preset.
574
+ */
575
+ export function normalizeWind(input: unknown): TrackWind | undefined {
576
+ if (input === undefined || input === null) return undefined;
577
+ if (!isRecord(input))
578
+ throw new FxValidationError("track wind must be an object or null");
579
+ const params: Record<string, unknown> = {};
580
+ let presetName: WindPresetName | undefined;
581
+ for (const [key, value] of Object.entries(input)) {
582
+ if (value === undefined || value === null) continue;
583
+ if (key === "preset") {
584
+ const resolved =
585
+ typeof value === "string" ? windPresetFor(value) : undefined;
586
+ if (!resolved)
587
+ throw new FxValidationError(
588
+ `wind preset must be one of ${WIND_PRESET_NAMES.join(", ")}`,
589
+ );
590
+ presetName = resolved;
591
+ continue;
592
+ }
593
+ const name = windParamName(key);
594
+ if (!name)
595
+ throw new FxValidationError(
596
+ `wind has no parameter "${key.slice(0, 32)}"`,
597
+ );
598
+ params[name] = value;
599
+ }
600
+ const values = normalizeParams(WIND_PARAMS, params, "wind", false);
601
+ return Object.freeze({
602
+ ...(presetName ? { preset: presetName } : {}),
603
+ ...values,
604
+ }) as TrackWind;
605
+ }
606
+
607
+ /** The preset a `wind` field plays. */
608
+ export function windPresetOf(wind: TrackWind | undefined): WindPresetName {
609
+ return wind?.preset ?? DEFAULT_WIND_PRESET;
610
+ }
611
+
612
+ /**
613
+ * The settings a `wind` field plays: defaults, then the preset, then
614
+ * explicit overrides, then any automation lane values from `lane`.
615
+ */
616
+ export function windSettings(
617
+ wind: TrackWind | undefined,
618
+ lane?: (param: string) => number | undefined,
619
+ ): WindSettings {
620
+ const base = WIND_PRESETS[windPresetOf(wind)].settings;
621
+ const out: Record<string, number | string | boolean> = { ...base };
622
+ if (wind)
623
+ for (const [key, value] of Object.entries(wind))
624
+ if (key !== "preset" && value !== undefined) out[key] = value;
625
+ if (lane)
626
+ for (const { param } of WIND_LANE_PARAMS) {
627
+ const value = lane(param);
628
+ if (value !== undefined) out[param] = value;
629
+ }
630
+ return Object.freeze(out) as WindSettings;
631
+ }
632
+
633
+ /** Parameters with a `wind-<param>` automation lane. */
634
+ export const WIND_LANE_PARAMS: readonly Readonly<{
635
+ param: string;
636
+ spec: NumberParam;
637
+ }>[] = Object.freeze(
638
+ Object.entries(WIND_PARAMS)
639
+ .filter(
640
+ (entry): entry is [string, NumberParam] =>
641
+ entry[1].kind === "number" && entry[1].automate === true,
642
+ )
643
+ .map(([param, spec]) => Object.freeze({ param, spec })),
644
+ );
645
+
646
+ /** Voices one wind track sounds at once; the oldest onset is stolen. */
647
+ export const MAX_WIND_VOICES = 24;
648
+
649
+ /** Ring after the last breath ends, in seconds. */
650
+ export function windTailSeconds(wind: TrackWind | undefined): number {
651
+ return windSettings(wind).release + 0.15;
652
+ }
@@ -19,6 +19,7 @@ A lane moves a value over beats. Every numeric effect parameter has one.
19
19
  - `automate pan at 0 -1` · `automate delay-mix at 4 0`
20
20
  - `automate distort-drive points 0:1 8:6`
21
21
  - `automate filter remove 4` delete one point
22
+ - `automate grain-pos points 0:0.1 8:0.9` sweeps a granular track's head (every movable `grain-<param>`)
22
23
  - `clear filter automation` · `clear automation`
23
24
 
24
25
  ## Menu
package/guides/chords.md CHANGED
@@ -16,13 +16,15 @@ order: 4
16
16
  - `1 2 3 4` dim min maj sus · `5 6 7 8` add 6, m7, M7, 9
17
17
  - `- =` voicing down / up · `9` perform mode · `b` bass mode
18
18
  - `n` play the suggested next chord · `0` clear the latches
19
+ - perform guitar strums them; `[ ]` strum slower / faster
19
20
 
20
21
  ## Type it yourself
21
22
 
22
23
  - `/chords auto` · `/chords manual` · `/chords off`
23
24
  - `/chords voicing 2` · `/chords spread wide` · `/chords bass chords`
24
25
  - `/chords perform arp-up` · `/chords sevenths on` · `/chords`
26
+ - `strum G D Em C folk` · `guitar tune dadgad` · `guitar capo 2`
25
27
 
26
28
  ## Menu
27
29
 
28
- - Ctrl-K › Chords: key, voicing, bass, perform, progression, style
30
+ - Ctrl-K › Chords: key, voicing, bass, perform, strokes, speed, style
package/guides/effects.md CHANGED
@@ -11,17 +11,20 @@ chorus, delay, reverb, and more (phaser, bitcrush, vowel, leslie).
11
11
  ## Ask
12
12
 
13
13
  - "add a dub delay to the snare" · "more room on the keys"
14
+ - "give the guitar a crunchy amp"
14
15
 
15
16
  ## Type it yourself
16
17
 
17
18
  - `fx` list the focused track's effects
18
19
  - `fx delay mix 0.3` · `fx filter type hpf cutoff 300`
19
- - `fx distort drive 4 tone 5000` · `fx reverb ir hall`
20
+ - `fx distort drive 4 tone 5000` · `fx reverb ir builtin:reverse`
20
21
  - `fx chorus on` · `fx chorus off` · `fx chorus reset`
21
22
  - `fx compressor preset <name>` load a preset
23
+ - `rig crunch` guitar rig · `head lead` · `cab 4x12` · `stomp fuzz`
24
+ - `rig shoegaze` · `fx wobble depth 30` · `fx bloom` · `fx swell` · `fx double`
22
25
  - `filter 1200 0.6` · `delay 0.375 0.3` · `reverb 0.4 0.8` · `filter off`
23
26
 
24
27
  ## Menu
25
28
 
26
- - Ctrl-K › Effects: core effects first, then more effects, advanced
29
+ - Ctrl-K › Effects: core effects, Guitar rig, Shoegaze, more, advanced
27
30
  - `fx filter` alone opens the filter's faders, stacked
package/guides/media.md CHANGED
@@ -24,4 +24,4 @@ helper is reported with its install command; `dawg media doctor` checks.
24
24
 
25
25
  ## In the app
26
26
 
27
- - `/sample <path> as <voice>` · `/sample` lists the voices
27
+ - `/sample <path> as <voice>` · `/sample` lists the voices · `/sample set sn vel 0-63 rr sn` layers