@hraness/dawg 0.5.0 → 0.6.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 (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/DAWG.md +269 -13
  3. package/README.md +4 -4
  4. package/core/diff.ts +4 -0
  5. package/core/fx.ts +437 -2
  6. package/core/granular.ts +528 -0
  7. package/core/instruments.ts +281 -0
  8. package/core/keys.ts +386 -0
  9. package/core/resonators.ts +574 -0
  10. package/core/score.ts +181 -6
  11. package/core/sdk/eval-child.ts +2 -0
  12. package/core/sdk/print.ts +169 -13
  13. package/core/sdk/sync-instruments.ts +58 -0
  14. package/core/sdk/v1.ts +1114 -21
  15. package/core/sections.ts +34 -8
  16. package/core/strings.ts +845 -0
  17. package/guides/effects.md +3 -1
  18. package/guides/sounds.md +4 -0
  19. package/guides/tempo.md +1 -0
  20. package/package.json +1 -1
  21. package/src/agent/agent.ts +7 -0
  22. package/src/agent/granular-tools.ts +138 -0
  23. package/src/agent/models.ts +4 -4
  24. package/src/agent/ops.ts +39 -2
  25. package/src/agent/preview-tool.ts +6 -1
  26. package/src/agent/tools.ts +538 -14
  27. package/src/agent/xcb-agent.ts +7 -0
  28. package/src/audio/arrange.ts +21 -3
  29. package/src/audio/dsp/bank.ts +233 -0
  30. package/src/audio/dsp/fft.ts +6 -0
  31. package/src/audio/dsp/filters.ts +57 -0
  32. package/src/audio/dsp/interp.ts +112 -0
  33. package/src/audio/dsp/modal.ts +684 -0
  34. package/src/audio/dsp/onset.ts +197 -0
  35. package/src/audio/dsp/oversample.ts +202 -0
  36. package/src/audio/dsp/rng.ts +37 -0
  37. package/src/audio/dsp/shape.ts +81 -0
  38. package/src/audio/dsp/stft.ts +73 -0
  39. package/src/audio/dsp/window.ts +77 -0
  40. package/src/audio/effects/chain.ts +6 -1
  41. package/src/audio/effects/rig/cab.ts +99 -0
  42. package/src/audio/effects/rig/filters.ts +152 -0
  43. package/src/audio/effects/rig/gate.ts +39 -0
  44. package/src/audio/effects/rig/head.ts +487 -0
  45. package/src/audio/effects/rig/index.ts +170 -0
  46. package/src/audio/effects/rig/section.ts +78 -0
  47. package/src/audio/effects/rig/stomp.ts +238 -0
  48. package/src/audio/engine.ts +10 -1
  49. package/src/audio/fit.ts +402 -0
  50. package/src/audio/granular.ts +664 -0
  51. package/src/audio/instrument-check.ts +133 -0
  52. package/src/audio/instruments.ts +111 -0
  53. package/src/audio/keys/dsp.ts +323 -0
  54. package/src/audio/keys/engine.ts +361 -0
  55. package/src/audio/keys/piano.ts +432 -0
  56. package/src/audio/live-worker.ts +54 -0
  57. package/src/audio/live.ts +231 -12
  58. package/src/audio/loudness.ts +65 -20
  59. package/src/audio/preview.ts +2 -0
  60. package/src/audio/resonators.ts +287 -0
  61. package/src/audio/sampler.ts +163 -17
  62. package/src/audio/samples.ts +30 -7
  63. package/src/audio/strings/body.ts +250 -0
  64. package/src/audio/strings/engine.ts +369 -0
  65. package/src/audio/strings/loop.ts +119 -0
  66. package/src/audio/strings/measure.test-helpers.ts +198 -0
  67. package/src/audio/strings/pluck.ts +354 -0
  68. package/src/audio/warp.ts +61 -0
  69. package/src/audio/wav.ts +78 -6
  70. package/src/commands/expression.ts +9 -2
  71. package/src/commands/fit.ts +135 -0
  72. package/src/commands/fx.ts +31 -0
  73. package/src/commands/granular.ts +427 -0
  74. package/src/commands/help.ts +74 -3
  75. package/src/commands/keys.ts +353 -0
  76. package/src/commands/modal.ts +247 -0
  77. package/src/commands/rig.ts +260 -0
  78. package/src/commands/sample.ts +3 -0
  79. package/src/commands/string.ts +175 -0
  80. package/src/commands/time.ts +4 -1
  81. package/src/main.ts +210 -8
  82. package/src/project/check.ts +13 -0
  83. package/src/render.ts +33 -4
  84. package/src/tui/audition.ts +2 -2
  85. package/src/tui/granular-menu.ts +278 -0
  86. package/src/tui/menu.ts +332 -9
  87. package/src/tui/modal-menu.ts +118 -0
  88. package/src/tui/play-session.ts +45 -2
@@ -0,0 +1,427 @@
1
+ /**
2
+ * The `grain` prompt command: the focused track's granular instrument
3
+ * (core/granular.ts, `Track.granular`).
4
+ *
5
+ * grain this track's preset, source and overrides
6
+ * grain presets the granular presets
7
+ * grain <preset> | preset <name> play a preset (instrument "granular")
8
+ * grain on [voice V] grain this track's own sound
9
+ * grain src synth:<name>[@note] a built-in synth source
10
+ * grain src voice <V> a sampler voice of this track as source
11
+ * grain <param> <value> [...] override parameters (`off` unsets)
12
+ * grain reset drop the overrides, keep preset and source
13
+ * grain off back to the track's previous voice
14
+ *
15
+ * On a synth track the source is that synth (`synth:<instrument>`), on a
16
+ * sampler the first (or named) voice, pinned like the sampler voice. Each
17
+ * command is one `updateTrack` revision and one undo step.
18
+ */
19
+ import {
20
+ DEFAULT_GRANULAR_SOURCE,
21
+ GRANULAR_INSTRUMENT,
22
+ GRANULAR_PARAMS,
23
+ GRANULAR_PRESET_NAMES,
24
+ GRANULAR_PRESETS,
25
+ granularSourceLabel,
26
+ isGranularInstrument,
27
+ isGranularPreset,
28
+ normalizeGranular,
29
+ resolveGranular,
30
+ parseSynthSource,
31
+ synthSourceNames,
32
+ SYNTH_SOURCE_PREFIX,
33
+ type GranularPresetName,
34
+ type GranularSource,
35
+ type TrackGranular,
36
+ } from "../../core/granular.ts";
37
+ import { isDrumInstrument } from "../../core/drums.ts";
38
+ import { FxValidationError } from "../../core/params.ts";
39
+ import {
40
+ ScoreValidationError,
41
+ updateTrack,
42
+ type Track,
43
+ type TrackScore,
44
+ } from "../../core/score.ts";
45
+ import { pitchToMidi } from "../../core/pitch.ts";
46
+ import { normalizeSynth, SYNTH_PRESETS } from "../../core/synth.ts";
47
+ import { parseParamValue } from "./fx.ts";
48
+
49
+ /** `grain 0.12s · overlap 6 · scan 0.25x …`; overrides marked `*`. */
50
+ function basicsLine(settings: TrackGranular | undefined): string {
51
+ const resolved = resolveGranular(settings) as Record<string, unknown>;
52
+ return GRANULAR_SIMPLE_PARAMS.map((name) => {
53
+ const spec = GRANULAR_PARAMS[name]!;
54
+ const value = resolved[name];
55
+ const unit =
56
+ spec.kind === "number" && spec.unit
57
+ ? spec.unit
58
+ : name === "scan"
59
+ ? "x"
60
+ : "";
61
+ const shown =
62
+ typeof value === "number"
63
+ ? `${Number(value.toFixed(3))}${unit}`
64
+ : String(value);
65
+ const mark =
66
+ settings && Object.prototype.hasOwnProperty.call(settings, name)
67
+ ? "*"
68
+ : "";
69
+ return `${name} ${shown}${mark}`;
70
+ }).join(" · ");
71
+ }
72
+
73
+ /** The basics shown first in the menu and the `grain` listing. */
74
+ export const GRANULAR_SIMPLE_PARAMS = Object.freeze([
75
+ "grain",
76
+ "overlap",
77
+ "scan",
78
+ "pos",
79
+ "spray",
80
+ "pitch",
81
+ "shimmer",
82
+ "spread",
83
+ "freeze",
84
+ ]);
85
+
86
+ export type GranularCommand =
87
+ | { type: "grain-list" }
88
+ | { type: "grain-presets" }
89
+ | { type: "grain-reset" }
90
+ | { type: "grain-off" }
91
+ | { type: "grain-on"; voice?: string }
92
+ | { type: "grain-preset"; preset: GranularPresetName; voice?: string }
93
+ | { type: "grain-src"; synth?: string; voice?: string }
94
+ | {
95
+ type: "grain-set";
96
+ /** `null` unsets a parameter (the preset's value again). */
97
+ values: Readonly<Record<string, number | string | boolean | null>>;
98
+ };
99
+
100
+ const VOICE_NAME = /^[a-z0-9._-]{1,64}$/;
101
+
102
+ export function parseGranularCommand(
103
+ prompt: string,
104
+ ): GranularCommand | undefined {
105
+ const words = prompt.trim().split(/\s+/);
106
+ const head = words[0]?.toLowerCase();
107
+ if (head !== "grain" && head !== "granular") return undefined;
108
+ if (words.length === 1) return { type: "grain-list" };
109
+ if (prompt.length > 1_024) return undefined;
110
+ const rest = words.slice(1).map((word) => word.toLowerCase());
111
+ const voiceAt = (index: number): string | undefined | null => {
112
+ if (rest.length === index) return undefined;
113
+ if (
114
+ rest.length === index + 2 &&
115
+ rest[index] === "voice" &&
116
+ VOICE_NAME.test(rest[index + 1]!)
117
+ )
118
+ return rest[index + 1]!;
119
+ return null;
120
+ };
121
+ if (rest[0] === "reset" && rest.length === 1) return { type: "grain-reset" };
122
+ if (rest[0] === "off" && rest.length === 1) return { type: "grain-off" };
123
+ if ((rest[0] === "presets" || rest[0] === "list") && rest.length === 1)
124
+ return { type: "grain-presets" };
125
+ if (rest[0] === "on") {
126
+ const voice = voiceAt(1);
127
+ return voice === null ? undefined : { type: "grain-on", voice };
128
+ }
129
+ // `hold` is a preset and a parameter: `grain hold 4` sets the parameter.
130
+ const presetWord =
131
+ rest[0] === "preset" ||
132
+ (isGranularPreset(rest[0]!) &&
133
+ (rest.length === 1 ||
134
+ rest[1] === "voice" ||
135
+ !GRANULAR_PARAMS[rest[0]!] ||
136
+ rest.length % 2 !== 0));
137
+ if (presetWord) {
138
+ const at = rest[0] === "preset" ? 1 : 0;
139
+ const name = rest[at];
140
+ if (!name || !isGranularPreset(name)) return undefined;
141
+ const voice = voiceAt(at + 1);
142
+ return voice === null
143
+ ? undefined
144
+ : { type: "grain-preset", preset: name, voice };
145
+ }
146
+ if (rest[0] === "src" || rest[0] === "source") {
147
+ if (rest.length === 3 && rest[1] === "voice" && VOICE_NAME.test(rest[2]!))
148
+ return { type: "grain-src", voice: rest[2]! };
149
+ if (rest.length !== 2) return undefined;
150
+ const text = rest[1]!.startsWith(SYNTH_SOURCE_PREFIX)
151
+ ? rest[1]!
152
+ : `${SYNTH_SOURCE_PREFIX}${rest[1]!}`;
153
+ return parseSynthSource(text)
154
+ ? { type: "grain-src", synth: text }
155
+ : undefined;
156
+ }
157
+ if (rest.length % 2 !== 0) return undefined;
158
+ const values: Record<string, number | string | boolean | null> = {};
159
+ for (let index = 0; index < rest.length; index += 2) {
160
+ const name = rest[index]!;
161
+ const spec = GRANULAR_PARAMS[name];
162
+ if (!spec) return undefined;
163
+ const word = rest[index + 1]!;
164
+ if ((word === "off" || word === "unset") && spec.kind !== "boolean") {
165
+ values[name] = null;
166
+ continue;
167
+ }
168
+ const value =
169
+ name === "root" && !/^\d/.test(word)
170
+ ? noteNumber(word)
171
+ : parseParamValue(spec, word);
172
+ if (value === undefined) return undefined;
173
+ values[name] = value;
174
+ }
175
+ return { type: "grain-set", values };
176
+ }
177
+
178
+ /** A note name (`c4`, `f#3`) as its MIDI number. */
179
+ function noteNumber(word: string): number | undefined {
180
+ const midi = pitchToMidi(word);
181
+ return Number.isFinite(midi) ? midi : undefined;
182
+ }
183
+
184
+ /** `cloud · synth:pad · scan 0.1`, preset first, then source and overrides. */
185
+ export function describeGranular(settings: TrackGranular | undefined): string {
186
+ const parts = [
187
+ settings?.preset ?? "default",
188
+ granularSourceLabel(settings?.src),
189
+ ];
190
+ for (const [key, value] of Object.entries(settings ?? {}))
191
+ if (
192
+ key !== "preset" &&
193
+ key !== "src" &&
194
+ key !== "from" &&
195
+ value !== undefined
196
+ )
197
+ parts.push(`${key} ${String(value)}`);
198
+ return parts.join(" · ");
199
+ }
200
+
201
+ /**
202
+ * The source a track's own sound gives: a sampler voice (the named one or
203
+ * the first), the track's synth when it is a known synth source, else the
204
+ * built-in pad.
205
+ */
206
+ export function ownGranularSource(
207
+ track: Track,
208
+ voice?: string,
209
+ ): { src: GranularSource } | { error: string } {
210
+ if (track.sampler) {
211
+ const names = Object.keys(track.sampler.voices);
212
+ const name = voice ?? names[0];
213
+ const ref = name === undefined ? undefined : track.sampler.voices[name];
214
+ if (!ref)
215
+ return {
216
+ error: `no sampler voice ${voice ?? ""} on ${track.id} (${names.join(" ") || "none"})`,
217
+ };
218
+ return { src: ref };
219
+ }
220
+ if (voice !== undefined)
221
+ return { error: `${track.id} has no sampler voices` };
222
+ if (track.granular?.src !== undefined) return { src: track.granular.src };
223
+ // `synth preset pad` stores supersaw plus pad's params: grain the preset.
224
+ const preset = synthPresetOf(track);
225
+ if (preset) return { src: `${SYNTH_SOURCE_PREFIX}${preset}` };
226
+ // A sound plays with the track's own synth params (src/audio/granular.ts).
227
+ if (synthSourceNames().includes(track.instrument))
228
+ return { src: `${SYNTH_SOURCE_PREFIX}${track.instrument}` };
229
+ return { src: DEFAULT_GRANULAR_SOURCE };
230
+ }
231
+
232
+ /** The synth preset a track's instrument and synth params are, if any. */
233
+ export function synthPresetOf(track: Track): string | undefined {
234
+ if (!track.synth) return undefined;
235
+ const own = JSON.stringify(track.synth);
236
+ for (const [name, preset] of Object.entries(SYNTH_PRESETS))
237
+ if (
238
+ preset.instrument === track.instrument &&
239
+ JSON.stringify(normalizeSynth({ ...preset.synth })) === own
240
+ )
241
+ return name;
242
+ return undefined;
243
+ }
244
+
245
+ export type GranularResult = Readonly<{
246
+ ok: boolean;
247
+ message: string;
248
+ next?: TrackScore;
249
+ kind?: string;
250
+ payload?: Record<string, unknown>;
251
+ }>;
252
+
253
+ /**
254
+ * The patch a granular change writes: instrument granular plus the
255
+ * validated settings, or `null` settings to turn it off.
256
+ */
257
+ export function applyGranularCommand(
258
+ score: TrackScore,
259
+ trackId: string,
260
+ command: GranularCommand,
261
+ ): GranularResult {
262
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
263
+ if (!track) return { ok: false, message: `no track · ${trackId}` };
264
+ if (command.type === "grain-presets")
265
+ return {
266
+ ok: true,
267
+ message: `grain presets · ${GRANULAR_PRESET_NAMES.map((name) => `${name} (${GRANULAR_PRESETS[name].doc})`).join(" · ")}`,
268
+ };
269
+ if (track.kit || isDrumInstrument(track.instrument))
270
+ return {
271
+ ok: false,
272
+ message: `grain · ${trackId} is a drum track; granular plays pitched notes`,
273
+ };
274
+ const active = isGranularInstrument(track.instrument);
275
+ if (command.type === "grain-list")
276
+ return {
277
+ ok: true,
278
+ message: active
279
+ ? `grain · ${track.granular?.preset ?? "default"} · ${granularSourceLabel(track.granular?.src)} · ${basicsLine(track.granular)}`
280
+ : `grain · off (${track.instrument}) · grain cloud turns it on · grain presets`,
281
+ };
282
+ let instrument = track.instrument;
283
+ let granular: Record<string, unknown> | null;
284
+ let synthPatch: Record<string, unknown> | undefined;
285
+ const keepSrc = (): Record<string, unknown> | { error: string } => {
286
+ const own = ownGranularSource(track);
287
+ if ("error" in own) return own;
288
+ return own.src === DEFAULT_GRANULAR_SOURCE ? {} : { src: own.src };
289
+ };
290
+ if (command.type === "grain-off") {
291
+ if (!active) return { ok: true, message: "grain · already off" };
292
+ // A sampler keeps its voices; anything else returns to its synth
293
+ // source's voice (or sine), and the settings stay for `grain on`.
294
+ // The voice granular turned on from (`from`), else a wavetable, else
295
+ // the synth source's own instrument (a preset maps to its instrument
296
+ // and params), else sine.
297
+ const src = track.granular?.src;
298
+ const parsed = typeof src === "string" ? parseSynthSource(src) : undefined;
299
+ if (track.sampler) instrument = "sampler";
300
+ else if (track.granular?.from) instrument = track.granular.from;
301
+ else if (track.wavetable) instrument = "wavetable";
302
+ else if (parsed && SYNTH_PRESETS[parsed.name]) {
303
+ instrument = SYNTH_PRESETS[parsed.name]!.instrument;
304
+ if (!track.synth) synthPatch = { ...SYNTH_PRESETS[parsed.name]!.synth };
305
+ } else if (parsed) instrument = parsed.name;
306
+ else instrument = "sine";
307
+ granular = track.granular ? { ...track.granular } : null;
308
+ } else if (command.type === "grain-reset") {
309
+ if (!active) return { ok: true, message: "grain · already off" };
310
+ granular = {};
311
+ if (track.granular?.preset) granular.preset = track.granular.preset;
312
+ if (track.granular?.src !== undefined) granular.src = track.granular.src;
313
+ } else if (command.type === "grain-on" || command.type === "grain-preset") {
314
+ instrument = GRANULAR_INSTRUMENT;
315
+ const base: Record<string, unknown> =
316
+ active || (track.granular && command.voice === undefined)
317
+ ? { ...track.granular }
318
+ : {};
319
+ if (command.voice !== undefined || base.src === undefined) {
320
+ const own =
321
+ command.voice !== undefined
322
+ ? ownGranularSource(track, command.voice)
323
+ : ((): { src: GranularSource } | { error: string } => {
324
+ const kept = keepSrc();
325
+ if ("error" in kept) return kept as { error: string };
326
+ return {
327
+ src: (kept.src as GranularSource) ?? DEFAULT_GRANULAR_SOURCE,
328
+ };
329
+ })();
330
+ if ("error" in own) return { ok: false, message: `grain · ${own.error}` };
331
+ if (own.src === DEFAULT_GRANULAR_SOURCE) delete base.src;
332
+ else base.src = own.src;
333
+ }
334
+ if (command.type === "grain-preset") {
335
+ // A preset replaces the overrides; the source stays.
336
+ granular = { preset: command.preset };
337
+ if (base.src !== undefined) granular.src = base.src;
338
+ } else granular = base;
339
+ } else if (command.type === "grain-src") {
340
+ instrument = GRANULAR_INSTRUMENT;
341
+ granular = active || track.granular ? { ...track.granular } : {};
342
+ if (command.voice !== undefined) {
343
+ const own = ownGranularSource(track, command.voice);
344
+ if ("error" in own) return { ok: false, message: `grain · ${own.error}` };
345
+ granular.src = own.src;
346
+ } else if (command.synth === DEFAULT_GRANULAR_SOURCE) delete granular.src;
347
+ else granular.src = command.synth;
348
+ } else {
349
+ instrument = GRANULAR_INSTRUMENT;
350
+ granular = active && track.granular ? { ...track.granular } : {};
351
+ if (!active) {
352
+ const kept = keepSrc();
353
+ if ("error" in kept)
354
+ return { ok: false, message: `grain · ${kept.error}` };
355
+ Object.assign(granular, track.granular ?? {}, kept);
356
+ }
357
+ for (const [key, value] of Object.entries(command.values)) {
358
+ if (value === null) delete granular[key];
359
+ else granular[key] = value;
360
+ }
361
+ }
362
+ // Remember the voice to go back to when granular turns on.
363
+ if (granular !== null && command.type !== "grain-off") {
364
+ const from = active
365
+ ? track.granular?.from
366
+ : track.sampler || isGranularInstrument(track.instrument)
367
+ ? undefined
368
+ : track.instrument;
369
+ if (from) granular.from = from;
370
+ else delete granular.from;
371
+ }
372
+ let next: TrackScore;
373
+ try {
374
+ next = updateTrack(score, trackId, {
375
+ instrument,
376
+ granular: granular === null ? null : granularOrEmpty(granular),
377
+ ...(synthPatch ? { synth: synthPatch as never } : {}),
378
+ });
379
+ } catch (error) {
380
+ if (
381
+ error instanceof ScoreValidationError ||
382
+ error instanceof FxValidationError
383
+ )
384
+ return { ok: false, message: `grain · ${error.message}` };
385
+ throw error;
386
+ }
387
+ const stored = next.tracks.find((candidate) => candidate.id === trackId);
388
+ return {
389
+ ok: true,
390
+ message:
391
+ command.type === "grain-off"
392
+ ? `grain · off · ${instrument}`
393
+ : `grain · ${describeGranular(stored?.granular)}`,
394
+ next,
395
+ kind: "score.granular",
396
+ payload: {
397
+ trackId,
398
+ instrument: stored?.instrument ?? instrument,
399
+ granular: stored?.granular ?? null,
400
+ },
401
+ };
402
+ }
403
+
404
+ /** Validated settings; the score's normaliser re-checks the SampleRef. */
405
+ function granularOrEmpty(value: Record<string, unknown>): TrackGranular {
406
+ return normalizeGranular(value, (ref) => ref as never) ?? Object.freeze({});
407
+ }
408
+
409
+ /** Track names that create a granular track: `cloud`, `hold-2`, … */
410
+ export function granularTrackPreset(
411
+ trackId: string,
412
+ ): GranularPresetName | undefined {
413
+ const match = /^([a-z]+)(?:-\d{1,3})?$/.exec(trackId);
414
+ return match && isGranularPreset(match[1]!) ? match[1] : undefined;
415
+ }
416
+
417
+ /**
418
+ * The local answer for `grain src <something else>` (`grain src bus:guitars`),
419
+ * so a source dawg cannot read yet never goes to the agent.
420
+ */
421
+ export function grainSrcHint(prompt: string): string | undefined {
422
+ const words = prompt.trim().toLowerCase().split(/\s+/);
423
+ if (words[0] !== "grain" || (words[1] !== "src" && words[1] !== "source"))
424
+ return undefined;
425
+ if (parseGranularCommand(prompt)) return undefined;
426
+ return "grain src synth:<preset>[@note] | voice <name> · a bus or another track is not a source yet: render it and load the file as a sampler voice";
427
+ }
@@ -53,6 +53,12 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
53
53
  command: "wtenv|wtattack|wtdecay|wtrate|wtdepth|warp <n>",
54
54
  summary: "scan the table · warpmode bendp",
55
55
  },
