@hraness/dawg 0.6.1 → 0.7.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 (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  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 +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Ctrl-k menu rows for modal percussion (0.6): the "Mallets and bells"
3
- * group under Sound › browse sounds, and the MODAL_PARAMS rows under
2
+ * Ctrl-k menu rows for modal percussion (0.6): the "mallets"
3
+ * group under Sound › instruments, and the MODAL_PARAMS rows under
4
4
  * Sound › Parameters on a modal track. Every row runs a `modal …` command.
5
5
  */
6
6
  import type { EnumParam, NumberParam } from "../../core/params.ts";
@@ -18,12 +18,12 @@ import {
18
18
  import type { Track } from "../../core/score.ts";
19
19
  import { num, specStep, type MenuNode } from "./menu.ts";
20
20
 
21
- /** Sound › browse sounds › Mallets and bells: one row per preset. */
21
+ /** Sound › instruments › mallets: one row per preset. */
22
22
  export function malletsMenu(): MenuNode {
23
23
  return {
24
24
  kind: "menu",
25
25
  id: "mallets",
26
- label: "Mallets and bells",
26
+ label: "mallets",
27
27
  detail: `${MODAL_PRESET_NAMES.length} modal presets · marimba, vibes, gong …`,
28
28
  help: "struck bars, bells and bowls on the modal resonator engine",
29
29
  build: () => [
@@ -47,14 +47,14 @@ function presetRow(name: ModalPresetName): MenuNode {
47
47
  }
48
48
 
49
49
  /**
50
- * Mallets and bells › Gamelan (f061-gamelan-winds): the Javanese and
50
+ * mallets › gamelan (f061-gamelan-winds): the Javanese and
51
51
  * Balinese bronzes. `modal gamelan` points here.
52
52
  */
53
53
  export function gamelanMenu(): MenuNode {
54
54
  return {
55
55
  kind: "menu",
56
56
  id: "gamelan",
57
- label: "Gamelan",
57
+ label: "gamelan",
58
58
  detail: `${GAMELAN_PRESETS.length} bronzes · saron, gangsa (ombak), gong …`,
59
59
  help: "Javanese and Balinese gamelan; pair two gangsa tracks with modal pair, tune with tuning slendro or pelog",
60
60
  build: () => GAMELAN_PRESETS.map(presetRow),
@@ -3,6 +3,7 @@
3
3
  * runs the same prompt command a user could type (src/commands/expression.ts),
4
4
  * so the menu, the prompt and the agent share one grammar.
5
5
  */
6
+ import { singVowelNodes } from "./sing-menu.ts";
6
7
  import {
7
8
  ARTICULATIONS,
8
9
  DEFAULT_FIXED_VELOCITY,
@@ -187,7 +188,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
187
188
  : "pedal",
188
189
  help: "bars replaces the lane with a re-pedal on each downbeat; in play mode press Tab (latch) or hold Shift while recording",
189
190
  },
190
- // keys-electric (0.6.1): the piano's other two pedals, on modelled piano
191
+ // keys-electric (0.6.1): the piano's other two pedals, on modeled piano
191
192
  // tracks (or wherever a lane is already set).
192
193
  ...pianoPedalNodes(track),
193
194
  {
@@ -233,7 +234,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
233
234
  },
234
235
  {
235
236
  kind: "number",
236
- label: "humanize velocity (%)",
237
+ label: "humanize vel (%)",
237
238
  value: humanize ? velocity : undefined,
238
239
  min: 0,
239
240
  max: EXPRESSION_LIMITS.maxHumanizePercent,
@@ -273,5 +274,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
273
274
  command: "humanize reseed",
274
275
  help: "the same amounts with a different seeded performance",
275
276
  });
277
+ // 0.7 sing: Vowels on a sing track.
278
+ nodes.push(...singVowelNodes(context));
276
279
  return nodes;
277
280
  }
@@ -605,9 +605,11 @@ export function applyChordsCommand(
605
605
  return done(`chords preset ${value}`);
606
606
  case "style":
607
607
  if (!(PROGRESSION_STYLES as readonly string[]).includes(value ?? ""))
608
- return bad(`style takes ${PROGRESSION_STYLES.join("|")}`);
608
+ return bad(
609
+ `idiom takes ${PROGRESSION_STYLES.join("|")} · chords idiom jazz`,
610
+ );
609
611
  settings.style = value as ProgressionStyle;
610
- return done(`chords style ${value}`);
612
+ return done(`chords idiom ${value}`);
611
613
  default:
612
614
  return bad(
613
615
  "/chords auto|manual|off · voicing · spread · bass · perform · pattern · strokes · speed · rate · octaves · sevenths · preset · style",
@@ -32,6 +32,7 @@ import {
32
32
  type Track,
33
33
  } from "../../core/score.ts";
34
34
  import type { TuningTable } from "../../core/tuning.ts";
35
+ import { DRUM_VOICES, isDrumInstrument } from "../../core/drums.ts";
35
36
  import { pitchName } from "../../tui/highway.ts";
36
37
 
37
38
  /** Semitone offset from the base C for each note key. */
@@ -112,10 +113,21 @@ export function defaultBaseFor(instrument: string | undefined): number {
112
113
  */
113
114
  export type PlayLayout = Readonly<{
114
115
  base: number;
115
- /** Pitch → label (voice names on one-shot samplers). */
116
+ /** Pitch → label (voice names on one-shot samplers, drums on kits). */
116
117
  labels: ReadonlyMap<number, string>;
118
+ /** A drum kit: keys name drums and the chord row stays hidden. */
119
+ drums?: boolean | undefined;
117
120
  }>;
118
121
 
122
+ /**
123
+ * A kit's keys on the GM map from C2 (36), the same keys the agent's
124
+ * show-me gestures press: A kick, W rim, S snare, E clap, T hat, H tom,
125
+ * U open hat. Other keys still sound their nearest drum, unlabeled.
126
+ */
127
+ export function drumKeyLabels(): ReadonlyMap<number, string> {
128
+ return new Map(DRUM_VOICES.map((info) => [info.pitch, info.label]));
129
+ }
130
+
119
131
  export function playLayoutFor(track: Track | undefined): PlayLayout {
120
132
  const labels = new Map<number, string>();
121
133
  if (track && isSamplerInstrument(track.instrument) && track.sampler) {
@@ -130,6 +142,8 @@ export function playLayoutFor(track: Track | undefined): PlayLayout {
130
142
  const lowest = roots.length > 0 ? Math.min(...roots) : 60;
131
143
  return { base: clampBase(Math.floor(lowest / 12) * 12), labels };
132
144
  }
145
+ if (track && isDrumInstrument(track.instrument))
146
+ return { base: 36, labels: drumKeyLabels(), drums: true };
133
147
  // A plain instrument takes its range from the track's name (`bass`, `lead`).
134
148
  const byInstrument = defaultBaseFor(track?.instrument);
135
149
  const base =
@@ -50,6 +50,7 @@ import {
50
50
  import {
51
51
  LiveFullRenderer,
52
52
  LiveSynth,
53
+ warmLive,
53
54
  MAX_LIVE_NOTE_SECONDS,
54
55
  type LiveNotePcm,
55
56
  } from "../audio/live.ts";
@@ -57,6 +58,7 @@ import type { SampleBank } from "../audio/samples.ts";
57
58
  import { fitting, onFitReady } from "../audio/fit.ts";
58
59
  import {
59
60
  DEFAULT_STRUM,
61
+ degreeOf,
60
62
  perform,
61
63
  type PerformMode,
62
64
  type PerformOptions,
@@ -73,6 +75,7 @@ import {
73
75
  } from "./play-chords.ts";
74
76
  import type { PlayHeaderView, PlayStripKey } from "../../tui/play-strip.ts";
75
77
  import { resolveTuning, type Tuning } from "../../core/tuning.ts";
78
+ import { singThroat } from "../../core/sing.ts";
76
79
  import {
77
80
  NOTE_KEYS,
78
81
  PlayKeyboard,
@@ -91,6 +94,10 @@ export interface LiveEngine {
91
94
  readonly sampleRate: number;
92
95
  readonly canMonitor: boolean;
93
96
  readonly leadMs: number;
97
+ /** The lead play mode pins (15 ms on the native sink, else 60 ms). */
98
+ readonly playLeadMs?: number;
99
+ /** How play mode sounds (`native sink`, `ffplay · no native sink`). */
100
+ readonly audioNote?: string;
94
101
  monitor(on: boolean): Promise<void>;
95
102
  setLeadMs(ms: number | undefined): void;
96
103
  noteOn(id: number, note: LiveNotePcm): number;
@@ -124,6 +131,8 @@ export interface PlayHost {
124
131
  startTransport(beat: number): Promise<void>;
125
132
  stopTransport(): Promise<void>;
126
133
  card(text: string, tone: "info" | "success" | "warning" | "error"): void;
134
+ /** A played key that is not recorded: the screen glows its lane. */
135
+ ghost?(pitch: number): void;
127
136
  newNoteId(): string;
128
137
  }
129
138
 
@@ -240,6 +249,8 @@ export class PlaySession {
240
249
  private synth: LiveSynth | undefined;
241
250
  /** Rig voices awaiting their full render: id → released since. */
242
251
  private readonly windows = new Map<number, boolean>();
252
+ /** f07-sing: the sounding key on a throat track (one drone, mono). */
253
+ private throatId: number | undefined;
243
254
  private readonly fullRenderer = new LiveFullRenderer();
244
255
  private readonly pending = new Map<number, Pending>();
245
256
  /** Sustain changes while recording (Tab latch or Shift), as pedal events. */
@@ -289,7 +300,8 @@ export class PlaySession {
289
300
  settings.mode =
290
301
  chordCapable(track) &&
291
302
  twelveTet(this.host.score(), track) &&
292
- (track?.glide === undefined || track.glide.mode === "poly")
303
+ (track?.glide === undefined || track.glide.mode === "poly") &&
304
+ !singThroat(track)
293
305
  ? "auto"
294
306
  : "manual";
295
307
  this.chords = new ChordPad(settings, () => this.host.score().key);
@@ -331,8 +343,12 @@ export class PlaySession {
331
343
  this.active = true;
332
344
  const engine = this.host.engine();
333
345
  if (engine?.canMonitor) {
334
- engine.setLeadMs(PLAY_LEAD_MS);
346
+ engine.setLeadMs(engine.playLeadMs ?? PLAY_LEAD_MS);
347
+ // The voice's tables and loops are built now, not on the first key.
348
+ warmLive(this.host.score(), this.trackId, engine.sampleRate);
335
349
  await engine.monitor(true);
350
+ if (engine.audioNote)
351
+ this.status = `${engine.audioNote} · ${Math.round(engine.leadMs)} ms lead`;
336
352
  } else this.status = "no audio · keys still record";
337
353
  this.applyClick();
338
354
  }
@@ -483,6 +499,8 @@ export class PlaySession {
483
499
  return { type: "handled" };
484
500
  case "note": {
485
501
  this.release(action.released, now);
502
+ // A key that is not recording glows its lane for a moment.
503
+ if (!this.recording) this.host.ghost?.(action.note.pitch);
486
504
  const chord = this.chords.chordFor(action.note.pitch);
487
505
  if (chord) {
488
506
  const played = this.chords.voice(chord, action.note.pitch);
@@ -501,6 +519,12 @@ export class PlaySession {
501
519
  );
502
520
  return { type: "handled" };
503
521
  }
522
+ // f07-sing: a throat track is one drone; a new key releases the last.
523
+ if (singThroat(this.trackData())) {
524
+ if (this.throatId !== undefined && this.throatId !== action.note.id)
525
+ this.release([this.throatId], now);
526
+ this.throatId = action.note.id;
527
+ }
504
528
  this.sound(action.note.id, action.note);
505
529
  this.record(action.note);
506
530
  return { type: "handled" };
@@ -1143,7 +1167,7 @@ export class PlaySession {
1143
1167
  };
1144
1168
  }
1145
1169
  return {
1146
- range: this.keyboard.range,
1170
+ range: this.layout.drums ? "drums" : this.keyboard.range,
1147
1171
  velocity: this.keyboard.velocity,
1148
1172
  armed: this.armed,
1149
1173
  recording: this.recording,
@@ -1153,10 +1177,14 @@ export class PlaySession {
1153
1177
  countIn,
1154
1178
  beat,
1155
1179
  grid: `grid ${this.grid}`,
1156
- chords: this.chords.glance(songKey(this.host.score().key).set),
1180
+ // A kit plays drums, not harmony: no chord row and no key chip.
1181
+ chords: this.layout.drums
1182
+ ? undefined
1183
+ : this.chords.glance(songKey(this.host.score().key).set),
1157
1184
  status: this.status,
1158
1185
  keys: this.strip(now),
1159
- legend: this.chords.on ? this.chords.legend() : undefined,
1186
+ legend:
1187
+ this.chords.on && !this.layout.drums ? this.chords.legend() : undefined,
1160
1188
  };
1161
1189
  }
1162
1190
 
@@ -1178,8 +1206,20 @@ export class PlaySession {
1178
1206
  ).map((cell) => {
1179
1207
  const chord = this.keyboard.pitchFor(cell.key);
1180
1208
  const name =
1181
- chord === undefined ? undefined : this.chords.keyLabel(chord);
1182
- return name ? { ...cell, label: name } : this.noteCell(cell);
1209
+ chord === undefined || this.layout.drums
1210
+ ? undefined
1211
+ : this.chords.keyLabel(chord);
1212
+ // Chords in the key are the safe ones to reach for: the strip lights
1213
+ // them; a borrowed chord still plays but stays plain.
1214
+ return name && chord !== undefined
1215
+ ? {
1216
+ ...cell,
1217
+ label: name,
1218
+ chord: true,
1219
+ diatonic:
1220
+ degreeOf(this.chords.key, ((chord % 12) + 12) % 12) !== undefined,
1221
+ }
1222
+ : this.noteCell(cell);
1183
1223
  });
1184
1224
  }
1185
1225
 
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Ctrl-k menu rows for the sing engine (0.7): the Choir, Solo and Throat
3
+ * groups under Voice › voice presets, the SING_PARAMS rows under
4
+ * Sound › Parameters on a sing track (with a Throat sub-menu) and the
5
+ * Vowels row under Sound › Performance. Every row runs a `sing …` or
6
+ * `note vowel …` command, so it stages for the audition loop's A/B.
7
+ */
8
+ import type { EnumParam, NumberParam } from "../../core/params.ts";
9
+ import type { Note, Track } from "../../core/score.ts";
10
+ import {
11
+ DEFAULT_SING_PRESET,
12
+ resolveSing,
13
+ SING_GROUPS,
14
+ SING_PARAMS,
15
+ SING_PRESETS,
16
+ SING_PRESET_NAMES,
17
+ SING_THROAT_PARAMS,
18
+ SING_VOWELS,
19
+ singNoteName,
20
+ singDroneFollows,
21
+ singTrack,
22
+ type SingPreset,
23
+ } from "../../core/sing.ts";
24
+ import { num, specStep, type MenuContext, type MenuNode } from "./menu.ts";
25
+
26
+ function presetRow(name: SingPreset): MenuNode {
27
+ return {
28
+ kind: "action",
29
+ label: `${name.padEnd(9)} ${SING_PRESETS[name].doc}`,
30
+ command: `sing ${name}`,
31
+ help: SING_PRESETS[name].styles,
32
+ };
33
+ }
34
+
35
+ /** Voice › voice presets: choir, solo and throat (sing lane). */
36
+ export function singBrowseRows(): MenuNode[] {
37
+ return SING_GROUPS.map((group): MenuNode => ({
38
+ kind: "menu",
39
+ id: `voices:${group.label.toLowerCase()}`,
40
+ label: group.label.toLowerCase(),
41
+ detail: group.presets.slice(0, 4).join(", ") + " …",
42
+ help:
43
+ group.label === "Throat"
44
+ ? "Tuvan throat singing: a held drone, notes steer the overtone"
45
+ : `${group.label.toLowerCase()} on the built-in singing voice`,
46
+ build: () => group.presets.map(presetRow),
47
+ }));
48
+ }
49
+
50
+ /** Parameters shown first; the rest sit under "advanced". */
51
+ const SING_SIMPLE = Object.freeze([
52
+ "vowel",
53
+ "voice",
54
+ "voices",
55
+ "bright",
56
+ "breath",
57
+ "vibmod",
58
+ "formant",
59
+ "ring",
60
+ ] as const);
61
+
62
+ function numberRow(
63
+ name: string,
64
+ spec: NumberParam,
65
+ value: number,
66
+ fallback: number,
67
+ ): MenuNode {
68
+ return {
69
+ kind: "number",
70
+ label: name,
71
+ value,
72
+ min: spec.min,
73
+ max: spec.max,
74
+ step: specStep(spec),
75
+ format: (v) => (spec.unit ? `${num(v)} ${spec.unit}` : num(v)),
76
+ command: (v) => `sing ${name} ${num(v)}`,
77
+ reset: `sing ${name} off`,
78
+ help: `${spec.doc} · preset ${num(fallback)} · x resets`,
79
+ };
80
+ }
81
+
82
+ /** Every vowel and two-vowel morph, for the pickers. */
83
+ export function vowelOptions(): string[] {
84
+ const out: string[] = [...SING_VOWELS];
85
+ for (const from of SING_VOWELS)
86
+ for (const to of SING_VOWELS) if (from !== to) out.push(`${from}>${to}`);
87
+ return out;
88
+ }
89
+
90
+ /** Sound › Parameters rows on a sing track. */
91
+ export function singParameterNodes(track: Track, keyRoot?: number): MenuNode[] {
92
+ if (!singTrack(track)) return [];
93
+ const sing = track.sing!;
94
+ const follows = singDroneFollows(sing, keyRoot);
95
+ const resolved = resolveSing(sing, keyRoot) as unknown as Readonly<
96
+ Record<string, unknown>
97
+ >;
98
+ const fallback = SING_PRESETS[sing.preset ?? DEFAULT_SING_PRESET]
99
+ .settings as unknown as Readonly<Record<string, unknown>>;
100
+ const own = sing as Readonly<Record<string, unknown>>;
101
+ const row = (name: string): MenuNode | undefined => {
102
+ if (name === "vowel")
103
+ return {
104
+ kind: "choice",
105
+ label: "vowel",
106
+ value: String(resolved.vowel),
107
+ options: vowelOptions(),
108
+ command: (option) => `sing vowel ${option}`,
109
+ help: `${SING_PARAMS.vowel!.doc} · preset ${String(fallback.vowel)}`,
110
+ };
111
+ if (name === "drone") {
112
+ const drone = resolved.drone as number | undefined;
113
+ const spec = SING_PARAMS.drone as NumberParam;
114
+ return {
115
+ kind: "number",
116
+ label: "drone",
117
+ value: drone,
118
+ min: spec.min,
119
+ max: spec.max,
120
+ step: (v, direction) =>
121
+ Math.max(spec.min, Math.min(spec.max, Math.round(v) + direction)),
122
+ // The drone you hear: the preset's moved to the song key.
123
+ format: (v) =>
124
+ follows && v === drone ? `${singNoteName(v)} (key)` : singNoteName(v),
125
+ command: (v) => `sing drone ${singNoteName(v)}`,
126
+ off: "off (melody)",
127
+ start: 50,
128
+ reset: "sing drone off",
129
+ help: `${spec.doc} · x back to the preset`,
130
+ };
131
+ }
132
+ if (name === "harmonics") {
133
+ const [lo, hi] = resolved.harmonics as readonly [number, number];
134
+ const options = ["4-8", "4-12", "5-10", "6-12", "6-16", "8-16", "8-24"];
135
+ const value = `${lo}-${hi}`;
136
+ return {
137
+ kind: "choice",
138
+ label: "harmonics",
139
+ value,
140
+ options: options.includes(value) ? options : [value, ...options],
141
+ command: (option) => `sing harmonics ${option}`,
142
+ help: "the overtone range a throat track's notes pick from (khoomei 6-12, sygyt 8-16)",
143
+ };
144
+ }
145
+ const spec = SING_PARAMS[name]!;
146
+ if (spec.kind === "enum")
147
+ return {
148
+ kind: "choice",
149
+ label: name,
150
+ value: own[name] === undefined ? "preset" : String(own[name]),
151
+ options: ["preset", ...(spec as EnumParam).values],
152
+ command: (option) =>
153
+ option === "preset" ? `sing ${name} unset` : `sing ${name} ${option}`,
154
+ help: `${spec.doc} · preset ${String(fallback[name])}`,
155
+ };
156
+ if (spec.kind !== "number") return undefined;
157
+ return numberRow(
158
+ name,
159
+ spec,
160
+ Number(resolved[name]),
161
+ Number(fallback[name]),
162
+ );
163
+ };
164
+ const nodes: MenuNode[] = [
165
+ {
166
+ kind: "choice",
167
+ label: "preset",
168
+ value: sing.preset ?? DEFAULT_SING_PRESET,
169
+ options: SING_PRESET_NAMES,
170
+ command: (option) => `sing preset ${option}`,
171
+ help: "the voice: choir, solo or throat singing",
172
+ },
173
+ ];
174
+ for (const name of SING_SIMPLE) {
175
+ const node = row(name);
176
+ if (node) nodes.push(node);
177
+ }
178
+ const throat = SING_THROAT_PARAMS as readonly string[];
179
+ nodes.push({
180
+ kind: "menu",
181
+ id: "sing:throat",
182
+ label: "throat",
183
+ detail:
184
+ resolved.drone === undefined
185
+ ? "off · drone, overtone, harmonics, sub"
186
+ : `drone ${singNoteName(resolved.drone as number)} · ${throat.slice(1).join(", ")}`,
187
+ help: "throat singing: a drone, notes pick its overtones; sub for kargyraa",
188
+ build: () => throat.flatMap((name) => row(name) ?? []),
189
+ });
190
+ const rest = Object.keys(SING_PARAMS).filter(
191
+ (name) =>
192
+ !(SING_SIMPLE as readonly string[]).includes(name) &&
193
+ !throat.includes(name),
194
+ );
195
+ nodes.push({
196
+ kind: "menu",
197
+ id: "sing:advanced",
198
+ label: "advanced",
199
+ detail: rest.join(", "),
200
+ help: "every sing parameter",
201
+ build: () => rest.flatMap((name) => row(name) ?? []),
202
+ });
203
+ if (Object.keys(sing).some((key) => key !== "preset"))
204
+ nodes.push({
205
+ kind: "action",
206
+ label: "reset to preset",
207
+ command: "sing reset",
208
+ help: "clear overrides, keep the preset",
209
+ });
210
+ return nodes;
211
+ }
212
+
213
+ function sharedVowel(notes: readonly Note[]): string {
214
+ const values = new Set(notes.map((note) => note.vowel ?? "track"));
215
+ if (values.size === 0) return "track";
216
+ return values.size === 1 ? [...values][0]! : "mixed";
217
+ }
218
+
219
+ /**
220
+ * Sound › Performance › Vowels on a sing track: left/right steps the notes'
221
+ * vowel through a e i o u, enter opens the morph picker, and a pattern
222
+ * row cycles vowels over the notes in order.
223
+ */
224
+ export function singVowelNodes(context: MenuContext): MenuNode[] {
225
+ const track = context.score.tracks.find(
226
+ (item) => item.id === context.trackId,
227
+ );
228
+ if (!singTrack(track)) return [];
229
+ const notes = context.score.notes.filter(
230
+ (note) => note.trackId === track!.id,
231
+ );
232
+ const current = sharedVowel(notes);
233
+ return [
234
+ {
235
+ kind: "menu",
236
+ id: "sing:vowels",
237
+ label: "vowels",
238
+ detail: `${current} · ${notes.length} notes`,
239
+ help: "the sung vowel of every note on the track (a note's vowel wins over the track's)",
240
+ build: (inner) => {
241
+ const now = inner.score.notes.filter(
242
+ (note) => note.trackId === context.trackId,
243
+ );
244
+ return [
245
+ {
246
+ kind: "choice",
247
+ label: "vowel",
248
+ value: sharedVowel(now),
249
+ options: ["track", ...SING_VOWELS],
250
+ command: (option) =>
251
+ `note vowel ${option === "track" ? "off" : option}`,
252
+ help: "left/right steps a e i o u; track uses the track's vowel",
253
+ },
254
+ {
255
+ kind: "choice",
256
+ label: "morph",
257
+ value: sharedVowel(now),
258
+ options: [
259
+ "track",
260
+ ...vowelOptions().filter((v) => v.includes(">")),
261
+ ],
262
+ command: (option) =>
263
+ `note vowel ${option === "track" ? "off" : option}`,
264
+ help: "each note travels from the first vowel to the second",
265
+ },
266
+ {
267
+ kind: "choice",
268
+ label: "pattern",
269
+ value: "",
270
+ options: ["a e i o u", "a o", "o u", "a e i", "u o a"],
271
+ command: (option) => `sing vowels ${option}`,
272
+ help: "vowels over the notes in time order, cycled",
273
+ },
274
+ ];
275
+ },
276
+ },
277
+ ];
278
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * ctrl-k › Style: the style taxonomy as a menu tree (families, then each
3
+ * branch down to its leaves). Every row is a `style` command, so the menu
4
+ * and the prompt share one grammar; typing filters the rows of a level.
5
+ */
6
+ import { hasCard, STYLE_TREE } from "../../core/styles/index.ts";
7
+ import { STYLE_FAMILIES } from "../../core/styles/taxonomy.ts";
8
+ import type { StyleId } from "../../core/styles/schema.ts";
9
+ import type { MenuContext, MenuNode } from "./menu.ts";
10
+
11
+ const BAR_CHOICES = [4, 8, 16, 32] as const;
12
+
13
+ /** The Style row's detail: the song's style, or a hint. */
14
+ export function styleMenuDetail(context: MenuContext): string {
15
+ const style = context.score.style;
16
+ if (!style) return "a whole song from a style";
17
+ const name = style.blend ? `${style.id} + ${style.blend.id}` : style.id;
18
+ return `${name} · seed ${style.seed}`;
19
+ }
20
+
21
+ function styleActions(id: StyleId): MenuNode[] {
22
+ const node = STYLE_TREE.get(id)!;
23
+ return [
24
+ ...BAR_CHOICES.map((bars): MenuNode => ({
25
+ kind: "action",
26
+ label: `make ${bars} bars`,
27
+ command: `/style ${id} ${bars}`,
28
+ help: `replace the song with ${bars} bars of ${node.title} (undo restores it)`,
29
+ })),
30
+ {
31
+ kind: "action",
32
+ label: "about",
33
+ command: `/style info ${id}`,
34
+ help: "meter, tempo, groove, tuning, harmony and roles",
35
+ },
36
+ ];
37
+ }
38
+
39
+ function styleNode(id: StyleId): MenuNode {
40
+ const node = STYLE_TREE.get(id)!;
41
+ const card = hasCard(id) ? "" : " · inherits";
42
+ return {
43
+ kind: "menu",
44
+ id: `style:${id}`,
45
+ label: node.title,
46
+ detail: node.leaf
47
+ ? `${id}${card}`
48
+ : `${node.children.length} styles${card}`,
49
+ help: node.region.length ? `${id} · ${node.region.join(", ")}` : id,
50
+ build: () => [
51
+ ...styleActions(id),
52
+ ...node.children.map((child) => styleNode(child)),
53
+ ],
54
+ };
55
+ }
56
+
57
+ /** Style: again, find, then the families. */
58
+ export function styleMenuNodes(context: MenuContext): MenuNode[] {
59
+ const current = context.score.style;
60
+ return [
61
+ ...(current
62
+ ? [
63
+ {
64
+ kind: "action",
65
+ label: "again",
66
+ command: "/style again",
67
+ help: `${current.id} with the next seed`,
68
+ } satisfies MenuNode,
69
+ ]
70
+ : []),
71
+ {
72
+ kind: "entry",
73
+ label: "find",
74
+ value: "",
75
+ placeholder: "style name, alias or region",
76
+ command: (text) =>
77
+ text.trim() ? `/style search ${text.trim()}` : undefined,
78
+ example: "/style search samba",
79
+ help: "search every style by name, alias or region",
80
+ },
81
+ {
82
+ kind: "entry",
83
+ label: "blend",
84
+ value: "",
85
+ placeholder: "a b [weight]",
86
+ command: (text) => {
87
+ const words = text.trim().split(/\s+/).filter(Boolean);
88
+ return words.length >= 2 && words.length <= 3
89
+ ? `/style blend ${words.join(" ")}`
90
+ : undefined;
91
+ },
92
+ example: "/style blend bebop bossa-nova 0.3",
93
+ help: "mix two styles; weight 0 is the first, 1 the second",
94
+ },
95
+ ...STYLE_FAMILIES.map((family): MenuNode => ({
96
+ kind: "menu",
97
+ id: `family:${family.key}`,
98
+ label: family.title,
99
+ detail: family.roots.join(" · "),
100
+ help: `${family.key} family`,
101
+ build: () => family.roots.map((root) => styleNode(root)),
102
+ })),
103
+ ];
104
+ }