@hraness/dawg 0.4.0 → 0.5.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 (116) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/DAWG.md +300 -35
  3. package/README.md +4 -4
  4. package/core/chords.ts +288 -7
  5. package/core/diff.ts +37 -12
  6. package/core/expression.ts +1241 -0
  7. package/core/loop.ts +23 -0
  8. package/core/master.ts +455 -0
  9. package/core/midi.ts +452 -0
  10. package/core/rhythm.ts +7 -2
  11. package/core/score.ts +536 -56
  12. package/core/sdk/eval-child.ts +38 -3
  13. package/core/sdk/print.ts +445 -14
  14. package/core/sdk/v1.ts +1709 -23
  15. package/core/sections.ts +2046 -0
  16. package/core/synth.ts +11 -1
  17. package/core/tempo.ts +1318 -0
  18. package/core/tuning.ts +1180 -0
  19. package/guides/audition.md +26 -0
  20. package/guides/automation.md +26 -0
  21. package/guides/chords.md +28 -0
  22. package/guides/effects.md +27 -0
  23. package/guides/faders.md +29 -0
  24. package/guides/files.md +28 -0
  25. package/guides/getting-started.md +26 -0
  26. package/guides/index.ts +75 -0
  27. package/guides/keys.md +27 -0
  28. package/guides/media.md +27 -0
  29. package/guides/mix.md +23 -0
  30. package/guides/music.md +14 -0
  31. package/guides/notes.md +26 -0
  32. package/guides/performance.md +26 -0
  33. package/guides/play.md +24 -0
  34. package/guides/project.md +13 -0
  35. package/guides/providers.md +25 -0
  36. package/guides/rhythm.md +26 -0
  37. package/guides/sessions.md +21 -0
  38. package/guides/sound.md +15 -0
  39. package/guides/sounds.md +25 -0
  40. package/guides/tempo.md +26 -0
  41. package/guides/tracks.md +25 -0
  42. package/guides/web-search.md +21 -0
  43. package/package.json +3 -1
  44. package/src/agent/agent.ts +2 -0
  45. package/src/agent/brief.ts +77 -3
  46. package/src/agent/chord-tools.ts +9 -1
  47. package/src/agent/expression-tools.ts +336 -0
  48. package/src/agent/master-tools.ts +299 -0
  49. package/src/agent/models.ts +4 -4
  50. package/src/agent/ops.ts +29 -4
  51. package/src/agent/planner.ts +45 -2
  52. package/src/agent/preview-tool.ts +21 -3
  53. package/src/agent/section-tools.ts +411 -0
  54. package/src/agent/time-tools.ts +290 -0
  55. package/src/agent/tools.ts +36 -2
  56. package/src/agent/tuning-tools.ts +301 -0
  57. package/src/agent/xcb-agent.ts +2 -0
  58. package/src/audio/arrange.ts +471 -0
  59. package/src/audio/audition.ts +16 -3
  60. package/src/audio/click.ts +113 -1
  61. package/src/audio/clock.ts +71 -5
  62. package/src/audio/effects/bus.ts +5 -4
  63. package/src/audio/effects/chain.ts +12 -2
  64. package/src/audio/effects/common.ts +30 -1
  65. package/src/audio/effects/convolution.ts +43 -5
  66. package/src/audio/effects/dynamics.ts +2 -1
  67. package/src/audio/effects/filter.ts +5 -3
  68. package/src/audio/effects/modulation.ts +5 -1
  69. package/src/audio/effects/space.ts +197 -50
  70. package/src/audio/engine.ts +175 -20
  71. package/src/audio/live.ts +33 -6
  72. package/src/audio/loudness.ts +551 -0
  73. package/src/audio/master.ts +660 -0
  74. package/src/audio/measure-worker.ts +45 -0
  75. package/src/audio/measure.ts +110 -0
  76. package/src/audio/player.ts +11 -6
  77. package/src/audio/preview.ts +87 -16
  78. package/src/audio/render-worker.ts +6 -1
  79. package/src/audio/renderer.ts +7 -1
  80. package/src/audio/sampler.ts +108 -19
  81. package/src/audio/synth/voice.ts +60 -13
  82. package/src/audio/synth/zzfx.ts +10 -4
  83. package/src/audio/warp.ts +86 -0
  84. package/src/audio/wav.ts +448 -59
  85. package/src/commands/arrange.ts +949 -0
  86. package/src/commands/expression.ts +934 -0
  87. package/src/commands/help.ts +281 -24
  88. package/src/commands/master.ts +361 -0
  89. package/src/commands/music.ts +6 -1
  90. package/src/commands/synth.ts +11 -1
  91. package/src/commands/time.ts +964 -0
  92. package/src/commands/tuning.ts +490 -0
  93. package/src/main.ts +826 -47
  94. package/src/render.ts +109 -7
  95. package/src/session/daemon.ts +18 -8
  96. package/src/session/naming.ts +8 -1
  97. package/src/session/rebase.ts +21 -0
  98. package/src/tui/arrange-menu.ts +390 -0
  99. package/src/tui/audition.ts +58 -5
  100. package/src/tui/euclid.ts +50 -6
  101. package/src/tui/fader.ts +409 -0
  102. package/src/tui/menu-time.ts +401 -0
  103. package/src/tui/menu.ts +656 -16
  104. package/src/tui/performance-menu.ts +235 -0
  105. package/src/tui/play-chords.ts +95 -1
  106. package/src/tui/play-mode.ts +150 -6
  107. package/src/tui/play-session.ts +414 -41
  108. package/tui/app.ts +262 -13
  109. package/tui/arrange-strip.ts +174 -0
  110. package/tui/drawer.ts +478 -0
  111. package/tui/grammar.ts +79 -12
  112. package/tui/guide.ts +351 -0
  113. package/tui/highway.ts +87 -4
  114. package/tui/hits.ts +68 -0
  115. package/tui/input.ts +7 -0
  116. package/tui/keys.ts +72 -0