56
+ {
57
+ command:
58
+ "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off",
59
+ summary:
60
+ "granular cloud · grain cloud · grain scan 0.2 · track swarm-2 · grain presets",
61
+ },
56
62
  {
57
63
  command: "fx <effect> <param> <value> | on | off | preset <name>",
58
64
  summary: "effects · fx delay mix 0.3 · fx reverb on · fx lists them",
@@ -61,6 +67,39 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
61
67
  command: "synth <param> <value> | preset <name>",
62
68
  summary: "synth voice · synth lpf 1200 · synth lists every param",
63
69
  },
70
+ {
71
+ command: "string <preset> | <param> <value> | presets | reset | off",
72
+ summary:
73
+ "plucked strings · string sitar · string buzz 0.8 · string ring 6",
74
+ },
75
+ {
76
+ command: "rig <preset> | reset",
77
+ summary:
78
+ "guitar rig · rig crunch · rig metal · rig funk · rig lists them",
79
+ },
80
+ {
81
+ command: "track jangle|punk|funk|ragged|gtr-lead|gtr-metal|bachata",
82
+ summary: "new guitar track with that rig",
83
+ },
84
+ {
85
+ command: "stomp|head|cab <type> | <param> <value>",
86
+ summary:
87
+ "pedal, amp, cabinet · stomp rat · head gain 7 gate -55 · cab 4x12",
88
+ },
89
+ {
90
+ command: "piano [<preset>] | grand | upright | felt | honkytonk",
91
+ summary: "modelled piano · piano ballad · piano lofi · piano prepared",
92
+ },
93
+ {
94
+ command: "keys <param> <value> | preset <name> | reset",
95
+ summary: "piano params · keys hardness 0.3 decay 1.5 · keys lists them",
96
+ },
97
+ {
98
+ command:
99
+ "modal <preset> | <param> <value> | mallet <name> | reset | off",
100
+ summary:
101
+ "mallets and bells · modal vibes · modal hardness 0.8 · modal presets",
102
+ },
64
103
  {
65
104
  command: "art <articulation>|off [target]",
66
105
  summary:
@@ -224,6 +263,19 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
224
263
  command: "/sample [<path> [as <voice>]]",
225
264
  summary: "add a sample voice · list voices",
226
265
  },
