@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.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* right impulses share one transform: G = FFT(hL + i·hR), and because the
|
|
11
11
|
* input is real, Re(IFFT(X·G)) is the left output and Im(…) the right.
|
|
12
12
|
*
|
|
13
|
-
* Built-in impulses (`builtin:room`, `
|
|
13
|
+
* Built-in impulses (`builtin:room`, `hall`, `plate`, `reverse`, `gate`, `spring`) are
|
|
14
14
|
* generated from a seeded integer PRNG, so they need no files and are
|
|
15
15
|
* identical on every render path. Everything here is plain float64
|
|
16
16
|
* arithmetic over fixed loops: deterministic. Clean-room, from standard DSP
|
|
@@ -79,10 +79,13 @@ export function fftInPlace(
|
|
|
79
79
|
for (let length = 2; length <= n; length <<= 1) {
|
|
80
80
|
const half = length >> 1;
|
|
81
81
|
const stride = n / length;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for (let
|
|
82
|
+
// Butterfly groups outer, twiddles inner: memory is walked in order
|
|
83
|
+
// (the same operations as twiddle-outer, so the same bits).
|
|
84
|
+
for (let start = 0; start < n; start += length) {
|
|
85
|
+
for (let k = 0; k < half; k += 1) {
|
|
86
|
+
const wr = cos[k * stride]!;
|
|
87
|
+
const wi = sign * sin[k * stride]!;
|
|
88
|
+
const a = start + k;
|
|
86
89
|
const b = a + half;
|
|
87
90
|
const xr = re[b]! * wr - im[b]! * wi;
|
|
88
91
|
const xi = re[b]! * wi + im[b]! * wr;
|
|
@@ -236,6 +239,31 @@ export const BUILTIN_IMPULSES = Object.freeze({
|
|
|
236
239
|
attack: 0.06,
|
|
237
240
|
},
|
|
238
241
|
plate: { title: "plate", seconds: 1.8, damp: 9000, early: 0, attack: 0 },
|
|
242
|
+
// 0.6.1 shaped impulses (`shape`, see `shapedSide`).
|
|
243
|
+
reverse: {
|
|
244
|
+
title: "reverse",
|
|
245
|
+
seconds: 1.2,
|
|
246
|
+
damp: 7000,
|
|
247
|
+
early: 0,
|
|
248
|
+
attack: 0,
|
|
249
|
+
shape: "rise",
|
|
250
|
+
},
|
|
251
|
+
gate: {
|
|
252
|
+
title: "gated",
|
|
253
|
+
seconds: 0.45,
|
|
254
|
+
damp: 8000,
|
|
255
|
+
early: 6,
|
|
256
|
+
attack: 0.005,
|
|
257
|
+
shape: "gate",
|
|
258
|
+
},
|
|
259
|
+
spring: {
|
|
260
|
+
title: "spring tank",
|
|
261
|
+
seconds: 2.2,
|
|
262
|
+
damp: 4500,
|
|
263
|
+
early: 0,
|
|
264
|
+
attack: 0,
|
|
265
|
+
shape: "spring",
|
|
266
|
+
},
|
|
239
267
|
} as const);
|
|
240
268
|
export type BuiltinImpulse = keyof typeof BUILTIN_IMPULSES;
|
|
241
269
|
|
|
@@ -259,6 +287,15 @@ export function builtinImpulse(
|
|
|
259
287
|
if (cached) return cached;
|
|
260
288
|
const spec = BUILTIN_IMPULSES[name as BuiltinImpulse];
|
|
261
289
|
const length = Math.max(1, Math.round(spec.seconds * sampleRate));
|
|
290
|
+
if ("shape" in spec) {
|
|
291
|
+
const impulse = normalizeImpulse(
|
|
292
|
+
`builtin:${name}`,
|
|
293
|
+
shapedSide(name, "L", spec, sampleRate),
|
|
294
|
+
shapedSide(name, "R", spec, sampleRate),
|
|
295
|
+
);
|
|
296
|
+
BUILTIN_CACHE.set(key, impulse);
|
|
297
|
+
return impulse;
|
|
298
|
+
}
|
|
262
299
|
const sides = ["L", "R"].map((side) => {
|
|
263
300
|
const random = seededRandom(`dawg-ir:${name}:${side}`);
|
|
264
301
|
const out = new Float64Array(length);
|
|
@@ -289,6 +326,77 @@ export function builtinImpulse(
|
|
|
289
326
|
return impulse;
|
|
290
327
|
}
|
|
291
328
|
|
|
329
|
+
/**
|
|
330
|
+
* One side of a shaped built-in (0.6.1), all seeded noise through a
|
|
331
|
+
* one-pole low-pass at `damp`:
|
|
332
|
+
* - rise: the Yamaha SPX90 "reverse gate": energy rises exponentially to a
|
|
333
|
+
* hard cut at `seconds`, so the wash swells into the cut (a reverse
|
|
334
|
+
* reverb that stays causal and works in play mode).
|
|
335
|
+
* - gate: dense flat tail cut hard at `seconds` (the 1980s gated snare).
|
|
336
|
+
* - spring: a decaying noise tail plus a train of dispersive chirps every
|
|
337
|
+
* ~33 ms (high frequencies arrive first, the drip of a spring tank).
|
|
338
|
+
*/
|
|
339
|
+
function shapedSide(
|
|
340
|
+
name: string,
|
|
341
|
+
side: string,
|
|
342
|
+
spec: Readonly<{
|
|
343
|
+
seconds: number;
|
|
344
|
+
damp: number;
|
|
345
|
+
early: number;
|
|
346
|
+
attack: number;
|
|
347
|
+
shape: string;
|
|
348
|
+
}>,
|
|
349
|
+
sampleRate: number,
|
|
350
|
+
): Float64Array {
|
|
351
|
+
const random = seededRandom(`dawg-ir:${name}:${side}`);
|
|
352
|
+
const length = Math.max(16, Math.round(spec.seconds * sampleRate));
|
|
353
|
+
const out = new Float64Array(length);
|
|
354
|
+
const coefficient =
|
|
355
|
+
1 -
|
|
356
|
+
Math.exp(
|
|
357
|
+
(-2 * Math.PI * Math.min(spec.damp, sampleRate * 0.45)) / sampleRate,
|
|
358
|
+
);
|
|
359
|
+
const fadeOut = Math.min(length >> 2, Math.round(0.003 * sampleRate));
|
|
360
|
+
let state = 0;
|
|
361
|
+
for (let index = 0; index < length; index += 1) {
|
|
362
|
+
const x = (index + 0.5) / length;
|
|
363
|
+
const t = index / sampleRate;
|
|
364
|
+
state += coefficient * (random() * 2 - 1 - state);
|
|
365
|
+
let env: number;
|
|
366
|
+
if (spec.shape === "rise") env = Math.exp(4 * (x - 1));
|
|
367
|
+
else if (spec.shape === "gate")
|
|
368
|
+
env = Math.min(1, t / Math.max(1e-4, spec.attack)) * (1 - 0.3 * x);
|
|
369
|
+
else env = 0.35 * Math.exp((-Math.log(1000) * t) / spec.seconds);
|
|
370
|
+
// A short fade into the cut, so it clicks no more than a real gate.
|
|
371
|
+
if (spec.shape !== "spring" && index >= length - fadeOut)
|
|
372
|
+
env *= (length - index) / fadeOut;
|
|
373
|
+
out[index] = state * env;
|
|
374
|
+
}
|
|
375
|
+
for (let tap = 0; tap < spec.early; tap += 1) {
|
|
376
|
+
const at = Math.floor((0.004 + random() * 0.036) * sampleRate);
|
|
377
|
+
if (at < length) out[at]! += (random() < 0.5 ? -1 : 1) * (0.5 - tap * 0.05);
|
|
378
|
+
}
|
|
379
|
+
if (spec.shape === "spring") {
|
|
380
|
+
// Dispersive chirps: 4 kHz falling to 300 Hz over 25 ms, each echo
|
|
381
|
+
// -1.6 dB down on the last, the period jittered per side.
|
|
382
|
+
const period = (0.031 + 0.004 * random()) * sampleRate;
|
|
383
|
+
const chirp = 0.025;
|
|
384
|
+
const decay = Math.log(1000) / spec.seconds;
|
|
385
|
+
for (let echo = 0; echo * period < length; echo += 1) {
|
|
386
|
+
const start = Math.round(echo * period);
|
|
387
|
+
const level = Math.exp((-decay * start) / sampleRate);
|
|
388
|
+
let phase = random() * 2 * Math.PI;
|
|
389
|
+
for (let k = 0; k < chirp * sampleRate && start + k < length; k += 1) {
|
|
390
|
+
const u = k / (chirp * sampleRate);
|
|
391
|
+
const hz = 4000 * (300 / 4000) ** u;
|
|
392
|
+
phase += (2 * Math.PI * hz) / sampleRate;
|
|
393
|
+
out[start + k]! += level * Math.sin(phase) * Math.sin(Math.PI * u);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
292
400
|
/**
|
|
293
401
|
* An impulse from a decoded sample (mono, any rate): resampled linearly to
|
|
294
402
|
* the render rate, cut to `MAX_IMPULSE_SECONDS`, energy-normalized; both
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { SCORE_LIMITS, type Track } from "../../../core/score.ts";
|
|
7
7
|
import type { FxValues } from "../../../core/fx.ts";
|
|
8
|
+
import { morphFormants, VOWEL_FORMANTS } from "../dsp/formant.ts";
|
|
8
9
|
import {
|
|
9
10
|
Biquad,
|
|
10
11
|
CONTROL_SAMPLES,
|
|
11
12
|
OnePole,
|
|
13
|
+
Param,
|
|
12
14
|
Phasor,
|
|
13
15
|
clamp,
|
|
14
16
|
fxReader,
|
|
@@ -123,16 +125,26 @@ export function applyTrackFilter(
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Where the DJ filter's high-pass stops rising, as on a DJ mixer: a 20 kHz
|
|
130
|
+
* high-pass at 44.1 kHz left nothing (q08), so the top of the knob is thin.
|
|
131
|
+
* The curve below about 0.95 is unchanged.
|
|
132
|
+
*/
|
|
133
|
+
const DJ_HPF_MAX_HZ = 10_000;
|
|
134
|
+
|
|
135
|
+
/** Cutoff for a DJ-filter position: 0 → 20 Hz low-pass … 1 → 10 kHz high-pass. */
|
|
127
136
|
function djCutoff(value: number): { type: FilterType; cutoff: number } {
|
|
128
137
|
return value < 0.5
|
|
129
138
|
? { type: "lpf", cutoff: 20 * 1000 ** (value / 0.5) }
|
|
130
|
-
: {
|
|
139
|
+
: {
|
|
140
|
+
type: "hpf",
|
|
141
|
+
cutoff: Math.min(DJ_HPF_MAX_HZ, 20 * 1000 ** ((value - 0.5) / 0.5)),
|
|
142
|
+
};
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
/**
|
|
134
146
|
* One-knob DJ filter (Strudel `djf`): below 0.5 a low-pass closes toward
|
|
135
|
-
* 20 Hz, above 0.5 a high-pass rises toward
|
|
147
|
+
* 20 Hz, above 0.5 a high-pass rises toward 10 kHz; 0.5 passes unchanged.
|
|
136
148
|
*/
|
|
137
149
|
export function applyDjFilter(
|
|
138
150
|
buffer: Float64Array,
|
|
@@ -223,72 +235,6 @@ export function applyAutoFilter(
|
|
|
223
235
|
}
|
|
224
236
|
}
|
|
225
237
|
|
|
226
|
-
type Formants = readonly (readonly [number, number, number])[];
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Formant frequency (Hz), level (dB) and bandwidth (Hz). The five plain
|
|
230
|
-
* vowels use the classic tenor formant table (published acoustic
|
|
231
|
-
* phonetics data); the extended set approximates F1–F3 from standard
|
|
232
|
-
* vowel charts with tenor-typical F4/F5.
|
|
233
|
-
*/
|
|
234
|
-
const VOWEL_FORMANTS: Readonly<Record<string, Formants>> = Object.freeze({
|
|
235
|
-
a: [
|
|
236
|
-
[650, 0, 80],
|
|
237
|
-
[1080, -6, 90],
|
|
238
|
-
[2650, -7, 120],
|
|
239
|
-
[2900, -8, 130],
|
|
240
|
-
[3250, -22, 140],
|
|
241
|
-
],
|
|
242
|
-
e: [
|
|
243
|
-
[400, 0, 70],
|
|
244
|
-
[1700, -14, 80],
|
|
245
|
-
[2600, -12, 100],
|
|
246
|
-
[3200, -14, 120],
|
|
247
|
-
[3580, -20, 120],
|
|
248
|
-
],
|
|
249
|
-
i: [
|
|
250
|
-
[290, 0, 40],
|
|
251
|
-
[1870, -15, 90],
|
|
252
|
-
[2800, -18, 100],
|
|
253
|
-
[3250, -20, 120],
|
|
254
|
-
[3540, -30, 120],
|
|
255
|
-
],
|
|
256
|
-
o: [
|
|
257
|
-
[400, 0, 40],
|
|
258
|
-
[800, -10, 80],
|
|
259
|
-
[2600, -12, 100],
|
|
260
|
-
[2800, -12, 120],
|
|
261
|
-
[3000, -26, 120],
|
|
262
|
-
],
|
|
263
|
-
u: [
|
|
264
|
-
[350, 0, 40],
|
|
265
|
-
[600, -20, 60],
|
|
266
|
-
[2700, -17, 100],
|
|
267
|
-
[2900, -14, 120],
|
|
268
|
-
[3300, -26, 120],
|
|
269
|
-
],
|
|
270
|
-
ae: extended(660, 1720, 2410),
|
|
271
|
-
aa: extended(710, 1100, 2540),
|
|
272
|
-
oe: extended(390, 1680, 2400),
|
|
273
|
-
ue: extended(300, 1600, 2200),
|
|
274
|
-
y: extended(250, 1750, 2160),
|
|
275
|
-
uh: extended(600, 1170, 2390),
|
|
276
|
-
un: extended(500, 1400, 2500),
|
|
277
|
-
en: extended(550, 1650, 2500),
|
|
278
|
-
an: extended(650, 1050, 2550),
|
|
279
|
-
on: extended(450, 850, 2550),
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
function extended(f1: number, f2: number, f3: number): Formants {
|
|
283
|
-
return [
|
|
284
|
-
[f1, 0, 70],
|
|
285
|
-
[f2, -8, 90],
|
|
286
|
-
[f3, -14, 110],
|
|
287
|
-
[3300, -16, 130],
|
|
288
|
-
[3750, -24, 140],
|
|
289
|
-
];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
238
|
/** Gain that brings the summed formant bank near unity for speech-band input. */
|
|
293
239
|
const VOWEL_GAIN = 2.2;
|
|
294
240
|
|
|
@@ -307,11 +253,44 @@ export function applyVowel(
|
|
|
307
253
|
filter.set("bpf", frequency, frequency / bandwidth, context.sampleRate);
|
|
308
254
|
return { filter, gain: 10 ** (level / 20) };
|
|
309
255
|
});
|
|
256
|
+
// 0.7 morph: present only with `to`; absent keeps the static bank exactly.
|
|
257
|
+
const toName = values.to as string | undefined;
|
|
258
|
+
const to = toName === undefined ? undefined : VOWEL_FORMANTS[toName];
|
|
259
|
+
// Absent morph means 0, so a vowel-morph lane still sweeps without it.
|
|
260
|
+
const morph = to
|
|
261
|
+
? new Param(
|
|
262
|
+
(values.morph as number | undefined) ?? 0,
|
|
263
|
+
track.fxAutomation?.["vowel-morph"],
|
|
264
|
+
context.samplesPerTick,
|
|
265
|
+
context.warp,
|
|
266
|
+
)
|
|
267
|
+
: undefined;
|
|
268
|
+
const moving = morph !== undefined && (morph.automated || morph.fallback > 0);
|
|
269
|
+
let at = -1;
|
|
270
|
+
const retune = (position: number) => {
|
|
271
|
+
if (position === at) return;
|
|
272
|
+
at = position;
|
|
273
|
+
morphFormants(formants, to!, position).forEach(
|
|
274
|
+
([frequency, level, bandwidth], index) => {
|
|
275
|
+
const band = bank[index]!;
|
|
276
|
+
band.filter.set(
|
|
277
|
+
"bpf",
|
|
278
|
+
frequency,
|
|
279
|
+
frequency / bandwidth,
|
|
280
|
+
context.sampleRate,
|
|
281
|
+
);
|
|
282
|
+
band.gain = 10 ** (level / 20);
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
if (moving) retune(morph.at(0));
|
|
310
287
|
// Formant peaks are narrow; a gentle pre-emphasis keeps the vowel bright.
|
|
311
288
|
const tilt = new OnePole(8000, context.sampleRate);
|
|
312
289
|
let wet = mix.at(0);
|
|
313
290
|
for (let index = 0; index < buffer.length; index += 1) {
|
|
314
291
|
if (mix.automated && index % CONTROL_SAMPLES === 0) wet = mix.at(index);
|
|
292
|
+
if (moving && morph.automated && index % CONTROL_SAMPLES === 0)
|
|
293
|
+
retune(morph.at(index));
|
|
315
294
|
const input = buffer[index]!;
|
|
316
295
|
const emphasized = input + 0.5 * (input - tilt.process(input));
|
|
317
296
|
let sum = 0;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formant shift at constant pitch (0.7 `fx.formant`): the throat or gender
|
|
3
|
+
* knob. Per STFT frame the cepstral spectral envelope E is taken and each
|
|
4
|
+
* bin is scaled by E(k / a) / E(k), a = 2^(shift / 12), with its phase left
|
|
5
|
+
* untouched, then the frames overlap-add. The pitch never moves, so no
|
|
6
|
+
* phase vocoder is needed (Röbel and Rodet, "Efficient spectral envelope
|
|
7
|
+
* estimation and its application to pitch shifting and envelope
|
|
8
|
+
* preservation", DAFx 2005; Little AlterBoy and VocalSynth's formant knob).
|
|
9
|
+
*
|
|
10
|
+
* The lifter adapts to the voice: 0.75 periods of a 5-frame median
|
|
11
|
+
* autocorrelation f0, clamped to 1-2 ms, 1 ms when unvoiced. A fixed 2 ms
|
|
12
|
+
* lifter resolves single harmonics above about 500 Hz and makes high
|
|
13
|
+
* voices worse than no processing (design formant.md section 2).
|
|
14
|
+
*
|
|
15
|
+
* Frames sit at multiples of the hop in absolute track samples (an
|
|
16
|
+
* arranged window passes its origin as `seedSeconds`), and a window padded
|
|
17
|
+
* back by `FORMANT_PAD` samples and forward by one frame equals the same
|
|
18
|
+
* span of a full render bit for bit.
|
|
19
|
+
*/
|
|
20
|
+
import type { FxValues } from "../../../core/fx.ts";
|
|
21
|
+
import type { Track } from "../../../core/score.ts";
|
|
22
|
+
import { cepstralEnvelope, envelopeScratch } from "../dsp/envelope.ts";
|
|
23
|
+
import { fftInPlace } from "../dsp/fft.ts";
|
|
24
|
+
import { hann, HANN_SQUARED_OLA_QUARTER } from "../dsp/stft.ts";
|
|
25
|
+
import { fxReader, type EffectContext } from "./common.ts";
|
|
26
|
+
|
|
27
|
+
/** Per-bin gain clamp (24 dB), as shift.ts holds formants. */
|
|
28
|
+
const MAX_GAIN = 10 ** (24 / 20);
|
|
29
|
+
/** Lifter periods and clamps (design formant.md section 2). */
|
|
30
|
+
const LIFTER_PERIODS = 0.75;
|
|
31
|
+
const LIFTER_MIN_SECONDS = 0.001;
|
|
32
|
+
const LIFTER_MAX_SECONDS = 0.002;
|
|
33
|
+
/** Frames in the f0 median (this one and the four before it). */
|
|
34
|
+
const F0_MEDIAN = 5;
|
|
35
|
+
const F0_MIN_HZ = 60;
|
|
36
|
+
const F0_MAX_HZ = 1500;
|
|
37
|
+
/** Below this normalized autocorrelation a frame counts as unvoiced. */
|
|
38
|
+
const VOICED = 0.45;
|
|
39
|
+
/** The first peak within this fraction of the best is the period. */
|
|
40
|
+
const FIRST_PEAK = 0.85;
|
|
41
|
+
|
|
42
|
+
/** Frame length: 1024 at or below 24 kHz, 2048 above. */
|
|
43
|
+
export function formantFrame(sampleRate: number): number {
|
|
44
|
+
return sampleRate > 24_000 ? 2048 : 1024;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Samples a window render must start before its first output sample (4
|
|
49
|
+
* hops for the f0 median plus one frame), and it renders one frame past
|
|
50
|
+
* its last.
|
|
51
|
+
*/
|
|
52
|
+
export function formantPad(sampleRate: number): number {
|
|
53
|
+
const n = formantFrame(sampleRate);
|
|
54
|
+
return n + 4 * (n / 4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type FormantShiftOptions = Readonly<{
|
|
58
|
+
/** Wet amount 0..1 (1: fully shifted). */
|
|
59
|
+
mix?: number | ((index: number) => number);
|
|
60
|
+
/** Absolute track sample of `x[0]`; frames anchor at hop multiples of it. */
|
|
61
|
+
origin?: number;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
type Analysis = {
|
|
65
|
+
n: number;
|
|
66
|
+
hop: number;
|
|
67
|
+
win: Float64Array;
|
|
68
|
+
wac: Float64Array;
|
|
69
|
+
minLag: number;
|
|
70
|
+
maxLag: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const analyses = new Map<number, Analysis>();
|
|
74
|
+
|
|
75
|
+
function analysisFor(sampleRate: number): Analysis {
|
|
76
|
+
let a = analyses.get(sampleRate);
|
|
77
|
+
if (a) return a;
|
|
78
|
+
const n = formantFrame(sampleRate);
|
|
79
|
+
const win = hann(n);
|
|
80
|
+
const minLag = Math.floor(sampleRate / F0_MAX_HZ);
|
|
81
|
+
const maxLag = Math.min(n / 2 - 1, Math.ceil(sampleRate / F0_MIN_HZ));
|
|
82
|
+
// The window's own autocorrelation, divided out so long lags are not
|
|
83
|
+
// biased down (Boersma 1993).
|
|
84
|
+
const wac = new Float64Array(maxLag + 1);
|
|
85
|
+
for (let t = 0; t <= maxLag; t += 1) {
|
|
86
|
+
let sum = 0;
|
|
87
|
+
for (let i = 0; i + t < n; i += 1) sum += win[i]! * win[i + t]!;
|
|
88
|
+
wac[t] = sum;
|
|
89
|
+
}
|
|
90
|
+
a = { n, hop: n / 4, win, wac, minLag, maxLag };
|
|
91
|
+
analyses.set(sampleRate, a);
|
|
92
|
+
return a;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One frame's f0 from its power spectrum (0: unvoiced). */
|
|
96
|
+
function frameF0(
|
|
97
|
+
mag: Float64Array,
|
|
98
|
+
a: Analysis,
|
|
99
|
+
sampleRate: number,
|
|
100
|
+
re: Float64Array,
|
|
101
|
+
im: Float64Array,
|
|
102
|
+
): number {
|
|
103
|
+
const { n, wac, minLag, maxLag } = a;
|
|
104
|
+
const half = n / 2;
|
|
105
|
+
for (let k = 0; k <= half; k += 1) {
|
|
106
|
+
re[k] = mag[k]! * mag[k]!;
|
|
107
|
+
im[k] = 0;
|
|
108
|
+
if (k > 0 && k < half) {
|
|
109
|
+
re[n - k] = re[k]!;
|
|
110
|
+
im[n - k] = 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
fftInPlace(re, im, true);
|
|
114
|
+
const r0 = re[0]!;
|
|
115
|
+
if (r0 <= 1e-12) return 0;
|
|
116
|
+
const w0 = wac[0]! / r0;
|
|
117
|
+
for (let t = minLag - 1; t <= maxLag; t += 1) re[t] = (re[t]! * w0) / wac[t]!;
|
|
118
|
+
let best = 0;
|
|
119
|
+
for (let t = minLag; t <= maxLag; t += 1) best = Math.max(best, re[t]!);
|
|
120
|
+
if (best < VOICED) return 0;
|
|
121
|
+
for (let t = minLag + 1; t < maxLag; t += 1) {
|
|
122
|
+
const v = re[t]!;
|
|
123
|
+
if (v >= FIRST_PEAK * best && v >= re[t - 1]! && v >= re[t + 1]!) {
|
|
124
|
+
const l = re[t - 1]!;
|
|
125
|
+
const r = re[t + 1]!;
|
|
126
|
+
const d = l - 2 * v + r;
|
|
127
|
+
const offset = d < 0 ? (0.5 * (l - r)) / d : 0;
|
|
128
|
+
return sampleRate / (t + offset);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Shift the formants of `x` by `shift` semitones (a number, or a function
|
|
136
|
+
* of the buffer index read once per hop) at constant pitch. Returns a new
|
|
137
|
+
* buffer; `shift` 0 everywhere returns a copy of `x`.
|
|
138
|
+
*/
|
|
139
|
+
export function formantShift(
|
|
140
|
+
x: Float64Array,
|
|
141
|
+
sampleRate: number,
|
|
142
|
+
shift: number | ((index: number) => number),
|
|
143
|
+
options: FormantShiftOptions = {},
|
|
144
|
+
): Float64Array {
|
|
145
|
+
if (shift === 0) return Float64Array.from(x);
|
|
146
|
+
const a = analysisFor(sampleRate);
|
|
147
|
+
const { n, hop, win } = a;
|
|
148
|
+
const half = n / 2;
|
|
149
|
+
const origin = Math.round(options.origin ?? 0);
|
|
150
|
+
const out = new Float64Array(x.length);
|
|
151
|
+
const re = new Float64Array(n);
|
|
152
|
+
const im = new Float64Array(n);
|
|
153
|
+
const acRe = new Float64Array(n);
|
|
154
|
+
const acIm = new Float64Array(n);
|
|
155
|
+
const mag = new Float64Array(half + 1);
|
|
156
|
+
const env = new Float64Array(half + 1);
|
|
157
|
+
const scratch = envelopeScratch(n);
|
|
158
|
+
const history = new Float64Array(F0_MEDIAN);
|
|
159
|
+
const sorted = new Float64Array(F0_MEDIAN);
|
|
160
|
+
let frames = 0;
|
|
161
|
+
const ola = 1 / HANN_SQUARED_OLA_QUARTER;
|
|
162
|
+
const auto = typeof shift === "function";
|
|
163
|
+
// First frame: the latest hop multiple (absolute) whose frame still
|
|
164
|
+
// reaches x[0].
|
|
165
|
+
const firstAbs = Math.floor((origin - n + 1 + (hop - 1)) / hop) * hop;
|
|
166
|
+
for (let start = firstAbs - origin; start < x.length; start += hop) {
|
|
167
|
+
const st = auto ? shift(Math.max(0, start + half)) : shift;
|
|
168
|
+
for (let i = 0; i < n; i += 1) {
|
|
169
|
+
const j = start + i;
|
|
170
|
+
re[i] = j >= 0 && j < x.length ? x[j]! * win[i]! : 0;
|
|
171
|
+
im[i] = 0;
|
|
172
|
+
}
|
|
173
|
+
fftInPlace(re, im);
|
|
174
|
+
for (let k = 0; k <= half; k += 1) mag[k] = Math.hypot(re[k]!, im[k]!);
|
|
175
|
+
// Every frame joins the f0 history, so a window and the full render
|
|
176
|
+
// agree whatever the shift was.
|
|
177
|
+
history[frames % F0_MEDIAN] = frameF0(mag, a, sampleRate, acRe, acIm);
|
|
178
|
+
frames += 1;
|
|
179
|
+
if (st === 0) {
|
|
180
|
+
fftInPlace(re, im, true);
|
|
181
|
+
} else {
|
|
182
|
+
const m = Math.min(frames, F0_MEDIAN);
|
|
183
|
+
for (let i = 0; i < m; i += 1) sorted[i] = history[i]!;
|
|
184
|
+
for (let i = 1; i < m; i += 1)
|
|
185
|
+
for (let j = i; j > 0 && sorted[j - 1]! > sorted[j]!; j -= 1) {
|
|
186
|
+
const tmp = sorted[j]!;
|
|
187
|
+
sorted[j] = sorted[j - 1]!;
|
|
188
|
+
sorted[j - 1] = tmp;
|
|
189
|
+
}
|
|
190
|
+
const f0 = sorted[m >> 1]!;
|
|
191
|
+
const seconds =
|
|
192
|
+
f0 > 0
|
|
193
|
+
? Math.min(
|
|
194
|
+
LIFTER_MAX_SECONDS,
|
|
195
|
+
Math.max(LIFTER_MIN_SECONDS, LIFTER_PERIODS / f0),
|
|
196
|
+
)
|
|
197
|
+
: LIFTER_MIN_SECONDS;
|
|
198
|
+
const lifter = Math.max(4, Math.round(seconds * sampleRate));
|
|
199
|
+
cepstralEnvelope(mag, n, lifter, env, scratch);
|
|
200
|
+
const ratio = 2 ** (-st / 12);
|
|
201
|
+
for (let k = 0; k <= half; k += 1) {
|
|
202
|
+
const source = k * ratio;
|
|
203
|
+
const i0 = Math.floor(source);
|
|
204
|
+
const f = source - i0;
|
|
205
|
+
const e =
|
|
206
|
+
i0 >= half
|
|
207
|
+
? env[half]!
|
|
208
|
+
: env[i0]! + (env[Math.min(half, i0 + 1)]! - env[i0]!) * f;
|
|
209
|
+
const g = Math.min(
|
|
210
|
+
MAX_GAIN,
|
|
211
|
+
Math.max(1 / MAX_GAIN, e / Math.max(env[k]!, 1e-12)),
|
|
212
|
+
);
|
|
213
|
+
re[k] = re[k]! * g;
|
|
214
|
+
im[k] = im[k]! * g;
|
|
215
|
+
if (k > 0 && k < half) {
|
|
216
|
+
re[n - k] = re[k]!;
|
|
217
|
+
im[n - k] = -im[k]!;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
fftInPlace(re, im, true);
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i < n; i += 1) {
|
|
223
|
+
const j = start + i;
|
|
224
|
+
if (j >= 0 && j < x.length)
|
|
225
|
+
out[j] = out[j]! + (re[i]! / n) * win[i]! * ola;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const mix = options.mix ?? 1;
|
|
229
|
+
if (typeof mix === "function")
|
|
230
|
+
for (let i = 0; i < x.length; i += 1)
|
|
231
|
+
out[i] = x[i]! + (out[i]! - x[i]!) * mix(i);
|
|
232
|
+
else if (mix < 1)
|
|
233
|
+
for (let i = 0; i < x.length; i += 1)
|
|
234
|
+
out[i] = x[i]! + (out[i]! - x[i]!) * mix;
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `fx.formant`: formant shift with mix, both automatable. */
|
|
239
|
+
export function applyFormant(
|
|
240
|
+
buffer: Float64Array,
|
|
241
|
+
track: Track,
|
|
242
|
+
values: FxValues,
|
|
243
|
+
context: EffectContext,
|
|
244
|
+
): void {
|
|
245
|
+
const read = fxReader(track, "formant", values, context);
|
|
246
|
+
const shift = read.param("shift");
|
|
247
|
+
const mix = read.param("mix");
|
|
248
|
+
if (!shift.automated && shift.fallback === 0) return;
|
|
249
|
+
if (!mix.automated && mix.fallback <= 0) return;
|
|
250
|
+
const origin = context.seedSeconds
|
|
251
|
+
? Math.round(context.seedSeconds * context.sampleRate)
|
|
252
|
+
: 0;
|
|
253
|
+
const out = formantShift(
|
|
254
|
+
buffer,
|
|
255
|
+
context.sampleRate,
|
|
256
|
+
shift.automated ? (index) => shift.at(index) : shift.fallback,
|
|
257
|
+
{
|
|
258
|
+
origin,
|
|
259
|
+
mix: mix.automated ? (index) => mix.at(index) : mix.fallback,
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
buffer.set(out);
|
|
263
|
+
}
|