@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
@@ -0,0 +1,415 @@
1
+ /**
2
+ * The `style` prompt command: browse the style taxonomy (core/styles) and
3
+ * generate a song from a style card.
4
+ *
5
+ * style the families and their roots
6
+ * style list [id|family] children of a node (or a family's roots)
7
+ * style search <words> ranked name, alias and region search
8
+ * style info <id> one style: path, meter, tempo, pitch, harmony
9
+ * style <id> [bars] [seed] generate the song from a style (replaces it)
10
+ * style blend <a> <b> [w] [bars] [seed]
11
+ * a weighted blend; w 0 is a, 1 is b (0.5)
12
+ * style again the current style with the next seed
13
+ *
14
+ * A generated song replaces the score in one revision (one undo step) and
15
+ * records `style` provenance (id, seed, bars, blend) on the song.
16
+ */
17
+ import type { TrackScore } from "../../core/score.ts";
18
+ import {
19
+ blendStyles,
20
+ hasCard,
21
+ LEAF_IDS,
22
+ ROOT_IDS,
23
+ resolveStyle,
24
+ searchStyles,
25
+ STYLE_CARDS,
26
+ STYLE_IDS,
27
+ STYLE_TREE,
28
+ styleByName,
29
+ stylePath,
30
+ type StyleNode,
31
+ } from "../../core/styles/index.ts";
32
+ import {
33
+ generateStyle,
34
+ STYLE_LIMITS,
35
+ StyleError,
36
+ styleScore,
37
+ type GeneratedStyle,
38
+ } from "../../core/styles/generate.ts";
39
+ import { STYLE_FAMILIES } from "../../core/styles/taxonomy.ts";
40
+ import type { ResolvedStyle, StyleId } from "../../core/styles/schema.ts";
41
+ import { validateGenerated } from "../../core/styles/validate.ts";
42
+
43
+ export type StyleCommand =
44
+ | { type: "style-families" }
45
+ | { type: "style-list"; id: string }
46
+ | { type: "style-search"; query: string }
47
+ | { type: "style-info"; id: string }
48
+ | { type: "style-again" }
49
+ | {
50
+ type: "style-apply";
51
+ id: string;
52
+ bars?: number;
53
+ seed?: number;
54
+ blend?: Readonly<{ id: string; weight: number }>;
55
+ }
56
+ | { type: "style-bad"; message: string };
57
+
58
+ export type StyleResult = Readonly<{
59
+ ok: boolean;
60
+ message: string;
61
+ next?: TrackScore;
62
+ kind?: string;
63
+ payload?: Record<string, unknown>;
64
+ /** Detail for the ctrl-o log (seed, check counts), not the card. */
65
+ log?: string;
66
+ }>;
67
+
68
+ export const STYLE_USAGE =
69
+ "style [list [id]|search <words>|info <id>|<id> [bars] [seed]|blend <a> <b> [w] [bars] [seed]|again]";
70
+
71
+ const INTEGER = /^\d{1,10}$/;
72
+ const WEIGHT = /^(?:0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,3}%)$/;
73
+
74
+ function bad(message: string): StyleCommand {
75
+ return { type: "style-bad", message };
76
+ }
77
+
78
+ /** Trailing `[bars] [seed]` integers; ids may contain digits, so only the tail. */
79
+ function tailNumbers(words: string[]): {
80
+ rest: string[];
81
+ bars?: number;
82
+ seed?: number;
83
+ } {
84
+ const numbers: number[] = [];
85
+ let end = words.length;
86
+ while (end > 1 && numbers.length < 2 && INTEGER.test(words[end - 1]!)) {
87
+ numbers.unshift(Number(words[end - 1]));
88
+ end -= 1;
89
+ }
90
+ return {
91
+ rest: words.slice(0, end),
92
+ ...(numbers[0] !== undefined ? { bars: numbers[0] } : {}),
93
+ ...(numbers[1] !== undefined ? { seed: numbers[1] } : {}),
94
+ };
95
+ }
96
+
97
+ function weightOf(text: string): number {
98
+ return text.endsWith("%") ? Number(text.slice(0, -1)) / 100 : Number(text);
99
+ }
100
+
101
+ export function parseStyleCommand(prompt: string): StyleCommand | undefined {
102
+ if (prompt.length > 512) return undefined;
103
+ const words = prompt.trim().split(/\s+/);
104
+ const head = words[0]?.toLowerCase();
105
+ if (head !== "style" && head !== "/style" && head !== "styles")
106
+ return undefined;
107
+ const rest = words.slice(1);
108
+ if (rest.length === 0) return { type: "style-families" };
109
+ const verb = rest[0]!.toLowerCase();
110
+ if (verb === "list" || verb === "ls" || verb === "tree")
111
+ return rest.length === 1
112
+ ? { type: "style-families" }
113
+ : { type: "style-list", id: rest.slice(1).join(" ") };
114
+ if (verb === "search" || verb === "find") {
115
+ const query = rest.slice(1).join(" ").trim();
116
+ return query
117
+ ? { type: "style-search", query }
118
+ : bad("style search <words> · style search maqam");
119
+ }
120
+ if (verb === "info" || verb === "show" || verb === "about") {
121
+ const id = rest.slice(1).join(" ").trim();
122
+ return id ? { type: "style-info", id } : bad("style info <id>");
123
+ }
124
+ if (verb === "again" || verb === "next" || verb === "reroll")
125
+ return { type: "style-again" };
126
+ if (verb === "blend" || verb === "mix") {
127
+ // Ids are single words (`deep-house`); then [weight] [bars] [seed].
128
+ const [a, b, ...numbers] = rest.slice(1);
129
+ const usage = bad(
130
+ "style blend <a> <b> [weight 0..1] [bars] [seed] · style blend bebop bossa-nova 0.3",
131
+ );
132
+ if (!a || !b || numbers.length > 3) return usage;
133
+ let weight = 0.5;
134
+ if (numbers[0] !== undefined) {
135
+ if (!WEIGHT.test(numbers[0])) return usage;
136
+ weight = weightOf(numbers[0]);
137
+ if (!(weight >= 0 && weight <= 1)) return usage;
138
+ }
139
+ if (numbers.slice(1).some((word) => !INTEGER.test(word))) return usage;
140
+ return {
141
+ type: "style-apply",
142
+ id: a,
143
+ blend: { id: b, weight },
144
+ ...(numbers[1] !== undefined ? { bars: Number(numbers[1]) } : {}),
145
+ ...(numbers[2] !== undefined ? { seed: Number(numbers[2]) } : {}),
146
+ };
147
+ }
148
+ const tail = tailNumbers(rest);
149
+ return {
150
+ type: "style-apply",
151
+ id: tail.rest.join(" "),
152
+ ...(tail.bars !== undefined ? { bars: tail.bars } : {}),
153
+ ...(tail.seed !== undefined ? { seed: tail.seed } : {}),
154
+ };
155
+ }
156
+
157
+ /** A taxonomy id from typed text: id, title or alias. */
158
+ export function findStyle(text: string): StyleId | undefined {
159
+ const lowered = text.trim().toLowerCase();
160
+ if (STYLE_TREE.has(lowered)) return lowered;
161
+ return styleByName(text);
162
+ }
163
+
164
+ function unknown(text: string): string {
165
+ const near = searchStyles(text, 4).map((match) => match.id);
166
+ return `style · no style "${text}"${near.length ? ` · try ${near.join(" ")}` : " · style search <words>"}`;
167
+ }
168
+
169
+ function childCount(node: StyleNode): string {
170
+ if (node.leaf) return hasCard(node.id) ? "" : " ·";
171
+ return ` (${node.children.length})`;
172
+ }
173
+
174
+ /** One-line node label: id, a card mark, and its child count. */
175
+ function nodeLabel(node: StyleNode): string {
176
+ return `${node.id}${childCount(node)}`;
177
+ }
178
+
179
+ export function styleFamiliesText(): string {
180
+ const lines = STYLE_FAMILIES.map(
181
+ (family) =>
182
+ `${family.key}: ${family.roots.map((id) => nodeLabel(STYLE_TREE.get(id)!)).join(" ")}`,
183
+ );
184
+ return [
185
+ `styles · ${STYLE_IDS.length} in the tree · ${LEAF_IDS.length} leaves · ${STYLE_CARDS.size} cards`,
186
+ ...lines,
187
+ "style list <id> · style search <words> · style info <id> · style <id> [bars] [seed]",
188
+ ].join("\n");
189
+ }
190
+
191
+ export function styleListText(text: string): StyleResult {
192
+ const family = STYLE_FAMILIES.find((entry) => entry.key === text.trim());
193
+ if (family)
194
+ return {
195
+ ok: true,
196
+ message: `${family.title}: ${family.roots.map((id) => nodeLabel(STYLE_TREE.get(id)!)).join(" ")}`,
197
+ };
198
+ const id = findStyle(text);
199
+ if (!id) return { ok: false, message: unknown(text) };
200
+ const node = STYLE_TREE.get(id)!;
201
+ const path = stylePath(id).join(" › ");
202
+ if (node.leaf)
203
+ return { ok: true, message: `${path} · a leaf · style info ${id}` };
204
+ return {
205
+ ok: true,
206
+ message: `${path}: ${node.children.map((child) => nodeLabel(STYLE_TREE.get(child)!)).join(" ")}`,
207
+ };
208
+ }
209
+
210
+ export function styleSearchText(query: string): StyleResult {
211
+ const matches = searchStyles(query, 12);
212
+ if (matches.length === 0)
213
+ return { ok: false, message: `style search · nothing for "${query}"` };
214
+ return {
215
+ ok: true,
216
+ message: `style search ${query}: ${matches.map((match) => match.id).join(" ")}`,
217
+ };
218
+ }
219
+
220
+ const pct = (value: number) => `${Math.round(value * 100)}%`;
221
+
222
+ function weightedText<T>(
223
+ list: readonly (readonly [T, number])[] | undefined,
224
+ show: (value: T) => string = String,
225
+ ): string {
226
+ if (!list || list.length === 0) return "-";
227
+ const total = list.reduce((sum, [, weight]) => sum + weight, 0) || 1;
228
+ return [...list]
229
+ .sort((a, b) => b[1] - a[1])
230
+ .slice(0, 4)
231
+ .map(([value, weight]) =>
232
+ list.length === 1 ? show(value) : `${show(value)} ${pct(weight / total)}`,
233
+ )
234
+ .join(", ");
235
+ }
236
+
237
+ /** The facts of a resolved style, one per line, for /style info and tools. */
238
+ export function describeStyle(style: ResolvedStyle): string[] {
239
+ const node = STYLE_TREE.get(style.id);
240
+ const roles = Object.entries(style.texture.roles)
241
+ .filter(([, role]) => role)
242
+ .map(([name, role]) => (role!.required ? name : `${name}?`));
243
+ const harmony = [
244
+ style.harmony.model,
245
+ style.harmony.presets
246
+ ? `presets ${weightedText(style.harmony.presets)}`
247
+ : "",
248
+ style.harmony.forms ? `forms ${style.harmony.forms.length}` : "",
249
+ style.harmony.chain ? "numeral chain" : "",
250
+ ]
251
+ .filter(Boolean)
252
+ .join(" · ");
253
+ const era = node
254
+ ? node.era.from === null
255
+ ? ""
256
+ : `${node.era.from}${node.era.to === null ? "-" : `-${node.era.to}`}`
257
+ : "";
258
+ return [
259
+ `${style.title} (${style.id})${era ? ` · ${era}` : ""}${node?.region.length ? ` · ${node.region.join(", ")}` : ""}`,
260
+ `path ${stylePath(style.id).join(" › ")} · cards ${style.lineage.join(" › ") || "base"}`,
261
+ style.summary,
262
+ `meter ${weightedText(style.meter.signatures)}${style.meter.cycle ? ` · cycle ${style.meter.cycle.name}` : ""} · tempo ${style.tempo.bpm[0]}-${style.tempo.bpm[1]} (${style.tempo.typical}) BPM`,
263
+ `groove ${style.groove.subdivision}/beat · swing ${style.groove.swingRatio[0] === style.groove.swingRatio[1] ? style.groove.swingRatio[0] : style.groove.swingRatio.join("-")}`,
264
+ `pitch ${style.pitch.tuning ?? "12-TET"} · ${weightedText(style.pitch.scales)}`,
265
+ `harmony ${harmony}`,
266
+ `texture ${style.texture.kind} · ${roles.join(" ")}`,
267
+ ].filter((line) => line.trim().length > 0);
268
+ }
269
+
270
+ export function styleInfoText(text: string): StyleResult {
271
+ const id = findStyle(text);
272
+ if (!id) return { ok: false, message: unknown(text) };
273
+ const lines = describeStyle(resolveStyle(id));
274
+ const node = STYLE_TREE.get(id)!;
275
+ if (!node.leaf) lines.push(`children ${node.children.join(" ")}`);
276
+ if (!hasCard(id) && STYLE_CARDS.size > 0)
277
+ lines.push("no card of its own yet: plays its nearest ancestor's card");
278
+ return { ok: true, message: lines.join("\n") };
279
+ }
280
+
281
+ /** Generate a song for a resolved command; throws StyleError on bad input. */
282
+ export function generateFromCommand(
283
+ command: Extract<StyleCommand, { type: "style-apply" }>,
284
+ ): GeneratedStyle {
285
+ const id = findStyle(command.id);
286
+ if (!id) throw new StyleError(unknown(command.id));
287
+ let style: ResolvedStyle = resolveStyle(id);
288
+ if (command.blend) {
289
+ const other = findStyle(command.blend.id);
290
+ if (!other) throw new StyleError(unknown(command.blend.id));
291
+ style = blendStyles(style, resolveStyle(other), command.blend.weight);
292
+ }
293
+ if (
294
+ command.bars !== undefined &&
295
+ (command.bars < STYLE_LIMITS.minBars || command.bars > STYLE_LIMITS.maxBars)
296
+ )
297
+ throw new StyleError(
298
+ `style · bars take ${STYLE_LIMITS.minBars}..${STYLE_LIMITS.maxBars}`,
299
+ );
300
+ if (command.seed !== undefined && command.seed > STYLE_LIMITS.maxSeed)
301
+ throw new StyleError(`style · seed takes 0..${STYLE_LIMITS.maxSeed}`);
302
+ return generateStyle(style, {
303
+ ...(command.bars !== undefined ? { bars: command.bars } : {}),
304
+ ...(command.seed !== undefined ? { seed: command.seed } : {}),
305
+ });
306
+ }
307
+
308
+ /** One line about a generated song. */
309
+ export function generatedSummary(generated: GeneratedStyle): string {
310
+ const { plan, provenance } = generated;
311
+ const name = provenance.blend
312
+ ? `${provenance.id} + ${provenance.blend.id} ${pct(provenance.blend.weight)}`
313
+ : provenance.id;
314
+ const report = validateGenerated(generated);
315
+ const failed = report.checks.filter((check) => !check.ok);
316
+ return [
317
+ `style ${name} · seed ${plan.seed} · ${plan.bars} bars`,
318
+ `${plan.signature} ${Math.round(plan.bpm)} BPM`,
319
+ plan.keyText ?? plan.tuning?.name ?? "",
320
+ `${plan.tracks.length} tracks`,
321
+ failed.length === 0
322
+ ? `${report.checks.length} checks pass`
323
+ : `${failed.length} of ${report.checks.length} checks off: ${failed.map((check) => check.name).join(" ")}`,
324
+ ]
325
+ .filter(Boolean)
326
+ .join(" · ");
327
+ }
328
+
329
+ /**
330
+ * The card after a style lands, in musical terms: what the song now is.
331
+ * `lofi-hip-hop · F minor · 84 BPM · 8 bars · 4 tracks`.
332
+ */
333
+ export function styleReceipt(generated: GeneratedStyle): string {
334
+ const { plan, provenance } = generated;
335
+ const name = provenance.blend
336
+ ? `${provenance.id} + ${provenance.blend.id} ${pct(provenance.blend.weight)}`
337
+ : provenance.id;
338
+ return [
339
+ name,
340
+ plan.keyText ?? plan.tuning?.name ?? "",
341
+ `${Math.round(plan.bpm)} BPM`,
342
+ `${plan.bars} bars`,
343
+ `${plan.tracks.length} tracks`,
344
+ ]
345
+ .filter(Boolean)
346
+ .join(" · ");
347
+ }
348
+
349
+ /** Seed, meter and the generator's checks, for the ctrl-o log. */
350
+ export function styleCheckLine(generated: GeneratedStyle): string {
351
+ const { plan } = generated;
352
+ const report = validateGenerated(generated);
353
+ const failed = report.checks.filter((check) => !check.ok);
354
+ return [
355
+ `style · seed ${plan.seed} · ${plan.signature}`,
356
+ failed.length === 0
357
+ ? `${report.checks.length} checks pass`
358
+ : `${failed.length} of ${report.checks.length} checks off: ${failed.map((check) => check.name).join(" ")}`,
359
+ ].join(" · ");
360
+ }
361
+
362
+ export function applyStyleCommand(
363
+ score: TrackScore,
364
+ command: StyleCommand,
365
+ ): StyleResult {
366
+ switch (command.type) {
367
+ case "style-bad":
368
+ return { ok: false, message: command.message };
369
+ case "style-families":
370
+ return { ok: true, message: styleFamiliesText() };
371
+ case "style-list":
372
+ return styleListText(command.id);
373
+ case "style-search":
374
+ return styleSearchText(command.query);
375
+ case "style-info":
376
+ return styleInfoText(command.id);
377
+ case "style-again": {
378
+ const current = score.style;
379
+ if (!current)
380
+ return {
381
+ ok: false,
382
+ message: "style again · this song has no style yet · style <id>",
383
+ };
384
+ return applyStyleCommand(score, {
385
+ type: "style-apply",
386
+ id: current.id,
387
+ bars: Math.min(current.bars, STYLE_LIMITS.maxBars),
388
+ seed: (current.seed + 1) % (STYLE_LIMITS.maxSeed + 1),
389
+ ...(current.blend ? { blend: current.blend } : {}),
390
+ });
391
+ }
392
+ case "style-apply": {
393
+ if (!command.id) return { ok: false, message: `usage: ${STYLE_USAGE}` };
394
+ let generated: GeneratedStyle;
395
+ try {
396
+ generated = generateFromCommand(command);
397
+ } catch (error) {
398
+ if (error instanceof StyleError)
399
+ return { ok: false, message: error.message };
400
+ throw error;
401
+ }
402
+ return {
403
+ ok: true,
404
+ message: `${styleReceipt(generated)} · ctrl-z undo`,
405
+ log: styleCheckLine(generated),
406
+ next: styleScore(generated),
407
+ kind: "style.apply",
408
+ payload: { style: generated.provenance },
409
+ };
410
+ }
411
+ }
412
+ }
413
+
414
+ /** Taxonomy order of root ids, for the browser. */
415
+ export const STYLE_BROWSER_ROOTS: readonly StyleId[] = ROOT_IDS;
@@ -13,7 +13,7 @@
13
13
  * meter remove bar <n> · meter clear
