@hraness/dawg 0.6.1 → 0.7.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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- 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 +30 -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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +17 -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/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 +5 -2
- package/src/agent/agent.ts +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -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 +12 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -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/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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -3
- 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/doctor.ts +86 -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 +160 -28
- 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 +44 -29
- 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 +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -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 +616 -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/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- 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/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- 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 +759 -177
- 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 +47 -7
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -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 +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/src/tui/fader.ts
CHANGED
|
@@ -10,6 +10,25 @@
|
|
|
10
10
|
* all as one revision.
|
|
11
11
|
*/
|
|
12
12
|
import type { DrawerField, DrawerView } from "../../tui/drawer.ts";
|
|
13
|
+
import {
|
|
14
|
+
KEY_BACKSPACE,
|
|
15
|
+
KEY_BACKTAB,
|
|
16
|
+
KEY_COARSE_LEFT,
|
|
17
|
+
KEY_COARSE_RIGHT,
|
|
18
|
+
KEY_DOWN,
|
|
19
|
+
KEY_END,
|
|
20
|
+
KEY_ENTER,
|
|
21
|
+
KEY_FINE_LEFT,
|
|
22
|
+
KEY_FINE_RIGHT,
|
|
23
|
+
KEY_HOME,
|
|
24
|
+
KEY_LEFT,
|
|
25
|
+
KEY_PAGE_DOWN,
|
|
26
|
+
KEY_PAGE_UP,
|
|
27
|
+
KEY_RESET,
|
|
28
|
+
KEY_RIGHT,
|
|
29
|
+
KEY_TAB,
|
|
30
|
+
KEY_UP,
|
|
31
|
+
} from "../../tui/grammar.ts";
|
|
13
32
|
|
|
14
33
|
export type FaderNumber = Readonly<{
|
|
15
34
|
kind: "number";
|
|
@@ -22,7 +41,7 @@ export type FaderNumber = Readonly<{
|
|
|
22
41
|
command: (value: number) => string;
|
|
23
42
|
/** Typed text → command; undefined when it does not parse or fit. */
|
|
24
43
|
parse: (text: string) => string | undefined;
|
|
25
|
-
/** The command `
|
|
44
|
+
/** The command `x` / `d` / Delete runs to put the value back. */
|
|
26
45
|
reset?: string | undefined;
|
|
27
46
|
/** Starting value for a nudge while off. */
|
|
28
47
|
start?: number | undefined;
|
|
@@ -44,6 +63,8 @@ export interface FaderState {
|
|
|
44
63
|
label: string;
|
|
45
64
|
/** Digits typed for the focused field. */
|
|
46
65
|
typing?: string | undefined;
|
|
66
|
+
/** The field that just snapped onto a detent: flashes for one frame. */
|
|
67
|
+
flash?: string | undefined;
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
export type FaderResult =
|
|
@@ -111,6 +132,85 @@ function snap(spec: FaderNumber, raw: number): number {
|
|
|
111
132
|
return Math.min(spec.max, Math.max(spec.min, best));
|
|
112
133
|
}
|
|
113
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Detents: values a fader catches on its way past, so the common resting
|
|
137
|
+
* points are easy to hit by feel. Volume 1 (0 dB), pan 0 (center), whole
|
|
138
|
+
* BPM, mix 0 / 0.5 / 1 and filter cutoffs on the octaves of A (27.5 Hz,
|
|
139
|
+
* 55 Hz … 14080 Hz). Read off the field's command, so every row that runs
|
|
140
|
+
* `volume`, `pan`, `tempo`, `… mix` or a cutoff gets them.
|
|
141
|
+
*/
|
|
142
|
+
export type Detents = readonly number[] | "integer";
|
|
143
|
+
|
|
144
|
+
/** How close (as a share of the bar) a value must be to catch a detent. */
|
|
145
|
+
export const DETENT_REACH = 0.02;
|
|
146
|
+
|
|
147
|
+
const A_OCTAVES = Object.freeze(
|
|
148
|
+
Array.from({ length: 10 }, (_, octave) => 27.5 * 2 ** octave),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
export function detentsFor(spec: FaderNumber): Detents | undefined {
|
|
152
|
+
const probe = spec.value ?? spec.start ?? spec.min;
|
|
153
|
+
const command = spec.command(probe).replace(/\s+\S+$/, "");
|
|
154
|
+
const words = command.split(/\s+/);
|
|
155
|
+
const verb = words[0]?.replace(/^\//, "");
|
|
156
|
+
if (verb === "tempo" && words.length === 1) return "integer";
|
|
157
|
+
if (words.includes("volume")) return [1];
|
|
158
|
+
if (words.includes("pan")) return [0];
|
|
159
|
+
if (words.at(-1) === "mix") return [0, 0.5, 1];
|
|
160
|
+
if (/^(cutoff|lpf|hpf)$/.test(words.at(-1) ?? "")) return A_OCTAVES;
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The detents of `spec` that fall inside its range. */
|
|
165
|
+
function detentList(spec: FaderNumber, detents: Detents): number[] {
|
|
166
|
+
if (detents === "integer") return [];
|
|
167
|
+
return detents.filter((d) => d >= spec.min - 1e-9 && d <= spec.max + 1e-9);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `value` caught by a detent, or undefined when none is in reach. With
|
|
172
|
+
* `from` (a key step) a detent also catches a step that jumps over it or
|
|
173
|
+
* lands near it while moving toward it, so leaving a detent never sticks.
|
|
174
|
+
*/
|
|
175
|
+
export function detentSnap(
|
|
176
|
+
spec: FaderNumber,
|
|
177
|
+
value: number,
|
|
178
|
+
from?: number,
|
|
179
|
+
): number | undefined {
|
|
180
|
+
const detents = detentsFor(spec);
|
|
181
|
+
if (!detents) return undefined;
|
|
182
|
+
if (detents === "integer") {
|
|
183
|
+
const whole = Math.min(spec.max, Math.max(spec.min, Math.round(value)));
|
|
184
|
+
return Math.abs(whole - value) < 1e-9 ? undefined : whole;
|
|
185
|
+
}
|
|
186
|
+
const at = (v: number) => faderPosition(spec, v);
|
|
187
|
+
let best: number | undefined;
|
|
188
|
+
for (const detent of detentList(spec, detents)) {
|
|
189
|
+
// Landing on a detent exactly counts as a catch (it flashes) unless
|
|
190
|
+
// the value was already there.
|
|
191
|
+
if (Math.abs(detent - value) < 1e-9)
|
|
192
|
+
return from !== undefined && Math.abs(detent - from) > 1e-9
|
|
193
|
+
? detent
|
|
194
|
+
: undefined;
|
|
195
|
+
const near = Math.abs(at(value) - at(detent)) <= DETENT_REACH;
|
|
196
|
+
let caught = near;
|
|
197
|
+
if (from !== undefined) {
|
|
198
|
+
const toward = Math.sign(detent - from) === Math.sign(value - from);
|
|
199
|
+
const crossed =
|
|
200
|
+
(from < detent && detent < value) || (value < detent && detent < from);
|
|
201
|
+
caught = Math.abs(detent - from) > 1e-9 && (crossed || (near && toward));
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
caught &&
|
|
205
|
+
(best === undefined ||
|
|
206
|
+
Math.abs(at(detent) - at(from ?? value)) <
|
|
207
|
+
Math.abs(at(best) - at(from ?? value)))
|
|
208
|
+
)
|
|
209
|
+
best = detent;
|
|
210
|
+
}
|
|
211
|
+
return best;
|
|
212
|
+
}
|
|
213
|
+
|
|
114
214
|
/** Fine (a tenth of a step), normal, coarse (five steps) or page (twenty). */
|
|
115
215
|
export type StepSize = "fine" | "normal" | "coarse" | "page";
|
|
116
216
|
|
|
@@ -142,20 +242,9 @@ export function stepValue(
|
|
|
142
242
|
return value;
|
|
143
243
|
}
|
|
144
244
|
|
|
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"]);
|
|
245
|
+
/** ↑ and shift-tab: the previous param; ↓ and tab: the next. */
|
|
246
|
+
const isUp = (value: string) => KEY_UP.has(value) || KEY_BACKTAB.has(value);
|
|
247
|
+
const isDown = (value: string) => KEY_DOWN.has(value) || KEY_TAB.has(value);
|
|
159
248
|
|
|
160
249
|
export interface FaderKeyOptions {
|
|
161
250
|
/** Edits are staged (Enter keeps, Esc reverts). */
|
|
@@ -185,6 +274,32 @@ function setNumber(spec: FaderNumber, value: number): FaderResult {
|
|
|
185
274
|
};
|
|
186
275
|
}
|
|
187
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Set `value`, caught by a detent unless the step is fine. `from` is the
|
|
279
|
+
* value a key step left (undefined for a drag or a click on the bar).
|
|
280
|
+
*/
|
|
281
|
+
function setSnapped(
|
|
282
|
+
state: FaderState,
|
|
283
|
+
spec: FaderNumber,
|
|
284
|
+
value: number,
|
|
285
|
+
from: number | undefined,
|
|
286
|
+
size: StepSize | "drag",
|
|
287
|
+
): FaderResult {
|
|
288
|
+
const snapped =
|
|
289
|
+
size === "fine"
|
|
290
|
+
? undefined
|
|
291
|
+
: size === "drag"
|
|
292
|
+
? (detentSnap(spec, value) ??
|
|
293
|
+
(spec.value !== undefined &&
|
|
294
|
+
detentSnap(spec, value, spec.value) === value
|
|
295
|
+
? value
|
|
296
|
+
: undefined))
|
|
297
|
+
: detentSnap(spec, value, from);
|
|
298
|
+
const result = setNumber(spec, snapped ?? value);
|
|
299
|
+
if (snapped !== undefined && result.type === "set") state.flash = spec.label;
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
188
303
|
function setChoice(spec: FaderChoice, index: number): FaderResult {
|
|
189
304
|
const option =
|
|
190
305
|
spec.options[Math.min(spec.options.length - 1, Math.max(0, index))];
|
|
@@ -215,12 +330,12 @@ export function faderKeyPress(
|
|
|
215
330
|
state.typing = undefined;
|
|
216
331
|
return { type: "handled" };
|
|
217
332
|
}
|
|
218
|
-
if (
|
|
333
|
+
if (KEY_BACKSPACE.has(value)) {
|
|
219
334
|
state.typing = state.typing.slice(0, -1);
|
|
220
335
|
if (!state.typing) state.typing = undefined;
|
|
221
336
|
return { type: "handled" };
|
|
222
337
|
}
|
|
223
|
-
if (
|
|
338
|
+
if (KEY_ENTER.has(value) || KEY_TAB.has(value)) {
|
|
224
339
|
const text = state.typing;
|
|
225
340
|
state.typing = undefined;
|
|
226
341
|
const command = field.kind === "number" ? field.parse(text) : undefined;
|
|
@@ -236,9 +351,9 @@ export function faderKeyPress(
|
|
|
236
351
|
}
|
|
237
352
|
if (value === "\u001b")
|
|
238
353
|
return options.dirty ? { type: "revert" } : { type: "close" };
|
|
239
|
-
if (
|
|
354
|
+
if (KEY_ENTER.has(value))
|
|
240
355
|
return options.dirty ? { type: "keep" } : { type: "close" };
|
|
241
|
-
const move =
|
|
356
|
+
const move = isUp(value) ? -1 : isDown(value) ? 1 : 0;
|
|
242
357
|
if (move) {
|
|
243
358
|
state.label = fields[(index + move + fields.length) % fields.length]!.label;
|
|
244
359
|
return { type: "handled" };
|
|
@@ -250,30 +365,46 @@ export function faderKeyPress(
|
|
|
250
365
|
}
|
|
251
366
|
if (field.kind === "choice") {
|
|
252
367
|
const at = Math.max(0, field.options.indexOf(field.value));
|
|
253
|
-
if (
|
|
368
|
+
if (
|
|
369
|
+
KEY_LEFT.has(value) ||
|
|
370
|
+
KEY_FINE_LEFT.has(value) ||
|
|
371
|
+
KEY_COARSE_LEFT.has(value)
|
|
372
|
+
)
|
|
254
373
|
return setChoice(field, at - 1);
|
|
255
|
-
if (
|
|
374
|
+
if (
|
|
375
|
+
KEY_RIGHT.has(value) ||
|
|
376
|
+
KEY_FINE_RIGHT.has(value) ||
|
|
377
|
+
KEY_COARSE_RIGHT.has(value)
|
|
378
|
+
)
|
|
256
379
|
return setChoice(field, at + 1);
|
|
257
|
-
if (
|
|
258
|
-
|
|
380
|
+
if (KEY_HOME.has(value) || KEY_PAGE_DOWN.has(value))
|
|
381
|
+
return setChoice(field, 0);
|
|
382
|
+
if (KEY_END.has(value) || KEY_PAGE_UP.has(value))
|
|
259
383
|
return setChoice(field, field.options.length - 1);
|
|
260
384
|
// 1…9 picks an option by number.
|
|
261
385
|
if (/^[1-9]$/.test(value)) return setChoice(field, Number(value) - 1);
|
|
262
386
|
return { type: "handled" };
|
|
263
387
|
}
|
|
264
388
|
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
|
|
389
|
+
setSnapped(
|
|
390
|
+
state,
|
|
391
|
+
field,
|
|
392
|
+
stepValue(field, direction, size),
|
|
393
|
+
field.value,
|
|
394
|
+
size,
|
|
395
|
+
);
|
|
396
|
+
if (KEY_LEFT.has(value)) return step(-1, "normal");
|
|
397
|
+
if (KEY_RIGHT.has(value)) return step(1, "normal");
|
|
398
|
+
if (KEY_COARSE_LEFT.has(value)) return step(-1, "coarse");
|
|
399
|
+
if (KEY_COARSE_RIGHT.has(value)) return step(1, "coarse");
|
|
400
|
+
if (KEY_FINE_LEFT.has(value)) return step(-1, "fine");
|
|
401
|
+
if (KEY_FINE_RIGHT.has(value)) return step(1, "fine");
|
|
402
|
+
if (KEY_PAGE_UP.has(value)) return step(1, "page");
|
|
403
|
+
if (KEY_PAGE_DOWN.has(value)) return step(-1, "page");
|
|
404
|
+
if (KEY_HOME.has(value)) return setNumber(field, field.min);
|
|
405
|
+
if (KEY_END.has(value)) return setNumber(field, field.max);
|
|
406
|
+
// x, d and Delete put the value back; 0-9 and . start typing.
|
|
407
|
+
if (KEY_RESET.has(value)) {
|
|
277
408
|
const reset =
|
|
278
409
|
field.reset ??
|
|
279
410
|
(field.start !== undefined ? field.command(field.start) : undefined);
|
|
@@ -281,7 +412,7 @@ export function faderKeyPress(
|
|
|
281
412
|
? { type: "set", command: reset, key: faderKey(field.label) }
|
|
282
413
|
: { type: "handled" };
|
|
283
414
|
}
|
|
284
|
-
if (/^[
|
|
415
|
+
if (/^[0-9.]$/.test(value)) {
|
|
285
416
|
state.typing = value === "." ? "0." : value;
|
|
286
417
|
return { type: "handled" };
|
|
287
418
|
}
|
|
@@ -302,7 +433,13 @@ export function faderSetPosition(
|
|
|
302
433
|
field,
|
|
303
434
|
Math.round(position * Math.max(0, field.options.length - 1)),
|
|
304
435
|
);
|
|
305
|
-
return
|
|
436
|
+
return setSnapped(
|
|
437
|
+
state,
|
|
438
|
+
field,
|
|
439
|
+
faderValueAt(field, position),
|
|
440
|
+
undefined,
|
|
441
|
+
"drag",
|
|
442
|
+
);
|
|
306
443
|
}
|
|
307
444
|
|
|
308
445
|
/** A [−] / [+] click or a wheel notch on a field. */
|
|
@@ -319,7 +456,13 @@ export function faderStep(
|
|
|
319
456
|
const at = Math.max(0, field.options.indexOf(field.value));
|
|
320
457
|
return setChoice(field, at + direction);
|
|
321
458
|
}
|
|
322
|
-
return
|
|
459
|
+
return setSnapped(
|
|
460
|
+
state,
|
|
461
|
+
field,
|
|
462
|
+
stepValue(field, direction, size),
|
|
463
|
+
field.value,
|
|
464
|
+
size,
|
|
465
|
+
);
|
|
323
466
|
}
|
|
324
467
|
|
|
325
468
|
/** A click on one option of a choice field. */
|
|
@@ -334,8 +477,25 @@ export function faderChoose(
|
|
|
334
477
|
return setChoice(field, option);
|
|
335
478
|
}
|
|
336
479
|
|
|
337
|
-
|
|
338
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Most important first: fitHint trims the middle from the right, so at 80
|
|
482
|
+
* columns the coarse and fine steps go before `enter keep` or `x reset`.
|
|
483
|
+
*/
|
|
484
|
+
export const FADER_HINT =
|
|
485
|
+
"←→ adjust · enter keep · x reset · 0-9 type · ↑↓ param · ⇧←→ coarse · [ ] fine · esc revert";
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The hint for a field the loop cannot stage (tempo, meter, loop length):
|
|
489
|
+
* each step is a new revision, so there is nothing to keep or revert.
|
|
490
|
+
*/
|
|
491
|
+
export const FADER_HINT_AT_ONCE =
|
|
492
|
+
"←→ adjust · applies at once · x reset · 0-9 type · ↑↓ param · ⇧←→ coarse · [ ] fine · esc back";
|
|
493
|
+
|
|
494
|
+
/** The value a field's next step starts from (for checking what it runs). */
|
|
495
|
+
export function faderCommand(field: FaderSpec): string {
|
|
496
|
+
if (field.kind === "choice") return field.command(field.value);
|
|
497
|
+
return field.command(field.value ?? field.start ?? field.min);
|
|
498
|
+
}
|
|
339
499
|
|
|
340
500
|
/** The drawer's paint model for `fields` (staged) beside `committed`. */
|
|
341
501
|
export function drawerView(
|
|
@@ -345,11 +505,18 @@ export function drawerView(
|
|
|
345
505
|
options: {
|
|
346
506
|
title: string;
|
|
347
507
|
dirty: boolean;
|
|
508
|
+
/** How many edits are staged (defaults to 1 while dirty). */
|
|
509
|
+
staged?: number | undefined;
|
|
348
510
|
status?: string | undefined;
|
|
349
511
|
hint?: string;
|
|
512
|
+
/** The focused field applies at once (cannot stage on the loop). */
|
|
513
|
+
atOnce?: boolean | undefined;
|
|
350
514
|
},
|
|
351
515
|
): DrawerView {
|
|
352
516
|
const before = new Map(committed.map((field) => [field.label, field]));
|
|
517
|
+
// A detent flash lasts one view: consume it here.
|
|
518
|
+
const flash = state.flash;
|
|
519
|
+
state.flash = undefined;
|
|
353
520
|
const drawn: DrawerField[] = fields.map((field) => {
|
|
354
521
|
const was = before.get(field.label);
|
|
355
522
|
if (field.kind === "choice") {
|
|
@@ -395,15 +562,35 @@ export function drawerView(
|
|
|
395
562
|
: undefined,
|
|
396
563
|
minText: field.format(field.min),
|
|
397
564
|
maxText: field.format(field.max),
|
|
565
|
+
...(flash === field.label && field.value !== undefined
|
|
566
|
+
? { flash: detentLabel(field, field.value) }
|
|
567
|
+
: {}),
|
|
398
568
|
};
|
|
399
569
|
});
|
|
570
|
+
const staged = options.staged ?? (options.dirty ? 1 : 0);
|
|
400
571
|
return {
|
|
401
572
|
title: options.title,
|
|
402
573
|
fields: drawn,
|
|
403
574
|
focus: focusIndex(state, fields),
|
|
404
575
|
typing: state.typing,
|
|
405
576
|
dirty: options.dirty,
|
|
577
|
+
badge: options.dirty ? stagedBadge(staged) : undefined,
|
|
406
578
|
status: options.status,
|
|
407
|
-
hint: options.hint ??
|
|
579
|
+
hint: options.hint ?? (options.atOnce ? FADER_HINT_AT_ONCE : FADER_HINT),
|
|
408
580
|
};
|
|
409
581
|
}
|
|
582
|
+
|
|
583
|
+
/** `A/B: 1 change staged · enter keep · esc revert`. */
|
|
584
|
+
export function stagedBadge(count: number): string {
|
|
585
|
+
const n = Math.max(1, count);
|
|
586
|
+
return `A/B: ${n} change${n === 1 ? "" : "s"} staged · enter keep · esc revert`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** The faint word a detent flashes: `0 dB`, `center`, or the value. */
|
|
590
|
+
export function detentLabel(spec: FaderNumber, value: number): string {
|
|
591
|
+
const text = spec.format(value);
|
|
592
|
+
// Volume formats as `1 · 0.0 dB`; the flash names the detent itself.
|
|
593
|
+
const db = /(-?[\d.]+) dB$/.exec(text);
|
|
594
|
+
if (db && Number(db[1]) === 0) return "0 dB";
|
|
595
|
+
return text;
|
|
596
|
+
}
|
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",
|