@hraness/dawg 0.5.0 → 0.6.1
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 +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
package/src/tui/menu.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { HINTS } from "../../tui/grammar.ts";
|
|
|
13
13
|
import { arrangeDetail, arrangeNodes } from "./arrange-menu.ts";
|
|
14
14
|
import { auditionKey, isStageable, type AuditionKey } from "./audition.ts";
|
|
15
15
|
import { performanceDetail, performanceNodes } from "./performance-menu.ts";
|
|
16
|
+
import { malletsMenu, modalParameterNodes } from "./modal-menu.ts";
|
|
17
|
+
import { windParameterNodes, windsMenu } from "./wind-menu.ts";
|
|
16
18
|
import type { FaderSpec } from "./fader.ts";
|
|
17
19
|
import {
|
|
18
20
|
openingMeterCommand,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
automationPoints,
|
|
26
28
|
automationRange,
|
|
27
29
|
REVERB_IR_BUILTINS,
|
|
30
|
+
SAMPLE_FIT_MODES,
|
|
28
31
|
SAMPLE_UNITS,
|
|
29
32
|
SCORE_LIMITS,
|
|
30
33
|
isTrackAutomationParameter,
|
|
@@ -42,6 +45,10 @@ import {
|
|
|
42
45
|
CORE_EFFECTS,
|
|
43
46
|
EFFECT_NAMES,
|
|
44
47
|
FX_LANES,
|
|
48
|
+
RIG_PRESETS,
|
|
49
|
+
RIG_STAGES,
|
|
50
|
+
rigPresetOf,
|
|
51
|
+
SHOEGAZE_EFFECTS,
|
|
45
52
|
effectPresetNames,
|
|
46
53
|
effectSpec,
|
|
47
54
|
type EffectName,
|
|
@@ -85,6 +92,35 @@ import {
|
|
|
85
92
|
SYNTH_SIMPLE,
|
|
86
93
|
normalizeSynth,
|
|
87
94
|
} from "../../core/synth.ts";
|
|
95
|
+
import {
|
|
96
|
+
isStringTrack,
|
|
97
|
+
STRING_PARAMS,
|
|
98
|
+
STRING_PRESET_NAMES,
|
|
99
|
+
STRING_PRESETS,
|
|
100
|
+
BOWED_PRESET_NAMES,
|
|
101
|
+
BOW_SIMPLE_PARAMS,
|
|
102
|
+
resolveString,
|
|
103
|
+
STRING_SIMPLE_PARAMS,
|
|
104
|
+
stringPresetOf,
|
|
105
|
+
} from "../../core/strings.ts";
|
|
106
|
+
import {
|
|
107
|
+
KEYS_PARAMS,
|
|
108
|
+
KEYS_PRESETS,
|
|
109
|
+
KEYS_FAMILIES,
|
|
110
|
+
ORGAN_ROWS,
|
|
111
|
+
ORGAN_TEXT,
|
|
112
|
+
PIPE_REGISTRATIONS,
|
|
113
|
+
PIPE_STOPS,
|
|
114
|
+
isElectricFamily,
|
|
115
|
+
isKeysFamily,
|
|
116
|
+
isOrganFamily,
|
|
117
|
+
isPianoFamily,
|
|
118
|
+
keysParamsFor,
|
|
119
|
+
keysSimpleFor,
|
|
120
|
+
pipeStops,
|
|
121
|
+
resolvedKeys,
|
|
122
|
+
type OrganFamily,
|
|
123
|
+
} from "../../core/keys.ts";
|
|
88
124
|
import type { PickerItem } from "../../tui/app.ts";
|
|
89
125
|
import { BUILTIN_TABLES, BUILTIN_TABLE_NAMES } from "../audio/wavetable.ts";
|
|
90
126
|
import {
|
|
@@ -103,6 +139,8 @@ import {
|
|
|
103
139
|
PROGRESSION_STYLES,
|
|
104
140
|
SCALE_NAMES,
|
|
105
141
|
SPREADS,
|
|
142
|
+
STROKE_PATTERN_NAMES,
|
|
143
|
+
GUITAR_TUNING_NAMES,
|
|
106
144
|
keyName,
|
|
107
145
|
parseKey,
|
|
108
146
|
} from "../../core/chords.ts";
|
|
@@ -114,9 +152,17 @@ import {
|
|
|
114
152
|
type Tuning,
|
|
115
153
|
} from "../../core/tuning.ts";
|
|
116
154
|
import { rootName, tuningStepsText } from "../commands/tuning.ts";
|
|
155
|
+
import { describeGuitar } from "../commands/strum.ts";
|
|
156
|
+
import {
|
|
157
|
+
granularBrowseNodes,
|
|
158
|
+
granularMenuDetail,
|
|
159
|
+
granularMenuLabel,
|
|
160
|
+
granularMenuNodes,
|
|
161
|
+
} from "./granular-menu.ts";
|
|
117
162
|
import {
|
|
118
163
|
ARP_RATES,
|
|
119
164
|
CHORD_MODES,
|
|
165
|
+
STROKE_SPEED_MS,
|
|
120
166
|
defaultChordSettings,
|
|
121
167
|
type ChordSettings,
|
|
122
168
|
} from "./play-chords.ts";
|
|
@@ -321,7 +367,7 @@ const TRACK_LANE_LABEL: Readonly<Record<TrackAutomationParameter, string>> = {
|
|
|
321
367
|
const FX_LANE_INFO = new Map(FX_LANES.map((entry) => [entry.lane, entry]));
|
|
322
368
|
|
|
323
369
|
/** Nudge for a number spec: linear by its step, or a sixth of an octave. */
|
|
324
|
-
function specStep(
|
|
370
|
+
export function specStep(
|
|
325
371
|
spec: NumberParam,
|
|
326
372
|
): (value: number, direction: 1 | -1) => number {
|
|
327
373
|
if (spec.step === "log")
|
|
@@ -354,7 +400,14 @@ function laneLabel(lane: AutomationParameter): string {
|
|
|
354
400
|
if (!info) return lane;
|
|
355
401
|
const unit = info.spec.unit ? ` (${info.spec.unit})` : "";
|
|
356
402
|
const owner =
|
|
357
|
-
info.effect === "synth"
|
|
403
|
+
info.effect === "synth" ||
|
|
404
|
+
info.effect === "string" ||
|
|
405
|
+
info.effect === "keys" ||
|
|
406
|
+
info.effect === "modal" ||
|
|
407
|
+
info.effect === "grain" ||
|
|
408
|
+
info.effect === "wind"
|
|
409
|
+
? info.effect
|
|
410
|
+
: effectSpec(info.effect).label;
|
|
358
411
|
return `${owner} ${info.param}${unit}`;
|
|
359
412
|
}
|
|
360
413
|
|
|
@@ -486,8 +539,77 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
486
539
|
},
|
|
487
540
|
]
|
|
488
541
|
: [];
|
|
542
|
+
const grainLabel = granularMenuLabel(track);
|
|
543
|
+
const granular: MenuNode[] =
|
|
544
|
+
track && grainLabel
|
|
545
|
+
? [
|
|
546
|
+
{
|
|
547
|
+
kind: "menu",
|
|
548
|
+
id: "granular",
|
|
549
|
+
label: grainLabel,
|
|
550
|
+
detail: granularMenuDetail(track),
|
|
551
|
+
help: "grain clouds: preset, source, position, scan, grain size, pitch, shimmer",
|
|
552
|
+
build: (inner) => granularMenuNodes(focused(inner)),
|
|
553
|
+
},
|
|
554
|
+
]
|
|
555
|
+
: [];
|
|
556
|
+
const keys: MenuNode[] =
|
|
557
|
+
track && isOrganFamily(track.instrument) && track.keys
|
|
558
|
+
? [
|
|
559
|
+
{
|
|
560
|
+
kind: "menu",
|
|
561
|
+
id: "keys",
|
|
562
|
+
label: "keys",
|
|
563
|
+
detail: `${track.instrument}${track.keys.preset ? ` · ${track.keys.preset}` : ""}`,
|
|
564
|
+
help: "the modelled organ: preset, drawbars, registers or stops, rotary",
|
|
565
|
+
build: (inner) => {
|
|
566
|
+
const current = focused(inner);
|
|
567
|
+
return current ? organNodes(current) : [];
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
]
|
|
571
|
+
: track && isKeysFamily(track.instrument) && track.keys
|
|
572
|
+
? [
|
|
573
|
+
{
|
|
574
|
+
kind: "menu",
|
|
575
|
+
id: "keys",
|
|
576
|
+
label: "keys",
|
|
577
|
+
detail: `${track.instrument}${track.keys.preset ? ` · ${track.keys.preset}` : ""}`,
|
|
578
|
+
help: isElectricFamily(track.instrument)
|
|
579
|
+
? "electric keys: preset, bark, bell, tone, vibe/trem, pickup and mute"
|
|
580
|
+
: "the modelled piano: preset, touch, hammers, dampers, stretch",
|
|
581
|
+
build: (inner) => {
|
|
582
|
+
const current = focused(inner);
|
|
583
|
+
return current
|
|
584
|
+
? keysNodes(current, keysParamsFor(current.instrument))
|
|
585
|
+
: [];
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
]
|
|
589
|
+
: [];
|
|
590
|
+
// 0.6.1: fretting for strum and the guitar perform mode, on guitar-like
|
|
591
|
+
// tracks (a string voice, a rig, or a guitar setup already stored).
|
|
592
|
+
const guitar: MenuNode[] =
|
|
593
|
+
track &&
|
|
594
|
+
(track.guitar ||
|
|
595
|
+
track.string ||
|
|
596
|
+
RIG_STAGES.some((stage) => track.fx?.[stage] !== undefined))
|
|
597
|
+
? [
|
|
598
|
+
{
|
|
599
|
+
kind: "menu",
|
|
600
|
+
id: "guitar",
|
|
601
|
+
label: "guitar",
|
|
602
|
+
detail: describeGuitar(track.guitar),
|
|
603
|
+
help: "tuning, capo, hand stretch, ringing open strings and position for strum and chords perform guitar",
|
|
604
|
+
build: (inner) => guitarNodes(focused(inner)),
|
|
605
|
+
},
|
|
606
|
+
]
|
|
607
|
+
: [];
|
|
489
608
|
return [
|
|
490
609
|
...parameterNodes(context),
|
|
610
|
+
...granular,
|
|
611
|
+
...keys,
|
|
612
|
+
...guitar,
|
|
491
613
|
...tuning,
|
|
492
614
|
{
|
|
493
615
|
kind: "menu",
|
|
@@ -508,6 +630,84 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
508
630
|
];
|
|
509
631
|
}
|
|
510
632
|
|
|
633
|
+
/** Sound > guitar: Track.guitar rows plus a strum action. */
|
|
634
|
+
function guitarNodes(track: Track | undefined): MenuNode[] {
|
|
635
|
+
if (!track) return [];
|
|
636
|
+
const g = track.guitar;
|
|
637
|
+
const tune = g?.tune ?? "standard";
|
|
638
|
+
const tuneName = typeof tune === "string" ? tune : tune.join(" ");
|
|
639
|
+
const int = (
|
|
640
|
+
label: string,
|
|
641
|
+
field: "capo" | "hand" | "position",
|
|
642
|
+
min: number,
|
|
643
|
+
max: number,
|
|
644
|
+
fallback: number,
|
|
645
|
+
help: string,
|
|
646
|
+
): MenuNode => ({
|
|
647
|
+
kind: "number",
|
|
648
|
+
label,
|
|
649
|
+
help,
|
|
650
|
+
value: g?.[field] ?? fallback,
|
|
651
|
+
min,
|
|
652
|
+
max,
|
|
653
|
+
step: linear(1, min, max),
|
|
654
|
+
format: (value) => num(Math.round(value)),
|
|
655
|
+
command: (value) => `guitar ${field} ${Math.round(value)}`,
|
|
656
|
+
});
|
|
657
|
+
return [
|
|
658
|
+
{
|
|
659
|
+
kind: "choice",
|
|
660
|
+
label: "tune",
|
|
661
|
+
help: "open strings; type guitar tune D A D G A D for your own",
|
|
662
|
+
value: tuneName,
|
|
663
|
+
options: (GUITAR_TUNING_NAMES as readonly string[]).includes(tuneName)
|
|
664
|
+
? GUITAR_TUNING_NAMES
|
|
665
|
+
: [tuneName, ...GUITAR_TUNING_NAMES],
|
|
666
|
+
command: (option) => `guitar tune ${option}`,
|
|
667
|
+
},
|
|
668
|
+
int(
|
|
669
|
+
"capo",
|
|
670
|
+
"capo",
|
|
671
|
+
0,
|
|
672
|
+
12,
|
|
673
|
+
0,
|
|
674
|
+
"capo fret; chord shapes sound this many semitones up",
|
|
675
|
+
),
|
|
676
|
+
int("hand", "hand", 3, 6, 4, "how many frets one hand shape may span"),
|
|
677
|
+
{
|
|
678
|
+
kind: "number",
|
|
679
|
+
label: "ring",
|
|
680
|
+
help: "0 closed shapes .. 1 lets open strings ring",
|
|
681
|
+
value: g?.ring ?? 0.5,
|
|
682
|
+
min: 0,
|
|
683
|
+
max: 1,
|
|
684
|
+
step: linear(0.1, 0, 1),
|
|
685
|
+
format: (value) => num(value),
|
|
686
|
+
command: (value) => `guitar ring ${Math.round(value * 10) / 10}`,
|
|
687
|
+
},
|
|
688
|
+
int(
|
|
689
|
+
"position",
|
|
690
|
+
"position",
|
|
691
|
+
0,
|
|
692
|
+
12,
|
|
693
|
+
0,
|
|
694
|
+
"preferred fret position for voicings",
|
|
695
|
+
),
|
|
696
|
+
{
|
|
697
|
+
kind: "action",
|
|
698
|
+
label: "strum the chords on this track",
|
|
699
|
+
command: "strum",
|
|
700
|
+
help: "replaces block chords with strummed guitar notes",
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
kind: "action",
|
|
704
|
+
label: "reset",
|
|
705
|
+
command: "guitar reset",
|
|
706
|
+
help: "standard tuning, no capo",
|
|
707
|
+
},
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
|
|
511
711
|
/** Rhythm: the euclid editor, then grooves and kits. */
|
|
512
712
|
function rhythmNodes(): MenuNode[] {
|
|
513
713
|
return [
|
|
@@ -815,6 +1015,34 @@ function chordNodes(context: MenuContext): MenuNode[] {
|
|
|
815
1015
|
options: CHORD_PATTERNS.map((pattern) => pattern.name),
|
|
816
1016
|
command: (option) => `/chords pattern ${option}`,
|
|
817
1017
|
},
|
|
1018
|
+
// Strum grid and speed only act when perform is guitar.
|
|
1019
|
+
...(chords.perform !== "guitar"
|
|
1020
|
+
? []
|
|
1021
|
+
: ([
|
|
1022
|
+
{
|
|
1023
|
+
kind: "choice",
|
|
1024
|
+
label: "strokes",
|
|
1025
|
+
help: "guitar strum grid (D down, U up, d u light, x chuck, - rest); type /chords strokes D-DU-UDU for your own",
|
|
1026
|
+
value: chords.strokes,
|
|
1027
|
+
options: (STROKE_PATTERN_NAMES as readonly string[]).includes(
|
|
1028
|
+
chords.strokes,
|
|
1029
|
+
)
|
|
1030
|
+
? STROKE_PATTERN_NAMES
|
|
1031
|
+
: [chords.strokes, ...STROKE_PATTERN_NAMES],
|
|
1032
|
+
command: (option) => `/chords strokes ${option}`,
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
kind: "number",
|
|
1036
|
+
label: "speed",
|
|
1037
|
+
help: "ms a full down stroke takes (0 hits every string at once); type /chords speed 1/32b to set it in beats",
|
|
1038
|
+
value: chords.speed,
|
|
1039
|
+
min: STROKE_SPEED_MS.min,
|
|
1040
|
+
max: STROKE_SPEED_MS.max,
|
|
1041
|
+
step: linear(1, STROKE_SPEED_MS.min, STROKE_SPEED_MS.max),
|
|
1042
|
+
format: (value) => `${num(value)} ms`,
|
|
1043
|
+
command: (value) => `/chords speed ${Math.round(value)}ms`,
|
|
1044
|
+
},
|
|
1045
|
+
] satisfies MenuNode[])),
|
|
818
1046
|
{
|
|
819
1047
|
kind: "choice",
|
|
820
1048
|
label: "arp rate",
|
|
@@ -903,7 +1131,7 @@ function trackNodes(context: MenuContext): MenuNode[] {
|
|
|
903
1131
|
}
|
|
904
1132
|
|
|
905
1133
|
function instrumentNode(track: Track): MenuNode {
|
|
906
|
-
const options: string[] = [...AVAILABLE_INSTRUMENTS];
|
|
1134
|
+
const options: string[] = [...AVAILABLE_INSTRUMENTS, ...KEYS_FAMILIES];
|
|
907
1135
|
if (!options.includes(track.instrument)) options.push(track.instrument);
|
|
908
1136
|
return {
|
|
909
1137
|
kind: "choice",
|
|
@@ -956,6 +1184,37 @@ function parameterNodes(context: MenuContext): MenuNode[] {
|
|
|
956
1184
|
// A wavetable track also has the synth voice's envelope, filters and FM.
|
|
957
1185
|
if (isWavetableInstrument(track.instrument))
|
|
958
1186
|
nodes.push(...wavetableNodes(track, context.projectRoot));
|
|
1187
|
+
if (isStringTrack(track)) {
|
|
1188
|
+
nodes.push(
|
|
1189
|
+
...stringNodes(
|
|
1190
|
+
track,
|
|
1191
|
+
resolveString(track.string).exciter === "bow"
|
|
1192
|
+
? BOW_SIMPLE_PARAMS
|
|
1193
|
+
: STRING_SIMPLE_PARAMS,
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
nodes.push({
|
|
1197
|
+
kind: "menu",
|
|
1198
|
+
id: "string:advanced",
|
|
1199
|
+
label: "advanced",
|
|
1200
|
+
detail: `all ${Object.keys(STRING_PARAMS).length} params`,
|
|
1201
|
+
help: "every string parameter: exciter, loss, body, buzz, sympathetics, bow (pressure, speed, attack, vib, vibdelay, tremhz, sord, dyn)",
|
|
1202
|
+
build: (inner) => {
|
|
1203
|
+
const current = focused(inner);
|
|
1204
|
+
return current
|
|
1205
|
+
? stringNodes(current, Object.keys(STRING_PARAMS), false)
|
|
1206
|
+
: [];
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
if (Object.keys(track.string ?? {}).some((key) => key !== "preset"))
|
|
1210
|
+
nodes.push({
|
|
1211
|
+
kind: "action",
|
|
1212
|
+
label: "reset to preset",
|
|
1213
|
+
command: "string reset",
|
|
1214
|
+
help: "drop this track's string overrides, keep the preset",
|
|
1215
|
+
});
|
|
1216
|
+
return nodes;
|
|
1217
|
+
}
|
|
959
1218
|
if (track.sampler) {
|
|
960
1219
|
nodes.push({
|
|
961
1220
|
kind: "info",
|
|
@@ -982,6 +1241,35 @@ function parameterNodes(context: MenuContext): MenuNode[] {
|
|
|
982
1241
|
label: "add a voice",
|
|
983
1242
|
value: "/sample <path> [as <voice>]",
|
|
984
1243
|
});
|
|
1244
|
+
} else if (isOrganFamily(track.instrument) && track.keys) {
|
|
1245
|
+
// f061-organ: preset, Drawbars/Registers/Stops sub-menus, organ rows.
|
|
1246
|
+
nodes.push(...organNodes(track));
|
|
1247
|
+
} else if (isKeysFamily(track.instrument) && track.keys) {
|
|
1248
|
+
nodes.push(...keysNodes(track, keysSimpleFor(track.instrument)));
|
|
1249
|
+
const all = keysParamsFor(track.instrument);
|
|
1250
|
+
nodes.push({
|
|
1251
|
+
kind: "menu",
|
|
1252
|
+
id: "keys:all",
|
|
1253
|
+
label: isElectricFamily(track.instrument)
|
|
1254
|
+
? `all ${track.instrument} params`
|
|
1255
|
+
: "all piano params",
|
|
1256
|
+
detail: `all ${all.length} params`,
|
|
1257
|
+
help: isElectricFamily(track.instrument)
|
|
1258
|
+
? "every electric keys parameter for this family"
|
|
1259
|
+
: "every modelled piano parameter",
|
|
1260
|
+
build: (inner) => {
|
|
1261
|
+
const current = focused(inner);
|
|
1262
|
+
return current
|
|
1263
|
+
? keysNodes(current, keysParamsFor(current.instrument))
|
|
1264
|
+
: [];
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
} else if (track.instrument === "modal") {
|
|
1268
|
+
// 0.6 modal percussion: preset, mallet and MODAL_PARAMS rows.
|
|
1269
|
+
nodes.push(...modalParameterNodes(track));
|
|
1270
|
+
} else if (track.instrument === "wind" && track.wind) {
|
|
1271
|
+
// 0.6.1 wind engine: preset and WIND_PARAMS rows.
|
|
1272
|
+
nodes.push(...windParameterNodes(track));
|
|
985
1273
|
} else if (!isDrumInstrument(track.instrument)) {
|
|
986
1274
|
nodes.push(...synthNodes(track, SYNTH_SIMPLE));
|
|
987
1275
|
nodes.push({
|
|
@@ -1089,10 +1377,141 @@ function sampleVoiceNodes(context: MenuContext, voice: string): MenuNode[] {
|
|
|
1089
1377
|
label: "cut / loopAt",
|
|
1090
1378
|
value: `/sample set ${voice} cut hats · loopAt 2`,
|
|
1091
1379
|
},
|
|
1380
|
+
// 0.6 fit: the sample's own tempo, its length in beats, and how it fits.
|
|
1381
|
+
{
|
|
1382
|
+
kind: "number",
|
|
1383
|
+
label: "bpm",
|
|
1384
|
+
value: ref.bpm,
|
|
1385
|
+
start: Math.round(context.score.tempoBpm),
|
|
1386
|
+
off: "off",
|
|
1387
|
+
min: SCORE_LIMITS.minSampleBpm,
|
|
1388
|
+
max: SCORE_LIMITS.maxSampleBpm,
|
|
1389
|
+
step: linear(1, SCORE_LIMITS.minSampleBpm, SCORE_LIMITS.maxSampleBpm),
|
|
1390
|
+
format: num,
|
|
1391
|
+
command: (value) => `/bpm ${num(value)} ${voice}`,
|
|
1392
|
+
reset: `/bpm off ${voice}`,
|
|
1393
|
+
help: SAMPLE_CONTROLS.bpm,
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
kind: "choice",
|
|
1397
|
+
label: "fitmode",
|
|
1398
|
+
// off: unset (plays as repitch); auto suggests one from the sound.
|
|
1399
|
+
value: ref.fitmode ?? "off",
|
|
1400
|
+
options: ["off", ...SAMPLE_FIT_MODES, "auto"],
|
|
1401
|
+
command: (option) => `/fitmode ${option} ${voice}`,
|
|
1402
|
+
help: `${SAMPLE_CONTROLS.fitmode} · beats for drums, tones for pads · set bpm or len first`,
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
kind: "number",
|
|
1406
|
+
label: "len",
|
|
1407
|
+
value: ref.len,
|
|
1408
|
+
start: 4,
|
|
1409
|
+
off: "off",
|
|
1410
|
+
min: 0.25,
|
|
1411
|
+
max: SCORE_LIMITS.maxSampleLenBeats,
|
|
1412
|
+
step: linear(0.25, 0.25, SCORE_LIMITS.maxSampleLenBeats),
|
|
1413
|
+
format: num,
|
|
1414
|
+
command: (value) => `/len ${num(value)} ${voice}`,
|
|
1415
|
+
reset: `/len off ${voice}`,
|
|
1416
|
+
help: SAMPLE_CONTROLS.len,
|
|
1417
|
+
},
|
|
1418
|
+
// 0.6.1 shift: pitch without changing length, formants kept or moved,
|
|
1419
|
+
// and the voice's fade in and out.
|
|
1420
|
+
{
|
|
1421
|
+
kind: "number",
|
|
1422
|
+
label: "shift",
|
|
1423
|
+
value: ref.shift,
|
|
1424
|
+
start: 0,
|
|
1425
|
+
off: "off",
|
|
1426
|
+
min: -SCORE_LIMITS.maxSampleShift,
|
|
1427
|
+
max: SCORE_LIMITS.maxSampleShift,
|
|
1428
|
+
step: linear(
|
|
1429
|
+
1,
|
|
1430
|
+
-SCORE_LIMITS.maxSampleShift,
|
|
1431
|
+
SCORE_LIMITS.maxSampleShift,
|
|
1432
|
+
),
|
|
1433
|
+
format: num,
|
|
1434
|
+
command: (value) => `/shift ${num(value)} ${voice}`,
|
|
1435
|
+
reset: `/shift off ${voice}`,
|
|
1436
|
+
help: SAMPLE_CONTROLS.shift,
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
kind: "number",
|
|
1440
|
+
label: "formant",
|
|
1441
|
+
value: ref.formant,
|
|
1442
|
+
start: 0,
|
|
1443
|
+
off: "follow",
|
|
1444
|
+
min: -SCORE_LIMITS.maxSampleShift,
|
|
1445
|
+
max: SCORE_LIMITS.maxSampleShift,
|
|
1446
|
+
step: linear(
|
|
1447
|
+
1,
|
|
1448
|
+
-SCORE_LIMITS.maxSampleShift,
|
|
1449
|
+
SCORE_LIMITS.maxSampleShift,
|
|
1450
|
+
),
|
|
1451
|
+
format: (value) => (value === 0 ? "keep" : num(value)),
|
|
1452
|
+
command: (value) => set(`formant ${num(value)}`),
|
|
1453
|
+
reset: set("formant off"),
|
|
1454
|
+
help: `${SAMPLE_CONTROLS.formant} · needs shift`,
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
kind: "number",
|
|
1458
|
+
label: "fade in",
|
|
1459
|
+
value: ref.fadeInTime,
|
|
1460
|
+
start: 0.01,
|
|
1461
|
+
off: "off",
|
|
1462
|
+
min: 0,
|
|
1463
|
+
max: SCORE_LIMITS.maxSampleFadeSeconds,
|
|
1464
|
+
step: linear(0.01, 0, SCORE_LIMITS.maxSampleFadeSeconds),
|
|
1465
|
+
format: num,
|
|
1466
|
+
command: (value) => `/fade in ${num(value)} ${voice}`,
|
|
1467
|
+
reset: `/fade in off ${voice}`,
|
|
1468
|
+
help: SAMPLE_CONTROLS.fadeInTime,
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
kind: "number",
|
|
1472
|
+
label: "fade out",
|
|
1473
|
+
value: ref.fadeTime,
|
|
1474
|
+
start: 0.1,
|
|
1475
|
+
off: "off",
|
|
1476
|
+
min: 0,
|
|
1477
|
+
max: SCORE_LIMITS.maxSampleFadeSeconds,
|
|
1478
|
+
step: linear(0.05, 0, SCORE_LIMITS.maxSampleFadeSeconds),
|
|
1479
|
+
format: num,
|
|
1480
|
+
command: (value) => `/fade out ${num(value)} ${voice}`,
|
|
1481
|
+
reset: `/fade out off ${voice}`,
|
|
1482
|
+
help: SAMPLE_CONTROLS.fadeTime,
|
|
1483
|
+
},
|
|
1484
|
+
// 0.6.1 layers: velocity layer (SFZ lovel/hivel) and round-robin group.
|
|
1485
|
+
{
|
|
1486
|
+
kind: "choice",
|
|
1487
|
+
label: "Velocity layer",
|
|
1488
|
+
value: ref.vel ? `${ref.vel[0]}-${ref.vel[1]}` : "off",
|
|
1489
|
+
options: velocityLayerOptions(ref.vel),
|
|
1490
|
+
command: (option) => set(`vel ${option}`),
|
|
1491
|
+
help: `${SAMPLE_CONTROLS.vel} · two layers: 0-63 and 64-127 · three: 0-42 43-84 85-127`,
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
kind: "entry",
|
|
1495
|
+
label: "Round robin",
|
|
1496
|
+
value: ref.rr ?? "",
|
|
1497
|
+
placeholder: "group name, e.g. sn",
|
|
1498
|
+
command: (text) => set(`rr ${text.trim() || "off"}`),
|
|
1499
|
+
example: `/sample set ${voice} rr sn`,
|
|
1500
|
+
help: SAMPLE_CONTROLS.rr,
|
|
1501
|
+
},
|
|
1092
1502
|
);
|
|
1093
1503
|
return nodes;
|
|
1094
1504
|
}
|
|
1095
1505
|
|
|
1506
|
+
/** Common velocity splits, plus the voice's own range when it is another. */
|
|
1507
|
+
function velocityLayerOptions(
|
|
1508
|
+
vel: readonly [number, number] | undefined,
|
|
1509
|
+
): string[] {
|
|
1510
|
+
const options = ["off", "0-63", "64-127", "0-42", "43-84", "85-127"];
|
|
1511
|
+
const own = vel ? `${vel[0]}-${vel[1]}` : undefined;
|
|
1512
|
+
return own && !options.includes(own) ? [...options, own] : options;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1096
1515
|
/** Synth preset first, then one row per parameter. */
|
|
1097
1516
|
/** The preset this track's voice still matches exactly, if any. */
|
|
1098
1517
|
function matchingSynthPreset(track: Track): string | undefined {
|
|
@@ -1105,6 +1524,233 @@ function matchingSynthPreset(track: Track): string | undefined {
|
|
|
1105
1524
|
)?.[0];
|
|
1106
1525
|
}
|
|
1107
1526
|
|
|
1527
|
+
/** Modelled piano rows: the preset, then each parameter (`keys <p> <v>`). */
|
|
1528
|
+
function keysNodes(
|
|
1529
|
+
track: Track,
|
|
1530
|
+
params: readonly string[],
|
|
1531
|
+
presets: readonly string[] = keysPresetNames(track.instrument),
|
|
1532
|
+
): MenuNode[] {
|
|
1533
|
+
const effective = resolvedKeys(track.instrument, track.keys);
|
|
1534
|
+
const nodes: MenuNode[] = [
|
|
1535
|
+
{
|
|
1536
|
+
kind: "choice",
|
|
1537
|
+
label: "preset",
|
|
1538
|
+
value: track.keys?.preset ?? "—",
|
|
1539
|
+
options: presets,
|
|
1540
|
+
command: (preset) => `keys preset ${preset}`,
|
|
1541
|
+
help: isOrganFamily(track.instrument)
|
|
1542
|
+
? "a starting organ; every value stays editable"
|
|
1543
|
+
: isElectricFamily(track.instrument)
|
|
1544
|
+
? "a starting electric piano or clav; every value stays editable"
|
|
1545
|
+
: "a starting piano; every value stays editable",
|
|
1546
|
+
},
|
|
1547
|
+
];
|
|
1548
|
+
for (const key of params) {
|
|
1549
|
+
const param = KEYS_PARAMS[key]!;
|
|
1550
|
+
const stored = track.keys?.[key];
|
|
1551
|
+
if (param.kind === "number") {
|
|
1552
|
+
const base = effective[key] as number;
|
|
1553
|
+
nodes.push({
|
|
1554
|
+
kind: "number",
|
|
1555
|
+
label: key,
|
|
1556
|
+
value: typeof stored === "number" ? stored : undefined,
|
|
1557
|
+
start: base,
|
|
1558
|
+
off: withUnit(formatParam(param, base), param.unit),
|
|
1559
|
+
min: param.min,
|
|
1560
|
+
max: param.max,
|
|
1561
|
+
step: specStep(param),
|
|
1562
|
+
format: (value) => withUnit(formatParam(param, value), param.unit),
|
|
1563
|
+
command: (value) => `keys ${key} ${formatParam(param, value)}`,
|
|
1564
|
+
reset: `keys ${key} off`,
|
|
1565
|
+
help: param.doc,
|
|
1566
|
+
});
|
|
1567
|
+
} else if (param.kind === "enum")
|
|
1568
|
+
nodes.push({
|
|
1569
|
+
kind: "choice",
|
|
1570
|
+
label: key,
|
|
1571
|
+
value: typeof stored === "string" ? stored : String(effective[key]),
|
|
1572
|
+
options: param.values,
|
|
1573
|
+
command: (option) => `keys ${key} ${option}`,
|
|
1574
|
+
help: param.doc,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
nodes.push({
|
|
1578
|
+
kind: "action",
|
|
1579
|
+
label: "reset to the family",
|
|
1580
|
+
command: "keys reset",
|
|
1581
|
+
help: "clear every override and the preset, and the effects the preset added (keeps the family's own sound)",
|
|
1582
|
+
});
|
|
1583
|
+
return nodes;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// ---- f061-organ menu rows -------------------------------------------------
|
|
1587
|
+
|
|
1588
|
+
const PIANO_PRESET_NAMES = Object.keys(KEYS_PRESETS).filter((name) =>
|
|
1589
|
+
isPianoFamily(KEYS_PRESETS[name]!.instrument),
|
|
1590
|
+
);
|
|
1591
|
+
const ORGAN_PRESET_NAMES = Object.keys(KEYS_PRESETS).filter((name) =>
|
|
1592
|
+
isOrganFamily(KEYS_PRESETS[name]!.instrument),
|
|
1593
|
+
);
|
|
1594
|
+
const ELECTRIC_PRESET_NAMES = Object.keys(KEYS_PRESETS).filter((name) =>
|
|
1595
|
+
isElectricFamily(KEYS_PRESETS[name]!.instrument),
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
/** The presets of the track's kind of keys (pianos, electric or organs). */
|
|
1599
|
+
function keysPresetNames(instrument: string): readonly string[] {
|
|
1600
|
+
if (isOrganFamily(instrument)) return ORGAN_PRESET_NAMES;
|
|
1601
|
+
if (isElectricFamily(instrument)) return ELECTRIC_PRESET_NAMES;
|
|
1602
|
+
return PIANO_PRESET_NAMES;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const DRAWBAR_FEET = [
|
|
1606
|
+
"16'",
|
|
1607
|
+
"5⅓'",
|
|
1608
|
+
"8'",
|
|
1609
|
+
"4'",
|
|
1610
|
+
"2⅔'",
|
|
1611
|
+
"2'",
|
|
1612
|
+
"1⅗'",
|
|
1613
|
+
"1⅓'",
|
|
1614
|
+
"1'",
|
|
1615
|
+
];
|
|
1616
|
+
const REGISTER_FEET = ["16'", "8'", "4'", "2⅔'", "2'"];
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* One 0-8 row per footage of a digit string (`keys drawbars 888000000`):
|
|
1620
|
+
* left/right pull a bar in or out, x puts the whole set back.
|
|
1621
|
+
*/
|
|
1622
|
+
function digitNodes(
|
|
1623
|
+
name: "drawbars" | "registers",
|
|
1624
|
+
text: string,
|
|
1625
|
+
feet: readonly string[],
|
|
1626
|
+
stored: boolean,
|
|
1627
|
+
): MenuNode[] {
|
|
1628
|
+
const digits = text.split("").map(Number);
|
|
1629
|
+
const nodes: MenuNode[] = feet.map((foot, index): MenuNode => {
|
|
1630
|
+
const at = (value: number) =>
|
|
1631
|
+
digits.map((d, i) => (i === index ? value : d)).join("");
|
|
1632
|
+
return {
|
|
1633
|
+
kind: "number",
|
|
1634
|
+
label: foot,
|
|
1635
|
+
value: digits[index]!,
|
|
1636
|
+
min: 0,
|
|
1637
|
+
max: 8,
|
|
1638
|
+
step: (value, direction) => Math.max(0, Math.min(8, value + direction)),
|
|
1639
|
+
format: (value) =>
|
|
1640
|
+
`${value} ${"█".repeat(value)}${"·".repeat(8 - value)}`,
|
|
1641
|
+
command: (value) => `keys ${name} ${at(value)}`,
|
|
1642
|
+
// x resets this bar only, to its default digit.
|
|
1643
|
+
reset: `keys ${name} ${at(Number(ORGAN_TEXT[name].default[index] ?? 0))}`,
|
|
1644
|
+
help: `${foot} ${name === "drawbars" ? "drawbar" : "register"} 0-8`,
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1647
|
+
nodes.push({
|
|
1648
|
+
kind: "entry",
|
|
1649
|
+
label: `${name} (type)`,
|
|
1650
|
+
value: stored ? text : `${text} (default)`,
|
|
1651
|
+
placeholder: `${feet.length} digits 0-8`,
|
|
1652
|
+
command: (typed) =>
|
|
1653
|
+
new RegExp(`^[0-8]{${feet.length}}$`).test(typed.trim())
|
|
1654
|
+
? `keys ${name} ${typed.trim()}`
|
|
1655
|
+
: undefined,
|
|
1656
|
+
example: `keys ${name} ${text}`,
|
|
1657
|
+
});
|
|
1658
|
+
return nodes;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/** Pipe stops: a registration, then one toggle per stop. */
|
|
1662
|
+
function stopNodes(text: string): MenuNode[] {
|
|
1663
|
+
const on = pipeStops(text);
|
|
1664
|
+
const nodes: MenuNode[] = [
|
|
1665
|
+
{
|
|
1666
|
+
kind: "choice",
|
|
1667
|
+
label: "registration",
|
|
1668
|
+
value: Object.keys(PIPE_REGISTRATIONS).includes(text) ? text : "—",
|
|
1669
|
+
options: Object.keys(PIPE_REGISTRATIONS),
|
|
1670
|
+
command: (option) => `keys stops ${option}`,
|
|
1671
|
+
help: "a named registration (stops drawn together)",
|
|
1672
|
+
},
|
|
1673
|
+
];
|
|
1674
|
+
for (const stop of PIPE_STOPS)
|
|
1675
|
+
nodes.push({
|
|
1676
|
+
kind: "toggle",
|
|
1677
|
+
label: stop,
|
|
1678
|
+
value: on.includes(stop),
|
|
1679
|
+
command: (value) => {
|
|
1680
|
+
const next = value
|
|
1681
|
+
? PIPE_STOPS.filter((name) => name === stop || on.includes(name))
|
|
1682
|
+
: on.filter((name) => name !== stop);
|
|
1683
|
+
return next.length > 0
|
|
1684
|
+
? `keys stops ${next.join(" ")}`
|
|
1685
|
+
: "keys stops off";
|
|
1686
|
+
},
|
|
1687
|
+
help: `draw or retire the ${stop} stop`,
|
|
1688
|
+
});
|
|
1689
|
+
return nodes;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/** The organ's rows: preset, its family's sub-menu, then its parameters. */
|
|
1693
|
+
function organNodes(track: Track): MenuNode[] {
|
|
1694
|
+
const family = track.instrument as OrganFamily;
|
|
1695
|
+
const effective = resolvedKeys(track.instrument, track.keys);
|
|
1696
|
+
const rows = ORGAN_ROWS[family].filter((row) => row in KEYS_PARAMS);
|
|
1697
|
+
const nodes = keysNodes(track, rows, ORGAN_PRESET_NAMES);
|
|
1698
|
+
const text = (name: keyof typeof ORGAN_TEXT) =>
|
|
1699
|
+
String(effective[name] ?? ORGAN_TEXT[name].default);
|
|
1700
|
+
const sub: MenuNode =
|
|
1701
|
+
family === "tonewheel"
|
|
1702
|
+
? {
|
|
1703
|
+
kind: "menu",
|
|
1704
|
+
id: "keys:drawbars",
|
|
1705
|
+
label: "Drawbars",
|
|
1706
|
+
detail: text("drawbars"),
|
|
1707
|
+
help: "nine drawbars 16' to 1' (0 in, 8 full out)",
|
|
1708
|
+
build: (inner) => {
|
|
1709
|
+
const current = focused(inner) ?? track;
|
|
1710
|
+
const value = resolvedKeys(current.instrument, current.keys);
|
|
1711
|
+
return digitNodes(
|
|
1712
|
+
"drawbars",
|
|
1713
|
+
String(value.drawbars ?? ORGAN_TEXT.drawbars.default),
|
|
1714
|
+
DRAWBAR_FEET,
|
|
1715
|
+
current.keys?.drawbars !== undefined,
|
|
1716
|
+
);
|
|
1717
|
+
},
|
|
1718
|
+
}
|
|
1719
|
+
: family === "combo"
|
|
1720
|
+
? {
|
|
1721
|
+
kind: "menu",
|
|
1722
|
+
id: "keys:registers",
|
|
1723
|
+
label: "Registers",
|
|
1724
|
+
detail: text("registers"),
|
|
1725
|
+
help: "five combo-organ registers 16' to 2' (0 off, 8 full)",
|
|
1726
|
+
build: (inner) => {
|
|
1727
|
+
const current = focused(inner) ?? track;
|
|
1728
|
+
const value = resolvedKeys(current.instrument, current.keys);
|
|
1729
|
+
return digitNodes(
|
|
1730
|
+
"registers",
|
|
1731
|
+
String(value.registers ?? ORGAN_TEXT.registers.default),
|
|
1732
|
+
REGISTER_FEET,
|
|
1733
|
+
current.keys?.registers !== undefined,
|
|
1734
|
+
);
|
|
1735
|
+
},
|
|
1736
|
+
}
|
|
1737
|
+
: {
|
|
1738
|
+
kind: "menu",
|
|
1739
|
+
id: "keys:stops",
|
|
1740
|
+
label: "Stops",
|
|
1741
|
+
detail: text("stops"),
|
|
1742
|
+
help: "pipe stops and registrations",
|
|
1743
|
+
build: (inner) => {
|
|
1744
|
+
const current = focused(inner) ?? track;
|
|
1745
|
+
const value = resolvedKeys(current.instrument, current.keys);
|
|
1746
|
+
return stopNodes(String(value.stops ?? ORGAN_TEXT.stops.default));
|
|
1747
|
+
},
|
|
1748
|
+
};
|
|
1749
|
+
// After the preset row, before the parameters.
|
|
1750
|
+
nodes.splice(1, 0, sub);
|
|
1751
|
+
return nodes;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1108
1754
|
function synthNodes(track: Track, keys: readonly string[]): MenuNode[] {
|
|
1109
1755
|
const nodes: MenuNode[] = [];
|
|
1110
1756
|
if (keys === SYNTH_SIMPLE)
|
|
@@ -1175,6 +1821,57 @@ const SYNTH_LABEL: Readonly<Record<string, string>> = {
|
|
|
1175
1821
|
fm: "FM amount",
|
|
1176
1822
|
};
|
|
1177
1823
|
|
|
1824
|
+
/** String preset first (simple view), then one row per parameter. */
|
|
1825
|
+
function stringNodes(
|
|
1826
|
+
track: Track,
|
|
1827
|
+
keys: readonly string[],
|
|
1828
|
+
withPreset = true,
|
|
1829
|
+
): MenuNode[] {
|
|
1830
|
+
const nodes: MenuNode[] = [];
|
|
1831
|
+
const preset = STRING_PRESETS[stringPresetOf(track.string)]!;
|
|
1832
|
+
if (withPreset)
|
|
1833
|
+
nodes.push({
|
|
1834
|
+
kind: "choice",
|
|
1835
|
+
label: "preset",
|
|
1836
|
+
help: preset.doc,
|
|
1837
|
+
value: stringPresetOf(track.string),
|
|
1838
|
+
options: STRING_PRESET_NAMES,
|
|
1839
|
+
command: (name) => `string preset ${name}`,
|
|
1840
|
+
});
|
|
1841
|
+
for (const key of keys) {
|
|
1842
|
+
const param = STRING_PARAMS[key]!;
|
|
1843
|
+
const own = track.string?.[key];
|
|
1844
|
+
const base = preset.values[key] ?? param.default;
|
|
1845
|
+
if (param.kind === "number")
|
|
1846
|
+
nodes.push({
|
|
1847
|
+
kind: "number",
|
|
1848
|
+
label: param.unit ? `${key} ${param.unit}` : key,
|
|
1849
|
+
help: param.doc,
|
|
1850
|
+
value: typeof own === "number" ? own : undefined,
|
|
1851
|
+
start: typeof base === "number" ? base : param.default,
|
|
1852
|
+
off: formatParam(
|
|
1853
|
+
param,
|
|
1854
|
+
typeof base === "number" ? base : param.default,
|
|
1855
|
+
),
|
|
1856
|
+
min: param.min,
|
|
1857
|
+
max: param.max,
|
|
1858
|
+
step: specStep(param),
|
|
1859
|
+
format: (value) => formatParam(param, value),
|
|
1860
|
+
command: (value) => `string ${key} ${formatParam(param, value)}`,
|
|
1861
|
+
});
|
|
1862
|
+
else if (param.kind === "enum")
|
|
1863
|
+
nodes.push({
|
|
1864
|
+
kind: "choice",
|
|
1865
|
+
label: key,
|
|
1866
|
+
help: param.doc,
|
|
1867
|
+
value: typeof own === "string" ? own : String(base),
|
|
1868
|
+
options: param.values,
|
|
1869
|
+
command: (option) => `string ${key} ${option}`,
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
return nodes;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1178
1875
|
/** Every synth parameter, grouped (amplitude, oscillator, FM 1..8, …). */
|
|
1179
1876
|
function synthAdvancedNodes(context: MenuContext): MenuNode[] {
|
|
1180
1877
|
const track = focused(context);
|
|
@@ -1324,10 +2021,58 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
1324
2021
|
build: (inner) => effectParamNodes(inner, effect, false),
|
|
1325
2022
|
};
|
|
1326
2023
|
};
|
|
1327
|
-
const
|
|
2024
|
+
const rigStages = RIG_STAGES as readonly string[];
|
|
2025
|
+
// 0.6.1 shoegaze stages get their own sub-menu.
|
|
2026
|
+
const gaze = SHOEGAZE_EFFECTS as readonly string[];
|
|
2027
|
+
const more = EFFECT_NAMES.filter(
|
|
2028
|
+
(effect) =>
|
|
2029
|
+
!CORE_EFFECTS.includes(effect) &&
|
|
2030
|
+
!rigStages.includes(effect) &&
|
|
2031
|
+
!gaze.includes(effect),
|
|
2032
|
+
);
|
|
2033
|
+
const gazeOn = SHOEGAZE_EFFECTS.filter((effect) =>
|
|
2034
|
+
effectValues(track, effect),
|
|
2035
|
+
);
|
|
2036
|
+
const shoegaze: MenuNode = {
|
|
2037
|
+
kind: "menu",
|
|
2038
|
+
id: "shoegaze",
|
|
2039
|
+
label: "Shoegaze",
|
|
2040
|
+
help: "wobble (tremolo-arm bend), bloom (feedback), swell (volume swell), double (two takes); `rig shoegaze` loads them all",
|
|
2041
|
+
detail:
|
|
2042
|
+
gazeOn.length === 0
|
|
2043
|
+
? "off"
|
|
2044
|
+
: gazeOn.map((effect) => effectSpec(effect).label).join(", "),
|
|
2045
|
+
build: () => SHOEGAZE_EFFECTS.map(node),
|
|
2046
|
+
};
|
|
1328
2047
|
const moreOn = more.filter((effect) => effectValues(track, effect));
|
|
2048
|
+
// 0.6 guitar rig: stomp → head (with its gate) → cab, plus whole rigs.
|
|
2049
|
+
const rigOn = RIG_STAGES.filter((stage) => effectValues(track, stage));
|
|
2050
|
+
const rig: MenuNode = {
|
|
2051
|
+
kind: "menu",
|
|
2052
|
+
id: "guitar rig",
|
|
2053
|
+
label: "Guitar rig",
|
|
2054
|
+
help: "stomp box, amp head with noise gate and speaker cabinet; `rig <name>` loads a whole rig",
|
|
2055
|
+
detail:
|
|
2056
|
+
rigOn.length === 0 ? "off" : (rigPresetOf(track.fx) ?? rigOn.join(" → ")),
|
|
2057
|
+
build: (inner) => {
|
|
2058
|
+
const current = focused(inner);
|
|
2059
|
+
return [
|
|
2060
|
+
{
|
|
2061
|
+
kind: "choice",
|
|
2062
|
+
label: "rig",
|
|
2063
|
+
value: rigPresetOf(current?.fx) ?? "—",
|
|
2064
|
+
options: [...Object.keys(RIG_PRESETS), "reset"],
|
|
2065
|
+
command: (name) => `rig ${name}`,
|
|
2066
|
+
help: "a whole rig (stomp, head, cab); every stage stays editable",
|
|
2067
|
+
},
|
|
2068
|
+
...RIG_STAGES.map(node),
|
|
2069
|
+
];
|
|
2070
|
+
},
|
|
2071
|
+
};
|
|
1329
2072
|
return [
|
|
1330
2073
|
...CORE_EFFECTS.map(node),
|
|
2074
|
+
rig,
|
|
2075
|
+
shoegaze,
|
|
1331
2076
|
{
|
|
1332
2077
|
kind: "menu",
|
|
1333
2078
|
id: "more effects",
|
|
@@ -1495,6 +2240,17 @@ function automationNodes(context: MenuContext): MenuNode[] {
|
|
|
1495
2240
|
const info = FX_LANE_INFO.get(lane as FxLane);
|
|
1496
2241
|
if (info?.effect === "synth")
|
|
1497
2242
|
return track.synth?.[info.param] !== undefined;
|
|
2243
|
+
if (info?.effect === "string") return track.string !== undefined;
|
|
2244
|
+
// Only the lanes this family's engine reads (keys-rotary on organs,
|
|
2245
|
+
// keys-hardness on pianos); the rest wait under "all lanes".
|
|
2246
|
+
if (info?.effect === "keys")
|
|
2247
|
+
return (
|
|
2248
|
+
track.keys !== undefined &&
|
|
2249
|
+
keysParamsFor(track.instrument).includes(info.param)
|
|
2250
|
+
);
|
|
2251
|
+
if (info?.effect === "modal") return track.modal !== undefined;
|
|
2252
|
+
if (info?.effect === "grain") return track.granular !== undefined;
|
|
2253
|
+
if (info?.effect === "wind") return track.wind !== undefined;
|
|
1498
2254
|
return info !== undefined && effectValues(track, info.effect) !== undefined;
|
|
1499
2255
|
});
|
|
1500
2256
|
const hidden = AUTOMATION_PARAMETERS.filter((lane) => !shown.includes(lane));
|
|
@@ -1676,14 +2432,111 @@ function patternsMenu(): MenuNode {
|
|
|
1676
2432
|
};
|
|
1677
2433
|
}
|
|
1678
2434
|
|
|
2435
|
+
const GRANULAR_PRESETS_COUNT = granularBrowseNodes().length;
|
|
2436
|
+
|
|
1679
2437
|
function soundNodes(): MenuNode[] {
|
|
1680
2438
|
return [
|
|
2439
|
+
{
|
|
2440
|
+
kind: "menu",
|
|
2441
|
+
id: "group:keys",
|
|
2442
|
+
label: "Keys",
|
|
2443
|
+
help: "modelled pianos, electric keys and organs, built in (no download)",
|
|
2444
|
+
detail: `${PIANO_PRESET_NAMES.join(" ")} · electric: ${ELECTRIC_PRESET_NAMES.join(" ")} · organs: ${ORGAN_PRESET_NAMES.join(" ")}`,
|
|
2445
|
+
build: () => [
|
|
2446
|
+
...PIANO_PRESET_NAMES.map((name): MenuNode => {
|
|
2447
|
+
const preset = KEYS_PRESETS[name]!;
|
|
2448
|
+
return {
|
|
2449
|
+
kind: "action",
|
|
2450
|
+
label: `${name.padEnd(10)} ${preset.doc}`,
|
|
2451
|
+
command: `piano ${name}`,
|
|
2452
|
+
help: preset.styles,
|
|
2453
|
+
};
|
|
2454
|
+
}),
|
|
2455
|
+
// keys-electric (0.6.1): tine and reed pianos and the clavinet.
|
|
2456
|
+
{
|
|
2457
|
+
kind: "menu",
|
|
2458
|
+
id: "group:keys:electric",
|
|
2459
|
+
label: "Electric",
|
|
2460
|
+
help: "electric pianos (tine, reed) and the clavinet, modelled",
|
|
2461
|
+
detail: ELECTRIC_PRESET_NAMES.join(" "),
|
|
2462
|
+
build: () =>
|
|
2463
|
+
Object.entries(KEYS_PRESETS)
|
|
2464
|
+
.filter(([, preset]) => isElectricFamily(preset.instrument))
|
|
2465
|
+
.map(([name, preset]): MenuNode => ({
|
|
2466
|
+
kind: "action",
|
|
2467
|
+
label: `${name.padEnd(10)} ${preset.doc}`,
|
|
2468
|
+
command: `keys preset ${name}`,
|
|
2469
|
+
help: preset.styles,
|
|
2470
|
+
})),
|
|
2471
|
+
},
|
|
2472
|
+
// f061-organ
|
|
2473
|
+
{
|
|
2474
|
+
kind: "menu",
|
|
2475
|
+
id: "group:organs",
|
|
2476
|
+
label: "Organs",
|
|
2477
|
+
help: "tonewheel, combo and pipe organs on the keys engine",
|
|
2478
|
+
detail: ORGAN_PRESET_NAMES.join(" "),
|
|
2479
|
+
build: () =>
|
|
2480
|
+
ORGAN_PRESET_NAMES.map((name): MenuNode => {
|
|
2481
|
+
const preset = KEYS_PRESETS[name]!;
|
|
2482
|
+
return {
|
|
2483
|
+
kind: "action",
|
|
2484
|
+
label: `${name.padEnd(10)} ${preset.doc}`,
|
|
2485
|
+
command: name,
|
|
2486
|
+
help: preset.styles,
|
|
2487
|
+
};
|
|
2488
|
+
}),
|
|
2489
|
+
},
|
|
2490
|
+
],
|
|
2491
|
+
},
|
|
1681
2492
|
{
|
|
1682
2493
|
kind: "action",
|
|
1683
2494
|
label: "wavetable synth basic shapes morph · built-in",
|
|
1684
2495
|
command: "wt basic",
|
|
1685
2496
|
help: "switch this track to the morphing wavetable synth",
|
|
1686
2497
|
},
|
|
2498
|
+
// 0.6 instrument groups, one per lane.
|
|
2499
|
+
malletsMenu(),
|
|
2500
|
+
windsMenu(),
|
|
2501
|
+
{
|
|
2502
|
+
kind: "menu",
|
|
2503
|
+
id: "strings",
|
|
2504
|
+
label: "Strings",
|
|
2505
|
+
help: "plucked strings: guitars, basses, sitar, harpsichord, oud, koto…; Bowed: violin to contrabass, sections",
|
|
2506
|
+
detail: `${STRING_PRESET_NAMES.length - BOWED_PRESET_NAMES.length} plucked · ${BOWED_PRESET_NAMES.length} bowed · built-in`,
|
|
2507
|
+
build: () => [
|
|
2508
|
+
...STRING_PRESET_NAMES.filter(
|
|
2509
|
+
(name) => !BOWED_PRESET_NAMES.includes(name),
|
|
2510
|
+
).map((name): MenuNode => ({
|
|
2511
|
+
kind: "action",
|
|
2512
|
+
label: `${name.padEnd(12)} ${STRING_PRESETS[name]!.doc}`,
|
|
2513
|
+
command: `string ${name}`,
|
|
2514
|
+
help: STRING_PRESETS[name]!.styles,
|
|
2515
|
+
})),
|
|
2516
|
+
{
|
|
2517
|
+
kind: "menu",
|
|
2518
|
+
id: "strings:bowed",
|
|
2519
|
+
label: "Bowed",
|
|
2520
|
+
help: "bowed strings: violin, viola, cello, bass, fiddle, erhu, sections",
|
|
2521
|
+
detail: BOWED_PRESET_NAMES.join(" "),
|
|
2522
|
+
build: () =>
|
|
2523
|
+
BOWED_PRESET_NAMES.map((name): MenuNode => ({
|
|
2524
|
+
kind: "action",
|
|
2525
|
+
label: `${name.padEnd(12)} ${STRING_PRESETS[name]!.doc}`,
|
|
2526
|
+
command: `bowed ${name}`,
|
|
2527
|
+
help: STRING_PRESETS[name]!.styles,
|
|
2528
|
+
})),
|
|
2529
|
+
},
|
|
2530
|
+
],
|
|
2531
|
+
},
|
|
2532
|
+
{
|
|
2533
|
+
kind: "menu",
|
|
2534
|
+
id: "granular-browse",
|
|
2535
|
+
label: "Granular",
|
|
2536
|
+
help: "grain clouds from a built-in synth or this track's own sound",
|
|
2537
|
+
detail: `${GRANULAR_PRESETS_COUNT} presets · built-in`,
|
|
2538
|
+
build: granularBrowseNodes,
|
|
2539
|
+
},
|
|
1687
2540
|
{
|
|
1688
2541
|
kind: "menu",
|
|
1689
2542
|
id: "instruments",
|
|
@@ -1812,9 +2665,75 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
1812
2665
|
help: "the song tuning (12-TET, EDOs, just, gamelan, Scala) and scale",
|
|
1813
2666
|
build: songTuningNodes,
|
|
1814
2667
|
},
|
|
2668
|
+
{
|
|
2669
|
+
kind: "menu",
|
|
2670
|
+
id: "resample",
|
|
2671
|
+
label: "resample",
|
|
2672
|
+
detail: "track · section · bars → new sample track",
|
|
2673
|
+
help: "render a track, an orbit or the mix to a pinned WAV on a new sampler or granular track",
|
|
2674
|
+
build: resampleNodes,
|
|
2675
|
+
},
|
|
1815
2676
|
];
|
|
1816
2677
|
}
|
|
1817
2678
|
|
|
2679
|
+
/** Project > Resample: one action per useful source and range. */
|
|
2680
|
+
function resampleNodes(context: MenuContext): MenuNode[] {
|
|
2681
|
+
const track = focused(context);
|
|
2682
|
+
const score = context.score;
|
|
2683
|
+
const nodes: MenuNode[] = [];
|
|
2684
|
+
if (track) {
|
|
2685
|
+
nodes.push(
|
|
2686
|
+
{
|
|
2687
|
+
kind: "action",
|
|
2688
|
+
label: `${track.id} → sampler`,
|
|
2689
|
+
command: `resample ${track.id}`,
|
|
2690
|
+
help: "render this track (pre-master) to a one-shot sampler track",
|
|
2691
|
+
},
|
|
2692
|
+
{
|
|
2693
|
+
kind: "action",
|
|
2694
|
+
label: `${track.id} → granular`,
|
|
2695
|
+
command: `resample ${track.id} grain`,
|
|
2696
|
+
help: "render this track to a granular track (cloud preset) holding one note",
|
|
2697
|
+
},
|
|
2698
|
+
);
|
|
2699
|
+
for (const section of score.sections)
|
|
2700
|
+
nodes.push({
|
|
2701
|
+
kind: "action",
|
|
2702
|
+
label: `${track.id} · section ${section.name}`,
|
|
2703
|
+
command: `resample ${track.id} section ${section.name}`,
|
|
2704
|
+
help: `render bars ${section.startBar + 1}-${section.startBar + section.bars} of this track`,
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
nodes.push(
|
|
2708
|
+
{
|
|
2709
|
+
kind: "entry",
|
|
2710
|
+
label: "bars",
|
|
2711
|
+
value: "",
|
|
2712
|
+
placeholder: "a-b [grain]",
|
|
2713
|
+
example: "/resample <track> bars 1-2",
|
|
2714
|
+
command: (text) => {
|
|
2715
|
+
const match = /^\s*(\d{1,4})(?:-(\d{1,4}))?(\s+grain)?\s*$/.exec(text);
|
|
2716
|
+
if (!match || !track) return undefined;
|
|
2717
|
+
return `resample ${track.id} bars ${match[1]}-${match[2] ?? match[1]}${match[3] ? " grain" : ""}`;
|
|
2718
|
+
},
|
|
2719
|
+
help: "render a bar range of the focused track",
|
|
2720
|
+
},
|
|
2721
|
+
{
|
|
2722
|
+
kind: "action",
|
|
2723
|
+
label: "mix → sampler (pre-master)",
|
|
2724
|
+
command: "resample orbit 1",
|
|
2725
|
+
help: "render every track on orbit 1 (the default bus) without the song master",
|
|
2726
|
+
},
|
|
2727
|
+
{
|
|
2728
|
+
kind: "action",
|
|
2729
|
+
label: "master → sampler",
|
|
2730
|
+
command: "resample master",
|
|
2731
|
+
help: "render the whole song through the master chain",
|
|
2732
|
+
},
|
|
2733
|
+
);
|
|
2734
|
+
return nodes;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
1818
2737
|
// ── tuning ────────────────────────────────────────────────────────────
|
|
1819
2738
|
|
|
1820
2739
|
/** Short name of a tuning for a choice row: its library name or table. */
|
|
@@ -2372,11 +3291,17 @@ export class EditMenu {
|
|
|
2372
3291
|
candidate.kind === "choice" &&
|
|
2373
3292
|
candidate.label === node.label,
|
|
2374
3293
|
) ?? node;
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
3294
|
+
// The current value is shown, not re-run: re-confirming a
|
|
3295
|
+
// legacy `piano` would otherwise re-voice it as the grand.
|
|
3296
|
+
return live.options.map((option): MenuNode =>
|
|
3297
|
+
option === live.value
|
|
3298
|
+
? { kind: "info", label: `${option} ✓`, value: "current" }
|
|
3299
|
+
: {
|
|
3300
|
+
kind: "action",
|
|
3301
|
+
label: option,
|
|
3302
|
+
command: live.command(option),
|
|
3303
|
+
},
|
|
3304
|
+
);
|
|
2380
3305
|
},
|
|
2381
3306
|
});
|
|
2382
3307
|
return { type: "handled" };
|