14
14
  * track rate <0.125..8>|<a>/<b>|off polytempo: the track's tempo ratio
15
15
  * track phase <beats>|off start the track later
16
- * track cycle <beats>|off polymeter: loop the first beats
16
+ * track loop <beats>|off polymeter: loop the first beats (alias cycle)
17
17
  * track phasing <cycle> [over <beats>] [cycles <n>] Reich-style drift
18
18
  * track phasing <cycle> [hold <n>] [drift <n>] [shift <beats>]
19
19
  * stepped, as in Piano Phase
@@ -323,7 +323,10 @@ function meterCommand(rest: readonly string[]): TimeCommand | undefined {
323
323
  }
324
324
 
325
325
  function trackTimeCommand(rest: readonly string[]): TimeCommand | undefined {
326
- const [field, value, ...tail] = rest;
326
+ // `track loop` is the canonical word for a track's own repeat length;
327
+ // `track cycle` stays as its alias.
328
+ const [word, value, ...tail] = rest;
329
+ const field = word === "loop" ? "cycle" : word;
327
330
  if (field === "time")
328
331
  return rest.length === 2 && (value === "off" || value === "reset")
329
332
  ? { type: "track-time-off" }
@@ -806,7 +809,7 @@ function trackTime(
806
809
  } else {
807
810
  const ticks = Math.round(command.value * tpb);
808
811
  if (command.field === "cycle" && ticks < 1)
809
- return { ok: false, message: "track cycle · too short" };
812
+ return { ok: false, message: "track loop · too short" };
810
813
  current[command.field] = ticks;
811
814
  message = `${trackId} · ${command.field} ${fmt(command.value)} beat${command.value === 1 ? "" : "s"}`;
812
815
  if (command.field === "cycle" && loopTicksOf(score) % ticks !== 0) {
@@ -384,11 +384,11 @@ export function tuningSummary(score: TrackScore, trackId: string): string {
384
384
  return `tuning · ${song} · ${trackId} ${own}`;
385
385
  }
386
386
 
387
- /** `scale · D bayati · 0 1.5 3 5 7 8 10`. */
387
+ /** `key · D bayati · 0 1.5 3 5 7 8 10` (key is the canonical verb). */
388
388
  export function scaleSummary(key: string | null | undefined): string {
389
389
  const parsed = parseKey(key ?? undefined);
390
- if (!parsed) return "scale · none (C major assumed) · scale <tonic> <name>";
391
- return `scale · ${keyName(parsed)} · ${scaleSteps(parsed).map(formatStep).join(" ")}`;
390
+ if (!parsed) return "key · none (C major assumed) · key <tonic> <scale>";
391
+ return `key · ${keyName(parsed)} · ${scaleSteps(parsed).map(formatStep).join(" ")}`;
392
392
  }
393
393
 
394
394
  function formatStep(step: number): string {