@hraness/dawg 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -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/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- 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/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ctrl-k menu's Arrange section (0.5): song sections, the form, and
|
|
3
|
+
* build/drop/fill generators. Every row runs a `section …`, `form …`,
|
|
4
|
+
* `build …`, `drop …` or `fill …` prompt command (src/commands/arrange.ts),
|
|
5
|
+
* so the menu, the prompt and the agent tools share one code path.
|
|
6
|
+
*/
|
|
7
|
+
import type { Section } from "../../core/score.ts";
|
|
8
|
+
import { formatForm } from "../../core/sections.ts";
|
|
9
|
+
import { barsLabel } from "../commands/arrange.ts";
|
|
10
|
+
import type { MenuContext, MenuNode } from "./menu.ts";
|
|
11
|
+
|
|
12
|
+
/** `3 sections · form A×2 B · loop chorus`, or how to start. */
|
|
13
|
+
export function arrangeDetail(context: MenuContext): string {
|
|
14
|
+
const score = context.score;
|
|
15
|
+
if (score.sections.length === 0) return "no sections · mark bars";
|
|
16
|
+
const count = score.sections.length;
|
|
17
|
+
const parts = [`${count} section${count === 1 ? "" : "s"}`];
|
|
18
|
+
if (score.form.length) parts.push(`form ${formatForm(score.form)}`);
|
|
19
|
+
if (score.loopSection) parts.push(`loop ${score.loopSection}`);
|
|
20
|
+
return parts.join(" · ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function arrangeNodes(context: MenuContext): MenuNode[] {
|
|
24
|
+
const score = context.score;
|
|
25
|
+
const nodes: MenuNode[] = score.sections.map((section) => ({
|
|
26
|
+
kind: "menu",
|
|
27
|
+
id: `section:${section.name}`,
|
|
28
|
+
label: section.name,
|
|
29
|
+
detail: sectionDetail(context, section),
|
|
30
|
+
help: "loop, jump, mute, vary, move, rename, transitions",
|
|
31
|
+
build: (next) => sectionNodes(next, section.name),
|
|
32
|
+
}));
|
|
33
|
+
nodes.push(
|
|
34
|
+
{
|
|
35
|
+
kind: "entry",
|
|
36
|
+
label: "mark bars",
|
|
37
|
+
value: "",
|
|
38
|
+
placeholder: "name and bars, e.g. verse 1-8",
|
|
39
|
+
example: "section verse 1-8",
|
|
40
|
+
help: "name a bar range; re-marking a name moves its marker",
|
|
41
|
+
command: (text) => {
|
|
42
|
+
const trimmed = text.trim();
|
|
43
|
+
return /\S\s+\d{1,4}(?:[-–]\d{1,4})?$/u.test(trimmed)
|
|
44
|
+
? `section ${trimmed}`
|
|
45
|
+
: undefined;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
kind: "entry",
|
|
50
|
+
label: "add section",
|
|
51
|
+
value: "",
|
|
52
|
+
placeholder: "name and bars, e.g. chorus 8 (empty: next name, 8 bars)",
|
|
53
|
+
example: "section add chorus 8",
|
|
54
|
+
help: "a new section after the last one (extends the song)",
|
|
55
|
+
command: (text) => `section add ${text.trim()}`.trim(),
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
if (score.sections.length > 0) {
|
|
59
|
+
nodes.push({
|
|
60
|
+
kind: "entry",
|
|
61
|
+
label: "form",
|
|
62
|
+
value: score.form.length ? formatForm(score.form) : "bars in order",
|
|
63
|
+
placeholder: "sections in play order, e.g. intro verse chorus*2 outro",
|
|
64
|
+
example: "form intro verse chorus*2 outro",
|
|
65
|
+
help: "the order playback and export follow; empty clears it",
|
|
66
|
+
command: (text) => (text.trim() ? `form ${text.trim()}` : "form off"),
|
|
67
|
+
});
|
|
68
|
+
if (score.form.length)
|
|
69
|
+
nodes.push({
|
|
70
|
+
kind: "action",
|
|
71
|
+
label: "bake form",
|
|
72
|
+
command: "form bake",
|
|
73
|
+
help: "write the form out as plain bars, then clear it",
|
|
74
|
+
});
|
|
75
|
+
if (score.loopSection)
|
|
76
|
+
nodes.push({
|
|
77
|
+
kind: "action",
|
|
78
|
+
label: `stop looping ${score.loopSection}`,
|
|
79
|
+
command: "section loop off",
|
|
80
|
+
help: "play the whole song (or form) again",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return nodes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sectionDetail(context: MenuContext, section: Section): string {
|
|
87
|
+
const parts = [barsLabel(section)];
|
|
88
|
+
if (context.score.loopSection === section.name) parts.push("looping");
|
|
89
|
+
if (section.mute?.length) parts.push(`mutes ${section.mute.join(" ")}`);
|
|
90
|
+
const varied = Object.keys(section.vary ?? {});
|
|
91
|
+
if (varied.length) parts.push(`varies ${varied.join(" ")}`);
|
|
92
|
+
return parts.join(" · ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sectionNodes(context: MenuContext, name: string): MenuNode[] {
|
|
96
|
+
const score = context.score;
|
|
97
|
+
const section = score.sections.find((entry) => entry.name === name);
|
|
98
|
+
if (!section)
|
|
99
|
+
return [{ kind: "info", label: "gone", value: `${name} was removed` }];
|
|
100
|
+
const looping = score.loopSection === name;
|
|
101
|
+
const track = context.trackId;
|
|
102
|
+
const vary = section.vary?.[track];
|
|
103
|
+
const mutes: MenuNode[] = score.tracks.map((entry) => ({
|
|
104
|
+
kind: "toggle",
|
|
105
|
+
label: `mute ${entry.id}`,
|
|
106
|
+
value: section.mute?.includes(entry.id) ?? false,
|
|
107
|
+
help: `silence ${entry.name} in this section only`,
|
|
108
|
+
command: (on) => `section ${on ? "mute" : "unmute"} ${name} ${entry.id}`,
|
|
109
|
+
}));
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
kind: "action",
|
|
113
|
+
label: looping ? "stop looping" : "loop",
|
|
114
|
+
command: looping ? "section loop off" : `section loop ${name}`,
|
|
115
|
+
help: "loop this section in playback (export still plays the song)",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
kind: "action",
|
|
119
|
+
label: "jump here",
|
|
120
|
+
command: `section jump ${name}`,
|
|
121
|
+
help: "move the playhead to its first bar",
|
|
122
|
+
},
|
|
123
|
+
...mutes,
|
|
124
|
+
{
|
|
125
|
+
kind: "number",
|
|
126
|
+
label: `transpose ${track}`,
|
|
127
|
+
help: "shift the focused track here, in semitones (x resets)",
|
|
128
|
+
// As written is +0, so the first nudge already moves a semitone.
|
|
129
|
+
value: vary?.transpose ?? 0,
|
|
130
|
+
min: -24,
|
|
131
|
+
max: 24,
|
|
132
|
+
step: (value, direction) =>
|
|
133
|
+
Math.max(-24, Math.min(24, Math.round(value) + direction)),
|
|
134
|
+
format: (value) => `${value > 0 ? "+" : ""}${Math.round(value)} st`,
|
|
135
|
+
command: (value) => {
|
|
136
|
+
const rounded = Math.round(value);
|
|
137
|
+
return `section vary ${name} ${track} ${rounded < 0 ? "" : "+"}${rounded}`;
|
|
138
|
+
},
|
|
139
|
+
reset: `section vary ${name} ${track} off`,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
kind: "number",
|
|
143
|
+
label: `gain ${track}`,
|
|
144
|
+
help: "scale the focused track's velocity here (x resets)",
|
|
145
|
+
value: vary?.gain ?? 1,
|
|
146
|
+
min: 0,
|
|
147
|
+
max: 2,
|
|
148
|
+
step: (value, direction) =>
|
|
149
|
+
Math.max(
|
|
150
|
+
0,
|
|
151
|
+
Math.min(2, Math.round((value + 0.1 * direction) * 10) / 10),
|
|
152
|
+
),
|
|
153
|
+
format: (value) => `×${value.toFixed(1)}`,
|
|
154
|
+
command: (value) =>
|
|
155
|
+
`section vary ${name} ${track} gain ${(Math.round(value * 10) / 10).toString()}`,
|
|
156
|
+
reset: `section vary ${name} ${track} off`,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
kind: "menu",
|
|
160
|
+
id: `section:${name}:build`,
|
|
161
|
+
label: "build",
|
|
162
|
+
detail: section.startBar > 0 ? "into it or over it" : "over it",
|
|
163
|
+
help: "riser, snare roll, filter sweep and uplifter",
|
|
164
|
+
build: () => buildNodes(section),
|
|
165
|
+
},
|
|
166
|
+
...(section.startBar > 0
|
|
167
|
+
? ([
|
|
168
|
+
{
|
|
169
|
+
kind: "menu",
|
|
170
|
+
id: `section:${name}:drop`,
|
|
171
|
+
label: "drop",
|
|
172
|
+
detail: "cut and impact",
|
|
173
|
+
help: "a pre-drop cut before it and an impact on its downbeat",
|
|
174
|
+
build: () => dropNodes(section),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
kind: "menu",
|
|
178
|
+
id: `section:${name}:fill`,
|
|
179
|
+
label: "fill",
|
|
180
|
+
detail: "toms, roll or kick",
|
|
181
|
+
help: "a drum fill on the beats before it and a crash on its downbeat",
|
|
182
|
+
build: () => fillNodes(section),
|
|
183
|
+
},
|
|
184
|
+
] satisfies MenuNode[])
|
|
185
|
+
: []),
|
|
186
|
+
{
|
|
187
|
+
kind: "action",
|
|
188
|
+
label: "duplicate",
|
|
189
|
+
command: `section dup ${name}`,
|
|
190
|
+
help: "copy it and its bars right after it",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
kind: "entry",
|
|
194
|
+
label: "duplicate as",
|
|
195
|
+
value: "",
|
|
196
|
+
placeholder: "the copy's name",
|
|
197
|
+
example: `section dup ${name} as ${name} 2`,
|
|
198
|
+
help: "copy it and its bars right after it, under a new name",
|
|
199
|
+
command: (text) =>
|
|
200
|
+
text.trim() ? `section dup ${name} as ${text.trim()}` : undefined,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
kind: "entry",
|
|
204
|
+
label: "move to",
|
|
205
|
+
value: "",
|
|
206
|
+
placeholder: "a bar, or before/after a section, e.g. 17 or after chorus",
|
|
207
|
+
example: `section move ${name} to 17`,
|
|
208
|
+
help: "move it and its bars; later sections make room",
|
|
209
|
+
command: (text) => {
|
|
210
|
+
const trimmed = text.trim();
|
|
211
|
+
if (/^\d{1,4}$/u.test(trimmed))
|
|
212
|
+
return `section move ${name} to ${trimmed}`;
|
|
213
|
+
return /^(before|after)\s+\S/u.test(trimmed)
|
|
214
|
+
? `section move ${name} ${trimmed}`
|
|
215
|
+
: undefined;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
kind: "action",
|
|
220
|
+
label: "move left",
|
|
221
|
+
command: `section move ${name} left`,
|
|
222
|
+
help: "swap it with the section before (its bars move too)",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
kind: "action",
|
|
226
|
+
label: "move right",
|
|
227
|
+
command: `section move ${name} right`,
|
|
228
|
+
help: "swap it with the section after (its bars move too)",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
kind: "entry",
|
|
232
|
+
label: "rename",
|
|
233
|
+
value: name,
|
|
234
|
+
placeholder: "new name",
|
|
235
|
+
example: `section rename ${name} to hook`,
|
|
236
|
+
help: "the form and the loop follow the new name",
|
|
237
|
+
command: (text) =>
|
|
238
|
+
text.trim() ? `section rename ${name} to ${text.trim()}` : undefined,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
kind: "action",
|
|
242
|
+
label: "clear mutes and variations",
|
|
243
|
+
command: `section reset ${name}`,
|
|
244
|
+
help: "every track plays as written here",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
kind: "action",
|
|
248
|
+
label: "unmark",
|
|
249
|
+
command: `section unmark ${name}`,
|
|
250
|
+
help: "remove the marker; the music stays",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
kind: "action",
|
|
254
|
+
label: "delete with its bars",
|
|
255
|
+
command: `section delete ${name}`,
|
|
256
|
+
help: "remove it and its bars; later bars move up",
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Build rows: into the section (the bars before it) or over it. */
|
|
262
|
+
export function buildNodes(section: Section): MenuNode[] {
|
|
263
|
+
const name = section.name;
|
|
264
|
+
const nodes: MenuNode[] = [];
|
|
265
|
+
if (section.startBar > 0) {
|
|
266
|
+
const bars = Math.min(4, section.startBar);
|
|
267
|
+
nodes.push({
|
|
268
|
+
kind: "action",
|
|
269
|
+
label: `into it (${bars} bar${bars === 1 ? "" : "s"} before)`,
|
|
270
|
+
command: `build into ${name}`,
|
|
271
|
+
help: `riser, snare roll, filter sweep and uplifter over bars ${section.startBar - bars + 1}–${section.startBar}, landing on its downbeat`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
nodes.push(
|
|
275
|
+
{
|
|
276
|
+
kind: "action",
|
|
277
|
+
label: "over it",
|
|
278
|
+
command: `build ${name}`,
|
|
279
|
+
help: "riser, snare roll, filter sweep and uplifter over this section, landing on the bar after",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
kind: "entry",
|
|
283
|
+
label: "custom",
|
|
284
|
+
value: "",
|
|
285
|
+
placeholder:
|
|
286
|
+
"bars and layers, e.g. 8 bars riser sweep (layers: riser roll sweep uplifter)",
|
|
287
|
+
example: `build into ${name} 8 bars riser sweep`,
|
|
288
|
+
help: "choose the length and which layers play",
|
|
289
|
+
command: (text) => {
|
|
290
|
+
const words = text.trim().toLowerCase().split(/\s+/u).filter(Boolean);
|
|
291
|
+
const ok = words.every(
|
|
292
|
+
(word, index) =>
|
|
293
|
+
/^(riser|roll|sweep|uplifter)s?$/u.test(word) ||
|
|
294
|
+
(/^\d{1,3}$/u.test(word) &&
|
|
295
|
+
/^bars?$/u.test(words[index + 1] ?? "")) ||
|
|
296
|
+
(/^bars?$/u.test(word) &&
|
|
297
|
+
/^\d{1,3}$/u.test(words[index - 1] ?? "")),
|
|
298
|
+
);
|
|
299
|
+
if (!ok) return undefined;
|
|
300
|
+
const target = section.startBar > 0 ? `into ${name}` : name;
|
|
301
|
+
return `build ${target} ${words.join(" ")}`.trim();
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
return nodes;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Drop rows: cut length and the impact. */
|
|
309
|
+
export function dropNodes(section: Section): MenuNode[] {
|
|
310
|
+
const name = section.name;
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
kind: "action",
|
|
314
|
+
label: "cut 1 beat and impact",
|
|
315
|
+
command: `drop ${name}`,
|
|
316
|
+
help: "a one-beat cut before it and an impact on its downbeat",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
kind: "action",
|
|
320
|
+
label: "cut 2 beats and impact",
|
|
321
|
+
command: `drop ${name} cut 2`,
|
|
322
|
+
help: "a two-beat cut before it and an impact on its downbeat",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
kind: "action",
|
|
326
|
+
label: "impact only",
|
|
327
|
+
command: `drop ${name} no cut`,
|
|
328
|
+
help: "an impact on its downbeat, no cut",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
kind: "action",
|
|
332
|
+
label: "cut only",
|
|
333
|
+
command: `drop ${name} no impact`,
|
|
334
|
+
help: "a one-beat cut before it, no impact",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
kind: "entry",
|
|
338
|
+
label: "custom",
|
|
339
|
+
value: "",
|
|
340
|
+
placeholder: "cut beats and impact, e.g. cut 4 no impact",
|
|
341
|
+
example: `drop ${name} cut 4 no impact`,
|
|
342
|
+
help: "cut 0 up to two bars of beats; no impact skips the hit",
|
|
343
|
+
command: (text) =>
|
|
344
|
+
/^(cut\s+\d+(\.\d+)?|no\s+cut|no\s+impact|impact|\s)*$/iu.test(
|
|
345
|
+
text.trim(),
|
|
346
|
+
)
|
|
347
|
+
? `drop ${name} ${text.trim()}`.trim()
|
|
348
|
+
: undefined,
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Fill rows: a style each, plus length and crash. */
|
|
354
|
+
export function fillNodes(section: Section): MenuNode[] {
|
|
355
|
+
const name = section.name;
|
|
356
|
+
return [
|
|
357
|
+
{
|
|
358
|
+
kind: "action",
|
|
359
|
+
label: "toms",
|
|
360
|
+
command: `fill ${name} toms`,
|
|
361
|
+
help: "snare into high, mid and low toms on the beat before it, crash on its downbeat",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
kind: "action",
|
|
365
|
+
label: "snare roll",
|
|
366
|
+
command: `fill ${name} roll`,
|
|
367
|
+
help: "snare 16ths on the beat before it, crash on its downbeat",
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
kind: "action",
|
|
371
|
+
label: "kick and snare",
|
|
372
|
+
command: `fill ${name} kick`,
|
|
373
|
+
help: "kick and snare 16ths on the beat before it, crash on its downbeat",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
kind: "entry",
|
|
377
|
+
label: "custom",
|
|
378
|
+
value: "",
|
|
379
|
+
placeholder: "style, beats and crash, e.g. toms 2 beats no crash",
|
|
380
|
+
example: `fill ${name} toms 2 beats no crash`,
|
|
381
|
+
help: "styles toms, roll, kick; half a beat up to two bars",
|
|
382
|
+
command: (text) =>
|
|
383
|
+
/^(toms|roll|kick|\d+(\.\d+)?\s+beats?|no\s+crash|crash|\s)*$/iu.test(
|
|
384
|
+
text.trim(),
|
|
385
|
+
)
|
|
386
|
+
? `fill ${name} ${text.trim()}`.trim()
|
|
387
|
+
: undefined,
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
}
|
package/src/tui/audition.ts
CHANGED
|
@@ -68,6 +68,11 @@ export type AuditionHost = {
|
|
|
68
68
|
* settings screen plays a progression with its settings this way.
|
|
69
69
|
*/
|
|
70
70
|
phrase?(showing: "A" | "B"): PreviewOptions["phrase"];
|
|
71
|
+
/**
|
|
72
|
+
* The drive the song's master solved for its target on the full mix (the
|
|
73
|
+
* last song loop), so previews hear a fixed master gain.
|
|
74
|
+
*/
|
|
75
|
+
masterGainDb?(): number | undefined;
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
export type AuditionOptions = Readonly<{
|
|
@@ -108,6 +113,8 @@ export class Audition {
|
|
|
108
113
|
private staged: Staged[] = [];
|
|
109
114
|
private work: Promise<unknown> = Promise.resolve();
|
|
110
115
|
private rendering = false;
|
|
116
|
+
/** The song master's solved drive when the loop started (see host). */
|
|
117
|
+
private masterGainDb: number | undefined;
|
|
111
118
|
private dirty = false;
|
|
112
119
|
private pendingKeyAt: number | undefined;
|
|
113
120
|
private lastRenderAt = -Infinity;
|
|
@@ -166,6 +173,7 @@ export class Audition {
|
|
|
166
173
|
|
|
167
174
|
public start(): void {
|
|
168
175
|
if (this.looping) return;
|
|
176
|
+
this.masterGainDb = this.host.masterGainDb?.();
|
|
169
177
|
this.looping = true;
|
|
170
178
|
this.lastRenderAt = -Infinity;
|
|
171
179
|
this.request();
|
|
@@ -192,6 +200,18 @@ export class Audition {
|
|
|
192
200
|
this.host.changed?.();
|
|
193
201
|
}
|
|
194
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Whether the loop plays the full mix: the `c` toggle, or forced while a
|
|
205
|
+
* master edit is staged, because the master works on the whole mix and a
|
|
206
|
+
* solo track says nothing about its loudness.
|
|
207
|
+
*/
|
|
208
|
+
public get inContext(): boolean {
|
|
209
|
+
return (
|
|
210
|
+
this.context ||
|
|
211
|
+
this.staged.some((entry) => MASTER_COMMAND.test(entry.command))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
195
215
|
/** A/B; a no-op (stays on B) while nothing is staged. */
|
|
196
216
|
public toggleAB(): void {
|
|
197
217
|
if (!this.dirtyEdits) {
|
|
@@ -412,10 +432,18 @@ export class Audition {
|
|
|
412
432
|
if (!this.looping || this.rendering) return;
|
|
413
433
|
this.cancelTimer();
|
|
414
434
|
const phrase = this.host.phrase?.(this.dirtyEdits ? this.showing : "A");
|
|
435
|
+
// A staged master edit re-solves its target on the mix; otherwise the
|
|
436
|
+
// master plays at the song's solved drive, so a track edit is audible.
|
|
437
|
+
const masterStaged = this.staged.some((entry) =>
|
|
438
|
+
MASTER_COMMAND.test(entry.command),
|
|
439
|
+
);
|
|
415
440
|
const preview = previewScore(this.sounding, this.trackId, {
|
|
416
|
-
context: this.
|
|
441
|
+
context: this.inContext,
|
|
417
442
|
beat: this.host.beat?.() ?? 0,
|
|
418
443
|
...(phrase ? { phrase } : {}),
|
|
444
|
+
...(this.masterGainDb !== undefined && !masterStaged
|
|
445
|
+
? { masterGainDb: this.masterGainDb }
|
|
446
|
+
: {}),
|
|
419
447
|
});
|
|
420
448
|
if (!preview) return;
|
|
421
449
|
this.rendering = true;
|
|
@@ -451,7 +479,7 @@ export class Audition {
|
|
|
451
479
|
public status(): string | undefined {
|
|
452
480
|
if (!this.looping && !this.dirtyEdits && !this.fetching) return undefined;
|
|
453
481
|
const parts: string[] = [];
|
|
454
|
-
if (this.looping) parts.push(`♪ ${this.
|
|
482
|
+
if (this.looping) parts.push(`♪ ${this.inContext ? "in context" : "solo"}`);
|
|
455
483
|
else parts.push("loop off");
|
|
456
484
|
if (this.fetching) parts.push("fetching…");
|
|
457
485
|
if (this.dirtyEdits)
|
|
@@ -500,13 +528,18 @@ export function auditionKey(value: string): AuditionKey | undefined {
|
|
|
500
528
|
* before.
|
|
501
529
|
*/
|
|
502
530
|
const STAGEABLE =
|
|
503
|
-
/^\/?(?:(fx|effects|filter|lowpass|synth|wt|wavetable|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid)\s+\S|pack\s+use\s+\S)/i;
|
|
531
|
+
/^\/?(?:(fx|effects|filter|lowpass|synth|string|modal|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)\s+\S|pack\s+use\s+\S|(?:piano|grand|upright|felt|honkytonk|prepared|ballad)(?:\s+piano)?\s*$)/i;
|
|
504
532
|
/** Subcommands that list or show instead of changing the sound. */
|
|
505
|
-
const READ_ONLY = /^\/?\S+\s+(list|show|info|help)\s*$/i;
|
|
533
|
+
const READ_ONLY = /^\/?\S+\s+(list|show|info|help|measure|meter|presets?)\s*$/i;
|
|
534
|
+
/** `master <unit>` alone only shows the unit. */
|
|
535
|
+
const MASTER_SHOW = /^\/?master\s+(eq|glue|tape|width|limiter)\s*$/i;
|
|
536
|
+
const MASTER_COMMAND = /^\/?master\s/i;
|
|
506
537
|
|
|
507
538
|
export function isStageable(command: string): boolean {
|
|
508
539
|
const text = command.trim();
|
|
509
|
-
return
|
|
540
|
+
return (
|
|
541
|
+
STAGEABLE.test(text) && !READ_ONLY.test(text) && !MASTER_SHOW.test(text)
|
|
542
|
+
);
|
|
510
543
|
}
|
|
511
544
|
|
|
512
545
|
/**
|