@hraness/dawg 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/core/vocoder.ts
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocoder (0.7 "Voice", lane vocoder): a track's sound (the carrier) shaped
|
|
3
|
+
* by another track's sound (the modulator, `src`). Pure and import-light,
|
|
4
|
+
* like `core/winds.ts`: the parameter table, the eight presets and the
|
|
5
|
+
* validator that the score, the prompt, the menu, the agent and the SDK
|
|
6
|
+
* printer share. The DSP lives in `src/audio/vocoder/`.
|
|
7
|
+
*
|
|
8
|
+
* Two ways in:
|
|
9
|
+
* - any track gains `vocoder: { src, ... }`: its own voice is the carrier;
|
|
10
|
+
* - instrument `vocoder` with a `vocoder` field plays a built-in carrier
|
|
11
|
+
* (saw, supersaw, pulse or noise) following the track's notes, the song's
|
|
12
|
+
* chords or a drone.
|
|
13
|
+
*
|
|
14
|
+
* The stage runs only when `vocoder.src` is present; without it the track
|
|
15
|
+
* plays dry (a `vocoder` instrument without a source is silent). Absent
|
|
16
|
+
* field: today's sound, byte for byte.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
FxValidationError,
|
|
20
|
+
isRecord,
|
|
21
|
+
normalizeParams,
|
|
22
|
+
type NumberParam,
|
|
23
|
+
type ParamSpec,
|
|
24
|
+
} from "./params.ts";
|
|
25
|
+
|
|
26
|
+
/** Instrument value that selects the built-in carrier. */
|
|
27
|
+
export const VOCODER_INSTRUMENT = "vocoder" as const;
|
|
28
|
+
|
|
29
|
+
export const VOCODER_PRESET_NAMES = Object.freeze([
|
|
30
|
+
"classic",
|
|
31
|
+
"robot",
|
|
32
|
+
"talkbox",
|
|
33
|
+
"choir",
|
|
34
|
+
"glass",
|
|
35
|
+
"whisper",
|
|
36
|
+
"smear",
|
|
37
|
+
"lofi",
|
|
38
|
+
] as const);
|
|
39
|
+
export type VocoderPresetName = (typeof VOCODER_PRESET_NAMES)[number];
|
|
40
|
+
|
|
41
|
+
/** Default preset of a `vocoder` field that names none. */
|
|
42
|
+
export const DEFAULT_VOCODER_PRESET: VocoderPresetName = "classic";
|
|
43
|
+
|
|
44
|
+
/** Modulator taps (append-only). */
|
|
45
|
+
export const VOCODER_TAPS = Object.freeze(["chain", "dry"]);
|
|
46
|
+
/** Modes (append-only): a band bank or an LPC talkbox. */
|
|
47
|
+
export const VOCODER_MODES = Object.freeze(["channel", "talkbox"]);
|
|
48
|
+
/** Built-in carriers (append-only; `glottal` is 0.7.1). */
|
|
49
|
+
export const VOCODER_CARRIERS = Object.freeze([
|
|
50
|
+
"saw",
|
|
51
|
+
"supersaw",
|
|
52
|
+
"pulse",
|
|
53
|
+
"noise",
|
|
54
|
+
]);
|
|
55
|
+
/** Built-in carrier pitch (append-only; `voice` is 0.7.1). */
|
|
56
|
+
export const VOCODER_FOLLOWS = Object.freeze(["notes", "chords", "drone"]);
|
|
57
|
+
|
|
58
|
+
/** `gate` value that reads the threshold off the modulator's own assets. */
|
|
59
|
+
export const VOCODER_GATE_AUTO = "auto" as const;
|
|
60
|
+
|
|
61
|
+
/** Talkbox formant bound (the channel bank takes the full ±24). */
|
|
62
|
+
export const TALKBOX_FORMANT_MAX = 12;
|
|
63
|
+
|
|
64
|
+
const num = (
|
|
65
|
+
min: number,
|
|
66
|
+
max: number,
|
|
67
|
+
fallback: number,
|
|
68
|
+
step: number | "log",
|
|
69
|
+
doc: string,
|
|
70
|
+
extra: Partial<NumberParam> = {},
|
|
71
|
+
): NumberParam =>
|
|
72
|
+
Object.freeze({
|
|
73
|
+
kind: "number",
|
|
74
|
+
min,
|
|
75
|
+
max,
|
|
76
|
+
default: fallback,
|
|
77
|
+
step,
|
|
78
|
+
optional: true,
|
|
79
|
+
doc,
|
|
80
|
+
...extra,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const choice = (values: readonly string[], fallback: string, doc: string) =>
|
|
84
|
+
Object.freeze({
|
|
85
|
+
kind: "enum" as const,
|
|
86
|
+
values,
|
|
87
|
+
default: fallback,
|
|
88
|
+
optional: true,
|
|
89
|
+
doc,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every vocoder parameter in table order (`src` and `preset` excluded). All
|
|
94
|
+
* optional: an absent one takes the preset's value, then the default here.
|
|
95
|
+
* `automate` ones have a `vocoder-<param>` lane. `gate` also takes "auto".
|
|
96
|
+
*/
|
|
97
|
+
export const VOCODER_PARAMS: Readonly<Record<string, ParamSpec>> =
|
|
98
|
+
Object.freeze({
|
|
99
|
+
tap: choice(
|
|
100
|
+
VOCODER_TAPS,
|
|
101
|
+
"chain",
|
|
102
|
+
"modulator after its mono chain (pre-pan) or dry before it",
|
|
103
|
+
),
|
|
104
|
+
mode: choice(VOCODER_MODES, "channel", "band bank or LPC talkbox"),
|
|
105
|
+
carrier: choice(
|
|
106
|
+
VOCODER_CARRIERS,
|
|
107
|
+
"supersaw",
|
|
108
|
+
"built-in carrier (instrument vocoder only)",
|
|
109
|
+
),
|
|
110
|
+
follow: choice(
|
|
111
|
+
VOCODER_FOLLOWS,
|
|
112
|
+
"notes",
|
|
113
|
+
"built-in carrier pitch: notes, song chords or a drone",
|
|
114
|
+
),
|
|
115
|
+
root: num(24, 96, 45, 1, "drone pitch (MIDI)", { integer: true }),
|
|
116
|
+
spread: num(0, 1, 0.15, 0.05, "supersaw detune spread (semitones)", {
|
|
117
|
+
unit: "st",
|
|
118
|
+
automate: true,
|
|
119
|
+
}),
|
|
120
|
+
bands: num(4, 40, 16, 1, "channel bands (heavy above 24)", {
|
|
121
|
+
integer: true,
|
|
122
|
+
}),
|
|
123
|
+
lo: num(50, 1000, 100, "log", "lowest band center", { unit: "Hz" }),
|
|
124
|
+
hi: num(2000, 12000, 8000, "log", "highest band center", { unit: "Hz" }),
|
|
125
|
+
width: num(0.25, 4, 1, 0.05, "band width (x spacing)", {
|
|
126
|
+
automate: true,
|
|
127
|
+
}),
|
|
128
|
+
attack: num(0.0005, 0.2, 0.005, "log", "follower attack", {
|
|
129
|
+
unit: "s",
|
|
130
|
+
strudel: ["att"],
|
|
131
|
+
}),
|
|
132
|
+
release: num(0.005, 2, 0.04, "log", "follower release (long = smear)", {
|
|
133
|
+
unit: "s",
|
|
134
|
+
automate: true,
|
|
135
|
+
strudel: ["rel"],
|
|
136
|
+
}),
|
|
137
|
+
formant: num(
|
|
138
|
+
-24,
|
|
139
|
+
24,
|
|
140
|
+
0,
|
|
141
|
+
0.5,
|
|
142
|
+
"formant shift (+ is smaller and brighter; talkbox ±12)",
|
|
143
|
+
{ unit: "st", automate: true },
|
|
144
|
+
),
|
|
145
|
+
unvoiced: num(0, 1, 0.5, 0.05, "noise carrier on fricatives", {
|
|
146
|
+
automate: true,
|
|
147
|
+
}),
|
|
148
|
+
sens: num(0, 1, 0.5, 0.05, "unvoiced detection sensitivity"),
|
|
149
|
+
hiss: num(0, 1, 0, 0.05, "high-passed modulator added through", {
|
|
150
|
+
automate: true,
|
|
151
|
+
}),
|
|
152
|
+
gate: num(-90, 0, -60, 1, 'modulator gate in dBFS, or "auto"', {
|
|
153
|
+
unit: "dB",
|
|
154
|
+
}),
|
|
155
|
+
enhance: Object.freeze({
|
|
156
|
+
kind: "boolean" as const,
|
|
157
|
+
default: true,
|
|
158
|
+
optional: true,
|
|
159
|
+
doc: "whiten the carrier so every band speaks",
|
|
160
|
+
}),
|
|
161
|
+
depth: num(0, 1, 1, 0.05, "how much the voice shapes the carrier", {
|
|
162
|
+
automate: true,
|
|
163
|
+
}),
|
|
164
|
+
freeze: Object.freeze({
|
|
165
|
+
kind: "boolean" as const,
|
|
166
|
+
default: false,
|
|
167
|
+
optional: true,
|
|
168
|
+
doc: "hold the last sung vowel through rests",
|
|
169
|
+
}),
|
|
170
|
+
mix: num(0, 1, 1, 0.05, "wet against the carrier", { automate: true }),
|
|
171
|
+
gain: num(-24, 24, 0, 0.5, "output trim", { unit: "dB", automate: true }),
|
|
172
|
+
seed: num(0, 2 ** 31, 1, 1, "noise seed (absent: from the track id)", {
|
|
173
|
+
integer: true,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export type VocoderTap = "chain" | "dry";
|
|
178
|
+
export type VocoderMode = "channel" | "talkbox";
|
|
179
|
+
export type VocoderCarrier = "saw" | "supersaw" | "pulse" | "noise";
|
|
180
|
+
export type VocoderFollow = "notes" | "chords" | "drone";
|
|
181
|
+
|
|
182
|
+
/** Overrides over a preset, all optional. */
|
|
183
|
+
export type VocoderOverrides = Readonly<{
|
|
184
|
+
tap?: VocoderTap;
|
|
185
|
+
mode?: VocoderMode;
|
|
186
|
+
carrier?: VocoderCarrier;
|
|
187
|
+
follow?: VocoderFollow;
|
|
188
|
+
root?: number;
|
|
189
|
+
spread?: number;
|
|
190
|
+
bands?: number;
|
|
191
|
+
lo?: number;
|
|
192
|
+
hi?: number;
|
|
193
|
+
width?: number;
|
|
194
|
+
attack?: number;
|
|
195
|
+
release?: number;
|
|
196
|
+
formant?: number;
|
|
197
|
+
unvoiced?: number;
|
|
198
|
+
sens?: number;
|
|
199
|
+
hiss?: number;
|
|
200
|
+
gate?: number | "auto";
|
|
201
|
+
enhance?: boolean;
|
|
202
|
+
depth?: number;
|
|
203
|
+
freeze?: boolean;
|
|
204
|
+
mix?: number;
|
|
205
|
+
gain?: number;
|
|
206
|
+
seed?: number;
|
|
207
|
+
}>;
|
|
208
|
+
|
|
209
|
+
/** A track's stored vocoder: the modulator, a preset and overrides. */
|
|
210
|
+
export type TrackVocoder = Readonly<
|
|
211
|
+
{ src?: string; preset?: VocoderPresetName } & VocoderOverrides
|
|
212
|
+
>;
|
|
213
|
+
|
|
214
|
+
/** Everything the DSP reads, resolved: defaults <- preset <- overrides. */
|
|
215
|
+
export type ResolvedVocoder = Readonly<{
|
|
216
|
+
tap: VocoderTap;
|
|
217
|
+
mode: VocoderMode;
|
|
218
|
+
carrier: VocoderCarrier;
|
|
219
|
+
follow: VocoderFollow;
|
|
220
|
+
root: number;
|
|
221
|
+
spread: number;
|
|
222
|
+
bands: number;
|
|
223
|
+
lo: number;
|
|
224
|
+
hi: number;
|
|
225
|
+
width: number;
|
|
226
|
+
attack: number;
|
|
227
|
+
release: number;
|
|
228
|
+
formant: number;
|
|
229
|
+
unvoiced: number;
|
|
230
|
+
sens: number;
|
|
231
|
+
hiss: number;
|
|
232
|
+
gate: number | "auto";
|
|
233
|
+
enhance: boolean;
|
|
234
|
+
depth: number;
|
|
235
|
+
freeze: boolean;
|
|
236
|
+
mix: number;
|
|
237
|
+
gain: number;
|
|
238
|
+
/** Undefined: derived from the track id by the renderer. */
|
|
239
|
+
seed?: number;
|
|
240
|
+
}>;
|
|
241
|
+
|
|
242
|
+
export const VOCODER_DEFAULTS: ResolvedVocoder = Object.freeze({
|
|
243
|
+
tap: "chain",
|
|
244
|
+
mode: "channel",
|
|
245
|
+
carrier: "supersaw",
|
|
246
|
+
follow: "notes",
|
|
247
|
+
root: 45,
|
|
248
|
+
spread: 0.15,
|
|
249
|
+
bands: 16,
|
|
250
|
+
lo: 100,
|
|
251
|
+
hi: 8000,
|
|
252
|
+
width: 1,
|
|
253
|
+
attack: 0.005,
|
|
254
|
+
release: 0.04,
|
|
255
|
+
formant: 0,
|
|
256
|
+
unvoiced: 0.5,
|
|
257
|
+
sens: 0.5,
|
|
258
|
+
hiss: 0,
|
|
259
|
+
gate: "auto",
|
|
260
|
+
enhance: true,
|
|
261
|
+
depth: 1,
|
|
262
|
+
freeze: false,
|
|
263
|
+
mix: 1,
|
|
264
|
+
gain: 0,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
export type VocoderPreset = Readonly<{
|
|
268
|
+
/** One line for the menu and /help. */
|
|
269
|
+
doc: string;
|
|
270
|
+
values: VocoderOverrides;
|
|
271
|
+
}>;
|
|
272
|
+
|
|
273
|
+
/** Presets over the defaults (all at 24 bands or fewer). */
|
|
274
|
+
export const VOCODER_PRESETS: Readonly<
|
|
275
|
+
Record<VocoderPresetName, VocoderPreset>
|
|
276
|
+
> = Object.freeze({
|
|
277
|
+
classic: { doc: "70s/80s band vocoder lead", values: {} },
|
|
278
|
+
robot: {
|
|
279
|
+
doc: "monotone machine voice on a drone",
|
|
280
|
+
values: {
|
|
281
|
+
bands: 12,
|
|
282
|
+
carrier: "pulse",
|
|
283
|
+
follow: "drone",
|
|
284
|
+
root: 45,
|
|
285
|
+
release: 0.03,
|
|
286
|
+
unvoiced: 0.3,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
talkbox: {
|
|
290
|
+
doc: "wet mouth-shaped lead, sharp vowels",
|
|
291
|
+
values: { mode: "talkbox", carrier: "saw", unvoiced: 0.3, hiss: 0.1 },
|
|
292
|
+
},
|
|
293
|
+
choir: {
|
|
294
|
+
doc: "stereo vocal-stack chord pad",
|
|
295
|
+
values: {
|
|
296
|
+
bands: 24,
|
|
297
|
+
spread: 0.25,
|
|
298
|
+
follow: "chords",
|
|
299
|
+
release: 0.08,
|
|
300
|
+
width: 1.3,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
glass: {
|
|
304
|
+
doc: "bright crystalline lead",
|
|
305
|
+
values: {
|
|
306
|
+
bands: 24,
|
|
307
|
+
carrier: "saw",
|
|
308
|
+
formant: 3,
|
|
309
|
+
hiss: 0.3,
|
|
310
|
+
attack: 0.001,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
whisper: {
|
|
314
|
+
doc: "breathy unpitched ghost voice",
|
|
315
|
+
values: { bands: 24, carrier: "noise", unvoiced: 0, hiss: 0.2 },
|
|
316
|
+
},
|
|
317
|
+
smear: {
|
|
318
|
+
doc: "blurred vowel wash (try freeze)",
|
|
319
|
+
values: {
|
|
320
|
+
bands: 24,
|
|
321
|
+
follow: "chords",
|
|
322
|
+
release: 0.6,
|
|
323
|
+
attack: 0.02,
|
|
324
|
+
width: 1.5,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
lofi: {
|
|
328
|
+
doc: "narrow murky 8-band",
|
|
329
|
+
values: {
|
|
330
|
+
bands: 8,
|
|
331
|
+
carrier: "pulse",
|
|
332
|
+
formant: -3,
|
|
333
|
+
width: 0.7,
|
|
334
|
+
hi: 4000,
|
|
335
|
+
// The narrow layout loses ~5 dB the fixed makeup does not restore.
|
|
336
|
+
gain: 5,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/** Parameters with a `vocoder-<param>` lane (freeze is a 0/1 step lane). */
|
|
342
|
+
export const VOCODER_LANE_PARAMS: readonly Readonly<{
|
|
343
|
+
param: string;
|
|
344
|
+
spec: NumberParam;
|
|
345
|
+
}>[] = Object.freeze([
|
|
346
|
+
...Object.entries(VOCODER_PARAMS)
|
|
347
|
+
.filter(
|
|
348
|
+
(entry): entry is [string, NumberParam] =>
|
|
349
|
+
entry[1].kind === "number" && entry[1].automate === true,
|
|
350
|
+
)
|
|
351
|
+
.map(([param, spec]) => Object.freeze({ param, spec })),
|
|
352
|
+
Object.freeze({
|
|
353
|
+
param: "freeze",
|
|
354
|
+
spec: num(0, 1, 0, 1, "hold the vowel while 1", { automate: true }),
|
|
355
|
+
}),
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
export function isVocoderPreset(name: string): name is VocoderPresetName {
|
|
359
|
+
return (VOCODER_PRESET_NAMES as readonly string[]).includes(name);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function isVocoderParam(name: string): boolean {
|
|
363
|
+
return Object.prototype.hasOwnProperty.call(VOCODER_PARAMS, name);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Strudel spelling (`att`, `rel`) to a vocoder parameter name. */
|
|
367
|
+
export function vocoderParamName(name: string): string | undefined {
|
|
368
|
+
if (isVocoderParam(name)) return name;
|
|
369
|
+
for (const [key, spec] of Object.entries(VOCODER_PARAMS))
|
|
370
|
+
if (spec.strudel?.includes(name)) return key;
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Validates a `vocoder` field: `{ src?, preset?, ...overrides }`, null or
|
|
376
|
+
* absent meaning none. Keys come out in canonical order (src, preset, then
|
|
377
|
+
* `VOCODER_PARAMS` order). `{}` is kept: it means the default preset with no
|
|
378
|
+
* modulator (the track plays dry). Whether `src` names another track is the
|
|
379
|
+
* score's check.
|
|
380
|
+
*/
|
|
381
|
+
export function normalizeVocoder(input: unknown): TrackVocoder | undefined {
|
|
382
|
+
if (input === undefined || input === null) return undefined;
|
|
383
|
+
if (!isRecord(input))
|
|
384
|
+
throw new FxValidationError("track vocoder must be an object or null");
|
|
385
|
+
const params: Record<string, unknown> = {};
|
|
386
|
+
let presetName: VocoderPresetName | undefined;
|
|
387
|
+
let src: string | undefined;
|
|
388
|
+
let gate: number | "auto" | undefined;
|
|
389
|
+
for (const [key, value] of Object.entries(input)) {
|
|
390
|
+
if (value === undefined || value === null) continue;
|
|
391
|
+
if (key === "src") {
|
|
392
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 64)
|
|
393
|
+
throw new FxValidationError(
|
|
394
|
+
"vocoder src must be a track id (1 to 64 characters)",
|
|
395
|
+
);
|
|
396
|
+
src = value;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (key === "preset") {
|
|
400
|
+
const lower = typeof value === "string" ? value.toLowerCase() : "";
|
|
401
|
+
if (!isVocoderPreset(lower))
|
|
402
|
+
throw new FxValidationError(
|
|
403
|
+
`vocoder preset must be one of ${VOCODER_PRESET_NAMES.join(", ")}`,
|
|
404
|
+
);
|
|
405
|
+
presetName = lower;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const name = vocoderParamName(key);
|
|
409
|
+
if (!name)
|
|
410
|
+
throw new FxValidationError(
|
|
411
|
+
`vocoder has no parameter "${key.slice(0, 32)}"`,
|
|
412
|
+
);
|
|
413
|
+
if (name === "gate" && value === VOCODER_GATE_AUTO) {
|
|
414
|
+
gate = VOCODER_GATE_AUTO;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
params[name] = value;
|
|
418
|
+
}
|
|
419
|
+
const values = normalizeParams(VOCODER_PARAMS, params, "vocoder", false);
|
|
420
|
+
const out: Record<string, unknown> = {};
|
|
421
|
+
if (src !== undefined) out.src = src;
|
|
422
|
+
if (presetName) out.preset = presetName;
|
|
423
|
+
for (const key of Object.keys(VOCODER_PARAMS)) {
|
|
424
|
+
if (key === "gate" && gate !== undefined) out.gate = gate;
|
|
425
|
+
else if (values[key] !== undefined) out[key] = values[key];
|
|
426
|
+
}
|
|
427
|
+
return Object.freeze(out) as TrackVocoder;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** The preset a `vocoder` field plays. */
|
|
431
|
+
export function vocoderPresetOf(
|
|
432
|
+
vocoder: TrackVocoder | undefined,
|
|
433
|
+
): VocoderPresetName {
|
|
434
|
+
return vocoder?.preset ?? DEFAULT_VOCODER_PRESET;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The settings a `vocoder` field plays: defaults, then the preset, then
|
|
439
|
+
* overrides, then lane values from `lane`. A talkbox's formant is clamped
|
|
440
|
+
* to ±12 st.
|
|
441
|
+
*/
|
|
442
|
+
export function resolveVocoder(
|
|
443
|
+
vocoder: TrackVocoder | undefined,
|
|
444
|
+
lane?: (param: string) => number | undefined,
|
|
445
|
+
): ResolvedVocoder {
|
|
446
|
+
const out: Record<string, unknown> = {
|
|
447
|
+
...VOCODER_DEFAULTS,
|
|
448
|
+
...VOCODER_PRESETS[vocoderPresetOf(vocoder)].values,
|
|
449
|
+
};
|
|
450
|
+
if (vocoder)
|
|
451
|
+
for (const [key, value] of Object.entries(vocoder))
|
|
452
|
+
if (key !== "preset" && key !== "src" && value !== undefined)
|
|
453
|
+
out[key] = value;
|
|
454
|
+
if (lane)
|
|
455
|
+
for (const { param } of VOCODER_LANE_PARAMS) {
|
|
456
|
+
const value = lane(param);
|
|
457
|
+
if (value === undefined) continue;
|
|
458
|
+
out[param] = param === "freeze" ? value >= 0.5 : value;
|
|
459
|
+
}
|
|
460
|
+
if (out.mode === "talkbox")
|
|
461
|
+
out.formant = Math.max(
|
|
462
|
+
-TALKBOX_FORMANT_MAX,
|
|
463
|
+
Math.min(TALKBOX_FORMANT_MAX, out.formant as number),
|
|
464
|
+
);
|
|
465
|
+
return Object.freeze(out) as ResolvedVocoder;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The value one key plays (for menu rows and `/vocoder <param>`). */
|
|
469
|
+
export function vocoderValue(
|
|
470
|
+
vocoder: TrackVocoder | undefined,
|
|
471
|
+
key: string,
|
|
472
|
+
): number | string | boolean | undefined {
|
|
473
|
+
return (resolveVocoder(vocoder) as Record<string, unknown>)[key] as
|
|
474
|
+
number | string | boolean | undefined;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Ring after the modulator stops: the release plus 50 ms, at most 2.05 s. */
|
|
478
|
+
export function vocoderTailSeconds(vocoder: TrackVocoder | undefined): number {
|
|
479
|
+
if (!vocoder) return 0;
|
|
480
|
+
return Math.min(2.05, resolveVocoder(vocoder).release + 0.05);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Pre-roll a window needs so its envelopes have settled. */
|
|
484
|
+
export function vocoderPrerollSeconds(
|
|
485
|
+
vocoder: TrackVocoder | undefined,
|
|
486
|
+
): number {
|
|
487
|
+
if (!vocoder) return 0;
|
|
488
|
+
return Math.max(0.5, 5 * resolveVocoder(vocoder).release + 0.1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Score-level checks over a track list: `src` never names its own track,
|
|
493
|
+
* at most `maxTracks` vocoder tracks, and no modulator cycle (A vocodes B
|
|
494
|
+
* while B vocodes A). Returns an error message or undefined. `src` naming
|
|
495
|
+
* no track is left to the edit surfaces, because a preview or live score
|
|
496
|
+
* holds only some of the song's tracks.
|
|
497
|
+
*/
|
|
498
|
+
export function vocoderScoreError(
|
|
499
|
+
tracks: readonly Readonly<{ id: string; vocoder?: TrackVocoder }>[],
|
|
500
|
+
maxTracks: number,
|
|
501
|
+
): string | undefined {
|
|
502
|
+
const vocoded = tracks.filter((track) => track.vocoder);
|
|
503
|
+
if (vocoded.length > maxTracks)
|
|
504
|
+
return `a song holds at most ${maxTracks} vocoder tracks`;
|
|
505
|
+
const src = new Map<string, string>();
|
|
506
|
+
for (const track of vocoded) {
|
|
507
|
+
const from = track.vocoder!.src;
|
|
508
|
+
if (from === undefined) continue;
|
|
509
|
+
if (from === track.id)
|
|
510
|
+
return `track ${track.id} cannot vocode itself (vocoder src)`;
|
|
511
|
+
src.set(track.id, from);
|
|
512
|
+
}
|
|
513
|
+
for (const start of src.keys()) {
|
|
514
|
+
const path = [start];
|
|
515
|
+
let at = src.get(start);
|
|
516
|
+
while (at !== undefined) {
|
|
517
|
+
if (at === start) return `vocoder cycle: ${[...path, at].join(" -> ")}`;
|
|
518
|
+
if (path.includes(at)) break;
|
|
519
|
+
path.push(at);
|
|
520
|
+
at = src.get(at);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|