@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/loop.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type Track,
11
11
  } from "./score.ts";
12
12
  import type { SongMaster } from "./master.ts";
13
+ import type { SongStyle } from "./style-provenance.ts";
13
14
  import type { Tuning } from "./tuning.ts";
14
15
 
15
16
  export const LOOP_FORMAT = "track.loop/v1" as const;
@@ -30,6 +31,8 @@ export type TrackLoopV1 = Readonly<{
30
31
  notes: readonly Note[];
31
32
  /** Song master (dawg 0.5); absent means none. */
32
33
  master?: SongMaster;
34
+ /** Which style and seed made the song (quality-08); absent means none. */
35
+ style?: SongStyle;
33
36
  /** Song sections (0.5); omitted when the song has none. */
34
37
  sections?: readonly Section[];
35
38
  /** Song form (0.5); omitted when empty. */
@@ -53,11 +56,13 @@ export function encodeLoopDocument(score: TrackScore): TrackLoopV1 {
53
56
  tracks: score.tracks,
54
57
  notes: score.notes,
55
58
  ...(score.master ? { master: score.master } : {}),
59
+ ...(score.style ? { style: score.style } : {}),
56
60
  ...(score.sections.length > 0 ? { sections: score.sections } : {}),
57
61
  ...(score.form.length > 0 ? { form: score.form } : {}),
58
62
  ...(score.loopSection === undefined
59
63
  ? {}
60
64
  : { loopSection: score.loopSection }),
65
+ ...(score.calibration ? { calibration: score.calibration } : {}),
61
66
  });
62
67
  }
63
68
 
