@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/history.ts
CHANGED
|
@@ -17,6 +17,14 @@
|
|
|
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 { DiffError, diffScores } from "../../core/diff.ts";
|
|
21
|
+
import {
|
|
22
|
+
scoreFromJSON,
|
|
23
|
+
type ScoreOperation,
|
|
24
|
+
type TrackScore,
|
|
25
|
+
} from "../../core/score.ts";
|
|
26
|
+
import { rebaseOperations } from "../session/rebase.ts";
|
|
27
|
+
import { musicalReceipt } from "../session/receipt.ts";
|
|
20
28
|
import {
|
|
21
29
|
isIdentityRewind,
|
|
22
30
|
rewindComposition,
|
|
@@ -27,6 +35,9 @@ export type HistoryEvent = Readonly<{
|
|
|
27
35
|
revision: number;
|
|
28
36
|
kind: string;
|
|
29
37
|
rewind?: Rewind | undefined;
|
|
38
|
+
payload?: unknown;
|
|
39
|
+
/** Who wrote it; per-pane undo keys on `clientId` (design §12.6). */
|
|
40
|
+
actor?: Readonly<{ clientId: string }> | undefined;
|
|
30
41
|
}>;
|
|
31
42
|
|
|
32
43
|
export type HistoryTarget = Readonly<{
|
|
@@ -39,31 +50,149 @@ export type HistoryTarget = Readonly<{
|
|
|
39
50
|
export const UNDO_KIND = "score.undo";
|
|
40
51
|
export const REDO_KIND = "score.redo";
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Replays the log into undo and redo stacks of event indexes, over the
|
|
55
|
+
* events `include` admits. An undo or redo event pops its stack only when
|
|
56
|
+
* it stepped that stack's top (its payload names the revision); otherwise
|
|
57
|
+
* it is an ordinary edit there. So a pane's own undo is an edit in the
|
|
58
|
+
* global history (`undo all` can undo it), and an `undo all` that undid
|
|
59
|
+
* another pane's edit is an edit in this pane's history. Old events without
|
|
60
|
+
* the payload revision pop unconditionally, as they always did.
|
|
61
|
+
*/
|
|
62
|
+
function historyStacks(
|
|
44
63
|
events: readonly HistoryEvent[],
|
|
45
|
-
|
|
46
|
-
):
|
|
64
|
+
include: (event: HistoryEvent) => boolean,
|
|
65
|
+
): { undo: number[]; redo: number[] } {
|
|
47
66
|
const undo: number[] = [];
|
|
48
67
|
const redo: number[] = [];
|
|
68
|
+
const steps = (stack: number[], event: HistoryEvent, key: string) => {
|
|
69
|
+
const top = stack.at(-1);
|
|
70
|
+
if (top === undefined) return false;
|
|
71
|
+
const named = (event.payload as Record<string, unknown> | null)?.[key];
|
|
72
|
+
return typeof named !== "number" || named === events[top]!.revision;
|
|
73
|
+
};
|
|
49
74
|
events.forEach((event, index) => {
|
|
50
|
-
if (!changesComposition(event)) return;
|
|
51
|
-
if (event.kind === UNDO_KIND) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
if (!changesComposition(event) || !include(event)) return;
|
|
76
|
+
if (event.kind === UNDO_KIND && steps(undo, event, "undoneRevision")) {
|
|
77
|
+
undo.pop();
|
|
78
|
+
redo.push(index);
|
|
79
|
+
} else if (
|
|
80
|
+
event.kind === REDO_KIND &&
|
|
81
|
+
steps(redo, event, "redoneRevision")
|
|
82
|
+
) {
|
|
83
|
+
redo.pop();
|
|
84
|
+
undo.push(index);
|
|
55
85
|
} else {
|
|
56
86
|
undo.push(index);
|
|
57
87
|
redo.length = 0;
|
|
58
88
|
}
|
|
59
89
|
});
|
|
60
|
-
|
|
90
|
+
return { undo, redo };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `undo all` / `redo all`: the global history, whoever made each edit. */
|
|
94
|
+
export function historyTarget(
|
|
95
|
+
composition: unknown,
|
|
96
|
+
events: readonly HistoryEvent[],
|
|
97
|
+
direction: "undo" | "redo",
|
|
98
|
+
): HistoryTarget | undefined {
|
|
99
|
+
const stacks = historyStacks(events, () => true);
|
|
100
|
+
const top = stacks[direction].at(-1);
|
|
61
101
|
if (top === undefined) return undefined;
|
|
62
102
|
const restored = rewindComposition(composition, events, top);
|
|
63
103
|
if (restored === undefined) return undefined;
|
|
64
104
|
return { revision: events[top]!.revision, composition: restored };
|
|
65
105
|
}
|
|
66
106
|
|
|
107
|
+
export type PaneStep =
|
|
108
|
+
| Readonly<{ ok: true; revision: number; next: TrackScore }>
|
|
109
|
+
| Readonly<{
|
|
110
|
+
ok: false;
|
|
111
|
+
revision: number;
|
|
112
|
+
/** Why the inverse cannot land (rebase reason or session-wide). */
|
|
113
|
+
reason: string;
|
|
114
|
+
/** Other clients that changed the composition since, oldest first. */
|
|
115
|
+
others: readonly string[];
|
|
116
|
+
}>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Per-pane undo and redo (design §12.6): the newest edit `clientId` made,
|
|
120
|
+
* inverted as score operations and rebased onto the current score with the
|
|
121
|
+
* same touch rules as agent intents. Another pane's later edit to the same
|
|
122
|
+
* note or track property refuses with a reason; unrelated edits stay.
|
|
123
|
+
* Undefined when this pane has nothing to step.
|
|
124
|
+
*/
|
|
125
|
+
export function paneHistoryStep(
|
|
126
|
+
composition: unknown,
|
|
127
|
+
events: readonly HistoryEvent[],
|
|
128
|
+
direction: "undo" | "redo",
|
|
129
|
+
clientId: string,
|
|
130
|
+
): PaneStep | undefined {
|
|
131
|
+
const stacks = historyStacks(
|
|
132
|
+
events,
|
|
133
|
+
(event) => event.actor?.clientId === clientId,
|
|
134
|
+
);
|
|
135
|
+
const top = stacks[direction].at(-1);
|
|
136
|
+
if (top === undefined) return undefined;
|
|
137
|
+
const revision = events[top]!.revision;
|
|
138
|
+
const others = [
|
|
139
|
+
...new Set(
|
|
140
|
+
events
|
|
141
|
+
.slice(top + 1)
|
|
142
|
+
.filter(
|
|
143
|
+
(event) =>
|
|
144
|
+
changesComposition(event) && event.actor?.clientId !== clientId,
|
|
145
|
+
)
|
|
146
|
+
.map((event) => event.actor?.clientId ?? ""),
|
|
147
|
+
),
|
|
148
|
+
];
|
|
149
|
+
const refuse = (reason: string): PaneStep => ({
|
|
150
|
+
ok: false,
|
|
151
|
+
revision,
|
|
152
|
+
reason,
|
|
153
|
+
others,
|
|
154
|
+
});
|
|
155
|
+
const before = rewindComposition(composition, events, top);
|
|
156
|
+
const after =
|
|
157
|
+
top + 1 === events.length
|
|
158
|
+
? composition
|
|
159
|
+
: rewindComposition(composition, events, top + 1);
|
|
160
|
+
if (before === undefined || after === undefined)
|
|
161
|
+
return refuse("history was compacted");
|
|
162
|
+
let operations: readonly ScoreOperation[];
|
|
163
|
+
let base: TrackScore;
|
|
164
|
+
let current: TrackScore;
|
|
165
|
+
try {
|
|
166
|
+
base = scoreFromJSON(after);
|
|
167
|
+
current = scoreFromJSON(composition);
|
|
168
|
+
operations = diffScores(base, scoreFromJSON(before));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof DiffError) return refuse("session-wide");
|
|
171
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
172
|
+
}
|
|
173
|
+
if (operations.length === 0) return refuse("session-wide");
|
|
174
|
+
const result = rebaseOperations(base, current, operations);
|
|
175
|
+
return result.ok
|
|
176
|
+
? { ok: true, revision, next: result.next }
|
|
177
|
+
: refuse(result.reason);
|
|
178
|
+
}
|
|
179
|
+
|
|
67
180
|
function changesComposition(event: HistoryEvent): boolean {
|
|
68
181
|
return event.rewind !== undefined && !isIdentityRewind(event.rewind);
|
|
69
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The card after undo or redo names the musical change it made
|
|
186
|
+
* (`undid · −1 note on bass (C3)`, `redid · 96 BPM`), and falls back to the
|
|
187
|
+
* revision when the change is not in the music (a rename, a section label).
|
|
188
|
+
*/
|
|
189
|
+
export function historyReceipt(
|
|
190
|
+
direction: "undo" | "redo",
|
|
191
|
+
before: TrackScore,
|
|
192
|
+
after: TrackScore,
|
|
193
|
+
revision: number,
|
|
194
|
+
): string {
|
|
195
|
+
const verb = direction === "undo" ? "undid" : "redid";
|
|
196
|
+
const change = musicalReceipt(before, after);
|
|
197
|
+
return change ? `${verb} · ${change}` : `${verb} · rev ${revision}`;
|
|
198
|
+
}
|
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
|
|
@@ -210,11 +210,28 @@ export function keysRowMismatch(
|
|
|
210
210
|
return `${instrument} has no ${wrong.join(" ")} (${familiesReading(wrong[0]!).join("/")} row); its parameters: ${rows.join(" ")}`;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* `keys record` / `keys record replace` / `keys record off` (op1-ux §4.7):
|
|
215
|
+
* arm recording in PLAY, overdub or replace each pass. Not a score write
|
|
216
|
+
* of its own; each pass the take commits is.
|
|
217
|
+
*/
|
|
218
|
+
export function parseRecordCommand(
|
|
219
|
+
prompt: string,
|
|
220
|
+
): "overdub" | "replace" | "off" | undefined {
|
|
221
|
+
const match = prompt
|
|
222
|
+
.trim()
|
|
223
|
+
.match(/^\/?keys\s+record(?:\s+(overdub|replace|off))?$/i);
|
|
224
|
+
if (!match) return undefined;
|
|
225
|
+
return (match[1]?.toLowerCase() ?? "overdub") as
|
|
226
|
+
"overdub" | "replace" | "off";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Parse one keys / piano / organ command. */
|
|
213
230
|
export function parseKeysCommand(prompt: string): KeysCommand | undefined {
|
|
214
231
|
if (prompt.length > 1_024) return undefined;
|
|
215
232
|
let words = prompt.trim().toLowerCase().split(/\s+/);
|
|
216
233
|
// `instrument piano` / `sound grand`: the generic instrument command with
|
|
217
|
-
// a keys word is a new write, so it stores the
|
|
234
|
+
// a keys word is a new write, so it stores the modeled piano.
|
|
218
235
|
if (
|
|
219
236
|
words.length === 2 &&
|
|
220
237
|
["instrument", "sound", "voice"].includes(words[0]!) &&
|
|
@@ -455,8 +472,8 @@ export function applyKeysCommand(
|
|
|
455
472
|
const piano = isKeysFamily(track.instrument);
|
|
456
473
|
const elsewhere =
|
|
457
474
|
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
|
|
475
|
+
? `keys · ${trackId} is the legacy organ; type tonewheel (or combo, pipe) for drawbars and rotary, or piano for the modeled piano`
|
|
476
|
+
: `keys · ${trackId} is ${track.instrument}; type piano first (${PIANO_FAMILIES.join(" ")}), ${ELECTRIC_FAMILIES.join(" ")} or ${ORGAN_FAMILIES.join(" ")} for modeled keys`;
|
|
460
477
|
if (command.type === "keys-list")
|
|
461
478
|
return {
|
|
462
479
|
ok: true,
|
|
@@ -483,7 +500,7 @@ export function applyKeysCommand(
|
|
|
483
500
|
if (track.sampler && command.type !== "keys-preset")
|
|
484
501
|
return {
|
|
485
502
|
ok: false,
|
|
486
|
-
message: `keys · ${trackId} is a sampler track; type piano to make it a
|
|
503
|
+
message: `keys · ${trackId} is a sampler track; type piano to make it a modeled piano`,
|
|
487
504
|
};
|
|
488
505
|
let patch: TrackPatch;
|
|
489
506
|
if (command.type === "keys-preset") {
|
|
@@ -516,7 +533,7 @@ export function applyKeysCommand(
|
|
|
516
533
|
return {
|
|
517
534
|
ok: false,
|
|
518
535
|
message: keysSynth
|
|
519
|
-
? `keys · keys shapes the
|
|
536
|
+
? `keys · keys shapes the modeled piano; ${trackId} is the keys synth: use synth ${first ?? "decay"} …, or type piano`
|
|
520
537
|
: elsewhere,
|
|
521
538
|
};
|
|
522
539
|
} else if (command.type === "keys-rotary-at") {
|
|
@@ -605,7 +622,7 @@ export function applyKeysCommand(
|
|
|
605
622
|
|
|
606
623
|
/**
|
|
607
624
|
* The fields a new track named after a piano word starts with
|
|
608
|
-
* (`/track piano` → the
|
|
625
|
+
* (`/track piano` → the modeled grand); empty for any other name. `lofi`
|
|
609
626
|
* is left out: it also names a drum kit and pattern.
|
|
610
627
|
*/
|
|
611
628
|
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,135 @@
|
|
|
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 { parseRangeCommand } from "./range.ts";
|
|
11
|
+
import { parseAutotuneCommand } from "./autotune.ts";
|
|
12
|
+
import { parseClipCommand, parseLyricsCommand } from "./clips.ts";
|
|
13
|
+
import { parsePatternCommand } from "./drums.ts";
|
|
14
|
+
import { parseProgressionCommand } from "./progression.ts";
|
|
15
|
+
import { parseEditCommand } from "./edit.ts";
|
|
16
|
+
import { parseExpressionCommand } from "./expression.ts";
|
|
17
|
+
import { parseFitCommand } from "./fit.ts";
|
|
18
|
+
import { parseFormantCommand, parseVowelCommand } from "./formant.ts";
|
|
19
|
+
import { parseFxCommand } from "./fx.ts";
|
|
20
|
+
import { parseGranularCommand } from "./granular.ts";
|
|
21
|
+
import { parseKeysCommand, parseRecordCommand } from "./keys.ts";
|
|
22
|
+
import { parseMasterCommand } from "./master.ts";
|
|
23
|
+
import { parseModalCommand } from "./modal.ts";
|
|
24
|
+
import { parseMusicCommand } from "./music.ts";
|
|
25
|
+
import { parseKitCommand, parsePackCommand } from "./pack.ts";
|
|
26
|
+
import { parseResampleCommand } from "./resample.ts";
|
|
27
|
+
import { parseRhythmCommand } from "./rhythm.ts";
|
|
28
|
+
import { parseRigCommand } from "./rig.ts";
|
|
29
|
+
import { parseSampleCommand } from "./sample.ts";
|
|
30
|
+
import { parseShiftCommand } from "./shift.ts";
|
|
31
|
+
import { parseSingCommand } from "./sing.ts";
|
|
32
|
+
import { parseStringCommand } from "./string.ts";
|
|
33
|
+
import { parseGuitarCommand, parseStrumCommand } from "./strum.ts";
|
|
34
|
+
import { parseSynthCommand } from "./synth.ts";
|
|
35
|
+
import { parseTimeCommand } from "./time.ts";
|
|
36
|
+
import { parseTuningCommand } from "./tuning.ts";
|
|
37
|
+
import { parseVocalCommand } from "./vocal.ts";
|
|
38
|
+
import { parseVocoderCommand } from "./vocoder.ts";
|
|
39
|
+
import { parseWavetableCommand } from "./wavetable.ts";
|
|
40
|
+
import { parseWindCommand } from "./wind.ts";
|
|
41
|
+
import {
|
|
42
|
+
candidates,
|
|
43
|
+
parseExportCommand,
|
|
44
|
+
parseLoopCommand,
|
|
45
|
+
parseTrackEdit,
|
|
46
|
+
} from "./grammar.ts";
|
|
47
|
+
|
|
48
|
+
/** One parser's reading of a line: its name and what it parsed. */
|
|
49
|
+
export type ParsedCommand = Readonly<{ parser: string; value: unknown }>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The first parser that takes `text` exactly as typed, with its result
|
|
53
|
+
* (no grammar rewrites): what the prompt bar would run for this spelling.
|
|
54
|
+
*/
|
|
55
|
+
export function parseExact(
|
|
56
|
+
text: string,
|
|
57
|
+
score: TrackScore,
|
|
58
|
+
): ParsedCommand | undefined {
|
|
59
|
+
for (const parse of parsers(score)) {
|
|
60
|
+
const value = parse(text);
|
|
61
|
+
if (value !== undefined)
|
|
62
|
+
return { parser: parse.name || "anonymous", value };
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What `text` runs as: the exact spelling first, then the grammar's other
|
|
69
|
+
* readings (`/x` ≡ `x`, aliases, remove and list words) in order.
|
|
70
|
+
*/
|
|
71
|
+
export function parseCommand(
|
|
72
|
+
text: string,
|
|
73
|
+
score: TrackScore,
|
|
74
|
+
): ParsedCommand | undefined {
|
|
75
|
+
for (const line of [text, ...candidates(text)]) {
|
|
76
|
+
const parsed = parseExact(line, score);
|
|
77
|
+
if (parsed) return parsed;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether a line runs locally, in any spelling the grammar accepts. */
|
|
83
|
+
export function commandParses(text: string, score: TrackScore): boolean {
|
|
84
|
+
return parseCommand(text, score) !== undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parsers(score: TrackScore): ((text: string) => unknown)[] {
|
|
88
|
+
return [
|
|
89
|
+
function parseRange(value: string) {
|
|
90
|
+
return parseRangeCommand(value, score);
|
|
91
|
+
},
|
|
92
|
+
parsePrompt,
|
|
93
|
+
parseMusicCommand,
|
|
94
|
+
parseEditCommand,
|
|
95
|
+
parseRhythmCommand,
|
|
96
|
+
parseFxCommand,
|
|
97
|
+
parseSynthCommand,
|
|
98
|
+
parseStringCommand,
|
|
99
|
+
parseGranularCommand,
|
|
100
|
+
parseRecordCommand,
|
|
101
|
+
parseKeysCommand,
|
|
102
|
+
parseExpressionCommand,
|
|
103
|
+
parseMasterCommand,
|
|
104
|
+
function parseSection(value: string) {
|
|
105
|
+
return parseSectionCommand(value, score);
|
|
106
|
+
},
|
|
107
|
+
parsePatternCommand,
|
|
108
|
+
parseKitCommand,
|
|
109
|
+
parsePackCommand,
|
|
110
|
+
parseSampleCommand,
|
|
111
|
+
parseFitCommand,
|
|
112
|
+
parseShiftCommand,
|
|
113
|
+
parseResampleCommand,
|
|
114
|
+
parseWavetableCommand,
|
|
115
|
+
parseTimeCommand,
|
|
116
|
+
parseTuningCommand,
|
|
117
|
+
parseRigCommand,
|
|
118
|
+
parseModalCommand,
|
|
119
|
+
parseGuitarCommand,
|
|
120
|
+
parseStrumCommand,
|
|
121
|
+
parseProgressionCommand,
|
|
122
|
+
parseWindCommand,
|
|
123
|
+
parseAutotuneCommand,
|
|
124
|
+
parseSingCommand,
|
|
125
|
+
parseVocoderCommand,
|
|
126
|
+
parseVocalCommand,
|
|
127
|
+
parseFormantCommand,
|
|
128
|
+
parseVowelCommand,
|
|
129
|
+
parseClipCommand,
|
|
130
|
+
parseLyricsCommand,
|
|
131
|
+
parseExportCommand,
|
|
132
|
+
parseLoopCommand,
|
|
133
|
+
parseTrackEdit,
|
|
134
|
+
];
|
|
135
|
+
}
|