@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.
- package/CHANGELOG.md +29 -0
- package/DAWG.md +269 -13
- package/README.md +4 -4
- package/core/diff.ts +4 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/resonators.ts +574 -0
- package/core/score.ts +181 -6
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +169 -13
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +1114 -21
- package/core/sections.ts +34 -8
- package/core/strings.ts +845 -0
- package/guides/effects.md +3 -1
- package/guides/sounds.md +4 -0
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +7 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +39 -2
- package/src/agent/preview-tool.ts +6 -1
- package/src/agent/tools.ts +538 -14
- package/src/agent/xcb-agent.ts +7 -0
- package/src/audio/arrange.ts +21 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +10 -1
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +231 -12
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +2 -0
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +163 -17
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +78 -6
- package/src/commands/expression.ts +9 -2
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +74 -3
- package/src/commands/keys.ts +353 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/time.ts +4 -1
- package/src/main.ts +210 -8
- package/src/project/check.ts +13 -0
- package/src/render.ts +33 -4
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +332 -9
- package/src/tui/modal-menu.ts +118 -0
- 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
|
+
}
|
package/src/audio/engine.ts
CHANGED
|
@@ -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:
|
|
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);
|