@hraness/dawg 0.0.0-stage → 0.3.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 (113) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +327 -0
  3. package/LICENSE +21 -0
  4. package/README.md +213 -2
  5. package/core/diff.ts +249 -0
  6. package/core/drums.ts +102 -0
  7. package/core/key.ts +43 -0
  8. package/core/loop.ts +78 -0
  9. package/core/pitch.ts +60 -0
  10. package/core/score.ts +1388 -0
  11. package/core/sdk/eval-child.ts +113 -0
  12. package/core/sdk/eval.ts +257 -0
  13. package/core/sdk/print.ts +393 -0
  14. package/core/sdk/v1.ts +954 -0
  15. package/core/slug.ts +19 -0
  16. package/package.json +45 -4
  17. package/src/agent/agent.ts +853 -0
  18. package/src/agent/brief.ts +160 -0
  19. package/src/agent/gateway.ts +441 -0
  20. package/src/agent/models.ts +633 -0
  21. package/src/agent/ops.ts +157 -0
  22. package/src/agent/planner.ts +259 -0
  23. package/src/agent/provider.ts +454 -0
  24. package/src/agent/sse.ts +114 -0
  25. package/src/agent/tools.ts +1373 -0
  26. package/src/agent/usage.ts +296 -0
  27. package/src/agent/workspace.ts +683 -0
  28. package/src/agent/xcb-agent.ts +262 -0
  29. package/src/agent/xcb.ts +579 -0
  30. package/src/audio/click.ts +125 -0
  31. package/src/audio/clock.ts +68 -0
  32. package/src/audio/engine.ts +841 -0
  33. package/src/audio/live.ts +152 -0
  34. package/src/audio/lock.ts +57 -0
  35. package/src/audio/player.ts +134 -0
  36. package/src/audio/render-worker.ts +68 -0
  37. package/src/audio/renderer.ts +174 -0
  38. package/src/audio/sampler.ts +292 -0
  39. package/src/audio/samples.ts +683 -0
  40. package/src/audio/wav.ts +861 -0
  41. package/src/auth/cli.ts +231 -0
  42. package/src/auth/credentials.ts +411 -0
  43. package/src/auth/discover.ts +481 -0
  44. package/src/auth/login.ts +1191 -0
  45. package/src/auth/openrouter.ts +206 -0
  46. package/src/auth/picker.ts +282 -0
  47. package/src/auth/runner.ts +207 -0
  48. package/src/auth/tui.ts +107 -0
  49. package/src/commands/edit.ts +170 -0
  50. package/src/commands/help.ts +247 -0
  51. package/src/commands/history.ts +69 -0
  52. package/src/commands/music.ts +461 -0
  53. package/src/commands/sample.ts +302 -0
  54. package/src/daemon.ts +31 -0
  55. package/src/main.ts +2209 -0
  56. package/src/media/analyze.ts +364 -0
  57. package/src/media/backend.ts +253 -0
  58. package/src/media/cli.ts +173 -0
  59. package/src/media/download.ts +281 -0
  60. package/src/media/dsp.ts +281 -0
  61. package/src/media/import.ts +130 -0
  62. package/src/media/lyrics.ts +201 -0
  63. package/src/media/notes.ts +363 -0
  64. package/src/media/paths.ts +168 -0
  65. package/src/media/process.ts +226 -0
  66. package/src/media/registry.ts +9 -0
  67. package/src/media/sidecar.ts +72 -0
  68. package/src/media/stemdeck.ts +254 -0
  69. package/src/media/stems.ts +173 -0
  70. package/src/media/tools.ts +292 -0
  71. package/src/media/types.ts +92 -0
  72. package/src/media/vendor/basic-pitch.ts +261 -0
  73. package/src/media/vendor/drums.ts +817 -0
  74. package/src/media/vendor/grid.ts +203 -0
  75. package/src/media/vendor/util.ts +139 -0
  76. package/src/media/vendor/wav.ts +233 -0
  77. package/src/project/check.ts +80 -0
  78. package/src/project/init.ts +253 -0
  79. package/src/project/sync.ts +432 -0
  80. package/src/project/typecheck.ts +149 -0
  81. package/src/render.ts +121 -0
  82. package/src/session/attach.ts +181 -0
  83. package/src/session/client.ts +498 -0
  84. package/src/session/daemon.ts +740 -0
  85. package/src/session/delta.ts +249 -0
  86. package/src/session/list.ts +180 -0
  87. package/src/session/lock.ts +92 -0
  88. package/src/session/meta.ts +253 -0
  89. package/src/session/naming.ts +430 -0
  90. package/src/session/port.ts +481 -0
  91. package/src/session/presence.ts +159 -0
  92. package/src/session/protocol.ts +618 -0
  93. package/src/session/rebase.ts +168 -0
  94. package/src/session/store.ts +581 -0
  95. package/src/tui/menu.ts +1083 -0
  96. package/src/tui/play-mode.ts +442 -0
  97. package/src/tui/play-session.ts +636 -0
  98. package/src/web/fetch.ts +340 -0
  99. package/src/web/http.ts +137 -0
  100. package/src/web/search.ts +681 -0
  101. package/tui/activity.ts +364 -0
  102. package/tui/app.ts +1372 -0
  103. package/tui/drums.ts +65 -0
  104. package/tui/highway.ts +921 -0
  105. package/tui/input.ts +63 -0
  106. package/tui/keys.ts +102 -0
  107. package/tui/layers.ts +80 -0
  108. package/tui/play-strip.ts +143 -0
  109. package/tui/prompt.ts +609 -0
  110. package/tui/render.ts +124 -0
  111. package/tui/screen.ts +247 -0
  112. package/tui/text.ts +72 -0
  113. package/tui/theme.ts +451 -0
