@hraness/dawg 0.0.0-stage → 0.3.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 +327 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/core/diff.ts +249 -0
- package/core/drums.ts +102 -0
- package/core/key.ts +43 -0
- package/core/loop.ts +78 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +1388 -0
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +45 -4
- package/src/agent/agent.ts +853 -0
- package/src/agent/brief.ts +160 -0
- package/src/agent/gateway.ts +441 -0
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +157 -0
- package/src/agent/planner.ts +259 -0
- package/src/agent/provider.ts +454 -0
- package/src/agent/sse.ts +114 -0
- package/src/agent/tools.ts +1373 -0
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +262 -0
- package/src/agent/xcb.ts +579 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +841 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +134 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +861 -0
- package/src/auth/cli.ts +231 -0
- package/src/auth/credentials.ts +411 -0
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +1191 -0
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +207 -0
- package/src/auth/tui.ts +107 -0
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +69 -0
- package/src/commands/music.ts +461 -0
- package/src/commands/sample.ts +302 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +2209 -0
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +292 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +121 -0
- package/src/session/attach.ts +181 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +740 -0
- package/src/session/delta.ts +249 -0
- package/src/session/list.ts +180 -0
- package/src/session/lock.ts +92 -0
- package/src/session/meta.ts +253 -0
- package/src/session/naming.ts +430 -0
- package/src/session/port.ts +481 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +168 -0
- package/src/session/store.ts +581 -0
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +364 -0
- package/tui/app.ts +1372 -0
- package/tui/drums.ts +65 -0
- package/tui/highway.ts +921 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +80 -0
- package/tui/play-strip.ts +143 -0
- package/tui/prompt.ts +609 -0
- package/tui/render.ts +124 -0
- package/tui/screen.ts +247 -0
- package/tui/text.ts +72 -0
- package/tui/theme.ts +451 -0
package/src/agent/ops.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { pitchToMidi } from "../../core/pitch.ts";
|
|
2
|
+
import { SCORE_LIMITS } from "../../core/score.ts";
|
|
3
|
+
|
|
4
|
+
export { pitchToMidi };
|
|
5
|
+
|
|
6
|
+
export type AgentOperation =
|
|
7
|
+
| { type: "add-track"; trackId: string }
|
|
8
|
+
| {
|
|
9
|
+
type: "add-note";
|
|
10
|
+
pitch: number;
|
|
11
|
+
start: number;
|
|
12
|
+
duration: number;
|
|
13
|
+
velocity: number;
|
|
14
|
+
}
|
|
15
|
+
| { type: "remove-note"; noteId: string }
|
|
16
|
+
| {
|
|
17
|
+
type: "update-note";
|
|
18
|
+
noteId: string;
|
|
19
|
+
patch: {
|
|
20
|
+
start?: number;
|
|
21
|
+
duration?: number;
|
|
22
|
+
pitch?: number;
|
|
23
|
+
velocity?: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
| { type: "set-tempo"; tempoBpm: number }
|
|
27
|
+
| { type: "set-bars"; bars: number }
|
|
28
|
+
| { type: "extend-bars"; bars: number }
|
|
29
|
+
| {
|
|
30
|
+
type: "track";
|
|
31
|
+
patch: {
|
|
32
|
+
instrument?: string;
|
|
33
|
+
muted?: boolean;
|
|
34
|
+
volume?: number;
|
|
35
|
+
pan?: number;
|
|
36
|
+
name?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
type: "automation";
|
|
41
|
+
parameter: "volume" | "pan";
|
|
42
|
+
points: readonly { beat: number; value: number }[];
|
|
43
|
+
}
|
|
44
|
+
| { type: "clear-track" }
|
|
45
|
+
| { type: "transport"; action: "play" | "pause" | "toggle" };
|
|
46
|
+
|
|
47
|
+
export function parsePrompt(prompt: string): AgentOperation | undefined {
|
|
48
|
+
const text = prompt.trim().toLowerCase();
|
|
49
|
+
if (/^(play|start)\b/.test(text))
|
|
50
|
+
return { type: "transport", action: "play" };
|
|
51
|
+
if (/^(pause|stop)\b/.test(text))
|
|
52
|
+
return { type: "transport", action: "pause" };
|
|
53
|
+
if (/^toggle\b/.test(text)) return { type: "transport", action: "toggle" };
|
|
54
|
+
const track = text.match(/^(?:add\s+)?track\s+([a-z0-9._-]{1,64})$/);
|
|
55
|
+
if (track) return { type: "add-track", trackId: track[1]! };
|
|
56
|
+
const bars = text.match(/^bars\s+(\d+)$/);
|
|
57
|
+
const extension = text.match(/^extend\s+(\d+)\s+bars?$/);
|
|
58
|
+
if (bars || extension) {
|
|
59
|
+
const count = Number((bars ?? extension)![1]);
|
|
60
|
+
if (Number.isInteger(count) && count >= 1 && count <= SCORE_LIMITS.maxBars)
|
|
61
|
+
return { type: bars ? "set-bars" : "extend-bars", bars: count };
|
|
62
|
+
}
|
|
63
|
+
const tempo = text.match(/^(?:tempo|bpm)\s+(\d+(?:\.\d+)?)\s*$/);
|
|
64
|
+
if (tempo) {
|
|
65
|
+
const tempoBpm = Number(tempo[1]);
|
|
66
|
+
if (Number.isFinite(tempoBpm)) return { type: "set-tempo", tempoBpm };
|
|
67
|
+
}
|
|
68
|
+
if (/^(?:clear|clear\s+track|remove\s+all)$/.test(text))
|
|
69
|
+
return { type: "clear-track" };
|
|
70
|
+
const instrument = text.match(
|
|
71
|
+
/^(?:instrument|sound|voice)\s+([a-z0-9._ -]{1,64})$/,
|
|
72
|
+
);
|
|
73
|
+
if (instrument)
|
|
74
|
+
return { type: "track", patch: { instrument: instrument[1]!.trim() } };
|
|
75
|
+
if (/^(?:mute|silence)\b/.test(text))
|
|
76
|
+
return { type: "track", patch: { muted: true } };
|
|
77
|
+
if (/^(?:unmute|unsilence)\b/.test(text))
|
|
78
|
+
return { type: "track", patch: { muted: false } };
|
|
79
|
+
const volume = text.match(/^(?:volume|vol)\s+(0(?:\.\d+)?|1(?:\.0+)?)$/);
|
|
80
|
+
if (volume) return { type: "track", patch: { volume: Number(volume[1]) } };
|
|
81
|
+
const clearAutomation = text.match(
|
|
82
|
+
/^(?:clear|reset)\s+(?:(volume|pan)\s+)?automation$/,
|
|
83
|
+
);
|
|
84
|
+
if (clearAutomation)
|
|
85
|
+
return {
|
|
86
|
+
type: "automation",
|
|
87
|
+
parameter:
|
|
88
|
+
(clearAutomation[1] as "volume" | "pan" | undefined) ?? "volume",
|
|
89
|
+
points: [],
|
|
90
|
+
};
|
|
91
|
+
const automation = text.match(
|
|
92
|
+
/^(?:automate|automation)\s+(volume|pan)\s+at\s+(\d+(?:\.\d+)?)\s+(-?(?:0(?:\.\d+)?|1(?:\.0+)?))$/,
|
|
93
|
+
);
|
|
94
|
+
if (automation) {
|
|
95
|
+
const parameter = automation[1] as "volume" | "pan";
|
|
96
|
+
const beat = Number(automation[2]);
|
|
97
|
+
const value = Number(automation[3]);
|
|
98
|
+
if (
|
|
99
|
+
Number.isFinite(beat) &&
|
|
100
|
+
Number.isFinite(value) &&
|
|
101
|
+
(parameter === "pan" ? value >= -1 : value >= 0)
|
|
102
|
+
)
|
|
103
|
+
return {
|
|
104
|
+
type: "automation",
|
|
105
|
+
parameter,
|
|
106
|
+
points: [{ beat, value }],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const pan = text.match(/^(?:pan)\s+(-?1(?:\.0+)?|-?0(?:\.\d+)?)$/);
|
|
110
|
+
if (pan) return { type: "track", patch: { pan: Number(pan[1]) } };
|
|
111
|
+
const remove = text.match(
|
|
112
|
+
/^(?:remove|delete)(?:\s+note)?\s+([a-z0-9._-]{1,64})$/,
|
|
113
|
+
);
|
|
114
|
+
if (remove) return { type: "remove-note", noteId: remove[1]! };
|
|
115
|
+
const move = text.match(
|
|
116
|
+
/^(?:move|shift)\s+(?:note\s+)?([a-z0-9._-]{1,64})\s+(?:to|at)\s+(\d+(?:\.\d+)?)$/,
|
|
117
|
+
);
|
|
118
|
+
if (move)
|
|
119
|
+
return {
|
|
120
|
+
type: "update-note",
|
|
121
|
+
noteId: move[1]!,
|
|
122
|
+
patch: { start: Number(move[2]) },
|
|
123
|
+
};
|
|
124
|
+
const length = text.match(
|
|
125
|
+
/^(?:length|duration)\s+(?:note\s+)?([a-z0-9._-]{1,64})\s+(\d+(?:\.\d+)?)$/,
|
|
126
|
+
);
|
|
127
|
+
if (length)
|
|
128
|
+
return {
|
|
129
|
+
type: "update-note",
|
|
130
|
+
noteId: length[1]!,
|
|
131
|
+
patch: { duration: Number(length[2]) },
|
|
132
|
+
};
|
|
133
|
+
const velocity = text.match(
|
|
134
|
+
/^(?:velocity|vel)\s+(?:note\s+)?([a-z0-9._-]{1,64})\s+(0(?:\.\d+)?|1(?:\.0+)?)$/,
|
|
135
|
+
);
|
|
136
|
+
if (velocity)
|
|
137
|
+
return {
|
|
138
|
+
type: "update-note",
|
|
139
|
+
noteId: velocity[1]!,
|
|
140
|
+
patch: { velocity: Number(velocity[2]) },
|
|
141
|
+
};
|
|
142
|
+
const match = text.match(
|
|
143
|
+
/^(?:add|put)\s+(?:note\s+)?([a-g](?:#|b)?-?\d+)\s+(?:at\s+)?(\d+(?:\.\d+)?)\s*(?:for|dur|duration)?\s*(\d+(?:\.\d+)?)?$/,
|
|
144
|
+
);
|
|
145
|
+
if (!match) return undefined;
|
|
146
|
+
const pitch = pitchToMidi(match[1] ?? "c4");
|
|
147
|
+
const start = Number(match[2] ?? 0);
|
|
148
|
+
const duration = Number(match[3] ?? 1);
|
|
149
|
+
if (
|
|
150
|
+
!Number.isFinite(pitch) ||
|
|
151
|
+
!Number.isFinite(start) ||
|
|
152
|
+
!Number.isFinite(duration) ||
|
|
153
|
+
duration <= 0
|
|
154
|
+
)
|
|
155
|
+
return undefined;
|
|
156
|
+
return { type: "add-note", pitch, start, duration, velocity: 0.8 };
|
|
157
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SCORE_LIMITS,
|
|
3
|
+
AUTOMATION_LANES,
|
|
4
|
+
isAutomationParameter,
|
|
5
|
+
normalizeDelay,
|
|
6
|
+
normalizeFilter,
|
|
7
|
+
normalizeReverb,
|
|
8
|
+
normalizeSampler,
|
|
9
|
+
scoreFromJSON,
|
|
10
|
+
type ScoreOperation,
|
|
11
|
+
} from "../../core/score.ts";
|
|
12
|
+
import { DRUM_VOICES } from "../../core/drums.ts";
|
|
13
|
+
|
|
14
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
15
|
+
const MAX_OPERATIONS = 32;
|
|
16
|
+
|
|
17
|
+
export type CompositionPlan = Readonly<{
|
|
18
|
+
operations: readonly ScoreOperation[];
|
|
19
|
+
explanation?: string;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
/** Parse the only model output shape that the local reducer is allowed to apply. */
|
|
23
|
+
export function parseCompositionPlan(text: string): CompositionPlan {
|
|
24
|
+
if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES)
|
|
25
|
+
throw new Error("agent response is too large");
|
|
26
|
+
const candidate = text.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1] ?? text;
|
|
27
|
+
let value: unknown;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(candidate.trim());
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error("agent response was not valid JSON");
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
!isRecord(value) ||
|
|
35
|
+
!Array.isArray(value.operations) ||
|
|
36
|
+
value.operations.length > MAX_OPERATIONS
|
|
37
|
+
) {
|
|
38
|
+
throw new Error("agent response must contain a bounded operations array");
|
|
39
|
+
}
|
|
40
|
+
const operations = value.operations.map(parseOperation);
|
|
41
|
+
const explanation =
|
|
42
|
+
typeof value.explanation === "string"
|
|
43
|
+
? value.explanation.slice(0, 2_000)
|
|
44
|
+
: undefined;
|
|
45
|
+
return explanation === undefined
|
|
46
|
+
? { operations }
|
|
47
|
+
: { operations, explanation };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate one foreign operation against the bounded planner contract. Every
|
|
52
|
+
* tool call and legacy JSON plan passes through here before the reducer sees it.
|
|
53
|
+
*/
|
|
54
|
+
export function validateAgentOperation(value: unknown): ScoreOperation {
|
|
55
|
+
return parseOperation(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseOperation(value: unknown): ScoreOperation {
|
|
59
|
+
if (!isRecord(value) || typeof value.type !== "string")
|
|
60
|
+
throw new Error("agent operation is malformed");
|
|
61
|
+
if (value.type === "addTrack") {
|
|
62
|
+
const track = scoreFromJSON({ tracks: [value.track] }).tracks[0]!;
|
|
63
|
+
return { type: "addTrack", track };
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
value.type === "setBars" &&
|
|
67
|
+
typeof value.bars === "number" &&
|
|
68
|
+
Number.isInteger(value.bars) &&
|
|
69
|
+
value.bars >= 1 &&
|
|
70
|
+
value.bars <= SCORE_LIMITS.maxBars
|
|
71
|
+
) {
|
|
72
|
+
return { type: "setBars", bars: value.bars };
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
value.type === "removeNote" &&
|
|
76
|
+
typeof value.noteId === "string" &&
|
|
77
|
+
value.noteId.length <= 64
|
|
78
|
+
) {
|
|
79
|
+
return { type: "removeNote", noteId: value.noteId };
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
value.type === "setTempo" &&
|
|
83
|
+
typeof value.tempoBpm === "number" &&
|
|
84
|
+
Number.isFinite(value.tempoBpm)
|
|
85
|
+
) {
|
|
86
|
+
return { type: "setTempo", tempoBpm: value.tempoBpm };
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
value.type === "clearTrack" &&
|
|
90
|
+
typeof value.trackId === "string" &&
|
|
91
|
+
value.trackId.length <= 64
|
|
92
|
+
) {
|
|
93
|
+
return { type: "clearTrack", trackId: value.trackId };
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
value.type === "removeTrack" &&
|
|
97
|
+
typeof value.trackId === "string" &&
|
|
98
|
+
value.trackId.length <= 64
|
|
99
|
+
) {
|
|
100
|
+
return { type: "removeTrack", trackId: value.trackId };
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
value.type === "moveTrack" &&
|
|
104
|
+
typeof value.trackId === "string" &&
|
|
105
|
+
value.trackId.length <= 64 &&
|
|
106
|
+
typeof value.index === "number" &&
|
|
107
|
+
Number.isInteger(value.index) &&
|
|
108
|
+
value.index >= 0 &&
|
|
109
|
+
value.index < SCORE_LIMITS.maxTracks
|
|
110
|
+
) {
|
|
111
|
+
return { type: "moveTrack", trackId: value.trackId, index: value.index };
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
value.type === "setKey" &&
|
|
115
|
+
(value.key === null ||
|
|
116
|
+
(typeof value.key === "string" &&
|
|
117
|
+
value.key.length <= SCORE_LIMITS.maxNameLength))
|
|
118
|
+
) {
|
|
119
|
+
return { type: "setKey", key: value.key };
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
value.type === "setMeter" &&
|
|
123
|
+
typeof value.beatsPerBar === "number" &&
|
|
124
|
+
Number.isInteger(value.beatsPerBar) &&
|
|
125
|
+
value.beatsPerBar >= 1 &&
|
|
126
|
+
value.beatsPerBar <= SCORE_LIMITS.maxBeatsPerBar
|
|
127
|
+
) {
|
|
128
|
+
return { type: "setMeter", beatsPerBar: value.beatsPerBar };
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
value.type === "updateTrack" &&
|
|
132
|
+
typeof value.trackId === "string" &&
|
|
133
|
+
value.trackId.length <= 64 &&
|
|
134
|
+
isRecord(value.patch)
|
|
135
|
+
) {
|
|
136
|
+
const patch = value.patch;
|
|
137
|
+
const safe: Record<string, unknown> = {};
|
|
138
|
+
if (typeof patch.name === "string") safe.name = patch.name.slice(0, 96);
|
|
139
|
+
if (typeof patch.instrument === "string")
|
|
140
|
+
safe.instrument = patch.instrument.slice(0, 64);
|
|
141
|
+
if (typeof patch.muted === "boolean") safe.muted = patch.muted;
|
|
142
|
+
if (typeof patch.volume === "number" && Number.isFinite(patch.volume))
|
|
143
|
+
safe.volume = patch.volume;
|
|
144
|
+
if (typeof patch.pan === "number" && Number.isFinite(patch.pan))
|
|
145
|
+
safe.pan = patch.pan;
|
|
146
|
+
if (typeof patch.solo === "boolean") safe.solo = patch.solo;
|
|
147
|
+
// Effects reuse the score's bounded validators; null removes an effect.
|
|
148
|
+
if (patch.filter !== undefined)
|
|
149
|
+
safe.filter = normalizeFilter(patch.filter) ?? null;
|
|
150
|
+
if (patch.delay !== undefined)
|
|
151
|
+
safe.delay = normalizeDelay(patch.delay) ?? null;
|
|
152
|
+
if (patch.reverb !== undefined)
|
|
153
|
+
safe.reverb = normalizeReverb(patch.reverb) ?? null;
|
|
154
|
+
if (patch.sampler !== undefined)
|
|
155
|
+
safe.sampler = normalizeSampler(patch.sampler) ?? null;
|
|
156
|
+
return { type: "updateTrack", trackId: value.trackId, patch: safe };
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
value.type === "setAutomation" &&
|
|
160
|
+
typeof value.trackId === "string" &&
|
|
161
|
+
value.trackId.length <= SCORE_LIMITS.maxIdLength &&
|
|
162
|
+
isAutomationParameter(value.parameter) &&
|
|
163
|
+
Array.isArray(value.points) &&
|
|
164
|
+
value.points.length <= SCORE_LIMITS.maxAutomationPoints
|
|
165
|
+
) {
|
|
166
|
+
const { min: minValue, max: maxValue } = AUTOMATION_LANES[value.parameter];
|
|
167
|
+
const points = value.points.map((candidate) => {
|
|
168
|
+
if (!isRecord(candidate))
|
|
169
|
+
throw new Error("agent automation point is malformed");
|
|
170
|
+
if (
|
|
171
|
+
typeof candidate.tick !== "number" ||
|
|
172
|
+
!Number.isInteger(candidate.tick) ||
|
|
173
|
+
candidate.tick < 0 ||
|
|
174
|
+
candidate.tick > SCORE_LIMITS.maxTick ||
|
|
175
|
+
typeof candidate.value !== "number" ||
|
|
176
|
+
!Number.isFinite(candidate.value) ||
|
|
177
|
+
candidate.value < minValue ||
|
|
178
|
+
candidate.value > maxValue
|
|
179
|
+
) {
|
|
180
|
+
throw new Error("agent automation point is invalid");
|
|
181
|
+
}
|
|
182
|
+
return { tick: candidate.tick, value: candidate.value };
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
type: "setAutomation",
|
|
186
|
+
trackId: value.trackId,
|
|
187
|
+
parameter: value.parameter,
|
|
188
|
+
points,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (
|
|
192
|
+
value.type === "updateNote" &&
|
|
193
|
+
typeof value.noteId === "string" &&
|
|
194
|
+
value.noteId.length <= 64 &&
|
|
195
|
+
isRecord(value.patch)
|
|
196
|
+
) {
|
|
197
|
+
const patch = value.patch;
|
|
198
|
+
const safe: Record<string, number> = {};
|
|
199
|
+
for (const key of [
|
|
200
|
+
"startTick",
|
|
201
|
+
"durationTicks",
|
|
202
|
+
"pitch",
|
|
203
|
+
"velocity",
|
|
204
|
+
] as const) {
|
|
205
|
+
if (typeof patch[key] === "number" && Number.isFinite(patch[key]))
|
|
206
|
+
safe[key] = patch[key];
|
|
207
|
+
}
|
|
208
|
+
return { type: "updateNote", noteId: value.noteId, patch: safe };
|
|
209
|
+
}
|
|
210
|
+
if (value.type === "addNote" && isRecord(value.note)) {
|
|
211
|
+
const note = value.note;
|
|
212
|
+
if (
|
|
213
|
+
typeof note.id !== "string" ||
|
|
214
|
+
typeof note.trackId !== "string" ||
|
|
215
|
+
typeof note.pitch !== "number" ||
|
|
216
|
+
typeof note.velocity !== "number"
|
|
217
|
+
) {
|
|
218
|
+
throw new Error("agent note is malformed");
|
|
219
|
+
}
|
|
220
|
+
const ticks =
|
|
221
|
+
typeof note.startTick === "number" &&
|
|
222
|
+
typeof note.durationTicks === "number";
|
|
223
|
+
const start = ticks ? note.startTick : note.start;
|
|
224
|
+
const duration = ticks ? note.durationTicks : note.duration;
|
|
225
|
+
if (typeof start !== "number" || typeof duration !== "number")
|
|
226
|
+
throw new Error("agent note timing is malformed");
|
|
227
|
+
if (
|
|
228
|
+
!Number.isFinite(start) ||
|
|
229
|
+
!Number.isFinite(duration) ||
|
|
230
|
+
duration <= 0 ||
|
|
231
|
+
!Number.isFinite(note.pitch) ||
|
|
232
|
+
!Number.isFinite(note.velocity)
|
|
233
|
+
) {
|
|
234
|
+
throw new Error("agent note timing is invalid");
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
type: "addNote",
|
|
238
|
+
note: {
|
|
239
|
+
id: note.id.slice(0, 64),
|
|
240
|
+
trackId: note.trackId.slice(0, 64),
|
|
241
|
+
...(ticks
|
|
242
|
+
? { startTick: start, durationTicks: duration }
|
|
243
|
+
: { start, duration }),
|
|
244
|
+
pitch: note.pitch,
|
|
245
|
+
velocity: note.velocity,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
throw new Error("unsupported agent operation");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
253
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** "kick=36, snare=38, ..." for the model's drum vocabulary. */
|
|
257
|
+
export function drumVoiceGuide(): string {
|
|
258
|
+
return DRUM_VOICES.map((info) => `${info.voice}=${info.pitch}`).join(", ");
|
|
259
|
+
}
|