@hraness/dawg 0.6.1 → 0.8.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 (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
package/src/tui/euclid.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  * runs through the normal prompt path, so every change is one revision and
10
10
  * one undo step and the row's detail teaches the command.
11
11
  *
12
- * Keys: ↑↓ voice · ←→ (h l + -) nudge · tab / shift-tab (] [) parameter ·
12
+ * Keys: ↑↓ knob (● drum ▲ pulses ■ rotate ◆ velocity) · ←→ (h l + -) turn ·
13
+ * tab / shift-tab (] [) every parameter ·
13
14
  * digits type a value · enter add/edit · space loop · x off · f freeze ·
14
15
  * esc back.
15
16
  *
@@ -18,7 +19,18 @@
18
19
  * `a` flips A/B, `c` solo ↔ in context, Enter keeps the staged changes as
19
20
  * one undo step and Esc reverts them.
20
21
  */
21
- import { HINTS } from "../../tui/grammar.ts";
22
+ import {
23
+ HINTS,
24
+ KEY_BACKSPACE,
25
+ KEY_BACKTAB,
26
+ KEY_DOWN,
27
+ KEY_ENTER,
28
+ KEY_LEFT,
29
+ KEY_RESET,
30
+ KEY_RIGHT,
31
+ KEY_TAB,
32
+ KEY_UP,
33
+ } from "../../tui/grammar.ts";
22
34
  import { DRUM_VOICES, isDrumInstrument } from "../../core/drums.ts";
23
35
  import {
24
36
  DIVISIONS,
@@ -37,6 +49,8 @@ import {
37
49
  import type { PickerItem } from "../../tui/app.ts";
38
50
  import { auditionKey, type AuditionKey } from "./audition.ts";
39
51
  import type { MenuAudition } from "./menu.ts";
52
+ import { KNOB_MAPS } from "./knob-map.ts";
53
+ import { knobGlyph, KNOBS, nextKnob, type KnobIndex } from "../../tui/knobs.ts";
40
54
 
41
55
  export type EuclidContext = Readonly<{
42
56
  /** The score shown: the staged one while auditioning. */
@@ -284,20 +298,38 @@ export function ringText(row: RhythmRow | undefined): string {
284
298
  .join("");
285
299
  }
286
300
 
287
- const KEY_UP = new Set(["\u001b[A", "\u001bOA", "k"]);
288
- const KEY_DOWN = new Set(["\u001b[B", "\u001bOB", "j"]);
289
- const KEY_LEFT = new Set(["\u001b[D", "\u001bOD", "h", "-", "_"]);
290
- const KEY_RIGHT = new Set(["\u001b[C", "\u001bOC", "l", "+", "="]);
291
- const KEY_NEXT_PARAM = new Set(["\t", "]"]);
292
- const KEY_PREV_PARAM = new Set(["\u001b[Z", "["]);
293
- const KEY_ENTER = new Set(["\r", "\n"]);
294
- const KEY_BACKSPACE = new Set(["\u007f", "\b"]);
295
- const KEY_OFF = new Set(["x", "\u001b[3~"]);
301
+ /**
302
+ * The four knobs (KNOB_MAPS.euclid): blue `drum` picks the row, the others
303
+ * name EUCLID_PARAMS fields.
304
+ */
305
+ const EUCLID_KNOBS = KNOB_MAPS.euclid!;
306
+
307
+ /** After the strip, the knob keys, then the way out; fitHint drops the knob keys first. */
308
+ const KNOB_KEYS = "↑↓ knob · ←→ turn · esc back · ? keys";
309
+
310
+ /** The knob that turns `param`, if one does. */
311
+ function knobOfParam(param: number): KnobIndex | undefined {
312
+ const at = EUCLID_KNOBS.indexOf(EUCLID_PARAMS[param]!.field);
313
+ return at < 0 ? undefined : (at as KnobIndex);
314
+ }
315
+
316
+ /** EUCLID_PARAMS index a knob turns; undefined for the drum knob. */
317
+ function paramOfKnob(knob: KnobIndex): number | undefined {
318
+ const field = EUCLID_KNOBS[knob];
319
+ const at = EUCLID_PARAMS.findIndex((param) => param.field === field);
320
+ return at < 0 ? undefined : at;
321
+ }
322
+
323
+ /** Tab ] next field, shift-tab [ previous (the brackets are the old keys). */
324
+ const isNextField = (value: string) => KEY_TAB.has(value) || value === "]";
325
+ const isPrevField = (value: string) => KEY_BACKTAB.has(value) || value === "[";
296
326
 
297
327
  export class EuclidEditor {
298
328
  private visible = false;
299
329
  private lane = 0;
300
330
  private param = 0;
331
+ /** True while the blue knob (which drum) has focus, not a field. */
332
+ private onDrum = true;
301
333
  private entry: string | undefined;
302
334
  /** Where Esc returns to (`menu` when opened from `/menu`). */
303
335
  private origin: string | undefined;
@@ -322,6 +354,10 @@ export class EuclidEditor {
322
354
  this.origin = origin;
323
355
  const lanes = this.lanes(context);
324
356
  const at = voice ? lanes.findIndex((lane) => lane.voice === voice) : -1;
357
+ // Opened on a named drum, the green knob (pulses) has focus; otherwise
358
+ // the blue one, to pick the drum.
359
+ this.onDrum = at < 0;
360
+ if (!this.onDrum) this.param = paramOfKnob(1) ?? 0;
325
361
  if (at >= 0) this.lane = at;
326
362
  else {
327
363
  // Land on the first voice that already has a row.
@@ -347,9 +383,14 @@ export class EuclidEditor {
347
383
  return lanes[Math.min(this.lane, lanes.length - 1)]?.voice;
348
384
  }
349
385
 
350
- /** Parameter under the cursor. */
386
+ /** Parameter under the cursor (`drum` on the blue knob). */
351
387
  get selectedParam(): string {
352
- return EUCLID_PARAMS[this.param]!.field;
388
+ return this.onDrum ? "drum" : EUCLID_PARAMS[this.param]!.field;
389
+ }
390
+
391
+ /** The knob with focus; undefined on a field no knob turns. */
392
+ get selectedKnob(): KnobIndex | undefined {
393
+ return this.onDrum ? 0 : knobOfParam(this.param);
353
394
  }
354
395
 
355
396
  key(value: string, context: EuclidContext): EuclidResult {
@@ -371,7 +412,8 @@ export class EuclidEditor {
371
412
  if (KEY_ENTER.has(value)) {
372
413
  const text = this.entry.trim();
373
414
  this.entry = undefined;
374
- if (!lane || !/^-?[0-9./tT]+$/.test(text)) return { type: "handled" };
415
+ if (this.onDrum || !lane || !/^-?[0-9./tT]+$/.test(text))
416
+ return { type: "handled" };
375
417
  return {
376
418
  type: "run",
377
419
  command: `euclid ${lane.voice} ${param.field} ${text}`,
@@ -387,21 +429,45 @@ export class EuclidEditor {
387
429
  return audition?.dirty ? { type: "revert" } : { type: "close" };
388
430
  const loopKey = audition ? auditionKey(value) : undefined;
389
431
  if (loopKey) return { type: "loop", key: loopKey };
432
+ // ↑↓ pick a knob (design §8.7); from a field no knob turns, ↑ lands
433
+ // on orange and ↓ on blue.
390
434
  if (KEY_UP.has(value) || KEY_DOWN.has(value)) {
391
- if (lanes.length)
392
- this.lane =
393
- (this.lane + (KEY_UP.has(value) ? -1 : 1) + lanes.length) %
394
- lanes.length;
435
+ const up = KEY_UP.has(value);
436
+ const from = this.selectedKnob;
437
+ const knob =
438
+ from === undefined
439
+ ? up
440
+ ? 3
441
+ : 0
442
+ : nextKnob(from, up ? -1 : 1, (index) =>
443
+ index === 0 ? true : paramOfKnob(index) !== undefined,
444
+ );
445
+ this.onDrum = knob === 0;
446
+ if (!this.onDrum) this.param = paramOfKnob(knob) ?? this.param;
395
447
  return { type: "handled" };
396
448
  }
397
- if (KEY_NEXT_PARAM.has(value) || KEY_PREV_PARAM.has(value)) {
398
- const direction = KEY_NEXT_PARAM.has(value) ? 1 : -1;
399
- this.param =
400
- (this.param + direction + EUCLID_PARAMS.length) % EUCLID_PARAMS.length;
449
+ // Tab walks every field (the full list behind the knobs).
450
+ if (isNextField(value) || isPrevField(value)) {
451
+ const direction = isNextField(value) ? 1 : -1;
452
+ if (this.onDrum) {
453
+ this.onDrum = false;
454
+ this.param = direction > 0 ? 0 : EUCLID_PARAMS.length - 1;
455
+ } else
456
+ this.param =
457
+ (this.param + direction + EUCLID_PARAMS.length) %
458
+ EUCLID_PARAMS.length;
401
459
  return { type: "handled" };
402
460
  }
403
461
  if (!lane) return { type: "handled" };
404
462
  if (value === " ") return { type: "audition", voice: lane.voice };
463
+ if (this.onDrum && (KEY_LEFT.has(value) || KEY_RIGHT.has(value))) {
464
+ // The blue knob turns which drum row has focus.
465
+ if (lanes.length)
466
+ this.lane =
467
+ (this.lane + (KEY_RIGHT.has(value) ? 1 : -1) + lanes.length) %
468
+ lanes.length;
469
+ return { type: "handled" };
470
+ }
405
471
  if (KEY_LEFT.has(value) || KEY_RIGHT.has(value)) {
406
472
  const direction = KEY_RIGHT.has(value) ? 1 : -1;
407
473
  const row: RhythmRow = lane.row ?? { voice: lane.voice };
@@ -422,7 +488,7 @@ export class EuclidEditor {
422
488
  audition: lane.voice,
423
489
  };
424
490
  }
425
- if (/^[0-9]$/.test(value)) {
491
+ if (/^[0-9]$/.test(value) && !this.onDrum) {
426
492
  this.entry = value;
427
493
  return { type: "handled" };
428
494
  }
@@ -434,10 +500,10 @@ export class EuclidEditor {
434
500
  command: `euclid ${lane.voice} ${RHYTHM_DEFAULTS.pulses} ${RHYTHM_DEFAULTS.steps}`,
435
501
  audition: lane.voice,
436
502
  };
437
- this.entry = "";
503
+ if (!this.onDrum) this.entry = "";
438
504
  return { type: "handled" };
439
505
  }
440
- if (KEY_OFF.has(value))
506
+ if (KEY_RESET.has(value))
441
507
  return lane.row
442
508
  ? { type: "run", command: `euclid ${lane.voice} off` }
443
509
  : { type: "handled" };
@@ -449,6 +515,23 @@ export class EuclidEditor {
449
515
  return { type: "handled" };
450
516
  }
451
517
 
518
+ /**
519
+ * The knob strip for the hint row: `●›kick ▲ pulses 4 ■ rotate 0 ◆ velocity
520
+ * 0.8`, the focused knob marked `›`. Glyphs carry which knob is which.
521
+ */
522
+ private strip(drum: string | undefined, row: RhythmRow | undefined): string {
523
+ const focused = this.selectedKnob;
524
+ return KNOBS.map((knob) => {
525
+ const mark = knob.index === focused ? "›" : " ";
526
+ if (knob.index === 0) return `${knob.glyph}${mark}${drum ?? "·"}`;
527
+ const at = paramOfKnob(knob.index);
528
+ if (at === undefined) return `${knob.glyph} ·`;
529
+ const param = EUCLID_PARAMS[at]!;
530
+ const shown = row ? formatValue(param.value(row)) : "—";
531
+ return `${knob.glyph}${mark}${param.label} ${shown}`;
532
+ }).join(" ");
533
+ }
534
+
452
535
  view(context: EuclidContext): EuclidView {
453
536
  const lanes = this.lanes(context);
454
537
  const index = Math.max(0, Math.min(this.lane, lanes.length - 1));
@@ -484,7 +567,10 @@ export class EuclidEditor {
484
567
  const voice = lanes[index]?.voice ?? "";
485
568
  const row = lanes[index]?.row;
486
569
  const value = row ? formatValue(param.value(row)) : "—";
487
- let title = `rhythm › ${context.trackId}${voice ? ` · ${voice}` : ""} · ${param.label} ${value}`;
570
+ const focus = this.onDrum
571
+ ? `${knobGlyph(0, true)} drum`
572
+ : `${param.label} ${value}`;
573
+ let title = `rhythm › ${context.trackId}${voice ? ` · ${voice}` : ""} · ${focus}`;
488
574
  if (this.entry !== undefined) title += ` · ${param.label}: ${this.entry}▏`;
489
575
  if (audition?.dirty) title = `● ${title}`;
490
576
  if (audition?.status && this.entry === undefined)
@@ -494,7 +580,7 @@ export class EuclidEditor {
494
580
  ? HINTS.typing
495
581
  : audition && (audition.looping || audition.dirty)
496
582
  ? audition.hint
497
- : HINTS.euclid;
583
+ : `${this.strip(lanes[index]?.label.trim(), row)} · ${KNOB_KEYS}`;
498
584
  return {
499
585
  title,
500
586
  items: items.length