@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
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Send effects on the stereo pair: delay and reverb. Without their
3
+ * optional parameters both render exactly as the original renderer did.
4
+ */
5
+ import type { Track } from "../../../core/score.ts";
6
+ import type { DecodedSample } from "../samples.ts";
7
+ import {
8
+ builtinImpulse,
9
+ convolveStereo,
10
+ sampleImpulse,
11
+ type Impulse,
12
+ } from "./convolution.ts";
13
+ import {
14
+ CONTROL_SAMPLES,
15
+ OnePole,
16
+ Param,
17
+ interpolateAutomation,
18
+ type EffectContext,
19
+ } from "./common.ts";
20
+
21
+ /** Delay length in seconds: `time` when set, otherwise `beats` at the tempo. */
22
+ export function delaySeconds(track: Track, tempoBpm: number): number {
23
+ const delay = track.delay;
24
+ if (!delay) return 0;
25
+ return delay.time !== undefined && delay.time > 0
26
+ ? delay.time
27
+ : (delay.beats * 60) / tempoBpm;
28
+ }
29
+
30
+ /**
31
+ * Stereo feedback delay. Absent `pingpong`, each side echoes its own input
32
+ * and feeds back into the opposite side (the original sound). With
33
+ * `pingpong`, the mono sum enters the left line only and every repeat
34
+ * crosses over, so echoes alternate left, right, left. `highcut` runs a
35
+ * one-pole low-pass on everything written into the lines, so each repeat
36
+ * is darker than the last. Feedback and mix follow their lanes. `wetOnly`
37
+ * (a shared orbit bus) replaces the input with the repeats alone.
38
+ */
39
+ export function applyDelay(
40
+ left: Float64Array,
41
+ right: Float64Array,
42
+ track: Track,
43
+ context: EffectContext,
44
+ wetOnly = false,
45
+ ): void {
46
+ const delay = track.delay;
47
+ if (!delay) return;
48
+ const feedbackLane = track.delayFeedbackAutomation ?? [];
49
+ const mixLane = track.delayMixAutomation ?? [];
50
+ if (delay.mix <= 0 && mixLane.length === 0) return;
51
+ const { sampleRate, samplesPerTick } = context;
52
+ const length = Math.max(
53
+ 1,
54
+ Math.round(delaySeconds(track, context.tempoBpm) * sampleRate),
55
+ );
56
+ const lineL = new Float64Array(length);
57
+ const lineR = new Float64Array(length);
58
+ let feedback = delay.feedback;
59
+ let mix = delay.mix;
60
+ const automated = feedbackLane.length > 0 || mixLane.length > 0;
61
+ const pingpong = delay.pingpong === true;
62
+ const highcut =
63
+ delay.highcut === undefined
64
+ ? undefined
65
+ : [
66
+ new OnePole(delay.highcut, sampleRate),
67
+ new OnePole(delay.highcut, sampleRate),
68
+ ];
69
+ for (let index = 0; index < left.length; index += 1) {
70
+ if (automated && index % CONTROL_SAMPLES === 0) {
71
+ const tick = index / samplesPerTick;
72
+ feedback = interpolateAutomation(feedbackLane, tick, delay.feedback);
73
+ mix = interpolateAutomation(mixLane, tick, delay.mix);
74
+ }
75
+ const slot = index % length;
76
+ const wetL = lineL[slot]!;
77
+ const wetR = lineR[slot]!;
78
+ const dryL = left[index]!;
79
+ const dryR = right[index]!;
80
+ let writeL: number;
81
+ let writeR: number;
82
+ if (pingpong) {
83
+ writeL = (dryL + dryR) * 0.5 + wetR * feedback;
84
+ writeR = wetL * feedback;
85
+ } else {
86
+ writeL = dryL + wetR * feedback;
87
+ writeR = dryR + wetL * feedback;
88
+ }
89
+ if (highcut) {
90
+ writeL = highcut[0]!.process(writeL);
91
+ writeR = highcut[1]!.process(writeR);
92
+ }
93
+ lineL[slot] = writeL;
94
+ lineR[slot] = writeR;
95
+ left[index] = (wetOnly ? 0 : dryL) + wetL * mix;
96
+ right[index] = (wetOnly ? 0 : dryR) + wetR * mix;
97
+ }
98
+ }
99
+
100
+ /** Seconds until the track's delay decays below -60 dB, bounded. */
101
+ export function delayTailFor(track: Track, tempoBpm: number): number {
102
+ if (!track.delay) return 0;
103
+ const feedback = Math.max(
104
+ track.delay.feedback,
105
+ ...(track.delayFeedbackAutomation ?? []).map((point) => point.value),
106
+ );
107
+ const repeats =
108
+ feedback <= 0.001 ? 1 : Math.min(64, Math.log(0.001) / Math.log(feedback));
109
+ return delaySeconds(track, tempoBpm) * (repeats + 1);
110
+ }
111
+
112
+ /** Freeverb comb and allpass tunings at 44.1 kHz; the right side is spread. */
113
+ const COMB_TUNING = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617] as const;
114
+ const ALLPASS_TUNING = [556, 441, 341, 225] as const;
115
+ const STEREO_SPREAD = 23;
116
+ const REVERB_INPUT_GAIN = 0.015;
117
+ const REVERB_WET_SCALE = 3;
118
+
119
+ /**
120
+ * Convolution wet level: an energy-normalized impulse on the summed pair,
121
+ * scaled so `mix` sounds as loud as the algorithmic tail at the same mix.
122
+ */
123
+ const IR_WET_SCALE = 2.1;
124
+
125
+ /** Impulses decoded from samples, by sha256 and rate (small: a few IRs). */
126
+ const SAMPLE_IMPULSES = new Map<string, Impulse>();
127
+ const MAX_SAMPLE_IMPULSES = 8;
128
+
129
+ /**
130
+ * The impulse a track's `reverb.ir` names at this rate: a generated
131
+ * built-in, or the decoded sample in `irs`. Undefined when the track has no
132
+ * `ir` or its sample did not load (the algorithmic reverb then plays).
133
+ */
134
+ export function reverbImpulse(
135
+ track: Track,
136
+ sampleRate: number,
137
+ irs?: ReadonlyMap<string, DecodedSample>,
138
+ ): Impulse | undefined {
139
+ const ir = track.reverb?.ir;
140
+ if (!ir) return undefined;
141
+ if (ir.src.startsWith("builtin:"))
142
+ return builtinImpulse(ir.src.slice("builtin:".length), sampleRate);
143
+ const sample = irs?.get(track.id);
144
+ if (!sample) return undefined;
145
+ const key = `${sample.sha256}@${sampleRate}`;
146
+ let impulse = SAMPLE_IMPULSES.get(key);
147
+ if (!impulse) {
148
+ impulse = sampleImpulse(
149
+ `${ir.src}#${sample.sha256}`,
150
+ sample.mono,
151
+ sample.sampleRate,
152
+ sampleRate,
153
+ );
154
+ if (SAMPLE_IMPULSES.size >= MAX_SAMPLE_IMPULSES)
155
+ SAMPLE_IMPULSES.delete(SAMPLE_IMPULSES.keys().next().value!);
156
+ SAMPLE_IMPULSES.set(key, impulse);
157
+ }
158
+ return impulse;
159
+ }
160
+
161
+ /** Seconds of reverb tail a render keeps for this track. */
162
+ export function reverbTailFor(
163
+ track: Track,
164
+ sampleRate = 44_100,
165
+ irs?: ReadonlyMap<string, DecodedSample>,
166
+ ): number {
167
+ const reverb = track.reverb;
168
+ const lane = track.fxAutomation?.["reverb-mix"] ?? [];
169
+ if (!reverb || (reverb.mix <= 0 && lane.length === 0)) return 0;
170
+ const impulse = reverbImpulse(track, sampleRate, irs);
171
+ const decay = impulse
172
+ ? impulse.left.length / sampleRate
173
+ : (reverb.fade ?? 1 + 3 * reverb.size);
174
+ return Math.min(20, decay + (reverb.predelay ?? 0));
175
+ }
176
+
177
+ /**
178
+ * Schroeder–Moorer reverb in the Freeverb arrangement: eight damped
179
+ * feedback combs then four allpasses per side, the right side detuned.
180
+ * `size` sets comb feedback and damping; `fade` instead sets each comb's
181
+ * feedback for an exact -60 dB decay time; `dim` sets the in-loop damping
182
+ * frequency, `lowpass` filters the input and `predelay` delays it.
183
+ * `wetOnly` (a shared orbit bus) replaces the input with the tail alone.
184
+ */
185
+ export function applyReverb(
186
+ left: Float64Array,
187
+ right: Float64Array,
188
+ track: Track,
189
+ context: EffectContext,
190
+ wetOnly = false,
191
+ ): void {
192
+ const reverb = track.reverb;
193
+ if (!reverb) return;
194
+ const mixParam = new Param(
195
+ reverb.mix,
196
+ track.fxAutomation?.["reverb-mix"],
197
+ context.samplesPerTick,
198
+ );
199
+ if (reverb.mix <= 0 && !mixParam.automated) return;
200
+ const { sampleRate } = context;
201
+ const impulse = reverbImpulse(track, sampleRate, context.irs);
202
+ if (impulse) {
203
+ applyConvolutionReverb(
204
+ left,
205
+ right,
206
+ track,
207
+ context,
208
+ impulse,
209
+ mixParam,
210
+ wetOnly,
211
+ );
212
+ return;
213
+ }
214
+ const scale = sampleRate / 44_100;
215
+ const sizeFeedback = 0.7 + 0.28 * reverb.size;
216
+ const damp =
217
+ reverb.dim === undefined
218
+ ? 0.2 + 0.3 * reverb.size
219
+ : Math.exp(
220
+ (-2 * Math.PI * Math.min(reverb.dim, sampleRate * 0.45)) / sampleRate,
221
+ );
222
+ let wet = reverb.mix * REVERB_WET_SCALE;
223
+ const channel = (spread: number) => ({
224
+ combs: COMB_TUNING.map((tuning) => {
225
+ const length = Math.max(1, Math.round((tuning + spread) * scale));
226
+ return {
227
+ buffer: new Float64Array(length),
228
+ index: 0,
229
+ store: 0,
230
+ feedback:
231
+ reverb.fade === undefined
232
+ ? sizeFeedback
233
+ : Math.min(0.995, 10 ** ((-3 * length) / sampleRate / reverb.fade)),
234
+ };
235
+ }),
236
+ allpasses: ALLPASS_TUNING.map((tuning) => ({
237
+ buffer: new Float64Array(
238
+ Math.max(1, Math.round((tuning + spread) * scale)),
239
+ ),
240
+ index: 0,
241
+ })),
242
+ });
243
+ const sides = [channel(0), channel(STEREO_SPREAD)] as const;
244
+ const process = (side: (typeof sides)[number], input: number): number => {
245
+ let output = 0;
246
+ for (const comb of side.combs) {
247
+ const delayed = comb.buffer[comb.index]!;
248
+ comb.store = delayed * (1 - damp) + comb.store * damp;
249
+ comb.buffer[comb.index] = input + comb.store * comb.feedback;
250
+ comb.index = comb.index + 1 === comb.buffer.length ? 0 : comb.index + 1;
251
+ output += delayed;
252
+ }
253
+ for (const allpass of side.allpasses) {
254
+ const delayed = allpass.buffer[allpass.index]!;
255
+ allpass.buffer[allpass.index] = output + delayed * 0.5;
256
+ output = delayed - output;
257
+ allpass.index =
258
+ allpass.index + 1 === allpass.buffer.length ? 0 : allpass.index + 1;
259
+ }
260
+ return output;
261
+ };
262
+ const inputFilter =
263
+ reverb.lowpass === undefined
264
+ ? undefined
265
+ : new OnePole(reverb.lowpass, sampleRate);
266
+ const predelaySamples = Math.round((reverb.predelay ?? 0) * sampleRate);
267
+ const predelay =
268
+ predelaySamples > 0 ? new Float64Array(predelaySamples) : undefined;
269
+ for (let index = 0; index < left.length; index += 1) {
270
+ if (mixParam.automated && index % CONTROL_SAMPLES === 0)
271
+ wet = mixParam.at(index) * REVERB_WET_SCALE;
272
+ let input = (left[index]! + right[index]!) * REVERB_INPUT_GAIN;
273
+ if (inputFilter) input = inputFilter.process(input);
274
+ if (predelay) {
275
+ const slot = index % predelaySamples;
276
+ const delayed = predelay[slot]!;
277
+ predelay[slot] = input;
278
+ input = delayed;
279
+ }
280
+ if (wetOnly) {
281
+ left[index] = process(sides[0], input) * wet;
282
+ right[index] = process(sides[1], input) * wet;
283
+ } else {
284
+ left[index]! += process(sides[0], input) * wet;
285
+ right[index]! += process(sides[1], input) * wet;
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * `reverb.ir`: the summed pair (through `lowpass` and `predelay`, as the
292
+ * algorithmic reverb) convolved with the impulse and added at `mix`.
293
+ * `size`, `fade` and `dim` describe the algorithmic tail and do nothing
294
+ * here: the impulse is the room.
295
+ */
296
+ function applyConvolutionReverb(
297
+ left: Float64Array,
298
+ right: Float64Array,
299
+ track: Track,
300
+ context: EffectContext,
301
+ impulse: Impulse,
302
+ mixParam: Param,
303
+ wetOnly: boolean,
304
+ ): void {
305
+ const reverb = track.reverb!;
306
+ const { sampleRate } = context;
307
+ const input = new Float64Array(left.length);
308
+ const inputFilter =
309
+ reverb.lowpass === undefined
310
+ ? undefined
311
+ : new OnePole(reverb.lowpass, sampleRate);
312
+ const offset = Math.round((reverb.predelay ?? 0) * sampleRate);
313
+ for (let index = 0; index + offset < left.length; index += 1) {
314
+ let value = (left[index]! + right[index]!) * 0.5;
315
+ if (inputFilter) value = inputFilter.process(value);
316
+ input[index + offset] = value;
317
+ }
318
+ if (wetOnly) {
319
+ left.fill(0);
320
+ right.fill(0);
321
+ }
322
+ let wet = reverb.mix * IR_WET_SCALE;
323
+ convolveStereo(
324
+ input,
325
+ impulse,
326
+ left,
327
+ right,
328
+ wet,
329
+ mixParam.automated
330
+ ? (index) => {
331
+ if (index % CONTROL_SAMPLES === 0)
332
+ wet = mixParam.at(index) * IR_WET_SCALE;
333
+ return wet;
334
+ }
335
+ : undefined,
336
+ );
337
+ }