package/tui/drawer.ts ADDED
@@ -0,0 +1,478 @@
1
+ /**
2
+ * The fader drawer: a panel docked directly above the prompt bar, over the
3
+ * bottom of the piano roll, for editing one or more parameters of a device
4
+ * (Ableton's device view and the OP-1/Elektron "one encoder per value"
5
+ * habit). The piano roll stays visible above it while the terminal is tall
6
+ * enough.
7
+ *
8
+ * ╭ fader · filter ────────────────────── ● staged [keep] [revert] ╮
9
+ * │ › cutoff 1200 Hz ← 800 Hz 20 … 20000 Hz │
10
+ * │ [−] ━━━━━━━━━━━━━━━━━━●────────────────────────────────── [+] │
11
+ * │ type lowpass │ highpass │ bandpass │ notch │
12
+ * ╰──────────────────── ←→ adjust · ⇧ coarse · enter keep · esc revert ╯
13
+ *
14
+ * Sizes: two rows per field while the drawer takes at most half of the
15
+ * piano roll; one row per field (label, value and a short bar) below that;
16
+ * a window of fields around the focused one when even that does not fit.
17
+ * The smallest is a single borderless row, which fits the 8-row minimum.
18
+ *
19
+ * Painting and hit-testing share `drawerLayout`, and the paint pass records
20
+ * every click target into the frame's HitMap.
21
+ */
22
+ import type { CellBuffer } from "./screen.ts";
23
+ import type { HitMap } from "./hits.ts";
24
+ import { onBackground, type Style, type Theme } from "./theme.ts";
25
+ import { displayWidth, truncate } from "./text.ts";
26
+ import { asciiHint, fitHint } from "./grammar.ts";
27
+
28
+ export type DrawerField =
29
+ | Readonly<{
30
+ kind: "number";
31
+ label: string;
32
+ /** Formatted staged (or committed) value; `off` while unset. */
33
+ text: string;
34
+ /** Formatted committed value when it differs from `text`. */
35
+ committed?: string | undefined;
36
+ /** 0…1 along the bar; undefined while the parameter is off. */
37
+ position: number | undefined;
38
+ /** Where the committed value sits, when it differs. */
39
+ committedPosition?: number | undefined;
40
+ minText: string;
41
+ maxText: string;
42
+ }>
43
+ | Readonly<{
44
+ kind: "choice";
45
+ label: string;
46
+ options: readonly string[];
47
+ index: number;
48
+ committedIndex?: number | undefined;
49
+ }>;
50
+
51
+ export type DrawerView = Readonly<{
52
+ title: string;
53
+ fields: readonly DrawerField[];
54
+ focus: number;
55
+ /** Digits typed for the focused field (shown in place of its value). */
56
+ typing?: string | undefined;
57
+ /** Edits differ from the committed score. */
58
+ dirty: boolean;
59
+ /** `♪ solo · B` while the audition loop plays. */
60
+ status?: string | undefined;
61
+ hint: string;
62
+ }>;
63
+
64
+ export type DrawerMode = "full" | "compact" | "line";
65
+
66
+ export interface DrawerLayout {
67
+ mode: DrawerMode;
68
+ top: number;
69
+ height: number;
70
+ /** Indexes of the fields shown, top to bottom. */
71
+ first: number;
72
+ count: number;
73
+ /** Rows per field. */
74
+ rowsPerField: 1 | 2;
75
+ bordered: boolean;
76
+ }
77
+
78
+ /** Fewest highway rows the drawer leaves for the piano roll above it. */
79
+ export function keepAbove(regionHeight: number): number {
80
+ return Math.max(3, Math.floor(regionHeight * 0.4));
81
+ }
82
+
83
+ /**
84
+ * Where the drawer sits in the highway region (`y`, `height`): docked at
85
+ * its bottom, as tall as its fields need within the budget.
86
+ */
87
+ export function drawerLayout(
88
+ view: Pick<DrawerView, "fields" | "focus">,
89
+ region: { y: number; height: number },
90
+ ): DrawerLayout {
91
+ const total = Math.max(1, view.fields.length);
92
+ const bottom = region.y + region.height;
93
+ const budget = region.height - keepAbove(region.height);
94
+ const window = (rows: number): { first: number; count: number } => {
95
+ const count = Math.max(1, Math.min(total, rows));
96
+ const focus = Math.min(Math.max(0, view.focus), total - 1);
97
+ const first = Math.max(0, Math.min(total - count, focus - count + 1));
98
+ return { first, count };
99
+ };
100
+ if (2 + 2 * total <= Math.floor(region.height / 2)) {
101
+ const height = 2 + 2 * total;
102
+ return {
103
+ mode: "full",
104
+ top: bottom - height,
105
+ height,
106
+ first: 0,
107
+ count: total,
108
+ rowsPerField: 2,
109
+ bordered: true,
110
+ };
111
+ }
112
+ if (budget >= 3) {
113
+ const { first, count } = window(budget - 2);
114
+ const height = 2 + count;
115
+ return {
116
+ mode: "compact",
117
+ top: bottom - height,
118
+ height,
119
+ first,
120
+ count,
121
+ rowsPerField: 1,
122
+ bordered: true,
123
+ };
124
+ }
125
+ const { first, count } = window(1);
126
+ return {
127
+ mode: "line",
128
+ top: bottom - 1,
129
+ height: Math.min(1, region.height),
130
+ first,
131
+ count,
132
+ rowsPerField: 1,
133
+ bordered: false,
134
+ };
135
+ }
136
+
137
+ interface Glyphs {
138
+ box: { tl: string; tr: string; bl: string; br: string; h: string; v: string };
139
+ minus: string;
140
+ plus: string;
141
+ filled: string;
142
+ empty: string;
143
+ knob: string;
144
+ ghost: string;
145
+ marker: string;
146
+ arrow: string;
147
+ sep: string;
148
+ }
149
+
150
+ const UNICODE: Glyphs = {
151
+ box: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
152
+ minus: "[−]",
153
+ plus: "[+]",
154
+ filled: "━",
155
+ empty: "─",
156
+ knob: "●",
157
+ ghost: "┃",
158
+ marker: "›",
159
+ arrow: "←",
160
+ sep: "│",
161
+ };
162
+ const ASCII: Glyphs = {
163
+ box: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" },
164
+ minus: "[-]",
165
+ plus: "[+]",
166
+ filled: "=",
167
+ empty: "-",
168
+ knob: "O",
169
+ ghost: "|",
170
+ marker: ">",
171
+ arrow: "<-",
172
+ sep: "|",
173
+ };
174
+
175
+ export interface DrawerPaint {
176
+ theme: Theme;
177
+ unicode: boolean;
178
+ }
179
+
180
+ /** Paint the drawer into the highway region; returns its layout. */
181
+ export function paintDrawer(
182
+ buffer: CellBuffer,
183
+ region: { y: number; height: number },
184
+ width: number,
185
+ view: DrawerView,
186
+ options: DrawerPaint,
187
+ hits?: HitMap,
188
+ ): DrawerLayout | undefined {
189
+ if (region.height <= 0 || width < 16 || view.fields.length === 0)
190
+ return undefined;
191
+ const layout = drawerLayout(view, region);
192
+ const roles = options.theme.roles;
193
+ const glyphs = options.unicode ? UNICODE : ASCII;
194
+ const panel = roles.panel;
195
+ const on = (style: Style): Style => onBackground(style, panel);
196
+ const { top, height } = layout;
197
+ buffer.fill(0, top, width, height, panel);
198
+ let inner = { left: 1, right: width - 1 };
199
+ if (layout.bordered) {
200
+ const border = on(roles.borderFocus);
201
+ const box = glyphs.box;
202
+ buffer.set(0, top, box.tl, border);
203
+ buffer.set(width - 1, top, box.tr, border);
204
+ buffer.set(0, top + height - 1, box.bl, border);
205
+ buffer.set(width - 1, top + height - 1, box.br, border);
206
+ for (let x = 1; x < width - 1; x += 1) {
207
+ buffer.set(x, top, box.h, border);
208
+ buffer.set(x, top + height - 1, box.h, border);
209
+ }
210
+ for (let y = top + 1; y < top + height - 1; y += 1) {
211
+ buffer.set(0, y, box.v, border);
212
+ buffer.set(width - 1, y, box.v, border);
213
+ }
214
+ inner = { left: 2, right: width - 2 };
215
+ // Title left; staged state and the keep / revert buttons right.
216
+ const keep = "[keep]";
217
+ const revert = "[revert]";
218
+ const buttons = view.dirty ? `${keep} ${revert}` : "";
219
+ const state = view.dirty ? `${options.unicode ? "●" : "*"} staged` : "";
220
+ const status = [view.status, state].filter(Boolean).join(" · ");
221
+ const right = [status, buttons].filter(Boolean).join(" ");
222
+ const rightWidth = displayWidth(right);
223
+ const titleRoom = Math.max(0, width - 6 - (right ? rightWidth + 3 : 0));
224
+ buffer.text(
225
+ 2,
226
+ top,
227
+ ` ${truncate(view.title, Math.max(0, titleRoom - 2))} `,
228
+ on({ ...roles.text, bold: true }),
229
+ );
230
+ if (right && rightWidth + 4 < width - displayWidth(view.title)) {
231
+ const rx = width - 3 - rightWidth;
232
+ buffer.text(
233
+ rx - 1,
234
+ top,
235
+ ` ${status}${status && buttons ? " " : ""}`,
236
+ on(view.dirty ? roles.warning : roles.muted),
237
+ );
238
+ if (buttons) {
239
+ const kx = width - 3 - displayWidth(buttons);
240
+ buffer.text(kx, top, `${keep} `, on({ ...roles.success, bold: true }));
241
+ buffer.text(kx + keep.length + 1, top, revert, on(roles.muted));
242
+ hits?.add(kx, top, keep.length, 1, { kind: "fader-keep" });
243
+ hits?.add(kx + keep.length + 1, top, revert.length, 1, {
244
+ kind: "fader-revert",
245
+ });
246
+ }
247
+ }
248
+ const hint = fitHint(
249
+ options.unicode ? view.hint : asciiHint(view.hint),
250
+ width - 6,
251
+ );
252
+ if (hint)
253
+ buffer.text(
254
+ width - 2 - displayWidth(hint),
255
+ top + height - 1,
256
+ hint,
257
+ on(roles.muted),
258
+ );
259
+ }
260
+ const labelWidth = Math.min(
261
+ layout.mode === "full" ? 22 : 16,
262
+ Math.max(...view.fields.map((field) => displayWidth(field.label))),
263
+ );
264
+ const rowTop = layout.bordered ? top + 1 : top;
265
+ for (let offset = 0; offset < layout.count; offset += 1) {
266
+ const index = layout.first + offset;
267
+ const field = view.fields[index]!;
268
+ const focused = index === view.focus;
269
+ const y = rowTop + offset * layout.rowsPerField;
270
+ hits?.add(inner.left, y, inner.right - inner.left, layout.rowsPerField, {
271
+ kind: "fader-row",
272
+ field: index,
273
+ });
274
+ const marker = focused ? glyphs.marker : " ";
275
+ const labelStyle = focused
276
+ ? on({ ...roles.borderFocus, bold: true })
277
+ : on(roles.text);
278
+ buffer.text(inner.left, y, marker, labelStyle);
279
+ buffer.text(
280
+ inner.left + 2,
281
+ y,
282
+ truncate(field.label, labelWidth),
283
+ labelStyle,
284
+ );
285
+ const valueX = inner.left + 2 + labelWidth + 2;
286
+ const typing = focused && view.typing !== undefined;
287
+ if (field.kind === "number") {
288
+ const value = typing ? `${view.typing}▏` : field.text;
289
+ const committed =
290
+ !typing && field.committed !== undefined
291
+ ? ` ${glyphs.arrow} ${field.committed}`
292
+ : "";
293
+ if (layout.rowsPerField === 2) {
294
+ const used = buffer.text(
295
+ valueX,
296
+ y,
297
+ value,
298
+ on({ ...roles.text, bold: true }),
299
+ inner.right - valueX,
300
+ );
301
+ if (committed)
302
+ buffer.text(
303
+ valueX + used,
304
+ y,
305
+ committed,
306
+ on(roles.muted),
307
+ inner.right - valueX - used,
308
+ );
309
+ const range = `${field.minText} … ${field.maxText}`;
310
+ const rangeX = inner.right - displayWidth(range);
311
+ if (rangeX > valueX + used + displayWidth(committed) + 2)
312
+ buffer.text(rangeX, y, range, on(roles.faint));
313
+ paintBar(buffer, y + 1, inner.left + 2, inner.right, field, index, {
314
+ glyphs,
315
+ focused,
316
+ roles,
317
+ on,
318
+ ...(hits ? { hits } : {}),
319
+ });
320
+ } else {
321
+ const valueWidth = Math.min(
322
+ 18,
323
+ Math.max(8, Math.floor((inner.right - valueX) / 3)),
324
+ );
325
+ buffer.text(
326
+ valueX,
327
+ y,
328
+ truncate(value, valueWidth),
329
+ on({ ...roles.text, bold: true }),
330
+ );
331
+ paintBar(
332
+ buffer,
333
+ y,
334
+ valueX + valueWidth + 1,
335
+ inner.right,
336
+ field,
337
+ index,
338
+ { glyphs, focused, roles, on, ...(hits ? { hits } : {}) },
339
+ );
340
+ }
341
+ } else {
342
+ paintOptions(buffer, y, valueX, inner.right, field, index, {
343
+ glyphs,
344
+ focused,
345
+ roles,
346
+ on,
347
+ ...(hits ? { hits } : {}),
348
+ });
349
+ if (layout.rowsPerField === 2 && field.committedIndex !== undefined) {
350
+ const was = field.options[field.committedIndex];
351
+ if (was !== undefined)
352
+ buffer.text(
353
+ valueX,
354
+ y + 1,
355
+ truncate(`${glyphs.arrow} ${was}`, inner.right - valueX),
356
+ on(roles.muted),
357
+ );
358
+ }
359
+ }
360
+ }
361
+ return layout;
362
+ }
363
+
364
+ interface RowPaint {
365
+ glyphs: Glyphs;
366
+ focused: boolean;
367
+ roles: Theme["roles"];
368
+ on: (style: Style) => Style;
369
+ hits?: HitMap;
370
+ }
371
+
372
+ /** `[−] ━━━━━●─────── [+]` from `left` to `right`, with its hit regions. */
373
+ function paintBar(
374
+ buffer: CellBuffer,
375
+ y: number,
376
+ left: number,
377
+ right: number,
378
+ field: Extract<DrawerField, { kind: "number" }>,
379
+ index: number,
380
+ paint: RowPaint,
381
+ ): void {
382
+ const { glyphs, roles, on, hits } = paint;
383
+ const buttonWidth = displayWidth(glyphs.minus);
384
+ const room = right - left;
385
+ if (room < buttonWidth * 2 + 2) return;
386
+ const barLeft = left + buttonWidth + 1;
387
+ const barWidth = right - left - buttonWidth * 2 - 2;
388
+ const button = on({ ...roles.borderFocus, bold: paint.focused });
389
+ buffer.text(left, y, glyphs.minus, button);
390
+ buffer.text(right - buttonWidth, y, glyphs.plus, button);
391
+ hits?.add(left, y, buttonWidth, 1, {
392
+ kind: "fader-step",
393
+ field: index,
394
+ direction: -1,
395
+ });
396
+ hits?.add(right - buttonWidth, y, buttonWidth, 1, {
397
+ kind: "fader-step",
398
+ field: index,
399
+ direction: 1,
400
+ });
401
+ if (barWidth < 3) return;
402
+ hits?.add(barLeft, y, barWidth, 1, {
403
+ kind: "fader-bar",
404
+ field: index,
405
+ left: barLeft,
406
+ width: barWidth,
407
+ });
408
+ const at = (position: number) =>
409
+ Math.round(Math.min(1, Math.max(0, position)) * (barWidth - 1));
410
+ const knob = field.position === undefined ? undefined : at(field.position);
411
+ const ghost =
412
+ field.committedPosition === undefined
413
+ ? undefined
414
+ : at(field.committedPosition);
415
+ const filled = on(paint.focused ? roles.borderFocus : roles.text);
416
+ const empty = on(roles.faint);
417
+ for (let column = 0; column < barWidth; column += 1) {
418
+ const x = barLeft + column;
419
+ if (column === knob)
420
+ buffer.set(x, y, glyphs.knob, on({ ...roles.warning, bold: true }));
421
+ else if (column === ghost) buffer.set(x, y, glyphs.ghost, on(roles.muted));
422
+ else if (knob !== undefined && column < knob)
423
+ buffer.set(x, y, glyphs.filled, filled);
424
+ else buffer.set(x, y, glyphs.empty, empty);
425
+ }
426
+ }
427
+
428
+ /** `lowpass │ highpass │ bandpass`: a segmented selector, windowed. */
429
+ function paintOptions(
430
+ buffer: CellBuffer,
431
+ y: number,
432
+ left: number,
433
+ right: number,
434
+ field: Extract<DrawerField, { kind: "choice" }>,
435
+ index: number,
436
+ paint: RowPaint,
437
+ ): void {
438
+ const { glyphs, roles, on, hits } = paint;
439
+ const room = right - left;
440
+ // Start far enough left that the selected option is in view.
441
+ let first = 0;
442
+ const widthFrom = (start: number, end: number) =>
443
+ field.options
444
+ .slice(start, end + 1)
445
+ .reduce((sum, option) => sum + displayWidth(option) + 2, 0) +
446
+ (end - start);
447
+ while (first < field.index && widthFrom(first, field.index) + 2 > room)
448
+ first += 1;
449
+ let x = left;
450
+ if (first > 0) x += buffer.text(x, y, "‹ ", on(roles.faint));
451
+ for (let at = first; at < field.options.length; at += 1) {
452
+ const option = field.options[at]!;
453
+ const width = displayWidth(option) + 2;
454
+ if (x + width > right - (at < field.options.length - 1 ? 2 : 0)) {
455
+ buffer.text(x, y, " ›", on(roles.faint));
456
+ break;
457
+ }
458
+ const selected = at === field.index;
459
+ const style = selected
460
+ ? on({
461
+ ...(paint.focused ? roles.borderFocus : roles.text),
462
+ bold: true,
463
+ reverse: true,
464
+ })
465
+ : at === field.committedIndex
466
+ ? on({ ...roles.muted, underline: true })
467
+ : on(roles.muted);
468
+ buffer.text(x, y, ` ${option} `, style);
469
+ hits?.add(x, y, width, 1, {
470
+ kind: "fader-option",
471
+ field: index,
472
+ option: at,
473
+ });
474
+ x += width;
475
+ if (at < field.options.length - 1 && x + 1 < right)
476
+ x += buffer.text(x, y, glyphs.sep, on(roles.faint));
477
+ }
478
+ }
package/tui/grammar.ts CHANGED
@@ -80,7 +80,7 @@ export const HINTS = {
80
80
  action: " ↑↓ move · enter apply · / filter · esc back · ? keys ",
81
81
  typing: " type a value · enter apply · esc cancel ",
82
82
  euclid:
83
- " ↑↓ voice · ←→ adjust · tab field · space hear · x off · esc back · ? keys ",
83
+ " ↑↓ voice · ←→ adjust · tab field · space loop · x off · esc back · ? keys ",
84
84
  text: " ↑↓ scroll · pgup pgdn page · esc back · ? keys ",
85
85
  log: " ↑↓ scroll · / filter · esc back · ? keys ",
86
86
  keys: " any key closes ",
@@ -88,6 +88,27 @@ export const HINTS = {
88
88
 
89
89
  // ── the `?` panel ─────────────────────────────────────────────────────
90
90
 
91
+ /** The audition loop's keys, shared by the menu and the rhythm editor. */
92
+ const AUDITIONING: readonly KeySection[] = [
93
+ {
94
+ title: "auditioning",
95
+ rows: [
96
+ ["space", "loop the focused track · again stops"],
97
+ ["c", "solo ↔ in context (the whole mix)"],
98
+ ["a", "A/B: committed ↔ staged"],
99
+ ["enter", "keep staged changes (one undo step)"],
100
+ ["esc", "revert staged changes"],
101
+ ],
102
+ },
103
+ {
104
+ title: "every change",
105
+ rows: [
106
+ ["", "while looping, stages; otherwise runs the command shown"],
107
+ ["ctrl-z", "undoes it"],
108
+ ],
109
+ },
110
+ ];
111
+
91
112
  const LIST: readonly KeyRow[] = [
92
113
  ["↑ ↓ j k", "move"],
93
114
  ["pgup pgdn", "page"],
@@ -103,7 +124,10 @@ export const KEYS = {
103
124
  rows: [
104
125
  ["type + enter", "ask for a change in plain words, or a command"],
105
126
  ["ctrl-p", "play notes on the keyboard (chords: q)"],
106
- ["ctrl-k", "menu: sound, effects, rhythm, chords, mix"],
127
+ [
128
+ "ctrl-k",
129
+ "menu: sound, effects, rhythm, chords, performance, mix, master",
130
+ ],
107
131
  ["space", "play / pause (empty prompt)"],
108
132
  ["/help", "what dawg can do · /help <topic> for more"],
109
133
  ],
@@ -120,6 +144,15 @@ export const KEYS = {
120
144
  ["ctrl-c", "quit"],
121
145
  ],
122
146
  },
147
+ {
148
+ title: "mouse",
149
+ rows: [
150
+ ["click ▶/⏸ BPM", "play / pause"],
151
+ ["click track name", "track list (click one to focus it)"],
152
+ ["click model", "model picker"],
153
+ ["volume · fx filter", "a bare param opens its fader drawer"],
154
+ ],
155
+ },
123
156
  ],
124
157
  list: [{ title: "list", rows: LIST }],
125
158
  audition: [
@@ -164,23 +197,43 @@ export const KEYS = {
164
197
  ["enter", "type a value · pick from a list"],
165
198
  ["space", "toggle on/off (elsewhere: hear the track)"],
166
199
  ["x delete", "reset to default (deletes an automation point)"],
200
+ ["enter (number)", "open its fader drawer"],
167
201
  ],
168
202
  },
169
203
  {
170
- title: "auditioning",
204
+ title: "mouse",
171
205
  rows: [
172
- ["space", "loop the focused track · again stops"],
173
- ["c", "solo ↔ in context (the whole mix)"],
174
- ["a", "A/B: committed ↔ staged"],
175
- ["enter", "keep staged changes (one undo step)"],
176
- ["esc", "revert staged changes"],
206
+ ["click", "select a row · click again opens it"],
207
+ ["wheel", "move through the list"],
208
+ ],
209
+ },
210
+ ...AUDITIONING,
211
+ ],
212
+ fader: [
213
+ {
214
+ title: "fader drawer",
215
+ rows: [
216
+ ["← → - +", "step the value (staged, heard on the loop)"],
217
+ ["shift-← → { }", "coarse step (five)"],
218
+ ["[ ] alt-← →", "fine step (a tenth)"],
219
+ ["pgup pgdn", "big step (twenty)"],
220
+ ["home end", "minimum / maximum"],
221
+ ["0-9 .", "type an exact value, enter sets it"],
222
+ ["0 d", "back to the default"],
223
+ ["↑ ↓ tab shift-tab", "next / previous param of this device"],
224
+ ["enter", "keep every staged change (one undo step)"],
225
+ ["esc", "revert and close"],
226
+ ["space a c", "loop · A/B · solo ↔ in context"],
177
227
  ],
178
228
  },
179
229
  {
180
- title: "every change",
230
+ title: "mouse",
181
231
  rows: [
182
- ["", "while looping, stages; otherwise runs the command shown"],
183
- ["ctrl-z", "undoes it"],
232
+ ["click [−] [+]", "step (shift-click: coarse)"],
233
+ ["click / drag bar", "set the value there"],
234
+ ["wheel on a fader", "step it (shift: coarse)"],
235
+ ["click an option", "choose it"],
236
+ ["[keep] [revert]", "same as enter / esc"],
184
237
  ],
185
238
  },
186
239
  ],
@@ -193,12 +246,12 @@ export const KEYS = {
193
246
  ["tab shift-tab ] [", "next / previous field"],
194
247
  ["0-9", "type a value, enter applies"],
195
248
  ["enter", "add a row (or type a value)"],
196
- ["space", "hear the voice"],
197
249
  ["x", "turn the row off"],
198
250
  ["f", "freeze into plain hits"],
199
251
  ["esc", "back"],
200
252
  ],
201
253
  },
254
+ ...AUDITIONING,
202
255
  ],
203
256
  text: [
204
257
  {
@@ -210,6 +263,19 @@ export const KEYS = {
210
263
  ],
211
264
  },
212
265
  ],
266
+ guide: [
267
+ {
268
+ title: "guides",
269
+ rows: [
270
+ ["↑ ↓ j k", "move · scroll a guide"],
271
+ ["→ l enter", "expand a section, then open the guide"],
272
+ ["← h", "collapse · go to the parent · back from a guide"],
273
+ ["/", "filter by title and text (enter opens)"],
274
+ ["esc", "clear the filter, then back, then close"],
275
+ ["f1", "open or close the guides"],
276
+ ],
277
+ },
278
+ ],
213
279
  log: [
214
280
  {
215
281
  title: "transcript",
@@ -233,6 +299,7 @@ export const KEYS = {
233
299
  ["space", "play / pause (with count-in)"],
234
300
  ["r · R", "record · record replacing"],
235
301
  ["m", "metronome click"],
302
+ ["i", "scale degrees ⇄ chromatic (home row in key)"],
236
303
  ["q", "chord mode: auto ⇄ manual"],
237
304
  ["/", "type a command, still in play mode"],
238
305
  ["ctrl-k", "menu"],