@hraness/dawg 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
package/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
  /**
@@ -117,6 +119,11 @@ export type Chord = Readonly<{
117
119
  * such as `C11`, `G13` or `A7b9` carry them (0.6.1).
118
120
  */
119
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;
120
127
  }>;
121
128
 
122
129
  export function makeChord(
@@ -195,6 +202,25 @@ export function noteName(pc: number, flats = false): string {
195
202
  return (flats ? FLAT_NAMES : SHARP_NAMES)[mod12(pc)]!;
196
203
  }
197
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
+
198
224
  const SECRET_SUFFIX: Readonly<Partial<Record<Quality, string>>> = Object.freeze(
199
225
  { madd4: "m(add4)", mb6: "m(b6)", b6: "(b6)", "7#9": "7#9" },
200
226
  );
@@ -273,6 +299,9 @@ export function chordSuffix(chord: Chord): string {
273
299
  base = `${seventh}(no3)`;
274
300
  if (nine) extras.push("9");
275
301
  break;
302
+ case "b5":
303
+ base = `${ninth}b5`;
304
+ break;
276
305
  default:
277
306
  base = seventh; // secret qualities returned above
278
307
  }
@@ -291,6 +320,7 @@ export function chordSuffix(chord: Chord): string {
291
320
  mb6: "m(b6)",
292
321
  b6: "(b6)",
293
322
  "7#9": "7#9",
323
+ b5: "(b5)",
294
324
  };
295
325
  base = triad[q];
296
326
  if (six && nine && (q === "maj" || q === "min")) return `${base}6/9`;
@@ -310,7 +340,11 @@ export function chordSuffix(chord: Chord): string {
310
340
  export function chordName(chord: Chord, flats = false): string {
311
341
  const slash =
312
342
  chord.bass === undefined ? "" : `/${noteName(chord.bass, flats)}`;
313
- 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}`;
314
348
  }
315
349
 
316
350
  /** Suffix → quality and extensions, longest first when parsing. */
@@ -375,6 +409,31 @@ const SUFFIXES: readonly (readonly [string, Quality, readonly Extension[]])[] =
375
409
  ["(b6)", "b6", []],
376
410
  ["addb6", "b6", []],
377
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"]],
378
437
  ];
379
438
 
380
439
  /** Typed upper-tension chords (0.6.1): suffix, quality, buttons, tensions. */
@@ -409,10 +468,7 @@ const TENSION_NAMES: Readonly<Record<number, string>> = Object.freeze({
409
468
  21: "13",
410
469
  });
411
470
 
412
- const SUFFIX_TABLE = new Map<
413
- string,
414
- { quality: Quality; ext: readonly Extension[]; tensions?: readonly number[] }
415
- >([
471
+ const SUFFIX_TABLE = new Map<string, SuffixEntry>([
416
472
  ...SUFFIXES.map(
417
473
  ([suffix, quality, ext]) => [suffix, { quality, ext }] as const,
418
474
  ),
@@ -445,6 +501,81 @@ export function parsePitchClass(text: string): number | undefined {
445
501
  return mod12(LETTER[match[1]!.toLowerCase()]! + accidental);
446
502
  }
447
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
+
448
579
  /** Parse a chord symbol (`Cm7`, `F#dim`, `Bbmaj9`, `G7sus4`, `C/E`). */
449
580
  export function parseChord(symbol: string): Chord | undefined {
450
581
  if (typeof symbol !== "string" || symbol.length > 24) return undefined;
@@ -455,7 +586,8 @@ export function parseChord(symbol: string): Chord | undefined {
455
586
  const match = trimmed.match(/^([A-Ga-g])(#|b|♯|♭)?([^/]*)(?:\/(.+))?$/);
456
587
  if (!match) return undefined;
457
588
  const root = parsePitchClass(`${match[1]}${match[2] ?? ""}`);
458
- const entry = SUFFIX_TABLE.get(match[3] ?? "");
589
+ const entry =
590
+ SUFFIX_TABLE.get(match[3] ?? "") ?? parseAlteredSuffix(match[3] ?? "");
459
591
  if (root === undefined || !entry) return undefined;
460
592
  let bass: number | undefined;
461
593
  if (match[4] !== undefined) {
@@ -515,7 +647,15 @@ const MODE_ALIASES: Readonly<Record<string, ModeName>> = Object.freeze({
515
647
  });
516
648
 
517
649
  export type ScaleFamily =
518
- "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";
519
659
 
520
660
  /**
521
661
  * Scales beyond the chord modes, for keys such as `D bayati`, `C yaman` or
@@ -562,6 +702,12 @@ export const SCALES = Object.freeze({
562
702
  family: "blues",
563
703
  aliases: ["major blues"],
564
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
+ },
565
711
  hijaz: {
566
712
  steps: [0, 1, 4, 5, 7, 8, 10],
567
713
  mode: "phrygian-dominant",
@@ -738,6 +884,32 @@ export const SCALES = Object.freeze({
738
884
  mode: "harmonic-minor",
739
885
  family: "messiaen",
740
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
+ },
741
913
  } as const satisfies Record<string, ScaleInfo>);
742
914
  export type ScaleName = keyof typeof SCALES;
743
915
  export const SCALE_NAMES = Object.keys(SCALES) as ScaleName[];
@@ -944,9 +1116,14 @@ export function romanOf(key: Key, chord: Chord): string {
944
1116
  if (degree < 0) {
945
1117
  // Name chromatic roots against the major scale: bIII, #iv°.
946
1118
  const major = MODES.major.map((step) => mod12(key.tonic + step));
1119
+ const natural = major.indexOf(chord.root);
947
1120
  const flat = major.indexOf(mod12(chord.root + 1));
948
1121
  const sharp = major.indexOf(mod12(chord.root - 1));
949
- 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) {
950
1127
  degree = flat;
951
1128
  accidental = "b";
952
1129
  } else {
@@ -957,7 +1134,6 @@ export function romanOf(key: Key, chord: Chord): string {
957
1134
  const lower =
958
1135
  chord.quality === "min" ||
959
1136
  chord.quality === "dim" ||
960
- chord.quality === "5" ||
961
1137
  chord.quality === "madd4" ||
962
1138
  chord.quality === "mb6";
963
1139
  const numeral = NUMERALS[degree]!;
@@ -974,7 +1150,33 @@ export function romanOf(key: Key, chord: Chord): string {
974
1150
  : ext.has("M7")
975
1151
  ? "maj7"
976
1152
  : "";
977
- 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
+ );
978
1180
  }
979
1181
 
980
1182
  /**
@@ -987,31 +1189,64 @@ export function romanOf(key: Key, chord: Chord): string {
987
1189
  * major). `7` adds the diatonic seventh; `maj7`/`M7` and `dom7` are exact.
988
1190
  */
989
1191
  export function parseRoman(key: Key, text: string): Chord | undefined {
990
- 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;
991
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
+ }
992
1221
  const slash = trimmed.match(/^(.+)\/(.+)$/);
993
1222
  if (slash) {
994
1223
  // V/x: the chord built on the degree of x in the key (secondary function).
995
- const target = parseRoman(key, slash[2]!);
1224
+ const target = romanIn(key, slash[2]!, tonic);
996
1225
  if (!target) return undefined;
997
- const sub = parseRoman({ tonic: target.root, mode: "major" }, slash[1]!);
998
- return sub;
1226
+ return romanIn(
1227
+ { tonic: target.root, mode: "major" },
1228
+ slash[1]!,
1229
+ target.letter ?? tonic,
1230
+ );
999
1231
  }
1000
1232
  const match = trimmed.match(
1001
- /^(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)?$/,
1002
1234
  );
1003
1235
  if (!match) return undefined;
1004
1236
  const accidental =
1005
- 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;
1006
1242
  const numeral = match[2]!;
1007
1243
  const lower = numeral === numeral.toLowerCase();
1008
1244
  const degree = NUMERALS.indexOf(numeral.toLowerCase());
1009
1245
  const mark = match[3];
1010
1246
  const suffix = match[4] ?? "";
1011
- const root =
1012
- accidental === 0
1013
- ? scaleOf(key)[degree]!
1014
- : mod12(key.tonic + MODES.major[degree]! + accidental);
1247
+ const root = !match[1]
1248
+ ? scaleOf(key)[degree]!
1249
+ : mod12(key.tonic + MODES.major[degree]! + accidental);
1015
1250
  const inKey = degreeOf(key, root);
1016
1251
  const triad = inKey === undefined ? undefined : diatonicChord(key, inKey);
1017
1252
  const seventh =
@@ -1065,7 +1300,10 @@ export function parseRoman(key: Key, text: string): Chord | undefined {
1065
1300
  quality = "sus2";
1066
1301
  break;
1067
1302
  }
1068
- return makeChord(root, quality, ext);
1303
+ return Object.freeze({
1304
+ ...makeChord(root, quality, ext),
1305
+ letter: (tonic + degree) % 7,
1306
+ });
1069
1307
  }
1070
1308
 
1071
1309
  // ---------------------------------------------------------------------------
@@ -1107,7 +1345,17 @@ export function rotate(pitches: readonly number[], steps: number): number[] {
1107
1345
 
1108
1346
  export function rootPosition(chord: Chord, anchor = 60): number[] {
1109
1347
  const rootPitch = anchor + mod12(chord.root - anchor);
1110
- 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);
1111
1359
  }
1112
1360
 
1113
1361
  /** Open voicings: `open` drops the second voice from the top an octave
@@ -2488,6 +2736,6 @@ export function renderProgression(options: RenderOptions): RenderedProgression {
2488
2736
  export const CHORD_PROCESS = [
2489
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).",
2490
2738
  "Move tonic (I vi iii) → predominant (IV ii) → dominant (V vii°) → tonic; loops end on V or IV to lead home.",
2491
- "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.",
2492
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.",
2493
2741
  ].join(" ");