@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.
- package/CHANGELOG.md +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- 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/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- 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 +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -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 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -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 +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -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 +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- 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/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -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/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- 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 +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- 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 +82 -32
- 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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- 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 +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- 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 +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- 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 +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- 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 +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-varying TD-PSOLA (Moulines and Charpentier, Speech Communication
|
|
3
|
+
* 1990) for 0.7 Voice. Analysis marks sit one per period on voiced runs;
|
|
4
|
+
* two-period Hann grains are re-spaced at the corrected period and
|
|
5
|
+
* overlap-added at the same time position, so duration is kept and the
|
|
6
|
+
* spectral envelope (formants) stays where it was. Unvoiced stretches are
|
|
7
|
+
* copied with fixed 4 ms grains at ratio 1, so breath and consonants pass
|
|
8
|
+
* through unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Review fixes over the prototype: mark polarity is chosen once per voiced
|
|
11
|
+
* run (the sign of sum x^3), so negative-going glottal closures and inverted
|
|
12
|
+
* microphones mark the same way every period; grains are placed at
|
|
13
|
+
* fractional output positions with 4-point Hermite reads instead of a 2-tap
|
|
14
|
+
* split; stereo shares one set of marks found on the mid signal.
|
|
15
|
+
*
|
|
16
|
+
* Autotune and the 0.7.1 harmony lane import these unchanged.
|
|
17
|
+
*/
|
|
18
|
+
import { hermiteAt } from "./interp.ts";
|
|
19
|
+
|
|
20
|
+
/** Unvoiced grain spacing in seconds. */
|
|
21
|
+
export const UNVOICED_PERIOD = 0.004;
|
|
22
|
+
|
|
23
|
+
/** What a psola pass does, per input sample. */
|
|
24
|
+
export type ShiftCurve = Readonly<{
|
|
25
|
+
/** Input f0 in Hz per input sample (0 = unvoiced). */
|
|
26
|
+
f0: Float64Array;
|
|
27
|
+
/** Shift in cents per input sample (ignored where unvoiced). */
|
|
28
|
+
cents: Float64Array;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
export type PsolaOptions = Readonly<{
|
|
32
|
+
/**
|
|
33
|
+
* Formant factor: grains are read this many times faster, which moves the
|
|
34
|
+
* spectral envelope by the same ratio (1 keeps it, the default). Limited to
|
|
35
|
+
* 0.5..2.
|
|
36
|
+
*/
|
|
37
|
+
formant?: number;
|
|
38
|
+
/** First input sample of the output (default 0). */
|
|
39
|
+
offset?: number;
|
|
40
|
+
/** Output length in samples (default the rest of the input). */
|
|
41
|
+
length?: number;
|
|
42
|
+
/** Precomputed analysis marks (from `pitchMarks`), e.g. shared by L/R. */
|
|
43
|
+
marks?: Int32Array;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Analysis marks: one per period at the waveform's largest peak (in the
|
|
48
|
+
* run's polarity) near the expected place, and one every 4 ms where
|
|
49
|
+
* unvoiced.
|
|
50
|
+
*/
|
|
51
|
+
export function pitchMarks(
|
|
52
|
+
x: Float64Array,
|
|
53
|
+
sampleRate: number,
|
|
54
|
+
f0: Float64Array,
|
|
55
|
+
): Int32Array {
|
|
56
|
+
const n = x.length;
|
|
57
|
+
const up = Math.max(1, Math.round(UNVOICED_PERIOD * sampleRate));
|
|
58
|
+
const polarity = new Int8Array(n).fill(1);
|
|
59
|
+
for (let a = 0; a < n;) {
|
|
60
|
+
if (!(f0[a]! > 0)) {
|
|
61
|
+
a += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
let b = a;
|
|
65
|
+
let s3 = 0;
|
|
66
|
+
while (b < n && f0[b]! > 0) {
|
|
67
|
+
s3 += x[b]! ** 3;
|
|
68
|
+
b += 1;
|
|
69
|
+
}
|
|
70
|
+
if (s3 < 0) polarity.fill(-1, a, b);
|
|
71
|
+
a = b;
|
|
72
|
+
}
|
|
73
|
+
const marks: number[] = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < n) {
|
|
76
|
+
const hz = f0[i]!;
|
|
77
|
+
if (!(hz > 0)) {
|
|
78
|
+
marks.push(i);
|
|
79
|
+
i += up;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const period = sampleRate / hz;
|
|
83
|
+
const last = marks.length ? marks[marks.length - 1]! : -1;
|
|
84
|
+
const afterVoiced = last >= 0 && f0[last]! > 0;
|
|
85
|
+
const lo = Math.max(0, Math.round(i - (afterVoiced ? period / 4 : 0)));
|
|
86
|
+
const hi = Math.min(
|
|
87
|
+
n - 1,
|
|
88
|
+
Math.round(i + (afterVoiced ? period / 4 : period)),
|
|
89
|
+
);
|
|
90
|
+
const sign = polarity[i]!;
|
|
91
|
+
let best = lo;
|
|
92
|
+
for (let j = lo + 1; j <= hi; j += 1)
|
|
93
|
+
if (sign * x[j]! > sign * x[best]!) best = j;
|
|
94
|
+
if (last >= 0 && best <= last)
|
|
95
|
+
best = last + Math.max(1, Math.round(period / 2));
|
|
96
|
+
if (best >= n) break;
|
|
97
|
+
marks.push(best);
|
|
98
|
+
const next = f0[best]! > 0 ? sampleRate / f0[best]! : period;
|
|
99
|
+
i = best + Math.max(1, Math.round(next));
|
|
100
|
+
}
|
|
101
|
+
return Int32Array.from(marks);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Shifts the pitch of `x` by `curve.cents` while keeping duration and (at
|
|
106
|
+
* formant 1) the formants. Deterministic: same input, same bytes.
|
|
107
|
+
*/
|
|
108
|
+
export function psola(
|
|
109
|
+
x: Float64Array,
|
|
110
|
+
sampleRate: number,
|
|
111
|
+
curve: ShiftCurve,
|
|
112
|
+
options: PsolaOptions = {},
|
|
113
|
+
): Float64Array {
|
|
114
|
+
const n = x.length;
|
|
115
|
+
const offset = Math.max(0, Math.min(n, Math.round(options.offset ?? 0)));
|
|
116
|
+
const length = Math.max(0, Math.round(options.length ?? n - offset));
|
|
117
|
+
const out = new Float64Array(length);
|
|
118
|
+
const marks = options.marks ?? pitchMarks(x, sampleRate, curve.f0);
|
|
119
|
+
if (marks.length === 0 || length === 0) return out;
|
|
120
|
+
const formant = Math.min(2, Math.max(0.5, options.formant ?? 1));
|
|
121
|
+
const up = UNVOICED_PERIOD * sampleRate;
|
|
122
|
+
const end = offset + length;
|
|
123
|
+
// synthesis marks walk the output at the target period; each takes the
|
|
124
|
+
// nearest analysis mark
|
|
125
|
+
// The walk always starts at the first mark, so a window (offset, length)
|
|
126
|
+
// renders exactly the samples of the whole pass.
|
|
127
|
+
let k = 0;
|
|
128
|
+
let t = marks[0]!;
|
|
129
|
+
while (t < end + up) {
|
|
130
|
+
while (
|
|
131
|
+
k + 1 < marks.length &&
|
|
132
|
+
Math.abs(marks[k + 1]! - t) <= Math.abs(marks[k]! - t)
|
|
133
|
+
)
|
|
134
|
+
k += 1;
|
|
135
|
+
const m = marks[k]!;
|
|
136
|
+
const hz = curve.f0[m]!;
|
|
137
|
+
const voiced = hz > 0;
|
|
138
|
+
const period = voiced ? sampleRate / hz : up;
|
|
139
|
+
// the analysis spacing: the next mark when it is about a period away,
|
|
140
|
+
// so ratio 1 lands every synthesis mark on its analysis mark
|
|
141
|
+
const gap = k + 1 < marks.length ? marks[k + 1]! - m : 0;
|
|
142
|
+
const pin = gap >= period * 0.5 && gap <= period * 1.5 ? gap : period;
|
|
143
|
+
const at = Math.min(n - 1, Math.max(0, Math.round(t)));
|
|
144
|
+
const ratio = voiced ? 2 ** ((curve.cents[at] || 0) / 1200) : 1;
|
|
145
|
+
const pout = pin / ratio;
|
|
146
|
+
// the grain spans the neighbouring marks, capped at 1.5 periods a side
|
|
147
|
+
const left =
|
|
148
|
+
k > 0 ? Math.min(period * 1.5, Math.max(2, m - marks[k - 1]!)) : period;
|
|
149
|
+
const right =
|
|
150
|
+
k + 1 < marks.length
|
|
151
|
+
? Math.min(period * 1.5, Math.max(2, marks[k + 1]! - m))
|
|
152
|
+
: period;
|
|
153
|
+
const f = voiced ? formant : 1;
|
|
154
|
+
const gain = voiced ? Math.min(2, (pout * f) / pin) : 1;
|
|
155
|
+
// output span of the grain, in output samples around t
|
|
156
|
+
const lo = Math.max(offset, Math.ceil(t - left / f));
|
|
157
|
+
const hi = Math.min(end - 1, Math.floor(t + right / f));
|
|
158
|
+
for (let o = lo; o <= hi; o += 1) {
|
|
159
|
+
const j = (o - t) * f;
|
|
160
|
+
const w =
|
|
161
|
+
j < 0
|
|
162
|
+
? 0.5 * (1 + Math.cos((Math.PI * j) / left))
|
|
163
|
+
: 0.5 * (1 + Math.cos((Math.PI * j) / right));
|
|
164
|
+
out[o - offset]! += hermiteAt(x, m + j) * w * gain;
|
|
165
|
+
}
|
|
166
|
+
// unvoiced grains are copied in place, so the walk re-anchors on the
|
|
167
|
+
// marks there and enters every voiced run on its first mark
|
|
168
|
+
t = !voiced && k + 1 < marks.length ? marks[k + 1]! : t + pout;
|
|
169
|
+
if (k + 1 >= marks.length && t > m + 2 * period) break;
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Stereo PSOLA: marks found once on the mid signal and shared by both
|
|
176
|
+
* channels, so the image does not smear. `curve.f0` comes from the mono
|
|
177
|
+
* mixdown.
|
|
178
|
+
*/
|
|
179
|
+
export function psolaStereo(
|
|
180
|
+
left: Float64Array,
|
|
181
|
+
right: Float64Array,
|
|
182
|
+
sampleRate: number,
|
|
183
|
+
curve: ShiftCurve,
|
|
184
|
+
options: PsolaOptions = {},
|
|
185
|
+
): [Float64Array, Float64Array] {
|
|
186
|
+
const mid = new Float64Array(left.length);
|
|
187
|
+
for (let i = 0; i < mid.length; i += 1)
|
|
188
|
+
mid[i] = 0.5 * (left[i]! + (right[i] ?? 0));
|
|
189
|
+
const marks = options.marks ?? pitchMarks(mid, sampleRate, curve.f0);
|
|
190
|
+
return [
|
|
191
|
+
psola(left, sampleRate, curve, { ...options, marks }),
|
|
192
|
+
psola(right, sampleRate, curve, { ...options, marks }),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A constant shift of `cents` over an f0-per-sample curve. */
|
|
197
|
+
export function constantShift(f0: Float64Array, cents: number): ShiftCurve {
|
|
198
|
+
return { f0, cents: new Float64Array(f0.length).fill(cents) };
|
|
199
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The fixed per-track effects chain (`FX_CHAIN` in core/fx.ts):
|
|
3
3
|
*
|
|
4
|
-
* filter → djf → autofilter → vowel → crush → distort → stomp → head →
|
|
4
|
+
* filter → djf → autofilter → formant → vowel → crush → distort → stomp → head →
|
|
5
5
|
* cab → wobble → bloom → swell → tremolo →
|
|
6
6
|
* compressor → pan → double → phaser → chorus → leslie → postgain →
|
|
7
7
|
* delay → reverb
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
applyTrackFilter,
|
|
22
22
|
applyVowel,
|
|
23
23
|
} from "./filter.ts";
|
|
24
|
+
import { applyFormant } from "./formant.ts";
|
|
24
25
|
import { applyChorus, applyLeslie, applyPhaser } from "./modulation.ts";
|
|
25
26
|
import { applyCab, applyHead, applyStomp } from "./rig/index.ts";
|
|
26
27
|
import { applyDelay, applyReverb } from "./space.ts";
|
|
@@ -62,6 +63,7 @@ type StereoStage = (
|
|
|
62
63
|
const MONO: Readonly<Partial<Record<FxName, MonoStage>>> = Object.freeze({
|
|
63
64
|
djf: applyDjFilter,
|
|
64
65
|
autofilter: applyAutoFilter,
|
|
66
|
+
formant: applyFormant,
|
|
65
67
|
vowel: applyVowel,
|
|
66
68
|
crush: applyCrush,
|
|
67
69
|
distort: applyDistort,
|
|
@@ -30,6 +30,11 @@ export type EffectContext = Readonly<{
|
|
|
30
30
|
* swell; 0.6.1). Filled only when such a stage is on.
|
|
31
31
|
*/
|
|
32
32
|
notes?: readonly EffectNote[];
|
|
33
|
+
/**
|
|
34
|
+
* Song seconds at buffer sample 0 when an arranged window renders (the
|
|
35
|
+
* renderer's `seedSeconds`): the formant stage anchors its frames there.
|
|
36
|
+
*/
|
|
37
|
+
seedSeconds?: number;
|
|
33
38
|
}>;
|
|
34
39
|
|
|
35
40
|
/** One note as a note-aware effect sees it: where it sounds and its pitch. */
|
|
@@ -194,6 +199,44 @@ export class Biquad {
|
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Topology-preserving-transform state-variable filter (Zavalishin, Cytomic):
|
|
204
|
+
* g = tan(pi fc / fs), k = 1 / Q. Its state stays valid when the cutoff
|
|
205
|
+
* moves between samples, so it is the filter for modulated cutoffs where a
|
|
206
|
+
* direct-form biquad's state would no longer match its new coefficients.
|
|
207
|
+
* `bpf` is normalized to 0 dB at the peak, like `Biquad`'s band-pass.
|
|
208
|
+
*/
|
|
209
|
+
export class Svf {
|
|
210
|
+
private k = Math.SQRT2;
|
|
211
|
+
private a1 = 1;
|
|
212
|
+
private a2 = 0;
|
|
213
|
+
private a3 = 0;
|
|
214
|
+
private ic1 = 0;
|
|
215
|
+
private ic2 = 0;
|
|
216
|
+
|
|
217
|
+
constructor(private readonly type: FilterType) {}
|
|
218
|
+
|
|
219
|
+
set(cutoff: number, q: number, sampleRate: number): void {
|
|
220
|
+
const frequency = clamp(cutoff, 20, sampleRate * 0.45);
|
|
221
|
+
const g = Math.tan((Math.PI * frequency) / sampleRate);
|
|
222
|
+
this.k = 1 / Math.max(0.1, q);
|
|
223
|
+
this.a1 = 1 / (1 + g * (g + this.k));
|
|
224
|
+
this.a2 = g * this.a1;
|
|
225
|
+
this.a3 = g * this.a2;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
process(input: number): number {
|
|
229
|
+
const v3 = input - this.ic2;
|
|
230
|
+
const v1 = this.a1 * this.ic1 + this.a2 * v3;
|
|
231
|
+
const v2 = this.ic2 + this.a2 * this.ic1 + this.a3 * v3;
|
|
232
|
+
this.ic1 = 2 * v1 - this.ic1;
|
|
233
|
+
this.ic2 = 2 * v2 - this.ic2;
|
|
234
|
+
if (this.type === "lpf") return v2;
|
|
235
|
+
if (this.type === "hpf") return input - this.k * v1 - v2;
|
|
236
|
+
return this.k * v1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
197
240
|
/** One-pole low-pass; `set` takes a cutoff in Hz. */
|
|
198
241
|
export class OnePole {
|
|
199
242
|
private coefficient = 0;
|
|
@@ -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;
|
|
@@ -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;
|