@hraness/dawg 0.6.1 → 0.8.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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- 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/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- 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 +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -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 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -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 +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -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 +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- 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/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -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/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- 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 +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- 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 +82 -32
- 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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- 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 +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- 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 +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- 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 +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- 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 +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
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,6 +21,7 @@ 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,
|
|
@@ -28,6 +29,18 @@ import {
|
|
|
28
29
|
type TrackModal,
|
|
29
30
|
} from "../resonators.ts";
|
|
30
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";
|
|
31
44
|
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
32
45
|
import {
|
|
33
46
|
BUILTIN_TABLE_PREFIX,
|
|
@@ -45,13 +58,15 @@ import {
|
|
|
45
58
|
type SampleRef,
|
|
46
59
|
type Sampler,
|
|
47
60
|
type Section,
|
|
61
|
+
type AudioClip,
|
|
62
|
+
type Take,
|
|
48
63
|
type Track,
|
|
49
64
|
} from "../score.ts";
|
|
50
65
|
import { trackSlug } from "../slug.ts";
|
|
51
|
-
import { barStartTick } from "../tempo.ts";
|
|
66
|
+
import { barStartTick, timeWithinSong } from "../tempo.ts";
|
|
52
67
|
import type { Tuning } from "../tuning.ts";
|
|
53
68
|
import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
|
|
54
|
-
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
|
|
69
|
+
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY, defaultClipId } from "./v1.ts";
|
|
55
70
|
|
|
56
71
|
const WIDTH = 80;
|
|
57
72
|
const INDENT = " ";
|
|
@@ -157,6 +172,13 @@ const RESERVED = new Set([
|
|
|
157
172
|
"granular",
|
|
158
173
|
"modal",
|
|
159
174
|
"wind",
|
|
175
|
+
"sing",
|
|
176
|
+
"audio",
|
|
177
|
+
"take",
|
|
178
|
+
"lyrics",
|
|
179
|
+
"repeatAudio",
|
|
180
|
+
"vocoder",
|
|
181
|
+
"autotune",
|
|
160
182
|
"slices",
|
|
161
183
|
"euclid",
|
|
162
184
|
"euclidRot",
|
|
@@ -197,6 +219,7 @@ export function printSong(
|
|
|
197
219
|
const helpers = [
|
|
198
220
|
"song",
|
|
199
221
|
...TIME_HELPERS.filter((name) => time.used.has(name)),
|
|
222
|
+
...(score.style ? ["style"] : []),
|
|
200
223
|
];
|
|
201
224
|
const lines: string[] = [`import { ${helpers.join(", ")} } from "dawg";`];
|
|
202
225
|
for (const track of score.tracks)
|
|
@@ -227,6 +250,7 @@ export function printSong(
|
|
|
227
250
|
)}`,
|
|
228
251
|
);
|
|
229
252
|
if (score.master) entries.push(printMaster(score.master));
|
|
253
|
+
if (score.style) entries.push(`style: ${printStyle(score.style)}`);
|
|
230
254
|
// Song sections and form (0.5): printed only when the song has them.
|
|
231
255
|
if (score.sections.length > 0)
|
|
232
256
|
entries.push(
|
|
@@ -239,8 +263,18 @@ export function printSong(
|
|
|
239
263
|
);
|
|
240
264
|
if (score.form.length > 0)
|
|
241
265
|
entries.push(`form: ${printForm(score.form, INDENT)}`);
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
// One field for both: `loop: "5-6"` (1-based bars) or `loop: "chorus"`.
|
|
267
|
+
if (score.loop)
|
|
268
|
+
entries.push(
|
|
269
|
+
`loop: ${str(
|
|
270
|
+
score.loop.bars === 1
|
|
271
|
+
? `${score.loop.startBar + 1}`
|
|
272
|
+
: `${score.loop.startBar + 1}-${score.loop.startBar + score.loop.bars}`,
|
|
273
|
+
)}`,
|
|
274
|
+
);
|
|
275
|
+
else if (score.loopSection !== undefined)
|
|
276
|
+
entries.push(`loop: ${str(score.loopSection)}`);
|
|
277
|
+
if (score.calibration) entries.push(`calibration: ${num(score.calibration)}`);
|
|
244
278
|
lines.push("export default song({");
|
|
245
279
|
for (const entry of entries) lines.push(`${INDENT}${entry},`);
|
|
246
280
|
lines.push("});", "");
|
|
@@ -257,7 +291,8 @@ function printTime(score: TrackScore): {
|
|
|
257
291
|
} {
|
|
258
292
|
const marks: string[] = [];
|
|
259
293
|
const used = new Set<string>();
|
|
260
|
-
|
|
294
|
+
// Marks past the final barline are never heard and the SDK refuses them.
|
|
295
|
+
const time = timeWithinSong(score);
|
|
261
296
|
if (!time) return { marks, used };
|
|
262
297
|
const beat = (tick: number) => num(tick / score.ticksPerBeat);
|
|
263
298
|
type Mark = { tick: number; order: number; text: string };
|
|
@@ -421,6 +456,20 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
421
456
|
? printWind(track.wind, INDENT)
|
|
422
457
|
: undefined;
|
|
423
458
|
if (windCall?.startsWith("wind(")) used.add("wind");
|
|
459
|
+
const singCall =
|
|
460
|
+
track.instrument === SING_INSTRUMENT && track.sing
|
|
461
|
+
? printSing(track.sing, INDENT)
|
|
462
|
+
: undefined;
|
|
463
|
+
if (singCall?.startsWith("sing(")) used.add("sing");
|
|
464
|
+
const vocoderCall = track.vocoder
|
|
465
|
+
? printVocoder(
|
|
466
|
+
track.vocoder,
|
|
467
|
+
vocoderSrcText(score, track.vocoder.src),
|
|
468
|
+
track.instrument === VOCODER_INSTRUMENT ? "instrument: " : "vocoder: ",
|
|
469
|
+
INDENT,
|
|
470
|
+
)
|
|
471
|
+
: undefined;
|
|
472
|
+
if (vocoderCall?.startsWith("vocoder(")) used.add("vocoder");
|
|
424
473
|
|
|
425
474
|
const entries: string[] = [
|
|
426
475
|
`id: ${str(track.id)}`,
|
|
@@ -441,8 +490,19 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
441
490
|
entries.push(`instrument: ${modalCall}`);
|
|
442
491
|
} else if (windCall) {
|
|
443
492
|
entries.push(`instrument: ${windCall}`);
|
|
493
|
+
} else if (singCall) {
|
|
494
|
+
entries.push(`instrument: ${singCall}`);
|
|
495
|
+
} else if (vocoderCall && track.instrument === VOCODER_INSTRUMENT) {
|
|
496
|
+
entries.push(`instrument: ${vocoderCall}`);
|
|
444
497
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
498
|
+
if (vocoderCall && track.instrument !== VOCODER_INSTRUMENT)
|
|
499
|
+
entries.push(`vocoder: ${vocoderCall}`);
|
|
445
500
|
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
501
|
+
// The table a track keeps after leaving the wavetable instrument.
|
|
502
|
+
if (track.wavetable && !wavetable) {
|
|
503
|
+
used.add("wavetable");
|
|
504
|
+
entries.push(`wavetable: ${printWavetable(track.wavetable, INDENT)}`);
|
|
505
|
+
}
|
|
446
506
|
if (track.granular && !grained)
|
|
447
507
|
entries.push(
|
|
448
508
|
`granular: ${obj(
|
|
@@ -595,6 +655,11 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
595
655
|
);
|
|
596
656
|
}
|
|
597
657
|
entries.push(...performanceEntries(score, track));
|
|
658
|
+
if (track.autotune) {
|
|
659
|
+
const call = printAutotune(track.autotune, INDENT);
|
|
660
|
+
if (call.startsWith("autotune(")) used.add("autotune");
|
|
661
|
+
entries.push(`autotune: ${call}`);
|
|
662
|
+
}
|
|
598
663
|
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
599
664
|
["volume", track.volumeAutomation],
|
|
600
665
|
["pan", track.panAutomation],
|
|
@@ -653,6 +718,20 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
653
718
|
}
|
|
654
719
|
if (rows.length === 0 || printed.length > 0)
|
|
655
720
|
entries.push(`notes: ${list(printed, INDENT, "notes: ".length, 1)}`);
|
|
721
|
+
if (track.takes && track.takes.length > 0) {
|
|
722
|
+
used.add("take");
|
|
723
|
+
const items = track.takes.map((t) => printTake(score, t, INDENT + INDENT));
|
|
724
|
+
entries.push(`takes: ${list(items, INDENT, "takes: ".length, 1, true)}`);
|
|
725
|
+
}
|
|
726
|
+
if (track.clips && track.clips.length > 0) {
|
|
727
|
+
used.add("audio");
|
|
728
|
+
const items = track.clips.map((clip, index) =>
|
|
729
|
+
printClip(score, clip, index, INDENT + INDENT),
|
|
730
|
+
);
|
|
731
|
+
entries.push(
|
|
732
|
+
`clips: ${list(items, INDENT, "clips: ".length, 1, items.length > 1)}`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
656
735
|
|
|
657
736
|
const names = [
|
|
658
737
|
"track",
|
|
@@ -668,8 +747,13 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
668
747
|
"granular",
|
|
669
748
|
"modal",
|
|
670
749
|
"wind",
|
|
750
|
+
"sing",
|
|
751
|
+
"vocoder",
|
|
752
|
+
"autotune",
|
|
671
753
|
"euclid",
|
|
672
754
|
"grid",
|
|
755
|
+
"audio",
|
|
756
|
+
"take",
|
|
673
757
|
]
|
|
674
758
|
.filter((name) => used.has(name))
|
|
675
759
|
.join(", ");
|
|
@@ -778,6 +862,9 @@ function expressionEntries(
|
|
|
778
862
|
),
|
|
779
863
|
]);
|
|
780
864
|
}
|
|
865
|
+
if (note.vowel) entries.push(["vowel", str(note.vowel)]);
|
|
866
|
+
if (note.lyric !== undefined) entries.push(["lyric", str(note.lyric)]);
|
|
867
|
+
if (note.drift !== undefined) entries.push(["drift", num(note.drift)]);
|
|
781
868
|
return entries.length === 0 ? undefined : entries;
|
|
782
869
|
}
|
|
783
870
|
|
|
@@ -801,6 +888,66 @@ function call(
|
|
|
801
888
|
return `${name}(${[...args, "{"].join(", ")}\n${body}\n${indent}})`;
|
|
802
889
|
}
|
|
803
890
|
|
|
891
|
+
/** Seconds at 4 decimals (sources.md 6). */
|
|
892
|
+
function secs(value: number): string {
|
|
893
|
+
return num(Math.round(value * 1e4) / 1e4);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* `audio(src, { at, offset, dur, gain, fadeInTime, fadeTime, rev, take,
|
|
898
|
+
* mute, text, say, sha256 })` (SDK 1.32.0); defaults omitted, an id printed
|
|
899
|
+
* only when it differs from the positional default.
|
|
900
|
+
*/
|
|
901
|
+
function printClip(
|
|
902
|
+
score: TrackScore,
|
|
903
|
+
clip: AudioClip,
|
|
904
|
+
index: number,
|
|
905
|
+
indent: string,
|
|
906
|
+
): string {
|
|
907
|
+
const how: [string, string][] = [];
|
|
908
|
+
if (clip.id !== defaultClipId(index)) how.push(["id", str(clip.id)]);
|
|
909
|
+
if (clip.startTick !== 0)
|
|
910
|
+
how.push(["at", num(clip.startTick / score.ticksPerBeat)]);
|
|
911
|
+
if (clip.offset) how.push(["offset", secs(clip.offset)]);
|
|
912
|
+
if (clip.dur !== undefined) how.push(["dur", secs(clip.dur)]);
|
|
913
|
+
// Gain is linear in the file; a comment shows the dB the commands speak.
|
|
914
|
+
if (clip.gain !== undefined && clip.gain !== 1)
|
|
915
|
+
how.push([
|
|
916
|
+
"gain",
|
|
917
|
+
`${num(clip.gain)} /* ${
|
|
918
|
+
clip.gain > 0 ? (20 * Math.log10(clip.gain)).toFixed(1) : "-inf"
|
|
919
|
+
} dB */`,
|
|
920
|
+
]);
|
|
921
|
+
if (clip.fadeInTime !== undefined)
|
|
922
|
+
how.push(["fadeInTime", secs(clip.fadeInTime)]);
|
|
923
|
+
if (clip.fadeTime !== undefined) how.push(["fadeTime", secs(clip.fadeTime)]);
|
|
924
|
+
if (clip.rev) how.push(["rev", "true"]);
|
|
925
|
+
if (clip.take !== undefined) how.push(["take", str(clip.take)]);
|
|
926
|
+
if (clip.mute) how.push(["mute", "true"]);
|
|
927
|
+
if (clip.text !== undefined) how.push(["text", str(clip.text)]);
|
|
928
|
+
if (clip.say !== undefined) how.push(["say", JSON.stringify(clip.say)]);
|
|
929
|
+
how.push(["sha256", str(clip.sha256)]);
|
|
930
|
+
return call("audio", [str(clip.src)], how, indent);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** `take(name, src, { at, in, out, ... })` (SDK 1.32.0). */
|
|
934
|
+
function printTake(score: TrackScore, t: Take, indent: string): string {
|
|
935
|
+
const beatsAt = (tick: number) => num(tick / score.ticksPerBeat);
|
|
936
|
+
const how: [string, string][] = [];
|
|
937
|
+
if (t.startTick !== 0) how.push(["at", beatsAt(t.startTick)]);
|
|
938
|
+
how.push(["in", beatsAt(t.inTick)], ["out", beatsAt(t.outTick)]);
|
|
939
|
+
if (t.offset) how.push(["offset", secs(t.offset)]);
|
|
940
|
+
if (t.latency) how.push(["latency", secs(t.latency)]);
|
|
941
|
+
if (t.latencyAssumed) how.push(["latencyAssumed", "true"]);
|
|
942
|
+
if (t.ppm !== undefined)
|
|
943
|
+
how.push(["ppm", num(Math.round(t.ppm * 100) / 100)]);
|
|
944
|
+
if (t.fit !== undefined) how.push(["fit", num(t.fit)]);
|
|
945
|
+
if (t.warn !== undefined) how.push(["warn", str(t.warn)]);
|
|
946
|
+
if (t.nudge !== undefined) how.push(["nudge", num(t.nudge)]);
|
|
947
|
+
how.push(["sha256", str(t.sha256)]);
|
|
948
|
+
return call("take", [str(t.name), str(t.src)], how, indent);
|
|
949
|
+
}
|
|
950
|
+
|
|
804
951
|
/** A track's performance fields (SDK 1.15.0) as `key: literal` entries. */
|
|
805
952
|
function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
806
953
|
const entries: string[] = [];
|
|
@@ -1070,6 +1217,133 @@ function printWind(settings: TrackWind, indent: string): string {
|
|
|
1070
1217
|
return `wind(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1071
1218
|
}
|
|
1072
1219
|
|
|
1220
|
+
/**
|
|
1221
|
+
* A sing track's instrument: the preset word alone (`"choir"`) when there
|
|
1222
|
+
* are no overrides and the preset is an instrument word, else
|
|
1223
|
+
* `sing("khoomei", { drone: "D3" })` with keys in `SING_KEY_ORDER`; the
|
|
1224
|
+
* drone prints as a note name.
|
|
1225
|
+
*/
|
|
1226
|
+
function printSing(settings: TrackSing, indent: string): string {
|
|
1227
|
+
const params: [string, string][] = [];
|
|
1228
|
+
for (const key of SING_KEY_ORDER) {
|
|
1229
|
+
const value = (settings as Record<string, unknown>)[key];
|
|
1230
|
+
if (value === undefined) continue;
|
|
1231
|
+
params.push([
|
|
1232
|
+
key,
|
|
1233
|
+
key === "drone"
|
|
1234
|
+
? str(singNoteName(value as number))
|
|
1235
|
+
: Array.isArray(value)
|
|
1236
|
+
? `[${value.map((v) => num(v as number)).join(", ")}]`
|
|
1237
|
+
: typeof value === "number"
|
|
1238
|
+
? num(value)
|
|
1239
|
+
: str(String(value)),
|
|
1240
|
+
]);
|
|
1241
|
+
}
|
|
1242
|
+
const preset = settings.preset;
|
|
1243
|
+
if (params.length === 0 && preset && SING_BARE_WORDS.has(preset))
|
|
1244
|
+
return str(preset);
|
|
1245
|
+
if (params.length === 0 && !preset) return str(SING_INSTRUMENT);
|
|
1246
|
+
const head = preset ? str(preset) : "";
|
|
1247
|
+
if (params.length === 0) return `sing(${head})`;
|
|
1248
|
+
const lead = head ? `${head}, ` : "";
|
|
1249
|
+
const inline = `sing(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1250
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
1251
|
+
return inline;
|
|
1252
|
+
const inner = indent + INDENT;
|
|
1253
|
+
return `sing(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** Sing presets that are also instrument words (`"choir"`). */
|
|
1257
|
+
const SING_BARE_WORDS: ReadonlySet<string> = new Set([
|
|
1258
|
+
"aah",
|
|
1259
|
+
"ooh",
|
|
1260
|
+
"choir",
|
|
1261
|
+
"chorale",
|
|
1262
|
+
"khoomei",
|
|
1263
|
+
"sygyt",
|
|
1264
|
+
"kargyraa",
|
|
1265
|
+
]);
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* A vocoder's `src` as the SDK reads it back: the track's name slug when it
|
|
1269
|
+
* names exactly that track (no other slug or id collides), else the id.
|
|
1270
|
+
*/
|
|
1271
|
+
function vocoderSrcText(
|
|
1272
|
+
score: TrackScore,
|
|
1273
|
+
src: string | undefined,
|
|
1274
|
+
): string | undefined {
|
|
1275
|
+
if (src === undefined) return undefined;
|
|
1276
|
+
const target = score.tracks.find((track) => track.id === src);
|
|
1277
|
+
if (!target) return src;
|
|
1278
|
+
const slug = trackSlug(target.name);
|
|
1279
|
+
const clash = score.tracks.some(
|
|
1280
|
+
(track) =>
|
|
1281
|
+
track.id !== target.id &&
|
|
1282
|
+
(track.id === slug || trackSlug(track.name) === slug),
|
|
1283
|
+
);
|
|
1284
|
+
return clash ? src : slug;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* A vocoder: the bare word `"vocoder"` for a built-in carrier with nothing
|
|
1289
|
+
* set, else `vocoder("talkbox", { src: "vox", formant: 2 })` with `src`
|
|
1290
|
+
* first and the rest in `VOCODER_PARAMS` order.
|
|
1291
|
+
*/
|
|
1292
|
+
function printVocoder(
|
|
1293
|
+
settings: TrackVocoder,
|
|
1294
|
+
src: string | undefined,
|
|
1295
|
+
prefix: string,
|
|
1296
|
+
indent: string,
|
|
1297
|
+
): string {
|
|
1298
|
+
const params: [string, string][] = [];
|
|
1299
|
+
if (src !== undefined) params.push(["src", str(src)]);
|
|
1300
|
+
for (const key of Object.keys(VOCODER_PARAMS)) {
|
|
1301
|
+
const value = (settings as Record<string, unknown>)[key];
|
|
1302
|
+
if (value === undefined) continue;
|
|
1303
|
+
params.push([
|
|
1304
|
+
key,
|
|
1305
|
+
typeof value === "number"
|
|
1306
|
+
? num(value)
|
|
1307
|
+
: typeof value === "boolean"
|
|
1308
|
+
? String(value)
|
|
1309
|
+
: str(String(value)),
|
|
1310
|
+
]);
|
|
1311
|
+
}
|
|
1312
|
+
const preset = settings.preset;
|
|
1313
|
+
if (params.length === 0 && !preset && prefix === "instrument: ")
|
|
1314
|
+
return str(VOCODER_INSTRUMENT);
|
|
1315
|
+
const head = preset ? str(preset) : "";
|
|
1316
|
+
if (params.length === 0) return `vocoder(${head})`;
|
|
1317
|
+
const lead = head ? `${head}, ` : "";
|
|
1318
|
+
const inline = `vocoder(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1319
|
+
if (indent.length + prefix.length + inline.length + 1 <= WIDTH) return inline;
|
|
1320
|
+
const inner = indent + INDENT;
|
|
1321
|
+
return `vocoder(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* A track's autotune: the preset word alone (`"hard"`) when nothing
|
|
1326
|
+
* overrides it, else `autotune("pop", { speed: 40 })` with keys in
|
|
1327
|
+
* `AUTOTUNE_FIELDS` order.
|
|
1328
|
+
*/
|
|
1329
|
+
function printAutotune(settings: TrackAutotune, indent: string): string {
|
|
1330
|
+
const params: [string, string][] = [];
|
|
1331
|
+
for (const key of AUTOTUNE_FIELDS) {
|
|
1332
|
+
if (key === "preset") continue;
|
|
1333
|
+
const value = settings[key];
|
|
1334
|
+
if (value === undefined) continue;
|
|
1335
|
+
params.push([key, typeof value === "number" ? num(value) : str(value)]);
|
|
1336
|
+
}
|
|
1337
|
+
const preset = settings.preset;
|
|
1338
|
+
if (params.length === 0 && preset) return str(preset);
|
|
1339
|
+
const lead = preset ? `${str(preset)}, ` : "";
|
|
1340
|
+
const inline = `autotune(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1341
|
+
if (indent.length + "autotune: ".length + inline.length + 1 <= WIDTH)
|
|
1342
|
+
return inline;
|
|
1343
|
+
const inner = indent + INDENT;
|
|
1344
|
+
return `autotune(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1073
1347
|
/** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
|
|
1074
1348
|
function printWavetable(settings: TrackWavetable, indent: string): string {
|
|
1075
1349
|
const src = settings.table.src;
|
|
@@ -1213,6 +1487,14 @@ function fill(
|
|
|
1213
1487
|
return `[\n${lines.join("\n")}\n${indent}]`;
|
|
1214
1488
|
}
|
|
1215
1489
|
|
|
1490
|
+
/** `style("id", { seed, bars, blend })`: provenance, seed and bars always written. */
|
|
1491
|
+
function printStyle(value: SongStyle): string {
|
|
1492
|
+
const options = [`seed: ${num(value.seed)}`, `bars: ${num(value.bars)}`];
|
|
1493
|
+
if (value.blend)
|
|
1494
|
+
options.push(`blend: [${str(value.blend.id)}, ${num(value.blend.weight)}]`);
|
|
1495
|
+
return `style(${str(value.id)}, { ${options.join(", ")} })`;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1216
1498
|
/** `master: { … }`, always expanded like `fx`; units in chain order. */
|
|
1217
1499
|
function printMaster(master: SongMaster): string {
|
|
1218
1500
|
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
|
+
}
|