@hraness/dawg 0.6.1 → 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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- 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 +30 -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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- 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 -13
- 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 +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- 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 +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -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 +4 -1
- package/src/agent/provider.ts +22 -8
- 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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -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 +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 +160 -28
- 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 +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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -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 +616 -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/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- 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/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 +34 -4
- 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 +759 -177
- 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 +47 -7
- 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 +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 +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,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
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glottal source: the Liljencrants-Fant (LF) flow-derivative pulse shaped by
|
|
3
|
+
* one voice-quality knob Rd (Fant 1995, "The LF-model revisited"): 0.3 is
|
|
4
|
+
* pressed and bright, 1.0 modal, 2.7 breathy and dark. One period tables are
|
|
5
|
+
* precomputed on an Rd grid and read with linear interpolation in phase and
|
|
6
|
+
* across Rd, so the per-sample cost is two table reads.
|
|
7
|
+
*
|
|
8
|
+
* Fant's Rd relations (all in units of the period T0 = 1):
|
|
9
|
+
* Ra = (-1 + 4.8 Rd) / 100, Rk = (22.4 + 11.8 Rd) / 100,
|
|
10
|
+
* Rg = Rk / (4 (0.11 Rd / (0.5 + 1.2 Rk) - Ra)),
|
|
11
|
+
* tp = 1 / (2 Rg), te = tp (1 + Rk), ta = Ra.
|
|
12
|
+
*/
|
|
13
|
+
import { fftInPlace } from "./fft.ts";
|
|
14
|
+
|
|
15
|
+
export const GLOTTAL_TABLE = 2048;
|
|
16
|
+
export const RD_MIN = 0.3;
|
|
17
|
+
export const RD_MAX = 2.7;
|
|
18
|
+
export const RD_STEP = 0.1;
|
|
19
|
+
const RD_COUNT = Math.round((RD_MAX - RD_MIN) / RD_STEP) + 1;
|
|
20
|
+
|
|
21
|
+
function lfTable(rd: number, n: number): Float64Array {
|
|
22
|
+
const ra = (-1 + 4.8 * rd) / 100;
|
|
23
|
+
const rk = (22.4 + 11.8 * rd) / 100;
|
|
24
|
+
const rg = rk / (4 * ((0.11 * rd) / (0.5 + 1.2 * rk) - ra));
|
|
25
|
+
const tp = 1 / (2 * rg);
|
|
26
|
+
let te = tp * (1 + rk);
|
|
27
|
+
if (te > 0.98) te = 0.98;
|
|
28
|
+
const ta = Math.max(ra, 1e-4);
|
|
29
|
+
const tc = 1;
|
|
30
|
+
// return-phase constant: eps * ta = 1 - exp(-eps (tc - te))
|
|
31
|
+
let eps = 1 / ta;
|
|
32
|
+
for (let i = 0; i < 60; i += 1) eps = (1 - Math.exp(-eps * (tc - te))) / ta;
|
|
33
|
+
const wg = Math.PI / tp;
|
|
34
|
+
const shape = (alpha: number, out: Float64Array): number => {
|
|
35
|
+
const e0 = -1 / (Math.exp(alpha * te) * Math.sin(wg * te));
|
|
36
|
+
let sum = 0;
|
|
37
|
+
for (let i = 0; i < n; i += 1) {
|
|
38
|
+
const t = i / n;
|
|
39
|
+
let v: number;
|
|
40
|
+
if (t <= te) v = e0 * Math.exp(alpha * t) * Math.sin(wg * t);
|
|
41
|
+
else
|
|
42
|
+
v =
|
|
43
|
+
-(1 / (eps * ta)) *
|
|
44
|
+
(Math.exp(-eps * (t - te)) - Math.exp(-eps * (tc - te)));
|
|
45
|
+
out[i] = v;
|
|
46
|
+
sum += v;
|
|
47
|
+
}
|
|
48
|
+
return sum / n;
|
|
49
|
+
};
|
|
50
|
+
// alpha makes the net flow zero (the derivative integrates to 0)
|
|
51
|
+
const out = new Float64Array(n);
|
|
52
|
+
let lo = -50;
|
|
53
|
+
let hi = 200;
|
|
54
|
+
for (let i = 0; i < 80; i += 1) {
|
|
55
|
+
const mid = (lo + hi) / 2;
|
|
56
|
+
if (shape(mid, out) > 0) hi = mid;
|
|
57
|
+
else lo = mid;
|
|
58
|
+
}
|
|
59
|
+
shape((lo + hi) / 2, out);
|
|
60
|
+
// remove residual DC exactly
|
|
61
|
+
let mean = 0;
|
|
62
|
+
for (const v of out) mean += v;
|
|
63
|
+
mean /= n;
|
|
64
|
+
for (let i = 0; i < n; i += 1) out[i] = out[i]! - mean;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let tables: Float64Array[] | undefined;
|
|
69
|
+
function lfTables(): Float64Array[] {
|
|
70
|
+
if (!tables) {
|
|
71
|
+
tables = [];
|
|
72
|
+
for (let i = 0; i < RD_COUNT; i += 1)
|
|
73
|
+
tables.push(lfTable(RD_MIN + i * RD_STEP, GLOTTAL_TABLE));
|
|
74
|
+
}
|
|
75
|
+
return tables;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Band-limited mip levels (review fix: the single table aliased badly at
|
|
80
|
+
* 22,050 Hz). Each Rd table is truncated by FFT to its first H harmonics,
|
|
81
|
+
* H on a half-octave grid. A voice at f0 reads the two levels below
|
|
82
|
+
* nmax = fs / (2 f0) and crossfades between them by log(nmax), so the
|
|
83
|
+
* output never holds a harmonic above Nyquist and the timbre moves
|
|
84
|
+
* continuously with pitch (no level jumps under vibrato). The build stores
|
|
85
|
+
* Float32 tables of size max(64, 8 H) rounded up to a power of two
|
|
86
|
+
* (about 0.6 MB for 25 Rd x 19 levels).
|
|
87
|
+
*/
|
|
88
|
+
export const MIP_HARMONICS = [
|
|
89
|
+
2, 3, 4, 6, 8, 11, 16, 23, 32, 45, 64, 91, 128, 181, 256, 362, 512, 724, 1023,
|
|
90
|
+
];
|
|
91
|
+
let mips: Float64Array[][] | undefined; // [level][rd]
|
|
92
|
+
function truncate(src: Float64Array, harmonics: number): Float64Array {
|
|
93
|
+
const n = src.length;
|
|
94
|
+
const re = Float64Array.from(src);
|
|
95
|
+
const im = new Float64Array(n);
|
|
96
|
+
fftInPlace(re, im);
|
|
97
|
+
for (let k = 0; k < n; k += 1) {
|
|
98
|
+
const h = k <= n / 2 ? k : n - k;
|
|
99
|
+
if (h > harmonics) {
|
|
100
|
+
re[k] = 0;
|
|
101
|
+
im[k] = 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
fftInPlace(re, im, true);
|
|
105
|
+
for (let i = 0; i < n; i += 1) re[i] = re[i]! / n;
|
|
106
|
+
return re;
|
|
107
|
+
}
|
|
108
|
+
function mipTables(): Float64Array[][] {
|
|
109
|
+
if (!mips) {
|
|
110
|
+
const base = lfTables();
|
|
111
|
+
mips = MIP_HARMONICS.map((h) => base.map((t) => truncate(t, h)));
|
|
112
|
+
}
|
|
113
|
+
return mips;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readLevel(
|
|
117
|
+
level: Float64Array[],
|
|
118
|
+
ri: number,
|
|
119
|
+
rf: number,
|
|
120
|
+
i0: number,
|
|
121
|
+
i1: number,
|
|
122
|
+
pf: number,
|
|
123
|
+
): number {
|
|
124
|
+
const a = level[ri]!;
|
|
125
|
+
const b = level[ri + 1]!;
|
|
126
|
+
const va = a[i0]! + (a[i1]! - a[i0]!) * pf;
|
|
127
|
+
const vb = b[i0]! + (b[i1]! - b[i0]!) * pf;
|
|
128
|
+
return va + (vb - va) * rf;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* LF flow derivative at `phase` in [0,1) for voice quality `rd`; peak
|
|
133
|
+
* excitation -1. `inc` is f0 / sampleRate: when given, the read is
|
|
134
|
+
* band-limited (mip levels); without it, the full table (aliases; kept
|
|
135
|
+
* only to measure the difference).
|
|
136
|
+
*/
|
|
137
|
+
// The mip level choice depends only on `inc`, which changes once per period
|
|
138
|
+
// or control step: memoised so the per-sample cost is the table reads.
|
|
139
|
+
let lastInc = Number.NaN;
|
|
140
|
+
let lastLevel = 0;
|
|
141
|
+
let lastBlend = 0;
|
|
142
|
+
function chooseLevel(inc: number): void {
|
|
143
|
+
lastInc = inc;
|
|
144
|
+
const nmax = 0.5 / Math.max(inc, 1e-9);
|
|
145
|
+
let j = 0;
|
|
146
|
+
while (j + 1 < MIP_HARMONICS.length && MIP_HARMONICS[j + 1]! <= nmax) j += 1;
|
|
147
|
+
if (j === 0 || MIP_HARMONICS[j]! > nmax) {
|
|
148
|
+
lastLevel = 0;
|
|
149
|
+
lastBlend = -1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// blend level j-1 (weight 1-f) and level j (weight f): both are below nmax
|
|
153
|
+
const top =
|
|
154
|
+
j + 1 < MIP_HARMONICS.length
|
|
155
|
+
? MIP_HARMONICS[j + 1]!
|
|
156
|
+
: MIP_HARMONICS[j]! * Math.SQRT2;
|
|
157
|
+
lastLevel = j;
|
|
158
|
+
lastBlend = Math.min(
|
|
159
|
+
1,
|
|
160
|
+
Math.log(nmax / MIP_HARMONICS[j]!) / Math.log(top / MIP_HARMONICS[j]!),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Builds the LF tables and their mip levels now (about 50 ms) instead of on
|
|
166
|
+
* the first sounding sample, so a live key never pays for them. Idempotent.
|
|
167
|
+
*/
|
|
168
|
+
export function warmGlottal(): void {
|
|
169
|
+
mipTables();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* LF flow derivative at `phase` in [0,1) for voice quality `rd`; peak
|
|
174
|
+
* excitation -1. `inc` is f0 / sampleRate: when given, the read is
|
|
175
|
+
* band-limited (mip levels); without it, the full table (aliases; kept
|
|
176
|
+
* only to measure the difference).
|
|
177
|
+
*/
|
|
178
|
+
export function glottal(phase: number, rd: number, inc?: number): number {
|
|
179
|
+
const r = (Math.min(RD_MAX, Math.max(RD_MIN, rd)) - RD_MIN) / RD_STEP;
|
|
180
|
+
const ri = Math.min(RD_COUNT - 2, Math.floor(r));
|
|
181
|
+
const rf = r - ri;
|
|
182
|
+
const p = phase * GLOTTAL_TABLE;
|
|
183
|
+
const pi = Math.floor(p);
|
|
184
|
+
const pf = p - pi;
|
|
185
|
+
const i0 = pi % GLOTTAL_TABLE;
|
|
186
|
+
const i1 = (pi + 1) % GLOTTAL_TABLE;
|
|
187
|
+
if (inc === undefined) return readLevel(lfTables(), ri, rf, i0, i1, pf);
|
|
188
|
+
const m = mipTables();
|
|
189
|
+
if (inc !== lastInc) chooseLevel(inc);
|
|
190
|
+
const j = lastLevel;
|
|
191
|
+
if (lastBlend < 0) return readLevel(m[0]!, ri, rf, i0, i1, pf);
|
|
192
|
+
const lo = readLevel(m[j - 1]!, ri, rf, i0, i1, pf);
|
|
193
|
+
const hi = readLevel(m[j]!, ri, rf, i0, i1, pf);
|
|
194
|
+
return lo + (hi - lo) * lastBlend;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Open-phase weight (1 while the glottis is open) for breath-noise gating. */
|
|
198
|
+
let owRd = Number.NaN;
|
|
199
|
+
let owTe = 0;
|
|
200
|
+
export function openWeight(phase: number, rd: number): number {
|
|
201
|
+
if (rd !== owRd) {
|
|
202
|
+
const rk = (22.4 + 11.8 * rd) / 100;
|
|
203
|
+
const ra = (-1 + 4.8 * rd) / 100;
|
|
204
|
+
const rg = rk / (4 * ((0.11 * rd) / (0.5 + 1.2 * rk) - ra));
|
|
205
|
+
owRd = rd;
|
|
206
|
+
owTe = Math.min(0.98, (1 / (2 * rg)) * (1 + rk));
|
|
207
|
+
}
|
|
208
|
+
const te = owTe;
|
|
209
|
+
return phase < te ? 0.5 - 0.5 * Math.cos((2 * Math.PI * phase) / te) : 0.15;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const spectra = new Map<number, Float64Array>();
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Power of harmonics 1..`count` of the LF flow derivative at `rd`
|
|
216
|
+
* (rounded to the table's 0.1 grid), from a DFT of one period. Cached:
|
|
217
|
+
* the sing engine's level normalization reads it at control rate.
|
|
218
|
+
*/
|
|
219
|
+
export function glottalSpectrum(rd: number, count: number): Float64Array {
|
|
220
|
+
const index = Math.round(
|
|
221
|
+
(Math.min(RD_MAX, Math.max(RD_MIN, rd)) - RD_MIN) / RD_STEP,
|
|
222
|
+
);
|
|
223
|
+
const key = index * 1024 + count;
|
|
224
|
+
const hit = spectra.get(key);
|
|
225
|
+
if (hit) return hit;
|
|
226
|
+
const at = RD_MIN + index * RD_STEP;
|
|
227
|
+
const n = 1024;
|
|
228
|
+
const x = new Float64Array(n);
|
|
229
|
+
for (let i = 0; i < n; i += 1) x[i] = glottal(i / n, at);
|
|
230
|
+
const out = new Float64Array(count + 1);
|
|
231
|
+
for (let h = 1; h <= count; h += 1) {
|
|
232
|
+
let re = 0;
|
|
233
|
+
let im = 0;
|
|
234
|
+
for (let i = 0; i < n; i += 1) {
|
|
235
|
+
const a = (2 * Math.PI * h * i) / n;
|
|
236
|
+
re += x[i]! * Math.cos(a);
|
|
237
|
+
im -= x[i]! * Math.sin(a);
|
|
238
|
+
}
|
|
239
|
+
out[h] = (re * re + im * im) / (n * n);
|
|
240
|
+
}
|
|
241
|
+
spectra.set(key, out);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
package/src/audio/dsp/interp.ts
CHANGED
|
@@ -51,11 +51,16 @@ export function allpassDelay(a: number, w: number): number {
|
|
|
51
51
|
export function thiranFor(d: number, w = 0): number {
|
|
52
52
|
let a = (1 - d) / (1 + d);
|
|
53
53
|
if (w < 1e-9) return a;
|
|
54
|
+
const s = Math.sin(w);
|
|
55
|
+
const c = Math.cos(w);
|
|
54
56
|
for (let i = 0; i < 4; i += 1) {
|
|
55
57
|
const f = allpassDelay(a, w) - d;
|
|
56
58
|
if (Math.abs(f) < 1e-10) break;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
// d(delay)/da in closed form: the phase is atan2(-s, a + c) -
|
|
60
|
+
// atan2(-a s, 1 + a c), so no extra atan2 pair per step.
|
|
61
|
+
const p = a + c;
|
|
62
|
+
const q = 1 + a * c;
|
|
63
|
+
const df = -(s / (p * p + s * s) + s / (q * q + a * a * s * s)) / w;
|
|
59
64
|
if (df === 0) break;
|
|
60
65
|
a = clamp(a - f / df, -0.9, 0.9);
|
|
61
66
|
}
|