@hraness/dawg 0.3.0 → 0.4.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 +122 -0
- package/DAWG.md +605 -39
- package/README.md +4 -4
- package/core/chords.ts +1724 -0
- package/core/diff.ts +13 -1
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +735 -18
- package/core/sdk/eval-child.ts +34 -22
- package/core/sdk/eval.ts +4 -1
- package/core/sdk/print.ts +304 -22
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +3226 -21
- package/core/synth.ts +1001 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +38 -3
- package/src/agent/brief.ts +29 -4
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +17 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/tools.ts +308 -14
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +27 -4
- package/src/audio/kits.ts +200 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/sampler.ts +157 -29
- package/src/audio/samples.ts +386 -44
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +280 -278
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +26 -4
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +298 -10
- package/src/commands/music.ts +10 -8
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +162 -2
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +1119 -30
- package/src/media/cli.ts +15 -2
- package/src/media/tools.ts +106 -1
- package/src/media/wavetable.ts +202 -0
- package/src/render.ts +22 -1
- package/src/session/naming.ts +3 -1
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +1286 -244
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-session.ts +364 -16
- package/src/tui/sketch.ts +108 -0
- package/src/web/search.ts +4 -1
- package/tui/app.ts +161 -39
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +7 -1
- package/tui/play-strip.ts +59 -14
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drum patterns and synthesized kits as score edits.
|
|
3
|
+
*
|
|
4
|
+
* /pattern browse the library (picker with preview)
|
|
5
|
+
* /pattern <name> put the pattern on the focused drum track
|
|
6
|
+
* /pattern list the library as text
|
|
7
|
+
* /kit <synth kit> syn808, syn909, acoustic, lofi, electro, trap
|
|
8
|
+
* /kit default the built-in voices again
|
|
9
|
+
*
|
|
10
|
+
* Sample kits (`/kit 909`, `/kit RolandTR808`) stay in `commands/pack.ts`;
|
|
11
|
+
* `/kit` with no name opens one picker listing both kinds.
|
|
12
|
+
*
|
|
13
|
+
* Every edit is one next score, so one revision and one undo step.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
drumVoicePitch,
|
|
17
|
+
isDrumInstrument,
|
|
18
|
+
parseDrumVoice,
|
|
19
|
+
} from "../../core/drums.ts";
|
|
20
|
+
import { normalizeRhythmRow, type RhythmRow } from "../../core/euclid.ts";
|
|
21
|
+
import { synthKit, SYNTH_KITS } from "../../core/kits.ts";
|
|
22
|
+
import { rhythmVoicePitch, withTrackRhythm } from "../../core/rhythm.ts";
|
|
23
|
+
import {
|
|
24
|
+
isSamplerInstrument,
|
|
25
|
+
samplerVoiceSlots,
|
|
26
|
+
ScoreValidationError,
|
|
27
|
+
TrackScore,
|
|
28
|
+
type Note,
|
|
29
|
+
type Track,
|
|
30
|
+
} from "../../core/score.ts";
|
|
31
|
+
import {
|
|
32
|
+
DRUM_PATTERNS,
|
|
33
|
+
findPattern,
|
|
34
|
+
type DrumPattern,
|
|
35
|
+
} from "../../core/sdk/v1.ts";
|
|
36
|
+
|
|
37
|
+
export { DRUM_PATTERNS, findPattern, type DrumPattern };
|
|
38
|
+
|
|
39
|
+
export type PatternCommand =
|
|
40
|
+
| Readonly<{ kind: "browse" }>
|
|
41
|
+
| Readonly<{ kind: "list" }>
|
|
42
|
+
| Readonly<{ kind: "apply"; name: string; tempo: TempoMode }>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `auto` moves the tempo only when it is outside the pattern's range;
|
|
46
|
+
* `keep` never moves it; `set` always uses the pattern's tempo.
|
|
47
|
+
*/
|
|
48
|
+
export type TempoMode = "auto" | "keep" | "set";
|
|
49
|
+
|
|
50
|
+
/** `/pattern`, `/pattern list`, `/pattern <name> [keep-tempo|tempo]`. */
|
|
51
|
+
export function parsePatternCommand(
|
|
52
|
+
command: string,
|
|
53
|
+
): PatternCommand | undefined {
|
|
54
|
+
const match = command
|
|
55
|
+
.trim()
|
|
56
|
+
.match(
|
|
57
|
+
/^\/patterns?(?:\s+([a-z0-9][\w-]{0,40}))?(?:\s+(keep-tempo|keep|tempo))?\s*$/i,
|
|
58
|
+
);
|
|
59
|
+
if (!match) return undefined;
|
|
60
|
+
const name = match[1]?.toLowerCase();
|
|
61
|
+
if (!name) return { kind: "browse" };
|
|
62
|
+
if (name === "list" || name === "ls") return { kind: "list" };
|
|
63
|
+
const flag = match[2]?.toLowerCase();
|
|
64
|
+
return {
|
|
65
|
+
kind: "apply",
|
|
66
|
+
name,
|
|
67
|
+
tempo: flag === "tempo" ? "set" : flag ? "keep" : "auto",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One line per pattern for `/pattern list` and the agent. */
|
|
72
|
+
export function patternLine(entry: DrumPattern): string {
|
|
73
|
+
const voices = entry.rows.map((row) => row.voice).join(" ");
|
|
74
|
+
return `${entry.name} · ${entry.tempo.min}–${entry.tempo.max} BPM · ${entry.tags.join(", ")} · ${voices}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type DrumEdit = Readonly<{
|
|
78
|
+
ok: boolean;
|
|
79
|
+
message: string;
|
|
80
|
+
next?: TrackScore;
|
|
81
|
+
kind?: string;
|
|
82
|
+
payload?: Record<string, unknown>;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
function rowsOf(entry: DrumPattern): RhythmRow[] {
|
|
86
|
+
return entry.rows.map((spec, index) => {
|
|
87
|
+
const { kind: _kind, ...fields } = spec;
|
|
88
|
+
return normalizeRhythmRow(fields, `${entry.name} rows[${index}]`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The focused track as a drum track: a kit track as is, a missing or empty
|
|
94
|
+
* melodic track as a new kit track. Undefined for a melodic track with
|
|
95
|
+
* notes or a keyed sampler, which a pattern must not overwrite.
|
|
96
|
+
*/
|
|
97
|
+
function drumTrack(
|
|
98
|
+
score: TrackScore,
|
|
99
|
+
trackId: string,
|
|
100
|
+
): { score: TrackScore; track: Track } | undefined {
|
|
101
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
102
|
+
if (!track) {
|
|
103
|
+
const next = score.withTracks([
|
|
104
|
+
...score.tracks,
|
|
105
|
+
{ id: trackId, name: trackId, instrument: "kit" },
|
|
106
|
+
]);
|
|
107
|
+
return { score: next, track: next.tracks.at(-1)! };
|
|
108
|
+
}
|
|
109
|
+
if (isDrumInstrument(track.instrument)) return { score, track };
|
|
110
|
+
if (isSamplerInstrument(track.instrument))
|
|
111
|
+
return track.sampler?.mode === "oneshot" ? { score, track } : undefined;
|
|
112
|
+
if (score.notes.some((note) => note.trackId === trackId)) return undefined;
|
|
113
|
+
const next = score.withTracks(
|
|
114
|
+
score.tracks.map((candidate) =>
|
|
115
|
+
candidate.id === trackId
|
|
116
|
+
? { ...candidate, instrument: "kit", rhythm: null }
|
|
117
|
+
: candidate,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
return { score: next, track: next.tracks.find((t) => t.id === trackId)! };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The score with `name` on `trackId`: the track's notes and rows are
|
|
125
|
+
* replaced by the pattern's rows (voices the track lacks are skipped), and
|
|
126
|
+
* the tempo follows `tempo`.
|
|
127
|
+
*/
|
|
128
|
+
export function applyDrumPattern(
|
|
129
|
+
score: TrackScore,
|
|
130
|
+
trackId: string,
|
|
131
|
+
name: string,
|
|
132
|
+
tempo: TempoMode = "auto",
|
|
133
|
+
): DrumEdit {
|
|
134
|
+
const entry = findPattern(name);
|
|
135
|
+
if (!entry)
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
message: `pattern · no ${name.slice(0, 40)} · /pattern list`,
|
|
139
|
+
};
|
|
140
|
+
const target = drumTrack(score, trackId);
|
|
141
|
+
if (!target)
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: `pattern · ${trackId} is a melodic track · focus a drum track (/track drums) and retry`,
|
|
145
|
+
};
|
|
146
|
+
const rows = rowsOf(entry);
|
|
147
|
+
const playable = rows.filter(
|
|
148
|
+
(row) => rhythmVoicePitch(target.track, row.voice) !== undefined,
|
|
149
|
+
);
|
|
150
|
+
const skipped = rows
|
|
151
|
+
.filter((row) => !playable.includes(row))
|
|
152
|
+
.map((row) => row.voice);
|
|
153
|
+
if (playable.length === 0)
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
message: `pattern · ${trackId} has none of ${rows.map((row) => row.voice).join("/")}`,
|
|
157
|
+
};
|
|
158
|
+
const moveTempo =
|
|
159
|
+
tempo === "set" ||
|
|
160
|
+
(tempo === "auto" &&
|
|
161
|
+
(score.tempoBpm < entry.tempo.min || score.tempoBpm > entry.tempo.max));
|
|
162
|
+
const cleared = new TrackScore({
|
|
163
|
+
...target.score.toJSON(),
|
|
164
|
+
...(moveTempo ? { tempoBpm: entry.tempo.bpm } : {}),
|
|
165
|
+
tracks: target.score.tracks.map((candidate) =>
|
|
166
|
+
candidate.id === trackId ? { ...candidate, rhythm: null } : candidate,
|
|
167
|
+
),
|
|
168
|
+
notes: target.score.notes.filter((note) => note.trackId !== trackId),
|
|
169
|
+
});
|
|
170
|
+
let next: TrackScore;
|
|
171
|
+
try {
|
|
172
|
+
next = withTrackRhythm(cleared, trackId, playable);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof ScoreValidationError)
|
|
175
|
+
return { ok: false, message: `pattern · ${error.message}` };
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
const parts = [`pattern ${entry.name} on ${trackId}`];
|
|
179
|
+
if (moveTempo) parts.push(`${entry.tempo.bpm} BPM`);
|
|
180
|
+
if (skipped.length) parts.push(`no ${skipped.join("/")}`);
|
|
181
|
+
const track = next.tracks.find((candidate) => candidate.id === trackId);
|
|
182
|
+
if (isDrumInstrument(track?.instrument) && !track?.kit)
|
|
183
|
+
parts.push(`try /kit ${entry.kit}`);
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
message: parts.join(" · "),
|
|
187
|
+
next,
|
|
188
|
+
kind: "score.pattern",
|
|
189
|
+
payload: {
|
|
190
|
+
trackId,
|
|
191
|
+
pattern: entry.name,
|
|
192
|
+
...(moveTempo ? { tempoBpm: entry.tempo.bpm } : {}),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** True for names `/kit` should treat as a synthesized kit. */
|
|
198
|
+
export function isSynthKitName(name: string): boolean {
|
|
199
|
+
return (
|
|
200
|
+
name.trim().toLowerCase() === "default" || synthKit(name) !== undefined
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The score with synth kit `name` (`default` for the built-in voices) on
|
|
206
|
+
* `trackId`. A oneshot sampler kit (from `/kit 909`) turns back into a
|
|
207
|
+
* synth kit track; its hits and rows move to the drum voices of the same
|
|
208
|
+
* name, and voices without one are dropped.
|
|
209
|
+
*/
|
|
210
|
+
export function applySynthKit(
|
|
211
|
+
score: TrackScore,
|
|
212
|
+
trackId: string,
|
|
213
|
+
name: string,
|
|
214
|
+
): DrumEdit {
|
|
215
|
+
const isDefault = name.trim().toLowerCase() === "default";
|
|
216
|
+
const kit = isDefault ? undefined : synthKit(name);
|
|
217
|
+
if (!isDefault && !kit)
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
message: `kit · no synth kit ${name.slice(0, 40)} (default ${SYNTH_KITS.map((entry) => entry.name).join(" ")})`,
|
|
221
|
+
};
|
|
222
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
223
|
+
const label = kit?.name ?? "default";
|
|
224
|
+
const done = (next: TrackScore, extra = ""): DrumEdit => ({
|
|
225
|
+
ok: true,
|
|
226
|
+
message: `kit ${label} on ${trackId}${extra}`,
|
|
227
|
+
next,
|
|
228
|
+
kind: "score.kit",
|
|
229
|
+
payload: { trackId, kit: label },
|
|
230
|
+
});
|
|
231
|
+
if (!track)
|
|
232
|
+
return done(
|
|
233
|
+
score.withTracks([
|
|
234
|
+
...score.tracks,
|
|
235
|
+
{
|
|
236
|
+
id: trackId,
|
|
237
|
+
name: trackId,
|
|
238
|
+
instrument: "kit",
|
|
239
|
+
...(kit ? { kit: kit.name } : {}),
|
|
240
|
+
},
|
|
241
|
+
]),
|
|
242
|
+
);
|
|
243
|
+
if (isDrumInstrument(track.instrument)) {
|
|
244
|
+
if ((track.kit ?? undefined) === kit?.name)
|
|
245
|
+
return { ok: true, message: `kit ${label} on ${trackId} already` };
|
|
246
|
+
return done(
|
|
247
|
+
score.withTracks(
|
|
248
|
+
score.tracks.map((candidate) =>
|
|
249
|
+
candidate.id === trackId
|
|
250
|
+
? { ...candidate, kit: kit ? kit.name : null }
|
|
251
|
+
: candidate,
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const oneshot =
|
|
257
|
+
isSamplerInstrument(track.instrument) && track.sampler?.mode === "oneshot";
|
|
258
|
+
const hasNotes = score.notes.some((note) => note.trackId === trackId);
|
|
259
|
+
if (!oneshot && hasNotes)
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
message: `kit · ${trackId} is a melodic track · focus a drum track (/track drums) and retry`,
|
|
263
|
+
};
|
|
264
|
+
const pitchOf = new Map<number, number>();
|
|
265
|
+
if (oneshot && track.sampler)
|
|
266
|
+
for (const [voice, slot] of samplerVoiceSlots(track.sampler)) {
|
|
267
|
+
const drum = parseDrumVoice(voice);
|
|
268
|
+
if (drum) pitchOf.set(slot, drumVoicePitch(drum));
|
|
269
|
+
}
|
|
270
|
+
const notes: Note[] = [];
|
|
271
|
+
let dropped = 0;
|
|
272
|
+
for (const note of score.notes) {
|
|
273
|
+
if (note.trackId !== trackId) {
|
|
274
|
+
notes.push(note);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const pitch = pitchOf.get(note.pitch);
|
|
278
|
+
if (pitch === undefined) dropped += 1;
|
|
279
|
+
else notes.push({ ...note, pitch });
|
|
280
|
+
}
|
|
281
|
+
const rows = (track.rhythm ?? []).filter(
|
|
282
|
+
(row) => parseDrumVoice(row.voice) !== undefined,
|
|
283
|
+
);
|
|
284
|
+
const next = new TrackScore({
|
|
285
|
+
...score.toJSON(),
|
|
286
|
+
tracks: score.tracks.map((candidate) => {
|
|
287
|
+
if (candidate.id !== trackId) return candidate;
|
|
288
|
+
const { sampler: _sampler, ...rest } = candidate;
|
|
289
|
+
return {
|
|
290
|
+
...rest,
|
|
291
|
+
instrument: "kit",
|
|
292
|
+
rhythm: rows.length > 0 ? rows : null,
|
|
293
|
+
...(kit ? { kit: kit.name } : {}),
|
|
294
|
+
};
|
|
295
|
+
}),
|
|
296
|
+
notes,
|
|
297
|
+
});
|
|
298
|
+
return done(next, dropped ? ` · ${dropped} hits had no synth voice` : "");
|
|
299
|
+
}
|
package/src/commands/edit.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* track name <text> rename the focused track
|
|
6
6
|
* meter <beats per bar> time signature numerator (1..16)
|
|
7
|
+
* key <name>|none song key (`C major`, `F# dorian`)
|
|
7
8
|
* automate <lane> points <beat:value>... merge points into a lane
|
|
8
9
|
* automate <lane> remove <beat> drop the point at a beat
|
|
9
10
|
*
|
|
@@ -12,7 +13,8 @@
|
|
|
12
13
|
* score and the session event, leaving persistence to the caller.
|
|
13
14
|
*/
|
|
14
15
|
import {
|
|
15
|
-
|
|
16
|
+
automationPoints,
|
|
17
|
+
automationRange,
|
|
16
18
|
SCORE_LIMITS,
|
|
17
19
|
applyScoreOperation,
|
|
18
20
|
type AutomationParameter,
|
|
@@ -20,11 +22,13 @@ import {
|
|
|
20
22
|
type ScoreOperation,
|
|
21
23
|
type TrackScore,
|
|
22
24
|
} from "../../core/score.ts";
|
|
25
|
+
import { keyName, parseKey } from "../../core/chords.ts";
|
|
23
26
|
import { parseEffectLane } from "./music.ts";
|
|
24
27
|
|
|
25
28
|
export type EditCommand =
|
|
26
29
|
| { type: "track-name"; name: string }
|
|
27
30
|
| { type: "meter"; beatsPerBar: number }
|
|
31
|
+
| { type: "key"; key: string | null }
|
|
28
32
|
| {
|
|
29
33
|
type: "automation-points";
|
|
30
34
|
parameter: AutomationParameter;
|
|
@@ -58,11 +62,18 @@ export function parseEditCommand(prompt: string): EditCommand | undefined {
|
|
|
58
62
|
? { type: "meter", beatsPerBar }
|
|
59
63
|
: undefined;
|
|
60
64
|
}
|
|
65
|
+
const key = text.match(/^\/?key (.+)$/i);
|
|
66
|
+
if (key) {
|
|
67
|
+
const value = key[1]!.trim();
|
|
68
|
+
if (/^(none|off|clear)$/i.test(value)) return { type: "key", key: null };
|
|
69
|
+
const parsed = parseKey(value);
|
|
70
|
+
return parsed ? { type: "key", key: keyName(parsed) } : undefined;
|
|
71
|
+
}
|
|
61
72
|
const points = text.match(/^automate ([a-z-]+) points((?: \S+)+)$/i);
|
|
62
73
|
if (points) {
|
|
63
74
|
const parameter = parseLane(points[1]!);
|
|
64
75
|
if (!parameter) return undefined;
|
|
65
|
-
const { min, max } =
|
|
76
|
+
const { min, max } = automationRange(parameter);
|
|
66
77
|
const parsed: { beat: number; value: number }[] = [];
|
|
67
78
|
for (const pair of points[2]!.trim().split(" ")) {
|
|
68
79
|
const [beatText, valueText, extra] = pair.split(":");
|
|
@@ -116,6 +127,15 @@ export function applyEditCommand(
|
|
|
116
127
|
payload: { beatsPerBar: command.beatsPerBar },
|
|
117
128
|
};
|
|
118
129
|
}
|
|
130
|
+
if (command.type === "key") {
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
message: `key · ${command.key ?? "none"}`,
|
|
134
|
+
next: applyScoreOperation(score, { type: "setKey", key: command.key }),
|
|
135
|
+
kind: "score.key",
|
|
136
|
+
payload: { key: command.key },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
119
139
|
if (!track) return { ok: false, message: `no track ${trackId}` };
|
|
120
140
|
if (command.type === "track-name") {
|
|
121
141
|
const patch = { name: command.name };
|
|
@@ -131,8 +151,10 @@ export function applyEditCommand(
|
|
|
131
151
|
payload: { trackId, patch },
|
|
132
152
|
};
|
|
133
153
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
154
|
+
const current: readonly AutomationPoint[] = automationPoints(
|
|
155
|
+
track,
|
|
156
|
+
command.parameter,
|
|
157
|
+
);
|
|
136
158
|
let points: AutomationPoint[];
|
|
137
159
|
if (command.type === "automation-points") {
|
|
138
160
|
const byTick = new Map(current.map((point) => [point.tick, point]));
|