@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
package/core/chords.ts CHANGED
@@ -49,6 +49,7 @@ export const QUALITIES = [
49
49
  "mb6",
50
50
  "b6",
51
51
  "7#9",
52
+ "b5",
52
53
  ] as const;
53
54
  export type Quality = (typeof QUALITIES)[number];
54
55
 
@@ -65,6 +66,7 @@ const QUALITY_INTERVALS: Readonly<Record<Quality, readonly number[]>> =
65
66
  mb6: [0, 3, 7, 8],
66
67
  b6: [0, 4, 7, 8],
67
68
  "7#9": [0, 4, 7, 10, 15],
69
+ b5: [0, 4, 6],
68
70
  });
69
71
 
70
72
  /**
@@ -111,6 +113,17 @@ export type Chord = Readonly<{
111
113
  extensions: readonly Extension[];
112
114
  /** Slash bass pitch class, when not the root. */
113
115
  bass?: number | undefined;
116
+ /**
117
+ * Upper tensions beyond the four extension buttons, as semitones above
118
+ * the root (13 b9, 15 #9, 17 11, 18 #11, 21 13): only typed symbols
119
+ * such as `C11`, `G13` or `A7b9` carry them (0.6.1).
120
+ */
121
+ tensions?: readonly number[] | undefined;
122
+ /**
123
+ * The root's letter, 0..6 for C..B, when a roman numeral spelled it:
124
+ * `bVII` in C names Bb (not A#) and `vii` in F# names E# (not F).
125
+ */
126
+ letter?: number | undefined;
114
127
  }>;
115
128
 
116
129
  export function makeChord(
@@ -118,6 +131,7 @@ export function makeChord(
118
131
  quality: Quality,
119
132
  extensions: Iterable<Extension> = [],
120
133
  bass?: number,
134
+ tensions: readonly number[] = [],
121
135
  ): Chord {
122
136
  const held = new Set(extensions);
123
137
  const own = SECRET_EXTENSION[quality];
@@ -129,6 +143,11 @@ export function makeChord(
129
143
  quality,
130
144
  extensions: Object.freeze(ext),
131
145
  ...(slash !== undefined && slash !== pc ? { bass: slash } : {}),
146
+ ...(tensions.length > 0
147
+ ? {
148
+ tensions: Object.freeze([...new Set(tensions)].sort((a, b) => a - b)),
149
+ }
150
+ : {}),
132
151
  });
133
152
  }
134
153
 
