@hraness/dawg 0.2.0 → 0.4.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 (140) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/DAWG.md +762 -11
  3. package/README.md +79 -30
  4. package/core/chords.ts +1724 -0
  5. package/core/diff.ts +261 -0
  6. package/core/euclid.ts +670 -0
  7. package/core/fx.ts +1065 -0
  8. package/core/key.ts +43 -0
  9. package/core/kits.ts +320 -0
  10. package/core/params.ts +111 -0
  11. package/core/pitch.ts +60 -0
  12. package/core/rhythm.ts +287 -0
  13. package/core/score.ts +1054 -14
  14. package/core/sdk/eval-child.ts +125 -0
  15. package/core/sdk/eval.ts +260 -0
  16. package/core/sdk/print.ts +675 -0
  17. package/core/sdk/sync-chords.ts +54 -0
  18. package/core/sdk/v1.ts +4159 -0
  19. package/core/slug.ts +19 -0
  20. package/core/synth.ts +1001 -0
  21. package/package.json +8 -5
  22. package/src/agent/agent.ts +317 -16
  23. package/src/agent/brief.ts +50 -5
  24. package/src/agent/chord-tools.ts +357 -0
  25. package/src/agent/drum-tools.ts +135 -0
  26. package/src/agent/gateway.ts +215 -41
  27. package/src/agent/models.ts +633 -0
  28. package/src/agent/ops.ts +3 -18
  29. package/src/agent/pack-tools.ts +369 -0
  30. package/src/agent/planner.ts +55 -2
  31. package/src/agent/preview-tool.ts +270 -0
  32. package/src/agent/provider.ts +234 -106
  33. package/src/agent/rhythm-tools.ts +145 -0
  34. package/src/agent/sse.ts +31 -7
  35. package/src/agent/tools.ts +705 -14
  36. package/src/agent/usage.ts +296 -0
  37. package/src/agent/workspace.ts +683 -0
  38. package/src/agent/xcb-agent.ts +35 -14
  39. package/src/agent/xcb.ts +233 -21
  40. package/src/audio/audition.ts +93 -0
  41. package/src/audio/cache.ts +160 -0
  42. package/src/audio/click.ts +125 -0
  43. package/src/audio/effects/bus.ts +147 -0
  44. package/src/audio/effects/chain.ts +99 -0
  45. package/src/audio/effects/common.ts +251 -0
  46. package/src/audio/effects/convolution.ts +301 -0
  47. package/src/audio/effects/drive.ts +142 -0
  48. package/src/audio/effects/duck.ts +122 -0
  49. package/src/audio/effects/dynamics.ts +121 -0
  50. package/src/audio/effects/filter.ts +319 -0
  51. package/src/audio/effects/modulation.ts +174 -0
  52. package/src/audio/effects/space.ts +337 -0
  53. package/src/audio/engine.ts +430 -33
  54. package/src/audio/kits.ts +200 -0
  55. package/src/audio/live.ts +152 -0
  56. package/src/audio/packs.ts +1787 -0
  57. package/src/audio/player.ts +19 -4
  58. package/src/audio/preview.ts +470 -0
  59. package/src/audio/random.ts +15 -0
  60. package/src/audio/render-worker.ts +68 -0
  61. package/src/audio/renderer.ts +174 -0
  62. package/src/audio/sampler.ts +420 -0
  63. package/src/audio/samples.ts +1025 -0
  64. package/src/audio/synth/oscillators.ts +268 -0
  65. package/src/audio/synth/voice.ts +555 -0
  66. package/src/audio/synth/zzfx.ts +137 -0
  67. package/src/audio/wav.ts +550 -334
  68. package/src/audio/wavetable-maker.ts +717 -0
  69. package/src/audio/wavetable.ts +624 -0
  70. package/src/auth/cli.ts +146 -27
  71. package/src/auth/credentials.ts +167 -41
  72. package/src/auth/discover.ts +481 -0
  73. package/src/auth/login.ts +885 -128
  74. package/src/auth/openrouter.ts +206 -0
  75. package/src/auth/picker.ts +282 -0
  76. package/src/auth/runner.ts +25 -2
  77. package/src/auth/tui.ts +60 -43
  78. package/src/commands/drums.ts +299 -0
  79. package/src/commands/edit.ts +192 -0
  80. package/src/commands/fx.ts +360 -0
  81. package/src/commands/help.ts +535 -0
  82. package/src/commands/history.ts +32 -19
  83. package/src/commands/music.ts +34 -15
  84. package/src/commands/pack.ts +423 -0
  85. package/src/commands/rhythm.ts +230 -0
  86. package/src/commands/sample.ts +462 -0
  87. package/src/commands/synth.ts +229 -0
  88. package/src/commands/wavetable.ts +370 -0
  89. package/src/main.ts +2136 -170
  90. package/src/media/analyze.ts +364 -0
  91. package/src/media/backend.ts +253 -0
  92. package/src/media/cli.ts +186 -0
  93. package/src/media/download.ts +281 -0
  94. package/src/media/dsp.ts +281 -0
  95. package/src/media/import.ts +130 -0
  96. package/src/media/lyrics.ts +201 -0
  97. package/src/media/notes.ts +363 -0
  98. package/src/media/paths.ts +168 -0
  99. package/src/media/process.ts +226 -0
  100. package/src/media/registry.ts +9 -0
  101. package/src/media/sidecar.ts +72 -0
  102. package/src/media/stemdeck.ts +254 -0
  103. package/src/media/stems.ts +173 -0
  104. package/src/media/tools.ts +397 -0
  105. package/src/media/types.ts +92 -0
  106. package/src/media/vendor/basic-pitch.ts +261 -0
  107. package/src/media/vendor/drums.ts +817 -0
  108. package/src/media/vendor/grid.ts +203 -0
  109. package/src/media/vendor/util.ts +139 -0
  110. package/src/media/vendor/wav.ts +233 -0
  111. package/src/media/wavetable.ts +202 -0
  112. package/src/project/check.ts +80 -0
  113. package/src/project/init.ts +253 -0
  114. package/src/project/sync.ts +432 -0
  115. package/src/project/typecheck.ts +149 -0
  116. package/src/render.ts +49 -6
  117. package/src/session/attach.ts +3 -4
  118. package/src/session/daemon.ts +25 -8
  119. package/src/session/delta.ts +249 -0
  120. package/src/session/naming.ts +6 -38
  121. package/src/session/port.ts +26 -6
  122. package/src/session/rebase.ts +38 -8
  123. package/src/session/store.ts +116 -21
  124. package/src/tui/audition.ts +501 -0
  125. package/src/tui/euclid.ts +472 -0
  126. package/src/tui/menu.ts +2125 -0
  127. package/src/tui/play-chords.ts +538 -0
  128. package/src/tui/play-mode.ts +442 -0
  129. package/src/tui/play-session.ts +984 -0
  130. package/src/tui/sketch.ts +108 -0
  131. package/src/web/fetch.ts +340 -0
  132. package/src/web/http.ts +137 -0
  133. package/src/web/search.ts +684 -0
  134. package/tui/activity.ts +42 -3
  135. package/tui/app.ts +398 -33
  136. package/tui/drums.ts +44 -0
  137. package/tui/grammar.ts +281 -0
  138. package/tui/highway.ts +24 -2
  139. package/tui/layers.ts +14 -2
  140. package/tui/play-strip.ts +188 -0
