@hraness/dawg 0.6.1 → 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 (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  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 +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
@@ -21,7 +21,11 @@
21
21
  * force and falls with beta and the mute (`sord`).
22
22
  * - Slur: `slurTo` retunes over 7 ms and keeps the bow (no new attack).
23
23
  * - Tremolo: `tremhz` reverses the bow that many times a second, with a
24
- * seeded per-voice rate jitter.
24
+ * seeded per-voice rate jitter. At each reversal the bow force eases off
25
+ * with the bow speed (the friction curve narrows as dir^2), so the
26
+ * string rings through the turn instead of sticking to a slow bow and
27
+ * flattening; the pitch lock averages each stroke's middle (one update
28
+ * per stroke) instead of chasing the turns' transient periods.
25
29
  */
26
30
  import { Decimate2 } from "../dsp/oversample.ts";
27
31
  import { thiranFor } from "../dsp/interp.ts";
@@ -52,6 +56,12 @@ export const SLUR_RANGE = 24;
52
56
  */
53
57
  const LOCK_GAIN = 0.25;
54
58
  const LOCK_RANGE = 0.03;
59
+ /** Tremolo: |sin| of the reversal phase past which a stroke is steady. */
60
+ const STROKE_STEADY = 0.7;
61
+ /** Tremolo: lock step per stroke, on the stroke's mean period error. */
62
+ const STROKE_LOCK_GAIN = 0.2;
63
+ /** Tremolo: the friction slope widens at most 1 / FLOOR^2 at a turn. */
64
+ const TURN_FORCE_FLOOR = 0.05;
55
65
  /** Speed ratio between dyn 0 and dyn 1 (the review's 8:1). */
56
66
  const SPEED_RATIO = 8;
57
67
  /** Extra gain law across dyn, in dB (with the 18 dB speed law: 28 dB). */
@@ -323,6 +333,11 @@ export class BowedString {
323
333
  private readonly decim?: Decimate2;
324
334
  private readonly tremRate: number;
325
335
  private readonly tremPhase: number;
336
+ /** Tremolo: between turns, and that stroke's lock measurements. */
337
+ private steady = true;
338
+ private strokeErr = 0;
339
+ private strokeCount = 0;
340
+ private strokeSkip = 0;
326
341
  /** Internal-rate sample count. */
327
342
  private n = 0;
328
343
  private off: number;
@@ -548,6 +563,20 @@ export class BowedString {
548
563
  Math.PI * (this.tremRate * (n / this.rate) + this.tremPhase),
549
564
  );
550
565
  dir = Math.tanh(4 * s);
566
+ const steady = Math.abs(s) > STROKE_STEADY;
567
+ if (steady !== this.steady) {
568
+ // Leaving a stroke's middle: one lock step on its mean error.
569
+ if (this.steady && this.strokeCount > 0)
570
+ this.lock = clamp(
571
+ this.lock - (STROKE_LOCK_GAIN * this.strokeErr) / this.strokeCount,
572
+ -LOCK_RANGE,
573
+ LOCK_RANGE,
574
+ );
575
+ this.steady = steady;
576
+ this.strokeErr = 0;
577
+ this.strokeCount = 0;
578
+ this.strokeSkip = 0;
579
+ }
551
580
  }
552
581
  // Note-off lifts the bow: the normal force falls over `release` (the
553
582
  // friction curve narrows, STK's bow pressure), with the bow still
