@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/rhythm.ts ADDED
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Rhythm rows on a score: the glue between `core/euclid.ts` generators and
3
+ * the stored notes.
4
+ *
5
+ * A track's `rhythm` rows each own one voice lane (a drum voice, a one-shot
6
+ * sampler voice, or a pitch). `withTrackRhythm` replaces the rows and
7
+ * regenerates exactly those lanes' notes, so an edit is one next score and
8
+ * therefore one revision and one undo step. Other notes on the track, and
9
+ * every other track, are untouched. Generated notes carry deterministic ids
10
+ * (`rh.<track hash>.<voice>.<n>`), so the same rows always produce the same
11
+ * score and `diffScores` sees only real changes.
12
+ */
13
+ import { isDrumInstrument, parseDrumVoice, drumVoicePitch } from "./drums.ts";
14
+ import { expandRow, rowSummary, type RhythmRow } from "./euclid.ts";
15
+ import { pitchToMidi } from "./pitch.ts";
16
+ import {
17
+ SCORE_LIMITS,
18
+ ScoreValidationError,
19
+ TrackScore,
20
+ isSamplerInstrument,
21
+ samplerVoiceSlots,
22
+ type Note,
23
+ type Track,
24
+ } from "./score.ts";
25
+
26
+ /** MIDI pitch a row's voice plays on `track`, or undefined when unknown. */
27
+ export function rhythmVoicePitch(
28
+ track: Track,
29
+ voice: string,
30
+ ): number | undefined {
31
+ const name = voice.trim();
32
+ if (isSamplerInstrument(track.instrument) && track.sampler) {
33
+ const slot = samplerVoiceSlots(track.sampler).get(name);
34
+ if (slot !== undefined) return slot;
35
+ }
36
+ if (isDrumInstrument(track.instrument)) {
37
+ const drum = parseDrumVoice(name);
38
+ if (drum) return drumVoicePitch(drum);
39
+ }
40
+ if (/^\d{1,3}$/.test(name)) {
41
+ const midi = Number(name);
42
+ return midi <= 127 ? midi : undefined;
43
+ }
44
+ const midi = pitchToMidi(name);
45
+ return Number.isNaN(midi) ? undefined : midi;
46
+ }
47
+
48
+ function trackHash(trackId: string): string {
49
+ let hash = 0x811c9dc5;
50
+ for (let index = 0; index < trackId.length; index += 1) {
51
+ hash ^= trackId.charCodeAt(index);
52
+ hash = Math.imul(hash, 0x01000193);
53
+ }
54
+ return (hash >>> 0).toString(16).padStart(8, "0");
55
+ }
56
+
57
+ function idVoice(voice: string): string {
58
+ return voice.replace(/[^A-Za-z0-9_#-]/g, "_").slice(0, 32);
59
+ }
60
+
61
+ /** The notes `row` generates on `track` over the score's loop. */
62
+ export function expandTrackRow(
63
+ score: TrackScore,
64
+ track: Track,
65
+ row: RhythmRow,
66
+ ): Note[] {
67
+ const pitch = rhythmVoicePitch(track, row.voice);
68
+ if (pitch === undefined)
69
+ throw new ScoreValidationError(
70
+ `track ${track.id} has no voice "${row.voice}"`,
71
+ "invalid-track",
72
+ );
73
+ const loopTicks = score.bars * score.beatsPerBar * score.ticksPerBeat;
74
+ const prefix = `rh.${trackHash(track.id)}.${idVoice(row.voice)}.`;
75
+ return expandRow(row, { ticksPerBeat: score.ticksPerBeat, loopTicks }).map(
76
+ (hit, index) => ({
77
+ id: `${prefix}${index}`,
78
+ trackId: track.id,
79
+ startTick: hit.startTick,
80
+ durationTicks: Math.min(hit.durationTicks, SCORE_LIMITS.maxTick),
81
+ pitch,
82
+ velocity: hit.velocity,
83
+ }),
84
+ );
85
+ }
86
+
87
+ /** Pitches the rows own on `track` (unresolvable voices are skipped). */
88
+ function ownedPitches(track: Track, rows: readonly RhythmRow[]): Set<number> {
89
+ const pitches = new Set<number>();
90
+ for (const row of rows) {
91
+ const pitch = rhythmVoicePitch(track, row.voice);
92
+ if (pitch !== undefined) pitches.add(pitch);
93
+ }
94
+ return pitches;
95
+ }
96
+
97
+ /**
98
+ * The score with `trackId`'s rows replaced by `rows` (empty removes them)
99
+ * and the lanes of the old and new rows regenerated. Throws
100
+ * `ScoreValidationError` for an unknown track or voice, or past the note
101
+ * limit.
102
+ */
103
+ export function withTrackRhythm(
104
+ score: TrackScore,
105
+ trackId: string,
106
+ rows: readonly RhythmRow[],
107
+ ): TrackScore {
108
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
109
+ if (!track)
110
+ throw new ScoreValidationError(`no track ${trackId}`, "invalid-track");
111
+ const nextTrack: Track = { ...track };
112
+ const tracks = score.tracks.map((candidate) =>
113
+ candidate.id === trackId
114
+ ? { ...candidate, rhythm: rows.length > 0 ? rows : null }
115
+ : candidate,
116
+ );
117
+ const cleared = ownedPitches(track, [...(track.rhythm ?? []), ...rows]);
118
+ const generated = rows.flatMap((row) =>
119
+ expandTrackRow(score, nextTrack, row),
120
+ );
121
+ const notes = [
122
+ ...score.notes.filter(
123
+ (note) => note.trackId !== trackId || !cleared.has(note.pitch),
124
+ ),
125
+ ...generated,
126
+ ];
127
+ if (notes.length > SCORE_LIMITS.maxNotes)
128
+ throw new ScoreValidationError(
129
+ `score cannot contain more than ${SCORE_LIMITS.maxNotes} notes`,
130
+ "invalid-score",
131
+ );
132
+ return new TrackScore({ ...score.toJSON(), tracks, notes });
133
+ }
134
+
135
+ /** Adds or replaces the row for `row.voice` on the track. */
136
+ export function setRhythmRow(
137
+ score: TrackScore,
138
+ trackId: string,
139
+ row: RhythmRow,
140
+ ): TrackScore {
141
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
142
+ const current = track?.rhythm ?? [];
143
+ const index = current.findIndex((existing) => existing.voice === row.voice);
144
+ const rows =
145
+ index < 0
146
+ ? [...current, row]
147
+ : current.map((existing, at) => (at === index ? row : existing));
148
+ return withTrackRhythm(score, trackId, rows);
149
+ }
150
+
151
+ /**
152
+ * Drops the row for `voice`. With `keepNotes` its generated notes stay as
153
+ * plain notes (the row is "frozen"); otherwise the lane is cleared.
154
+ */
155
+ export function removeRhythmRow(
156
+ score: TrackScore,
157
+ trackId: string,
158
+ voice: string,
159
+ keepNotes = false,
160
+ ): TrackScore {
161
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
162
+ const current = track?.rhythm ?? [];
163
+ const rows = current.filter((row) => row.voice !== voice);
164
+ if (rows.length === current.length) return score;
165
+ if (!keepNotes) return withTrackRhythm(score, trackId, rows);
166
+ return new TrackScore({
167
+ ...score.toJSON(),
168
+ tracks: score.tracks.map((candidate) =>
169
+ candidate.id === trackId
170
+ ? { ...candidate, rhythm: rows.length > 0 ? rows : null }
171
+ : candidate,
172
+ ),
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Every track's rows regenerated for the current loop length (after a
178
+ * `bars` change, or for a score fresh from `song.ts`). Returns `score`
179
+ * itself when nothing changes.
180
+ */
181
+ export function refreshRhythm(score: TrackScore): TrackScore {
182
+ let next = score;
183
+ for (const track of score.tracks)
184
+ if (track.rhythm && !rhythmInSync(next, track.id))
185
+ next = withTrackRhythm(next, track.id, track.rhythm);
186
+ return next;
187
+ }
188
+
189
+ /** True when the track's owned lanes hold exactly what its rows generate. */
190
+ export function rhythmInSync(score: TrackScore, trackId: string): boolean {
191
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
192
+ if (!track?.rhythm) return true;
193
+ let expected: Note[];
194
+ try {
195
+ expected = track.rhythm.flatMap((row) => expandTrackRow(score, track, row));
196
+ } catch {
197
+ return false;
198
+ }
199
+ const owned = ownedPitches(track, track.rhythm);
200
+ const actual = score.notes.filter(
201
+ (note) => note.trackId === trackId && owned.has(note.pitch),
202
+ );
203
+ return sameNotes(expected, actual);
204
+ }
205
+
206
+ /** True when `row`'s lane on the track holds exactly what the row generates. */
207
+ export function rowInSync(
208
+ score: TrackScore,
209
+ track: Track,
210
+ row: RhythmRow,
211
+ ): boolean {
212
+ const pitch = rhythmVoicePitch(track, row.voice);
213
+ if (pitch === undefined) return false;
214
+ let expected: Note[];
215
+ try {
216
+ expected = expandTrackRow(score, track, row);
217
+ } catch {
218
+ return false;
219
+ }
220
+ const actual = score.notes.filter(
221
+ (note) => note.trackId === track.id && note.pitch === pitch,
222
+ );
223
+ return sameNotes(expected, actual);
224
+ }
225
+
226
+ function sameNotes(
227
+ expected: readonly Note[],
228
+ actual: readonly Note[],
229
+ ): boolean {
230
+ if (actual.length !== expected.length) return false;
231
+ const key = (note: Note) =>
232
+ `${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}`;
233
+ const want = new Map<string, number>();
234
+ for (const note of expected)
235
+ want.set(key(note), (want.get(key(note)) ?? 0) + 1);
236
+ for (const note of actual) {
237
+ const left = want.get(key(note));
238
+ if (!left) return false;
239
+ want.set(key(note), left - 1);
240
+ }
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Keeps rows and their lanes consistent after an arbitrary edit from
246
+ * `previous` to `next`: when the loop grid changed (bars, meter, ticks per
247
+ * beat) every row regenerates; otherwise a row whose lane was hand-edited
248
+ * is frozen (dropped, its notes kept as plain notes). Returns `next` itself
249
+ * when nothing needs to change.
250
+ */
251
+ export function reconcileRhythm(
252
+ previous: TrackScore,
253
+ next: TrackScore,
254
+ ): TrackScore {
255
+ const regridded =
256
+ previous.bars !== next.bars ||
257
+ previous.beatsPerBar !== next.beatsPerBar ||
258
+ previous.ticksPerBeat !== next.ticksPerBeat;
259
+ let out = next;
260
+ for (const track of next.tracks) {
261
+ if (!track.rhythm) continue;
262
+ if (regridded) {
263
+ if (!rhythmInSync(out, track.id))
264
+ out = withTrackRhythm(out, track.id, track.rhythm);
265
+ continue;
266
+ }
267
+ const current = out.tracks.find((candidate) => candidate.id === track.id)!;
268
+ const kept = track.rhythm.filter((row) => rowInSync(out, current, row));
269
+ if (kept.length !== track.rhythm.length)
270
+ out = new TrackScore({
271
+ ...out.toJSON(),
272
+ tracks: out.tracks.map((candidate) =>
273
+ candidate.id === track.id
274
+ ? { ...candidate, rhythm: kept.length > 0 ? kept : null }
275
+ : candidate,
276
+ ),
277
+ });
278
+ }
279
+ return out;
280
+ }
281
+
282
+ /** `kick E(4,16) · hat E(7,16,r2)` for a status line. */
283
+ export function rhythmSummary(track: Track): string {
284
+ return (track.rhythm ?? [])
285
+ .map((row) => `${row.voice} ${rowSummary(row)}`)
286
+ .join(" · ");
287
+ }