@hraness/dawg 0.4.1 → 0.6.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 +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The modelled piano commands (0.6 keys lane, `core/keys.ts`):
|
|
3
|
+
*
|
|
4
|
+
* piano the focused track becomes the modelled grand
|
|
5
|
+
* piano <preset> grand ballad upright felt lofi honkytonk prepared
|
|
6
|
+
* grand | upright | felt … the same, by the preset word alone
|
|
7
|
+
* keys list this track's piano settings
|
|
8
|
+
* keys preset <name> load a preset (instrument, keys and its effects)
|
|
9
|
+
* keys <param> <value> […] set parameters (hardness 0.4 decay 1.5 …)
|
|
10
|
+
* keys <param> off unset one parameter (back to the preset's)
|
|
11
|
+
* keys reset the family's own sound (`keys: {}`),
|
|
12
|
+
* dropping the preset's unedited effects
|
|
13
|
+
*
|
|
14
|
+
* Every new write of `piano` stores `instrument: "grand"` with `keys`; a
|
|
15
|
+
* stored legacy `instrument: "piano"` is never rewritten unless asked. Each
|
|
16
|
+
* command is one `updateTrack` revision and one undo step.
|
|
17
|
+
*/
|
|
18
|
+
import { FxValidationError } from "../../core/params.ts";
|
|
19
|
+
import {
|
|
20
|
+
KEYS_PARAMS,
|
|
21
|
+
KEYS_PRESETS,
|
|
22
|
+
KEYS_SIMPLE,
|
|
23
|
+
isKeysPreset,
|
|
24
|
+
isPianoFamily,
|
|
25
|
+
keysParamName,
|
|
26
|
+
normalizeKeys,
|
|
27
|
+
pianoWrite,
|
|
28
|
+
type TrackKeys,
|
|
29
|
+
} from "../../core/keys.ts";
|
|
30
|
+
import {
|
|
31
|
+
createScore,
|
|
32
|
+
ScoreValidationError,
|
|
33
|
+
updateTrack,
|
|
34
|
+
type TrackPatch,
|
|
35
|
+
type TrackScore,
|
|
36
|
+
} from "../../core/score.ts";
|
|
37
|
+
import { parseParamValue } from "./fx.ts";
|
|
38
|
+
|
|
39
|
+
export type KeysCommand =
|
|
40
|
+
| { type: "keys-list" }
|
|
41
|
+
| { type: "keys-presets" }
|
|
42
|
+
| { type: "keys-reset" }
|
|
43
|
+
| { type: "keys-preset"; preset: string }
|
|
44
|
+
| {
|
|
45
|
+
type: "keys-set";
|
|
46
|
+
/** `null` unsets a parameter. */
|
|
47
|
+
values: Readonly<Record<string, number | string | null>>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The words that start a keys command (registered with /help). */
|
|
51
|
+
export const KEYS_COMMAND_WORDS = Object.freeze([
|
|
52
|
+
"keys",
|
|
53
|
+
"piano",
|
|
54
|
+
...Object.keys(KEYS_PRESETS).filter((name) => name !== "lofi"),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export function parseKeysCommand(prompt: string): KeysCommand | undefined {
|
|
58
|
+
if (prompt.length > 1_024) return undefined;
|
|
59
|
+
let words = prompt.trim().toLowerCase().split(/\s+/);
|
|
60
|
+
// `instrument piano` / `sound grand`: the generic instrument command with
|
|
61
|
+
// a keys word is a new write, so it stores the modelled piano.
|
|
62
|
+
if (
|
|
63
|
+
words.length === 2 &&
|
|
64
|
+
["instrument", "sound", "voice"].includes(words[0]!) &&
|
|
65
|
+
pianoWrite(words[1]!)
|
|
66
|
+
)
|
|
67
|
+
words = [words[1]!];
|
|
68
|
+
const head = words[0] ?? "";
|
|
69
|
+
// `piano`, `piano <preset>`, and a bare preset word (`grand`, `felt`).
|
|
70
|
+
if (head === "piano" || head === "uprightpiano" || head === "feltpiano") {
|
|
71
|
+
if (words.length === 1)
|
|
72
|
+
return { type: "keys-preset", preset: pianoWrite(head)!.preset! };
|
|
73
|
+
return words.length === 2 && isKeysPreset(words[1]!)
|
|
74
|
+
? { type: "keys-preset", preset: words[1]! }
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
if (head !== "keys") {
|
|
78
|
+
if (words.length === 1 && isKeysPreset(head) && head !== "lofi")
|
|
79
|
+
return { type: "keys-preset", preset: head };
|
|
80
|
+
if (words.length === 2 && isKeysPreset(head) && words[1] === "piano")
|
|
81
|
+
return { type: "keys-preset", preset: head };
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (words.length === 1) return { type: "keys-list" };
|
|
85
|
+
const rest = words.slice(1);
|
|
86
|
+
if (rest.length === 1 && (rest[0] === "reset" || rest[0] === "off"))
|
|
87
|
+
return { type: "keys-reset" };
|
|
88
|
+
if (rest.length === 1 && rest[0] === "presets")
|
|
89
|
+
return { type: "keys-presets" };
|
|
90
|
+
if (rest[0] === "preset")
|
|
91
|
+
return rest.length === 2 && isKeysPreset(rest[1]!)
|
|
92
|
+
? { type: "keys-preset", preset: rest[1]! }
|
|
93
|
+
: rest.length === 1
|
|
94
|
+
? { type: "keys-presets" }
|
|
95
|
+
: undefined;
|
|
96
|
+
if (rest.length === 1 && isKeysPreset(rest[0]!))
|
|
97
|
+
return { type: "keys-preset", preset: rest[0]! };
|
|
98
|
+
if (rest.length % 2 !== 0) return undefined;
|
|
99
|
+
const values: Record<string, number | string | null> = {};
|
|
100
|
+
for (let index = 0; index < rest.length; index += 2) {
|
|
101
|
+
const name = keysParamName(rest[index]!);
|
|
102
|
+
if (!name) return undefined;
|
|
103
|
+
const word = rest[index + 1]!;
|
|
104
|
+
if (word === "off" || word === "unset") {
|
|
105
|
+
values[name] = null;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const value = parseParamValue(KEYS_PARAMS[name]!, word);
|
|
109
|
+
if (value === undefined || typeof value === "boolean") return undefined;
|
|
110
|
+
values[name] = value as number | string;
|
|
111
|
+
}
|
|
112
|
+
return { type: "keys-set", values };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function describeKeys(keys: TrackKeys | undefined): string {
|
|
116
|
+
if (!keys) return "off";
|
|
117
|
+
const entries = Object.entries(keys).filter(([key]) => key !== "preset");
|
|
118
|
+
const parts = [
|
|
119
|
+
...(keys.preset ? [`preset ${keys.preset}`] : []),
|
|
120
|
+
...entries.map(([key, value]) => `${key} ${String(value)}`),
|
|
121
|
+
];
|
|
122
|
+
return parts.length > 0 ? parts.join(" · ") : "defaults";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type KeysResult = Readonly<{
|
|
126
|
+
ok: boolean;
|
|
127
|
+
message: string;
|
|
128
|
+
next?: TrackScore;
|
|
129
|
+
kind?: string;
|
|
130
|
+
payload?: Record<string, unknown>;
|
|
131
|
+
}>;
|
|
132
|
+
|
|
133
|
+
/** One line per preset for `keys presets`, /help and the agent prompt. */
|
|
134
|
+
export function keysPresetLines(): string[] {
|
|
135
|
+
return Object.entries(KEYS_PRESETS).map(
|
|
136
|
+
([name, preset]) => `${name} · ${preset.doc} · ${preset.styles}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type PresetTrack = Pick<
|
|
141
|
+
TrackScore["tracks"][number],
|
|
142
|
+
"fx" | "keys" | "filter" | "reverb"
|
|
143
|
+
>;
|
|
144
|
+
|
|
145
|
+
/** A preset's effects as the score stores them (defaults filled in). */
|
|
146
|
+
const storedPresetFx = new Map<string, Track>();
|
|
147
|
+
type Track = TrackScore["tracks"][number];
|
|
148
|
+
function presetEffects(preset: string): Track | undefined {
|
|
149
|
+
if (storedPresetFx.has(preset)) return storedPresetFx.get(preset);
|
|
150
|
+
const write = pianoWrite(preset);
|
|
151
|
+
const track = write
|
|
152
|
+
? createScore({
|
|
153
|
+
tracks: [
|
|
154
|
+
{
|
|
155
|
+
id: "p",
|
|
156
|
+
name: "p",
|
|
157
|
+
instrument: write.instrument,
|
|
158
|
+
...(write.filter ? { filter: write.filter } : {}),
|
|
159
|
+
...(write.fx ? { fx: write.fx } : {}),
|
|
160
|
+
...(write.reverb ? { reverb: write.reverb } : {}),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}).tracks[0]
|
|
164
|
+
: undefined;
|
|
165
|
+
if (track) storedPresetFx.set(preset, track);
|
|
166
|
+
return track;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** True when `stored` still equals the preset's stored value. */
|
|
170
|
+
function unchanged(stored: unknown, preset: unknown): boolean {
|
|
171
|
+
return (
|
|
172
|
+
stored !== undefined &&
|
|
173
|
+
preset !== undefined &&
|
|
174
|
+
JSON.stringify(stored) === JSON.stringify(preset)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clears the effects the track's current preset brought (its filter, fx
|
|
180
|
+
* entries and reverb) while they still hold the preset's values, so moving
|
|
181
|
+
* between presets or resetting never piles them up. Effects the user has
|
|
182
|
+
* since edited stay.
|
|
183
|
+
*/
|
|
184
|
+
export function keysPresetClear(track: PresetTrack): TrackPatch {
|
|
185
|
+
const name = track.keys?.preset;
|
|
186
|
+
const old = typeof name === "string" ? presetEffects(name) : undefined;
|
|
187
|
+
if (!old) return {};
|
|
188
|
+
const patch: {
|
|
189
|
+
filter?: null;
|
|
190
|
+
reverb?: null;
|
|
191
|
+
fx?: TrackPatch["fx"];
|
|
192
|
+
} = {};
|
|
193
|
+
if (old.filter && unchanged(track.filter, old.filter)) patch.filter = null;
|
|
194
|
+
if (old.reverb && unchanged(track.reverb, old.reverb)) patch.reverb = null;
|
|
195
|
+
if (old.fx && track.fx) {
|
|
196
|
+
const fx: Record<string, unknown> = { ...track.fx };
|
|
197
|
+
let removed = false;
|
|
198
|
+
for (const [effect, params] of Object.entries(old.fx))
|
|
199
|
+
if (unchanged(fx[effect], params)) {
|
|
200
|
+
delete fx[effect];
|
|
201
|
+
removed = true;
|
|
202
|
+
}
|
|
203
|
+
if (removed)
|
|
204
|
+
patch.fx = Object.keys(fx).length > 0 ? (fx as TrackPatch["fx"]) : null;
|
|
205
|
+
}
|
|
206
|
+
return patch;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The track patch for a preset write (instrument, keys, preset effects),
|
|
211
|
+
* first clearing the effects the previous preset brought.
|
|
212
|
+
*/
|
|
213
|
+
export function keysPresetPatch(
|
|
214
|
+
track: PresetTrack,
|
|
215
|
+
preset: string,
|
|
216
|
+
): TrackPatch {
|
|
217
|
+
const write = pianoWrite(preset)!;
|
|
218
|
+
const clear = keysPresetClear(track);
|
|
219
|
+
const fx = "fx" in clear ? (clear.fx ?? undefined) : track.fx;
|
|
220
|
+
return {
|
|
221
|
+
...clear,
|
|
222
|
+
instrument: write.instrument,
|
|
223
|
+
keys: write.keys,
|
|
224
|
+
...(write.filter ? { filter: write.filter } : {}),
|
|
225
|
+
...(write.fx
|
|
226
|
+
? { fx: { ...(fx ?? {}), ...write.fx } as TrackPatch["fx"] }
|
|
227
|
+
: {}),
|
|
228
|
+
...(write.reverb ? { reverb: write.reverb } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function applyKeysCommand(
|
|
233
|
+
score: TrackScore,
|
|
234
|
+
trackId: string,
|
|
235
|
+
command: KeysCommand,
|
|
236
|
+
): KeysResult {
|
|
237
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
238
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
239
|
+
if (command.type === "keys-presets")
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
message: `keys presets · ${Object.keys(KEYS_PRESETS).join(" ")}`,
|
|
243
|
+
};
|
|
244
|
+
const piano = isPianoFamily(track.instrument);
|
|
245
|
+
if (command.type === "keys-list")
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
message: piano
|
|
249
|
+
? `keys · ${track.instrument} · ${describeKeys(track.keys)} · basics ${KEYS_SIMPLE.join(" ")}`
|
|
250
|
+
: `keys · ${trackId} is ${track.instrument}; type piano (or grand upright felt honkytonk prepared) for the modelled piano`,
|
|
251
|
+
};
|
|
252
|
+
if (track.sampler && command.type !== "keys-preset")
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
message: `keys · ${trackId} is a sampler track; type piano to make it a modelled piano`,
|
|
256
|
+
};
|
|
257
|
+
let patch: TrackPatch;
|
|
258
|
+
if (command.type === "keys-preset") {
|
|
259
|
+
patch = {
|
|
260
|
+
...keysPresetPatch(track, command.preset),
|
|
261
|
+
...(track.sampler ? { sampler: null } : {}),
|
|
262
|
+
...(track.synth ? { synth: null } : {}),
|
|
263
|
+
};
|
|
264
|
+
} else if (!piano) {
|
|
265
|
+
// `keys` is also the legacy synth sound (`instrument keys`): name both
|
|
266
|
+
// ways out.
|
|
267
|
+
const keysSynth = track.instrument === "keys";
|
|
268
|
+
const [first] = Object.keys(
|
|
269
|
+
command.type === "keys-set" ? command.values : {},
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
message: keysSynth
|
|
274
|
+
? `keys · keys shapes the modelled piano; ${trackId} is the keys synth: use synth ${first ?? "decay"} …, or type piano`
|
|
275
|
+
: `keys · ${trackId} is ${track.instrument}; type piano first (keys shapes the modelled piano)`,
|
|
276
|
+
};
|
|
277
|
+
} else if (command.type === "keys-reset") {
|
|
278
|
+
// The family's own sound: the preset and the effects it brought go.
|
|
279
|
+
patch = { ...keysPresetClear(track), keys: {} };
|
|
280
|
+
} else {
|
|
281
|
+
const keys: Record<string, unknown> = { ...(track.keys ?? {}) };
|
|
282
|
+
for (const [key, value] of Object.entries(command.values)) {
|
|
283
|
+
if (value === null) delete keys[key];
|
|
284
|
+
else keys[key] = value;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
patch = { keys: normalizeKeys(keys) ?? {} };
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof FxValidationError)
|
|
290
|
+
return { ok: false, message: `keys · ${error.message}` };
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let next: TrackScore;
|
|
295
|
+
try {
|
|
296
|
+
next = updateTrack(score, trackId, patch);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (
|
|
299
|
+
error instanceof ScoreValidationError ||
|
|
300
|
+
error instanceof FxValidationError
|
|
301
|
+
)
|
|
302
|
+
return { ok: false, message: `keys · ${error.message}` };
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
const stored = next.tracks.find((candidate) => candidate.id === trackId)!;
|
|
306
|
+
const extras =
|
|
307
|
+
command.type === "keys-preset"
|
|
308
|
+
? [
|
|
309
|
+
...(patch.reverb ? ["reverb"] : []),
|
|
310
|
+
...(patch.filter ? ["filter"] : []),
|
|
311
|
+
...Object.keys(pianoWrite(command.preset)?.fx ?? {}),
|
|
312
|
+
]
|
|
313
|
+
: [];
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
message:
|
|
317
|
+
command.type === "keys-preset"
|
|
318
|
+
? `keys · ${stored.instrument} · preset ${command.preset} · ${KEYS_PRESETS[command.preset]!.doc}${extras.length > 0 ? ` · with ${extras.join(" ")}` : ""}`
|
|
319
|
+
: `keys · ${stored.instrument} · ${describeKeys(stored.keys)}`,
|
|
320
|
+
next,
|
|
321
|
+
kind: "score.keys",
|
|
322
|
+
payload: {
|
|
323
|
+
trackId,
|
|
324
|
+
instrument: stored.instrument,
|
|
325
|
+
keys: stored.keys ?? null,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The fields a new track named after a piano word starts with
|
|
332
|
+
* (`/track piano` → the modelled grand); empty for any other name. `lofi`
|
|
333
|
+
* is left out: it also names a drum kit and pattern.
|
|
334
|
+
*/
|
|
335
|
+
export function newPianoTrack(name: string): {
|
|
336
|
+
instrument?: string;
|
|
337
|
+
keys?: TrackKeys;
|
|
338
|
+
filter?: TrackPatch["filter"];
|
|
339
|
+
fx?: TrackPatch["fx"];
|
|
340
|
+
reverb?: TrackPatch["reverb"];
|
|
341
|
+
} {
|
|
342
|
+
const word = name.toLowerCase();
|
|
343
|
+
if (word === "lofi") return {};
|
|
344
|
+
const write = pianoWrite(word);
|
|
345
|
+
if (!write) return {};
|
|
346
|
+
return {
|
|
347
|
+
instrument: write.instrument,
|
|
348
|
+
keys: write.keys,
|
|
349
|
+
...(write.filter ? { filter: write.filter } : {}),
|
|
350
|
+
...(write.fx ? { fx: write.fx } : {}),
|
|
351
|
+
...(write.reverb ? { reverb: write.reverb } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|