@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
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Built-in carriers for instrument `vocoder` (0.7): saw, supersaw, pulse or
3
+ * noise, pitched by the track's performed notes, the song's chord timeline
4
+ * or a drone at `root`, through the track tuning. PolyBLEP edges (the synth
5
+ * oscillators' residual); supersaw unison copies alternate left and right
6
+ * on a stereo carrier. Phases are seeded from the note (never Math.random);
7
+ * noise is keyed to the absolute song sample.
8
+ */
9
+ import type { PerformedNote } from "../../../core/expression.ts";
10
+ import { noteHz, type TuningTable } from "../../../core/tuning.ts";
11
+ import type { ResolvedVocoder } from "../../../core/vocoder.ts";
12
+ import { unit } from "../dsp/rng.ts";
13
+ import { polyBlep } from "../synth/oscillators.ts";
14
+
15
+ /** Unison copies of a supersaw, in units of `spread` semitones. */
16
+ const UNISON = Object.freeze([0, -0.73, 0.73, -1.67, 1.67, -2.67, 2.67]);
17
+ /** Short fades so carrier notes never click (the vocoder adds no envelope). */
18
+ const FADE_SECONDS = 0.004;
19
+
20
+ export type CarrierSpan = Readonly<{
21
+ start: number;
22
+ length: number;
23
+ pitches: readonly number[];
24
+ /** Pitch offset in cents at `t` seconds from the span start (glide, bend, vibrato). */
25
+ cents?: (t: number) => number;
26
+ velocity: number;
27
+ seed: number;
28
+ /**
29
+ * Phase runs on the absolute song sample (chord and drone spans), so a
30
+ * window that cuts into the span hears the same wave as one pass.
31
+ */
32
+ absolute?: boolean;
33
+ }>;
34
+
35
+ export type CarrierOptions = Readonly<{
36
+ sampleRate: number;
37
+ /** Absolute song sample of index 0 (noise only). */
38
+ origin: number;
39
+ seed: number;
40
+ tuning?: TuningTable;
41
+ }>;
42
+
43
+ /**
44
+ * Adds one span (one note, chord or drone segment) of carrier into `left`
45
+ * (and `right`). Supersaw copies after the centre alternate channels.
46
+ */
47
+ export function renderCarrierSpan(
48
+ left: Float64Array,
49
+ right: Float64Array | undefined,
50
+ span: CarrierSpan,
51
+ settings: Pick<ResolvedVocoder, "carrier" | "spread">,
52
+ options: CarrierOptions,
53
+ ): void {
54
+ const { sampleRate } = options;
55
+ const end = Math.min(left.length, span.start + span.length);
56
+ const begin = Math.max(0, span.start);
57
+ if (end <= begin) return;
58
+ const fade = Math.max(1, Math.round(FADE_SECONDS * sampleRate));
59
+ const level = 0.3 * (0.4 + 0.6 * Math.max(0, Math.min(1, span.velocity)));
60
+ const envelope = (i: number): number => {
61
+ const a = i - span.start;
62
+ const b = span.start + span.length - i;
63
+ return Math.min(1, a / fade, b / fade);
64
+ };
65
+ if (settings.carrier === "noise") {
66
+ for (let i = begin; i < end; i += 1) {
67
+ const v = (unit(options.seed, options.origin + i, 7) * 2 - 1) * 0.5;
68
+ const e = envelope(i) * level;
69
+ left[i]! += v * e;
70
+ if (right) right[i]! += v * e;
71
+ }
72
+ return;
73
+ }
74
+ const copies = settings.carrier === "supersaw" ? UNISON : [0];
75
+ const voices = span.pitches.length * copies.length;
76
+ const norm = 1 / Math.sqrt(Math.max(1, voices));
77
+ span.pitches.forEach((pitch, p) => {
78
+ const base = noteHz(pitch, undefined, options.tuning);
79
+ const bend = span.cents;
80
+ copies.forEach((offset, c) => {
81
+ const hz = base * 2 ** ((offset * settings.spread) / 12);
82
+ let dt = hz / sampleRate;
83
+ if (!(dt > 0 && dt < 0.5)) return;
84
+ let phase =
85
+ settings.carrier === "supersaw" ? unit(span.seed, p * 8 + c, 3) : 0;
86
+ // Phase at `begin` (a span that starts before the buffer), or on the
87
+ // absolute song sample for a span a render window may cut into.
88
+ phase = span.absolute
89
+ ? (phase + (((options.origin + begin) * dt) % 1)) % 1
90
+ : (phase + (begin - span.start) * dt) % 1;
91
+ const weight = (c === 0 ? 1 : 0.6) * norm * level;
92
+ const side = c === 0 ? 0 : c % 2 === 1 ? -1 : 1;
93
+ for (let i = begin; i < end; i += 1) {
94
+ // Bends are read every 32 samples (control rate).
95
+ if (bend && (i - begin) % 32 === 0)
96
+ dt = Math.min(
97
+ 0.49,
98
+ (hz * 2 ** (bend((i - span.start) / sampleRate) / 1200)) /
99
+ sampleRate,
100
+ );
101
+ const v =
102
+ settings.carrier === "pulse"
103
+ ? (phase < 0.5 ? 1 : -1) +
104
+ polyBlep(phase, dt) -
105
+ polyBlep((phase + 0.5) % 1, dt)
106
+ : 2 * phase - 1 - polyBlep(phase, dt);
107
+ const s = v * weight * envelope(i);
108
+ if (!right || side === 0) {
109
+ left[i]! += s;
110
+ if (right) right[i]! += s;
111
+ } else if (side < 0) left[i]! += s * 1.4;
112
+ else right[i]! += s * 1.4;
113
+ phase += dt;
114
+ if (phase >= 1) phase -= 1;
115
+ }
116
+ });
117
+ });
118
+ }
119
+
120
+ /** Spans for `follow: notes`: one per performed note. */
121
+ export function noteSpans(
122
+ notes: readonly PerformedNote[],
123
+ span: (note: PerformedNote) => { start: number; length: number },
124
+ seedOf: (note: PerformedNote) => number,
125
+ ): CarrierSpan[] {
126
+ return notes.map((note) => {
127
+ const { start, length } = span(note);
128
+ const cents = note.performance?.cents;
129
+ return {
130
+ start,
131
+ length,
132
+ pitches: [note.pitch],
133
+ ...(cents ? { cents } : {}),
134
+ velocity: note.velocity,
135
+ seed: seedOf(note),
136
+ };
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Spans for `follow: chords`: the chord timeline's pitch classes voiced in
142
+ * the octave from `root` upward, one span per chord change; no chord, no
143
+ * carrier.
144
+ */
145
+ export function chordSpans(
146
+ changes: readonly Readonly<{
147
+ start: number;
148
+ end: number;
149
+ pcs: readonly number[];
150
+ }>[],
151
+ root: number,
152
+ seed: number,
153
+ ): CarrierSpan[] {
154
+ return changes
155
+ .filter((change) => change.pcs.length > 0 && change.end > change.start)
156
+ .map((change) => ({
157
+ start: change.start,
158
+ length: change.end - change.start,
159
+ pitches: change.pcs.map((pc) => root + ((((pc - root) % 12) + 12) % 12)),
160
+ velocity: 0.8,
161
+ // One seed for the track: a render window's chord index differs.
162
+ seed,
163
+ absolute: true,
164
+ }));
165
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Control values for one vocoder render: the resolved settings plus, for
3
+ * each automated parameter, a per-sample curve read once per 32-sample block
4
+ * on the absolute song grid (so a window and the full render agree).
5
+ */
6
+ import type { ResolvedVocoder } from "../../../core/vocoder.ts";
7
+
8
+ export const VOCODER_BLOCK = 32;
9
+
10
+ export type VocoderCurves = Partial<
11
+ Record<
12
+ | "spread"
13
+ | "width"
14
+ | "release"
15
+ | "formant"
16
+ | "unvoiced"
17
+ | "hiss"
18
+ | "depth"
19
+ | "mix"
20
+ | "gain",
21
+ Float64Array
22
+ >
23
+ >;
24
+
25
+ export type VocoderControl = Readonly<{
26
+ settings: ResolvedVocoder;
27
+ sampleRate: number;
28
+ /** Absolute song sample of index 0. */
29
+ origin: number;
30
+ seed: number;
31
+ /** Gate threshold in dBFS (auto already resolved); -120 is off. */
32
+ gateDb: number;
33
+ curves: VocoderCurves;
34
+ /** Non-zero where the envelopes hold (freeze on, or its lane at 1). */
35
+ hold?: Uint8Array;
36
+ }>;
37
+
38
+ /** A value at sample `i`: its curve when automated, else the static value. */
39
+ export function at(
40
+ control: VocoderControl,
41
+ name: keyof VocoderCurves,
42
+ i: number,
43
+ ): number {
44
+ const curve = control.curves[name];
45
+ return curve ? curve[i]! : (control.settings[name] as number);
46
+ }
47
+
48
+ /**
49
+ * Per-sample curve from `valueAt(index)`, sampled at each block's first
50
+ * song-grid sample (clamped to 0) and held for the block.
51
+ */
52
+ export function blockCurve(
53
+ n: number,
54
+ origin: number,
55
+ valueAt: (index: number) => number,
56
+ ): Float64Array {
57
+ const out = new Float64Array(n);
58
+ const phase = ((origin % VOCODER_BLOCK) + VOCODER_BLOCK) % VOCODER_BLOCK;
59
+ let i = 0;
60
+ let start = -phase;
61
+ while (i < n) {
62
+ const end = Math.min(n, start + VOCODER_BLOCK);
63
+ const value = valueAt(start);
64
+ for (; i < end; i += 1) out[i] = value;
65
+ start += VOCODER_BLOCK;
66
+ }
67
+ return out;
68
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The vocoder's detectors (0.7): the unvoiced curve (fricatives and
3
+ * breaths get a noise carrier). A sample counts as unvoiced when its share
4
+ * of energy above 4 kHz passes a `sens`-scaled threshold AND the modulator
5
+ * is aperiodic (low normalised autocorrelation), and only while the gate is
6
+ * open: either cue alone misfires on bright vowels or on reverb tails.
7
+ */
8
+ import { highpass, runBiquad } from "../dsp/bandbank.ts";
9
+ import { follow, followCoef } from "../dsp/follow.ts";
10
+ import { periodicityCurve } from "../dsp/periodicity.ts";
11
+
12
+ export function unvoicedCurve(
13
+ mod: Float64Array,
14
+ sampleRate: number,
15
+ sens: number,
16
+ origin = 0,
17
+ gate?: Float64Array,
18
+ ): Float64Array {
19
+ const hp = Float64Array.from(mod);
20
+ const filter = highpass(4000, sampleRate);
21
+ runBiquad(filter, hp);
22
+ runBiquad(filter, hp);
23
+ const high = follow(hp, 0.002, 0.02, sampleRate);
24
+ const full = follow(mod, 0.002, 0.02, sampleRate);
25
+ const periodic = periodicityCurve(mod, sampleRate, origin);
26
+ const threshold = 0.5 - 0.35 * sens;
27
+ const out = new Float64Array(mod.length);
28
+ const c = followCoef(0.004, sampleRate);
29
+ let s = 0;
30
+ for (let i = 0; i < mod.length; i += 1) {
31
+ const ratio = full[i]! > 1e-4 ? high[i]! / full[i]! : 0;
32
+ const share = Math.min(1, Math.max(0, (ratio - threshold) / 0.15));
33
+ const aperiodic = Math.min(1, Math.max(0, (0.75 - periodic[i]!) / 0.2));
34
+ const t = share * aperiodic * (gate ? gate[i]! : 1);
35
+ s = c * s + (1 - c) * t;
36
+ out[i] = s;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ /** The modulator above 5 kHz, gated: the `hiss` path that keeps "s" crisp. */
42
+ export function hissPath(
43
+ mod: Float64Array,
44
+ n: number,
45
+ sampleRate: number,
46
+ ): Float64Array {
47
+ const hp = mod.slice(0, n);
48
+ runBiquad(highpass(5000, sampleRate), hp);
49
+ return hp;
50
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * The vocoder stage and its built-in carrier (0.7). `applyVocoder` runs
3
+ * right after a carrier track's voice renders into `dry` (and `dryR`), before
4
+ * its FX_CHAIN: the modulator (another track's tap) shapes the carrier
5
+ * through a channel bank or an LPC talkbox. Output gain is fixed by the
6
+ * layout times `gain`, then a stateless soft peak guard, then `mix`; nothing
7
+ * is measured over the render span, so a window equals the full render.
8
+ * `VOCODER_ENGINE` plays instrument `vocoder`: a saw, supersaw, pulse or
9
+ * noise carrier following the track's notes, the song's chords or a drone.
10
+ */
11
+ import type { PerformedNote } from "../../../core/expression.ts";
12
+ import type { AutomationPoint, Track } from "../../../core/score.ts";
13
+ import {
14
+ resolveVocoder,
15
+ vocoderTailSeconds,
16
+ VOCODER_INSTRUMENT,
17
+ VOCODER_LANE_PARAMS,
18
+ type ResolvedVocoder,
19
+ } from "../../../core/vocoder.ts";
20
+ import { seedHash } from "../dsp/rng.ts";
21
+ import { interpolateAutomation } from "../effects/common.ts";
22
+ import { chordAt, chordTimeline } from "../granular.ts";
23
+ import type { EngineContext, InstrumentEngine } from "../instruments.ts";
24
+ import type { RenderContext } from "../wav.ts";
25
+ import { warpedSpan } from "../warp.ts";
26
+ import { channelVocode } from "./bank.ts";
27
+ import {
28
+ chordSpans,
29
+ noteSpans,
30
+ renderCarrierSpan,
31
+ type CarrierSpan,
32
+ } from "./carrier.ts";
33
+ import {
34
+ blockCurve,
35
+ type VocoderControl,
36
+ type VocoderCurves,
37
+ } from "./control.ts";
38
+ import { talkboxVocode } from "./talkbox.ts";
39
+ import { quietHold } from "../dsp/follow.ts";
40
+
41
+ export { autoGateDb } from "../dsp/follow.ts";
42
+
43
+ /** Stateless soft knee: identity below 0.9, ceiling 1.0 (window-safe). */
44
+ export function peakGuard(x: Float64Array): void {
45
+ for (let i = 0; i < x.length; i += 1) {
46
+ const v = x[i]!;
47
+ const a = Math.abs(v);
48
+ if (a > 0.9) x[i] = Math.sign(v) * (0.9 + 0.1 * Math.tanh((a - 0.9) / 0.1));
49
+ }
50
+ }
51
+
52
+ type LaneContext = Pick<
53
+ RenderContext,
54
+ "sampleRate" | "samplesPerTick" | "warp" | "seedSeconds"
55
+ >;
56
+
57
+ /** Absolute song sample of a render's index 0. */
58
+ export function originOf(
59
+ context: Pick<RenderContext, "sampleRate" | "seedSeconds">,
60
+ ): number {
61
+ return Math.round((context.seedSeconds ?? 0) * context.sampleRate);
62
+ }
63
+
64
+ /** The noise seed: the stored one, else a hash of the track id. */
65
+ export function vocoderSeed(track: Track, settings: ResolvedVocoder): number {
66
+ return settings.seed ?? seedHash(`vocoder:${track.id}`);
67
+ }
68
+
69
+ function laneOf(
70
+ track: Track,
71
+ param: string,
72
+ ): readonly AutomationPoint[] | undefined {
73
+ const lanes = track.fxAutomation as
74
+ Readonly<Record<string, readonly AutomationPoint[]>> | undefined;
75
+ const points = lanes?.[`vocoder-${param}`];
76
+ return points && points.length > 0 ? points : undefined;
77
+ }
78
+
79
+ function tickAt(context: LaneContext, index: number): number {
80
+ return context.warp
81
+ ? context.warp.tick(Math.max(0, index))
82
+ : index / context.samplesPerTick;
83
+ }
84
+
85
+ /** The control values for one render: settings, lane curves, hold. */
86
+ export function vocoderControl(
87
+ track: Track,
88
+ n: number,
89
+ context: LaneContext,
90
+ gateDb: number,
91
+ mod?: Float64Array,
92
+ ): VocoderControl {
93
+ const settings = resolveVocoder(track.vocoder);
94
+ const origin = originOf(context);
95
+ const curves: Record<string, Float64Array> = {};
96
+ let hold: Uint8Array | undefined;
97
+ for (const { param } of VOCODER_LANE_PARAMS) {
98
+ const points = laneOf(track, param);
99
+ if (!points) continue;
100
+ const fallback =
101
+ param === "freeze"
102
+ ? settings.freeze
103
+ ? 1
104
+ : 0
105
+ : (settings as Record<string, unknown>)[param];
106
+ const curve = blockCurve(n, origin, (index) =>
107
+ interpolateAutomation(points, tickAt(context, index), fallback as number),
108
+ );
109
+ if (param === "freeze") {
110
+ hold = new Uint8Array(n);
111
+ for (let i = 0; i < n; i += 1) hold[i] = curve[i]! >= 0.5 ? 1 : 0;
112
+ } else curves[param] = curve;
113
+ }
114
+ // A static freeze holds the last sung vowel through every rest (a hold
115
+ // from sample 0 would freeze the silence before the voice comes in).
116
+ if (!hold && settings.freeze)
117
+ hold = mod
118
+ ? quietHold(mod, n, context.sampleRate, gateDb)
119
+ : new Uint8Array(n).fill(1);
120
+ return {
121
+ settings,
122
+ sampleRate: context.sampleRate,
123
+ origin,
124
+ seed: vocoderSeed(track, settings),
125
+ gateDb,
126
+ curves: curves as VocoderCurves,
127
+ ...(hold ? { hold } : {}),
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Vocodes `dry` (and `dryR`) in place with modulator `mod` (the source's
133
+ * mono tap over the same span, plus any lookahead past it).
134
+ */
135
+ export function applyVocoder(
136
+ dry: Float64Array,
137
+ dryR: Float64Array | undefined,
138
+ mod: Float64Array,
139
+ track: Track,
140
+ context: LaneContext,
141
+ gateDb: number,
142
+ ): void {
143
+ const n = dry.length;
144
+ const cars = dryR ? [dry, dryR] : [dry];
145
+ const padded =
146
+ mod.length >= n
147
+ ? mod
148
+ : (() => {
149
+ const out = new Float64Array(n);
150
+ out.set(mod);
151
+ return out;
152
+ })();
153
+ const control = vocoderControl(track, n, context, gateDb, padded);
154
+ const outs =
155
+ control.settings.mode === "talkbox"
156
+ ? talkboxVocode(padded, cars, control)
157
+ : channelVocode(padded, cars, control);
158
+ const { curves, settings } = control;
159
+ outs.forEach((out, c) => {
160
+ const car = cars[c]!;
161
+ const gain = curves.gain;
162
+ if (gain || settings.gain !== 0) {
163
+ const fixed = 10 ** (settings.gain / 20);
164
+ for (let i = 0; i < n; i += 1)
165
+ out[i] = out[i]! * (gain ? 10 ** (gain[i]! / 20) : fixed);
166
+ }
167
+ peakGuard(out);
168
+ const mix = curves.mix;
169
+ if (mix || settings.mix < 1)
170
+ for (let i = 0; i < n; i += 1) {
171
+ const m = mix ? mix[i]! : settings.mix;
172
+ out[i] = out[i]! * m + car[i]! * (1 - m);
173
+ }
174
+ car.set(out.subarray(0, n));
175
+ });
176
+ }
177
+
178
+ function noteSpanOf(
179
+ note: PerformedNote,
180
+ context: EngineContext,
181
+ ): { start: number; length: number } {
182
+ if (context.warp)
183
+ return warpedSpan(context.warp, note.startTick, note.durationTicks);
184
+ const perTick =
185
+ (60 * context.sampleRate) / (context.tempoBpm * context.ticksPerBeat);
186
+ return {
187
+ start: Math.max(0, Math.floor(note.startTick * perTick)),
188
+ length: Math.max(1, Math.floor(note.durationTicks * perTick)),
189
+ };
190
+ }
191
+
192
+ function sampleAtTick(context: EngineContext, tick: number): number {
193
+ if (context.warp) return warpedSpan(context.warp, tick, 0).start;
194
+ return Math.max(0, Math.floor(tick * context.samplesPerTick));
195
+ }
196
+
197
+ type ChordChange = { start: number; end: number; pcs: readonly number[] };
198
+
199
+ const samePcs = (a: readonly number[], b: readonly number[]) =>
200
+ a.length === b.length && a.every((pc, index) => pc === b[index]);
201
+
202
+ /**
203
+ * Chord changes as the carrier holds them: neighbours with the same pitch
204
+ * classes join (a staccato bass under a held chord never re-strikes the
205
+ * pad), and a silence shorter than `hold` samples holds the chord before it.
206
+ */
207
+ export function mergeChordChanges(
208
+ changes: readonly ChordChange[],
209
+ hold: number,
210
+ ): ChordChange[] {
211
+ const merged: ChordChange[] = [];
212
+ for (const change of changes) {
213
+ if (change.end <= change.start) continue;
214
+ const last = merged[merged.length - 1];
215
+ if (
216
+ last &&
217
+ (samePcs(last.pcs, change.pcs) ||
218
+ (change.pcs.length === 0 &&
219
+ last.pcs.length > 0 &&
220
+ change.end - change.start < hold))
221
+ ) {
222
+ last.end = change.end;
223
+ continue;
224
+ }
225
+ merged.push({ ...change });
226
+ }
227
+ return merged;
228
+ }
229
+
230
+ /** The built-in carrier's spans for a track. */
231
+ export function carrierSpans(
232
+ notes: readonly PerformedNote[],
233
+ track: Track,
234
+ settings: ResolvedVocoder,
235
+ context: EngineContext,
236
+ ): CarrierSpan[] {
237
+ const seed = vocoderSeed(track, settings);
238
+ if (settings.follow === "drone")
239
+ return [
240
+ {
241
+ start: 0,
242
+ length: context.samples,
243
+ pitches: [settings.root],
244
+ velocity: 0.8,
245
+ seed,
246
+ absolute: true,
247
+ },
248
+ ];
249
+ if (settings.follow === "chords") {
250
+ const timeline = chordTimeline(context.score);
251
+ const changes: { start: number; end: number; pcs: readonly number[] }[] =
252
+ [];
253
+ const ticks = [0, ...timeline.ticks.filter((tick) => tick > 0)];
254
+ ticks.forEach((tick, index) => {
255
+ const next = ticks[index + 1];
256
+ changes.push({
257
+ start: sampleAtTick(context, tick),
258
+ end: next === undefined ? context.samples : sampleAtTick(context, next),
259
+ pcs: chordAt(timeline, tick),
260
+ });
261
+ });
262
+ return chordSpans(
263
+ mergeChordChanges(changes, Math.round(0.05 * context.sampleRate)),
264
+ settings.root,
265
+ seed,
266
+ );
267
+ }
268
+ return noteSpans(
269
+ notes,
270
+ (note) => noteSpanOf(note, context),
271
+ (note) => seedHash(`${seed}:${note.id}`),
272
+ ).map((span, index) => {
273
+ const cents = notes[index]!.cents;
274
+ if (!cents) return span;
275
+ const curve = span.cents;
276
+ return {
277
+ ...span,
278
+ cents: curve ? (t: number) => cents + curve(t) : () => cents,
279
+ };
280
+ });
281
+ }
282
+
283
+ /** Instrument `vocoder`: the built-in carrier (its field is `vocoder`). */
284
+ export const VOCODER_ENGINE: InstrumentEngine = {
285
+ id: VOCODER_INSTRUMENT,
286
+ field: "vocoder",
287
+ render(dry, dryR, notes, track, context) {
288
+ const settings = resolveVocoder(track.vocoder);
289
+ const origin = originOf(context);
290
+ for (const span of carrierSpans(notes, track, settings, context))
291
+ renderCarrierSpan(dry, dryR, span, settings, {
292
+ sampleRate: context.sampleRate,
293
+ origin,
294
+ seed: span.seed,
295
+ ...(context.tuning ? { tuning: context.tuning } : {}),
296
+ });
297
+ },
298
+ tailSeconds(track) {
299
+ return vocoderTailSeconds(track.vocoder);
300
+ },
301
+ stereo(track) {
302
+ return resolveVocoder(track.vocoder).carrier === "supersaw";
303
+ },
304
+ };