@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/input.ts
CHANGED
|
@@ -7,16 +7,42 @@
|
|
|
7
7
|
export type TerminalInputEvent =
|
|
8
8
|
string | Readonly<{ type: "paste"; text: string }>;
|
|
9
9
|
|
|
10
|
+
/** A bracketed paste longer than this is emitted as it stands. */
|
|
11
|
+
export const MAX_PASTE_CHARS = 1 << 20;
|
|
12
|
+
|
|
13
|
+
/** A chunk marker the read loop yields when input went idle mid-sequence. */
|
|
14
|
+
export const INPUT_FLUSH = "\u0000flush";
|
|
15
|
+
|
|
16
|
+
/** Idle time after which a partial escape prefix is a key of its own. */
|
|
17
|
+
export const ESCAPE_FLUSH_MS = 50;
|
|
18
|
+
/** Idle time after which an unterminated bracketed paste is emitted. */
|
|
19
|
+
export const PASTE_FLUSH_MS = 1_000;
|
|
20
|
+
|
|
10
21
|
export class TerminalInputDecoder {
|
|
11
22
|
private buffer = "";
|
|
12
23
|
|
|
24
|
+
/**
|
|
25
|
+
* What the decoder holds back: `escape` for a partial escape sequence,
|
|
26
|
+
* `paste` for an unterminated bracketed paste. The caller flushes after
|
|
27
|
+
* `ESCAPE_FLUSH_MS` or `PASTE_FLUSH_MS` with no further input.
|
|
28
|
+
*/
|
|
29
|
+
pending(): "escape" | "paste" | undefined {
|
|
30
|
+
if (this.buffer.length === 0) return undefined;
|
|
31
|
+
return this.buffer.startsWith("\u001b[200~") ? "paste" : "escape";
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
push(chunk: string): TerminalInputEvent[] {
|
|
14
35
|
this.buffer += chunk;
|
|
15
36
|
const events: TerminalInputEvent[] = [];
|
|
16
37
|
while (this.buffer.length > 0) {
|
|
17
38
|
if (this.buffer.startsWith("\u001b[200~")) {
|
|
18
39
|
const end = this.buffer.indexOf("\u001b[201~", 6);
|
|
19
|
-
if (end < 0)
|
|
40
|
+
if (end < 0) {
|
|
41
|
+
if (this.buffer.length - 6 < MAX_PASTE_CHARS) break;
|
|
42
|
+
events.push({ type: "paste", text: this.buffer.slice(6) });
|
|
43
|
+
this.buffer = "";
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
20
46
|
events.push({ type: "paste", text: this.buffer.slice(6, end) });
|
|
21
47
|
this.buffer = this.buffer.slice(end + 6);
|
|
22
48
|
continue;
|
|
@@ -38,12 +64,27 @@ export class TerminalInputDecoder {
|
|
|
38
64
|
return events;
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Emits whatever is held back, once no more input is coming: a cut-off
|
|
69
|
+
* bracketed paste as a paste, a lone ESC as Esc, and a partial prefix such
|
|
70
|
+
* as `ESC [` (Alt+[) or `ESC O` (Alt+O) as one Alt key, then decodes the
|
|
71
|
+
* rest normally.
|
|
72
|
+
*/
|
|
41
73
|
flush(): TerminalInputEvent[] {
|
|
42
|
-
|
|
74
|
+
const events: TerminalInputEvent[] = [];
|
|
75
|
+
while (this.buffer.length > 0) {
|
|
76
|
+
if (this.buffer.startsWith("\u001b[200~")) {
|
|
77
|
+
events.push({ type: "paste", text: this.buffer.slice(6) });
|
|
78
|
+
this.buffer = "";
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const head =
|
|
82
|
+
this.buffer.length === 1 ? "\u001b" : this.buffer.slice(0, 2);
|
|
83
|
+
const rest = this.buffer.slice(head.length);
|
|
43
84
|
this.buffer = "";
|
|
44
|
-
|
|
85
|
+
events.push(head, ...this.push(rest));
|
|
45
86
|
}
|
|
46
|
-
return
|
|
87
|
+
return events;
|
|
47
88
|
}
|
|
48
89
|
|
|
49
90
|
private readEscapeSequence(): string | undefined {
|
|
@@ -59,12 +100,22 @@ export class TerminalInputDecoder {
|
|
|
59
100
|
return this.buffer.length >= 6 ? this.buffer.slice(0, 6) : undefined;
|
|
60
101
|
const final = this.buffer.match(/^\u001b\[[\x20-?]*[\x40-~]/)?.[0];
|
|
61
102
|
if (final !== undefined) return final;
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
103
|
+
// A byte that cannot continue a CSI ends it: the `ESC [` was Alt+[.
|
|
104
|
+
if (this.buffer.startsWith("\u001b[")) {
|
|
105
|
+
const partial = this.buffer.match(/^\u001b\[[\x20-?]*/)![0];
|
|
106
|
+
if (partial.length < this.buffer.length) return "\u001b[";
|
|
107
|
+
}
|
|
108
|
+
// SS3: arrows in application mode and F1-F4 (`ESC O P` is F1). A bare
|
|
109
|
+
// `ESC O` waits for its final byte, which a PTY may deliver separately;
|
|
110
|
+
// `flush` turns it into Alt+O if nothing follows.
|
|
111
|
+
if (this.buffer.startsWith("\u001bO")) {
|
|
112
|
+
if (this.buffer.length === 2) return undefined;
|
|
113
|
+
const ss3 = this.buffer.match(/^\u001bO[A-DHFPQRS]/)?.[0];
|
|
114
|
+
if (ss3 !== undefined) return ss3;
|
|
115
|
+
return "\u001bO";
|
|
116
|
+
}
|
|
65
117
|
// An unrecognised Alt-prefixed character is still one complete event.
|
|
66
|
-
if (this.buffer
|
|
67
|
-
return this.buffer.slice(0, 2);
|
|
118
|
+
if (this.buffer[1] !== "[") return this.buffer.slice(0, 2);
|
|
68
119
|
return undefined;
|
|
69
120
|
}
|
|
70
121
|
}
|
package/tui/keys.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The prompt is always focused, so printable keys always type. Control
|
|
5
5
|
* chords carry the global actions:
|
|
6
|
-
* Ctrl+C quit · Ctrl+Q toggle
|
|
6
|
+
* Ctrl+C quit · Ctrl+Q toggle NOW/NEXT · Ctrl+Z undo · Ctrl+Y redo
|
|
7
7
|
* Ctrl+O transcript overlay · Esc closes the overlay
|
|
8
8
|
*/
|
|
9
9
|
|
package/tui/knobs.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four knobs (design §7, the OP-1's four colour encoders adapted): one
|
|
3
|
+
* table names each knob's colour, shape glyph and meaning, and every screen
|
|
4
|
+
* that shows knobs (the fader drawer's front page, the mixer page, euclid,
|
|
5
|
+
* and later TAPE and PLAY) reads it, so a knob looks and means the same
|
|
6
|
+
* everywhere.
|
|
7
|
+
*
|
|
8
|
+
* ● blue moves position, selection, which
|
|
9
|
+
* ▲ green sizes length, range, amount of steps
|
|
10
|
+
* ■ white shapes tone, time, character
|
|
11
|
+
* ◆ orange level loudness, mix, velocity
|
|
12
|
+
*
|
|
13
|
+
* Colour is never the only cue: each knob has a shape glyph, which is all
|
|
14
|
+
* that is left under NO_COLOR and the mono theme.
|
|
15
|
+
*
|
|
16
|
+
* Keys, the same on every screen: ↑↓ picks a knob (skipping empty slots),
|
|
17
|
+
* ←→ turns it, ⇧←→ turns it coarse, x resets it, enter opens it big.
|
|
18
|
+
* Every turn is a typed command, so the receipt teaches the words.
|
|
19
|
+
*
|
|
20
|
+
* Pure: the reducer returns what to run; the caller runs it.
|
|
21
|
+
*/
|
|
22
|
+
import type { CellBuffer } from "./screen.ts";
|
|
23
|
+
import { displayWidth, truncate } from "./text.ts";
|
|
24
|
+
import type { SemanticRole, Style, Theme } from "./theme.ts";
|
|
25
|
+
import {
|
|
26
|
+
KEY_COARSE_LEFT,
|
|
27
|
+
KEY_COARSE_RIGHT,
|
|
28
|
+
KEY_DOWN,
|
|
29
|
+
KEY_ENTER,
|
|
30
|
+
KEY_LEFT,
|
|
31
|
+
KEY_RESET,
|
|
32
|
+
KEY_RIGHT,
|
|
33
|
+
KEY_UP,
|
|
34
|
+
} from "./grammar.ts";
|
|
35
|
+
|
|
36
|
+
export type KnobIndex = 0 | 1 | 2 | 3;
|
|
37
|
+
|
|
38
|
+
export type Knob = Readonly<{
|
|
39
|
+
index: KnobIndex;
|
|
40
|
+
colour: "blue" | "green" | "white" | "orange";
|
|
41
|
+
/** What this knob means on every screen. */
|
|
42
|
+
meaning: "moves" | "sizes" | "shapes" | "level";
|
|
43
|
+
role: Extract<SemanticRole, "knob1" | "knob2" | "knob3" | "knob4">;
|
|
44
|
+
/** Shape glyph (width 1), so colour is never the only cue. */
|
|
45
|
+
glyph: string;
|
|
46
|
+
/** The glyph where the terminal has no Unicode. */
|
|
47
|
+
ascii: string;
|
|
48
|
+
}>;
|
|
49
|
+
|
|
50
|
+
/** The one table: glyphs, colours and meanings of the four knobs. */
|
|
51
|
+
export const KNOBS: readonly [Knob, Knob, Knob, Knob] = Object.freeze([
|
|
52
|
+
{
|
|
53
|
+
index: 0,
|
|
54
|
+
colour: "blue",
|
|
55
|
+
meaning: "moves",
|
|
56
|
+
role: "knob1",
|
|
57
|
+
glyph: "●",
|
|
58
|
+
ascii: "o",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
index: 1,
|
|
62
|
+
colour: "green",
|
|
63
|
+
meaning: "sizes",
|
|
64
|
+
role: "knob2",
|
|
65
|
+
glyph: "▲",
|
|
66
|
+
ascii: "^",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
index: 2,
|
|
70
|
+
colour: "white",
|
|
71
|
+
meaning: "shapes",
|
|
72
|
+
role: "knob3",
|
|
73
|
+
glyph: "■",
|
|
74
|
+
ascii: "#",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
index: 3,
|
|
78
|
+
colour: "orange",
|
|
79
|
+
meaning: "level",
|
|
80
|
+
role: "knob4",
|
|
81
|
+
glyph: "◆",
|
|
82
|
+
ascii: "*",
|
|
83
|
+
},
|
|
84
|
+
] as const);
|
|
85
|
+
|
|
86
|
+
export function knobGlyph(index: KnobIndex, unicode: boolean): string {
|
|
87
|
+
const knob = KNOBS[index];
|
|
88
|
+
return unicode ? knob.glyph : knob.ascii;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function knobStyle(index: KnobIndex, theme: Theme): Style {
|
|
92
|
+
return theme.roles[KNOBS[index].role];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The footer hint of a knob page, the same words on every screen. */
|
|
96
|
+
export const KNOB_HINT =
|
|
97
|
+
"↑↓ knob · ←→ turn · ⇧ coarse · x reset · tab more · esc close";
|
|
98
|
+
|
|
99
|
+
/** One knob slot as a screen shows and turns it. */
|
|
100
|
+
export type KnobSlot = Readonly<{
|
|
101
|
+
label: string;
|
|
102
|
+
/** Formatted value: `1200 Hz`, `saw`, `−4.9 dB`. */
|
|
103
|
+
text: string;
|
|
104
|
+
/** 0…1 along the gauge; undefined while off or for a choice. */
|
|
105
|
+
position?: number | undefined;
|
|
106
|
+
/** The command one turn runs, or undefined at the end of the range. */
|
|
107
|
+
turn: (direction: 1 | -1, coarse: boolean) => string | undefined;
|
|
108
|
+
/** The command `x` runs, when the slot has a default. */
|
|
109
|
+
reset?: string | undefined;
|
|
110
|
+
}>;
|
|
111
|
+
|
|
112
|
+
/** Four slots; an undefined one draws as `·` and ↑↓ skips it. */
|
|
113
|
+
export type KnobSlots = readonly [
|
|
114
|
+
KnobSlot | undefined,
|
|
115
|
+
KnobSlot | undefined,
|
|
116
|
+
KnobSlot | undefined,
|
|
117
|
+
KnobSlot | undefined,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
export type KnobState = { selected: KnobIndex };
|
|
121
|
+
|
|
122
|
+
export type KnobResult =
|
|
123
|
+
| { type: "handled" }
|
|
124
|
+
| { type: "pass" }
|
|
125
|
+
| { type: "run"; command: string }
|
|
126
|
+
| { type: "open"; knob: KnobIndex };
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The next knob from `from` in `direction`, wrapping blue → green → white →
|
|
130
|
+
* orange and skipping empty slots; `from` itself when every slot is empty.
|
|
131
|
+
*/
|
|
132
|
+
export function nextKnob(
|
|
133
|
+
from: KnobIndex,
|
|
134
|
+
direction: 1 | -1,
|
|
135
|
+
present: (index: KnobIndex) => boolean,
|
|
136
|
+
): KnobIndex {
|
|
137
|
+
for (let step = 1; step <= 4; step += 1) {
|
|
138
|
+
const index = (((from + direction * step) % 4) + 4) % 4;
|
|
139
|
+
if (present(index as KnobIndex)) return index as KnobIndex;
|
|
140
|
+
}
|
|
141
|
+
return from;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The first non-empty slot, for opening on a page. */
|
|
145
|
+
export function firstKnob(slots: KnobSlots): KnobIndex {
|
|
146
|
+
const at = slots.findIndex(Boolean);
|
|
147
|
+
return (at < 0 ? 0 : at) as KnobIndex;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One key on a knob strip. */
|
|
151
|
+
export function knobKey(
|
|
152
|
+
state: KnobState,
|
|
153
|
+
slots: KnobSlots,
|
|
154
|
+
value: string,
|
|
155
|
+
): KnobResult {
|
|
156
|
+
if (!slots[state.selected]) state.selected = firstKnob(slots);
|
|
157
|
+
const up = KEY_UP.has(value);
|
|
158
|
+
if (up || KEY_DOWN.has(value)) {
|
|
159
|
+
state.selected = nextKnob(state.selected, up ? -1 : 1, (index) =>
|
|
160
|
+
Boolean(slots[index]),
|
|
161
|
+
);
|
|
162
|
+
return { type: "handled" };
|
|
163
|
+
}
|
|
164
|
+
const slot = slots[state.selected];
|
|
165
|
+
if (!slot) return { type: "pass" };
|
|
166
|
+
const coarseLeft = KEY_COARSE_LEFT.has(value);
|
|
167
|
+
const coarseRight = KEY_COARSE_RIGHT.has(value);
|
|
168
|
+
if (
|
|
169
|
+
coarseLeft ||
|
|
170
|
+
coarseRight ||
|
|
171
|
+
KEY_LEFT.has(value) ||
|
|
172
|
+
KEY_RIGHT.has(value)
|
|
173
|
+
) {
|
|
174
|
+
const direction = coarseRight || KEY_RIGHT.has(value) ? 1 : -1;
|
|
175
|
+
const command = slot.turn(direction, coarseLeft || coarseRight);
|
|
176
|
+
return command ? { type: "run", command } : { type: "handled" };
|
|
177
|
+
}
|
|
178
|
+
if (KEY_RESET.has(value))
|
|
179
|
+
return slot.reset
|
|
180
|
+
? { type: "run", command: slot.reset }
|
|
181
|
+
: { type: "handled" };
|
|
182
|
+
if (KEY_ENTER.has(value)) return { type: "open", knob: state.selected };
|
|
183
|
+
return { type: "pass" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `●›cutoff 1.2k ▲ q 0.7 ■ type lowpass ◆ mix 0.3`: the strip as plain
|
|
188
|
+
* text, for pickers and receipts. The selected knob carries `›` (`>`).
|
|
189
|
+
*/
|
|
190
|
+
export function knobStripText(
|
|
191
|
+
slots: KnobSlots,
|
|
192
|
+
selected: KnobIndex,
|
|
193
|
+
unicode: boolean,
|
|
194
|
+
): string {
|
|
195
|
+
return slots
|
|
196
|
+
.map((slot, index) => {
|
|
197
|
+
const knob = index as KnobIndex;
|
|
198
|
+
const mark = knob === selected ? (unicode ? "›" : ">") : " ";
|
|
199
|
+
const glyph = knobGlyph(knob, unicode);
|
|
200
|
+
return slot ? `${glyph}${mark}${slot.label} ${slot.text}` : `${glyph} ·`;
|
|
201
|
+
})
|
|
202
|
+
.join(" ");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Paint a one-row knob strip at (x, y) within `width`: each slot is its
|
|
207
|
+
* glyph in its knob colour, the label, the value and a short gauge. The
|
|
208
|
+
* selected slot's label is reverse video. Returns the cells used.
|
|
209
|
+
*/
|
|
210
|
+
/** The widest one knob-strip slot gets (label, value and gauge). */
|
|
211
|
+
export const KNOB_CELL_MAX = 40;
|
|
212
|
+
|
|
213
|
+
export function paintKnobStrip(
|
|
214
|
+
buffer: CellBuffer,
|
|
215
|
+
x: number,
|
|
216
|
+
y: number,
|
|
217
|
+
width: number,
|
|
218
|
+
slots: KnobSlots,
|
|
219
|
+
selected: KnobIndex,
|
|
220
|
+
options: { theme: Theme; unicode: boolean; background?: Style },
|
|
221
|
+
): number {
|
|
222
|
+
// Past KNOB_CELL_MAX a slot's gauge only stretches; the strip stays
|
|
223
|
+
// left-aligned and the rest of the row is free.
|
|
224
|
+
const cell = Math.min(KNOB_CELL_MAX, Math.floor(width / 4));
|
|
225
|
+
if (cell < 6) return 0;
|
|
226
|
+
const background = options.background ?? {};
|
|
227
|
+
const on = (style: Style): Style => ({ ...background, ...style });
|
|
228
|
+
slots.forEach((slot, index) => {
|
|
229
|
+
const knob = index as KnobIndex;
|
|
230
|
+
const left = x + cell * index;
|
|
231
|
+
const style = knobStyle(knob, options.theme);
|
|
232
|
+
buffer.text(left, y, knobGlyph(knob, options.unicode), on(style));
|
|
233
|
+
if (!slot) {
|
|
234
|
+
buffer.text(left + 2, y, "·", on(options.theme.roles.faint));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const label = truncate(`${slot.label} ${slot.text}`, cell - 3);
|
|
238
|
+
buffer.text(
|
|
239
|
+
left + 2,
|
|
240
|
+
y,
|
|
241
|
+
label,
|
|
242
|
+
on(
|
|
243
|
+
knob === selected
|
|
244
|
+
? { ...options.theme.roles.text, reverse: true }
|
|
245
|
+
: options.theme.roles.text,
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
const gauge = cell - 3 - displayWidth(label) - 1;
|
|
249
|
+
if (slot.position !== undefined && gauge >= 3) {
|
|
250
|
+
const filled = Math.round(
|
|
251
|
+
Math.min(1, Math.max(0, slot.position)) * gauge,
|
|
252
|
+
);
|
|
253
|
+
buffer.text(
|
|
254
|
+
left + 3 + displayWidth(label),
|
|
255
|
+
y,
|
|
256
|
+
(options.unicode ? "█" : "=").repeat(filled),
|
|
257
|
+
on(style),
|
|
258
|
+
);
|
|
259
|
+
buffer.text(
|
|
260
|
+
left + 3 + displayWidth(label) + filled,
|
|
261
|
+
y,
|
|
262
|
+
(options.unicode ? "─" : "-").repeat(gauge - filled),
|
|
263
|
+
on(options.theme.roles.faint),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return cell * 4;
|
|
268
|
+
}
|
package/tui/play-strip.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Play mode's two fixed rows: the header (`PLAY
|
|
2
|
+
* Play mode's two fixed rows: the header (`PLAY MODE ▶ 120 BPM · lead ·
|
|
3
|
+
* rev 3 C3–F4 vel 100 ● REC
|
|
3
4
|
* click ✓` plus a beat flash) and a one-line keyboard strip with the keys
|
|
4
5
|
* that are sounding lit. Both repaint in place every frame, so playing never
|
|
5
6
|
* scrolls the transcript.
|
|
6
7
|
*/
|
|
7
8
|
import type { CellBuffer } from "./screen.ts";
|
|
8
9
|
import { displayWidth, truncate } from "./text.ts";
|
|
9
|
-
import { onBackground, type Theme } from "./theme.ts";
|
|
10
|
+
import { onBackground, type Style, type Theme } from "./theme.ts";
|
|
10
11
|
|
|
11
12
|
export type PlayStripKey = Readonly<{
|
|
12
13
|
key: string;
|
|
13
14
|
label: string;
|
|
14
15
|
black: boolean;
|
|
15
16
|
lit: boolean;
|
|
17
|
+
/** Chord mode: the label names the chord this key plays. */
|
|
18
|
+
chord?: boolean | undefined;
|
|
19
|
+
/** That chord is built on a degree of the song's key. */
|
|
20
|
+
diatonic?: boolean | undefined;
|
|
16
21
|
}>;
|
|
17
22
|
|
|
18
23
|
export type PlayHeaderView = Readonly<{
|
|
24
|
+
/**
|
|
25
|
+
* The song header compressed to the left of the row while play mode
|
|
26
|
+
* covers it: `▶ 120 BPM · drums · rev 1`.
|
|
27
|
+
*/
|
|
28
|
+
context?: string | undefined;
|
|
19
29
|
/** `C3–F4`. */
|
|
20
30
|
range: string;
|
|
21
31
|
velocity: number;
|
|
@@ -24,6 +34,8 @@ export type PlayHeaderView = Readonly<{
|
|
|
24
34
|
/** Recording right now (armed and the transport is running). */
|
|
25
35
|
recording: boolean;
|
|
26
36
|
replace: boolean;
|
|
37
|
+
/** Loop recording: `↻ 5–6 · pass 3`. */
|
|
38
|
+
pass?: string | undefined;
|
|
27
39
|
click: boolean;
|
|
28
40
|
sustain: boolean;
|
|
29
41
|
/** `count-in 3` while counting in. */
|
|
@@ -51,11 +63,17 @@ export type ChordLegendCell = Readonly<{
|
|
|
51
63
|
/** Text of the header row, for tests and narrow terminals. */
|
|
52
64
|
export function playHeaderText(view: PlayHeaderView, unicode = true): string {
|
|
53
65
|
const parts = [
|
|
54
|
-
"PLAY",
|
|
66
|
+
"PLAY MODE",
|
|
67
|
+
view.context ?? "",
|
|
55
68
|
view.range,
|
|
56
69
|
view.armed
|
|
57
70
|
? `${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`
|
|
58
71
|
: "",
|
|
72
|
+
view.pass
|
|
73
|
+
? unicode
|
|
74
|
+
? view.pass
|
|
75
|
+
: view.pass.replace("↻", "@").replace("–", "-")
|
|
76
|
+
: "",
|
|
59
77
|
view.click ? "click" : "",
|
|
60
78
|
view.sustain ? "SUSTAIN" : "",
|
|
61
79
|
view.chords ?? "",
|
|
@@ -87,19 +105,29 @@ export function paintPlayHeader(
|
|
|
87
105
|
);
|
|
88
106
|
x += buffer.text(x, y, " ", panel);
|
|
89
107
|
};
|
|
90
|
-
put(" PLAY ", roles.pillSteer);
|
|
91
|
-
|
|
108
|
+
put(" PLAY MODE ", roles.pillSteer);
|
|
109
|
+
const context = fitContext(view, width, unicode);
|
|
110
|
+
if (context) put(context, roles.muted);
|
|
111
|
+
// A kit names its range by the track already in the context.
|
|
112
|
+
if (!context.includes(" · ") || view.range !== "drums")
|
|
113
|
+
put(view.range, { ...roles.text, bold: true });
|
|
92
114
|
if (view.armed)
|
|
93
115
|
put(
|
|
94
116
|
`${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`,
|
|
95
117
|
view.recording ? roles.error : roles.warning,
|
|
96
118
|
);
|
|
119
|
+
if (view.pass)
|
|
120
|
+
put(
|
|
121
|
+
unicode ? view.pass : view.pass.replace("↻", "@").replace("–", "-"),
|
|
122
|
+
roles.knob2,
|
|
123
|
+
);
|
|
97
124
|
// Velocity, grid and click details live in the `?` panel.
|
|
98
125
|
if (view.click) put("click", roles.success);
|
|
99
126
|
if (view.sustain) put("SUSTAIN", roles.pillQueue);
|
|
100
127
|
if (view.chords) put(view.chords, roles.hit);
|
|
101
128
|
if (view.countIn) put(view.countIn, roles.warning);
|
|
102
|
-
|
|
129
|
+
const hintWidth = displayWidth("? keys · esc leave");
|
|
130
|
+
if (view.beat && x + view.beat.of + 2 <= width - 1 - hintWidth) {
|
|
103
131
|
const cells = Array.from({ length: view.beat.of }, (_, index) =>
|
|
104
132
|
index + 1 === view.beat!.index
|
|
105
133
|
? unicode
|
|
@@ -136,6 +164,31 @@ export function paintPlayHeader(
|
|
|
136
164
|
if (right > x) buffer.text(right, y, hint, onBackground(roles.faint, panel));
|
|
137
165
|
}
|
|
138
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The compressed song header (`▶ 120 BPM · bass · rev 1`). It is reserved
|
|
169
|
+
* first: the beat dots and the status give way before it does, and only a
|
|
170
|
+
* terminal too narrow for it with the badge and the way out shortens it to
|
|
171
|
+
* its first clause.
|
|
172
|
+
*/
|
|
173
|
+
function fitContext(
|
|
174
|
+
view: PlayHeaderView,
|
|
175
|
+
width: number,
|
|
176
|
+
unicode: boolean,
|
|
177
|
+
): string {
|
|
178
|
+
if (!view.context) return "";
|
|
179
|
+
const rest = displayWidth(
|
|
180
|
+
playHeaderText(
|
|
181
|
+
{ ...view, context: undefined, status: undefined, beat: undefined },
|
|
182
|
+
unicode,
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
// Badge padding, the right-hand hint and the gaps between parts.
|
|
186
|
+
const room = width - 2 - displayWidth("? keys · esc leave") - 4 - rest;
|
|
187
|
+
for (const candidate of [view.context, view.context.split(" · ")[0]!])
|
|
188
|
+
if (displayWidth(candidate) + 2 <= room) return candidate;
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
|
|
139
192
|
/** Chord mode's number row: `1 dim 2 min … 9 block b bass off n next`. */
|
|
140
193
|
export function paintChordLegend(
|
|
141
194
|
buffer: CellBuffer,
|
|
@@ -162,7 +215,11 @@ export function paintChordLegend(
|
|
|
162
215
|
}
|
|
163
216
|
}
|
|
164
217
|
|
|
165
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* `A C3 W C#3 …`: each key with what it plays. The key letter is muted so
|
|
220
|
+
* the music reads first; a chord label is bold, and chords in the song's
|
|
221
|
+
* key are lit (they always fit). Sounding keys are reversed.
|
|
222
|
+
*/
|
|
166
223
|
export function paintPlayStrip(
|
|
167
224
|
buffer: CellBuffer,
|
|
168
225
|
y: number,
|
|
@@ -174,15 +231,24 @@ export function paintPlayStrip(
|
|
|
174
231
|
buffer.fill(0, y, width, 1, roles.canvas);
|
|
175
232
|
let x = 1;
|
|
176
233
|
for (const key of keys) {
|
|
177
|
-
const
|
|
178
|
-
const cellWidth = displayWidth(
|
|
234
|
+
const letter = key.key.toUpperCase();
|
|
235
|
+
const cellWidth = displayWidth(letter) + 1 + displayWidth(key.label) + 1;
|
|
179
236
|
if (x + cellWidth > width - 1) break;
|
|
180
|
-
const
|
|
237
|
+
const labelStyle: Style = key.lit
|
|
181
238
|
? { ...(key.black ? roles.selected : roles.hit), reverse: true }
|
|
239
|
+
: key.chord
|
|
240
|
+
? { ...(key.diatonic ? roles.hit : roles.text), bold: true }
|
|
241
|
+
: key.black
|
|
242
|
+
? roles.muted
|
|
243
|
+
: roles.text;
|
|
244
|
+
const letterStyle: Style = key.lit
|
|
245
|
+
? labelStyle
|
|
182
246
|
: key.black
|
|
183
|
-
? roles.
|
|
184
|
-
: roles.
|
|
185
|
-
buffer.text(x, y,
|
|
186
|
-
x +=
|
|
247
|
+
? roles.faint
|
|
248
|
+
: roles.muted;
|
|
249
|
+
x += buffer.text(x, y, letter, letterStyle);
|
|
250
|
+
x += buffer.text(x, y, " ", key.lit ? labelStyle : roles.canvas);
|
|
251
|
+
x += buffer.text(x, y, key.label, labelStyle);
|
|
252
|
+
x += 1;
|
|
187
253
|
}
|
|
188
254
|
}
|
package/tui/prompt.ts
CHANGED
|
@@ -316,7 +316,7 @@ export function applyPromptKey(
|
|
|
316
316
|
}
|
|
317
317
|
switch (keyInput.toUpperCase()) {
|
|
318
318
|
case "ENTER":
|
|
319
|
-
// The mode pill decides what Enter does:
|
|
319
|
+
// The mode pill decides what Enter does: NOW runs now, NEXT waits.
|
|
320
320
|
return submit(state, state.mode === "queue" ? "queue" : "submit");
|
|
321
321
|
case "ALT+ENTER":
|
|
322
322
|
return submit(state, "queue");
|