@@ -556,7 +585,10 @@ export class BowedString {
556
585
  // half-reflecting junction that brakes the string.
557
586
  const bowVel = this.vtarget * (on ? env : 1) * dir;
558
587
  const contact = on || env > 0 ? 1 : 0;
559
- const slope = on ? this.slope : this.slope / Math.max(1e-3, env * env);
588
+ let slope = on ? this.slope : this.slope / Math.max(1e-3, env * env);
589
+ // A turning bow presses less: the friction curve narrows with speed.
590
+ if (this.tremRate > 0)
591
+ slope /= Math.max(TURN_FORCE_FLOOR, Math.abs(dir)) ** 2;
560
592
  const bOut = bb[(this.bw - this.Nb) & mask]!;
561
593
  const raw = nb[(this.nw - this.Nn) & mask]!;
562
594
  const nOut = this.a * raw + this.tx1 - this.a * this.ty1;
@@ -566,7 +598,9 @@ export class BowedString {
566
598
  const bridgeRefl = -this.sy;
567
599
  const nutRefl = -nOut;
568
600
  const dv = bowVel - (bridgeRefl + nutRefl);
569
- let f = (Math.abs(dv * slope) + 0.75) ** -4;
601
+ const r = 1 / (Math.abs(dv * slope) + 0.75);
602
+ const r2 = r * r;
603
+ let f = r2 * r2;
570
604
  if (f > 1) f = 1;
571
605
  const nv = contact * dv * f;
572
606
  nb[this.nw & mask] = bridgeRefl + nv;
@@ -591,8 +625,9 @@ export class BowedString {
591
625
  this.bpY1 = y;
592
626
  if (!(prev < 0 && y >= 0)) return;
593
627
  const cross = n - 1 + prev / (prev - y);
594
- const last = this.lastCross;
595
- this.lastCross = cross;
628
+ // Tremolo: only periods wholly inside a stroke's middle count.
629
+ const last = this.steady ? this.lastCross : -1;
630
+ this.lastCross = this.steady ? cross : -1;
596
631
  // Settle first: the attack and a slur's glide are not steady periods.
597
632
  if (last < 0 || n < 0.05 * this.rate) return;
598
633
  if (this.slurAt >= 0 && n < this.slurAt + this.slurLen + 0.02 * this.rate)
@@ -600,6 +635,14 @@ export class BowedString {
600
635
  const period = cross - last;
601
636
  const err = period / this.target - 1;
602
637
  if (Math.abs(err) > 0.1) return;
638
+ if (this.tremRate > 0) {
639
+ // The first period of a stroke still carries the turn.
640
+ if ((this.strokeSkip += 1) > 1) {
641
+ this.strokeErr += err;
642
+ this.strokeCount += 1;
643
+ }
644
+ return;
645
+ }
603
646
  this.lock = clamp(this.lock - LOCK_GAIN * err, -LOCK_RANGE, LOCK_RANGE);
604
647
  }
605
648
 
@@ -20,6 +20,7 @@ import { interpolateAutomation } from "../effects/common.ts";
20
20
  import { seedHash, unit } from "../dsp/rng.ts";
21
21
  import type { EngineContext, InstrumentEngine } from "../instruments.ts";
22
22
  import { applyBody, bodyFor, symKeys, sympathetic } from "./body.ts";
23
+ import { keysTrim } from "../keys/calibration.ts";
23
24
  import { clamp } from "./loop.ts";
24
25
  import { PluckString, type Exciter, type PluckSpec } from "./pluck.ts";
25
26
  import {
@@ -337,6 +338,10 @@ export function renderStrings(
337
338
  const cap = Math.max(1, Math.round(values.voices as number));
338
339
  const tuning = context.tuning;
339
340
  const bowed = isBowed(values);
341
+ // Calibration 1 (q08): the keyboard string presets (the harpsichord) take
342
+ // the keys' key-tracked level trim; other presets have no row.
343
+ const trimPreset =
344
+ (context.score.calibration ?? 0) >= 1 ? track.string?.preset : undefined;
340
345
  const L = new Float64Array(total);
341
346
  const R = dryR ? new Float64Array(total) : undefined;
342
347
  const voices: Voice[] = [];
@@ -416,6 +421,7 @@ export function renderStrings(
416
421
  `${note.id}:${note.startTick + (context.seedTick ?? 0)}:string`,
417
422
  );
418
423
  const strings: { hz: number; gain: number; ratio: number }[] = [];
424
+ const trim = trimPreset ? keysTrim(trimPreset, note.pitch) : 1;
419
425
  // The course's tuning error belongs to the key, not to the strike: it
420
426
  // is seeded by track and key (so every velocity of a key beats the
421
427
  // same way) and centred, so the course's mean pitch is the table's.
@@ -430,7 +436,7 @@ export function renderStrings(
430
436
  const ratio = 2 ** ((d * 100 + jitter) / 1200);
431
437
  strings.push({
432
438
  hz: baseHz * ratio,
433
- gain: 1 / Math.sqrt(unison),
439
+ gain: trim / Math.sqrt(unison),
434
440
  ratio,
435
441
  });
436
442
  }
@@ -442,7 +448,7 @@ export function renderStrings(
442
448
  hz * 2 ** (((unit(course, unison, 1) - 0.5) * 4) / 1200);
443
449
  strings.push({
444
450
  hz: tuned,
445
- gain: oct / Math.sqrt(unison),
451
+ gain: (trim * oct) / Math.sqrt(unison),
446
452
  ratio: tuned / baseHz,
447
453
  });
448
454
  }
@@ -110,28 +110,38 @@ function triangle(phase: number): number {
110
110
  * names when a track sets synth parameters.
111
111
  */
112
112
  export function legacyWave(instrument: string, phase: number): number {
113
+ return legacyWaveFor(instrument)(phase);
114
+ }
115
+
116
+ /**
117
+ * `legacyWave` with the tone chosen once: `legacyWaveFor(name)(phase)`
118
+ * equals `legacyWave(name, phase)`, without matching the name per sample.
119
+ */
120
+ export function legacyWaveFor(instrument: string): (phase: number) => number {
113
121
  const name = instrument.trim().toLowerCase();
114
- const cycle = phase - Math.floor(phase);
115
- const sine = Math.sin(2 * Math.PI * phase);
116
- if (name.includes("square")) return cycle < 0.5 ? 1 : -1;
117
- if (name.includes("saw")) return 2 * cycle - 1;
118
- if (name.includes("triangle")) return 1 - 4 * Math.abs(cycle - 0.5);
119
- if (name.includes("bass")) {
122
+ if (name.includes("square"))
123
+ return (phase) => (phase - Math.floor(phase) < 0.5 ? 1 : -1);
124
+ if (name.includes("saw"))
125
+ return (phase) => 2 * (phase - Math.floor(phase)) - 1;
126
+ if (name.includes("triangle"))
127
+ return (phase) => 1 - 4 * Math.abs(phase - Math.floor(phase) - 0.5);
128
+ if (name.includes("bass"))
120
129
  // A rounded fundamental plus a quiet octave gives bass tracks useful weight.
121
- return Math.tanh(
122
- 0.9 * Math.sin(2 * Math.PI * phase) +
123
- 0.25 * Math.sin(4 * Math.PI * phase),
124
- );
125
- }
126
- if (name.includes("piano") || name.includes("pluck")) {
130
+ return (phase) =>
131
+ Math.tanh(
132
+ 0.9 * Math.sin(2 * Math.PI * phase) +
133
+ 0.25 * Math.sin(4 * Math.PI * phase),
134
+ );
135
+ if (name.includes("piano") || name.includes("pluck"))
127
136
  // Add stable harmonics; the envelope above supplies the note decay.
128
- const harmonic =
129
- Math.sin(4 * Math.PI * phase) * 0.28 +
130
- Math.sin(6 * Math.PI * phase) * 0.12;
131
- return Math.tanh(sine + harmonic);
132
- }
137
+ return (phase) => {
138
+ const harmonic =
139
+ Math.sin(4 * Math.PI * phase) * 0.28 +
140
+ Math.sin(6 * Math.PI * phase) * 0.12;
141
+ return Math.tanh(Math.sin(2 * Math.PI * phase) + harmonic);
142
+ };
133
143
  // Unknown instruments deliberately fall back to the original sine voice.
134
- return sine;
144
+ return (phase) => Math.sin(2 * Math.PI * phase);
135
145
  }
136
146
 
137
147
  /**
@@ -230,9 +240,9 @@ const BUILT_IN: Readonly<Record<string, OscillatorFactory>> = Object.freeze({
230
240
  pulse: (init) => (phase, increment) => pulse(phase, increment, init.width()),
231
241
  triangle: (init) => (init.partials ? additive(init) : triangle),
232
242
  user: (init) => additive(init),
233
- piano: () => (phase) => legacyWave("piano", phase),
234
- pluck: () => (phase) => legacyWave("pluck", phase),
235
- bass: () => (phase) => legacyWave("bass", phase),
243
+ piano: () => legacyWaveFor("piano"),
244
+ pluck: () => legacyWaveFor("pluck"),
245
+ bass: () => legacyWaveFor("bass"),
236
246
  white: (init) => () => (init.random() * 2 - 1) * 0.6,
237
247
  pink: (init) => pinkNoise(init.random),
238
248
  brown: (init) => {
@@ -31,6 +31,7 @@ import {
31
31
  } from "../../../core/synth.ts";
32
32
  import {
33
33
  Biquad,
34
+ Svf,
34
35
  CONTROL_SAMPLES,
35
36
  clamp,
36
37
  interpolateAutomation,
@@ -197,6 +198,8 @@ const BUTTERWORTH_Q2 = 1.3066;
197
198
  class NoteFilter {
198
199
  private readonly first = new Biquad();
199
200
  private readonly second = new Biquad();
201
+ private readonly svfFirst: Svf;
202
+ private readonly svfSecond: Svf;
200
203
  private readonly ladder = new Float64Array(4);
201
204
  private ladderG = 0;
202
205
  private ladderK = 0;
@@ -210,7 +213,22 @@ class NoteFilter {
210
213
  readonly anchor: number,
211
214
  readonly envelope: Envelope,
212
215
  private readonly sampleRate: number,
213
- ) {}
216
+ ) {
217
+ this.svfFirst = new Svf(type);
218
+ this.svfSecond = new Svf(type);
219
+ }
220
+
221
+ /**
222
+ * A negative depth sweeps the cutoff down during the attack, often to
223
+ * the 20 Hz floor within a few milliseconds. A direct-form biquad
224
+ * re-designed every control block blows up under that fall (its state
225
+ * belongs to the old coefficients), so those notes run on the SVF, whose
226
+ * state stays valid. Positive and zero depths keep the biquad, so their
227
+ * renders are unchanged.
228
+ */
229
+ private get isSvf(): boolean {
230
+ return this.depth < 0;
231
+ }
214
232
 
215
233
  private get isLadder(): boolean {
216
234
  return this.slope === "ladder" && this.type === "lpf";
@@ -231,6 +249,17 @@ class NoteFilter {
231
249
  this.ladderK = 3.9 * clamp((q - 0.707) / 11.3, 0, 1);
232
250
  return;
233
251
  }
252
+ if (this.isSvf) {
253
+ if (this.slope === "24db") {
254
+ this.svfFirst.set(frequency, BUTTERWORTH_Q1, this.sampleRate);
255
+ this.svfSecond.set(
256
+ frequency,
257
+ (q * BUTTERWORTH_Q2) / 0.707,
258
+ this.sampleRate,
259
+ );
260
+ } else this.svfFirst.set(frequency, q, this.sampleRate);
261
+ return;
262
+ }
234
263
  if (this.slope === "24db") {
235
264
  this.first.set(this.type, frequency, BUTTERWORTH_Q1, this.sampleRate);
236
265
  this.second.set(
@@ -255,6 +284,10 @@ class NoteFilter {
255
284
  stages[3]! += g * (stages[2]! - stages[3]!);
256
285
  return stages[3]! * (1 + 0.5 * this.ladderK);
257
286
  }
287
+ if (this.isSvf) {
288
+ const once = this.svfFirst.process(input);
289
+ return this.slope === "24db" ? this.svfSecond.process(once) : once;
290
+ }
258
291
  const once = this.first.process(input);
259
292
  return this.slope === "24db" ? this.second.process(once) : once;
260
293
  }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * The channel vocoder (0.7): a bank of cascaded RBJ band-passes analyses
3
+ * the modulator, each band's follower shapes the same band of the carrier.
4
+ * Ported from the reviewed prototype (proto/vocoder, DSP v2):
5
+ * - layout from bands/lo/hi/width only (sample-rate independent);
6
+ * - band k reads the modulator advanced by its group delay plus the attack;
7
+ * - formant: synthesis band k takes the envelope at fractional analysis
8
+ * index k - formant / 12 / log2(ratio);
9
+ * - enhance whitens the carrier per band (each band normalised by its own
10
+ * follower), so a dull carrier still speaks in every band;
11
+ * - fixed makeup gain from the layout only, never from the rendered span.
12
+ * A stereo carrier runs one analysis and one synthesis bank per channel.
13
+ */
14
+ import {
15
+ bandpass,
16
+ bankDesign,
17
+ runBiquad,
18
+ type Biquad,
19
+ } from "../dsp/bandbank.ts";
20
+ import { follow, followCoef, gateCurve, holdCurve } from "../dsp/follow.ts";
21
+ import { unit } from "../dsp/rng.ts";
22
+ import { at, VOCODER_BLOCK, type VocoderControl } from "./control.ts";
23
+ import { hissPath, unvoicedCurve } from "./detect.ts";
24
+
25
+ /** Fixed channel makeup from the layout only (fitted once on the voice fixture). */
26
+ export function channelMakeup(bands: number, width: number): number {
27
+ return (
28
+ (1.4 * (bands / 16) ** 0.29) / (width <= 1 ? width ** 0.35 : width ** 1.1)
29
+ );
30
+ }
31
+
32
+ /** Reference band level for depth < 1: envelope^depth * REF^(1 - depth). */
33
+ const REF = 0.02;
34
+
35
+ /** A noise sample in -1..1 at an absolute song sample. */
36
+ export function noiseSample(seed: number, songSample: number): number {
37
+ return unit(seed, songSample, 11) * 2 - 1;
38
+ }
39
+
40
+ /** Follower with an optional per-sample release curve and hold. */
41
+ function followVarying(
42
+ x: Float64Array,
43
+ attack: number,
44
+ release: number,
45
+ releaseCurve: Float64Array | undefined,
46
+ sampleRate: number,
47
+ hold?: Uint8Array,
48
+ ): Float64Array {
49
+ if (!releaseCurve) return follow(x, attack, release, sampleRate, hold);
50
+ const a = followCoef(attack, sampleRate);
51
+ const out = new Float64Array(x.length);
52
+ let e = 0;
53
+ let lastRelease = Number.NaN;
54
+ let r = 0;
55
+ for (let i = 0; i < x.length; i += 1) {
56
+ if (releaseCurve[i] !== lastRelease) {
57
+ lastRelease = releaseCurve[i]!;
58
+ r = followCoef(lastRelease, sampleRate);
59
+ }
60
+ if (!hold || hold[i] === 0) {
61
+ const v = Math.abs(x[i]!);
62
+ const c = v > e ? a : r;
63
+ e = c * e + (1 - c) * v;
64
+ }
65
+ out[i] = e;
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Runs a biquad whose coefficients may change at 32-sample blocks. */
71
+ function runBand(
72
+ x: Float64Array,
73
+ filter: Biquad,
74
+ filterAt: ((i: number) => Biquad) | undefined,
75
+ ): void {
76
+ if (!filterAt) {
77
+ runBiquad(filter, x);
78
+ return;
79
+ }
80
+ let z1 = 0;
81
+ let z2 = 0;
82
+ let f = filterAt(0);
83
+ for (let i = 0; i < x.length; i += 1) {
84
+ if (i % VOCODER_BLOCK === 0) f = filterAt(i);
85
+ const v = x[i]!;
86
+ const y = f.b0 * v + z1;
87
+ z1 = f.b1 * v - f.a1 * y + z2;
88
+ z2 = f.b2 * v - f.a2 * y;
89
+ x[i] = y;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * The band filter run twice in cascade, in one pass when the coefficients
95
+ * are fixed (the same arithmetic per sample as two passes).
96
+ */
97
+ function runBandTwice(
98
+ x: Float64Array,
99
+ filter: Biquad,
100
+ filterAt: ((i: number) => Biquad) | undefined,
101
+ ): void {
102
+ if (filterAt) {
103
+ runBand(x, filter, filterAt);
104
+ runBand(x, filter, filterAt);
105
+ return;
106
+ }
107
+ const { b0, b1, b2, a1, a2 } = filter;
108
+ let p1 = 0;
109
+ let p2 = 0;
110
+ let q1 = 0;
111
+ let q2 = 0;
112
+ for (let i = 0; i < x.length; i += 1) {
113
+ const v = x[i]!;
114
+ const y = b0 * v + p1;
115
+ p1 = b1 * v - a1 * y + p2;
116
+ p2 = b2 * v - a2 * y;
117
+ const w = b0 * y + q1;
118
+ q1 = b1 * y - a1 * w + q2;
119
+ q2 = b2 * y - a2 * w;
120
+ x[i] = w;
121
+ }
122
+ }
123
+
124
+ /** `e[i] *= g[i]` (its own function so the loop is optimised). */
125
+ function multiplyInto(e: Float64Array, g: Float64Array): void {
126
+ for (let i = 0; i < e.length; i += 1) e[i] = e[i]! * g[i]!;
127
+ }
128
+
129
+ /** Cross-fades the carrier toward level-matched noise where `u` is unvoiced. */
130
+ function blendUnvoiced(
131
+ carrier: Float64Array,
132
+ u: Float64Array,
133
+ control: VocoderControl,
134
+ seed: number,
135
+ origin: number,
136
+ sr: number,
137
+ ): void {
138
+ const level = follow(carrier, 0.01, 0.1, sr);
139
+ const curve = control.curves.unvoiced;
140
+ const fixed = control.settings.unvoiced;
141
+ for (let i = 0; i < carrier.length; i += 1) {
142
+ const w = u[i]! * (curve ? curve[i]! : fixed);
143
+ if (w <= 0) continue;
144
+ const noise = noiseSample(seed, origin + i);
145
+ carrier[i] =
146
+ carrier[i]! * (1 - w) + noise * 1.7 * Math.max(level[i]!, 0.05) * w;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * One synthesis band into `out` with a fixed formant shift and depth: the
152
+ * envelope interpolated between analysis bands `ea` and `eb` by `fr`, shaped
153
+ * by `depth`, over the carrier band `syn` (whitened by `ec` when enhancing).
154
+ * A small function of its own so the JIT optimises the loop: inlined in
155
+ * `channelVocode`, which runs once per render, it stayed in the baseline tier.
156
+ */
157
+ function mixBandFixed(
158
+ out: Float64Array,
159
+ syn: Float64Array,
160
+ ea: Float64Array,
161
+ eb: Float64Array,
162
+ fr: number,
163
+ ec: Float64Array | undefined,
164
+ depth: number,
165
+ scale: number,
166
+ ): void {
167
+ const n = out.length;
168
+ const refPow = REF ** (1 - depth);
169
+ if (ec)
170
+ for (let i = 0; i < n; i += 1) {
171
+ const e = ea[i]! * (1 - fr) + eb[i]! * fr;
172
+ const level = depth === 1 ? e : e ** depth * refPow;
173
+ const g = level / (ec[i]! + 1e-5);
174
+ out[i]! += syn[i]! * Math.min(g, 1e3) * scale;
175
+ }
176
+ else
177
+ for (let i = 0; i < n; i += 1) {
178
+ const e = ea[i]! * (1 - fr) + eb[i]! * fr;
179
+ const level = depth === 1 ? e : e ** depth * refPow;
180
+ out[i]! += syn[i]! * level * 8 * scale;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Vocodes each carrier channel with `mod`. The modulator is read up to the
186
+ * largest band advance past each output sample (zero past its end).
187
+ * Returns the wet signal per channel, after `gain` and before the peak guard
188
+ * and `mix` (`applyVocoderStage` adds those).
189
+ */
190
+ export function channelVocode(
191
+ mod: Float64Array,
192
+ cars: readonly Float64Array[],
193
+ control: VocoderControl,
194
+ ): Float64Array[] {
195
+ const { settings: p, sampleRate: sr, origin, seed } = control;
196
+ const n = Math.min(mod.length, ...cars.map((c) => c.length));
197
+ const { bands, ratio } = bankDesign(p, sr);
198
+ const nb = bands.length;
199
+ const widthCurve = control.curves.width;
200
+ // Width automation re-designs the band filters per block.
201
+ const designs = new Map<number, Biquad[]>();
202
+ const filtersFor = (width: number): Biquad[] => {
203
+ let known = designs.get(width);
204
+ if (!known) {
205
+ const octaves = Math.log2(ratio) * width * 1.55;
206
+ known = bands.map((b) =>
207
+ bandpass(Math.min(b.hz, 0.45 * sr), octaves, sr),
208
+ );
209
+ designs.set(width, known);
210
+ }
211
+ return known;
212
+ };
213
+ const filterAt = (k: number, offset: number) =>
214
+ widthCurve
215
+ ? (i: number): Biquad =>
216
+ filtersFor(widthCurve[Math.min(n - 1, i + offset)]!)[k]!
217
+ : undefined;
218
+ const gate = gateCurve(mod.subarray(0, n), sr, control.gateDb);
219
+ if (gate && control.hold) holdCurve(gate, control.hold);
220
+ const unvoicedOn = p.unvoiced > 0 || control.curves.unvoiced !== undefined;
221
+ const u = unvoicedOn
222
+ ? unvoicedCurve(mod.subarray(0, n), sr, p.sens, origin, gate)
223
+ : undefined;
224
+ // Analysis: one envelope per band.
225
+ const env: Float64Array[] = [];
226
+ const band = new Float64Array(n);
227
+ for (let k = 0; k < nb; k += 1) {
228
+ const b = bands[k]!;
229
+ if (!b.live) {
230
+ env.push(new Float64Array(n));
231
+ continue;
232
+ }
233
+ // The modulator advanced by the band's delay, zero past its end.
234
+ const from = Math.min(b.advance, mod.length);
235
+ const take = Math.min(n, mod.length - from);
236
+ band.set(mod.subarray(from, from + take));
237
+ band.fill(0, take);
238
+ runBandTwice(band, b.filter, filterAt(k, b.advance));
239
+ const e = followVarying(
240
+ band,
241
+ p.attack,
242
+ p.release,
243
+ control.curves.release,
244
+ sr,
245
+ control.hold,
246
+ );
247
+ if (gate) multiplyInto(e, gate);
248
+ env.push(e);
249
+ }
250
+ const shiftOf = (i: number): number =>
251
+ at(control, "formant", i) / 12 / Math.log2(ratio);
252
+ const staticShift = control.curves.formant ? undefined : shiftOf(0);
253
+ const synthEnv = (k: number, i: number): number => {
254
+ const pos = k - (staticShift ?? shiftOf(i));
255
+ const i0 = Math.floor(pos);
256
+ const fr = pos - i0;
257
+ const a = i0 >= 0 && i0 < nb ? env[i0]![i]! : 0;
258
+ const b = i0 + 1 >= 0 && i0 + 1 < nb ? env[i0 + 1]![i]! : 0;
259
+ return a * (1 - fr) + b * fr;
260
+ };
261
+ const scale = channelMakeup(p.bands, p.width);
262
+ const depthCurve = control.curves.depth;
263
+ const shape = (e: number, i: number): number => {
264
+ const depth = depthCurve ? depthCurve[i]! : p.depth;
265
+ return depth === 1 ? e : e ** depth * REF ** (1 - depth);
266
+ };
267
+ const hiss =
268
+ p.hiss > 0 || control.curves.hiss ? hissPath(mod, n, sr) : undefined;
269
+ const zeros = new Float64Array(n);
270
+ const outs: Float64Array[] = [];
271
+ for (const car of cars) {
272
+ const carrier = car.slice(0, n);
273
+ if (u) blendUnvoiced(carrier, u, control, seed, origin, sr);
274
+ const out = new Float64Array(n);
275
+ const syn = new Float64Array(n);
276
+ for (let k = 0; k < nb; k += 1) {
277
+ const b = bands[k]!;
278
+ if (!b.live) continue;
279
+ syn.set(carrier);
280
+ runBandTwice(syn, b.filter, filterAt(k, 0));
281
+ // The envelope this band reads: a fixed pair of analysis bands and
282
+ // weight unless the formant is automated (same arithmetic as
283
+ // `synthEnv`, without a call per sample).
284
+ let ea: Float64Array = zeros;
285
+ let eb: Float64Array = zeros;
286
+ let fr = 0;
287
+ if (staticShift !== undefined) {
288
+ const pos = k - staticShift;
289
+ const i0 = Math.floor(pos);
290
+ fr = pos - i0;
291
+ if (i0 >= 0 && i0 < nb) ea = env[i0]!;
292
+ if (i0 + 1 >= 0 && i0 + 1 < nb) eb = env[i0 + 1]!;
293
+ }
294
+ const ec = p.enhance
295
+ ? followVarying(syn, p.attack, p.release, control.curves.release, sr)
296
+ : undefined;
297
+ if (staticShift !== undefined && depthCurve === undefined)
298
+ mixBandFixed(out, syn, ea, eb, fr, ec, p.depth, scale);
299
+ else
300
+ for (let i = 0; i < n; i += 1) {
301
+ const level = shape(synthEnv(k, i), i);
302
+ if (ec) {
303
+ const g = level / (ec[i]! + 1e-5);
304
+ out[i]! += syn[i]! * Math.min(g, 1e3) * scale;
305
+ } else out[i]! += syn[i]! * level * 8 * scale;
306
+ }
307
+ }
308
+ if (hiss)
309
+ for (let i = 0; i < n; i += 1)
310
+ out[i]! += at(control, "hiss", i) * hiss[i]! * (gate ? gate[i]! : 1);
311
+ outs.push(out);
312
+ }
313
+ return outs;
314
+ }