@hraness/dawg 0.7.0 → 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 +26 -0
- package/DAWG.md +117 -30
- package/README.md +7 -5
- package/core/diff.ts +2 -0
- package/core/ids.ts +52 -0
- package/core/loop.ts +3 -0
- package/core/range.ts +581 -0
- package/core/score.ts +123 -1
- package/core/sdk/print.ts +11 -2
- package/core/sdk/v1.ts +281 -2
- package/core/sections.ts +198 -19
- package/guides/arrange.md +9 -8
- package/guides/audio.md +31 -0
- package/guides/mix.md +3 -1
- package/guides/panes.md +32 -0
- package/guides/project.md +3 -2
- package/guides/rhythm.md +2 -1
- package/guides/tape.md +32 -0
- package/package.json +2 -1
- package/src/agent/agent.ts +4 -4
- package/src/agent/command-agent.ts +19 -10
- package/src/agent/ops.ts +15 -0
- package/src/agent/planner.ts +12 -0
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/show-me.ts +1 -1
- package/src/agent/tools.ts +2 -0
- package/src/agent/xcb-agent.ts +2 -3
- package/src/audio/arrange.ts +12 -2
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +84 -13
- package/src/audio/engine.ts +216 -28
- package/src/commands/arrange.ts +60 -25
- package/src/commands/grammar.ts +3 -1
- package/src/commands/help.ts +187 -9
- package/src/commands/history.ts +122 -11
- package/src/commands/keys.ts +17 -0
- package/src/commands/parses.ts +6 -1
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +2 -6
- package/src/commands/vocal-pitch.ts +2 -1
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +18 -0
- package/src/launch-args.ts +105 -1
- package/src/main.ts +1142 -86
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +196 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/origin.ts +20 -0
- package/src/session/port.ts +87 -10
- package/src/session/protocol.ts +360 -6
- package/src/session/rebase.ts +46 -8
- package/src/session/receipt.ts +7 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +61 -2
- package/src/tui/arrange-menu.ts +169 -1
- package/src/tui/euclid.ts +95 -14
- package/src/tui/fader.ts +55 -2
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu.ts +228 -2
- package/src/tui/play-session.ts +196 -21
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/tui/app.ts +217 -44
- package/tui/delight.ts +49 -0
- package/tui/drawer.ts +167 -9
- package/tui/grammar.ts +34 -5
- package/tui/guide.ts +8 -1
- package/tui/highway.ts +40 -5
- package/tui/hints.ts +5 -0
- package/tui/hits.ts +7 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +12 -0
- package/tui/screen.ts +7 -4
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +19 -1
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
|
@@ -34,6 +34,8 @@ export type PlayHeaderView = Readonly<{
|
|
|
34
34
|
/** Recording right now (armed and the transport is running). */
|
|
35
35
|
recording: boolean;
|
|
36
36
|
replace: boolean;
|
|
37
|
+
/** Loop recording: `↻ 5–6 · pass 3`. */
|
|
38
|
+
pass?: string | undefined;
|
|
37
39
|
click: boolean;
|
|
38
40
|
sustain: boolean;
|
|
39
41
|
/** `count-in 3` while counting in. */
|
|
@@ -67,6 +69,11 @@ export function playHeaderText(view: PlayHeaderView, unicode = true): string {
|
|
|
67
69
|
view.armed
|
|
68
70
|
? `${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`
|
|
69
71
|
: "",
|
|
72
|
+
view.pass
|
|
73
|
+
? unicode
|
|
74
|
+
? view.pass
|
|
75
|
+
: view.pass.replace("↻", "@").replace("–", "-")
|
|
76
|
+
: "",
|
|
70
77
|
view.click ? "click" : "",
|
|
71
78
|
view.sustain ? "SUSTAIN" : "",
|
|
72
79
|
view.chords ?? "",
|
|
@@ -109,6 +116,11 @@ export function paintPlayHeader(
|
|
|
109
116
|
`${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`,
|
|
110
117
|
view.recording ? roles.error : roles.warning,
|
|
111
118
|
);
|
|
119
|
+
if (view.pass)
|
|
120
|
+
put(
|
|
121
|
+
unicode ? view.pass : view.pass.replace("↻", "@").replace("–", "-"),
|
|
122
|
+
roles.knob2,
|
|
123
|
+
);
|
|
112
124
|
// Velocity, grid and click details live in the `?` panel.
|
|
113
125
|
if (view.click) put("click", roles.success);
|
|
114
126
|
if (view.sustain) put("SUSTAIN", roles.pillQueue);
|
package/tui/screen.ts
CHANGED
|
@@ -33,10 +33,10 @@ export class CellBuffer {
|
|
|
33
33
|
constructor(width: number, height: number, fill: Style = EMPTY) {
|
|
34
34
|
this.width = Math.max(0, Math.floor(width));
|
|
35
35
|
this.height = Math.max(0, Math.floor(height));
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const count = this.width * this.height;
|
|
37
|
+
this.cells = new Array<Cell>(count);
|
|
38
|
+
for (let index = 0; index < count; index += 1)
|
|
39
|
+
this.cells[index] = { ch: " ", style: fill };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
get(x: number, y: number): Cell | undefined {
|
|
@@ -200,6 +200,9 @@ export class CellBuffer {
|
|
|
200
200
|
private clearWide(x: number, y: number): void {
|
|
201
201
|
const offset = y * this.width;
|
|
202
202
|
const cell = this.cells[offset + x]!;
|
|
203
|
+
// Fast path: one UTF-16 unit below U+1100 is never wide (nor a wide
|
|
204
|
+
// character's empty trail), and it is nearly every cell of a frame.
|
|
205
|
+
if (cell.ch.length === 1 && cell.ch.charCodeAt(0) < 0x1100) return;
|
|
203
206
|
if (cell.ch === "" && x > 0) {
|
|
204
207
|
const lead = this.cells[offset + x - 1]!;
|
|
205
208
|
if (graphemeWidth(lead.ch) === 2) lead.ch = " ";
|