@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/DAWG.md +117 -30
  3. package/README.md +7 -5
  4. package/core/diff.ts +2 -0
  5. package/core/ids.ts +52 -0
  6. package/core/loop.ts +3 -0
  7. package/core/range.ts +581 -0
  8. package/core/score.ts +123 -1
  9. package/core/sdk/print.ts +11 -2
  10. package/core/sdk/v1.ts +281 -2
  11. package/core/sections.ts +198 -19
  12. package/guides/arrange.md +9 -8
  13. package/guides/audio.md +31 -0
  14. package/guides/mix.md +3 -1
  15. package/guides/panes.md +32 -0
  16. package/guides/project.md +3 -2
  17. package/guides/rhythm.md +2 -1
  18. package/guides/tape.md +32 -0
  19. package/package.json +2 -1
  20. package/src/agent/agent.ts +4 -4
  21. package/src/agent/command-agent.ts +19 -10
  22. package/src/agent/ops.ts +15 -0
  23. package/src/agent/planner.ts +12 -0
  24. package/src/agent/range-tools.ts +216 -0
  25. package/src/agent/show-me.ts +1 -1
  26. package/src/agent/tools.ts +2 -0
  27. package/src/agent/xcb-agent.ts +2 -3
  28. package/src/audio/arrange.ts +12 -2
  29. package/src/audio/audio-command.ts +218 -0
  30. package/src/audio/devices.ts +264 -0
  31. package/src/audio/doctor.ts +84 -13
  32. package/src/audio/engine.ts +216 -28
  33. package/src/commands/arrange.ts +60 -25
  34. package/src/commands/grammar.ts +3 -1
  35. package/src/commands/help.ts +187 -9
  36. package/src/commands/history.ts +122 -11
  37. package/src/commands/keys.ts +17 -0
  38. package/src/commands/parses.ts +6 -1
  39. package/src/commands/range.ts +763 -0
  40. package/src/commands/resample.ts +13 -10
  41. package/src/commands/sample.ts +11 -1
  42. package/src/commands/sing.ts +2 -6
  43. package/src/commands/vocal-pitch.ts +2 -1
  44. package/src/identity/actor.ts +127 -0
  45. package/src/lang/glossary.ts +18 -0
  46. package/src/launch-args.ts +105 -1
  47. package/src/main.ts +1142 -86
  48. package/src/session/client.ts +107 -3
  49. package/src/session/clipboard.ts +79 -0
  50. package/src/session/daemon.ts +196 -5
  51. package/src/session/live-host.ts +232 -0
  52. package/src/session/origin.ts +20 -0
  53. package/src/session/port.ts +87 -10
  54. package/src/session/protocol.ts +360 -6
  55. package/src/session/rebase.ts +46 -8
  56. package/src/session/receipt.ts +7 -0
  57. package/src/session/shared-live.ts +131 -0
  58. package/src/session/store.ts +61 -2
  59. package/src/tui/arrange-menu.ts +169 -1
  60. package/src/tui/euclid.ts +95 -14
  61. package/src/tui/fader.ts +55 -2
  62. package/src/tui/knob-fields.ts +107 -0
  63. package/src/tui/knob-map.ts +198 -0
  64. package/src/tui/menu.ts +228 -2
  65. package/src/tui/play-session.ts +196 -21
  66. package/src/tui/tape-mode.ts +580 -0
  67. package/src/tui/tape-view.ts +263 -0
  68. package/tui/app.ts +217 -44
  69. package/tui/delight.ts +49 -0
  70. package/tui/drawer.ts +167 -9
  71. package/tui/grammar.ts +34 -5
  72. package/tui/guide.ts +8 -1
  73. package/tui/highway.ts +40 -5
  74. package/tui/hints.ts +5 -0
  75. package/tui/hits.ts +7 -1
  76. package/tui/knobs.ts +268 -0
  77. package/tui/play-strip.ts +12 -0
  78. package/tui/screen.ts +7 -4
  79. package/tui/tape.ts +439 -0
  80. package/tui/text.ts +35 -2
  81. 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 };