@@ -0,0 +1,261 @@
1
+ // Vendored from soundfish `lib/song-import/basic-pitch.ts` (same owner): the CSV parser and per-stem parameters.
2
+ import type { TimedNote } from "../types.ts";
3
+ import { clamp, decodeUtf8 } from "./util.ts";
4
+
5
+ const MAX_NOTES = 100_000;
6
+
7
+ const HEADER_ALIASES = {
8
+ start: [
9
+ "start_time_s",
10
+ "onset_time_s",
11
+ "onset_time",
12
+ "onset",
13
+ "start_time",
14
+ "start",
15
+ ],
16
+ end: [
17
+ "end_time_s",
18
+ "offset_time_s",
19
+ "offset_time",
20
+ "offset",
21
+ "end_time",
22
+ "end",
23
+ ],
24
+ pitch: ["pitch_midi", "midi_pitch", "pitch"],
25
+ amplitude: ["amplitude", "velocity"],
26
+ confidence: ["confidence", "note_confidence"],
27
+ } as const;
28
+
29
+ function parseCsvRows(text: string): readonly string[][] {
30
+ const rows: string[][] = [];
31
+ let row: string[] = [];
32
+ let field = "";
33
+ let quoted = false;
34
+ for (let index = 0; index < text.length; index += 1) {
35
+ const character = text[index]!;
36
+ if (quoted) {
37
+ if (character === '"' && text[index + 1] === '"') {
38
+ field += '"';
39
+ index += 1;
40
+ } else if (character === '"') {
41
+ quoted = false;
42
+ } else {
43
+ field += character;
44
+ }
45
+ continue;
46
+ }
47
+ if (character === '"') quoted = true;
48
+ else if (character === ",") {
49
+ row.push(field);
50
+ field = "";
51
+ } else if (character === "\n") {
52
+ row.push(field.replace(/\r$/u, ""));
53
+ if (row.some((value) => value.length > 0)) rows.push(row);
54
+ row = [];
55
+ field = "";
56
+ } else field += character;
57
+ }
58
+ if (quoted)
59
+ throw new Error("Basic Pitch note CSV ended inside a quoted field.");
60
+ row.push(field.replace(/\r$/u, ""));
61
+ if (row.some((value) => value.length > 0)) rows.push(row);
62
+ // Leading `#` lines are comments (dawg's fixtures carry attribution there).
63
+ return rows.filter((entry) => !entry[0]?.startsWith("#"));
64
+ }
65
+
66
+ function columnMatch(
67
+ headers: readonly string[],
68
+ aliases: readonly string[],
69
+ label: string,
70
+ ): { index: number; alias: string } {
71
+ for (const alias of aliases) {
72
+ const index = headers.indexOf(alias);
73
+ if (index >= 0) return { index, alias };
74
+ }
75
+ throw new Error(
76
+ `Basic Pitch note CSV is missing a recognized ${label} column.`,
77
+ );
78
+ }
79
+
80
+ function optionalColumnIndex(
81
+ headers: readonly string[],
82
+ aliases: readonly string[],
83
+ ): number | undefined {
84
+ for (const alias of aliases) {
85
+ const index = headers.indexOf(alias);
86
+ if (index >= 0) return index;
87
+ }
88
+ return undefined;
89
+ }
90
+
91
+ /** Parses `<base>_basic_pitch.csv` (`--save-note-events`) into seconds-based notes. */
92
+ export function parseBasicPitchNoteCsv(
93
+ bytes: Uint8Array,
94
+ ): readonly TimedNote[] {
95
+ const rows = parseCsvRows(decodeUtf8(bytes, "Basic Pitch note CSV"));
96
+ const header = rows[0]?.map((value) =>
97
+ value.trim().toLowerCase().replace(/^/u, ""),
98
+ );
99
+ if (!header || header.length === 0)
100
+ throw new Error("Basic Pitch note CSV is empty.");
101
+ const startIndex = columnMatch(
102
+ header,
103
+ HEADER_ALIASES.start,
104
+ "onset/start",
105
+ ).index;
106
+ const endIndex = columnMatch(header, HEADER_ALIASES.end, "offset/end").index;
107
+ const pitchIndex = columnMatch(
108
+ header,
109
+ HEADER_ALIASES.pitch,
110
+ "MIDI pitch",
111
+ ).index;
112
+ const amplitudeColumn = columnMatch(
113
+ header,
114
+ HEADER_ALIASES.amplitude,
115
+ "amplitude/velocity",
116
+ );
117
+ const amplitudeIndex = amplitudeColumn.index;
118
+ const confidenceIndex = optionalColumnIndex(
119
+ header,
120
+ HEADER_ALIASES.confidence,
121
+ );
122
+ if (rows.length - 1 > MAX_NOTES)
123
+ throw new Error(`Basic Pitch produced more than ${MAX_NOTES} notes.`);
124
+
125
+ const notes: TimedNote[] = [];
126
+ for (let index = 1; index < rows.length; index += 1) {
127
+ const row = rows[index]!;
128
+ if (
129
+ row[startIndex]?.trim() === "" ||
130
+ row[endIndex]?.trim() === "" ||
131
+ row[pitchIndex]?.trim() === "" ||
132
+ row[amplitudeIndex]?.trim() === ""
133
+ )
134
+ continue;
135
+ const startSeconds = Number(row[startIndex]);
136
+ const endSeconds = Number(row[endIndex]);
137
+ const rawPitch = Number(row[pitchIndex]);
138
+ const amplitude = Number(row[amplitudeIndex]);
139
+ const confidenceText =
140
+ confidenceIndex === undefined ? undefined : row[confidenceIndex]?.trim();
141
+ const confidence =
142
+ confidenceText === undefined || confidenceText === ""
143
+ ? undefined
144
+ : Number(confidenceText);
145
+ if (
146
+ !Number.isFinite(startSeconds) ||
147
+ !Number.isFinite(endSeconds) ||
148
+ !Number.isFinite(rawPitch) ||
149
+ !Number.isFinite(amplitude) ||
150
+ startSeconds < 0 ||
151
+ endSeconds <= startSeconds ||
152
+ endSeconds > 7_200
153
+ ) {
154
+ continue;
155
+ }
156
+ const pitch = Math.round(rawPitch);
157
+ if (pitch < 0 || pitch > 127) continue;
158
+ // dawg velocities are 0..1; Basic Pitch writes 1..127 under "velocity"
159
+ // and 0..1 under "amplitude".
160
+ const velocity127 =
161
+ amplitudeColumn.alias === "velocity"
162
+ ? clamp(Math.round(amplitude), 1, 127)
163
+ : clamp(Math.round(1 + clamp(amplitude, 0, 1) * 126), 1, 127);
164
+ const velocity = Math.round((velocity127 / 127) * 100) / 100;
165
+ notes.push({
166
+ pitch,
167
+ startSeconds,
168
+ endSeconds,
169
+ velocity,
170
+ ...(confidence === undefined || !Number.isFinite(confidence)
171
+ ? {}
172
+ : { confidence: clamp(confidence, 0, 1) }),
173
+ });
174
+ }
175
+ return notes.sort(
176
+ (left, right) =>
177
+ left.startSeconds - right.startSeconds ||
178
+ left.pitch - right.pitch ||
179
+ left.endSeconds - right.endSeconds ||
180
+ right.velocity - left.velocity,
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Basic Pitch parameters chosen per instrument. Bass and vocals get frequency
186
+ * bounds that exclude bleed outside their register; the polyphonic stems need
187
+ * a firmer onset and a shorter minimum note than Basic Pitch's defaults
188
+ * (onset 0.5, 127.7 ms) so chords stop fragmenting into ghost re-triggers.
189
+ */
190
+ export type BasicPitchStemParameters = Readonly<{
191
+ onsetThreshold: number;
192
+ minimumNoteLengthMs: number;
193
+ minimumFrequencyHz?: number;
194
+ maximumFrequencyHz?: number;
195
+ }>;
196
+
197
+ export const PITCHED_KINDS = [
198
+ "bass",
199
+ "vocals",
200
+ "guitar",
201
+ "piano",
202
+ "other",
203
+ ] as const;
204
+ export type PitchedKind = (typeof PITCHED_KINDS)[number];
205
+
206
+ const DEFAULT_ONSET_THRESHOLD = 0.5;
207
+ const DEFAULT_MINIMUM_NOTE_LENGTH_MS = 127.7;
208
+ const POLYPHONIC_PARAMETERS: BasicPitchStemParameters = {
209
+ onsetThreshold: 0.6,
210
+ minimumNoteLengthMs: 100,
211
+ };
212
+
213
+ export const BASIC_PITCH_STEM_PARAMETERS: Readonly<
214
+ Record<PitchedKind, BasicPitchStemParameters>
215
+ > = {
216
+ bass: {
217
+ onsetThreshold: DEFAULT_ONSET_THRESHOLD,
218
+ minimumNoteLengthMs: DEFAULT_MINIMUM_NOTE_LENGTH_MS,
219
+ minimumFrequencyHz: 30,
220
+ maximumFrequencyHz: 400,
221
+ },
222
+ vocals: {
223
+ onsetThreshold: DEFAULT_ONSET_THRESHOLD,
224
+ minimumNoteLengthMs: DEFAULT_MINIMUM_NOTE_LENGTH_MS,
225
+ minimumFrequencyHz: 80,
226
+ maximumFrequencyHz: 1_100,
227
+ },
228
+ guitar: POLYPHONIC_PARAMETERS,
229
+ piano: POLYPHONIC_PARAMETERS,
230
+ other: POLYPHONIC_PARAMETERS,
231
+ };
232
+
233
+ function numberArgument(value: number): string {
234
+ if (!Number.isFinite(value) || value < 0)
235
+ throw new Error("Basic Pitch parameters must be finite and non-negative.");
236
+ return String(value);
237
+ }
238
+
239
+ /** Command-line form of one parameter set, in a fixed order. */
240
+ export function basicPitchParameterArguments(
241
+ parameters: BasicPitchStemParameters,
242
+ ): readonly string[] {
243
+ return [
244
+ "--onset-threshold",
245
+ numberArgument(parameters.onsetThreshold),
246
+ "--minimum-note-length",
247
+ numberArgument(parameters.minimumNoteLengthMs),
248
+ ...(parameters.minimumFrequencyHz === undefined
249
+ ? []
250
+ : ["--minimum-frequency", numberArgument(parameters.minimumFrequencyHz)]),
251
+ ...(parameters.maximumFrequencyHz === undefined
252
+ ? []
253
+ : ["--maximum-frequency", numberArgument(parameters.maximumFrequencyHz)]),
254
+ ];
255
+ }
256
+
257
+ /** `<name>.wav` → `<name>_basic_pitch`, the stem of Basic Pitch's output files. */
258
+ export function basicPitchOutputBase(fileName: string): string {
259
+ const dot = fileName.lastIndexOf(".");
260
+ return `${dot < 0 ? fileName : fileName.slice(0, dot)}_basic_pitch`;
261
+ }