@hraness/dawg 0.7.0 → 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 +26 -0
- package/DAWG.md +117 -30
- package/README.md +7 -5
- package/core/diff.ts +2 -0
- package/core/ids.ts +52 -0
- package/core/loop.ts +3 -0
- package/core/range.ts +581 -0
- package/core/score.ts +123 -1
- package/core/sdk/print.ts +11 -2
- package/core/sdk/v1.ts +281 -2
- package/core/sections.ts +198 -19
- package/guides/arrange.md +9 -8
- package/guides/audio.md +31 -0
- package/guides/mix.md +3 -1
- package/guides/panes.md +32 -0
- package/guides/project.md +3 -2
- package/guides/rhythm.md +2 -1
- package/guides/tape.md +32 -0
- package/package.json +2 -1
- package/src/agent/agent.ts +4 -4
- package/src/agent/command-agent.ts +19 -10
- package/src/agent/ops.ts +15 -0
- package/src/agent/planner.ts +12 -0
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/show-me.ts +1 -1
- package/src/agent/tools.ts +2 -0
- package/src/agent/xcb-agent.ts +2 -3
- package/src/audio/arrange.ts +12 -2
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +84 -13
- package/src/audio/engine.ts +216 -28
- package/src/commands/arrange.ts +60 -25
- package/src/commands/grammar.ts +3 -1
- package/src/commands/help.ts +187 -9
- package/src/commands/history.ts +122 -11
- package/src/commands/keys.ts +17 -0
- package/src/commands/parses.ts +6 -1
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +2 -6
- package/src/commands/vocal-pitch.ts +2 -1
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +18 -0
- package/src/launch-args.ts +105 -1
- package/src/main.ts +1142 -86
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +196 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/origin.ts +20 -0
- package/src/session/port.ts +87 -10
- package/src/session/protocol.ts +360 -6
- package/src/session/rebase.ts +46 -8
- package/src/session/receipt.ts +7 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +61 -2
- package/src/tui/arrange-menu.ts +169 -1
- package/src/tui/euclid.ts +95 -14
- package/src/tui/fader.ts +55 -2
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu.ts +228 -2
- package/src/tui/play-session.ts +196 -21
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/tui/app.ts +217 -44
- package/tui/delight.ts +49 -0
- package/tui/drawer.ts +167 -9
- package/tui/grammar.ts +34 -5
- package/tui/guide.ts +8 -1
- package/tui/highway.ts +40 -5
- package/tui/hints.ts +5 -0
- package/tui/hits.ts +7 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +12 -0
- package/tui/screen.ts +7 -4
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +19 -1
package/src/tui/fader.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* field, so repeated nudges replace one staged edit and Enter keeps them
|
|
10
10
|
* all as one revision.
|
|
11
11
|
*/
|
|
12
|
-
import type { DrawerField, DrawerView } from "../../tui/drawer.ts";
|
|
12
|
+
import type { DrawerAside, DrawerField, DrawerView } from "../../tui/drawer.ts";
|
|
13
|
+
import type { KnobIndex } from "../../tui/knobs.ts";
|
|
13
14
|
import {
|
|
14
15
|
KEY_BACKSPACE,
|
|
15
16
|
KEY_BACKTAB,
|
|
@@ -488,6 +489,14 @@ export const FADER_HINT =
|
|
|
488
489
|
* The hint for a field the loop cannot stage (tempo, meter, loop length):
|
|
489
490
|
* each step is a new revision, so there is nothing to keep or revert.
|
|
490
491
|
*/
|
|
492
|
+
/** The knob front page (design §8.6): the four knobs, then Tab for all. */
|
|
493
|
+
export const KNOB_FADER_HINT =
|
|
494
|
+
"↑↓ knob · ←→ turn · tab all · enter keep · ⇧ coarse · x reset · esc revert";
|
|
495
|
+
|
|
496
|
+
/** Every param behind Tab: Tab goes back to the four knobs. */
|
|
497
|
+
export const ALL_FADER_HINT =
|
|
498
|
+
"←→ adjust · ↑↓ param · tab knobs · enter keep · ⇧ coarse · x reset · esc revert";
|
|
499
|
+
|
|
491
500
|
export const FADER_HINT_AT_ONCE =
|
|
492
501
|
"←→ adjust · applies at once · x reset · 0-9 type · ↑↓ param · ⇧←→ coarse · [ ] fine · esc back";
|
|
493
502
|
|
|
@@ -511,6 +520,8 @@ export function drawerView(
|
|
|
511
520
|
hint?: string;
|
|
512
521
|
/** The focused field applies at once (cannot stage on the loop). */
|
|
513
522
|
atOnce?: boolean | undefined;
|
|
523
|
+
/** The drawer has a knob page: which side of Tab is up. */
|
|
524
|
+
knobs?: "knobs" | "all" | undefined;
|
|
514
525
|
},
|
|
515
526
|
): DrawerView {
|
|
516
527
|
const before = new Map(committed.map((field) => [field.label, field]));
|
|
@@ -528,6 +539,7 @@ export function drawerView(
|
|
|
528
539
|
return {
|
|
529
540
|
kind: "choice",
|
|
530
541
|
label: field.label,
|
|
542
|
+
knob: knobOf(field),
|
|
531
543
|
options: field.options,
|
|
532
544
|
index,
|
|
533
545
|
committedIndex:
|
|
@@ -550,6 +562,7 @@ export function drawerView(
|
|
|
550
562
|
return {
|
|
551
563
|
kind: "number",
|
|
552
564
|
label: field.label,
|
|
565
|
+
knob: knobOf(field),
|
|
553
566
|
text,
|
|
554
567
|
committed: changed ? wasText : undefined,
|
|
555
568
|
position:
|
|
@@ -576,10 +589,50 @@ export function drawerView(
|
|
|
576
589
|
dirty: options.dirty,
|
|
577
590
|
badge: options.dirty ? stagedBadge(staged) : undefined,
|
|
578
591
|
status: options.status,
|
|
579
|
-
hint:
|
|
592
|
+
hint:
|
|
593
|
+
options.hint ??
|
|
594
|
+
(options.knobs
|
|
595
|
+
? (options.knobs === "knobs" ? KNOB_FADER_HINT : ALL_FADER_HINT)
|
|
596
|
+
.replace(
|
|
597
|
+
"enter keep",
|
|
598
|
+
options.atOnce ? "applies at once" : "enter keep",
|
|
599
|
+
)
|
|
600
|
+
.replace("esc revert", options.atOnce ? "esc back" : "esc revert")
|
|
601
|
+
: options.atOnce
|
|
602
|
+
? FADER_HINT_AT_ONCE
|
|
603
|
+
: FADER_HINT),
|
|
580
604
|
};
|
|
581
605
|
}
|
|
582
606
|
|
|
607
|
+
/**
|
|
608
|
+
* The drawer's side list: every param on the page with its value, each one
|
|
609
|
+
* a knob also drives wearing that knob (matched by label).
|
|
610
|
+
*/
|
|
611
|
+
export function drawerAside(
|
|
612
|
+
page: readonly FaderSpec[],
|
|
613
|
+
knobs: readonly FaderSpec[],
|
|
614
|
+
): DrawerAside[] {
|
|
615
|
+
const knobByLabel = new Map(
|
|
616
|
+
knobs.map((field) => [field.label, knobOf(field)] as const),
|
|
617
|
+
);
|
|
618
|
+
return page.map((field) => ({
|
|
619
|
+
label: field.label,
|
|
620
|
+
text:
|
|
621
|
+
field.kind === "choice"
|
|
622
|
+
? field.value
|
|
623
|
+
: field.value === undefined
|
|
624
|
+
? (field.off ?? "off")
|
|
625
|
+
: field.format(field.value),
|
|
626
|
+
knob: knobByLabel.get(field.label),
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** A knob field's knob (0 blue … 3 orange); plain rows have none. */
|
|
631
|
+
function knobOf(field: FaderSpec): KnobIndex | undefined {
|
|
632
|
+
const knob = (field as FaderSpec & { knob?: KnobIndex }).knob;
|
|
633
|
+
return knob;
|
|
634
|
+
}
|
|
635
|
+
|
|
583
636
|
/** `A/B: 1 change staged · enter keep · esc revert`. */
|
|
584
637
|
export function stagedBadge(count: number): string {
|
|
585
638
|
const n = Math.max(1, count);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four knobs of a page as fader fields: each path of a KNOB_MAPS row
|
|
3
|
+
* (src/tui/knob-map.ts) resolved in the live Ctrl-K tree, so a knob turns
|
|
4
|
+
* exactly what that menu row turns and runs the same typed command.
|
|
5
|
+
*/
|
|
6
|
+
import type { KnobIndex } from "../../tui/knobs.ts";
|
|
7
|
+
import type { FaderSpec } from "./fader.ts";
|
|
8
|
+
import { knobMap, type KnobMap } from "./knob-map.ts";
|
|
9
|
+
import {
|
|
10
|
+
faderSpecs,
|
|
11
|
+
rootNodes,
|
|
12
|
+
type MenuContext,
|
|
13
|
+
type MenuNode,
|
|
14
|
+
} from "./menu.ts";
|
|
15
|
+
|
|
16
|
+
export type KnobField = FaderSpec & { knob: KnobIndex };
|
|
17
|
+
|
|
18
|
+
const norm = (text: string) => text.trim().toLowerCase();
|
|
19
|
+
|
|
20
|
+
function matches(node: MenuNode, segment: string): boolean {
|
|
21
|
+
const want = norm(segment);
|
|
22
|
+
if (want.endsWith("*"))
|
|
23
|
+
return node.kind === "menu" && norm(node.id).startsWith(want.slice(0, -1));
|
|
24
|
+
if (node.kind === "menu" && norm(node.id) === want) return true;
|
|
25
|
+
return norm(node.label) === want;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The row a knob path reaches from the root, or undefined. */
|
|
29
|
+
export function resolveKnobPath(
|
|
30
|
+
context: MenuContext,
|
|
31
|
+
path: string,
|
|
32
|
+
): MenuNode | undefined {
|
|
33
|
+
let nodes: readonly MenuNode[] = rootNodes(context);
|
|
34
|
+
let found: MenuNode | undefined;
|
|
35
|
+
for (const segment of path.split("/")) {
|
|
36
|
+
found = nodes.find((node) => matches(node, segment));
|
|
37
|
+
if (!found) {
|
|
38
|
+
// Effects beyond the first few live under "more effects".
|
|
39
|
+
const more = nodes.find(
|
|
40
|
+
(node) => node.kind === "menu" && node.id === "more effects",
|
|
41
|
+
);
|
|
42
|
+
if (more?.kind === "menu")
|
|
43
|
+
found = more.build(context).find((node) => matches(node, segment));
|
|
44
|
+
}
|
|
45
|
+
if (!found) return undefined;
|
|
46
|
+
nodes = found.kind === "menu" ? found.build(context) : [];
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The knob label: the row's own label, or `reverb mix` when the row lives in
|
|
53
|
+
* another effect than the page's (the mixer's reverb send).
|
|
54
|
+
*/
|
|
55
|
+
function knobLabel(page: string, path: string, label: string): string {
|
|
56
|
+
const segments = path.split("/");
|
|
57
|
+
if (segments[0] === "effects" && segments.length >= 3) {
|
|
58
|
+
const effect = segments[1]!;
|
|
59
|
+
if (page !== `fx:${effect}`) return `${effect} ${label}`;
|
|
60
|
+
}
|
|
61
|
+
return label;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The four slots of `page` for `context`: a field per slot whose path
|
|
66
|
+
* resolves to a number or choice row, else undefined (drawn `·`, skipped).
|
|
67
|
+
* A page without a row (an effect not in the table) derives one from
|
|
68
|
+
* `fallback`: its first three number rows, and `mix` on orange.
|
|
69
|
+
*/
|
|
70
|
+
export function knobSlots(
|
|
71
|
+
context: MenuContext,
|
|
72
|
+
page: string,
|
|
73
|
+
fallback: readonly FaderSpec[] = [],
|
|
74
|
+
): (KnobField | undefined)[] {
|
|
75
|
+
const map: KnobMap | undefined = knobMap(page);
|
|
76
|
+
if (!map) {
|
|
77
|
+
const numbers = fallback.filter(
|
|
78
|
+
(field) => field.kind === "number" && field.label !== "mix",
|
|
79
|
+
);
|
|
80
|
+
const mix = fallback.find((field) => field.label === "mix");
|
|
81
|
+
return [numbers[0], numbers[1], numbers[2], mix].map((field, index) =>
|
|
82
|
+
field ? { ...field, knob: index as KnobIndex } : undefined,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return map.map((path, index) => {
|
|
86
|
+
if (!path || path.startsWith("@") || !path.includes("/")) return undefined;
|
|
87
|
+
const node = resolveKnobPath(context, path);
|
|
88
|
+
const spec = node ? faderSpecs([node])[0] : undefined;
|
|
89
|
+
if (!spec) return undefined;
|
|
90
|
+
return {
|
|
91
|
+
...spec,
|
|
92
|
+
label: knobLabel(page, path, spec.label),
|
|
93
|
+
knob: index as KnobIndex,
|
|
94
|
+
} as KnobField;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The present knobs of `page`, in knob order: the drawer's front page. */
|
|
99
|
+
export function knobFields(
|
|
100
|
+
context: MenuContext,
|
|
101
|
+
page: string,
|
|
102
|
+
fallback: readonly FaderSpec[] = [],
|
|
103
|
+
): KnobField[] {
|
|
104
|
+
return knobSlots(context, page, fallback).filter(
|
|
105
|
+
(field): field is KnobField => field !== undefined,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which value each of the four knobs turns, per page (design §7.3): one
|
|
3
|
+
* table, read by the fader drawer's front page, the mixer page and euclid.
|
|
4
|
+
* A row is four menu paths from the Ctrl-K root (`sound/attack`,
|
|
5
|
+
* `effects/reverb/mix`, `mix/volume`): segments match a submenu's id or a
|
|
6
|
+
* row's label, and `more effects` is searched on the way. An undefined slot
|
|
7
|
+
* shows as `·` and ↑↓ skips it.
|
|
8
|
+
*
|
|
9
|
+
* The rule (tui/knobs.ts): blue moves, green sizes, white shapes, orange
|
|
10
|
+
* level. Pages not in the table (an effect without a row) derive one: their
|
|
11
|
+
* first three number rows on blue, green and white, and `mix` on orange.
|
|
12
|
+
*
|
|
13
|
+
* Data, not code, so the patcher can map a patch's macros onto the same
|
|
14
|
+
* four knobs by adding rows (`patch:<id>`), and a test checks every path
|
|
15
|
+
* resolves for every instrument family. A `*` at the end of a segment
|
|
16
|
+
* matches the first submenu whose id starts with it (`sample:*`).
|
|
17
|
+
*/
|
|
18
|
+
import { isWavetableInstrument, type Track } from "../../core/score.ts";
|
|
19
|
+
import { isDrumInstrument } from "../../core/drums.ts";
|
|
20
|
+
import {
|
|
21
|
+
isElectricFamily,
|
|
22
|
+
isKeysFamily,
|
|
23
|
+
isOrganFamily,
|
|
24
|
+
} from "../../core/keys.ts";
|
|
25
|
+
import { isStringTrack } from "../../core/strings.ts";
|
|
26
|
+
import { isGranularInstrument } from "../../core/granular.ts";
|
|
27
|
+
|
|
28
|
+
export type KnobRef = string | undefined;
|
|
29
|
+
export type KnobMap = readonly [KnobRef, KnobRef, KnobRef, KnobRef];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pages, by id. `sound:<family>` rows use each engine's own param names (the
|
|
33
|
+
* rows its Sound menu shows); orange is the track level on every sound page,
|
|
34
|
+
* so ◆ always means "how loud".
|
|
35
|
+
*/
|
|
36
|
+
export const KNOB_MAPS: Readonly<Record<string, KnobMap>> = Object.freeze({
|
|
37
|
+
"sound:synth": [
|
|
38
|
+
"sound/preset",
|
|
39
|
+
"sound/attack",
|
|
40
|
+
"sound/synth filter",
|
|
41
|
+
"mix/volume",
|
|
42
|
+
],
|
|
43
|
+
"sound:wavetable": [
|
|
44
|
+
"sound/position (wt)",
|
|
45
|
+
"sound/attack",
|
|
46
|
+
"sound/synth filter",
|
|
47
|
+
"mix/volume",
|
|
48
|
+
],
|
|
49
|
+
"sound:piano": [
|
|
50
|
+
"sound/preset",
|
|
51
|
+
"sound/decay",
|
|
52
|
+
"sound/hardness",
|
|
53
|
+
"mix/volume",
|
|
54
|
+
],
|
|
55
|
+
"sound:electric": ["sound/preset", "sound/bell", "sound/tone", "mix/volume"],
|
|
56
|
+
"sound:organ": ["sound/preset", "sound/perc", "sound/drive", "mix/volume"],
|
|
57
|
+
"sound:string": [
|
|
58
|
+
"sound/preset",
|
|
59
|
+
"sound/ring s",
|
|
60
|
+
"sound/bright",
|
|
61
|
+
"mix/volume",
|
|
62
|
+
],
|
|
63
|
+
"sound:wind": [
|
|
64
|
+
"sound/breath",
|
|
65
|
+
"sound/wind:advanced/vib",
|
|
66
|
+
"sound/bright",
|
|
67
|
+
"mix/volume",
|
|
68
|
+
],
|
|
69
|
+
"sound:modal": [
|
|
70
|
+
"sound/position",
|
|
71
|
+
"sound/ring",
|
|
72
|
+
"sound/hardness",
|
|
73
|
+
"mix/volume",
|
|
74
|
+
],
|
|
75
|
+
"sound:sing": ["sound/vowel", "sound/voices", "sound/bright", "mix/volume"],
|
|
76
|
+
"sound:granular": [
|
|
77
|
+
"sound/granular/position",
|
|
78
|
+
"sound/granular/grain (s)",
|
|
79
|
+
"sound/granular/spray (s)",
|
|
80
|
+
"mix/volume",
|
|
81
|
+
],
|
|
82
|
+
// `sample:*` is the track's first sample voice.
|
|
83
|
+
"sound:sampler": [
|
|
84
|
+
"sound/sample:*/begin",
|
|
85
|
+
"sound/sample:*/len",
|
|
86
|
+
"sound/sample:*/speed",
|
|
87
|
+
"mix/volume",
|
|
88
|
+
],
|
|
89
|
+
// A kit has no voice params: blue loosens the timing, green the room it
|
|
90
|
+
// plays in, white its tone.
|
|
91
|
+
"sound:kit": [
|
|
92
|
+
"sound/performance/humanize timing (ms)",
|
|
93
|
+
"effects/reverb/mix",
|
|
94
|
+
"effects/filter/cutoff",
|
|
95
|
+
"mix/volume",
|
|
96
|
+
],
|
|
97
|
+
// The mixer page: this track's pan, reverb send, tone and level.
|
|
98
|
+
mix: ["mix/pan", "effects/reverb/mix", "effects/filter/cutoff", "mix/volume"],
|
|
99
|
+
master: [
|
|
100
|
+
"mix/master/master:eq/low",
|
|
101
|
+
"mix/master/master:eq/bell1",
|
|
102
|
+
"mix/master/master:eq/high",
|
|
103
|
+
"mix/master/target LUFS",
|
|
104
|
+
],
|
|
105
|
+
tempo: [
|
|
106
|
+
"project/tempo",
|
|
107
|
+
"project/loop length",
|
|
108
|
+
"project/beats per bar",
|
|
109
|
+
undefined,
|
|
110
|
+
],
|
|
111
|
+
// Effects: time or rate, feedback / size / depth, tone, mix.
|
|
112
|
+
"fx:filter": [
|
|
113
|
+
"effects/filter/type",
|
|
114
|
+
"effects/filter/resonance",
|
|
115
|
+
"effects/filter/cutoff",
|
|
116
|
+
undefined,
|
|
117
|
+
],
|
|
118
|
+
"fx:delay": [
|
|
119
|
+
"effects/delay/beats",
|
|
120
|
+
"effects/delay/feedback",
|
|
121
|
+
undefined,
|
|
122
|
+
"effects/delay/mix",
|
|
123
|
+
],
|
|
124
|
+
"fx:reverb": [
|
|
125
|
+
"effects/reverb/impulse (ir)",
|
|
126
|
+
"effects/reverb/size",
|
|
127
|
+
undefined,
|
|
128
|
+
"effects/reverb/mix",
|
|
129
|
+
],
|
|
130
|
+
"fx:chorus": [
|
|
131
|
+
"effects/chorus/rate",
|
|
132
|
+
"effects/chorus/depth",
|
|
133
|
+
undefined,
|
|
134
|
+
"effects/chorus/mix",
|
|
135
|
+
],
|
|
136
|
+
/**
|
|
137
|
+
* The Euclid editor (design §8.7): its row's own fields, not menu paths.
|
|
138
|
+
* Blue picks the drum row; ←→ on it moves between rows.
|
|
139
|
+
*/
|
|
140
|
+
euclid: ["drum", "pulses", "rotate", "velocity"],
|
|
141
|
+
/**
|
|
142
|
+
* TAPE (design §7.3): its own values, not menu paths. Blue the playhead
|
|
143
|
+
* (a beat; ⇧ a bar), green the loop length, white the tempo of the
|
|
144
|
+
* segment under the playhead, orange the focused track's volume.
|
|
145
|
+
*/
|
|
146
|
+
tape: ["playhead", "loop", "tempo", "volume"],
|
|
147
|
+
"fx:distort": [
|
|
148
|
+
undefined,
|
|
149
|
+
"effects/distort/drive",
|
|
150
|
+
"effects/distort/tone",
|
|
151
|
+
"effects/distort/mix",
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/** The sound page's family for a track: which engine its Sound menu shows. */
|
|
156
|
+
export function soundFamily(track: Track): string {
|
|
157
|
+
if (isDrumInstrument(track.instrument)) return "kit";
|
|
158
|
+
if (track.sampler) return "sampler";
|
|
159
|
+
if (isGranularInstrument(track.instrument)) return "granular";
|
|
160
|
+
if (isOrganFamily(track.instrument) && track.keys) return "organ";
|
|
161
|
+
if (isKeysFamily(track.instrument) && track.keys)
|
|
162
|
+
return isElectricFamily(track.instrument) ? "electric" : "piano";
|
|
163
|
+
if (isStringTrack(track)) return "string";
|
|
164
|
+
if (track.instrument === "modal") return "modal";
|
|
165
|
+
if (track.instrument === "wind" && track.wind) return "wind";
|
|
166
|
+
if (track.instrument === "sing" && track.sing) return "sing";
|
|
167
|
+
if (isWavetableInstrument(track.instrument)) return "wavetable";
|
|
168
|
+
return "synth";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The page id for `knobs <page>`, or undefined when no page has that name. */
|
|
172
|
+
export function knobPageId(
|
|
173
|
+
word: string | undefined,
|
|
174
|
+
track: Track | undefined,
|
|
175
|
+
): string | undefined {
|
|
176
|
+
const name = (word ?? "sound").trim().toLowerCase().replace(/\s+/g, " ");
|
|
177
|
+
if (name === "sound" || name === "")
|
|
178
|
+
return `sound:${track ? soundFamily(track) : "synth"}`;
|
|
179
|
+
if (name === "mix" || name === "master" || name === "tempo") return name;
|
|
180
|
+
const fx = name.match(/^(?:fx|effect|effects)\s+(\S+)$/)?.[1];
|
|
181
|
+
if (fx) return `fx:${fx}`;
|
|
182
|
+
if (`sound:${name}` in KNOB_MAPS) return `sound:${name}`;
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The page's row; an unmapped effect gets none (the menu derives one). */
|
|
187
|
+
export function knobMap(page: string): KnobMap | undefined {
|
|
188
|
+
return KNOB_MAPS[page];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The pages `knobs` names, for help and completion. */
|
|
192
|
+
export const KNOB_PAGE_WORDS = Object.freeze([
|
|
193
|
+
"sound",
|
|
194
|
+
"mix",
|
|
195
|
+
"master",
|
|
196
|
+
"tempo",
|
|
197
|
+
"fx <effect>",
|
|
198
|
+
]);
|