@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/tui/drawer.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* habit). The piano roll stays visible above it while the terminal is tall
|
|
6
6
|
* enough.
|
|
7
7
|
*
|
|
8
|
-
* ╭
|
|
8
|
+
* ╭ Effects › filter ─── A/B: 1 change staged · enter keep · esc revert ╮
|
|
9
9
|
* │ › cutoff 1200 Hz ← 800 Hz 20 … 20000 Hz │
|
|
10
10
|
* │ [−] ━━━━━━━━━━━━━━━━━━●────────────────────────────────── [+] │
|
|
11
11
|
* │ type lowpass │ highpass │ bandpass │ notch │
|
|
@@ -24,6 +24,7 @@ import type { HitMap } from "./hits.ts";
|
|
|
24
24
|
import { onBackground, type Style, type Theme } from "./theme.ts";
|
|
25
25
|
import { displayWidth, truncate } from "./text.ts";
|
|
26
26
|
import { asciiHint, fitHint } from "./grammar.ts";
|
|
27
|
+
import { knobGlyph, knobStyle, type KnobIndex } from "./knobs.ts";
|
|
27
28
|
|
|
28
29
|
export type DrawerField =
|
|
29
30
|
| Readonly<{
|
|
@@ -39,6 +40,12 @@ export type DrawerField =
|
|
|
39
40
|
committedPosition?: number | undefined;
|
|
40
41
|
minText: string;
|
|
41
42
|
maxText: string;
|
|
43
|
+
/** Just caught by a detent: the bar brightens for one frame. */
|
|
44
|
+
flash?: string | undefined;
|
|
45
|
+
/** Other panes with this parameter open (`B C`), dim at the right. */
|
|
46
|
+
peers?: string | undefined;
|
|
47
|
+
/** The knob this row is (front page): its glyph and colour. */
|
|
48
|
+
knob?: KnobIndex | undefined;
|
|
42
49
|
}>
|
|
43
50
|
| Readonly<{
|
|
44
51
|
kind: "choice";
|
|
@@ -46,6 +53,8 @@ export type DrawerField =
|
|
|
46
53
|
options: readonly string[];
|
|
47
54
|
index: number;
|
|
48
55
|
committedIndex?: number | undefined;
|
|
56
|
+
peers?: string | undefined;
|
|
57
|
+
knob?: KnobIndex | undefined;
|
|
49
58
|
}>;
|
|
50
59
|
|
|
51
60
|
export type DrawerView = Readonly<{
|
|
@@ -56,11 +65,40 @@ export type DrawerView = Readonly<{
|
|
|
56
65
|
typing?: string | undefined;
|
|
57
66
|
/** Edits differ from the committed score. */
|
|
58
67
|
dirty: boolean;
|
|
68
|
+
/** `A/B: 1 change staged · enter keep · esc revert` while dirty. */
|
|
69
|
+
badge?: string | undefined;
|
|
59
70
|
/** `♪ solo · B` while the audition loop plays. */
|
|
60
71
|
status?: string | undefined;
|
|
61
72
|
hint: string;
|
|
73
|
+
/**
|
|
74
|
+
* Every param on the page behind Tab, while the drawer shows the four
|
|
75
|
+
* knobs: a wide drawer lists them beside the knobs (`DRAWER_MEASURE`).
|
|
76
|
+
*/
|
|
77
|
+
aside?: readonly DrawerAside[] | undefined;
|
|
62
78
|
}>;
|
|
63
79
|
|
|
80
|
+
/** One row of the drawer's side list: a param and its value. */
|
|
81
|
+
export type DrawerAside = Readonly<{
|
|
82
|
+
label: string;
|
|
83
|
+
text: string;
|
|
84
|
+
knob?: KnobIndex | undefined;
|
|
85
|
+
}>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The widest a drawer row (label, value, bar, range) runs: a fader bar past
|
|
89
|
+
* this is no easier to read or aim. Room right of it lists the page's other
|
|
90
|
+
* params when the view has an aside, else stays panel.
|
|
91
|
+
*/
|
|
92
|
+
export const DRAWER_MEASURE = 96;
|
|
93
|
+
/** The fewest columns the side list needs beside the rows. */
|
|
94
|
+
export const ASIDE_MIN_WIDTH = 28;
|
|
95
|
+
|
|
96
|
+
/** Columns the side list gets in a drawer `width` wide (0: none). */
|
|
97
|
+
export function asideWidth(width: number): number {
|
|
98
|
+
const room = width - 4 - DRAWER_MEASURE - 3;
|
|
99
|
+
return room >= ASIDE_MIN_WIDTH ? Math.min(room, 48) : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
export type DrawerMode = "full" | "compact" | "line";
|
|
65
103
|
|
|
66
104
|
export interface DrawerLayout {
|
|
@@ -85,12 +123,20 @@ export function keepAbove(regionHeight: number): number {
|
|
|
85
123
|
* its bottom, as tall as its fields need within the budget.
|
|
86
124
|
*/
|
|
87
125
|
export function drawerLayout(
|
|
88
|
-
view: Pick<DrawerView, "fields" | "focus"
|
|
126
|
+
view: Pick<DrawerView, "fields" | "focus"> &
|
|
127
|
+
Partial<Pick<DrawerView, "aside">>,
|
|
89
128
|
region: { y: number; height: number },
|
|
129
|
+
width = 0,
|
|
90
130
|
): DrawerLayout {
|
|
91
131
|
const total = Math.max(1, view.fields.length);
|
|
92
132
|
const bottom = region.y + region.height;
|
|
93
133
|
const budget = region.height - keepAbove(region.height);
|
|
134
|
+
// A side list beside the knobs may make a full drawer taller, never
|
|
135
|
+
// past half the region.
|
|
136
|
+
const aside =
|
|
137
|
+
view.aside && asideWidth(width) > 0
|
|
138
|
+
? Math.min(view.aside.length + 1, Math.floor(region.height / 2) - 2)
|
|
139
|
+
: 0;
|
|
94
140
|
const window = (rows: number): { first: number; count: number } => {
|
|
95
141
|
const count = Math.max(1, Math.min(total, rows));
|
|
96
142
|
const focus = Math.min(Math.max(0, view.focus), total - 1);
|
|
@@ -98,7 +144,7 @@ export function drawerLayout(
|
|
|
98
144
|
return { first, count };
|
|
99
145
|
};
|
|
100
146
|
if (2 + 2 * total <= Math.floor(region.height / 2)) {
|
|
101
|
-
const height = 2 + 2 * total;
|
|
147
|
+
const height = 2 + Math.max(2 * total, aside);
|
|
102
148
|
return {
|
|
103
149
|
mode: "full",
|
|
104
150
|
top: bottom - height,
|
|
@@ -175,8 +221,13 @@ const ASCII: Glyphs = {
|
|
|
175
221
|
export interface DrawerPaint {
|
|
176
222
|
theme: Theme;
|
|
177
223
|
unicode: boolean;
|
|
224
|
+
/** /motion off: detents still snap, the flash is not drawn. */
|
|
225
|
+
reducedMotion?: boolean;
|
|
178
226
|
}
|
|
179
227
|
|
|
228
|
+
/** Views whose detent flash has been drawn: a flash lasts one frame. */
|
|
229
|
+
const flashed = new WeakSet<DrawerView>();
|
|
230
|
+
|
|
180
231
|
/** Paint the drawer into the highway region; returns its layout. */
|
|
181
232
|
export function paintDrawer(
|
|
182
233
|
buffer: CellBuffer,
|
|
@@ -188,7 +239,10 @@ export function paintDrawer(
|
|
|
188
239
|
): DrawerLayout | undefined {
|
|
189
240
|
if (region.height <= 0 || width < 16 || view.fields.length === 0)
|
|
190
241
|
return undefined;
|
|
191
|
-
const layout = drawerLayout(view, region);
|
|
242
|
+
const layout = drawerLayout(view, region, width);
|
|
243
|
+
const flashing = !options.reducedMotion && !flashed.has(view);
|
|
244
|
+
if (view.fields.some((field) => field.kind === "number" && field.flash))
|
|
245
|
+
flashed.add(view);
|
|
192
246
|
const roles = options.theme.roles;
|
|
193
247
|
const glyphs = options.unicode ? UNICODE : ASCII;
|
|
194
248
|
const panel = roles.panel;
|
|
@@ -212,37 +266,58 @@ export function paintDrawer(
|
|
|
212
266
|
buffer.set(width - 1, y, box.v, border);
|
|
213
267
|
}
|
|
214
268
|
inner = { left: 2, right: width - 2 };
|
|
215
|
-
// Title left;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
269
|
+
// Title left; the status and the staged badge right. The badge's
|
|
270
|
+
// `enter keep` and `esc revert` are click targets; when room is short
|
|
271
|
+
// the breadcrumb truncates first, then the badge drops its key words.
|
|
272
|
+
const room = width - 6;
|
|
273
|
+
const full = view.badge ?? (view.dirty ? "A/B: changes staged" : "");
|
|
274
|
+
// The badge already counts staged changes; the audition status's
|
|
275
|
+
// `B staged N` would say it twice.
|
|
276
|
+
const status = (view.status ?? "")
|
|
277
|
+
.split(" · ")
|
|
278
|
+
.filter((part) => !(full && /^B staged \d+$/.test(part)))
|
|
279
|
+
.join(" · ");
|
|
280
|
+
const short = full.replace(/ · enter keep · esc revert$/, "");
|
|
281
|
+
const join = (badge: string) => [status, badge].filter(Boolean).join(" · ");
|
|
282
|
+
const minTitle = Math.min(displayWidth(view.title), 12) + 2;
|
|
283
|
+
let right = join(full);
|
|
284
|
+
if (displayWidth(right) + 3 + minTitle > room) right = join(short);
|
|
285
|
+
if (displayWidth(right) + 3 + minTitle > room) right = short;
|
|
286
|
+
if (displayWidth(right) + 3 + minTitle > room) right = "";
|
|
222
287
|
const rightWidth = displayWidth(right);
|
|
223
|
-
const titleRoom = Math.max(0,
|
|
288
|
+
const titleRoom = Math.max(0, room - (right ? rightWidth + 3 : 0));
|
|
224
289
|
buffer.text(
|
|
225
290
|
2,
|
|
226
291
|
top,
|
|
227
292
|
` ${truncate(view.title, Math.max(0, titleRoom - 2))} `,
|
|
228
293
|
on({ ...roles.text, bold: true }),
|
|
229
294
|
);
|
|
230
|
-
if (right
|
|
295
|
+
if (right) {
|
|
231
296
|
const rx = width - 3 - rightWidth;
|
|
232
297
|
buffer.text(
|
|
233
298
|
rx - 1,
|
|
234
299
|
top,
|
|
235
|
-
` ${
|
|
300
|
+
` ${right}`,
|
|
236
301
|
on(view.dirty ? roles.warning : roles.muted),
|
|
237
302
|
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
303
|
+
for (const [word, kind] of [
|
|
304
|
+
["enter keep", "fader-keep"],
|
|
305
|
+
["esc revert", "fader-revert"],
|
|
306
|
+
] as const) {
|
|
307
|
+
const at = right.lastIndexOf(word);
|
|
308
|
+
if (at < 0) continue;
|
|
309
|
+
const x = rx + displayWidth(right.slice(0, at));
|
|
310
|
+
buffer.text(
|
|
311
|
+
x,
|
|
312
|
+
top,
|
|
313
|
+
word,
|
|
314
|
+
on(
|
|
315
|
+
kind === "fader-keep"
|
|
316
|
+
? { ...roles.success, bold: true }
|
|
317
|
+
: roles.muted,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
hits?.add(x, top, word.length, 1, { kind });
|
|
246
321
|
}
|
|
247
322
|
}
|
|
248
323
|
const hint = fitHint(
|
|
@@ -257,6 +332,19 @@ export function paintDrawer(
|
|
|
257
332
|
on(roles.muted),
|
|
258
333
|
);
|
|
259
334
|
}
|
|
335
|
+
const outer = inner;
|
|
336
|
+
inner = {
|
|
337
|
+
left: inner.left,
|
|
338
|
+
right: Math.min(inner.right, inner.left + DRAWER_MEASURE),
|
|
339
|
+
};
|
|
340
|
+
if (layout.bordered && view.aside)
|
|
341
|
+
paintAside(buffer, layout, outer.right, view.aside, {
|
|
342
|
+
glyphs,
|
|
343
|
+
roles,
|
|
344
|
+
on,
|
|
345
|
+
theme: options.theme,
|
|
346
|
+
unicode: options.unicode,
|
|
347
|
+
});
|
|
260
348
|
const labelWidth = Math.min(
|
|
261
349
|
layout.mode === "full" ? 22 : 16,
|
|
262
350
|
Math.max(...view.fields.map((field) => displayWidth(field.label))),
|
|
@@ -275,12 +363,25 @@ export function paintDrawer(
|
|
|
275
363
|
const labelStyle = focused
|
|
276
364
|
? on({ ...roles.borderFocus, bold: true })
|
|
277
365
|
: on(roles.text);
|
|
278
|
-
|
|
366
|
+
if (field.knob !== undefined) {
|
|
367
|
+
// A knob row: its shape glyph in its colour, then the marker, so
|
|
368
|
+
// colour is never the only cue (NO_COLOR keeps the shape).
|
|
369
|
+
buffer.text(
|
|
370
|
+
inner.left,
|
|
371
|
+
y,
|
|
372
|
+
knobGlyph(field.knob, options.unicode),
|
|
373
|
+
on(knobStyle(field.knob, options.theme)),
|
|
374
|
+
);
|
|
375
|
+
buffer.text(inner.left + 1, y, focused ? glyphs.marker : " ", labelStyle);
|
|
376
|
+
} else buffer.text(inner.left, y, marker, labelStyle);
|
|
279
377
|
buffer.text(
|
|
280
378
|
inner.left + 2,
|
|
281
379
|
y,
|
|
282
380
|
truncate(field.label, labelWidth),
|
|
283
|
-
|
|
381
|
+
// The selected knob's label is reverse video (§7.2).
|
|
382
|
+
field.knob !== undefined && focused
|
|
383
|
+
? on({ ...roles.text, bold: true, reverse: true })
|
|
384
|
+
: labelStyle,
|
|
284
385
|
);
|
|
285
386
|
const valueX = inner.left + 2 + labelWidth + 2;
|
|
286
387
|
const typing = focused && view.typing !== undefined;
|
|
@@ -306,15 +407,21 @@ export function paintDrawer(
|
|
|
306
407
|
on(roles.muted),
|
|
307
408
|
inner.right - valueX - used,
|
|
308
409
|
);
|
|
309
|
-
|
|
410
|
+
// For the frame a detent catches, the range gives way to its name.
|
|
411
|
+
const range =
|
|
412
|
+
flashing && field.flash !== undefined
|
|
413
|
+
? field.flash
|
|
414
|
+
: `${field.minText} … ${field.maxText}`;
|
|
310
415
|
const rangeX = inner.right - displayWidth(range);
|
|
311
416
|
if (rangeX > valueX + used + displayWidth(committed) + 2)
|
|
312
417
|
buffer.text(rangeX, y, range, on(roles.faint));
|
|
313
418
|
paintBar(buffer, y + 1, inner.left + 2, inner.right, field, index, {
|
|
314
419
|
glyphs,
|
|
315
420
|
focused,
|
|
421
|
+
flashing,
|
|
316
422
|
roles,
|
|
317
423
|
on,
|
|
424
|
+
knob: knobFill(field, options.theme),
|
|
318
425
|
...(hits ? { hits } : {}),
|
|
319
426
|
});
|
|
320
427
|
} else {
|
|
@@ -335,7 +442,15 @@ export function paintDrawer(
|
|
|
335
442
|
inner.right,
|
|
336
443
|
field,
|
|
337
444
|
index,
|
|
338
|
-
{
|
|
445
|
+
{
|
|
446
|
+
glyphs,
|
|
447
|
+
focused,
|
|
448
|
+
flashing,
|
|
449
|
+
roles,
|
|
450
|
+
on,
|
|
451
|
+
knob: knobFill(field, options.theme),
|
|
452
|
+
...(hits ? { hits } : {}),
|
|
453
|
+
},
|
|
339
454
|
);
|
|
340
455
|
}
|
|
341
456
|
} else {
|
|
@@ -357,16 +472,97 @@ export function paintDrawer(
|
|
|
357
472
|
);
|
|
358
473
|
}
|
|
359
474
|
}
|
|
475
|
+
// Other panes with this parameter open: their letters, dim, at the
|
|
476
|
+
// row's right edge (design §12.7).
|
|
477
|
+
if (field.peers) {
|
|
478
|
+
const peers = ` ${field.peers}`;
|
|
479
|
+
buffer.text(
|
|
480
|
+
Math.max(valueX, inner.right - displayWidth(peers)),
|
|
481
|
+
y,
|
|
482
|
+
peers,
|
|
483
|
+
on(roles.muted),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
360
486
|
}
|
|
361
487
|
return layout;
|
|
362
488
|
}
|
|
363
489
|
|
|
490
|
+
/**
|
|
491
|
+
* `all params · tab` and every param on the page, one per row, beside the
|
|
492
|
+
* knobs: what Tab would show, without leaving the knobs.
|
|
493
|
+
*/
|
|
494
|
+
function paintAside(
|
|
495
|
+
buffer: CellBuffer,
|
|
496
|
+
layout: DrawerLayout,
|
|
497
|
+
right: number,
|
|
498
|
+
aside: readonly DrawerAside[],
|
|
499
|
+
paint: {
|
|
500
|
+
glyphs: Glyphs;
|
|
501
|
+
roles: Theme["roles"];
|
|
502
|
+
on: (style: Style) => Style;
|
|
503
|
+
theme: Theme;
|
|
504
|
+
unicode: boolean;
|
|
505
|
+
},
|
|
506
|
+
): void {
|
|
507
|
+
const width = asideWidth(right + 2);
|
|
508
|
+
if (width === 0) return;
|
|
509
|
+
const { roles, on } = paint;
|
|
510
|
+
const left = right - width;
|
|
511
|
+
const rows = layout.height - 2;
|
|
512
|
+
if (rows < 2) return;
|
|
513
|
+
const top = layout.top + 1;
|
|
514
|
+
const rule = paint.unicode ? "│" : "|";
|
|
515
|
+
for (let y = top; y < top + rows; y += 1)
|
|
516
|
+
buffer.set(left - 2, y, rule, on(roles.faint));
|
|
517
|
+
buffer.text(left, top, truncate("all params · tab", width), on(roles.muted));
|
|
518
|
+
const room = rows - 1;
|
|
519
|
+
const shown =
|
|
520
|
+
aside.length > room ? aside.slice(0, Math.max(0, room - 1)) : aside;
|
|
521
|
+
const labelWidth = Math.min(
|
|
522
|
+
Math.floor(width / 2),
|
|
523
|
+
Math.max(0, ...shown.map((row) => displayWidth(row.label))),
|
|
524
|
+
);
|
|
525
|
+
shown.forEach((row, index) => {
|
|
526
|
+
const y = top + 1 + index;
|
|
527
|
+
if (row.knob !== undefined)
|
|
528
|
+
buffer.text(
|
|
529
|
+
left,
|
|
530
|
+
y,
|
|
531
|
+
knobGlyph(row.knob, paint.unicode),
|
|
532
|
+
on(knobStyle(row.knob, paint.theme)),
|
|
533
|
+
);
|
|
534
|
+
buffer.text(left + 2, y, truncate(row.label, labelWidth), on(roles.text));
|
|
535
|
+
const valueX = left + 2 + labelWidth + 1;
|
|
536
|
+
buffer.text(
|
|
537
|
+
valueX,
|
|
538
|
+
y,
|
|
539
|
+
truncate(row.text, Math.max(0, right - valueX)),
|
|
540
|
+
on(roles.muted),
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
if (shown.length < aside.length)
|
|
544
|
+
buffer.text(
|
|
545
|
+
left + 2,
|
|
546
|
+
top + room,
|
|
547
|
+
truncate(`+${aside.length - shown.length} more · tab`, width - 2),
|
|
548
|
+
on(roles.faint),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function knobFill(field: DrawerField, theme: Theme): Style | undefined {
|
|
553
|
+
return field.knob === undefined ? undefined : knobStyle(field.knob, theme);
|
|
554
|
+
}
|
|
555
|
+
|
|
364
556
|
interface RowPaint {
|
|
365
557
|
glyphs: Glyphs;
|
|
366
558
|
focused: boolean;
|
|
559
|
+
/** Draw this field's detent flash (motion on, first frame only). */
|
|
560
|
+
flashing?: boolean;
|
|
367
561
|
roles: Theme["roles"];
|
|
368
562
|
on: (style: Style) => Style;
|
|
369
563
|
hits?: HitMap;
|
|
564
|
+
/** A knob row's bar fills in its knob colour. */
|
|
565
|
+
knob?: Style | undefined;
|
|
370
566
|
}
|
|
371
567
|
|
|
372
568
|
/** `[−] ━━━━━●─────── [+]` from `left` to `right`, with its hit regions. */
|
|
@@ -412,7 +608,17 @@ function paintBar(
|
|
|
412
608
|
field.committedPosition === undefined
|
|
413
609
|
? undefined
|
|
414
610
|
: at(field.committedPosition);
|
|
415
|
-
|
|
611
|
+
// A detent catch brightens the bar for one frame (motion on).
|
|
612
|
+
const flash = paint.flashing === true && field.flash !== undefined;
|
|
613
|
+
const filled = on(
|
|
614
|
+
flash
|
|
615
|
+
? { ...roles.success, bold: true }
|
|
616
|
+
: paint.knob
|
|
617
|
+
? paint.knob
|
|
618
|
+
: paint.focused
|
|
619
|
+
? roles.borderFocus
|
|
620
|
+
: roles.text,
|
|
621
|
+
);
|
|
416
622
|
const empty = on(roles.faint);
|
|
417
623
|
for (let column = 0; column < barWidth; column += 1) {
|
|
418
624
|
const x = barLeft + column;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the interactive loop builds a frame. The 30 fps timer used to build
|
|
3
|
+
* the whole view (score snapshot, layout, every panel) on every tick even
|
|
4
|
+
* with nothing moving, only for the differential writer to find no change.
|
|
5
|
+
* The gate builds a frame when something can have changed: a forced frame
|
|
6
|
+
* (input, resize, an edit landing), a change the caller reports (activity,
|
|
7
|
+
* a different score or status key), or a running animation (transport,
|
|
8
|
+
* play mode, an audition, a spinner, a card's glow). Otherwise it builds
|
|
9
|
+
* one heartbeat frame every `heartbeatMs`, which catches anything slow that
|
|
10
|
+
* nothing reported (a loudness reading, a card ageing out).
|
|
11
|
+
*/
|
|
12
|
+
export const IDLE_HEARTBEAT_MS = 500;
|
|
13
|
+
/**
|
|
14
|
+
* The shortest gap between two built frames in the interactive loop: a
|
|
15
|
+
* burst of forced frames (keystrokes, edits landing, agent events) is
|
|
16
|
+
* coalesced to at most about 30 a second. A deferred frame stays dirty, so
|
|
17
|
+
* the next 33 ms timer tick builds it; nothing is dropped.
|
|
18
|
+
*/
|
|
19
|
+
export const MIN_FRAME_GAP_MS = 33;
|
|
20
|
+
|
|
21
|
+
export class FrameGate {
|
|
22
|
+
private dirty = true;
|
|
23
|
+
private lastBuiltMs = Number.NEGATIVE_INFINITY;
|
|
24
|
+
private lastKeys: readonly unknown[] = [];
|
|
25
|
+
/** Frames built and ticks skipped, for tests and diagnostics. */
|
|
26
|
+
built = 0;
|
|
27
|
+
skipped = 0;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
readonly heartbeatMs = IDLE_HEARTBEAT_MS,
|
|
31
|
+
readonly minGapMs = 0,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
/** Something visible changed; the next tick builds. */
|
|
35
|
+
markDirty(): void {
|
|
36
|
+
this.dirty = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True when this tick should build a frame; counts it as built. Each of
|
|
41
|
+
* `keys` is compared by identity (a score object) or value (a status
|
|
42
|
+
* string) with the last built frame's: any difference builds.
|
|
43
|
+
*/
|
|
44
|
+
shouldBuild(options: {
|
|
45
|
+
nowMs: number;
|
|
46
|
+
force?: boolean;
|
|
47
|
+
animating?: boolean;
|
|
48
|
+
keys?: readonly unknown[];
|
|
49
|
+
}): boolean {
|
|
50
|
+
const keys = options.keys ?? [];
|
|
51
|
+
const keyChanged =
|
|
52
|
+
keys.length !== this.lastKeys.length ||
|
|
53
|
+
keys.some((key, index) => !Object.is(key, this.lastKeys[index]));
|
|
54
|
+
const build =
|
|
55
|
+
options.force === true ||
|
|
56
|
+
options.animating === true ||
|
|
57
|
+
this.dirty ||
|
|
58
|
+
keyChanged ||
|
|
59
|
+
options.nowMs - this.lastBuiltMs >= this.heartbeatMs;
|
|
60
|
+
if (!build) {
|
|
61
|
+
this.skipped += 1;
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (options.nowMs - this.lastBuiltMs < this.minGapMs) {
|
|
65
|
+
// Too soon after the last frame: build on the next tick instead.
|
|
66
|
+
this.dirty = true;
|
|
67
|
+
this.skipped += 1;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
this.dirty = false;
|
|
71
|
+
this.lastKeys = keys;
|
|
72
|
+
this.lastBuiltMs = options.nowMs;
|
|
73
|
+
this.built += 1;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|