@hraness/dawg 0.4.1 → 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 (166) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/DAWG.md +551 -33
  3. package/README.md +4 -4
  4. package/core/chords.ts +288 -7
  5. package/core/diff.ts +41 -12
  6. package/core/expression.ts +1241 -0
  7. package/core/fx.ts +437 -2
  8. package/core/granular.ts +528 -0
  9. package/core/instruments.ts +281 -0
  10. package/core/keys.ts +386 -0
  11. package/core/loop.ts +23 -0
  12. package/core/master.ts +455 -0
  13. package/core/midi.ts +452 -0
  14. package/core/resonators.ts +574 -0
  15. package/core/rhythm.ts +7 -2
  16. package/core/score.ts +717 -62
  17. package/core/sdk/eval-child.ts +40 -3
  18. package/core/sdk/print.ts +614 -27
  19. package/core/sdk/sync-instruments.ts +58 -0
  20. package/core/sdk/v1.ts +2824 -45
  21. package/core/sections.ts +2072 -0
  22. package/core/strings.ts +845 -0
  23. package/core/synth.ts +11 -1
  24. package/core/tempo.ts +1318 -0
  25. package/core/tuning.ts +1180 -0
  26. package/guides/audition.md +26 -0
  27. package/guides/automation.md +26 -0
  28. package/guides/chords.md +28 -0
  29. package/guides/effects.md +29 -0
  30. package/guides/faders.md +29 -0
  31. package/guides/files.md +28 -0
  32. package/guides/getting-started.md +26 -0
  33. package/guides/index.ts +75 -0
  34. package/guides/keys.md +27 -0
  35. package/guides/media.md +27 -0
  36. package/guides/mix.md +23 -0
  37. package/guides/music.md +14 -0
  38. package/guides/notes.md +26 -0
  39. package/guides/performance.md +26 -0
  40. package/guides/play.md +24 -0
  41. package/guides/project.md +13 -0
  42. package/guides/providers.md +25 -0
  43. package/guides/rhythm.md +26 -0
  44. package/guides/sessions.md +21 -0
  45. package/guides/sound.md +15 -0
  46. package/guides/sounds.md +29 -0
  47. package/guides/tempo.md +27 -0
  48. package/guides/tracks.md +25 -0
  49. package/guides/web-search.md +21 -0
  50. package/package.json +3 -1
  51. package/src/agent/agent.ts +9 -0
  52. package/src/agent/brief.ts +77 -3
  53. package/src/agent/chord-tools.ts +9 -1
  54. package/src/agent/expression-tools.ts +336 -0
  55. package/src/agent/granular-tools.ts +138 -0
  56. package/src/agent/master-tools.ts +299 -0
  57. package/src/agent/models.ts +4 -4
  58. package/src/agent/ops.ts +68 -6
  59. package/src/agent/planner.ts +45 -2
  60. package/src/agent/preview-tool.ts +27 -4
  61. package/src/agent/section-tools.ts +411 -0
  62. package/src/agent/time-tools.ts +290 -0
  63. package/src/agent/tools.ts +574 -16
  64. package/src/agent/tuning-tools.ts +301 -0
  65. package/src/agent/xcb-agent.ts +9 -0
  66. package/src/audio/arrange.ts +489 -0
  67. package/src/audio/audition.ts +16 -3
  68. package/src/audio/click.ts +113 -1
  69. package/src/audio/clock.ts +71 -5
  70. package/src/audio/dsp/bank.ts +233 -0
  71. package/src/audio/dsp/fft.ts +6 -0
  72. package/src/audio/dsp/filters.ts +57 -0
  73. package/src/audio/dsp/interp.ts +112 -0
  74. package/src/audio/dsp/modal.ts +684 -0
  75. package/src/audio/dsp/onset.ts +197 -0
  76. package/src/audio/dsp/oversample.ts +202 -0
  77. package/src/audio/dsp/rng.ts +37 -0
  78. package/src/audio/dsp/shape.ts +81 -0
  79. package/src/audio/dsp/stft.ts +73 -0
  80. package/src/audio/dsp/window.ts +77 -0
  81. package/src/audio/effects/bus.ts +5 -4
  82. package/src/audio/effects/chain.ts +6 -1
  83. package/src/audio/effects/common.ts +30 -1
  84. package/src/audio/effects/dynamics.ts +2 -1
  85. package/src/audio/effects/filter.ts +5 -3
  86. package/src/audio/effects/modulation.ts +5 -1
  87. package/src/audio/effects/rig/cab.ts +99 -0
  88. package/src/audio/effects/rig/filters.ts +152 -0
  89. package/src/audio/effects/rig/gate.ts +39 -0
  90. package/src/audio/effects/rig/head.ts +487 -0
  91. package/src/audio/effects/rig/index.ts +170 -0
  92. package/src/audio/effects/rig/section.ts +78 -0
  93. package/src/audio/effects/rig/stomp.ts +238 -0
  94. package/src/audio/effects/space.ts +92 -2
  95. package/src/audio/engine.ts +100 -21
  96. package/src/audio/fit.ts +402 -0
  97. package/src/audio/granular.ts +664 -0
  98. package/src/audio/instrument-check.ts +133 -0
  99. package/src/audio/instruments.ts +111 -0
  100. package/src/audio/keys/dsp.ts +323 -0
  101. package/src/audio/keys/engine.ts +361 -0
  102. package/src/audio/keys/piano.ts +432 -0
  103. package/src/audio/live-worker.ts +54 -0
  104. package/src/audio/live.ts +263 -17
  105. package/src/audio/loudness.ts +596 -0
  106. package/src/audio/master.ts +660 -0
  107. package/src/audio/measure-worker.ts +45 -0
  108. package/src/audio/measure.ts +110 -0
  109. package/src/audio/player.ts +11 -6
  110. package/src/audio/preview.ts +76 -14
  111. package/src/audio/render-worker.ts +6 -1
  112. package/src/audio/renderer.ts +7 -1
  113. package/src/audio/resonators.ts +287 -0
  114. package/src/audio/sampler.ts +269 -34
  115. package/src/audio/samples.ts +30 -7
  116. package/src/audio/strings/body.ts +250 -0
  117. package/src/audio/strings/engine.ts +369 -0
  118. package/src/audio/strings/loop.ts +119 -0
  119. package/src/audio/strings/measure.test-helpers.ts +198 -0
  120. package/src/audio/strings/pluck.ts +354 -0
  121. package/src/audio/synth/voice.ts +60 -13
  122. package/src/audio/synth/zzfx.ts +10 -4
  123. package/src/audio/warp.ts +147 -0
  124. package/src/audio/wav.ts +377 -30
  125. package/src/commands/arrange.ts +949 -0
  126. package/src/commands/expression.ts +941 -0
  127. package/src/commands/fit.ts +135 -0
  128. package/src/commands/fx.ts +31 -0
  129. package/src/commands/granular.ts +427 -0
  130. package/src/commands/help.ts +355 -27
  131. package/src/commands/keys.ts +353 -0
  132. package/src/commands/master.ts +361 -0
  133. package/src/commands/modal.ts +247 -0
  134. package/src/commands/music.ts +6 -1
  135. package/src/commands/rig.ts +260 -0
  136. package/src/commands/sample.ts +3 -0
  137. package/src/commands/string.ts +175 -0
  138. package/src/commands/synth.ts +11 -1
  139. package/src/commands/time.ts +967 -0
  140. package/src/commands/tuning.ts +490 -0
  141. package/src/main.ts +919 -41
  142. package/src/project/check.ts +13 -0
  143. package/src/render.ts +139 -8
  144. package/src/session/daemon.ts +18 -8
  145. package/src/session/naming.ts +8 -1
  146. package/src/session/rebase.ts +21 -0
  147. package/src/tui/arrange-menu.ts +390 -0
  148. package/src/tui/audition.ts +38 -5
  149. package/src/tui/fader.ts +409 -0
  150. package/src/tui/granular-menu.ts +278 -0
  151. package/src/tui/menu-time.ts +401 -0
  152. package/src/tui/menu.ts +967 -23
  153. package/src/tui/modal-menu.ts +118 -0
  154. package/src/tui/performance-menu.ts +235 -0
  155. package/src/tui/play-chords.ts +3 -1
  156. package/src/tui/play-mode.ts +150 -6
  157. package/src/tui/play-session.ts +459 -43
  158. package/tui/app.ts +262 -13
  159. package/tui/arrange-strip.ts +174 -0
  160. package/tui/drawer.ts +478 -0
  161. package/tui/grammar.ts +63 -1
  162. package/tui/guide.ts +351 -0
  163. package/tui/highway.ts +87 -4
  164. package/tui/hits.ts +68 -0
  165. package/tui/input.ts +7 -0
  166. 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
