@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
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The style registry (quality-08): the taxonomy tree, the cards of every
|
|
3
|
+
* family file, inheritance (`resolveStyle`), name lookup (`styleByName`,
|
|
4
|
+
* `searchStyles`) and blending (`blendStyles`).
|
|
5
|
+
*
|
|
6
|
+
* Every taxonomy id resolves, card or not: `resolveStyle` folds BASE_STYLE
|
|
7
|
+
* and whichever cards exist on the path root..id. A family lane adds a leaf
|
|
8
|
+
* by appending a `card({...})` to its family file; nothing here changes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BASE_STYLE } from "./base.ts";
|
|
12
|
+
import { AFRICA_MENA_SOUTHASIA_CARDS } from "./africa-mena-southasia.ts";
|
|
13
|
+
import { AMERICAS_CARDS } from "./americas.ts";
|
|
14
|
+
import { ART_CARDS } from "./art.ts";
|
|
15
|
+
import { ELECTRONIC_CARDS } from "./electronic.ts";
|
|
16
|
+
import { EUROPE_ASIA_PACIFIC_CARDS } from "./europe-asia-pacific.ts";
|
|
17
|
+
import { POP_CARDS } from "./pop.ts";
|
|
18
|
+
import { ROCK_CARDS } from "./rock.ts";
|
|
19
|
+
import { ROOTS_CARDS } from "./roots.ts";
|
|
20
|
+
import type {
|
|
21
|
+
ResolvedStyle,
|
|
22
|
+
StyleBody,
|
|
23
|
+
StyleCard,
|
|
24
|
+
StyleId,
|
|
25
|
+
Weighted,
|
|
26
|
+
} from "./schema.ts";
|
|
27
|
+
import {
|
|
28
|
+
STYLE_FAMILIES,
|
|
29
|
+
TAXONOMY_ROWS,
|
|
30
|
+
type StyleFamilyKey,
|
|
31
|
+
} from "./taxonomy.ts";
|
|
32
|
+
|
|
33
|
+
export * from "./schema.ts";
|
|
34
|
+
export { BASE_STYLE } from "./base.ts";
|
|
35
|
+
export { STYLE_FAMILIES, type StyleFamilyKey } from "./taxonomy.ts";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Taxonomy
|
|
39
|
+
|
|
40
|
+
/** One taxonomy entry with its tree links. */
|
|
41
|
+
export type StyleNode = Readonly<{
|
|
42
|
+
id: StyleId;
|
|
43
|
+
parent: StyleId | null;
|
|
44
|
+
family: StyleFamilyKey;
|
|
45
|
+
title: string;
|
|
46
|
+
aliases: readonly string[];
|
|
47
|
+
region: readonly string[];
|
|
48
|
+
era: Readonly<{ from: number | null; to: number | null }>;
|
|
49
|
+
children: readonly StyleId[];
|
|
50
|
+
/** True when no row names this id as its parent. */
|
|
51
|
+
leaf: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
function buildTree(): ReadonlyMap<StyleId, StyleNode> {
|
|
55
|
+
const children = new Map<StyleId, StyleId[]>();
|
|
56
|
+
for (const [id, parent] of TAXONOMY_ROWS) {
|
|
57
|
+
if (parent === null) continue;
|
|
58
|
+
const list = children.get(parent) ?? [];
|
|
59
|
+
list.push(id);
|
|
60
|
+
children.set(parent, list);
|
|
61
|
+
}
|
|
62
|
+
const out = new Map<StyleId, StyleNode>();
|
|
63
|
+
for (const [
|
|
64
|
+
id,
|
|
65
|
+
parent,
|
|
66
|
+
family,
|
|
67
|
+
title,
|
|
68
|
+
aliases,
|
|
69
|
+
region,
|
|
70
|
+
from,
|
|
71
|
+
to,
|
|
72
|
+
] of TAXONOMY_ROWS) {
|
|
73
|
+
const kids = children.get(id) ?? [];
|
|
74
|
+
out.set(
|
|
75
|
+
id,
|
|
76
|
+
Object.freeze({
|
|
77
|
+
id,
|
|
78
|
+
parent,
|
|
79
|
+
family,
|
|
80
|
+
title,
|
|
81
|
+
aliases,
|
|
82
|
+
region,
|
|
83
|
+
era: Object.freeze({ from, to }),
|
|
84
|
+
children: Object.freeze(kids),
|
|
85
|
+
leaf: kids.length === 0,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every taxonomy entry by id, in taxonomy order. */
|
|
93
|
+
export const STYLE_TREE: ReadonlyMap<StyleId, StyleNode> = buildTree();
|
|
94
|
+
/** Every taxonomy id in taxonomy order. */
|
|
95
|
+
export const STYLE_IDS: readonly StyleId[] = Object.freeze([
|
|
96
|
+
...STYLE_TREE.keys(),
|
|
97
|
+
]);
|
|
98
|
+
/** Leaf ids only. */
|
|
99
|
+
export const LEAF_IDS: readonly StyleId[] = Object.freeze(
|
|
100
|
+
STYLE_IDS.filter((id) => STYLE_TREE.get(id)!.leaf),
|
|
101
|
+
);
|
|
102
|
+
/** Family roots in family order (`jazz`, `blues`, ... `oceania`). */
|
|
103
|
+
export const ROOT_IDS: readonly StyleId[] = Object.freeze(
|
|
104
|
+
STYLE_FAMILIES.flatMap((family) => family.roots),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export function styleNode(id: StyleId): StyleNode | undefined {
|
|
108
|
+
return STYLE_TREE.get(id);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Root..id path of taxonomy ids (empty for an unknown id). */
|
|
112
|
+
export function stylePath(id: StyleId): StyleId[] {
|
|
113
|
+
const path: StyleId[] = [];
|
|
114
|
+
let node = STYLE_TREE.get(id);
|
|
115
|
+
while (node) {
|
|
116
|
+
path.unshift(node.id);
|
|
117
|
+
node = node.parent === null ? undefined : STYLE_TREE.get(node.parent);
|
|
118
|
+
}
|
|
119
|
+
return path;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Cards
|
|
124
|
+
|
|
125
|
+
/** Every card from the family files, by id. */
|
|
126
|
+
export const STYLE_CARDS: ReadonlyMap<StyleId, StyleCard> = (() => {
|
|
127
|
+
const out = new Map<StyleId, StyleCard>();
|
|
128
|
+
for (const list of [
|
|
129
|
+
ART_CARDS,
|
|
130
|
+
ROOTS_CARDS,
|
|
131
|
+
ROCK_CARDS,
|
|
132
|
+
POP_CARDS,
|
|
133
|
+
ELECTRONIC_CARDS,
|
|
134
|
+
AMERICAS_CARDS,
|
|
135
|
+
AFRICA_MENA_SOUTHASIA_CARDS,
|
|
136
|
+
EUROPE_ASIA_PACIFIC_CARDS,
|
|
137
|
+
])
|
|
138
|
+
for (const card of list) {
|
|
139
|
+
if (!STYLE_TREE.has(card.id))
|
|
140
|
+
throw new Error(`style card "${card.id}" is not a taxonomy id`);
|
|
141
|
+
if (out.has(card.id))
|
|
142
|
+
throw new Error(`style card "${card.id}" is defined twice`);
|
|
143
|
+
out.set(card.id, card);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
})();
|
|
147
|
+
|
|
148
|
+
/** True when `id` has a card of its own (not just inherited defaults). */
|
|
149
|
+
export function hasCard(id: StyleId): boolean {
|
|
150
|
+
return STYLE_CARDS.has(id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Merge
|
|
155
|
+
|
|
156
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
157
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isAppend(value: unknown): value is { "+": readonly unknown[] } {
|
|
161
|
+
return (
|
|
162
|
+
isRecord(value) &&
|
|
163
|
+
Object.keys(value).length === 1 &&
|
|
164
|
+
Array.isArray(value["+"])
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** `[value, weight]` pairs. */
|
|
169
|
+
function isWeightedList(value: unknown): value is Weighted<unknown> {
|
|
170
|
+
return (
|
|
171
|
+
Array.isArray(value) &&
|
|
172
|
+
value.length > 0 &&
|
|
173
|
+
value.every(
|
|
174
|
+
(entry) =>
|
|
175
|
+
Array.isArray(entry) &&
|
|
176
|
+
entry.length === 2 &&
|
|
177
|
+
typeof entry[1] === "number",
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const same = (a: unknown, b: unknown) =>
|
|
183
|
+
JSON.stringify(a) === JSON.stringify(b);
|
|
184
|
+
|
|
185
|
+
/** Append a weighted list: an existing value takes the new weight. */
|
|
186
|
+
function appendWeighted(
|
|
187
|
+
base: unknown,
|
|
188
|
+
extra: readonly unknown[],
|
|
189
|
+
): readonly unknown[] {
|
|
190
|
+
const out: (readonly [unknown, number])[] = isWeightedList(base)
|
|
191
|
+
? base.map((entry) => [entry[0], entry[1]] as const)
|
|
192
|
+
: [];
|
|
193
|
+
for (const entry of extra as Weighted<unknown>) {
|
|
194
|
+
const at = out.findIndex((item) => same(item[0], entry[0]));
|
|
195
|
+
if (at >= 0) out[at] = [entry[0], entry[1]];
|
|
196
|
+
else out.push([entry[0], entry[1]]);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Apply one card patch to a body: records merge per key and `null` deletes
|
|
203
|
+
* a key; arrays, ranges and scalars replace; `{ "+": list }` appends to a
|
|
204
|
+
* weighted list.
|
|
205
|
+
*/
|
|
206
|
+
export function mergePatch(base: unknown, patch: unknown): unknown {
|
|
207
|
+
if (patch === undefined) return base;
|
|
208
|
+
if (isAppend(patch)) return appendWeighted(base, patch["+"]);
|
|
209
|
+
if (isRecord(patch) && isRecord(base)) {
|
|
210
|
+
const out: Record<string, unknown> = { ...base };
|
|
211
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
212
|
+
if (value === null) delete out[key];
|
|
213
|
+
else if (value !== undefined) out[key] = mergePatch(base[key], value);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
if (isRecord(patch)) return mergePatch({}, patch);
|
|
218
|
+
return patch;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function deepFreeze<T>(value: T): T {
|
|
222
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
223
|
+
Object.freeze(value);
|
|
224
|
+
for (const inner of Object.values(value)) deepFreeze(inner);
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Merge card patches over BASE_STYLE in order. */
|
|
230
|
+
export function mergeCards(cards: readonly StyleCard[]): StyleBody {
|
|
231
|
+
let body: unknown = BASE_STYLE;
|
|
232
|
+
for (const card of cards) {
|
|
233
|
+
const { id: _id, abstract: _abstract, ...patch } = card;
|
|
234
|
+
body = mergePatch(body, patch);
|
|
235
|
+
}
|
|
236
|
+
return body as StyleBody;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const resolved = new Map<StyleId, ResolvedStyle>();
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The resolved style for a taxonomy id: BASE_STYLE, then the card of each
|
|
243
|
+
* id on the path root..id that has one. Frozen and cached. Throws on an
|
|
244
|
+
* unknown id (use `styleByName` for user text).
|
|
245
|
+
*/
|
|
246
|
+
export function resolveStyle(id: StyleId): ResolvedStyle {
|
|
247
|
+
const hit = resolved.get(id);
|
|
248
|
+
if (hit) return hit;
|
|
249
|
+
const node = STYLE_TREE.get(id);
|
|
250
|
+
if (!node) throw new Error(`unknown style "${id}"`);
|
|
251
|
+
const path = stylePath(id);
|
|
252
|
+
const cards = path.flatMap((step) => {
|
|
253
|
+
const card = STYLE_CARDS.get(step);
|
|
254
|
+
return card ? [card] : [];
|
|
255
|
+
});
|
|
256
|
+
const body = mergeCards(cards);
|
|
257
|
+
const style = deepFreeze({
|
|
258
|
+
...body,
|
|
259
|
+
id,
|
|
260
|
+
title: node.title,
|
|
261
|
+
lineage: cards.map((card) => card.id),
|
|
262
|
+
}) as ResolvedStyle;
|
|
263
|
+
resolved.set(id, style);
|
|
264
|
+
return style;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Names
|
|
269
|
+
|
|
270
|
+
/** Lowercase, hyphens and underscores as spaces, accents folded. */
|
|
271
|
+
export function normalizeStyleName(text: string): string {
|
|
272
|
+
return text
|
|
273
|
+
.normalize("NFKD")
|
|
274
|
+
.replace(/[̀-ͯ]/g, "")
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.replace(/[_-]+/g, " ")
|
|
277
|
+
.replace(/[^a-z0-9&'+/ ]+/g, " ")
|
|
278
|
+
.replace(/\s+/g, " ")
|
|
279
|
+
.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const NAME_INDEX: ReadonlyMap<string, StyleId> = (() => {
|
|
283
|
+
const out = new Map<string, StyleId>();
|
|
284
|
+
// Ids first, then titles, then aliases: an earlier kind wins a clash.
|
|
285
|
+
for (const node of STYLE_TREE.values())
|
|
286
|
+
out.set(normalizeStyleName(node.id), node.id);
|
|
287
|
+
for (const node of STYLE_TREE.values()) {
|
|
288
|
+
const key = normalizeStyleName(node.title);
|
|
289
|
+
if (!out.has(key)) out.set(key, node.id);
|
|
290
|
+
}
|
|
291
|
+
for (const node of STYLE_TREE.values())
|
|
292
|
+
for (const alias of node.aliases) {
|
|
293
|
+
const key = normalizeStyleName(alias);
|
|
294
|
+
if (!out.has(key)) out.set(key, node.id);
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
})();
|
|
298
|
+
|
|
299
|
+
/** The taxonomy id an id, title or alias names (case and accents ignored). */
|
|
300
|
+
export function styleByName(text: string): StyleId | undefined {
|
|
301
|
+
if (typeof text !== "string" || text.length > 120) return undefined;
|
|
302
|
+
return NAME_INDEX.get(normalizeStyleName(text));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type StyleMatch = Readonly<{ id: StyleId; score: number; via: string }>;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Ranked lexical search over ids, titles, aliases, regions and summaries:
|
|
309
|
+
* exact names first, then prefix, word and substring matches. Cheap and
|
|
310
|
+
* offline, so the agent and the menu can call it on every keystroke.
|
|
311
|
+
*/
|
|
312
|
+
export function searchStyles(query: string, limit = 20): StyleMatch[] {
|
|
313
|
+
const q = normalizeStyleName(query);
|
|
314
|
+
if (!q) return [];
|
|
315
|
+
const words = q.split(" ");
|
|
316
|
+
const out: StyleMatch[] = [];
|
|
317
|
+
for (const node of STYLE_TREE.values()) {
|
|
318
|
+
const names = [node.id, node.title, ...node.aliases].map(
|
|
319
|
+
normalizeStyleName,
|
|
320
|
+
);
|
|
321
|
+
let best = 0;
|
|
322
|
+
let via = "";
|
|
323
|
+
for (const name of names) {
|
|
324
|
+
let score = 0;
|
|
325
|
+
if (name === q) score = 100;
|
|
326
|
+
else if (name.startsWith(q)) score = 60;
|
|
327
|
+
else if (` ${name} `.includes(` ${q} `)) score = 45;
|
|
328
|
+
else if (name.includes(q)) score = 30;
|
|
329
|
+
else if (words.length > 1 && words.every((word) => name.includes(word)))
|
|
330
|
+
score = 25;
|
|
331
|
+
if (score > best) {
|
|
332
|
+
best = score;
|
|
333
|
+
via = name;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (best === 0) {
|
|
337
|
+
const extra = [...node.region, STYLE_CARDS.get(node.id)?.summary ?? ""]
|
|
338
|
+
.map(normalizeStyleName)
|
|
339
|
+
.join(" ");
|
|
340
|
+
if (words.every((word) => extra.includes(word))) {
|
|
341
|
+
best = 10;
|
|
342
|
+
via = "summary";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (best > 0)
|
|
346
|
+
out.push({ id: node.id, score: best + (node.leaf ? 1 : 0), via });
|
|
347
|
+
}
|
|
348
|
+
return out
|
|
349
|
+
.sort(
|
|
350
|
+
(a, b) =>
|
|
351
|
+
b.score - a.score || STYLE_IDS.indexOf(a.id) - STYLE_IDS.indexOf(b.id),
|
|
352
|
+
)
|
|
353
|
+
.slice(0, Math.max(0, limit));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Blend
|
|
358
|
+
|
|
359
|
+
/** Keys whose values are categorical: the heavier side wins whole. */
|
|
360
|
+
const DISCRETE = new Set([
|
|
361
|
+
"meter",
|
|
362
|
+
"summary",
|
|
363
|
+
"seedSalt",
|
|
364
|
+
"subdivision",
|
|
365
|
+
"tuning",
|
|
366
|
+
"degrees",
|
|
367
|
+
"tonic",
|
|
368
|
+
"maqam",
|
|
369
|
+
"raga",
|
|
370
|
+
"model",
|
|
371
|
+
"kind",
|
|
372
|
+
"archetype",
|
|
373
|
+
"fills",
|
|
374
|
+
"locks",
|
|
375
|
+
"walk",
|
|
376
|
+
"instrument",
|
|
377
|
+
"voices",
|
|
378
|
+
"loudness",
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
const lerp = (a: number, b: number, w: number) => a + (b - a) * w;
|
|
382
|
+
|
|
383
|
+
/** Stretch a probability grid to `size` steps (nearest step). */
|
|
384
|
+
export function stretchGrid(grid: readonly number[], size: number): number[] {
|
|
385
|
+
if (grid.length === size || grid.length === 0) return [...grid];
|
|
386
|
+
const out = new Array<number>(size).fill(0);
|
|
387
|
+
for (let i = 0; i < grid.length; i += 1) {
|
|
388
|
+
const at = Math.round((i * size) / grid.length);
|
|
389
|
+
if (at < size) out[at] = Math.max(out[at]!, grid[i]!);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* A role's onset grid for one bar of a plan. An aksak hand drum (perc
|
|
396
|
+
* under a beat grouping) whose grid was written for another bar length
|
|
397
|
+
* strikes the group starts instead: stretching would smear 2+2+3.
|
|
398
|
+
*/
|
|
399
|
+
export function roleGrid(
|
|
400
|
+
grid: readonly number[] | undefined,
|
|
401
|
+
role: string,
|
|
402
|
+
plan: {
|
|
403
|
+
stepsPerBar: number;
|
|
404
|
+
beatsPerBar: number;
|
|
405
|
+
grouping?: readonly number[] | undefined;
|
|
406
|
+
},
|
|
407
|
+
): number[] | undefined {
|
|
408
|
+
if (!grid || !grid.length) return undefined;
|
|
409
|
+
if (role === "perc" && plan.grouping && grid.length !== plan.stepsPerBar) {
|
|
410
|
+
const out = new Array<number>(plan.stepsPerBar).fill(0);
|
|
411
|
+
const stepsPerUnit = plan.stepsPerBar / plan.beatsPerBar;
|
|
412
|
+
let at = 0;
|
|
413
|
+
for (const group of plan.grouping) {
|
|
414
|
+
out[Math.round(at * stepsPerUnit) % plan.stepsPerBar] = 1;
|
|
415
|
+
at += group;
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
return stretchGrid(grid, plan.stepsPerBar);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function blendValue(a: unknown, b: unknown, w: number, key: string): unknown {
|
|
423
|
+
if (a === undefined) return w >= 0.5 || key === "onsets-role" ? b : undefined;
|
|
424
|
+
if (b === undefined) return w < 0.5 || key === "onsets-role" ? a : undefined;
|
|
425
|
+
if (DISCRETE.has(key)) return w < 0.5 ? a : b;
|
|
426
|
+
if (typeof a === "number" && typeof b === "number") return lerp(a, b, w);
|
|
427
|
+
if (isWeightedList(a) && isWeightedList(b)) {
|
|
428
|
+
const out: [unknown, number][] = a.map((entry) => [
|
|
429
|
+
entry[0],
|
|
430
|
+
entry[1] * (1 - w),
|
|
431
|
+
]);
|
|
432
|
+
for (const entry of b) {
|
|
433
|
+
const at = out.findIndex((item) => same(item[0], entry[0]));
|
|
434
|
+
if (at >= 0) out[at]![1] += entry[1] * w;
|
|
435
|
+
else out.push([entry[0], entry[1] * w]);
|
|
436
|
+
}
|
|
437
|
+
return out.filter((entry) => entry[1] > 0);
|
|
438
|
+
}
|
|
439
|
+
if (
|
|
440
|
+
Array.isArray(a) &&
|
|
441
|
+
Array.isArray(b) &&
|
|
442
|
+
a.every((x) => typeof x === "number") &&
|
|
443
|
+
b.every((x) => typeof x === "number")
|
|
444
|
+
) {
|
|
445
|
+
const bb = stretchGrid(b as number[], a.length);
|
|
446
|
+
return (a as number[]).map((x, i) => lerp(x, bb[i]!, w));
|
|
447
|
+
}
|
|
448
|
+
if (isRecord(a) && isRecord(b)) {
|
|
449
|
+
const out: Record<string, unknown> = {};
|
|
450
|
+
for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
451
|
+
// A role grid on one side only fades in by its weight.
|
|
452
|
+
const childKey = key === "onsets" ? "onsets-role" : k;
|
|
453
|
+
let value: unknown;
|
|
454
|
+
if (key === "onsets") {
|
|
455
|
+
const left = a[k] as number[] | undefined;
|
|
456
|
+
const right = b[k] as number[] | undefined;
|
|
457
|
+
const size = (left ?? right)!.length;
|
|
458
|
+
value = blendValue(
|
|
459
|
+
left ?? new Array<number>(size).fill(0),
|
|
460
|
+
right ? stretchGrid(right, size) : new Array<number>(size).fill(0),
|
|
461
|
+
w,
|
|
462
|
+
childKey,
|
|
463
|
+
);
|
|
464
|
+
} else value = blendValue(a[k], b[k], w, childKey);
|
|
465
|
+
if (value !== undefined) out[k] = value;
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
return w < 0.5 ? a : b;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* A weighted merge of two styles: `weight` 0 is `a`, 1 is `b`. Numbers,
|
|
474
|
+
* ranges and same-shape grids interpolate; weighted lists pool their
|
|
475
|
+
* weights; meter, tuning, harmony model and instrument choices come whole
|
|
476
|
+
* from the heavier side. When the two use different tunings or degree
|
|
477
|
+
* sets the heavier side's pitch system comes whole, so the result is
|
|
478
|
+
* always playable.
|
|
479
|
+
*/
|
|
480
|
+
export function blendStyles(
|
|
481
|
+
a: ResolvedStyle,
|
|
482
|
+
b: ResolvedStyle,
|
|
483
|
+
weight: number,
|
|
484
|
+
): ResolvedStyle {
|
|
485
|
+
const w = Math.min(1, Math.max(0, Number.isFinite(weight) ? weight : 0.5));
|
|
486
|
+
const { id: _a, title: titleA, lineage: lineageA, ...bodyA } = a;
|
|
487
|
+
const { id: _b, title: titleB, lineage: lineageB, ...bodyB } = b;
|
|
488
|
+
const body = blendValue(bodyA, bodyB, w, "") as Record<string, unknown>;
|
|
489
|
+
const heavy = w < 0.5 ? bodyA : bodyB;
|
|
490
|
+
// Pitch systems with different tunings or degree sets do not mix.
|
|
491
|
+
if (
|
|
492
|
+
!same(bodyA.pitch.tuning, bodyB.pitch.tuning) ||
|
|
493
|
+
bodyA.pitch.degrees ||
|
|
494
|
+
bodyB.pitch.degrees
|
|
495
|
+
)
|
|
496
|
+
body.pitch = heavy.pitch;
|
|
497
|
+
// A harmony model change brings its whole grammar.
|
|
498
|
+
if (bodyA.harmony.model !== bodyB.harmony.model) body.harmony = heavy.harmony;
|
|
499
|
+
// Rhythm grids follow the heavier side's meter.
|
|
500
|
+
if (
|
|
501
|
+
!same(bodyA.meter, bodyB.meter) ||
|
|
502
|
+
bodyA.groove.subdivision !== bodyB.groove.subdivision
|
|
503
|
+
)
|
|
504
|
+
body.rhythm = heavy.rhythm;
|
|
505
|
+
// Section energies and role maps stay consistent with the chosen plans.
|
|
506
|
+
body.form = heavy.form;
|
|
507
|
+
body.summary = `${Math.round((1 - w) * 100)}% ${titleA}, ${Math.round(w * 100)}% ${titleB}`;
|
|
508
|
+
return deepFreeze({
|
|
509
|
+
...(body as StyleBody),
|
|
510
|
+
id: w < 0.5 ? a.id : b.id,
|
|
511
|
+
title: `${titleA} + ${titleB}`,
|
|
512
|
+
lineage: [...lineageA, ...lineageB],
|
|
513
|
+
blend: { a: a.id, b: b.id, weight: w },
|
|
514
|
+
}) as ResolvedStyle;
|
|
515
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card-writing helpers (quality-08): short notations family files use so a
|
|
3
|
+
* card reads like the theory it encodes.
|
|
4
|
+
*
|
|
5
|
+
* `grid("x..x..x.")` writes an onset grid: `x` is a sure hit, `.` none,
|
|
6
|
+
* `1`..`9` a probability of 0.1..0.9 (`5` = half the bars). One character
|
|
7
|
+
* per grid step; spaces and `|` are ignored so beats can be marked.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RoleTexture, RoleVoice } from "./schema.ts";
|
|
11
|
+
|
|
12
|
+
export function grid(pattern: string): readonly number[] {
|
|
13
|
+
const out: number[] = [];
|
|
14
|
+
for (const char of pattern) {
|
|
15
|
+
if (char === " " || char === "|") continue;
|
|
16
|
+
if (char === "x" || char === "X") out.push(1);
|
|
17
|
+
else if (char === ".") out.push(0);
|
|
18
|
+
else if (char >= "1" && char <= "9") out.push(Number(char) / 10);
|
|
19
|
+
else throw new Error(`grid: bad step "${char}" in "${pattern}"`);
|
|
20
|
+
}
|
|
21
|
+
return Object.freeze(out);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A role's voices: `voices("piano", "keys:0.5", "electric@crunch:0.5")`
|
|
26
|
+
* (word, optional `@rig` amp preset, optional `:weight`).
|
|
27
|
+
*/
|
|
28
|
+
export function voices(...words: string[]): readonly RoleVoice[] {
|
|
29
|
+
return Object.freeze(
|
|
30
|
+
words.map((text) => {
|
|
31
|
+
const [head, weight] = text.split(":");
|
|
32
|
+
const [instrument, rig] = head!.split("@");
|
|
33
|
+
const value = weight === undefined ? 1 : Number(weight);
|
|
34
|
+
if (!instrument || !(value >= 0))
|
|
35
|
+
throw new Error(`voices: bad voice "${text}"`);
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
instrument,
|
|
38
|
+
...(rig ? { rig } : {}),
|
|
39
|
+
weight: value,
|
|
40
|
+
});
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A kit voice for the drum roles. */
|
|
46
|
+
export function kit(name: string, weight = 1): RoleVoice {
|
|
47
|
+
return Object.freeze({ instrument: "drums", kit: name, weight });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A required role played by these voices. */
|
|
51
|
+
export function role(...words: string[]): RoleTexture {
|
|
52
|
+
return Object.freeze({ required: true, voices: voices(...words) });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** An optional role (joins about three times in four). */
|
|
56
|
+
export function maybe(...words: string[]): RoleTexture {
|
|
57
|
+
return Object.freeze({ required: false, voices: voices(...words) });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The kit roles all on one kit. */
|
|
61
|
+
export function kitRoles(
|
|
62
|
+
name: string,
|
|
63
|
+
roles: readonly string[] = ["kick", "snare", "hat"],
|
|
64
|
+
optional: readonly string[] = [],
|
|
65
|
+
): Record<string, RoleTexture> {
|
|
66
|
+
const voice = Object.freeze([kit(name)]);
|
|
67
|
+
const out: Record<string, RoleTexture> = {};
|
|
68
|
+
for (const r of roles)
|
|
69
|
+
out[r] = Object.freeze({ required: true, voices: voice });
|
|
70
|
+
for (const r of optional)
|
|
71
|
+
out[r] = Object.freeze({ required: false, voices: voice });
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Interval weights for -12..12 semitones from a step/leap profile:
|
|
77
|
+
* `steps` weight on seconds, `thirds` on thirds, `leaps` on fourths and
|
|
78
|
+
* up, `repeat` on unisons; `up` above 1 favours rising motion.
|
|
79
|
+
*/
|
|
80
|
+
export function intervals(
|
|
81
|
+
steps: number,
|
|
82
|
+
thirds: number,
|
|
83
|
+
leaps: number,
|
|
84
|
+
repeat: number,
|
|
85
|
+
up = 1,
|
|
86
|
+
): readonly number[] {
|
|
87
|
+
const out: number[] = [];
|
|
88
|
+
for (let i = -12; i <= 12; i += 1) {
|
|
89
|
+
const size = Math.abs(i);
|
|
90
|
+
let w =
|
|
91
|
+
size === 0
|
|
92
|
+
? repeat
|
|
93
|
+
: size <= 2
|
|
94
|
+
? steps
|
|
95
|
+
: size <= 4
|
|
96
|
+
? thirds
|
|
97
|
+
: size === 12
|
|
98
|
+
? leaps * 0.6
|
|
99
|
+
: size <= 7
|
|
100
|
+
? leaps * (size === 5 || size === 7 ? 1 : 0.5)
|
|
101
|
+
: leaps * 0.25;
|
|
102
|
+
if (i > 0) w *= up;
|
|
103
|
+
out.push(Math.round(w * 100) / 100);
|
|
104
|
+
}
|
|
105
|
+
return Object.freeze(out);
|
|
106
|
+
}
|