@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
package/src/auth/tui.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function tuiAuthCommand(
|
|
|
51
51
|
else lines.push(`unknown auth command: ${(name ?? "").slice(0, 20)}`);
|
|
52
52
|
} catch (error) {
|
|
53
53
|
lines.push(
|
|
54
|
-
`auth failed · ${error instanceof Error ? error.message : String(error)} · dawg
|
|
54
|
+
`auth failed · ${error instanceof Error ? error.message : String(error)} · dawg model key in a shell`,
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
return lines.map((line) => line.slice(0, 240));
|
|
@@ -86,22 +86,31 @@ export async function modelPickerItems(
|
|
|
86
86
|
}));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* `/model <value>`: save for the active provider; returns the receipt and
|
|
91
|
+
* whether it worked, so the caller shows a refusal as one (never a ✓).
|
|
92
|
+
*/
|
|
90
93
|
export async function tuiSetModel(
|
|
91
94
|
value: string,
|
|
92
95
|
runner: CommandRunner = systemRunner,
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
auth = defaultAuthEnv(runner),
|
|
97
|
+
): Promise<{ readonly ok: boolean; readonly text: string }> {
|
|
95
98
|
const selection = await selectProvider(auth);
|
|
96
99
|
if (selection.kind === "offline")
|
|
97
|
-
return `model · unavailable: ${selection.reason}
|
|
100
|
+
return { ok: false, text: `model · unavailable: ${selection.reason}` };
|
|
98
101
|
try {
|
|
99
|
-
return
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
text: `model · ${await saveModelChoice(auth, selection, value)}`,
|
|
105
|
+
};
|
|
100
106
|
} catch (error) {
|
|
101
107
|
const rows = await modelPickerItems(selection, runner).catch(() => []);
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
text: `model · ${error instanceof Error ? error.message : String(error)}; try ${rows
|
|
111
|
+
.slice(0, 8)
|
|
112
|
+
.map((row) => row.label)
|
|
113
|
+
.join(", ")} · current ${providerLabel(selection)}`,
|
|
114
|
+
};
|
|
106
115
|
}
|
|
107
116
|
}
|
package/src/commands/arrange.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type Section,
|
|
31
31
|
type TrackScore,
|
|
32
32
|
} from "../../core/score.ts";
|
|
33
|
+
import { nearest } from "./nearest.ts";
|
|
33
34
|
import {
|
|
34
35
|
addSection,
|
|
35
36
|
arrangedBars,
|
|
@@ -240,41 +241,17 @@ function unknownSection(
|
|
|
240
241
|
return { type: "section-unknown", sub, name };
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
function editDistance(a: string, b: string): number {
|
|
244
|
-
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
245
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
246
|
-
let previous = row[0]!;
|
|
247
|
-
row[0] = i;
|
|
248
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
249
|
-
const current = row[j]!;
|
|
250
|
-
row[j] = Math.min(
|
|
251
|
-
row[j]! + 1,
|
|
252
|
-
row[j - 1]! + 1,
|
|
253
|
-
previous + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
254
|
-
);
|
|
255
|
-
previous = current;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return row[b.length]!;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
244
|
/** `no section named chrous (sections: …) · did you mean chorus?` */
|
|
262
245
|
function unknownSectionMessage(score: TrackScore, name: string): string {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!best || distance < best.distance)
|
|
268
|
-
best = { name: section.name, distance };
|
|
269
|
-
}
|
|
246
|
+
const match = nearest(
|
|
247
|
+
name,
|
|
248
|
+
score.sections.map((section) => section.name),
|
|
249
|
+
);
|
|
270
250
|
const list =
|
|
271
251
|
score.sections.length > 0
|
|
272
252
|
? ` (sections: ${score.sections.map((s) => s.name).join(", ")})`
|
|
273
253
|
: " (no sections yet: section verse 1-8 marks one)";
|
|
274
|
-
const near =
|
|
275
|
-
best && best.distance <= Math.max(2, Math.floor(wanted.length / 3))
|
|
276
|
-
? ` · did you mean ${best.name}?`
|
|
277
|
-
: "";
|
|
254
|
+
const near = match ? ` · did you mean ${match}?` : "";
|
|
278
255
|
return `section · no section named ${name}${list}${near}`;
|
|
279
256
|
}
|
|
280
257
|
|
|
@@ -947,3 +924,41 @@ function describeVary(vary: { transpose?: number; gain?: number }): string {
|
|
|
947
924
|
if (vary.gain !== undefined) bits.push(`gain ${vary.gain}`);
|
|
948
925
|
return bits.join(" ");
|
|
949
926
|
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* `loop 2-3` when no section spans those bars: mark the section named
|
|
930
|
+
* `loop` over them (moving it if it exists) and loop it, as one next score.
|
|
931
|
+
*/
|
|
932
|
+
export function loopSpan(
|
|
933
|
+
score: TrackScore,
|
|
934
|
+
trackId: string,
|
|
935
|
+
from: number,
|
|
936
|
+
to: number,
|
|
937
|
+
):
|
|
938
|
+
| Readonly<{ ok: true; next: TrackScore; name: string; message: string }>
|
|
939
|
+
| Readonly<{ ok: false; message: string }> {
|
|
940
|
+
const bars = from === to ? `${from}` : `${from}-${to}`;
|
|
941
|
+
const name = "loop";
|
|
942
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 1 || to < from)
|
|
943
|
+
return { ok: false, message: `loop ${bars} · bars run low-high from 1` };
|
|
944
|
+
const marked = applySectionCommand(score, trackId, {
|
|
945
|
+
type: "section-mark",
|
|
946
|
+
name,
|
|
947
|
+
startBar: from - 1,
|
|
948
|
+
bars: to - from + 1,
|
|
949
|
+
});
|
|
950
|
+
if (!marked.ok || !marked.next)
|
|
951
|
+
return { ok: false, message: `loop ${bars} · ${marked.message}` };
|
|
952
|
+
const looped = applySectionCommand(marked.next, trackId, {
|
|
953
|
+
type: "section-loop",
|
|
954
|
+
name,
|
|
955
|
+
});
|
|
956
|
+
if (!looped.ok || !looped.next)
|
|
957
|
+
return { ok: false, message: `loop ${bars} · ${looped.message}` };
|
|
958
|
+
return {
|
|
959
|
+
ok: true,
|
|
960
|
+
next: looped.next,
|
|
961
|
+
name,
|
|
962
|
+
message: `loop · bars ${from === to ? from : `${from}–${to}`} · section ${name} · loop off plays the song`,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `autotune` prompt command (0.7): pitch correction on the focused
|
|
3
|
+
* track's clips and sampler voices (core/autotune.ts). `/vocal autotune`
|
|
4
|
+
* is the same command; the menu and the `autotune_vocal` agent tool build
|
|
5
|
+
* the same edits through `nextAutotune`.
|
|
6
|
+
*
|
|
7
|
+
* autotune pop on the focused track (or show it)
|
|
8
|
+
* autotune <preset> hard robot warble trap pop natural
|
|
9
|
+
* gentle guided locked
|
|
10
|
+
* autotune <preset> <field> <v>… a preset with overrides
|
|
11
|
+
* autotune speed 35 relax 0.4 fields (base pop)
|
|
12
|
+
* autotune to chord follow the chords
|
|
13
|
+
* autotune to notes melody note-guided from another track
|
|
14
|
+
* autotune key D bayati a key for the scale target
|
|
15
|
+
* autotune voice alto the tracking range
|
|
16
|
+
* autotune <field> off back to the preset's value
|
|
17
|
+
* autotune reset keep only the preset
|
|
18
|
+
* autotune off remove autotune
|
|
19
|
+
* autotune presets the presets with one-line characters
|
|
20
|
+
*
|
|
21
|
+
* `/tune` stays the alias of `/tuning`: `/tune hard` explains the
|
|
22
|
+
* difference and points here. One `updateTrack` revision per command.
|
|
23
|
+
*/
|
|
24
|
+
import {
|
|
25
|
+
AUTOTUNE_FIELDS,
|
|
26
|
+
AUTOTUNE_PARAMS,
|
|
27
|
+
AUTOTUNE_PRESET_HELP,
|
|
28
|
+
AUTOTUNE_PRESETS,
|
|
29
|
+
AUTOTUNE_TARGETS,
|
|
30
|
+
AUTOTUNE_VOICES,
|
|
31
|
+
describeAutotune,
|
|
32
|
+
isAutotunePreset,
|
|
33
|
+
normalizeAutotune,
|
|
34
|
+
resolveAutotune,
|
|
35
|
+
type AutotunePreset,
|
|
36
|
+
type TrackAutotune,
|
|
37
|
+
} from "../../core/autotune.ts";
|
|
38
|
+
import { parseKey } from "../../core/chords.ts";
|
|
39
|
+
import { FxValidationError } from "../../core/params.ts";
|
|
40
|
+
import {
|
|
41
|
+
ScoreValidationError,
|
|
42
|
+
updateTrack,
|
|
43
|
+
type TrackScore,
|
|
44
|
+
} from "../../core/score.ts";
|
|
45
|
+
import { autotuneEngineNote, hasGuideNotes } from "../audio/autotune.ts";
|
|
46
|
+
import { nearest } from "./nearest.ts";
|
|
47
|
+
|
|
48
|
+
/** A field value from a command: `null` returns it to the preset's value. */
|
|
49
|
+
export type AutotuneValue = number | string | null;
|
|
50
|
+
|
|
51
|
+
export type AutotuneCommand =
|
|
52
|
+
| { type: "autotune-show" }
|
|
53
|
+
| { type: "autotune-list" }
|
|
54
|
+
| { type: "autotune-reset" }
|
|
55
|
+
| { type: "autotune-off" }
|
|
56
|
+
| { type: "autotune-usage"; message: string }
|
|
57
|
+
| {
|
|
58
|
+
type: "autotune-set";
|
|
59
|
+
preset?: AutotunePreset;
|
|
60
|
+
values: Readonly<Partial<Record<keyof TrackAutotune, AutotuneValue>>>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const AUTOTUNE_USAGE =
|
|
64
|
+
"autotune [preset] [field value …] | autotune to scale|chromatic|chord|notes [track] | autotune key <key> | autotune off | autotune presets";
|
|
65
|
+
|
|
66
|
+
/** Fields the command takes, in type order (`preset` is a leading word). */
|
|
67
|
+
const FIELD_WORDS = AUTOTUNE_FIELDS.filter((field) => field !== "preset");
|
|
68
|
+
|
|
69
|
+
/** Short spellings people type. */
|
|
70
|
+
const FIELD_ALIASES: Readonly<Record<string, keyof TrackAutotune>> = {
|
|
71
|
+
target: "to",
|
|
72
|
+
rate: "vib",
|
|
73
|
+
depth: "vibmod",
|
|
74
|
+
mix: "amount",
|
|
75
|
+
retune: "speed",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function fieldName(word: string): keyof TrackAutotune | undefined {
|
|
79
|
+
if ((FIELD_WORDS as readonly string[]).includes(word))
|
|
80
|
+
return word as keyof TrackAutotune;
|
|
81
|
+
return FIELD_ALIASES[word];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rangeOf(name: keyof TrackAutotune): string {
|
|
85
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === name);
|
|
86
|
+
if (name === "glide" && spec)
|
|
87
|
+
return `glide is 0..${spec.max * 1000} ms (glide 40 or 40ms)`;
|
|
88
|
+
if (spec)
|
|
89
|
+
return `${name} is ${spec.min}..${spec.max}${spec.unit ? ` ${spec.unit}` : ""}`;
|
|
90
|
+
if (name === "to") return `to is one of ${AUTOTUNE_TARGETS.join(" ")}`;
|
|
91
|
+
if (name === "voice") return `voice is one of ${AUTOTUNE_VOICES.join(" ")}`;
|
|
92
|
+
return `${name} needs a value`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const OFF = /^(off|unset|default)$/;
|
|
96
|
+
|
|
97
|
+
/** Fields in time units: speed and hold store ms, glide stores seconds. */
|
|
98
|
+
const TIME_FIELDS: Readonly<Partial<Record<keyof TrackAutotune, number>>> = {
|
|
99
|
+
speed: 1,
|
|
100
|
+
hold: 1,
|
|
101
|
+
glide: 0.001,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Suffixes each numeric field takes besides a bare number. */
|
|
105
|
+
const UNIT_SUFFIX: Readonly<Partial<Record<keyof TrackAutotune, RegExp>>> = {
|
|
106
|
+
vib: /^hz$/,
|
|
107
|
+
vibmod: /^st$/,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A field's number, or the message why not. Time fields read like /glide:
|
|
112
|
+
* a bare number is ms, `ms` and `s` convert (`glide 40`, `speed 0.2s`),
|
|
113
|
+
* and a bare fraction of a millisecond is a unit slip. `%` scales 0..1
|
|
114
|
+
* fields. A suffix that does not fit the field is refused.
|
|
115
|
+
*/
|
|
116
|
+
function fieldNumber(
|
|
117
|
+
name: keyof TrackAutotune,
|
|
118
|
+
value: string,
|
|
119
|
+
): number | string {
|
|
120
|
+
const match = /^(-?(?:\d+\.?\d*|\.\d+))([a-z%]*)$/.exec(value);
|
|
121
|
+
if (!match) return rangeOf(name);
|
|
122
|
+
const number = Number(match[1]);
|
|
123
|
+
const suffix = match[2]!;
|
|
124
|
+
const toUnit = TIME_FIELDS[name];
|
|
125
|
+
if (toUnit !== undefined) {
|
|
126
|
+
if (suffix === "ms") return number * toUnit;
|
|
127
|
+
if (suffix === "s") return number * 1000 * toUnit;
|
|
128
|
+
if (suffix !== "")
|
|
129
|
+
return `${name} takes ms or s, not ${suffix.slice(0, 4)}`;
|
|
130
|
+
if (number > 0 && number < 1)
|
|
131
|
+
return `${name} · a bare number is ms · write ${name} ${Math.round(number * 1000)}ms or ${name} ${match[1]}s`;
|
|
132
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === name);
|
|
133
|
+
if (spec && (number * toUnit < spec.min || number * toUnit > spec.max))
|
|
134
|
+
return rangeOf(name);
|
|
135
|
+
return number * toUnit;
|
|
136
|
+
}
|
|
137
|
+
if (suffix === "") return number;
|
|
138
|
+
if (suffix === "%") {
|
|
139
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === name);
|
|
140
|
+
if (spec && spec.max === 1) return number / 100;
|
|
141
|
+
if (spec && spec.max === 100) return number;
|
|
142
|
+
}
|
|
143
|
+
if (UNIT_SUFFIX[name]?.test(suffix)) return number;
|
|
144
|
+
return `${name} takes no ${suffix.slice(0, 4)} · ${rangeOf(name)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Parses `/autotune …`, `/vocal autotune …` args, and `/tune <preset>`. */
|
|
148
|
+
export function parseAutotuneCommand(
|
|
149
|
+
prompt: string,
|
|
150
|
+
): AutotuneCommand | undefined {
|
|
151
|
+
if (prompt.length > 512) return undefined;
|
|
152
|
+
const raw = prompt.trim().replace(/^\//, "").split(/\s+/);
|
|
153
|
+
const verb = raw[0]?.toLowerCase();
|
|
154
|
+
// `/tune hard`: `/tune` is `/tuning`; an autotune word gets a pointer.
|
|
155
|
+
if (verb === "tune") {
|
|
156
|
+
const next = raw[1]?.toLowerCase();
|
|
157
|
+
return raw.length >= 2 && (isAutotunePreset(next) || next === "autotune")
|
|
158
|
+
? {
|
|
159
|
+
type: "autotune-usage",
|
|
160
|
+
message:
|
|
161
|
+
`/tune sets the tuning (edo, ratios, scl) · for pitch correction use /autotune ${isAutotunePreset(next) ? next : ""}`.trimEnd(),
|
|
162
|
+
}
|
|
163
|
+
: undefined;
|
|
164
|
+
}
|
|
165
|
+
if (verb !== "autotune") return undefined;
|
|
166
|
+
return parseAutotuneArgs(raw.slice(1));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The words after `autotune` (also `/vocal autotune <args>`). */
|
|
170
|
+
export function parseAutotuneArgs(raw: readonly string[]): AutotuneCommand {
|
|
171
|
+
const words = raw.filter((word) => word.length > 0);
|
|
172
|
+
const lower = words.map((word) => word.toLowerCase());
|
|
173
|
+
if (lower.length === 0) return { type: "autotune-show" };
|
|
174
|
+
if (lower.length === 1) {
|
|
175
|
+
const only = lower[0]!;
|
|
176
|
+
if (only === "off" || only === "none" || only === "remove")
|
|
177
|
+
return { type: "autotune-off" };
|
|
178
|
+
if (only === "reset") return { type: "autotune-reset" };
|
|
179
|
+
if (only === "list" || only === "presets") return { type: "autotune-list" };
|
|
180
|
+
if (only === "on") return { type: "autotune-set", values: {} };
|
|
181
|
+
}
|
|
182
|
+
let index = 0;
|
|
183
|
+
let preset: AutotunePreset | undefined;
|
|
184
|
+
if (lower[0] === "preset") {
|
|
185
|
+
if (!isAutotunePreset(lower[1]))
|
|
186
|
+
return {
|
|
187
|
+
type: "autotune-usage",
|
|
188
|
+
message: `preset is one of ${AUTOTUNE_PRESETS.join(" ")}`,
|
|
189
|
+
};
|
|
190
|
+
preset = lower[1];
|
|
191
|
+
index = 2;
|
|
192
|
+
} else if (isAutotunePreset(lower[0])) {
|
|
193
|
+
preset = lower[0];
|
|
194
|
+
index = 1;
|
|
195
|
+
}
|
|
196
|
+
const values: Partial<Record<keyof TrackAutotune, AutotuneValue>> = {};
|
|
197
|
+
while (index < lower.length) {
|
|
198
|
+
const word = lower[index]!;
|
|
199
|
+
const name = fieldName(word);
|
|
200
|
+
if (!name && isAutotunePreset(word))
|
|
201
|
+
return {
|
|
202
|
+
type: "autotune-usage",
|
|
203
|
+
message: preset
|
|
204
|
+
? `one preset at a time (${preset} or ${word})`
|
|
205
|
+
: `the preset comes first · autotune ${word} …`,
|
|
206
|
+
};
|
|
207
|
+
if (!name) {
|
|
208
|
+
const near = nearest(word.slice(0, 24), [
|
|
209
|
+
...FIELD_WORDS,
|
|
210
|
+
...AUTOTUNE_PRESETS,
|
|
211
|
+
]);
|
|
212
|
+
return {
|
|
213
|
+
type: "autotune-usage",
|
|
214
|
+
message: `no field or preset ${word.slice(0, 24)}${near ? ` · did you mean ${near}?` : ""} · ${AUTOTUNE_USAGE}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
index += 1;
|
|
218
|
+
const value = lower[index];
|
|
219
|
+
if (value === undefined)
|
|
220
|
+
return { type: "autotune-usage", message: rangeOf(name) };
|
|
221
|
+
if (OFF.test(value)) {
|
|
222
|
+
values[name] = null;
|
|
223
|
+
index += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (name === "to") {
|
|
227
|
+
const target = value === "melody" || value === "note" ? "notes" : value;
|
|
228
|
+
if (!(AUTOTUNE_TARGETS as readonly string[]).includes(target))
|
|
229
|
+
return { type: "autotune-usage", message: rangeOf(name) };
|
|
230
|
+
values.to = target;
|
|
231
|
+
index += 1;
|
|
232
|
+
// `to notes melody`: a track id (anything that is not a field word).
|
|
233
|
+
const from = words[index];
|
|
234
|
+
if (
|
|
235
|
+
target === "notes" &&
|
|
236
|
+
from !== undefined &&
|
|
237
|
+
!fieldName(from.toLowerCase())
|
|
238
|
+
) {
|
|
239
|
+
values.from = from;
|
|
240
|
+
index += 1;
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (name === "from") {
|
|
245
|
+
values.from = words[index]!;
|
|
246
|
+
index += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (name === "voice") {
|
|
250
|
+
if (!(AUTOTUNE_VOICES as readonly string[]).includes(value))
|
|
251
|
+
return { type: "autotune-usage", message: rangeOf(name) };
|
|
252
|
+
values.voice = value;
|
|
253
|
+
index += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (name === "key") {
|
|
257
|
+
// `key D bayati`, `key A minor`: words up to the next field word.
|
|
258
|
+
const parts: string[] = [];
|
|
259
|
+
while (index < words.length && !fieldName(lower[index]!)) {
|
|
260
|
+
parts.push(words[index]!);
|
|
261
|
+
index += 1;
|
|
262
|
+
}
|
|
263
|
+
const text = parts.join(" ");
|
|
264
|
+
if (!parseKey(text))
|
|
265
|
+
return {
|
|
266
|
+
type: "autotune-usage",
|
|
267
|
+
message: `key ${text.slice(0, 32)} is not a key (A minor, D bayati, C# major)`,
|
|
268
|
+
};
|
|
269
|
+
values.key = text;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const number = fieldNumber(name, value);
|
|
273
|
+
if (typeof number === "string")
|
|
274
|
+
return { type: "autotune-usage", message: number };
|
|
275
|
+
values[name] = number;
|
|
276
|
+
index += 1;
|
|
277
|
+
}
|
|
278
|
+
return preset
|
|
279
|
+
? { type: "autotune-set", preset, values }
|
|
280
|
+
: { type: "autotune-set", values };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** One line per preset, for `autotune presets` and the agent brief. */
|
|
284
|
+
export function autotuneListLines(): string[] {
|
|
285
|
+
return AUTOTUNE_PRESETS.map(
|
|
286
|
+
(name) => `${name.padEnd(8)} ${AUTOTUNE_PRESET_HELP[name]}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The autotune field a command leaves on a track (pure; the menu and the
|
|
292
|
+
* agent tool share it). A preset switch keeps the overrides; `reset`
|
|
293
|
+
* keeps only the preset; a bare `autotune` on a track without it is pop.
|
|
294
|
+
*/
|
|
295
|
+
export function nextAutotune(
|
|
296
|
+
current: TrackAutotune | undefined,
|
|
297
|
+
command: Extract<
|
|
298
|
+
AutotuneCommand,
|
|
299
|
+
{ type: "autotune-reset" | "autotune-set" }
|
|
300
|
+
>,
|
|
301
|
+
instrument?: string,
|
|
302
|
+
): TrackAutotune {
|
|
303
|
+
if (command.type === "autotune-reset")
|
|
304
|
+
return normalizeAutotune({ preset: current?.preset ?? "pop" }, instrument)!;
|
|
305
|
+
const base: Record<string, unknown> = { ...current };
|
|
306
|
+
if (command.preset) base.preset = command.preset;
|
|
307
|
+
for (const [key, value] of Object.entries(command.values)) {
|
|
308
|
+
if (value === null) delete base[key];
|
|
309
|
+
else base[key] = value;
|
|
310
|
+
}
|
|
311
|
+
// A target other than notes drops a stale `from`.
|
|
312
|
+
if (base.to !== undefined && base.to !== "notes") delete base.from;
|
|
313
|
+
if (Object.keys(base).length === 0) base.preset = "pop";
|
|
314
|
+
return normalizeAutotune(base, instrument)!;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `· no guide notes · try autotune guided from lead`. */
|
|
318
|
+
function noGuideHint(
|
|
319
|
+
score: TrackScore,
|
|
320
|
+
trackId: string,
|
|
321
|
+
preset: string | undefined,
|
|
322
|
+
): string {
|
|
323
|
+
const melodic = score.tracks
|
|
324
|
+
.filter(
|
|
325
|
+
(other) =>
|
|
326
|
+
other.id !== trackId &&
|
|
327
|
+
other.instrument !== "kit" &&
|
|
328
|
+
other.kit === undefined &&
|
|
329
|
+
hasGuideNotes(score, other.id),
|
|
330
|
+
)
|
|
331
|
+
.map((other) => other.id)
|
|
332
|
+
.slice(0, 4);
|
|
333
|
+
const word = preset ?? "guided";
|
|
334
|
+
return melodic.length > 0
|
|
335
|
+
? ` · no guide notes on ${trackId} · try autotune ${word} from ${melodic[0]}${melodic.length > 1 ? ` (or ${melodic.slice(1).join(", ")})` : ""}`
|
|
336
|
+
: ` · no guide notes on ${trackId} · write notes on it or autotune ${word} from <track>`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export type AutotuneResult = Readonly<{
|
|
340
|
+
ok: boolean;
|
|
341
|
+
message: string;
|
|
342
|
+
next?: TrackScore;
|
|
343
|
+
kind?: string;
|
|
344
|
+
payload?: Record<string, unknown>;
|
|
345
|
+
}>;
|
|
346
|
+
|
|
347
|
+
export function applyAutotuneCommand(
|
|
348
|
+
score: TrackScore,
|
|
349
|
+
trackId: string,
|
|
350
|
+
command: AutotuneCommand,
|
|
351
|
+
): AutotuneResult {
|
|
352
|
+
if (command.type === "autotune-usage")
|
|
353
|
+
return { ok: false, message: `autotune · ${command.message}` };
|
|
354
|
+
if (command.type === "autotune-list")
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
message: `autotune presets · ${AUTOTUNE_PRESETS.join(" ")}`,
|
|
358
|
+
};
|
|
359
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
360
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
361
|
+
const current = track.autotune;
|
|
362
|
+
if (command.type === "autotune-show" && current)
|
|
363
|
+
return { ok: true, message: `autotune · ${describeAutotune(current)}` };
|
|
364
|
+
if (command.type === "autotune-off") {
|
|
365
|
+
if (!current) return { ok: true, message: "autotune · already off" };
|
|
366
|
+
const next = updateTrack(score, trackId, { autotune: null });
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
message: `autotune · off · /autotune ${current.preset ?? "pop"} turns it back on`,
|
|
370
|
+
next,
|
|
371
|
+
kind: "score.autotune",
|
|
372
|
+
payload: { trackId, autotune: null },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
let autotune: TrackAutotune;
|
|
376
|
+
let next: TrackScore;
|
|
377
|
+
try {
|
|
378
|
+
autotune = nextAutotune(
|
|
379
|
+
current,
|
|
380
|
+
command.type === "autotune-show"
|
|
381
|
+
? { type: "autotune-set", values: {} }
|
|
382
|
+
: command,
|
|
383
|
+
track.instrument,
|
|
384
|
+
);
|
|
385
|
+
next = updateTrack(score, trackId, { autotune });
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (
|
|
388
|
+
error instanceof ScoreValidationError ||
|
|
389
|
+
error instanceof FxValidationError
|
|
390
|
+
)
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
message: `autotune · ${error.message.replace(/^autotune: /, "")}`,
|
|
394
|
+
};
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
const inert =
|
|
398
|
+
!track.sampler && track.clips === undefined
|
|
399
|
+
? ` · ${trackId} has no audio yet (it tunes clips and sampler voices)`
|
|
400
|
+
: "";
|
|
401
|
+
const keyHint =
|
|
402
|
+
(autotune.to ?? "scale") === "scale" &&
|
|
403
|
+
autotune.key === undefined &&
|
|
404
|
+
!score.key &&
|
|
405
|
+
!(autotune.preset === "guided" || autotune.preset === "locked")
|
|
406
|
+
? " · no song key, so chromatic (/scale or autotune key …)"
|
|
407
|
+
: "";
|
|
408
|
+
const guide =
|
|
409
|
+
resolveAutotune(autotune).to === "notes" &&
|
|
410
|
+
autotune.from === undefined &&
|
|
411
|
+
!hasGuideNotes(score, trackId)
|
|
412
|
+
? noGuideHint(score, trackId, autotune.preset)
|
|
413
|
+
: "";
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
message: `autotune · ${describeAutotune(autotune)}${keyHint}${guide}${inert}${autotuneEngineNote()}`,
|
|
417
|
+
next,
|
|
418
|
+
kind: "score.autotune",
|
|
419
|
+
payload: { trackId, autotune },
|
|
420
|
+
};
|
|
421
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/calibration [0|1|latest|off]`: the song's sound calibration revision
|
|
3
|
+
* (core/score.ts CALIBRATION_LATEST). 0 (or off) keeps the 0.4 to 0.6.1
|
|
4
|
+
* sound byte-identical; 1 renders the 0.7 hat choke, tuned toms, crash and
|
|
5
|
+
* ride, levelled keys and steady lip brass. The menu and the agent's
|
|
6
|
+
* set_calibration tool run the same command.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
applyScoreOperation,
|
|
11
|
+
CALIBRATION_LATEST,
|
|
12
|
+
type TrackScore,
|
|
13
|
+
} from "../../core/score.ts";
|
|
14
|
+
|
|
15
|
+
export type CalibrationCommand =
|
|
16
|
+
| Readonly<{ type: "calibration-show" }>
|
|
17
|
+
| Readonly<{ type: "calibration-set"; calibration: number }>
|
|
18
|
+
| Readonly<{ type: "calibration-invalid"; message: string }>;
|
|
19
|
+
|
|
20
|
+
export type CalibrationResult = Readonly<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
message: string;
|
|
23
|
+
next?: TrackScore;
|
|
24
|
+
kind?: "score.calibration";
|
|
25
|
+
payload?: Record<string, unknown>;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
const USAGE = `calibration · /calibration 0..${CALIBRATION_LATEST}|latest|off`;
|
|
29
|
+
|
|
30
|
+
export function parseCalibrationCommand(
|
|
31
|
+
text: string,
|
|
32
|
+
): CalibrationCommand | undefined {
|
|
33
|
+
const words = text.trim().replace(/^\//, "").split(/\s+/);
|
|
34
|
+
if (words[0]?.toLowerCase() !== "calibration") return undefined;
|
|
35
|
+
if (words.length === 1) return { type: "calibration-show" };
|
|
36
|
+
if (words.length > 2) return { type: "calibration-invalid", message: USAGE };
|
|
37
|
+
const word = words[1]!.toLowerCase();
|
|
38
|
+
if (word === "latest")
|
|
39
|
+
return { type: "calibration-set", calibration: CALIBRATION_LATEST };
|
|
40
|
+
if (word === "off" || word === "legacy")
|
|
41
|
+
return { type: "calibration-set", calibration: 0 };
|
|
42
|
+
if (/^\d$/.test(word) && Number(word) <= CALIBRATION_LATEST)
|
|
43
|
+
return { type: "calibration-set", calibration: Number(word) };
|
|
44
|
+
return { type: "calibration-invalid", message: USAGE };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function calibrationLabel(calibration: number | undefined): string {
|
|
48
|
+
return calibration ? String(calibration) : "0 (legacy)";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function applyCalibrationCommand(
|
|
52
|
+
score: TrackScore,
|
|
53
|
+
command: CalibrationCommand,
|
|
54
|
+
): CalibrationResult {
|
|
55
|
+
if (command.type === "calibration-invalid")
|
|
56
|
+
return { ok: false, message: command.message };
|
|
57
|
+
if (command.type === "calibration-show")
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
message: `calibration · ${calibrationLabel(score.calibration)} · latest ${CALIBRATION_LATEST}`,
|
|
61
|
+
};
|
|
62
|
+
const calibration = command.calibration || null;
|
|
63
|
+
const next = applyScoreOperation(score, {
|
|
64
|
+
type: "setCalibration",
|
|
65
|
+
calibration,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
message: `calibration · ${calibrationLabel(next.calibration)}`,
|
|
70
|
+
next,
|
|
71
|
+
kind: "score.calibration",
|
|
72
|
+
payload: { calibration },
|
|
73
|
+
};
|
|
74
|
+
}
|