@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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Band-pass banks (0.7 vocoder): RBJ biquads, their group delay and a
|
|
3
|
+
* sample-rate-independent band layout. Band centres and widths depend only
|
|
4
|
+
* on the band count, range and width, never on the sample rate: a band whose
|
|
5
|
+
* centre lies above 0.45 sr is muted instead, so a 22 050 Hz audition and a
|
|
6
|
+
* 48 kHz export have the same layout.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Biquad = Readonly<{
|
|
10
|
+
b0: number;
|
|
11
|
+
b1: number;
|
|
12
|
+
b2: number;
|
|
13
|
+
a1: number;
|
|
14
|
+
a2: number;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** RBJ constant-0-dB-peak band-pass, bandwidth in octaves, pre-warped. */
|
|
18
|
+
export function bandpass(
|
|
19
|
+
hz: number,
|
|
20
|
+
octaves: number,
|
|
21
|
+
sampleRate: number,
|
|
22
|
+
): Biquad {
|
|
23
|
+
const w = (2 * Math.PI * hz) / sampleRate;
|
|
24
|
+
const s = Math.sin(w);
|
|
25
|
+
const alpha = s * Math.sinh(((Math.LN2 / 2) * octaves * w) / s);
|
|
26
|
+
const a0 = 1 + alpha;
|
|
27
|
+
return {
|
|
28
|
+
b0: alpha / a0,
|
|
29
|
+
b1: 0,
|
|
30
|
+
b2: -alpha / a0,
|
|
31
|
+
a1: (-2 * Math.cos(w)) / a0,
|
|
32
|
+
a2: (1 - alpha) / a0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** RBJ Butterworth high-pass (Q = 1/sqrt 2). */
|
|
37
|
+
export function highpass(hz: number, sampleRate: number): Biquad {
|
|
38
|
+
const w = (2 * Math.PI * hz) / sampleRate;
|
|
39
|
+
const alpha = Math.sin(w) / (2 * Math.SQRT1_2);
|
|
40
|
+
const c = Math.cos(w);
|
|
41
|
+
const a0 = 1 + alpha;
|
|
42
|
+
return {
|
|
43
|
+
b0: (1 + c) / 2 / a0,
|
|
44
|
+
b1: -(1 + c) / a0,
|
|
45
|
+
b2: (1 + c) / 2 / a0,
|
|
46
|
+
a1: (-2 * c) / a0,
|
|
47
|
+
a2: (1 - alpha) / a0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Group delay in samples of one biquad at angle `w` (numeric phase slope). */
|
|
52
|
+
export function groupDelay(f: Biquad, w: number): number {
|
|
53
|
+
const phase = (x: number): number => {
|
|
54
|
+
const nr = f.b0 + f.b1 * Math.cos(x) + f.b2 * Math.cos(2 * x);
|
|
55
|
+
const ni = -(f.b1 * Math.sin(x) + f.b2 * Math.sin(2 * x));
|
|
56
|
+
const dr = 1 + f.a1 * Math.cos(x) + f.a2 * Math.cos(2 * x);
|
|
57
|
+
const di = -(f.a1 * Math.sin(x) + f.a2 * Math.sin(2 * x));
|
|
58
|
+
return Math.atan2(ni, nr) - Math.atan2(di, dr);
|
|
59
|
+
};
|
|
60
|
+
const d = 1e-5;
|
|
61
|
+
let dp = phase(w + d) - phase(w - d);
|
|
62
|
+
while (dp > Math.PI) dp -= 2 * Math.PI;
|
|
63
|
+
while (dp < -Math.PI) dp += 2 * Math.PI;
|
|
64
|
+
return -dp / (2 * d);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Runs a biquad over `x` in place (transposed direct form II). */
|
|
68
|
+
export function runBiquad(f: Biquad, x: Float64Array): void {
|
|
69
|
+
let z1 = 0;
|
|
70
|
+
let z2 = 0;
|
|
71
|
+
const { b0, b1, b2, a1, a2 } = f;
|
|
72
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
73
|
+
const v = x[i]!;
|
|
74
|
+
const y = b0 * v + z1;
|
|
75
|
+
z1 = b1 * v - a1 * y + z2;
|
|
76
|
+
z2 = b2 * v - a2 * y;
|
|
77
|
+
x[i] = y;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type BandLayout = Readonly<{
|
|
82
|
+
bands: number;
|
|
83
|
+
lo: number;
|
|
84
|
+
hi: number;
|
|
85
|
+
width: number;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Log-spaced centres from `lo` to `hi`. Each of the two cascaded sections is
|
|
90
|
+
* about 1.55x wider than the target so the pair's -3 dB width matches
|
|
91
|
+
* spacing x width.
|
|
92
|
+
*/
|
|
93
|
+
export function bandLayout(p: BandLayout): {
|
|
94
|
+
centres: number[];
|
|
95
|
+
ratio: number;
|
|
96
|
+
octaves: number;
|
|
97
|
+
} {
|
|
98
|
+
const n = Math.max(2, Math.round(p.bands));
|
|
99
|
+
const ratio = (p.hi / p.lo) ** (1 / (n - 1));
|
|
100
|
+
const centres = Array.from({ length: n }, (_, k) => p.lo * ratio ** k);
|
|
101
|
+
return { centres, ratio, octaves: Math.log2(ratio) * p.width * 1.55 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type BankBand = Readonly<{
|
|
105
|
+
hz: number;
|
|
106
|
+
live: boolean;
|
|
107
|
+
filter: Biquad;
|
|
108
|
+
/** Modulator advance: the pair's group delay at hz plus the attack. */
|
|
109
|
+
advance: number;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The bank at one sample rate. Each band's modulator advance cancels the
|
|
114
|
+
* analysis filters' group delay and the follower attack, so consonants land
|
|
115
|
+
* on the beat.
|
|
116
|
+
*/
|
|
117
|
+
export function bankDesign(
|
|
118
|
+
p: BandLayout & { attack?: number },
|
|
119
|
+
sampleRate: number,
|
|
120
|
+
): { bands: BankBand[]; ratio: number } {
|
|
121
|
+
const { centres, ratio, octaves } = bandLayout(p);
|
|
122
|
+
const bands = centres.map((hz) => {
|
|
123
|
+
const live = hz <= 0.45 * sampleRate;
|
|
124
|
+
const filter = bandpass(
|
|
125
|
+
Math.min(hz, 0.45 * sampleRate),
|
|
126
|
+
octaves,
|
|
127
|
+
sampleRate,
|
|
128
|
+
);
|
|
129
|
+
const advance = live
|
|
130
|
+
? Math.round(
|
|
131
|
+
2 * groupDelay(filter, (2 * Math.PI * hz) / sampleRate) +
|
|
132
|
+
(p.attack ?? 0) * sampleRate,
|
|
133
|
+
)
|
|
134
|
+
: 0;
|
|
135
|
+
return { hz, live, filter, advance };
|
|
136
|
+
});
|
|
137
|
+
return { bands, ratio };
|
|
138
|
+
}
|
|
@@ -93,6 +93,15 @@ export function cepstralEnvelope(
|
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* A peak joins the envelope only within this factor (-9 dB) of the smooth
|
|
98
|
+
* cepstral envelope: harmonics sit on it, while window sidelobes and breath
|
|
99
|
+
* noise in the valleys between harmonics sit far below. Without this the
|
|
100
|
+
* envelope followed the source's harmonic comb, and `formant 0` imposed the
|
|
101
|
+
* old pitch on the shifted one (pitch.md 3.5).
|
|
102
|
+
*/
|
|
103
|
+
const HARMONIC_PEAK_FLOOR = 10 ** (-9 / 20);
|
|
104
|
+
|
|
96
105
|
/**
|
|
97
106
|
* Harmonic-peak envelope (0.6.1): log magnitude interpolated linearly
|
|
98
107
|
* between the spectral peaks (each refined by a parabola to its true bin
|
|
@@ -122,6 +131,7 @@ export function peakEnvelope(
|
|
|
122
131
|
const v = mag[k]!;
|
|
123
132
|
if (
|
|
124
133
|
v > floor &&
|
|
134
|
+
v >= smooth[k]! * HARMONIC_PEAK_FLOOR &&
|
|
125
135
|
v > mag[k - 1]! &&
|
|
126
136
|
v >= mag[k + 1]! &&
|
|
127
137
|
v > mag[k - 2]! &&
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope followers (0.7 vocoder): one-pole attack/release on |x|, and the
|
|
3
|
+
* gate curve built on one. Pure functions of the input buffer, so a render
|
|
4
|
+
* window with enough pre-roll settles to the full render's values.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** One-pole coefficient for a time constant in seconds (0 = instant). */
|
|
8
|
+
export function followCoef(seconds: number, sampleRate: number): number {
|
|
9
|
+
return seconds <= 0 ? 0 : Math.exp(-1 / (seconds * sampleRate));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Follows |x| with separate attack and release times. Where `hold[i]` is
|
|
14
|
+
* non-zero the envelope keeps its last value (the vocoder's freeze).
|
|
15
|
+
*/
|
|
16
|
+
export function follow(
|
|
17
|
+
x: Float64Array,
|
|
18
|
+
attack: number,
|
|
19
|
+
release: number,
|
|
20
|
+
sampleRate: number,
|
|
21
|
+
hold?: Uint8Array,
|
|
22
|
+
): Float64Array {
|
|
23
|
+
const a = followCoef(attack, sampleRate);
|
|
24
|
+
const r = followCoef(release, sampleRate);
|
|
25
|
+
const out = new Float64Array(x.length);
|
|
26
|
+
let e = 0;
|
|
27
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
28
|
+
if (!hold || hold[i] === 0) {
|
|
29
|
+
const v = Math.abs(x[i]!);
|
|
30
|
+
const c = v > e ? a : r;
|
|
31
|
+
e = c * e + (1 - c) * v;
|
|
32
|
+
}
|
|
33
|
+
out[i] = e;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gate gain 0..1 per sample: a broadband follower against `thresholdDb`
|
|
40
|
+
* with a 6 dB soft knee, smoothed over 5 ms. Undefined when the gate is off
|
|
41
|
+
* (threshold at or below -119 dBFS).
|
|
42
|
+
*/
|
|
43
|
+
export function gateCurve(
|
|
44
|
+
x: Float64Array,
|
|
45
|
+
sampleRate: number,
|
|
46
|
+
thresholdDb: number,
|
|
47
|
+
): Float64Array | undefined {
|
|
48
|
+
if (thresholdDb <= -119) return undefined;
|
|
49
|
+
const level = follow(x, 0.001, 0.05, sampleRate);
|
|
50
|
+
const out = new Float64Array(x.length);
|
|
51
|
+
const c = followCoef(0.005, sampleRate);
|
|
52
|
+
let s = 0;
|
|
53
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
54
|
+
const db = 20 * Math.log10(level[i]! + 1e-12);
|
|
55
|
+
const t = Math.min(1, Math.max(0, (db - thresholdDb) / 6));
|
|
56
|
+
s = c * s + (1 - c) * t;
|
|
57
|
+
out[i] = s;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where `hold[i]` is non-zero, `curve` keeps its value from the sample
|
|
64
|
+
* before (in place): a frozen vocoder keeps its gate where it was, so the
|
|
65
|
+
* held vowel does not fade out once the modulator goes quiet.
|
|
66
|
+
*/
|
|
67
|
+
export function holdCurve(curve: Float64Array, hold: Uint8Array): void {
|
|
68
|
+
for (let i = 1; i < curve.length && i < hold.length; i += 1)
|
|
69
|
+
if (hold[i] !== 0) curve[i] = curve[i - 1]!;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The static `freeze` hold: 1 wherever the modulator is quiet (under the
|
|
74
|
+
* gate, or under -50 dBFS when the gate is lower or off), engaged
|
|
75
|
+
* `lead` seconds before the voice falls quiet so the envelopes are caught
|
|
76
|
+
* before their release. The carrier keeps the last sung vowel through every
|
|
77
|
+
* rest and follows the voice while it sings. Before the first note it holds
|
|
78
|
+
* silence. Local (the mod span plus its lookahead), so windows agree.
|
|
79
|
+
*/
|
|
80
|
+
export function quietHold(
|
|
81
|
+
x: Float64Array,
|
|
82
|
+
n: number,
|
|
83
|
+
sampleRate: number,
|
|
84
|
+
thresholdDb: number,
|
|
85
|
+
lead = 0.03,
|
|
86
|
+
): Uint8Array {
|
|
87
|
+
const level = follow(x, 0.001, 0.01, sampleRate);
|
|
88
|
+
const floor = 10 ** (Math.max(thresholdDb, -50) / 20);
|
|
89
|
+
const ahead = Math.max(0, Math.round(lead * sampleRate));
|
|
90
|
+
const hold = new Uint8Array(n);
|
|
91
|
+
let nextQuiet = Infinity;
|
|
92
|
+
for (let i = Math.min(level.length, n + ahead) - 1; i >= 0; i -= 1) {
|
|
93
|
+
if (level[i]! < floor) {
|
|
94
|
+
// the start of a quiet run reaches back `ahead` samples
|
|
95
|
+
if (i === 0 || level[i - 1]! >= floor) nextQuiet = i;
|
|
96
|
+
if (i < n) hold[i] = 1;
|
|
97
|
+
} else if (i < n && nextQuiet - i <= ahead) hold[i] = 1;
|
|
98
|
+
}
|
|
99
|
+
for (let i = level.length; i < n; i += 1) hold[i] = 1;
|
|
100
|
+
return hold;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Auto gate threshold for a whole asset: the 10th-percentile 10 ms frame
|
|
105
|
+
* level plus 6 dB (the mean-abs follower reads about 1 dB under RMS). A
|
|
106
|
+
* property of the asset, so every render window agrees. -120 when silent.
|
|
107
|
+
*/
|
|
108
|
+
export function autoGateDb(asset: Float64Array, sampleRate: number): number {
|
|
109
|
+
const hop = Math.max(1, Math.round(0.01 * sampleRate));
|
|
110
|
+
const levels: number[] = [];
|
|
111
|
+
for (let s = 0; s + hop <= asset.length; s += hop) {
|
|
112
|
+
let e = 0;
|
|
113
|
+
for (let i = s; i < s + hop; i += 1) e += asset[i]! * asset[i]!;
|
|
114
|
+
const db = 10 * Math.log10(e / hop + 1e-24);
|
|
115
|
+
if (db > -100) levels.push(db);
|
|
116
|
+
}
|
|
117
|
+
if (levels.length === 0) return -120;
|
|
118
|
+
levels.sort((a, b) => a - b);
|
|
119
|
+
return levels[Math.floor(levels.length * 0.1)]! + 6;
|
|
120
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vowel formant tables shared by the `vowel` filter (and, from the sing
|
|
3
|
+
* lane, the Klatt cascade): frequency (Hz), level (dB) and bandwidth (Hz)
|
|
4
|
+
* per formant, plus a log-frequency morph between two vowels.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type Formants = readonly (readonly [number, number, number])[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Formant frequency (Hz), level (dB) and bandwidth (Hz). The five plain
|
|
11
|
+
* vowels use the classic tenor formant table (published acoustic
|
|
12
|
+
* phonetics data); the extended set approximates F1–F3 from standard
|
|
13
|
+
* vowel charts with tenor-typical F4/F5.
|
|
14
|
+
*/
|
|
15
|
+
export const VOWEL_FORMANTS: Readonly<Record<string, Formants>> = Object.freeze(
|
|
16
|
+
{
|
|
17
|
+
a: [
|
|
18
|
+
[650, 0, 80],
|
|
19
|
+
[1080, -6, 90],
|
|
20
|
+
[2650, -7, 120],
|
|
21
|
+
[2900, -8, 130],
|
|
22
|
+
[3250, -22, 140],
|
|
23
|
+
],
|
|
24
|
+
e: [
|
|
25
|
+
[400, 0, 70],
|
|
26
|
+
[1700, -14, 80],
|
|
27
|
+
[2600, -12, 100],
|
|
28
|
+
[3200, -14, 120],
|
|
29
|
+
[3580, -20, 120],
|
|
30
|
+
],
|
|
31
|
+
i: [
|
|
32
|
+
[290, 0, 40],
|
|
33
|
+
[1870, -15, 90],
|
|
34
|
+
[2800, -18, 100],
|
|
35
|
+
[3250, -20, 120],
|
|
36
|
+
[3540, -30, 120],
|
|
37
|
+
],
|
|
38
|
+
o: [
|
|
39
|
+
[400, 0, 40],
|
|
40
|
+
[800, -10, 80],
|
|
41
|
+
[2600, -12, 100],
|
|
42
|
+
[2800, -12, 120],
|
|
43
|
+
[3000, -26, 120],
|
|
44
|
+
],
|
|
45
|
+
u: [
|
|
46
|
+
[350, 0, 40],
|
|
47
|
+
[600, -20, 60],
|
|
48
|
+
[2700, -17, 100],
|
|
49
|
+
[2900, -14, 120],
|
|
50
|
+
[3300, -26, 120],
|
|
51
|
+
],
|
|
52
|
+
ae: extended(660, 1720, 2410),
|
|
53
|
+
aa: extended(710, 1100, 2540),
|
|
54
|
+
oe: extended(390, 1680, 2400),
|
|
55
|
+
ue: extended(300, 1600, 2200),
|
|
56
|
+
y: extended(250, 1750, 2160),
|
|
57
|
+
uh: extended(600, 1170, 2390),
|
|
58
|
+
un: extended(500, 1400, 2500),
|
|
59
|
+
en: extended(550, 1650, 2500),
|
|
60
|
+
an: extended(650, 1050, 2550),
|
|
61
|
+
on: extended(450, 850, 2550),
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
function extended(f1: number, f2: number, f3: number): Formants {
|
|
66
|
+
return [
|
|
67
|
+
[f1, 0, 70],
|
|
68
|
+
[f2, -8, 90],
|
|
69
|
+
[f3, -14, 110],
|
|
70
|
+
[3300, -16, 130],
|
|
71
|
+
[3750, -24, 140],
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Formants `morph` (0..1) of the way from `from` to `to`: frequency and
|
|
77
|
+
* bandwidth interpolate on a log scale (equal steps sound equal), level in
|
|
78
|
+
* dB. 0 returns `from`'s values exactly, 1 returns `to`'s.
|
|
79
|
+
*/
|
|
80
|
+
export function morphFormants(
|
|
81
|
+
from: Formants,
|
|
82
|
+
to: Formants,
|
|
83
|
+
morph: number,
|
|
84
|
+
): Array<[number, number, number]> {
|
|
85
|
+
const m = Math.min(1, Math.max(0, morph));
|
|
86
|
+
return from.map(([hz, level, bw], index) => {
|
|
87
|
+
if (m === 0) return [hz, level, bw];
|
|
88
|
+
const [toHz, toLevel, toBw] = to[index] ?? [hz, level, bw];
|
|
89
|
+
if (m === 1) return [toHz, toLevel, toBw];
|
|
90
|
+
return [
|
|
91
|
+
hz * (toHz / hz) ** m,
|
|
92
|
+
level + (toLevel - level) * m,
|
|
93
|
+
bw * (toBw / bw) ** m,
|
|
94
|
+
];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- sing lane: SATB tables, Klatt cascade, morph and tuning ----
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Vowel formant tables and the resonators that sing them.
|
|
102
|
+
*
|
|
103
|
+
* Tables: F1-F5 frequency (Hz), level (dB), bandwidth (Hz) for soprano,
|
|
104
|
+
* alto, tenor and bass singers on a e i o u: the Csound manual's "Formant
|
|
105
|
+
* Values" appendix (the FOF table, after Sundberg's singer data). The tenor
|
|
106
|
+
* rows equal VOWEL_FORMANTS above.
|
|
107
|
+
*
|
|
108
|
+
* Resonator: Klatt 1980 ("Software for a cascade/parallel formant
|
|
109
|
+
* synthesizer", JASA 67(3)) two-pole digital resonator with unity DC gain:
|
|
110
|
+
* C = -exp(-2 pi BW T), B = 2 exp(-pi BW T) cos(2 pi F T), A = 1 - B - C,
|
|
111
|
+
* y[n] = A x[n] + B y[n-1] + C y[n-2].
|
|
112
|
+
* In cascade the relative formant levels come out right on their own.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
export type Formant = readonly [hz: number, db: number, bw: number];
|
|
116
|
+
export type VowelTable = readonly Formant[];
|
|
117
|
+
export const VOICE_TYPES = ["soprano", "alto", "tenor", "bass"] as const;
|
|
118
|
+
export type VoiceType = (typeof VOICE_TYPES)[number];
|
|
119
|
+
export const VOWELS = ["a", "e", "i", "o", "u"] as const;
|
|
120
|
+
|
|
121
|
+
const row = (f: number[], db: number[], bw: number[]): VowelTable =>
|
|
122
|
+
f.map((hz, i) => [hz, db[i]!, bw[i]!] as const);
|
|
123
|
+
|
|
124
|
+
export const SINGER_FORMANTS: Readonly<
|
|
125
|
+
Record<VoiceType, Record<string, VowelTable>>
|
|
126
|
+
> = {
|
|
127
|
+
soprano: {
|
|
128
|
+
a: row(
|
|
129
|
+
[800, 1150, 2900, 3900, 4950],
|
|
130
|
+
[0, -6, -32, -20, -50],
|
|
131
|
+
[80, 90, 120, 130, 140],
|
|
132
|
+
),
|
|
133
|
+
e: row(
|
|
134
|
+
[350, 2000, 2800, 3600, 4950],
|
|
135
|
+
[0, -20, -15, -40, -56],
|
|
136
|
+
[60, 100, 120, 150, 200],
|
|
137
|
+
),
|
|
138
|
+
i: row(
|
|
139
|
+
[270, 2140, 2950, 3900, 4950],
|
|
140
|
+
[0, -12, -26, -26, -44],
|
|
141
|
+
[60, 90, 100, 120, 120],
|
|
142
|
+
),
|
|
143
|
+
o: row(
|
|
144
|
+
[450, 800, 2830, 3800, 4950],
|
|
145
|
+
[0, -11, -22, -22, -50],
|
|
146
|
+
[70, 80, 100, 130, 135],
|
|
147
|
+
),
|
|
148
|
+
u: row(
|
|
149
|
+
[325, 700, 2700, 3800, 4950],
|
|
150
|
+
[0, -16, -35, -40, -60],
|
|
151
|
+
[50, 60, 170, 180, 200],
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
alto: {
|
|
155
|
+
a: row(
|
|
156
|
+
[800, 1150, 2800, 3500, 4950],
|
|
157
|
+
[0, -4, -20, -36, -60],
|
|
158
|
+
[80, 90, 120, 130, 140],
|
|
159
|
+
),
|
|
160
|
+
e: row(
|
|
161
|
+
[400, 1600, 2700, 3300, 4950],
|
|
162
|
+
[0, -24, -30, -35, -60],
|
|
163
|
+
[60, 80, 120, 150, 200],
|
|
164
|
+
),
|
|
165
|
+
i: row(
|
|
166
|
+
[350, 1700, 2700, 3700, 4950],
|
|
167
|
+
[0, -20, -30, -36, -60],
|
|
168
|
+
[50, 100, 120, 150, 200],
|
|
169
|
+
),
|
|
170
|
+
o: row(
|
|
171
|
+
[450, 800, 2830, 3500, 4950],
|
|
172
|
+
[0, -9, -16, -28, -55],
|
|
173
|
+
[70, 80, 100, 130, 135],
|
|
174
|
+
),
|
|
175
|
+
u: row(
|
|
176
|
+
[325, 700, 2530, 3500, 4950],
|
|
177
|
+
[0, -12, -30, -40, -64],
|
|
178
|
+
[50, 60, 170, 180, 200],
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
tenor: {
|
|
182
|
+
a: row(
|
|
183
|
+
[650, 1080, 2650, 2900, 3250],
|
|
184
|
+
[0, -6, -7, -8, -22],
|
|
185
|
+
[80, 90, 120, 130, 140],
|
|
186
|
+
),
|
|
187
|
+
e: row(
|
|
188
|
+
[400, 1700, 2600, 3200, 3580],
|
|
189
|
+
[0, -14, -12, -14, -20],
|
|
190
|
+
[70, 80, 100, 120, 120],
|
|
191
|
+
),
|
|
192
|
+
i: row(
|
|
193
|
+
[290, 1870, 2800, 3250, 3540],
|
|
194
|
+
[0, -15, -18, -20, -30],
|
|
195
|
+
[40, 90, 100, 120, 120],
|
|
196
|
+
),
|
|
197
|
+
o: row(
|
|
198
|
+
[400, 800, 2600, 2800, 3000],
|
|
199
|
+
[0, -10, -12, -12, -26],
|
|
200
|
+
[40, 80, 100, 120, 120],
|
|
201
|
+
),
|
|
202
|
+
u: row(
|
|
203
|
+
[350, 600, 2700, 2900, 3300],
|
|
204
|
+
[0, -20, -17, -14, -26],
|
|
205
|
+
[40, 60, 100, 120, 120],
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
bass: {
|
|
209
|
+
a: row(
|
|
210
|
+
[600, 1040, 2250, 2450, 2750],
|
|
211
|
+
[0, -7, -9, -9, -20],
|
|
212
|
+
[60, 70, 110, 120, 130],
|
|
213
|
+
),
|
|
214
|
+
e: row(
|
|
215
|
+
[400, 1620, 2400, 2800, 3100],
|
|
216
|
+
[0, -12, -9, -12, -18],
|
|
217
|
+
[40, 80, 100, 120, 120],
|
|
218
|
+
),
|
|
219
|
+
i: row(
|
|
220
|
+
[250, 1750, 2600, 3050, 3340],
|
|
221
|
+
[0, -30, -16, -22, -28],
|
|
222
|
+
[60, 90, 100, 120, 120],
|
|
223
|
+
),
|
|
224
|
+
o: row(
|
|
225
|
+
[400, 750, 2400, 2600, 2900],
|
|
226
|
+
[0, -11, -21, -20, -40],
|
|
227
|
+
[40, 80, 100, 120, 120],
|
|
228
|
+
),
|
|
229
|
+
u: row(
|
|
230
|
+
[350, 600, 2400, 2675, 2950],
|
|
231
|
+
[0, -20, -32, -28, -36],
|
|
232
|
+
[40, 80, 100, 120, 120],
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Morph two tables: frequencies and bandwidths in log Hz, levels in dB. */
|
|
238
|
+
export function morphVowel(a: VowelTable, b: VowelTable, t: number): Formant[] {
|
|
239
|
+
return a.map(([fa, da, ba], i) => {
|
|
240
|
+
const [fb, db, bb] = b[i]!;
|
|
241
|
+
return [
|
|
242
|
+
fa * (fb / fa) ** t,
|
|
243
|
+
da + (db - da) * t,
|
|
244
|
+
ba * (bb / ba) ** t,
|
|
245
|
+
] as const;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Vowel position along a sequence, e.g. "a>o>u" at t in [0,1]; plain "a"
|
|
251
|
+
* is constant. Unknown letters fall back to "a".
|
|
252
|
+
*/
|
|
253
|
+
export function vowelAt(voice: VoiceType, spec: string, t: number): Formant[] {
|
|
254
|
+
const table = SINGER_FORMANTS[voice];
|
|
255
|
+
const names = spec.split(">").map((s) => (table[s] ? s : "a"));
|
|
256
|
+
if (names.length === 1) return [...table[names[0]!]!];
|
|
257
|
+
if (t >= 1) return [...table[names[names.length - 1]!]!];
|
|
258
|
+
const x = Math.min(0.999999, Math.max(0, t)) * (names.length - 1);
|
|
259
|
+
const i = Math.floor(x);
|
|
260
|
+
return morphVowel(table[names[i]!]!, table[names[i + 1]!]!, x - i);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Klatt resonator with coefficient updates at control rate. */
|
|
264
|
+
export class Resonator {
|
|
265
|
+
private a = 1;
|
|
266
|
+
private b = 0;
|
|
267
|
+
private c = 0;
|
|
268
|
+
private y1 = 0;
|
|
269
|
+
private y2 = 0;
|
|
270
|
+
set(hz: number, bw: number, sampleRate: number): void {
|
|
271
|
+
const t = 1 / sampleRate;
|
|
272
|
+
const f = Math.min(hz, sampleRate * 0.45);
|
|
273
|
+
this.c = -Math.exp(-2 * Math.PI * bw * t);
|
|
274
|
+
this.b = 2 * Math.exp(-Math.PI * bw * t) * Math.cos(2 * Math.PI * f * t);
|
|
275
|
+
this.a = 1 - this.b - this.c;
|
|
276
|
+
}
|
|
277
|
+
/** Writes the coefficients at `out[at..at+2]`; returns the next index. */
|
|
278
|
+
coeffsInto(out: Float64Array, at: number): number {
|
|
279
|
+
out[at] = this.a;
|
|
280
|
+
out[at + 1] = this.b;
|
|
281
|
+
out[at + 2] = this.c;
|
|
282
|
+
return at + 3;
|
|
283
|
+
}
|
|
284
|
+
/** H(e^jw) as [re, im], given cos/sin of w and 2w. */
|
|
285
|
+
response(cw: number, sw: number, c2w: number, s2w: number): [number, number] {
|
|
286
|
+
const out = new Float64Array(2);
|
|
287
|
+
this.responseInto(cw, sw, c2w, s2w, out);
|
|
288
|
+
return [out[0]!, out[1]!];
|
|
289
|
+
}
|
|
290
|
+
/** `response` written into `out[0..1]` (no allocation on the hot path). */
|
|
291
|
+
responseInto(
|
|
292
|
+
cw: number,
|
|
293
|
+
sw: number,
|
|
294
|
+
c2w: number,
|
|
295
|
+
s2w: number,
|
|
296
|
+
out: Float64Array,
|
|
297
|
+
): void {
|
|
298
|
+
const re = 1 - this.b * cw - this.c * c2w;
|
|
299
|
+
const im = this.b * sw + this.c * s2w;
|
|
300
|
+
const d = re * re + im * im;
|
|
301
|
+
out[0] = (this.a * re) / d;
|
|
302
|
+
out[1] = (-this.a * im) / d;
|
|
303
|
+
}
|
|
304
|
+
process(x: number): number {
|
|
305
|
+
const y = this.a * x + this.b * this.y1 + this.c * this.y2;
|
|
306
|
+
this.y2 = this.y1;
|
|
307
|
+
this.y1 = y;
|
|
308
|
+
return y;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Constant-peak-gain band-pass (RBJ, 0 dB at centre) for parallel banks. */
|
|
313
|
+
export class Bandpass {
|
|
314
|
+
private b0 = 0;
|
|
315
|
+
private b2 = 0;
|
|
316
|
+
private a1 = 0;
|
|
317
|
+
private a2 = 0;
|
|
318
|
+
private x1 = 0;
|
|
319
|
+
private x2 = 0;
|
|
320
|
+
private y1 = 0;
|
|
321
|
+
private y2 = 0;
|
|
322
|
+
set(hz: number, bw: number, sampleRate: number): void {
|
|
323
|
+
const w = (2 * Math.PI * Math.min(hz, sampleRate * 0.45)) / sampleRate;
|
|
324
|
+
const q = Math.max(0.3, hz / Math.max(1, bw));
|
|
325
|
+
const alpha = Math.sin(w) / (2 * q);
|
|
326
|
+
const a0 = 1 + alpha;
|
|
327
|
+
this.b0 = alpha / a0;
|
|
328
|
+
this.b2 = -alpha / a0;
|
|
329
|
+
this.a1 = (-2 * Math.cos(w)) / a0;
|
|
330
|
+
this.a2 = (1 - alpha) / a0;
|
|
331
|
+
}
|
|
332
|
+
/** Writes the coefficients at `out[at..at+3]`; returns the next index. */
|
|
333
|
+
coeffsInto(out: Float64Array, at: number): number {
|
|
334
|
+
out[at] = this.b0;
|
|
335
|
+
out[at + 1] = this.b2;
|
|
336
|
+
out[at + 2] = this.a1;
|
|
337
|
+
out[at + 3] = this.a2;
|
|
338
|
+
return at + 4;
|
|
339
|
+
}
|
|
340
|
+
/** H(e^jw) as [re, im], given cos/sin of w and 2w. */
|
|
341
|
+
response(cw: number, sw: number, c2w: number, s2w: number): [number, number] {
|
|
342
|
+
const out = new Float64Array(2);
|
|
343
|
+
this.responseInto(cw, sw, c2w, s2w, out);
|
|
344
|
+
return [out[0]!, out[1]!];
|
|
345
|
+
}
|
|
346
|
+
/** `response` written into `out[0..1]` (no allocation on the hot path). */
|
|
347
|
+
responseInto(
|
|
348
|
+
cw: number,
|
|
349
|
+
sw: number,
|
|
350
|
+
c2w: number,
|
|
351
|
+
s2w: number,
|
|
352
|
+
out: Float64Array,
|
|
353
|
+
): void {
|
|
354
|
+
const nr = this.b0 + this.b2 * c2w;
|
|
355
|
+
const ni = -this.b2 * s2w;
|
|
356
|
+
const dr = 1 + this.a1 * cw + this.a2 * c2w;
|
|
357
|
+
const di = -this.a1 * sw - this.a2 * s2w;
|
|
358
|
+
const d = dr * dr + di * di;
|
|
359
|
+
out[0] = (nr * dr + ni * di) / d;
|
|
360
|
+
out[1] = (ni * dr - nr * di) / d;
|
|
361
|
+
}
|
|
362
|
+
process(x: number): number {
|
|
363
|
+
const y =
|
|
364
|
+
this.b0 * x + this.b2 * this.x2 - this.a1 * this.y1 - this.a2 * this.y2;
|
|
365
|
+
this.x2 = this.x1;
|
|
366
|
+
this.x1 = x;
|
|
367
|
+
this.y2 = this.y1;
|
|
368
|
+
this.y1 = y;
|
|
369
|
+
return y;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Five-formant cascade (the Klatt voicing branch). */
|
|
374
|
+
export class Cascade {
|
|
375
|
+
readonly res = [
|
|
376
|
+
new Resonator(),
|
|
377
|
+
new Resonator(),
|
|
378
|
+
new Resonator(),
|
|
379
|
+
new Resonator(),
|
|
380
|
+
new Resonator(),
|
|
381
|
+
];
|
|
382
|
+
set(formants: readonly Formant[], scale: number, sampleRate: number): void {
|
|
383
|
+
for (let i = 0; i < 5; i += 1) {
|
|
384
|
+
const [hz, , bw] = formants[i]!;
|
|
385
|
+
this.res[i]!.set(hz * scale, bw * Math.sqrt(scale), sampleRate);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
process(x: number): number {
|
|
389
|
+
let y = x;
|
|
390
|
+
for (const r of this.res) y = r.process(y);
|
|
391
|
+
return y;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Formant tuning for high voices (review fix; Sundberg 1987, Joliveau,
|
|
397
|
+
* Smith and Wolfe 2004): when f0 rises above F1 (and 2 f0 above F2), the
|
|
398
|
+
* singer opens the jaw so F1 tracks f0 and F2 tracks 2 f0. Applied to the
|
|
399
|
+
* effective (formant-scaled) frequencies; bandwidths widen in proportion.
|
|
400
|
+
* A no-op below the crossing, so low voices keep the table exactly.
|
|
401
|
+
*/
|
|
402
|
+
export function tuneToPitch(
|
|
403
|
+
formants: readonly Formant[],
|
|
404
|
+
f0: number,
|
|
405
|
+
scale: number,
|
|
406
|
+
): readonly Formant[] {
|
|
407
|
+
const need1 = (1.05 * f0) / scale;
|
|
408
|
+
if (
|
|
409
|
+
formants[0]![0] >= need1 &&
|
|
410
|
+
formants[1]![0] >= Math.max((2.05 * f0) / scale, formants[0]![0] * 1.25)
|
|
411
|
+
)
|
|
412
|
+
return formants; // below the crossing: the table as-is, no allocation
|
|
413
|
+
const out = formants.map((f) => [...f] as [number, number, number]);
|
|
414
|
+
const f1 = out[0]!;
|
|
415
|
+
const want1 = (1.05 * f0) / scale;
|
|
416
|
+
if (f1[0] < want1) {
|
|
417
|
+
f1[2] *= want1 / f1[0];
|
|
418
|
+
f1[0] = want1;
|
|
419
|
+
}
|
|
420
|
+
const f2 = out[1]!;
|
|
421
|
+
const want2 = Math.max((2.05 * f0) / scale, f1[0] * 1.25);
|
|
422
|
+
if (f2[0] < want2) {
|
|
423
|
+
f2[2] *= want2 / f2[0];
|
|
424
|
+
f2[0] = want2;
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|