@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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `style` prompt command: browse the style taxonomy (core/styles) and
|
|
3
|
+
* generate a song from a style card.
|
|
4
|
+
*
|
|
5
|
+
* style the families and their roots
|
|
6
|
+
* style list [id|family] children of a node (or a family's roots)
|
|
7
|
+
* style search <words> ranked name, alias and region search
|
|
8
|
+
* style info <id> one style: path, meter, tempo, pitch, harmony
|
|
9
|
+
* style <id> [bars] [seed] generate the song from a style (replaces it)
|
|
10
|
+
* style blend <a> <b> [w] [bars] [seed]
|
|
11
|
+
* a weighted blend; w 0 is a, 1 is b (0.5)
|
|
12
|
+
* style again the current style with the next seed
|
|
13
|
+
*
|
|
14
|
+
* A generated song replaces the score in one revision (one undo step) and
|
|
15
|
+
* records `style` provenance (id, seed, bars, blend) on the song.
|
|
16
|
+
*/
|
|
17
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
18
|
+
import {
|
|
19
|
+
blendStyles,
|
|
20
|
+
hasCard,
|
|
21
|
+
LEAF_IDS,
|
|
22
|
+
ROOT_IDS,
|
|
23
|
+
resolveStyle,
|
|
24
|
+
searchStyles,
|
|
25
|
+
STYLE_CARDS,
|
|
26
|
+
STYLE_IDS,
|
|
27
|
+
STYLE_TREE,
|
|
28
|
+
styleByName,
|
|
29
|
+
stylePath,
|
|
30
|
+
type StyleNode,
|
|
31
|
+
} from "../../core/styles/index.ts";
|
|
32
|
+
import {
|
|
33
|
+
generateStyle,
|
|
34
|
+
STYLE_LIMITS,
|
|
35
|
+
StyleError,
|
|
36
|
+
styleScore,
|
|
37
|
+
type GeneratedStyle,
|
|
38
|
+
} from "../../core/styles/generate.ts";
|
|
39
|
+
import { STYLE_FAMILIES } from "../../core/styles/taxonomy.ts";
|
|
40
|
+
import type { ResolvedStyle, StyleId } from "../../core/styles/schema.ts";
|
|
41
|
+
import { validateGenerated } from "../../core/styles/validate.ts";
|
|
42
|
+
|
|
43
|
+
export type StyleCommand =
|
|
44
|
+
| { type: "style-families" }
|
|
45
|
+
| { type: "style-list"; id: string }
|
|
46
|
+
| { type: "style-search"; query: string }
|
|
47
|
+
| { type: "style-info"; id: string }
|
|
48
|
+
| { type: "style-again" }
|
|
49
|
+
| {
|
|
50
|
+
type: "style-apply";
|
|
51
|
+
id: string;
|
|
52
|
+
bars?: number;
|
|
53
|
+
seed?: number;
|
|
54
|
+
blend?: Readonly<{ id: string; weight: number }>;
|
|
55
|
+
}
|
|
56
|
+
| { type: "style-bad"; message: string };
|
|
57
|
+
|
|
58
|
+
export type StyleResult = Readonly<{
|
|
59
|
+
ok: boolean;
|
|
60
|
+
message: string;
|
|
61
|
+
next?: TrackScore;
|
|
62
|
+
kind?: string;
|
|
63
|
+
payload?: Record<string, unknown>;
|
|
64
|
+
/** Detail for the ctrl-o log (seed, check counts), not the card. */
|
|
65
|
+
log?: string;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
export const STYLE_USAGE =
|
|
69
|
+
"style [list [id]|search <words>|info <id>|<id> [bars] [seed]|blend <a> <b> [w] [bars] [seed]|again]";
|
|
70
|
+
|
|
71
|
+
const INTEGER = /^\d{1,10}$/;
|
|
72
|
+
const WEIGHT = /^(?:0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,3}%)$/;
|
|
73
|
+
|
|
74
|
+
function bad(message: string): StyleCommand {
|
|
75
|
+
return { type: "style-bad", message };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Trailing `[bars] [seed]` integers; ids may contain digits, so only the tail. */
|
|
79
|
+
function tailNumbers(words: string[]): {
|
|
80
|
+
rest: string[];
|
|
81
|
+
bars?: number;
|
|
82
|
+
seed?: number;
|
|
83
|
+
} {
|
|
84
|
+
const numbers: number[] = [];
|
|
85
|
+
let end = words.length;
|
|
86
|
+
while (end > 1 && numbers.length < 2 && INTEGER.test(words[end - 1]!)) {
|
|
87
|
+
numbers.unshift(Number(words[end - 1]));
|
|
88
|
+
end -= 1;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
rest: words.slice(0, end),
|
|
92
|
+
...(numbers[0] !== undefined ? { bars: numbers[0] } : {}),
|
|
93
|
+
...(numbers[1] !== undefined ? { seed: numbers[1] } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function weightOf(text: string): number {
|
|
98
|
+
return text.endsWith("%") ? Number(text.slice(0, -1)) / 100 : Number(text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseStyleCommand(prompt: string): StyleCommand | undefined {
|
|
102
|
+
if (prompt.length > 512) return undefined;
|
|
103
|
+
const words = prompt.trim().split(/\s+/);
|
|
104
|
+
const head = words[0]?.toLowerCase();
|
|
105
|
+
if (head !== "style" && head !== "/style" && head !== "styles")
|
|
106
|
+
return undefined;
|
|
107
|
+
const rest = words.slice(1);
|
|
108
|
+
if (rest.length === 0) return { type: "style-families" };
|
|
109
|
+
const verb = rest[0]!.toLowerCase();
|
|
110
|
+
if (verb === "list" || verb === "ls" || verb === "tree")
|
|
111
|
+
return rest.length === 1
|
|
112
|
+
? { type: "style-families" }
|
|
113
|
+
: { type: "style-list", id: rest.slice(1).join(" ") };
|
|
114
|
+
if (verb === "search" || verb === "find") {
|
|
115
|
+
const query = rest.slice(1).join(" ").trim();
|
|
116
|
+
return query
|
|
117
|
+
? { type: "style-search", query }
|
|
118
|
+
: bad("style search <words> · style search maqam");
|
|
119
|
+
}
|
|
120
|
+
if (verb === "info" || verb === "show" || verb === "about") {
|
|
121
|
+
const id = rest.slice(1).join(" ").trim();
|
|
122
|
+
return id ? { type: "style-info", id } : bad("style info <id>");
|
|
123
|
+
}
|
|
124
|
+
if (verb === "again" || verb === "next" || verb === "reroll")
|
|
125
|
+
return { type: "style-again" };
|
|
126
|
+
if (verb === "blend" || verb === "mix") {
|
|
127
|
+
// Ids are single words (`deep-house`); then [weight] [bars] [seed].
|
|
128
|
+
const [a, b, ...numbers] = rest.slice(1);
|
|
129
|
+
const usage = bad(
|
|
130
|
+
"style blend <a> <b> [weight 0..1] [bars] [seed] · style blend bebop bossa-nova 0.3",
|
|
131
|
+
);
|
|
132
|
+
if (!a || !b || numbers.length > 3) return usage;
|
|
133
|
+
let weight = 0.5;
|
|
134
|
+
if (numbers[0] !== undefined) {
|
|
135
|
+
if (!WEIGHT.test(numbers[0])) return usage;
|
|
136
|
+
weight = weightOf(numbers[0]);
|
|
137
|
+
if (!(weight >= 0 && weight <= 1)) return usage;
|
|
138
|
+
}
|
|
139
|
+
if (numbers.slice(1).some((word) => !INTEGER.test(word))) return usage;
|
|
140
|
+
return {
|
|
141
|
+
type: "style-apply",
|
|
142
|
+
id: a,
|
|
143
|
+
blend: { id: b, weight },
|
|
144
|
+
...(numbers[1] !== undefined ? { bars: Number(numbers[1]) } : {}),
|
|
145
|
+
...(numbers[2] !== undefined ? { seed: Number(numbers[2]) } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const tail = tailNumbers(rest);
|
|
149
|
+
return {
|
|
150
|
+
type: "style-apply",
|
|
151
|
+
id: tail.rest.join(" "),
|
|
152
|
+
...(tail.bars !== undefined ? { bars: tail.bars } : {}),
|
|
153
|
+
...(tail.seed !== undefined ? { seed: tail.seed } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A taxonomy id from typed text: id, title or alias. */
|
|
158
|
+
export function findStyle(text: string): StyleId | undefined {
|
|
159
|
+
const lowered = text.trim().toLowerCase();
|
|
160
|
+
if (STYLE_TREE.has(lowered)) return lowered;
|
|
161
|
+
return styleByName(text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function unknown(text: string): string {
|
|
165
|
+
const near = searchStyles(text, 4).map((match) => match.id);
|
|
166
|
+
return `style · no style "${text}"${near.length ? ` · try ${near.join(" ")}` : " · style search <words>"}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function childCount(node: StyleNode): string {
|
|
170
|
+
if (node.leaf) return hasCard(node.id) ? "" : " ·";
|
|
171
|
+
return ` (${node.children.length})`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One-line node label: id, a card mark, and its child count. */
|
|
175
|
+
function nodeLabel(node: StyleNode): string {
|
|
176
|
+
return `${node.id}${childCount(node)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function styleFamiliesText(): string {
|
|
180
|
+
const lines = STYLE_FAMILIES.map(
|
|
181
|
+
(family) =>
|
|
182
|
+
`${family.key}: ${family.roots.map((id) => nodeLabel(STYLE_TREE.get(id)!)).join(" ")}`,
|
|
183
|
+
);
|
|
184
|
+
return [
|
|
185
|
+
`styles · ${STYLE_IDS.length} in the tree · ${LEAF_IDS.length} leaves · ${STYLE_CARDS.size} cards`,
|
|
186
|
+
...lines,
|
|
187
|
+
"style list <id> · style search <words> · style info <id> · style <id> [bars] [seed]",
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function styleListText(text: string): StyleResult {
|
|
192
|
+
const family = STYLE_FAMILIES.find((entry) => entry.key === text.trim());
|
|
193
|
+
if (family)
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
message: `${family.title}: ${family.roots.map((id) => nodeLabel(STYLE_TREE.get(id)!)).join(" ")}`,
|
|
197
|
+
};
|
|
198
|
+
const id = findStyle(text);
|
|
199
|
+
if (!id) return { ok: false, message: unknown(text) };
|
|
200
|
+
const node = STYLE_TREE.get(id)!;
|
|
201
|
+
const path = stylePath(id).join(" › ");
|
|
202
|
+
if (node.leaf)
|
|
203
|
+
return { ok: true, message: `${path} · a leaf · style info ${id}` };
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
message: `${path}: ${node.children.map((child) => nodeLabel(STYLE_TREE.get(child)!)).join(" ")}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function styleSearchText(query: string): StyleResult {
|
|
211
|
+
const matches = searchStyles(query, 12);
|
|
212
|
+
if (matches.length === 0)
|
|
213
|
+
return { ok: false, message: `style search · nothing for "${query}"` };
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
message: `style search ${query}: ${matches.map((match) => match.id).join(" ")}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pct = (value: number) => `${Math.round(value * 100)}%`;
|
|
221
|
+
|
|
222
|
+
function weightedText<T>(
|
|
223
|
+
list: readonly (readonly [T, number])[] | undefined,
|
|
224
|
+
show: (value: T) => string = String,
|
|
225
|
+
): string {
|
|
226
|
+
if (!list || list.length === 0) return "-";
|
|
227
|
+
const total = list.reduce((sum, [, weight]) => sum + weight, 0) || 1;
|
|
228
|
+
return [...list]
|
|
229
|
+
.sort((a, b) => b[1] - a[1])
|
|
230
|
+
.slice(0, 4)
|
|
231
|
+
.map(([value, weight]) =>
|
|
232
|
+
list.length === 1 ? show(value) : `${show(value)} ${pct(weight / total)}`,
|
|
233
|
+
)
|
|
234
|
+
.join(", ");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The facts of a resolved style, one per line, for /style info and tools. */
|
|
238
|
+
export function describeStyle(style: ResolvedStyle): string[] {
|
|
239
|
+
const node = STYLE_TREE.get(style.id);
|
|
240
|
+
const roles = Object.entries(style.texture.roles)
|
|
241
|
+
.filter(([, role]) => role)
|
|
242
|
+
.map(([name, role]) => (role!.required ? name : `${name}?`));
|
|
243
|
+
const harmony = [
|
|
244
|
+
style.harmony.model,
|
|
245
|
+
style.harmony.presets
|
|
246
|
+
? `presets ${weightedText(style.harmony.presets)}`
|
|
247
|
+
: "",
|
|
248
|
+
style.harmony.forms ? `forms ${style.harmony.forms.length}` : "",
|
|
249
|
+
style.harmony.chain ? "numeral chain" : "",
|
|
250
|
+
]
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.join(" · ");
|
|
253
|
+
const era = node
|
|
254
|
+
? node.era.from === null
|
|
255
|
+
? ""
|
|
256
|
+
: `${node.era.from}${node.era.to === null ? "-" : `-${node.era.to}`}`
|
|
257
|
+
: "";
|
|
258
|
+
return [
|
|
259
|
+
`${style.title} (${style.id})${era ? ` · ${era}` : ""}${node?.region.length ? ` · ${node.region.join(", ")}` : ""}`,
|
|
260
|
+
`path ${stylePath(style.id).join(" › ")} · cards ${style.lineage.join(" › ") || "base"}`,
|
|
261
|
+
style.summary,
|
|
262
|
+
`meter ${weightedText(style.meter.signatures)}${style.meter.cycle ? ` · cycle ${style.meter.cycle.name}` : ""} · tempo ${style.tempo.bpm[0]}-${style.tempo.bpm[1]} (${style.tempo.typical}) BPM`,
|
|
263
|
+
`groove ${style.groove.subdivision}/beat · swing ${style.groove.swingRatio[0] === style.groove.swingRatio[1] ? style.groove.swingRatio[0] : style.groove.swingRatio.join("-")}`,
|
|
264
|
+
`pitch ${style.pitch.tuning ?? "12-TET"} · ${weightedText(style.pitch.scales)}`,
|
|
265
|
+
`harmony ${harmony}`,
|
|
266
|
+
`texture ${style.texture.kind} · ${roles.join(" ")}`,
|
|
267
|
+
].filter((line) => line.trim().length > 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function styleInfoText(text: string): StyleResult {
|
|
271
|
+
const id = findStyle(text);
|
|
272
|
+
if (!id) return { ok: false, message: unknown(text) };
|
|
273
|
+
const lines = describeStyle(resolveStyle(id));
|
|
274
|
+
const node = STYLE_TREE.get(id)!;
|
|
275
|
+
if (!node.leaf) lines.push(`children ${node.children.join(" ")}`);
|
|
276
|
+
if (!hasCard(id) && STYLE_CARDS.size > 0)
|
|
277
|
+
lines.push("no card of its own yet: plays its nearest ancestor's card");
|
|
278
|
+
return { ok: true, message: lines.join("\n") };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Generate a song for a resolved command; throws StyleError on bad input. */
|
|
282
|
+
export function generateFromCommand(
|
|
283
|
+
command: Extract<StyleCommand, { type: "style-apply" }>,
|
|
284
|
+
): GeneratedStyle {
|
|
285
|
+
const id = findStyle(command.id);
|
|
286
|
+
if (!id) throw new StyleError(unknown(command.id));
|
|
287
|
+
let style: ResolvedStyle = resolveStyle(id);
|
|
288
|
+
if (command.blend) {
|
|
289
|
+
const other = findStyle(command.blend.id);
|
|
290
|
+
if (!other) throw new StyleError(unknown(command.blend.id));
|
|
291
|
+
style = blendStyles(style, resolveStyle(other), command.blend.weight);
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
command.bars !== undefined &&
|
|
295
|
+
(command.bars < STYLE_LIMITS.minBars || command.bars > STYLE_LIMITS.maxBars)
|
|
296
|
+
)
|
|
297
|
+
throw new StyleError(
|
|
298
|
+
`style · bars take ${STYLE_LIMITS.minBars}..${STYLE_LIMITS.maxBars}`,
|
|
299
|
+
);
|
|
300
|
+
if (command.seed !== undefined && command.seed > STYLE_LIMITS.maxSeed)
|
|
301
|
+
throw new StyleError(`style · seed takes 0..${STYLE_LIMITS.maxSeed}`);
|
|
302
|
+
return generateStyle(style, {
|
|
303
|
+
...(command.bars !== undefined ? { bars: command.bars } : {}),
|
|
304
|
+
...(command.seed !== undefined ? { seed: command.seed } : {}),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** One line about a generated song. */
|
|
309
|
+
export function generatedSummary(generated: GeneratedStyle): string {
|
|
310
|
+
const { plan, provenance } = generated;
|
|
311
|
+
const name = provenance.blend
|
|
312
|
+
? `${provenance.id} + ${provenance.blend.id} ${pct(provenance.blend.weight)}`
|
|
313
|
+
: provenance.id;
|
|
314
|
+
const report = validateGenerated(generated);
|
|
315
|
+
const failed = report.checks.filter((check) => !check.ok);
|
|
316
|
+
return [
|
|
317
|
+
`style ${name} · seed ${plan.seed} · ${plan.bars} bars`,
|
|
318
|
+
`${plan.signature} ${Math.round(plan.bpm)} BPM`,
|
|
319
|
+
plan.keyText ?? plan.tuning?.name ?? "",
|
|
320
|
+
`${plan.tracks.length} tracks`,
|
|
321
|
+
failed.length === 0
|
|
322
|
+
? `${report.checks.length} checks pass`
|
|
323
|
+
: `${failed.length} of ${report.checks.length} checks off: ${failed.map((check) => check.name).join(" ")}`,
|
|
324
|
+
]
|
|
325
|
+
.filter(Boolean)
|
|
326
|
+
.join(" · ");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The card after a style lands, in musical terms: what the song now is.
|
|
331
|
+
* `lofi-hip-hop · F minor · 84 BPM · 8 bars · 4 tracks`.
|
|
332
|
+
*/
|
|
333
|
+
export function styleReceipt(generated: GeneratedStyle): string {
|
|
334
|
+
const { plan, provenance } = generated;
|
|
335
|
+
const name = provenance.blend
|
|
336
|
+
? `${provenance.id} + ${provenance.blend.id} ${pct(provenance.blend.weight)}`
|
|
337
|
+
: provenance.id;
|
|
338
|
+
return [
|
|
339
|
+
name,
|
|
340
|
+
plan.keyText ?? plan.tuning?.name ?? "",
|
|
341
|
+
`${Math.round(plan.bpm)} BPM`,
|
|
342
|
+
`${plan.bars} bars`,
|
|
343
|
+
`${plan.tracks.length} tracks`,
|
|
344
|
+
]
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
.join(" · ");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Seed, meter and the generator's checks, for the ctrl-o log. */
|
|
350
|
+
export function styleCheckLine(generated: GeneratedStyle): string {
|
|
351
|
+
const { plan } = generated;
|
|
352
|
+
const report = validateGenerated(generated);
|
|
353
|
+
const failed = report.checks.filter((check) => !check.ok);
|
|
354
|
+
return [
|
|
355
|
+
`style · seed ${plan.seed} · ${plan.signature}`,
|
|
356
|
+
failed.length === 0
|
|
357
|
+
? `${report.checks.length} checks pass`
|
|
358
|
+
: `${failed.length} of ${report.checks.length} checks off: ${failed.map((check) => check.name).join(" ")}`,
|
|
359
|
+
].join(" · ");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function applyStyleCommand(
|
|
363
|
+
score: TrackScore,
|
|
364
|
+
command: StyleCommand,
|
|
365
|
+
): StyleResult {
|
|
366
|
+
switch (command.type) {
|
|
367
|
+
case "style-bad":
|
|
368
|
+
return { ok: false, message: command.message };
|
|
369
|
+
case "style-families":
|
|
370
|
+
return { ok: true, message: styleFamiliesText() };
|
|
371
|
+
case "style-list":
|
|
372
|
+
return styleListText(command.id);
|
|
373
|
+
case "style-search":
|
|
374
|
+
return styleSearchText(command.query);
|
|
375
|
+
case "style-info":
|
|
376
|
+
return styleInfoText(command.id);
|
|
377
|
+
case "style-again": {
|
|
378
|
+
const current = score.style;
|
|
379
|
+
if (!current)
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
message: "style again · this song has no style yet · style <id>",
|
|
383
|
+
};
|
|
384
|
+
return applyStyleCommand(score, {
|
|
385
|
+
type: "style-apply",
|
|
386
|
+
id: current.id,
|
|
387
|
+
bars: Math.min(current.bars, STYLE_LIMITS.maxBars),
|
|
388
|
+
seed: (current.seed + 1) % (STYLE_LIMITS.maxSeed + 1),
|
|
389
|
+
...(current.blend ? { blend: current.blend } : {}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
case "style-apply": {
|
|
393
|
+
if (!command.id) return { ok: false, message: `usage: ${STYLE_USAGE}` };
|
|
394
|
+
let generated: GeneratedStyle;
|
|
395
|
+
try {
|
|
396
|
+
generated = generateFromCommand(command);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof StyleError)
|
|
399
|
+
return { ok: false, message: error.message };
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
message: `${styleReceipt(generated)} · ctrl-z undo`,
|
|
405
|
+
log: styleCheckLine(generated),
|
|
406
|
+
next: styleScore(generated),
|
|
407
|
+
kind: "style.apply",
|
|
408
|
+
payload: { style: generated.provenance },
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Taxonomy order of root ids, for the browser. */
|
|
415
|
+
export const STYLE_BROWSER_ROOTS: readonly StyleId[] = ROOT_IDS;
|
package/src/commands/time.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* meter remove bar <n> · meter clear
|
|
14
14
|
* track rate <0.125..8>|<a>/<b>|off polytempo: the track's tempo ratio
|
|
15
15
|
* track phase <beats>|off start the track later
|
|
16
|
-
* track
|
|
16
|
+
* track loop <beats>|off polymeter: loop the first beats (alias cycle)
|
|
17
17
|
* track phasing <cycle> [over <beats>] [cycles <n>] Reich-style drift
|
|
18
18
|
* track phasing <cycle> [hold <n>] [drift <n>] [shift <beats>]
|
|
19
19
|
* stepped, as in Piano Phase
|
|
@@ -323,7 +323,10 @@ function meterCommand(rest: readonly string[]): TimeCommand | undefined {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
function trackTimeCommand(rest: readonly string[]): TimeCommand | undefined {
|
|
326
|
-
|
|
326
|
+
// `track loop` is the canonical word for a track's own repeat length;
|
|
327
|
+
// `track cycle` stays as its alias.
|
|
328
|
+
const [word, value, ...tail] = rest;
|
|
329
|
+
const field = word === "loop" ? "cycle" : word;
|
|
327
330
|
if (field === "time")
|
|
328
331
|
return rest.length === 2 && (value === "off" || value === "reset")
|
|
329
332
|
? { type: "track-time-off" }
|
|
@@ -806,7 +809,7 @@ function trackTime(
|
|
|
806
809
|
} else {
|
|
807
810
|
const ticks = Math.round(command.value * tpb);
|
|
808
811
|
if (command.field === "cycle" && ticks < 1)
|
|
809
|
-
return { ok: false, message: "track
|
|
812
|
+
return { ok: false, message: "track loop · too short" };
|
|
810
813
|
current[command.field] = ticks;
|
|
811
814
|
message = `${trackId} · ${command.field} ${fmt(command.value)} beat${command.value === 1 ? "" : "s"}`;
|
|
812
815
|
if (command.field === "cycle" && loopTicksOf(score) % ticks !== 0) {
|
package/src/commands/tuning.ts
CHANGED
|
@@ -384,11 +384,11 @@ export function tuningSummary(score: TrackScore, trackId: string): string {
|
|
|
384
384
|
return `tuning · ${song} · ${trackId} ${own}`;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
/** `
|
|
387
|
+
/** `key · D bayati · 0 1.5 3 5 7 8 10` (key is the canonical verb). */
|
|
388
388
|
export function scaleSummary(key: string | null | undefined): string {
|
|
389
389
|
const parsed = parseKey(key ?? undefined);
|
|
390
|
-
if (!parsed) return "
|
|
391
|
-
return `
|
|
390
|
+
if (!parsed) return "key · none (C major assumed) · key <tonic> <scale>";
|
|
391
|
+
return `key · ${keyName(parsed)} · ${scaleSteps(parsed).map(formatStep).join(" ")}`;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
function formatStep(step: number): string {
|