@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TAPE view model: a score, the playhead and this pane's state become
|
|
3
|
+
* the cells tui/tape.ts paints. Density per cell is the notes starting in
|
|
4
|
+
* it, weighted by velocity (` ▁▂▃▄▅▆▇█`); a held note shows `▁` across the
|
|
5
|
+
* cells it sustains through, and an audio clip `▃` where it plays.
|
|
6
|
+
*
|
|
7
|
+
* With a form the tape is drawn unrolled (op1-ux §4): every pass in play
|
|
8
|
+
* order, the first pass of each section solid and every later one ghosted
|
|
9
|
+
* `░`. Each cell keeps the score bar it shows, so a click, a drag or a
|
|
10
|
+
* gesture on a ghost acts on the source section.
|
|
11
|
+
*/
|
|
12
|
+
import { formPositionAt, formSegments } from "../../core/sections.ts";
|
|
13
|
+
import type { TrackScore } from "../../core/score.ts";
|
|
14
|
+
import { barStartTick, bpmAtTick } from "../../core/tempo.ts";
|
|
15
|
+
import type { KnobIndex } from "../../tui/knobs.ts";
|
|
16
|
+
import type {
|
|
17
|
+
TapeRow,
|
|
18
|
+
TapeSection,
|
|
19
|
+
TapeView,
|
|
20
|
+
TapeZoom,
|
|
21
|
+
} from "../../tui/tape.ts";
|
|
22
|
+
import {
|
|
23
|
+
barOfBeat,
|
|
24
|
+
clipboardLabel,
|
|
25
|
+
formPasses,
|
|
26
|
+
loopRange,
|
|
27
|
+
rangeLine,
|
|
28
|
+
tapeKnobs,
|
|
29
|
+
TAPE_HINT,
|
|
30
|
+
type TapeContext,
|
|
31
|
+
} from "./tape-mode.ts";
|
|
32
|
+
|
|
33
|
+
export type TapeViewInput = TapeContext &
|
|
34
|
+
Readonly<{
|
|
35
|
+
zoom: TapeZoom;
|
|
36
|
+
selected: KnobIndex;
|
|
37
|
+
/** Other panes on a track (§12.7): `B`, `C●`. */
|
|
38
|
+
marks: (trackId: string) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Transport beat (arranged time), for the playhead on an unrolled form:
|
|
41
|
+
* which pass it is in. Absent, the playhead sits on the first pass.
|
|
42
|
+
*/
|
|
43
|
+
transportBeat?: number | undefined;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
/** One drawn cell on an unrolled form: a score cell, its pass, a ghost. */
|
|
47
|
+
type Unrolled = Readonly<{ cell: number; segment: number; ghost: boolean }>;
|
|
48
|
+
|
|
49
|
+
/** The form in play order as score cells; undefined without a form. */
|
|
50
|
+
function unroll(
|
|
51
|
+
score: TrackScore,
|
|
52
|
+
bars: readonly number[],
|
|
53
|
+
): Unrolled[] | undefined {
|
|
54
|
+
const segments = formSegments(score);
|
|
55
|
+
if (segments.length === 0) return undefined;
|
|
56
|
+
const firstCellOf = new Map<number, number>();
|
|
57
|
+
bars.forEach((bar, index) => {
|
|
58
|
+
if (!firstCellOf.has(bar)) firstCellOf.set(bar, index);
|
|
59
|
+
});
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
const cells: Unrolled[] = [];
|
|
62
|
+
segments.forEach((segment, index) => {
|
|
63
|
+
const ghost = seen.has(segment.section.name);
|
|
64
|
+
seen.add(segment.section.name);
|
|
65
|
+
const from = firstCellOf.get(segment.section.startBar);
|
|
66
|
+
if (from === undefined) return;
|
|
67
|
+
const end = segment.section.startBar + segment.section.bars;
|
|
68
|
+
for (let cell = from; cell < bars.length && bars[cell]! < end; cell += 1)
|
|
69
|
+
cells.push({ cell, segment: index, ghost });
|
|
70
|
+
});
|
|
71
|
+
// Bars the form never plays stay reachable, after it, in score order.
|
|
72
|
+
const played = new Set(cells.map((item) => item.cell));
|
|
73
|
+
bars.forEach((_, cell) => {
|
|
74
|
+
if (!played.has(cell)) cells.push({ cell, segment: -1, ghost: false });
|
|
75
|
+
});
|
|
76
|
+
return cells;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Cell start ticks and the bar each cell is in, for a zoom. */
|
|
80
|
+
export function tapeCells(
|
|
81
|
+
score: TrackScore,
|
|
82
|
+
zoom: TapeZoom,
|
|
83
|
+
): { starts: number[]; bars: number[] } {
|
|
84
|
+
const starts: number[] = [];
|
|
85
|
+
const bars: number[] = [];
|
|
86
|
+
for (let bar = 0; bar < score.bars; bar += 1) {
|
|
87
|
+
const from = barStartTick(score, bar);
|
|
88
|
+
const to = barStartTick(score, bar + 1);
|
|
89
|
+
const step =
|
|
90
|
+
zoom === "bar"
|
|
91
|
+
? to - from
|
|
92
|
+
: zoom === "beat"
|
|
93
|
+
? score.ticksPerBeat
|
|
94
|
+
: score.ticksPerBeat / 2;
|
|
95
|
+
for (let tick = from; tick < to - 1e-6; tick += step) {
|
|
96
|
+
starts.push(tick);
|
|
97
|
+
bars.push(bar);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { starts, bars };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cellAt(starts: readonly number[], tick: number): number {
|
|
104
|
+
let low = 0;
|
|
105
|
+
let high = starts.length - 1;
|
|
106
|
+
if (high < 0 || tick < starts[0]!) return -1;
|
|
107
|
+
while (low < high) {
|
|
108
|
+
const mid = (low + high + 1) >> 1;
|
|
109
|
+
if (starts[mid]! <= tick) low = mid;
|
|
110
|
+
else high = mid - 1;
|
|
111
|
+
}
|
|
112
|
+
return low;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Density levels (0…8) per cell for one track. */
|
|
116
|
+
export function trackLevels(
|
|
117
|
+
score: TrackScore,
|
|
118
|
+
trackId: string,
|
|
119
|
+
starts: readonly number[],
|
|
120
|
+
end: number,
|
|
121
|
+
): number[] {
|
|
122
|
+
const weight = starts.map(() => 0);
|
|
123
|
+
const held = starts.map(() => false);
|
|
124
|
+
for (const note of score.notes) {
|
|
125
|
+
if (note.trackId !== trackId || note.startTick >= end) continue;
|
|
126
|
+
const cell = cellAt(starts, note.startTick);
|
|
127
|
+
if (cell < 0) continue;
|
|
128
|
+
weight[cell]! += Math.max(0.15, note.velocity);
|
|
129
|
+
const last = cellAt(
|
|
130
|
+
starts,
|
|
131
|
+
Math.min(end - 1, note.startTick + note.durationTicks - 1),
|
|
132
|
+
);
|
|
133
|
+
for (let index = cell + 1; index <= last; index += 1) held[index] = true;
|
|
134
|
+
}
|
|
135
|
+
const track = score.tracks.find((item) => item.id === trackId);
|
|
136
|
+
for (const clip of track?.clips ?? []) {
|
|
137
|
+
const first = cellAt(starts, clip.startTick);
|
|
138
|
+
if (first < 0) continue;
|
|
139
|
+
const length =
|
|
140
|
+
clip.dur !== undefined
|
|
141
|
+
? clip.dur *
|
|
142
|
+
(bpmAtTick(score, clip.startTick) / 60) *
|
|
143
|
+
score.ticksPerBeat
|
|
144
|
+
: starts.length > first + 1
|
|
145
|
+
? starts[first + 1]! - starts[first]!
|
|
146
|
+
: 0;
|
|
147
|
+
const last = cellAt(starts, Math.min(end - 1, clip.startTick + length - 1));
|
|
148
|
+
for (let index = first; index <= last; index += 1)
|
|
149
|
+
weight[index] = Math.max(weight[index]!, 1);
|
|
150
|
+
}
|
|
151
|
+
return weight.map((value, index) =>
|
|
152
|
+
value > 0
|
|
153
|
+
? Math.max(1, Math.min(8, Math.ceil(value * 3)))
|
|
154
|
+
: held[index]
|
|
155
|
+
? 1
|
|
156
|
+
: 0,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function tapeView(input: TapeViewInput): TapeView {
|
|
161
|
+
const score = input.score;
|
|
162
|
+
const { starts, bars } = tapeCells(score, input.zoom);
|
|
163
|
+
const end = barStartTick(score, score.bars);
|
|
164
|
+
const tick = Math.max(0, Math.round(input.beat * score.ticksPerBeat));
|
|
165
|
+
const scoreHead = Math.max(0, cellAt(starts, Math.min(end - 1, tick)));
|
|
166
|
+
const unrolled = unroll(score, bars);
|
|
167
|
+
// The pass under the transport, so the playhead walks through ghosts.
|
|
168
|
+
const pass =
|
|
169
|
+
unrolled && input.transportBeat !== undefined
|
|
170
|
+
? formPositionAt(score, input.transportBeat).index
|
|
171
|
+
: undefined;
|
|
172
|
+
const inPass = unrolled?.findIndex(
|
|
173
|
+
(item) =>
|
|
174
|
+
item.cell === scoreHead && (pass === undefined || item.segment === pass),
|
|
175
|
+
);
|
|
176
|
+
const playheadCell = unrolled
|
|
177
|
+
? Math.max(
|
|
178
|
+
0,
|
|
179
|
+
inPass !== undefined && inPass >= 0
|
|
180
|
+
? inPass
|
|
181
|
+
: unrolled.findIndex((item) => item.cell === scoreHead),
|
|
182
|
+
)
|
|
183
|
+
: scoreHead;
|
|
184
|
+
// A section the form plays more than once shows `×N` after its name.
|
|
185
|
+
const passes = formPasses(score);
|
|
186
|
+
const named = (name: string): string => {
|
|
187
|
+
const times = passes.get(name) ?? 1;
|
|
188
|
+
return times > 1 ? `${name} ×${times}` : name;
|
|
189
|
+
};
|
|
190
|
+
const sections: TapeSection[] = score.sections.map((section) => ({
|
|
191
|
+
name: named(section.name),
|
|
192
|
+
startBar: section.startBar,
|
|
193
|
+
bars: section.bars,
|
|
194
|
+
}));
|
|
195
|
+
const loopSection =
|
|
196
|
+
score.loop === undefined && score.loopSection !== undefined
|
|
197
|
+
? sections.find((section) => section.name.startsWith(score.loopSection!))
|
|
198
|
+
?.name
|
|
199
|
+
: undefined;
|
|
200
|
+
const tempo = [...(score.time?.tempo ?? [])]
|
|
201
|
+
.filter((event) => event.tick > 0 && event.tick < end)
|
|
202
|
+
.sort((a, b) => a.tick - b.tick);
|
|
203
|
+
const pick = <T>(values: readonly T[]): T[] =>
|
|
204
|
+
unrolled ? unrolled.map((item) => values[item.cell]!) : [...values];
|
|
205
|
+
const rows: TapeRow[] = score.tracks.map((track) => ({
|
|
206
|
+
id: track.id,
|
|
207
|
+
name: track.name,
|
|
208
|
+
muted: track.muted,
|
|
209
|
+
levels: pick(trackLevels(score, track.id, starts, end)),
|
|
210
|
+
marks: input.marks(track.id).trim(),
|
|
211
|
+
}));
|
|
212
|
+
// Tempo marks land on the cell their tick starts (the first pass).
|
|
213
|
+
const tempoCell = (at: number): number => {
|
|
214
|
+
const cell = Math.max(0, cellAt(starts, at));
|
|
215
|
+
return unrolled
|
|
216
|
+
? Math.max(
|
|
217
|
+
0,
|
|
218
|
+
unrolled.findIndex((item) => item.cell === cell),
|
|
219
|
+
)
|
|
220
|
+
: cell;
|
|
221
|
+
};
|
|
222
|
+
const focused = Math.max(
|
|
223
|
+
0,
|
|
224
|
+
score.tracks.findIndex((track) => track.id === input.trackId),
|
|
225
|
+
);
|
|
226
|
+
return {
|
|
227
|
+
bars: score.bars,
|
|
228
|
+
cellBars: pick(bars),
|
|
229
|
+
...(unrolled
|
|
230
|
+
? {
|
|
231
|
+
ghosts: unrolled.map((item) => item.ghost),
|
|
232
|
+
passes: unrolled.map((item) => item.segment),
|
|
233
|
+
passNames: formSegments(score).map((segment) =>
|
|
234
|
+
named(segment.section.name),
|
|
235
|
+
),
|
|
236
|
+
}
|
|
237
|
+
: {}),
|
|
238
|
+
playheadCell,
|
|
239
|
+
loop: loopRange(score),
|
|
240
|
+
sections,
|
|
241
|
+
loopSection,
|
|
242
|
+
tempo:
|
|
243
|
+
tempo.length > 0
|
|
244
|
+
? [
|
|
245
|
+
{ cell: 0, bpm: Math.round(score.tempoBpm) },
|
|
246
|
+
...tempo.map((event) => ({
|
|
247
|
+
cell: tempoCell(event.tick),
|
|
248
|
+
bpm: Math.round(event.bpm),
|
|
249
|
+
})),
|
|
250
|
+
]
|
|
251
|
+
: [],
|
|
252
|
+
rows,
|
|
253
|
+
focused,
|
|
254
|
+
range: rangeLine(input),
|
|
255
|
+
clipboard: clipboardLabel(score, input.clipboard),
|
|
256
|
+
knobs: tapeKnobs(input),
|
|
257
|
+
selected: input.selected,
|
|
258
|
+
hint: TAPE_HINT,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The playhead's bar, for the header (`5.3`) callers. */
|
|
263
|
+
export { barOfBeat };
|