@hraness/dawg 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Envelope followers (0.7 vocoder): one-pole attack/release on |x|, and the
3
+ * gate curve built on one. Pure functions of the input buffer, so a render
4
+ * window with enough pre-roll settles to the full render's values.
5
+ */
6
+
7
+ /** One-pole coefficient for a time constant in seconds (0 = instant). */
8
+ export function followCoef(seconds: number, sampleRate: number): number {
9
+ return seconds <= 0 ? 0 : Math.exp(-1 / (seconds * sampleRate));
10
+ }
11
+
12
+ /**
13
+ * Follows |x| with separate attack and release times. Where `hold[i]` is
14
+ * non-zero the envelope keeps its last value (the vocoder's freeze).
15
+ */
16
+ export function follow(
17
+ x: Float64Array,
18
+ attack: number,
19
+ release: number,
20
+ sampleRate: number,
21
+ hold?: Uint8Array,
22
+ ): Float64Array {
23
+ const a = followCoef(attack, sampleRate);
24
+ const r = followCoef(release, sampleRate);
25
+ const out = new Float64Array(x.length);
26
+ let e = 0;
27
+ for (let i = 0; i < x.length; i += 1) {
28
+ if (!hold || hold[i] === 0) {
29
+ const v = Math.abs(x[i]!);
30
+ const c = v > e ? a : r;
31
+ e = c * e + (1 - c) * v;
32
+ }
33
+ out[i] = e;
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /**
39
+ * Gate gain 0..1 per sample: a broadband follower against `thresholdDb`
40
+ * with a 6 dB soft knee, smoothed over 5 ms. Undefined when the gate is off
41
+ * (threshold at or below -119 dBFS).
42
+ */
43
+ export function gateCurve(
44
+ x: Float64Array,
45
+ sampleRate: number,
46
+ thresholdDb: number,
47
+ ): Float64Array | undefined {
48
+ if (thresholdDb <= -119) return undefined;
49
+ const level = follow(x, 0.001, 0.05, sampleRate);
50
+ const out = new Float64Array(x.length);
51
+ const c = followCoef(0.005, sampleRate);
52
+ let s = 0;
53
+ for (let i = 0; i < x.length; i += 1) {
54
+ const db = 20 * Math.log10(level[i]! + 1e-12);
55
+ const t = Math.min(1, Math.max(0, (db - thresholdDb) / 6));
56
+ s = c * s + (1 - c) * t;
57
+ out[i] = s;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /**
63
+ * Where `hold[i]` is non-zero, `curve` keeps its value from the sample
64
+ * before (in place): a frozen vocoder keeps its gate where it was, so the
65
+ * held vowel does not fade out once the modulator goes quiet.
66
+ */
67
+ export function holdCurve(curve: Float64Array, hold: Uint8Array): void {
68
+ for (let i = 1; i < curve.length && i < hold.length; i += 1)
69
+ if (hold[i] !== 0) curve[i] = curve[i - 1]!;
70
+ }
71
+
72
+ /**
73
+ * The static `freeze` hold: 1 wherever the modulator is quiet (under the
74
+ * gate, or under -50 dBFS when the gate is lower or off), engaged
75
+ * `lead` seconds before the voice falls quiet so the envelopes are caught
76
+ * before their release. The carrier keeps the last sung vowel through every
77
+ * rest and follows the voice while it sings. Before the first note it holds
78
+ * silence. Local (the mod span plus its lookahead), so windows agree.
79
+ */
80
+ export function quietHold(
81
+ x: Float64Array,
82
+ n: number,
83
+ sampleRate: number,
84
+ thresholdDb: number,
85
+ lead = 0.03,
86
+ ): Uint8Array {
87
+ const level = follow(x, 0.001, 0.01, sampleRate);
88
+ const floor = 10 ** (Math.max(thresholdDb, -50) / 20);
89
+ const ahead = Math.max(0, Math.round(lead * sampleRate));
90
+ const hold = new Uint8Array(n);
91
+ let nextQuiet = Infinity;
92
+ for (let i = Math.min(level.length, n + ahead) - 1; i >= 0; i -= 1) {
93
+ if (level[i]! < floor) {
94
+ // the start of a quiet run reaches back `ahead` samples
95
+ if (i === 0 || level[i - 1]! >= floor) nextQuiet = i;
96
+ if (i < n) hold[i] = 1;
97
+ } else if (i < n && nextQuiet - i <= ahead) hold[i] = 1;
98
+ }
99
+ for (let i = level.length; i < n; i += 1) hold[i] = 1;
100
+ return hold;
101
+ }
102
+
103
+ /**
104
+ * Auto gate threshold for a whole asset: the 10th-percentile 10 ms frame
105
+ * level plus 6 dB (the mean-abs follower reads about 1 dB under RMS). A
106
+ * property of the asset, so every render window agrees. -120 when silent.
107
+ */
108
+ export function autoGateDb(asset: Float64Array, sampleRate: number): number {
109
+ const hop = Math.max(1, Math.round(0.01 * sampleRate));
110
+ const levels: number[] = [];
111
+ for (let s = 0; s + hop <= asset.length; s += hop) {
112
+ let e = 0;
113
+ for (let i = s; i < s + hop; i += 1) e += asset[i]! * asset[i]!;
114
+ const db = 10 * Math.log10(e / hop + 1e-24);
115
+ if (db > -100) levels.push(db);
116
+ }
117
+ if (levels.length === 0) return -120;
118
+ levels.sort((a, b) => a - b);
119
+ return levels[Math.floor(levels.length * 0.1)]! + 6;
120
+ }
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Vowel formant tables shared by the `vowel` filter (and, from the sing
3
+ * lane, the Klatt cascade): frequency (Hz), level (dB) and bandwidth (Hz)
4
+ * per formant, plus a log-frequency morph between two vowels.
5
+ */
6
+
7
+ export type Formants = readonly (readonly [number, number, number])[];
8
+
9
+ /**
10
+ * Formant frequency (Hz), level (dB) and bandwidth (Hz). The five plain
11
+ * vowels use the classic tenor formant table (published acoustic
12
+ * phonetics data); the extended set approximates F1–F3 from standard
13
+ * vowel charts with tenor-typical F4/F5.
14
+ */
15
+ export const VOWEL_FORMANTS: Readonly<Record<string, Formants>> = Object.freeze(
16
+ {
17
+ a: [
18
+ [650, 0, 80],
19
+ [1080, -6, 90],
20
+ [2650, -7, 120],
21
+ [2900, -8, 130],
22
+ [3250, -22, 140],
23
+ ],
24
+ e: [
25
+ [400, 0, 70],
26
+ [1700, -14, 80],
27
+ [2600, -12, 100],
28
+ [3200, -14, 120],
29
+ [3580, -20, 120],
30
+ ],
31
+ i: [
32
+ [290, 0, 40],
33
+ [1870, -15, 90],
34
+ [2800, -18, 100],
35
+ [3250, -20, 120],
36
+ [3540, -30, 120],
37
+ ],
38
+ o: [
39
+ [400, 0, 40],
40
+ [800, -10, 80],
41
+ [2600, -12, 100],
42
+ [2800, -12, 120],
43
+ [3000, -26, 120],
44
+ ],
45
+ u: [
46
+ [350, 0, 40],
47
+ [600, -20, 60],
48
+ [2700, -17, 100],
49
+ [2900, -14, 120],
50
+ [3300, -26, 120],
51
+ ],
52
+ ae: extended(660, 1720, 2410),
53
+ aa: extended(710, 1100, 2540),
54
+ oe: extended(390, 1680, 2400),
55
+ ue: extended(300, 1600, 2200),
56
+ y: extended(250, 1750, 2160),
57
+ uh: extended(600, 1170, 2390),
58
+ un: extended(500, 1400, 2500),
59
+ en: extended(550, 1650, 2500),
60
+ an: extended(650, 1050, 2550),
61
+ on: extended(450, 850, 2550),
62
+ },
63
+ );
64
+
65
+ function extended(f1: number, f2: number, f3: number): Formants {
66
+ return [
67
+ [f1, 0, 70],
68
+ [f2, -8, 90],
69
+ [f3, -14, 110],
70
+ [3300, -16, 130],
71
+ [3750, -24, 140],
72
+ ];
73
+ }
74
+
75
+ /**
76
+ * Formants `morph` (0..1) of the way from `from` to `to`: frequency and
77
+ * bandwidth interpolate on a log scale (equal steps sound equal), level in
78
+ * dB. 0 returns `from`'s values exactly, 1 returns `to`'s.
79
+ */
80
+ export function morphFormants(
81
+ from: Formants,
82
+ to: Formants,
83
+ morph: number,
84
+ ): Array<[number, number, number]> {
85
+ const m = Math.min(1, Math.max(0, morph));
86
+ return from.map(([hz, level, bw], index) => {
87
+ if (m === 0) return [hz, level, bw];
88
+ const [toHz, toLevel, toBw] = to[index] ?? [hz, level, bw];
89
+ if (m === 1) return [toHz, toLevel, toBw];
90
+ return [
91
+ hz * (toHz / hz) ** m,
92
+ level + (toLevel - level) * m,
93
+ bw * (toBw / bw) ** m,
94
+ ];
95
+ });
96
+ }
97
+
98
+ // ---- sing lane: SATB tables, Klatt cascade, morph and tuning ----
99
+
100
+ /**
101
+ * Vowel formant tables and the resonators that sing them.
102
+ *
103
+ * Tables: F1-F5 frequency (Hz), level (dB), bandwidth (Hz) for soprano,
104
+ * alto, tenor and bass singers on a e i o u: the Csound manual's "Formant
105
+ * Values" appendix (the FOF table, after Sundberg's singer data). The tenor
106
+ * rows equal VOWEL_FORMANTS above.
107
+ *
108
+ * Resonator: Klatt 1980 ("Software for a cascade/parallel formant
109
+ * synthesizer", JASA 67(3)) two-pole digital resonator with unity DC gain:
110
+ * C = -exp(-2 pi BW T), B = 2 exp(-pi BW T) cos(2 pi F T), A = 1 - B - C,
111
+ * y[n] = A x[n] + B y[n-1] + C y[n-2].
112
+ * In cascade the relative formant levels come out right on their own.
113
+ */
114
+
115
+ export type Formant = readonly [hz: number, db: number, bw: number];
116
+ export type VowelTable = readonly Formant[];
117
+ export const VOICE_TYPES = ["soprano", "alto", "tenor", "bass"] as const;
118
+ export type VoiceType = (typeof VOICE_TYPES)[number];
119
+ export const VOWELS = ["a", "e", "i", "o", "u"] as const;
120
+
121
+ const row = (f: number[], db: number[], bw: number[]): VowelTable =>
122
+ f.map((hz, i) => [hz, db[i]!, bw[i]!] as const);
123
+
124
+ export const SINGER_FORMANTS: Readonly<
125
+ Record<VoiceType, Record<string, VowelTable>>
126
+ > = {
127
+ soprano: {
128
+ a: row(
129
+ [800, 1150, 2900, 3900, 4950],
130
+ [0, -6, -32, -20, -50],
131
+ [80, 90, 120, 130, 140],
132
+ ),
133
+ e: row(
134
+ [350, 2000, 2800, 3600, 4950],
135
+ [0, -20, -15, -40, -56],
136
+ [60, 100, 120, 150, 200],
137
+ ),
138
+ i: row(
139
+ [270, 2140, 2950, 3900, 4950],
140
+ [0, -12, -26, -26, -44],
141
+ [60, 90, 100, 120, 120],
142
+ ),
143
+ o: row(
144
+ [450, 800, 2830, 3800, 4950],
145
+ [0, -11, -22, -22, -50],
146
+ [70, 80, 100, 130, 135],
147
+ ),
148
+ u: row(
149
+ [325, 700, 2700, 3800, 4950],
150
+ [0, -16, -35, -40, -60],
151
+ [50, 60, 170, 180, 200],
152
+ ),
153
+ },
154
+ alto: {
155
+ a: row(
156
+ [800, 1150, 2800, 3500, 4950],
157
+ [0, -4, -20, -36, -60],
158
+ [80, 90, 120, 130, 140],
159
+ ),
160
+ e: row(
161
+ [400, 1600, 2700, 3300, 4950],
162
+ [0, -24, -30, -35, -60],
163
+ [60, 80, 120, 150, 200],
164
+ ),
165
+ i: row(
166
+ [350, 1700, 2700, 3700, 4950],
167
+ [0, -20, -30, -36, -60],
168
+ [50, 100, 120, 150, 200],
169
+ ),
170
+ o: row(
171
+ [450, 800, 2830, 3500, 4950],
172
+ [0, -9, -16, -28, -55],
173
+ [70, 80, 100, 130, 135],
174
+ ),
175
+ u: row(
176
+ [325, 700, 2530, 3500, 4950],
177
+ [0, -12, -30, -40, -64],
178
+ [50, 60, 170, 180, 200],
179
+ ),
180
+ },
181
+ tenor: {
182
+ a: row(
183
+ [650, 1080, 2650, 2900, 3250],
184
+ [0, -6, -7, -8, -22],
185
+ [80, 90, 120, 130, 140],
186
+ ),
187
+ e: row(
188
+ [400, 1700, 2600, 3200, 3580],
189
+ [0, -14, -12, -14, -20],
190
+ [70, 80, 100, 120, 120],
191
+ ),
192
+ i: row(
193
+ [290, 1870, 2800, 3250, 3540],
194
+ [0, -15, -18, -20, -30],
195
+ [40, 90, 100, 120, 120],
196
+ ),
197
+ o: row(
198
+ [400, 800, 2600, 2800, 3000],
199
+ [0, -10, -12, -12, -26],
200
+ [40, 80, 100, 120, 120],
201
+ ),
202
+ u: row(
203
+ [350, 600, 2700, 2900, 3300],
204
+ [0, -20, -17, -14, -26],
205
+ [40, 60, 100, 120, 120],
206
+ ),
207
+ },
208
+ bass: {
209
+ a: row(
210
+ [600, 1040, 2250, 2450, 2750],
211
+ [0, -7, -9, -9, -20],
212
+ [60, 70, 110, 120, 130],
213
+ ),
214
+ e: row(
215
+ [400, 1620, 2400, 2800, 3100],
216
+ [0, -12, -9, -12, -18],
217
+ [40, 80, 100, 120, 120],
218
+ ),
219
+ i: row(
220
+ [250, 1750, 2600, 3050, 3340],
221
+ [0, -30, -16, -22, -28],
222
+ [60, 90, 100, 120, 120],
223
+ ),
224
+ o: row(
225
+ [400, 750, 2400, 2600, 2900],
226
+ [0, -11, -21, -20, -40],
227
+ [40, 80, 100, 120, 120],
228
+ ),
229
+ u: row(
230
+ [350, 600, 2400, 2675, 2950],
231
+ [0, -20, -32, -28, -36],
232
+ [40, 80, 100, 120, 120],
233
+ ),
234
+ },
235
+ };
236
+
237
+ /** Morph two tables: frequencies and bandwidths in log Hz, levels in dB. */
238
+ export function morphVowel(a: VowelTable, b: VowelTable, t: number): Formant[] {
239
+ return a.map(([fa, da, ba], i) => {
240
+ const [fb, db, bb] = b[i]!;
241
+ return [
242
+ fa * (fb / fa) ** t,
243
+ da + (db - da) * t,
244
+ ba * (bb / ba) ** t,
245
+ ] as const;
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Vowel position along a sequence, e.g. "a>o>u" at t in [0,1]; plain "a"
251
+ * is constant. Unknown letters fall back to "a".
252
+ */
253
+ export function vowelAt(voice: VoiceType, spec: string, t: number): Formant[] {
254
+ const table = SINGER_FORMANTS[voice];
255
+ const names = spec.split(">").map((s) => (table[s] ? s : "a"));
256
+ if (names.length === 1) return [...table[names[0]!]!];
257
+ if (t >= 1) return [...table[names[names.length - 1]!]!];
258
+ const x = Math.min(0.999999, Math.max(0, t)) * (names.length - 1);
259
+ const i = Math.floor(x);
260
+ return morphVowel(table[names[i]!]!, table[names[i + 1]!]!, x - i);
261
+ }
262
+
263
+ /** Klatt resonator with coefficient updates at control rate. */
264
+ export class Resonator {
265
+ private a = 1;
266
+ private b = 0;
267
+ private c = 0;
268
+ private y1 = 0;
269
+ private y2 = 0;
270
+ set(hz: number, bw: number, sampleRate: number): void {
271
+ const t = 1 / sampleRate;
272
+ const f = Math.min(hz, sampleRate * 0.45);
273
+ this.c = -Math.exp(-2 * Math.PI * bw * t);
274
+ this.b = 2 * Math.exp(-Math.PI * bw * t) * Math.cos(2 * Math.PI * f * t);
275
+ this.a = 1 - this.b - this.c;
276
+ }
277
+ /** Writes the coefficients at `out[at..at+2]`; returns the next index. */
278
+ coeffsInto(out: Float64Array, at: number): number {
279
+ out[at] = this.a;
280
+ out[at + 1] = this.b;
281
+ out[at + 2] = this.c;
282
+ return at + 3;
283
+ }
284
+ /** H(e^jw) as [re, im], given cos/sin of w and 2w. */
285
+ response(cw: number, sw: number, c2w: number, s2w: number): [number, number] {
286
+ const out = new Float64Array(2);
287
+ this.responseInto(cw, sw, c2w, s2w, out);
288
+ return [out[0]!, out[1]!];
289
+ }
290
+ /** `response` written into `out[0..1]` (no allocation on the hot path). */
291
+ responseInto(
292
+ cw: number,
293
+ sw: number,
294
+ c2w: number,
295
+ s2w: number,
296
+ out: Float64Array,
297
+ ): void {
298
+ const re = 1 - this.b * cw - this.c * c2w;
299
+ const im = this.b * sw + this.c * s2w;
300
+ const d = re * re + im * im;
301
+ out[0] = (this.a * re) / d;
302
+ out[1] = (-this.a * im) / d;
303
+ }
304
+ process(x: number): number {
305
+ const y = this.a * x + this.b * this.y1 + this.c * this.y2;
306
+ this.y2 = this.y1;
307
+ this.y1 = y;
308
+ return y;
309
+ }
310
+ }
311
+
312
+ /** Constant-peak-gain band-pass (RBJ, 0 dB at centre) for parallel banks. */
313
+ export class Bandpass {
314
+ private b0 = 0;
315
+ private b2 = 0;
316
+ private a1 = 0;
317
+ private a2 = 0;
318
+ private x1 = 0;
319
+ private x2 = 0;
320
+ private y1 = 0;
321
+ private y2 = 0;
322
+ set(hz: number, bw: number, sampleRate: number): void {
323
+ const w = (2 * Math.PI * Math.min(hz, sampleRate * 0.45)) / sampleRate;
324
+ const q = Math.max(0.3, hz / Math.max(1, bw));
325
+ const alpha = Math.sin(w) / (2 * q);
326
+ const a0 = 1 + alpha;
327
+ this.b0 = alpha / a0;
328
+ this.b2 = -alpha / a0;
329
+ this.a1 = (-2 * Math.cos(w)) / a0;
330
+ this.a2 = (1 - alpha) / a0;
331
+ }
332
+ /** Writes the coefficients at `out[at..at+3]`; returns the next index. */
333
+ coeffsInto(out: Float64Array, at: number): number {
334
+ out[at] = this.b0;
335
+ out[at + 1] = this.b2;
336
+ out[at + 2] = this.a1;
337
+ out[at + 3] = this.a2;
338
+ return at + 4;
339
+ }
340
+ /** H(e^jw) as [re, im], given cos/sin of w and 2w. */
341
+ response(cw: number, sw: number, c2w: number, s2w: number): [number, number] {
342
+ const out = new Float64Array(2);
343
+ this.responseInto(cw, sw, c2w, s2w, out);
344
+ return [out[0]!, out[1]!];
345
+ }
346
+ /** `response` written into `out[0..1]` (no allocation on the hot path). */
347
+ responseInto(
348
+ cw: number,
349
+ sw: number,
350
+ c2w: number,
351
+ s2w: number,
352
+ out: Float64Array,
353
+ ): void {
354
+ const nr = this.b0 + this.b2 * c2w;
355
+ const ni = -this.b2 * s2w;
356
+ const dr = 1 + this.a1 * cw + this.a2 * c2w;
357
+ const di = -this.a1 * sw - this.a2 * s2w;
358
+ const d = dr * dr + di * di;
359
+ out[0] = (nr * dr + ni * di) / d;
360
+ out[1] = (ni * dr - nr * di) / d;
361
+ }
362
+ process(x: number): number {
363
+ const y =
364
+ this.b0 * x + this.b2 * this.x2 - this.a1 * this.y1 - this.a2 * this.y2;
365
+ this.x2 = this.x1;
366
+ this.x1 = x;
367
+ this.y2 = this.y1;
368
+ this.y1 = y;
369
+ return y;
370
+ }
371
+ }
372
+
373
+ /** Five-formant cascade (the Klatt voicing branch). */
374
+ export class Cascade {
375
+ readonly res = [
376
+ new Resonator(),
377
+ new Resonator(),
378
+ new Resonator(),
379
+ new Resonator(),
380
+ new Resonator(),
381
+ ];
382
+ set(formants: readonly Formant[], scale: number, sampleRate: number): void {
383
+ for (let i = 0; i < 5; i += 1) {
384
+ const [hz, , bw] = formants[i]!;
385
+ this.res[i]!.set(hz * scale, bw * Math.sqrt(scale), sampleRate);
386
+ }
387
+ }
388
+ process(x: number): number {
389
+ let y = x;
390
+ for (const r of this.res) y = r.process(y);
391
+ return y;
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Formant tuning for high voices (review fix; Sundberg 1987, Joliveau,
397
+ * Smith and Wolfe 2004): when f0 rises above F1 (and 2 f0 above F2), the
398
+ * singer opens the jaw so F1 tracks f0 and F2 tracks 2 f0. Applied to the
399
+ * effective (formant-scaled) frequencies; bandwidths widen in proportion.
400
+ * A no-op below the crossing, so low voices keep the table exactly.
401
+ */
402
+ export function tuneToPitch(
403
+ formants: readonly Formant[],
404
+ f0: number,
405
+ scale: number,
406
+ ): readonly Formant[] {
407
+ const need1 = (1.05 * f0) / scale;
408
+ if (
409
+ formants[0]![0] >= need1 &&
410
+ formants[1]![0] >= Math.max((2.05 * f0) / scale, formants[0]![0] * 1.25)
411
+ )
412
+ return formants; // below the crossing: the table as-is, no allocation
413
+ const out = formants.map((f) => [...f] as [number, number, number]);
414
+ const f1 = out[0]!;
415
+ const want1 = (1.05 * f0) / scale;
416
+ if (f1[0] < want1) {
417
+ f1[2] *= want1 / f1[0];
418
+ f1[0] = want1;
419
+ }
420
+ const f2 = out[1]!;
421
+ const want2 = Math.max((2.05 * f0) / scale, f1[0] * 1.25);
422
+ if (f2[0] < want2) {
423
+ f2[2] *= want2 / f2[0];
424
+ f2[0] = want2;
425
+ }
426
+ return out;
427
+ }