@hraness/dawg 0.6.1 → 0.8.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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +82 -32
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/src/tui/fader.ts
CHANGED
|
@@ -9,7 +9,27 @@
|
|
|
9
9
|
* field, so repeated nudges replace one staged edit and Enter keeps them
|
|
10
10
|
* all as one revision.
|
|
11
11
|
*/
|
|
12
|
-
import type { DrawerField, DrawerView } from "../../tui/drawer.ts";
|
|
12
|
+
import type { DrawerAside, DrawerField, DrawerView } from "../../tui/drawer.ts";
|
|
13
|
+
import type { KnobIndex } from "../../tui/knobs.ts";
|
|
14
|
+
import {
|
|
15
|
+
KEY_BACKSPACE,
|
|
16
|
+
KEY_BACKTAB,
|
|
17
|
+
KEY_COARSE_LEFT,
|
|
18
|
+
KEY_COARSE_RIGHT,
|
|
19
|
+
KEY_DOWN,
|
|
20
|
+
KEY_END,
|
|
21
|
+
KEY_ENTER,
|
|
22
|
+
KEY_FINE_LEFT,
|
|
23
|
+
KEY_FINE_RIGHT,
|
|
24
|
+
KEY_HOME,
|
|
25
|
+
KEY_LEFT,
|
|
26
|
+
KEY_PAGE_DOWN,
|
|
27
|
+
KEY_PAGE_UP,
|
|
28
|
+
KEY_RESET,
|
|
29
|
+
KEY_RIGHT,
|
|
30
|
+
KEY_TAB,
|
|
31
|
+
KEY_UP,
|
|
32
|
+
} from "../../tui/grammar.ts";
|
|
13
33
|
|
|
14
34
|
export type FaderNumber = Readonly<{
|
|
15
35
|
kind: "number";
|
|
@@ -22,7 +42,7 @@ export type FaderNumber = Readonly<{
|
|
|
22
42
|
command: (value: number) => string;
|
|
23
43
|
/** Typed text → command; undefined when it does not parse or fit. */
|
|
24
44
|
parse: (text: string) => string | undefined;
|
|
25
|
-
/** The command `
|
|
45
|
+
/** The command `x` / `d` / Delete runs to put the value back. */
|
|
26
46
|
reset?: string | undefined;
|
|
27
47
|
/** Starting value for a nudge while off. */
|
|
28
48
|
start?: number | undefined;
|
|
@@ -44,6 +64,8 @@ export interface FaderState {
|
|
|
44
64
|
label: string;
|
|
45
65
|
/** Digits typed for the focused field. */
|
|
46
66
|
typing?: string | undefined;
|
|
67
|
+
/** The field that just snapped onto a detent: flashes for one frame. */
|
|
68
|
+
flash?: string | undefined;
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
export type FaderResult =
|
|
@@ -111,6 +133,85 @@ function snap(spec: FaderNumber, raw: number): number {
|
|
|
111
133
|
return Math.min(spec.max, Math.max(spec.min, best));
|
|
112
134
|
}
|
|
113
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Detents: values a fader catches on its way past, so the common resting
|
|
138
|
+
* points are easy to hit by feel. Volume 1 (0 dB), pan 0 (center), whole
|
|
139
|
+
* BPM, mix 0 / 0.5 / 1 and filter cutoffs on the octaves of A (27.5 Hz,
|
|
140
|
+
* 55 Hz … 14080 Hz). Read off the field's command, so every row that runs
|
|
141
|
+
* `volume`, `pan`, `tempo`, `… mix` or a cutoff gets them.
|
|
142
|
+
*/
|
|
143
|
+
export type Detents = readonly number[] | "integer";
|
|
144
|
+
|
|
145
|
+
/** How close (as a share of the bar) a value must be to catch a detent. */
|
|
146
|
+
export const DETENT_REACH = 0.02;
|
|
147
|
+
|
|
148
|
+
const A_OCTAVES = Object.freeze(
|
|
149
|
+
Array.from({ length: 10 }, (_, octave) => 27.5 * 2 ** octave),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
export function detentsFor(spec: FaderNumber): Detents | undefined {
|
|
153
|
+
const probe = spec.value ?? spec.start ?? spec.min;
|
|
154
|
+
const command = spec.command(probe).replace(/\s+\S+$/, "");
|
|
155
|
+
const words = command.split(/\s+/);
|
|
156
|
+
const verb = words[0]?.replace(/^\//, "");
|
|
157
|
+
if (verb === "tempo" && words.length === 1) return "integer";
|
|
158
|
+
if (words.includes("volume")) return [1];
|
|
159
|
+
if (words.includes("pan")) return [0];
|
|
160
|
+
if (words.at(-1) === "mix") return [0, 0.5, 1];
|
|
161
|
+
if (/^(cutoff|lpf|hpf)$/.test(words.at(-1) ?? "")) return A_OCTAVES;
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The detents of `spec` that fall inside its range. */
|
|
166
|
+
function detentList(spec: FaderNumber, detents: Detents): number[] {
|
|
167
|
+
if (detents === "integer") return [];
|
|
168
|
+
return detents.filter((d) => d >= spec.min - 1e-9 && d <= spec.max + 1e-9);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `value` caught by a detent, or undefined when none is in reach. With
|
|
173
|
+
* `from` (a key step) a detent also catches a step that jumps over it or
|
|
174
|
+
* lands near it while moving toward it, so leaving a detent never sticks.
|
|
175
|
+
*/
|
|
176
|
+
export function detentSnap(
|
|
177
|
+
spec: FaderNumber,
|
|
178
|
+
value: number,
|
|
179
|
+
from?: number,
|
|
180
|
+
): number | undefined {
|
|
181
|
+
const detents = detentsFor(spec);
|
|
182
|
+
if (!detents) return undefined;
|
|
183
|
+
if (detents === "integer") {
|
|
184
|
+
const whole = Math.min(spec.max, Math.max(spec.min, Math.round(value)));
|
|
185
|
+
return Math.abs(whole - value) < 1e-9 ? undefined : whole;
|
|
186
|
+
}
|
|
187
|
+
const at = (v: number) => faderPosition(spec, v);
|
|
188
|
+
let best: number | undefined;
|
|
189
|
+
for (const detent of detentList(spec, detents)) {
|
|
190
|
+
// Landing on a detent exactly counts as a catch (it flashes) unless
|
|
191
|
+
// the value was already there.
|
|
192
|
+
if (Math.abs(detent - value) < 1e-9)
|
|
193
|
+
return from !== undefined && Math.abs(detent - from) > 1e-9
|
|
194
|
+
? detent
|
|
195
|
+
: undefined;
|
|
196
|
+
const near = Math.abs(at(value) - at(detent)) <= DETENT_REACH;
|
|
197
|
+
let caught = near;
|
|
198
|
+
if (from !== undefined) {
|
|
199
|
+
const toward = Math.sign(detent - from) === Math.sign(value - from);
|
|
200
|
+
const crossed =
|
|
201
|
+
(from < detent && detent < value) || (value < detent && detent < from);
|
|
202
|
+
caught = Math.abs(detent - from) > 1e-9 && (crossed || (near && toward));
|
|
203
|
+
}
|
|
204
|
+
if (
|
|
205
|
+
caught &&
|
|
206
|
+
(best === undefined ||
|
|
207
|
+
Math.abs(at(detent) - at(from ?? value)) <
|
|
208
|
+
Math.abs(at(best) - at(from ?? value)))
|
|
209
|
+
)
|
|
210
|
+
best = detent;
|
|
211
|
+
}
|
|
212
|
+
return best;
|
|
213
|
+
}
|
|
214
|
+
|
|
114
215
|
/** Fine (a tenth of a step), normal, coarse (five steps) or page (twenty). */
|
|
115
216
|
export type StepSize = "fine" | "normal" | "coarse" | "page";
|
|
116
217
|
|
|
@@ -142,20 +243,9 @@ export function stepValue(
|
|
|
142
243
|
return value;
|
|
143
244
|
}
|
|
144
245
|
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const COARSE_RIGHT = new Set(["\u001b[1;2C", "}"]);
|
|
149
|
-
const FINE_LEFT = new Set(["\u001b[1;3D", "\u001b[1;5D", "\u001bb", "["]);
|
|
150
|
-
const FINE_RIGHT = new Set(["\u001b[1;3C", "\u001b[1;5C", "\u001bf", "]"]);
|
|
151
|
-
const UP = new Set(["\u001b[A", "\u001bOA", "k", "\u001b[Z"]);
|
|
152
|
-
const DOWN = new Set(["\u001b[B", "\u001bOB", "j", "\t"]);
|
|
153
|
-
const PAGE_UP = "\u001b[5~";
|
|
154
|
-
const PAGE_DOWN = "\u001b[6~";
|
|
155
|
-
const HOME = new Set(["\u001b[H", "\u001bOH", "\u001b[1~"]);
|
|
156
|
-
const END = new Set(["\u001b[F", "\u001bOF", "\u001b[4~"]);
|
|
157
|
-
const ENTER = new Set(["\r", "\n"]);
|
|
158
|
-
const BACKSPACE = new Set(["\u007f", "\b"]);
|
|
246
|
+
/** ↑ and shift-tab: the previous param; ↓ and tab: the next. */
|
|
247
|
+
const isUp = (value: string) => KEY_UP.has(value) || KEY_BACKTAB.has(value);
|
|
248
|
+
const isDown = (value: string) => KEY_DOWN.has(value) || KEY_TAB.has(value);
|
|
159
249
|
|
|
160
250
|
export interface FaderKeyOptions {
|
|
161
251
|
/** Edits are staged (Enter keeps, Esc reverts). */
|
|
@@ -185,6 +275,32 @@ function setNumber(spec: FaderNumber, value: number): FaderResult {
|
|
|
185
275
|
};
|
|
186
276
|
}
|
|
187
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Set `value`, caught by a detent unless the step is fine. `from` is the
|
|
280
|
+
* value a key step left (undefined for a drag or a click on the bar).
|
|
281
|
+
*/
|
|
282
|
+
function setSnapped(
|
|
283
|
+
state: FaderState,
|
|
284
|
+
spec: FaderNumber,
|
|
285
|
+
value: number,
|
|
286
|
+
from: number | undefined,
|
|
287
|
+
size: StepSize | "drag",
|
|
288
|
+
): FaderResult {
|
|
289
|
+
const snapped =
|
|
290
|
+
size === "fine"
|
|
291
|
+
? undefined
|
|
292
|
+
: size === "drag"
|
|
293
|
+
? (detentSnap(spec, value) ??
|
|
294
|
+
(spec.value !== undefined &&
|
|
295
|
+
detentSnap(spec, value, spec.value) === value
|
|
296
|
+
? value
|
|
297
|
+
: undefined))
|
|
298
|
+
: detentSnap(spec, value, from);
|
|
299
|
+
const result = setNumber(spec, snapped ?? value);
|
|
300
|
+
if (snapped !== undefined && result.type === "set") state.flash = spec.label;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
188
304
|
function setChoice(spec: FaderChoice, index: number): FaderResult {
|
|
189
305
|
const option =
|
|
190
306
|
spec.options[Math.min(spec.options.length - 1, Math.max(0, index))];
|
|
@@ -215,12 +331,12 @@ export function faderKeyPress(
|
|
|
215
331
|
state.typing = undefined;
|
|
216
332
|
return { type: "handled" };
|
|
217
333
|
}
|
|
218
|
-
if (
|
|
334
|
+
if (KEY_BACKSPACE.has(value)) {
|
|
219
335
|
state.typing = state.typing.slice(0, -1);
|
|
220
336
|
if (!state.typing) state.typing = undefined;
|
|
221
337
|
return { type: "handled" };
|
|
222
338
|
}
|
|
223
|
-
if (
|
|
339
|
+
if (KEY_ENTER.has(value) || KEY_TAB.has(value)) {
|
|
224
340
|
const text = state.typing;
|
|
225
341
|
state.typing = undefined;
|
|
226
342
|
const command = field.kind === "number" ? field.parse(text) : undefined;
|
|
@@ -236,9 +352,9 @@ export function faderKeyPress(
|
|
|
236
352
|
}
|
|
237
353
|
if (value === "\u001b")
|
|
238
354
|
return options.dirty ? { type: "revert" } : { type: "close" };
|
|
239
|
-
if (
|
|
355
|
+
if (KEY_ENTER.has(value))
|
|
240
356
|
return options.dirty ? { type: "keep" } : { type: "close" };
|
|
241
|
-
const move =
|
|
357
|
+
const move = isUp(value) ? -1 : isDown(value) ? 1 : 0;
|
|
242
358
|
if (move) {
|
|
243
359
|
state.label = fields[(index + move + fields.length) % fields.length]!.label;
|
|
244
360
|
return { type: "handled" };
|
|
@@ -250,30 +366,46 @@ export function faderKeyPress(
|
|
|
250
366
|
}
|
|
251
367
|
if (field.kind === "choice") {
|
|
252
368
|
const at = Math.max(0, field.options.indexOf(field.value));
|
|
253
|
-
if (
|
|
369
|
+
if (
|
|
370
|
+
KEY_LEFT.has(value) ||
|
|
371
|
+
KEY_FINE_LEFT.has(value) ||
|
|
372
|
+
KEY_COARSE_LEFT.has(value)
|
|
373
|
+
)
|
|
254
374
|
return setChoice(field, at - 1);
|
|
255
|
-
if (
|
|
375
|
+
if (
|
|
376
|
+
KEY_RIGHT.has(value) ||
|
|
377
|
+
KEY_FINE_RIGHT.has(value) ||
|
|
378
|
+
KEY_COARSE_RIGHT.has(value)
|
|
379
|
+
)
|
|
256
380
|
return setChoice(field, at + 1);
|
|
257
|
-
if (
|
|
258
|
-
|
|
381
|
+
if (KEY_HOME.has(value) || KEY_PAGE_DOWN.has(value))
|
|
382
|
+
return setChoice(field, 0);
|
|
383
|
+
if (KEY_END.has(value) || KEY_PAGE_UP.has(value))
|
|
259
384
|
return setChoice(field, field.options.length - 1);
|
|
260
385
|
// 1…9 picks an option by number.
|
|
261
386
|
if (/^[1-9]$/.test(value)) return setChoice(field, Number(value) - 1);
|
|
262
387
|
return { type: "handled" };
|
|
263
388
|
}
|
|
264
389
|
const step = (direction: 1 | -1, size: StepSize) =>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (value
|
|
273
|
-
if (value
|
|
274
|
-
if (
|
|
275
|
-
if (
|
|
276
|
-
if (value
|
|
390
|
+
setSnapped(
|
|
391
|
+
state,
|
|
392
|
+
field,
|
|
393
|
+
stepValue(field, direction, size),
|
|
394
|
+
field.value,
|
|
395
|
+
size,
|
|
396
|
+
);
|
|
397
|
+
if (KEY_LEFT.has(value)) return step(-1, "normal");
|
|
398
|
+
if (KEY_RIGHT.has(value)) return step(1, "normal");
|
|
399
|
+
if (KEY_COARSE_LEFT.has(value)) return step(-1, "coarse");
|
|
400
|
+
if (KEY_COARSE_RIGHT.has(value)) return step(1, "coarse");
|
|
401
|
+
if (KEY_FINE_LEFT.has(value)) return step(-1, "fine");
|
|
402
|
+
if (KEY_FINE_RIGHT.has(value)) return step(1, "fine");
|
|
403
|
+
if (KEY_PAGE_UP.has(value)) return step(1, "page");
|
|
404
|
+
if (KEY_PAGE_DOWN.has(value)) return step(-1, "page");
|
|
405
|
+
if (KEY_HOME.has(value)) return setNumber(field, field.min);
|
|
406
|
+
if (KEY_END.has(value)) return setNumber(field, field.max);
|
|
407
|
+
// x, d and Delete put the value back; 0-9 and . start typing.
|
|
408
|
+
if (KEY_RESET.has(value)) {
|
|
277
409
|
const reset =
|
|
278
410
|
field.reset ??
|
|
279
411
|
(field.start !== undefined ? field.command(field.start) : undefined);
|
|
@@ -281,7 +413,7 @@ export function faderKeyPress(
|
|
|
281
413
|
? { type: "set", command: reset, key: faderKey(field.label) }
|
|
282
414
|
: { type: "handled" };
|
|
283
415
|
}
|
|
284
|
-
if (/^[
|
|
416
|
+
if (/^[0-9.]$/.test(value)) {
|
|
285
417
|
state.typing = value === "." ? "0." : value;
|
|
286
418
|
return { type: "handled" };
|
|
287
419
|
}
|
|
@@ -302,7 +434,13 @@ export function faderSetPosition(
|
|
|
302
434
|
field,
|
|
303
435
|
Math.round(position * Math.max(0, field.options.length - 1)),
|
|
304
436
|
);
|
|
305
|
-
return
|
|
437
|
+
return setSnapped(
|
|
438
|
+
state,
|
|
439
|
+
field,
|
|
440
|
+
faderValueAt(field, position),
|
|
441
|
+
undefined,
|
|
442
|
+
"drag",
|
|
443
|
+
);
|
|
306
444
|
}
|
|
307
445
|
|
|
308
446
|
/** A [−] / [+] click or a wheel notch on a field. */
|
|
@@ -319,7 +457,13 @@ export function faderStep(
|
|
|
319
457
|
const at = Math.max(0, field.options.indexOf(field.value));
|
|
320
458
|
return setChoice(field, at + direction);
|
|
321
459
|
}
|
|
322
|
-
return
|
|
460
|
+
return setSnapped(
|
|
461
|
+
state,
|
|
462
|
+
field,
|
|
463
|
+
stepValue(field, direction, size),
|
|
464
|
+
field.value,
|
|
465
|
+
size,
|
|
466
|
+
);
|
|
323
467
|
}
|
|
324
468
|
|
|
325
469
|
/** A click on one option of a choice field. */
|
|
@@ -334,8 +478,33 @@ export function faderChoose(
|
|
|
334
478
|
return setChoice(field, option);
|
|
335
479
|
}
|
|
336
480
|
|
|
337
|
-
|
|
338
|
-
|
|
481
|
+
/**
|
|
482
|
+
* Most important first: fitHint trims the middle from the right, so at 80
|
|
483
|
+
* columns the coarse and fine steps go before `enter keep` or `x reset`.
|
|
484
|
+
*/
|
|
485
|
+
export const FADER_HINT =
|
|
486
|
+
"←→ adjust · enter keep · x reset · 0-9 type · ↑↓ param · ⇧←→ coarse · [ ] fine · esc revert";
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* The hint for a field the loop cannot stage (tempo, meter, loop length):
|
|
490
|
+
* each step is a new revision, so there is nothing to keep or revert.
|
|
491
|
+
*/
|
|
492
|
+
/** The knob front page (design §8.6): the four knobs, then Tab for all. */
|
|
493
|
+
export const KNOB_FADER_HINT =
|
|
494
|
+
"↑↓ knob · ←→ turn · tab all · enter keep · ⇧ coarse · x reset · esc revert";
|
|
495
|
+
|
|
496
|
+
/** Every param behind Tab: Tab goes back to the four knobs. */
|
|
497
|
+
export const ALL_FADER_HINT =
|
|
498
|
+
"←→ adjust · ↑↓ param · tab knobs · enter keep · ⇧ coarse · x reset · esc revert";
|
|
499
|
+
|
|
500
|
+
export const FADER_HINT_AT_ONCE =
|
|
501
|
+
"←→ adjust · applies at once · x reset · 0-9 type · ↑↓ param · ⇧←→ coarse · [ ] fine · esc back";
|
|
502
|
+
|
|
503
|
+
/** The value a field's next step starts from (for checking what it runs). */
|
|
504
|
+
export function faderCommand(field: FaderSpec): string {
|
|
505
|
+
if (field.kind === "choice") return field.command(field.value);
|
|
506
|
+
return field.command(field.value ?? field.start ?? field.min);
|
|
507
|
+
}
|
|
339
508
|
|
|
340
509
|
/** The drawer's paint model for `fields` (staged) beside `committed`. */
|
|
341
510
|
export function drawerView(
|
|
@@ -345,11 +514,20 @@ export function drawerView(
|
|
|
345
514
|
options: {
|
|
346
515
|
title: string;
|
|
347
516
|
dirty: boolean;
|
|
517
|
+
/** How many edits are staged (defaults to 1 while dirty). */
|
|
518
|
+
staged?: number | undefined;
|
|
348
519
|
status?: string | undefined;
|
|
349
520
|
hint?: string;
|
|
521
|
+
/** The focused field applies at once (cannot stage on the loop). */
|
|
522
|
+
atOnce?: boolean | undefined;
|
|
523
|
+
/** The drawer has a knob page: which side of Tab is up. */
|
|
524
|
+
knobs?: "knobs" | "all" | undefined;
|
|
350
525
|
},
|
|
351
526
|
): DrawerView {
|
|
352
527
|
const before = new Map(committed.map((field) => [field.label, field]));
|
|
528
|
+
// A detent flash lasts one view: consume it here.
|
|
529
|
+
const flash = state.flash;
|
|
530
|
+
state.flash = undefined;
|
|
353
531
|
const drawn: DrawerField[] = fields.map((field) => {
|
|
354
532
|
const was = before.get(field.label);
|
|
355
533
|
if (field.kind === "choice") {
|
|
@@ -361,6 +539,7 @@ export function drawerView(
|
|
|
361
539
|
return {
|
|
362
540
|
kind: "choice",
|
|
363
541
|
label: field.label,
|
|
542
|
+
knob: knobOf(field),
|
|
364
543
|
options: field.options,
|
|
365
544
|
index,
|
|
366
545
|
committedIndex:
|
|
@@ -383,6 +562,7 @@ export function drawerView(
|
|
|
383
562
|
return {
|
|
384
563
|
kind: "number",
|
|
385
564
|
label: field.label,
|
|
565
|
+
knob: knobOf(field),
|
|
386
566
|
text,
|
|
387
567
|
committed: changed ? wasText : undefined,
|
|
388
568
|
position:
|
|
@@ -395,15 +575,75 @@ export function drawerView(
|
|
|
395
575
|
: undefined,
|
|
396
576
|
minText: field.format(field.min),
|
|
397
577
|
maxText: field.format(field.max),
|
|
578
|
+
...(flash === field.label && field.value !== undefined
|
|
579
|
+
? { flash: detentLabel(field, field.value) }
|
|
580
|
+
: {}),
|
|
398
581
|
};
|
|
399
582
|
});
|
|
583
|
+
const staged = options.staged ?? (options.dirty ? 1 : 0);
|
|
400
584
|
return {
|
|
401
585
|
title: options.title,
|
|
402
586
|
fields: drawn,
|
|
403
587
|
focus: focusIndex(state, fields),
|
|
404
588
|
typing: state.typing,
|
|
405
589
|
dirty: options.dirty,
|
|
590
|
+
badge: options.dirty ? stagedBadge(staged) : undefined,
|
|
406
591
|
status: options.status,
|
|
407
|
-
hint:
|
|
592
|
+
hint:
|
|
593
|
+
options.hint ??
|
|
594
|
+
(options.knobs
|
|
595
|
+
? (options.knobs === "knobs" ? KNOB_FADER_HINT : ALL_FADER_HINT)
|
|
596
|
+
.replace(
|
|
597
|
+
"enter keep",
|
|
598
|
+
options.atOnce ? "applies at once" : "enter keep",
|
|
599
|
+
)
|
|
600
|
+
.replace("esc revert", options.atOnce ? "esc back" : "esc revert")
|
|
601
|
+
: options.atOnce
|
|
602
|
+
? FADER_HINT_AT_ONCE
|
|
603
|
+
: FADER_HINT),
|
|
408
604
|
};
|
|
409
605
|
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* The drawer's side list: every param on the page with its value, each one
|
|
609
|
+
* a knob also drives wearing that knob (matched by label).
|
|
610
|
+
*/
|
|
611
|
+
export function drawerAside(
|
|
612
|
+
page: readonly FaderSpec[],
|
|
613
|
+
knobs: readonly FaderSpec[],
|
|
614
|
+
): DrawerAside[] {
|
|
615
|
+
const knobByLabel = new Map(
|
|
616
|
+
knobs.map((field) => [field.label, knobOf(field)] as const),
|
|
617
|
+
);
|
|
618
|
+
return page.map((field) => ({
|
|
619
|
+
label: field.label,
|
|
620
|
+
text:
|
|
621
|
+
field.kind === "choice"
|
|
622
|
+
? field.value
|
|
623
|
+
: field.value === undefined
|
|
624
|
+
? (field.off ?? "off")
|
|
625
|
+
: field.format(field.value),
|
|
626
|
+
knob: knobByLabel.get(field.label),
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** A knob field's knob (0 blue … 3 orange); plain rows have none. */
|
|
631
|
+
function knobOf(field: FaderSpec): KnobIndex | undefined {
|
|
632
|
+
const knob = (field as FaderSpec & { knob?: KnobIndex }).knob;
|
|
633
|
+
return knob;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** `A/B: 1 change staged · enter keep · esc revert`. */
|
|
637
|
+
export function stagedBadge(count: number): string {
|
|
638
|
+
const n = Math.max(1, count);
|
|
639
|
+
return `A/B: ${n} change${n === 1 ? "" : "s"} staged · enter keep · esc revert`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** The faint word a detent flashes: `0 dB`, `center`, or the value. */
|
|
643
|
+
export function detentLabel(spec: FaderNumber, value: number): string {
|
|
644
|
+
const text = spec.format(value);
|
|
645
|
+
// Volume formats as `1 · 0.0 dB`; the flash names the detent itself.
|
|
646
|
+
const db = /(-?[\d.]+) dB$/.exec(text);
|
|
647
|
+
if (db && Number(db[1]) === 0) return "0 dB";
|
|
648
|
+
return text;
|
|
649
|
+
}
|
package/src/tui/granular-menu.ts
CHANGED
|
@@ -135,9 +135,7 @@ export function granularMenuLabel(
|
|
|
135
135
|
): string | undefined {
|
|
136
136
|
if (!track || track.kit || isDrumInstrument(track.instrument))
|
|
137
137
|
return undefined;
|
|
138
|
-
return isGranularInstrument(track.instrument)
|
|
139
|
-
? "granular"
|
|
140
|
-
: "granular (convert)";
|
|
138
|
+
return isGranularInstrument(track.instrument) ? "granular" : "granular";
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
/** One-line state for the subsection's detail column. */
|
|
@@ -265,7 +263,7 @@ export function granularMenuNodes(
|
|
|
265
263
|
return nodes;
|
|
266
264
|
}
|
|
267
265
|
|
|
268
|
-
/** Sound
|
|
266
|
+
/** Sound › instruments › granular: one row per preset. */
|
|
269
267
|
export function granularBrowseNodes(): GranularMenuNode[] {
|
|
270
268
|
return GRANULAR_PRESET_NAMES.map((name) => ({
|
|
271
269
|
kind: "action",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four knobs of a page as fader fields: each path of a KNOB_MAPS row
|
|
3
|
+
* (src/tui/knob-map.ts) resolved in the live Ctrl-K tree, so a knob turns
|
|
4
|
+
* exactly what that menu row turns and runs the same typed command.
|
|
5
|
+
*/
|
|
6
|
+
import type { KnobIndex } from "../../tui/knobs.ts";
|
|
7
|
+
import type { FaderSpec } from "./fader.ts";
|
|
8
|
+
import { knobMap, type KnobMap } from "./knob-map.ts";
|
|
9
|
+
import {
|
|
10
|
+
faderSpecs,
|
|
11
|
+
rootNodes,
|
|
12
|
+
type MenuContext,
|
|
13
|
+
type MenuNode,
|
|
14
|
+
} from "./menu.ts";
|
|
15
|
+
|
|
16
|
+
export type KnobField = FaderSpec & { knob: KnobIndex };
|
|
17
|
+
|
|
18
|
+
const norm = (text: string) => text.trim().toLowerCase();
|
|
19
|
+
|
|
20
|
+
function matches(node: MenuNode, segment: string): boolean {
|
|
21
|
+
const want = norm(segment);
|
|
22
|
+
if (want.endsWith("*"))
|
|
23
|
+
return node.kind === "menu" && norm(node.id).startsWith(want.slice(0, -1));
|
|
24
|
+
if (node.kind === "menu" && norm(node.id) === want) return true;
|
|
25
|
+
return norm(node.label) === want;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The row a knob path reaches from the root, or undefined. */
|
|
29
|
+
export function resolveKnobPath(
|
|
30
|
+
context: MenuContext,
|
|
31
|
+
path: string,
|
|
32
|
+
): MenuNode | undefined {
|
|
33
|
+
let nodes: readonly MenuNode[] = rootNodes(context);
|
|
34
|
+
let found: MenuNode | undefined;
|
|
35
|
+
for (const segment of path.split("/")) {
|
|
36
|
+
found = nodes.find((node) => matches(node, segment));
|
|
37
|
+
if (!found) {
|
|
38
|
+
// Effects beyond the first few live under "more effects".
|
|
39
|
+
const more = nodes.find(
|
|
40
|
+
(node) => node.kind === "menu" && node.id === "more effects",
|
|
41
|
+
);
|
|
42
|
+
if (more?.kind === "menu")
|
|
43
|
+
found = more.build(context).find((node) => matches(node, segment));
|
|
44
|
+
}
|
|
45
|
+
if (!found) return undefined;
|
|
46
|
+
nodes = found.kind === "menu" ? found.build(context) : [];
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The knob label: the row's own label, or `reverb mix` when the row lives in
|
|
53
|
+
* another effect than the page's (the mixer's reverb send).
|
|
54
|
+
*/
|
|
55
|
+
function knobLabel(page: string, path: string, label: string): string {
|
|
56
|
+
const segments = path.split("/");
|
|
57
|
+
if (segments[0] === "effects" && segments.length >= 3) {
|
|
58
|
+
const effect = segments[1]!;
|
|
59
|
+
if (page !== `fx:${effect}`) return `${effect} ${label}`;
|
|
60
|
+
}
|
|
61
|
+
return label;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The four slots of `page` for `context`: a field per slot whose path
|
|
66
|
+
* resolves to a number or choice row, else undefined (drawn `·`, skipped).
|
|
67
|
+
* A page without a row (an effect not in the table) derives one from
|
|
68
|
+
* `fallback`: its first three number rows, and `mix` on orange.
|
|
69
|
+
*/
|
|
70
|
+
export function knobSlots(
|
|
71
|
+
context: MenuContext,
|
|
72
|
+
page: string,
|
|
73
|
+
fallback: readonly FaderSpec[] = [],
|
|
74
|
+
): (KnobField | undefined)[] {
|
|
75
|
+
const map: KnobMap | undefined = knobMap(page);
|
|
76
|
+
if (!map) {
|
|
77
|
+
const numbers = fallback.filter(
|
|
78
|
+
(field) => field.kind === "number" && field.label !== "mix",
|
|
79
|
+
);
|
|
80
|
+
const mix = fallback.find((field) => field.label === "mix");
|
|
81
|
+
return [numbers[0], numbers[1], numbers[2], mix].map((field, index) =>
|
|
82
|
+
field ? { ...field, knob: index as KnobIndex } : undefined,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return map.map((path, index) => {
|
|
86
|
+
if (!path || path.startsWith("@") || !path.includes("/")) return undefined;
|
|
87
|
+
const node = resolveKnobPath(context, path);
|
|
88
|
+
const spec = node ? faderSpecs([node])[0] : undefined;
|
|
89
|
+
if (!spec) return undefined;
|
|
90
|
+
return {
|
|
91
|
+
...spec,
|
|
92
|
+
label: knobLabel(page, path, spec.label),
|
|
93
|
+
knob: index as KnobIndex,
|
|
94
|
+
} as KnobField;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The present knobs of `page`, in knob order: the drawer's front page. */
|
|
99
|
+
export function knobFields(
|
|
100
|
+
context: MenuContext,
|
|
101
|
+
page: string,
|
|
102
|
+
fallback: readonly FaderSpec[] = [],
|
|
103
|
+
): KnobField[] {
|
|
104
|
+
return knobSlots(context, page, fallback).filter(
|
|
105
|
+
(field): field is KnobField => field !== undefined,
|
|
106
|
+
);
|
|
107
|
+
}
|