@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pitch shift at constant duration (0.6 B2 `shift`, `formant`), ported from
|
|
3
|
+
* the granular prototype (`proto/granular/stretch.ts` `pitchShift`):
|
|
4
|
+
* phase-vocoder stretch by rho = 2^(st/12) onto an intermediate axis (bins
|
|
5
|
+
* above Nyquist/rho dropped so the read-back cannot alias), then read it
|
|
6
|
+
* rho times faster with a 4-point Hermite, so the length is kept exactly.
|
|
7
|
+
*
|
|
8
|
+
* Formants: absent follows the pitch (like a tape); a number moves them
|
|
9
|
+
* that many semitones from where they were, so `0` keeps them (cepstral
|
|
10
|
+
* envelope from `envelope.ts`: magnitudes are divided by the frame's
|
|
11
|
+
* envelope and multiplied by the envelope warped to where it must land
|
|
12
|
+
* after the read-back). Identity phase locking after Laroche and Dolson,
|
|
13
|
+
* "Improved phase vocoder time-scale modification of audio", IEEE TSAP 1999.
|
|
14
|
+
* Strudel's `stretch` (superdough's phase-vocoder worklet) is the same idea.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
ENVELOPE_ITERATIONS,
|
|
18
|
+
ENVELOPE_LIFTER_SECONDS,
|
|
19
|
+
cepstralEnvelope,
|
|
20
|
+
envelopeScratch,
|
|
21
|
+
peakEnvelope,
|
|
22
|
+
} from "./envelope.ts";
|
|
23
|
+
import { hermite4 } from "./interp.ts";
|
|
24
|
+
import {
|
|
25
|
+
HANN_SQUARED_OLA_QUARTER,
|
|
26
|
+
analyse,
|
|
27
|
+
frameSize,
|
|
28
|
+
hann,
|
|
29
|
+
inverse,
|
|
30
|
+
princarg,
|
|
31
|
+
sampleAt,
|
|
32
|
+
} from "./stft.ts";
|
|
33
|
+
|
|
34
|
+
/** Analysis frame, as the `tones` fit (85 ms). */
|
|
35
|
+
export const SHIFT_FRAME_SECONDS = 0.085;
|
|
36
|
+
/** Vocoder hops between yields of `pitchShiftJob`. */
|
|
37
|
+
const SHIFT_YIELD_HOPS = 16;
|
|
38
|
+
/** Largest per-frame level restore after formant correction (12 dB). */
|
|
39
|
+
const FORMANT_LEVEL_CAP = 4;
|
|
40
|
+
/** Largest formant correction gain (24 dB), so a deep envelope dip cannot explode. */
|
|
41
|
+
const MAX_FORMANT_GAIN = 16;
|
|
42
|
+
|
|
43
|
+
export type ShiftOptions = Readonly<{
|
|
44
|
+
/** Formant shift in semitones; 0 keeps them; absent follows the pitch. */
|
|
45
|
+
formant?: number;
|
|
46
|
+
/** Frame length in seconds (rounded up to a power of two). */
|
|
47
|
+
frame?: number;
|
|
48
|
+
/** Cepstral lifter in seconds. */
|
|
49
|
+
lifter?: number;
|
|
50
|
+
/** True-envelope passes (0 is the plain cepstral envelope; default 16). */
|
|
51
|
+
iterations?: number;
|
|
52
|
+
/** Shrink the lifter to a quarter pitch period per frame (default on). */
|
|
53
|
+
adaptive?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Envelope used to keep formants: "peaks" (default) joins the harmonic
|
|
56
|
+
* peaks; "cepstral" is the true cepstral envelope alone.
|
|
57
|
+
*/
|
|
58
|
+
envelope?: "peaks" | "cepstral";
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Phase vocoder reading `x` at `t / rho` for `outLength` frames, with
|
|
63
|
+
* bins above Nyquist/rho removed and optional formant correction for a
|
|
64
|
+
* later read-back at `rho`.
|
|
65
|
+
*/
|
|
66
|
+
function* stretchForShift(
|
|
67
|
+
x: ArrayLike<number>,
|
|
68
|
+
sampleRate: number,
|
|
69
|
+
rho: number,
|
|
70
|
+
outLength: number,
|
|
71
|
+
formantRatio: number | undefined,
|
|
72
|
+
options: ShiftOptions,
|
|
73
|
+
): Generator<void, Float64Array> {
|
|
74
|
+
const n = frameSize(options.frame ?? SHIFT_FRAME_SECONDS, sampleRate);
|
|
75
|
+
const hs = n / 4;
|
|
76
|
+
const half = n / 2;
|
|
77
|
+
const w = hann(n);
|
|
78
|
+
const norm = 1 / HANN_SQUARED_OLA_QUARTER;
|
|
79
|
+
const keep = rho > 1 ? Math.floor(half / rho) : half;
|
|
80
|
+
const lifter = Math.max(
|
|
81
|
+
4,
|
|
82
|
+
Math.round((options.lifter ?? ENVELOPE_LIFTER_SECONDS) * sampleRate),
|
|
83
|
+
);
|
|
84
|
+
const periodRange = [sampleRate / 800, sampleRate / 50] as const;
|
|
85
|
+
const out = new Float64Array(outLength + n);
|
|
86
|
+
const reA = new Float64Array(n);
|
|
87
|
+
const imA = new Float64Array(n);
|
|
88
|
+
const reB = new Float64Array(n);
|
|
89
|
+
const imB = new Float64Array(n);
|
|
90
|
+
const mag = new Float64Array(half + 1);
|
|
91
|
+
const phA = new Float64Array(half + 1);
|
|
92
|
+
const syn = new Float64Array(half + 1);
|
|
93
|
+
const next = new Float64Array(half + 1);
|
|
94
|
+
const omega = new Float64Array(half + 1);
|
|
95
|
+
const env = new Float64Array(half + 1);
|
|
96
|
+
const scratch = envelopeScratch(n);
|
|
97
|
+
const smooth = new Float64Array(half + 1);
|
|
98
|
+
const envPeaks: number[] = [];
|
|
99
|
+
const peaks: number[] = [];
|
|
100
|
+
for (let m = 0; m * hs < outLength; m += 1) {
|
|
101
|
+
if (m % SHIFT_YIELD_HOPS === SHIFT_YIELD_HOPS - 1) yield;
|
|
102
|
+
const at = Math.round((m * hs + half) / rho - half);
|
|
103
|
+
analyse(x, at, w, reA, imA);
|
|
104
|
+
analyse(x, at - hs, w, reB, imB);
|
|
105
|
+
for (let k = 0; k <= half; k += 1) {
|
|
106
|
+
mag[k] = Math.hypot(reA[k]!, imA[k]!);
|
|
107
|
+
phA[k] = Math.atan2(imA[k]!, reA[k]!);
|
|
108
|
+
const phB = Math.atan2(imB[k]!, reB[k]!);
|
|
109
|
+
const bin = (2 * Math.PI * k * hs) / n;
|
|
110
|
+
omega[k] = bin + princarg(phA[k]! - phB - bin);
|
|
111
|
+
}
|
|
112
|
+
if (formantRatio !== undefined)
|
|
113
|
+
cepstralEnvelope(
|
|
114
|
+
mag,
|
|
115
|
+
n,
|
|
116
|
+
lifter,
|
|
117
|
+
env,
|
|
118
|
+
scratch,
|
|
119
|
+
options.iterations ?? ENVELOPE_ITERATIONS,
|
|
120
|
+
options.adaptive === false ? undefined : periodRange,
|
|
121
|
+
);
|
|
122
|
+
if (formantRatio !== undefined && options.envelope !== "cepstral") {
|
|
123
|
+
smooth.set(env);
|
|
124
|
+
peakEnvelope(mag, half, env, smooth, envPeaks);
|
|
125
|
+
}
|
|
126
|
+
if (m === 0) syn.set(phA);
|
|
127
|
+
else {
|
|
128
|
+
peaks.length = 0;
|
|
129
|
+
for (let k = 2; k <= half - 2; k += 1) {
|
|
130
|
+
const v = mag[k]!;
|
|
131
|
+
if (
|
|
132
|
+
v > mag[k - 1]! &&
|
|
133
|
+
v >= mag[k + 1]! &&
|
|
134
|
+
v > mag[k - 2]! &&
|
|
135
|
+
v >= mag[k + 2]!
|
|
136
|
+
)
|
|
137
|
+
peaks.push(k);
|
|
138
|
+
}
|
|
139
|
+
if (peaks.length === 0)
|
|
140
|
+
for (let k = 0; k <= half; k += 1) syn[k] = syn[k]! + omega[k]!;
|
|
141
|
+
else {
|
|
142
|
+
for (const p of peaks) next[p] = syn[p]! + omega[p]!;
|
|
143
|
+
let j = 0;
|
|
144
|
+
for (let k = 0; k <= half; k += 1) {
|
|
145
|
+
while (
|
|
146
|
+
j + 1 < peaks.length &&
|
|
147
|
+
Math.abs(peaks[j + 1]! - k) < Math.abs(peaks[j]! - k)
|
|
148
|
+
)
|
|
149
|
+
j += 1;
|
|
150
|
+
const p = peaks[j]!;
|
|
151
|
+
if (k !== p) next[k] = next[p]! + phA[k]! - phA[p]!;
|
|
152
|
+
}
|
|
153
|
+
syn.set(next);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
let before = 0;
|
|
157
|
+
let after = 0;
|
|
158
|
+
for (let k = 0; k <= half; k += 1) {
|
|
159
|
+
let a = k > keep ? 0 : mag[k]!;
|
|
160
|
+
before += a * a;
|
|
161
|
+
if (formantRatio !== undefined && a > 0) {
|
|
162
|
+
// Bin k lands at k*rho after the read-back; give it the envelope
|
|
163
|
+
// found at k*rho/formantRatio in the source frame.
|
|
164
|
+
const src = (k * rho) / formantRatio;
|
|
165
|
+
const i0 = Math.floor(src);
|
|
166
|
+
const target =
|
|
167
|
+
i0 + 1 <= half
|
|
168
|
+
? env[i0]! + (env[i0 + 1]! - env[i0]!) * (src - i0)
|
|
169
|
+
: env[half]! * 1e-3;
|
|
170
|
+
a *= Math.min(target / env[k]!, MAX_FORMANT_GAIN);
|
|
171
|
+
}
|
|
172
|
+
after += a * a;
|
|
173
|
+
reA[k] = a * Math.cos(syn[k]!);
|
|
174
|
+
imA[k] = a * Math.sin(syn[k]!);
|
|
175
|
+
if (k > 0 && k < half) {
|
|
176
|
+
reA[n - k] = reA[k]!;
|
|
177
|
+
imA[n - k] = -imA[k]!;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
imA[0] = 0;
|
|
181
|
+
imA[half] = 0;
|
|
182
|
+
// Formant correction reshapes the spectrum, not the loudness: give the
|
|
183
|
+
// frame back its energy (a scale moves no peak), capped at 12 dB.
|
|
184
|
+
if (formantRatio !== undefined && after > 0) {
|
|
185
|
+
const gain = Math.min(Math.sqrt(before / after), FORMANT_LEVEL_CAP);
|
|
186
|
+
for (let k = 0; k < n; k += 1) {
|
|
187
|
+
reA[k] = reA[k]! * gain;
|
|
188
|
+
imA[k] = imA[k]! * gain;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
inverse(reA, imA);
|
|
192
|
+
const o0 = m * hs;
|
|
193
|
+
for (let i = 0; i < n; i += 1)
|
|
194
|
+
out[o0 + i] = out[o0 + i]! + reA[i]! * w[i]! * norm;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `x` shifted by `semitones` at the same length. `formant` (semitones)
|
|
201
|
+
* moves the formants independently; 0 keeps them.
|
|
202
|
+
*/
|
|
203
|
+
export function pitchShift(
|
|
204
|
+
x: ArrayLike<number>,
|
|
205
|
+
sampleRate: number,
|
|
206
|
+
semitones: number,
|
|
207
|
+
options: ShiftOptions = {},
|
|
208
|
+
): Float32Array {
|
|
209
|
+
const job = pitchShiftJob(x, sampleRate, semitones, options);
|
|
210
|
+
for (;;) {
|
|
211
|
+
const step = job.next();
|
|
212
|
+
if (step.done) return step.value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* `pitchShift` as resumable work: yields every few vocoder hops so a live
|
|
218
|
+
* engine can spread a long shift over idle time (`src/audio/fit.ts`).
|
|
219
|
+
*/
|
|
220
|
+
export function* pitchShiftJob(
|
|
221
|
+
x: ArrayLike<number>,
|
|
222
|
+
sampleRate: number,
|
|
223
|
+
semitones: number,
|
|
224
|
+
options: ShiftOptions = {},
|
|
225
|
+
): Generator<void, Float32Array> {
|
|
226
|
+
const length = x.length;
|
|
227
|
+
const out = new Float32Array(length);
|
|
228
|
+
if (semitones === 0 && options.formant === undefined) {
|
|
229
|
+
for (let i = 0; i < length; i += 1) out[i] = x[i]!;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
const rho = 2 ** (semitones / 12);
|
|
233
|
+
const formantRatio =
|
|
234
|
+
options.formant === undefined ? undefined : 2 ** (options.formant / 12);
|
|
235
|
+
const midLength = Math.ceil(length * rho) + 4;
|
|
236
|
+
const y = yield* stretchForShift(
|
|
237
|
+
x,
|
|
238
|
+
sampleRate,
|
|
239
|
+
rho,
|
|
240
|
+
midLength,
|
|
241
|
+
formantRatio,
|
|
242
|
+
options,
|
|
243
|
+
);
|
|
244
|
+
for (let i = 0; i < length; i += 1) {
|
|
245
|
+
const pos = i * rho;
|
|
246
|
+
const j = Math.floor(pos);
|
|
247
|
+
out[i] = hermite4(
|
|
248
|
+
sampleAt(y, j - 1),
|
|
249
|
+
sampleAt(y, j),
|
|
250
|
+
sampleAt(y, j + 1),
|
|
251
|
+
sampleAt(y, j + 2),
|
|
252
|
+
pos - j,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
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 →
|
|
5
|
-
* cab → tremolo →
|
|
6
|
-
* compressor → pan → phaser → chorus → leslie → postgain →
|
|
4
|
+
* filter → djf → autofilter → formant → vowel → crush → distort → stomp → head →
|
|
5
|
+
* cab → wobble → bloom → swell → tremolo →
|
|
6
|
+
* compressor → pan → double → phaser → chorus → leslie → postgain →
|
|
7
|
+
* delay → reverb
|
|
7
8
|
*
|
|
8
9
|
* Stages before pan run on the mono voice sum; pan spreads it to stereo;
|
|
9
10
|
* the rest run on the stereo pair. Disabled stages cost nothing.
|
|
@@ -12,6 +13,7 @@ import { FX_CHAIN, type FxName, type FxValues } from "../../../core/fx.ts";
|
|
|
12
13
|
import type { Track } from "../../../core/score.ts";
|
|
13
14
|
import type { EffectContext } from "./common.ts";
|
|
14
15
|
import { applyCrush, applyDistort } from "./drive.ts";
|
|
16
|
+
import { applyBloom, applyDouble, applySwell, applyWobble } from "./gaze.ts";
|
|
15
17
|
import { applyCompressor, applyPostgain, applyTremolo } from "./dynamics.ts";
|
|
16
18
|
import {
|
|
17
19
|
applyAutoFilter,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
applyTrackFilter,
|
|
20
22
|
applyVowel,
|
|
21
23
|
} from "./filter.ts";
|
|
24
|
+
import { applyFormant } from "./formant.ts";
|
|
22
25
|
import { applyChorus, applyLeslie, applyPhaser } from "./modulation.ts";
|
|
23
26
|
import { applyCab, applyHead, applyStomp } from "./rig/index.ts";
|
|
24
27
|
import { applyDelay, applyReverb } from "./space.ts";
|
|
@@ -32,7 +35,16 @@ export {
|
|
|
32
35
|
reverbWet,
|
|
33
36
|
type ReverbWet,
|
|
34
37
|
} from "./space.ts";
|
|
35
|
-
export {
|
|
38
|
+
export {
|
|
39
|
+
interpolateAutomation,
|
|
40
|
+
type EffectContext,
|
|
41
|
+
type EffectNote,
|
|
42
|
+
} from "./common.ts";
|
|
43
|
+
|
|
44
|
+
/** Whether `track` has a stage that reads `EffectContext.notes`. */
|
|
45
|
+
export function needsEffectNotes(track: Track | undefined): boolean {
|
|
46
|
+
return Boolean(track?.fx?.bloom || track?.fx?.swell);
|
|
47
|
+
}
|
|
36
48
|
|
|
37
49
|
type MonoStage = (
|
|
38
50
|
buffer: Float64Array,
|
|
@@ -51,17 +63,22 @@ type StereoStage = (
|
|
|
51
63
|
const MONO: Readonly<Partial<Record<FxName, MonoStage>>> = Object.freeze({
|
|
52
64
|
djf: applyDjFilter,
|
|
53
65
|
autofilter: applyAutoFilter,
|
|
66
|
+
formant: applyFormant,
|
|
54
67
|
vowel: applyVowel,
|
|
55
68
|
crush: applyCrush,
|
|
56
69
|
distort: applyDistort,
|
|
57
70
|
stomp: applyStomp,
|
|
58
71
|
head: applyHead,
|
|
59
72
|
cab: applyCab,
|
|
73
|
+
wobble: applyWobble,
|
|
74
|
+
bloom: applyBloom,
|
|
75
|
+
swell: applySwell,
|
|
60
76
|
tremolo: applyTremolo,
|
|
61
77
|
compressor: applyCompressor,
|
|
62
78
|
});
|
|
63
79
|
|
|
64
80
|
const STEREO: Readonly<Partial<Record<FxName, StereoStage>>> = Object.freeze({
|
|
81
|
+
double: applyDouble,
|
|
65
82
|
phaser: applyPhaser,
|
|
66
83
|
chorus: applyChorus,
|
|
67
84
|
leslie: applyLeslie,
|
|
@@ -25,6 +25,25 @@ export type EffectContext = Readonly<{
|
|
|
25
25
|
irs?: ReadonlyMap<string, DecodedSample>;
|
|
26
26
|
/** The song's tempo map in samples; absent: constant tempo. */
|
|
27
27
|
warp?: SampleWarp;
|
|
28
|
+
/**
|
|
29
|
+
* The track's notes in buffer samples, for note-aware stages (bloom,
|
|
30
|
+
* swell; 0.6.1). Filled only when such a stage is on.
|
|
31
|
+
*/
|
|
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;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
/** One note as a note-aware effect sees it: where it sounds and its pitch. */
|
|
41
|
+
export type EffectNote = Readonly<{
|
|
42
|
+
id: string;
|
|
43
|
+
start: number;
|
|
44
|
+
length: number;
|
|
45
|
+
hz: number;
|
|
46
|
+
velocity: number;
|
|
28
47
|
}>;
|
|
29
48
|
|
|
30
49
|
/** Score tick at sample `index`, through the tempo map when there is one. */
|
|
@@ -180,6 +199,44 @@ export class Biquad {
|
|
|
180
199
|
}
|
|
181
200
|
}
|
|
182
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
|
+
|
|
183
240
|
/** One-pole low-pass; `set` takes a cutoff in Hz. */
|
|
184
241
|
export class OnePole {
|
|
185
242
|
private coefficient = 0;
|