@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/session/presence.ts
CHANGED
|
@@ -32,6 +32,8 @@ export class FilePresence {
|
|
|
32
32
|
private readonly lock: string;
|
|
33
33
|
private entry: PresenceEntry;
|
|
34
34
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
35
|
+
private writing: Promise<void> = Promise.resolve();
|
|
36
|
+
private writes = 0;
|
|
35
37
|
|
|
36
38
|
public constructor(paths: SessionPaths, entry: PresenceEntry) {
|
|
37
39
|
this.dir = `${paths.record}.presence`;
|
|
@@ -56,7 +58,8 @@ export class FilePresence {
|
|
|
56
58
|
|
|
57
59
|
public async focus(trackId: string | null): Promise<void> {
|
|
58
60
|
this.entry = { ...this.entry, focusedTrackId: trackId };
|
|
59
|
-
|
|
61
|
+
// Presence is advisory: a failed heartbeat write never refuses a focus change.
|
|
62
|
+
await this.write().catch(() => undefined);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
public async claim(
|
|
@@ -133,9 +136,19 @@ export class FilePresence {
|
|
|
133
136
|
return join(this.dir, `${this.entry.clientId}.json`);
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
/** Writes are serialized so the heartbeat and focus() never share a rename. */
|
|
140
|
+
private write(): Promise<void> {
|
|
141
|
+
const next = this.writing.then(
|
|
142
|
+
() => this.writeNow(),
|
|
143
|
+
() => this.writeNow(),
|
|
144
|
+
);
|
|
145
|
+
this.writing = next.catch(() => undefined);
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async writeNow(): Promise<void> {
|
|
137
150
|
await mkdir(this.dir, { recursive: true });
|
|
138
|
-
const temporary = `${this.file()}.${process.pid}.tmp`;
|
|
151
|
+
const temporary = `${this.file()}.${process.pid}.${++this.writes}.tmp`;
|
|
139
152
|
await writeFile(
|
|
140
153
|
temporary,
|
|
141
154
|
JSON.stringify({ ...this.entry, at: Date.now() }),
|
package/src/tui/audition.ts
CHANGED
|
@@ -528,9 +528,9 @@ export function auditionKey(value: string): AuditionKey | undefined {
|
|
|
528
528
|
* before.
|
|
529
529
|
*/
|
|
530
530
|
const STAGEABLE =
|
|
531
|
-
/^\/?(?:(fx|effects|filter|lowpass|synth|wt|wavetable|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid|art|articulation|bend|vibrato|glide|portamento|velcurve|vel-curve|pedal|sustain|humanize|tuning|tune|master)\s+\S|pack\s+use\s+\S)/i;
|
|
531
|
+
/^\/?(?:(fx|effects|filter|lowpass|synth|string|bowed|modal|wind|wt|wavetable|grain|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid|art|articulation|bend|vibrato|glide|portamento|velcurve|vel-curve|pedal|sustain|humanize|tuning|tune|master|keys|piano|epiano|rhodes|wurli|wurlitzer|clav|clavinet)\s+\S|pack\s+use\s+\S|(?:piano|grand|upright|felt|honkytonk|prepared|ballad)(?:\s+piano)?\s*$|(?:epiano|rhodes|suitcase|dyno|wurli|wurlitzer|clav|clavinet|funkclav)\s*$|(?:tonewheel|hammond|b3|gospel|jazzorgan|combo|farfisa|vox|pipe|church|pipeorgan|churchorgan|flutes|cornet|reeds|celeste)(?:\s+\S.*)?$|rotary\s+(?:slow|fast|stop)\s*$)/i;
|
|
532
532
|
/** Subcommands that list or show instead of changing the sound. */
|
|
533
|
-
const READ_ONLY = /^\/?\S+\s+(list|show|info|help|measure|meter)\s*$/i;
|
|
533
|
+
const READ_ONLY = /^\/?\S+\s+(list|show|info|help|measure|meter|presets?)\s*$/i;
|
|
534
534
|
/** `master <unit>` alone only shows the unit. */
|
|
535
535
|
const MASTER_SHOW = /^\/?master\s+(eq|glue|tape|width|limiter)\s*$/i;
|
|
536
536
|
const MASTER_COMMAND = /^\/?master\s/i;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl-K rows for the granular instrument (core/granular.ts): the
|
|
3
|
+
* Sound > Granular subsection and the Granular group of Sound > browse
|
|
4
|
+
* sounds. Every row is a `grain …` command, so the menu teaches the command
|
|
5
|
+
* and an edit is one revision, one receipt and one undo step.
|
|
6
|
+
*/
|
|
7
|
+
import { isDrumInstrument } from "../../core/drums.ts";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_GRANULAR_SOURCE,
|
|
10
|
+
GRANULAR_PARAMS,
|
|
11
|
+
GRANULAR_PRESET_NAMES,
|
|
12
|
+
GRANULAR_PRESETS,
|
|
13
|
+
granularSourceLabel,
|
|
14
|
+
isGranularInstrument,
|
|
15
|
+
parseSynthSource,
|
|
16
|
+
resolveGranular,
|
|
17
|
+
type GranularSettings,
|
|
18
|
+
} from "../../core/granular.ts";
|
|
19
|
+
import type { NumberParam } from "../../core/params.ts";
|
|
20
|
+
import type { Track } from "../../core/score.ts";
|
|
21
|
+
|
|
22
|
+
/** The subset of the menu's node shape these rows use. */
|
|
23
|
+
export type GranularMenuNode =
|
|
24
|
+
| Readonly<{
|
|
25
|
+
kind: "number";
|
|
26
|
+
label: string;
|
|
27
|
+
help: string;
|
|
28
|
+
value: number | undefined;
|
|
29
|
+
start: number;
|
|
30
|
+
off: string;
|
|
31
|
+
min: number;
|
|
32
|
+
max: number;
|
|
33
|
+
step: (value: number, direction: 1 | -1) => number;
|
|
34
|
+
format: (value: number) => string;
|
|
35
|
+
command: (value: number) => string;
|
|
36
|
+
reset: string;
|
|
37
|
+
}>
|
|
38
|
+
| Readonly<{
|
|
39
|
+
kind: "toggle";
|
|
40
|
+
label: string;
|
|
41
|
+
help: string;
|
|
42
|
+
value: boolean;
|
|
43
|
+
command: (value: boolean) => string;
|
|
44
|
+
}>
|
|
45
|
+
| Readonly<{
|
|
46
|
+
kind: "choice";
|
|
47
|
+
label: string;
|
|
48
|
+
help: string;
|
|
49
|
+
value: string;
|
|
50
|
+
options: readonly string[];
|
|
51
|
+
command: (option: string) => string;
|
|
52
|
+
}>
|
|
53
|
+
| Readonly<{
|
|
54
|
+
kind: "entry";
|
|
55
|
+
label: string;
|
|
56
|
+
help: string;
|
|
57
|
+
value: string;
|
|
58
|
+
placeholder: string;
|
|
59
|
+
command: (text: string) => string | undefined;
|
|
60
|
+
example: string;
|
|
61
|
+
}>
|
|
62
|
+
| Readonly<{ kind: "action"; label: string; help: string; command: string }>
|
|
63
|
+
| Readonly<{ kind: "info"; label: string; value: string; help?: string }>;
|
|
64
|
+
|
|
65
|
+
/** Menu labels for the stored keys, in the review's row order. */
|
|
66
|
+
export const GRANULAR_MENU_ROWS: readonly (readonly [string, string])[] =
|
|
67
|
+
Object.freeze([
|
|
68
|
+
["begin", "begin"],
|
|
69
|
+
["end", "end"],
|
|
70
|
+
["root", "root note"],
|
|
71
|
+
["pos", "position"],
|
|
72
|
+
["scan", "scan"],
|
|
73
|
+
["grain", "grain"],
|
|
74
|
+
["overlap", "overlap"],
|
|
75
|
+
["jitter", "jitter"],
|
|
76
|
+
["spray", "spray"],
|
|
77
|
+
["pitch", "pitch"],
|
|
78
|
+
["detune", "detune"],
|
|
79
|
+
["shimmer", "shimmer"],
|
|
80
|
+
["shimint", "shimmer interval"],
|
|
81
|
+
["repeat", "repeat"],
|
|
82
|
+
["hold", "hold"],
|
|
83
|
+
["drift", "drift"],
|
|
84
|
+
["drate", "drift rate"],
|
|
85
|
+
["spread", "spread"],
|
|
86
|
+
["window", "window"],
|
|
87
|
+
["reverse", "reverse"],
|
|
88
|
+
["freeze", "freeze"],
|
|
89
|
+
["attack", "attack"],
|
|
90
|
+
["release", "release"],
|
|
91
|
+
["veltone", "velocity tone"],
|
|
92
|
+
["gain", "gain"],
|
|
93
|
+
["seed", "seed"],
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/** Presets that need a resampled phrase to sound as named. */
|
|
97
|
+
const PHRASE_PRESETS: ReadonlySet<string> = new Set([
|
|
98
|
+
"microloop",
|
|
99
|
+
"sparkle",
|
|
100
|
+
"backwards",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
function text(value: number): string {
|
|
104
|
+
return Number(value.toFixed(4)).toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stepper(
|
|
108
|
+
spec: NumberParam,
|
|
109
|
+
): (value: number, direction: 1 | -1) => number {
|
|
110
|
+
if (spec.step === "log")
|
|
111
|
+
return (value, direction) =>
|
|
112
|
+
Math.min(
|
|
113
|
+
spec.max,
|
|
114
|
+
Math.max(
|
|
115
|
+
spec.min,
|
|
116
|
+
Number((Math.max(value, 1e-3) * 2 ** (direction / 6)).toFixed(4)),
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
const size = spec.step;
|
|
120
|
+
return (value, direction) =>
|
|
121
|
+
Math.min(
|
|
122
|
+
spec.max,
|
|
123
|
+
Math.max(
|
|
124
|
+
spec.min,
|
|
125
|
+
Number(
|
|
126
|
+
(Math.round((value + size * direction) / size) * size).toFixed(6),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether a track shows the granular subsection, and how it is labelled. */
|
|
133
|
+
export function granularMenuLabel(
|
|
134
|
+
track: Track | undefined,
|
|
135
|
+
): string | undefined {
|
|
136
|
+
if (!track || track.kit || isDrumInstrument(track.instrument))
|
|
137
|
+
return undefined;
|
|
138
|
+
return isGranularInstrument(track.instrument)
|
|
139
|
+
? "granular"
|
|
140
|
+
: "granular (convert)";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One-line state for the subsection's detail column. */
|
|
144
|
+
export function granularMenuDetail(track: Track): string {
|
|
145
|
+
if (!isGranularInstrument(track.instrument))
|
|
146
|
+
return track.sampler
|
|
147
|
+
? "grain this sampler's voice"
|
|
148
|
+
: "grain this track's synth";
|
|
149
|
+
return `${track.granular?.preset ?? "default"} · ${granularSourceLabel(track.granular?.src)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Sound > Granular: on a granular track the preset, source and every
|
|
154
|
+
* parameter; elsewhere the presets that convert the track (its own sound
|
|
155
|
+
* becomes the source) plus a plain `grain on`.
|
|
156
|
+
*/
|
|
157
|
+
export function granularMenuNodes(
|
|
158
|
+
track: Track | undefined,
|
|
159
|
+
): GranularMenuNode[] {
|
|
160
|
+
if (!track || !granularMenuLabel(track)) return [];
|
|
161
|
+
if (!isGranularInstrument(track.instrument))
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
kind: "action",
|
|
165
|
+
label: "grain this sound",
|
|
166
|
+
help: track.sampler
|
|
167
|
+
? "turn this sampler's first voice into a grain cloud (grain off goes back)"
|
|
168
|
+
: "turn this track's synth into a grain cloud (grain off goes back)",
|
|
169
|
+
command: "grain on",
|
|
170
|
+
},
|
|
171
|
+
...GRANULAR_PRESET_NAMES.map((name): GranularMenuNode => ({
|
|
172
|
+
kind: "action",
|
|
173
|
+
label: `${name.padEnd(10)} ${GRANULAR_PRESETS[name].doc}`,
|
|
174
|
+
help: `grain ${name}: this track's sound through the ${name} preset`,
|
|
175
|
+
command: `grain ${name}`,
|
|
176
|
+
})),
|
|
177
|
+
];
|
|
178
|
+
const settings = resolveGranular(track.granular);
|
|
179
|
+
const own = track.granular ?? {};
|
|
180
|
+
const preset = track.granular?.preset;
|
|
181
|
+
const builtIn =
|
|
182
|
+
typeof settings.src === "string" &&
|
|
183
|
+
parseSynthSource(settings.src) !== undefined;
|
|
184
|
+
const nodes: GranularMenuNode[] = [
|
|
185
|
+
{
|
|
186
|
+
kind: "choice",
|
|
187
|
+
label: "preset",
|
|
188
|
+
help:
|
|
189
|
+
preset && PHRASE_PRESETS.has(preset) && builtIn
|
|
190
|
+
? `${GRANULAR_PRESETS[preset].doc} (on a resample; a texture on a synth source)`
|
|
191
|
+
: preset
|
|
192
|
+
? GRANULAR_PRESETS[preset].doc
|
|
193
|
+
: "left/right cycles presets; space previews",
|
|
194
|
+
value: preset ?? "",
|
|
195
|
+
options: GRANULAR_PRESET_NAMES,
|
|
196
|
+
command: (name) => `grain ${name}`,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
kind: "entry",
|
|
200
|
+
label: "source",
|
|
201
|
+
help: "synth:<preset>[@note] or voice <V> of this track's sampler",
|
|
202
|
+
value: granularSourceLabel(track.granular?.src),
|
|
203
|
+
placeholder: DEFAULT_GRANULAR_SOURCE,
|
|
204
|
+
command: (value) => {
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
if (!trimmed) return undefined;
|
|
207
|
+
return /^voice\s+\S+$/.test(trimmed)
|
|
208
|
+
? `grain src ${trimmed}`
|
|
209
|
+
: `grain src ${trimmed.startsWith("synth:") ? trimmed : `synth:${trimmed}`}`;
|
|
210
|
+
},
|
|
211
|
+
example: "grain src synth:bell",
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
for (const [key, label] of GRANULAR_MENU_ROWS) {
|
|
215
|
+
const spec = GRANULAR_PARAMS[key]!;
|
|
216
|
+
const stored = (own as Record<string, unknown>)[key];
|
|
217
|
+
const current = settings[key as keyof GranularSettings];
|
|
218
|
+
if (spec.kind === "number") {
|
|
219
|
+
const base = typeof current === "number" ? current : spec.default;
|
|
220
|
+
nodes.push({
|
|
221
|
+
kind: "number",
|
|
222
|
+
label: spec.unit ? `${label} (${spec.unit})` : label,
|
|
223
|
+
help: spec.doc,
|
|
224
|
+
value: typeof stored === "number" ? stored : undefined,
|
|
225
|
+
start: base,
|
|
226
|
+
off: text(base),
|
|
227
|
+
min: spec.min,
|
|
228
|
+
max: spec.max,
|
|
229
|
+
step: stepper(spec),
|
|
230
|
+
format: text,
|
|
231
|
+
command: (value) => `grain ${key} ${text(value)}`,
|
|
232
|
+
reset: `grain ${key} off`,
|
|
233
|
+
});
|
|
234
|
+
} else if (spec.kind === "enum")
|
|
235
|
+
nodes.push({
|
|
236
|
+
kind: "choice",
|
|
237
|
+
label,
|
|
238
|
+
help: spec.doc,
|
|
239
|
+
value: String(current),
|
|
240
|
+
options: spec.values,
|
|
241
|
+
command: (option) => `grain ${key} ${option}`,
|
|
242
|
+
});
|
|
243
|
+
else if (spec.kind === "boolean")
|
|
244
|
+
nodes.push({
|
|
245
|
+
kind: "toggle",
|
|
246
|
+
label,
|
|
247
|
+
help: spec.doc,
|
|
248
|
+
value: current === true,
|
|
249
|
+
command: (on) => `grain ${key} ${on ? "on" : "off"}`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (Object.keys(own).some((key) => key !== "preset" && key !== "src"))
|
|
253
|
+
nodes.push({
|
|
254
|
+
kind: "action",
|
|
255
|
+
label: "reset to preset",
|
|
256
|
+
help: "drop this track's grain overrides, keep the preset and source",
|
|
257
|
+
command: "grain reset",
|
|
258
|
+
});
|
|
259
|
+
nodes.push({
|
|
260
|
+
kind: "action",
|
|
261
|
+
label: "grain off",
|
|
262
|
+
help: "back to this track's previous voice (settings are kept)",
|
|
263
|
+
command: "grain off",
|
|
264
|
+
});
|
|
265
|
+
return nodes;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Sound > browse sounds > Granular: one row per preset. */
|
|
269
|
+
export function granularBrowseNodes(): GranularMenuNode[] {
|
|
270
|
+
return GRANULAR_PRESET_NAMES.map((name) => ({
|
|
271
|
+
kind: "action",
|
|
272
|
+
label: `${name.padEnd(10)} ${GRANULAR_PRESETS[name].doc}`,
|
|
273
|
+
help: PHRASE_PRESETS.has(name)
|
|
274
|
+
? `grain ${name} (comes into its own on a resampled phrase)`
|
|
275
|
+
: `grain ${name}: the built-in or this track's sound as a grain cloud`,
|
|
276
|
+
command: `grain ${name}`,
|
|
277
|
+
}));
|
|
278
|
+
}
|