@hraness/dawg 0.6.0 → 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 (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  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/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  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 +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -1,10 +1,11 @@
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";
7
7
  import {
8
+ GAMELAN_PRESETS,
8
9
  MODAL_INSTRUMENT,
9
10
  MODAL_MALLET_NAMES,
10
11
  MODAL_PARAMS,
@@ -12,25 +13,51 @@ import {
12
13
  MODAL_PRESET_NAMES,
13
14
  MODAL_SIMPLE,
14
15
  modalSettings,
16
+ type ModalPresetName,
15
17
  } from "../../core/resonators.ts";
16
18
  import type { Track } from "../../core/score.ts";
17
19
  import { num, specStep, type MenuNode } from "./menu.ts";
18
20
 
19
- /** Sound › browse sounds › Mallets and bells: one row per preset. */
21
+ /** Sound › instruments › mallets: one row per preset. */
20
22
  export function malletsMenu(): MenuNode {
21
23
  return {
22
24
  kind: "menu",
23
25
  id: "mallets",
24
- label: "Mallets and bells",
26
+ label: "mallets",
25
27
  detail: `${MODAL_PRESET_NAMES.length} modal presets · marimba, vibes, gong …`,
26
28
  help: "struck bars, bells and bowls on the modal resonator engine",
27
- build: () =>
28
- MODAL_PRESET_NAMES.map((name): MenuNode => ({
29
- kind: "action",
30
- label: `${name} ${MODAL_PRESETS[name].doc}`,
31
- command: `modal ${name}`,
32
- help: MODAL_PRESETS[name].styles,
33
- })),
29
+ build: () => [
30
+ // The core presets keep their rows (gong too); the gamelan bronzes
31
+ // added in 0.6.1 sit in their own sub-group.
32
+ ...MODAL_PRESET_NAMES.filter(
33
+ (name) => name === "gong" || !GAMELAN_PRESETS.includes(name),
34
+ ).map(presetRow),
35
+ gamelanMenu(),
36
+ ],
37
+ };
38
+ }
39
+
40
+ function presetRow(name: ModalPresetName): MenuNode {
41
+ return {
42
+ kind: "action",
43
+ label: `${name} ${MODAL_PRESETS[name].doc}`,
44
+ command: `modal ${name}`,
45
+ help: MODAL_PRESETS[name].styles,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * mallets › gamelan (f061-gamelan-winds): the Javanese and
51
+ * Balinese bronzes. `modal gamelan` points here.
52
+ */
53
+ export function gamelanMenu(): MenuNode {
54
+ return {
55
+ kind: "menu",
56
+ id: "gamelan",
57
+ label: "gamelan",
58
+ detail: `${GAMELAN_PRESETS.length} bronzes · saron, gangsa (ombak), gong …`,
59
+ help: "Javanese and Balinese gamelan; pair two gangsa tracks with modal pair, tune with tuning slendro or pelog",
60
+ build: () => GAMELAN_PRESETS.map(presetRow),
34
61
  };
35
62
  }
36
63
 
@@ -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,
@@ -12,6 +13,7 @@ import {
12
13
  VELOCITY_CURVES,
13
14
  } from "../../core/expression.ts";
14
15
  import type { Note, Track } from "../../core/score.ts";
16
+ import { isPianoFamily } from "../../core/keys.ts";
15
17
  import {
16
18
  BEND_SHAPES,
17
19
  DEFAULT_HUMANIZE,
@@ -50,12 +52,50 @@ export function performanceDetail(track: Track | undefined): string {
50
52
  const parts = [
51
53
  track.glide ? `glide ${track.glide.mode}` : "",
52
54
  track.pedal?.length ? "pedal" : "",
55
+ track.softPedal?.length ? "soft" : "",
56
+ track.sostenuto?.length ? "sost" : "",
53
57
  track.velocityCurve ? `vel ${track.velocityCurve.curve}` : "",
54
58
  track.humanize ? "humanized" : "",
55
59
  ].filter(Boolean);
56
60
  return parts.length ? parts.join(" · ") : "as written";
57
61
  }
58
62
 
63
+ /** Soft pedal and Sostenuto rows: off, the current lane, or the whole loop. */
64
+ function pianoPedalNodes(track: Track): MenuNode[] {
65
+ const rows = [
66
+ {
67
+ lane: "soft",
68
+ label: "soft pedal",
69
+ events: track.softPedal,
70
+ help: "una corda: the hammers shift to strike fewer strings, softer and darker; read at each note's onset",
71
+ },
72
+ {
73
+ lane: "sost",
74
+ label: "sostenuto",
75
+ events: track.sostenuto,
76
+ help: "holds only the keys already down when it presses (the middle pedal); later notes damp normally",
77
+ },
78
+ ] as const;
79
+ return rows
80
+ .filter((row) => isPianoFamily(track.instrument) || row.events?.length)
81
+ .map((row): MenuNode => {
82
+ const value = row.events?.length ? `${row.events.length} events` : "off";
83
+ return {
84
+ kind: "choice",
85
+ label: row.label,
86
+ value,
87
+ options: ["off", ...(value === "off" ? [] : [value]), "bars"],
88
+ command: (option) =>
89
+ option === "bars"
90
+ ? `pedal ${row.lane} bars`
91
+ : option === "off"
92
+ ? `pedal ${row.lane} off`
93
+ : `pedal ${row.lane}`,
94
+ help: row.help,
95
+ };
96
+ });
97
+ }
98
+
59
99
  export function performanceNodes(context: MenuContext): MenuNode[] {
60
100
  const track = context.score.tracks.find(
61
101
  (item) => item.id === context.trackId,
@@ -148,6 +188,9 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
148
188
  : "pedal",
149
189
  help: "bars replaces the lane with a re-pedal on each downbeat; in play mode press Tab (latch) or hold Shift while recording",
150
190
  },
191
+ // keys-electric (0.6.1): the piano's other two pedals, on modeled piano
192
+ // tracks (or wherever a lane is already set).
193
+ ...pianoPedalNodes(track),
151
194
  {
152
195
  kind: "choice",
153
196
  label: "velocity curve",
@@ -191,7 +234,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
191
234
  },
192
235
  {
193
236
  kind: "number",
194
- label: "humanize velocity (%)",
237
+ label: "humanize vel (%)",
195
238
  value: humanize ? velocity : undefined,
196
239
  min: 0,
197
240
  max: EXPRESSION_LIMITS.maxHumanizePercent,
@@ -231,5 +274,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
231
274
  command: "humanize reseed",
232
275
  help: "the same amounts with a different seeded performance",
233
276
  });
277
+ // 0.7 sing: Vowels on a sing track.
278
+ nodes.push(...singVowelNodes(context));
234
279
  return nodes;
235
280
  }
@@ -35,6 +35,10 @@ import {
35
35
  EXTENSIONS,
36
36
  MAX_VOICING_STEP,
37
37
  PERFORM_MODES,
38
+ DEFAULT_STROKE_SPEED,
39
+ STROKE_PATTERN_NAMES,
40
+ strokeGrid,
41
+ type PerformOptions,
38
42
  PROGRESSION_PRESETS,
39
43
  PROGRESSION_STYLES,
40
44
  SPREADS,
@@ -100,6 +104,10 @@ export type ChordSettings = {
100
104
  /** `none` or a PROGRESSION_PRESETS name; drives the "next" suggestion. */
101
105
  preset: string;
102
106
  style: ProgressionStyle;
107
+ /** Guitar perform mode: a STROKE_PATTERNS name or a D U d u x - . grid. */
108
+ strokes: string;
109
+ /** Guitar perform mode: ms a full six-string down stroke takes. */
110
+ speed: number;
103
111
  };
104
112
 
105
113
  export function defaultChordSettings(): ChordSettings {
@@ -116,9 +124,45 @@ export function defaultChordSettings(): ChordSettings {
116
124
  sevenths: false,
117
125
  preset: "none",
118
126
  style: "pop",
127
+ strokes: "down",
128
+ speed: DEFAULT_STROKE_SPEED * 1000,
119
129
  };
120
130
  }
121
131
 
132
+ /** Play mode's `[` / `]` strum speed step in ms. */
133
+ export const STROKE_SPEED_STEP = 5;
134
+
135
+ /** Strum speed bounds in ms (0 = every string at once). */
136
+ export const STROKE_SPEED_MS = Object.freeze({ min: 0, max: 200 });
137
+
138
+ /**
139
+ * A strum speed from `/chords speed` text: milliseconds (`22`, `22ms`) or
140
+ * beats (`1/32b`, `0.05b`) at `bpm`. Undefined when not a speed.
141
+ */
142
+ export function parseStrokeSpeed(
143
+ text: string | undefined,
144
+ bpm: number,
145
+ ): number | undefined {
146
+ const raw = (text ?? "").trim().toLowerCase();
147
+ const beats = /^(\d+(?:\.\d+)?)(?:\/(\d+))?\s*(?:b|beats?)$/.exec(raw);
148
+ let ms: number;
149
+ if (beats) {
150
+ const value = Number(beats[1]) / (beats[2] ? Number(beats[2]) : 1);
151
+ ms = (value * 60_000) / bpm;
152
+ } else {
153
+ const plain = /^(\d+(?:\.\d+)?)\s*(?:ms)?$/.exec(raw);
154
+ if (!plain) return undefined;
155
+ ms = Number(plain[1]);
156
+ }
157
+ if (
158
+ !Number.isFinite(ms) ||
159
+ ms < STROKE_SPEED_MS.min ||
160
+ ms > STROKE_SPEED_MS.max
161
+ )
162
+ return undefined;
163
+ return Math.round(ms * 10) / 10;
164
+ }
165
+
122
166
  /**
123
167
  * Tracks where auto chords are the default: anything pitched that is not a
124
168
  * bass, a drum kit or a one-shot sampler.
@@ -236,6 +280,16 @@ export class ChordPad {
236
280
  s.perform = PERFORM_MODES[(index + 1) % PERFORM_MODES.length]!;
237
281
  return { type: "status", status: `perform ${s.perform}` };
238
282
  }
283
+ if ((value === "[" || value === "]") && s.perform === "guitar") {
284
+ // Strum speed: slower ([) or faster (]) in 5 ms steps.
285
+ const wanted =
286
+ s.speed + (value === "[" ? STROKE_SPEED_STEP : -STROKE_SPEED_STEP);
287
+ s.speed = Math.max(
288
+ STROKE_SPEED_MS.min,
289
+ Math.min(STROKE_SPEED_MS.max, Math.round(wanted)),
290
+ );
291
+ return { type: "status", status: `strum speed ${s.speed}ms` };
292
+ }
239
293
  if (value === "b") {
240
294
  const index = BASS_MODES.indexOf(s.bass);
241
295
  s.bass = BASS_MODES[(index + 1) % BASS_MODES.length]!;
@@ -392,6 +446,9 @@ export class ChordPad {
392
446
  { key: "0", label: "clear", on: false },
393
447
  { key: "-=", label: `voicing ${signed(s.inversion)}`, on: false },
394
448
  { key: "9", label: perform, on: false },
449
+ ...(s.perform === "guitar"
450
+ ? [{ key: "[]", label: `speed ${s.speed}ms`, on: false }]
451
+ : []),
395
452
  { key: "b", label: `bass ${s.bass}`, on: false },
396
453
  { key: "n", label: "next", on: false },
397
454
  { key: "q", label: s.mode === "auto" ? "auto" : "manual", on: false },
@@ -412,6 +469,8 @@ export class ChordPad {
412
469
  if (s.spread !== "close") parts.push(s.spread);
413
470
  if (s.perform === "pattern")
414
471
  parts.push(`pattern ${patternLabel(s.pattern)}`);
472
+ else if (s.perform === "guitar")
473
+ parts.push(`guitar ${s.strokes} ${s.speed}ms`);
415
474
  else if (s.perform !== "block") parts.push(s.perform);
416
475
  if (s.bass !== "off") parts.push(`bass ${s.bass}`);
417
476
  return parts.join(" · ");
@@ -435,15 +494,18 @@ function signed(value: number): string {
435
494
  /**
436
495
  * `/chords …`: `auto|manual|off`, `voicing <-12..12>`, `spread <close|open|
437
496
  * wide>`, `bass off|chords|unison|single|solo` (`on` = chords), `perform
438
- * <mode>`, `pattern <1..13|name>`, `rate <grid|1/8…>`, `octaves
497
+ * <mode>`, `pattern <1..13|name>`, `strokes <name|D-DU-UDU>` (guitar
498
+ * mode), `speed <ms|1/32b>`, `rate <grid|1/8…>`, `octaves
439
499
  * <1..4>`, `sevenths on|off`, `preset <name|none>`, `style <pop|jazz|…>`.
440
500
  * Mutates `settings`; returns the receipt or an error.
441
501
  */
442
502
  export function applyChordsCommand(
443
503
  settings: ChordSettings,
444
504
  argument: string,
505
+ bpm = 120,
445
506
  ): { ok: boolean; message: string } {
446
- const words = argument.trim().toLowerCase().split(/\s+/).filter(Boolean);
507
+ const raw = argument.trim().split(/\s+/).filter(Boolean);
508
+ const words = raw.map((word) => word.toLowerCase());
447
509
  const [head, value] = words;
448
510
  const done = (message: string) => ({ ok: true, message });
449
511
  const bad = (message: string) => ({ ok: false, message });
@@ -505,6 +567,28 @@ export function applyChordsCommand(
505
567
  return bad(`rate takes ${ARP_RATES.join("|")}`);
506
568
  settings.rate = value as ArpRate;
507
569
  return done(`chords rate ${value}`);
570
+ case "strokes": {
571
+ // Case matters: D is a full down stroke, d a light one.
572
+ const grid = raw.slice(1).join("");
573
+ if (!strokeGrid(grid))
574
+ return bad(
575
+ `strokes takes ${STROKE_PATTERN_NAMES.join("|")} or a grid of D U d u x - .`,
576
+ );
577
+ settings.strokes = (STROKE_PATTERN_NAMES as readonly string[]).includes(
578
+ grid.toLowerCase(),
579
+ )
580
+ ? grid.toLowerCase()
581
+ : grid;
582
+ settings.perform = "guitar";
583
+ return done(`chords strokes ${settings.strokes}`);
584
+ }
585
+ case "speed": {
586
+ const ms = parseStrokeSpeed(words.slice(1).join(""), bpm);
587
+ if (ms === undefined)
588
+ return bad("speed takes 0..200 ms (22, 22ms) or beats (1/32b)");
589
+ settings.speed = ms;
590
+ return done(`chords speed ${ms}ms`);
591
+ }
508
592
  case "octaves": {
509
593
  const n = Number(value);
510
594
  if (!Number.isInteger(n) || n < 1 || n > 4)
@@ -521,12 +605,14 @@ export function applyChordsCommand(
521
605
  return done(`chords preset ${value}`);
522
606
  case "style":
523
607
  if (!(PROGRESSION_STYLES as readonly string[]).includes(value ?? ""))
524
- return bad(`style takes ${PROGRESSION_STYLES.join("|")}`);
608
+ return bad(
609
+ `idiom takes ${PROGRESSION_STYLES.join("|")} · chords idiom jazz`,
610
+ );
525
611
  settings.style = value as ProgressionStyle;
526
- return done(`chords style ${value}`);
612
+ return done(`chords idiom ${value}`);
527
613
  default:
528
614
  return bad(
529
- "/chords auto|manual|off · voicing · spread · bass · perform · pattern · rate · octaves · sevenths · preset · style",
615
+ "/chords auto|manual|off · voicing · spread · bass · perform · pattern · strokes · speed · rate · octaves · sevenths · preset · style",
530
616
  );
531
617
  }
532
618
  }
@@ -539,6 +625,8 @@ export function chordsSummary(settings: ChordSettings): string {
539
625
  s.spread,
540
626
  s.perform,
541
627
  `pattern ${patternLabel(s.pattern)}`,
628
+ `strokes ${s.strokes}`,
629
+ `speed ${s.speed}ms`,
542
630
  `rate ${s.rate}`,
543
631
  `octaves ${s.octaves}`,
544
632
  `bass ${s.bass}`,
@@ -603,6 +691,7 @@ export function chordPhrase(
603
691
  seed: 1,
604
692
  velocity: 0.8,
605
693
  ...(mode === "pattern" ? { pattern: settings.pattern } : {}),
694
+ ...(mode === "guitar" ? guitarPerform(settings, score, track) : {}),
606
695
  },
607
696
  });
608
697
  // With chords off a key plays one note: the root, in the chord's octave.
@@ -630,3 +719,17 @@ export function chordPhrase(
630
719
  }
631
720
  return notes;
632
721
  }
722
+
723
+ /** Perform options for the guitar mode: strokes, speed, tempo and fretting. */
724
+ export function guitarPerform(
725
+ settings: Pick<ChordSettings, "strokes" | "speed">,
726
+ score: Pick<TrackScore, "tempoBpm">,
727
+ track: Pick<Track, "guitar"> | undefined,
728
+ ): Partial<PerformOptions> {
729
+ return {
730
+ strokes: settings.strokes,
731
+ speed: settings.speed / 1000,
732
+ tempo: score.tempoBpm,
733
+ ...(track?.guitar ? { guitar: track.guitar } : {}),
734
+ };
735
+ }
@@ -12,6 +12,7 @@
12
12
  * Shift sustain while held Tab sustain latch Esc leave the mode
13
13
  * (while recording, sustain changes record as track pedal events)
14
14
  * I scale degrees: the home row plays the key's scale (any tuning)
15
+ * [ / ] strum slower / faster (chords perform guitar, 5 ms steps)
15
16
  *
16
17
  * Terminals report key-down only, so a held key is synthesized from the
17
18
  * keyboard's auto-repeat: a press sounds for the gate (one grid step by
@@ -31,6 +32,7 @@ import {
31
32
  type Track,
32
33
  } from "../../core/score.ts";
33
34
  import type { TuningTable } from "../../core/tuning.ts";
35
+ import { DRUM_VOICES, isDrumInstrument } from "../../core/drums.ts";
34
36
  import { pitchName } from "../../tui/highway.ts";
35
37
 
36
38
  /** Semitone offset from the base C for each note key. */
@@ -111,10 +113,21 @@ export function defaultBaseFor(instrument: string | undefined): number {
111
113
  */
112
114
  export type PlayLayout = Readonly<{
113
115
  base: number;
114
- /** Pitch → label (voice names on one-shot samplers). */
116
+ /** Pitch → label (voice names on one-shot samplers, drums on kits). */
115
117
  labels: ReadonlyMap<number, string>;
118
+ /** A drum kit: keys name drums and the chord row stays hidden. */
119
+ drums?: boolean | undefined;
116
120
  }>;
117
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
+
118
131
  export function playLayoutFor(track: Track | undefined): PlayLayout {
119
132
  const labels = new Map<number, string>();
120
133
  if (track && isSamplerInstrument(track.instrument) && track.sampler) {
@@ -129,6 +142,8 @@ export function playLayoutFor(track: Track | undefined): PlayLayout {
129
142
  const lowest = roots.length > 0 ? Math.min(...roots) : 60;
130
143
  return { base: clampBase(Math.floor(lowest / 12) * 12), labels };
131
144
  }
145
+ if (track && isDrumInstrument(track.instrument))
146
+ return { base: 36, labels: drumKeyLabels(), drums: true };
132
147
  // A plain instrument takes its range from the track's name (`bass`, `lead`).
133
148
  const byInstrument = defaultBaseFor(track?.instrument);
134
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,20 +58,24 @@ 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,
64
+ type PerformOptions,
62
65
  type PerformedNote,
63
66
  } from "../../core/chords.ts";
64
67
  import {
65
68
  ChordPad,
66
69
  chordCapable,
67
70
  defaultChordSettings,
71
+ guitarPerform,
68
72
  songKey,
69
73
  type ChordSettings,
70
74
  type PlayedChord,
71
75
  } from "./play-chords.ts";
72
76
  import type { PlayHeaderView, PlayStripKey } from "../../tui/play-strip.ts";
73
77
  import { resolveTuning, type Tuning } from "../../core/tuning.ts";
78
+ import { singThroat } from "../../core/sing.ts";
74
79
  import {
75
80
  NOTE_KEYS,
76
81
  PlayKeyboard,
@@ -89,6 +94,10 @@ export interface LiveEngine {
89
94
  readonly sampleRate: number;
90
95
  readonly canMonitor: boolean;
91
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;
92
101
  monitor(on: boolean): Promise<void>;
93
102
  setLeadMs(ms: number | undefined): void;
94
103
  noteOn(id: number, note: LiveNotePcm): number;
@@ -122,6 +131,8 @@ export interface PlayHost {
122
131
  startTransport(beat: number): Promise<void>;
123
132
  stopTransport(): Promise<void>;
124
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;
125
136
  newNoteId(): string;
126
137
  }
127
138
 
@@ -181,6 +192,8 @@ export type RecordedChord = Readonly<{
181
192
  seed: number;
182
193
  /** CHORD_PATTERNS name when `mode` is `pattern`. */
183
194
  pattern?: string;
195
+ /** Guitar mode: strokes, speed, tempo, fretting and the chord root. */
196
+ guitar?: Partial<PerformOptions>;
184
197
  }>;
185
198
 
186
199
  /** A chord sounding live: its voices start and stop from `tick()`. */
@@ -236,6 +249,8 @@ export class PlaySession {
236
249
  private synth: LiveSynth | undefined;
237
250
  /** Rig voices awaiting their full render: id → released since. */
238
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;
239
254
  private readonly fullRenderer = new LiveFullRenderer();
240
255
  private readonly pending = new Map<number, Pending>();
241
256
  /** Sustain changes while recording (Tab latch or Shift), as pedal events. */
@@ -285,7 +300,8 @@ export class PlaySession {
285
300
  settings.mode =
286
301
  chordCapable(track) &&
287
302
  twelveTet(this.host.score(), track) &&
288
- (track?.glide === undefined || track.glide.mode === "poly")
303
+ (track?.glide === undefined || track.glide.mode === "poly") &&
304
+ !singThroat(track)
289
305
  ? "auto"
290
306
  : "manual";
291
307
  this.chords = new ChordPad(settings, () => this.host.score().key);
@@ -327,8 +343,12 @@ export class PlaySession {
327
343
  this.active = true;
328
344
  const engine = this.host.engine();
329
345
  if (engine?.canMonitor) {
330
- 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);
331
349
  await engine.monitor(true);
350
+ if (engine.audioNote)
351
+ this.status = `${engine.audioNote} · ${Math.round(engine.leadMs)} ms lead`;
332
352
  } else this.status = "no audio · keys still record";
333
353
  this.applyClick();
334
354
  }
@@ -479,6 +499,8 @@ export class PlaySession {
479
499
  return { type: "handled" };
480
500
  case "note": {
481
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);
482
504
  const chord = this.chords.chordFor(action.note.pitch);
483
505
  if (chord) {
484
506
  const played = this.chords.voice(chord, action.note.pitch);
@@ -497,6 +519,12 @@ export class PlaySession {
497
519
  );
498
520
  return { type: "handled" };
499
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
+ }
500
528
  this.sound(action.note.id, action.note);
501
529
  this.record(action.note);
502
530
  return { type: "handled" };
@@ -643,16 +671,21 @@ export class PlaySession {
643
671
  // The full pass runs on a worker, in key order, so a strummed chord or a
644
672
  // fast run never stalls key handling behind an oversampled render.
645
673
  if (pcm.partial)
646
- void this.fullRenderer.full(synth, request).then((full) => {
647
- // Not after note-off: a swap would cancel the release fade.
648
- if (
649
- full &&
650
- this.windows.get(id) === false &&
651
- engine.voicePosition?.(id) !== undefined
674
+ void this.fullRenderer
675
+ .full(
676
+ synth,
677
+ pcm.clock === undefined ? request : { ...request, clock: pcm.clock },
652
678
  )
653
- engine.noteOn(id, full);
654
- this.windows.delete(id);
655
- });
679
+ .then((full) => {
680
+ // Not after note-off: a swap would cancel the release fade.
681
+ if (
682
+ full &&
683
+ this.windows.get(id) === false &&
684
+ engine.voicePosition?.(id) !== undefined
685
+ )
686
+ engine.noteOn(id, full);
687
+ this.windows.delete(id);
688
+ });
656
689
  }
657
690
 
658
691
  private release(ids: readonly number[], atMs: number): void {
@@ -729,6 +762,20 @@ export class PlaySession {
729
762
  octaves: settings.octaves,
730
763
  seed,
731
764
  ...(mode === "pattern" ? { pattern: settings.pattern } : {}),
765
+ ...(mode === "guitar" ? { guitar: this.guitarOptions(played) } : {}),
766
+ };
767
+ }
768
+
769
+ /** Guitar-mode perform options for a played chord (strokes, speed, frets). */
770
+ private guitarOptions(played: PlayedChord): Partial<PerformOptions> {
771
+ return {
772
+ ...guitarPerform(
773
+ this.chords.settings,
774
+ { tempoBpm: this.currentBpm() },
775
+ this.trackData(),
776
+ ),
777
+ root: played.chord.root,
778
+ ...(played.chord.bass !== undefined ? { slash: played.chord.bass } : {}),
732
779
  };
733
780
  }
734
781
 
@@ -749,6 +796,7 @@ export class PlaySession {
749
796
  seed: note.id,
750
797
  pattern: settings.pattern,
751
798
  velocity: 1,
799
+ ...(mode === "guitar" ? this.guitarOptions(played) : {}),
752
800
  });
753
801
  if (played.bass !== undefined)
754
802
  plan.push({
@@ -1119,7 +1167,7 @@ export class PlaySession {
1119
1167
  };
1120
1168
  }
1121
1169
  return {
1122
- range: this.keyboard.range,
1170
+ range: this.layout.drums ? "drums" : this.keyboard.range,
1123
1171
  velocity: this.keyboard.velocity,
1124
1172
  armed: this.armed,
1125
1173
  recording: this.recording,
@@ -1129,10 +1177,14 @@ export class PlaySession {
1129
1177
  countIn,
1130
1178
  beat,
1131
1179
  grid: `grid ${this.grid}`,
1132
- 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),
1133
1184
  status: this.status,
1134
1185
  keys: this.strip(now),
1135
- legend: this.chords.on ? this.chords.legend() : undefined,
1186
+ legend:
1187
+ this.chords.on && !this.layout.drums ? this.chords.legend() : undefined,
1136
1188
  };
1137
1189
  }
1138
1190
 
@@ -1154,8 +1206,20 @@ export class PlaySession {
1154
1206
  ).map((cell) => {
1155
1207
  const chord = this.keyboard.pitchFor(cell.key);
1156
1208
  const name =
1157
- chord === undefined ? undefined : this.chords.keyLabel(chord);
1158
- 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);
1159
1223
  });
1160
1224
  }
1161
1225
 
@@ -1213,6 +1277,7 @@ export function chordNotes(
1213
1277
  seed: chord.seed,
1214
1278
  ...(chord.pattern !== undefined ? { pattern: chord.pattern } : {}),
1215
1279
  velocity: 1,
1280
+ ...chord.guitar,
1216
1281
  }).map((note) => ({
1217
1282
  pitch: note.pitch,
1218
1283
  velocity: scaleVelocity(press.velocity, note.velocity),