@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/src/media/cli.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* dawg media analyze <file>
|
|
8
8
|
* dawg media notes <file> [--kind drums|bass|vocals|guitar|piano|other] [--from s] [--to s]
|
|
9
9
|
* dawg media sample <file> <name> [--begin 0..1] [--end 0..1] [--root C4]
|
|
10
|
-
* dawg media
|
|
10
|
+
* dawg media wavetable <file> <name> [--frames 64] [--start s] [--end s] [--method auto|slice|spectral] [--smooth 0..1]
|
|
11
|
+
2048-sample frames → tracks/<slug>/wavetables/<name>.wav
|
|
12
|
+
dawg media lyrics <file> [--lang en]
|
|
11
13
|
*
|
|
12
14
|
* Progress goes to stderr, the JSON result to stdout. Ctrl-C sends SIGTERM to
|
|
13
15
|
* the helper (SIGKILL after 15 s) and exits 130.
|
|
@@ -32,6 +34,8 @@ export const MEDIA_HELP = `dawg media — local media tools (see DAWG.md "Media
|
|
|
32
34
|
notes/hits + quantized snippet → <file>.<kind>.notes.json
|
|
33
35
|
dawg media sample <file> <name> [--begin f] [--end f] [--root C4]
|
|
34
36
|
48 kHz wav → tracks/<slug>/samples/<name>.wav + sampler snippet
|
|
37
|
+
dawg media wavetable <file> <name> [--frames 64] [--start s] [--end s] [--method auto|slice|spectral] [--smooth 0..1]
|
|
38
|
+
2048-sample frames → tracks/<slug>/wavetables/<name>.wav
|
|
35
39
|
dawg media lyrics <file> [--lang en] whisper transcript → <file>.lyrics.json/.txt
|
|
36
40
|
|
|
37
41
|
Options: --track <name> picks the track folder (default main); --json prints only the result.
|
|
@@ -44,9 +48,18 @@ const VERBS: Record<string, { tool: string; positional: string[] }> = {
|
|
|
44
48
|
notes: { tool: "transcribe_notes", positional: ["file"] },
|
|
45
49
|
sample: { tool: "import_sample", positional: ["file", "name"] },
|
|
46
50
|
lyrics: { tool: "transcribe_lyrics", positional: ["file"] },
|
|
51
|
+
wavetable: { tool: "make_wavetable", positional: ["file", "name"] },
|
|
47
52
|
};
|
|
48
53
|
|
|
49
|
-
const NUMERIC = new Set([
|
|
54
|
+
const NUMERIC = new Set([
|
|
55
|
+
"from",
|
|
56
|
+
"to",
|
|
57
|
+
"begin",
|
|
58
|
+
"end",
|
|
59
|
+
"frames",
|
|
60
|
+
"start",
|
|
61
|
+
"smooth",
|
|
62
|
+
]);
|
|
50
63
|
|
|
51
64
|
export function parseMediaArgv(argv: readonly string[]): {
|
|
52
65
|
verb: string | undefined;
|
package/src/media/tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The seven media tools as `AgentTool`s. `plan()` validates arguments from
|
|
3
3
|
* `unknown` and returns a `media` plan whose `run()` the agent loop executes
|
|
4
4
|
* with the host's `MediaHost` (project root, focused slug, runner, fetch),
|
|
5
5
|
* a cancellation signal and a progress sink. `dawg media <verb>` reuses the
|
|
@@ -9,6 +9,14 @@ import type { AgentTool, ToolPlan } from "../agent/tools.ts";
|
|
|
9
9
|
import { analyzeAudio } from "./analyze.ts";
|
|
10
10
|
import { downloadAudio } from "./download.ts";
|
|
11
11
|
import { importSample } from "./import.ts";
|
|
12
|
+
import { makeWavetableFile } from "./wavetable.ts";
|
|
13
|
+
import {
|
|
14
|
+
MAKE_MAX_FRAMES,
|
|
15
|
+
MAKE_METHODS,
|
|
16
|
+
MAKE_NORMALIZE,
|
|
17
|
+
type MakeMethod,
|
|
18
|
+
type MakeNormalize,
|
|
19
|
+
} from "../audio/wavetable-maker.ts";
|
|
12
20
|
import { transcribeLyrics } from "./lyrics.ts";
|
|
13
21
|
import { NOTE_KINDS, transcribeNotes, type NoteKind } from "./notes.ts";
|
|
14
22
|
import { MAX_NAME_LENGTH, trackSlug } from "./paths.ts";
|
|
@@ -259,6 +267,103 @@ export const MEDIA_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
259
267
|
);
|
|
260
268
|
},
|
|
261
269
|
},
|
|
270
|
+
{
|
|
271
|
+
name: "make_wavetable",
|
|
272
|
+
description:
|
|
273
|
+
"Make a wavetable from any audio file in the project (download, stem, imported sample): writes tracks/<slug>/wavetables/<name>.wav (float32, 2048-sample frames) and reports the detected pitch, method and how the timbre moves across the frames. Region defaults to the most stable tonal 2 s; method auto picks slice (pitched single cycles) or spectral (vocals, pads, noise). Then set_wavetable with that path as table.",
|
|
274
|
+
parameters: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
file: fileSchema,
|
|
278
|
+
name: {
|
|
279
|
+
type: "string",
|
|
280
|
+
maxLength: MAX_NAME_LENGTH,
|
|
281
|
+
description: "Table name: lowercase identifier such as vox or growl.",
|
|
282
|
+
},
|
|
283
|
+
frames: {
|
|
284
|
+
type: "integer",
|
|
285
|
+
minimum: 1,
|
|
286
|
+
maximum: MAKE_MAX_FRAMES,
|
|
287
|
+
description: "Frame count (default 64).",
|
|
288
|
+
},
|
|
289
|
+
start: {
|
|
290
|
+
type: "number",
|
|
291
|
+
minimum: 0,
|
|
292
|
+
description: "Region start, seconds.",
|
|
293
|
+
},
|
|
294
|
+
end: {
|
|
295
|
+
type: "number",
|
|
296
|
+
minimum: 0,
|
|
297
|
+
description: "Region end, seconds.",
|
|
298
|
+
},
|
|
299
|
+
method: { type: "string", enum: [...MAKE_METHODS] },
|
|
300
|
+
smooth: {
|
|
301
|
+
type: "number",
|
|
302
|
+
minimum: 0,
|
|
303
|
+
maximum: 1,
|
|
304
|
+
description: "Blend frames with neighbours for a smoother morph.",
|
|
305
|
+
},
|
|
306
|
+
normalize: { type: "string", enum: [...MAKE_NORMALIZE] },
|
|
307
|
+
},
|
|
308
|
+
required: ["file", "name"],
|
|
309
|
+
additionalProperties: false,
|
|
310
|
+
},
|
|
311
|
+
plan: (args) => {
|
|
312
|
+
const file = text(args, "file", 1024);
|
|
313
|
+
const name = sampleName(args);
|
|
314
|
+
const frames = args.frames;
|
|
315
|
+
if (
|
|
316
|
+
frames !== undefined &&
|
|
317
|
+
(typeof frames !== "number" ||
|
|
318
|
+
!Number.isInteger(frames) ||
|
|
319
|
+
frames < 1 ||
|
|
320
|
+
frames > MAKE_MAX_FRAMES)
|
|
321
|
+
)
|
|
322
|
+
throw new MediaArgumentError(
|
|
323
|
+
`frames must be an integer 1..${MAKE_MAX_FRAMES}`,
|
|
324
|
+
);
|
|
325
|
+
const seconds = (key: "start" | "end") => {
|
|
326
|
+
const value = args[key];
|
|
327
|
+
if (value === undefined) return undefined;
|
|
328
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
329
|
+
throw new MediaArgumentError(`${key} must be seconds ≥ 0`);
|
|
330
|
+
return value;
|
|
331
|
+
};
|
|
332
|
+
const start = seconds("start");
|
|
333
|
+
const end = seconds("end");
|
|
334
|
+
if (start !== undefined && end !== undefined && end <= start)
|
|
335
|
+
throw new MediaArgumentError("end must be greater than start");
|
|
336
|
+
const method = optionalText(args, "method", 16);
|
|
337
|
+
if (method !== undefined && !MAKE_METHODS.includes(method as MakeMethod))
|
|
338
|
+
throw new MediaArgumentError(
|
|
339
|
+
`method must be ${MAKE_METHODS.join(", ")}`,
|
|
340
|
+
);
|
|
341
|
+
const normalize = optionalText(args, "normalize", 16);
|
|
342
|
+
if (
|
|
343
|
+
normalize !== undefined &&
|
|
344
|
+
!MAKE_NORMALIZE.includes(normalize as MakeNormalize)
|
|
345
|
+
)
|
|
346
|
+
throw new MediaArgumentError(
|
|
347
|
+
`normalize must be ${MAKE_NORMALIZE.join(", ")}`,
|
|
348
|
+
);
|
|
349
|
+
const smooth = optionalFraction(args, "smooth");
|
|
350
|
+
return media(`make wavetable ${name} from ${file}`, (context) =>
|
|
351
|
+
makeWavetableFile(
|
|
352
|
+
{
|
|
353
|
+
file,
|
|
354
|
+
name,
|
|
355
|
+
...(frames !== undefined ? { frames: frames as number } : {}),
|
|
356
|
+
...(start !== undefined ? { start } : {}),
|
|
357
|
+
...(end !== undefined ? { end } : {}),
|
|
358
|
+
...(method ? { method: method as MakeMethod } : {}),
|
|
359
|
+
...(smooth !== undefined ? { smooth } : {}),
|
|
360
|
+
...(normalize ? { normalize: normalize as MakeNormalize } : {}),
|
|
361
|
+
},
|
|
362
|
+
context,
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
},
|
|
366
|
+
},
|
|
262
367
|
{
|
|
263
368
|
name: "transcribe_lyrics",
|
|
264
369
|
description:
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `make_wavetable(file, name, {...})`: turns any audio file in the project
|
|
3
|
+
* (a download, a stem, an imported sample) into a wavetable at
|
|
4
|
+
* `tracks/<slug>/wavetables/<name>.wav` — float32 frames of 2048 samples
|
|
5
|
+
* with a `clm ` chunk, the format Serum, Vital and uzu-wavetables use — and
|
|
6
|
+
* reports how the timbre moves across it so the model can pick a position.
|
|
7
|
+
*
|
|
8
|
+
* WAV inputs are read directly; anything else goes through ffmpeg to
|
|
9
|
+
* 48 kHz mono float. The extraction itself is `src/audio/wavetable-maker.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import { rename, stat } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { SCORE_LIMITS } from "../../core/score.ts";
|
|
14
|
+
import {
|
|
15
|
+
MAKE_FRAME_SIZE,
|
|
16
|
+
WavetableMakeError,
|
|
17
|
+
encodeWavetableWav,
|
|
18
|
+
makeWavetable,
|
|
19
|
+
type MakeMethod,
|
|
20
|
+
type MakeNormalize,
|
|
21
|
+
} from "../audio/wavetable-maker.ts";
|
|
22
|
+
import { missingTool } from "./backend.ts";
|
|
23
|
+
import {
|
|
24
|
+
ensureDir,
|
|
25
|
+
formatBytes,
|
|
26
|
+
projectPath,
|
|
27
|
+
resolveInput,
|
|
28
|
+
sha256File,
|
|
29
|
+
} from "./paths.ts";
|
|
30
|
+
import { MediaToolError, runHelper } from "./process.ts";
|
|
31
|
+
import {
|
|
32
|
+
MEDIA_LIMITS,
|
|
33
|
+
type MediaHost,
|
|
34
|
+
type MediaResult,
|
|
35
|
+
type MediaRunContext,
|
|
36
|
+
} from "./types.ts";
|
|
37
|
+
import { parseWav } from "./vendor/wav.ts";
|
|
38
|
+
|
|
39
|
+
/** Longest source read; the maker only analyses a bounded region of it. */
|
|
40
|
+
const MAX_SOURCE_SECONDS = 10 * 60;
|
|
41
|
+
const DECODE_RATE = 48_000;
|
|
42
|
+
|
|
43
|
+
export type MakeWavetableArgs = Readonly<{
|
|
44
|
+
file: string;
|
|
45
|
+
name: string;
|
|
46
|
+
frames?: number;
|
|
47
|
+
start?: number;
|
|
48
|
+
end?: number;
|
|
49
|
+
method?: MakeMethod;
|
|
50
|
+
smooth?: number;
|
|
51
|
+
normalize?: MakeNormalize;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
export function wavetablesDir(
|
|
55
|
+
host: Pick<MediaHost, "projectRoot" | "trackSlug">,
|
|
56
|
+
): string {
|
|
57
|
+
return join(host.projectRoot, "tracks", host.trackSlug, "wavetables");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mixdown(
|
|
61
|
+
wav: ReturnType<typeof parseWav>,
|
|
62
|
+
maxFrames: number,
|
|
63
|
+
): Float32Array {
|
|
64
|
+
const frames = Math.min(wav.sampleCount, maxFrames);
|
|
65
|
+
const out = new Float32Array(frames);
|
|
66
|
+
for (let i = 0; i < frames; i += 1) {
|
|
67
|
+
let sum = 0;
|
|
68
|
+
for (let c = 0; c < wav.channels; c += 1) sum += wav.sample(i, c);
|
|
69
|
+
out[i] = sum / wav.channels;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readSource(
|
|
75
|
+
context: MediaRunContext,
|
|
76
|
+
absolute: string,
|
|
77
|
+
size: number,
|
|
78
|
+
): Promise<{ mono: Float32Array; sampleRate: number }> {
|
|
79
|
+
const options = {
|
|
80
|
+
maximumBytes: 2 ** 31,
|
|
81
|
+
maximumDurationSeconds: 24 * 3600,
|
|
82
|
+
maximumChannels: 32,
|
|
83
|
+
};
|
|
84
|
+
if (/\.wav$/i.test(absolute) && size <= MEDIA_LIMITS.downloadMaxBytes) {
|
|
85
|
+
try {
|
|
86
|
+
const wav = parseWav(
|
|
87
|
+
new Uint8Array(await Bun.file(absolute).arrayBuffer()),
|
|
88
|
+
options,
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
mono: mixdown(wav, MAX_SOURCE_SECONDS * wav.sampleRate),
|
|
92
|
+
sampleRate: wav.sampleRate,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
// Unusual WAV (ADPCM, extensible quirks): let ffmpeg read it.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!context.runner.which("ffmpeg"))
|
|
99
|
+
throw new MediaToolError(missingTool("ffmpeg"));
|
|
100
|
+
const temp = join(
|
|
101
|
+
await ensureDir(wavetablesDir(context)),
|
|
102
|
+
`.decode-${process.pid}-${Date.now()}.part.wav`,
|
|
103
|
+
);
|
|
104
|
+
context.progress("ffmpeg → mono 48 kHz");
|
|
105
|
+
try {
|
|
106
|
+
await runHelper(
|
|
107
|
+
context,
|
|
108
|
+
[
|
|
109
|
+
"ffmpeg",
|
|
110
|
+
"-hide_banner",
|
|
111
|
+
"-nostdin",
|
|
112
|
+
"-loglevel",
|
|
113
|
+
"error",
|
|
114
|
+
"-y",
|
|
115
|
+
"-i",
|
|
116
|
+
absolute,
|
|
117
|
+
"-vn",
|
|
118
|
+
"-t",
|
|
119
|
+
String(MAX_SOURCE_SECONDS),
|
|
120
|
+
"-ar",
|
|
121
|
+
String(DECODE_RATE),
|
|
122
|
+
"-ac",
|
|
123
|
+
"1",
|
|
124
|
+
"-c:a",
|
|
125
|
+
"pcm_f32le",
|
|
126
|
+
temp,
|
|
127
|
+
],
|
|
128
|
+
{ timeoutMs: MEDIA_LIMITS.sampleTimeoutMs },
|
|
129
|
+
);
|
|
130
|
+
const wav = parseWav(
|
|
131
|
+
new Uint8Array(await Bun.file(temp).arrayBuffer()),
|
|
132
|
+
options,
|
|
133
|
+
);
|
|
134
|
+
return { mono: mixdown(wav, wav.sampleCount), sampleRate: wav.sampleRate };
|
|
135
|
+
} finally {
|
|
136
|
+
await Bun.file(temp)
|
|
137
|
+
.delete()
|
|
138
|
+
.catch(() => undefined);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function makeWavetableFile(
|
|
143
|
+
args: MakeWavetableArgs,
|
|
144
|
+
context: MediaRunContext,
|
|
145
|
+
): Promise<MediaResult> {
|
|
146
|
+
const input = await resolveInput(context, args.file);
|
|
147
|
+
const { mono, sampleRate } = await readSource(
|
|
148
|
+
context,
|
|
149
|
+
input.absolute,
|
|
150
|
+
input.size,
|
|
151
|
+
);
|
|
152
|
+
context.progress(`analysing ${input.relative}`);
|
|
153
|
+
let made;
|
|
154
|
+
try {
|
|
155
|
+
made = makeWavetable(mono, sampleRate, {
|
|
156
|
+
...(args.frames !== undefined ? { frames: args.frames } : {}),
|
|
157
|
+
...(args.start !== undefined ? { start: args.start } : {}),
|
|
158
|
+
...(args.end !== undefined ? { end: args.end } : {}),
|
|
159
|
+
...(args.method !== undefined ? { method: args.method } : {}),
|
|
160
|
+
...(args.smooth !== undefined ? { smooth: args.smooth } : {}),
|
|
161
|
+
...(args.normalize !== undefined ? { normalize: args.normalize } : {}),
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error instanceof WavetableMakeError)
|
|
165
|
+
throw new MediaToolError(error.message);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const bytes = encodeWavetableWav(made.frames);
|
|
169
|
+
if (bytes.byteLength > SCORE_LIMITS.maxSampleFileBytes)
|
|
170
|
+
throw new MediaToolError(
|
|
171
|
+
`${formatBytes(bytes.byteLength)} is over the sample file limit; use fewer frames`,
|
|
172
|
+
);
|
|
173
|
+
const dir = await ensureDir(wavetablesDir(context));
|
|
174
|
+
const target = join(dir, `${args.name}.wav`);
|
|
175
|
+
const temp = `${target}.part`;
|
|
176
|
+
await Bun.write(temp, bytes);
|
|
177
|
+
await rename(temp, target);
|
|
178
|
+
const sha256 = await sha256File(target);
|
|
179
|
+
const path = projectPath(context.projectRoot, target);
|
|
180
|
+
const size = (await stat(target)).size;
|
|
181
|
+
const region = `${made.region.start.toFixed(2)}–${made.region.end.toFixed(2)} s${made.region.auto ? " (auto)" : ""}`;
|
|
182
|
+
return {
|
|
183
|
+
summary: `wavetable ${args.name} · ${made.frames.length} frames · ${made.method} · ${region}`,
|
|
184
|
+
outputs: [path],
|
|
185
|
+
content: {
|
|
186
|
+
wavetable: {
|
|
187
|
+
name: args.name,
|
|
188
|
+
path,
|
|
189
|
+
sha256,
|
|
190
|
+
frames: made.frames.length,
|
|
191
|
+
frameSize: MAKE_FRAME_SIZE,
|
|
192
|
+
bytes: size,
|
|
193
|
+
source: input.relative,
|
|
194
|
+
method: made.method,
|
|
195
|
+
region: made.region,
|
|
196
|
+
...(made.pitch ? { pitch: made.pitch } : {}),
|
|
197
|
+
},
|
|
198
|
+
sweep: made.sweep,
|
|
199
|
+
next: `set_wavetable with table "${path}" plays it on a track (wt 0..1 picks the frame; wtenv or set_automation wt sweeps it). In track.ts: instrument: wavetable("./wavetables/${args.name}.wav").`,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -12,6 +12,13 @@ import { scoreFromJSON, type TrackScore } from "../core/score.ts";
|
|
|
12
12
|
import { decodeLoop } from "../core/loop.ts";
|
|
13
13
|
import { renderScoreWav } from "./audio/wav.ts";
|
|
14
14
|
import { SampleLibrary, hasSamplerTracks } from "./audio/samples.ts";
|
|
15
|
+
import {
|
|
16
|
+
PackStore,
|
|
17
|
+
creditsLine,
|
|
18
|
+
packCredits,
|
|
19
|
+
withWavComment,
|
|
20
|
+
writeCredits,
|
|
21
|
+
} from "./audio/packs.ts";
|
|
15
22
|
import { evaluateProject, formatDiagnostic } from "../core/sdk/eval.ts";
|
|
16
23
|
import { isProject } from "./project/init.ts";
|
|
17
24
|
import { resolveSessionArg } from "./session/attach.ts";
|
|
@@ -76,13 +83,27 @@ export async function runRenderCommand(
|
|
|
76
83
|
for (const problem of samples.problems)
|
|
77
84
|
stderr.write(`sample ${problem.level} · ${problem.message}\n`);
|
|
78
85
|
}
|
|
79
|
-
|
|
86
|
+
let wav = renderScoreWav(score, { samples });
|
|
87
|
+
// Pack sounds: name the packs (and CC-BY attributions) in the WAV's INFO
|
|
88
|
+
// comment and on stdout; CREDITS.md in a project keeps the attributions.
|
|
89
|
+
const refs = score.tracks.flatMap((track) =>
|
|
90
|
+
Object.values(track.sampler?.voices ?? {}),
|
|
91
|
+
);
|
|
92
|
+
let credits: string | undefined;
|
|
93
|
+
if (refs.some((ref) => ref.src.startsWith("pack:"))) {
|
|
94
|
+
const list = packCredits(refs, await new PackStore().list());
|
|
95
|
+
credits = creditsLine(list);
|
|
96
|
+
if (credits) wav = withWavComment(wav, `samples: ${credits}`);
|
|
97
|
+
if (await isProject(workspace).catch(() => false))
|
|
98
|
+
await writeCredits(workspace, list).catch(() => undefined);
|
|
99
|
+
}
|
|
80
100
|
const path = resolve(workspace, target);
|
|
81
101
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
82
102
|
await writeFile(temporary, wav);
|
|
83
103
|
await rename(temporary, path);
|
|
84
104
|
const sha = createHash("sha256").update(wav).digest("hex");
|
|
85
105
|
stdout.write(`rendered · ${target} · ${wav.byteLength} bytes · ${sha}\n`);
|
|
106
|
+
if (credits) stdout.write(`credits · ${credits}\n`);
|
|
86
107
|
return 0;
|
|
87
108
|
}
|
|
88
109
|
|
package/src/session/naming.ts
CHANGED
|
@@ -89,13 +89,15 @@ export function musicalFingerprint(score: TrackScore): Fingerprint {
|
|
|
89
89
|
if (track.filter) effects.add("filter");
|
|
90
90
|
if (track.delay) effects.add("delay");
|
|
91
91
|
if (track.reverb) effects.add("reverb");
|
|
92
|
+
for (const name of Object.keys(track.fx ?? {})) effects.add(name);
|
|
92
93
|
if (track.volumeAutomation.length || track.panAutomation.length)
|
|
93
94
|
effects.add("automation");
|
|
94
95
|
if (
|
|
95
96
|
track.filterAutomation?.length ||
|
|
96
97
|
track.resonanceAutomation?.length ||
|
|
97
98
|
track.delayFeedbackAutomation?.length ||
|
|
98
|
-
track.delayMixAutomation?.length
|
|
99
|
+
track.delayMixAutomation?.length ||
|
|
100
|
+
Object.keys(track.fxAutomation ?? {}).length
|
|
99
101
|
)
|
|
100
102
|
effects.add("sweep");
|
|
101
103
|
const instrument = track.instrument.toLowerCase().slice(0, 24);
|