@hraness/dawg 0.6.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 +34 -0
- package/DAWG.md +338 -51
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +5 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +258 -6
- package/core/granular.ts +91 -0
- package/core/instruments.ts +161 -0
- package/core/keys.ts +573 -4
- package/core/resonators.ts +291 -3
- package/core/score.ts +349 -3
- package/core/sdk/print.ts +78 -3
- package/core/sdk/v1.ts +1320 -36
- package/core/sections.ts +12 -4
- package/core/strings.ts +237 -2
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +3 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -6
- package/package.json +1 -1
- package/src/agent/agent.ts +8 -2
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/ops.ts +4 -1
- package/src/agent/preview-tool.ts +2 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +200 -15
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/arrange.ts +21 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +18 -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/engine.ts +46 -5
- package/src/audio/fit.ts +45 -0
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +9 -5
- package/src/audio/instruments.ts +32 -3
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +143 -19
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +50 -6
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +136 -9
- package/src/audio/preview.ts +15 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +168 -21
- package/src/audio/samples.ts +26 -1
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +266 -20
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/wav.ts +119 -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 +103 -22
- package/src/commands/fx.ts +4 -1
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +91 -8
- package/src/commands/keys.ts +287 -11
- package/src/commands/modal.ts +67 -4
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +5 -1
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +473 -0
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +129 -7
- package/src/project/check.ts +5 -1
- package/src/render.ts +16 -0
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +1 -1
- package/src/tui/menu.ts +637 -35
- package/src/tui/modal-menu.ts +34 -7
- 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 +34 -9
- package/src/tui/wind-menu.ts +144 -0
package/src/tui/menu.ts
CHANGED
|
@@ -14,6 +14,7 @@ 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
16
|
import { malletsMenu, modalParameterNodes } from "./modal-menu.ts";
|
|
17
|
+
import { windParameterNodes, windsMenu } from "./wind-menu.ts";
|
|
17
18
|
import type { FaderSpec } from "./fader.ts";
|
|
18
19
|
import {
|
|
19
20
|
openingMeterCommand,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
RIG_PRESETS,
|
|
48
49
|
RIG_STAGES,
|
|
49
50
|
rigPresetOf,
|
|
51
|
+
SHOEGAZE_EFFECTS,
|
|
50
52
|
effectPresetNames,
|
|
51
53
|
effectSpec,
|
|
52
54
|
type EffectName,
|
|
@@ -95,16 +97,29 @@ import {
|
|
|
95
97
|
STRING_PARAMS,
|
|
96
98
|
STRING_PRESET_NAMES,
|
|
97
99
|
STRING_PRESETS,
|
|
100
|
+
BOWED_PRESET_NAMES,
|
|
101
|
+
BOW_SIMPLE_PARAMS,
|
|
102
|
+
resolveString,
|
|
98
103
|
STRING_SIMPLE_PARAMS,
|
|
99
104
|
stringPresetOf,
|
|
100
105
|
} from "../../core/strings.ts";
|
|
101
106
|
import {
|
|
102
107
|
KEYS_PARAMS,
|
|
103
108
|
KEYS_PRESETS,
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
KEYS_FAMILIES,
|
|
110
|
+
ORGAN_ROWS,
|
|
111
|
+
ORGAN_TEXT,
|
|
112
|
+
PIPE_REGISTRATIONS,
|
|
113
|
+
PIPE_STOPS,
|
|
114
|
+
isElectricFamily,
|
|
115
|
+
isKeysFamily,
|
|
116
|
+
isOrganFamily,
|
|
106
117
|
isPianoFamily,
|
|
118
|
+
keysParamsFor,
|
|
119
|
+
keysSimpleFor,
|
|
120
|
+
pipeStops,
|
|
107
121
|
resolvedKeys,
|
|
122
|
+
type OrganFamily,
|
|
108
123
|
} from "../../core/keys.ts";
|
|
109
124
|
import type { PickerItem } from "../../tui/app.ts";
|
|
110
125
|
import { BUILTIN_TABLES, BUILTIN_TABLE_NAMES } from "../audio/wavetable.ts";
|
|
@@ -124,6 +139,8 @@ import {
|
|
|
124
139
|
PROGRESSION_STYLES,
|
|
125
140
|
SCALE_NAMES,
|
|
126
141
|
SPREADS,
|
|
142
|
+
STROKE_PATTERN_NAMES,
|
|
143
|
+
GUITAR_TUNING_NAMES,
|
|
127
144
|
keyName,
|
|
128
145
|
parseKey,
|
|
129
146
|
} from "../../core/chords.ts";
|
|
@@ -135,6 +152,7 @@ import {
|
|
|
135
152
|
type Tuning,
|
|
136
153
|
} from "../../core/tuning.ts";
|
|
137
154
|
import { rootName, tuningStepsText } from "../commands/tuning.ts";
|
|
155
|
+
import { describeGuitar } from "../commands/strum.ts";
|
|
138
156
|
import {
|
|
139
157
|
granularBrowseNodes,
|
|
140
158
|
granularMenuDetail,
|
|
@@ -144,6 +162,7 @@ import {
|
|
|
144
162
|
import {
|
|
145
163
|
ARP_RATES,
|
|
146
164
|
CHORD_MODES,
|
|
165
|
+
STROKE_SPEED_MS,
|
|
147
166
|
defaultChordSettings,
|
|
148
167
|
type ChordSettings,
|
|
149
168
|
} from "./play-chords.ts";
|
|
@@ -384,7 +403,9 @@ function laneLabel(lane: AutomationParameter): string {
|
|
|
384
403
|
info.effect === "synth" ||
|
|
385
404
|
info.effect === "string" ||
|
|
386
405
|
info.effect === "keys" ||
|
|
387
|
-
info.effect === "modal"
|
|
406
|
+
info.effect === "modal" ||
|
|
407
|
+
info.effect === "grain" ||
|
|
408
|
+
info.effect === "wind"
|
|
388
409
|
? info.effect
|
|
389
410
|
: effectSpec(info.effect).label;
|
|
390
411
|
return `${owner} ${info.param}${unit}`;
|
|
@@ -533,27 +554,62 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
533
554
|
]
|
|
534
555
|
: [];
|
|
535
556
|
const keys: MenuNode[] =
|
|
536
|
-
track &&
|
|
557
|
+
track && isOrganFamily(track.instrument) && track.keys
|
|
537
558
|
? [
|
|
538
559
|
{
|
|
539
560
|
kind: "menu",
|
|
540
561
|
id: "keys",
|
|
541
562
|
label: "keys",
|
|
542
563
|
detail: `${track.instrument}${track.keys.preset ? ` · ${track.keys.preset}` : ""}`,
|
|
543
|
-
help: "the modelled
|
|
564
|
+
help: "the modelled organ: preset, drawbars, registers or stops, rotary",
|
|
544
565
|
build: (inner) => {
|
|
545
566
|
const current = focused(inner);
|
|
546
|
-
return current
|
|
547
|
-
? keysNodes(current, Object.keys(KEYS_PARAMS))
|
|
548
|
-
: [];
|
|
567
|
+
return current ? organNodes(current) : [];
|
|
549
568
|
},
|
|
550
569
|
},
|
|
551
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
|
+
]
|
|
552
607
|
: [];
|
|
553
608
|
return [
|
|
554
609
|
...parameterNodes(context),
|
|
555
610
|
...granular,
|
|
556
611
|
...keys,
|
|
612
|
+
...guitar,
|
|
557
613
|
...tuning,
|
|
558
614
|
{
|
|
559
615
|
kind: "menu",
|
|
@@ -574,6 +630,84 @@ function soundSectionNodes(context: MenuContext): MenuNode[] {
|
|
|
574
630
|
];
|
|
575
631
|
}
|
|
576
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
|
+
|
|
577
711
|
/** Rhythm: the euclid editor, then grooves and kits. */
|
|
578
712
|
function rhythmNodes(): MenuNode[] {
|
|
579
713
|
return [
|
|
@@ -881,6 +1015,34 @@ function chordNodes(context: MenuContext): MenuNode[] {
|
|
|
881
1015
|
options: CHORD_PATTERNS.map((pattern) => pattern.name),
|
|
882
1016
|
command: (option) => `/chords pattern ${option}`,
|
|
883
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[])),
|
|
884
1046
|
{
|
|
885
1047
|
kind: "choice",
|
|
886
1048
|
label: "arp rate",
|
|
@@ -969,7 +1131,7 @@ function trackNodes(context: MenuContext): MenuNode[] {
|
|
|
969
1131
|
}
|
|
970
1132
|
|
|
971
1133
|
function instrumentNode(track: Track): MenuNode {
|
|
972
|
-
const options: string[] = [...AVAILABLE_INSTRUMENTS, ...
|
|
1134
|
+
const options: string[] = [...AVAILABLE_INSTRUMENTS, ...KEYS_FAMILIES];
|
|
973
1135
|
if (!options.includes(track.instrument)) options.push(track.instrument);
|
|
974
1136
|
return {
|
|
975
1137
|
kind: "choice",
|
|
@@ -1023,13 +1185,20 @@ function parameterNodes(context: MenuContext): MenuNode[] {
|
|
|
1023
1185
|
if (isWavetableInstrument(track.instrument))
|
|
1024
1186
|
nodes.push(...wavetableNodes(track, context.projectRoot));
|
|
1025
1187
|
if (isStringTrack(track)) {
|
|
1026
|
-
nodes.push(
|
|
1188
|
+
nodes.push(
|
|
1189
|
+
...stringNodes(
|
|
1190
|
+
track,
|
|
1191
|
+
resolveString(track.string).exciter === "bow"
|
|
1192
|
+
? BOW_SIMPLE_PARAMS
|
|
1193
|
+
: STRING_SIMPLE_PARAMS,
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1027
1196
|
nodes.push({
|
|
1028
1197
|
kind: "menu",
|
|
1029
1198
|
id: "string:advanced",
|
|
1030
1199
|
label: "advanced",
|
|
1031
1200
|
detail: `all ${Object.keys(STRING_PARAMS).length} params`,
|
|
1032
|
-
help: "every string parameter: exciter, loss, body, buzz, sympathetics",
|
|
1201
|
+
help: "every string parameter: exciter, loss, body, buzz, sympathetics, bow (pressure, speed, attack, vib, vibdelay, tremhz, sord, dyn)",
|
|
1033
1202
|
build: (inner) => {
|
|
1034
1203
|
const current = focused(inner);
|
|
1035
1204
|
return current
|
|
@@ -1072,22 +1241,35 @@ function parameterNodes(context: MenuContext): MenuNode[] {
|
|
|
1072
1241
|
label: "add a voice",
|
|
1073
1242
|
value: "/sample <path> [as <voice>]",
|
|
1074
1243
|
});
|
|
1075
|
-
} else if (
|
|
1076
|
-
|
|
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);
|
|
1077
1250
|
nodes.push({
|
|
1078
1251
|
kind: "menu",
|
|
1079
1252
|
id: "keys:all",
|
|
1080
|
-
label:
|
|
1081
|
-
|
|
1082
|
-
|
|
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",
|
|
1083
1260
|
build: (inner) => {
|
|
1084
1261
|
const current = focused(inner);
|
|
1085
|
-
return current
|
|
1262
|
+
return current
|
|
1263
|
+
? keysNodes(current, keysParamsFor(current.instrument))
|
|
1264
|
+
: [];
|
|
1086
1265
|
},
|
|
1087
1266
|
});
|
|
1088
1267
|
} else if (track.instrument === "modal") {
|
|
1089
1268
|
// 0.6 modal percussion: preset, mallet and MODAL_PARAMS rows.
|
|
1090
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));
|
|
1091
1273
|
} else if (!isDrumInstrument(track.instrument)) {
|
|
1092
1274
|
nodes.push(...synthNodes(track, SYNTH_SIMPLE));
|
|
1093
1275
|
nodes.push({
|
|
@@ -1233,10 +1415,103 @@ function sampleVoiceNodes(context: MenuContext, voice: string): MenuNode[] {
|
|
|
1233
1415
|
reset: `/len off ${voice}`,
|
|
1234
1416
|
help: SAMPLE_CONTROLS.len,
|
|
1235
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
|
+
},
|
|
1236
1502
|
);
|
|
1237
1503
|
return nodes;
|
|
1238
1504
|
}
|
|
1239
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
|
+
|
|
1240
1515
|
/** Synth preset first, then one row per parameter. */
|
|
1241
1516
|
/** The preset this track's voice still matches exactly, if any. */
|
|
1242
1517
|
function matchingSynthPreset(track: Track): string | undefined {
|
|
@@ -1250,16 +1525,24 @@ function matchingSynthPreset(track: Track): string | undefined {
|
|
|
1250
1525
|
}
|
|
1251
1526
|
|
|
1252
1527
|
/** Modelled piano rows: the preset, then each parameter (`keys <p> <v>`). */
|
|
1253
|
-
function keysNodes(
|
|
1528
|
+
function keysNodes(
|
|
1529
|
+
track: Track,
|
|
1530
|
+
params: readonly string[],
|
|
1531
|
+
presets: readonly string[] = keysPresetNames(track.instrument),
|
|
1532
|
+
): MenuNode[] {
|
|
1254
1533
|
const effective = resolvedKeys(track.instrument, track.keys);
|
|
1255
1534
|
const nodes: MenuNode[] = [
|
|
1256
1535
|
{
|
|
1257
1536
|
kind: "choice",
|
|
1258
1537
|
label: "preset",
|
|
1259
1538
|
value: track.keys?.preset ?? "—",
|
|
1260
|
-
options:
|
|
1539
|
+
options: presets,
|
|
1261
1540
|
command: (preset) => `keys preset ${preset}`,
|
|
1262
|
-
help:
|
|
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",
|
|
1263
1546
|
},
|
|
1264
1547
|
];
|
|
1265
1548
|
for (const key of params) {
|
|
@@ -1300,6 +1583,174 @@ function keysNodes(track: Track, params: readonly string[]): MenuNode[] {
|
|
|
1300
1583
|
return nodes;
|
|
1301
1584
|
}
|
|
1302
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
|
+
|
|
1303
1754
|
function synthNodes(track: Track, keys: readonly string[]): MenuNode[] {
|
|
1304
1755
|
const nodes: MenuNode[] = [];
|
|
1305
1756
|
if (keys === SYNTH_SIMPLE)
|
|
@@ -1571,9 +2022,28 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
1571
2022
|
};
|
|
1572
2023
|
};
|
|
1573
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[];
|
|
1574
2027
|
const more = EFFECT_NAMES.filter(
|
|
1575
|
-
(effect) =>
|
|
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),
|
|
1576
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
|
+
};
|
|
1577
2047
|
const moreOn = more.filter((effect) => effectValues(track, effect));
|
|
1578
2048
|
// 0.6 guitar rig: stomp → head (with its gate) → cab, plus whole rigs.
|
|
1579
2049
|
const rigOn = RIG_STAGES.filter((stage) => effectValues(track, stage));
|
|
@@ -1602,6 +2072,7 @@ function effectNodes(context: MenuContext): MenuNode[] {
|
|
|
1602
2072
|
return [
|
|
1603
2073
|
...CORE_EFFECTS.map(node),
|
|
1604
2074
|
rig,
|
|
2075
|
+
shoegaze,
|
|
1605
2076
|
{
|
|
1606
2077
|
kind: "menu",
|
|
1607
2078
|
id: "more effects",
|
|
@@ -1770,8 +2241,16 @@ function automationNodes(context: MenuContext): MenuNode[] {
|
|
|
1770
2241
|
if (info?.effect === "synth")
|
|
1771
2242
|
return track.synth?.[info.param] !== undefined;
|
|
1772
2243
|
if (info?.effect === "string") return track.string !== undefined;
|
|
1773
|
-
|
|
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
|
+
);
|
|
1774
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;
|
|
1775
2254
|
return info !== undefined && effectValues(track, info.effect) !== undefined;
|
|
1776
2255
|
});
|
|
1777
2256
|
const hidden = AUTOMATION_PARAMETERS.filter((lane) => !shown.includes(lane));
|
|
@@ -1961,15 +2440,54 @@ function soundNodes(): MenuNode[] {
|
|
|
1961
2440
|
kind: "menu",
|
|
1962
2441
|
id: "group:keys",
|
|
1963
2442
|
label: "Keys",
|
|
1964
|
-
help: "modelled pianos, built in (no download)",
|
|
1965
|
-
detail:
|
|
1966
|
-
build: () =>
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
+
],
|
|
1973
2491
|
},
|
|
1974
2492
|
{
|
|
1975
2493
|
kind: "action",
|
|
@@ -1979,19 +2497,37 @@ function soundNodes(): MenuNode[] {
|
|
|
1979
2497
|
},
|
|
1980
2498
|
// 0.6 instrument groups, one per lane.
|
|
1981
2499
|
malletsMenu(),
|
|
2500
|
+
windsMenu(),
|
|
1982
2501
|
{
|
|
1983
2502
|
kind: "menu",
|
|
1984
2503
|
id: "strings",
|
|
1985
2504
|
label: "Strings",
|
|
1986
|
-
help: "plucked strings: guitars, basses, sitar, harpsichord, oud, koto
|
|
1987
|
-
detail: `${STRING_PRESET_NAMES.length} plucked · built-in`,
|
|
1988
|
-
build: () =>
|
|
1989
|
-
STRING_PRESET_NAMES.
|
|
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 => ({
|
|
1990
2511
|
kind: "action",
|
|
1991
2512
|
label: `${name.padEnd(12)} ${STRING_PRESETS[name]!.doc}`,
|
|
1992
2513
|
command: `string ${name}`,
|
|
1993
2514
|
help: STRING_PRESETS[name]!.styles,
|
|
1994
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
|
+
],
|
|
1995
2531
|
},
|
|
1996
2532
|
{
|
|
1997
2533
|
kind: "menu",
|
|
@@ -2129,9 +2665,75 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
2129
2665
|
help: "the song tuning (12-TET, EDOs, just, gamelan, Scala) and scale",
|
|
2130
2666
|
build: songTuningNodes,
|
|
2131
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
|
+
},
|
|
2132
2676
|
];
|
|
2133
2677
|
}
|
|
2134
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
|
+
|
|
2135
2737
|
// ── tuning ────────────────────────────────────────────────────────────
|
|
2136
2738
|
|
|
2137
2739
|
/** Short name of a tuning for a choice row: its library name or table. */
|