@hraness/dawg 0.4.0 → 0.5.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 +86 -0
- package/DAWG.md +300 -35
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +37 -12
- package/core/expression.ts +1241 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +536 -56
- package/core/sdk/eval-child.ts +38 -3
- package/core/sdk/print.ts +445 -14
- package/core/sdk/v1.ts +1709 -23
- package/core/sections.ts +2046 -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 +27 -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 +25 -0
- package/guides/tempo.md +26 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +2 -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/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +29 -4
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +21 -3
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +36 -2
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +2 -0
- package/src/audio/arrange.ts +471 -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/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +12 -2
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/convolution.ts +43 -5
- 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/space.ts +197 -50
- package/src/audio/engine.ts +175 -20
- package/src/audio/live.ts +33 -6
- package/src/audio/loudness.ts +551 -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 +87 -16
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/sampler.ts +108 -19
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +86 -0
- package/src/audio/wav.ts +448 -59
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +934 -0
- package/src/commands/help.ts +281 -24
- package/src/commands/master.ts +361 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +964 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +826 -47
- package/src/render.ts +109 -7
- 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 +58 -5
- package/src/tui/euclid.ts +50 -6
- package/src/tui/fader.ts +409 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +656 -16
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +95 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +414 -41
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +79 -12
- 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
package/core/sdk/eval-child.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runs inside the evaluation subprocess (`bun --no-addons --no-install
|
|
3
3
|
* --smol eval-child.ts <project>`): imports the project's `song.ts`, fills
|
|
4
|
-
* sample hashes, and prints one JSON line. Never
|
|
4
|
+
* sample hashes and Scala tuning files, and prints one JSON line. Never
|
|
5
|
+
* imported by dawg itself.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { realpathSync } from "node:fs";
|
|
9
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
10
|
import { readFile, stat } from "node:fs/promises";
|
|
10
11
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
11
12
|
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { readTuningFiles } from "../tuning.ts";
|
|
12
14
|
|
|
13
15
|
const MAX_SAMPLE_FILE_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
const MAX_TUNING_FILE_BYTES = 1024 * 1024;
|
|
14
17
|
|
|
15
18
|
type Diagnostic = {
|
|
16
19
|
message: string;
|
|
@@ -30,7 +33,10 @@ async function main(): Promise<void> {
|
|
|
30
33
|
const song = module.default;
|
|
31
34
|
if (typeof song !== "object" || song === null)
|
|
32
35
|
throw new Error("song.ts must `export default song({...})`");
|
|
33
|
-
output = {
|
|
36
|
+
output = {
|
|
37
|
+
ok: true,
|
|
38
|
+
score: withTuningFiles(await withSampleHashes(song, project), project),
|
|
39
|
+
};
|
|
34
40
|
} catch (error) {
|
|
35
41
|
output = { ok: false, error: diagnostic(error, project) };
|
|
36
42
|
}
|
|
@@ -122,4 +128,33 @@ async function withSampleHashes(
|
|
|
122
128
|
return plain;
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Reads the Scala `.scl` and `.kbm` files a song or track tuning names, so
|
|
133
|
+
* the score carries the resolved table and renders without file access.
|
|
134
|
+
*/
|
|
135
|
+
function withTuningFiles(score: unknown, project: string): unknown {
|
|
136
|
+
if (typeof score !== "object" || score === null) return score;
|
|
137
|
+
const plain = score as Record<string, unknown>;
|
|
138
|
+
const read = (path: string): string | undefined => {
|
|
139
|
+
const file = resolve(project, path);
|
|
140
|
+
if (relative(project, file).startsWith("..")) return undefined;
|
|
141
|
+
try {
|
|
142
|
+
const info = statSync(file);
|
|
143
|
+
if (!info.isFile() || info.size > MAX_TUNING_FILE_BYTES) return undefined;
|
|
144
|
+
return readFileSync(file, "utf8");
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
if (plain.tuning) plain.tuning = readTuningFiles(plain.tuning, read, "song");
|
|
150
|
+
for (const track of Array.isArray(plain.tracks) ? plain.tracks : [])
|
|
151
|
+
if (typeof track === "object" && track !== null && track.tuning)
|
|
152
|
+
track.tuning = readTuningFiles(
|
|
153
|
+
track.tuning,
|
|
154
|
+
read,
|
|
155
|
+
`track ${String(track.name ?? track.id)}`,
|
|
156
|
+
);
|
|
157
|
+
return plain;
|
|
158
|
+
}
|
|
159
|
+
|
|
125
160
|
await main();
|
package/core/sdk/print.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { DRUM_VOICES, isDrumInstrument } from "../drums.ts";
|
|
11
|
+
import { DEFAULT_FIXED_VELOCITY } from "../expression.ts";
|
|
11
12
|
import type { RhythmRow } from "../euclid.ts";
|
|
12
13
|
import { FX_LANES, fxSpec, type FxName, type FxValues } from "../fx.ts";
|
|
14
|
+
import { MASTER_SPECS, MASTER_UNITS, type SongMaster } from "../master.ts";
|
|
13
15
|
import { midiToPitch } from "../pitch.ts";
|
|
14
16
|
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
15
17
|
import {
|
|
@@ -23,12 +25,16 @@ import {
|
|
|
23
25
|
type WavetableParam,
|
|
24
26
|
TrackScore,
|
|
25
27
|
type AutomationPoint,
|
|
28
|
+
type FormEntry,
|
|
26
29
|
type Note,
|
|
27
30
|
type SampleRef,
|
|
28
31
|
type Sampler,
|
|
32
|
+
type Section,
|
|
29
33
|
type Track,
|
|
30
34
|
} from "../score.ts";
|
|
31
35
|
import { trackSlug } from "../slug.ts";
|
|
36
|
+
import { barStartTick } from "../tempo.ts";
|
|
37
|
+
import type { Tuning } from "../tuning.ts";
|
|
32
38
|
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
|
|
33
39
|
|
|
34
40
|
const WIDTH = 80;
|
|
@@ -147,7 +153,12 @@ export function trackIdentifiers(
|
|
|
147
153
|
const out = new Map<string, string>();
|
|
148
154
|
for (const track of score.tracks) {
|
|
149
155
|
let base = dirs.get(track.id)!.replace(/-/g, "_");
|
|
150
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
!/^[a-z_]/.test(base) ||
|
|
158
|
+
RESERVED.has(base) ||
|
|
159
|
+
(score.time && TIME_HELPERS.includes(base))
|
|
160
|
+
)
|
|
161
|
+
base = `track_${base}`;
|
|
151
162
|
let name = base;
|
|
152
163
|
for (let n = 2; used.has(name); n += 1) name = `${base}_${n}`;
|
|
153
164
|
used.add(name);
|
|
@@ -162,7 +173,12 @@ export function printSong(
|
|
|
162
173
|
dirs: ReadonlyMap<string, string> = trackDirectories(score),
|
|
163
174
|
): string {
|
|
164
175
|
const ids = trackIdentifiers(score, dirs);
|
|
165
|
-
const
|
|
176
|
+
const time = printTime(score);
|
|
177
|
+
const helpers = [
|
|
178
|
+
"song",
|
|
179
|
+
...TIME_HELPERS.filter((name) => time.used.has(name)),
|
|
180
|
+
];
|
|
181
|
+
const lines: string[] = [`import { ${helpers.join(", ")} } from "dawg";`];
|
|
166
182
|
for (const track of score.tracks)
|
|
167
183
|
lines.push(
|
|
168
184
|
`import ${ids.get(track.id)!} from "./tracks/${dirs.get(track.id)!}/track.ts";`,
|
|
@@ -174,8 +190,14 @@ export function printSong(
|
|
|
174
190
|
`bars: ${num(score.bars)}`,
|
|
175
191
|
];
|
|
176
192
|
if (score.key !== null) entries.push(`key: ${str(score.key)}`);
|
|
193
|
+
if (score.tuning)
|
|
194
|
+
entries.push(
|
|
195
|
+
`tuning: ${printTuning(score.tuning, INDENT, "tuning: ".length)}`,
|
|
196
|
+
);
|
|
177
197
|
if (score.ticksPerBeat !== 480)
|
|
178
198
|
entries.push(`ticksPerBeat: ${num(score.ticksPerBeat)}`);
|
|
199
|
+
if (time.marks.length > 0)
|
|
200
|
+
entries.push(`time: ${list(time.marks, INDENT, "time: ".length, 1)}`);
|
|
179
201
|
entries.push(
|
|
180
202
|
`tracks: ${list(
|
|
181
203
|
score.tracks.map((track) => ids.get(track.id)!),
|
|
@@ -184,12 +206,150 @@ export function printSong(
|
|
|
184
206
|
1,
|
|
185
207
|
)}`,
|
|
186
208
|
);
|
|
209
|
+
if (score.master) entries.push(printMaster(score.master));
|
|
210
|
+
// Song sections and form (0.5): printed only when the song has them.
|
|
211
|
+
if (score.sections.length > 0)
|
|
212
|
+
entries.push(
|
|
213
|
+
`sections: ${list(
|
|
214
|
+
score.sections.map((section) => printSection(section, INDENT + INDENT)),
|
|
215
|
+
INDENT,
|
|
216
|
+
"sections: ".length,
|
|
217
|
+
1,
|
|
218
|
+
)}`,
|
|
219
|
+
);
|
|
220
|
+
if (score.form.length > 0)
|
|
221
|
+
entries.push(`form: ${printForm(score.form, INDENT)}`);
|
|
222
|
+
if (score.loopSection !== undefined)
|
|
223
|
+
entries.push(`loopSection: ${str(score.loopSection)}`);
|
|
187
224
|
lines.push("export default song({");
|
|
188
225
|
for (const entry of entries) lines.push(`${INDENT}${entry},`);
|
|
189
226
|
lines.push("});", "");
|
|
190
227
|
return lines.join("\n");
|
|
191
228
|
}
|
|
192
229
|
|
|
230
|
+
/** SDK time helpers `song.ts` may import, in import order. */
|
|
231
|
+
const TIME_HELPERS: readonly string[] = ["tempo", "ramp", "meter", "fermata"];
|
|
232
|
+
|
|
233
|
+
/** `song({ time })` marks for a score's tempo map, meters and fermatas. */
|
|
234
|
+
function printTime(score: TrackScore): {
|
|
235
|
+
marks: string[];
|
|
236
|
+
used: Set<string>;
|
|
237
|
+
} {
|
|
238
|
+
const marks: string[] = [];
|
|
239
|
+
const used = new Set<string>();
|
|
240
|
+
const time = score.time;
|
|
241
|
+
if (!time) return { marks, used };
|
|
242
|
+
const beat = (tick: number) => num(tick / score.ticksPerBeat);
|
|
243
|
+
type Mark = { tick: number; order: number; text: string };
|
|
244
|
+
const all: Mark[] = [];
|
|
245
|
+
for (const event of time.tempo ?? []) {
|
|
246
|
+
if (event.ramp) {
|
|
247
|
+
used.add("ramp");
|
|
248
|
+
const curve = event.ramp === "exp" ? ', "exp"' : "";
|
|
249
|
+
all.push({
|
|
250
|
+
tick: event.tick,
|
|
251
|
+
order: 1,
|
|
252
|
+
text: `ramp(${beat(event.tick)}, ${num(event.bpm)}${curve})`,
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
used.add("tempo");
|
|
256
|
+
all.push({
|
|
257
|
+
tick: event.tick,
|
|
258
|
+
order: 1,
|
|
259
|
+
text: `tempo(${beat(event.tick)}, ${num(event.bpm)})`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const change of time.meter ?? []) {
|
|
264
|
+
used.add("meter");
|
|
265
|
+
const tick = barStartTick(score, change.bar);
|
|
266
|
+
const unit = change.beatUnit ?? 4;
|
|
267
|
+
const value =
|
|
268
|
+
unit === 4
|
|
269
|
+
? num(change.beatsPerBar)
|
|
270
|
+
: `[${num(change.beatsPerBar)}, ${num(unit)}]`;
|
|
271
|
+
all.push({ tick, order: 0, text: `meter(${beat(tick)}, ${value})` });
|
|
272
|
+
}
|
|
273
|
+
for (const hold of time.fermatas ?? []) {
|
|
274
|
+
used.add("fermata");
|
|
275
|
+
all.push({
|
|
276
|
+
tick: hold.tick,
|
|
277
|
+
order: 2,
|
|
278
|
+
text: `fermata(${beat(hold.tick)}, ${num(hold.beats)})`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
all.sort((a, b) => a.tick - b.tick || a.order - b.order);
|
|
282
|
+
for (const mark of all) marks.push(mark.text);
|
|
283
|
+
return { marks, used };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* One `sections` item: `{ name: "verse", startBar: 0, bars: 8 }`. `indent`
|
|
288
|
+
* is the item's own line; expanded fields sit one level deeper.
|
|
289
|
+
*/
|
|
290
|
+
function printSection(section: Section, indent: string): string {
|
|
291
|
+
const inner = indent + INDENT;
|
|
292
|
+
const entries: (readonly [string, string])[] = [
|
|
293
|
+
["name", str(section.name)],
|
|
294
|
+
["startBar", num(section.startBar)],
|
|
295
|
+
["bars", num(section.bars)],
|
|
296
|
+
];
|
|
297
|
+
if (section.mute && section.mute.length > 0)
|
|
298
|
+
entries.push([
|
|
299
|
+
"mute",
|
|
300
|
+
list(section.mute.map(str), inner, "mute: ".length, 1),
|
|
301
|
+
]);
|
|
302
|
+
if (section.vary) {
|
|
303
|
+
const vary = Object.entries(section.vary).map(([id, change]) => {
|
|
304
|
+
const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(id) ? id : str(id);
|
|
305
|
+
const fields: (readonly [string, string])[] = [];
|
|
306
|
+
if (change.transpose !== undefined)
|
|
307
|
+
fields.push(["transpose", num(change.transpose)]);
|
|
308
|
+
if (change.gain !== undefined) fields.push(["gain", num(change.gain)]);
|
|
309
|
+
return [key, obj(fields, inner + INDENT, `${key}: `.length, 1)] as const;
|
|
310
|
+
});
|
|
311
|
+
if (vary.length > 0)
|
|
312
|
+
entries.push(["vary", obj(vary, inner, "vary: ".length, 1)]);
|
|
313
|
+
}
|
|
314
|
+
return obj(entries, indent, 0, 1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The form as a string when every name is plain (`"intro verse chorus*2"`),
|
|
319
|
+
* else as a list of entries.
|
|
320
|
+
*/
|
|
321
|
+
function printForm(form: readonly FormEntry[], indent: string): string {
|
|
322
|
+
// Word characters only, so the string reads back as the same entries.
|
|
323
|
+
const plain = form.every((entry) => /^\w+$/u.test(entry.section));
|
|
324
|
+
if (plain) {
|
|
325
|
+
const text = form
|
|
326
|
+
.map(
|
|
327
|
+
(entry) =>
|
|
328
|
+
`${entry.section}${(entry.repeat ?? 1) > 1 ? `*${entry.repeat}` : ""}`,
|
|
329
|
+
)
|
|
330
|
+
.join(" ");
|
|
331
|
+
return str(text);
|
|
332
|
+
}
|
|
333
|
+
return list(
|
|
334
|
+
form.map((entry) =>
|
|
335
|
+
(entry.repeat ?? 1) > 1
|
|
336
|
+
? obj(
|
|
337
|
+
[
|
|
338
|
+
["section", str(entry.section)],
|
|
339
|
+
["repeat", num(entry.repeat!)],
|
|
340
|
+
],
|
|
341
|
+
indent + INDENT,
|
|
342
|
+
0,
|
|
343
|
+
1,
|
|
344
|
+
)
|
|
345
|
+
: str(entry.section),
|
|
346
|
+
),
|
|
347
|
+
indent,
|
|
348
|
+
"form: ".length,
|
|
349
|
+
1,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
193
353
|
/** `tracks/<slug>/track.ts` for one track of a score. */
|
|
194
354
|
export function printTrack(score: TrackScore, track: Track): string {
|
|
195
355
|
// Rows whose lane still matches their expansion print as generators and
|
|
@@ -213,13 +373,13 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
213
373
|
if (slots) for (const [voice, slot] of slots) voiceFor.set(slot, voice);
|
|
214
374
|
const used = new Set<string>(["track"]);
|
|
215
375
|
const printed = notes.map((note) => {
|
|
216
|
-
const voice = voiceFor.get(note.pitch);
|
|
376
|
+
const voice = note.cents ? undefined : voiceFor.get(note.pitch);
|
|
217
377
|
if (voice !== undefined) {
|
|
218
378
|
used.add("hit");
|
|
219
|
-
return printHit(score, note, voice);
|
|
379
|
+
return printHit(score, note, voice, INDENT + INDENT);
|
|
220
380
|
}
|
|
221
381
|
used.add("note");
|
|
222
|
-
return printNote(score, note, !kit && !slots);
|
|
382
|
+
return printNote(score, note, !kit && !slots, INDENT + INDENT);
|
|
223
383
|
});
|
|
224
384
|
if (track.sampler) used.add("sampler");
|
|
225
385
|
const wavetable = isWavetableInstrument(track.instrument)
|
|
@@ -237,6 +397,28 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
237
397
|
entries.push(`instrument: ${printWavetable(wavetable, INDENT)}`);
|
|
238
398
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
239
399
|
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
400
|
+
if (track.time) {
|
|
401
|
+
const beats = (ticks: number) => num(ticks / score.ticksPerBeat);
|
|
402
|
+
const fields: (readonly [string, string])[] = [];
|
|
403
|
+
if (track.time.rate !== undefined)
|
|
404
|
+
fields.push(["rate", num(track.time.rate)]);
|
|
405
|
+
if (track.time.phase !== undefined)
|
|
406
|
+
fields.push(["phase", beats(track.time.phase)]);
|
|
407
|
+
if (track.time.cycle !== undefined)
|
|
408
|
+
fields.push(["cycle", beats(track.time.cycle)]);
|
|
409
|
+
if (track.time.steps) {
|
|
410
|
+
const { shift, hold, drift } = track.time.steps;
|
|
411
|
+
fields.push([
|
|
412
|
+
"steps",
|
|
413
|
+
`{ shift: ${beats(shift)}, hold: ${num(hold)}, drift: ${num(drift)} }`,
|
|
414
|
+
]);
|
|
415
|
+
}
|
|
416
|
+
entries.push(`time: ${obj(fields, INDENT, "time: ".length, 1)}`);
|
|
417
|
+
}
|
|
418
|
+
if (track.tuning)
|
|
419
|
+
entries.push(
|
|
420
|
+
`tuning: ${printTuning(track.tuning, INDENT, "tuning: ".length)}`,
|
|
421
|
+
);
|
|
240
422
|
if (track.muted) entries.push("muted: true");
|
|
241
423
|
if (track.solo) entries.push("solo: true");
|
|
242
424
|
if (track.volume !== 1) entries.push(`volume: ${num(track.volume)}`);
|
|
@@ -324,6 +506,7 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
324
506
|
);
|
|
325
507
|
entries.push(`synth: ${obj(params, INDENT, "synth: ".length, 1)}`);
|
|
326
508
|
}
|
|
509
|
+
entries.push(...performanceEntries(score, track));
|
|
327
510
|
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
328
511
|
["volume", track.volumeAutomation],
|
|
329
512
|
["pan", track.panAutomation],
|
|
@@ -407,23 +590,185 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
407
590
|
return lines.join("\n");
|
|
408
591
|
}
|
|
409
592
|
|
|
410
|
-
function printNote(
|
|
411
|
-
|
|
593
|
+
function printNote(
|
|
594
|
+
score: TrackScore,
|
|
595
|
+
note: Note,
|
|
596
|
+
named: boolean,
|
|
597
|
+
indent: string,
|
|
598
|
+
): string {
|
|
599
|
+
// A detuned note always prints by name: `"E4-14c"`.
|
|
600
|
+
const pitch = note.cents
|
|
601
|
+
? str(
|
|
602
|
+
`${midiToPitch(note.pitch)}${note.cents > 0 ? "+" : ""}${num(note.cents)}c`,
|
|
603
|
+
)
|
|
604
|
+
: named
|
|
605
|
+
? str(midiToPitch(note.pitch))
|
|
606
|
+
: num(note.pitch);
|
|
412
607
|
const args = [pitch, num(note.startTick / score.ticksPerBeat)];
|
|
413
608
|
const length = note.durationTicks / score.ticksPerBeat;
|
|
414
|
-
|
|
609
|
+
const how = expressionEntries(note, indent + INDENT);
|
|
610
|
+
if (length !== 1 || note.velocity !== DEFAULT_VELOCITY || how)
|
|
415
611
|
args.push(num(length));
|
|
416
|
-
if (note.velocity !== DEFAULT_VELOCITY) args.push(num(note.velocity));
|
|
417
|
-
return
|
|
612
|
+
if (note.velocity !== DEFAULT_VELOCITY || how) args.push(num(note.velocity));
|
|
613
|
+
return call("note", args, how, indent);
|
|
418
614
|
}
|
|
419
615
|
|
|
420
|
-
function printHit(
|
|
616
|
+
function printHit(
|
|
617
|
+
score: TrackScore,
|
|
618
|
+
note: Note,
|
|
619
|
+
voice: string,
|
|
620
|
+
indent: string,
|
|
621
|
+
): string {
|
|
421
622
|
const args = [str(voice), num(note.startTick / score.ticksPerBeat)];
|
|
422
623
|
const length = note.durationTicks / score.ticksPerBeat;
|
|
423
|
-
|
|
624
|
+
const how = expressionEntries(note, indent + INDENT);
|
|
625
|
+
if (
|
|
626
|
+
note.velocity !== DEFAULT_VELOCITY ||
|
|
627
|
+
length !== DEFAULT_HIT_LENGTH ||
|
|
628
|
+
how
|
|
629
|
+
)
|
|
424
630
|
args.push(num(note.velocity));
|
|
425
|
-
if (length !== DEFAULT_HIT_LENGTH) args.push(num(length));
|
|
426
|
-
return
|
|
631
|
+
if (length !== DEFAULT_HIT_LENGTH || how) args.push(num(length));
|
|
632
|
+
return call("hit", args, how, indent);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* A note's expression as the fields of its `how` argument, values laid out
|
|
637
|
+
* for property lines at `indent`; undefined when it has none.
|
|
638
|
+
*/
|
|
639
|
+
function expressionEntries(
|
|
640
|
+
note: Note,
|
|
641
|
+
indent: string,
|
|
642
|
+
): (readonly [string, string])[] | undefined {
|
|
643
|
+
const entries: (readonly [string, string])[] = [];
|
|
644
|
+
if (note.articulation) entries.push(["art", str(note.articulation)]);
|
|
645
|
+
if (note.glide !== undefined) entries.push(["glide", num(note.glide)]);
|
|
646
|
+
if (note.bend) {
|
|
647
|
+
const points = note.bend.map((p) => `[${num(p.at)}, ${num(p.cents)}]`);
|
|
648
|
+
// Prettier forces a break on arrays of 2+ arrays that each hold 2+ items.
|
|
649
|
+
entries.push([
|
|
650
|
+
"bend",
|
|
651
|
+
list(points, indent, "bend: ".length, 1, points.length > 1),
|
|
652
|
+
]);
|
|
653
|
+
}
|
|
654
|
+
if (note.vibrato)
|
|
655
|
+
entries.push([
|
|
656
|
+
"vibrato",
|
|
657
|
+
obj(
|
|
658
|
+
[
|
|
659
|
+
["rate", num(note.vibrato.rate)],
|
|
660
|
+
["depth", num(note.vibrato.depth)],
|
|
661
|
+
...(note.vibrato.delay !== undefined
|
|
662
|
+
? ([["delay", num(note.vibrato.delay)]] as const)
|
|
663
|
+
: []),
|
|
664
|
+
],
|
|
665
|
+
indent,
|
|
666
|
+
"vibrato: ".length,
|
|
667
|
+
1,
|
|
668
|
+
),
|
|
669
|
+
]);
|
|
670
|
+
if (note.humanize) {
|
|
671
|
+
const { timing, velocity, length } = note.humanize;
|
|
672
|
+
entries.push([
|
|
673
|
+
"humanize",
|
|
674
|
+
obj(
|
|
675
|
+
[
|
|
676
|
+
...(timing !== undefined ? ([["timing", num(timing)]] as const) : []),
|
|
677
|
+
...(velocity !== undefined
|
|
678
|
+
? ([["velocity", num(velocity)]] as const)
|
|
679
|
+
: []),
|
|
680
|
+
...(length !== undefined ? ([["length", num(length)]] as const) : []),
|
|
681
|
+
],
|
|
682
|
+
indent,
|
|
683
|
+
"humanize: ".length,
|
|
684
|
+
1,
|
|
685
|
+
),
|
|
686
|
+
]);
|
|
687
|
+
}
|
|
688
|
+
return entries.length === 0 ? undefined : entries;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* `name(args, how)` as a list item at `indent`: on one line when it fits,
|
|
693
|
+
* otherwise with the trailing `how` object hugged and broken one field per
|
|
694
|
+
* line, the way prettier prints a last object argument.
|
|
695
|
+
*/
|
|
696
|
+
function call(
|
|
697
|
+
name: string,
|
|
698
|
+
args: readonly string[],
|
|
699
|
+
how: readonly (readonly [string, string])[] | undefined,
|
|
700
|
+
indent: string,
|
|
701
|
+
): string {
|
|
702
|
+
if (!how) return `${name}(${args.join(", ")})`;
|
|
703
|
+
const inline = `${name}(${[...args, `{ ${how.map(([k, v]) => `${k}: ${v}`).join(", ")} }`].join(", ")})`;
|
|
704
|
+
if (!inline.includes("\n") && indent.length + inline.length + 1 <= WIDTH)
|
|
705
|
+
return inline;
|
|
706
|
+
const inner = indent + INDENT;
|
|
707
|
+
const body = how.map(([k, v]) => property(k, v, inner)).join("\n");
|
|
708
|
+
return `${name}(${[...args, "{"].join(", ")}\n${body}\n${indent}})`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** A track's performance fields (SDK 1.15.0) as `key: literal` entries. */
|
|
712
|
+
function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
713
|
+
const entries: string[] = [];
|
|
714
|
+
if (track.glide)
|
|
715
|
+
entries.push(
|
|
716
|
+
track.glide.mode === "legato"
|
|
717
|
+
? `glide: ${num(track.glide.time)}`
|
|
718
|
+
: `glide: ${obj(
|
|
719
|
+
[
|
|
720
|
+
["time", num(track.glide.time)],
|
|
721
|
+
["mode", str(track.glide.mode)],
|
|
722
|
+
],
|
|
723
|
+
INDENT,
|
|
724
|
+
"glide: ".length,
|
|
725
|
+
1,
|
|
726
|
+
)}`,
|
|
727
|
+
);
|
|
728
|
+
if (track.pedal) {
|
|
729
|
+
const events = track.pedal.map(
|
|
730
|
+
(event) =>
|
|
731
|
+
`[${num(event.tick / score.ticksPerBeat)}, ${str(event.state)}]`,
|
|
732
|
+
);
|
|
733
|
+
entries.push(
|
|
734
|
+
`pedal: ${list(events, INDENT, "pedal: ".length, 1, events.length > 1)}`,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (track.velocityCurve)
|
|
738
|
+
entries.push(
|
|
739
|
+
track.velocityCurve.curve !== "fixed" ||
|
|
740
|
+
(track.velocityCurve.fixed ?? DEFAULT_FIXED_VELOCITY) ===
|
|
741
|
+
DEFAULT_FIXED_VELOCITY
|
|
742
|
+
? `velocityCurve: ${str(track.velocityCurve.curve)}`
|
|
743
|
+
: `velocityCurve: ${obj(
|
|
744
|
+
[
|
|
745
|
+
["curve", str(track.velocityCurve.curve)],
|
|
746
|
+
["fixed", num(track.velocityCurve.fixed!)],
|
|
747
|
+
],
|
|
748
|
+
INDENT,
|
|
749
|
+
"velocityCurve: ".length,
|
|
750
|
+
1,
|
|
751
|
+
)}`,
|
|
752
|
+
);
|
|
753
|
+
if (track.humanize) {
|
|
754
|
+
const { timing, velocity, length, seed } = track.humanize;
|
|
755
|
+
entries.push(
|
|
756
|
+
`humanize: ${obj(
|
|
757
|
+
[
|
|
758
|
+
...(timing !== undefined ? ([["timing", num(timing)]] as const) : []),
|
|
759
|
+
...(velocity !== undefined
|
|
760
|
+
? ([["velocity", num(velocity)]] as const)
|
|
761
|
+
: []),
|
|
762
|
+
...(length !== undefined ? ([["length", num(length)]] as const) : []),
|
|
763
|
+
["seed", num(seed)],
|
|
764
|
+
],
|
|
765
|
+
INDENT,
|
|
766
|
+
"humanize: ".length,
|
|
767
|
+
1,
|
|
768
|
+
)}`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
return entries;
|
|
427
772
|
}
|
|
428
773
|
|
|
429
774
|
/** `euclid("hat", 7, 16, 2, { velocity: 0.5 })` or `grid("sd", "....x...")`. */
|
|
@@ -574,6 +919,92 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
|
574
919
|
return obj(entries, indent, prefix, 1);
|
|
575
920
|
}
|
|
576
921
|
|
|
922
|
+
/**
|
|
923
|
+
* A song or track tuning: a bare library name, or an object without the
|
|
924
|
+
* fields dawg resolved from Scala files (the table of an `scl`, the keymap
|
|
925
|
+
* of a `kbm`).
|
|
926
|
+
*/
|
|
927
|
+
function printTuning(tuning: Tuning, indent: string, prefix: number): string {
|
|
928
|
+
const entries: [string, string][] = [];
|
|
929
|
+
if (tuning.name !== undefined) entries.push(["name", str(tuning.name)]);
|
|
930
|
+
if (tuning.scl === undefined) {
|
|
931
|
+
const inner = indent + INDENT;
|
|
932
|
+
if (tuning.edo !== undefined) entries.push(["edo", num(tuning.edo)]);
|
|
933
|
+
if (tuning.ratios)
|
|
934
|
+
entries.push([
|
|
935
|
+
"ratios",
|
|
936
|
+
list(tuning.ratios.map(str), inner, "ratios: ".length, 1),
|
|
937
|
+
]);
|
|
938
|
+
if (tuning.cents)
|
|
939
|
+
entries.push([
|
|
940
|
+
"cents",
|
|
941
|
+
fill(tuning.cents.map(num), inner, "cents: ".length, 1),
|
|
942
|
+
]);
|
|
943
|
+
} else entries.push(["scl", str(tuning.scl)]);
|
|
944
|
+
if (tuning.kbm !== undefined) entries.push(["kbm", str(tuning.kbm)]);
|
|
945
|
+
if (tuning.ref !== undefined) entries.push(["ref", num(tuning.ref)]);
|
|
946
|
+
if (tuning.root !== undefined)
|
|
947
|
+
entries.push(["root", str(midiToPitch(tuning.root))]);
|
|
948
|
+
if (tuning.map !== undefined) entries.push(["map", str(tuning.map)]);
|
|
949
|
+
if (entries.length === 1 && entries[0]![0] === "name") return entries[0]![1];
|
|
950
|
+
return obj(entries, indent, prefix, 1);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Number array the way prettier prints one: inline when it fits, else
|
|
955
|
+
* filled (as many per line as fit, each followed by a comma).
|
|
956
|
+
*/
|
|
957
|
+
function fill(
|
|
958
|
+
items: readonly string[],
|
|
959
|
+
indent: string,
|
|
960
|
+
prefix: number,
|
|
961
|
+
trailing: number,
|
|
962
|
+
): string {
|
|
963
|
+
const inline = `[${items.join(", ")}]`;
|
|
964
|
+
if (
|
|
965
|
+
items.length < 2 ||
|
|
966
|
+
indent.length + prefix + inline.length + trailing <= WIDTH
|
|
967
|
+
)
|
|
968
|
+
return inline;
|
|
969
|
+
const inner = indent + INDENT;
|
|
970
|
+
const lines: string[] = [];
|
|
971
|
+
let line = "";
|
|
972
|
+
for (const item of items) {
|
|
973
|
+
const next = `${item},`;
|
|
974
|
+
if (line !== "" && inner.length + line.length + 1 + next.length <= WIDTH)
|
|
975
|
+
line += ` ${next}`;
|
|
976
|
+
else {
|
|
977
|
+
if (line !== "") lines.push(inner + line);
|
|
978
|
+
line = next;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
lines.push(inner + line);
|
|
982
|
+
return `[\n${lines.join("\n")}\n${indent}]`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** `master: { … }`, always expanded like `fx`; units in chain order. */
|
|
986
|
+
function printMaster(master: SongMaster): string {
|
|
987
|
+
const inner = INDENT + INDENT;
|
|
988
|
+
const body: string[] = [];
|
|
989
|
+
for (const unit of MASTER_UNITS) {
|
|
990
|
+
const values = master[unit];
|
|
991
|
+
if (!values) continue;
|
|
992
|
+
// Only values that differ from the default: loading fills the rest.
|
|
993
|
+
const params = Object.entries(MASTER_SPECS[unit].params)
|
|
994
|
+
.filter(
|
|
995
|
+
([key, spec]) =>
|
|
996
|
+
values[key] !== undefined && values[key] !== spec.default,
|
|
997
|
+
)
|
|
998
|
+
.map(([key]) => [key, value(values[key]!)] as const);
|
|
999
|
+
body.push(
|
|
1000
|
+
`${inner}${unit}: ${params.length === 0 ? "{}" : obj(params, inner, `${unit}: `.length, 1)},`,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (master.target !== undefined)
|
|
1004
|
+
body.push(`${inner}target: ${num(master.target)},`);
|
|
1005
|
+
return `master: {\n${body.join("\n")}\n${INDENT}}`;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
577
1008
|
/** Object literal: inline when the line fits, expanded otherwise (prettier keeps both). */
|
|
578
1009
|
function obj(
|
|
579
1010
|
entries: readonly (readonly [string, string])[],
|