@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/commands/history.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* every newer event. Events whose rewind was compacted away, and events that
|
|
18
18
|
* did not change the composition (transport), take no part.
|
|
19
19
|
*/
|
|
20
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
21
|
+
import { musicalReceipt } from "../session/receipt.ts";
|
|
20
22
|
import {
|
|
21
23
|
isIdentityRewind,
|
|
22
24
|
rewindComposition,
|
|
@@ -67,3 +69,19 @@ export function historyTarget(
|
|
|
67
69
|
function changesComposition(event: HistoryEvent): boolean {
|
|
68
70
|
return event.rewind !== undefined && !isIdentityRewind(event.rewind);
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The card after undo or redo names the musical change it made
|
|
75
|
+
* (`undid · −1 note on bass (C3)`, `redid · 96 BPM`), and falls back to the
|
|
76
|
+
* revision when the change is not in the music (a rename, a section label).
|
|
77
|
+
*/
|
|
78
|
+
export function historyReceipt(
|
|
79
|
+
direction: "undo" | "redo",
|
|
80
|
+
before: TrackScore,
|
|
81
|
+
after: TrackScore,
|
|
82
|
+
revision: number,
|
|
83
|
+
): string {
|
|
84
|
+
const verb = direction === "undo" ? "undid" : "redid";
|
|
85
|
+
const change = musicalReceipt(before, after);
|
|
86
|
+
return change ? `${verb} · ${change}` : `${verb} · rev ${revision}`;
|
|
87
|
+
}
|
package/src/commands/keys.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The modeled piano commands (0.6 keys lane, `core/keys.ts`):
|
|
3
3
|
*
|
|
4
|
-
* piano the focused track becomes the
|
|
4
|
+
* piano the focused track becomes the modeled grand
|
|
5
5
|
* piano <preset> grand ballad upright felt lofi honkytonk prepared
|
|
6
6
|
* grand | upright | felt … the same, by the preset word alone
|
|
7
7
|
* keys list this track's piano settings
|
|
@@ -214,7 +214,7 @@ export function parseKeysCommand(prompt: string): KeysCommand | undefined {
|
|
|
214
214
|
if (prompt.length > 1_024) return undefined;
|
|
215
215
|
let words = prompt.trim().toLowerCase().split(/\s+/);
|
|
216
216
|
// `instrument piano` / `sound grand`: the generic instrument command with
|
|
217
|
-
// a keys word is a new write, so it stores the
|
|
217
|
+
// a keys word is a new write, so it stores the modeled piano.
|
|
218
218
|
if (
|
|
219
219
|
words.length === 2 &&
|
|
220
220
|
["instrument", "sound", "voice"].includes(words[0]!) &&
|
|
@@ -455,8 +455,8 @@ export function applyKeysCommand(
|
|
|
455
455
|
const piano = isKeysFamily(track.instrument);
|
|
456
456
|
const elsewhere =
|
|
457
457
|
track.instrument === "organ"
|
|
458
|
-
? `keys · ${trackId} is the legacy organ; type tonewheel (or combo, pipe) for drawbars and rotary, or piano for the
|
|
459
|
-
: `keys · ${trackId} is ${track.instrument}; type piano first (${PIANO_FAMILIES.join(" ")}), ${ELECTRIC_FAMILIES.join(" ")} or ${ORGAN_FAMILIES.join(" ")} for
|
|
458
|
+
? `keys · ${trackId} is the legacy organ; type tonewheel (or combo, pipe) for drawbars and rotary, or piano for the modeled piano`
|
|
459
|
+
: `keys · ${trackId} is ${track.instrument}; type piano first (${PIANO_FAMILIES.join(" ")}), ${ELECTRIC_FAMILIES.join(" ")} or ${ORGAN_FAMILIES.join(" ")} for modeled keys`;
|
|
460
460
|
if (command.type === "keys-list")
|
|
461
461
|
return {
|
|
462
462
|
ok: true,
|
|
@@ -483,7 +483,7 @@ export function applyKeysCommand(
|
|
|
483
483
|
if (track.sampler && command.type !== "keys-preset")
|
|
484
484
|
return {
|
|
485
485
|
ok: false,
|
|
486
|
-
message: `keys · ${trackId} is a sampler track; type piano to make it a
|
|
486
|
+
message: `keys · ${trackId} is a sampler track; type piano to make it a modeled piano`,
|
|
487
487
|
};
|
|
488
488
|
let patch: TrackPatch;
|
|
489
489
|
if (command.type === "keys-preset") {
|
|
@@ -516,7 +516,7 @@ export function applyKeysCommand(
|
|
|
516
516
|
return {
|
|
517
517
|
ok: false,
|
|
518
518
|
message: keysSynth
|
|
519
|
-
? `keys · keys shapes the
|
|
519
|
+
? `keys · keys shapes the modeled piano; ${trackId} is the keys synth: use synth ${first ?? "decay"} …, or type piano`
|
|
520
520
|
: elsewhere,
|
|
521
521
|
};
|
|
522
522
|
} else if (command.type === "keys-rotary-at") {
|
|
@@ -605,7 +605,7 @@ export function applyKeysCommand(
|
|
|
605
605
|
|
|
606
606
|
/**
|
|
607
607
|
* The fields a new track named after a piano word starts with
|
|
608
|
-
* (`/track piano` → the
|
|
608
|
+
* (`/track piano` → the modeled grand); empty for any other name. `lofi`
|
|
609
609
|
* is left out: it also names a drum kit and pattern.
|
|
610
610
|
*/
|
|
611
611
|
export function newPianoTrack(name: string): {
|
package/src/commands/modal.ts
CHANGED
|
@@ -86,7 +86,7 @@ export function parseModalCommand(prompt: string): ModalCommand | undefined {
|
|
|
86
86
|
if (rest.length === 1 && rest[0] === "gamelan")
|
|
87
87
|
return {
|
|
88
88
|
type: "modal-hint",
|
|
89
|
-
message: `gamelan · ${GAMELAN_PRESETS.join(" ")} · modal gangsa to start · pair two tracks with modal pair <track> · tuning slendro or pelog · ctrl-k Sound ›
|
|
89
|
+
message: `gamelan · ${GAMELAN_PRESETS.join(" ")} · modal gangsa to start · pair two tracks with modal pair <track> · tuning slendro or pelog · ctrl-k Sound › instruments › mallets › gamelan`,
|
|
90
90
|
};
|
|
91
91
|
if (rest[0] === "preset") {
|
|
92
92
|
const preset = rest.length === 2 ? modalPresetFor(rest[1]!) : undefined;
|
package/src/commands/music.ts
CHANGED
|
@@ -83,7 +83,7 @@ export type EffectAutomationParameter = Exclude<
|
|
|
83
83
|
"volume" | "pan"
|
|
84
84
|
>;
|
|
85
85
|
|
|
86
|
-
const LANE_ALIASES: Readonly<Record<string, EffectAutomationParameter>> =
|
|
86
|
+
export const LANE_ALIASES: Readonly<Record<string, EffectAutomationParameter>> =
|
|
87
87
|
Object.freeze({
|
|
88
88
|
filter: "filter",
|
|
89
89
|
cutoff: "filter",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One vocabulary matcher for every "did you mean" in dawg: instruments,
|
|
3
|
+
* effects, styles, kits, rigs, sections, presets, params and verbs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Optimal-string-alignment distance: insert, delete, substitute, and an
|
|
8
|
+
* adjacent swap (`/hlep` → `/help`) each cost one edit.
|
|
9
|
+
*/
|
|
10
|
+
export function editDistance(a: string, b: string): number {
|
|
11
|
+
let before: number[] = [];
|
|
12
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
13
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
14
|
+
const row = [i];
|
|
15
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
16
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
17
|
+
let best = Math.min(
|
|
18
|
+
previous[j]! + 1,
|
|
19
|
+
row[j - 1]! + 1,
|
|
20
|
+
previous[j - 1]! + cost,
|
|
21
|
+
);
|
|
22
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
23
|
+
best = Math.min(best, before[j - 2]! + 1);
|
|
24
|
+
row.push(best);
|
|
25
|
+
}
|
|
26
|
+
before = previous;
|
|
27
|
+
previous = row;
|
|
28
|
+
}
|
|
29
|
+
return previous[b.length]!;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The word in `vocabulary` nearest to `word` (edit distance, adjacent swaps
|
|
34
|
+
* cost one), within one edit for words of four letters or fewer and two
|
|
35
|
+
* otherwise; undefined when nothing is that close or `word` is itself in it.
|
|
36
|
+
*/
|
|
37
|
+
export function nearest(
|
|
38
|
+
word: string,
|
|
39
|
+
vocabulary: Iterable<string>,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
const typed = word.trim().toLowerCase();
|
|
42
|
+
if (!typed) return undefined;
|
|
43
|
+
const limit = typed.length <= 4 ? 1 : 2;
|
|
44
|
+
let best: { word: string; distance: number } | undefined;
|
|
45
|
+
for (const candidate of vocabulary) {
|
|
46
|
+
const lower = candidate.toLowerCase();
|
|
47
|
+
if (lower === typed) return undefined;
|
|
48
|
+
const distance = editDistance(typed, lower);
|
|
49
|
+
if (distance <= limit && (!best || distance < best.distance))
|
|
50
|
+
best = { word: candidate, distance };
|
|
51
|
+
}
|
|
52
|
+
return best?.word;
|
|
53
|
+
}
|
package/src/commands/pack.ts
CHANGED
|
@@ -124,10 +124,10 @@ export type KitCommand =
|
|
|
124
124
|
|
|
125
125
|
/** `/kit [bank]` (default kit) and `/kit list`; undefined for other commands. */
|
|
126
126
|
export function parseKitCommand(command: string): KitCommand | undefined {
|
|
127
|
-
const match = command.trim().match(
|
|
127
|
+
const match = command.trim().match(/^\/?kits?(?:\s+(\S+))?\s*$/i);
|
|
128
128
|
if (!match) return undefined;
|
|
129
129
|
const bank = match[1];
|
|
130
|
-
if (bank && /^(list|ls)$/i.test(bank)) return { kind: "list" };
|
|
130
|
+
if (bank && /^(list|ls|presets)$/i.test(bank)) return { kind: "list" };
|
|
131
131
|
return { kind: "set", bank: bank ?? DEFAULT_KIT };
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -389,6 +389,8 @@ export function kitListLines(aliases: BankAliases): string[] {
|
|
|
389
389
|
export type CacheReport = Readonly<{
|
|
390
390
|
packs: CacheUsage & { max: number };
|
|
391
391
|
assets: CacheUsage & { max: number };
|
|
392
|
+
/** Pitch curves in `.dawg/analysis` (0.7); omitted when empty. */
|
|
393
|
+
analysis?: CacheUsage & { max: number };
|
|
392
394
|
freed?: Readonly<{ files: number; bytes: number }>;
|
|
393
395
|
}>;
|
|
394
396
|
|
|
@@ -408,6 +410,11 @@ export function cacheLines(report: CacheReport): string[] {
|
|
|
408
410
|
: []),
|
|
409
411
|
row("pack downloads", report.packs, "DAWG_PACKS_CACHE_MAX"),
|
|
410
412
|
row("decoded audio (this project)", report.assets, "DAWG_ASSETS_CACHE_MAX"),
|
|
413
|
+
...(report.analysis?.files
|
|
414
|
+
? [
|
|
415
|
+
`pitch analysis (this project) · ${formatBytes(report.analysis.bytes)} of ${formatBytes(report.analysis.max)} · ${report.analysis.files} curve${report.analysis.files === 1 ? "" : "s"} · .dawg/analysis, re-tracked on demand`,
|
|
416
|
+
]
|
|
417
|
+
: []),
|
|
411
418
|
"least recently used files go first; this project's sounds are never evicted",
|
|
412
419
|
"evicted sounds re-fetch by their pinned sha256 on next use",
|
|
413
420
|
"/pack cache prune [size] · /pack cache clear",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `✗ synth lpf 99999 · lpf takes 20…20000 Hz · synth lpf 1200`: the range
|
|
3
|
+
* card for a synth or effect parameter, built from the same specs that
|
|
4
|
+
* validate it (core/params), so the card and the check never disagree.
|
|
5
|
+
*/
|
|
6
|
+
import type { ParamSpec } from "../../core/params.ts";
|
|
7
|
+
import { SYNTH_PARAMS, synthParamName } from "../../core/synth.ts";
|
|
8
|
+
import { effectSpec } from "../../core/fx.ts";
|
|
9
|
+
import { parseEffectName, parseParamName } from "./fx.ts";
|
|
10
|
+
import { usageError } from "./grammar.ts";
|
|
11
|
+
|
|
12
|
+
/** The range card for the first out-of-range number in the line, if any. */
|
|
13
|
+
export function paramRangeError(line: string): string | undefined {
|
|
14
|
+
const words = line.trim().replace(/^\//, "").split(/\s+/);
|
|
15
|
+
const verb = words[0]?.toLowerCase();
|
|
16
|
+
if (verb === "synth")
|
|
17
|
+
return firstOutOfRange(line, words.slice(1), "synth", (word) => {
|
|
18
|
+
const name = synthParamName(word);
|
|
19
|
+
return name ? [name, SYNTH_PARAMS[name]] : undefined;
|
|
20
|
+
});
|
|
21
|
+
if (verb === "fx" && words[1]) {
|
|
22
|
+
const effect = parseEffectName(words[1]);
|
|
23
|
+
if (!effect) return undefined;
|
|
24
|
+
const params = effectSpec(effect).params;
|
|
25
|
+
return firstOutOfRange(line, words.slice(2), `fx ${effect}`, (word) => {
|
|
26
|
+
const name = parseParamName(effect, word);
|
|
27
|
+
return name ? [name, params[name]] : undefined;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function firstOutOfRange(
|
|
34
|
+
line: string,
|
|
35
|
+
rest: readonly string[],
|
|
36
|
+
prefix: string,
|
|
37
|
+
lookup: (word: string) => [string, ParamSpec | undefined] | undefined,
|
|
38
|
+
): string | undefined {
|
|
39
|
+
for (let index = 0; index + 1 < rest.length; index += 2) {
|
|
40
|
+
const found = lookup(rest[index]!);
|
|
41
|
+
if (!found) continue;
|
|
42
|
+
const [name, spec] = found;
|
|
43
|
+
if (spec?.kind !== "number") continue;
|
|
44
|
+
const value = Number(rest[index + 1]);
|
|
45
|
+
if (!Number.isFinite(value)) continue;
|
|
46
|
+
if (value >= spec.min && value <= spec.max) continue;
|
|
47
|
+
return usageError(line.trim(), {
|
|
48
|
+
command: name,
|
|
49
|
+
min: spec.min,
|
|
50
|
+
max: spec.max,
|
|
51
|
+
unit: spec.unit ?? "",
|
|
52
|
+
example: `${prefix} ${name} ${spec.default}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a prompt line is a command the human prompt bar runs locally (no
|
|
3
|
+
* model call): every command parser the prompt tries, in one list. The TUI
|
|
4
|
+
* uses it for typo fixes; show-me (src/agent/show-me.ts) uses it in tests to
|
|
5
|
+
* prove every command it ghost-types is one a human can type.
|
|
6
|
+
*/
|
|
7
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
8
|
+
import { parsePrompt } from "../agent/ops.ts";
|
|
9
|
+
import { parseSectionCommand } from "./arrange.ts";
|
|
10
|
+
import { parseAutotuneCommand } from "./autotune.ts";
|
|
11
|
+
import { parseClipCommand, parseLyricsCommand } from "./clips.ts";
|
|
12
|
+
import { parsePatternCommand } from "./drums.ts";
|
|
13
|
+
import { parseProgressionCommand } from "./progression.ts";
|
|
14
|
+
import { parseEditCommand } from "./edit.ts";
|
|
15
|
+
import { parseExpressionCommand } from "./expression.ts";
|
|
16
|
+
import { parseFitCommand } from "./fit.ts";
|
|
17
|
+
import { parseFormantCommand, parseVowelCommand } from "./formant.ts";
|
|
18
|
+
import { parseFxCommand } from "./fx.ts";
|
|
19
|
+
import { parseGranularCommand } from "./granular.ts";
|
|
20
|
+
import { parseKeysCommand } from "./keys.ts";
|
|
21
|
+
import { parseMasterCommand } from "./master.ts";
|
|
22
|
+
import { parseModalCommand } from "./modal.ts";
|
|
23
|
+
import { parseMusicCommand } from "./music.ts";
|
|
24
|
+
import { parseKitCommand, parsePackCommand } from "./pack.ts";
|
|
25
|
+
import { parseResampleCommand } from "./resample.ts";
|
|
26
|
+
import { parseRhythmCommand } from "./rhythm.ts";
|
|
27
|
+
import { parseRigCommand } from "./rig.ts";
|
|
28
|
+
import { parseSampleCommand } from "./sample.ts";
|
|
29
|
+
import { parseShiftCommand } from "./shift.ts";
|
|
30
|
+
import { parseSingCommand } from "./sing.ts";
|
|
31
|
+
import { parseStringCommand } from "./string.ts";
|
|
32
|
+
import { parseGuitarCommand, parseStrumCommand } from "./strum.ts";
|
|
33
|
+
import { parseSynthCommand } from "./synth.ts";
|
|
34
|
+
import { parseTimeCommand } from "./time.ts";
|
|
35
|
+
import { parseTuningCommand } from "./tuning.ts";
|
|
36
|
+
import { parseVocalCommand } from "./vocal.ts";
|
|
37
|
+
import { parseVocoderCommand } from "./vocoder.ts";
|
|
38
|
+
import { parseWavetableCommand } from "./wavetable.ts";
|
|
39
|
+
import { parseWindCommand } from "./wind.ts";
|
|
40
|
+
import {
|
|
41
|
+
candidates,
|
|
42
|
+
parseExportCommand,
|
|
43
|
+
parseLoopCommand,
|
|
44
|
+
parseTrackEdit,
|
|
45
|
+
} from "./grammar.ts";
|
|
46
|
+
|
|
47
|
+
/** One parser's reading of a line: its name and what it parsed. */
|
|
48
|
+
export type ParsedCommand = Readonly<{ parser: string; value: unknown }>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The first parser that takes `text` exactly as typed, with its result
|
|
52
|
+
* (no grammar rewrites): what the prompt bar would run for this spelling.
|
|
53
|
+
*/
|
|
54
|
+
export function parseExact(
|
|
55
|
+
text: string,
|
|
56
|
+
score: TrackScore,
|
|
57
|
+
): ParsedCommand | undefined {
|
|
58
|
+
for (const parse of parsers(score)) {
|
|
59
|
+
const value = parse(text);
|
|
60
|
+
if (value !== undefined)
|
|
61
|
+
return { parser: parse.name || "anonymous", value };
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What `text` runs as: the exact spelling first, then the grammar's other
|
|
68
|
+
* readings (`/x` ≡ `x`, aliases, remove and list words) in order.
|
|
69
|
+
*/
|
|
70
|
+
export function parseCommand(
|
|
71
|
+
text: string,
|
|
72
|
+
score: TrackScore,
|
|
73
|
+
): ParsedCommand | undefined {
|
|
74
|
+
for (const line of [text, ...candidates(text)]) {
|
|
75
|
+
const parsed = parseExact(line, score);
|
|
76
|
+
if (parsed) return parsed;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether a line runs locally, in any spelling the grammar accepts. */
|
|
82
|
+
export function commandParses(text: string, score: TrackScore): boolean {
|
|
83
|
+
return parseCommand(text, score) !== undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parsers(score: TrackScore): ((text: string) => unknown)[] {
|
|
87
|
+
return [
|
|
88
|
+
parsePrompt,
|
|
89
|
+
parseMusicCommand,
|
|
90
|
+
parseEditCommand,
|
|
91
|
+
parseRhythmCommand,
|
|
92
|
+
parseFxCommand,
|
|
93
|
+
parseSynthCommand,
|
|
94
|
+
parseStringCommand,
|
|
95
|
+
parseGranularCommand,
|
|
96
|
+
parseKeysCommand,
|
|
97
|
+
parseExpressionCommand,
|
|
98
|
+
parseMasterCommand,
|
|
99
|
+
function parseSection(value: string) {
|
|
100
|
+
return parseSectionCommand(value, score);
|
|
101
|
+
},
|
|
102
|
+
parsePatternCommand,
|
|
103
|
+
parseKitCommand,
|
|
104
|
+
parsePackCommand,
|
|
105
|
+
parseSampleCommand,
|
|
106
|
+
parseFitCommand,
|
|
107
|
+
parseShiftCommand,
|
|
108
|
+
parseResampleCommand,
|
|
109
|
+
parseWavetableCommand,
|
|
110
|
+
parseTimeCommand,
|
|
111
|
+
parseTuningCommand,
|
|
112
|
+
parseRigCommand,
|
|
113
|
+
parseModalCommand,
|
|
114
|
+
parseGuitarCommand,
|
|
115
|
+
parseStrumCommand,
|
|
116
|
+
parseProgressionCommand,
|
|
117
|
+
parseWindCommand,
|
|
118
|
+
parseAutotuneCommand,
|
|
119
|
+
parseSingCommand,
|
|
120
|
+
parseVocoderCommand,
|
|
121
|
+
parseVocalCommand,
|
|
122
|
+
parseFormantCommand,
|
|
123
|
+
parseVowelCommand,
|
|
124
|
+
parseClipCommand,
|
|
125
|
+
parseLyricsCommand,
|
|
126
|
+
parseExportCommand,
|
|
127
|
+
parseLoopCommand,
|
|
128
|
+
parseTrackEdit,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `progression …`: sustained, voice-led block chords on the focused track,
|
|
3
|
+
* the prompt form of the SDK's `progression()` and the agent's write_chords.
|
|
4
|
+
*
|
|
5
|
+
* progression i7 IV7 i7 IV7 each 8 roman numerals in the song key
|
|
6
|
+
* progression Am7 D9 at 16 bass chord symbols, a root under each
|
|
7
|
+
*
|
|
8
|
+
* Chords run core/chords.ts renderProgression in `block` mode with voice
|
|
9
|
+
* leading, so a carrier for a vocoder or talkbox holds each chord for its
|
|
10
|
+
* whole span. Existing notes stay; chords past the end grow the song.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
chordName,
|
|
14
|
+
keyUsesFlats,
|
|
15
|
+
parseKey,
|
|
16
|
+
renderProgression,
|
|
17
|
+
resolveChord,
|
|
18
|
+
type Chord,
|
|
19
|
+
type Key,
|
|
20
|
+
} from "../../core/chords.ts";
|
|
21
|
+
import { applyScoreOperations } from "../../core/diff.ts";
|
|
22
|
+
import {
|
|
23
|
+
SCORE_LIMITS,
|
|
24
|
+
ScoreValidationError,
|
|
25
|
+
type ScoreOperation,
|
|
26
|
+
type TrackScore,
|
|
27
|
+
} from "../../core/score.ts";
|
|
28
|
+
import { barTicks } from "../../core/sections.ts";
|
|
29
|
+
import { resolveTuning, snapToTuning } from "../../core/tuning.ts";
|
|
30
|
+
import type { CommandResult } from "./strum.ts";
|
|
31
|
+
|
|
32
|
+
export const PROGRESSION_USAGE =
|
|
33
|
+
"progression <chords> [each <beats>] [at <beat>] [bass] · progression i7 IV7 each 8 · progression Am7 D9 bass";
|
|
34
|
+
|
|
35
|
+
export type ProgressionOptions = Readonly<{
|
|
36
|
+
chords: readonly string[];
|
|
37
|
+
each?: number;
|
|
38
|
+
at?: number;
|
|
39
|
+
bass?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export type ProgressionCommand =
|
|
43
|
+
| Readonly<{ type: "progression"; options: ProgressionOptions }>
|
|
44
|
+
| Readonly<{ type: "progression-hint"; message: string }>;
|
|
45
|
+
|
|
46
|
+
/** `progression …`, or undefined when the prompt is not one. */
|
|
47
|
+
export function parseProgressionCommand(
|
|
48
|
+
prompt: string,
|
|
49
|
+
): ProgressionCommand | undefined {
|
|
50
|
+
if (prompt.length > 1_024) return undefined;
|
|
51
|
+
const words = prompt.trim().replace(/^\//, "").split(/\s+/);
|
|
52
|
+
const head = words[0]?.toLowerCase();
|
|
53
|
+
if (head !== "progression" && head !== "prog") return undefined;
|
|
54
|
+
const chords: string[] = [];
|
|
55
|
+
let each: number | undefined;
|
|
56
|
+
let at: number | undefined;
|
|
57
|
+
let bass = false;
|
|
58
|
+
for (let i = 1; i < words.length; i += 1) {
|
|
59
|
+
const word = words[i]!;
|
|
60
|
+
const lower = word.toLowerCase();
|
|
61
|
+
if (lower === "each" || lower === "at") {
|
|
62
|
+
const value = Number(words[i + 1]);
|
|
63
|
+
if (
|
|
64
|
+
!Number.isFinite(value) ||
|
|
65
|
+
value < 0 ||
|
|
66
|
+
(lower === "each" && value <= 0)
|
|
67
|
+
)
|
|
68
|
+
return { type: "progression-hint", message: PROGRESSION_USAGE };
|
|
69
|
+
if (lower === "each") each = value;
|
|
70
|
+
else at = value;
|
|
71
|
+
i += 1;
|
|
72
|
+
} else if (lower === "bass") bass = true;
|
|
73
|
+
else chords.push(word);
|
|
74
|
+
}
|
|
75
|
+
if (chords.length === 0)
|
|
76
|
+
return { type: "progression-hint", message: PROGRESSION_USAGE };
|
|
77
|
+
if (chords.length > 64)
|
|
78
|
+
return {
|
|
79
|
+
type: "progression-hint",
|
|
80
|
+
message: "progression takes up to 64 chords",
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
type: "progression",
|
|
84
|
+
options: {
|
|
85
|
+
chords,
|
|
86
|
+
...(each !== undefined ? { each } : {}),
|
|
87
|
+
...(at !== undefined ? { at } : {}),
|
|
88
|
+
...(bass ? { bass } : {}),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Writes a progression command's chords onto `trackId`. */
|
|
94
|
+
export function applyProgressionCommand(
|
|
95
|
+
score: TrackScore,
|
|
96
|
+
trackId: string,
|
|
97
|
+
command: ProgressionCommand,
|
|
98
|
+
newId: (index: number) => string,
|
|
99
|
+
): CommandResult {
|
|
100
|
+
if (command.type === "progression-hint")
|
|
101
|
+
return { ok: false, message: command.message };
|
|
102
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
103
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
104
|
+
const { options } = command;
|
|
105
|
+
const key: Key = parseKey(score.key) ?? { tonic: 0, mode: "major" };
|
|
106
|
+
const chords: Chord[] = [];
|
|
107
|
+
for (const text of options.chords) {
|
|
108
|
+
const chord = resolveChord(key, text);
|
|
109
|
+
if (!chord)
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
message: `unknown chord ${JSON.stringify(text.slice(0, 32))} (a symbol like Am7 or a numeral like ii7)`,
|
|
113
|
+
};
|
|
114
|
+
chords.push(chord);
|
|
115
|
+
}
|
|
116
|
+
const each = options.each ?? score.beatsPerBar;
|
|
117
|
+
const rendered = renderProgression({
|
|
118
|
+
key,
|
|
119
|
+
chords,
|
|
120
|
+
beatsPerChord: each,
|
|
121
|
+
start: options.at ?? 0,
|
|
122
|
+
bass: options.bass === true,
|
|
123
|
+
lead: true,
|
|
124
|
+
perform: { mode: "block" },
|
|
125
|
+
});
|
|
126
|
+
const performed = [...rendered.notes, ...rendered.bass];
|
|
127
|
+
if (performed.length > 4096)
|
|
128
|
+
return { ok: false, message: "progression would write over 4096 notes" };
|
|
129
|
+
const tpb = score.ticksPerBeat;
|
|
130
|
+
const table = resolveTuning(score.tuning, track.tuning, score.key);
|
|
131
|
+
const operations: ScoreOperation[] = performed.map((note, index) => ({
|
|
132
|
+
type: "addNote",
|
|
133
|
+
note: {
|
|
134
|
+
id: newId(index),
|
|
135
|
+
trackId,
|
|
136
|
+
startTick: Math.round(note.start * tpb),
|
|
137
|
+
durationTicks: Math.max(1, Math.round(note.length * tpb)),
|
|
138
|
+
pitch: snapToTuning(note.pitch, table),
|
|
139
|
+
velocity: note.velocity,
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
let next: TrackScore;
|
|
143
|
+
try {
|
|
144
|
+
next = applyScoreOperations(score, operations);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof ScoreValidationError)
|
|
147
|
+
return { ok: false, message: `progression · ${error.message}` };
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
const end = next.notes.reduce(
|
|
151
|
+
(max, note) => Math.max(max, note.startTick + note.durationTicks),
|
|
152
|
+
0,
|
|
153
|
+
);
|
|
154
|
+
const needed = Math.min(
|
|
155
|
+
SCORE_LIMITS.maxBars,
|
|
156
|
+
Math.ceil(end / barTicks(next)),
|
|
157
|
+
);
|
|
158
|
+
const grew = needed > next.bars;
|
|
159
|
+
if (grew)
|
|
160
|
+
next = applyScoreOperations(next, [{ type: "setBars", bars: needed }]);
|
|
161
|
+
const flats = keyUsesFlats(key);
|
|
162
|
+
const names = chords.map((chord) => chordName(chord, flats));
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
message: `progression · ${names.join(" ")} · ${each} beats each · ${performed.length} notes${grew ? ` · song now ${needed} bars` : ""}`,
|
|
166
|
+
next,
|
|
167
|
+
kind: "score.notes",
|
|
168
|
+
payload: { trackId, progression: names },
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/commands/rhythm.ts
CHANGED
|
@@ -120,6 +120,9 @@ export function parseRhythmCommand(prompt: string): RhythmCommand | undefined {
|
|
|
120
120
|
fields[key] = Number(raw);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
// `euclid hat` alone names a row to edit: the window opens the editor on
|
|
124
|
+
// it (`/euclid hat`), so it is not an empty edit here.
|
|
125
|
+
if (Object.keys(fields).length === 0) return undefined;
|
|
123
126
|
return { type: "rhythm-set", voice: lowerDrum(voice), fields };
|
|
124
127
|
}
|
|
125
128
|
|
package/src/commands/rig.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type FxCommand,
|
|
46
46
|
type FxResult,
|
|
47
47
|
} from "./fx.ts";
|
|
48
|
+
import { nearest } from "./nearest.ts";
|
|
48
49
|
|
|
49
50
|
export type RigCommand =
|
|
50
51
|
| { type: "rig-show" }
|
|
@@ -67,33 +68,11 @@ function isStage(word: string): word is RigStage {
|
|
|
67
68
|
|
|
68
69
|
/** `unknown rig crunh · did you mean crunch? · rigs clean crunch …` */
|
|
69
70
|
function unknownRigMessage(name: string): string {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const distance = editDistance(name, candidate);
|
|
73
|
-
if (!best || distance < best.distance) best = { name: candidate, distance };
|
|
74
|
-
}
|
|
75
|
-
const near =
|
|
76
|
-
best && best.distance <= Math.max(2, Math.floor(name.length / 3))
|
|
77
|
-
? ` · did you mean ${best.name}?`
|
|
78
|
-
: "";
|
|
71
|
+
const match = nearest(name, RIG_PRESET_NAMES);
|
|
72
|
+
const near = match ? ` · did you mean ${match}?` : "";
|
|
79
73
|
return `unknown rig ${name}${near} · rigs ${RIG_PRESET_NAMES.join(" ")} reset`;
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
function editDistance(a: string, b: string): number {
|
|
83
|
-
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
84
|
-
for (let i = 1; i <= a.length; i++) {
|
|
85
|
-
const next = [i];
|
|
86
|
-
for (let j = 1; j <= b.length; j++)
|
|
87
|
-
next[j] = Math.min(
|
|
88
|
-
row[j]! + 1,
|
|
89
|
-
next[j - 1]! + 1,
|
|
90
|
-
row[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
91
|
-
);
|
|
92
|
-
row = next;
|
|
93
|
-
}
|
|
94
|
-
return row[b.length]!;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
76
|
export function parseRigCommand(prompt: string): RigCommand | undefined {
|
|
98
77
|
if (prompt.length > 1_024) return undefined;
|
|
99
78
|
const words = prompt.trim().replace(/^\//, "").toLowerCase().split(/\s+/);
|