package/core/diff.ts ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Score diff: the shortest list of `ScoreOperation`s that turns score A into
3
+ * score B, so a project edit commits as ordinary session operations that
4
+ * rebase, undo and broadcast like everything else.
5
+ *
6
+ * Order: song settings, note removals, track removals, track additions,
7
+ * track moves, track patches, note updates, note additions. Each track and
8
+ * each note appears at most once per kind. `applyScoreOperations(a,
9
+ * diffScores(a, b))` deep-equals `b` (notes and tracks are stored in
10
+ * canonical order, so insertion order never matters).
11
+ */
12
+
13
+ import {
14
+ applyScoreOperation,
15
+ TrackScore,
16
+ type Note,
17
+ type ScoreOperation,
18
+ type Track,
19
+ type TrackPatch,
20
+ } from "./score.ts";
21
+
22
+ export class DiffError extends Error {
23
+ constructor(message: string) {
24
+ super(message);
25
+ this.name = "DiffError";
26
+ }
27
+ }
28
+
29
+ const EFFECTS = [
30
+ "filter",
31
+ "delay",
32
+ "reverb",
33
+ "sampler",
34
+ "fx",
35
+ "fxAutomation",
36
+ "synth",
37
+ "wavetable",
38
+ ] as const;
39
+ const LANES = [
40
+ "volumeAutomation",
41
+ "panAutomation",
42
+ "filterAutomation",
43
+ "resonanceAutomation",
44
+ "delayFeedbackAutomation",
45
+ "delayMixAutomation",
46
+ "wtAutomation",
47
+ ] as const;
48
+
49
+ /** Operations turning `a` into `b`; empty when they are equal. */
50
+ export function diffScores(
51
+ a: TrackScore,
52
+ b: TrackScore,
53
+ ): readonly ScoreOperation[] {
54
+ if (a.ticksPerBeat !== b.ticksPerBeat)
55
+ throw new DiffError(
56
+ `ticksPerBeat differs (${a.ticksPerBeat} → ${b.ticksPerBeat}); the session resolution is fixed`,
57
+ );
58
+ const ops: ScoreOperation[] = [];
59
+ if (a.beatsPerBar !== b.beatsPerBar)
60
+ ops.push({ type: "setMeter", beatsPerBar: b.beatsPerBar });
61
+ if (a.bars !== b.bars) ops.push({ type: "setBars", bars: b.bars });
62
+ if (a.tempoBpm !== b.tempoBpm)
63
+ ops.push({ type: "setTempo", tempoBpm: b.tempoBpm });
64
+ if (a.key !== b.key) ops.push({ type: "setKey", key: b.key });
65
+
66
+ const aTracks = new Map(a.tracks.map((track) => [track.id, track]));
67
+ const bTracks = new Map(b.tracks.map((track) => [track.id, track]));
68
+ const aNotes = new Map(a.notes.map((note) => [note.id, note]));
69
+ const bNotes = new Map(b.notes.map((note) => [note.id, note]));
70
+
71
+ const updates: ScoreOperation[] = [];
72
+ const additions: ScoreOperation[] = [];
73
+ for (const note of a.notes) {
74
+ if (!bTracks.has(note.trackId)) continue; // removed with its track
75
+ const next = bNotes.get(note.id);
76
+ if (!next || next.trackId !== note.trackId) {
77
+ ops.push({ type: "removeNote", noteId: note.id });
78
+ continue;
79
+ }
80
+ const patch = notePatch(note, next);
81
+ if (patch) updates.push({ type: "updateNote", noteId: note.id, patch });
82
+ }
83
+ for (const note of b.notes) {
84
+ const previous = aNotes.get(note.id);
85
+ if (
86
+ previous &&
87
+ previous.trackId === note.trackId &&
88
+ bTracks.has(note.trackId)
89
+ )
90
+ continue;
91
+ if (
92
+ previous &&
93
+ previous.trackId === note.trackId &&
94
+ !aTracks.has(note.trackId)
95
+ )
96
+ continue;
97
+ additions.push({ type: "addNote", note });
98
+ }
99
+
100
+ for (const track of a.tracks)
101
+ if (!bTracks.has(track.id))
102
+ ops.push({ type: "removeTrack", trackId: track.id });
103
+ const order = a.tracks
104
+ .filter((track) => bTracks.has(track.id))
105
+ .map((t) => t.id);
106
+ for (const track of b.tracks)
107
+ if (!aTracks.has(track.id)) {
108
+ ops.push({ type: "addTrack", track });
109
+ order.push(track.id);
110
+ }
111
+ b.tracks.forEach((track, index) => {
112
+ if (order[index] === track.id) return;
113
+ const from = order.indexOf(track.id);
114
+ order.splice(from, 1);
115
+ order.splice(index, 0, track.id);
116
+ ops.push({ type: "moveTrack", trackId: track.id, index });
117
+ });
118
+ for (const track of b.tracks) {
119
+ const previous = aTracks.get(track.id);
120
+ if (!previous) continue;
121
+ const patch = trackPatch(previous, track);
122
+ if (patch) ops.push({ type: "updateTrack", trackId: track.id, patch });
123
+ }
124
+ ops.push(...updates, ...additions);
125
+ return Object.freeze(ops);
126
+ }
127
+
128
+ /** Applies operations in order; the inverse of `diffScores`. */
129
+ export function applyScoreOperations(
130
+ score: TrackScore,
131
+ operations: readonly ScoreOperation[],
132
+ ): TrackScore {
133
+ return operations.reduce(applyScoreOperation, score);
134
+ }
135
+
136
+ function notePatch(
137
+ a: Note,
138
+ b: Note,
139
+ ):
140
+ | Readonly<
141
+ Partial<Pick<Note, "startTick" | "durationTicks" | "pitch" | "velocity">>
142
+ >
143
+ | undefined {
144
+ const patch: Record<string, number> = {};
145
+ for (const key of [
146
+ "startTick",
147
+ "durationTicks",
148
+ "pitch",
149
+ "velocity",
150
+ ] as const)
151
+ if (a[key] !== b[key]) patch[key] = b[key];
152
+ return Object.keys(patch).length > 0 ? patch : undefined;
153
+ }
154
+
155
+ function trackPatch(a: Track, b: Track): TrackPatch | undefined {
156
+ const patch: Record<string, unknown> = {};
157
+ for (const key of ["name", "instrument", "muted", "volume", "pan"] as const)
158
+ if (a[key] !== b[key]) patch[key] = b[key];
159
+ if ((a.solo ?? false) !== (b.solo ?? false)) patch.solo = b.solo ?? false;
160
+ for (const key of EFFECTS)
161
+ if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
162
+ for (const key of ["rhythm", "kit"] as const)
163
+ if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
164
+ for (const key of LANES)
165
+ if (!deepEqual(a[key] ?? [], b[key] ?? [])) patch[key] = b[key] ?? [];
166
+ return Object.keys(patch).length > 0 ? (patch as TrackPatch) : undefined;
167
+ }
168
+
169
+ /** Structural equality for plain JSON-like data. */
170
+ export function deepEqual(a: unknown, b: unknown): boolean {
171
+ if (Object.is(a, b)) return true;
172
+ if (
173
+ typeof a !== "object" ||
174
+ typeof b !== "object" ||
175
+ a === null ||
176
+ b === null
177
+ )
178
+ return false;
179
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
180
+ if (Array.isArray(a) && Array.isArray(b)) {
181
+ if (a.length !== b.length) return false;
182
+ return a.every((item, index) => deepEqual(item, b[index]));
183
+ }
184
+ const left = a as Record<string, unknown>;
185
+ const right = b as Record<string, unknown>;
186
+ const keys = Object.keys(left).filter((key) => left[key] !== undefined);
187
+ const other = Object.keys(right).filter((key) => right[key] !== undefined);
188
+ if (keys.length !== other.length) return false;
189
+ return keys.every((key) => deepEqual(left[key], right[key]));
190
+ }
191
+
192
+ /**
193
+ * Gives the notes of `evaluated` (fresh from `song.ts`, content-hash ids)
194
+ * the ids of matching notes in `current` (the session, whose ids the TUI
195
+ * and agent may have chosen) so `diffScores(current, result)` updates
196
+ * notes in place instead of replacing them. A note matches by exact
197
+ * content first, then by track, pitch and start.
198
+ */
199
+ export function adoptNoteIds(
200
+ current: TrackScore,
201
+ evaluated: TrackScore,
202
+ ): TrackScore {
203
+ const exact = new Map<string, Note[]>();
204
+ const loose = new Map<string, Note[]>();
205
+ for (const note of current.notes) {
206
+ push(exact, exactKey(note), note);
207
+ push(loose, looseKey(note), note);
208
+ }
209
+ const taken = new Set<string>();
210
+ const adopted: Note[] = [];
211
+ const pending: Note[] = [];
212
+ for (const note of evaluated.notes) {
213
+ const match = take(exact, exactKey(note), taken);
214
+ if (match) {
215
+ taken.add(match.id);
216
+ adopted.push(match.id === note.id ? note : { ...note, id: match.id });
217
+ } else pending.push(note);
218
+ }
219
+ for (const note of pending) {
220
+ const match = take(loose, looseKey(note), taken);
221
+ if (match) {
222
+ taken.add(match.id);
223
+ adopted.push({ ...note, id: match.id });
224
+ } else adopted.push(note);
225
+ }
226
+ // A fresh content id can collide with an adopted one; keep ids unique.
227
+ const used = new Set<string>();
228
+ const unique = adopted.map((note) => {
229
+ let id = note.id;
230
+ for (let n = 2; used.has(id); n += 1) id = `${note.id.slice(0, 60)}-${n}`;
231
+ used.add(id);
232
+ return id === note.id ? note : { ...note, id };
233
+ });
234
+ return new TrackScore({ ...evaluated.toJSON(), notes: unique });
235
+ }
236
+
237
+ function exactKey(note: Note): string {
238
+ return `${note.trackId}|${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}`;
239
+ }
240
+
241
+ function looseKey(note: Note): string {
242
+ return `${note.trackId}|${note.pitch}|${note.startTick}`;
243
+ }
244
+
245
+ function push(map: Map<string, Note[]>, key: string, note: Note): void {
246
+ const list = map.get(key);
247
+ if (list) list.push(note);
248
+ else map.set(key, [note]);
249
+ }
250
+
251
+ function take(
252
+ map: Map<string, Note[]>,
253
+ key: string,
254
+ taken: Set<string>,
255
+ ): Note | undefined {
256
+ const list = map.get(key);
257
+ if (!list) return undefined;
258
+ const index = list.findIndex((note) => !taken.has(note.id));
259
+ if (index < 0) return undefined;
260
+ return list.splice(index, 1)[0];
261
+ }