@hraness/dawg 0.6.1 → 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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- 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 +30 -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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- 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 -13
- 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 +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- 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 +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -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 +4 -1
- package/src/agent/provider.ts +22 -8
- 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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -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 +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 +160 -28
- 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 +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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -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 +616 -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/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- 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/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 +34 -4
- 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 +759 -177
- 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 +47 -7
- 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 +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 +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/src/tui/modal-menu.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ctrl-k menu rows for modal percussion (0.6): the "
|
|
3
|
-
* group under Sound ›
|
|
2
|
+
* Ctrl-k menu rows for modal percussion (0.6): the "mallets"
|
|
3
|
+
* group under Sound › instruments, and the MODAL_PARAMS rows under
|
|
4
4
|
* Sound › Parameters on a modal track. Every row runs a `modal …` command.
|
|
5
5
|
*/
|
|
6
6
|
import type { EnumParam, NumberParam } from "../../core/params.ts";
|
|
@@ -18,12 +18,12 @@ import {
|
|
|
18
18
|
import type { Track } from "../../core/score.ts";
|
|
19
19
|
import { num, specStep, type MenuNode } from "./menu.ts";
|
|
20
20
|
|
|
21
|
-
/** Sound ›
|
|
21
|
+
/** Sound › instruments › mallets: one row per preset. */
|
|
22
22
|
export function malletsMenu(): MenuNode {
|
|
23
23
|
return {
|
|
24
24
|
kind: "menu",
|
|
25
25
|
id: "mallets",
|
|
26
|
-
label: "
|
|
26
|
+
label: "mallets",
|
|
27
27
|
detail: `${MODAL_PRESET_NAMES.length} modal presets · marimba, vibes, gong …`,
|
|
28
28
|
help: "struck bars, bells and bowls on the modal resonator engine",
|
|
29
29
|
build: () => [
|
|
@@ -47,14 +47,14 @@ function presetRow(name: ModalPresetName): MenuNode {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* mallets › gamelan (f061-gamelan-winds): the Javanese and
|
|
51
51
|
* Balinese bronzes. `modal gamelan` points here.
|
|
52
52
|
*/
|
|
53
53
|
export function gamelanMenu(): MenuNode {
|
|
54
54
|
return {
|
|
55
55
|
kind: "menu",
|
|
56
56
|
id: "gamelan",
|
|
57
|
-
label: "
|
|
57
|
+
label: "gamelan",
|
|
58
58
|
detail: `${GAMELAN_PRESETS.length} bronzes · saron, gangsa (ombak), gong …`,
|
|
59
59
|
help: "Javanese and Balinese gamelan; pair two gangsa tracks with modal pair, tune with tuning slendro or pelog",
|
|
60
60
|
build: () => GAMELAN_PRESETS.map(presetRow),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* runs the same prompt command a user could type (src/commands/expression.ts),
|
|
4
4
|
* so the menu, the prompt and the agent share one grammar.
|
|
5
5
|
*/
|
|
6
|
+
import { singVowelNodes } from "./sing-menu.ts";
|
|
6
7
|
import {
|
|
7
8
|
ARTICULATIONS,
|
|
8
9
|
DEFAULT_FIXED_VELOCITY,
|
|
@@ -187,7 +188,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
187
188
|
: "pedal",
|
|
188
189
|
help: "bars replaces the lane with a re-pedal on each downbeat; in play mode press Tab (latch) or hold Shift while recording",
|
|
189
190
|
},
|
|
190
|
-
// keys-electric (0.6.1): the piano's other two pedals, on
|
|
191
|
+
// keys-electric (0.6.1): the piano's other two pedals, on modeled piano
|
|
191
192
|
// tracks (or wherever a lane is already set).
|
|
192
193
|
...pianoPedalNodes(track),
|
|
193
194
|
{
|
|
@@ -233,7 +234,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
233
234
|
},
|
|
234
235
|
{
|
|
235
236
|
kind: "number",
|
|
236
|
-
label: "humanize
|
|
237
|
+
label: "humanize vel (%)",
|
|
237
238
|
value: humanize ? velocity : undefined,
|
|
238
239
|
min: 0,
|
|
239
240
|
max: EXPRESSION_LIMITS.maxHumanizePercent,
|
|
@@ -273,5 +274,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
273
274
|
command: "humanize reseed",
|
|
274
275
|
help: "the same amounts with a different seeded performance",
|
|
275
276
|
});
|
|
277
|
+
// 0.7 sing: Vowels on a sing track.
|
|
278
|
+
nodes.push(...singVowelNodes(context));
|
|
276
279
|
return nodes;
|
|
277
280
|
}
|
package/src/tui/play-chords.ts
CHANGED
|
@@ -605,9 +605,11 @@ export function applyChordsCommand(
|
|
|
605
605
|
return done(`chords preset ${value}`);
|
|
606
606
|
case "style":
|
|
607
607
|
if (!(PROGRESSION_STYLES as readonly string[]).includes(value ?? ""))
|
|
608
|
-
return bad(
|
|
608
|
+
return bad(
|
|
609
|
+
`idiom takes ${PROGRESSION_STYLES.join("|")} · chords idiom jazz`,
|
|
610
|
+
);
|
|
609
611
|
settings.style = value as ProgressionStyle;
|
|
610
|
-
return done(`chords
|
|
612
|
+
return done(`chords idiom ${value}`);
|
|
611
613
|
default:
|
|
612
614
|
return bad(
|
|
613
615
|
"/chords auto|manual|off · voicing · spread · bass · perform · pattern · strokes · speed · rate · octaves · sevenths · preset · style",
|
package/src/tui/play-mode.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type Track,
|
|
33
33
|
} from "../../core/score.ts";
|
|
34
34
|
import type { TuningTable } from "../../core/tuning.ts";
|
|
35
|
+
import { DRUM_VOICES, isDrumInstrument } from "../../core/drums.ts";
|
|
35
36
|
import { pitchName } from "../../tui/highway.ts";
|
|
36
37
|
|
|
37
38
|
/** Semitone offset from the base C for each note key. */
|
|
@@ -112,10 +113,21 @@ export function defaultBaseFor(instrument: string | undefined): number {
|
|
|
112
113
|
*/
|
|
113
114
|
export type PlayLayout = Readonly<{
|
|
114
115
|
base: number;
|
|
115
|
-
/** Pitch → label (voice names on one-shot samplers). */
|
|
116
|
+
/** Pitch → label (voice names on one-shot samplers, drums on kits). */
|
|
116
117
|
labels: ReadonlyMap<number, string>;
|
|
118
|
+
/** A drum kit: keys name drums and the chord row stays hidden. */
|
|
119
|
+
drums?: boolean | undefined;
|
|
117
120
|
}>;
|
|
118
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A kit's keys on the GM map from C2 (36), the same keys the agent's
|
|
124
|
+
* show-me gestures press: A kick, W rim, S snare, E clap, T hat, H tom,
|
|
125
|
+
* U open hat. Other keys still sound their nearest drum, unlabeled.
|
|
126
|
+
*/
|
|
127
|
+
export function drumKeyLabels(): ReadonlyMap<number, string> {
|
|
128
|
+
return new Map(DRUM_VOICES.map((info) => [info.pitch, info.label]));
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
export function playLayoutFor(track: Track | undefined): PlayLayout {
|
|
120
132
|
const labels = new Map<number, string>();
|
|
121
133
|
if (track && isSamplerInstrument(track.instrument) && track.sampler) {
|
|
@@ -130,6 +142,8 @@ export function playLayoutFor(track: Track | undefined): PlayLayout {
|
|
|
130
142
|
const lowest = roots.length > 0 ? Math.min(...roots) : 60;
|
|
131
143
|
return { base: clampBase(Math.floor(lowest / 12) * 12), labels };
|
|
132
144
|
}
|
|
145
|
+
if (track && isDrumInstrument(track.instrument))
|
|
146
|
+
return { base: 36, labels: drumKeyLabels(), drums: true };
|
|
133
147
|
// A plain instrument takes its range from the track's name (`bass`, `lead`).
|
|
134
148
|
const byInstrument = defaultBaseFor(track?.instrument);
|
|
135
149
|
const base =
|
package/src/tui/play-session.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import {
|
|
51
51
|
LiveFullRenderer,
|
|
52
52
|
LiveSynth,
|
|
53
|
+
warmLive,
|
|
53
54
|
MAX_LIVE_NOTE_SECONDS,
|
|
54
55
|
type LiveNotePcm,
|
|
55
56
|
} from "../audio/live.ts";
|
|
@@ -57,6 +58,7 @@ import type { SampleBank } from "../audio/samples.ts";
|
|
|
57
58
|
import { fitting, onFitReady } from "../audio/fit.ts";
|
|
58
59
|
import {
|
|
59
60
|
DEFAULT_STRUM,
|
|
61
|
+
degreeOf,
|
|
60
62
|
perform,
|
|
61
63
|
type PerformMode,
|
|
62
64
|
type PerformOptions,
|
|
@@ -73,6 +75,7 @@ import {
|
|
|
73
75
|
} from "./play-chords.ts";
|
|
74
76
|
import type { PlayHeaderView, PlayStripKey } from "../../tui/play-strip.ts";
|
|
75
77
|
import { resolveTuning, type Tuning } from "../../core/tuning.ts";
|
|
78
|
+
import { singThroat } from "../../core/sing.ts";
|
|
76
79
|
import {
|
|
77
80
|
NOTE_KEYS,
|
|
78
81
|
PlayKeyboard,
|
|
@@ -91,6 +94,10 @@ export interface LiveEngine {
|
|
|
91
94
|
readonly sampleRate: number;
|
|
92
95
|
readonly canMonitor: boolean;
|
|
93
96
|
readonly leadMs: number;
|
|
97
|
+
/** The lead play mode pins (15 ms on the native sink, else 60 ms). */
|
|
98
|
+
readonly playLeadMs?: number;
|
|
99
|
+
/** How play mode sounds (`native sink`, `ffplay · no native sink`). */
|
|
100
|
+
readonly audioNote?: string;
|
|
94
101
|
monitor(on: boolean): Promise<void>;
|
|
95
102
|
setLeadMs(ms: number | undefined): void;
|
|
96
103
|
noteOn(id: number, note: LiveNotePcm): number;
|
|
@@ -124,6 +131,8 @@ export interface PlayHost {
|
|
|
124
131
|
startTransport(beat: number): Promise<void>;
|
|
125
132
|
stopTransport(): Promise<void>;
|
|
126
133
|
card(text: string, tone: "info" | "success" | "warning" | "error"): void;
|
|
134
|
+
/** A played key that is not recorded: the screen glows its lane. */
|
|
135
|
+
ghost?(pitch: number): void;
|
|
127
136
|
newNoteId(): string;
|
|
128
137
|
}
|
|
129
138
|
|
|
@@ -240,6 +249,8 @@ export class PlaySession {
|
|
|
240
249
|
private synth: LiveSynth | undefined;
|
|
241
250
|
/** Rig voices awaiting their full render: id → released since. */
|
|
242
251
|
private readonly windows = new Map<number, boolean>();
|
|
252
|
+
/** f07-sing: the sounding key on a throat track (one drone, mono). */
|
|
253
|
+
private throatId: number | undefined;
|
|
243
254
|
private readonly fullRenderer = new LiveFullRenderer();
|
|
244
255
|
private readonly pending = new Map<number, Pending>();
|
|
245
256
|
/** Sustain changes while recording (Tab latch or Shift), as pedal events. */
|
|
@@ -289,7 +300,8 @@ export class PlaySession {
|
|
|
289
300
|
settings.mode =
|
|
290
301
|
chordCapable(track) &&
|
|
291
302
|
twelveTet(this.host.score(), track) &&
|
|
292
|
-
(track?.glide === undefined || track.glide.mode === "poly")
|
|
303
|
+
(track?.glide === undefined || track.glide.mode === "poly") &&
|
|
304
|
+
!singThroat(track)
|
|
293
305
|
? "auto"
|
|
294
306
|
: "manual";
|
|
295
307
|
this.chords = new ChordPad(settings, () => this.host.score().key);
|
|
@@ -331,8 +343,12 @@ export class PlaySession {
|
|
|
331
343
|
this.active = true;
|
|
332
344
|
const engine = this.host.engine();
|
|
333
345
|
if (engine?.canMonitor) {
|
|
334
|
-
engine.setLeadMs(PLAY_LEAD_MS);
|
|
346
|
+
engine.setLeadMs(engine.playLeadMs ?? PLAY_LEAD_MS);
|
|
347
|
+
// The voice's tables and loops are built now, not on the first key.
|
|
348
|
+
warmLive(this.host.score(), this.trackId, engine.sampleRate);
|
|
335
349
|
await engine.monitor(true);
|
|
350
|
+
if (engine.audioNote)
|
|
351
|
+
this.status = `${engine.audioNote} · ${Math.round(engine.leadMs)} ms lead`;
|
|
336
352
|
} else this.status = "no audio · keys still record";
|
|
337
353
|
this.applyClick();
|
|
338
354
|
}
|
|
@@ -483,6 +499,8 @@ export class PlaySession {
|
|
|
483
499
|
return { type: "handled" };
|
|
484
500
|
case "note": {
|
|
485
501
|
this.release(action.released, now);
|
|
502
|
+
// A key that is not recording glows its lane for a moment.
|
|
503
|
+
if (!this.recording) this.host.ghost?.(action.note.pitch);
|
|
486
504
|
const chord = this.chords.chordFor(action.note.pitch);
|
|
487
505
|
if (chord) {
|
|
488
506
|
const played = this.chords.voice(chord, action.note.pitch);
|
|
@@ -501,6 +519,12 @@ export class PlaySession {
|
|
|
501
519
|
);
|
|
502
520
|
return { type: "handled" };
|
|
503
521
|
}
|
|
522
|
+
// f07-sing: a throat track is one drone; a new key releases the last.
|
|
523
|
+
if (singThroat(this.trackData())) {
|
|
524
|
+
if (this.throatId !== undefined && this.throatId !== action.note.id)
|
|
525
|
+
this.release([this.throatId], now);
|
|
526
|
+
this.throatId = action.note.id;
|
|
527
|
+
}
|
|
504
528
|
this.sound(action.note.id, action.note);
|
|
505
529
|
this.record(action.note);
|
|
506
530
|
return { type: "handled" };
|
|
@@ -1143,7 +1167,7 @@ export class PlaySession {
|
|
|
1143
1167
|
};
|
|
1144
1168
|
}
|
|
1145
1169
|
return {
|
|
1146
|
-
range: this.keyboard.range,
|
|
1170
|
+
range: this.layout.drums ? "drums" : this.keyboard.range,
|
|
1147
1171
|
velocity: this.keyboard.velocity,
|
|
1148
1172
|
armed: this.armed,
|
|
1149
1173
|
recording: this.recording,
|
|
@@ -1153,10 +1177,14 @@ export class PlaySession {
|
|
|
1153
1177
|
countIn,
|
|
1154
1178
|
beat,
|
|
1155
1179
|
grid: `grid ${this.grid}`,
|
|
1156
|
-
|
|
1180
|
+
// A kit plays drums, not harmony: no chord row and no key chip.
|
|
1181
|
+
chords: this.layout.drums
|
|
1182
|
+
? undefined
|
|
1183
|
+
: this.chords.glance(songKey(this.host.score().key).set),
|
|
1157
1184
|
status: this.status,
|
|
1158
1185
|
keys: this.strip(now),
|
|
1159
|
-
legend:
|
|
1186
|
+
legend:
|
|
1187
|
+
this.chords.on && !this.layout.drums ? this.chords.legend() : undefined,
|
|
1160
1188
|
};
|
|
1161
1189
|
}
|
|
1162
1190
|
|
|
@@ -1178,8 +1206,20 @@ export class PlaySession {
|
|
|
1178
1206
|
).map((cell) => {
|
|
1179
1207
|
const chord = this.keyboard.pitchFor(cell.key);
|
|
1180
1208
|
const name =
|
|
1181
|
-
chord === undefined
|
|
1182
|
-
|
|
1209
|
+
chord === undefined || this.layout.drums
|
|
1210
|
+
? undefined
|
|
1211
|
+
: this.chords.keyLabel(chord);
|
|
1212
|
+
// Chords in the key are the safe ones to reach for: the strip lights
|
|
1213
|
+
// them; a borrowed chord still plays but stays plain.
|
|
1214
|
+
return name && chord !== undefined
|
|
1215
|
+
? {
|
|
1216
|
+
...cell,
|
|
1217
|
+
label: name,
|
|
1218
|
+
chord: true,
|
|
1219
|
+
diatonic:
|
|
1220
|
+
degreeOf(this.chords.key, ((chord % 12) + 12) % 12) !== undefined,
|
|
1221
|
+
}
|
|
1222
|
+
: this.noteCell(cell);
|
|
1183
1223
|
});
|
|
1184
1224
|
}
|
|
1185
1225
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl-k menu rows for the sing engine (0.7): the Choir, Solo and Throat
|
|
3
|
+
* groups under Voice › voice presets, the SING_PARAMS rows under
|
|
4
|
+
* Sound › Parameters on a sing track (with a Throat sub-menu) and the
|
|
5
|
+
* Vowels row under Sound › Performance. Every row runs a `sing …` or
|
|
6
|
+
* `note vowel …` command, so it stages for the audition loop's A/B.
|
|
7
|
+
*/
|
|
8
|
+
import type { EnumParam, NumberParam } from "../../core/params.ts";
|
|
9
|
+
import type { Note, Track } from "../../core/score.ts";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_SING_PRESET,
|
|
12
|
+
resolveSing,
|
|
13
|
+
SING_GROUPS,
|
|
14
|
+
SING_PARAMS,
|
|
15
|
+
SING_PRESETS,
|
|
16
|
+
SING_PRESET_NAMES,
|
|
17
|
+
SING_THROAT_PARAMS,
|
|
18
|
+
SING_VOWELS,
|
|
19
|
+
singNoteName,
|
|
20
|
+
singDroneFollows,
|
|
21
|
+
singTrack,
|
|
22
|
+
type SingPreset,
|
|
23
|
+
} from "../../core/sing.ts";
|
|
24
|
+
import { num, specStep, type MenuContext, type MenuNode } from "./menu.ts";
|
|
25
|
+
|
|
26
|
+
function presetRow(name: SingPreset): MenuNode {
|
|
27
|
+
return {
|
|
28
|
+
kind: "action",
|
|
29
|
+
label: `${name.padEnd(9)} ${SING_PRESETS[name].doc}`,
|
|
30
|
+
command: `sing ${name}`,
|
|
31
|
+
help: SING_PRESETS[name].styles,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Voice › voice presets: choir, solo and throat (sing lane). */
|
|
36
|
+
export function singBrowseRows(): MenuNode[] {
|
|
37
|
+
return SING_GROUPS.map((group): MenuNode => ({
|
|
38
|
+
kind: "menu",
|
|
39
|
+
id: `voices:${group.label.toLowerCase()}`,
|
|
40
|
+
label: group.label.toLowerCase(),
|
|
41
|
+
detail: group.presets.slice(0, 4).join(", ") + " …",
|
|
42
|
+
help:
|
|
43
|
+
group.label === "Throat"
|
|
44
|
+
? "Tuvan throat singing: a held drone, notes steer the overtone"
|
|
45
|
+
: `${group.label.toLowerCase()} on the built-in singing voice`,
|
|
46
|
+
build: () => group.presets.map(presetRow),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parameters shown first; the rest sit under "advanced". */
|
|
51
|
+
const SING_SIMPLE = Object.freeze([
|
|
52
|
+
"vowel",
|
|
53
|
+
"voice",
|
|
54
|
+
"voices",
|
|
55
|
+
"bright",
|
|
56
|
+
"breath",
|
|
57
|
+
"vibmod",
|
|
58
|
+
"formant",
|
|
59
|
+
"ring",
|
|
60
|
+
] as const);
|
|
61
|
+
|
|
62
|
+
function numberRow(
|
|
63
|
+
name: string,
|
|
64
|
+
spec: NumberParam,
|
|
65
|
+
value: number,
|
|
66
|
+
fallback: number,
|
|
67
|
+
): MenuNode {
|
|
68
|
+
return {
|
|
69
|
+
kind: "number",
|
|
70
|
+
label: name,
|
|
71
|
+
value,
|
|
72
|
+
min: spec.min,
|
|
73
|
+
max: spec.max,
|
|
74
|
+
step: specStep(spec),
|
|
75
|
+
format: (v) => (spec.unit ? `${num(v)} ${spec.unit}` : num(v)),
|
|
76
|
+
command: (v) => `sing ${name} ${num(v)}`,
|
|
77
|
+
reset: `sing ${name} off`,
|
|
78
|
+
help: `${spec.doc} · preset ${num(fallback)} · x resets`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every vowel and two-vowel morph, for the pickers. */
|
|
83
|
+
export function vowelOptions(): string[] {
|
|
84
|
+
const out: string[] = [...SING_VOWELS];
|
|
85
|
+
for (const from of SING_VOWELS)
|
|
86
|
+
for (const to of SING_VOWELS) if (from !== to) out.push(`${from}>${to}`);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Sound › Parameters rows on a sing track. */
|
|
91
|
+
export function singParameterNodes(track: Track, keyRoot?: number): MenuNode[] {
|
|
92
|
+
if (!singTrack(track)) return [];
|
|
93
|
+
const sing = track.sing!;
|
|
94
|
+
const follows = singDroneFollows(sing, keyRoot);
|
|
95
|
+
const resolved = resolveSing(sing, keyRoot) as unknown as Readonly<
|
|
96
|
+
Record<string, unknown>
|
|
97
|
+
>;
|
|
98
|
+
const fallback = SING_PRESETS[sing.preset ?? DEFAULT_SING_PRESET]
|
|
99
|
+
.settings as unknown as Readonly<Record<string, unknown>>;
|
|
100
|
+
const own = sing as Readonly<Record<string, unknown>>;
|
|
101
|
+
const row = (name: string): MenuNode | undefined => {
|
|
102
|
+
if (name === "vowel")
|
|
103
|
+
return {
|
|
104
|
+
kind: "choice",
|
|
105
|
+
label: "vowel",
|
|
106
|
+
value: String(resolved.vowel),
|
|
107
|
+
options: vowelOptions(),
|
|
108
|
+
command: (option) => `sing vowel ${option}`,
|
|
109
|
+
help: `${SING_PARAMS.vowel!.doc} · preset ${String(fallback.vowel)}`,
|
|
110
|
+
};
|
|
111
|
+
if (name === "drone") {
|
|
112
|
+
const drone = resolved.drone as number | undefined;
|
|
113
|
+
const spec = SING_PARAMS.drone as NumberParam;
|
|
114
|
+
return {
|
|
115
|
+
kind: "number",
|
|
116
|
+
label: "drone",
|
|
117
|
+
value: drone,
|
|
118
|
+
min: spec.min,
|
|
119
|
+
max: spec.max,
|
|
120
|
+
step: (v, direction) =>
|
|
121
|
+
Math.max(spec.min, Math.min(spec.max, Math.round(v) + direction)),
|
|
122
|
+
// The drone you hear: the preset's moved to the song key.
|
|
123
|
+
format: (v) =>
|
|
124
|
+
follows && v === drone ? `${singNoteName(v)} (key)` : singNoteName(v),
|
|
125
|
+
command: (v) => `sing drone ${singNoteName(v)}`,
|
|
126
|
+
off: "off (melody)",
|
|
127
|
+
start: 50,
|
|
128
|
+
reset: "sing drone off",
|
|
129
|
+
help: `${spec.doc} · x back to the preset`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (name === "harmonics") {
|
|
133
|
+
const [lo, hi] = resolved.harmonics as readonly [number, number];
|
|
134
|
+
const options = ["4-8", "4-12", "5-10", "6-12", "6-16", "8-16", "8-24"];
|
|
135
|
+
const value = `${lo}-${hi}`;
|
|
136
|
+
return {
|
|
137
|
+
kind: "choice",
|
|
138
|
+
label: "harmonics",
|
|
139
|
+
value,
|
|
140
|
+
options: options.includes(value) ? options : [value, ...options],
|
|
141
|
+
command: (option) => `sing harmonics ${option}`,
|
|
142
|
+
help: "the overtone range a throat track's notes pick from (khoomei 6-12, sygyt 8-16)",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const spec = SING_PARAMS[name]!;
|
|
146
|
+
if (spec.kind === "enum")
|
|
147
|
+
return {
|
|
148
|
+
kind: "choice",
|
|
149
|
+
label: name,
|
|
150
|
+
value: own[name] === undefined ? "preset" : String(own[name]),
|
|
151
|
+
options: ["preset", ...(spec as EnumParam).values],
|
|
152
|
+
command: (option) =>
|
|
153
|
+
option === "preset" ? `sing ${name} unset` : `sing ${name} ${option}`,
|
|
154
|
+
help: `${spec.doc} · preset ${String(fallback[name])}`,
|
|
155
|
+
};
|
|
156
|
+
if (spec.kind !== "number") return undefined;
|
|
157
|
+
return numberRow(
|
|
158
|
+
name,
|
|
159
|
+
spec,
|
|
160
|
+
Number(resolved[name]),
|
|
161
|
+
Number(fallback[name]),
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
const nodes: MenuNode[] = [
|
|
165
|
+
{
|
|
166
|
+
kind: "choice",
|
|
167
|
+
label: "preset",
|
|
168
|
+
value: sing.preset ?? DEFAULT_SING_PRESET,
|
|
169
|
+
options: SING_PRESET_NAMES,
|
|
170
|
+
command: (option) => `sing preset ${option}`,
|
|
171
|
+
help: "the voice: choir, solo or throat singing",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
for (const name of SING_SIMPLE) {
|
|
175
|
+
const node = row(name);
|
|
176
|
+
if (node) nodes.push(node);
|
|
177
|
+
}
|
|
178
|
+
const throat = SING_THROAT_PARAMS as readonly string[];
|
|
179
|
+
nodes.push({
|
|
180
|
+
kind: "menu",
|
|
181
|
+
id: "sing:throat",
|
|
182
|
+
label: "throat",
|
|
183
|
+
detail:
|
|
184
|
+
resolved.drone === undefined
|
|
185
|
+
? "off · drone, overtone, harmonics, sub"
|
|
186
|
+
: `drone ${singNoteName(resolved.drone as number)} · ${throat.slice(1).join(", ")}`,
|
|
187
|
+
help: "throat singing: a drone, notes pick its overtones; sub for kargyraa",
|
|
188
|
+
build: () => throat.flatMap((name) => row(name) ?? []),
|
|
189
|
+
});
|
|
190
|
+
const rest = Object.keys(SING_PARAMS).filter(
|
|
191
|
+
(name) =>
|
|
192
|
+
!(SING_SIMPLE as readonly string[]).includes(name) &&
|
|
193
|
+
!throat.includes(name),
|
|
194
|
+
);
|
|
195
|
+
nodes.push({
|
|
196
|
+
kind: "menu",
|
|
197
|
+
id: "sing:advanced",
|
|
198
|
+
label: "advanced",
|
|
199
|
+
detail: rest.join(", "),
|
|
200
|
+
help: "every sing parameter",
|
|
201
|
+
build: () => rest.flatMap((name) => row(name) ?? []),
|
|
202
|
+
});
|
|
203
|
+
if (Object.keys(sing).some((key) => key !== "preset"))
|
|
204
|
+
nodes.push({
|
|
205
|
+
kind: "action",
|
|
206
|
+
label: "reset to preset",
|
|
207
|
+
command: "sing reset",
|
|
208
|
+
help: "clear overrides, keep the preset",
|
|
209
|
+
});
|
|
210
|
+
return nodes;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sharedVowel(notes: readonly Note[]): string {
|
|
214
|
+
const values = new Set(notes.map((note) => note.vowel ?? "track"));
|
|
215
|
+
if (values.size === 0) return "track";
|
|
216
|
+
return values.size === 1 ? [...values][0]! : "mixed";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Sound › Performance › Vowels on a sing track: left/right steps the notes'
|
|
221
|
+
* vowel through a e i o u, enter opens the morph picker, and a pattern
|
|
222
|
+
* row cycles vowels over the notes in order.
|
|
223
|
+
*/
|
|
224
|
+
export function singVowelNodes(context: MenuContext): MenuNode[] {
|
|
225
|
+
const track = context.score.tracks.find(
|
|
226
|
+
(item) => item.id === context.trackId,
|
|
227
|
+
);
|
|
228
|
+
if (!singTrack(track)) return [];
|
|
229
|
+
const notes = context.score.notes.filter(
|
|
230
|
+
(note) => note.trackId === track!.id,
|
|
231
|
+
);
|
|
232
|
+
const current = sharedVowel(notes);
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
kind: "menu",
|
|
236
|
+
id: "sing:vowels",
|
|
237
|
+
label: "vowels",
|
|
238
|
+
detail: `${current} · ${notes.length} notes`,
|
|
239
|
+
help: "the sung vowel of every note on the track (a note's vowel wins over the track's)",
|
|
240
|
+
build: (inner) => {
|
|
241
|
+
const now = inner.score.notes.filter(
|
|
242
|
+
(note) => note.trackId === context.trackId,
|
|
243
|
+
);
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
kind: "choice",
|
|
247
|
+
label: "vowel",
|
|
248
|
+
value: sharedVowel(now),
|
|
249
|
+
options: ["track", ...SING_VOWELS],
|
|
250
|
+
command: (option) =>
|
|
251
|
+
`note vowel ${option === "track" ? "off" : option}`,
|
|
252
|
+
help: "left/right steps a e i o u; track uses the track's vowel",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
kind: "choice",
|
|
256
|
+
label: "morph",
|
|
257
|
+
value: sharedVowel(now),
|
|
258
|
+
options: [
|
|
259
|
+
"track",
|
|
260
|
+
...vowelOptions().filter((v) => v.includes(">")),
|
|
261
|
+
],
|
|
262
|
+
command: (option) =>
|
|
263
|
+
`note vowel ${option === "track" ? "off" : option}`,
|
|
264
|
+
help: "each note travels from the first vowel to the second",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
kind: "choice",
|
|
268
|
+
label: "pattern",
|
|
269
|
+
value: "",
|
|
270
|
+
options: ["a e i o u", "a o", "o u", "a e i", "u o a"],
|
|
271
|
+
command: (option) => `sing vowels ${option}`,
|
|
272
|
+
help: "vowels over the notes in time order, cycled",
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctrl-k › Style: the style taxonomy as a menu tree (families, then each
|
|
3
|
+
* branch down to its leaves). Every row is a `style` command, so the menu
|
|
4
|
+
* and the prompt share one grammar; typing filters the rows of a level.
|
|
5
|
+
*/
|
|
6
|
+
import { hasCard, STYLE_TREE } from "../../core/styles/index.ts";
|
|
7
|
+
import { STYLE_FAMILIES } from "../../core/styles/taxonomy.ts";
|
|
8
|
+
import type { StyleId } from "../../core/styles/schema.ts";
|
|
9
|
+
import type { MenuContext, MenuNode } from "./menu.ts";
|
|
10
|
+
|
|
11
|
+
const BAR_CHOICES = [4, 8, 16, 32] as const;
|
|
12
|
+
|
|
13
|
+
/** The Style row's detail: the song's style, or a hint. */
|
|
14
|
+
export function styleMenuDetail(context: MenuContext): string {
|
|
15
|
+
const style = context.score.style;
|
|
16
|
+
if (!style) return "a whole song from a style";
|
|
17
|
+
const name = style.blend ? `${style.id} + ${style.blend.id}` : style.id;
|
|
18
|
+
return `${name} · seed ${style.seed}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function styleActions(id: StyleId): MenuNode[] {
|
|
22
|
+
const node = STYLE_TREE.get(id)!;
|
|
23
|
+
return [
|
|
24
|
+
...BAR_CHOICES.map((bars): MenuNode => ({
|
|
25
|
+
kind: "action",
|
|
26
|
+
label: `make ${bars} bars`,
|
|
27
|
+
command: `/style ${id} ${bars}`,
|
|
28
|
+
help: `replace the song with ${bars} bars of ${node.title} (undo restores it)`,
|
|
29
|
+
})),
|
|
30
|
+
{
|
|
31
|
+
kind: "action",
|
|
32
|
+
label: "about",
|
|
33
|
+
command: `/style info ${id}`,
|
|
34
|
+
help: "meter, tempo, groove, tuning, harmony and roles",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function styleNode(id: StyleId): MenuNode {
|
|
40
|
+
const node = STYLE_TREE.get(id)!;
|
|
41
|
+
const card = hasCard(id) ? "" : " · inherits";
|
|
42
|
+
return {
|
|
43
|
+
kind: "menu",
|
|
44
|
+
id: `style:${id}`,
|
|
45
|
+
label: node.title,
|
|
46
|
+
detail: node.leaf
|
|
47
|
+
? `${id}${card}`
|
|
48
|
+
: `${node.children.length} styles${card}`,
|
|
49
|
+
help: node.region.length ? `${id} · ${node.region.join(", ")}` : id,
|
|
50
|
+
build: () => [
|
|
51
|
+
...styleActions(id),
|
|
52
|
+
...node.children.map((child) => styleNode(child)),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Style: again, find, then the families. */
|
|
58
|
+
export function styleMenuNodes(context: MenuContext): MenuNode[] {
|
|
59
|
+
const current = context.score.style;
|
|
60
|
+
return [
|
|
61
|
+
...(current
|
|
62
|
+
? [
|
|
63
|
+
{
|
|
64
|
+
kind: "action",
|
|
65
|
+
label: "again",
|
|
66
|
+
command: "/style again",
|
|
67
|
+
help: `${current.id} with the next seed`,
|
|
68
|
+
} satisfies MenuNode,
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
{
|
|
72
|
+
kind: "entry",
|
|
73
|
+
label: "find",
|
|
74
|
+
value: "",
|
|
75
|
+
placeholder: "style name, alias or region",
|
|
76
|
+
command: (text) =>
|
|
77
|
+
text.trim() ? `/style search ${text.trim()}` : undefined,
|
|
78
|
+
example: "/style search samba",
|
|
79
|
+
help: "search every style by name, alias or region",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
kind: "entry",
|
|
83
|
+
label: "blend",
|
|
84
|
+
value: "",
|
|
85
|
+
placeholder: "a b [weight]",
|
|
86
|
+
command: (text) => {
|
|
87
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
88
|
+
return words.length >= 2 && words.length <= 3
|
|
89
|
+
? `/style blend ${words.join(" ")}`
|
|
90
|
+
: undefined;
|
|
91
|
+
},
|
|
92
|
+
example: "/style blend bebop bossa-nova 0.3",
|
|
93
|
+
help: "mix two styles; weight 0 is the first, 1 the second",
|
|
94
|
+
},
|
|
95
|
+
...STYLE_FAMILIES.map((family): MenuNode => ({
|
|
96
|
+
kind: "menu",
|
|
97
|
+
id: `family:${family.key}`,
|
|
98
|
+
label: family.title,
|
|
99
|
+
detail: family.roots.join(" · "),
|
|
100
|
+
help: `${family.key} family`,
|
|
101
|
+
build: () => family.roots.map((root) => styleNode(root)),
|
|
102
|
+
})),
|
|
103
|
+
];
|
|
104
|
+
}
|