266
+ {
267
+ command: "/bpm <n> [<voice>]",
268
+ summary:
269
+ "the sample's own tempo (slash needed: bare bpm is song tempo)",
270
+ },
271
+ {
272
+ command: "/fitmode [repitch|beats|tones|auto] [<voice>]",
273
+ summary: "how it fits · alone suggests one from the sound",
274
+ },
275
+ {
276
+ command: "/len <beats> [<voice>]",
277
+ summary: "the sample lasts n beats of the song",
278
+ },
227
279
  ],
228
280
  },
229
281
  {
@@ -366,7 +418,7 @@ export const HELP_GUIDE: readonly HelpSection[] = [
366
418
  entries: [
367
419
  {
368
420
  command: "ctrl-k › Sound",
369
- summary: "instrument, envelope, filter, wavetable",
421
+ summary: "instrument, strings, envelope, filter, wavetable",
370
422
  },
371
423
  { command: "ctrl-k › Effects", summary: "delay, reverb, distortion …" },
372
424
  {
@@ -561,7 +613,7 @@ export const USAGE: Readonly<Record<string, string>> = {
561
613
  grid: "/grid 1/4|1/8|1/8T|1/16|1/16T|1/32",
562
614
  tempo:
563
615
  "tempo takes 20…300 · tempo 120 · tempo 90 at bar 9 [ramp|exp] · tempo remove bar 9 · tempo clear · tempo map",
564
- bpm: "tempo takes 20…300 · tempo 120",
616
+ bpm: "tempo takes 20…300 · tempo 120 · a sample's own tempo: /bpm 174 [<voice>] · /bpm off",
565
617
  rit: "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
566
618
  ritardando:
567
619
  "rit [<n> bars|beats] [to <bpm>] [at bar <n>|<beat>] [exp] · rit 4 bars to 80",
@@ -583,12 +635,16 @@ export const USAGE: Readonly<Record<string, string>> = {
583
635
  bars: "bars takes 1…256 · bars 8",
584
636
  extend: "extend <count> bars · extend 4 bars",
585
637
  instrument:
586
- "instrument <name> · sine piano pluck bass saw square triangle wavetable kit · synth: sawtooth supersaw pulse white pink z_square…",
638
+ "instrument <name> · sine piano pluck bass saw square triangle wavetable kit · pianos: grand upright felt honkytonk prepared · synth: sawtooth supersaw pulse white pink z_square…",
587
639
  volume: "volume takes 0…1 · volume 0.8",
588
640
  vol: "volume takes 0…1 · volume 0.8",
589
641
  pan: "pan takes -1…1 · pan -0.5",
590
642
  wt: "wt <table> | wt <0..1> | wt list · wt basic · wt wt_digital:2",
591
643
  wavetable: "wt <table> | wt <0..1> | wt list · wt basic · wt wt_digital:2",
644
+ grain:
645
+ "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off | presets · grain cloud · grain pitch 12",
646
+ granular:
647
+ "grain <preset> | <param> <value> | on [voice V] | src synth:<name>|voice V | reset | off | presets · grain cloud",
592
648
  warpmode: "warpmode none|asym|bendp|bendm|bendmp|sync|quant",
593
649
  filter: "filter <hz> [res] · filter 800 0.3 · filter off",
594
650
  delay: "delay <beats> [fb] [mix] · delay 0.75 0.4 0.3 · delay off",
@@ -611,6 +667,9 @@ export const USAGE: Readonly<Record<string, string>> = {
611
667
  sample:
612
668
  "/sample <path> [as <voice>] · /sample set <voice> <control> <value>… · /sample set brk fit on clip 1",
613
669
  samples: "/sample · lists the focused track's voices",
670
+ fitmode:
671
+ "/fitmode [repitch|beats|tones|auto|off] [<voice>] · /fitmode beats · /fitmode auto brk",
672
+ len: "/len <beats> [<voice>] · /len 16 · /len off",
614
673
  view: "/view focus | all",
615
674
  transcript: "/transcript",
616
675
  log: "/transcript",
@@ -624,6 +683,18 @@ export const USAGE: Readonly<Record<string, string>> = {
624
683
  guide: "/guide [topic] · /guide chords · F1",
625
684
  fx: "fx <effect> <param> <value> | on | off | preset <name> · fx delay mix 0.3",
626
685
  synth: "synth <param> <value> | preset <name> · synth lpf 1200",
686
+ string:
687
+ "string <preset> | preset <name> | <param> <value> | presets | reset | off · string koto",
688
+ rig: "rig clean|crunch|punk|ragged|lead|metal|fuzz|octave|funk|wah|bachata|spring|bassdrive|reese|jangle|alt | reset",
689
+ stomp:
690
+ "stomp fuzz|face|od|rat|octave | gain <0-10> tone <0-1> level <dB> | off",
691
+ head: "head clean|chime|crunch|lead|high|solid|bass | gain bass mid treble presence master <0-10> | gate <dB> | off",
692
+ cab: "cab 1x12|2x12|4x12|1x10|open|8x10|1x15|di | mic <0-1> | off",
693
+ keys: "keys <param> <value> | preset <name> | reset | presets · keys hardness 0.3 · keys stretch 0",
694
+ piano:
695
+ "piano [grand|ballad|upright|felt|lofi|honkytonk|prepared] · piano ballad",
696
+ modal:
697
+ "modal <preset> | <body> | <param> <value> | mallet <name> | reset | off | presets · modal vibes · modal saron · modal ring 3",
627
698
  pack: "/pack list | info <name> | use <pack>/<sound> | add <url>",
628
699
  kit: "/kit [name] · /kit syn909",
629
700
  euclid: "/euclid [voice] · euclid hat 7 16",