@hraness/dawg 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show-me: the agent acts the way a human would, live, as its tokens stream.
|
|
3
|
+
*
|
|
4
|
+
* In command mode the model writes dawg prompt commands, one per line. The
|
|
5
|
+
* prompt bar shows the line being written as ghost text at the model's own
|
|
6
|
+
* speed, and each line runs through the same `submit()` a human's Enter
|
|
7
|
+
* runs the moment it is complete. This module holds the pure parts: the
|
|
8
|
+
* incremental line splitter, which lines count as commands, the caption that
|
|
9
|
+
* names the human gesture (the typed command, the fader, the keys of play
|
|
10
|
+
* mode), the fader glide values and the note scheduler that keeps streamed
|
|
11
|
+
* notes in time or falls back to step entry. Nothing here waits on a clock:
|
|
12
|
+
* callers pass times in, so tests use fake clocks.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
drumVoicePitch,
|
|
16
|
+
isDrumInstrument,
|
|
17
|
+
parseDrumVoice,
|
|
18
|
+
} from "../../core/drums.ts";
|
|
19
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
20
|
+
import { workspaceRelative } from "../commands/grammar.ts";
|
|
21
|
+
import { looksLikeProse, nearestCommand, usageHint } from "../commands/help.ts";
|
|
22
|
+
import { DEFAULT_KITS } from "../audio/packs.ts";
|
|
23
|
+
import { SYNTH_KIT_NAMES } from "../../core/kits.ts";
|
|
24
|
+
import { DRUM_PATTERNS, findPattern } from "../../core/sdk/v1.ts";
|
|
25
|
+
import { parsePatternCommand } from "../commands/drums.ts";
|
|
26
|
+
import { parseEffectName } from "../commands/fx.ts";
|
|
27
|
+
import { nearest } from "../commands/nearest.ts";
|
|
28
|
+
import { parseKitCommand } from "../commands/pack.ts";
|
|
29
|
+
import { commandParses } from "../commands/parses.ts";
|
|
30
|
+
import { menuPath } from "../tui/menu.ts";
|
|
31
|
+
import { NOTE_KEYS, defaultBaseFor } from "../tui/play-mode.ts";
|
|
32
|
+
import { parsePrompt } from "./ops.ts";
|
|
33
|
+
|
|
34
|
+
/** How much a show-me turn shows: captions and key display, or neither. */
|
|
35
|
+
export type ShowMeLevel = "on" | "quiet" | "off";
|
|
36
|
+
|
|
37
|
+
export const SHOW_ME_LEVELS: readonly ShowMeLevel[] = ["on", "quiet", "off"];
|
|
38
|
+
|
|
39
|
+
export function parseShowMe(text: string): ShowMeLevel | undefined {
|
|
40
|
+
const word = text.trim().toLowerCase();
|
|
41
|
+
return (SHOW_ME_LEVELS as readonly string[]).includes(word)
|
|
42
|
+
? (word as ShowMeLevel)
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Longest command line the splitter accepts; longer lines are prose. */
|
|
47
|
+
export const MAX_COMMAND_LINE = 400;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Splits a token stream into lines. `push` returns the lines completed by a
|
|
51
|
+
* delta and the partial line still being written (the ghost text). Code
|
|
52
|
+
* fences, list bullets and a leading `$ ` or `> ` are stripped, since small
|
|
53
|
+
* models wrap commands in them.
|
|
54
|
+
*/
|
|
55
|
+
export class CommandLines {
|
|
56
|
+
private buffer = "";
|
|
57
|
+
|
|
58
|
+
push(delta: string): { complete: string[]; partial: string } {
|
|
59
|
+
this.buffer += delta;
|
|
60
|
+
const parts = this.buffer.split(/\r?\n/);
|
|
61
|
+
this.buffer = parts.pop() ?? "";
|
|
62
|
+
if (this.buffer.length > MAX_COMMAND_LINE * 4)
|
|
63
|
+
this.buffer = this.buffer.slice(-MAX_COMMAND_LINE * 4);
|
|
64
|
+
return {
|
|
65
|
+
complete: parts.map(cleanLine).filter((line) => line.length > 0),
|
|
66
|
+
partial: cleanLine(this.buffer),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The last line, when the stream ends without a newline. */
|
|
71
|
+
flush(): string | undefined {
|
|
72
|
+
const line = cleanLine(this.buffer);
|
|
73
|
+
this.buffer = "";
|
|
74
|
+
return line.length > 0 ? line : undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cleanLine(line: string): string {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
if (/^```/.test(trimmed)) return "";
|
|
81
|
+
return trimmed
|
|
82
|
+
.replace(/^(?:[-*]\s+|\$\s+|>\s+|\d+[.)]\s+)/, "")
|
|
83
|
+
.replace(/^`(.*)`$/, "$1")
|
|
84
|
+
.trim()
|
|
85
|
+
.replace(/^track(?=\s)/i, "/track");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Prompt-bar commands the agent may not run: they open windows, set a model key,
|
|
90
|
+
* quit or change this window's settings, none of which edits the song.
|
|
91
|
+
*/
|
|
92
|
+
const WINDOW_ONLY =
|
|
93
|
+
/^\/(?:help|guide|menu|play|login|logout|auth|model|quit|exit|sessions?|new|showme|status|undo|redo|tracks|euclid|try|record|grid|count-?in|click|chords|theme|motion|view|transcript)\b/i;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a complete line is a command the agent runs: one a human could
|
|
97
|
+
* type into the prompt bar that edits the song. Anything else is the
|
|
98
|
+
* model's prose (its short reply).
|
|
99
|
+
*/
|
|
100
|
+
export function isAgentCommand(line: string, score: TrackScore): boolean {
|
|
101
|
+
if (line.length === 0 || line.length > MAX_COMMAND_LINE) return false;
|
|
102
|
+
if (WINDOW_ONLY.test(line)) return false;
|
|
103
|
+
if (!agentPathsAllowed(line)) return false;
|
|
104
|
+
if (/^\/?track\s+[a-z0-9._ -]{1,64}$/i.test(line)) return true;
|
|
105
|
+
// `pattern housee`, `kit 8088`: parses, but names nothing in the library.
|
|
106
|
+
if (catalogMiss(line) !== undefined) return false;
|
|
107
|
+
return commandParses(line, score);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Kit names show-me knows offline: the synth kits and `/kit` short names. */
|
|
111
|
+
const KIT_NAMES: readonly string[] = [
|
|
112
|
+
...SYNTH_KIT_NAMES,
|
|
113
|
+
...Object.keys(DEFAULT_KITS),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The corrected line when `pattern <name>` or `kit <name>` names nothing
|
|
118
|
+
* dawg has but is near a name it does (`pattern housee` → `pattern
|
|
119
|
+
* house`); `""` for a pattern name with no near match; undefined for any
|
|
120
|
+
* other line. A kit name far from every short name may be a pack bank, so
|
|
121
|
+
* it is left to the prompt bar.
|
|
122
|
+
*/
|
|
123
|
+
export function catalogMiss(line: string): string | undefined {
|
|
124
|
+
const pattern = parsePatternCommand(line);
|
|
125
|
+
if (pattern?.kind === "apply") {
|
|
126
|
+
if (findPattern(pattern.name)) return undefined;
|
|
127
|
+
const near = nearest(
|
|
128
|
+
pattern.name,
|
|
129
|
+
DRUM_PATTERNS.map((entry) => entry.name),
|
|
130
|
+
);
|
|
131
|
+
return near ? withName(line, near) : "";
|
|
132
|
+
}
|
|
133
|
+
const kit = parseKitCommand(line);
|
|
134
|
+
if (kit?.kind === "set" && /^[a-z0-9]+$/i.test(kit.bank)) {
|
|
135
|
+
const bank = kit.bank.toLowerCase();
|
|
136
|
+
if (KIT_NAMES.includes(bank)) return undefined;
|
|
137
|
+
const near = nearest(bank, KIT_NAMES);
|
|
138
|
+
return near ? withName(line, near) : undefined;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `line` with its second word (the pattern or kit name) replaced. */
|
|
144
|
+
function withName(line: string, name: string): string {
|
|
145
|
+
const words = line.trim().split(/\s+/);
|
|
146
|
+
words[1] = name;
|
|
147
|
+
return words.join(" ");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Verbs whose arguments name files on disk. */
|
|
151
|
+
const PATH_VERBS = /^\/?(?:export|import|sample|tuning)\s+(.*)$/i;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Whether every file the line names stays inside the workspace: the agent
|
|
155
|
+
* may not export over, import or load a path that is absolute, under home
|
|
156
|
+
* or climbs out with `..`. A typed command keeps absolute paths.
|
|
157
|
+
*/
|
|
158
|
+
export function agentPathsAllowed(line: string): boolean {
|
|
159
|
+
const args = line.trim().match(PATH_VERBS)?.[1];
|
|
160
|
+
if (args === undefined) return true;
|
|
161
|
+
return args
|
|
162
|
+
.split(/\s+/)
|
|
163
|
+
.filter((token) => token.length > 0)
|
|
164
|
+
.every((token) => workspaceRelative(token));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A line the agent meant as a command that does not parse: a slash word,
|
|
169
|
+
* or a lowercase line led by a known verb (or a near typo of one) that is
|
|
170
|
+
* not a sentence. Show-me runs nothing for it and shows a red receipt
|
|
171
|
+
* (brokenCommandReceipt) instead of letting it pass as prose.
|
|
172
|
+
*/
|
|
173
|
+
export function isBrokenCommand(line: string, score: TrackScore): boolean {
|
|
174
|
+
if (line.length === 0 || line.length > MAX_COMMAND_LINE) return false;
|
|
175
|
+
if (isAgentCommand(line, score) || WINDOW_ONLY.test(line)) return false;
|
|
176
|
+
if (catalogMiss(line) !== undefined) return true;
|
|
177
|
+
if (line.startsWith("/")) return /^\/[a-z]/i.test(line);
|
|
178
|
+
if (!/^[a-z]/.test(line) || /[.!?:]$/.test(line)) return false;
|
|
179
|
+
if (looksLikeProse(line) && !usageHint(line)) return false;
|
|
180
|
+
return usageHint(line) !== undefined || nearestCommand(line) !== undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The red receipt for a broken agent command: what failed, the usage or
|
|
185
|
+
* the nearest command when there is one. Never prose.
|
|
186
|
+
*/
|
|
187
|
+
export function brokenCommandReceipt(line: string): string {
|
|
188
|
+
const miss = catalogMiss(line);
|
|
189
|
+
if (miss !== undefined)
|
|
190
|
+
return `✗ ${line} · ${miss ? `did you mean ${miss}?` : "no such pattern · pattern list"}`;
|
|
191
|
+
const usage = usageHint(line);
|
|
192
|
+
const verb = nearestCommand(line);
|
|
193
|
+
// `patern house` → `pattern house`: the fixed verb with its arguments.
|
|
194
|
+
const nearer = verb
|
|
195
|
+
? [verb, ...line.trim().split(/\s+/).slice(1)].join(" ")
|
|
196
|
+
: undefined;
|
|
197
|
+
const hint = usage ?? (nearer ? `did you mean ${nearer}?` : undefined);
|
|
198
|
+
return `✗ ${line}${hint ? ` · ${hint}` : " · not a dawg command"}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** One key of the qwerty play keyboard and how to reach its octave. */
|
|
202
|
+
export type KeyPress = Readonly<{
|
|
203
|
+
/** The note key (`a`, `w`, `;`), lower case. */
|
|
204
|
+
key: string;
|
|
205
|
+
/** Octave keys pressed first: `z` down, `x` up, repeated. */
|
|
206
|
+
octave: string;
|
|
207
|
+
}>;
|
|
208
|
+
|
|
209
|
+
const KEY_BY_OFFSET = new Map<number, string>(
|
|
210
|
+
Object.entries(NOTE_KEYS)
|
|
211
|
+
.sort((a, b) => a[1] - b[1])
|
|
212
|
+
.reverse()
|
|
213
|
+
.map(([key, offset]) => [offset, key] as const),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The play-mode key for `pitch` on a keyboard whose base C is `base`: the
|
|
218
|
+
* home row first, moving octaves (Z/X) only when the note is off the keys.
|
|
219
|
+
*/
|
|
220
|
+
export function keyForPitch(pitch: number, base: number): KeyPress {
|
|
221
|
+
for (let shift = 0; shift <= 10; shift += 1)
|
|
222
|
+
for (const octaves of shift === 0 ? [0] : [shift, -shift]) {
|
|
223
|
+
const key = KEY_BY_OFFSET.get(pitch - base - octaves * 12);
|
|
224
|
+
if (key)
|
|
225
|
+
return {
|
|
226
|
+
key,
|
|
227
|
+
octave:
|
|
228
|
+
octaves === 0
|
|
229
|
+
? ""
|
|
230
|
+
: (octaves > 0 ? "x" : "z").repeat(Math.abs(octaves)),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { key: "a", octave: "" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The GM pitch of a drum word (`kick`, `hat`), when it is one. */
|
|
237
|
+
export function drumPitch(voice: string): number | undefined {
|
|
238
|
+
const parsed = parseDrumVoice(voice.toLowerCase());
|
|
239
|
+
return parsed === undefined ? undefined : drumVoicePitch(parsed);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type Gesture =
|
|
243
|
+
| Readonly<{ kind: "typed"; command: string; caption: string }>
|
|
244
|
+
| Readonly<{
|
|
245
|
+
kind: "fader";
|
|
246
|
+
command: string;
|
|
247
|
+
caption: string;
|
|
248
|
+
/** The bare parameter the fader drawer opens on (`fx reverb mix`). */
|
|
249
|
+
param: string;
|
|
250
|
+
value: number;
|
|
251
|
+
}>
|
|
252
|
+
| Readonly<{
|
|
253
|
+
kind: "keys";
|
|
254
|
+
command: string;
|
|
255
|
+
caption: string;
|
|
256
|
+
notes: readonly Readonly<{
|
|
257
|
+
pitch: number;
|
|
258
|
+
start: number;
|
|
259
|
+
duration: number;
|
|
260
|
+
press: KeyPress;
|
|
261
|
+
}>[];
|
|
262
|
+
}>;
|
|
263
|
+
|
|
264
|
+
const FADER_COMMAND =
|
|
265
|
+
/^\/?(volume|pan|fx\s+[a-z][\w-]*\s+[a-z][\w-]*)\s+(-?\d+(?:\.\d+)?)$/i;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The human gesture for a command: a fader slide for a parameter value, the
|
|
269
|
+
* play-mode keys for notes and drum hits, otherwise the typed command.
|
|
270
|
+
*/
|
|
271
|
+
export function gestureFor(
|
|
272
|
+
command: string,
|
|
273
|
+
context: Readonly<{ score: TrackScore; trackId: string }>,
|
|
274
|
+
): Gesture {
|
|
275
|
+
const fader = command.match(FADER_COMMAND);
|
|
276
|
+
if (fader) {
|
|
277
|
+
const param = fader[1]!.toLowerCase().replace(/\s+/g, " ");
|
|
278
|
+
const value = Number(fader[2]);
|
|
279
|
+
return {
|
|
280
|
+
kind: "fader",
|
|
281
|
+
command,
|
|
282
|
+
param,
|
|
283
|
+
value,
|
|
284
|
+
caption: `fader · ${param} → ${fader[2]} · or type ${command}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const track = context.score.tracks.find(
|
|
288
|
+
(candidate) => candidate.id === context.trackId,
|
|
289
|
+
);
|
|
290
|
+
const drums = isDrumInstrument(track?.instrument);
|
|
291
|
+
const hit = command.match(/^\/?hit\s+([a-z]+)\s+at\s+(\d+(?:\.\d+)?)$/i);
|
|
292
|
+
if (hit) {
|
|
293
|
+
const pitch = drumPitch(hit[1]!);
|
|
294
|
+
if (pitch !== undefined) {
|
|
295
|
+
const press = keyForPitch(pitch, 36);
|
|
296
|
+
return {
|
|
297
|
+
kind: "keys",
|
|
298
|
+
command,
|
|
299
|
+
caption: `playing on keys: ${press.key.toUpperCase()} (${hit[1]!.toLowerCase()}) · ctrl-p play mode · or type ${command}`,
|
|
300
|
+
notes: [{ pitch, start: Number(hit[2]), duration: 0.25, press }],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const pattern = command.match(/^\/?pattern\s+([a-z]+)\s+([\d.\s]+)$/i);
|
|
305
|
+
if (pattern) {
|
|
306
|
+
const pitch = drumPitch(pattern[1]!);
|
|
307
|
+
const beats = pattern[2]!
|
|
308
|
+
.trim()
|
|
309
|
+
.split(/\s+/)
|
|
310
|
+
.map(Number)
|
|
311
|
+
.filter((beat) => Number.isFinite(beat));
|
|
312
|
+
if (pitch !== undefined && beats.length > 0) {
|
|
313
|
+
const press = keyForPitch(pitch, 36);
|
|
314
|
+
return {
|
|
315
|
+
kind: "keys",
|
|
316
|
+
command,
|
|
317
|
+
caption: `playing on keys: ${press.key.toUpperCase()} ×${beats.length} (${pattern[1]!.toLowerCase()}) · ctrl-p play mode · or type ${command}`,
|
|
318
|
+
notes: beats.map((start) => ({ pitch, start, duration: 0.25, press })),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const parsed = parsePrompt(command);
|
|
323
|
+
if (parsed?.type === "add-note") {
|
|
324
|
+
const base = drums ? 36 : defaultBaseFor(track?.instrument);
|
|
325
|
+
const press = keyForPitch(parsed.pitch, base);
|
|
326
|
+
const octave = press.octave
|
|
327
|
+
? `, octave ${press.octave.toUpperCase().split("").join(" ")}`
|
|
328
|
+
: "";
|
|
329
|
+
return {
|
|
330
|
+
kind: "keys",
|
|
331
|
+
command,
|
|
332
|
+
caption: `playing on keys: ${press.key.toUpperCase()}${octave} · ctrl-p play mode · or type ${command}`,
|
|
333
|
+
notes: [
|
|
334
|
+
{
|
|
335
|
+
pitch: parsed.pitch,
|
|
336
|
+
start: parsed.start,
|
|
337
|
+
duration: parsed.duration,
|
|
338
|
+
press,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return { kind: "typed", command, caption: `typing ${command}` };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Where each command's setting lives in Ctrl-K, as a `/menu` id. */
|
|
347
|
+
const MENU_HOME: Readonly<Record<string, string>> = {
|
|
348
|
+
synth: "sound",
|
|
349
|
+
sound: "sound",
|
|
350
|
+
tempo: "tempo",
|
|
351
|
+
bpm: "tempo",
|
|
352
|
+
meter: "tempo",
|
|
353
|
+
euclid: "rhythm",
|
|
354
|
+
groove: "patterns",
|
|
355
|
+
pattern: "patterns",
|
|
356
|
+
kit: "kits",
|
|
357
|
+
master: "master",
|
|
358
|
+
automate: "automation",
|
|
359
|
+
section: "arrange",
|
|
360
|
+
form: "arrange",
|
|
361
|
+
style: "style",
|
|
362
|
+
tuning: "tuning",
|
|
363
|
+
key: "chords",
|
|
364
|
+
chords: "chords",
|
|
365
|
+
sing: "voice",
|
|
366
|
+
lyrics: "voice",
|
|
367
|
+
autotune: "voice",
|
|
368
|
+
vocode: "voice",
|
|
369
|
+
export: "export",
|
|
370
|
+
model: "agent",
|
|
371
|
+
showme: "agent",
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The menu path a command's setting also lives at, for the finish hint:
|
|
376
|
+
* `Ctrl-K › Effects › reverb`, from the menu's live labels (menuPath).
|
|
377
|
+
*/
|
|
378
|
+
export function menuPathFor(command: string): string | undefined {
|
|
379
|
+
const words = command.replace(/^\//, "").toLowerCase().split(/\s+/);
|
|
380
|
+
const verb = words[0] ?? "";
|
|
381
|
+
if (verb === "volume" || verb === "pan") {
|
|
382
|
+
const mix = menuPath("mix");
|
|
383
|
+
return mix && `${mix} › ${verb}`;
|
|
384
|
+
}
|
|
385
|
+
if (verb === "fx" && words[1]) {
|
|
386
|
+
// The row's live label and real place (`more effects › phaser`, the
|
|
387
|
+
// formant shift under Voice); Effects when nothing names it.
|
|
388
|
+
const id =
|
|
389
|
+
words[1] === "rig"
|
|
390
|
+
? "guitar rig"
|
|
391
|
+
: (parseEffectName(words[1]) ?? words[1]);
|
|
392
|
+
return menuPath(id) ?? menuPath("effects");
|
|
393
|
+
}
|
|
394
|
+
const home = MENU_HOME[verb];
|
|
395
|
+
return home ? menuPath(home) : undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Glide length for a fader: short enough to never stall a turn. */
|
|
399
|
+
export const GLIDE_MS = 150;
|
|
400
|
+
/** Step between glide values: each step re-renders the loop, so not faster. */
|
|
401
|
+
export const GLIDE_STEP_MS = 30;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Eased intermediate values from `from` to `to` (exclusive of `from`,
|
|
405
|
+
* ending exactly on `to`), one per `stepMs` over `ms`.
|
|
406
|
+
*/
|
|
407
|
+
export function glideValues(
|
|
408
|
+
from: number,
|
|
409
|
+
to: number,
|
|
410
|
+
ms = GLIDE_MS,
|
|
411
|
+
stepMs = GLIDE_STEP_MS,
|
|
412
|
+
): number[] {
|
|
413
|
+
if (!Number.isFinite(from) || from === to) return [to];
|
|
414
|
+
const steps = Math.max(1, Math.round(ms / stepMs));
|
|
415
|
+
const values: number[] = [];
|
|
416
|
+
for (let index = 1; index < steps; index += 1) {
|
|
417
|
+
const t = index / steps;
|
|
418
|
+
const eased = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
419
|
+
values.push(Number((from + (to - from) * eased).toFixed(4)));
|
|
420
|
+
}
|
|
421
|
+
values.push(to);
|
|
422
|
+
return values;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* When each streamed note sounds. The first note anchors a clock at its
|
|
427
|
+
* arrival; a note at beat `b` is due `b` beats later at the tempo. A note
|
|
428
|
+
* that arrives before it is due waits for its place on the grid (the stream
|
|
429
|
+
* runs ahead: stay in time); one that arrives late plays at once (the stream
|
|
430
|
+
* is slower than the tempo: step entry). The score never waits on this.
|
|
431
|
+
*/
|
|
432
|
+
export class NoteScheduler {
|
|
433
|
+
private anchor: { atMs: number; beat: number } | undefined;
|
|
434
|
+
private lastDueMs = -Infinity;
|
|
435
|
+
|
|
436
|
+
constructor(private readonly tempoBpm: () => number) {}
|
|
437
|
+
|
|
438
|
+
/** Monotonic ms at which the note should sound, and how it is shown. */
|
|
439
|
+
schedule(
|
|
440
|
+
beat: number,
|
|
441
|
+
nowMs: number,
|
|
442
|
+
): { atMs: number; mode: "in-time" | "step" } {
|
|
443
|
+
const msPerBeat = 60_000 / Math.max(1, this.tempoBpm());
|
|
444
|
+
if (!this.anchor || beat < this.anchor.beat) {
|
|
445
|
+
this.anchor = { atMs: nowMs, beat };
|
|
446
|
+
this.lastDueMs = nowMs;
|
|
447
|
+
return { atMs: nowMs, mode: "in-time" };
|
|
448
|
+
}
|
|
449
|
+
const due = this.anchor.atMs + (beat - this.anchor.beat) * msPerBeat;
|
|
450
|
+
if (due >= nowMs) {
|
|
451
|
+
this.lastDueMs = Math.max(this.lastDueMs, due);
|
|
452
|
+
return { atMs: due, mode: "in-time" };
|
|
453
|
+
}
|
|
454
|
+
// Late: re-anchor so the notes after it keep their spacing from here.
|
|
455
|
+
this.anchor = { atMs: nowMs, beat };
|
|
456
|
+
this.lastDueMs = nowMs;
|
|
457
|
+
return { atMs: nowMs, mode: "step" };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
reset(): void {
|
|
461
|
+
this.anchor = undefined;
|
|
462
|
+
this.lastDueMs = -Infinity;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The finish hint: how to do the turn's last gesture by hand. */
|
|
467
|
+
export function finishHint(commands: readonly string[]): string | undefined {
|
|
468
|
+
const last = commands.at(-1);
|
|
469
|
+
if (!last) return undefined;
|
|
470
|
+
const menu = menuPathFor(last);
|
|
471
|
+
const count = commands.length;
|
|
472
|
+
return `do it yourself: type ${last}${count > 1 ? ` (+${count - 1} more in ^o log)` : ""}${menu ? ` · or ${menu}` : ""}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** `dawg media` verbs by agent tool: the CLI a person runs for the same job. */
|
|
476
|
+
const MEDIA_VERB: Readonly<Record<string, string>> = {
|
|
477
|
+
download_audio: "download",
|
|
478
|
+
split_stems: "stems",
|
|
479
|
+
analyze_audio: "analyze",
|
|
480
|
+
transcribe_notes: "notes",
|
|
481
|
+
import_sample: "sample",
|
|
482
|
+
transcribe_lyrics: "lyrics",
|
|
483
|
+
make_wavetable: "wavetable",
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The caption for a JSON tool the command language cannot express: names
|
|
488
|
+
* the manual route (`dawg media stems`, the project files) when one exists.
|
|
489
|
+
*/
|
|
490
|
+
export function toolCaption(name: string): string | undefined {
|
|
491
|
+
const verb = MEDIA_VERB[name];
|
|
492
|
+
if (verb) return `media · or run dawg media ${verb} in a shell`;
|
|
493
|
+
if (/^(?:write_file|edit_file)$/.test(name))
|
|
494
|
+
return "editing a project file · song.ts and tracks/ are yours to edit";
|
|
495
|
+
if (/^(?:web_search|fetch_url)$/.test(name)) return "searching the web";
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enter during an agent turn: a typed command that parses locally (a music
|
|
3
|
+
* edit, transport, a mix change) runs at once beside the turn, so it never
|
|
4
|
+
* waits on the model; anything else steers the turn. Slash commands keep
|
|
5
|
+
* steering off: most of them change the session (provider, resume, model)
|
|
6
|
+
* under the running turn.
|
|
7
|
+
*/
|
|
8
|
+
export function routeDuringTurn(
|
|
9
|
+
text: string,
|
|
10
|
+
parsesLocally: (text: string) => boolean,
|
|
11
|
+
): "local" | "steer" {
|
|
12
|
+
const value = text.trim();
|
|
13
|
+
if (value === "" || value.startsWith("/")) return "steer";
|
|
14
|
+
return parsesLocally(value) ? "local" : "steer";
|
|
15
|
+
}
|