@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/menu.ts
CHANGED
|
@@ -8,19 +8,47 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Rendering reuses the TUI's picker overlay: `view()` returns a picker.
|
|
10
10
|
*/
|
|
11
|
+
import { TOPIC_ALIASES } from "../lang/glossary.ts";
|
|
12
|
+
import {
|
|
13
|
+
voiceEffectRows,
|
|
14
|
+
voiceRootDetail,
|
|
15
|
+
voiceRootNodes,
|
|
16
|
+
} from "./menu-voice.ts";
|
|
11
17
|
import { commandParam, sketchFor } from "./sketch.ts";
|
|
12
|
-
import {
|
|
18
|
+
import { nearest } from "../commands/nearest.ts";
|
|
19
|
+
import {
|
|
20
|
+
ARROW_DOWN,
|
|
21
|
+
ARROW_UP,
|
|
22
|
+
HINTS,
|
|
23
|
+
KEY_BACK,
|
|
24
|
+
KEY_BACKSPACE,
|
|
25
|
+
KEY_DOWN,
|
|
26
|
+
KEY_END,
|
|
27
|
+
KEY_ENTER,
|
|
28
|
+
KEY_FORWARD,
|
|
29
|
+
KEY_HOME,
|
|
30
|
+
KEY_LEFT,
|
|
31
|
+
KEY_PAGE_DOWN,
|
|
32
|
+
KEY_PAGE_UP,
|
|
33
|
+
KEY_RESET,
|
|
34
|
+
KEY_RIGHT,
|
|
35
|
+
KEY_UP,
|
|
36
|
+
} from "../../tui/grammar.ts";
|
|
13
37
|
import { arrangeDetail, arrangeNodes } from "./arrange-menu.ts";
|
|
14
38
|
import { auditionKey, isStageable, type AuditionKey } from "./audition.ts";
|
|
15
39
|
import { performanceDetail, performanceNodes } from "./performance-menu.ts";
|
|
16
40
|
import { malletsMenu, modalParameterNodes } from "./modal-menu.ts";
|
|
17
41
|
import { windParameterNodes, windsMenu } from "./wind-menu.ts";
|
|
42
|
+
import { singParameterNodes } from "./sing-menu.ts";
|
|
18
43
|
import type { FaderSpec } from "./fader.ts";
|
|
19
44
|
import {
|
|
20
45
|
openingMeterCommand,
|
|
21
46
|
openingUnit,
|
|
22
47
|
tempoDetail,
|
|
23
48
|
tempoMenuNode,
|
|
49
|
+
START_BARS,
|
|
50
|
+
START_BEATS_PER_BAR,
|
|
51
|
+
START_TEMPO_BPM,
|
|
24
52
|
} from "./menu-time.ts";
|
|
25
53
|
import {
|
|
26
54
|
AUTOMATION_PARAMETERS,
|
|
@@ -40,6 +68,8 @@ import {
|
|
|
40
68
|
type Track,
|
|
41
69
|
type TrackAutomationParameter,
|
|
42
70
|
type TrackScore,
|
|
71
|
+
CALIBRATION_LATEST,
|
|
72
|
+
createScore,
|
|
43
73
|
} from "../../core/score.ts";
|
|
44
74
|
import {
|
|
45
75
|
CORE_EFFECTS,
|
|
@@ -49,6 +79,7 @@ import {
|
|
|
49
79
|
RIG_STAGES,
|
|
50
80
|
rigPresetOf,
|
|
51
81
|
SHOEGAZE_EFFECTS,
|
|
82
|
+
VOWEL_VALUES,
|
|
52
83
|
effectPresetNames,
|
|
53
84
|
effectSpec,
|
|
54
85
|
type EffectName,
|
|
@@ -176,6 +207,10 @@ export type MenuContext = Readonly<{
|
|
|
176
207
|
grids: readonly string[];
|
|
177
208
|
clickOn: boolean;
|
|
178
209
|
countInBars: number;
|
|
210
|
+
/** How much agent turns show (`/showme`); absent hides the row. */
|
|
211
|
+
showMe?: string;
|
|
212
|
+
/** The session's name, for Project › session and export file names. */
|
|
213
|
+
sessionName?: string;
|
|
179
214
|
/** Play mode's chord settings (defaults when absent). */
|
|
180
215
|
chords?: ChordSettings;
|
|
181
216
|
/** Project root, for the project's own wavetables (none when absent). */
|
|
@@ -193,6 +228,8 @@ export type MenuAudition = Readonly<{
|
|
|
193
228
|
looping: boolean;
|
|
194
229
|
/** Edits are staged (an Enter keeps them, Esc reverts them). */
|
|
195
230
|
dirty: boolean;
|
|
231
|
+
/** How many edits are staged, for the drawer's A/B badge. */
|
|
232
|
+
staged?: number;
|
|
196
233
|
/** The committed score, for `staged ← committed` on changed rows. */
|
|
197
234
|
committed: TrackScore;
|
|
198
235
|
/** The footer while auditioning. */
|
|
@@ -361,7 +398,7 @@ const TRACK_LANE_LABEL: Readonly<Record<TrackAutomationParameter, string>> = {
|
|
|
361
398
|
resonance: "filter resonance",
|
|
362
399
|
"delay-feedback": "delay feedback",
|
|
363
400
|
"delay-mix": "delay mix",
|
|
364
|
-
wt: "
|
|
401
|
+
wt: "table position",
|
|
365
402
|
};
|
|
366
403
|
|
|
367
404
|
const FX_LANE_INFO = new Map(FX_LANES.map((entry) => [entry.lane, entry]));
|
|
@@ -405,7 +442,9 @@ function laneLabel(lane: AutomationParameter): string {
|
|
|
405
442
|
info.effect === "keys" ||
|
|
406
443
|
info.effect === "modal" ||
|
|
407
444
|
info.effect === "grain" ||
|
|
408
|
-
info.effect === "wind"
|
|
445
|
+
info.effect === "wind" ||
|
|
446
|
+
info.effect === "sing" ||
|
|
447
|
+
info.effect === "vocoder"
|
|
409
448
|
? info.effect
|
|
410
449
|
: effectSpec(info.effect).label;
|
|
411
450
|
return `${owner} ${info.param}${unit}`;
|
|
@@ -439,9 +478,17 @@ export function rootNodes(context: MenuContext): MenuNode[] {
|
|
|
439
478
|
id: "sound",
|
|
440
479
|
label: "Sound",
|
|
441
480
|
detail: `${name} · ${track?.instrument ?? "?"}`,
|
|
442
|
-
help: "the focused track's instrument and
|
|
481
|
+
help: "the focused track's instrument, its controls and performance",
|
|
443
482
|
build: soundSectionNodes,
|
|
444
483
|
},
|
|
484
|
+
{
|
|
485
|
+
kind: "menu",
|
|
486
|
+
id: "voice",
|
|
487
|
+
label: "Voice",
|
|
488
|
+
detail: voiceRootDetail(context),
|
|
489
|
+
help: "sing, lyrics, clips, pitch, autotune, formant and vocoder",
|
|
490
|
+
build: voiceRootNodes,
|
|
491
|
+
},
|
|
445
492
|
{
|
|
446
493
|
kind: "menu",
|
|
447
494
|
id: "effects",
|
|
@@ -454,26 +501,34 @@ export function rootNodes(context: MenuContext): MenuNode[] {
|
|
|
454
501
|
kind: "menu",
|
|
455
502
|
id: "rhythm",
|
|
456
503
|
label: "Rhythm",
|
|
457
|
-
detail: "euclid editor ·
|
|
458
|
-
help: "
|
|
504
|
+
detail: "euclid editor · grooves · kits",
|
|
505
|
+
help: "hits per drum, ready-made grooves and kits",
|
|
459
506
|
build: rhythmNodes,
|
|
460
507
|
},
|
|
461
508
|
{
|
|
462
509
|
kind: "menu",
|
|
463
510
|
id: "chords",
|
|
464
|
-
label: "Chords",
|
|
511
|
+
label: "Chords and key",
|
|
465
512
|
detail: chordsDetail(context),
|
|
466
|
-
help: "song key and how chords play in play mode (Ctrl-P)",
|
|
513
|
+
help: "the song key and tuning, and how chords play in play mode (Ctrl-P)",
|
|
467
514
|
build: chordNodes,
|
|
468
515
|
},
|
|
469
516
|
{
|
|
470
517
|
kind: "menu",
|
|
471
518
|
id: "mix",
|
|
472
|
-
label: "Mix
|
|
519
|
+
label: "Mix",
|
|
473
520
|
detail: `${tracks} track${tracks === 1 ? "" : "s"} · ${automated} lane${automated === 1 ? "" : "s"} moving`,
|
|
474
|
-
help: "volume, pan, mute and solo per track
|
|
521
|
+
help: "volume, pan, mute and solo per track, automation and the master",
|
|
475
522
|
build: mixSectionNodes,
|
|
476
523
|
},
|
|
524
|
+
{
|
|
525
|
+
kind: "menu",
|
|
526
|
+
id: "arrange",
|
|
527
|
+
label: "Arrange",
|
|
528
|
+
detail: arrangeDetail(context),
|
|
529
|
+
help: "tracks, song sections, the form, builds, drops, fills and styles",
|
|
530
|
+
build: arrangeRootNodes,
|
|
531
|
+
},
|
|
477
532
|
{
|
|
478
533
|
kind: "menu",
|
|
479
534
|
id: "project",
|
|
@@ -481,47 +536,92 @@ export function rootNodes(context: MenuContext): MenuNode[] {
|
|
|
481
536
|
detail: context.score.time
|
|
482
537
|
? `${tempoDetail(context.score)} · ${context.score.bars} bars`
|
|
483
538
|
: `${num(context.score.tempoBpm)} BPM · ${context.score.beatsPerBar}/4 · ${context.score.bars} bars`,
|
|
484
|
-
help: "tempo, meter,
|
|
539
|
+
help: "play, tempo, meter, loop, click, export, session, agent and help",
|
|
485
540
|
build: transportNodes,
|
|
486
541
|
},
|
|
487
|
-
{
|
|
488
|
-
kind: "menu",
|
|
489
|
-
id: "arrange",
|
|
490
|
-
label: "Arrange",
|
|
491
|
-
detail: arrangeDetail(context),
|
|
492
|
-
help: "song sections, the form, builds, drops and fills",
|
|
493
|
-
build: arrangeNodes,
|
|
494
|
-
},
|
|
495
542
|
];
|
|
496
543
|
}
|
|
497
544
|
|
|
498
|
-
/**
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
545
|
+
/**
|
|
546
|
+
* Where each `/menu <id>` opens: the ten topic ids (sound voice effects
|
|
547
|
+
* rhythm chords mix arrange project keys agent) first, then every older id,
|
|
548
|
+
* as a path of menu ids from the root. `keys` opens Project › help and
|
|
549
|
+
* guides; the ? panel itself belongs to the window.
|
|
550
|
+
*/
|
|
551
|
+
export const SECTION_ALIASES: Readonly<Record<string, readonly string[]>> =
|
|
552
|
+
Object.freeze(
|
|
553
|
+
withTopicAliases({
|
|
554
|
+
sound: ["sound"],
|
|
555
|
+
voice: ["voice"],
|
|
556
|
+
effects: ["effects"],
|
|
557
|
+
rhythm: ["rhythm"],
|
|
558
|
+
chords: ["chords"],
|
|
559
|
+
mix: ["mix"],
|
|
560
|
+
arrange: ["arrange"],
|
|
561
|
+
project: ["project"],
|
|
562
|
+
keys: ["project", "help"],
|
|
563
|
+
agent: ["project", "agent"],
|
|
564
|
+
// Older ids, kept as aliases.
|
|
565
|
+
parameters: ["sound"],
|
|
566
|
+
sounds: ["sound", "browse"],
|
|
567
|
+
instruments: ["sound", "browse"],
|
|
568
|
+
performance: ["sound", "performance"],
|
|
569
|
+
expression: ["sound", "performance"],
|
|
570
|
+
clips: ["voice"],
|
|
571
|
+
lyrics: ["voice"],
|
|
572
|
+
autotune: ["voice"],
|
|
573
|
+
sing: ["voice"],
|
|
574
|
+
vocoder: ["voice"],
|
|
575
|
+
formant: ["voice"],
|
|
576
|
+
grooves: ["rhythm", "patterns"],
|
|
577
|
+
groove: ["rhythm", "patterns"],
|
|
578
|
+
patterns: ["rhythm", "patterns"],
|
|
579
|
+
kits: ["rhythm", "kits"],
|
|
580
|
+
euclid: ["rhythm"],
|
|
581
|
+
tuning: ["chords", "tuning"],
|
|
582
|
+
scale: ["chords"],
|
|
583
|
+
key: ["chords"],
|
|
584
|
+
track: ["mix"],
|
|
585
|
+
automation: ["mix", "automation"],
|
|
586
|
+
master: ["mix", "master"],
|
|
587
|
+
tracks: ["arrange", "tracks"],
|
|
588
|
+
music: ["arrange"],
|
|
589
|
+
style: ["arrange", "style"],
|
|
590
|
+
styles: ["arrange", "style"],
|
|
591
|
+
genre: ["arrange", "style"],
|
|
592
|
+
sections: ["arrange", "sections"],
|
|
593
|
+
form: ["arrange"],
|
|
594
|
+
transport: ["project"],
|
|
595
|
+
tempo: ["project", "tempo"],
|
|
596
|
+
meter: ["project", "tempo"],
|
|
597
|
+
time: ["project", "tempo"],
|
|
598
|
+
export: ["project", "export"],
|
|
599
|
+
session: ["project", "session"],
|
|
600
|
+
sessions: ["project", "session"],
|
|
601
|
+
window: ["project", "help"],
|
|
602
|
+
help: ["project", "help"],
|
|
603
|
+
guides: ["project", "help"],
|
|
604
|
+
model: ["project", "agent"],
|
|
605
|
+
showme: ["project", "agent"],
|
|
606
|
+
models: ["project", "agent"],
|
|
607
|
+
fx: ["effects"],
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Every glossary topic alias (`/help drums`, `/guide mixer`) opens the same
|
|
613
|
+
* topic in `/menu` too: a word with no deeper path here opens its topic.
|
|
614
|
+
*/
|
|
615
|
+
function withTopicAliases(
|
|
616
|
+
paths: Record<string, readonly string[]>,
|
|
617
|
+
): Record<string, readonly string[]> {
|
|
618
|
+
const all = { ...paths };
|
|
619
|
+
for (const [word, topic] of Object.entries(TOPIC_ALIASES))
|
|
620
|
+
if (!all[word] && all[topic]) all[word] = all[topic]!;
|
|
621
|
+
return all;
|
|
622
|
+
}
|
|
523
623
|
|
|
524
|
-
/** Sound: instrument and
|
|
624
|
+
/** Sound: the instrument and its controls first, then the instruments. */
|
|
525
625
|
function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
526
626
|
const track = focused(context);
|
|
527
627
|
const tuning: MenuNode[] =
|
|
@@ -530,7 +630,7 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
530
630
|
{
|
|
531
631
|
kind: "menu",
|
|
532
632
|
id: "track-tuning",
|
|
533
|
-
label: "tuning",
|
|
633
|
+
label: "track tuning",
|
|
534
634
|
detail: track.tuning
|
|
535
635
|
? tuningLabel(track.tuning)
|
|
536
636
|
: `song · ${tuningLabel(context.score.tuning)}`,
|
|
@@ -561,7 +661,7 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
561
661
|
id: "keys",
|
|
562
662
|
label: "keys",
|
|
563
663
|
detail: `${track.instrument}${track.keys.preset ? ` · ${track.keys.preset}` : ""}`,
|
|
564
|
-
help: "the
|
|
664
|
+
help: "the modeled organ: preset, drawbars, registers or stops, rotary",
|
|
565
665
|
build: (inner) => {
|
|
566
666
|
const current = focused(inner);
|
|
567
667
|
return current ? organNodes(current) : [];
|
|
@@ -577,7 +677,7 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
577
677
|
detail: `${track.instrument}${track.keys.preset ? ` · ${track.keys.preset}` : ""}`,
|
|
578
678
|
help: isElectricFamily(track.instrument)
|
|
579
679
|
? "electric keys: preset, bark, bell, tone, vibe/trem, pickup and mute"
|
|
580
|
-
: "the
|
|
680
|
+
: "the modeled piano: preset, touch, hammers, dampers, stretch",
|
|
581
681
|
build: (inner) => {
|
|
582
682
|
const current = focused(inner);
|
|
583
683
|
return current
|
|
@@ -622,9 +722,9 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
622
722
|
{
|
|
623
723
|
kind: "menu",
|
|
624
724
|
id: "browse",
|
|
625
|
-
label: "
|
|
725
|
+
label: "instruments",
|
|
626
726
|
detail: soundsDetail(focused(context)),
|
|
627
|
-
help: "
|
|
727
|
+
help: "every instrument: keys, strings, mallets, winds, granular, wavetables, sample packs",
|
|
628
728
|
build: soundNodes,
|
|
629
729
|
},
|
|
630
730
|
];
|
|
@@ -709,20 +809,262 @@ function guitarNodes(track: Track | undefined): MenuNode[] {
|
|
|
709
809
|
}
|
|
710
810
|
|
|
711
811
|
/** Rhythm: the euclid editor, then grooves and kits. */
|
|
712
|
-
function rhythmNodes(): MenuNode[] {
|
|
812
|
+
function rhythmNodes(context: MenuContext): MenuNode[] {
|
|
713
813
|
return [
|
|
714
814
|
{
|
|
715
815
|
kind: "action",
|
|
716
816
|
label: "euclid editor",
|
|
717
817
|
command: "/euclid",
|
|
718
|
-
help: "hits, steps and rotation per drum
|
|
818
|
+
help: "hits, steps and rotation per drum",
|
|
719
819
|
},
|
|
720
820
|
patternsMenu(),
|
|
721
821
|
kitsMenu(),
|
|
822
|
+
gridNode(context),
|
|
823
|
+
];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** The record and euclid grid (Rhythm and Project share this row). */
|
|
827
|
+
function gridNode(context: MenuContext): MenuNode {
|
|
828
|
+
return {
|
|
829
|
+
kind: "choice",
|
|
830
|
+
label: "grid",
|
|
831
|
+
help: "the step play-mode recording and the euclid editor snap to",
|
|
832
|
+
value: context.grid,
|
|
833
|
+
options: context.grids,
|
|
834
|
+
command: (option) => `/grid ${option}`,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Arrange: the tracks first, then sections, form and styles. */
|
|
839
|
+
function arrangeRootNodes(context: MenuContext): MenuNode[] {
|
|
840
|
+
const count = context.score.tracks.length;
|
|
841
|
+
return [
|
|
842
|
+
{
|
|
843
|
+
kind: "menu",
|
|
844
|
+
id: "tracks",
|
|
845
|
+
label: "tracks",
|
|
846
|
+
detail: `${count} track${count === 1 ? "" : "s"} · add, focus, move, remove`,
|
|
847
|
+
help: "add a track, focus one, rename, reorder or remove it",
|
|
848
|
+
build: tracksNodes,
|
|
849
|
+
},
|
|
850
|
+
...arrangeNodes(context),
|
|
851
|
+
];
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** Arrange › tracks: add, focus, rename, move and remove. */
|
|
855
|
+
function tracksNodes(context: MenuContext): MenuNode[] {
|
|
856
|
+
const score = context.score;
|
|
857
|
+
const track = focused(context);
|
|
858
|
+
const nodes: MenuNode[] = [
|
|
859
|
+
{
|
|
860
|
+
kind: "entry",
|
|
861
|
+
label: "add a track",
|
|
862
|
+
value: "",
|
|
863
|
+
placeholder: "name, e.g. bass or piano b",
|
|
864
|
+
example: "/track bass",
|
|
865
|
+
help: "a new track with that name (or focus it when it exists)",
|
|
866
|
+
command: (text) => {
|
|
867
|
+
const name = text.trim().replace(/\s+/g, " ");
|
|
868
|
+
return /^[a-z0-9._ -]{1,64}$/i.test(name)
|
|
869
|
+
? `/track ${name}`
|
|
870
|
+
: undefined;
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
...score.tracks.map((item): MenuNode => ({
|
|
874
|
+
kind: "action",
|
|
875
|
+
label: `${item.id === context.trackId ? "● " : ""}${item.name ?? item.id}`,
|
|
876
|
+
command: `/track ${item.id}`,
|
|
877
|
+
help: `focus ${item.id} · ${item.instrument}`,
|
|
878
|
+
})),
|
|
879
|
+
];
|
|
880
|
+
if (track) {
|
|
881
|
+
nodes.push({
|
|
882
|
+
kind: "entry",
|
|
883
|
+
label: "rename",
|
|
884
|
+
value: track.name,
|
|
885
|
+
placeholder: "new name",
|
|
886
|
+
command: (text) =>
|
|
887
|
+
text.trim() ? `track name ${text.trim()}` : undefined,
|
|
888
|
+
example: `track name ${track.name}`,
|
|
889
|
+
help: "rename the focused track",
|
|
890
|
+
});
|
|
891
|
+
if (score.tracks.length > 1) {
|
|
892
|
+
const index = score.tracks.findIndex((item) => item.id === track.id);
|
|
893
|
+
nodes.push(
|
|
894
|
+
{
|
|
895
|
+
kind: "number",
|
|
896
|
+
label: "position",
|
|
897
|
+
help: "where the focused track sits in the list",
|
|
898
|
+
value: index + 1,
|
|
899
|
+
min: 1,
|
|
900
|
+
max: score.tracks.length,
|
|
901
|
+
step: linear(1, 1, score.tracks.length),
|
|
902
|
+
format: (value) => `${num(value)} of ${score.tracks.length}`,
|
|
903
|
+
command: (value) => `/track move ${track.id} ${Math.round(value)}`,
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
kind: "action",
|
|
907
|
+
label: `remove ${track.id}`,
|
|
908
|
+
command: `/track remove ${track.id}`,
|
|
909
|
+
help: "remove the focused track (^z brings it back)",
|
|
910
|
+
},
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return nodes;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** A file-safe stem for export names: the session's song name or `song`. */
|
|
918
|
+
function exportStem(context: MenuContext): string {
|
|
919
|
+
const stem = (context.sessionName ?? "song")
|
|
920
|
+
.toLowerCase()
|
|
921
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
922
|
+
.replace(/^-+|-+$/g, "")
|
|
923
|
+
.slice(0, 40);
|
|
924
|
+
return stem || "song";
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Project › export: the project file and MIDI; audio renders offline. */
|
|
928
|
+
function exportNodes(context: MenuContext): MenuNode[] {
|
|
929
|
+
const stem = exportStem(context);
|
|
930
|
+
return [
|
|
931
|
+
{
|
|
932
|
+
kind: "entry",
|
|
933
|
+
label: "project file",
|
|
934
|
+
value: "",
|
|
935
|
+
placeholder: `file name, e.g. ${stem}.track.json`,
|
|
936
|
+
example: `/export ${stem}.track.json`,
|
|
937
|
+
help: "the whole song as one .track.json file (open it with /import)",
|
|
938
|
+
command: (text) => {
|
|
939
|
+
const name = text.trim() || `${stem}.track.json`;
|
|
940
|
+
return /^\S+$/.test(name)
|
|
941
|
+
? `/export ${/\.json$/i.test(name) ? name : `${name}.track.json`}`
|
|
942
|
+
: undefined;
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
kind: "entry",
|
|
947
|
+
label: "MIDI",
|
|
948
|
+
value: "",
|
|
949
|
+
placeholder: `file name, e.g. ${stem}.mid`,
|
|
950
|
+
example: `/export ${stem}.mid`,
|
|
951
|
+
help: "notes, tempo and tracks as a standard MIDI file",
|
|
952
|
+
command: (text) => {
|
|
953
|
+
const name = text.trim() || `${stem}.mid`;
|
|
954
|
+
return /^\S+$/.test(name)
|
|
955
|
+
? `/export ${/\.midi?$/i.test(name) ? name : `${name}.mid`}`
|
|
956
|
+
: undefined;
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
kind: "info",
|
|
961
|
+
label: "WAV and stems",
|
|
962
|
+
value: `dawg render ${stem}.wav`,
|
|
963
|
+
help: "audio renders offline in the shell: dawg render out.wav (--stems for one file per track)",
|
|
964
|
+
},
|
|
965
|
+
];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** Project › session: rename, fork and resume (the window's verbs). */
|
|
969
|
+
function sessionNodes(context: MenuContext): MenuNode[] {
|
|
970
|
+
return [
|
|
971
|
+
{
|
|
972
|
+
kind: "entry",
|
|
973
|
+
label: "rename",
|
|
974
|
+
value: context.sessionName ?? "",
|
|
975
|
+
placeholder: "song name (--auto names it for you)",
|
|
976
|
+
example: "/rename night drive",
|
|
977
|
+
help: "name this session; --auto hands naming back to dawg",
|
|
978
|
+
command: (text) => (text.trim() ? `/rename ${text.trim()}` : undefined),
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
kind: "action",
|
|
982
|
+
label: "fork",
|
|
983
|
+
command: "/fork",
|
|
984
|
+
help: "a copy of this session to try something; the original stays",
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
kind: "action",
|
|
988
|
+
label: "resume",
|
|
989
|
+
command: "/resume",
|
|
990
|
+
help: "pick an earlier session in this folder",
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
kind: "action",
|
|
994
|
+
label: "list sessions",
|
|
995
|
+
command: "/sessions",
|
|
996
|
+
help: "every session in this folder, newest first",
|
|
997
|
+
},
|
|
998
|
+
];
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Project › agent: the model, show-me and the model key. */
|
|
1002
|
+
function agentNodes(context: MenuContext): MenuNode[] {
|
|
1003
|
+
return [
|
|
1004
|
+
{
|
|
1005
|
+
kind: "action",
|
|
1006
|
+
label: "model",
|
|
1007
|
+
command: "/model",
|
|
1008
|
+
help: "pick the model the agent runs on",
|
|
1009
|
+
},
|
|
1010
|
+
...(context.showMe === undefined
|
|
1011
|
+
? []
|
|
1012
|
+
: [
|
|
1013
|
+
{
|
|
1014
|
+
kind: "choice" as const,
|
|
1015
|
+
label: "show me",
|
|
1016
|
+
help: "agent turns: its commands as ghost text, faders, keys",
|
|
1017
|
+
value: context.showMe,
|
|
1018
|
+
options: ["on", "quiet", "off"],
|
|
1019
|
+
command: (option: string) => `/showme ${option}`,
|
|
1020
|
+
},
|
|
1021
|
+
]),
|
|
1022
|
+
{
|
|
1023
|
+
kind: "action",
|
|
1024
|
+
label: "model key",
|
|
1025
|
+
command: "/model key",
|
|
1026
|
+
help: "add a model key so typed requests reach the agent (dawg works offline without it)",
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
kind: "action",
|
|
1030
|
+
label: "agent help",
|
|
1031
|
+
command: "/help agent",
|
|
1032
|
+
help: "what the agent can do and how its turns show",
|
|
1033
|
+
},
|
|
1034
|
+
];
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** Project › help and guides: the reference, guides and keys. */
|
|
1038
|
+
function helpNodes(): MenuNode[] {
|
|
1039
|
+
return [
|
|
1040
|
+
{
|
|
1041
|
+
kind: "action",
|
|
1042
|
+
label: "help",
|
|
1043
|
+
command: "/help",
|
|
1044
|
+
help: "start here: the ten topics",
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
kind: "action",
|
|
1048
|
+
label: "all commands",
|
|
1049
|
+
command: "/help all",
|
|
1050
|
+
help: "the full command reference",
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
kind: "action",
|
|
1054
|
+
label: "guides",
|
|
1055
|
+
command: "/guide",
|
|
1056
|
+
help: "short walkthroughs, one screen each",
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
kind: "action",
|
|
1060
|
+
label: "keys",
|
|
1061
|
+
command: "/help keys",
|
|
1062
|
+
help: "every key in the window (? shows them too)",
|
|
1063
|
+
},
|
|
722
1064
|
];
|
|
723
1065
|
}
|
|
724
1066
|
|
|
725
|
-
/** Mix
|
|
1067
|
+
/** Mix: this track, every track, automation lanes, then the master. */
|
|
726
1068
|
function mixSectionNodes(context: MenuContext): MenuNode[] {
|
|
727
1069
|
const track = focused(context);
|
|
728
1070
|
const automated = AUTOMATION_PARAMETERS.filter(
|
|
@@ -759,7 +1101,7 @@ function mixSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
759
1101
|
];
|
|
760
1102
|
}
|
|
761
1103
|
|
|
762
|
-
/** Mix
|
|
1104
|
+
/** Mix › master: the loudness target, then each unit in order. */
|
|
763
1105
|
function masterNodes(context: MenuContext): MenuNode[] {
|
|
764
1106
|
const master = context.score.master;
|
|
765
1107
|
// A name only when the LUFS and the limiter ceiling both match it, and no
|
|
@@ -932,7 +1274,7 @@ function chordsDetail(context: MenuContext): string {
|
|
|
932
1274
|
return `${chords.mode} · ${key ? keyName(key) : "no key"} · ${chords.perform}`;
|
|
933
1275
|
}
|
|
934
1276
|
|
|
935
|
-
/**
|
|
1277
|
+
/** Chords and key: the song key and tuning, then play mode's chord settings. */
|
|
936
1278
|
function chordNodes(context: MenuContext): MenuNode[] {
|
|
937
1279
|
const chords = context.chords ?? defaultChordSettings();
|
|
938
1280
|
const key = parseKey(context.score.key ?? undefined);
|
|
@@ -965,11 +1307,20 @@ function chordNodes(context: MenuContext): MenuNode[] {
|
|
|
965
1307
|
options: [...MODE_NAMES, ...SCALE_NAMES],
|
|
966
1308
|
command: (option) => `key ${tonic} ${option}`,
|
|
967
1309
|
},
|
|
1310
|
+
{
|
|
1311
|
+
kind: "menu",
|
|
1312
|
+
id: "tuning",
|
|
1313
|
+
label: "tuning",
|
|
1314
|
+
detail: `${tuningLabel(context.score.tuning)} · ${keyLabel(context.score.key)}`,
|
|
1315
|
+
help: "the song tuning: 12-TET, EDOs, just, gamelan, Scala, and the scale",
|
|
1316
|
+
build: songTuningNodes,
|
|
1317
|
+
},
|
|
968
1318
|
{
|
|
969
1319
|
kind: "number",
|
|
970
1320
|
label: "voicing",
|
|
971
1321
|
help: "inversion steps up or down (play mode: - =)",
|
|
972
1322
|
value: chords.inversion,
|
|
1323
|
+
start: 0,
|
|
973
1324
|
min: -MAX_VOICING_STEP,
|
|
974
1325
|
max: MAX_VOICING_STEP,
|
|
975
1326
|
step: linear(1, -MAX_VOICING_STEP, MAX_VOICING_STEP),
|
|
@@ -1056,6 +1407,7 @@ function chordNodes(context: MenuContext): MenuNode[] {
|
|
|
1056
1407
|
label: "arp octaves",
|
|
1057
1408
|
help: "how many octaves an arpeggio climbs",
|
|
1058
1409
|
value: chords.octaves,
|
|
1410
|
+
start: 1,
|
|
1059
1411
|
min: 1,
|
|
1060
1412
|
max: 4,
|
|
1061
1413
|
step: linear(1, 1, 4),
|
|
@@ -1072,12 +1424,22 @@ function chordNodes(context: MenuContext): MenuNode[] {
|
|
|
1072
1424
|
},
|
|
1073
1425
|
{
|
|
1074
1426
|
kind: "choice",
|
|
1075
|
-
label: "
|
|
1076
|
-
help: "the
|
|
1427
|
+
label: "idiom",
|
|
1428
|
+
help: "the musical idiom n suggests next chords in (pop, jazz, blues…)",
|
|
1077
1429
|
value: chords.style,
|
|
1078
1430
|
options: PROGRESSION_STYLES,
|
|
1079
1431
|
command: (option) => `/chords style ${option}`,
|
|
1080
1432
|
},
|
|
1433
|
+
{
|
|
1434
|
+
kind: "entry",
|
|
1435
|
+
label: "progression",
|
|
1436
|
+
value: "",
|
|
1437
|
+
placeholder: "chords, e.g. i7 IV7 each 8",
|
|
1438
|
+
help: "sustained, voice-led block chords on this track in the song key",
|
|
1439
|
+
command: (text) =>
|
|
1440
|
+
text.trim() ? `progression ${text.trim()}` : undefined,
|
|
1441
|
+
example: "progression i7 IV7 each 8",
|
|
1442
|
+
},
|
|
1081
1443
|
];
|
|
1082
1444
|
}
|
|
1083
1445
|
|
|
@@ -1173,7 +1535,7 @@ function panNode(track: Track): MenuNode {
|
|
|
1173
1535
|
: `${num(Math.abs(value))} ${value < 0 ? "L" : "R"}`,
|
|
1174
1536
|
command: (value) => `pan ${num(value)}`,
|
|
1175
1537
|
reset: "pan 0",
|
|
1176
|
-
help: "-1 left … 1 right · x
|
|
1538
|
+
help: "-1 left … 1 right · x centers",
|
|
1177
1539
|
};
|
|
1178
1540
|
}
|
|
1179
1541
|
|
|
@@ -1256,7 +1618,7 @@ function parameterNodes(context: MenuContext): MenuNode[] {
|
|
|
1256
1618
|
detail: `all ${all.length} params`,
|
|
1257
1619
|
help: isElectricFamily(track.instrument)
|
|
1258
1620
|
? "every electric keys parameter for this family"
|
|
1259
|
-
: "every
|
|
1621
|
+
: "every modeled piano parameter",
|
|
1260
1622
|
build: (inner) => {
|
|
1261
1623
|
const current = focused(inner);
|
|
1262
1624
|
return current
|
|
@@ -1270,6 +1632,11 @@ function parameterNodes(context: MenuContext): MenuNode[] {
|
|
|
1270
1632
|
} else if (track.instrument === "wind" && track.wind) {
|
|
1271
1633
|
// 0.6.1 wind engine: preset and WIND_PARAMS rows.
|
|
1272
1634
|
nodes.push(...windParameterNodes(track));
|
|
1635
|
+
} else if (track.instrument === "sing" && track.sing) {
|
|
1636
|
+
// 0.7 sing engine: preset, SING_PARAMS rows and a Throat sub-menu.
|
|
1637
|
+
nodes.push(
|
|
1638
|
+
...singParameterNodes(track, parseKey(context.score.key)?.tonic),
|
|
1639
|
+
);
|
|
1273
1640
|
} else if (!isDrumInstrument(track.instrument)) {
|
|
1274
1641
|
nodes.push(...synthNodes(track, SYNTH_SIMPLE));
|
|
1275
1642
|
nodes.push({
|
|
@@ -1484,7 +1851,7 @@ function sampleVoiceNodes(context: MenuContext, voice: string): MenuNode[] {
|
|
|
1484
1851
|
// 0.6.1 layers: velocity layer (SFZ lovel/hivel) and round-robin group.
|
|
1485
1852
|
{
|
|
1486
1853
|
kind: "choice",
|
|
1487
|
-
label: "
|
|
1854
|
+
label: "velocity layer",
|
|
1488
1855
|
value: ref.vel ? `${ref.vel[0]}-${ref.vel[1]}` : "off",
|
|
1489
1856
|
options: velocityLayerOptions(ref.vel),
|
|
1490
1857
|
command: (option) => set(`vel ${option}`),
|
|
@@ -1492,7 +1859,7 @@ function sampleVoiceNodes(context: MenuContext, voice: string): MenuNode[] {
|
|
|
1492
1859
|
},
|
|
1493
1860
|
{
|
|
1494
1861
|
kind: "entry",
|
|
1495
|
-
label: "
|
|
1862
|
+
label: "round robin",
|
|
1496
1863
|
value: ref.rr ?? "",
|
|
1497
1864
|
placeholder: "group name, e.g. sn",
|
|
1498
1865
|
command: (text) => set(`rr ${text.trim() || "off"}`),
|
|
@@ -1524,7 +1891,7 @@ function matchingSynthPreset(track: Track): string | undefined {
|
|
|
1524
1891
|
)?.[0];
|
|
1525
1892
|
}
|
|
1526
1893
|
|
|
1527
|
-
/**
|
|
1894
|
+
/** Modeled piano rows: the preset, then each parameter (`keys <p> <v>`). */
|
|
1528
1895
|
function keysNodes(
|
|
1529
1896
|
track: Track,
|
|
1530
1897
|
params: readonly string[],
|
|
@@ -1702,7 +2069,7 @@ function organNodes(track: Track): MenuNode[] {
|
|
|
1702
2069
|
? {
|
|
1703
2070
|
kind: "menu",
|
|
1704
2071
|
id: "keys:drawbars",
|
|
1705
|
-
label: "
|
|
2072
|
+
label: "drawbars",
|
|
1706
2073
|
detail: text("drawbars"),
|
|
1707
2074
|
help: "nine drawbars 16' to 1' (0 in, 8 full out)",
|
|
1708
2075
|
build: (inner) => {
|
|
@@ -1720,7 +2087,7 @@ function organNodes(track: Track): MenuNode[] {
|
|
|
1720
2087
|
? {
|
|
1721
2088
|
kind: "menu",
|
|
1722
2089
|
id: "keys:registers",
|
|
1723
|
-
label: "
|
|
2090
|
+
label: "registers",
|
|
1724
2091
|
detail: text("registers"),
|
|
1725
2092
|
help: "five combo-organ registers 16' to 2' (0 off, 8 full)",
|
|
1726
2093
|
build: (inner) => {
|
|
@@ -1737,7 +2104,7 @@ function organNodes(track: Track): MenuNode[] {
|
|
|
1737
2104
|
: {
|
|
1738
2105
|
kind: "menu",
|
|
1739
2106
|
id: "keys:stops",
|
|
1740
|
-
label: "
|
|
2107
|
+
label: "stops",
|
|
1741
2108
|
detail: text("stops"),
|
|
1742
2109
|
help: "pipe stops and registrations",
|
|
1743
2110
|
build: (inner) => {
|
|
@@ -1814,9 +2181,9 @@ function synthParamNode(track: Track, key: string, named: boolean): MenuNode {
|
|
|
1814
2181
|
|
|
1815
2182
|
/** Plain names for the simple synth rows; the note shows the command name. */
|
|
1816
2183
|
const SYNTH_LABEL: Readonly<Record<string, string>> = {
|
|
1817
|
-
lpf: "filter
|
|
1818
|
-
lpq: "filter res",
|
|
1819
|
-
lpenv: "filter env",
|
|
2184
|
+
lpf: "synth filter",
|
|
2185
|
+
lpq: "synth filter res",
|
|
2186
|
+
lpenv: "synth filter env",
|
|
1820
2187
|
vib: "vibrato",
|
|
1821
2188
|
fm: "FM amount",
|
|
1822
2189
|
};
|
|
@@ -2000,7 +2367,7 @@ function wavetableNodes(track: Track, projectRoot?: string): MenuNode[] {
|
|
|
2000
2367
|
nodes.push({
|
|
2001
2368
|
kind: "info",
|
|
2002
2369
|
label: "position automation",
|
|
2003
|
-
value: `${track.wtAutomation!.length} points · Automation ›
|
|
2370
|
+
value: `${track.wtAutomation!.length} points · Automation › table position`,
|
|
2004
2371
|
});
|
|
2005
2372
|
return nodes;
|
|
2006
2373
|
}
|
|
@@ -2015,7 +2382,7 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
2015
2382
|
return {
|
|
2016
2383
|
kind: "menu",
|
|
2017
2384
|
id: effect,
|
|
2018
|
-
label: spec.label
|
|
2385
|
+
label: spec.label,
|
|
2019
2386
|
detail: values ? effectSummary(effect, values) : "off",
|
|
2020
2387
|
help: spec.doc,
|
|
2021
2388
|
build: (inner) => effectParamNodes(inner, effect, false),
|
|
@@ -2028,7 +2395,9 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
2028
2395
|
(effect) =>
|
|
2029
2396
|
!CORE_EFFECTS.includes(effect) &&
|
|
2030
2397
|
!rigStages.includes(effect) &&
|
|
2031
|
-
!gaze.includes(effect)
|
|
2398
|
+
!gaze.includes(effect) &&
|
|
2399
|
+
// 0.7: the formant shift lives in Effects > Voice.
|
|
2400
|
+
effect !== "formant",
|
|
2032
2401
|
);
|
|
2033
2402
|
const gazeOn = SHOEGAZE_EFFECTS.filter((effect) =>
|
|
2034
2403
|
effectValues(track, effect),
|
|
@@ -2036,7 +2405,7 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
2036
2405
|
const shoegaze: MenuNode = {
|
|
2037
2406
|
kind: "menu",
|
|
2038
2407
|
id: "shoegaze",
|
|
2039
|
-
label: "
|
|
2408
|
+
label: "shoegaze",
|
|
2040
2409
|
help: "wobble (tremolo-arm bend), bloom (feedback), swell (volume swell), double (two takes); `rig shoegaze` loads them all",
|
|
2041
2410
|
detail:
|
|
2042
2411
|
gazeOn.length === 0
|
|
@@ -2050,7 +2419,7 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
2050
2419
|
const rig: MenuNode = {
|
|
2051
2420
|
kind: "menu",
|
|
2052
2421
|
id: "guitar rig",
|
|
2053
|
-
label: "
|
|
2422
|
+
label: "guitar rig",
|
|
2054
2423
|
help: "stomp box, amp head with noise gate and speaker cabinet; `rig <name>` loads a whole rig",
|
|
2055
2424
|
detail:
|
|
2056
2425
|
rigOn.length === 0 ? "off" : (rigPresetOf(track.fx) ?? rigOn.join(" → ")),
|
|
@@ -2073,6 +2442,19 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
2073
2442
|
...CORE_EFFECTS.map(node),
|
|
2074
2443
|
rig,
|
|
2075
2444
|
shoegaze,
|
|
2445
|
+
// Formant and vocoder live under Voice; this row is the way there.
|
|
2446
|
+
...(voiceEffectRows(context).length > 0
|
|
2447
|
+
? [
|
|
2448
|
+
{
|
|
2449
|
+
kind: "menu" as const,
|
|
2450
|
+
id: "voice",
|
|
2451
|
+
label: "voice effects",
|
|
2452
|
+
detail: "formant · vocoder · also in Voice",
|
|
2453
|
+
help: "formant and vocoder for voices (the same rows as Ctrl-K › Voice)",
|
|
2454
|
+
build: voiceEffectRows,
|
|
2455
|
+
},
|
|
2456
|
+
]
|
|
2457
|
+
: []),
|
|
2076
2458
|
{
|
|
2077
2459
|
kind: "menu",
|
|
2078
2460
|
id: "more effects",
|
|
@@ -2109,16 +2491,17 @@ function withUnit(text: string, unit: string | undefined): string {
|
|
|
2109
2491
|
|
|
2110
2492
|
function formatParam(spec: ParamSpec, value: number): string {
|
|
2111
2493
|
if (spec.kind !== "number") return num(value);
|
|
2112
|
-
|
|
2113
|
-
? `${Math.round(value)}`
|
|
2114
|
-
|
|
2494
|
+
const text =
|
|
2495
|
+
spec.step === "log" && value >= 100 ? `${Math.round(value)}` : num(value);
|
|
2496
|
+
// Rounding never leaves the range: attack 0.0001 types as 0.0001, not 0.
|
|
2497
|
+
return Number(text) < spec.min ? String(spec.min) : text;
|
|
2115
2498
|
}
|
|
2116
2499
|
|
|
2117
2500
|
/**
|
|
2118
2501
|
* One effect's rows: on/off, presets, then its simple parameters; the
|
|
2119
2502
|
* `advanced` submenu holds every parameter with its Strudel names.
|
|
2120
2503
|
*/
|
|
2121
|
-
function effectParamNodes(
|
|
2504
|
+
export function effectParamNodes(
|
|
2122
2505
|
context: MenuContext,
|
|
2123
2506
|
effect: EffectName,
|
|
2124
2507
|
advanced: boolean,
|
|
@@ -2209,6 +2592,37 @@ function effectParamNodes(
|
|
|
2209
2592
|
});
|
|
2210
2593
|
}
|
|
2211
2594
|
}
|
|
2595
|
+
if (!advanced && effect === "vowel") {
|
|
2596
|
+
// 0.7 vowel morph: To picks the target (morph starts halfway), Morph
|
|
2597
|
+
// moves between the two; both through `/vowel`.
|
|
2598
|
+
const to = values?.to;
|
|
2599
|
+
nodes.push(
|
|
2600
|
+
{
|
|
2601
|
+
kind: "choice",
|
|
2602
|
+
label: "to",
|
|
2603
|
+
value: typeof to === "string" ? to : "—",
|
|
2604
|
+
options: ["—", ...VOWEL_VALUES],
|
|
2605
|
+
command: (option) =>
|
|
2606
|
+
option === "—" ? "/vowel to off" : `/vowel to ${option}`,
|
|
2607
|
+
help: "a second vowel to morph toward (— keeps one vowel)",
|
|
2608
|
+
},
|
|
2609
|
+
{
|
|
2610
|
+
kind: "number",
|
|
2611
|
+
label: "morph",
|
|
2612
|
+
value: typeof values?.morph === "number" ? values.morph : undefined,
|
|
2613
|
+
start: 0.5,
|
|
2614
|
+
off: typeof to === "string" ? "0" : "set to first",
|
|
2615
|
+
min: 0,
|
|
2616
|
+
max: 1,
|
|
2617
|
+
step: specStep(spec.params.morph as NumberParam),
|
|
2618
|
+
format: (value) => num(value),
|
|
2619
|
+
command: (value) =>
|
|
2620
|
+
typeof to === "string" ? `/vowel morph ${num(value)}` : `/vowel to o`,
|
|
2621
|
+
help: "0 is the vowel, 1 is the to vowel; log-frequency formant glide, automatable as vowel-morph",
|
|
2622
|
+
...(typeof to === "string" ? { reset: "/vowel morph 0" } : {}),
|
|
2623
|
+
},
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2212
2626
|
if (!advanced) {
|
|
2213
2627
|
nodes.push({
|
|
2214
2628
|
kind: "menu",
|
|
@@ -2251,6 +2665,8 @@ function automationNodes(context: MenuContext): MenuNode[] {
|
|
|
2251
2665
|
if (info?.effect === "modal") return track.modal !== undefined;
|
|
2252
2666
|
if (info?.effect === "grain") return track.granular !== undefined;
|
|
2253
2667
|
if (info?.effect === "wind") return track.wind !== undefined;
|
|
2668
|
+
if (info?.effect === "sing") return track.sing !== undefined;
|
|
2669
|
+
if (info?.effect === "vocoder") return track.vocoder !== undefined;
|
|
2254
2670
|
return info !== undefined && effectValues(track, info.effect) !== undefined;
|
|
2255
2671
|
});
|
|
2256
2672
|
const hidden = AUTOMATION_PARAMETERS.filter((lane) => !shown.includes(lane));
|
|
@@ -2376,14 +2792,14 @@ function soundsDetail(track: Track | undefined): string {
|
|
|
2376
2792
|
}
|
|
2377
2793
|
|
|
2378
2794
|
/**
|
|
2379
|
-
* The instrument browser:
|
|
2795
|
+
* The instrument browser: kits and soundfont instruments from the
|
|
2380
2796
|
* built-in packs (fetched on first use), and the pack list.
|
|
2381
2797
|
*/
|
|
2382
2798
|
function kitsMenu(): MenuNode {
|
|
2383
2799
|
return {
|
|
2384
2800
|
kind: "menu",
|
|
2385
2801
|
id: "kits",
|
|
2386
|
-
label: "
|
|
2802
|
+
label: "kits",
|
|
2387
2803
|
help: "make this a drum track with a synth or sample kit",
|
|
2388
2804
|
detail: `synth ${SYNTH_KIT_NAMES.join(" ")} · samples ${Object.keys(DEFAULT_KITS).join(" ")}`,
|
|
2389
2805
|
// Synth kits first (offline), then the pack sample kits.
|
|
@@ -2420,27 +2836,27 @@ function patternsMenu(): MenuNode {
|
|
|
2420
2836
|
return {
|
|
2421
2837
|
kind: "menu",
|
|
2422
2838
|
id: "patterns",
|
|
2423
|
-
label: "
|
|
2424
|
-
detail: `${DRUM_PATTERNS.length}
|
|
2839
|
+
label: "grooves",
|
|
2840
|
+
detail: `${DRUM_PATTERNS.length} ready-made drum parts`,
|
|
2425
2841
|
help: "replace the drum part with a ready-made groove",
|
|
2426
2842
|
build: () =>
|
|
2427
2843
|
DRUM_PATTERNS.map((entry): MenuNode => ({
|
|
2428
2844
|
kind: "action",
|
|
2429
2845
|
label: `${entry.label} ${entry.tempo.bpm} BPM · ${entry.tags.join(", ")}`,
|
|
2430
|
-
command: `/
|
|
2846
|
+
command: `/groove ${entry.name}`,
|
|
2431
2847
|
})),
|
|
2432
2848
|
};
|
|
2433
2849
|
}
|
|
2434
2850
|
|
|
2435
2851
|
const GRANULAR_PRESETS_COUNT = granularBrowseNodes().length;
|
|
2436
2852
|
|
|
2437
|
-
function soundNodes(): MenuNode[] {
|
|
2853
|
+
function soundNodes(context: MenuContext): MenuNode[] {
|
|
2438
2854
|
return [
|
|
2439
2855
|
{
|
|
2440
2856
|
kind: "menu",
|
|
2441
2857
|
id: "group:keys",
|
|
2442
|
-
label: "
|
|
2443
|
-
help: "
|
|
2858
|
+
label: "keys",
|
|
2859
|
+
help: "modeled pianos, electric keys and organs, built in (no download)",
|
|
2444
2860
|
detail: `${PIANO_PRESET_NAMES.join(" ")} · electric: ${ELECTRIC_PRESET_NAMES.join(" ")} · organs: ${ORGAN_PRESET_NAMES.join(" ")}`,
|
|
2445
2861
|
build: () => [
|
|
2446
2862
|
...PIANO_PRESET_NAMES.map((name): MenuNode => {
|
|
@@ -2456,8 +2872,8 @@ function soundNodes(): MenuNode[] {
|
|
|
2456
2872
|
{
|
|
2457
2873
|
kind: "menu",
|
|
2458
2874
|
id: "group:keys:electric",
|
|
2459
|
-
label: "
|
|
2460
|
-
help: "electric pianos (tine, reed) and the clavinet,
|
|
2875
|
+
label: "electric",
|
|
2876
|
+
help: "electric pianos (tine, reed) and the clavinet, modeled",
|
|
2461
2877
|
detail: ELECTRIC_PRESET_NAMES.join(" "),
|
|
2462
2878
|
build: () =>
|
|
2463
2879
|
Object.entries(KEYS_PRESETS)
|
|
@@ -2473,7 +2889,7 @@ function soundNodes(): MenuNode[] {
|
|
|
2473
2889
|
{
|
|
2474
2890
|
kind: "menu",
|
|
2475
2891
|
id: "group:organs",
|
|
2476
|
-
label: "
|
|
2892
|
+
label: "organs",
|
|
2477
2893
|
help: "tonewheel, combo and pipe organs on the keys engine",
|
|
2478
2894
|
detail: ORGAN_PRESET_NAMES.join(" "),
|
|
2479
2895
|
build: () =>
|
|
@@ -2491,7 +2907,7 @@ function soundNodes(): MenuNode[] {
|
|
|
2491
2907
|
},
|
|
2492
2908
|
{
|
|
2493
2909
|
kind: "action",
|
|
2494
|
-
label: "wavetable
|
|
2910
|
+
label: "wavetable basic shapes morph · built-in",
|
|
2495
2911
|
command: "wt basic",
|
|
2496
2912
|
help: "switch this track to the morphing wavetable synth",
|
|
2497
2913
|
},
|
|
@@ -2501,7 +2917,7 @@ function soundNodes(): MenuNode[] {
|
|
|
2501
2917
|
{
|
|
2502
2918
|
kind: "menu",
|
|
2503
2919
|
id: "strings",
|
|
2504
|
-
label: "
|
|
2920
|
+
label: "strings",
|
|
2505
2921
|
help: "plucked strings: guitars, basses, sitar, harpsichord, oud, koto…; Bowed: violin to contrabass, sections",
|
|
2506
2922
|
detail: `${STRING_PRESET_NAMES.length - BOWED_PRESET_NAMES.length} plucked · ${BOWED_PRESET_NAMES.length} bowed · built-in`,
|
|
2507
2923
|
build: () => [
|
|
@@ -2516,7 +2932,7 @@ function soundNodes(): MenuNode[] {
|
|
|
2516
2932
|
{
|
|
2517
2933
|
kind: "menu",
|
|
2518
2934
|
id: "strings:bowed",
|
|
2519
|
-
label: "
|
|
2935
|
+
label: "bowed",
|
|
2520
2936
|
help: "bowed strings: violin, viola, cello, bass, fiddle, erhu, sections",
|
|
2521
2937
|
detail: BOWED_PRESET_NAMES.join(" "),
|
|
2522
2938
|
build: () =>
|
|
@@ -2532,7 +2948,7 @@ function soundNodes(): MenuNode[] {
|
|
|
2532
2948
|
{
|
|
2533
2949
|
kind: "menu",
|
|
2534
2950
|
id: "granular-browse",
|
|
2535
|
-
label: "
|
|
2951
|
+
label: "granular",
|
|
2536
2952
|
help: "grain clouds from a built-in synth or this track's own sound",
|
|
2537
2953
|
detail: `${GRANULAR_PRESETS_COUNT} presets · built-in`,
|
|
2538
2954
|
build: granularBrowseNodes,
|
|
@@ -2540,7 +2956,7 @@ function soundNodes(): MenuNode[] {
|
|
|
2540
2956
|
{
|
|
2541
2957
|
kind: "menu",
|
|
2542
2958
|
id: "instruments",
|
|
2543
|
-
label: "instruments",
|
|
2959
|
+
label: "all instruments",
|
|
2544
2960
|
help: "sampled piano and General MIDI instruments",
|
|
2545
2961
|
detail: "General MIDI soundfont, piano",
|
|
2546
2962
|
build: () => [
|
|
@@ -2558,9 +2974,9 @@ function soundNodes(): MenuNode[] {
|
|
|
2558
2974
|
},
|
|
2559
2975
|
{
|
|
2560
2976
|
kind: "entry",
|
|
2561
|
-
label: "use a
|
|
2977
|
+
label: "use a sample",
|
|
2562
2978
|
value: "",
|
|
2563
|
-
help: "type a pack
|
|
2979
|
+
help: "type a pack sample name, e.g. dirt-samples/bd:3",
|
|
2564
2980
|
placeholder: "<pack>/<sound>[:<n>]",
|
|
2565
2981
|
command: (text) => (text.trim() ? `/pack use ${text.trim()}` : undefined),
|
|
2566
2982
|
example: "/pack use dirt-samples/bd:3",
|
|
@@ -2605,6 +3021,7 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
2605
3021
|
label: "tempo",
|
|
2606
3022
|
help: "beats per minute",
|
|
2607
3023
|
value: score.tempoBpm,
|
|
3024
|
+
start: START_TEMPO_BPM,
|
|
2608
3025
|
min: SCORE_LIMITS.minTempoBpm,
|
|
2609
3026
|
max: SCORE_LIMITS.maxTempoBpm,
|
|
2610
3027
|
step: linear(1, SCORE_LIMITS.minTempoBpm, SCORE_LIMITS.maxTempoBpm),
|
|
@@ -2616,6 +3033,7 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
2616
3033
|
label: "beats per bar",
|
|
2617
3034
|
help: "the meter",
|
|
2618
3035
|
value: score.beatsPerBar,
|
|
3036
|
+
start: START_BEATS_PER_BAR,
|
|
2619
3037
|
min: 1,
|
|
2620
3038
|
max: SCORE_LIMITS.maxBeatsPerBar,
|
|
2621
3039
|
step: linear(1, 1, SCORE_LIMITS.maxBeatsPerBar),
|
|
@@ -2628,20 +3046,14 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
2628
3046
|
label: "loop length",
|
|
2629
3047
|
help: "how long the loop is",
|
|
2630
3048
|
value: score.bars,
|
|
3049
|
+
start: START_BARS,
|
|
2631
3050
|
min: 1,
|
|
2632
3051
|
max: SCORE_LIMITS.maxBars,
|
|
2633
3052
|
step: linear(1, 1, SCORE_LIMITS.maxBars),
|
|
2634
3053
|
format: (value) => `${num(value)} bar${value === 1 ? "" : "s"}`,
|
|
2635
3054
|
command: (value) => `bars ${Math.round(value)}`,
|
|
2636
3055
|
},
|
|
2637
|
-
|
|
2638
|
-
kind: "choice",
|
|
2639
|
-
label: "grid",
|
|
2640
|
-
help: "the step play-mode recording and the euclid editor snap to",
|
|
2641
|
-
value: context.grid,
|
|
2642
|
-
options: context.grids,
|
|
2643
|
-
command: (option) => `/grid ${option}`,
|
|
2644
|
-
},
|
|
3056
|
+
gridNode(context),
|
|
2645
3057
|
{
|
|
2646
3058
|
kind: "toggle",
|
|
2647
3059
|
label: "click",
|
|
@@ -2658,12 +3070,14 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
2658
3070
|
command: (option) => `/count-in ${option}`,
|
|
2659
3071
|
},
|
|
2660
3072
|
{
|
|
2661
|
-
kind: "
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
3073
|
+
kind: "choice",
|
|
3074
|
+
label: "calibration",
|
|
3075
|
+
help: "sound fixes: 1 chokes hats, tunes toms, levels keys, steadies brass · 0 keeps the legacy sound",
|
|
3076
|
+
value: String(score.calibration ?? 0),
|
|
3077
|
+
options: Array.from({ length: CALIBRATION_LATEST + 1 }, (_, i) =>
|
|
3078
|
+
String(i),
|
|
3079
|
+
),
|
|
3080
|
+
command: (option) => `/calibration ${option}`,
|
|
2667
3081
|
},
|
|
2668
3082
|
{
|
|
2669
3083
|
kind: "menu",
|
|
@@ -2673,6 +3087,40 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
2673
3087
|
help: "render a track, an orbit or the mix to a pinned WAV on a new sampler or granular track",
|
|
2674
3088
|
build: resampleNodes,
|
|
2675
3089
|
},
|
|
3090
|
+
{
|
|
3091
|
+
kind: "menu",
|
|
3092
|
+
id: "export",
|
|
3093
|
+
label: "export",
|
|
3094
|
+
detail: "project file · MIDI · WAV",
|
|
3095
|
+
help: "save the song as a project file or MIDI; audio renders offline",
|
|
3096
|
+
build: exportNodes,
|
|
3097
|
+
},
|
|
3098
|
+
{
|
|
3099
|
+
kind: "menu",
|
|
3100
|
+
id: "session",
|
|
3101
|
+
label: "session",
|
|
3102
|
+
detail: context.sessionName ?? "rename · fork · resume",
|
|
3103
|
+
help: "rename, fork or resume a session",
|
|
3104
|
+
build: sessionNodes,
|
|
3105
|
+
},
|
|
3106
|
+
{
|
|
3107
|
+
kind: "menu",
|
|
3108
|
+
id: "agent",
|
|
3109
|
+
label: "agent",
|
|
3110
|
+
detail: context.showMe
|
|
3111
|
+
? `show me ${context.showMe}`
|
|
3112
|
+
: "model · model key",
|
|
3113
|
+
help: "the agent's model, show-me and model key",
|
|
3114
|
+
build: agentNodes,
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
kind: "menu",
|
|
3118
|
+
id: "help",
|
|
3119
|
+
label: "help and guides",
|
|
3120
|
+
detail: "help · guides · keys",
|
|
3121
|
+
help: "the command reference, the guides and the keys",
|
|
3122
|
+
build: helpNodes,
|
|
3123
|
+
},
|
|
2676
3124
|
];
|
|
2677
3125
|
}
|
|
2678
3126
|
|
|
@@ -2720,9 +3168,9 @@ function resampleNodes(context: MenuContext): MenuNode[] {
|
|
|
2720
3168
|
},
|
|
2721
3169
|
{
|
|
2722
3170
|
kind: "action",
|
|
2723
|
-
label: "mix → sampler
|
|
3171
|
+
label: "mix → sampler",
|
|
2724
3172
|
command: "resample orbit 1",
|
|
2725
|
-
help: "render every track on orbit 1 (the default bus) without the song master",
|
|
3173
|
+
help: "pre-master: render every track on orbit 1 (the default bus) without the song master",
|
|
2726
3174
|
},
|
|
2727
3175
|
{
|
|
2728
3176
|
kind: "action",
|
|
@@ -2899,7 +3347,7 @@ function songTuningNodes(context: MenuContext): MenuNode[] {
|
|
|
2899
3347
|
help: "the song scale: modes, minors, pentatonics, maqam, ragas, Messiaen",
|
|
2900
3348
|
value: scale,
|
|
2901
3349
|
options: [...MODE_NAMES, ...SCALE_NAMES],
|
|
2902
|
-
command: (option) => `
|
|
3350
|
+
command: (option) => `key ${option}`,
|
|
2903
3351
|
},
|
|
2904
3352
|
{
|
|
2905
3353
|
kind: "choice",
|
|
@@ -2907,7 +3355,7 @@ function songTuningNodes(context: MenuContext): MenuNode[] {
|
|
|
2907
3355
|
help: "the song key's tonic; keeps the scale",
|
|
2908
3356
|
value: tonic,
|
|
2909
3357
|
options: TONICS,
|
|
2910
|
-
command: (option) => `
|
|
3358
|
+
command: (option) => `key ${option} ${scale}`,
|
|
2911
3359
|
},
|
|
2912
3360
|
{
|
|
2913
3361
|
kind: "action",
|
|
@@ -2918,7 +3366,7 @@ function songTuningNodes(context: MenuContext): MenuNode[] {
|
|
|
2918
3366
|
{
|
|
2919
3367
|
kind: "action",
|
|
2920
3368
|
label: "list scales",
|
|
2921
|
-
command: "
|
|
3369
|
+
command: "key list",
|
|
2922
3370
|
help: "every mode and scale, by family",
|
|
2923
3371
|
},
|
|
2924
3372
|
];
|
|
@@ -2960,14 +3408,7 @@ type Frame = {
|
|
|
2960
3408
|
hovering?: boolean;
|
|
2961
3409
|
};
|
|
2962
3410
|
|
|
2963
|
-
const
|
|
2964
|
-
const KEY_DOWN = new Set(["\u001b[B", "\u001bOB", "j"]);
|
|
2965
|
-
const KEY_LEFT = new Set(["\u001b[D", "\u001bOD", "h", "-", "_"]);
|
|
2966
|
-
const KEY_RIGHT = new Set(["\u001b[C", "\u001bOC", "l", "+", "="]);
|
|
2967
|
-
const KEY_ENTER = new Set(["\r", "\n"]);
|
|
2968
|
-
const KEY_BACKSPACE = new Set(["\u007f", "\b"]);
|
|
2969
|
-
const KEY_DELETE = new Set(["\u001b[3~", "x"]);
|
|
2970
|
-
const LABEL_WIDTH = 16;
|
|
3411
|
+
export const LABEL_WIDTH = 16;
|
|
2971
3412
|
|
|
2972
3413
|
/**
|
|
2973
3414
|
* A submenu frame whose rows are rebuilt from the parent's fresh build, so
|
|
@@ -3020,10 +3461,8 @@ export class EditMenu {
|
|
|
3020
3461
|
this.stack = [{ title: "menu", build: rootNodes, index: 0, query: "" }];
|
|
3021
3462
|
this.filtering = false;
|
|
3022
3463
|
this.entry = undefined;
|
|
3023
|
-
// Walk the section path (`automation` is Mix
|
|
3024
|
-
for (const id of section
|
|
3025
|
-
? (SECTION_ALIASES[section.toLowerCase()] ?? [])
|
|
3026
|
-
: []) {
|
|
3464
|
+
// Walk the section path (`automation` is Mix › automation).
|
|
3465
|
+
for (const id of section ? (menuSectionPath(context, section) ?? []) : []) {
|
|
3027
3466
|
const frame = this.stack.at(-1)!;
|
|
3028
3467
|
const nodes = frame.build(context);
|
|
3029
3468
|
const index = nodes.findIndex(
|
|
@@ -3130,9 +3569,15 @@ export class EditMenu {
|
|
|
3130
3569
|
if (!frame) return [];
|
|
3131
3570
|
const all = frame.build(context);
|
|
3132
3571
|
const needle = frame.query.toLowerCase();
|
|
3133
|
-
return
|
|
3134
|
-
|
|
3135
|
-
|
|
3572
|
+
if (!needle) return all;
|
|
3573
|
+
const direct = all.filter((node) =>
|
|
3574
|
+
nodeText(node).toLowerCase().includes(needle),
|
|
3575
|
+
);
|
|
3576
|
+
// At the root a filter also looks two levels down for groups
|
|
3577
|
+
// (`/autotune`, `/formant`, `/tuning`) and offers them by path, after
|
|
3578
|
+
// the roots that match.
|
|
3579
|
+
if (this.stack.length !== 1) return direct;
|
|
3580
|
+
return [...direct, ...deepGroups(all, context, needle)];
|
|
3136
3581
|
}
|
|
3137
3582
|
|
|
3138
3583
|
private selected(context: MenuContext): MenuNode | undefined {
|
|
@@ -3160,11 +3605,7 @@ export class EditMenu {
|
|
|
3160
3605
|
frame.index = 0;
|
|
3161
3606
|
return { type: "handled" };
|
|
3162
3607
|
}
|
|
3163
|
-
if (
|
|
3164
|
-
KEY_ENTER.has(value) ||
|
|
3165
|
-
value === "\u001b[B" ||
|
|
3166
|
-
value === "\u001b[A"
|
|
3167
|
-
) {
|
|
3608
|
+
if (KEY_ENTER.has(value) || value === ARROW_DOWN || value === ARROW_UP) {
|
|
3168
3609
|
this.filtering = false;
|
|
3169
3610
|
return this.key(value, context);
|
|
3170
3611
|
}
|
|
@@ -3203,11 +3644,11 @@ export class EditMenu {
|
|
|
3203
3644
|
frame.index = (frame.index + step + nodes.length) % nodes.length;
|
|
3204
3645
|
return this.hovered(frame, context);
|
|
3205
3646
|
}
|
|
3206
|
-
if (value
|
|
3647
|
+
if (KEY_PAGE_UP.has(value) || KEY_HOME.has(value)) {
|
|
3207
3648
|
frame.index = 0;
|
|
3208
3649
|
return this.hovered(frame, context);
|
|
3209
3650
|
}
|
|
3210
|
-
if (value
|
|
3651
|
+
if (KEY_PAGE_DOWN.has(value) || KEY_END.has(value)) {
|
|
3211
3652
|
frame.index = Math.max(0, nodes.length - 1);
|
|
3212
3653
|
return this.hovered(frame, context);
|
|
3213
3654
|
}
|
|
@@ -3226,11 +3667,19 @@ export class EditMenu {
|
|
|
3226
3667
|
node.kind === "info")
|
|
3227
3668
|
)
|
|
3228
3669
|
return { type: "keep" };
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3670
|
+
// ← → adjust a value; on any other row ← h go back and → l go in
|
|
3671
|
+
// (or run the action), and - + do nothing.
|
|
3672
|
+
const isValue = isValueNode(node);
|
|
3673
|
+
if (isValue && (KEY_LEFT.has(value) || KEY_RIGHT.has(value)))
|
|
3674
|
+
return this.nudge(node, KEY_RIGHT.has(value) ? 1 : -1);
|
|
3675
|
+
if (!isValue && KEY_BACK.has(value)) return this.back();
|
|
3676
|
+
if (!isValue && (KEY_LEFT.has(value) || KEY_RIGHT.has(value)))
|
|
3677
|
+
if (!(
|
|
3678
|
+
KEY_FORWARD.has(value) &&
|
|
3679
|
+
(node.kind === "menu" || node.kind === "action")
|
|
3680
|
+
))
|
|
3681
|
+
return { type: "handled" };
|
|
3682
|
+
if (KEY_RESET.has(value) && node.kind === "number") {
|
|
3234
3683
|
const reset =
|
|
3235
3684
|
node.reset ??
|
|
3236
3685
|
(node.start !== undefined && node.value !== undefined
|
|
@@ -3238,7 +3687,7 @@ export class EditMenu {
|
|
|
3238
3687
|
: undefined);
|
|
3239
3688
|
return reset ? { type: "run", command: reset } : { type: "handled" };
|
|
3240
3689
|
}
|
|
3241
|
-
if (
|
|
3690
|
+
if (KEY_RESET.has(value) && node.kind === "point")
|
|
3242
3691
|
return {
|
|
3243
3692
|
type: "run",
|
|
3244
3693
|
command: `automate ${node.lane} remove ${num(node.beat)}`,
|
|
@@ -3250,7 +3699,7 @@ export class EditMenu {
|
|
|
3250
3699
|
this.entry = { label: node.label, buffer: value };
|
|
3251
3700
|
return { type: "handled" };
|
|
3252
3701
|
}
|
|
3253
|
-
if (KEY_ENTER.has(value) || value === " ") {
|
|
3702
|
+
if (KEY_ENTER.has(value) || KEY_FORWARD.has(value) || value === " ") {
|
|
3254
3703
|
if (node.kind === "menu") {
|
|
3255
3704
|
// Going back lands on the opened row with the filter cleared.
|
|
3256
3705
|
if (frame.query) {
|
|
@@ -3326,15 +3775,16 @@ export class EditMenu {
|
|
|
3326
3775
|
return { type: "hover", command: node.command, key: frame.hover };
|
|
3327
3776
|
}
|
|
3328
3777
|
|
|
3778
|
+
/** ← on a row that is not a value: up one level (never past the root). */
|
|
3779
|
+
private back(): MenuResult {
|
|
3780
|
+
if (this.stack.length <= 1) return { type: "handled" };
|
|
3781
|
+
const left = this.stack.pop();
|
|
3782
|
+
if (left?.hover && left.hovering)
|
|
3783
|
+
return { type: "unhover", key: left.hover };
|
|
3784
|
+
return { type: "handled" };
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3329
3787
|
private nudge(node: MenuNode, direction: 1 | -1): MenuResult {
|
|
3330
|
-
if (node.kind === "menu") {
|
|
3331
|
-
if (direction > 0) {
|
|
3332
|
-
this.stack.push(childFrame(this.stack.at(-1)!, node));
|
|
3333
|
-
return { type: "handled" };
|
|
3334
|
-
}
|
|
3335
|
-
if (this.stack.length > 1) this.stack.pop();
|
|
3336
|
-
return { type: "handled" };
|
|
3337
|
-
}
|
|
3338
3788
|
if (node.kind === "number") {
|
|
3339
3789
|
const next =
|
|
3340
3790
|
node.value === undefined
|
|
@@ -3367,11 +3817,6 @@ export class EditMenu {
|
|
|
3367
3817
|
? { type: "handled" }
|
|
3368
3818
|
: { type: "run", command: node.command(next) };
|
|
3369
3819
|
}
|
|
3370
|
-
if (direction < 0 && this.stack.length > 1) {
|
|
3371
|
-
const left = this.stack.pop();
|
|
3372
|
-
if (left?.hover && left.hovering)
|
|
3373
|
-
return { type: "unhover", key: left.hover };
|
|
3374
|
-
}
|
|
3375
3820
|
return { type: "handled" };
|
|
3376
3821
|
}
|
|
3377
3822
|
|
|
@@ -3502,6 +3947,16 @@ export class EditMenu {
|
|
|
3502
3947
|
}
|
|
3503
3948
|
}
|
|
3504
3949
|
|
|
3950
|
+
/** Rows that hold a value: ← → adjust them instead of navigating. */
|
|
3951
|
+
function isValueNode(node: MenuNode): boolean {
|
|
3952
|
+
return (
|
|
3953
|
+
node.kind === "number" ||
|
|
3954
|
+
node.kind === "point" ||
|
|
3955
|
+
node.kind === "toggle" ||
|
|
3956
|
+
node.kind === "choice"
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3505
3960
|
/** The focused row's one-line description. */
|
|
3506
3961
|
function describe(node: MenuNode): string {
|
|
3507
3962
|
if (node.help) return node.help;
|
|
@@ -3663,28 +4118,155 @@ function commandText(node: MenuNode): string | undefined {
|
|
|
3663
4118
|
}
|
|
3664
4119
|
}
|
|
3665
4120
|
|
|
4121
|
+
/** Menu groups below `nodes` (two levels) whose label matches `needle`. */
|
|
4122
|
+
function deepGroups(
|
|
4123
|
+
nodes: readonly MenuNode[],
|
|
4124
|
+
context: MenuContext,
|
|
4125
|
+
needle: string,
|
|
4126
|
+
): MenuNode[] {
|
|
4127
|
+
const found: MenuNode[] = [];
|
|
4128
|
+
const visit = (list: readonly MenuNode[], path: string, depth: number) => {
|
|
4129
|
+
for (const node of list) {
|
|
4130
|
+
if (node.kind !== "menu" || found.length >= 12) continue;
|
|
4131
|
+
const label = path ? `${path} › ${node.label}` : node.label;
|
|
4132
|
+
if (path && node.label.toLowerCase().includes(needle))
|
|
4133
|
+
found.push({ ...node, label });
|
|
4134
|
+
if (depth < 2) {
|
|
4135
|
+
let children: MenuNode[] = [];
|
|
4136
|
+
try {
|
|
4137
|
+
children = node.build(context);
|
|
4138
|
+
} catch {
|
|
4139
|
+
continue;
|
|
4140
|
+
}
|
|
4141
|
+
visit(children, label, depth + 1);
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
};
|
|
4145
|
+
visit(nodes, "", 0);
|
|
4146
|
+
return found;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
3666
4149
|
function nodeText(node: MenuNode): string {
|
|
3667
4150
|
return `${node.label} ${valueText(node)} ${commandText(node) ?? ""}`;
|
|
3668
4151
|
}
|
|
3669
4152
|
|
|
3670
|
-
/**
|
|
3671
|
-
export const MENU_SECTIONS =
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4153
|
+
/** Every id `/menu <id>` opens: the ten topics first, then older ids. */
|
|
4154
|
+
export const MENU_SECTIONS: readonly string[] = Object.freeze(
|
|
4155
|
+
Object.keys(SECTION_ALIASES),
|
|
4156
|
+
);
|
|
4157
|
+
|
|
4158
|
+
/** The ten topic ids, the roots `/menu` names first. */
|
|
4159
|
+
export const MENU_TOPICS: readonly string[] = Object.freeze(
|
|
4160
|
+
MENU_SECTIONS.slice(0, 10),
|
|
4161
|
+
);
|
|
4162
|
+
|
|
4163
|
+
/** The names `/menu` and `/help menu` list: one per topic, no aliases. */
|
|
4164
|
+
export const MENU_SHOWN_SECTIONS: readonly string[] = MENU_TOPICS;
|
|
4165
|
+
|
|
4166
|
+
/** `/menu`'s usage line: the short form; `/help menu` lists every topic. */
|
|
4167
|
+
export const MENU_USAGE = "usage: /menu <topic or row> · /help menu lists them";
|
|
4168
|
+
|
|
4169
|
+
/**
|
|
4170
|
+
* The error for `/menu <section>` when nothing matches: the nearest topic
|
|
4171
|
+
* or row name first, so it fits 80 columns, then the short usage.
|
|
4172
|
+
*/
|
|
4173
|
+
export function menuUsage(context: MenuContext, section: string): string {
|
|
4174
|
+
const names = new Set<string>(MENU_SECTIONS);
|
|
4175
|
+
for (const node of rootNodes(context))
|
|
4176
|
+
if (node.kind === "menu") {
|
|
4177
|
+
let children: MenuNode[] = [];
|
|
4178
|
+
try {
|
|
4179
|
+
children = node.build(context);
|
|
4180
|
+
} catch {
|
|
4181
|
+
continue;
|
|
4182
|
+
}
|
|
4183
|
+
for (const child of children)
|
|
4184
|
+
if (child.kind === "menu") names.add(labelName(child.label));
|
|
4185
|
+
}
|
|
4186
|
+
const near = nearest(section, names);
|
|
4187
|
+
return `no menu "${section}"${near ? ` · did you mean /menu ${near}?` : ""} · /menu <topic or row>`;
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
/** A row label without its padded detail: `granular 12 presets` → `granular`. */
|
|
4191
|
+
function labelName(label: string): string {
|
|
4192
|
+
return label
|
|
4193
|
+
.split(/\s{2,}/)[0]!
|
|
4194
|
+
.trim()
|
|
4195
|
+
.toLowerCase();
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
/**
|
|
4199
|
+
* The path of menu ids `/menu <section>` walks: a topic or older id from
|
|
4200
|
+
* SECTION_ALIASES, else the first menu row (three levels deep, breadth
|
|
4201
|
+
* first) whose id or label is `section`. Undefined when nothing matches.
|
|
4202
|
+
*/
|
|
4203
|
+
export function menuSectionPath(
|
|
4204
|
+
context: MenuContext,
|
|
4205
|
+
section: string,
|
|
4206
|
+
): readonly string[] | undefined {
|
|
4207
|
+
const needle = section.trim().toLowerCase();
|
|
4208
|
+
if (!needle) return undefined;
|
|
4209
|
+
const alias = SECTION_ALIASES[needle];
|
|
4210
|
+
if (alias) return alias;
|
|
4211
|
+
type Step = { nodes: readonly MenuNode[]; path: readonly string[] };
|
|
4212
|
+
let level: Step[] = [{ nodes: rootNodes(context), path: [] }];
|
|
4213
|
+
for (let depth = 0; depth < 3 && level.length; depth += 1) {
|
|
4214
|
+
const next: Step[] = [];
|
|
4215
|
+
for (const { nodes, path } of level) {
|
|
4216
|
+
for (const node of nodes) {
|
|
4217
|
+
if (node.kind !== "menu") continue;
|
|
4218
|
+
const at = [...path, node.id];
|
|
4219
|
+
if (node.id === needle || node.label.toLowerCase() === needle)
|
|
4220
|
+
return at;
|
|
4221
|
+
if (depth < 2) {
|
|
4222
|
+
try {
|
|
4223
|
+
next.push({ nodes: node.build(context), path: at });
|
|
4224
|
+
} catch {
|
|
4225
|
+
// A section that cannot build here has no rows to match.
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
level = next;
|
|
4231
|
+
}
|
|
4232
|
+
return undefined;
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
/** A one-track project, for breadcrumbs asked for without a window. */
|
|
4236
|
+
function breadcrumbContext(): MenuContext {
|
|
4237
|
+
return {
|
|
4238
|
+
score: createScore({
|
|
4239
|
+
tempoBpm: 120,
|
|
4240
|
+
bars: 8,
|
|
4241
|
+
tracks: [{ id: "keys", name: "keys", instrument: "piano" }],
|
|
4242
|
+
notes: [],
|
|
4243
|
+
}),
|
|
4244
|
+
trackId: "keys",
|
|
4245
|
+
playing: false,
|
|
4246
|
+
grid: "1/16",
|
|
4247
|
+
grids: ["1/16"],
|
|
4248
|
+
clickOn: false,
|
|
4249
|
+
countInBars: 1,
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
/**
|
|
4254
|
+
* `Ctrl-K › Chords and key › tuning`: where `/menu <id>` opens, from the
|
|
4255
|
+
* live labels. Undefined when `id` opens nothing.
|
|
4256
|
+
*/
|
|
4257
|
+
export function menuPath(
|
|
4258
|
+
id: string,
|
|
4259
|
+
context: MenuContext = breadcrumbContext(),
|
|
4260
|
+
): string | undefined {
|
|
4261
|
+
const path = menuSectionPath(context, id);
|
|
4262
|
+
if (!path) return undefined;
|
|
4263
|
+
const labels: string[] = [];
|
|
4264
|
+
let nodes: readonly MenuNode[] = rootNodes(context);
|
|
4265
|
+
for (const step of path) {
|
|
4266
|
+
const node = nodes.find((row) => row.kind === "menu" && row.id === step);
|
|
4267
|
+
if (node?.kind !== "menu") return undefined;
|
|
4268
|
+
labels.push(node.label);
|
|
4269
|
+
nodes = node.build(context);
|
|
4270
|
+
}
|
|
4271
|
+
return ["Ctrl-K", ...labels].join(" › ");
|
|
4272
|
+
}
|