@hraness/dawg 0.3.0 → 0.4.1
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 +138 -0
- package/DAWG.md +608 -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 +109 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +339 -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 +394 -0
- package/src/audio/engine.ts +112 -4
- package/src/audio/kits.ts +200 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/preview.ts +481 -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 +411 -295
- 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 +1222 -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 +521 -0
- package/src/tui/euclid.ts +516 -0
- package/src/tui/menu.ts +1305 -244
- package/src/tui/play-chords.ts +630 -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 +286 -0
- package/tui/highway.ts +7 -1
- package/tui/play-strip.ts +59 -14
package/core/sdk/eval-child.ts
CHANGED
|
@@ -76,35 +76,47 @@ function diagnostic(error: unknown, project: string): Diagnostic {
|
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Adds `sha256` to sampler voices and project wavetable files that exist
|
|
81
|
+
* and are within the size limit.
|
|
82
|
+
*/
|
|
80
83
|
async function withSampleHashes(
|
|
81
84
|
song: object,
|
|
82
85
|
project: string,
|
|
83
86
|
): Promise<unknown> {
|
|
84
87
|
const plain = JSON.parse(JSON.stringify(song)) as Record<string, unknown>;
|
|
85
88
|
const tracks = Array.isArray(plain.tracks) ? plain.tracks : [];
|
|
89
|
+
const refs: Record<string, unknown>[] = [];
|
|
90
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
91
|
+
typeof value === "object" && value !== null;
|
|
86
92
|
for (const track of tracks) {
|
|
87
|
-
if (
|
|
88
|
-
const sampler =
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
93
|
+
if (!isObject(track)) continue;
|
|
94
|
+
const sampler = track.sampler;
|
|
95
|
+
if (isObject(sampler) && isObject(sampler.voices))
|
|
96
|
+
for (const voice of Object.values(sampler.voices))
|
|
97
|
+
if (isObject(voice)) refs.push(voice);
|
|
98
|
+
const wavetable = track.wavetable;
|
|
99
|
+
if (isObject(wavetable) && isObject(wavetable.table))
|
|
100
|
+
refs.push(wavetable.table);
|
|
101
|
+
}
|
|
102
|
+
for (const ref of refs) {
|
|
103
|
+
if (
|
|
104
|
+
typeof ref.src !== "string" ||
|
|
105
|
+
typeof ref.sha256 === "string" ||
|
|
106
|
+
ref.src.startsWith("pack:") ||
|
|
107
|
+
ref.src.startsWith("builtin:")
|
|
108
|
+
)
|
|
109
|
+
continue;
|
|
110
|
+
const path = resolve(project, ref.src);
|
|
111
|
+
if (relative(project, path).startsWith("..")) continue;
|
|
112
|
+
try {
|
|
113
|
+
const info = await stat(path);
|
|
114
|
+
if (!info.isFile() || info.size > MAX_SAMPLE_FILE_BYTES) continue;
|
|
115
|
+
ref.sha256 = createHash("sha256")
|
|
116
|
+
.update(await readFile(path))
|
|
117
|
+
.digest("hex");
|
|
118
|
+
} catch {
|
|
119
|
+
// Missing file: left for `dawg check` to report.
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
return plain;
|
package/core/sdk/eval.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { decodeLoopDocument } from "../loop.ts";
|
|
14
|
+
import { refreshRhythm } from "../rhythm.ts";
|
|
14
15
|
import { ScoreValidationError, TrackScore } from "../score.ts";
|
|
15
16
|
|
|
16
17
|
export const EVAL_TIMEOUT_MS = 10_000;
|
|
@@ -158,7 +159,9 @@ export function parseChildOutput(
|
|
|
158
159
|
return failure(started, [diagnostic]);
|
|
159
160
|
}
|
|
160
161
|
try {
|
|
161
|
-
|
|
162
|
+
// Rhythm rows are generators: their lanes are expanded here, on the
|
|
163
|
+
// host, so `song.ts` stores parameters and the score stores notes.
|
|
164
|
+
const score = refreshRhythm(decodeLoopDocument(record.score));
|
|
162
165
|
return { ok: true, score, ms: elapsed(started) };
|
|
163
166
|
} catch (error) {
|
|
164
167
|
return failure(started, [
|
package/core/sdk/print.ts
CHANGED
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { DRUM_VOICES, isDrumInstrument } from "../drums.ts";
|
|
11
|
+
import type { RhythmRow } from "../euclid.ts";
|
|
12
|
+
import { FX_LANES, fxSpec, type FxName, type FxValues } from "../fx.ts";
|
|
11
13
|
import { midiToPitch } from "../pitch.ts";
|
|
14
|
+
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
12
15
|
import {
|
|
16
|
+
BUILTIN_TABLE_PREFIX,
|
|
17
|
+
WAVETABLE_PARAMS,
|
|
13
18
|
isSamplerInstrument,
|
|
19
|
+
isWavetableInstrument,
|
|
14
20
|
samplerVoiceSlots,
|
|
21
|
+
wavetableOf,
|
|
22
|
+
type TrackWavetable,
|
|
23
|
+
type WavetableParam,
|
|
15
24
|
TrackScore,
|
|
16
25
|
type AutomationPoint,
|
|
17
26
|
type Note,
|
|
@@ -121,7 +130,12 @@ const RESERVED = new Set([
|
|
|
121
130
|
"hits",
|
|
122
131
|
"every",
|
|
123
132
|
"sampler",
|
|
133
|
+
"wavetable",
|
|
124
134
|
"slices",
|
|
135
|
+
"euclid",
|
|
136
|
+
"euclidRot",
|
|
137
|
+
"euclidLegato",
|
|
138
|
+
"grid",
|
|
125
139
|
]);
|
|
126
140
|
|
|
127
141
|
/** Import identifiers for each track, unique in score order. */
|
|
@@ -178,7 +192,15 @@ export function printSong(
|
|
|
178
192
|
|
|
179
193
|
/** `tracks/<slug>/track.ts` for one track of a score. */
|
|
180
194
|
export function printTrack(score: TrackScore, track: Track): string {
|
|
181
|
-
|
|
195
|
+
// Rows whose lane still matches their expansion print as generators and
|
|
196
|
+
// own their notes; a hand-edited lane prints as plain notes instead.
|
|
197
|
+
const rows = (track.rhythm ?? []).filter((row) =>
|
|
198
|
+
rowInSync(score, track, row),
|
|
199
|
+
);
|
|
200
|
+
const owned = new Set(rows.map((row) => rhythmVoicePitch(track, row.voice)));
|
|
201
|
+
const notes = score.notes.filter(
|
|
202
|
+
(note) => note.trackId === track.id && !owned.has(note.pitch),
|
|
203
|
+
);
|
|
182
204
|
const kit = isDrumInstrument(track.instrument);
|
|
183
205
|
const voiceSlots =
|
|
184
206
|
isSamplerInstrument(track.instrument) && track.sampler
|
|
@@ -200,6 +222,10 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
200
222
|
return printNote(score, note, !kit && !slots);
|
|
201
223
|
});
|
|
202
224
|
if (track.sampler) used.add("sampler");
|
|
225
|
+
const wavetable = isWavetableInstrument(track.instrument)
|
|
226
|
+
? wavetableOf(track)
|
|
227
|
+
: undefined;
|
|
228
|
+
if (wavetable) used.add("wavetable");
|
|
203
229
|
|
|
204
230
|
const entries: string[] = [
|
|
205
231
|
`id: ${str(track.id)}`,
|
|
@@ -207,7 +233,10 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
207
233
|
];
|
|
208
234
|
if (track.sampler) {
|
|
209
235
|
entries.push(`instrument: ${printSampler(track.sampler, INDENT)}`);
|
|
236
|
+
} else if (wavetable) {
|
|
237
|
+
entries.push(`instrument: ${printWavetable(wavetable, INDENT)}`);
|
|
210
238
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
239
|
+
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
211
240
|
if (track.muted) entries.push("muted: true");
|
|
212
241
|
if (track.solo) entries.push("solo: true");
|
|
213
242
|
if (track.volume !== 1) entries.push(`volume: ${num(track.volume)}`);
|
|
@@ -218,6 +247,7 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
218
247
|
[
|
|
219
248
|
["cutoff", num(track.filter.cutoff)],
|
|
220
249
|
["resonance", num(track.filter.resonance)],
|
|
250
|
+
...optional(track.filter, ["type", "ftype"]),
|
|
221
251
|
],
|
|
222
252
|
INDENT,
|
|
223
253
|
"filter: ".length,
|
|
@@ -231,6 +261,7 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
231
261
|
["beats", num(track.delay.beats)],
|
|
232
262
|
["feedback", num(track.delay.feedback)],
|
|
233
263
|
["mix", num(track.delay.mix)],
|
|
264
|
+
...optional(track.delay, ["time", "pingpong", "highcut"]),
|
|
234
265
|
],
|
|
235
266
|
INDENT,
|
|
236
267
|
"delay: ".length,
|
|
@@ -243,12 +274,56 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
243
274
|
[
|
|
244
275
|
["mix", num(track.reverb.mix)],
|
|
245
276
|
["size", num(track.reverb.size)],
|
|
277
|
+
...optional(track.reverb, ["fade", "lowpass", "dim", "predelay"]),
|
|
278
|
+
...(track.reverb.ir
|
|
279
|
+
? ([
|
|
280
|
+
[
|
|
281
|
+
"ir",
|
|
282
|
+
track.reverb.ir.src.startsWith(BUILTIN_TABLE_PREFIX)
|
|
283
|
+
? str(
|
|
284
|
+
track.reverb.ir.src.slice(BUILTIN_TABLE_PREFIX.length),
|
|
285
|
+
)
|
|
286
|
+
: printSample(
|
|
287
|
+
track.reverb.ir,
|
|
288
|
+
INDENT + INDENT,
|
|
289
|
+
"ir: ".length,
|
|
290
|
+
),
|
|
291
|
+
],
|
|
292
|
+
] as const)
|
|
293
|
+
: []),
|
|
246
294
|
],
|
|
247
295
|
INDENT,
|
|
248
296
|
"reverb: ".length,
|
|
249
297
|
1,
|
|
250
298
|
)}`,
|
|
251
299
|
);
|
|
300
|
+
if (track.fx) {
|
|
301
|
+
const effects = Object.entries(track.fx) as [FxName, FxValues][];
|
|
302
|
+
const inner = INDENT + INDENT;
|
|
303
|
+
const body = effects.map(([effect, values]) => {
|
|
304
|
+
// Only values that differ from the default: decoding fills the rest.
|
|
305
|
+
const params = Object.entries(fxSpec(effect).params)
|
|
306
|
+
.filter(
|
|
307
|
+
([key, spec]) =>
|
|
308
|
+
values[key] !== undefined && values[key] !== spec.default,
|
|
309
|
+
)
|
|
310
|
+
.map(([key]) => [key, value(values[key]!)] as const);
|
|
311
|
+
return `${inner}${effect}: ${params.length === 0 ? "{}" : obj(params, inner, `${effect}: `.length, 1)},`;
|
|
312
|
+
});
|
|
313
|
+
entries.push(`fx: {\n${body.join("\n")}\n${INDENT}}`);
|
|
314
|
+
}
|
|
315
|
+
if (track.synth) {
|
|
316
|
+
const params = Object.entries(track.synth).map(
|
|
317
|
+
([key, v]) =>
|
|
318
|
+
[
|
|
319
|
+
key,
|
|
320
|
+
Array.isArray(v)
|
|
321
|
+
? `[${(v as readonly number[]).map(num).join(", ")}]`
|
|
322
|
+
: value(v as number | string | boolean),
|
|
323
|
+
] as const,
|
|
324
|
+
);
|
|
325
|
+
entries.push(`synth: ${obj(params, INDENT, "synth: ".length, 1)}`);
|
|
326
|
+
}
|
|
252
327
|
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
253
328
|
["volume", track.volumeAutomation],
|
|
254
329
|
["pan", track.panAutomation],
|
|
@@ -256,23 +331,70 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
256
331
|
["resonance", track.resonanceAutomation],
|
|
257
332
|
["delayFeedback", track.delayFeedbackAutomation],
|
|
258
333
|
["delayMix", track.delayMixAutomation],
|
|
334
|
+
["wt", track.wtAutomation],
|
|
259
335
|
];
|
|
260
336
|
const automation = lanes.filter(([, points]) => points && points.length > 0);
|
|
261
|
-
|
|
337
|
+
const fxLanes = FX_LANES.filter(
|
|
338
|
+
({ lane }) => (track.fxAutomation?.[lane]?.length ?? 0) > 0,
|
|
339
|
+
).map(({ lane }) => [lane, track.fxAutomation![lane]!] as const);
|
|
340
|
+
const laneLine = (
|
|
341
|
+
name: string,
|
|
342
|
+
points: readonly AutomationPoint[],
|
|
343
|
+
indent: string,
|
|
344
|
+
) => {
|
|
345
|
+
const items = points.map(
|
|
346
|
+
(point) =>
|
|
347
|
+
`[${num(point.tick / score.ticksPerBeat)}, ${num(point.value)}]`,
|
|
348
|
+
);
|
|
349
|
+
// Prettier forces a break on arrays of 2+ arrays that each hold 2+ items.
|
|
350
|
+
return `${indent}${name}: ${list(items, indent, `${name}: `.length, 1, items.length > 1)},`;
|
|
351
|
+
};
|
|
352
|
+
if (automation.length > 0 || fxLanes.length > 0) {
|
|
262
353
|
const inner = INDENT + INDENT;
|
|
263
|
-
const body = automation.map(([lane, points]) =>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
354
|
+
const body = automation.map(([lane, points]) =>
|
|
355
|
+
laneLine(lane, points!, inner),
|
|
356
|
+
);
|
|
357
|
+
if (fxLanes.length > 0) {
|
|
358
|
+
const deeper = inner + INDENT;
|
|
359
|
+
const fxBody = fxLanes.map(([lane, points]) =>
|
|
360
|
+
laneLine(str(lane), points, deeper),
|
|
267
361
|
);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
362
|
+
body.push(`${inner}fx: {\n${fxBody.join("\n")}\n${inner}},`);
|
|
363
|
+
}
|
|
271
364
|
entries.push(`automation: {\n${body.join("\n")}\n${INDENT}}`);
|
|
272
365
|
}
|
|
273
|
-
|
|
366
|
+
if (rows.length > 0) {
|
|
367
|
+
const inner = INDENT + INDENT;
|
|
368
|
+
const items = rows.map((row) => {
|
|
369
|
+
const call = printRow(row, inner);
|
|
370
|
+
used.add(call.slice(0, call.indexOf("(")));
|
|
371
|
+
return call;
|
|
372
|
+
});
|
|
373
|
+
entries.push(
|
|
374
|
+
`rhythm: ${list(
|
|
375
|
+
items,
|
|
376
|
+
INDENT,
|
|
377
|
+
"rhythm: ".length,
|
|
378
|
+
1,
|
|
379
|
+
items.some((item) => item.includes("\n")),
|
|
380
|
+
)}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (rows.length === 0 || printed.length > 0)
|
|
384
|
+
entries.push(`notes: ${list(printed, INDENT, "notes: ".length, 1)}`);
|
|
274
385
|
|
|
275
|
-
const names = [
|
|
386
|
+
const names = [
|
|
387
|
+
"track",
|
|
388
|
+
"note",
|
|
389
|
+
"seq",
|
|
390
|
+
"hit",
|
|
391
|
+
"hits",
|
|
392
|
+
"every",
|
|
393
|
+
"sampler",
|
|
394
|
+
"wavetable",
|
|
395
|
+
"euclid",
|
|
396
|
+
"grid",
|
|
397
|
+
]
|
|
276
398
|
.filter((name) => used.has(name))
|
|
277
399
|
.join(", ");
|
|
278
400
|
const lines = [
|
|
@@ -304,23 +426,133 @@ function printHit(score: TrackScore, note: Note, voice: string): string {
|
|
|
304
426
|
return `hit(${args.join(", ")})`;
|
|
305
427
|
}
|
|
306
428
|
|
|
429
|
+
/** `euclid("hat", 7, 16, 2, { velocity: 0.5 })` or `grid("sd", "....x...")`. */
|
|
430
|
+
function printRow(row: RhythmRow, indent: string): string {
|
|
431
|
+
const args: string[] = [str(row.voice)];
|
|
432
|
+
const fields: [string, string][] = [];
|
|
433
|
+
if (row.grid !== undefined) args.push(str(row.grid));
|
|
434
|
+
else {
|
|
435
|
+
args.push(num(row.pulses ?? 4), num(row.steps ?? 16));
|
|
436
|
+
if (row.rotate) args.push(num(row.rotate));
|
|
437
|
+
}
|
|
438
|
+
const inner = indent + INDENT;
|
|
439
|
+
for (const key of [
|
|
440
|
+
"division",
|
|
441
|
+
"repeats",
|
|
442
|
+
"time",
|
|
443
|
+
"pace",
|
|
444
|
+
"ramp",
|
|
445
|
+
"velocity",
|
|
446
|
+
"accent",
|
|
447
|
+
"accents",
|
|
448
|
+
"gate",
|
|
449
|
+
"legato",
|
|
450
|
+
"probability",
|
|
451
|
+
"seed",
|
|
452
|
+
"swing",
|
|
453
|
+
"nudge",
|
|
454
|
+
"cycles",
|
|
455
|
+
] as const) {
|
|
456
|
+
const value = row[key];
|
|
457
|
+
if (value === undefined) continue;
|
|
458
|
+
if (key === "cycles") {
|
|
459
|
+
const cycles = row.cycles!.map((cycle) =>
|
|
460
|
+
Object.keys(cycle).length === 0
|
|
461
|
+
? "{}"
|
|
462
|
+
: obj(
|
|
463
|
+
Object.entries(cycle).map(
|
|
464
|
+
([k, v]) => [k, num(v as number)] as const,
|
|
465
|
+
),
|
|
466
|
+
inner + INDENT,
|
|
467
|
+
0,
|
|
468
|
+
1,
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
const force =
|
|
472
|
+
cycles.length > 1 &&
|
|
473
|
+
row.cycles!.every((cycle) => Object.keys(cycle).length > 1);
|
|
474
|
+
fields.push([key, list(cycles, inner, "cycles: ".length, 1, force)]);
|
|
475
|
+
} else
|
|
476
|
+
fields.push([
|
|
477
|
+
key,
|
|
478
|
+
typeof value === "string"
|
|
479
|
+
? str(value)
|
|
480
|
+
: typeof value === "boolean"
|
|
481
|
+
? String(value)
|
|
482
|
+
: num(value as number),
|
|
483
|
+
]);
|
|
484
|
+
}
|
|
485
|
+
// Options are euclid's fifth argument, so rotate 0 is spelled out before them.
|
|
486
|
+
if (row.grid === undefined && !row.rotate && fields.length > 0)
|
|
487
|
+
args.push("0");
|
|
488
|
+
const name = row.grid !== undefined ? "grid" : "euclid";
|
|
489
|
+
const head = `${name}(${args.join(", ")}`;
|
|
490
|
+
if (fields.length === 0) return `${head})`;
|
|
491
|
+
const inlineObject = `{ ${fields.map(([k, v]) => `${k}: ${v}`).join(", ")} }`;
|
|
492
|
+
const inline = `${head}, ${inlineObject})`;
|
|
493
|
+
if (!inline.includes("\n") && indent.length + inline.length + 1 <= WIDTH)
|
|
494
|
+
return inline;
|
|
495
|
+
const body = fields.map(([k, v]) => `${inner}${k}: ${v},`).join("\n");
|
|
496
|
+
return `${head}, {\n${body}\n${indent}})`;
|
|
497
|
+
}
|
|
498
|
+
|
|
307
499
|
function printSampler(sampler: Sampler, indent: string): string {
|
|
308
500
|
const inner = indent + INDENT;
|
|
309
|
-
const voices =
|
|
310
|
-
.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
501
|
+
const voices = (at: string) =>
|
|
502
|
+
Object.keys(sampler.voices)
|
|
503
|
+
.sort()
|
|
504
|
+
.map((name) => {
|
|
505
|
+
const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : str(name);
|
|
506
|
+
return `${at}${key}: ${printSample(sampler.voices[name]!, at, `${key}: `.length)},`;
|
|
507
|
+
})
|
|
508
|
+
.join("\n");
|
|
509
|
+
if (sampler.mode === "oneshot")
|
|
510
|
+
return `sampler({\n${voices(inner)}\n${indent}})`;
|
|
511
|
+
// Prettier breaks every argument out when an earlier one contains a break,
|
|
512
|
+
// so the voices sit one level deeper (multi-line voices included).
|
|
513
|
+
const shifted = `{\n${voices(inner + INDENT)}\n${inner}}`;
|
|
319
514
|
return `sampler(\n${inner}${shifted},\n${inner}{ mode: ${str(sampler.mode)} },\n${indent})`;
|
|
320
515
|
}
|
|
321
516
|
|
|
517
|
+
/** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
|
|
518
|
+
function printWavetable(settings: TrackWavetable, indent: string): string {
|
|
519
|
+
const src = settings.table.src;
|
|
520
|
+
const name = src.startsWith(BUILTIN_TABLE_PREFIX)
|
|
521
|
+
? str(src.slice(BUILTIN_TABLE_PREFIX.length))
|
|
522
|
+
: printSample(settings.table, indent, "wavetable(".length);
|
|
523
|
+
const params: [string, string][] = [];
|
|
524
|
+
for (const key of Object.keys(WAVETABLE_PARAMS) as WavetableParam[])
|
|
525
|
+
if (settings[key] !== undefined) params.push([key, num(settings[key])]);
|
|
526
|
+
if (settings.warpmode) params.push(["warpmode", str(settings.warpmode)]);
|
|
527
|
+
if (params.length === 0) return `wavetable(${name})`;
|
|
528
|
+
const inline = `wavetable(${name}, { ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
529
|
+
if (
|
|
530
|
+
!inline.includes("\n") &&
|
|
531
|
+
indent.length + "instrument: ".length + inline.length + 1 <= WIDTH
|
|
532
|
+
)
|
|
533
|
+
return inline;
|
|
534
|
+
const inner = indent + INDENT;
|
|
535
|
+
if (
|
|
536
|
+
!name.includes("\n") &&
|
|
537
|
+
indent.length + "instrument: wavetable(".length + name.length + 4 <= WIDTH
|
|
538
|
+
)
|
|
539
|
+
return `wavetable(${name}, {\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
540
|
+
// Prettier breaks every argument out when the first one breaks.
|
|
541
|
+
const deeper = inner + INDENT;
|
|
542
|
+
const table = src.startsWith(BUILTIN_TABLE_PREFIX)
|
|
543
|
+
? name
|
|
544
|
+
: printSample(settings.table, inner, 0);
|
|
545
|
+
return `wavetable(\n${inner}${table},\n${inner}{\n${params.map(([k, v]) => `${deeper}${k}: ${v},`).join("\n")}\n${inner}},\n${indent})`;
|
|
546
|
+
}
|
|
547
|
+
|
|
322
548
|
function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
323
549
|
const entries: [string, string][] = [["src", str(ref.src)]];
|
|
550
|
+
// Pack sounds keep their pin in the file; local files are hashed on eval.
|
|
551
|
+
if (ref.src.startsWith("pack:")) {
|
|
552
|
+
if (ref.sha256 !== undefined) entries.push(["sha256", str(ref.sha256)]);
|
|
553
|
+
if (ref.url !== undefined) entries.push(["url", str(ref.url)]);
|
|
554
|
+
if (ref.license !== undefined) entries.push(["license", str(ref.license)]);
|
|
555
|
+
}
|
|
324
556
|
if (ref.root !== undefined)
|
|
325
557
|
entries.push(["root", str(midiToPitch(ref.root))]);
|
|
326
558
|
if (ref.begin !== undefined) entries.push(["begin", num(ref.begin)]);
|
|
@@ -329,6 +561,15 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
|
329
561
|
if (ref.speed !== undefined) entries.push(["speed", num(ref.speed)]);
|
|
330
562
|
if (ref.loop !== undefined) entries.push(["loop", String(ref.loop)]);
|
|
331
563
|
if (ref.choke !== undefined) entries.push(["choke", str(ref.choke)]);
|
|
564
|
+
if (ref.loopBegin !== undefined)
|
|
565
|
+
entries.push(["loopBegin", num(ref.loopBegin)]);
|
|
566
|
+
if (ref.loopEnd !== undefined) entries.push(["loopEnd", num(ref.loopEnd)]);
|
|
567
|
+
if (ref.clip !== undefined) entries.push(["clip", num(ref.clip)]);
|
|
568
|
+
if (ref.unit !== undefined) entries.push(["unit", str(ref.unit)]);
|
|
569
|
+
if (ref.fit !== undefined) entries.push(["fit", String(ref.fit)]);
|
|
570
|
+
if (ref.accelerate !== undefined)
|
|
571
|
+
entries.push(["accelerate", num(ref.accelerate)]);
|
|
572
|
+
if (ref.squiz !== undefined) entries.push(["squiz", num(ref.squiz)]);
|
|
332
573
|
if (entries.length === 1) return str(ref.src);
|
|
333
574
|
return obj(entries, indent, prefix, 1);
|
|
334
575
|
}
|
|
@@ -343,7 +584,24 @@ function obj(
|
|
|
343
584
|
const inline = `{ ${entries.map(([k, v]) => `${k}: ${v}`).join(", ")} }`;
|
|
344
585
|
if (indent.length + prefix + inline.length + trailing <= WIDTH) return inline;
|
|
345
586
|
const inner = indent + INDENT;
|
|
346
|
-
return `{\n${entries.map(([k, v]) =>
|
|
587
|
+
return `{\n${entries.map(([k, v]) => property(k, v, inner)).join("\n")}\n${indent}}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* One expanded `key: value,` line. Like prettier, a string too long for the
|
|
592
|
+
* line moves under its key (a 64-hex sha256 pin in a nested object does).
|
|
593
|
+
*/
|
|
594
|
+
function property(key: string, value: string, indent: string): string {
|
|
595
|
+
const line = `${indent}${key}: ${value},`;
|
|
596
|
+
// Prettier keeps a short key (under tabWidth + 3 = 5 characters) inline.
|
|
597
|
+
if (
|
|
598
|
+
line.length <= WIDTH ||
|
|
599
|
+
key.length < 5 ||
|
|
600
|
+
!value.startsWith('"') ||
|
|
601
|
+
value.includes("\n")
|
|
602
|
+
)
|
|
603
|
+
return line;
|
|
604
|
+
return `${indent}${key}:\n${indent}${INDENT}${value},`;
|
|
347
605
|
}
|
|
348
606
|
|
|
349
607
|
/** Array literal: inline when the line fits (and no forced break), else one item per line. */
|
|
@@ -366,6 +624,30 @@ function list(
|
|
|
366
624
|
return `[\n${items.map((item) => `${inner}${item},`).join("\n")}\n${indent}]`;
|
|
367
625
|
}
|
|
368
626
|
|
|
627
|
+
/** Effect parameter literal. */
|
|
628
|
+
function value(v: number | string | boolean): string {
|
|
629
|
+
return typeof v === "number"
|
|
630
|
+
? num(v)
|
|
631
|
+
: typeof v === "string"
|
|
632
|
+
? str(v)
|
|
633
|
+
: `${v}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** `[key, literal]` for each optional effect field that is set. */
|
|
637
|
+
function optional(
|
|
638
|
+
record: object,
|
|
639
|
+
keys: readonly string[],
|
|
640
|
+
): (readonly [string, string])[] {
|
|
641
|
+
const out: (readonly [string, string])[] = [];
|
|
642
|
+
for (const key of keys) {
|
|
643
|
+
const v = (record as Record<string, number | string | boolean | undefined>)[
|
|
644
|
+
key
|
|
645
|
+
];
|
|
646
|
+
if (v !== undefined) out.push([key, value(v)]);
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
|
|
369
651
|
/** Number the way prettier normalizes literals. */
|
|
370
652
|
export function num(value: number): string {
|
|
371
653
|
if (!Number.isFinite(value)) throw new RangeError(`cannot print ${value}`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK is one vendored file with no imports, so `core/sdk/v1.ts` carries
|
|
3
|
+
* a copy of the chord engine (`core/chords.ts`) between two marker lines.
|
|
4
|
+
* This keeps the copy exact: `bun core/sdk/sync-chords.ts` rewrites it and
|
|
5
|
+
* `sync-chords.test.ts` fails when it is stale.
|
|
6
|
+
*
|
|
7
|
+
* The copy is the module body from its first section rule up to
|
|
8
|
+
* `CHORD_PROCESS` (agent prose the SDK does not need), with `export`
|
|
9
|
+
* dropped so the engine stays private to the SDK.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { format } from "prettier";
|
|
15
|
+
|
|
16
|
+
export const BEGIN_MARKER =
|
|
17
|
+
"// BEGIN chord engine: generated from core/chords.ts by core/sdk/sync-chords.ts";
|
|
18
|
+
export const END_MARKER = "// END chord engine";
|
|
19
|
+
|
|
20
|
+
const ROOT = join(import.meta.dir, "..", "..");
|
|
21
|
+
export const CHORDS_PATH = join(ROOT, "core", "chords.ts");
|
|
22
|
+
export const SDK_PATH = join(ROOT, "core", "sdk", "v1.ts");
|
|
23
|
+
|
|
24
|
+
/** The engine block (markers included) for a `core/chords.ts` source. */
|
|
25
|
+
export function chordBlock(source: string): string {
|
|
26
|
+
const start = source.indexOf("// ----");
|
|
27
|
+
const end = source.indexOf("/** How dawg builds chords");
|
|
28
|
+
if (start < 0 || end < 0) throw new Error("core/chords.ts layout changed");
|
|
29
|
+
const body = source
|
|
30
|
+
.slice(start, end)
|
|
31
|
+
.replace(/^export /gm, "")
|
|
32
|
+
.trimEnd();
|
|
33
|
+
return `${BEGIN_MARKER}\n${body}\n${END_MARKER}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `sdk` with its engine block replaced, formatted like the repo. */
|
|
37
|
+
export async function syncedSdk(sdk: string, chords: string): Promise<string> {
|
|
38
|
+
const from = sdk.indexOf(BEGIN_MARKER);
|
|
39
|
+
const to = sdk.indexOf(END_MARKER);
|
|
40
|
+
if (from < 0 || to < 0) throw new Error("v1.ts has no chord engine markers");
|
|
41
|
+
const next =
|
|
42
|
+
sdk.slice(0, from) + chordBlock(chords) + sdk.slice(to + END_MARKER.length);
|
|
43
|
+
return format(next, { parser: "typescript", filepath: SDK_PATH });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (import.meta.main) {
|
|
47
|
+
const [sdk, chords] = await Promise.all([
|
|
48
|
+
readFile(SDK_PATH, "utf8"),
|
|
49
|
+
readFile(CHORDS_PATH, "utf8"),
|
|
50
|
+
]);
|
|
51
|
+
const next = await syncedSdk(sdk, chords);
|
|
52
|
+
if (next !== sdk) await writeFile(SDK_PATH, next);
|
|
53
|
+
console.log(next === sdk ? "v1.ts chord engine up to date" : "v1.ts updated");
|
|
54
|
+
}
|