@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/src/auth/tui.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function tuiAuthCommand(
|
|
|
51
51
|
else lines.push(`unknown auth command: ${(name ?? "").slice(0, 20)}`);
|
|
52
52
|
} catch (error) {
|
|
53
53
|
lines.push(
|
|
54
|
-
`auth failed · ${error instanceof Error ? error.message : String(error)} · dawg
|
|
54
|
+
`auth failed · ${error instanceof Error ? error.message : String(error)} · dawg model key in a shell`,
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
return lines.map((line) => line.slice(0, 240));
|
|
@@ -86,22 +86,31 @@ export async function modelPickerItems(
|
|
|
86
86
|
}));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* `/model <value>`: save for the active provider; returns the receipt and
|
|
91
|
+
* whether it worked, so the caller shows a refusal as one (never a ✓).
|
|
92
|
+
*/
|
|
90
93
|
export async function tuiSetModel(
|
|
91
94
|
value: string,
|
|
92
95
|
runner: CommandRunner = systemRunner,
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
auth = defaultAuthEnv(runner),
|
|
97
|
+
): Promise<{ readonly ok: boolean; readonly text: string }> {
|
|
95
98
|
const selection = await selectProvider(auth);
|
|
96
99
|
if (selection.kind === "offline")
|
|
97
|
-
return `model · unavailable: ${selection.reason}
|
|
100
|
+
return { ok: false, text: `model · unavailable: ${selection.reason}` };
|
|
98
101
|
try {
|
|
99
|
-
return
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
text: `model · ${await saveModelChoice(auth, selection, value)}`,
|
|
105
|
+
};
|
|
100
106
|
} catch (error) {
|
|
101
107
|
const rows = await modelPickerItems(selection, runner).catch(() => []);
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
text: `model · ${error instanceof Error ? error.message : String(error)}; try ${rows
|
|
111
|
+
.slice(0, 8)
|
|
112
|
+
.map((row) => row.label)
|
|
113
|
+
.join(", ")} · current ${providerLabel(selection)}`,
|
|
114
|
+
};
|
|
106
115
|
}
|
|
107
116
|
}
|
package/src/commands/arrange.ts
CHANGED
|
@@ -30,12 +30,15 @@ import {
|
|
|
30
30
|
type Section,
|
|
31
31
|
type TrackScore,
|
|
32
32
|
} from "../../core/score.ts";
|
|
33
|
+
import { nearest } from "./nearest.ts";
|
|
33
34
|
import {
|
|
34
35
|
addSection,
|
|
35
36
|
arrangedBars,
|
|
36
37
|
arrangedStartBar,
|
|
37
38
|
deleteSection,
|
|
38
39
|
duplicateSection,
|
|
40
|
+
joinSection,
|
|
41
|
+
splitSection,
|
|
39
42
|
DROP_CUT_MIN,
|
|
40
43
|
FILL_STYLES,
|
|
41
44
|
findSection,
|
|
@@ -95,6 +98,8 @@ export type SectionCommand =
|
|
|
95
98
|
| { type: "section-reset"; name: string }
|
|
96
99
|
| { type: "section-loop"; name?: string }
|
|
97
100
|
| { type: "section-jump"; name: string }
|
|
101
|
+
| { type: "section-split"; name: string; atBar: number }
|
|
102
|
+
| { type: "section-join"; name: string; force: boolean }
|
|
98
103
|
| { type: "form-show" }
|
|
99
104
|
| { type: "form-set"; text: string }
|
|
100
105
|
| { type: "form-bake" }
|
|
@@ -133,6 +138,8 @@ const SUBCOMMANDS = new Set([
|
|
|
133
138
|
"loop",
|
|
134
139
|
"jump",
|
|
135
140
|
"go",
|
|
141
|
+
"split",
|
|
142
|
+
"join",
|
|
136
143
|
]);
|
|
137
144
|
|
|
138
145
|
/** `3-6` or `3..6` or `3` → 0-based start and length (1-based inclusive in). */
|
|
@@ -240,41 +247,17 @@ function unknownSection(
|
|
|
240
247
|
return { type: "section-unknown", sub, name };
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
function editDistance(a: string, b: string): number {
|
|
244
|
-
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
245
|
-
for (let i = 1; i <= a.length; i += 1) {
|
|
246
|
-
let previous = row[0]!;
|
|
247
|
-
row[0] = i;
|
|
248
|
-
for (let j = 1; j <= b.length; j += 1) {
|
|
249
|
-
const current = row[j]!;
|
|
250
|
-
row[j] = Math.min(
|
|
251
|
-
row[j]! + 1,
|
|
252
|
-
row[j - 1]! + 1,
|
|
253
|
-
previous + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
254
|
-
);
|
|
255
|
-
previous = current;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return row[b.length]!;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
250
|
/** `no section named chrous (sections: …) · did you mean chorus?` */
|
|
262
251
|
function unknownSectionMessage(score: TrackScore, name: string): string {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!best || distance < best.distance)
|
|
268
|
-
best = { name: section.name, distance };
|
|
269
|
-
}
|
|
252
|
+
const match = nearest(
|
|
253
|
+
name,
|
|
254
|
+
score.sections.map((section) => section.name),
|
|
255
|
+
);
|
|
270
256
|
const list =
|
|
271
257
|
score.sections.length > 0
|
|
272
258
|
? ` (sections: ${score.sections.map((s) => s.name).join(", ")})`
|
|
273
259
|
: " (no sections yet: section verse 1-8 marks one)";
|
|
274
|
-
const near =
|
|
275
|
-
best && best.distance <= Math.max(2, Math.floor(wanted.length / 3))
|
|
276
|
-
? ` · did you mean ${best.name}?`
|
|
277
|
-
: "";
|
|
260
|
+
const near = match ? ` · did you mean ${match}?` : "";
|
|
278
261
|
return `section · no section named ${name}${list}${near}`;
|
|
279
262
|
}
|
|
280
263
|
|
|
@@ -393,6 +376,22 @@ function parseSub(
|
|
|
393
376
|
};
|
|
394
377
|
case "vary":
|
|
395
378
|
return parseVary(lead.name, lead.rest);
|
|
379
|
+
case "split": {
|
|
380
|
+
// section split verse at 5 (1-based bar)
|
|
381
|
+
const where = tail[0] === "at" ? tail.slice(1) : tail;
|
|
382
|
+
if (where.length === 2 && where[0] === "bar") where.shift();
|
|
383
|
+
if (where.length !== 1 || !/^\d{1,4}$/u.test(where[0]!)) return undefined;
|
|
384
|
+
const bar = Number(where[0]);
|
|
385
|
+
return bar >= 1
|
|
386
|
+
? { type: "section-split", name: lead.name, atBar: bar - 1 }
|
|
387
|
+
: undefined;
|
|
388
|
+
}
|
|
389
|
+
case "join":
|
|
390
|
+
if (tail.length === 0)
|
|
391
|
+
return { type: "section-join", name: lead.name, force: false };
|
|
392
|
+
return tail.length === 1 && tail[0] === "force"
|
|
393
|
+
? { type: "section-join", name: lead.name, force: true }
|
|
394
|
+
: undefined;
|
|
396
395
|
default:
|
|
397
396
|
return undefined;
|
|
398
397
|
}
|
|
@@ -447,7 +446,10 @@ function parseFormWords(
|
|
|
447
446
|
lower: readonly string[],
|
|
448
447
|
): SectionCommand | undefined {
|
|
449
448
|
if (words.length === 0) return { type: "form-show" };
|
|
450
|
-
if (
|
|
449
|
+
if (
|
|
450
|
+
lower.length === 1 &&
|
|
451
|
+
(lower[0] === "bake" || lower[0] === "flatten" || lower[0] === "print")
|
|
452
|
+
)
|
|
451
453
|
return { type: "form-bake" };
|
|
452
454
|
if (lower.length === 1 && (lower[0] === "off" || lower[0] === "none"))
|
|
453
455
|
return { type: "form-set", text: "" };
|
|
@@ -736,6 +738,27 @@ function applyUnchecked(
|
|
|
736
738
|
`section · added ${section.name} ${barsLabel(section)}${next.bars > score.bars ? ` · song now ${next.bars} bars` : ""}`,
|
|
737
739
|
);
|
|
738
740
|
}
|
|
741
|
+
case "section-split": {
|
|
742
|
+
const next = splitSection(score, command.name, command.atBar);
|
|
743
|
+
const half = next.sections.find(
|
|
744
|
+
(candidate) => !findSection(score, candidate.name),
|
|
745
|
+
);
|
|
746
|
+
return changed(
|
|
747
|
+
score,
|
|
748
|
+
next,
|
|
749
|
+
`section · ${command.name} split at bar ${command.atBar + 1}${half ? ` · ${half.name} ${barsLabel(half)}` : ""}`,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
case "section-join": {
|
|
753
|
+
const section = findSection(score, command.name)!;
|
|
754
|
+
const next = joinSection(score, command.name, { force: command.force });
|
|
755
|
+
const joined = findSection(next, section.name);
|
|
756
|
+
return changed(
|
|
757
|
+
score,
|
|
758
|
+
next,
|
|
759
|
+
`section · joined into ${section.name}${joined ? ` ${barsLabel(joined)}` : ""}`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
739
762
|
case "section-dup": {
|
|
740
763
|
const next = duplicateSection(score, command.name, {
|
|
741
764
|
...(command.as ? { as: command.as } : {}),
|
|
@@ -905,10 +928,10 @@ function applyUnchecked(
|
|
|
905
928
|
if (score.form.length === 0)
|
|
906
929
|
return {
|
|
907
930
|
ok: false,
|
|
908
|
-
message: "form · no form to
|
|
931
|
+
message: "form · no form to print · form verse chorus verse",
|
|
909
932
|
};
|
|
910
933
|
const next = flattenForm(score);
|
|
911
|
-
return changed(score, next, `form
|
|
934
|
+
return changed(score, next, `printed form to tape · ${next.bars} bars`);
|
|
912
935
|
}
|
|
913
936
|
case "build": {
|
|
914
937
|
const result = generateBuild(score, command.options);
|
|
@@ -947,3 +970,30 @@ function describeVary(vary: { transpose?: number; gain?: number }): string {
|
|
|
947
970
|
if (vary.gain !== undefined) bits.push(`gain ${vary.gain}`);
|
|
948
971
|
return bits.join(" ");
|
|
949
972
|
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* `loop 2-3`: the loop range (score.loop, op1-ux §4), never a section. A
|
|
976
|
+
* range past the song's end is refused; a looped section stops looping.
|
|
977
|
+
*/
|
|
978
|
+
export function loopSpan(
|
|
979
|
+
score: TrackScore,
|
|
980
|
+
from: number,
|
|
981
|
+
to: number,
|
|
982
|
+
):
|
|
983
|
+
| Readonly<{ ok: true; next: TrackScore; message: string }>
|
|
984
|
+
| Readonly<{ ok: false; message: string }> {
|
|
985
|
+
const bars = from === to ? `${from}` : `${from}-${to}`;
|
|
986
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 1 || to < from)
|
|
987
|
+
return { ok: false, message: `loop ${bars} · bars run low-high from 1` };
|
|
988
|
+
if (to > score.bars)
|
|
989
|
+
return {
|
|
990
|
+
ok: false,
|
|
991
|
+
message: `loop ${bars} · the song has ${score.bars} bars · bars ${to} lengthens it`,
|
|
992
|
+
};
|
|
993
|
+
const next = score.withLoop({ startBar: from - 1, bars: to - from + 1 });
|
|
994
|
+
return {
|
|
995
|
+
ok: true,
|
|
996
|
+
next,
|
|
997
|
+
message: `loop · bars ${from === to ? from : `${from}–${to}`} · loop off plays the song`,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `autotune` prompt command (0.7): pitch correction on the focused
|
|
3
|
+
* track's clips and sampler voices (core/autotune.ts). `/vocal autotune`
|
|
4
|
+
* is the same command; the menu and the `autotune_vocal` agent tool build
|
|
5
|
+
* the same edits through `nextAutotune`.
|
|
6
|
+
*
|
|
7
|
+
* autotune pop on the focused track (or show it)
|
|
8
|
+
* autotune <preset> hard robot warble trap pop natural
|
|
9
|
+
* gentle guided locked
|
|
10
|
+
* autotune <preset> <field> <v>… a preset with overrides
|
|
11
|
+
* autotune speed 35 relax 0.4 fields (base pop)
|
|
12
|
+
* autotune to chord follow the chords
|
|
13
|
+
* autotune to notes melody note-guided from another track
|
|
14
|
+
* autotune key D bayati a key for the scale target
|
|
15
|
+
* autotune voice alto the tracking range
|
|
16
|
+
* autotune <field> off back to the preset's value
|
|
17
|
+
* autotune reset keep only the preset
|
|
18
|
+
* autotune off remove autotune
|
|
19
|
+
* autotune presets the presets with one-line characters
|
|
20
|
+
*
|
|
21
|
+
* `/tune` stays the alias of `/tuning`: `/tune hard` explains the
|
|
22
|
+
* difference and points here. One `updateTrack` revision per command.
|
|
23
|
+
*/
|
|
24
|
+
import {
|
|
25
|
+
AUTOTUNE_FIELDS,
|
|
26
|
+
AUTOTUNE_PARAMS,
|
|
27
|
+
AUTOTUNE_PRESET_HELP,
|
|
28
|
+
AUTOTUNE_PRESETS,
|
|
29
|
+
AUTOTUNE_TARGETS,
|
|
30
|
+
AUTOTUNE_VOICES,
|
|
31
|
+
describeAutotune,
|
|
32
|
+
isAutotunePreset,
|
|
33
|
+
normalizeAutotune,
|
|
34
|
+
resolveAutotune,
|
|
35
|
+
type AutotunePreset,
|
|
36
|
+
type TrackAutotune,
|
|
37
|
+
} from "../../core/autotune.ts";
|
|
38
|
+
import { parseKey } from "../../core/chords.ts";
|
|
39
|
+
import { FxValidationError } from "../../core/params.ts";
|
|
40
|
+
import {
|
|
41
|
+
ScoreValidationError,
|
|
42
|
+
updateTrack,
|
|
43
|
+
type TrackScore,
|
|
44
|
+
} from "../../core/score.ts";
|
|
45
|
+
import { autotuneEngineNote, hasGuideNotes } from "../audio/autotune.ts";
|
|
46
|
+
import { nearest } from "./nearest.ts";
|
|
47
|
+
|
|
48
|
+
/** A field value from a command: `null` returns it to the preset's value. */
|
|
49
|
+
export type AutotuneValue = number | string | null;
|
|
50
|
+
|
|
51
|
+
export type AutotuneCommand =
|
|
52
|
+
| { type: "autotune-show" }
|
|
53
|
+
| { type: "autotune-list" }
|
|
54
|
+
| { type: "autotune-reset" }
|
|
55
|
+
| { type: "autotune-off" }
|
|
56
|
+
| { type: "autotune-usage"; message: string }
|
|
57
|
+
| {
|
|
58
|
+
type: "autotune-set";
|
|
59
|
+
preset?: AutotunePreset;
|
|
60
|
+
values: Readonly<Partial<Record<keyof TrackAutotune, AutotuneValue>>>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const AUTOTUNE_USAGE =
|
|
64
|
+
"autotune [preset] [field value …] | autotune to scale|chromatic|chord|notes [track] | autotune key <key> | autotune off | autotune presets";
|
|
65
|
+
|
|
66
|
+
/** Fields the command takes, in type order (`preset` is a leading word). */
|
|
67
|
+
const FIELD_WORDS = AUTOTUNE_FIELDS.filter((field) => field !== "preset");
|
|
68
|
+
|
|
69
|
+
/** Short spellings people type. */
|
|
70
|
+
const FIELD_ALIASES: Readonly<Record<string, keyof TrackAutotune>> = {
|
|
71
|
+
target: "to",
|
|
72
|
+
rate: "vib",
|
|
73
|
+
depth: "vibmod",
|
|
74
|
+
mix: "amount",
|
|
75
|
+
retune: "speed",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function fieldName(word: string): keyof TrackAutotune | undefined {
|
|
79
|
+
if ((FIELD_WORDS as readonly string[]).includes(word))
|
|
80
|
+
return word as keyof TrackAutotune;
|
|
81
|
+
return FIELD_ALIASES[word];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rangeOf(name: keyof TrackAutotune): string {
|
|
85
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === name);
|
|
86
|
+
if (name === "glide" && spec)
|
|
87
|
+
return `glide is 0..${spec.max * 1000} ms (glide 40 or 40ms)`;
|
|
88
|
+
if (spec)
|
|
89
|
+
return `${name} is ${spec.min}..${spec.max}${spec.unit ? ` ${spec.unit}` : ""}`;
|
|
90
|
+
if (name === "to") return `to is one of ${AUTOTUNE_TARGETS.join(" ")}`;
|
|
91
|
+
if (name === "voice") return `voice is one of ${AUTOTUNE_VOICES.join(" ")}`;
|
|
92
|
+
return `${name} needs a value`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const OFF = /^(off|unset|default)$/;
|
|
96
|
+
|
|
97
|
+
/** Fields in time units: speed and hold store ms, glide stores seconds. */
|
|
98
|
+
const TIME_FIELDS: Readonly<Partial<Record<keyof TrackAutotune, number>>> = {
|
|
99
|
+
speed: 1,
|
|
100
|
+
hold: 1,
|
|
101
|
+
glide: 0.001,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Suffixes each numeric field takes besides a bare number. */
|
|
105
|
+
const UNIT_SUFFIX: Readonly<Partial<Record<keyof TrackAutotune, RegExp>>> = {
|
|
106
|
+
vib: /^hz$/,
|
|
107
|
+
vibmod: /^st$/,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A field's number, or the message why not. Time fields read like /glide:
|
|
112
|
+
* a bare number is ms, `ms` and `s` convert (`glide 40`, `speed 0.2s`),
|
|
113
|
+
* and a bare fraction of a millisecond is a unit slip. `%` scales 0..1
|
|
114
|
+
* fields. A suffix that does not fit the field is refused.
|
|
115
|
+
*/
|
|
116
|
+
function fieldNumber(
|
|
117
|
+
name: keyof TrackAutotune,
|
|
118
|
+
value: string,
|
|
119
|
+
): number | string {
|
|
120
|
+
const match = /^(-?(?:\d+\.?\d*|\.\d+))([a-z%]*)$/.exec(value);
|
|
121
|
+
if (!match) return rangeOf(name);
|
|
122
|
+
const number = Number(match[1]);
|
|
123
|
+
const suffix = match[2]!;
|
|
124
|
+
const toUnit = TIME_FIELDS[name];
|
|
125
|
+
if (toUnit !== undefined) {
|
|
126
|
+
if (suffix === "ms") return number * toUnit;
|
|
127
|
+
if (suffix === "s") return number * 1000 * toUnit;
|
|
128
|
+
if (suffix !== "")
|
|
129
|
+
return `${name} takes ms or s, not ${suffix.slice(0, 4)}`;
|
|
130
|
+
if (number > 0 && number < 1)
|
|
131
|
+
return `${name} · a bare number is ms · write ${name} ${Math.round(number * 1000)}ms or ${name} ${match[1]}s`;
|
|
132
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === name);
|
|
133
|
+
if (spec && (number * toUnit < spec.min || number * toUnit > spec.max))
|
|
134
|
+
return rangeOf(name);
|
|
135
|
+
return number * toUnit;
|
|
136
|
+
}
|
|
137
|
+
if (suffix === "") return number;
|
|
138
|
+
if (suffix === "%") {
|
|
139
|
+
const spec = AUTOTUNE_PARAMS.find((param) => param.name === name);
|
|
140
|
+
if (spec && spec.max === 1) return number / 100;
|
|
141
|
+
if (spec && spec.max === 100) return number;
|
|
142
|
+
}
|
|
143
|
+
if (UNIT_SUFFIX[name]?.test(suffix)) return number;
|
|
144
|
+
return `${name} takes no ${suffix.slice(0, 4)} · ${rangeOf(name)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Parses `/autotune …`, `/vocal autotune …` args, and `/tune <preset>`. */
|
|
148
|
+
export function parseAutotuneCommand(
|
|
149
|
+
prompt: string,
|
|
150
|
+
): AutotuneCommand | undefined {
|
|
151
|
+
if (prompt.length > 512) return undefined;
|
|
152
|
+
const raw = prompt.trim().replace(/^\//, "").split(/\s+/);
|
|
153
|
+
const verb = raw[0]?.toLowerCase();
|
|
154
|
+
// `/tune hard`: `/tune` is `/tuning`; an autotune word gets a pointer.
|
|
155
|
+
if (verb === "tune") {
|
|
156
|
+
const next = raw[1]?.toLowerCase();
|
|
157
|
+
return raw.length >= 2 && (isAutotunePreset(next) || next === "autotune")
|
|
158
|
+
? {
|
|
159
|
+
type: "autotune-usage",
|
|
160
|
+
message:
|
|
161
|
+
`/tune sets the tuning (edo, ratios, scl) · for pitch correction use /autotune ${isAutotunePreset(next) ? next : ""}`.trimEnd(),
|
|
162
|
+
}
|
|
163
|
+
: undefined;
|
|
164
|
+
}
|
|
165
|
+
if (verb !== "autotune") return undefined;
|
|
166
|
+
return parseAutotuneArgs(raw.slice(1));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The words after `autotune` (also `/vocal autotune <args>`). */
|
|
170
|
+
export function parseAutotuneArgs(raw: readonly string[]): AutotuneCommand {
|
|
171
|
+
const words = raw.filter((word) => word.length > 0);
|
|
172
|
+
const lower = words.map((word) => word.toLowerCase());
|
|
173
|
+
if (lower.length === 0) return { type: "autotune-show" };
|
|
174
|
+
if (lower.length === 1) {
|
|
175
|
+
const only = lower[0]!;
|
|
176
|
+
if (only === "off" || only === "none" || only === "remove")
|
|
177
|
+
return { type: "autotune-off" };
|
|
178
|
+
if (only === "reset") return { type: "autotune-reset" };
|
|
179
|
+
if (only === "list" || only === "presets") return { type: "autotune-list" };
|
|
180
|
+
if (only === "on") return { type: "autotune-set", values: {} };
|
|
181
|
+
}
|
|
182
|
+
let index = 0;
|
|
183
|
+
let preset: AutotunePreset | undefined;
|
|
184
|
+
if (lower[0] === "preset") {
|
|
185
|
+
if (!isAutotunePreset(lower[1]))
|
|
186
|
+
return {
|
|
187
|
+
type: "autotune-usage",
|
|
188
|
+
message: `preset is one of ${AUTOTUNE_PRESETS.join(" ")}`,
|
|
189
|
+
};
|
|
190
|
+
preset = lower[1];
|
|
191
|
+
index = 2;
|
|
192
|
+
} else if (isAutotunePreset(lower[0])) {
|
|
193
|
+
preset = lower[0];
|
|
194
|
+
index = 1;
|
|
195
|
+
}
|
|
196
|
+
const values: Partial<Record<keyof TrackAutotune, AutotuneValue>> = {};
|
|
197
|
+
while (index < lower.length) {
|
|
198
|
+
const word = lower[index]!;
|
|
199
|
+
const name = fieldName(word);
|
|
200
|
+
if (!name && isAutotunePreset(word))
|
|
201
|
+
return {
|
|
202
|
+
type: "autotune-usage",
|
|
203
|
+
message: preset
|
|
204
|
+
? `one preset at a time (${preset} or ${word})`
|
|
205
|
+
: `the preset comes first · autotune ${word} …`,
|
|
206
|
+
};
|
|
207
|
+
if (!name) {
|
|
208
|
+
const near = nearest(word.slice(0, 24), [
|
|
209
|
+
...FIELD_WORDS,
|
|
210
|
+
...AUTOTUNE_PRESETS,
|
|
211
|
+
]);
|
|
212
|
+
return {
|
|
213
|
+
type: "autotune-usage",
|
|
214
|
+
message: `no field or preset ${word.slice(0, 24)}${near ? ` · did you mean ${near}?` : ""} · ${AUTOTUNE_USAGE}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
index += 1;
|
|
218
|
+
const value = lower[index];
|
|
219
|
+
if (value === undefined)
|
|
220
|
+
return { type: "autotune-usage", message: rangeOf(name) };
|
|
221
|
+
if (OFF.test(value)) {
|
|
222
|
+
values[name] = null;
|
|
223
|
+
index += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (name === "to") {
|
|
227
|
+
const target = value === "melody" || value === "note" ? "notes" : value;
|
|
228
|
+
if (!(AUTOTUNE_TARGETS as readonly string[]).includes(target))
|
|
229
|
+
return { type: "autotune-usage", message: rangeOf(name) };
|
|
230
|
+
values.to = target;
|
|
231
|
+
index += 1;
|
|
232
|
+
// `to notes melody`: a track id (anything that is not a field word).
|
|
233
|
+
const from = words[index];
|
|
234
|
+
if (
|
|
235
|
+
target === "notes" &&
|
|
236
|
+
from !== undefined &&
|
|
237
|
+
!fieldName(from.toLowerCase())
|
|
238
|
+
) {
|
|
239
|
+
values.from = from;
|
|
240
|
+
index += 1;
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (name === "from") {
|
|
245
|
+
values.from = words[index]!;
|
|
246
|
+
index += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (name === "voice") {
|
|
250
|
+
if (!(AUTOTUNE_VOICES as readonly string[]).includes(value))
|
|
251
|
+
return { type: "autotune-usage", message: rangeOf(name) };
|
|
252
|
+
values.voice = value;
|
|
253
|
+
index += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (name === "key") {
|
|
257
|
+
// `key D bayati`, `key A minor`: words up to the next field word.
|
|
258
|
+
const parts: string[] = [];
|
|
259
|
+
while (index < words.length && !fieldName(lower[index]!)) {
|
|
260
|
+
parts.push(words[index]!);
|
|
261
|
+
index += 1;
|
|
262
|
+
}
|
|
263
|
+
const text = parts.join(" ");
|
|
264
|
+
if (!parseKey(text))
|
|
265
|
+
return {
|
|
266
|
+
type: "autotune-usage",
|
|
267
|
+
message: `key ${text.slice(0, 32)} is not a key (A minor, D bayati, C# major)`,
|
|
268
|
+
};
|
|
269
|
+
values.key = text;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const number = fieldNumber(name, value);
|
|
273
|
+
if (typeof number === "string")
|
|
274
|
+
return { type: "autotune-usage", message: number };
|
|
275
|
+
values[name] = number;
|
|
276
|
+
index += 1;
|
|
277
|
+
}
|
|
278
|
+
return preset
|
|
279
|
+
? { type: "autotune-set", preset, values }
|
|
280
|
+
: { type: "autotune-set", values };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** One line per preset, for `autotune presets` and the agent brief. */
|
|
284
|
+
export function autotuneListLines(): string[] {
|
|
285
|
+
return AUTOTUNE_PRESETS.map(
|
|
286
|
+
(name) => `${name.padEnd(8)} ${AUTOTUNE_PRESET_HELP[name]}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The autotune field a command leaves on a track (pure; the menu and the
|
|
292
|
+
* agent tool share it). A preset switch keeps the overrides; `reset`
|
|
293
|
+
* keeps only the preset; a bare `autotune` on a track without it is pop.
|
|
294
|
+
*/
|
|
295
|
+
export function nextAutotune(
|
|
296
|
+
current: TrackAutotune | undefined,
|
|
297
|
+
command: Extract<
|
|
298
|
+
AutotuneCommand,
|
|
299
|
+
{ type: "autotune-reset" | "autotune-set" }
|
|
300
|
+
>,
|
|
301
|
+
instrument?: string,
|
|
302
|
+
): TrackAutotune {
|
|
303
|
+
if (command.type === "autotune-reset")
|
|
304
|
+
return normalizeAutotune({ preset: current?.preset ?? "pop" }, instrument)!;
|
|
305
|
+
const base: Record<string, unknown> = { ...current };
|
|
306
|
+
if (command.preset) base.preset = command.preset;
|
|
307
|
+
for (const [key, value] of Object.entries(command.values)) {
|
|
308
|
+
if (value === null) delete base[key];
|
|
309
|
+
else base[key] = value;
|
|
310
|
+
}
|
|
311
|
+
// A target other than notes drops a stale `from`.
|
|
312
|
+
if (base.to !== undefined && base.to !== "notes") delete base.from;
|
|
313
|
+
if (Object.keys(base).length === 0) base.preset = "pop";
|
|
314
|
+
return normalizeAutotune(base, instrument)!;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `· no guide notes · try autotune guided from lead`. */
|
|
318
|
+
function noGuideHint(
|
|
319
|
+
score: TrackScore,
|
|
320
|
+
trackId: string,
|
|
321
|
+
preset: string | undefined,
|
|
322
|
+
): string {
|
|
323
|
+
const melodic = score.tracks
|
|
324
|
+
.filter(
|
|
325
|
+
(other) =>
|
|
326
|
+
other.id !== trackId &&
|
|
327
|
+
other.instrument !== "kit" &&
|
|
328
|
+
other.kit === undefined &&
|
|
329
|
+
hasGuideNotes(score, other.id),
|
|
330
|
+
)
|
|
331
|
+
.map((other) => other.id)
|
|
332
|
+
.slice(0, 4);
|
|
333
|
+
const word = preset ?? "guided";
|
|
334
|
+
return melodic.length > 0
|
|
335
|
+
? ` · no guide notes on ${trackId} · try autotune ${word} from ${melodic[0]}${melodic.length > 1 ? ` (or ${melodic.slice(1).join(", ")})` : ""}`
|
|
336
|
+
: ` · no guide notes on ${trackId} · write notes on it or autotune ${word} from <track>`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export type AutotuneResult = Readonly<{
|
|
340
|
+
ok: boolean;
|
|
341
|
+
message: string;
|
|
342
|
+
next?: TrackScore;
|
|
343
|
+
kind?: string;
|
|
344
|
+
payload?: Record<string, unknown>;
|
|
345
|
+
}>;
|
|
346
|
+
|
|
347
|
+
export function applyAutotuneCommand(
|
|
348
|
+
score: TrackScore,
|
|
349
|
+
trackId: string,
|
|
350
|
+
command: AutotuneCommand,
|
|
351
|
+
): AutotuneResult {
|
|
352
|
+
if (command.type === "autotune-usage")
|
|
353
|
+
return { ok: false, message: `autotune · ${command.message}` };
|
|
354
|
+
if (command.type === "autotune-list")
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
message: `autotune presets · ${AUTOTUNE_PRESETS.join(" ")}`,
|
|
358
|
+
};
|
|
359
|
+
const track = score.tracks.find((candidate) => candidate.id === trackId);
|
|
360
|
+
if (!track) return { ok: false, message: `no track · ${trackId}` };
|
|
361
|
+
const current = track.autotune;
|
|
362
|
+
if (command.type === "autotune-show" && current)
|
|
363
|
+
return { ok: true, message: `autotune · ${describeAutotune(current)}` };
|
|
364
|
+
if (command.type === "autotune-off") {
|
|
365
|
+
if (!current) return { ok: true, message: "autotune · already off" };
|
|
366
|
+
const next = updateTrack(score, trackId, { autotune: null });
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
message: `autotune · off · /autotune ${current.preset ?? "pop"} turns it back on`,
|
|
370
|
+
next,
|
|
371
|
+
kind: "score.autotune",
|
|
372
|
+
payload: { trackId, autotune: null },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
let autotune: TrackAutotune;
|
|
376
|
+
let next: TrackScore;
|
|
377
|
+
try {
|
|
378
|
+
autotune = nextAutotune(
|
|
379
|
+
current,
|
|
380
|
+
command.type === "autotune-show"
|
|
381
|
+
? { type: "autotune-set", values: {} }
|
|
382
|
+
: command,
|
|
383
|
+
track.instrument,
|
|
384
|
+
);
|
|
385
|
+
next = updateTrack(score, trackId, { autotune });
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (
|
|
388
|
+
error instanceof ScoreValidationError ||
|
|
389
|
+
error instanceof FxValidationError
|
|
390
|
+
)
|
|
391
|
+
return {
|
|
392
|
+
ok: false,
|
|
393
|
+
message: `autotune · ${error.message.replace(/^autotune: /, "")}`,
|
|
394
|
+
};
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
const inert =
|
|
398
|
+
!track.sampler && track.clips === undefined
|
|
399
|
+
? ` · ${trackId} has no audio yet (it tunes clips and sampler voices)`
|
|
400
|
+
: "";
|
|
401
|
+
const keyHint =
|
|
402
|
+
(autotune.to ?? "scale") === "scale" &&
|
|
403
|
+
autotune.key === undefined &&
|
|
404
|
+
!score.key &&
|
|
405
|
+
!(autotune.preset === "guided" || autotune.preset === "locked")
|
|
406
|
+
? " · no song key, so chromatic (/scale or autotune key …)"
|
|
407
|
+
: "";
|
|
408
|
+
const guide =
|
|
409
|
+
resolveAutotune(autotune).to === "notes" &&
|
|
410
|
+
autotune.from === undefined &&
|
|
411
|
+
!hasGuideNotes(score, trackId)
|
|
412
|
+
? noGuideHint(score, trackId, autotune.preset)
|
|
413
|
+
: "";
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
message: `autotune · ${describeAutotune(autotune)}${keyHint}${guide}${inert}${autotuneEngineNote()}`,
|
|
417
|
+
next,
|
|
418
|
+
kind: "score.autotune",
|
|
419
|
+
payload: { trackId, autotune },
|
|
420
|
+
};
|
|
421
|
+
}
|