@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
package/core/autotune.ts
ADDED
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pitch correction (0.7 `Track.autotune`): from gentle to hard-tuned.
|
|
3
|
+
*
|
|
4
|
+
* A tracked pitch curve (src/audio/dsp/pitch.ts) becomes a shift in cents
|
|
5
|
+
* per analysis frame, which a PSOLA pass (src/audio/dsp/psola.ts) applies
|
|
6
|
+
* with the formants kept. Two target sources:
|
|
7
|
+
*
|
|
8
|
+
* - a grid (`to: scale | chromatic | chord`): pitches in cents re A440,
|
|
9
|
+
* chosen per note with hysteresis, Auto-Tune style. `speed` is the retune
|
|
10
|
+
* time constant of a one-pole slew on the correction, so a slow speed
|
|
11
|
+
* corrects the note centre and lets vibrato and scoops through while
|
|
12
|
+
* speed 0 snaps (the stepped hard-tune sound). `relax` slows the retune
|
|
13
|
+
* on sustained notes (Auto-Tune Humanize), `flex` narrows the capture
|
|
14
|
+
* zone so only notes already near a target are pulled (Flex-Tune).
|
|
15
|
+
* - guide notes (`to: notes`): each frame's target is the note sounding
|
|
16
|
+
* then. Without `speed` it is Melodyne-style (`center` moves each note's
|
|
17
|
+
* mean to the written pitch, `drift` is the share of slow drift removed);
|
|
18
|
+
* with `speed` it is the grid retune locked to the melody (`locked`).
|
|
19
|
+
*
|
|
20
|
+
* Everything here is pure and deterministic. The render hooks build the
|
|
21
|
+
* targets (tuning table, key, chord timeline, guide notes through the tempo
|
|
22
|
+
* map) and call `autotuneBuffer`. Accuracy and cost are measured in
|
|
23
|
+
* .plans/dawg-07/pitch.md section 3.
|
|
24
|
+
*/
|
|
25
|
+
import { parseKey, scaleSteps } from "./chords.ts";
|
|
26
|
+
import { FxValidationError } from "./params.ts";
|
|
27
|
+
import { ratioCents, type TuningTable } from "./tuning.ts";
|
|
28
|
+
|
|
29
|
+
/** Bumped when correction output changes for the same inputs. */
|
|
30
|
+
export const AUTOTUNE_VERSION = 1;
|
|
31
|
+
|
|
32
|
+
export const AUTOTUNE_PRESETS = [
|
|
33
|
+
"hard",
|
|
34
|
+
"robot",
|
|
35
|
+
"warble",
|
|
36
|
+
"trap",
|
|
37
|
+
"pop",
|
|
38
|
+
"natural",
|
|
39
|
+
"gentle",
|
|
40
|
+
"guided",
|
|
41
|
+
"locked",
|
|
42
|
+
] as const;
|
|
43
|
+
export type AutotunePreset = (typeof AUTOTUNE_PRESETS)[number];
|
|
44
|
+
|
|
45
|
+
export const AUTOTUNE_TARGETS = [
|
|
46
|
+
"scale",
|
|
47
|
+
"chromatic",
|
|
48
|
+
"chord",
|
|
49
|
+
"notes",
|
|
50
|
+
] as const;
|
|
51
|
+
export type AutotuneTarget = (typeof AUTOTUNE_TARGETS)[number];
|
|
52
|
+
|
|
53
|
+
export const AUTOTUNE_VOICES = [
|
|
54
|
+
"auto",
|
|
55
|
+
"bass",
|
|
56
|
+
"tenor",
|
|
57
|
+
"alto",
|
|
58
|
+
"soprano",
|
|
59
|
+
] as const;
|
|
60
|
+
export type AutotuneVoice = (typeof AUTOTUNE_VOICES)[number];
|
|
61
|
+
|
|
62
|
+
/** Track.autotune: a preset and/or fields; explicit fields override it. */
|
|
63
|
+
export type TrackAutotune = Readonly<{
|
|
64
|
+
preset?: AutotunePreset;
|
|
65
|
+
/** Target source (default `scale`). */
|
|
66
|
+
to?: AutotuneTarget;
|
|
67
|
+
/** Guide track id for `to: notes`; omitted = the track's own notes. */
|
|
68
|
+
from?: string;
|
|
69
|
+
/** Scale override, anything `parseKey` reads (`D bayati`). */
|
|
70
|
+
key?: string;
|
|
71
|
+
/** Retune time constant in ms, 0..400; 0 is instant. */
|
|
72
|
+
speed?: number;
|
|
73
|
+
/** 0..1 slower retune on sustained notes. */
|
|
74
|
+
relax?: number;
|
|
75
|
+
/** ms 50..1000 before a note counts as sustained. */
|
|
76
|
+
hold?: number;
|
|
77
|
+
/** 0..100 Flex-Tune: capture zone 50·(1 − flex/100) cents. */
|
|
78
|
+
flex?: number;
|
|
79
|
+
/** Seconds 0..0.5 to move between targets. */
|
|
80
|
+
glide?: number;
|
|
81
|
+
/** 0..1 correction strength. */
|
|
82
|
+
amount?: number;
|
|
83
|
+
/** Added vibrato rate in Hz, 0..12 (0 off). */
|
|
84
|
+
vib?: number;
|
|
85
|
+
/** Added vibrato depth in semitones, 0..1 (0.2 when vib is on). */
|
|
86
|
+
vibmod?: number;
|
|
87
|
+
/** Note mode without speed: 0..1 how far each note's mean moves. */
|
|
88
|
+
center?: number;
|
|
89
|
+
/** Note mode without speed: 0..1 share of slow drift removed. */
|
|
90
|
+
drift?: number;
|
|
91
|
+
/** Tracker range. */
|
|
92
|
+
voice?: AutotuneVoice;
|
|
93
|
+
}>;
|
|
94
|
+
|
|
95
|
+
/** Every field filled: the preset applied, then the explicit fields. */
|
|
96
|
+
export type ResolvedAutotune = Readonly<{
|
|
97
|
+
to: AutotuneTarget;
|
|
98
|
+
from?: string;
|
|
99
|
+
key?: string;
|
|
100
|
+
/** Undefined in note mode means center/drift (Melodyne-style). */
|
|
101
|
+
speed?: number;
|
|
102
|
+
relax: number;
|
|
103
|
+
hold: number;
|
|
104
|
+
flex: number;
|
|
105
|
+
glide: number;
|
|
106
|
+
amount: number;
|
|
107
|
+
vib: number;
|
|
108
|
+
vibmod: number;
|
|
109
|
+
center: number;
|
|
110
|
+
drift: number;
|
|
111
|
+
voice: AutotuneVoice;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
export type AutotuneNumberField =
|
|
115
|
+
| "speed"
|
|
116
|
+
| "relax"
|
|
117
|
+
| "hold"
|
|
118
|
+
| "flex"
|
|
119
|
+
| "glide"
|
|
120
|
+
| "amount"
|
|
121
|
+
| "vib"
|
|
122
|
+
| "vibmod"
|
|
123
|
+
| "center"
|
|
124
|
+
| "drift";
|
|
125
|
+
|
|
126
|
+
export type AutotuneParam = Readonly<{
|
|
127
|
+
name: AutotuneNumberField;
|
|
128
|
+
min: number;
|
|
129
|
+
max: number;
|
|
130
|
+
def: number;
|
|
131
|
+
/** Step for menu left/right. */
|
|
132
|
+
step: number;
|
|
133
|
+
unit: string;
|
|
134
|
+
label: string;
|
|
135
|
+
help: string;
|
|
136
|
+
}>;
|
|
137
|
+
|
|
138
|
+
/** Numeric fields with ranges, defaults (= pop) and units, in type order. */
|
|
139
|
+
export const AUTOTUNE_PARAMS: readonly AutotuneParam[] = Object.freeze([
|
|
140
|
+
{
|
|
141
|
+
name: "speed",
|
|
142
|
+
min: 0,
|
|
143
|
+
max: 400,
|
|
144
|
+
def: 25,
|
|
145
|
+
step: 5,
|
|
146
|
+
unit: "ms",
|
|
147
|
+
label: "Speed",
|
|
148
|
+
help: "retune time; 0 is instant and stepped",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "relax",
|
|
152
|
+
min: 0,
|
|
153
|
+
max: 1,
|
|
154
|
+
def: 0.3,
|
|
155
|
+
step: 0.05,
|
|
156
|
+
unit: "",
|
|
157
|
+
label: "Relax",
|
|
158
|
+
help: "slower retune on held notes",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "hold",
|
|
162
|
+
min: 50,
|
|
163
|
+
max: 1000,
|
|
164
|
+
def: 150,
|
|
165
|
+
step: 10,
|
|
166
|
+
unit: "ms",
|
|
167
|
+
label: "Hold",
|
|
168
|
+
help: "when a note counts as held, for relax",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "flex",
|
|
172
|
+
min: 0,
|
|
173
|
+
max: 100,
|
|
174
|
+
def: 0,
|
|
175
|
+
step: 5,
|
|
176
|
+
unit: "",
|
|
177
|
+
label: "Flex",
|
|
178
|
+
help: "higher only pulls notes already near a target",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "glide",
|
|
182
|
+
min: 0,
|
|
183
|
+
max: 0.5,
|
|
184
|
+
def: 0,
|
|
185
|
+
step: 0.01,
|
|
186
|
+
unit: "s",
|
|
187
|
+
label: "Glide",
|
|
188
|
+
help: "time to move between targets",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "amount",
|
|
192
|
+
min: 0,
|
|
193
|
+
max: 1,
|
|
194
|
+
def: 1,
|
|
195
|
+
step: 0.05,
|
|
196
|
+
unit: "",
|
|
197
|
+
label: "Amount",
|
|
198
|
+
help: "correction strength",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "vib",
|
|
202
|
+
min: 0,
|
|
203
|
+
max: 12,
|
|
204
|
+
def: 0,
|
|
205
|
+
step: 0.5,
|
|
206
|
+
unit: "Hz",
|
|
207
|
+
label: "Vib",
|
|
208
|
+
help: "added vibrato rate (0 off)",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "vibmod",
|
|
212
|
+
min: 0,
|
|
213
|
+
max: 1,
|
|
214
|
+
def: 0.2,
|
|
215
|
+
step: 0.05,
|
|
216
|
+
unit: "st",
|
|
217
|
+
label: "Vib depth",
|
|
218
|
+
help: "added vibrato depth",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "center",
|
|
222
|
+
min: 0,
|
|
223
|
+
max: 1,
|
|
224
|
+
def: 1,
|
|
225
|
+
step: 0.05,
|
|
226
|
+
unit: "",
|
|
227
|
+
label: "Center",
|
|
228
|
+
help: "notes: move each note's middle to the written pitch",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: "drift",
|
|
232
|
+
min: 0,
|
|
233
|
+
max: 1,
|
|
234
|
+
def: 0.5,
|
|
235
|
+
step: 0.05,
|
|
236
|
+
unit: "",
|
|
237
|
+
label: "Drift",
|
|
238
|
+
help: "notes: share of slow drift removed",
|
|
239
|
+
},
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
export const AUTOTUNE_FIELDS = [
|
|
243
|
+
"preset",
|
|
244
|
+
"to",
|
|
245
|
+
"from",
|
|
246
|
+
"key",
|
|
247
|
+
"speed",
|
|
248
|
+
"relax",
|
|
249
|
+
"hold",
|
|
250
|
+
"flex",
|
|
251
|
+
"glide",
|
|
252
|
+
"amount",
|
|
253
|
+
"vib",
|
|
254
|
+
"vibmod",
|
|
255
|
+
"center",
|
|
256
|
+
"drift",
|
|
257
|
+
"voice",
|
|
258
|
+
] as const satisfies readonly (keyof TrackAutotune)[];
|
|
259
|
+
|
|
260
|
+
/** Preset fields (pitch.md section 5). */
|
|
261
|
+
export const AUTOTUNE_PRESET_FIELDS: Readonly<
|
|
262
|
+
Record<AutotunePreset, TrackAutotune>
|
|
263
|
+
> = Object.freeze({
|
|
264
|
+
hard: { speed: 0, relax: 0, glide: 0 },
|
|
265
|
+
robot: { speed: 0, relax: 0, to: "chromatic" },
|
|
266
|
+
warble: { speed: 0, relax: 0, vib: 6.5, vibmod: 0.35 },
|
|
267
|
+
trap: { speed: 10, relax: 0.2 },
|
|
268
|
+
pop: { speed: 25, relax: 0.3 },
|
|
269
|
+
natural: { speed: 60, relax: 0.5, flex: 40 },
|
|
270
|
+
gentle: { speed: 120, relax: 0.6, flex: 20 },
|
|
271
|
+
guided: { to: "notes", center: 1, drift: 0.5, glide: 0.04 },
|
|
272
|
+
locked: { to: "notes", speed: 0, relax: 0 },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/** One line per preset for help and menus. */
|
|
276
|
+
export const AUTOTUNE_PRESET_HELP: Readonly<Record<AutotunePreset, string>> =
|
|
277
|
+
Object.freeze({
|
|
278
|
+
hard: "instant, stepped notes",
|
|
279
|
+
robot: "stepped on every step of the tuning",
|
|
280
|
+
warble: "hard with wide synthetic vibrato",
|
|
281
|
+
trap: "fast and glossy",
|
|
282
|
+
pop: "polished but sung",
|
|
283
|
+
natural: "keeps scoops and vibrato",
|
|
284
|
+
gentle: "barely there",
|
|
285
|
+
guided: "notes to a written melody, vibrato kept",
|
|
286
|
+
locked: "hard tune locked to a melody",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
export function isAutotunePreset(value: unknown): value is AutotunePreset {
|
|
290
|
+
return (
|
|
291
|
+
typeof value === "string" &&
|
|
292
|
+
(AUTOTUNE_PRESETS as readonly string[]).includes(value)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function fail(message: string): never {
|
|
297
|
+
throw new FxValidationError(`autotune: ${message}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function word<T extends string>(
|
|
301
|
+
value: unknown,
|
|
302
|
+
words: readonly T[],
|
|
303
|
+
field: string,
|
|
304
|
+
): T {
|
|
305
|
+
if (
|
|
306
|
+
typeof value !== "string" ||
|
|
307
|
+
!(words as readonly string[]).includes(value)
|
|
308
|
+
)
|
|
309
|
+
fail(`${field} must be one of ${words.join(", ")}`);
|
|
310
|
+
return value as T;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validates Track.autotune. Without `from`, `to: notes` follows the
|
|
315
|
+
* track's own notes (any track). `null`/undefined → undefined.
|
|
316
|
+
* Ranges are checked, never clamped. `from` naming an existing track is
|
|
317
|
+
* checked at score level (core/score.ts).
|
|
318
|
+
*/
|
|
319
|
+
export function normalizeAutotune(
|
|
320
|
+
input: unknown,
|
|
321
|
+
_instrument?: string,
|
|
322
|
+
): TrackAutotune | undefined {
|
|
323
|
+
if (input === undefined || input === null) return undefined;
|
|
324
|
+
if (typeof input !== "object" || Array.isArray(input))
|
|
325
|
+
fail("must be an object");
|
|
326
|
+
const record = input as Readonly<Record<string, unknown>>;
|
|
327
|
+
for (const key of Object.keys(record))
|
|
328
|
+
if (!(AUTOTUNE_FIELDS as readonly string[]).includes(key))
|
|
329
|
+
fail(`unknown field ${key}`);
|
|
330
|
+
const out: Record<string, unknown> = {};
|
|
331
|
+
for (const field of AUTOTUNE_FIELDS) {
|
|
332
|
+
const value = record[field];
|
|
333
|
+
if (value === undefined) continue;
|
|
334
|
+
if (field === "preset") out.preset = word(value, AUTOTUNE_PRESETS, field);
|
|
335
|
+
else if (field === "to") out.to = word(value, AUTOTUNE_TARGETS, field);
|
|
336
|
+
else if (field === "voice") out.voice = word(value, AUTOTUNE_VOICES, field);
|
|
337
|
+
else if (field === "from") {
|
|
338
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 64)
|
|
339
|
+
fail("from must be a track id");
|
|
340
|
+
out.from = value;
|
|
341
|
+
} else if (field === "key") {
|
|
342
|
+
if (typeof value !== "string" || parseKey(value) === undefined)
|
|
343
|
+
fail(`key ${JSON.stringify(value)} is not a key (try "A minor")`);
|
|
344
|
+
out.key = value;
|
|
345
|
+
} else {
|
|
346
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === field)!;
|
|
347
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
348
|
+
fail(`${field} must be a number`);
|
|
349
|
+
if (value < spec.min || value > spec.max)
|
|
350
|
+
fail(
|
|
351
|
+
`${field} must be ${spec.min}..${spec.max}${spec.unit ? ` ${spec.unit}` : ""}`,
|
|
352
|
+
);
|
|
353
|
+
out[field] = value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (Object.keys(out).length === 0) fail("give a preset or settings");
|
|
357
|
+
const to =
|
|
358
|
+
(out.to as AutotuneTarget | undefined) ??
|
|
359
|
+
(out.preset
|
|
360
|
+
? AUTOTUNE_PRESET_FIELDS[out.preset as AutotunePreset].to
|
|
361
|
+
: undefined) ??
|
|
362
|
+
"scale";
|
|
363
|
+
if (out.from !== undefined && to !== "notes")
|
|
364
|
+
fail(`from needs to: notes (this one tunes to ${to})`);
|
|
365
|
+
const ordered: Record<string, unknown> = {};
|
|
366
|
+
for (const field of AUTOTUNE_FIELDS)
|
|
367
|
+
if (out[field] !== undefined) ordered[field] = out[field];
|
|
368
|
+
return Object.freeze(ordered) as TrackAutotune;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The preset (pop when none), then the explicit fields. */
|
|
372
|
+
export function resolveAutotune(input: TrackAutotune): ResolvedAutotune {
|
|
373
|
+
const base: TrackAutotune = AUTOTUNE_PRESET_FIELDS[input.preset ?? "pop"];
|
|
374
|
+
const pick = <K extends keyof TrackAutotune>(key: K) =>
|
|
375
|
+
input[key] !== undefined ? input[key] : base[key];
|
|
376
|
+
const to = pick("to") ?? "scale";
|
|
377
|
+
const def = (name: AutotuneNumberField) =>
|
|
378
|
+
(pick(name) as number | undefined) ??
|
|
379
|
+
AUTOTUNE_PARAMS.find((param) => param.name === name)!.def;
|
|
380
|
+
const vib = def("vib");
|
|
381
|
+
// Note mode keeps center/drift (Melodyne-style) unless speed is set by
|
|
382
|
+
// the user or the preset; the pop base speed does not count there.
|
|
383
|
+
const speed =
|
|
384
|
+
to === "notes"
|
|
385
|
+
? (input.speed ?? (input.preset ? base.speed : undefined))
|
|
386
|
+
: def("speed");
|
|
387
|
+
const from = input.from;
|
|
388
|
+
const key = input.key;
|
|
389
|
+
return Object.freeze({
|
|
390
|
+
to,
|
|
391
|
+
...(from !== undefined ? { from } : {}),
|
|
392
|
+
...(key !== undefined ? { key } : {}),
|
|
393
|
+
...(speed !== undefined ? { speed } : {}),
|
|
394
|
+
relax: def("relax"),
|
|
395
|
+
hold: def("hold"),
|
|
396
|
+
flex: def("flex"),
|
|
397
|
+
glide: def("glide"),
|
|
398
|
+
amount: def("amount"),
|
|
399
|
+
vib,
|
|
400
|
+
vibmod: vib > 0 ? def("vibmod") : 0,
|
|
401
|
+
center: def("center"),
|
|
402
|
+
drift: def("drift"),
|
|
403
|
+
voice: pick("voice") ?? "auto",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** A stable digest of the resolved settings, for cache and stem keys. */
|
|
408
|
+
export function autotuneDigest(resolved: ResolvedAutotune): string {
|
|
409
|
+
return `at${AUTOTUNE_VERSION}:${JSON.stringify(resolved)}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The shortest description: `hard`, `pop · speed 35 ms`. */
|
|
413
|
+
export function describeAutotune(input: TrackAutotune): string {
|
|
414
|
+
const parts: string[] = [input.preset ?? "pop"];
|
|
415
|
+
for (const field of AUTOTUNE_FIELDS) {
|
|
416
|
+
if (field === "preset") continue;
|
|
417
|
+
const value = input[field];
|
|
418
|
+
if (value === undefined) continue;
|
|
419
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === field);
|
|
420
|
+
// glide is stored in seconds (like Track.glide) and shown in ms.
|
|
421
|
+
parts.push(
|
|
422
|
+
field === "glide"
|
|
423
|
+
? `glide ${Math.round((value as number) * 1000)} ms`
|
|
424
|
+
: `${field} ${value}${spec?.unit ? ` ${spec.unit}` : ""}`.trimEnd(),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return parts.join(" · ");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Targets
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* A pitch curve, structurally the pitch lane's `PitchCurve`: frame f is
|
|
435
|
+
* centred at t0 + f·hop seconds; `f0` Hz (0 unvoiced); `prob` and
|
|
436
|
+
* `aperiodic` in 1/255 steps.
|
|
437
|
+
*/
|
|
438
|
+
export type AutotuneCurve = Readonly<{
|
|
439
|
+
t0: number;
|
|
440
|
+
hop: number;
|
|
441
|
+
f0: ArrayLike<number>;
|
|
442
|
+
prob: ArrayLike<number>;
|
|
443
|
+
aperiodic: ArrayLike<number>;
|
|
444
|
+
}>;
|
|
445
|
+
|
|
446
|
+
/** Target pitches in cents re A440, ascending (absolute, every octave). */
|
|
447
|
+
export type Grid = Readonly<{ points: Float64Array; minStep: number }>;
|
|
448
|
+
|
|
449
|
+
export const centsOfHz = (hz: number): number => 1200 * Math.log2(hz / 440);
|
|
450
|
+
|
|
451
|
+
function gridOf(points: number[]): Grid {
|
|
452
|
+
const sorted = [...new Set(points.map((p) => Math.round(p * 1e6) / 1e6))]
|
|
453
|
+
.filter(Number.isFinite)
|
|
454
|
+
.sort((a, b) => a - b);
|
|
455
|
+
let minStep = Infinity;
|
|
456
|
+
for (let i = 1; i < sorted.length; i += 1)
|
|
457
|
+
minStep = Math.min(minStep, sorted[i]! - sorted[i - 1]!);
|
|
458
|
+
return Object.freeze({
|
|
459
|
+
points: Float64Array.from(sorted),
|
|
460
|
+
minStep: Number.isFinite(minStep) ? minStep : 1200,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** 12-TET A440 cents of a (possibly fractional) key through a table. */
|
|
465
|
+
function keyCents(table: TuningTable | undefined, key: number): number {
|
|
466
|
+
const at = (k: number) => {
|
|
467
|
+
const hz = table?.hz[k];
|
|
468
|
+
return hz && hz > 0 ? ratioCents(hz / 440) : (k - 69) * 100;
|
|
469
|
+
};
|
|
470
|
+
const base = Math.min(126, Math.max(0, Math.floor(key)));
|
|
471
|
+
const frac = key - base;
|
|
472
|
+
if (frac === 0) return at(base);
|
|
473
|
+
// A maqam or dastgah scale preset (core/tuning.ts scalePreset) already
|
|
474
|
+
// lowered the upper key to the quarter tone: read it there. Only an
|
|
475
|
+
// untuned pair of keys interpolates.
|
|
476
|
+
const gap = at(base + 1) - at(base);
|
|
477
|
+
if (table && Math.abs(gap - 100) > 25) return at(base + 1);
|
|
478
|
+
return at(base) + gap * frac;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Every mapped pitch of a tuning (12-TET A440 without one). */
|
|
482
|
+
export function chromaticGrid(table?: TuningTable): Grid {
|
|
483
|
+
const points: number[] = [];
|
|
484
|
+
for (let key = 0; key < 128; key += 1) {
|
|
485
|
+
if (table) {
|
|
486
|
+
const hz = table.hz[key]!;
|
|
487
|
+
if (hz > 0) points.push(ratioCents(hz / 440));
|
|
488
|
+
} else points.push((key - 69) * 100);
|
|
489
|
+
}
|
|
490
|
+
return gridOf(points);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The key's scale through a tuning: degree positions are table pitches of
|
|
495
|
+
* the scale steps (fractional steps interpolate, so quarter tones and a
|
|
496
|
+
* raga tuning's intonation apply). A table that is not 12 to the period
|
|
497
|
+
* (19-EDO, Bohlen-Pierce) has no 12-note scale, so every table pitch is a
|
|
498
|
+
* target. No key: chromatic.
|
|
499
|
+
*/
|
|
500
|
+
export function scaleGrid(
|
|
501
|
+
keyText: string | null | undefined,
|
|
502
|
+
table?: TuningTable,
|
|
503
|
+
): Grid {
|
|
504
|
+
const key = parseKey(keyText ?? undefined);
|
|
505
|
+
if (
|
|
506
|
+
!key ||
|
|
507
|
+
(table && (table.size !== 12 || Math.abs(table.period - 1200) > 1e-6))
|
|
508
|
+
)
|
|
509
|
+
return chromaticGrid(table);
|
|
510
|
+
const steps = scaleSteps(key);
|
|
511
|
+
const points: number[] = [];
|
|
512
|
+
for (let octave = -1; octave <= 10; octave += 1)
|
|
513
|
+
for (const step of steps) {
|
|
514
|
+
const k = octave * 12 + key.tonic + step;
|
|
515
|
+
if (k >= 0 && k <= 127) points.push(keyCents(table, k));
|
|
516
|
+
}
|
|
517
|
+
return gridOf(points);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** A chord's pitch classes (0 = C) through a tuning, every octave. */
|
|
521
|
+
export function classGrid(
|
|
522
|
+
classes: readonly number[],
|
|
523
|
+
table?: TuningTable,
|
|
524
|
+
): Grid {
|
|
525
|
+
const points: number[] = [];
|
|
526
|
+
for (let key = 0; key < 128; key += 1)
|
|
527
|
+
if (classes.includes(((key % 12) + 12) % 12))
|
|
528
|
+
points.push(keyCents(table, key));
|
|
529
|
+
return gridOf(points);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Nearest grid target to `c` cents (lower on ties). */
|
|
533
|
+
export function nearestTarget(grid: Grid, c: number): number {
|
|
534
|
+
const p = grid.points;
|
|
535
|
+
if (p.length === 0) return c;
|
|
536
|
+
let lo = 0;
|
|
537
|
+
let hi = p.length - 1;
|
|
538
|
+
while (lo < hi) {
|
|
539
|
+
const mid = (lo + hi) >> 1;
|
|
540
|
+
if (p[mid]! < c) lo = mid + 1;
|
|
541
|
+
else hi = mid;
|
|
542
|
+
}
|
|
543
|
+
const above = p[lo]!;
|
|
544
|
+
const below = lo > 0 ? p[lo - 1]! : above;
|
|
545
|
+
return c - below <= above - c ? below : above;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Distance from target `t` to its nearer neighbouring point. */
|
|
549
|
+
function gapAt(grid: Grid, t: number): number {
|
|
550
|
+
const p = grid.points;
|
|
551
|
+
let best = Infinity;
|
|
552
|
+
for (let i = 0; i < p.length; i += 1) {
|
|
553
|
+
if (Math.abs(p[i]! - t) > 1e-6) continue;
|
|
554
|
+
if (i > 0) best = Math.min(best, t - p[i - 1]!);
|
|
555
|
+
if (i + 1 < p.length) best = Math.min(best, p[i + 1]! - t);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
return Number.isFinite(best) ? best : 1200;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
// Correction curves
|
|
563
|
+
|
|
564
|
+
const lp = (dtMs: number, tauMs: number) =>
|
|
565
|
+
tauMs <= 0 ? 1 : 1 - Math.exp(-dtMs / tauMs);
|
|
566
|
+
const smoothstep = (x: number, a: number, b: number) => {
|
|
567
|
+
const u = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
568
|
+
return u * u * (3 - 2 * u);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
function voicedAt(curve: AutotuneCurve, f: number): boolean {
|
|
572
|
+
return curve.f0[f]! > 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* How much correction a frame may take: breath, consonant tails and creak
|
|
577
|
+
* track as low-confidence or aperiodic frames, and hard tune on them would
|
|
578
|
+
* impose a periodic pitch on noise.
|
|
579
|
+
*/
|
|
580
|
+
export function voicingWeight(curve: AutotuneCurve, f: number): number {
|
|
581
|
+
return (
|
|
582
|
+
smoothstep((curve.prob[f] ?? 0) / 255, 0.3, 0.7) *
|
|
583
|
+
(1 - smoothstep((curve.aperiodic[f] ?? 255) / 255, 0.2, 0.4))
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Hard tune's weight: full correction on any pitched frame, including the
|
|
589
|
+
* lower-confidence onsets and glides between notes, so transitions land on
|
|
590
|
+
* the grid; breath and noise (aperiodic frames) still get none.
|
|
591
|
+
*/
|
|
592
|
+
export function hardVoicingWeight(curve: AutotuneCurve, f: number): number {
|
|
593
|
+
return (
|
|
594
|
+
smoothstep((curve.prob[f] ?? 0) / 255, 0.04, 0.18) *
|
|
595
|
+
(1 - smoothstep((curve.aperiodic[f] ?? 255) / 255, 0.2, 0.4))
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Centred moving average of cents over ±r frames, inside voiced runs. */
|
|
600
|
+
function centredAverage(curve: AutotuneCurve, r: number): Float64Array {
|
|
601
|
+
const n = curve.f0.length;
|
|
602
|
+
const out = new Float64Array(n).fill(NaN);
|
|
603
|
+
let f = 0;
|
|
604
|
+
while (f < n) {
|
|
605
|
+
if (!voicedAt(curve, f)) {
|
|
606
|
+
f += 1;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
let e = f;
|
|
610
|
+
while (e < n && voicedAt(curve, e)) e += 1;
|
|
611
|
+
// prefix sums keep it linear in the run length
|
|
612
|
+
const sums = new Float64Array(e - f + 1);
|
|
613
|
+
for (let i = f; i < e; i += 1)
|
|
614
|
+
sums[i - f + 1] = sums[i - f]! + centsOfHz(curve.f0[i]!);
|
|
615
|
+
for (let i = f; i < e; i += 1) {
|
|
616
|
+
const a = Math.max(f, i - r);
|
|
617
|
+
const b = Math.min(e - 1, i + r);
|
|
618
|
+
out[i] = (sums[b - f + 1]! - sums[a - f]!) / (b - a + 1);
|
|
619
|
+
}
|
|
620
|
+
f = e;
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Median of voiced cents over ±r frames (NaN where unvoiced). */
|
|
626
|
+
export function runningMedian(curve: AutotuneCurve, r: number): Float64Array {
|
|
627
|
+
const n = curve.f0.length;
|
|
628
|
+
const out = new Float64Array(n).fill(NaN);
|
|
629
|
+
const cents = new Float64Array(n);
|
|
630
|
+
for (let f = 0; f < n; f += 1)
|
|
631
|
+
cents[f] = voicedAt(curve, f) ? centsOfHz(curve.f0[f]!) : NaN;
|
|
632
|
+
const window: number[] = [];
|
|
633
|
+
for (let f = 0; f < n; f += 1) {
|
|
634
|
+
if (!voicedAt(curve, f)) continue;
|
|
635
|
+
window.length = 0;
|
|
636
|
+
for (let g = Math.max(0, f - r); g <= Math.min(n - 1, f + r); g += 1)
|
|
637
|
+
if (!Number.isNaN(cents[g]!)) window.push(cents[g]!);
|
|
638
|
+
window.sort((a, b) => a - b);
|
|
639
|
+
out[f] = window[window.length >> 1]!;
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Grid targets: one grid, or one per frame time (chord changes). */
|
|
645
|
+
export type GridSource = Grid | ((seconds: number) => Grid);
|
|
646
|
+
|
|
647
|
+
export type RetuneParams = Readonly<{
|
|
648
|
+
speed: number;
|
|
649
|
+
relax?: number;
|
|
650
|
+
hold?: number;
|
|
651
|
+
flex?: number;
|
|
652
|
+
glide?: number;
|
|
653
|
+
amount?: number;
|
|
654
|
+
/** Added vibrato rate Hz and peak depth in cents. */
|
|
655
|
+
vib?: number;
|
|
656
|
+
vibCents?: number;
|
|
657
|
+
/** Hysteresis floor in cents (default 12). */
|
|
658
|
+
hysteresis?: number;
|
|
659
|
+
}>;
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Grid correction: shift in cents per frame (NaN where unvoiced, i.e. pass
|
|
663
|
+
* through). Port of proto/pitch/correct.ts retuneCurve with its review
|
|
664
|
+
* fixes: density-scaled hysteresis, 180 ms centred selection on dense
|
|
665
|
+
* grids, octave guard, voicing weight.
|
|
666
|
+
*/
|
|
667
|
+
export function retuneCurve(
|
|
668
|
+
curve: AutotuneCurve,
|
|
669
|
+
source: GridSource,
|
|
670
|
+
p: RetuneParams,
|
|
671
|
+
): Float64Array {
|
|
672
|
+
const n = curve.f0.length;
|
|
673
|
+
const out = new Float64Array(n).fill(NaN);
|
|
674
|
+
const dtMs = curve.hop * 1000;
|
|
675
|
+
const amount = p.amount ?? 1;
|
|
676
|
+
const hystFloor = p.hysteresis ?? 12;
|
|
677
|
+
const relax = p.relax ?? 0;
|
|
678
|
+
const holdMs = p.hold ?? 150;
|
|
679
|
+
const flex = Math.min(100, Math.max(0, p.flex ?? 0));
|
|
680
|
+
const zone = 50 * (1 - flex / 100);
|
|
681
|
+
const glideMs = (p.glide ?? 0) * 1000;
|
|
682
|
+
const gridAt =
|
|
683
|
+
typeof source === "function" ? source : (_seconds: number) => source;
|
|
684
|
+
const dynamic = typeof source === "function";
|
|
685
|
+
const selTau = p.speed <= 5 ? 0 : 60;
|
|
686
|
+
// Hard tune also snaps the pitched glides between notes, which track
|
|
687
|
+
// with lower confidence than the held middles.
|
|
688
|
+
const hardTune = p.speed === 0 && relax === 0;
|
|
689
|
+
const first = gridAt(curve.t0);
|
|
690
|
+
const anyDense = dynamic || first.minStep < 100;
|
|
691
|
+
const avg = anyDense
|
|
692
|
+
? centredAverage(curve, Math.round(0.09 / curve.hop))
|
|
693
|
+
: null;
|
|
694
|
+
const med = runningMedian(curve, Math.round(0.15 / curve.hop));
|
|
695
|
+
let centre = NaN;
|
|
696
|
+
let target = NaN;
|
|
697
|
+
let shown = NaN;
|
|
698
|
+
let corr = 0;
|
|
699
|
+
let inNote = 0;
|
|
700
|
+
let lastGrid: Grid | undefined;
|
|
701
|
+
for (let f = 0; f < n; f += 1) {
|
|
702
|
+
const hz = curve.f0[f]!;
|
|
703
|
+
if (!(hz > 0)) {
|
|
704
|
+
centre = NaN;
|
|
705
|
+
target = NaN;
|
|
706
|
+
shown = NaN;
|
|
707
|
+
corr = 0;
|
|
708
|
+
inNote = 0;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const grid = gridAt(curve.t0 + f * curve.hop);
|
|
712
|
+
const dense = grid.minStep < 100;
|
|
713
|
+
const c = centsOfHz(hz);
|
|
714
|
+
centre =
|
|
715
|
+
dense && avg
|
|
716
|
+
? avg[f]!
|
|
717
|
+
: Number.isNaN(centre)
|
|
718
|
+
? c
|
|
719
|
+
: centre + (c - centre) * lp(dtMs, selTau);
|
|
720
|
+
const nearest = nearestTarget(grid, centre);
|
|
721
|
+
// the grid changed (a chord change) and the held target left it
|
|
722
|
+
if (lastGrid !== grid && !Number.isNaN(target)) {
|
|
723
|
+
if (Math.abs(nearestTarget(grid, target) - target) > 1e-6) {
|
|
724
|
+
target = nearest;
|
|
725
|
+
inNote = 0;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
lastGrid = grid;
|
|
729
|
+
const hyst =
|
|
730
|
+
Number.isNaN(target) || !dense
|
|
731
|
+
? hystFloor
|
|
732
|
+
: Math.max(hystFloor, 0.35 * gapAt(grid, target));
|
|
733
|
+
if (Number.isNaN(target)) target = nearest;
|
|
734
|
+
else if (
|
|
735
|
+
nearest !== target &&
|
|
736
|
+
Math.abs(centre - nearest) + hyst < Math.abs(centre - target)
|
|
737
|
+
) {
|
|
738
|
+
target = nearest;
|
|
739
|
+
inNote = 0;
|
|
740
|
+
}
|
|
741
|
+
if (Number.isNaN(shown) || glideMs <= 0) shown = target;
|
|
742
|
+
else {
|
|
743
|
+
const maxStep = (dtMs / glideMs) * 100;
|
|
744
|
+
shown += Math.max(-maxStep, Math.min(maxStep, target - shown));
|
|
745
|
+
}
|
|
746
|
+
inNote += dtMs;
|
|
747
|
+
let w = amount;
|
|
748
|
+
if (flex > 0) {
|
|
749
|
+
const dist = Math.abs(centre - target);
|
|
750
|
+
w *= dist <= zone ? 1 : Math.max(0, 1 - (dist - zone) / 10);
|
|
751
|
+
}
|
|
752
|
+
const voicing =
|
|
753
|
+
Math.abs(c - med[f]!) > 900
|
|
754
|
+
? 0
|
|
755
|
+
: hardTune
|
|
756
|
+
? hardVoicingWeight(curve, f)
|
|
757
|
+
: voicingWeight(curve, f);
|
|
758
|
+
w *= voicing;
|
|
759
|
+
const sustained =
|
|
760
|
+
relax > 0 && inNote > holdMs
|
|
761
|
+
? Math.min(1, (inNote - holdMs) / holdMs)
|
|
762
|
+
: 0;
|
|
763
|
+
const tau =
|
|
764
|
+
p.speed * (1 + 7 * relax * sustained) +
|
|
765
|
+
(sustained > 0 && p.speed === 0 ? 40 * relax * sustained : 0);
|
|
766
|
+
const err = (shown - c) * w;
|
|
767
|
+
corr += (err - corr) * lp(dtMs, tau);
|
|
768
|
+
// Added vibrato follows the voicing too: none on breath or octave slips.
|
|
769
|
+
out[f] =
|
|
770
|
+
corr + voicing * addedVibrato(p.vib ?? 0, p.vibCents ?? 0, inNote / 1000);
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Added vibrato: starts `delay` seconds into a note (0.2 s for the track's
|
|
777
|
+
* vib, the note's own vibrato delay for a guide note) and fades in over
|
|
778
|
+
* 0.15 s like core/expression.ts.
|
|
779
|
+
*/
|
|
780
|
+
function addedVibrato(
|
|
781
|
+
rate: number,
|
|
782
|
+
depth: number,
|
|
783
|
+
t: number,
|
|
784
|
+
delay = 0.2,
|
|
785
|
+
): number {
|
|
786
|
+
if (rate <= 0 || depth <= 0) return 0;
|
|
787
|
+
const into = t - delay;
|
|
788
|
+
if (into <= 0) return 0;
|
|
789
|
+
return depth * Math.min(1, into / 0.15) * Math.sin(2 * Math.PI * rate * into);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** A guide note in buffer seconds. */
|
|
793
|
+
export type GuideNote = Readonly<{
|
|
794
|
+
start: number;
|
|
795
|
+
end: number;
|
|
796
|
+
/** Target cents re A440 (noteHz of the note, cents included). */
|
|
797
|
+
cents: number;
|
|
798
|
+
/** Per-note drift override (NoteExpression.drift). */
|
|
799
|
+
drift?: number;
|
|
800
|
+
/** Ghost notes get no correction. */
|
|
801
|
+
ghost?: boolean;
|
|
802
|
+
/** Seconds of glide into this note (the note's glide). */
|
|
803
|
+
glide?: number;
|
|
804
|
+
/** The note's vibrato (rate Hz, depth semitones) replaces vib/vibmod. */
|
|
805
|
+
vib?: number;
|
|
806
|
+
vibmod?: number;
|
|
807
|
+
/** The note's vibrato delay in seconds (0 when it has none). */
|
|
808
|
+
vibDelay?: number;
|
|
809
|
+
}>;
|
|
810
|
+
|
|
811
|
+
export type GuideParams = Readonly<{
|
|
812
|
+
center?: number;
|
|
813
|
+
drift?: number;
|
|
814
|
+
/** Transition between notes in seconds. */
|
|
815
|
+
glide?: number;
|
|
816
|
+
/** When set, locked mode: the retune slew in ms. */
|
|
817
|
+
speed?: number;
|
|
818
|
+
amount?: number;
|
|
819
|
+
relax?: number;
|
|
820
|
+
hold?: number;
|
|
821
|
+
vib?: number;
|
|
822
|
+
vibCents?: number;
|
|
823
|
+
}>;
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Note-guided correction (port of proto guideCurve with its review fixes):
|
|
827
|
+
* Melodyne-style center/drift, or locked to the notes with `speed`; legato
|
|
828
|
+
* boundaries follow the straight line between the corrected pitches half a
|
|
829
|
+
* transition either side.
|
|
830
|
+
*/
|
|
831
|
+
export function guideCurve(
|
|
832
|
+
curve: AutotuneCurve,
|
|
833
|
+
notes: readonly GuideNote[],
|
|
834
|
+
p: GuideParams,
|
|
835
|
+
): Float64Array {
|
|
836
|
+
const n = curve.f0.length;
|
|
837
|
+
const out = new Float64Array(n).fill(NaN);
|
|
838
|
+
const center = p.center ?? 1;
|
|
839
|
+
const driftDefault = p.drift ?? 0.5;
|
|
840
|
+
const trans = p.glide ?? 0.04;
|
|
841
|
+
const dtMs = curve.hop * 1000;
|
|
842
|
+
const sorted = [...notes].sort((a, b) => a.start - b.start);
|
|
843
|
+
const noteOf = new Int32Array(n).fill(-1);
|
|
844
|
+
const sums = sorted.map(() => ({ s: 0, n: 0 }));
|
|
845
|
+
let cursor = 0;
|
|
846
|
+
for (let f = 0; f < n; f += 1) {
|
|
847
|
+
const hz = curve.f0[f]!;
|
|
848
|
+
if (!(hz > 0)) continue;
|
|
849
|
+
const t = curve.t0 + f * curve.hop;
|
|
850
|
+
while (cursor < sorted.length && sorted[cursor]!.end <= t) cursor += 1;
|
|
851
|
+
for (let k = cursor; k < sorted.length && sorted[k]!.start <= t; k += 1) {
|
|
852
|
+
const note = sorted[k]!;
|
|
853
|
+
if (t >= note.start && t < note.end) {
|
|
854
|
+
noteOf[f] = k;
|
|
855
|
+
if (t > note.start + 0.06) {
|
|
856
|
+
sums[k]!.s += centsOfHz(hz);
|
|
857
|
+
sums[k]!.n += 1;
|
|
858
|
+
}
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const mean = sums.map((s) => (s.n ? s.s / s.n : NaN));
|
|
864
|
+
// The slow (drift) estimate: a 150 ms one-pole run forwards then
|
|
865
|
+
// backwards inside each note, starting from the note mean. Zero phase
|
|
866
|
+
// (offline), so a scoop or the next note's step never leaks into it
|
|
867
|
+
// (review open item: drift 1 leaked the step with a causal estimate).
|
|
868
|
+
const slowArr = new Float64Array(n).fill(NaN);
|
|
869
|
+
{
|
|
870
|
+
const a = lp(dtMs, 150);
|
|
871
|
+
let f = 0;
|
|
872
|
+
while (f < n) {
|
|
873
|
+
const k = noteOf[f]!;
|
|
874
|
+
if (k < 0) {
|
|
875
|
+
f += 1;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
let e = f;
|
|
879
|
+
while (e < n && noteOf[e] === k) e += 1;
|
|
880
|
+
let y = Number.isNaN(mean[k]!) ? centsOfHz(curve.f0[f]!) : mean[k]!;
|
|
881
|
+
for (let g = f; g < e; g += 1) {
|
|
882
|
+
y += (centsOfHz(curve.f0[g]!) - y) * a;
|
|
883
|
+
slowArr[g] = y;
|
|
884
|
+
}
|
|
885
|
+
y = Number.isNaN(mean[k]!) ? slowArr[e - 1]! : mean[k]!;
|
|
886
|
+
for (let g = e - 1; g >= f; g -= 1) {
|
|
887
|
+
y += (slowArr[g]! - y) * a;
|
|
888
|
+
slowArr[g] = y;
|
|
889
|
+
}
|
|
890
|
+
f = e;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const hard = p.speed !== undefined;
|
|
894
|
+
const amount = p.amount ?? 1;
|
|
895
|
+
const relax = p.relax ?? 0;
|
|
896
|
+
const holdMs = p.hold ?? 150;
|
|
897
|
+
let slow = NaN;
|
|
898
|
+
let prevK = -1;
|
|
899
|
+
let corr = 0;
|
|
900
|
+
let inNote = 0;
|
|
901
|
+
for (let f = 0; f < n; f += 1) {
|
|
902
|
+
const k = noteOf[f]!;
|
|
903
|
+
if (k < 0) {
|
|
904
|
+
slow = NaN;
|
|
905
|
+
prevK = -1;
|
|
906
|
+
corr = 0;
|
|
907
|
+
inNote = 0;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const note = sorted[k]!;
|
|
911
|
+
if (k !== prevK) inNote = 0;
|
|
912
|
+
inNote += dtMs;
|
|
913
|
+
const c = centsOfHz(curve.f0[f]!);
|
|
914
|
+
let want: number;
|
|
915
|
+
if (note.ghost) want = 0;
|
|
916
|
+
else if (hard) {
|
|
917
|
+
const sustained =
|
|
918
|
+
relax > 0 && inNote > holdMs
|
|
919
|
+
? Math.min(1, (inNote - holdMs) / holdMs)
|
|
920
|
+
: 0;
|
|
921
|
+
const glideMs = (note.glide ?? 0) * 1000;
|
|
922
|
+
const tau = Math.max(p.speed! * (1 + 7 * relax * sustained), glideMs / 3);
|
|
923
|
+
const err = note.cents - c;
|
|
924
|
+
corr = prevK < 0 ? err : corr + (err - corr) * lp(dtMs, tau);
|
|
925
|
+
want = corr * amount;
|
|
926
|
+
} else {
|
|
927
|
+
slow = slowArr[f]!;
|
|
928
|
+
const m = Number.isNaN(mean[k]!) ? c : mean[k]!;
|
|
929
|
+
const drift = note.drift ?? driftDefault;
|
|
930
|
+
want = amount * (center * (note.cents - m) - drift * (slow - m));
|
|
931
|
+
}
|
|
932
|
+
const rate = note.vib ?? p.vib ?? 0;
|
|
933
|
+
const depth =
|
|
934
|
+
note.vib !== undefined ? (note.vibmod ?? 0) * 100 : (p.vibCents ?? 0);
|
|
935
|
+
const delay = note.vib !== undefined ? (note.vibDelay ?? 0) : 0.2;
|
|
936
|
+
out[f] = note.ghost
|
|
937
|
+
? 0
|
|
938
|
+
: want + addedVibrato(rate, depth, inNote / 1000, delay);
|
|
939
|
+
prevK = k;
|
|
940
|
+
}
|
|
941
|
+
const voiced = (f: number) => noteOf[f]! >= 0;
|
|
942
|
+
if (!hard) {
|
|
943
|
+
const src = Float64Array.from(out);
|
|
944
|
+
for (let f = 1; f < n; f += 1) {
|
|
945
|
+
if (!voiced(f) || !voiced(f - 1) || noteOf[f] === noteOf[f - 1]) continue;
|
|
946
|
+
const into = sorted[noteOf[f]!]!;
|
|
947
|
+
const width = into.glide ?? trans;
|
|
948
|
+
if (width <= 0 || into.ghost || sorted[noteOf[f - 1]!]!.ghost) continue;
|
|
949
|
+
const half = Math.max(1, Math.round(width / 2 / curve.hop));
|
|
950
|
+
const a = Math.max(0, f - half);
|
|
951
|
+
const b = Math.min(n - 1, f + half - 1);
|
|
952
|
+
let ok = true;
|
|
953
|
+
for (let g = a; g <= b; g += 1) if (!voiced(g)) ok = false;
|
|
954
|
+
if (!ok) continue;
|
|
955
|
+
const pa = centsOfHz(curve.f0[a]!) + src[a]!;
|
|
956
|
+
const pb = centsOfHz(curve.f0[b]!) + src[b]!;
|
|
957
|
+
for (let g = a; g <= b; g += 1) {
|
|
958
|
+
const u = (g - a) / Math.max(1, b - a);
|
|
959
|
+
out[g] = pa + (pb - pa) * u - centsOfHz(curve.f0[g]!);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
for (let f = 0; f < n; f += 1)
|
|
964
|
+
if (voiced(f) && !sorted[noteOf[f]!]!.ghost)
|
|
965
|
+
out[f] = out[f]! * voicingWeight(curve, f);
|
|
966
|
+
return out;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ---------------------------------------------------------------------------
|
|
970
|
+
// Buffer
|
|
971
|
+
|
|
972
|
+
/** Targets for one buffer, in buffer seconds. */
|
|
973
|
+
export type AutotuneTargets =
|
|
974
|
+
| Readonly<{ kind: "grid"; grid: GridSource }>
|
|
975
|
+
| Readonly<{ kind: "notes"; notes: readonly GuideNote[] }>;
|
|
976
|
+
|
|
977
|
+
/** The PSOLA pass: input f0 and shift in cents per sample. */
|
|
978
|
+
export type PsolaShift = (
|
|
979
|
+
x: Float64Array,
|
|
980
|
+
sampleRate: number,
|
|
981
|
+
curve: Readonly<{ f0: Float64Array; cents: Float64Array }>,
|
|
982
|
+
) => Float64Array;
|
|
983
|
+
|
|
984
|
+
/** Shift in cents per frame for resolved settings and targets. */
|
|
985
|
+
export function correctionCurve(
|
|
986
|
+
curve: AutotuneCurve,
|
|
987
|
+
targets: AutotuneTargets,
|
|
988
|
+
r: ResolvedAutotune,
|
|
989
|
+
maxShift = 12,
|
|
990
|
+
): Float64Array {
|
|
991
|
+
const vibCents = r.vibmod * 100;
|
|
992
|
+
const shift =
|
|
993
|
+
targets.kind === "grid"
|
|
994
|
+
? retuneCurve(curve, targets.grid, {
|
|
995
|
+
speed: r.speed ?? 25,
|
|
996
|
+
relax: r.relax,
|
|
997
|
+
hold: r.hold,
|
|
998
|
+
flex: r.flex,
|
|
999
|
+
glide: r.glide,
|
|
1000
|
+
amount: r.amount,
|
|
1001
|
+
vib: r.vib,
|
|
1002
|
+
vibCents,
|
|
1003
|
+
})
|
|
1004
|
+
: guideCurve(curve, targets.notes, {
|
|
1005
|
+
center: r.center,
|
|
1006
|
+
drift: r.drift,
|
|
1007
|
+
glide: r.glide,
|
|
1008
|
+
...(r.speed !== undefined ? { speed: r.speed } : {}),
|
|
1009
|
+
amount: r.amount,
|
|
1010
|
+
relax: r.relax,
|
|
1011
|
+
hold: r.hold,
|
|
1012
|
+
vib: r.vib,
|
|
1013
|
+
vibCents,
|
|
1014
|
+
});
|
|
1015
|
+
const cap = maxShift * 100;
|
|
1016
|
+
for (let f = 0; f < shift.length; f += 1)
|
|
1017
|
+
if (!Number.isNaN(shift[f]!))
|
|
1018
|
+
shift[f] = Math.max(-cap, Math.min(cap, shift[f]!));
|
|
1019
|
+
return shift;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** Per-sample input f0 and shift (linear between frames). */
|
|
1023
|
+
export function perSampleShift(
|
|
1024
|
+
curve: AutotuneCurve,
|
|
1025
|
+
shift: Float64Array,
|
|
1026
|
+
frames: number,
|
|
1027
|
+
sampleRate: number,
|
|
1028
|
+
): { f0: Float64Array; cents: Float64Array } {
|
|
1029
|
+
const f0 = new Float64Array(frames);
|
|
1030
|
+
const cents = new Float64Array(frames);
|
|
1031
|
+
const n = curve.f0.length;
|
|
1032
|
+
for (let i = 0; i < frames; i += 1) {
|
|
1033
|
+
const pos = (i / sampleRate - curve.t0) / curve.hop;
|
|
1034
|
+
const j = Math.floor(pos);
|
|
1035
|
+
if (j < 0 || j >= n) continue;
|
|
1036
|
+
const u = pos - j;
|
|
1037
|
+
const fa = curve.f0[j]!;
|
|
1038
|
+
const fb = j + 1 < n ? curve.f0[j + 1]! : fa;
|
|
1039
|
+
// the nearer frame decides voicing; both voiced interpolate in Hz
|
|
1040
|
+
f0[i] = fa > 0 && fb > 0 ? fa + (fb - fa) * u : u < 0.5 ? fa : fb;
|
|
1041
|
+
const a = shift[j]!;
|
|
1042
|
+
const b = j + 1 < n ? shift[j + 1]! : a;
|
|
1043
|
+
if (Number.isNaN(a) && Number.isNaN(b)) cents[i] = 0;
|
|
1044
|
+
else if (Number.isNaN(a)) cents[i] = b;
|
|
1045
|
+
else if (Number.isNaN(b)) cents[i] = a;
|
|
1046
|
+
else cents[i] = a + (b - a) * u;
|
|
1047
|
+
}
|
|
1048
|
+
return { f0, cents };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* A resumable PSOLA pass (the pitch lane's psola as a generator): yields
|
|
1053
|
+
* between blocks so the live scheduler can run it in slices.
|
|
1054
|
+
*/
|
|
1055
|
+
export type PsolaJob = (
|
|
1056
|
+
x: Float64Array,
|
|
1057
|
+
sampleRate: number,
|
|
1058
|
+
curve: Readonly<{ f0: Float64Array; cents: Float64Array }>,
|
|
1059
|
+
) => Generator<void, Float64Array>;
|
|
1060
|
+
|
|
1061
|
+
/** Samples between yields in the per-sample steps of `autotuneJob`. */
|
|
1062
|
+
const JOB_BLOCK = 16384;
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* The tuned buffer as a resumable job: correction curve, per-sample shift
|
|
1066
|
+
* and PSOLA, yielding between blocks (and inside `psolaJob` when given).
|
|
1067
|
+
* Its result is identical to `autotuneBuffer`, which drains it.
|
|
1068
|
+
*/
|
|
1069
|
+
export function* autotuneJob(
|
|
1070
|
+
x: Float32Array,
|
|
1071
|
+
sampleRate: number,
|
|
1072
|
+
curve: AutotuneCurve,
|
|
1073
|
+
targets: AutotuneTargets,
|
|
1074
|
+
r: ResolvedAutotune,
|
|
1075
|
+
psola: PsolaShift,
|
|
1076
|
+
maxShift = 12,
|
|
1077
|
+
psolaJob?: PsolaJob,
|
|
1078
|
+
): Generator<void, Float32Array> {
|
|
1079
|
+
const shift = correctionCurve(curve, targets, r, maxShift);
|
|
1080
|
+
let moves = false;
|
|
1081
|
+
for (let f = 0; f < shift.length && !moves; f += 1)
|
|
1082
|
+
if (Math.abs(shift[f]!) > 1e-9) moves = true;
|
|
1083
|
+
if (!moves) return x;
|
|
1084
|
+
yield;
|
|
1085
|
+
const input = new Float64Array(x.length);
|
|
1086
|
+
for (let i = 0; i < x.length; i += JOB_BLOCK) {
|
|
1087
|
+
const end = Math.min(x.length, i + JOB_BLOCK);
|
|
1088
|
+
for (let j = i; j < end; j += 1) input[j] = x[j]!;
|
|
1089
|
+
yield;
|
|
1090
|
+
}
|
|
1091
|
+
const per = perSampleShift(curve, shift, x.length, sampleRate);
|
|
1092
|
+
yield;
|
|
1093
|
+
const y = psolaJob
|
|
1094
|
+
? yield* psolaJob(input, sampleRate, per)
|
|
1095
|
+
: psola(input, sampleRate, per);
|
|
1096
|
+
yield;
|
|
1097
|
+
return Float32Array.from(y);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* The tuned buffer: correction curve, per-sample shift, PSOLA. The one
|
|
1102
|
+
* entry point the sampler and clip hooks call. Returns the input unchanged
|
|
1103
|
+
* (same object) when nothing would move.
|
|
1104
|
+
*/
|
|
1105
|
+
export function autotuneBuffer(
|
|
1106
|
+
x: Float32Array,
|
|
1107
|
+
sampleRate: number,
|
|
1108
|
+
curve: AutotuneCurve,
|
|
1109
|
+
targets: AutotuneTargets,
|
|
1110
|
+
r: ResolvedAutotune,
|
|
1111
|
+
psola: PsolaShift,
|
|
1112
|
+
maxShift = 12,
|
|
1113
|
+
): Float32Array {
|
|
1114
|
+
const job = autotuneJob(x, sampleRate, curve, targets, r, psola, maxShift);
|
|
1115
|
+
for (;;) {
|
|
1116
|
+
const step = job.next();
|
|
1117
|
+
if (step.done) return step.value;
|
|
1118
|
+
}
|
|
1119
|
+
}
|