@hraness/dawg 0.5.0 → 0.6.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 (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/DAWG.md +269 -13
  3. package/README.md +4 -4
  4. package/core/diff.ts +4 -0
  5. package/core/fx.ts +437 -2
  6. package/core/granular.ts +528 -0
  7. package/core/instruments.ts +281 -0
  8. package/core/keys.ts +386 -0
  9. package/core/resonators.ts +574 -0
  10. package/core/score.ts +181 -6
  11. package/core/sdk/eval-child.ts +2 -0
  12. package/core/sdk/print.ts +169 -13
  13. package/core/sdk/sync-instruments.ts +58 -0
  14. package/core/sdk/v1.ts +1114 -21
  15. package/core/sections.ts +34 -8
  16. package/core/strings.ts +845 -0
  17. package/guides/effects.md +3 -1
  18. package/guides/sounds.md +4 -0
  19. package/guides/tempo.md +1 -0
  20. package/package.json +1 -1
  21. package/src/agent/agent.ts +7 -0
  22. package/src/agent/granular-tools.ts +138 -0
  23. package/src/agent/models.ts +4 -4
  24. package/src/agent/ops.ts +39 -2
  25. package/src/agent/preview-tool.ts +6 -1
  26. package/src/agent/tools.ts +538 -14
  27. package/src/agent/xcb-agent.ts +7 -0
  28. package/src/audio/arrange.ts +21 -3
  29. package/src/audio/dsp/bank.ts +233 -0
  30. package/src/audio/dsp/fft.ts +6 -0
  31. package/src/audio/dsp/filters.ts +57 -0
  32. package/src/audio/dsp/interp.ts +112 -0
  33. package/src/audio/dsp/modal.ts +684 -0
  34. package/src/audio/dsp/onset.ts +197 -0
  35. package/src/audio/dsp/oversample.ts +202 -0
  36. package/src/audio/dsp/rng.ts +37 -0
  37. package/src/audio/dsp/shape.ts +81 -0
  38. package/src/audio/dsp/stft.ts +73 -0
  39. package/src/audio/dsp/window.ts +77 -0
  40. package/src/audio/effects/chain.ts +6 -1
  41. package/src/audio/effects/rig/cab.ts +99 -0
  42. package/src/audio/effects/rig/filters.ts +152 -0
  43. package/src/audio/effects/rig/gate.ts +39 -0
  44. package/src/audio/effects/rig/head.ts +487 -0
  45. package/src/audio/effects/rig/index.ts +170 -0
  46. package/src/audio/effects/rig/section.ts +78 -0
  47. package/src/audio/effects/rig/stomp.ts +238 -0
  48. package/src/audio/engine.ts +10 -1
  49. package/src/audio/fit.ts +402 -0
  50. package/src/audio/granular.ts +664 -0
  51. package/src/audio/instrument-check.ts +133 -0
  52. package/src/audio/instruments.ts +111 -0
  53. package/src/audio/keys/dsp.ts +323 -0
  54. package/src/audio/keys/engine.ts +361 -0
  55. package/src/audio/keys/piano.ts +432 -0
  56. package/src/audio/live-worker.ts +54 -0
  57. package/src/audio/live.ts +231 -12
  58. package/src/audio/loudness.ts +65 -20
  59. package/src/audio/preview.ts +2 -0
  60. package/src/audio/resonators.ts +287 -0
  61. package/src/audio/sampler.ts +163 -17
  62. package/src/audio/samples.ts +30 -7
  63. package/src/audio/strings/body.ts +250 -0
  64. package/src/audio/strings/engine.ts +369 -0
  65. package/src/audio/strings/loop.ts +119 -0
  66. package/src/audio/strings/measure.test-helpers.ts +198 -0
  67. package/src/audio/strings/pluck.ts +354 -0
  68. package/src/audio/warp.ts +61 -0
  69. package/src/audio/wav.ts +78 -6
  70. package/src/commands/expression.ts +9 -2
  71. package/src/commands/fit.ts +135 -0
  72. package/src/commands/fx.ts +31 -0
  73. package/src/commands/granular.ts +427 -0
  74. package/src/commands/help.ts +74 -3
  75. package/src/commands/keys.ts +353 -0
  76. package/src/commands/modal.ts +247 -0
  77. package/src/commands/rig.ts +260 -0
  78. package/src/commands/sample.ts +3 -0
  79. package/src/commands/string.ts +175 -0
  80. package/src/commands/time.ts +4 -1
  81. package/src/main.ts +210 -8
  82. package/src/project/check.ts +13 -0
  83. package/src/render.ts +33 -4
  84. package/src/tui/audition.ts +2 -2
  85. package/src/tui/granular-menu.ts +278 -0
  86. package/src/tui/menu.ts +332 -9
  87. package/src/tui/modal-menu.ts +118 -0
  88. package/src/tui/play-session.ts +45 -2
@@ -0,0 +1,78 @@
1
+ /**
2
+ * One oversampler per nonlinear rig section (stomp, head): 4x at 22.05 kHz
3
+ * and below, 2x at 44.1/48 kHz, from the shared half-band primitives in
4
+ * src/audio/dsp/oversample.ts. The section's linear-phase delay is removed
5
+ * so a rigged track stays on the grid.
6
+ */
7
+ import { Adaa1, logCosh } from "../../dsp/shape.ts";
8
+ import { Oversample2x, Oversample4x } from "../../dsp/oversample.ts";
9
+
10
+ /** Oversampling factor of a nonlinear section at `sampleRate`. */
11
+ export function sectionFactor(sampleRate: number): 2 | 4 {
12
+ return sampleRate <= 32_000 ? 4 : 2;
13
+ }
14
+
15
+ type Oversampler = { process(x: number, fn: (x: number) => number): number };
16
+
17
+ function oversampler(factor: 2 | 4): Oversampler {
18
+ return factor === 4 ? new Oversample4x() : new Oversample2x();
19
+ }
20
+
21
+ const latencies = new Map<number, number>();
22
+
23
+ /** Base-rate delay of the up/down pair (impulse peak), measured once. */
24
+ export function sectionLatency(factor: 2 | 4): number {
25
+ const known = latencies.get(factor);
26
+ if (known !== undefined) return known;
27
+ const os = oversampler(factor);
28
+ let peak = 0;
29
+ let at = 0;
30
+ for (let i = 0; i < 256; i += 1) {
31
+ const y = Math.abs(os.process(i === 0 ? 1 : 0, (x) => x));
32
+ if (y > peak) {
33
+ peak = y;
34
+ at = i;
35
+ }
36
+ }
37
+ latencies.set(factor, at);
38
+ return at;
39
+ }
40
+
41
+ /**
42
+ * Runs `fn` over `buffer` at the oversampled rate, in place and latency
43
+ * compensated. `control(i)` runs at the base rate before sample i.
44
+ */
45
+ export function runSection(
46
+ buffer: Float64Array,
47
+ factor: 2 | 4,
48
+ fn: (x: number) => number,
49
+ control?: (index: number) => void,
50
+ ): void {
51
+ const os = oversampler(factor);
52
+ const latency = sectionLatency(factor);
53
+ const n = buffer.length;
54
+ for (let i = 0; i < n + latency; i += 1) {
55
+ if (control && i < n) control(i);
56
+ const y = os.process(i < n ? buffer[i]! : 0, fn);
57
+ if (i >= latency) buffer[i - latency] = y;
58
+ }
59
+ }
60
+
61
+ // Static curves with closed-form antiderivatives for first-order ADAA.
62
+
63
+ /** Triode-like asymmetry: the positive half clips harder (grid current). */
64
+ const ASYM_B = 0.35;
65
+ const ASYM_T = Math.tanh(ASYM_B);
66
+ const asymF = (x: number) => Math.tanh(x + ASYM_B) - ASYM_T;
67
+ const asymAF = (x: number) => logCosh(x + ASYM_B) - ASYM_T * x;
68
+
69
+ export const adaaTanh = () => new Adaa1(Math.tanh, logCosh);
70
+ export const adaaTriode = () => new Adaa1(asymF, asymAF);
71
+ /** Full-wave rectifier (octave up): |x|, antiderivative x|x|/2. */
72
+ export const adaaRectify = () =>
73
+ new Adaa1(
74
+ (x) => Math.abs(x),
75
+ (x) => 0.5 * x * Math.abs(x),
76
+ );
77
+ /** RAT diode pair: tanh of a cubic-boosted input (no closed-form F). */
78
+ export const ratClip = (x: number) => Math.tanh(x + 0.18 * x * x * x);
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Stomp boxes (`fx.stomp`): fuzz (Big Muff family), face (Fuzz Face), od
3
+ * (Tube Screamer), rat (RAT) and octave (Octavia). Circuit-inspired
4
+ * per-sample models ported from the 0.6 guitar prototype, run inside one
5
+ * oversampler with ADAA shapers.
6
+ */
7
+ import type { Adaa1 } from "../../dsp/shape.ts";
8
+ import { Dc, Hp1, Lp1, clamp, dbToGain } from "./filters.ts";
9
+ import {
10
+ adaaRectify,
11
+ adaaTanh,
12
+ adaaTriode,
13
+ ratClip,
14
+ runSection,
15
+ } from "./section.ts";
16
+
17
+ export const STOMP_TYPES = ["fuzz", "face", "od", "rat", "octave"] as const;
18
+ export type StompType = (typeof STOMP_TYPES)[number];
19
+
20
+ export type StompSettings = {
21
+ gain: number;
22
+ tone: number;
23
+ level: number;
24
+ octave: number;
25
+ };
26
+
27
+ /** A stomp circuit at the oversampled rate `sr`. */
28
+ export type StompCircuit = {
29
+ process(x: number): number;
30
+ /** Re-reads gain, tone and level (control rate, automated only). */
31
+ set(settings: StompSettings): void;
32
+ };
33
+
34
+ export function stompCircuit(
35
+ type: string,
36
+ initial: StompSettings,
37
+ sr: number,
38
+ ): StompCircuit {
39
+ switch (type) {
40
+ case "fuzz":
41
+ return fuzz(initial, sr);
42
+ case "face":
43
+ return face(initial, sr);
44
+ case "rat":
45
+ return rat(initial, sr);
46
+ case "octave":
47
+ return octave(initial, sr);
48
+ default:
49
+ return od(initial, sr);
50
+ }
51
+ }
52
+
53
+ function fuzz(initial: StompSettings, sr: number): StompCircuit {
54
+ // Input HPF, two cascaded clipping stages with interstage lowpass, then
55
+ // the passive LP/HP tone blend (the Muff's mid scoop).
56
+ const hp = new Hp1(90, sr);
57
+ const s1 = adaaTanh();
58
+ const s2 = adaaTanh();
59
+ const lpA = new Lp1(3200, sr);
60
+ const hpB = new Hp1(60, sr);
61
+ const lpB = new Lp1(2600, sr);
62
+ const toneLp = new Lp1(800, sr);
63
+ const toneHp = new Hp1(1600, sr);
64
+ let pre = 1;
65
+ let tone = 0.5;
66
+ let out = 1;
67
+ const set = (p: StompSettings) => {
68
+ pre = 2 ** (0.5 + 0.55 * p.gain);
69
+ tone = clamp(p.tone, 0, 1);
70
+ out = dbToGain(p.level) * 0.7;
71
+ };
72
+ set(initial);
73
+ return {
74
+ set,
75
+ process(input) {
76
+ let x = hp.process(input) * pre;
77
+ x = lpA.process(s1.process(x)) * 6;
78
+ x = lpB.process(s2.process(hpB.process(x)));
79
+ const y = (1 - tone) * toneLp.process(x) + tone * toneHp.process(x) * 1.6;
80
+ return y * out;
81
+ },
82
+ };
83
+ }
84
+
85
+ function face(initial: StompSettings, sr: number): StompCircuit {
86
+ // Asymmetric and round; cleans up as the input drops.
87
+ const hp = new Hp1(40, sr);
88
+ const s = adaaTriode();
89
+ const dc = new Dc(sr, 12);
90
+ const lp = new Lp1(2500, sr);
91
+ let pre = 1;
92
+ let out = 1;
93
+ const set = (p: StompSettings) => {
94
+ pre = 2 ** (0.3 + 0.5 * p.gain);
95
+ lp.set(2500 + 4000 * clamp(p.tone, 0, 1), sr);
96
+ out = dbToGain(p.level) * 0.8;
97
+ };
98
+ set(initial);
99
+ return {
100
+ set,
101
+ process(input) {
102
+ return lp.process(dc.process(s.process(hp.process(input) * pre))) * out;
103
+ },
104
+ };
105
+ }
106
+
107
+ function od(initial: StompSettings, sr: number): StompCircuit {
108
+ // Clean path plus a soft-clipped 720 Hz high-passed gain path (diodes in
109
+ // the op-amp feedback), then the tone lowpass.
110
+ const hp = new Hp1(720, sr);
111
+ const lpGain = new Lp1(5600, sr);
112
+ const s = adaaTanh();
113
+ const tone = new Lp1(700, sr);
114
+ let pre = 1;
115
+ let out = 1;
116
+ const set = (p: StompSettings) => {
117
+ pre = 1 + 10 ** (p.gain / 5);
118
+ tone.set(700 * 2 ** (3 * clamp(p.tone, 0, 1)), sr);
119
+ out = dbToGain(p.level) * 0.6;
120
+ };
121
+ set(initial);
122
+ return {
123
+ set,
124
+ process(x) {
125
+ const y = x + s.process(lpGain.process(hp.process(x) * pre)) * 0.5;
126
+ return tone.process(y) * out;
127
+ },
128
+ };
129
+ }
130
+
131
+ function rat(initial: StompSettings, sr: number): StompCircuit {
132
+ // Op-amp gain whose bandwidth shrinks with gain (LM308 GBW), hard diode
133
+ // clip to ground, then the passive filter.
134
+ const hp = new Hp1(60, sr);
135
+ const lpOp = new Lp1(20_000, sr);
136
+ const filter = new Lp1(475, sr);
137
+ let gain = 1;
138
+ let out = 1;
139
+ const set = (p: StompSettings) => {
140
+ gain = Math.min(1 + 2 ** (0.67 * p.gain + 1.5), 2000);
141
+ lpOp.set(clamp(1.0e6 / 3 / gain, 900, 20_000), sr);
142
+ filter.set(475 * 2 ** (6 * clamp(p.tone, 0, 1)), sr);
143
+ out = dbToGain(p.level) * 0.55;
144
+ };
145
+ set(initial);
146
+ return {
147
+ set,
148
+ process(input) {
149
+ const x = lpOp.process(hp.process(input) * gain * 0.02);
150
+ return filter.process(ratClip(x)) * out;
151
+ },
152
+ };
153
+ }
154
+
155
+ function octave(initial: StompSettings, sr: number): StompCircuit {
156
+ // Rectified (octave-up) path blended into a fuzz.
157
+ const hp = new Hp1(120, sr);
158
+ const rect: Adaa1 = adaaRectify();
159
+ const dc = new Dc(sr, 20);
160
+ const s = adaaTanh();
161
+ const lp = new Lp1(3000, sr);
162
+ let pre = 1;
163
+ let mix = 0.7;
164
+ let out = 1;
165
+ const set = (p: StompSettings) => {
166
+ pre = 2 ** (0.5 + 0.5 * p.gain);
167
+ lp.set(3000 + 4000 * clamp(p.tone, 0, 1), sr);
168
+ mix = clamp(p.octave, 0, 1);
169
+ out = dbToGain(p.level) * 0.7;
170
+ };
171
+ set(initial);
172
+ return {
173
+ set,
174
+ process(input) {
175
+ const x = hp.process(input);
176
+ const o = dc.process(rect.process(x * 2)) * 2;
177
+ return lp.process(s.process(((1 - mix) * x + mix * o) * pre)) * out;
178
+ },
179
+ };
180
+ }
181
+
182
+ /** Stomp calibration input: a 196 Hz (G3) sine at guitar level. */
183
+ export const STOMP_CALIBRATION_DBFS = -18;
184
+
185
+ const stompMakeups = new Map<string, number>();
186
+
187
+ /**
188
+ * Linear gain that brings a pedal at `settings` (level taken as 0 dB) back
189
+ * to the calibration sine's level, so switching or kicking on a stomp does
190
+ * not jump the track by 15-25 dB; `level` stays a trim on top. Measured on
191
+ * the circuit itself, deterministic, cached per settings and rate.
192
+ */
193
+ export function stompMakeup(
194
+ type: string,
195
+ settings: StompSettings,
196
+ sampleRate: number,
197
+ factor: 2 | 4,
198
+ ): number {
199
+ const key = `${sampleRate}|${factor}|${type}|${settings.gain}|${settings.tone}|${settings.octave}`;
200
+ const known = stompMakeups.get(key);
201
+ if (known !== undefined) return known;
202
+ const amplitude = 10 ** (STOMP_CALIBRATION_DBFS / 20) * Math.SQRT2;
203
+ const settle = Math.round(sampleRate * 0.1);
204
+ const length = settle + Math.round(sampleRate * 0.1);
205
+ const buffer = new Float64Array(length);
206
+ const w = (2 * Math.PI * 196) / sampleRate;
207
+ for (let i = 0; i < length; i += 1) buffer[i] = amplitude * Math.sin(w * i);
208
+ const circuit = stompCircuit(
209
+ type,
210
+ { ...settings, level: 0 },
211
+ sampleRate * factor,
212
+ );
213
+ runSection(buffer, factor, (x) => circuit.process(x));
214
+ let sum = 0;
215
+ for (let i = settle; i < length; i += 1) sum += buffer[i]! * buffer[i]!;
216
+ const rms = Math.sqrt(sum / (length - settle));
217
+ const target = 10 ** (STOMP_CALIBRATION_DBFS / 20);
218
+ const makeup = rms > 1e-12 ? target / rms : 1;
219
+ stompMakeups.set(key, makeup);
220
+ if (stompMakeups.size > 4096)
221
+ stompMakeups.delete(stompMakeups.keys().next().value!);
222
+ return makeup;
223
+ }
224
+
225
+ /** Makeup for an automated gain: linear between whole steps. */
226
+ export function stompMakeupAt(
227
+ type: string,
228
+ settings: StompSettings,
229
+ sampleRate: number,
230
+ factor: 2 | 4,
231
+ ): number {
232
+ const g = clamp(settings.gain, 0, 10);
233
+ const g0 = Math.min(9, Math.floor(g));
234
+ const at = (gain: number) =>
235
+ stompMakeup(type, { ...settings, gain }, sampleRate, factor);
236
+ const a = at(g0);
237
+ return a + (at(g0 + 1) - a) * (g - g0);
238
+ }
@@ -530,13 +530,14 @@ export class AudioEngine {
530
530
  existing.pcm = note.pcm;
531
531
  existing.frames = note.frames;
532
532
  existing.fadeLeft = undefined;
533
+ existing.fadeFrames = this.releaseFrames(note);
533
534
  } else
534
535
  this.voices.set(id, {
535
536
  pcm: note.pcm,
536
537
  frames: note.frames,
537
538
  position: 0,
538
539
  fadeLeft: undefined,
539
- fadeFrames: Math.max(1, Math.round(this.sampleRate * 0.01)),
540
+ fadeFrames: this.releaseFrames(note),
540
541
  });
541
542
  // Write what is due now so the voice joins the very next chunk.
542
543
  const first = this.written;
@@ -544,6 +545,14 @@ export class AudioEngine {
544
545
  return this.frameMs(first);
545
546
  }
546
547
 
548
+ /** A live voice's note-off fade: 10 ms, or the instrument's release. */
549
+ private releaseFrames(note: LiveNotePcm): number {
550
+ return Math.max(
551
+ 1,
552
+ Math.round(this.sampleRate * (note.releaseSeconds ?? 0.01)),
553
+ );
554
+ }
555
+
547
556
  /** Fade a live voice out over ~10 ms (sustain lifted, mode left). */
548
557
  public noteOff(id: number): void {
549
558
  const voice = this.voices.get(id);