package/core/lyrics.ts ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * The lyric grammar and syllable assignment (0.7) shared by `/lyrics`,
3
+ * `set_lyrics` and the SDK's `lyrics()`. No imports: the SDK carries a copy
4
+ * between marker lines (`bun core/sdk/sync-lyrics.ts`).
5
+ */
6
+
7
+ // ---- lyrics (copied into core/sdk/v1.ts)
8
+
9
+ /** Longest lyric on one note (SCORE_LIMITS.maxLyricLength). */
10
+ export const LYRIC_LIMIT = 32;
11
+
12
+ /** One lyric token: a syllable, a held note (`_`) or a skipped note (`~`). */
13
+ export type LyricToken = Readonly<{
14
+ syl: string;
15
+ /** Index of the word the token belongs to. */
16
+ word: number;
17
+ /** First syllable of its word. */
18
+ first: boolean;
19
+ kind: "syl" | "hold" | "rest";
20
+ }>;
21
+
22
+ /**
23
+ * The lyric grammar: words split by spaces, syllables by `-`, `_` holds the
24
+ * previous syllable over the next note (melisma), `~` skips a note.
25
+ * "sun-lit morn-ing _ glow" gives sun lit morn ing _ glow.
26
+ */
27
+ export function parseLyric(text: string): LyricToken[] {
28
+ const out: LyricToken[] = [];
29
+ let word = -1;
30
+ for (const raw of text.trim().split(/\s+/u)) {
31
+ if (raw === "") continue;
32
+ if (raw === "_") out.push({ syl: "_", word, first: false, kind: "hold" });
33
+ else if (raw === "~")
34
+ out.push({ syl: "~", word, first: false, kind: "rest" });
35
+ else {
36
+ word += 1;
37
+ raw
38
+ .split("-")
39
+ .filter(Boolean)
40
+ .forEach((syl, index) =>
41
+ out.push({ syl, word, first: index === 0, kind: "syl" }),
42
+ );
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ const VOWEL = /[aeiouàáâäèéêëìíîïòóôöùúûü]/u;
49
+ /** Consonant pairs that sound as one consonant and are never split. */
50
+ const SYL_DIGRAPHS = new Set(["th", "sh", "ch", "ph", "wh", "ng", "ck", "gh"]);
51
+ /** Digraphs that end a syllable (no English word starts with them). */
52
+ const SYL_CODA_ONLY = new Set(["ng", "ck", "gh", "x"]);
53
+ /** Consonant clusters a syllable may start with (maximal onset). */
54
+ const SYL_ONSETS = new Set(
55
+ (
56
+ "bl br cl cr dr fl fr gl gr pl pr sc sk sl sm sn sp st sw tr tw dw " +
57
+ "thr shr chr phr phl spl spr str scr squ skr"
58
+ ).split(" "),
59
+ );
60
+ /** Common words ending in a silent `e` that start compounds (some-thing). */
61
+ const SYL_SILENT_E_HEADS = (
62
+ "some home life time love fire side care where there here more one " +
63
+ "make lone like name game base wide grace face place space stone bone"
64
+ ).split(" ");
65
+ /** Suffixes kept whole after a silent `e` (love-ly, care-ful). */
66
+ const SYL_SUFFIXES = ["ly", "ful", "less", "ness", "ment"];
67
+ /** Unstressed endings that close a short vowel before them (nev-er). */
68
+ const SYL_CLOSING_ENDINGS = new Set([
69
+ "er",
70
+ "en",
71
+ "el",
72
+ "et",
73
+ "ed",
74
+ "es",
75
+ "est",
76
+ "ing",
77
+ ]);
78
+
79
+ /**
80
+ * Syllables of a word typed without hyphens: a guess for English, which a
81
+ * hyphen always overrides (`nev-er`). Each run of vowels (and `y` after a
82
+ * consonant) is one syllable; a final silent `e` does not count, but a
83
+ * consonant plus `le` is its own syllable (lit-tle, ta-ble). Consonant
84
+ * pairs that sound as one (th sh ch ph wh ng ck gh) never split. Between
85
+ * vowels a cluster gives the next syllable the longest onset English
86
+ * allows (mon-ster, chil-dren); one consonant goes with the next vowel
87
+ * (ba-by, to-night) unless the previous vowel is short before an
88
+ * unstressed ending (nev-er, sing-ing). "something" gives some thing,
89
+ * "forever" for ev er.
90
+ */
91
+ export function autoSyllabify(word: string): string[] {
92
+ const w = word.toLowerCase();
93
+ // Compounds and suffixes after a silent e: some-thing, love-ly.
94
+ if (w.length >= 6) {
95
+ for (const head of SYL_SILENT_E_HEADS)
96
+ if (w.startsWith(head) && VOWEL.test(w.slice(head.length)))
97
+ return [
98
+ word.slice(0, head.length),
99
+ ...autoSyllabify(word.slice(head.length)),
100
+ ];
101
+ for (const suffix of SYL_SUFFIXES) {
102
+ const stem = w.slice(0, -suffix.length);
103
+ if (
104
+ w.endsWith(suffix) &&
105
+ stem.length >= 3 &&
106
+ stem.endsWith("e") &&
107
+ !VOWEL.test(stem[stem.length - 2]!)
108
+ )
109
+ return [
110
+ ...autoSyllabify(word.slice(0, stem.length)),
111
+ word.slice(stem.length),
112
+ ];
113
+ }
114
+ }
115
+ // Letters into units: a vowel, a consonant, a digraph, or `qu`.
116
+ type Unit = { at: number; text: string; vowel: boolean };
117
+ const units: Unit[] = [];
118
+ for (let i = 0; i < w.length;) {
119
+ const pair = w.slice(i, i + 2);
120
+ if (pair === "qu" || SYL_DIGRAPHS.has(pair)) {
121
+ units.push({ at: i, text: pair, vowel: false });
122
+ i += 2;
123
+ continue;
124
+ }
125
+ const ch = w[i]!;
126
+ const prev = units.at(-1);
127
+ const vowel =
128
+ VOWEL.test(ch) || (ch === "y" && prev !== undefined && !prev.vowel);
129
+ units.push({ at: i, text: ch, vowel });
130
+ i += 1;
131
+ }
132
+ // Vowel groups as [first unit, last unit].
133
+ const groups: [number, number][] = [];
134
+ for (let u = 0; u < units.length;) {
135
+ if (units[u]!.vowel) {
136
+ let v = u;
137
+ while (v + 1 < units.length && units[v + 1]!.vowel) v += 1;
138
+ groups.push([u, v]);
139
+ u = v + 1;
140
+ } else u += 1;
141
+ }
142
+ const lastUnit = units.length - 1;
143
+ const finalLe =
144
+ w.endsWith("le") &&
145
+ units.length >= 3 &&
146
+ units[lastUnit - 1]!.text === "l" &&
147
+ !units[lastUnit - 2]!.vowel;
148
+ const last = groups.at(-1);
149
+ if (
150
+ groups.length > 1 &&
151
+ last &&
152
+ last[0] === lastUnit &&
153
+ last[1] === lastUnit &&
154
+ units[lastUnit]!.text === "e" &&
155
+ !units[lastUnit - 1]!.vowel &&
156
+ !finalLe
157
+ )
158
+ groups.pop();
159
+ if (groups.length <= 1) return [word];
160
+ const cuts: number[] = [];
161
+ for (let g = 1; g < groups.length; g += 1) {
162
+ const prev = groups[g - 1]!;
163
+ const next = groups[g]!;
164
+ const cluster = units.slice(prev[1] + 1, next[0]);
165
+ const n = cluster.length;
166
+ const isLast = g === groups.length - 1;
167
+ let onset: number; // units of the cluster that start the next syllable
168
+ if (isLast && finalLe && n >= 2)
169
+ onset = cluster[n - 2]!.text === "ck" ? 1 : 2;
170
+ else if (n === 1) {
171
+ const unit = cluster[0]!.text;
172
+ const prevText = units
173
+ .slice(prev[0], prev[1] + 1)
174
+ .map((u) => u.text)
175
+ .join("");
176
+ const ending = w.slice(units[next[0]]!.at);
177
+ const short = prevText.length === 1 && "eiou".includes(prevText);
178
+ const closes =
179
+ SYL_CODA_ONLY.has(unit) ||
180
+ (short && isLast && SYL_CLOSING_ENDINGS.has(ending)) ||
181
+ (unit === "r" && short && units[next[0]]!.text === "e");
182
+ onset = closes ? 0 : 1;
183
+ } else {
184
+ onset = 1;
185
+ for (let k = n - 1; k >= 2; k -= 1)
186
+ if (
187
+ SYL_ONSETS.has(
188
+ cluster
189
+ .slice(n - k)
190
+ .map((u) => u.text)
191
+ .join(""),
192
+ )
193
+ ) {
194
+ onset = k;
195
+ break;
196
+ }
197
+ if (SYL_CODA_ONLY.has(cluster[n - 1]!.text)) onset = 0;
198
+ }
199
+ const first = units[next[0] - onset]!;
200
+ cuts.push(onset === 0 ? units[next[0]]!.at : first.at);
201
+ }
202
+ const out: string[] = [];
203
+ let at = 0;
204
+ for (const cut of cuts) {
205
+ out.push(word.slice(at, cut));
206
+ at = cut;
207
+ }
208
+ out.push(word.slice(at));
209
+ return out.filter(Boolean);
210
+ }
211
+
212
+ /** What `assignLyrics` put on each note, and what did not fit. */
213
+ export type LyricAssignment = Readonly<{
214
+ /** Note id to its lyric (`_` holds); notes `~` skipped are absent. */
215
+ lyrics: ReadonlyMap<string, string>;
216
+ /** Syllables left over after the last note. */
217
+ dropped: readonly string[];
218
+ /** Words split automatically. */
219
+ split: readonly string[];
220
+ }>;
221
+
222
+ /**
223
+ * Lyrics onto `notes` in time order. Hyphens split syllables as typed;
224
+ * when the text has fewer syllables than there are notes, words typed
225
+ * whole are split by `autoSyllabify`, and any notes still left hold the
226
+ * last syllable (melisma) instead of failing. Syllables past the last note
227
+ * are reported in `dropped`. Notes sharing an onset take one token, on
228
+ * the top note, with `_` on the others. Each lyric is cut to the 32-character limit.
229
+ */
230
+ export function assignLyrics(
231
+ text: string,
232
+ notes: readonly Readonly<{ id: string; startTick: number; pitch: number }>[],
233
+ ): LyricAssignment {
234
+ const sorted = [...notes].sort(
235
+ (a, b) => a.startTick - b.startTick || b.pitch - a.pitch,
236
+ );
237
+ // Notes sharing an onset (a chord or a doubled note) take one token: the
238
+ // top note carries it and the rest hold.
239
+ const ordered: (typeof sorted)[number][] = [];
240
+ const under = new Map<string, string[]>();
241
+ for (const note of sorted) {
242
+ const top = ordered.at(-1);
243
+ if (top && top.startTick === note.startTick)
244
+ under.get(top.id)!.push(note.id);
245
+ else {
246
+ ordered.push(note);
247
+ under.set(note.id, []);
248
+ }
249
+ }
250
+ let tokens = parseLyric(text);
251
+ const split: string[] = [];
252
+ if (tokens.length < ordered.length) {
253
+ // Split every word typed whole; keep the split only if it still fits.
254
+ const out: LyricToken[] = [];
255
+ const words: string[] = [];
256
+ for (const token of tokens) {
257
+ const whole =
258
+ token.kind === "syl" &&
259
+ token.first &&
260
+ !tokens.some((t) => t.word === token.word && !t.first);
261
+ if (!whole) {
262
+ out.push(token);
263
+ continue;
264
+ }
265
+ const parts = autoSyllabify(token.syl);
266
+ if (parts.length > 1) words.push(token.syl);
267
+ parts.forEach((syl, index) =>
268
+ out.push({ syl, word: token.word, first: index === 0, kind: "syl" }),
269
+ );
270
+ }
271
+ if (out.length <= ordered.length) {
272
+ tokens = out;
273
+ split.push(...words);
274
+ }
275
+ }
276
+ const lyrics = new Map<string, string>();
277
+ const limit = LYRIC_LIMIT;
278
+ ordered.forEach((note, index) => {
279
+ const token = tokens[index];
280
+ if (!token) {
281
+ if (tokens.length > 0)
282
+ for (const id of [note.id, ...under.get(note.id)!]) lyrics.set(id, "_");
283
+ return;
284
+ }
285
+ if (token.kind === "rest") return;
286
+ lyrics.set(
287
+ note.id,
288
+ token.kind === "hold" ? "_" : token.syl.slice(0, limit),
289
+ );
290
+ for (const id of under.get(note.id)!) lyrics.set(id, "_");
291
+ });
292
+ const dropped = tokens
293
+ .slice(ordered.length)
294
+ .filter((token) => token.kind === "syl")
295
+ .map((token) => token.syl);
296
+ return { lyrics, dropped, split };
297
+ }
package/core/master.ts CHANGED
@@ -105,10 +105,10 @@ export const MASTER_SPECS: Readonly<Record<MasterUnit, MasterUnitSpec>> =
105
105
  low: db(-12, 12, 0, "low shelf gain"),
106
106
  lowfreq: hz(20, 1000, 100, "low shelf corner"),
107
107
  bell1: db(-12, 12, 0, "first bell gain"),
108
- bell1freq: hz(40, 16_000, 400, "first bell centre"),
108
+ bell1freq: hz(40, 16_000, 400, "first bell center"),
109
109
  bell1q: q("first bell width: higher is narrower"),
110
110
  bell2: db(-12, 12, 0, "second bell gain"),
111
- bell2freq: hz(200, 18_000, 3_000, "second bell centre"),
111
+ bell2freq: hz(200, 18_000, 3_000, "second bell center"),
112
112
  bell2q: q("second bell width: higher is narrower"),
113
113
  high: db(-12, 12, 0, "high shelf gain"),
114
114
  highfreq: hz(1_000, 20_000, 10_000, "high shelf corner"),
@@ -222,7 +222,7 @@ export const MASTER_SPECS: Readonly<Record<MasterUnit, MasterUnitSpec>> =
222
222
  default: 120,
223
223
  step: 10,
224
224
  unit: "Hz",
225
- doc: "below this the mix is mono (keeps bass centred); 0 is off",
225
+ doc: "below this the mix is mono (keeps bass centered); 0 is off",
226
226
  },
227
227
  },
228
228
  },