@hraness/dawg 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
package/core/sdk/print.ts
CHANGED
|
@@ -8,9 +8,25 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { DRUM_VOICES, isDrumInstrument } from "../drums.ts";
|
|
11
|
+
import type { TrackGranular } from "../granular.ts";
|
|
12
|
+
import { DEFAULT_FIXED_VELOCITY } from "../expression.ts";
|
|
11
13
|
import type { RhythmRow } from "../euclid.ts";
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
FX_LANES,
|
|
16
|
+
RIG_STAGES,
|
|
17
|
+
fxSpec,
|
|
18
|
+
rigPresetKeys,
|
|
19
|
+
rigPresetOf,
|
|
20
|
+
type FxName,
|
|
21
|
+
type FxValues,
|
|
22
|
+
} from "../fx.ts";
|
|
23
|
+
import { MASTER_SPECS, MASTER_UNITS, type SongMaster } from "../master.ts";
|
|
13
24
|
import { midiToPitch } from "../pitch.ts";
|
|
25
|
+
import {
|
|
26
|
+
MODAL_INSTRUMENT,
|
|
27
|
+
MODAL_PARAMS,
|
|
28
|
+
type TrackModal,
|
|
29
|
+
} from "../resonators.ts";
|
|
14
30
|
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
15
31
|
import {
|
|
16
32
|
BUILTIN_TABLE_PREFIX,
|
|
@@ -23,12 +39,17 @@ import {
|
|
|
23
39
|
type WavetableParam,
|
|
24
40
|
TrackScore,
|
|
25
41
|
type AutomationPoint,
|
|
42
|
+
type FormEntry,
|
|
26
43
|
type Note,
|
|
27
44
|
type SampleRef,
|
|
28
45
|
type Sampler,
|
|
46
|
+
type Section,
|
|
29
47
|
type Track,
|
|
30
48
|
} from "../score.ts";
|
|
31
49
|
import { trackSlug } from "../slug.ts";
|
|
50
|
+
import { barStartTick } from "../tempo.ts";
|
|
51
|
+
import type { Tuning } from "../tuning.ts";
|
|
52
|
+
import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
|
|
32
53
|
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
|
|
33
54
|
|
|
34
55
|
const WIDTH = 80;
|
|
@@ -131,6 +152,9 @@ const RESERVED = new Set([
|
|
|
131
152
|
"every",
|
|
132
153
|
"sampler",
|
|
133
154
|
"wavetable",
|
|
155
|
+
"rig",
|
|
156
|
+
"granular",
|
|
157
|
+
"modal",
|
|
134
158
|
"slices",
|
|
135
159
|
"euclid",
|
|
136
160
|
"euclidRot",
|
|
@@ -147,7 +171,12 @@ export function trackIdentifiers(
|
|
|
147
171
|
const out = new Map<string, string>();
|
|
148
172
|
for (const track of score.tracks) {
|
|
149
173
|
let base = dirs.get(track.id)!.replace(/-/g, "_");
|
|
150
|
-
if (
|
|
174
|
+
if (
|
|
175
|
+
!/^[a-z_]/.test(base) ||
|
|
176
|
+
RESERVED.has(base) ||
|
|
177
|
+
(score.time && TIME_HELPERS.includes(base))
|
|
178
|
+
)
|
|
179
|
+
base = `track_${base}`;
|
|
151
180
|
let name = base;
|
|
152
181
|
for (let n = 2; used.has(name); n += 1) name = `${base}_${n}`;
|
|
153
182
|
used.add(name);
|
|
@@ -162,7 +191,12 @@ export function printSong(
|
|
|
162
191
|
dirs: ReadonlyMap<string, string> = trackDirectories(score),
|
|
163
192
|
): string {
|
|
164
193
|
const ids = trackIdentifiers(score, dirs);
|
|
165
|
-
const
|
|
194
|
+
const time = printTime(score);
|
|
195
|
+
const helpers = [
|
|
196
|
+
"song",
|
|
197
|
+
...TIME_HELPERS.filter((name) => time.used.has(name)),
|
|
198
|
+
];
|
|
199
|
+
const lines: string[] = [`import { ${helpers.join(", ")} } from "dawg";`];
|
|
166
200
|
for (const track of score.tracks)
|
|
167
201
|
lines.push(
|
|
168
202
|
`import ${ids.get(track.id)!} from "./tracks/${dirs.get(track.id)!}/track.ts";`,
|
|
@@ -174,8 +208,14 @@ export function printSong(
|
|
|
174
208
|
`bars: ${num(score.bars)}`,
|
|
175
209
|
];
|
|
176
210
|
if (score.key !== null) entries.push(`key: ${str(score.key)}`);
|
|
211
|
+
if (score.tuning)
|
|
212
|
+
entries.push(
|
|
213
|
+
`tuning: ${printTuning(score.tuning, INDENT, "tuning: ".length)}`,
|
|
214
|
+
);
|
|
177
215
|
if (score.ticksPerBeat !== 480)
|
|
178
216
|
entries.push(`ticksPerBeat: ${num(score.ticksPerBeat)}`);
|
|
217
|
+
if (time.marks.length > 0)
|
|
218
|
+
entries.push(`time: ${list(time.marks, INDENT, "time: ".length, 1)}`);
|
|
179
219
|
entries.push(
|
|
180
220
|
`tracks: ${list(
|
|
181
221
|
score.tracks.map((track) => ids.get(track.id)!),
|
|
@@ -184,12 +224,150 @@ export function printSong(
|
|
|
184
224
|
1,
|
|
185
225
|
)}`,
|
|
186
226
|
);
|
|
227
|
+
if (score.master) entries.push(printMaster(score.master));
|
|
228
|
+
// Song sections and form (0.5): printed only when the song has them.
|
|
229
|
+
if (score.sections.length > 0)
|
|
230
|
+
entries.push(
|
|
231
|
+
`sections: ${list(
|
|
232
|
+
score.sections.map((section) => printSection(section, INDENT + INDENT)),
|
|
233
|
+
INDENT,
|
|
234
|
+
"sections: ".length,
|
|
235
|
+
1,
|
|
236
|
+
)}`,
|
|
237
|
+
);
|
|
238
|
+
if (score.form.length > 0)
|
|
239
|
+
entries.push(`form: ${printForm(score.form, INDENT)}`);
|
|
240
|
+
if (score.loopSection !== undefined)
|
|
241
|
+
entries.push(`loopSection: ${str(score.loopSection)}`);
|
|
187
242
|
lines.push("export default song({");
|
|
188
243
|
for (const entry of entries) lines.push(`${INDENT}${entry},`);
|
|
189
244
|
lines.push("});", "");
|
|
190
245
|
return lines.join("\n");
|
|
191
246
|
}
|
|
192
247
|
|
|
248
|
+
/** SDK time helpers `song.ts` may import, in import order. */
|
|
249
|
+
const TIME_HELPERS: readonly string[] = ["tempo", "ramp", "meter", "fermata"];
|
|
250
|
+
|
|
251
|
+
/** `song({ time })` marks for a score's tempo map, meters and fermatas. */
|
|
252
|
+
function printTime(score: TrackScore): {
|
|
253
|
+
marks: string[];
|
|
254
|
+
used: Set<string>;
|
|
255
|
+
} {
|
|
256
|
+
const marks: string[] = [];
|
|
257
|
+
const used = new Set<string>();
|
|
258
|
+
const time = score.time;
|
|
259
|
+
if (!time) return { marks, used };
|
|
260
|
+
const beat = (tick: number) => num(tick / score.ticksPerBeat);
|
|
261
|
+
type Mark = { tick: number; order: number; text: string };
|
|
262
|
+
const all: Mark[] = [];
|
|
263
|
+
for (const event of time.tempo ?? []) {
|
|
264
|
+
if (event.ramp) {
|
|
265
|
+
used.add("ramp");
|
|
266
|
+
const curve = event.ramp === "exp" ? ', "exp"' : "";
|
|
267
|
+
all.push({
|
|
268
|
+
tick: event.tick,
|
|
269
|
+
order: 1,
|
|
270
|
+
text: `ramp(${beat(event.tick)}, ${num(event.bpm)}${curve})`,
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
used.add("tempo");
|
|
274
|
+
all.push({
|
|
275
|
+
tick: event.tick,
|
|
276
|
+
order: 1,
|
|
277
|
+
text: `tempo(${beat(event.tick)}, ${num(event.bpm)})`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const change of time.meter ?? []) {
|
|
282
|
+
used.add("meter");
|
|
283
|
+
const tick = barStartTick(score, change.bar);
|
|
284
|
+
const unit = change.beatUnit ?? 4;
|
|
285
|
+
const value =
|
|
286
|
+
unit === 4
|
|
287
|
+
? num(change.beatsPerBar)
|
|
288
|
+
: `[${num(change.beatsPerBar)}, ${num(unit)}]`;
|
|
289
|
+
all.push({ tick, order: 0, text: `meter(${beat(tick)}, ${value})` });
|
|
290
|
+
}
|
|
291
|
+
for (const hold of time.fermatas ?? []) {
|
|
292
|
+
used.add("fermata");
|
|
293
|
+
all.push({
|
|
294
|
+
tick: hold.tick,
|
|
295
|
+
order: 2,
|
|
296
|
+
text: `fermata(${beat(hold.tick)}, ${num(hold.beats)})`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
all.sort((a, b) => a.tick - b.tick || a.order - b.order);
|
|
300
|
+
for (const mark of all) marks.push(mark.text);
|
|
301
|
+
return { marks, used };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* One `sections` item: `{ name: "verse", startBar: 0, bars: 8 }`. `indent`
|
|
306
|
+
* is the item's own line; expanded fields sit one level deeper.
|
|
307
|
+
*/
|
|
308
|
+
function printSection(section: Section, indent: string): string {
|
|
309
|
+
const inner = indent + INDENT;
|
|
310
|
+
const entries: (readonly [string, string])[] = [
|
|
311
|
+
["name", str(section.name)],
|
|
312
|
+
["startBar", num(section.startBar)],
|
|
313
|
+
["bars", num(section.bars)],
|
|
314
|
+
];
|
|
315
|
+
if (section.mute && section.mute.length > 0)
|
|
316
|
+
entries.push([
|
|
317
|
+
"mute",
|
|
318
|
+
list(section.mute.map(str), inner, "mute: ".length, 1),
|
|
319
|
+
]);
|
|
320
|
+
if (section.vary) {
|
|
321
|
+
const vary = Object.entries(section.vary).map(([id, change]) => {
|
|
322
|
+
const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(id) ? id : str(id);
|
|
323
|
+
const fields: (readonly [string, string])[] = [];
|
|
324
|
+
if (change.transpose !== undefined)
|
|
325
|
+
fields.push(["transpose", num(change.transpose)]);
|
|
326
|
+
if (change.gain !== undefined) fields.push(["gain", num(change.gain)]);
|
|
327
|
+
return [key, obj(fields, inner + INDENT, `${key}: `.length, 1)] as const;
|
|
328
|
+
});
|
|
329
|
+
if (vary.length > 0)
|
|
330
|
+
entries.push(["vary", obj(vary, inner, "vary: ".length, 1)]);
|
|
331
|
+
}
|
|
332
|
+
return obj(entries, indent, 0, 1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The form as a string when every name is plain (`"intro verse chorus*2"`),
|
|
337
|
+
* else as a list of entries.
|
|
338
|
+
*/
|
|
339
|
+
function printForm(form: readonly FormEntry[], indent: string): string {
|
|
340
|
+
// Word characters only, so the string reads back as the same entries.
|
|
341
|
+
const plain = form.every((entry) => /^\w+$/u.test(entry.section));
|
|
342
|
+
if (plain) {
|
|
343
|
+
const text = form
|
|
344
|
+
.map(
|
|
345
|
+
(entry) =>
|
|
346
|
+
`${entry.section}${(entry.repeat ?? 1) > 1 ? `*${entry.repeat}` : ""}`,
|
|
347
|
+
)
|
|
348
|
+
.join(" ");
|
|
349
|
+
return str(text);
|
|
350
|
+
}
|
|
351
|
+
return list(
|
|
352
|
+
form.map((entry) =>
|
|
353
|
+
(entry.repeat ?? 1) > 1
|
|
354
|
+
? obj(
|
|
355
|
+
[
|
|
356
|
+
["section", str(entry.section)],
|
|
357
|
+
["repeat", num(entry.repeat!)],
|
|
358
|
+
],
|
|
359
|
+
indent + INDENT,
|
|
360
|
+
0,
|
|
361
|
+
1,
|
|
362
|
+
)
|
|
363
|
+
: str(entry.section),
|
|
364
|
+
),
|
|
365
|
+
indent,
|
|
366
|
+
"form: ".length,
|
|
367
|
+
1,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
193
371
|
/** `tracks/<slug>/track.ts` for one track of a score. */
|
|
194
372
|
export function printTrack(score: TrackScore, track: Track): string {
|
|
195
373
|
// Rows whose lane still matches their expansion print as generators and
|
|
@@ -213,30 +391,85 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
213
391
|
if (slots) for (const [voice, slot] of slots) voiceFor.set(slot, voice);
|
|
214
392
|
const used = new Set<string>(["track"]);
|
|
215
393
|
const printed = notes.map((note) => {
|
|
216
|
-
const voice = voiceFor.get(note.pitch);
|
|
394
|
+
const voice = note.cents ? undefined : voiceFor.get(note.pitch);
|
|
217
395
|
if (voice !== undefined) {
|
|
218
396
|
used.add("hit");
|
|
219
|
-
return printHit(score, note, voice);
|
|
397
|
+
return printHit(score, note, voice, INDENT + INDENT);
|
|
220
398
|
}
|
|
221
399
|
used.add("note");
|
|
222
|
-
return printNote(score, note, !kit && !slots);
|
|
400
|
+
return printNote(score, note, !kit && !slots, INDENT + INDENT);
|
|
223
401
|
});
|
|
402
|
+
const grained =
|
|
403
|
+
track.instrument === "granular" && track.granular !== undefined;
|
|
404
|
+
if (grained) used.add("granular");
|
|
224
405
|
if (track.sampler) used.add("sampler");
|
|
225
406
|
const wavetable = isWavetableInstrument(track.instrument)
|
|
226
407
|
? wavetableOf(track)
|
|
227
408
|
: undefined;
|
|
228
409
|
if (wavetable) used.add("wavetable");
|
|
410
|
+
const stringed = track.instrument === "string" && track.string !== undefined;
|
|
411
|
+
if (stringed) used.add("stringed");
|
|
412
|
+
const modalCall =
|
|
413
|
+
track.instrument === MODAL_INSTRUMENT && track.modal
|
|
414
|
+
? printModal(track.modal, INDENT)
|
|
415
|
+
: undefined;
|
|
416
|
+
if (modalCall?.startsWith("modal(")) used.add("modal");
|
|
229
417
|
|
|
230
418
|
const entries: string[] = [
|
|
231
419
|
`id: ${str(track.id)}`,
|
|
232
420
|
`name: ${str(track.name)}`,
|
|
233
421
|
];
|
|
234
|
-
if (
|
|
422
|
+
if (grained) {
|
|
423
|
+
entries.push(`instrument: ${printGranular(track.granular!, INDENT)}`);
|
|
424
|
+
// The sampler a granular track keeps for `grain off`.
|
|
425
|
+
if (track.sampler)
|
|
426
|
+
entries.push(`sampler: ${printSampler(track.sampler, INDENT)}`);
|
|
427
|
+
} else if (track.sampler) {
|
|
235
428
|
entries.push(`instrument: ${printSampler(track.sampler, INDENT)}`);
|
|
236
429
|
} else if (wavetable) {
|
|
237
430
|
entries.push(`instrument: ${printWavetable(wavetable, INDENT)}`);
|
|
431
|
+
} else if (stringed) {
|
|
432
|
+
entries.push(`instrument: ${printStringed(track.string!, INDENT)}`);
|
|
433
|
+
} else if (modalCall) {
|
|
434
|
+
entries.push(`instrument: ${modalCall}`);
|
|
238
435
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
239
436
|
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
437
|
+
if (track.granular && !grained)
|
|
438
|
+
entries.push(
|
|
439
|
+
`granular: ${obj(
|
|
440
|
+
[
|
|
441
|
+
...(track.granular.preset
|
|
442
|
+
? [["preset", str(track.granular.preset)] as const]
|
|
443
|
+
: []),
|
|
444
|
+
...granularEntries(track.granular, INDENT),
|
|
445
|
+
],
|
|
446
|
+
INDENT,
|
|
447
|
+
"granular: ".length,
|
|
448
|
+
1,
|
|
449
|
+
)}`,
|
|
450
|
+
);
|
|
451
|
+
if (track.time) {
|
|
452
|
+
const beats = (ticks: number) => num(ticks / score.ticksPerBeat);
|
|
453
|
+
const fields: (readonly [string, string])[] = [];
|
|
454
|
+
if (track.time.rate !== undefined)
|
|
455
|
+
fields.push(["rate", num(track.time.rate)]);
|
|
456
|
+
if (track.time.phase !== undefined)
|
|
457
|
+
fields.push(["phase", beats(track.time.phase)]);
|
|
458
|
+
if (track.time.cycle !== undefined)
|
|
459
|
+
fields.push(["cycle", beats(track.time.cycle)]);
|
|
460
|
+
if (track.time.steps) {
|
|
461
|
+
const { shift, hold, drift } = track.time.steps;
|
|
462
|
+
fields.push([
|
|
463
|
+
"steps",
|
|
464
|
+
`{ shift: ${beats(shift)}, hold: ${num(hold)}, drift: ${num(drift)} }`,
|
|
465
|
+
]);
|
|
466
|
+
}
|
|
467
|
+
entries.push(`time: ${obj(fields, INDENT, "time: ".length, 1)}`);
|
|
468
|
+
}
|
|
469
|
+
if (track.tuning)
|
|
470
|
+
entries.push(
|
|
471
|
+
`tuning: ${printTuning(track.tuning, INDENT, "tuning: ".length)}`,
|
|
472
|
+
);
|
|
240
473
|
if (track.muted) entries.push("muted: true");
|
|
241
474
|
if (track.solo) entries.push("solo: true");
|
|
242
475
|
if (track.volume !== 1) entries.push(`volume: ${num(track.volume)}`);
|
|
@@ -298,18 +531,31 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
298
531
|
)}`,
|
|
299
532
|
);
|
|
300
533
|
if (track.fx) {
|
|
301
|
-
|
|
534
|
+
// A whole rig preset prints as `...rig("crunch")` (SDK 1.22.0).
|
|
535
|
+
const rigName = rigPresetOf(track.fx);
|
|
536
|
+
if (rigName) used.add("rig");
|
|
537
|
+
const effects = (Object.entries(track.fx) as [FxName, FxValues][]).filter(
|
|
538
|
+
([effect]) =>
|
|
539
|
+
!rigName ||
|
|
540
|
+
!(
|
|
541
|
+
(RIG_STAGES as readonly string[]).includes(effect) ||
|
|
542
|
+
rigPresetKeys(rigName).includes(effect)
|
|
543
|
+
),
|
|
544
|
+
);
|
|
302
545
|
const inner = INDENT + INDENT;
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
546
|
+
const rigLine = rigName ? [`${inner}...rig(${str(rigName)}),`] : [];
|
|
547
|
+
const body = rigLine.concat(
|
|
548
|
+
effects.map(([effect, values]) => {
|
|
549
|
+
// Only values that differ from the default: decoding fills the rest.
|
|
550
|
+
const params = Object.entries(fxSpec(effect).params)
|
|
551
|
+
.filter(
|
|
552
|
+
([key, spec]) =>
|
|
553
|
+
values[key] !== undefined && values[key] !== spec.default,
|
|
554
|
+
)
|
|
555
|
+
.map(([key]) => [key, value(values[key]!)] as const);
|
|
556
|
+
return `${inner}${effect}: ${params.length === 0 ? "{}" : obj(params, inner, `${effect}: `.length, 1)},`;
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
313
559
|
entries.push(`fx: {\n${body.join("\n")}\n${INDENT}}`);
|
|
314
560
|
}
|
|
315
561
|
if (track.synth) {
|
|
@@ -324,6 +570,22 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
324
570
|
);
|
|
325
571
|
entries.push(`synth: ${obj(params, INDENT, "synth: ".length, 1)}`);
|
|
326
572
|
}
|
|
573
|
+
if (track.string && !stringed) {
|
|
574
|
+
const params = Object.entries(track.string).map(
|
|
575
|
+
([key, v]) => [key, value(v!)] as const,
|
|
576
|
+
);
|
|
577
|
+
entries.push(`string: ${obj(params, INDENT, "string: ".length, 1)}`);
|
|
578
|
+
}
|
|
579
|
+
if (track.keys) {
|
|
580
|
+
// `{}` is meaningful: it turns the modelled piano on.
|
|
581
|
+
const params = Object.entries(track.keys).map(
|
|
582
|
+
([key, v]) => [key, value(v)] as const,
|
|
583
|
+
);
|
|
584
|
+
entries.push(
|
|
585
|
+
`keys: ${params.length === 0 ? "{}" : obj(params, INDENT, "keys: ".length, 1)}`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
entries.push(...performanceEntries(score, track));
|
|
327
589
|
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
328
590
|
["volume", track.volumeAutomation],
|
|
329
591
|
["pan", track.panAutomation],
|
|
@@ -392,6 +654,10 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
392
654
|
"every",
|
|
393
655
|
"sampler",
|
|
394
656
|
"wavetable",
|
|
657
|
+
"stringed",
|
|
658
|
+
"rig",
|
|
659
|
+
"granular",
|
|
660
|
+
"modal",
|
|
395
661
|
"euclid",
|
|
396
662
|
"grid",
|
|
397
663
|
]
|
|
@@ -407,23 +673,185 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
407
673
|
return lines.join("\n");
|
|
408
674
|
}
|
|
409
675
|
|
|
410
|
-
function printNote(
|
|
411
|
-
|
|
676
|
+
function printNote(
|
|
677
|
+
score: TrackScore,
|
|
678
|
+
note: Note,
|
|
679
|
+
named: boolean,
|
|
680
|
+
indent: string,
|
|
681
|
+
): string {
|
|
682
|
+
// A detuned note always prints by name: `"E4-14c"`.
|
|
683
|
+
const pitch = note.cents
|
|
684
|
+
? str(
|
|
685
|
+
`${midiToPitch(note.pitch)}${note.cents > 0 ? "+" : ""}${num(note.cents)}c`,
|
|
686
|
+
)
|
|
687
|
+
: named
|
|
688
|
+
? str(midiToPitch(note.pitch))
|
|
689
|
+
: num(note.pitch);
|
|
412
690
|
const args = [pitch, num(note.startTick / score.ticksPerBeat)];
|
|
413
691
|
const length = note.durationTicks / score.ticksPerBeat;
|
|
414
|
-
|
|
692
|
+
const how = expressionEntries(note, indent + INDENT);
|
|
693
|
+
if (length !== 1 || note.velocity !== DEFAULT_VELOCITY || how)
|
|
415
694
|
args.push(num(length));
|
|
416
|
-
if (note.velocity !== DEFAULT_VELOCITY) args.push(num(note.velocity));
|
|
417
|
-
return
|
|
695
|
+
if (note.velocity !== DEFAULT_VELOCITY || how) args.push(num(note.velocity));
|
|
696
|
+
return call("note", args, how, indent);
|
|
418
697
|
}
|
|
419
698
|
|
|
420
|
-
function printHit(
|
|
699
|
+
function printHit(
|
|
700
|
+
score: TrackScore,
|
|
701
|
+
note: Note,
|
|
702
|
+
voice: string,
|
|
703
|
+
indent: string,
|
|
704
|
+
): string {
|
|
421
705
|
const args = [str(voice), num(note.startTick / score.ticksPerBeat)];
|
|
422
706
|
const length = note.durationTicks / score.ticksPerBeat;
|
|
423
|
-
|
|
707
|
+
const how = expressionEntries(note, indent + INDENT);
|
|
708
|
+
if (
|
|
709
|
+
note.velocity !== DEFAULT_VELOCITY ||
|
|
710
|
+
length !== DEFAULT_HIT_LENGTH ||
|
|
711
|
+
how
|
|
712
|
+
)
|
|
424
713
|
args.push(num(note.velocity));
|
|
425
|
-
if (length !== DEFAULT_HIT_LENGTH) args.push(num(length));
|
|
426
|
-
return
|
|
714
|
+
if (length !== DEFAULT_HIT_LENGTH || how) args.push(num(length));
|
|
715
|
+
return call("hit", args, how, indent);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* A note's expression as the fields of its `how` argument, values laid out
|
|
720
|
+
* for property lines at `indent`; undefined when it has none.
|
|
721
|
+
*/
|
|
722
|
+
function expressionEntries(
|
|
723
|
+
note: Note,
|
|
724
|
+
indent: string,
|
|
725
|
+
): (readonly [string, string])[] | undefined {
|
|
726
|
+
const entries: (readonly [string, string])[] = [];
|
|
727
|
+
if (note.articulation) entries.push(["art", str(note.articulation)]);
|
|
728
|
+
if (note.glide !== undefined) entries.push(["glide", num(note.glide)]);
|
|
729
|
+
if (note.bend) {
|
|
730
|
+
const points = note.bend.map((p) => `[${num(p.at)}, ${num(p.cents)}]`);
|
|
731
|
+
// Prettier forces a break on arrays of 2+ arrays that each hold 2+ items.
|
|
732
|
+
entries.push([
|
|
733
|
+
"bend",
|
|
734
|
+
list(points, indent, "bend: ".length, 1, points.length > 1),
|
|
735
|
+
]);
|
|
736
|
+
}
|
|
737
|
+
if (note.vibrato)
|
|
738
|
+
entries.push([
|
|
739
|
+
"vibrato",
|
|
740
|
+
obj(
|
|
741
|
+
[
|
|
742
|
+
["rate", num(note.vibrato.rate)],
|
|
743
|
+
["depth", num(note.vibrato.depth)],
|
|
744
|
+
...(note.vibrato.delay !== undefined
|
|
745
|
+
? ([["delay", num(note.vibrato.delay)]] as const)
|
|
746
|
+
: []),
|
|
747
|
+
],
|
|
748
|
+
indent,
|
|
749
|
+
"vibrato: ".length,
|
|
750
|
+
1,
|
|
751
|
+
),
|
|
752
|
+
]);
|
|
753
|
+
if (note.humanize) {
|
|
754
|
+
const { timing, velocity, length } = note.humanize;
|
|
755
|
+
entries.push([
|
|
756
|
+
"humanize",
|
|
757
|
+
obj(
|
|
758
|
+
[
|
|
759
|
+
...(timing !== undefined ? ([["timing", num(timing)]] as const) : []),
|
|
760
|
+
...(velocity !== undefined
|
|
761
|
+
? ([["velocity", num(velocity)]] as const)
|
|
762
|
+
: []),
|
|
763
|
+
...(length !== undefined ? ([["length", num(length)]] as const) : []),
|
|
764
|
+
],
|
|
765
|
+
indent,
|
|
766
|
+
"humanize: ".length,
|
|
767
|
+
1,
|
|
768
|
+
),
|
|
769
|
+
]);
|
|
770
|
+
}
|
|
771
|
+
return entries.length === 0 ? undefined : entries;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* `name(args, how)` as a list item at `indent`: on one line when it fits,
|
|
776
|
+
* otherwise with the trailing `how` object hugged and broken one field per
|
|
777
|
+
* line, the way prettier prints a last object argument.
|
|
778
|
+
*/
|
|
779
|
+
function call(
|
|
780
|
+
name: string,
|
|
781
|
+
args: readonly string[],
|
|
782
|
+
how: readonly (readonly [string, string])[] | undefined,
|
|
783
|
+
indent: string,
|
|
784
|
+
): string {
|
|
785
|
+
if (!how) return `${name}(${args.join(", ")})`;
|
|
786
|
+
const inline = `${name}(${[...args, `{ ${how.map(([k, v]) => `${k}: ${v}`).join(", ")} }`].join(", ")})`;
|
|
787
|
+
if (!inline.includes("\n") && indent.length + inline.length + 1 <= WIDTH)
|
|
788
|
+
return inline;
|
|
789
|
+
const inner = indent + INDENT;
|
|
790
|
+
const body = how.map(([k, v]) => property(k, v, inner)).join("\n");
|
|
791
|
+
return `${name}(${[...args, "{"].join(", ")}\n${body}\n${indent}})`;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** A track's performance fields (SDK 1.15.0) as `key: literal` entries. */
|
|
795
|
+
function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
796
|
+
const entries: string[] = [];
|
|
797
|
+
if (track.glide)
|
|
798
|
+
entries.push(
|
|
799
|
+
track.glide.mode === "legato"
|
|
800
|
+
? `glide: ${num(track.glide.time)}`
|
|
801
|
+
: `glide: ${obj(
|
|
802
|
+
[
|
|
803
|
+
["time", num(track.glide.time)],
|
|
804
|
+
["mode", str(track.glide.mode)],
|
|
805
|
+
],
|
|
806
|
+
INDENT,
|
|
807
|
+
"glide: ".length,
|
|
808
|
+
1,
|
|
809
|
+
)}`,
|
|
810
|
+
);
|
|
811
|
+
if (track.pedal) {
|
|
812
|
+
const events = track.pedal.map(
|
|
813
|
+
(event) =>
|
|
814
|
+
`[${num(event.tick / score.ticksPerBeat)}, ${str(event.state)}]`,
|
|
815
|
+
);
|
|
816
|
+
entries.push(
|
|
817
|
+
`pedal: ${list(events, INDENT, "pedal: ".length, 1, events.length > 1)}`,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (track.velocityCurve)
|
|
821
|
+
entries.push(
|
|
822
|
+
track.velocityCurve.curve !== "fixed" ||
|
|
823
|
+
(track.velocityCurve.fixed ?? DEFAULT_FIXED_VELOCITY) ===
|
|
824
|
+
DEFAULT_FIXED_VELOCITY
|
|
825
|
+
? `velocityCurve: ${str(track.velocityCurve.curve)}`
|
|
826
|
+
: `velocityCurve: ${obj(
|
|
827
|
+
[
|
|
828
|
+
["curve", str(track.velocityCurve.curve)],
|
|
829
|
+
["fixed", num(track.velocityCurve.fixed!)],
|
|
830
|
+
],
|
|
831
|
+
INDENT,
|
|
832
|
+
"velocityCurve: ".length,
|
|
833
|
+
1,
|
|
834
|
+
)}`,
|
|
835
|
+
);
|
|
836
|
+
if (track.humanize) {
|
|
837
|
+
const { timing, velocity, length, seed } = track.humanize;
|
|
838
|
+
entries.push(
|
|
839
|
+
`humanize: ${obj(
|
|
840
|
+
[
|
|
841
|
+
...(timing !== undefined ? ([["timing", num(timing)]] as const) : []),
|
|
842
|
+
...(velocity !== undefined
|
|
843
|
+
? ([["velocity", num(velocity)]] as const)
|
|
844
|
+
: []),
|
|
845
|
+
...(length !== undefined ? ([["length", num(length)]] as const) : []),
|
|
846
|
+
["seed", num(seed)],
|
|
847
|
+
],
|
|
848
|
+
INDENT,
|
|
849
|
+
"humanize: ".length,
|
|
850
|
+
1,
|
|
851
|
+
)}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return entries;
|
|
427
855
|
}
|
|
428
856
|
|
|
429
857
|
/** `euclid("hat", 7, 16, 2, { velocity: 0.5 })` or `grid("sd", "....x...")`. */
|
|
@@ -514,6 +942,76 @@ function printSampler(sampler: Sampler, indent: string): string {
|
|
|
514
942
|
return `sampler(\n${inner}${shifted},\n${inner}{ mode: ${str(sampler.mode)} },\n${indent})`;
|
|
515
943
|
}
|
|
516
944
|
|
|
945
|
+
/** `stringed("sitar", { buzz: 0.8 })`: the preset, then its overrides. */
|
|
946
|
+
function printStringed(settings: TrackString, indent: string): string {
|
|
947
|
+
const name = str(settings.preset ?? DEFAULT_STRING_PRESET);
|
|
948
|
+
const params = Object.entries(settings)
|
|
949
|
+
.filter(([key]) => key !== "preset")
|
|
950
|
+
.map(([key, v]) => [key, value(v!)] as const);
|
|
951
|
+
if (params.length === 0) return `stringed(${name})`;
|
|
952
|
+
const inline = `stringed(${name}, { ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
953
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
954
|
+
return inline;
|
|
955
|
+
const inner = indent + INDENT;
|
|
956
|
+
return `stringed(${name}, {\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** `[key, literal]` for each stored granular field but the preset. */
|
|
960
|
+
function granularEntries(
|
|
961
|
+
settings: TrackGranular,
|
|
962
|
+
indent: string,
|
|
963
|
+
): (readonly [string, string])[] {
|
|
964
|
+
return Object.entries(settings)
|
|
965
|
+
.filter(([key]) => key !== "preset")
|
|
966
|
+
.map(([key, v]) => [
|
|
967
|
+
key,
|
|
968
|
+
typeof v === "object"
|
|
969
|
+
? printSample(v as SampleRef, indent + INDENT, `${key}: `.length)
|
|
970
|
+
: value(v as number | string | boolean),
|
|
971
|
+
]);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** `granular("cloud", { scan: 0.1 })`: the preset, then its overrides. */
|
|
975
|
+
function printGranular(settings: TrackGranular, indent: string): string {
|
|
976
|
+
const inner = indent + INDENT;
|
|
977
|
+
const params = granularEntries(settings, indent);
|
|
978
|
+
const name = settings.preset ? str(settings.preset) : undefined;
|
|
979
|
+
if (params.length === 0) return name ? `granular(${name})` : "granular()";
|
|
980
|
+
const head = name ? `granular(${name}, ` : "granular(";
|
|
981
|
+
const inline = `${head}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
982
|
+
if (
|
|
983
|
+
!inline.includes("\n") &&
|
|
984
|
+
indent.length + "instrument: ".length + inline.length + 1 <= WIDTH
|
|
985
|
+
)
|
|
986
|
+
return inline;
|
|
987
|
+
return `${head}{\n${params.map(([k, v]) => property(k, v, inner)).join("\n")}\n${indent}})`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* A modal track's instrument: the preset word alone (`"vibes"`) when there
|
|
992
|
+
* are no overrides, else `modal("vibes", { motor: 4 })` with keys in
|
|
993
|
+
* `MODAL_PARAMS` order. `marimba` is a legacy word, so the modal marimba
|
|
994
|
+
* always prints as `modal("marimba")`.
|
|
995
|
+
*/
|
|
996
|
+
function printModal(settings: TrackModal, indent: string): string {
|
|
997
|
+
const params: [string, string][] = [];
|
|
998
|
+
for (const key of Object.keys(MODAL_PARAMS) as (keyof TrackModal)[]) {
|
|
999
|
+
const value = settings[key];
|
|
1000
|
+
if (value === undefined) continue;
|
|
1001
|
+
params.push([key, typeof value === "number" ? num(value) : str(value)]);
|
|
1002
|
+
}
|
|
1003
|
+
const preset = settings.preset;
|
|
1004
|
+
if (params.length === 0 && preset && preset !== "marimba") return str(preset);
|
|
1005
|
+
const head = preset ? str(preset) : "";
|
|
1006
|
+
if (params.length === 0) return `modal(${head})`;
|
|
1007
|
+
const lead = head ? `${head}, ` : "";
|
|
1008
|
+
const inline = `modal(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1009
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
1010
|
+
return inline;
|
|
1011
|
+
const inner = indent + INDENT;
|
|
1012
|
+
return `modal(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
517
1015
|
/** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
|
|
518
1016
|
function printWavetable(settings: TrackWavetable, indent: string): string {
|
|
519
1017
|
const src = settings.table.src;
|
|
@@ -570,10 +1068,99 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
|
570
1068
|
if (ref.accelerate !== undefined)
|
|
571
1069
|
entries.push(["accelerate", num(ref.accelerate)]);
|
|
572
1070
|
if (ref.squiz !== undefined) entries.push(["squiz", num(ref.squiz)]);
|
|
1071
|
+
if (ref.bpm !== undefined) entries.push(["bpm", num(ref.bpm)]);
|
|
1072
|
+
if (ref.fitmode !== undefined) entries.push(["fitmode", str(ref.fitmode)]);
|
|
1073
|
+
if (ref.len !== undefined) entries.push(["len", num(ref.len)]);
|
|
573
1074
|
if (entries.length === 1) return str(ref.src);
|
|
574
1075
|
return obj(entries, indent, prefix, 1);
|
|
575
1076
|
}
|
|
576
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* A song or track tuning: a bare library name, or an object without the
|
|
1080
|
+
* fields dawg resolved from Scala files (the table of an `scl`, the keymap
|
|
1081
|
+
* of a `kbm`).
|
|
1082
|
+
*/
|
|
1083
|
+
function printTuning(tuning: Tuning, indent: string, prefix: number): string {
|
|
1084
|
+
const entries: [string, string][] = [];
|
|
1085
|
+
if (tuning.name !== undefined) entries.push(["name", str(tuning.name)]);
|
|
1086
|
+
if (tuning.scl === undefined) {
|
|
1087
|
+
const inner = indent + INDENT;
|
|
1088
|
+
if (tuning.edo !== undefined) entries.push(["edo", num(tuning.edo)]);
|
|
1089
|
+
if (tuning.ratios)
|
|
1090
|
+
entries.push([
|
|
1091
|
+
"ratios",
|
|
1092
|
+
list(tuning.ratios.map(str), inner, "ratios: ".length, 1),
|
|
1093
|
+
]);
|
|
1094
|
+
if (tuning.cents)
|
|
1095
|
+
entries.push([
|
|
1096
|
+
"cents",
|
|
1097
|
+
fill(tuning.cents.map(num), inner, "cents: ".length, 1),
|
|
1098
|
+
]);
|
|
1099
|
+
} else entries.push(["scl", str(tuning.scl)]);
|
|
1100
|
+
if (tuning.kbm !== undefined) entries.push(["kbm", str(tuning.kbm)]);
|
|
1101
|
+
if (tuning.ref !== undefined) entries.push(["ref", num(tuning.ref)]);
|
|
1102
|
+
if (tuning.root !== undefined)
|
|
1103
|
+
entries.push(["root", str(midiToPitch(tuning.root))]);
|
|
1104
|
+
if (tuning.map !== undefined) entries.push(["map", str(tuning.map)]);
|
|
1105
|
+
if (entries.length === 1 && entries[0]![0] === "name") return entries[0]![1];
|
|
1106
|
+
return obj(entries, indent, prefix, 1);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Number array the way prettier prints one: inline when it fits, else
|
|
1111
|
+
* filled (as many per line as fit, each followed by a comma).
|
|
1112
|
+
*/
|
|
1113
|
+
function fill(
|
|
1114
|
+
items: readonly string[],
|
|
1115
|
+
indent: string,
|
|
1116
|
+
prefix: number,
|
|
1117
|
+
trailing: number,
|
|
1118
|
+
): string {
|
|
1119
|
+
const inline = `[${items.join(", ")}]`;
|
|
1120
|
+
if (
|
|
1121
|
+
items.length < 2 ||
|
|
1122
|
+
indent.length + prefix + inline.length + trailing <= WIDTH
|
|
1123
|
+
)
|
|
1124
|
+
return inline;
|
|
1125
|
+
const inner = indent + INDENT;
|
|
1126
|
+
const lines: string[] = [];
|
|
1127
|
+
let line = "";
|
|
1128
|
+
for (const item of items) {
|
|
1129
|
+
const next = `${item},`;
|
|
1130
|
+
if (line !== "" && inner.length + line.length + 1 + next.length <= WIDTH)
|
|
1131
|
+
line += ` ${next}`;
|
|
1132
|
+
else {
|
|
1133
|
+
if (line !== "") lines.push(inner + line);
|
|
1134
|
+
line = next;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
lines.push(inner + line);
|
|
1138
|
+
return `[\n${lines.join("\n")}\n${indent}]`;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** `master: { … }`, always expanded like `fx`; units in chain order. */
|
|
1142
|
+
function printMaster(master: SongMaster): string {
|
|
1143
|
+
const inner = INDENT + INDENT;
|
|
1144
|
+
const body: string[] = [];
|
|
1145
|
+
for (const unit of MASTER_UNITS) {
|
|
1146
|
+
const values = master[unit];
|
|
1147
|
+
if (!values) continue;
|
|
1148
|
+
// Only values that differ from the default: loading fills the rest.
|
|
1149
|
+
const params = Object.entries(MASTER_SPECS[unit].params)
|
|
1150
|
+
.filter(
|
|
1151
|
+
([key, spec]) =>
|
|
1152
|
+
values[key] !== undefined && values[key] !== spec.default,
|
|
1153
|
+
)
|
|
1154
|
+
.map(([key]) => [key, value(values[key]!)] as const);
|
|
1155
|
+
body.push(
|
|
1156
|
+
`${inner}${unit}: ${params.length === 0 ? "{}" : obj(params, inner, `${unit}: `.length, 1)},`,
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
if (master.target !== undefined)
|
|
1160
|
+
body.push(`${inner}target: ${num(master.target)},`);
|
|
1161
|
+
return `master: {\n${body.join("\n")}\n${INDENT}}`;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
577
1164
|
/** Object literal: inline when the line fits, expanded otherwise (prettier keeps both). */
|
|
578
1165
|
function obj(
|
|
579
1166
|
entries: readonly (readonly [string, string])[],
|