@hraness/dawg 0.6.0 → 0.7.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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/core/sdk/eval-child.ts
CHANGED
|
@@ -83,8 +83,8 @@ function diagnostic(error: unknown, project: string): Diagnostic {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* Adds `sha256` to sampler voices
|
|
87
|
-
* and are within the size limit.
|
|
86
|
+
* Adds `sha256` to sampler voices, project wavetable files, audio clips and
|
|
87
|
+
* takes that exist and are within the size limit.
|
|
88
88
|
*/
|
|
89
89
|
async function withSampleHashes(
|
|
90
90
|
song: object,
|
|
@@ -106,6 +106,11 @@ async function withSampleHashes(
|
|
|
106
106
|
refs.push(wavetable.table);
|
|
107
107
|
const granular = track.granular;
|
|
108
108
|
if (isObject(granular) && isObject(granular.src)) refs.push(granular.src);
|
|
109
|
+
// 0.7 audio clips and takes: a fresh `audio("samples/x.wav")` is pinned
|
|
110
|
+
// here; a written pin is kept and `dawg check` re-hashes it.
|
|
111
|
+
for (const key of ["clips", "takes"])
|
|
112
|
+
for (const item of Array.isArray(track[key]) ? track[key] : [])
|
|
113
|
+
if (isObject(item)) refs.push(item);
|
|
109
114
|
}
|
|
110
115
|
for (const ref of refs) {
|
|
111
116
|
if (
|
package/core/sdk/eval.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Evaluates a project's `song.ts` in
|
|
2
|
+
* Evaluates a project's `song.ts` in an isolated Bun subprocess and parses
|
|
3
3
|
* the result from `unknown` into a validated `TrackScore`.
|
|
4
4
|
*
|
|
5
5
|
* The child runs `eval-child.ts` with a scrubbed environment, the project as
|
|
6
6
|
* its working directory, no native addons or auto-install, a 10 second
|
|
7
|
-
* budget and a
|
|
7
|
+
* budget and a 32 MiB output bound. Anything it prints that is not the
|
|
8
8
|
* expected JSON line is a diagnostic.
|
|
9
|
+
*
|
|
10
|
+
* Isolation is not a sandbox: the child runs project code as the user, with
|
|
11
|
+
* the user's filesystem and network access. It protects dawg's own process
|
|
12
|
+
* (a crash, hang or runaway output in song.ts never takes the window down),
|
|
13
|
+
* not the machine, so only evaluate projects you trust.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { dirname, join } from "node:path";
|
|
@@ -15,7 +20,11 @@ import { refreshRhythm } from "../rhythm.ts";
|
|
|
15
20
|
import { ScoreValidationError, TrackScore } from "../score.ts";
|
|
16
21
|
|
|
17
22
|
export const EVAL_TIMEOUT_MS = 10_000;
|
|
18
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Above the JSON of the largest score `SCORE_LIMITS` allows (64 tracks with
|
|
25
|
+
* every automation lane full plus 4096 notes stays well under 16 MiB).
|
|
26
|
+
*/
|
|
27
|
+
export const EVAL_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
19
28
|
|
|
20
29
|
/** A problem in a project file; `file` is project-relative when known. */
|
|
21
30
|
export type Diagnostic = Readonly<{
|
|
@@ -39,7 +48,14 @@ export type EvalSpawn = (
|
|
|
39
48
|
maxOutputBytes: number;
|
|
40
49
|
}>,
|
|
41
50
|
) => Promise<
|
|
42
|
-
Readonly<{
|
|
51
|
+
Readonly<{
|
|
52
|
+
code: number;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
killed: boolean;
|
|
56
|
+
/** Why the child was killed; absent means the time budget ran out. */
|
|
57
|
+
reason?: "timeout" | "output";
|
|
58
|
+
}>
|
|
43
59
|
>;
|
|
44
60
|
|
|
45
61
|
export type EvalOptions = Readonly<{
|
|
@@ -86,12 +102,21 @@ export async function evaluateProject(
|
|
|
86
102
|
failure(started, [{ message: `could not start bun: ${text(error)}` }]),
|
|
87
103
|
);
|
|
88
104
|
}
|
|
105
|
+
if (run.killed && run.reason === "output")
|
|
106
|
+
return done(
|
|
107
|
+
failure(started, [
|
|
108
|
+
{
|
|
109
|
+
file: "song.ts",
|
|
110
|
+
message: `evaluation printed more than ${EVAL_MAX_OUTPUT_BYTES / (1024 * 1024)} MiB; song.ts must not log`,
|
|
111
|
+
},
|
|
112
|
+
]),
|
|
113
|
+
);
|
|
89
114
|
if (run.killed)
|
|
90
115
|
return done(
|
|
91
116
|
failure(started, [
|
|
92
117
|
{
|
|
93
118
|
file: "song.ts",
|
|
94
|
-
message:
|
|
119
|
+
message: `evaluation timed out (${Math.round((options.timeoutMs ?? EVAL_TIMEOUT_MS) / 1000)} s); song.ts must be pure`,
|
|
95
120
|
},
|
|
96
121
|
]),
|
|
97
122
|
);
|
|
@@ -224,6 +249,7 @@ export const bunSpawn: EvalSpawn = async (args, options) => {
|
|
|
224
249
|
stderr: "pipe",
|
|
225
250
|
});
|
|
226
251
|
let killed = false;
|
|
252
|
+
let reason: "timeout" | "output" = "timeout";
|
|
227
253
|
const timer = setTimeout(() => {
|
|
228
254
|
killed = true;
|
|
229
255
|
child.kill();
|
|
@@ -237,6 +263,7 @@ export const bunSpawn: EvalSpawn = async (args, options) => {
|
|
|
237
263
|
const { value, done } = await reader.read();
|
|
238
264
|
if (done || !value) break;
|
|
239
265
|
if (total + value.byteLength > options.maxOutputBytes) {
|
|
266
|
+
if (!killed) reason = "output";
|
|
240
267
|
killed = true;
|
|
241
268
|
child.kill();
|
|
242
269
|
break;
|
|
@@ -253,7 +280,9 @@ export const bunSpawn: EvalSpawn = async (args, options) => {
|
|
|
253
280
|
read(child.stderr),
|
|
254
281
|
child.exited,
|
|
255
282
|
]);
|
|
256
|
-
return
|
|
283
|
+
return killed
|
|
284
|
+
? { code, stdout, stderr, killed, reason }
|
|
285
|
+
: { code, stdout, stderr, killed };
|
|
257
286
|
} finally {
|
|
258
287
|
clearTimeout(timer);
|
|
259
288
|
}
|
package/core/sdk/print.ts
CHANGED
|
@@ -21,12 +21,26 @@ import {
|
|
|
21
21
|
type FxValues,
|
|
22
22
|
} from "../fx.ts";
|
|
23
23
|
import { MASTER_SPECS, MASTER_UNITS, type SongMaster } from "../master.ts";
|
|
24
|
+
import type { SongStyle } from "../style-provenance.ts";
|
|
24
25
|
import { midiToPitch } from "../pitch.ts";
|
|
25
26
|
import {
|
|
26
27
|
MODAL_INSTRUMENT,
|
|
27
28
|
MODAL_PARAMS,
|
|
28
29
|
type TrackModal,
|
|
29
30
|
} from "../resonators.ts";
|
|
31
|
+
import { WIND_INSTRUMENT, WIND_PARAMS, type TrackWind } from "../winds.ts";
|
|
32
|
+
import {
|
|
33
|
+
SING_INSTRUMENT,
|
|
34
|
+
SING_KEY_ORDER,
|
|
35
|
+
singNoteName,
|
|
36
|
+
type TrackSing,
|
|
37
|
+
} from "../sing.ts";
|
|
38
|
+
import {
|
|
39
|
+
VOCODER_INSTRUMENT,
|
|
40
|
+
VOCODER_PARAMS,
|
|
41
|
+
type TrackVocoder,
|
|
42
|
+
} from "../vocoder.ts";
|
|
43
|
+
import { AUTOTUNE_FIELDS, type TrackAutotune } from "../autotune.ts";
|
|
30
44
|
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
31
45
|
import {
|
|
32
46
|
BUILTIN_TABLE_PREFIX,
|
|
@@ -44,13 +58,15 @@ import {
|
|
|
44
58
|
type SampleRef,
|
|
45
59
|
type Sampler,
|
|
46
60
|
type Section,
|
|
61
|
+
type AudioClip,
|
|
62
|
+
type Take,
|
|
47
63
|
type Track,
|
|
48
64
|
} from "../score.ts";
|
|
49
65
|
import { trackSlug } from "../slug.ts";
|
|
50
|
-
import { barStartTick } from "../tempo.ts";
|
|
66
|
+
import { barStartTick, timeWithinSong } from "../tempo.ts";
|
|
51
67
|
import type { Tuning } from "../tuning.ts";
|
|
52
68
|
import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
|
|
53
|
-
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
|
|
69
|
+
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY, defaultClipId } from "./v1.ts";
|
|
54
70
|
|
|
55
71
|
const WIDTH = 80;
|
|
56
72
|
const INDENT = " ";
|
|
@@ -155,6 +171,14 @@ const RESERVED = new Set([
|
|
|
155
171
|
"rig",
|
|
156
172
|
"granular",
|
|
157
173
|
"modal",
|
|
174
|
+
"wind",
|
|
175
|
+
"sing",
|
|
176
|
+
"audio",
|
|
177
|
+
"take",
|
|
178
|
+
"lyrics",
|
|
179
|
+
"repeatAudio",
|
|
180
|
+
"vocoder",
|
|
181
|
+
"autotune",
|
|
158
182
|
"slices",
|
|
159
183
|
"euclid",
|
|
160
184
|
"euclidRot",
|
|
@@ -195,6 +219,7 @@ export function printSong(
|
|
|
195
219
|
const helpers = [
|
|
196
220
|
"song",
|
|
197
221
|
...TIME_HELPERS.filter((name) => time.used.has(name)),
|
|
222
|
+
...(score.style ? ["style"] : []),
|
|
198
223
|
];
|
|
199
224
|
const lines: string[] = [`import { ${helpers.join(", ")} } from "dawg";`];
|
|
200
225
|
for (const track of score.tracks)
|
|
@@ -225,6 +250,7 @@ export function printSong(
|
|
|
225
250
|
)}`,
|
|
226
251
|
);
|
|
227
252
|
if (score.master) entries.push(printMaster(score.master));
|
|
253
|
+
if (score.style) entries.push(`style: ${printStyle(score.style)}`);
|
|
228
254
|
// Song sections and form (0.5): printed only when the song has them.
|
|
229
255
|
if (score.sections.length > 0)
|
|
230
256
|
entries.push(
|
|
@@ -239,6 +265,7 @@ export function printSong(
|
|
|
239
265
|
entries.push(`form: ${printForm(score.form, INDENT)}`);
|
|
240
266
|
if (score.loopSection !== undefined)
|
|
241
267
|
entries.push(`loopSection: ${str(score.loopSection)}`);
|
|
268
|
+
if (score.calibration) entries.push(`calibration: ${num(score.calibration)}`);
|
|
242
269
|
lines.push("export default song({");
|
|
243
270
|
for (const entry of entries) lines.push(`${INDENT}${entry},`);
|
|
244
271
|
lines.push("});", "");
|
|
@@ -255,7 +282,8 @@ function printTime(score: TrackScore): {
|
|
|
255
282
|
} {
|
|
256
283
|
const marks: string[] = [];
|
|
257
284
|
const used = new Set<string>();
|
|
258
|
-
|
|
285
|
+
// Marks past the final barline are never heard and the SDK refuses them.
|
|
286
|
+
const time = timeWithinSong(score);
|
|
259
287
|
if (!time) return { marks, used };
|
|
260
288
|
const beat = (tick: number) => num(tick / score.ticksPerBeat);
|
|
261
289
|
type Mark = { tick: number; order: number; text: string };
|
|
@@ -414,6 +442,25 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
414
442
|
? printModal(track.modal, INDENT)
|
|
415
443
|
: undefined;
|
|
416
444
|
if (modalCall?.startsWith("modal(")) used.add("modal");
|
|
445
|
+
const windCall =
|
|
446
|
+
track.instrument === WIND_INSTRUMENT && track.wind
|
|
447
|
+
? printWind(track.wind, INDENT)
|
|
448
|
+
: undefined;
|
|
449
|
+
if (windCall?.startsWith("wind(")) used.add("wind");
|
|
450
|
+
const singCall =
|
|
451
|
+
track.instrument === SING_INSTRUMENT && track.sing
|
|
452
|
+
? printSing(track.sing, INDENT)
|
|
453
|
+
: undefined;
|
|
454
|
+
if (singCall?.startsWith("sing(")) used.add("sing");
|
|
455
|
+
const vocoderCall = track.vocoder
|
|
456
|
+
? printVocoder(
|
|
457
|
+
track.vocoder,
|
|
458
|
+
vocoderSrcText(score, track.vocoder.src),
|
|
459
|
+
track.instrument === VOCODER_INSTRUMENT ? "instrument: " : "vocoder: ",
|
|
460
|
+
INDENT,
|
|
461
|
+
)
|
|
462
|
+
: undefined;
|
|
463
|
+
if (vocoderCall?.startsWith("vocoder(")) used.add("vocoder");
|
|
417
464
|
|
|
418
465
|
const entries: string[] = [
|
|
419
466
|
`id: ${str(track.id)}`,
|
|
@@ -432,8 +479,21 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
432
479
|
entries.push(`instrument: ${printStringed(track.string!, INDENT)}`);
|
|
433
480
|
} else if (modalCall) {
|
|
434
481
|
entries.push(`instrument: ${modalCall}`);
|
|
482
|
+
} else if (windCall) {
|
|
483
|
+
entries.push(`instrument: ${windCall}`);
|
|
484
|
+
} else if (singCall) {
|
|
485
|
+
entries.push(`instrument: ${singCall}`);
|
|
486
|
+
} else if (vocoderCall && track.instrument === VOCODER_INSTRUMENT) {
|
|
487
|
+
entries.push(`instrument: ${vocoderCall}`);
|
|
435
488
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
489
|
+
if (vocoderCall && track.instrument !== VOCODER_INSTRUMENT)
|
|
490
|
+
entries.push(`vocoder: ${vocoderCall}`);
|
|
436
491
|
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
492
|
+
// The table a track keeps after leaving the wavetable instrument.
|
|
493
|
+
if (track.wavetable && !wavetable) {
|
|
494
|
+
used.add("wavetable");
|
|
495
|
+
entries.push(`wavetable: ${printWavetable(track.wavetable, INDENT)}`);
|
|
496
|
+
}
|
|
437
497
|
if (track.granular && !grained)
|
|
438
498
|
entries.push(
|
|
439
499
|
`granular: ${obj(
|
|
@@ -586,6 +646,11 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
586
646
|
);
|
|
587
647
|
}
|
|
588
648
|
entries.push(...performanceEntries(score, track));
|
|
649
|
+
if (track.autotune) {
|
|
650
|
+
const call = printAutotune(track.autotune, INDENT);
|
|
651
|
+
if (call.startsWith("autotune(")) used.add("autotune");
|
|
652
|
+
entries.push(`autotune: ${call}`);
|
|
653
|
+
}
|
|
589
654
|
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
590
655
|
["volume", track.volumeAutomation],
|
|
591
656
|
["pan", track.panAutomation],
|
|
@@ -644,6 +709,20 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
644
709
|
}
|
|
645
710
|
if (rows.length === 0 || printed.length > 0)
|
|
646
711
|
entries.push(`notes: ${list(printed, INDENT, "notes: ".length, 1)}`);
|
|
712
|
+
if (track.takes && track.takes.length > 0) {
|
|
713
|
+
used.add("take");
|
|
714
|
+
const items = track.takes.map((t) => printTake(score, t, INDENT + INDENT));
|
|
715
|
+
entries.push(`takes: ${list(items, INDENT, "takes: ".length, 1, true)}`);
|
|
716
|
+
}
|
|
717
|
+
if (track.clips && track.clips.length > 0) {
|
|
718
|
+
used.add("audio");
|
|
719
|
+
const items = track.clips.map((clip, index) =>
|
|
720
|
+
printClip(score, clip, index, INDENT + INDENT),
|
|
721
|
+
);
|
|
722
|
+
entries.push(
|
|
723
|
+
`clips: ${list(items, INDENT, "clips: ".length, 1, items.length > 1)}`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
647
726
|
|
|
648
727
|
const names = [
|
|
649
728
|
"track",
|
|
@@ -658,8 +737,14 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
658
737
|
"rig",
|
|
659
738
|
"granular",
|
|
660
739
|
"modal",
|
|
740
|
+
"wind",
|
|
741
|
+
"sing",
|
|
742
|
+
"vocoder",
|
|
743
|
+
"autotune",
|
|
661
744
|
"euclid",
|
|
662
745
|
"grid",
|
|
746
|
+
"audio",
|
|
747
|
+
"take",
|
|
663
748
|
]
|
|
664
749
|
.filter((name) => used.has(name))
|
|
665
750
|
.join(", ");
|
|
@@ -768,6 +853,9 @@ function expressionEntries(
|
|
|
768
853
|
),
|
|
769
854
|
]);
|
|
770
855
|
}
|
|
856
|
+
if (note.vowel) entries.push(["vowel", str(note.vowel)]);
|
|
857
|
+
if (note.lyric !== undefined) entries.push(["lyric", str(note.lyric)]);
|
|
858
|
+
if (note.drift !== undefined) entries.push(["drift", num(note.drift)]);
|
|
771
859
|
return entries.length === 0 ? undefined : entries;
|
|
772
860
|
}
|
|
773
861
|
|
|
@@ -791,6 +879,66 @@ function call(
|
|
|
791
879
|
return `${name}(${[...args, "{"].join(", ")}\n${body}\n${indent}})`;
|
|
792
880
|
}
|
|
793
881
|
|
|
882
|
+
/** Seconds at 4 decimals (sources.md 6). */
|
|
883
|
+
function secs(value: number): string {
|
|
884
|
+
return num(Math.round(value * 1e4) / 1e4);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* `audio(src, { at, offset, dur, gain, fadeInTime, fadeTime, rev, take,
|
|
889
|
+
* mute, text, say, sha256 })` (SDK 1.32.0); defaults omitted, an id printed
|
|
890
|
+
* only when it differs from the positional default.
|
|
891
|
+
*/
|
|
892
|
+
function printClip(
|
|
893
|
+
score: TrackScore,
|
|
894
|
+
clip: AudioClip,
|
|
895
|
+
index: number,
|
|
896
|
+
indent: string,
|
|
897
|
+
): string {
|
|
898
|
+
const how: [string, string][] = [];
|
|
899
|
+
if (clip.id !== defaultClipId(index)) how.push(["id", str(clip.id)]);
|
|
900
|
+
if (clip.startTick !== 0)
|
|
901
|
+
how.push(["at", num(clip.startTick / score.ticksPerBeat)]);
|
|
902
|
+
if (clip.offset) how.push(["offset", secs(clip.offset)]);
|
|
903
|
+
if (clip.dur !== undefined) how.push(["dur", secs(clip.dur)]);
|
|
904
|
+
// Gain is linear in the file; a comment shows the dB the commands speak.
|
|
905
|
+
if (clip.gain !== undefined && clip.gain !== 1)
|
|
906
|
+
how.push([
|
|
907
|
+
"gain",
|
|
908
|
+
`${num(clip.gain)} /* ${
|
|
909
|
+
clip.gain > 0 ? (20 * Math.log10(clip.gain)).toFixed(1) : "-inf"
|
|
910
|
+
} dB */`,
|
|
911
|
+
]);
|
|
912
|
+
if (clip.fadeInTime !== undefined)
|
|
913
|
+
how.push(["fadeInTime", secs(clip.fadeInTime)]);
|
|
914
|
+
if (clip.fadeTime !== undefined) how.push(["fadeTime", secs(clip.fadeTime)]);
|
|
915
|
+
if (clip.rev) how.push(["rev", "true"]);
|
|
916
|
+
if (clip.take !== undefined) how.push(["take", str(clip.take)]);
|
|
917
|
+
if (clip.mute) how.push(["mute", "true"]);
|
|
918
|
+
if (clip.text !== undefined) how.push(["text", str(clip.text)]);
|
|
919
|
+
if (clip.say !== undefined) how.push(["say", JSON.stringify(clip.say)]);
|
|
920
|
+
how.push(["sha256", str(clip.sha256)]);
|
|
921
|
+
return call("audio", [str(clip.src)], how, indent);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** `take(name, src, { at, in, out, ... })` (SDK 1.32.0). */
|
|
925
|
+
function printTake(score: TrackScore, t: Take, indent: string): string {
|
|
926
|
+
const beatsAt = (tick: number) => num(tick / score.ticksPerBeat);
|
|
927
|
+
const how: [string, string][] = [];
|
|
928
|
+
if (t.startTick !== 0) how.push(["at", beatsAt(t.startTick)]);
|
|
929
|
+
how.push(["in", beatsAt(t.inTick)], ["out", beatsAt(t.outTick)]);
|
|
930
|
+
if (t.offset) how.push(["offset", secs(t.offset)]);
|
|
931
|
+
if (t.latency) how.push(["latency", secs(t.latency)]);
|
|
932
|
+
if (t.latencyAssumed) how.push(["latencyAssumed", "true"]);
|
|
933
|
+
if (t.ppm !== undefined)
|
|
934
|
+
how.push(["ppm", num(Math.round(t.ppm * 100) / 100)]);
|
|
935
|
+
if (t.fit !== undefined) how.push(["fit", num(t.fit)]);
|
|
936
|
+
if (t.warn !== undefined) how.push(["warn", str(t.warn)]);
|
|
937
|
+
if (t.nudge !== undefined) how.push(["nudge", num(t.nudge)]);
|
|
938
|
+
how.push(["sha256", str(t.sha256)]);
|
|
939
|
+
return call("take", [str(t.name), str(t.src)], how, indent);
|
|
940
|
+
}
|
|
941
|
+
|
|
794
942
|
/** A track's performance fields (SDK 1.15.0) as `key: literal` entries. */
|
|
795
943
|
function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
796
944
|
const entries: string[] = [];
|
|
@@ -808,13 +956,15 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
|
808
956
|
1,
|
|
809
957
|
)}`,
|
|
810
958
|
);
|
|
811
|
-
|
|
812
|
-
const
|
|
959
|
+
for (const key of ["pedal", "softPedal", "sostenuto"] as const) {
|
|
960
|
+
const lane = track[key];
|
|
961
|
+
if (!lane) continue;
|
|
962
|
+
const events = lane.map(
|
|
813
963
|
(event) =>
|
|
814
964
|
`[${num(event.tick / score.ticksPerBeat)}, ${str(event.state)}]`,
|
|
815
965
|
);
|
|
816
966
|
entries.push(
|
|
817
|
-
|
|
967
|
+
`${key}: ${list(events, INDENT, `${key}: `.length, 1, events.length > 1)}`,
|
|
818
968
|
);
|
|
819
969
|
}
|
|
820
970
|
if (track.velocityCurve)
|
|
@@ -851,6 +1001,20 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
|
851
1001
|
)}`,
|
|
852
1002
|
);
|
|
853
1003
|
}
|
|
1004
|
+
if (track.guitar) {
|
|
1005
|
+
const { tune, capo, hand, ring, position } = track.guitar;
|
|
1006
|
+
const fields: [string, string][] = [];
|
|
1007
|
+
if (tune !== undefined)
|
|
1008
|
+
fields.push([
|
|
1009
|
+
"tune",
|
|
1010
|
+
typeof tune === "string" ? str(tune) : `[${tune.map(num).join(", ")}]`,
|
|
1011
|
+
]);
|
|
1012
|
+
if (capo !== undefined) fields.push(["capo", num(capo)]);
|
|
1013
|
+
if (hand !== undefined) fields.push(["hand", num(hand)]);
|
|
1014
|
+
if (ring !== undefined) fields.push(["ring", num(ring)]);
|
|
1015
|
+
if (position !== undefined) fields.push(["position", num(position)]);
|
|
1016
|
+
entries.push(`guitar: ${obj(fields, INDENT, "guitar: ".length, 1)}`);
|
|
1017
|
+
}
|
|
854
1018
|
return entries;
|
|
855
1019
|
}
|
|
856
1020
|
|
|
@@ -1000,6 +1164,7 @@ function printModal(settings: TrackModal, indent: string): string {
|
|
|
1000
1164
|
if (value === undefined) continue;
|
|
1001
1165
|
params.push([key, typeof value === "number" ? num(value) : str(value)]);
|
|
1002
1166
|
}
|
|
1167
|
+
if (settings.pair !== undefined) params.push(["pair", str(settings.pair)]);
|
|
1003
1168
|
const preset = settings.preset;
|
|
1004
1169
|
if (params.length === 0 && preset && preset !== "marimba") return str(preset);
|
|
1005
1170
|
const head = preset ? str(preset) : "";
|
|
@@ -1012,6 +1177,164 @@ function printModal(settings: TrackModal, indent: string): string {
|
|
|
1012
1177
|
return `modal(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1013
1178
|
}
|
|
1014
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* A wind track's instrument: the preset word alone (`"flute"`) when there
|
|
1182
|
+
* are no overrides, else `wind("sax", { breath: 0.8 })` with keys in
|
|
1183
|
+
* `WIND_PARAMS` order. Every wind preset word is also an instrument word.
|
|
1184
|
+
*/
|
|
1185
|
+
function printWind(settings: TrackWind, indent: string): string {
|
|
1186
|
+
const params: [string, string][] = [];
|
|
1187
|
+
for (const key of Object.keys(WIND_PARAMS)) {
|
|
1188
|
+
const value = (settings as Record<string, unknown>)[key];
|
|
1189
|
+
if (value === undefined) continue;
|
|
1190
|
+
params.push([
|
|
1191
|
+
key,
|
|
1192
|
+
typeof value === "number"
|
|
1193
|
+
? num(value)
|
|
1194
|
+
: typeof value === "boolean"
|
|
1195
|
+
? String(value)
|
|
1196
|
+
: str(String(value)),
|
|
1197
|
+
]);
|
|
1198
|
+
}
|
|
1199
|
+
const preset = settings.preset;
|
|
1200
|
+
if (params.length === 0 && preset) return str(preset);
|
|
1201
|
+
const head = preset ? str(preset) : "";
|
|
1202
|
+
if (params.length === 0) return `wind(${head})`;
|
|
1203
|
+
const lead = head ? `${head}, ` : "";
|
|
1204
|
+
const inline = `wind(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1205
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
1206
|
+
return inline;
|
|
1207
|
+
const inner = indent + INDENT;
|
|
1208
|
+
return `wind(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* A sing track's instrument: the preset word alone (`"choir"`) when there
|
|
1213
|
+
* are no overrides and the preset is an instrument word, else
|
|
1214
|
+
* `sing("khoomei", { drone: "D3" })` with keys in `SING_KEY_ORDER`; the
|
|
1215
|
+
* drone prints as a note name.
|
|
1216
|
+
*/
|
|
1217
|
+
function printSing(settings: TrackSing, indent: string): string {
|
|
1218
|
+
const params: [string, string][] = [];
|
|
1219
|
+
for (const key of SING_KEY_ORDER) {
|
|
1220
|
+
const value = (settings as Record<string, unknown>)[key];
|
|
1221
|
+
if (value === undefined) continue;
|
|
1222
|
+
params.push([
|
|
1223
|
+
key,
|
|
1224
|
+
key === "drone"
|
|
1225
|
+
? str(singNoteName(value as number))
|
|
1226
|
+
: Array.isArray(value)
|
|
1227
|
+
? `[${value.map((v) => num(v as number)).join(", ")}]`
|
|
1228
|
+
: typeof value === "number"
|
|
1229
|
+
? num(value)
|
|
1230
|
+
: str(String(value)),
|
|
1231
|
+
]);
|
|
1232
|
+
}
|
|
1233
|
+
const preset = settings.preset;
|
|
1234
|
+
if (params.length === 0 && preset && SING_BARE_WORDS.has(preset))
|
|
1235
|
+
return str(preset);
|
|
1236
|
+
if (params.length === 0 && !preset) return str(SING_INSTRUMENT);
|
|
1237
|
+
const head = preset ? str(preset) : "";
|
|
1238
|
+
if (params.length === 0) return `sing(${head})`;
|
|
1239
|
+
const lead = head ? `${head}, ` : "";
|
|
1240
|
+
const inline = `sing(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1241
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
1242
|
+
return inline;
|
|
1243
|
+
const inner = indent + INDENT;
|
|
1244
|
+
return `sing(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** Sing presets that are also instrument words (`"choir"`). */
|
|
1248
|
+
const SING_BARE_WORDS: ReadonlySet<string> = new Set([
|
|
1249
|
+
"aah",
|
|
1250
|
+
"ooh",
|
|
1251
|
+
"choir",
|
|
1252
|
+
"chorale",
|
|
1253
|
+
"khoomei",
|
|
1254
|
+
"sygyt",
|
|
1255
|
+
"kargyraa",
|
|
1256
|
+
]);
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* A vocoder's `src` as the SDK reads it back: the track's name slug when it
|
|
1260
|
+
* names exactly that track (no other slug or id collides), else the id.
|
|
1261
|
+
*/
|
|
1262
|
+
function vocoderSrcText(
|
|
1263
|
+
score: TrackScore,
|
|
1264
|
+
src: string | undefined,
|
|
1265
|
+
): string | undefined {
|
|
1266
|
+
if (src === undefined) return undefined;
|
|
1267
|
+
const target = score.tracks.find((track) => track.id === src);
|
|
1268
|
+
if (!target) return src;
|
|
1269
|
+
const slug = trackSlug(target.name);
|
|
1270
|
+
const clash = score.tracks.some(
|
|
1271
|
+
(track) =>
|
|
1272
|
+
track.id !== target.id &&
|
|
1273
|
+
(track.id === slug || trackSlug(track.name) === slug),
|
|
1274
|
+
);
|
|
1275
|
+
return clash ? src : slug;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* A vocoder: the bare word `"vocoder"` for a built-in carrier with nothing
|
|
1280
|
+
* set, else `vocoder("talkbox", { src: "vox", formant: 2 })` with `src`
|
|
1281
|
+
* first and the rest in `VOCODER_PARAMS` order.
|
|
1282
|
+
*/
|
|
1283
|
+
function printVocoder(
|
|
1284
|
+
settings: TrackVocoder,
|
|
1285
|
+
src: string | undefined,
|
|
1286
|
+
prefix: string,
|
|
1287
|
+
indent: string,
|
|
1288
|
+
): string {
|
|
1289
|
+
const params: [string, string][] = [];
|
|
1290
|
+
if (src !== undefined) params.push(["src", str(src)]);
|
|
1291
|
+
for (const key of Object.keys(VOCODER_PARAMS)) {
|
|
1292
|
+
const value = (settings as Record<string, unknown>)[key];
|
|
1293
|
+
if (value === undefined) continue;
|
|
1294
|
+
params.push([
|
|
1295
|
+
key,
|
|
1296
|
+
typeof value === "number"
|
|
1297
|
+
? num(value)
|
|
1298
|
+
: typeof value === "boolean"
|
|
1299
|
+
? String(value)
|
|
1300
|
+
: str(String(value)),
|
|
1301
|
+
]);
|
|
1302
|
+
}
|
|
1303
|
+
const preset = settings.preset;
|
|
1304
|
+
if (params.length === 0 && !preset && prefix === "instrument: ")
|
|
1305
|
+
return str(VOCODER_INSTRUMENT);
|
|
1306
|
+
const head = preset ? str(preset) : "";
|
|
1307
|
+
if (params.length === 0) return `vocoder(${head})`;
|
|
1308
|
+
const lead = head ? `${head}, ` : "";
|
|
1309
|
+
const inline = `vocoder(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1310
|
+
if (indent.length + prefix.length + inline.length + 1 <= WIDTH) return inline;
|
|
1311
|
+
const inner = indent + INDENT;
|
|
1312
|
+
return `vocoder(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* A track's autotune: the preset word alone (`"hard"`) when nothing
|
|
1317
|
+
* overrides it, else `autotune("pop", { speed: 40 })` with keys in
|
|
1318
|
+
* `AUTOTUNE_FIELDS` order.
|
|
1319
|
+
*/
|
|
1320
|
+
function printAutotune(settings: TrackAutotune, indent: string): string {
|
|
1321
|
+
const params: [string, string][] = [];
|
|
1322
|
+
for (const key of AUTOTUNE_FIELDS) {
|
|
1323
|
+
if (key === "preset") continue;
|
|
1324
|
+
const value = settings[key];
|
|
1325
|
+
if (value === undefined) continue;
|
|
1326
|
+
params.push([key, typeof value === "number" ? num(value) : str(value)]);
|
|
1327
|
+
}
|
|
1328
|
+
const preset = settings.preset;
|
|
1329
|
+
if (params.length === 0 && preset) return str(preset);
|
|
1330
|
+
const lead = preset ? `${str(preset)}, ` : "";
|
|
1331
|
+
const inline = `autotune(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1332
|
+
if (indent.length + "autotune: ".length + inline.length + 1 <= WIDTH)
|
|
1333
|
+
return inline;
|
|
1334
|
+
const inner = indent + INDENT;
|
|
1335
|
+
return `autotune(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1015
1338
|
/** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
|
|
1016
1339
|
function printWavetable(settings: TrackWavetable, indent: string): string {
|
|
1017
1340
|
const src = settings.table.src;
|
|
@@ -1071,6 +1394,23 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
|
1071
1394
|
if (ref.bpm !== undefined) entries.push(["bpm", num(ref.bpm)]);
|
|
1072
1395
|
if (ref.fitmode !== undefined) entries.push(["fitmode", str(ref.fitmode)]);
|
|
1073
1396
|
if (ref.len !== undefined) entries.push(["len", num(ref.len)]);
|
|
1397
|
+
if (ref.shift !== undefined) entries.push(["shift", num(ref.shift)]);
|
|
1398
|
+
if (ref.formant !== undefined) entries.push(["formant", num(ref.formant)]);
|
|
1399
|
+
if (ref.fadeInTime !== undefined)
|
|
1400
|
+
entries.push(["fadeInTime", num(ref.fadeInTime)]);
|
|
1401
|
+
if (ref.fadeTime !== undefined) entries.push(["fadeTime", num(ref.fadeTime)]);
|
|
1402
|
+
if (ref.from !== undefined) {
|
|
1403
|
+
const from = ref.from;
|
|
1404
|
+
const parts: [string, string][] = [["source", str(from.source)]];
|
|
1405
|
+
if (from.section !== undefined) parts.push(["section", str(from.section)]);
|
|
1406
|
+
if (from.bars !== undefined)
|
|
1407
|
+
parts.push(["bars", `[${num(from.bars[0])}, ${num(from.bars[1])}]`]);
|
|
1408
|
+
parts.push(["score", str(from.score)]);
|
|
1409
|
+
entries.push(["from", obj(parts, indent + INDENT, "from: ".length, 1)]);
|
|
1410
|
+
}
|
|
1411
|
+
if (ref.vel !== undefined)
|
|
1412
|
+
entries.push(["vel", `[${num(ref.vel[0])}, ${num(ref.vel[1])}]`]);
|
|
1413
|
+
if (ref.rr !== undefined) entries.push(["rr", str(ref.rr)]);
|
|
1074
1414
|
if (entries.length === 1) return str(ref.src);
|
|
1075
1415
|
return obj(entries, indent, prefix, 1);
|
|
1076
1416
|
}
|
|
@@ -1138,6 +1478,14 @@ function fill(
|
|
|
1138
1478
|
return `[\n${lines.join("\n")}\n${indent}]`;
|
|
1139
1479
|
}
|
|
1140
1480
|
|
|
1481
|
+
/** `style("id", { seed, bars, blend })`: provenance, seed and bars always written. */
|
|
1482
|
+
function printStyle(value: SongStyle): string {
|
|
1483
|
+
const options = [`seed: ${num(value.seed)}`, `bars: ${num(value.bars)}`];
|
|
1484
|
+
if (value.blend)
|
|
1485
|
+
options.push(`blend: [${str(value.blend.id)}, ${num(value.blend.weight)}]`);
|
|
1486
|
+
return `style(${str(value.id)}, { ${options.join(", ")} })`;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1141
1489
|
/** `master: { … }`, always expanded like `fx`; units in chain order. */
|
|
1142
1490
|
function printMaster(master: SongMaster): string {
|
|
1143
1491
|
const inner = INDENT + INDENT;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK is one vendored file with no imports, so `core/sdk/v1.ts` carries
|
|
3
|
+
* a copy of the lyric grammar (`core/lyrics.ts`) between two marker lines.
|
|
4
|
+
* `bun core/sdk/sync-lyrics.ts` rewrites it and `sync-lyrics.test.ts` fails
|
|
5
|
+
* when it is stale. Same scheme as `sync-instruments.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { format } from "prettier";
|
|
11
|
+
|
|
12
|
+
export const BEGIN_MARKER =
|
|
13
|
+
"// BEGIN lyrics: generated from core/lyrics.ts by core/sdk/sync-lyrics.ts";
|
|
14
|
+
export const END_MARKER = "// END lyrics";
|
|
15
|
+
|
|
16
|
+
const ROOT = join(import.meta.dir, "..", "..");
|
|
17
|
+
export const LYRICS_PATH = join(ROOT, "core", "lyrics.ts");
|
|
18
|
+
export const SDK_PATH = join(ROOT, "core", "sdk", "v1.ts");
|
|
19
|
+
|
|
20
|
+
/** The lyric block (markers included) for a `core/lyrics.ts` source. */
|
|
21
|
+
export function lyricBlock(source: string): string {
|
|
22
|
+
const start = source.indexOf("// ---- lyrics");
|
|
23
|
+
if (start < 0) throw new Error("core/lyrics.ts layout changed");
|
|
24
|
+
const body = source
|
|
25
|
+
.slice(source.indexOf("\n", start) + 1)
|
|
26
|
+
.replace(/^export /gm, "")
|
|
27
|
+
.trim();
|
|
28
|
+
return `${BEGIN_MARKER}\n${body}\n${END_MARKER}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `sdk` with its lyric block replaced, formatted like the repo. */
|
|
32
|
+
export async function syncedSdk(sdk: string, lyrics: string): Promise<string> {
|
|
33
|
+
const from = sdk.indexOf(BEGIN_MARKER);
|
|
34
|
+
const to = sdk.indexOf(END_MARKER);
|
|
35
|
+
if (from < 0 || to < 0) throw new Error("v1.ts has no lyric markers");
|
|
36
|
+
const next =
|
|
37
|
+
sdk.slice(0, from) + lyricBlock(lyrics) + sdk.slice(to + END_MARKER.length);
|
|
38
|
+
return format(next, { parser: "typescript", filepath: SDK_PATH });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (import.meta.main) {
|
|
42
|
+
const [sdk, lyrics] = await Promise.all([
|
|
43
|
+
readFile(SDK_PATH, "utf8"),
|
|
44
|
+
readFile(LYRICS_PATH, "utf8"),
|
|
45
|
+
]);
|
|
46
|
+
const next = await syncedSdk(sdk, lyrics);
|
|
47
|
+
if (next !== sdk) await writeFile(SDK_PATH, next);
|
|
48
|
+
console.log(next === sdk ? "v1.ts lyrics up to date" : "v1.ts updated");
|
|
49
|
+
}
|