@@ -124,7 +124,10 @@ export const KEYS = {
124
124
  rows: [
125
125
  ["type + enter", "ask for a change in plain words, or a command"],
126
126
  ["ctrl-p", "play notes on the keyboard (chords: q)"],
127
- ["ctrl-k", "menu: sound, effects, rhythm, chords, mix"],
127
+ [
128
+ "ctrl-k",
129
+ "menu: sound, effects, rhythm, chords, performance, mix, master",
130
+ ],
128
131
  ["space", "play / pause (empty prompt)"],
129
132
  ["/help", "what dawg can do · /help <topic> for more"],
130
133
  ],
@@ -141,6 +144,15 @@ export const KEYS = {
141
144
  ["ctrl-c", "quit"],
142
145
  ],
143
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
+ },
144
156
  ],
145
157
  list: [{ title: "list", rows: LIST }],
146
158
  audition: [
@@ -185,10 +197,46 @@ export const KEYS = {
185
197
  ["enter", "type a value · pick from a list"],
186
198
  ["space", "toggle on/off (elsewhere: hear the track)"],
187
199
  ["x delete", "reset to default (deletes an automation point)"],
200
+ ["enter (number)", "open its fader drawer"],
201
+ ],
202
+ },
203
+ {
204
+ title: "mouse",
205
+ rows: [
206
+ ["click", "select a row · click again opens it"],
207
+ ["wheel", "move through the list"],
188
208
  ],
189
209
  },
190
210
  ...AUDITIONING,
191
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"],
227
+ ],
228
+ },
229
+ {
230
+ title: "mouse",
231
+ rows: [
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"],
237
+ ],
238
+ },
239
+ ],
192
240
  euclid: [
193
241
  {
194
242
  title: "rhythm editor",
@@ -215,6 +263,19 @@ export const KEYS = {
215
263
  ],
216
264
  },
217
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
+ ],
218
279
  log: [
219
280
  {
220
281
  title: "transcript",
@@ -238,6 +299,7 @@ export const KEYS = {
238
299
  ["space", "play / pause (with count-in)"],
239
300
  ["r · R", "record · record replacing"],
240
301
  ["m", "metronome click"],
302
+ ["i", "scale degrees ⇄ chromatic (home row in key)"],
241
303
  ["q", "chord mode: auto ⇄ manual"],
242
304
  ["/", "type a command, still in play mode"],
243
305
  ["ctrl-k", "menu"],