@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/src/commands/drums.ts
CHANGED
|
@@ -54,12 +54,13 @@ export function parsePatternCommand(
|
|
|
54
54
|
const match = command
|
|
55
55
|
.trim()
|
|
56
56
|
.match(
|
|
57
|
-
|
|
57
|
+
/^\/?(?:patterns?|grooves?)(?:\s+([a-z0-9][\w-]{0,40}))?(?:\s+(keep-tempo|keep|tempo))?\s*$/i,
|
|
58
58
|
);
|
|
59
59
|
if (!match) return undefined;
|
|
60
60
|
const name = match[1]?.toLowerCase();
|
|
61
61
|
if (!name) return { kind: "browse" };
|
|
62
|
-
if (name === "list" || name === "ls"
|
|
62
|
+
if (name === "list" || name === "ls" || name === "presets")
|
|
63
|
+
return { kind: "list" };
|
|
63
64
|
const flag = match[2]?.toLowerCase();
|
|
64
65
|
return {
|
|
65
66
|
kind: "apply",
|
package/src/commands/edit.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* score and the session event, leaving persistence to the caller.
|
|
14
14
|
*/
|
|
15
15
|
import {
|
|
16
|
+
AUTOMATION_PARAMETERS,
|
|
16
17
|
automationPoints,
|
|
17
18
|
automationRange,
|
|
18
19
|
SCORE_LIMITS,
|
|
@@ -40,7 +41,13 @@ export function parseLane(name: string): AutomationParameter | undefined {
|
|
|
40
41
|
const lower = name.toLowerCase();
|
|
41
42
|
if (lower === "volume" || lower === "vol") return "volume";
|
|
42
43
|
if (lower === "pan") return "pan";
|
|
43
|
-
|
|
44
|
+
// Lanes are typed in any case: `synth-pitchjump` is `synth-pitchJump`.
|
|
45
|
+
return (
|
|
46
|
+
parseEffectLane(lower) ??
|
|
47
|
+
parseEffectLane(
|
|
48
|
+
AUTOMATION_PARAMETERS.find((lane) => lane.toLowerCase() === lower) ?? "",
|
|
49
|
+
)
|
|
50
|
+
);
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
@@ -69,7 +76,7 @@ export function parseEditCommand(prompt: string): EditCommand | undefined {
|
|
|
69
76
|
const parsed = parseKey(value);
|
|
70
77
|
return parsed ? { type: "key", key: keyName(parsed) } : undefined;
|
|
71
78
|
}
|
|
72
|
-
const points = text.match(/^automate ([a-
|
|
79
|
+
const points = text.match(/^automate ([a-z0-9-]+) points((?: \S+)+)$/i);
|
|
73
80
|
if (points) {
|
|
74
81
|
const parameter = parseLane(points[1]!);
|
|
75
82
|
if (!parameter) return undefined;
|
|
@@ -89,7 +96,7 @@ export function parseEditCommand(prompt: string): EditCommand | undefined {
|
|
|
89
96
|
return undefined;
|
|
90
97
|
return { type: "automation-points", parameter, points: parsed };
|
|
91
98
|
}
|
|
92
|
-
const remove = text.match(/^automate ([a-
|
|
99
|
+
const remove = text.match(/^automate ([a-z0-9-]+) remove (\d+(?:\.\d+)?)$/i);
|
|
93
100
|
if (remove) {
|
|
94
101
|
const parameter = parseLane(remove[1]!);
|
|
95
102
|
return parameter
|
|
@@ -174,7 +181,7 @@ export function applyEditCommand(
|
|
|
174
181
|
if (points.length === current.length)
|
|
175
182
|
return {
|
|
176
183
|
ok: false,
|
|
177
|
-
message: `no ${command.parameter} point at beat ${command.beat}`,
|
|
184
|
+
message: `no ${command.parameter} point at beat ${command.beat} · the ${command.parameter} lane shows them · help automate`,
|
|
178
185
|
};
|
|
179
186
|
}
|
|
180
187
|
return {
|
|
@@ -536,7 +536,7 @@ export function describeTarget(target: NoteTarget): string {
|
|
|
536
536
|
export function patchNotes(
|
|
537
537
|
score: TrackScore,
|
|
538
538
|
noteIds: ReadonlySet<string>,
|
|
539
|
-
patch: NoteExpressionPatch
|
|
539
|
+
patch: NoteExpressionPatch & Readonly<{ vowel?: string | null }>,
|
|
540
540
|
): TrackScore {
|
|
541
541
|
if (noteIds.size === 0) return score;
|
|
542
542
|
return new TrackScore({
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/formant` and `/vowel` (0.7): short forms of `fx formant` and `fx vowel`
|
|
3
|
+
* on the focused track. Both build the same `FxCommand` as `fx`, so the
|
|
4
|
+
* edit, validation and undo step are the fx command's.
|
|
5
|
+
*
|
|
6
|
+
* formant show the shift and mix
|
|
7
|
+
* formant <-12..12> [mix] shift the formants (pitch stays): -4 deeper
|
|
8
|
+
* formant <preset> deep giant bright tiny
|
|
9
|
+
* formant shift <st> mix <0..1> spelled out
|
|
10
|
+
* formant on | off | reset turn it on | remove it | back to 0 st, mix 1
|
|
11
|
+
*
|
|
12
|
+
* vowel show the vowel filter
|
|
13
|
+
* vowel <v> [<to> [<morph>]] a, or a morphing toward o (morph 0.5);
|
|
14
|
+
* keeps the mix you set
|
|
15
|
+
* vowel <preset> ee (also a, o)
|
|
16
|
+
* vowel to <v> | morph <0..1> change one part of the morph
|
|
17
|
+
* vowel to off drop the morph target (keeps vowel and mix)
|
|
18
|
+
* vowel mix <0..1> | off
|
|
19
|
+
*
|
|
20
|
+
* `/vocal formant …` runs the same `/formant` grammar.
|
|
21
|
+
*/
|
|
22
|
+
import { FX_PRESETS, VOWEL_VALUES } from "../../core/fx.ts";
|
|
23
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
24
|
+
import {
|
|
25
|
+
applyFxCommand,
|
|
26
|
+
effectValues,
|
|
27
|
+
FORMANT_VOWEL_HINT,
|
|
28
|
+
parseFxCommand,
|
|
29
|
+
type FxCommand,
|
|
30
|
+
type FxResult,
|
|
31
|
+
} from "./fx.ts";
|
|
32
|
+
|
|
33
|
+
export const FORMANT_USAGE =
|
|
34
|
+
"formant <-12..12> [mix] | <deep|giant|bright|tiny> | on | off";
|
|
35
|
+
export const VOWEL_USAGE =
|
|
36
|
+
"vowel <a|e|i|o|u…> [<to> [<morph 0..1>]] | ee | to off | off";
|
|
37
|
+
|
|
38
|
+
export type FormantCommand =
|
|
39
|
+
| Readonly<{ type: "show"; effect: "formant" | "vowel" }>
|
|
40
|
+
| Readonly<{ type: "fx"; command: FxCommand }>
|
|
41
|
+
| Readonly<{ type: "error"; message: string }>;
|
|
42
|
+
|
|
43
|
+
const isVowel = (word: string): boolean =>
|
|
44
|
+
(VOWEL_VALUES as readonly string[]).includes(word);
|
|
45
|
+
|
|
46
|
+
/** A number word such as `-3`, `+2.5` or `-4st`. */
|
|
47
|
+
function numberWord(word: string): number | undefined {
|
|
48
|
+
const match = word.match(/^([+-]?\d+(?:\.\d+)?|[+-]?\.\d+)(?:st)?$/i);
|
|
49
|
+
return match ? Number(match[1]) : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parse `/formant …` (the slash is optional only with arguments). */
|
|
53
|
+
export function parseFormantCommand(
|
|
54
|
+
command: string,
|
|
55
|
+
): FormantCommand | undefined {
|
|
56
|
+
const match = command.trim().match(/^\/formant(?:\s+(.*))?$/i);
|
|
57
|
+
if (!match) return undefined;
|
|
58
|
+
return formantArgs(match[1] ?? "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The words after `/formant` (shared with `/vocal formant`). */
|
|
62
|
+
export function formantArgs(args: string): FormantCommand {
|
|
63
|
+
const words = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
64
|
+
if (words.length === 0) return { type: "show", effect: "formant" };
|
|
65
|
+
const [first, ...rest] = words as [string, ...string[]];
|
|
66
|
+
if (rest.length === 0 && (first === "off" || first === "reset"))
|
|
67
|
+
return {
|
|
68
|
+
type: "fx",
|
|
69
|
+
command: {
|
|
70
|
+
type: first === "off" ? "fx-off" : "fx-reset",
|
|
71
|
+
effect: "formant",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
// `on` is also a nasal vowel word; as the only word it is the toggle.
|
|
75
|
+
if (rest.length === 0 && ["on", "true", "yes"].includes(first))
|
|
76
|
+
return { type: "fx", command: { type: "fx-on", effect: "formant" } };
|
|
77
|
+
if (rest.length === 0 && FX_PRESETS.formant?.[first])
|
|
78
|
+
return {
|
|
79
|
+
type: "fx",
|
|
80
|
+
command: { type: "fx-preset", effect: "formant", preset: first },
|
|
81
|
+
};
|
|
82
|
+
const shift = numberWord(first);
|
|
83
|
+
// `-4`, `-4 0.5` or `-4 mix 0.5`.
|
|
84
|
+
const mixWords = rest[0] === "mix" ? rest.slice(1) : rest;
|
|
85
|
+
if (shift !== undefined && mixWords.length <= 1 && rest.length <= 2) {
|
|
86
|
+
const mix = mixWords[0] === undefined ? undefined : numberWord(mixWords[0]);
|
|
87
|
+
if (rest.length > 0 && mix === undefined)
|
|
88
|
+
return { type: "error", message: `usage: /${FORMANT_USAGE}` };
|
|
89
|
+
return {
|
|
90
|
+
type: "fx",
|
|
91
|
+
command: {
|
|
92
|
+
type: "fx-set",
|
|
93
|
+
effect: "formant",
|
|
94
|
+
values: { shift, ...(mix === undefined ? {} : { mix }) },
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (isVowel(first) || words.includes("vowel"))
|
|
99
|
+
return {
|
|
100
|
+
type: "error",
|
|
101
|
+
message: `${FORMANT_VOWEL_HINT} · /vowel ${isVowel(first) ? first : "a"}`,
|
|
102
|
+
};
|
|
103
|
+
const generic = parseFxCommand(`fx formant ${words.join(" ")}`);
|
|
104
|
+
if (generic) return { type: "fx", command: generic };
|
|
105
|
+
return { type: "error", message: `usage: /${FORMANT_USAGE}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Parse `/vowel …`. */
|
|
109
|
+
export function parseVowelCommand(command: string): FormantCommand | undefined {
|
|
110
|
+
const match = command.trim().match(/^\/vowel(?:\s+(.*))?$/i);
|
|
111
|
+
if (!match) return undefined;
|
|
112
|
+
const words = (match[1] ?? "")
|
|
113
|
+
.trim()
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.split(/\s+/)
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
if (words.length === 0) return { type: "show", effect: "vowel" };
|
|
118
|
+
const [first, second, third] = words;
|
|
119
|
+
if (words.length === 1 && (first === "off" || first === "reset"))
|
|
120
|
+
return {
|
|
121
|
+
type: "fx",
|
|
122
|
+
command: {
|
|
123
|
+
type: first === "off" ? "fx-off" : "fx-reset",
|
|
124
|
+
effect: "vowel",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
if (words.length === 2 && first === "to" && second === "off")
|
|
128
|
+
return {
|
|
129
|
+
type: "fx",
|
|
130
|
+
command: { type: "fx-replace", effect: "vowel", values: {} },
|
|
131
|
+
};
|
|
132
|
+
if (
|
|
133
|
+
words.length === 1 &&
|
|
134
|
+
!isVowel(first!) &&
|
|
135
|
+
FX_PRESETS.vowel?.[first!] !== undefined
|
|
136
|
+
)
|
|
137
|
+
return {
|
|
138
|
+
type: "fx",
|
|
139
|
+
command: { type: "fx-preset", effect: "vowel", preset: first! },
|
|
140
|
+
};
|
|
141
|
+
if (isVowel(first!) && words.length <= 3) {
|
|
142
|
+
// `mix` is filled from the stored vowel when the command is applied.
|
|
143
|
+
if (second === undefined)
|
|
144
|
+
return {
|
|
145
|
+
type: "fx",
|
|
146
|
+
command: {
|
|
147
|
+
type: "fx-replace",
|
|
148
|
+
effect: "vowel",
|
|
149
|
+
values: { vowel: first! },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const morph = third === undefined ? 0.5 : numberWord(third);
|
|
153
|
+
if (!isVowel(second) || morph === undefined)
|
|
154
|
+
return { type: "error", message: `usage: /${VOWEL_USAGE}` };
|
|
155
|
+
return {
|
|
156
|
+
type: "fx",
|
|
157
|
+
command: {
|
|
158
|
+
type: "fx-replace",
|
|
159
|
+
effect: "vowel",
|
|
160
|
+
values: { vowel: first!, to: second, morph },
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const generic = parseFxCommand(`fx vowel ${words.join(" ")}`);
|
|
165
|
+
if (generic && generic.type === "fx-set")
|
|
166
|
+
return {
|
|
167
|
+
type: "fx",
|
|
168
|
+
// `/vowel to o` alone starts halfway so the morph is audible.
|
|
169
|
+
command:
|
|
170
|
+
generic.values.to !== undefined && generic.values.morph === undefined
|
|
171
|
+
? { ...generic, values: { morph: 0.5, ...generic.values } }
|
|
172
|
+
: generic,
|
|
173
|
+
};
|
|
174
|
+
return { type: "error", message: `usage: /${VOWEL_USAGE}` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Run a parsed `/formant` or `/vowel` on the focused track. */
|
|
178
|
+
export function applyFormantCommand(
|
|
179
|
+
score: TrackScore,
|
|
180
|
+
trackId: string,
|
|
181
|
+
command: FormantCommand,
|
|
182
|
+
): FxResult {
|
|
183
|
+
if (command.type === "error") return { ok: false, message: command.message };
|
|
184
|
+
if (command.type === "fx") {
|
|
185
|
+
// `/vowel <v> [...]` replaces the vowel and its morph but keeps the
|
|
186
|
+
// stored mix; `/vowel to off` keeps vowel and mix and drops the morph.
|
|
187
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
188
|
+
const fx = command.command;
|
|
189
|
+
if (fx.type === "fx-replace" && fx.effect === "vowel") {
|
|
190
|
+
const current = effectValues(track, "vowel");
|
|
191
|
+
const { vowel: vowelWord, ...morphPart } = fx.values;
|
|
192
|
+
if (vowelWord === undefined && !current)
|
|
193
|
+
return { ok: true, message: "vowel · off" };
|
|
194
|
+
return applyFxCommand(score, trackId, {
|
|
195
|
+
...fx,
|
|
196
|
+
values: {
|
|
197
|
+
vowel: vowelWord ?? current?.vowel ?? "a",
|
|
198
|
+
mix: current?.mix ?? 1,
|
|
199
|
+
...morphPart,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return applyFxCommand(score, trackId, fx);
|
|
204
|
+
}
|
|
205
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
206
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
207
|
+
const values = effectValues(track, command.effect);
|
|
208
|
+
if (command.effect === "formant")
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
message: values
|
|
212
|
+
? `formant · shift ${values.shift} st · mix ${values.mix}`
|
|
213
|
+
: `formant · off · /${FORMANT_USAGE}`,
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
message: values
|
|
218
|
+
? `vowel · ${values.vowel}${values.to !== undefined ? ` → ${values.to} · morph ${values.morph ?? 0}` : ""} · mix ${values.mix}`
|
|
219
|
+
: `vowel · off · /${VOWEL_USAGE}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/commands/fx.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* on/off/true/false; enums take one of their values. Each command is one
|
|
20
20
|
* `updateTrack` revision and one undo step.
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
22
|
+
import { nearest } from "./nearest.ts";
|
|
23
23
|
import {
|
|
24
24
|
EFFECT_NAMES,
|
|
25
25
|
FX_PRESETS,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
isEffectName,
|
|
30
30
|
normalizeParam,
|
|
31
31
|
FxValidationError,
|
|
32
|
+
VOWEL_VALUES,
|
|
32
33
|
type EffectName,
|
|
33
34
|
type FxName,
|
|
34
35
|
type FxValues,
|
|
@@ -57,37 +58,69 @@ export type FxCommand =
|
|
|
57
58
|
type: "fx-set";
|
|
58
59
|
effect: EffectName;
|
|
59
60
|
values: Readonly<Record<string, number | string | boolean>>;
|
|
61
|
+
}
|
|
62
|
+
/** Store exactly these values (`/vowel a` drops a previous morph). */
|
|
63
|
+
| {
|
|
64
|
+
type: "fx-replace";
|
|
65
|
+
effect: EffectName;
|
|
66
|
+
values: Readonly<Record<string, number | string | boolean>>;
|
|
60
67
|
};
|
|
61
68
|
|
|
62
|
-
const EFFECT_ALIASES: Readonly<Record<string, EffectName>> =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
69
|
+
export const EFFECT_ALIASES: Readonly<Record<string, EffectName>> =
|
|
70
|
+
Object.freeze({
|
|
71
|
+
lpf: "filter",
|
|
72
|
+
hpf: "filter",
|
|
73
|
+
bpf: "filter",
|
|
74
|
+
dj: "djf",
|
|
75
|
+
"dj-filter": "djf",
|
|
76
|
+
"auto-filter": "autofilter",
|
|
77
|
+
autof: "autofilter",
|
|
78
|
+
bitcrush: "crush",
|
|
79
|
+
bitcrusher: "crush",
|
|
80
|
+
coarse: "crush",
|
|
81
|
+
distortion: "distort",
|
|
82
|
+
dist: "distort",
|
|
83
|
+
drive: "distort",
|
|
84
|
+
shape: "distort",
|
|
85
|
+
trem: "tremolo",
|
|
86
|
+
comp: "compressor",
|
|
87
|
+
compress: "compressor",
|
|
88
|
+
rotary: "leslie",
|
|
89
|
+
gain: "postgain",
|
|
90
|
+
echo: "delay",
|
|
91
|
+
bus: "orbit",
|
|
92
|
+
o: "orbit",
|
|
93
|
+
sidechain: "duck",
|
|
94
|
+
duckorbit: "duck",
|
|
95
|
+
room: "reverb",
|
|
96
|
+
verb: "reverb",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 0.7: `formant` was an alias of the vowel filter; it now shifts formants.
|
|
101
|
+
* A vowel given to it gets this answer instead of a silent change.
|
|
102
|
+
*/
|
|
103
|
+
export const FORMANT_FX_USAGE =
|
|
104
|
+
"formant · shift -12..12 st, mix 0..1 · presets deep giant bright tiny";
|
|
105
|
+
|
|
106
|
+
/** `/vowel morph`, `fx vowel morph` and set_fx all refuse a morph with no target. */
|
|
107
|
+
export const VOWEL_MORPH_NEEDS_TO =
|
|
108
|
+
"set a target first: /vowel a o 0.5 or /vowel to o";
|
|
109
|
+
|
|
110
|
+
export const FORMANT_VOWEL_HINT =
|
|
111
|
+
"formant now shifts formants at constant pitch; the vowel filter is `vowel`";
|
|
112
|
+
|
|
113
|
+
/** Whether `fx formant …` words or set_fx params name a vowel. */
|
|
114
|
+
export function formantGotVowel(
|
|
115
|
+
words: readonly (string | number | boolean)[],
|
|
116
|
+
): boolean {
|
|
117
|
+
return words.some(
|
|
118
|
+
(word) =>
|
|
119
|
+
typeof word === "string" &&
|
|
120
|
+
(word.toLowerCase() === "vowel" ||
|
|
121
|
+
(VOWEL_VALUES as readonly string[]).includes(word.toLowerCase())),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
91
124
|
|
|
92
125
|
export function parseEffectName(name: string): EffectName | undefined {
|
|
93
126
|
const lower = name.toLowerCase();
|
|
@@ -134,10 +167,25 @@ export function parseParamValue(
|
|
|
134
167
|
}
|
|
135
168
|
}
|
|
136
169
|
|
|
170
|
+
/** The effects people reach for first, for the empty `fx` read. */
|
|
171
|
+
const COMMON_FIRST: readonly string[] = [
|
|
172
|
+
"reverb",
|
|
173
|
+
"delay",
|
|
174
|
+
"filter",
|
|
175
|
+
"distort",
|
|
176
|
+
"chorus",
|
|
177
|
+
"compressor",
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const FX_LIST_WORDS: readonly string[] = ["list", "ls", "presets"];
|
|
181
|
+
|
|
137
182
|
export function parseFxCommand(prompt: string): FxCommand | undefined {
|
|
138
183
|
const words = prompt.trim().split(/\s+/);
|
|
139
184
|
if (words[0]?.toLowerCase() !== "fx") return undefined;
|
|
140
185
|
if (words.length === 1) return { type: "fx-list" };
|
|
186
|
+
// `fx list`, `fx ls`, `fx presets`: the same read as bare `fx`.
|
|
187
|
+
if (words.length === 2 && FX_LIST_WORDS.includes(words[1]!.toLowerCase()))
|
|
188
|
+
return { type: "fx-list" };
|
|
141
189
|
if (prompt.length > 1_024) return undefined;
|
|
142
190
|
const irWord = (word: string | undefined) =>
|
|
143
191
|
word !== undefined && ["ir", "iresponse"].includes(word.toLowerCase());
|
|
@@ -203,9 +251,16 @@ export function unknownFxMessage(prompt: string): string | undefined {
|
|
|
203
251
|
const words = prompt.trim().split(/\s+/);
|
|
204
252
|
if (words[0]?.toLowerCase() !== "fx" || words.length < 2) return undefined;
|
|
205
253
|
const name = words[1]!.toLowerCase();
|
|
254
|
+
if (name === "formant" && formantGotVowel(words.slice(2)))
|
|
255
|
+
return FORMANT_VOWEL_HINT;
|
|
256
|
+
// A new numeric-first effect: out-of-range values and preset typos get its
|
|
257
|
+
// ranges and presets instead of the generic unknown-command path.
|
|
258
|
+
if (name === "formant" && words.length <= 6 && !parseFxCommand(prompt))
|
|
259
|
+
return FORMANT_FX_USAGE;
|
|
206
260
|
if (parseEffectName(name) || ["ir", "iresponse", "amp"].includes(name))
|
|
207
261
|
return undefined;
|
|
208
|
-
|
|
262
|
+
if (words.length === 2 && FX_LIST_WORDS.includes(name)) return undefined;
|
|
263
|
+
const near = nearest(name, [
|
|
209
264
|
...EFFECT_NAMES,
|
|
210
265
|
...Object.keys(EFFECT_ALIASES),
|
|
211
266
|
"reverb",
|
|
@@ -242,6 +297,11 @@ export function effectDefaults(effect: EffectName): FxValues {
|
|
|
242
297
|
delete out.sag;
|
|
243
298
|
delete out.gate;
|
|
244
299
|
}
|
|
300
|
+
// The vowel morph stays absent until set: `fx vowel on` stores 0.6 data.
|
|
301
|
+
if (effect === "vowel") {
|
|
302
|
+
delete out.to;
|
|
303
|
+
delete out.morph;
|
|
304
|
+
}
|
|
245
305
|
return out;
|
|
246
306
|
}
|
|
247
307
|
|
|
@@ -304,7 +364,7 @@ export function applyFxCommand(
|
|
|
304
364
|
ok: true,
|
|
305
365
|
message: on.length
|
|
306
366
|
? `fx · ${on.map((effect) => `${effect} (${describe(effect, effectValues(track, effect)!)})`).join(" → ")}`
|
|
307
|
-
: `fx · none ·
|
|
367
|
+
: `fx · none · ${COMMON_FIRST.join(" ")} … · fx list`,
|
|
308
368
|
};
|
|
309
369
|
}
|
|
310
370
|
if (command.type === "fx-ir")
|
|
@@ -324,7 +384,16 @@ export function applyFxCommand(
|
|
|
324
384
|
...effectDefaults(effect),
|
|
325
385
|
...FX_PRESETS[effect]![command.preset],
|
|
326
386
|
};
|
|
387
|
+
else if (command.type === "fx-replace") values = { ...command.values };
|
|
327
388
|
else values = { ...(current ?? effectDefaults(effect)), ...command.values };
|
|
389
|
+
if (
|
|
390
|
+
values &&
|
|
391
|
+
effect === "vowel" &&
|
|
392
|
+
command.type === "fx-set" &&
|
|
393
|
+
command.values.morph !== undefined &&
|
|
394
|
+
values.to === undefined
|
|
395
|
+
)
|
|
396
|
+
return { ok: false, message: `vowel · ${VOWEL_MORPH_NEEDS_TO}` };
|
|
328
397
|
if (values && effect === "delay" && values.time === 0) {
|
|
329
398
|
const { time: _time, ...rest } = values;
|
|
330
399
|
values = rest;
|