@@ -136,6 +155,7 @@ export function makeChord(
136
155
  export function chordIntervals(chord: Chord): number[] {
137
156
  const set = new Set(QUALITY_INTERVALS[chord.quality]);
138
157
  for (const ext of chord.extensions) set.add(EXTENSION_INTERVAL[ext]);
158
+ for (const step of chord.tensions ?? []) set.add(step);
139
159
  // m7 and M7 together keep both; 6 with m7 on a dim triad is the dim7's bb7.
140
160
  return [...set].sort((a, b) => a - b);
141
161
  }
@@ -182,12 +202,43 @@ export function noteName(pc: number, flats = false): string {
182
202
  return (flats ? FLAT_NAMES : SHARP_NAMES)[mod12(pc)]!;
183
203
  }
184
204
 
205
+ const LETTERS = "CDEFGAB";
206
+ const LETTER_PCS = [0, 2, 4, 5, 7, 9, 11] as const;
207
+
208
+ /**
209
+ * `pc` spelled on letter `letter` (0..6, C..B) with one accidental at
210
+ * most (E#, Cb, Bb); undefined when that would need a double accidental.
211
+ */
212
+ export function spellOnLetter(pc: number, letter: number): string | undefined {
213
+ const l = ((Math.trunc(letter) % 7) + 7) % 7;
214
+ const diff = ((mod12(pc) - LETTER_PCS[l]! + 18) % 12) - 6;
215
+ if (Math.abs(diff) > 1) return undefined;
216
+ return `${LETTERS[l]}${diff === 1 ? "#" : diff === -1 ? "b" : ""}`;
217
+ }
218
+
219
+ /** The letter (0..6, C..B) a key's tonic is spelled on. */
220
+ function tonicLetter(key: Key): number {
221
+ return LETTERS.indexOf(noteName(key.tonic, keyUsesFlats(key))[0]!);
222
+ }
223
+
185
224
  const SECRET_SUFFIX: Readonly<Partial<Record<Quality, string>>> = Object.freeze(
186
225
  { madd4: "m(add4)", mb6: "m(b6)", b6: "(b6)", "7#9": "7#9" },
187
226
  );
188
227
 
189
228
  /** Chord symbol suffix: `m7`, `maj9`, `7sus4`, `dim7`, `m7b5`, `6/9`. */
190
229
  export function chordSuffix(chord: Chord): string {
230
+ if (chord.tensions?.length) {
231
+ // A typed extended chord keeps the symbol it was typed as.
232
+ const typed = TENSION_SUFFIXES.find(
233
+ ([, quality, ext, tensions]) =>
234
+ quality === chord.quality &&
235
+ ext.join() === chord.extensions.join() &&
236
+ tensions.join() === chord.tensions!.join(),
237
+ );
238
+ if (typed) return typed[0];
239
+ const names = chord.tensions.map((step) => TENSION_NAMES[step] ?? step);
240
+ return `${chordSuffix({ ...chord, tensions: undefined })}(${names.join(",")})`;
241
+ }
191
242
  const ext = new Set(chord.extensions);
192
243
  const b7 = ext.has("m7");
193
244
  const M7 = ext.has("M7");
@@ -248,6 +299,9 @@ export function chordSuffix(chord: Chord): string {
248
299
  base = `${seventh}(no3)`;
249
300
  if (nine) extras.push("9");
250
301
  break;
302
+ case "b5":
303
+ base = `${ninth}b5`;
304
+ break;
251
305
  default:
252
306
  base = seventh; // secret qualities returned above
253
307
  }
@@ -266,6 +320,7 @@ export function chordSuffix(chord: Chord): string {
266
320
  mb6: "m(b6)",
267
321
  b6: "(b6)",
268
322
  "7#9": "7#9",
323
+ b5: "(b5)",
269
324
  };
270
325
  base = triad[q];
271
326
  if (six && nine && (q === "maj" || q === "min")) return `${base}6/9`;
@@ -285,7 +340,11 @@ export function chordSuffix(chord: Chord): string {
285
340
  export function chordName(chord: Chord, flats = false): string {
286
341
  const slash =
287
342
  chord.bass === undefined ? "" : `/${noteName(chord.bass, flats)}`;
288
- return `${noteName(chord.root, flats)}${chordSuffix(chord)}${slash}`;
343
+ const root =
344
+ (chord.letter === undefined
345
+ ? undefined
346
+ : spellOnLetter(chord.root, chord.letter)) ?? noteName(chord.root, flats);
347
+ return `${root}${chordSuffix(chord)}${slash}`;
289
348
  }
290
349
 
291
350
  /** Suffix → quality and extensions, longest first when parsing. */
@@ -350,10 +409,74 @@ const SUFFIXES: readonly (readonly [string, Quality, readonly Extension[]])[] =
350
409
  ["(b6)", "b6", []],
351
410
  ["addb6", "b6", []],
352
411
  ["7#9", "7#9", []],
412
+ // 0.7: more spellings of the same chords.
413
+ ["-maj7", "min", ["M7"]],
414
+ ["mmaj7", "min", ["M7"]],
415
+ ["mMaj7", "min", ["M7"]],
416
+ ["m(maj7)", "min", ["M7"]],
417
+ ["-Δ7", "min", ["M7"]],
418
+ ["mΔ7", "min", ["M7"]],
419
+ ["add2", "maj", ["9"]],
420
+ ["2", "maj", ["9"]],
421
+ ["madd2", "min", ["9"]],
422
+ ["6add9", "maj", ["6", "9"]],
423
+ ["m6add9", "min", ["6", "9"]],
424
+ ["-6", "min", ["6"]],
425
+ ["-9", "min", ["m7", "9"]],
426
+ ["dom9", "maj", ["m7", "9"]],
427
+ ["aug(maj7)", "aug", ["M7"]],
428
+ ["augmaj7", "aug", ["M7"]],
429
+ ["+maj7", "aug", ["M7"]],
430
+ ["aug9", "aug", ["m7", "9"]],
431
+ ["+9", "aug", ["m7", "9"]],
432
+ ["m(maj9)", "min", ["M7", "9"]],
433
+ ["mmaj9", "min", ["M7", "9"]],
434
+ ["dim(maj7)", "dim", ["M7"]],
435
+ ["7(no3)", "5", ["m7"]],
436
+ ["maj7(no3)", "5", ["M7"]],
353
437
  ];
354
- const SUFFIX_TABLE = new Map(
355
- SUFFIXES.map(([suffix, quality, ext]) => [suffix, { quality, ext }]),
356
- );
438
+
439
+ /** Typed upper-tension chords (0.6.1): suffix, quality, buttons, tensions. */
440
+ const TENSION_SUFFIXES: readonly (readonly [
441
+ string,
442
+ Quality,
443
+ readonly Extension[],
444
+ readonly number[],
445
+ ])[] = [
446
+ ["11", "maj", ["m7", "9"], [17]],
447
+ ["m11", "min", ["m7", "9"], [17]],
448
+ ["maj11", "maj", ["M7", "9"], [17]],
449
+ ["add11", "maj", [], [17]],
450
+ ["madd11", "min", [], [17]],
451
+ ["13", "maj", ["m7", "9"], [21]],
452
+ ["m13", "min", ["m7", "9"], [21]],
453
+ ["maj13", "maj", ["M7", "9"], [21]],
454
+ ["7b9", "maj", ["m7"], [13]],
455
+ ["7#11", "maj", ["m7"], [18]],
456
+ ["maj7#11", "maj", ["M7"], [18]],
457
+ ["M7#11", "maj", ["M7"], [18]],
458
+ ["7b13", "maj", ["m7"], [20]],
459
+ ["13b9", "maj", ["m7"], [13, 21]],
460
+ ];
461
+
462
+ const TENSION_NAMES: Readonly<Record<number, string>> = Object.freeze({
463
+ 13: "b9",
464
+ 15: "#9",
465
+ 17: "11",
466
+ 18: "#11",
467
+ 20: "b13",
468
+ 21: "13",
469
+ });
470
+
471
+ const SUFFIX_TABLE = new Map<string, SuffixEntry>([
472
+ ...SUFFIXES.map(
473
+ ([suffix, quality, ext]) => [suffix, { quality, ext }] as const,
474
+ ),
475
+ ...TENSION_SUFFIXES.map(
476
+ ([suffix, quality, ext, tensions]) =>
477
+ [suffix, { quality, ext, tensions }] as const,
478
+ ),
479
+ ]);
357
480
 
358
481
  const LETTER: Readonly<Record<string, number>> = Object.freeze({
359
482
  c: 0,
@@ -378,6 +501,81 @@ export function parsePitchClass(text: string): number | undefined {
378
501
  return mod12(LETTER[match[1]!.toLowerCase()]! + accidental);
379
502
  }
380
503
 
504
+ /** Alteration → upper tension (semitones above the root). */
505
+ const ALTERATION_TENSION: Readonly<Record<string, number>> = Object.freeze({
506
+ b9: 13,
507
+ "#9": 15,
508
+ "11": 17,
509
+ "#11": 18,
510
+ "+11": 18,
511
+ b13: 20,
512
+ "13": 21,
513
+ });
514
+
515
+ const TOKEN = "b5|#5|\\+5|b9|#9|#11|\\+11|b13|alt|add9|add11|add13|11|13|9|6";
516
+ /** Bare alterations or parenthesized comma lists of them, in any order. */
517
+ const ALTERATIONS = new RegExp(
518
+ `^(?:(?:${TOKEN})|\\((?:${TOKEN})(?:,(?:${TOKEN}))*\\))+$`,
519
+ );
520
+
521
+ type SuffixEntry = {
522
+ quality: Quality;
523
+ ext: readonly Extension[];
524
+ tensions?: readonly number[];
525
+ };
526
+
527
+ /**
528
+ * A suffix the table does not list, read as a listed base followed by
529
+ * alterations, bare or in parentheses: `7#5`, `9#11`, `13#11`, `7b9b13`,
530
+ * `9b5`, `maj7+5`, `7alt`, `7(b9,#9)`. A raised fifth makes the triad
531
+ * augmented, a lowered one makes a major triad `b5` (a minor one
532
+ * diminished); `alt` is b9, #9, #11 and b13 over a dominant seventh.
533
+ */
534
+ function parseAlteredSuffix(suffix: string): SuffixEntry | undefined {
535
+ for (let cut = suffix.length - 1; cut >= 0; cut -= 1) {
536
+ const base = SUFFIX_TABLE.get(suffix.slice(0, cut));
537
+ if (!base) continue;
538
+ const raw = suffix.slice(cut);
539
+ if (!ALTERATIONS.test(raw)) continue;
540
+ const rest = raw.replace(/[(),]/g, "");
541
+ const tokens = rest.match(
542
+ /b5|#5|\+5|b9|#9|#11|\+11|b13|alt|add9|add11|add13|11|13|9|6/g,
543
+ );
544
+ if (!tokens || tokens.join("") !== rest) continue;
545
+ let quality: Quality = base.quality;
546
+ const ext = new Set<Extension>(base.ext);
547
+ const tensions = new Set<number>(base.tensions ?? []);
548
+ let ok = true;
549
+ for (const token of tokens) {
550
+ if (token === "#5" || token === "+5") {
551
+ if (quality === "maj" || quality === "aug") quality = "aug";
552
+ else ok = false;
553
+ } else if (token === "b5") {
554
+ if (quality === "maj" || quality === "b5") quality = "b5";
555
+ else if (quality === "min" || quality === "dim") quality = "dim";
556
+ else ok = false;
557
+ } else if (token === "alt") {
558
+ if (quality !== "maj") ok = false;
559
+ ext.add("m7");
560
+ for (const step of [13, 15, 18, 20]) tensions.add(step);
561
+ } else if (token === "9" || token === "add9") ext.add("9");
562
+ else if (token === "6") ext.add("6");
563
+ else {
564
+ const step = ALTERATION_TENSION[token.replace(/^add/, "")];
565
+ if (step === undefined) ok = false;
566
+ else tensions.add(step);
567
+ }
568
+ }
569
+ if (!ok) continue;
570
+ return {
571
+ quality,
572
+ ext: [...ext],
573
+ tensions: [...tensions].sort((a, b) => a - b),
574
+ };
575
+ }
576
+ return undefined;
577
+ }
578
+
381
579
  /** Parse a chord symbol (`Cm7`, `F#dim`, `Bbmaj9`, `G7sus4`, `C/E`). */
382
580
  export function parseChord(symbol: string): Chord | undefined {
383
581
  if (typeof symbol !== "string" || symbol.length > 24) return undefined;
@@ -388,14 +586,15 @@ export function parseChord(symbol: string): Chord | undefined {
388
586
  const match = trimmed.match(/^([A-Ga-g])(#|b|♯|♭)?([^/]*)(?:\/(.+))?$/);
389
587
  if (!match) return undefined;
390
588
  const root = parsePitchClass(`${match[1]}${match[2] ?? ""}`);
391
- const entry = SUFFIX_TABLE.get(match[3] ?? "");
589
+ const entry =
590
+ SUFFIX_TABLE.get(match[3] ?? "") ?? parseAlteredSuffix(match[3] ?? "");
392
591
  if (root === undefined || !entry) return undefined;
393
592
  let bass: number | undefined;
394
593
  if (match[4] !== undefined) {
395
594
  bass = parsePitchClass(match[4]);
396
595
  if (bass === undefined) return undefined;
397
596
  }
398
- return makeChord(root, entry.quality, entry.ext, bass);
597
+ return makeChord(root, entry.quality, entry.ext, bass, entry.tensions);
399
598
  }
400
599
 
401
600
  // ---------------------------------------------------------------------------
@@ -448,7 +647,15 @@ const MODE_ALIASES: Readonly<Record<string, ModeName>> = Object.freeze({
448
647
  });
449
648
 
450
649
  export type ScaleFamily =
451
- "pentatonic" | "blues" | "maqam" | "dastgah" | "raga" | "messiaen";
650
+ | "pentatonic"
651
+ | "blues"
652
+ | "maqam"
653
+ | "dastgah"
654
+ | "raga"
655
+ | "messiaen"
656
+ | "chromatic"
657
+ | "overtone"
658
+ | "quarter-tone";
452
659
 
453
660
  /**
454
661
  * Scales beyond the chord modes, for keys such as `D bayati`, `C yaman` or
@@ -495,6 +702,12 @@ export const SCALES = Object.freeze({
495
702
  family: "blues",
496
703
  aliases: ["major blues"],
497
704
  },
705
+ "yonanuki-minor": {
706
+ steps: [0, 2, 3, 7, 8],
707
+ mode: "minor",
708
+ family: "pentatonic",
709
+ aliases: ["yonanuki", "yonanuki minor", "enka minor"],
710
+ },
498
711
  hijaz: {
499
712
  steps: [0, 1, 4, 5, 7, 8, 10],
500
713
  mode: "phrygian-dominant",
@@ -671,6 +884,32 @@ export const SCALES = Object.freeze({
671
884
  mode: "harmonic-minor",
672
885
  family: "messiaen",
673
886
  },
887
+ // All twelve pitch classes: the field of free atonality and the row.
888
+ chromatic: {
889
+ steps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
890
+ mode: "minor",
891
+ family: "chromatic",
892
+ aliases: ["twelve-tone", "aggregate"],
893
+ },
894
+ // Partials 8 to 15 of the harmonic series over the tonic (Grisey,
895
+ // Murail): the acoustic scale with the 11th and 13th partials' and the
896
+ // 7th's just pitches, which its named tuning applies.
897
+ "harmonic-series": {
898
+ steps: [0, 2, 4, 6, 7, 9, 10, 11],
899
+ mode: "mixolydian",
900
+ family: "overtone",
901
+ intonation: [0, 204, 386, 551, 702, 841, 969, 1088],
902
+ aliases: ["overtone", "overtone-scale", "partials"],
903
+ },
904
+ // Each tempered degree of the major scale beside its quarter-tone
905
+ // shadow (Haba, Wyschnegradsky): neutral 2nd, 3rd, 6th and 7th and a
906
+ // quarter-sharp 4th, sounded as note cents over twelve-tone keys.
907
+ "quarter-tone": {
908
+ steps: [0, 1.5, 2, 3.5, 4, 5, 5.5, 7, 8.5, 9, 10.5],
909
+ mode: "major",
910
+ family: "quarter-tone",
911
+ aliases: ["quartertone", "24-tone"],
912
+ },
674
913
  } as const satisfies Record<string, ScaleInfo>);
675
914
  export type ScaleName = keyof typeof SCALES;
676
915
  export const SCALE_NAMES = Object.keys(SCALES) as ScaleName[];
@@ -877,9 +1116,14 @@ export function romanOf(key: Key, chord: Chord): string {
877
1116
  if (degree < 0) {
878
1117
  // Name chromatic roots against the major scale: bIII, #iv°.
879
1118
  const major = MODES.major.map((step) => mod12(key.tonic + step));
1119
+ const natural = major.indexOf(chord.root);
880
1120
  const flat = major.indexOf(mod12(chord.root + 1));
881
1121
  const sharp = major.indexOf(mod12(chord.root - 1));
882
- if (flat >= 0) {
1122
+ if (natural >= 0) {
1123
+ // A major-scale note the mode alters: ♮II in Phrygian.
1124
+ degree = natural;
1125
+ accidental = "♮";
1126
+ } else if (flat >= 0) {
883
1127
  degree = flat;
884
1128
  accidental = "b";
885
1129
  } else {
@@ -890,7 +1134,6 @@ export function romanOf(key: Key, chord: Chord): string {
890
1134
  const lower =
891
1135
  chord.quality === "min" ||
892
1136
  chord.quality === "dim" ||
893
- chord.quality === "5" ||
894
1137
  chord.quality === "madd4" ||
895
1138
  chord.quality === "mb6";
896
1139
  const numeral = NUMERALS[degree]!;
@@ -907,7 +1150,33 @@ export function romanOf(key: Key, chord: Chord): string {
907
1150
  : ext.has("M7")
908
1151
  ? "maj7"
909
1152
  : "";
910
- return `${accidental}${body}${mark}${seventh}`;
1153
+ // The short numeral when it reads back as this chord; else the chord's
1154
+ // own suffix in brackets (`I[7]`, `i[m6]`, `V[7#9]`), which is exact.
1155
+ const plain = `${accidental}${body}${mark}${seventh}`;
1156
+ const bare = makeChord(
1157
+ chord.root,
1158
+ chord.quality,
1159
+ chord.extensions,
1160
+ undefined,
1161
+ chord.tensions,
1162
+ );
1163
+ for (const candidate of [
1164
+ plain,
1165
+ `${accidental}${body}${mark}${seventh === "7" ? "dom7" : seventh}`,
1166
+ ])
1167
+ if (sameChord(parseRoman(key, candidate), bare)) return candidate;
1168
+ return `${accidental}${body}[${chordSuffix(bare)}]`;
1169
+ }
1170
+
1171
+ function sameChord(a: Chord | undefined, b: Chord): boolean {
1172
+ return (
1173
+ a !== undefined &&
1174
+ a.root === b.root &&
1175
+ a.quality === b.quality &&
1176
+ a.bass === b.bass &&
1177
+ a.extensions.join() === b.extensions.join() &&
1178
+ (a.tensions ?? []).join() === (b.tensions ?? []).join()
1179
+ );
911
1180
  }
912
1181
 
913
1182
  /**
@@ -920,31 +1189,64 @@ export function romanOf(key: Key, chord: Chord): string {
920
1189
  * major). `7` adds the diatonic seventh; `maj7`/`M7` and `dom7` are exact.
921
1190
  */
922
1191
  export function parseRoman(key: Key, text: string): Chord | undefined {
923
- if (typeof text !== "string" || text.length > 16) return undefined;
1192
+ return romanIn(key, text, tonicLetter(key));
1193
+ }
1194
+
1195
+ /** `parseRoman` with the tonic spelled on `tonic` (0..6, C..B). */
1196
+ function romanIn(key: Key, text: string, tonic: number): Chord | undefined {
1197
+ if (typeof text !== "string" || text.length > 32) return undefined;
924
1198
  const trimmed = text.trim();
1199
+ const exact = trimmed.match(
1200
+ /^(b|#|♭|♯|♮)?(vii|vi|v|iv|iii|ii|i|VII|VI|V|IV|III|II|I)\[([^\]]*)\]$/,
1201
+ );
1202
+ if (exact) {
1203
+ // `I[7]`: the numeral names the root, the bracket is a chord suffix.
1204
+ const degree = NUMERALS.indexOf(exact[2]!.toLowerCase());
1205
+ const shift =
1206
+ exact[1] === "b" || exact[1] === "♭"
1207
+ ? -1
1208
+ : exact[1] === "♮" || !exact[1]
1209
+ ? 0
1210
+ : 1;
1211
+ const root = !exact[1]
1212
+ ? scaleOf(key)[degree]
1213
+ : mod12(key.tonic + MODES.major[degree]! + shift);
1214
+ if (root === undefined) return undefined;
1215
+ const letter = (tonic + degree) % 7;
1216
+ const chord = parseChord(
1217
+ `${spellOnLetter(root, letter) ?? noteName(root)}${exact[3]!}`,
1218
+ );
1219
+ return chord && Object.freeze({ ...chord, letter });
1220
+ }
925
1221
  const slash = trimmed.match(/^(.+)\/(.+)$/);
926
1222
  if (slash) {
927
1223
  // V/x: the chord built on the degree of x in the key (secondary function).
928
- const target = parseRoman(key, slash[2]!);
1224
+ const target = romanIn(key, slash[2]!, tonic);
929
1225
  if (!target) return undefined;
930
- const sub = parseRoman({ tonic: target.root, mode: "major" }, slash[1]!);
931
- return sub;
1226
+ return romanIn(
1227
+ { tonic: target.root, mode: "major" },
1228
+ slash[1]!,
1229
+ target.letter ?? tonic,
1230
+ );
932
1231
  }
933
1232
  const match = trimmed.match(
934
- /^(b|#|♭|♯)?(vii|vi|v|iv|iii|ii|i|VII|VI|V|IV|III|II|I)(°|o|ø|\+)?(maj7|M7|dom7|7|9|maj9|6|sus4|sus2|sus|add9)?$/,
1233
+ /^(b|#|♭|♯|♮)?(vii|vi|v|iv|iii|ii|i|VII|VI|V|IV|III|II|I)(°|o|ø|\+)?(maj7|M7|dom7|7|9|maj9|6|sus4|sus2|sus|add9)?$/,
935
1234
  );
936
1235
  if (!match) return undefined;
937
1236
  const accidental =
938
- match[1] === "b" || match[1] === "♭" ? -1 : match[1] ? 1 : 0;
1237
+ match[1] === "b" || match[1] === "♭"
1238
+ ? -1
1239
+ : match[1] === "#" || match[1] === "♯"
1240
+ ? 1
1241
+ : 0;
939
1242
  const numeral = match[2]!;
940
1243
  const lower = numeral === numeral.toLowerCase();
941
1244
  const degree = NUMERALS.indexOf(numeral.toLowerCase());
942
1245
  const mark = match[3];
943
1246
  const suffix = match[4] ?? "";
944
- const root =
945
- accidental === 0
946
- ? scaleOf(key)[degree]!
947
- : mod12(key.tonic + MODES.major[degree]! + accidental);
1247
+ const root = !match[1]
1248
+ ? scaleOf(key)[degree]!
1249
+ : mod12(key.tonic + MODES.major[degree]! + accidental);
948
1250
  const inKey = degreeOf(key, root);
949
1251
  const triad = inKey === undefined ? undefined : diatonicChord(key, inKey);
950
1252
  const seventh =
@@ -998,7 +1300,10 @@ export function parseRoman(key: Key, text: string): Chord | undefined {
998
1300
  quality = "sus2";
999
1301
  break;
1000
1302
  }
1001
- return makeChord(root, quality, ext);
1303
+ return Object.freeze({
1304
+ ...makeChord(root, quality, ext),
1305
+ letter: (tonic + degree) % 7,
1306
+ });
1002
1307
  }
1003
1308
 
1004
1309
  // ---------------------------------------------------------------------------
@@ -1040,7 +1345,17 @@ export function rotate(pitches: readonly number[], steps: number): number[] {
1040
1345
 
1041
1346
  export function rootPosition(chord: Chord, anchor = 60): number[] {
1042
1347
  const rootPitch = anchor + mod12(chord.root - anchor);
1043
- return chordIntervals(chord).map((step) => rootPitch + step);
1348
+ const steps = chordIntervals(chord);
1349
+ // A natural 11 over a major 3rd and a 7th is the avoid-note clash (E
1350
+ // under F a minor 9th up in C11), so the voicing drops the 3rd: C11 is
1351
+ // C G Bb D F, the sus voicing; C13 already leaves the 11 out.
1352
+ const clash =
1353
+ steps.includes(4) &&
1354
+ steps.includes(17) &&
1355
+ (steps.includes(10) || steps.includes(11));
1356
+ return steps
1357
+ .filter((step) => !(clash && step === 4))
1358
+ .map((step) => rootPitch + step);
1044
1359
  }
1045
1360
 
1046
1361
  /** Open voicings: `open` drops the second voice from the top an octave
@@ -1209,6 +1524,8 @@ export const PERFORM_MODES = [
1209
1524
  "harp",
1210
1525
  "slop",
1211
1526
  "pattern",
1527
+ // 0.6.1: fretboard-voiced strokes (see `voiceGuitar`, `strokeVoicing`).
1528
+ "guitar",
1212
1529
  ] as const;
1213
1530
  export type PerformMode = (typeof PERFORM_MODES)[number];
1214
1531
 
@@ -1228,6 +1545,23 @@ export type PerformOptions = Readonly<{
1228
1545
  pattern?: string | number;
1229
1546
  /** 0..1. */
1230
1547
  velocity?: number;
1548
+ /** Guitar mode: the stroke grid (a STROKE_PATTERNS name or D U d u x - .). */
1549
+ strokes?: string;
1550
+ /** Guitar mode: grid step in beats, default 1/2. */
1551
+ step?: number;
1552
+ /** Guitar mode: seconds a full six-string down stroke takes, default 0.022. */
1553
+ speed?: number;
1554
+ /** Guitar mode: tempo in BPM that `speed` converts with, default 120. */
1555
+ tempo?: number;
1556
+ /** Guitar mode: the guitar's tuning, capo, hand, ring and position. */
1557
+ guitar?: GuitarSetup;
1558
+ /** Guitar mode: the chord's root pitch class (default: the lowest note). */
1559
+ root?: number;
1560
+ /**
1561
+ * Guitar mode: a slash bass pitch class that must sound lowest, even when
1562
+ * it is not a chord tone (B/E keeps its E pedal). Default: the lowest note.
1563
+ */
1564
+ slash?: number;
1231
1565
  }>;
1232
1566
 
1233
1567
  export type PerformedNote = Readonly<{
@@ -1544,6 +1878,24 @@ export function perform(
1544
1878
  }
1545
1879
  return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
1546
1880
  }
1881
+ case "guitar": {
1882
+ const bass = options.slash ?? notes[0]!;
1883
+ const voicing = voiceGuitar(
1884
+ notes.map(mod12),
1885
+ mod12(bass),
1886
+ options.root ?? mod12(bass),
1887
+ options.guitar,
1888
+ );
1889
+ if (!voicing) return notes.map((pitch) => at(pitch, start, end));
1890
+ return strokeVoicing(voicing, start, length, {
1891
+ ...(options.strokes !== undefined ? { strokes: options.strokes } : {}),
1892
+ ...(options.step !== undefined ? { step: options.step } : {}),
1893
+ ...(options.speed !== undefined ? { speed: options.speed } : {}),
1894
+ ...(options.tempo !== undefined ? { tempo: options.tempo } : {}),
1895
+ velocity,
1896
+ strings: guitarStrings(options.guitar?.tune).length,
1897
+ });
1898
+ }
1547
1899
  case "harp": {
1548
1900
  const gap = Math.max(0, options.strum ?? DEFAULT_STRUM * 2);
1549
1901
  return spanned.map((pitch, index) =>
@@ -1578,6 +1930,382 @@ export function perform(
1578
1930
  }
1579
1931
  }
1580
1932
 
1933
+ // ---------------------------------------------------------------------------
1934
+ // Fretboard (0.6.1): guitar voicings and strokes
1935
+
1936
+ /**
1937
+ * Open-string MIDI pitches, low string to high, for `guitar tune <name>`.
1938
+ * Ported from the guitar design lane (proto/guitar/strum.ts TUNINGS).
1939
+ */
1940
+ export const GUITAR_TUNINGS = Object.freeze({
1941
+ standard: [40, 45, 50, 55, 59, 64],
1942
+ dropd: [38, 45, 50, 55, 59, 64],
1943
+ doubledropd: [38, 45, 50, 55, 59, 62],
1944
+ dadgad: [38, 45, 50, 55, 57, 62],
1945
+ openg: [38, 43, 50, 55, 59, 62],
1946
+ opend: [38, 45, 50, 54, 57, 62],
1947
+ opene: [40, 47, 52, 56, 59, 64],
1948
+ halfdown: [39, 44, 49, 54, 58, 63],
1949
+ nashville: [52, 57, 62, 67, 59, 64],
1950
+ bass: [28, 33, 38, 43],
1951
+ ukulele: [67, 60, 64, 69],
1952
+ requinto: [45, 50, 55, 60, 64, 69],
1953
+ } as const satisfies Record<string, readonly number[]>);
1954
+ export type GuitarTuningName = keyof typeof GUITAR_TUNINGS;
1955
+ export const GUITAR_TUNING_NAMES = Object.keys(
1956
+ GUITAR_TUNINGS,
1957
+ ) as GuitarTuningName[];
1958
+
1959
+ /** How a guitar is set up for voicing chords (`Track.guitar`). */
1960
+ export type GuitarSetup = Readonly<{
1961
+ /** A GUITAR_TUNINGS name or open-string pitches low to high. */
1962
+ tune?: string | readonly number[];
1963
+ /** Capo fret 0..12. */
1964
+ capo?: number;
1965
+ /** Hand stretch in frets 3..6 (default 4: one fret per finger). */
1966
+ hand?: number;
1967
+ /** 0 closed shapes (no open strings) .. 1 prefer ringing open strings. */
1968
+ ring?: number;
1969
+ /** Preferred fret position 0..12 (0 open position). */
1970
+ position?: number;
1971
+ }>;
1972
+
1973
+ /** One fretted chord: frets per string low to high (-1 muted). */
1974
+ export type GuitarVoicing = Readonly<{
1975
+ frets: readonly number[];
1976
+ /** Sounding pitches, low string to high. */
1977
+ pitches: readonly number[];
1978
+ /** The string each pitch sounds on. */
1979
+ strings: readonly number[];
1980
+ }>;
1981
+
1982
+ /** Open-string pitches for a tuning name or list (standard when unknown). */
1983
+ export function guitarStrings(tune: GuitarSetup["tune"]): readonly number[] {
1984
+ if (Array.isArray(tune)) return tune as readonly number[];
1985
+ const name = String(tune ?? "standard")
1986
+ .toLowerCase()
1987
+ .replace(/[\s_-]/g, "");
1988
+ return (
1989
+ (GUITAR_TUNINGS as Record<string, readonly number[]>)[name] ??
1990
+ GUITAR_TUNINGS.standard
1991
+ );
1992
+ }
1993
+
1994
+ /**
1995
+ * Fingers a fretting uses. Strings at the lowest fret count as one barre
1996
+ * only when no open string lies between them (a barre cannot skip an open
1997
+ * string); otherwise one finger each.
1998
+ */
1999
+ export function guitarFingers(frets: readonly number[]): number {
2000
+ const fretted = frets.filter((f) => f > 0);
2001
+ if (fretted.length === 0) return 0;
2002
+ const min = Math.min(...fretted);
2003
+ const at = frets.flatMap((f, i) => (f === min ? [i] : []));
2004
+ const between = frets.slice(at[0]!, at[at.length - 1]! + 1);
2005
+ const barre = at.length > 1 && !between.includes(0);
2006
+ return (barre ? 1 : at.length) + fretted.filter((f) => f > min).length;
2007
+ }
2008
+
2009
+ /**
2010
+ * The chord tones a voicing must keep, most required first, after dropping
2011
+ * `drops` tones in the guitarist's order: the 5th, then the 9th; the root,
2012
+ * the 3rd and the 7th always stay (after the design lane's review).
2013
+ */
2014
+ function requiredTones(tones: readonly number[], root: number, drops: number) {
2015
+ // The 5th, then the 11th (only beside a 3rd: a sus4 keeps its 4th), then
2016
+ // the 9th. dawg's symbols reach an 11th through m(add4)/madd4 (+9).
2017
+ const third = tones.some((pc) => [3, 4].includes(mod12(pc - root)));
2018
+ const order = (third ? [7, 5, 2] : [7, 2]).map((step) => mod12(root + step));
2019
+ const dropped = order.filter((pc) => tones.includes(pc)).slice(0, drops);
2020
+ return tones.filter((pc) => !dropped.includes(pc));
2021
+ }
2022
+
2023
+ /**
2024
+ * The most playable fretting of pitch classes `tones` over `bass` on a
2025
+ * guitar `setup`: the bass is the lowest sounding note, every required
2026
+ * tone sounds, at most four fingers within a `hand`-fret stretch, mutes
2027
+ * only under the bass or one inside, and no barre over an open string.
2028
+ * Cost prefers more strings, low positions, open strings (by `ring`), a
2029
+ * fifth present, no doubled third, and small moves from `previous`.
2030
+ * Undefined when nothing fits. Ported from proto/guitar/strum.ts.
2031
+ */
2032
+ export function voiceGuitar(
2033
+ tones: readonly number[],
2034
+ bass: number,
2035
+ root: number,
2036
+ setup: GuitarSetup = {},
2037
+ previous?: readonly number[],
2038
+ ): GuitarVoicing | undefined {
2039
+ // Play mode voices every pad press; dense chords at a wide hand take
2040
+ // ~15 ms to search, so repeats come from a small LRU (results are frozen).
2041
+ const key = JSON.stringify([
2042
+ [...new Set([...tones, bass].map(mod12))],
2043
+ mod12(bass),
2044
+ mod12(root),
2045
+ setup.tune ?? null,
2046
+ setup.capo ?? null,
2047
+ setup.hand ?? null,
2048
+ setup.ring ?? null,
2049
+ setup.position ?? null,
2050
+ previous ?? null,
2051
+ ]);
2052
+ if (voicingCache.has(key)) {
2053
+ const hit = voicingCache.get(key);
2054
+ voicingCache.delete(key);
2055
+ voicingCache.set(key, hit);
2056
+ return hit;
2057
+ }
2058
+ const voiced = searchGuitar(tones, bass, root, setup, previous);
2059
+ voicingCache.set(key, voiced);
2060
+ if (voicingCache.size > VOICING_CACHE_SIZE)
2061
+ voicingCache.delete(voicingCache.keys().next().value!);
2062
+ return voiced;
2063
+ }
2064
+
2065
+ const VOICING_CACHE_SIZE = 256;
2066
+ const voicingCache = new Map<string, GuitarVoicing | undefined>();
2067
+
2068
+ function searchGuitar(
2069
+ tones: readonly number[],
2070
+ bass: number,
2071
+ root: number,
2072
+ setup: GuitarSetup,
2073
+ previous?: readonly number[],
2074
+ ): GuitarVoicing | undefined {
2075
+ const pcs = [...new Set([...tones, bass].map(mod12))];
2076
+ const capo = clampInt(setup.capo ?? 0, 0, 12);
2077
+ const open = guitarStrings(setup.tune).map((pitch) => pitch + capo);
2078
+ const n = open.length;
2079
+ const ring = Math.min(1, Math.max(0, setup.ring ?? 0.5));
2080
+ const stretch = clampInt(setup.hand ?? 4, 3, 6);
2081
+ const fifth = mod12(root + 7);
2082
+ const third = pcs.find((pc) => [3, 4].includes(mod12(pc - root)));
2083
+ const prev = previous ? [...previous].sort((a, b) => a - b) : undefined;
2084
+ for (let drops = 0; drops <= 3; drops += 1) {
2085
+ const required = requiredTones(pcs, root, drops);
2086
+ let best: { frets: number[]; cost: number } | undefined;
2087
+ const choose = (frets: readonly number[]) => {
2088
+ const idx = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
2089
+ if (idx.length < Math.min(n, n >= 6 ? 4 : 3)) return;
2090
+ const interior = frets.slice(idx[0]).filter((f) => f < 0).length;
2091
+ if (interior > (n >= 6 ? 1 : 0)) return;
2092
+ const pitches = idx.map((i) => open[i]! + frets[i]!);
2093
+ if (mod12(Math.min(...pitches)) !== mod12(bass)) return;
2094
+ const sounding = new Set(pitches.map(mod12));
2095
+ if (required.some((pc) => !sounding.has(pc))) return;
2096
+ const fingers = guitarFingers(frets);
2097
+ if (fingers > 4) return;
2098
+ const fretted = frets.filter((f) => f > 0);
2099
+ const span = fretted.length
2100
+ ? Math.max(...fretted) - Math.min(...fretted)
2101
+ : 0;
2102
+ if (span > stretch - 1) return;
2103
+ const opens = frets.filter((f) => f === 0).length;
2104
+ if (ring === 0 && opens > 0) return;
2105
+ const pos = fretted.length ? Math.min(...fretted) : 0;
2106
+ let cost =
2107
+ span * 0.6 +
2108
+ pos * 0.25 +
2109
+ fingers * 0.4 -
2110
+ idx.length -
2111
+ opens * 0.7 * ring;
2112
+ cost += interior * 1.5;
2113
+ if (!sounding.has(fifth) && pcs.includes(fifth)) cost += 0.6;
2114
+ if (
2115
+ third !== undefined &&
2116
+ pitches.filter((p) => mod12(p) === third).length > 1
2117
+ )
2118
+ cost += 0.5;
2119
+ if (setup.position !== undefined)
2120
+ cost += Math.abs(pos - setup.position) * 0.5;
2121
+ if (prev && prev.length) {
2122
+ const cur = [...pitches].sort((a, b) => a - b);
2123
+ let move = 0;
2124
+ for (let i = 0; i < cur.length; i += 1)
2125
+ move += Math.abs(cur[i]! - prev[Math.min(i, prev.length - 1)]!);
2126
+ cost += move * 0.05;
2127
+ }
2128
+ if (
2129
+ !best ||
2130
+ cost < best.cost - 1e-9 ||
2131
+ (Math.abs(cost - best.cost) <= 1e-9 && frets.join() < best.frets.join())
2132
+ )
2133
+ best = { frets: [...frets], cost };
2134
+ };
2135
+ for (let p = 1; p <= 12; p += 1) {
2136
+ const options = open.map((o) => {
2137
+ const list = [-1];
2138
+ if (pcs.includes(mod12(o))) list.push(0);
2139
+ for (let f = p; f < p + stretch; f += 1)
2140
+ if (pcs.includes(mod12(o + f))) list.push(f);
2141
+ return list;
2142
+ });
2143
+ const frets = new Array<number>(n).fill(-1);
2144
+ const walk = (s: number): void => {
2145
+ if (s === n) return choose(frets);
2146
+ for (const f of options[s]!) {
2147
+ frets[s] = f;
2148
+ walk(s + 1);
2149
+ }
2150
+ };
2151
+ walk(0);
2152
+ }
2153
+ if (best) {
2154
+ const { frets } = best as { frets: number[] };
2155
+ const strings = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
2156
+ return Object.freeze({
2157
+ frets: Object.freeze(frets),
2158
+ pitches: Object.freeze(strings.map((i) => open[i]! + frets[i]!)),
2159
+ strings: Object.freeze(strings),
2160
+ });
2161
+ }
2162
+ }
2163
+ return undefined;
2164
+ }
2165
+
2166
+ /** A voicing as tab, low string first: `x 3 2 0 1 0`. */
2167
+ export function guitarTab(frets: readonly number[]): string {
2168
+ return frets.map((f) => (f < 0 ? "x" : String(f))).join(" ");
2169
+ }
2170
+
2171
+ /**
2172
+ * Stroke grids for perform mode `guitar`, one character per step: D down
2173
+ * (all strings), U up (top four), d light down (top four), u light up (top
2174
+ * three), x muted chuck, `-` or `.` rest (strings ring on). After the
2175
+ * design lane's STRUM_PATTERNS.
2176
+ */
2177
+ export const STROKE_PATTERNS = Object.freeze({
2178
+ down: "D",
2179
+ folk: "D-DU-UDU",
2180
+ pop: "D-DU-UD-",
2181
+ punk: "DDDDDDDD",
2182
+ funk: "xUxUDUxUxUDUxUxU",
2183
+ reggae: "-D-D",
2184
+ waltz: "Ddd",
2185
+ jangle: "D-DUDUDU",
2186
+ island: "D-DU-UDU",
2187
+ } as const satisfies Record<string, string>);
2188
+ export const STROKE_PATTERN_NAMES = Object.keys(STROKE_PATTERNS);
2189
+ const STROKE_CHARS = /^[DUdux.\-]+$/;
2190
+
2191
+ /** Default seconds a full six-string down stroke takes (22 ms). */
2192
+ export const DEFAULT_STROKE_SPEED = 0.022;
2193
+ /** Default stroke grid step in beats (8ths). */
2194
+ export const DEFAULT_STROKE_STEP = 0.5;
2195
+
2196
+ /** A stroke grid by name, or a literal grid of D U d u x - .; undefined if bad. */
2197
+ export function strokeGrid(text: string | undefined): string | undefined {
2198
+ const raw = (text ?? "down").trim();
2199
+ const named = (STROKE_PATTERNS as Record<string, string>)[raw.toLowerCase()];
2200
+ if (named) return named;
2201
+ const grid = raw.replace(/[|\s]/g, "");
2202
+ return grid.length > 0 && grid.length <= 64 && STROKE_CHARS.test(grid)
2203
+ ? grid
2204
+ : undefined;
2205
+ }
2206
+
2207
+ /**
2208
+ * Strum fretted `voicing` from `start` for `length` beats with stroke grid
2209
+ * `strokes` (repeats every grid length, one step per `step` beats). A full
2210
+ * down stroke sweeps the six strings in `speed` seconds at `tempo` BPM;
2211
+ * each struck string rings until it is struck again or the chord ends; a
2212
+ * chuck (x) is a short muted hit that stops the strings. Velocity accents
2213
+ * downbeats and lightens upstrokes.
2214
+ */
2215
+ export function strokeVoicing(
2216
+ voicing: GuitarVoicing,
2217
+ start: number,
2218
+ length: number,
2219
+ options: Readonly<{
2220
+ strokes?: string;
2221
+ step?: number;
2222
+ speed?: number;
2223
+ tempo?: number;
2224
+ velocity?: number;
2225
+ strings?: number;
2226
+ }> = {},
2227
+ ): PerformedNote[] {
2228
+ const grid = strokeGrid(options.strokes) ?? "D";
2229
+ const step =
2230
+ options.step && options.step > 0 ? options.step : DEFAULT_STROKE_STEP;
2231
+ const tempo = options.tempo && options.tempo > 0 ? options.tempo : 120;
2232
+ const sweep =
2233
+ (Math.max(0, options.speed ?? DEFAULT_STROKE_SPEED) * tempo) / 60;
2234
+ const base = options.velocity ?? 0.8;
2235
+ const total = Math.max(2, options.strings ?? 6);
2236
+ const sounding = voicing.strings;
2237
+ const end = start + length;
2238
+ type Open = { note: PerformedNote; index: number };
2239
+ const ringing = new Map<number, Open>();
2240
+ const out: PerformedNote[] = [];
2241
+ const stop = (string: number, at: number) => {
2242
+ const held = ringing.get(string);
2243
+ if (!held) return;
2244
+ const cut = round6(
2245
+ Math.max(1e-3, Math.min(held.note.length, at - held.note.start)),
2246
+ );
2247
+ out[held.index] = { ...held.note, length: cut };
2248
+ ringing.delete(string);
2249
+ };
2250
+ const steps = Math.max(1, Math.round(length / step));
2251
+ for (let s = 0; s < steps; s += 1) {
2252
+ const ch = grid[s % grid.length]!;
2253
+ if (ch === "-" || ch === ".") continue;
2254
+ const t0 = start + s * step;
2255
+ if (t0 >= end - 1e-9) break;
2256
+ const beat = s * step;
2257
+ const accent =
2258
+ Math.abs(beat - Math.round(beat)) < 1e-9
2259
+ ? Math.round(beat) % 2 === 0
2260
+ ? 1
2261
+ : 0.92
2262
+ : 0.82;
2263
+ let order: number[];
2264
+ let velocity = base * accent;
2265
+ let ring = end - t0;
2266
+ const all = sounding.map((_, k) => k);
2267
+ switch (ch) {
2268
+ case "D":
2269
+ order = all;
2270
+ break;
2271
+ case "U":
2272
+ order = all.slice(-4).reverse();
2273
+ velocity *= 0.85;
2274
+ break;
2275
+ case "d":
2276
+ order = all.slice(-4);
2277
+ velocity *= 0.62;
2278
+ break;
2279
+ case "u":
2280
+ order = all.slice(-3).reverse();
2281
+ velocity *= 0.55;
2282
+ break;
2283
+ default:
2284
+ order = all;
2285
+ velocity *= 0.5;
2286
+ ring = Math.min(ring, 0.03 * (tempo / 60));
2287
+ }
2288
+ const first = sounding[order[0]!]!;
2289
+ order.forEach((k, position) => {
2290
+ const string = sounding[k]!;
2291
+ const at = t0 + (Math.abs(string - first) / (total - 1)) * sweep;
2292
+ if (at >= end - 1e-9) return;
2293
+ stop(string, at);
2294
+ const note: PerformedNote = {
2295
+ pitch: voicing.pitches[k]!,
2296
+ start: round6(at),
2297
+ length: round6(Math.max(1e-3, Math.min(ring, end - at))),
2298
+ velocity: round6(
2299
+ Math.max(0.05, Math.min(1, velocity * (1 - 0.04 * position))),
2300
+ ),
2301
+ };
2302
+ out.push(note);
2303
+ if (ch !== "x") ringing.set(string, { note, index: out.length - 1 });
2304
+ });
2305
+ }
2306
+ return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
2307
+ }
2308
+
1581
2309
  // ---------------------------------------------------------------------------
1582
2310
  // Progressions
1583
2311
 
@@ -1969,6 +2697,14 @@ export function renderProgression(options: RenderOptions): RenderedProgression {
1969
2697
  ...perform(chord.pitches, at, span, {
1970
2698
  ...options.perform,
1971
2699
  seed: (options.perform?.seed ?? 0) + index,
2700
+ ...(options.perform?.mode === "guitar"
2701
+ ? {
2702
+ root: options.chords[index]!.root,
2703
+ ...(options.chords[index]!.bass !== undefined
2704
+ ? { slash: options.chords[index]!.bass }
2705
+ : {}),
2706
+ }
2707
+ : {}),
1972
2708
  }),
1973
2709
  );
1974
2710
  const source = options.chords[index]!;
@@ -2000,6 +2736,6 @@ export function renderProgression(options: RenderOptions): RenderedProgression {
2000
2736
  export const CHORD_PROCESS = [
2001
2737
  "Chords (Orchid-style): pick chords from the song key's diatonic set (in C major: C Dm Em F G Am Bdim; minor keys i ii° III iv v VI VII, V major for cadences).",
2002
2738
  "Move tonic (I vi iii) → predominant (IV ii) → dominant (V vii°) → tonic; loops end on V or IV to lead home.",
2003
- "Add sevenths/9ths as colour (6, m7, M7, 9 extensions); voice-lead each chord to the inversion nearest the previous one in C3–G5 so common tones hold; put the root an octave or two below as bass.",
2739
+ "Add sevenths/9ths as color (6, m7, M7, 9 extensions); voice-lead each chord to the inversion nearest the previous one in C3–G5 so common tones hold; put the root an octave or two below as bass.",
2004
2740
  "Use suggest_progression to get voice-led chords and write_chords to write them (block, strum, arpeggio, harp, slop or a rhythm pattern; bassMode chords|unison|single|solo), instead of hand-placing chord notes with add_notes.",
2005
2741
  ].join(" ");