@hraness/dawg 0.4.1 → 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 +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -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/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- 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/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -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/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- 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 +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- 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/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- 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 +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amp heads (`fx.head`): preamp triode stages, a Yeh & Smith tone stack
|
|
3
|
+
* ("Discretization of the '59 Fender Bassman Tone Stack", DAFx-06; Fender,
|
|
4
|
+
* Marshall and Vox-like component values), a push-pull power amp with
|
|
5
|
+
* supply sag and presence, then a makeup gain so every type and gain
|
|
6
|
+
* setting plays a fixed -30 dBFS sine back at -30 dBFS. Ported from the 0.6
|
|
7
|
+
* guitar prototype; `solid` (JC-120) and `bass` (SVT) are dawg additions in
|
|
8
|
+
* the same model.
|
|
9
|
+
*/
|
|
10
|
+
import type { Adaa1 } from "../../dsp/shape.ts";
|
|
11
|
+
import { Dc, Hp1, Lp1, Rbj, clamp } from "./filters.ts";
|
|
12
|
+
import { adaaTanh, adaaTriode, runSection } from "./section.ts";
|
|
13
|
+
|
|
14
|
+
export const HEAD_TYPES = [
|
|
15
|
+
"clean",
|
|
16
|
+
"chime",
|
|
17
|
+
"crunch",
|
|
18
|
+
"lead",
|
|
19
|
+
"high",
|
|
20
|
+
"solid",
|
|
21
|
+
"bass",
|
|
22
|
+
] as const;
|
|
23
|
+
export type HeadType = (typeof HEAD_TYPES)[number];
|
|
24
|
+
|
|
25
|
+
type StackValues = Readonly<{
|
|
26
|
+
C1: number;
|
|
27
|
+
C2: number;
|
|
28
|
+
C3: number;
|
|
29
|
+
R1: number;
|
|
30
|
+
R2: number;
|
|
31
|
+
R3: number;
|
|
32
|
+
R4: number;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
const STACKS = {
|
|
36
|
+
// Fender 5F6-A Bassman (Yeh & Smith 2006, table 1).
|
|
37
|
+
fender: {
|
|
38
|
+
C1: 250e-12,
|
|
39
|
+
C2: 20e-9,
|
|
40
|
+
C3: 20e-9,
|
|
41
|
+
R1: 250e3,
|
|
42
|
+
R2: 1e6,
|
|
43
|
+
R3: 25e3,
|
|
44
|
+
R4: 56e3,
|
|
45
|
+
},
|
|
46
|
+
// Marshall JCM800 2203 (common FMV values).
|
|
47
|
+
marshall: {
|
|
48
|
+
C1: 470e-12,
|
|
49
|
+
C2: 22e-9,
|
|
50
|
+
C3: 22e-9,
|
|
51
|
+
R1: 220e3,
|
|
52
|
+
R2: 1e6,
|
|
53
|
+
R3: 22e3,
|
|
54
|
+
R4: 33e3,
|
|
55
|
+
},
|
|
56
|
+
// Brighter Vox-like voicing approximated with FMV values.
|
|
57
|
+
vox: {
|
|
58
|
+
C1: 100e-12,
|
|
59
|
+
C2: 47e-9,
|
|
60
|
+
C3: 22e-9,
|
|
61
|
+
R1: 220e3,
|
|
62
|
+
R2: 1e6,
|
|
63
|
+
R3: 10e3,
|
|
64
|
+
R4: 100e3,
|
|
65
|
+
},
|
|
66
|
+
} satisfies Record<string, StackValues>;
|
|
67
|
+
|
|
68
|
+
/** Continuous FMV stack coefficients [b0..b3], [a0..a3]; pots t, m, l 0..1. */
|
|
69
|
+
export function stackAnalog(
|
|
70
|
+
v: StackValues,
|
|
71
|
+
t: number,
|
|
72
|
+
m: number,
|
|
73
|
+
l: number,
|
|
74
|
+
): { b: number[]; a: number[] } {
|
|
75
|
+
const { C1, C2, C3, R1, R2, R3, R4 } = v;
|
|
76
|
+
const b1 =
|
|
77
|
+
t * C1 * R1 + m * C3 * R3 + l * (C1 * R2 + C2 * R2) + (C1 * R3 + C2 * R3);
|
|
78
|
+
const b2 =
|
|
79
|
+
t * (C1 * C2 * R1 * R4 + C1 * C3 * R1 * R4) -
|
|
80
|
+
m * m * (C1 * C3 * R3 * R3 + C2 * C3 * R3 * R3) +
|
|
81
|
+
m * (C1 * C3 * R1 * R3 + C1 * C3 * R3 * R3 + C2 * C3 * R3 * R3) +
|
|
82
|
+
l * (C1 * C2 * R1 * R2 + C1 * C2 * R2 * R4 + C1 * C3 * R2 * R4) +
|
|
83
|
+
l * m * (C1 * C3 * R2 * R3 + C2 * C3 * R2 * R3) +
|
|
84
|
+
(C1 * C2 * R1 * R3 + C1 * C2 * R3 * R4 + C1 * C3 * R3 * R4);
|
|
85
|
+
const b3 =
|
|
86
|
+
l * m * (C1 * C2 * C3 * R1 * R2 * R3 + C1 * C2 * C3 * R2 * R3 * R4) -
|
|
87
|
+
m * m * (C1 * C2 * C3 * R1 * R3 * R3 + C1 * C2 * C3 * R3 * R3 * R4) +
|
|
88
|
+
m * (C1 * C2 * C3 * R1 * R3 * R3 + C1 * C2 * C3 * R3 * R3 * R4) +
|
|
89
|
+
t * C1 * C2 * C3 * R1 * R3 * R4 -
|
|
90
|
+
t * m * C1 * C2 * C3 * R1 * R3 * R4 +
|
|
91
|
+
t * l * C1 * C2 * C3 * R1 * R2 * R4;
|
|
92
|
+
const a1 =
|
|
93
|
+
C1 * R1 +
|
|
94
|
+
C1 * R3 +
|
|
95
|
+
C2 * R3 +
|
|
96
|
+
C2 * R4 +
|
|
97
|
+
C3 * R4 +
|
|
98
|
+
m * C3 * R3 +
|
|
99
|
+
l * (C1 * R2 + C2 * R2);
|
|
100
|
+
const a2 =
|
|
101
|
+
m *
|
|
102
|
+
(C1 * C3 * R1 * R3 -
|
|
103
|
+
C2 * C3 * R3 * R4 +
|
|
104
|
+
C1 * C3 * R3 * R3 +
|
|
105
|
+
C2 * C3 * R3 * R3) +
|
|
106
|
+
l * m * (C1 * C3 * R2 * R3 + C2 * C3 * R2 * R3) -
|
|
107
|
+
m * m * (C1 * C3 * R3 * R3 + C2 * C3 * R3 * R3) +
|
|
108
|
+
l *
|
|
109
|
+
(C1 * C2 * R2 * R4 +
|
|
110
|
+
C1 * C2 * R1 * R2 +
|
|
111
|
+
C1 * C3 * R2 * R4 +
|
|
112
|
+
C2 * C3 * R2 * R4) +
|
|
113
|
+
(C1 * C2 * R1 * R4 +
|
|
114
|
+
C1 * C3 * R1 * R4 +
|
|
115
|
+
C1 * C2 * R3 * R4 +
|
|
116
|
+
C1 * C2 * R1 * R3 +
|
|
117
|
+
C1 * C3 * R3 * R4 +
|
|
118
|
+
C2 * C3 * R3 * R4);
|
|
119
|
+
const a3 =
|
|
120
|
+
l * m * (C1 * C2 * C3 * R1 * R2 * R3 + C1 * C2 * C3 * R2 * R3 * R4) -
|
|
121
|
+
m * m * (C1 * C2 * C3 * R1 * R3 * R3 + C1 * C2 * C3 * R3 * R3 * R4) +
|
|
122
|
+
m *
|
|
123
|
+
(C1 * C2 * C3 * R3 * R3 * R4 +
|
|
124
|
+
C1 * C2 * C3 * R1 * R3 * R3 -
|
|
125
|
+
C1 * C2 * C3 * R1 * R3 * R4) +
|
|
126
|
+
l * C1 * C2 * C3 * R1 * R2 * R4 +
|
|
127
|
+
C1 * C2 * C3 * R1 * R3 * R4;
|
|
128
|
+
return { b: [0, b1, b2, b3], a: [1, a1, a2, a3] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** |H(j 2π f)| of the analog stack (for tests). */
|
|
132
|
+
export function stackAnalogMag(
|
|
133
|
+
v: StackValues,
|
|
134
|
+
t: number,
|
|
135
|
+
m: number,
|
|
136
|
+
l: number,
|
|
137
|
+
f: number,
|
|
138
|
+
): number {
|
|
139
|
+
const { b, a } = stackAnalog(v, t, m, l);
|
|
140
|
+
const w = 2 * Math.PI * f;
|
|
141
|
+
const nr = b[0]! - b[2]! * w * w;
|
|
142
|
+
const ni = b[1]! * w - b[3]! * w * w * w;
|
|
143
|
+
const dr = a[0]! - a[2]! * w * w;
|
|
144
|
+
const di = a[1]! * w - a[3]! * w * w * w;
|
|
145
|
+
return Math.hypot(nr, ni) / Math.hypot(dr, di);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Third-order IIR from the bilinear transform of the analog stack. */
|
|
149
|
+
export class ToneStack {
|
|
150
|
+
b = [0, 0, 0, 0];
|
|
151
|
+
a = [1, 0, 0, 0];
|
|
152
|
+
z0 = 0;
|
|
153
|
+
z1 = 0;
|
|
154
|
+
z2 = 0;
|
|
155
|
+
|
|
156
|
+
constructor(
|
|
157
|
+
readonly values: StackValues,
|
|
158
|
+
t: number,
|
|
159
|
+
m: number,
|
|
160
|
+
l: number,
|
|
161
|
+
readonly sr: number,
|
|
162
|
+
) {
|
|
163
|
+
this.set(t, m, l);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
set(t: number, m: number, l: number): void {
|
|
167
|
+
const { b, a } = stackAnalog(
|
|
168
|
+
this.values,
|
|
169
|
+
clamp(t, 0, 1),
|
|
170
|
+
clamp(m, 0, 1),
|
|
171
|
+
clamp(l, 0, 1),
|
|
172
|
+
);
|
|
173
|
+
const c = 2 * this.sr;
|
|
174
|
+
const c2 = c * c;
|
|
175
|
+
const c3 = c2 * c;
|
|
176
|
+
const B0 = -b[1]! * c - b[2]! * c2 - b[3]! * c3;
|
|
177
|
+
const B1 = -b[1]! * c + b[2]! * c2 + 3 * b[3]! * c3;
|
|
178
|
+
const B2 = b[1]! * c + b[2]! * c2 - 3 * b[3]! * c3;
|
|
179
|
+
const B3 = b[1]! * c - b[2]! * c2 + b[3]! * c3;
|
|
180
|
+
const A0 = -1 - a[1]! * c - a[2]! * c2 - a[3]! * c3;
|
|
181
|
+
const A1 = -3 - a[1]! * c + a[2]! * c2 + 3 * a[3]! * c3;
|
|
182
|
+
const A2 = -3 + a[1]! * c + a[2]! * c2 - 3 * a[3]! * c3;
|
|
183
|
+
const A3 = -1 + a[1]! * c - a[2]! * c2 + a[3]! * c3;
|
|
184
|
+
this.b = [B0 / A0, B1 / A0, B2 / A0, B3 / A0];
|
|
185
|
+
this.a = [1, A1 / A0, A2 / A0, A3 / A0];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process(x: number): number {
|
|
189
|
+
const { b, a } = this;
|
|
190
|
+
const y = b[0]! * x + this.z0;
|
|
191
|
+
this.z0 = b[1]! * x - a[1]! * y + this.z1;
|
|
192
|
+
this.z1 = b[2]! * x - a[2]! * y + this.z2;
|
|
193
|
+
this.z2 = b[3]! * x - a[3]! * y;
|
|
194
|
+
return y;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The bass pot is audio taper: knob 0..1 to resistance fraction. */
|
|
199
|
+
export const audioTaper = (k: number): number =>
|
|
200
|
+
k <= 0 ? 0 : (10 ** (2 * clamp(k, 0, 1)) - 1) / 99;
|
|
201
|
+
|
|
202
|
+
type HeadModel = Readonly<{
|
|
203
|
+
stages: number;
|
|
204
|
+
stack: keyof typeof STACKS;
|
|
205
|
+
/** Gain per preamp stage at gain 10 (linear). */
|
|
206
|
+
stageGain: number;
|
|
207
|
+
/** Interstage low-pass (Miller capacitance), Hz. */
|
|
208
|
+
miller: number;
|
|
209
|
+
/** Interstage coupling high-pass, Hz. */
|
|
210
|
+
coupling: number;
|
|
211
|
+
/** Bright cap: high shelf dB at low gain. */
|
|
212
|
+
bright: number;
|
|
213
|
+
/** Power amp drive into the push-pull stage. */
|
|
214
|
+
power: number;
|
|
215
|
+
sag: number;
|
|
216
|
+
/** Symmetric (solid state) preamp clipping. */
|
|
217
|
+
symmetric?: boolean;
|
|
218
|
+
}>;
|
|
219
|
+
|
|
220
|
+
const HEADS: Record<HeadType, HeadModel> = {
|
|
221
|
+
clean: {
|
|
222
|
+
stages: 1,
|
|
223
|
+
stack: "fender",
|
|
224
|
+
stageGain: 6,
|
|
225
|
+
miller: 9000,
|
|
226
|
+
coupling: 20,
|
|
227
|
+
bright: 4,
|
|
228
|
+
power: 0.6,
|
|
229
|
+
sag: 0.2,
|
|
230
|
+
},
|
|
231
|
+
chime: {
|
|
232
|
+
stages: 2,
|
|
233
|
+
stack: "vox",
|
|
234
|
+
stageGain: 5,
|
|
235
|
+
miller: 8000,
|
|
236
|
+
coupling: 60,
|
|
237
|
+
bright: 3,
|
|
238
|
+
power: 1.4,
|
|
239
|
+
sag: 0.45,
|
|
240
|
+
},
|
|
241
|
+
crunch: {
|
|
242
|
+
stages: 2,
|
|
243
|
+
stack: "marshall",
|
|
244
|
+
stageGain: 14,
|
|
245
|
+
miller: 7000,
|
|
246
|
+
coupling: 80,
|
|
247
|
+
bright: 2,
|
|
248
|
+
power: 1.0,
|
|
249
|
+
sag: 0.3,
|
|
250
|
+
},
|
|
251
|
+
lead: {
|
|
252
|
+
stages: 3,
|
|
253
|
+
stack: "marshall",
|
|
254
|
+
stageGain: 18,
|
|
255
|
+
miller: 6500,
|
|
256
|
+
coupling: 120,
|
|
257
|
+
bright: 1,
|
|
258
|
+
power: 1.0,
|
|
259
|
+
sag: 0.25,
|
|
260
|
+
},
|
|
261
|
+
high: {
|
|
262
|
+
stages: 4,
|
|
263
|
+
stack: "marshall",
|
|
264
|
+
stageGain: 20,
|
|
265
|
+
miller: 6000,
|
|
266
|
+
coupling: 160,
|
|
267
|
+
bright: 0,
|
|
268
|
+
power: 1.2,
|
|
269
|
+
sag: 0.2,
|
|
270
|
+
},
|
|
271
|
+
// Solid-state clean (Roland JC-120): symmetric, stiff supply, bright.
|
|
272
|
+
solid: {
|
|
273
|
+
stages: 1,
|
|
274
|
+
stack: "fender",
|
|
275
|
+
stageGain: 3,
|
|
276
|
+
miller: 12_000,
|
|
277
|
+
coupling: 20,
|
|
278
|
+
bright: 2,
|
|
279
|
+
power: 0.4,
|
|
280
|
+
sag: 0,
|
|
281
|
+
symmetric: true,
|
|
282
|
+
},
|
|
283
|
+
// Tube bass head (Ampeg SVT): low coupling, darker interstage, more sag.
|
|
284
|
+
bass: {
|
|
285
|
+
stages: 2,
|
|
286
|
+
stack: "fender",
|
|
287
|
+
stageGain: 8,
|
|
288
|
+
miller: 5000,
|
|
289
|
+
coupling: 15,
|
|
290
|
+
bright: 0,
|
|
291
|
+
power: 1.0,
|
|
292
|
+
sag: 0.35,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export type HeadSettings = Readonly<{
|
|
297
|
+
type: string;
|
|
298
|
+
gain: number;
|
|
299
|
+
bass: number;
|
|
300
|
+
mid: number;
|
|
301
|
+
treble: number;
|
|
302
|
+
presence: number;
|
|
303
|
+
master: number;
|
|
304
|
+
sag?: number;
|
|
305
|
+
}>;
|
|
306
|
+
|
|
307
|
+
/** A head running at the oversampled rate `sr`. */
|
|
308
|
+
export class HeadCircuit {
|
|
309
|
+
private readonly model: HeadModel;
|
|
310
|
+
private readonly inHp: Hp1;
|
|
311
|
+
private readonly bright: Rbj;
|
|
312
|
+
private readonly stages: {
|
|
313
|
+
pre: number;
|
|
314
|
+
shaper: Adaa1;
|
|
315
|
+
hp: Hp1;
|
|
316
|
+
lp: Lp1;
|
|
317
|
+
}[] = [];
|
|
318
|
+
private readonly stack: ToneStack;
|
|
319
|
+
private readonly power = adaaTanh();
|
|
320
|
+
private readonly presence: Rbj;
|
|
321
|
+
private readonly env: Lp1;
|
|
322
|
+
private readonly dc: Dc;
|
|
323
|
+
private drive = 1;
|
|
324
|
+
private sagAmount = 0;
|
|
325
|
+
private headroom = 1;
|
|
326
|
+
private envLevel = 0;
|
|
327
|
+
private count = 0;
|
|
328
|
+
/** Sag is updated every this many oversampled samples (32 base). */
|
|
329
|
+
private readonly control: number;
|
|
330
|
+
|
|
331
|
+
constructor(
|
|
332
|
+
settings: HeadSettings,
|
|
333
|
+
private readonly sr: number,
|
|
334
|
+
factor: number,
|
|
335
|
+
) {
|
|
336
|
+
this.model = HEADS[settings.type as HeadType] ?? HEADS.crunch;
|
|
337
|
+
this.control = 32 * factor;
|
|
338
|
+
this.inHp = new Hp1(30, sr);
|
|
339
|
+
this.bright = new Rbj("highshelf", 2500, 0.7, sr, 0);
|
|
340
|
+
for (let s = 0; s < this.model.stages; s += 1)
|
|
341
|
+
this.stages.push({
|
|
342
|
+
pre: 1,
|
|
343
|
+
shaper: this.model.symmetric ? adaaTanh() : adaaTriode(),
|
|
344
|
+
hp: new Hp1(s === 0 ? 20 : this.model.coupling, sr),
|
|
345
|
+
lp: new Lp1(this.model.miller, sr),
|
|
346
|
+
});
|
|
347
|
+
this.stack = new ToneStack(STACKS[this.model.stack], 0.5, 0.5, 0.5, sr);
|
|
348
|
+
this.presence = new Rbj("highshelf", 3200, 0.7, sr, 0);
|
|
349
|
+
this.env = new Lp1(6, sr);
|
|
350
|
+
this.dc = new Dc(sr, 10);
|
|
351
|
+
this.set(settings);
|
|
352
|
+
this.setTone(settings);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Gain and master (cheap; control rate when automated). */
|
|
356
|
+
set(p: HeadSettings): void {
|
|
357
|
+
const model = this.model;
|
|
358
|
+
const g = clamp(p.gain / 10, 0, 1);
|
|
359
|
+
this.bright.set("highshelf", 2500, 0.7, this.sr, model.bright * (1 - g));
|
|
360
|
+
for (let s = 0; s < this.stages.length; s += 1)
|
|
361
|
+
this.stages[s]!.pre =
|
|
362
|
+
s === 0
|
|
363
|
+
? 0.6 + model.stageGain * g * g * 1.6
|
|
364
|
+
: model.stageGain * (0.3 + 0.7 * g);
|
|
365
|
+
this.drive = model.power * (0.25 + 0.18 * clamp(p.master, 0, 10));
|
|
366
|
+
this.sagAmount = clamp(p.sag ?? model.sag, 0, 1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Tone stack and presence (static). */
|
|
370
|
+
setTone(p: HeadSettings): void {
|
|
371
|
+
this.stack.set(
|
|
372
|
+
clamp(p.treble / 10, 0, 1),
|
|
373
|
+
clamp(p.mid / 10, 0, 1),
|
|
374
|
+
audioTaper(p.bass / 10),
|
|
375
|
+
);
|
|
376
|
+
this.presence.set(
|
|
377
|
+
"highshelf",
|
|
378
|
+
3200,
|
|
379
|
+
0.7,
|
|
380
|
+
this.sr,
|
|
381
|
+
(clamp(p.presence, 0, 10) - 5) * 1.6,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
process(input: number): number {
|
|
386
|
+
let x = this.bright.process(this.inHp.process(input));
|
|
387
|
+
const stages = this.stages;
|
|
388
|
+
for (let s = 0; s < stages.length; s += 1) {
|
|
389
|
+
const st = stages[s]!;
|
|
390
|
+
x = st.lp.process(st.shaper.process(st.hp.process(x) * st.pre));
|
|
391
|
+
// Each triode stage inverts, alternating the asymmetry.
|
|
392
|
+
x = -x;
|
|
393
|
+
}
|
|
394
|
+
x = this.stack.process(x) * 4.5;
|
|
395
|
+
if (this.count % this.control === 0)
|
|
396
|
+
this.headroom = 1 / (1 + 2.5 * this.sagAmount * this.envLevel);
|
|
397
|
+
this.count += 1;
|
|
398
|
+
const u =
|
|
399
|
+
this.power.process((x * this.drive) / this.headroom) * this.headroom;
|
|
400
|
+
this.envLevel = this.env.process(Math.abs(u));
|
|
401
|
+
return this.dc.process(this.presence.process(u)) * 0.5;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Calibration input: a 196 Hz (G3) sine at -30 dBFS. */
|
|
406
|
+
export const CALIBRATION_HZ = 196;
|
|
407
|
+
export const CALIBRATION_DBFS = -30;
|
|
408
|
+
|
|
409
|
+
const makeups = new Map<string, number>();
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Linear gain that brings `settings` (tone stack included) back to the
|
|
413
|
+
* calibration sine's level: run the head on the sine, compare RMS after it
|
|
414
|
+
* settles. Deterministic and cached per settings and rate.
|
|
415
|
+
*/
|
|
416
|
+
export function headMakeup(
|
|
417
|
+
settings: HeadSettings,
|
|
418
|
+
sampleRate: number,
|
|
419
|
+
factor: 2 | 4,
|
|
420
|
+
): number {
|
|
421
|
+
const key = `${sampleRate}|${factor}|${settings.type}|${settings.gain}|${settings.bass}|${settings.mid}|${settings.treble}|${settings.presence}|${settings.master}|${settings.sag ?? ""}`;
|
|
422
|
+
const known = makeups.get(key);
|
|
423
|
+
if (known !== undefined) return known;
|
|
424
|
+
const amplitude = 10 ** (CALIBRATION_DBFS / 20) * Math.SQRT2;
|
|
425
|
+
const settle = Math.round(sampleRate * 0.1);
|
|
426
|
+
const length = settle + Math.round(sampleRate * 0.1);
|
|
427
|
+
const buffer = new Float64Array(length);
|
|
428
|
+
const w = (2 * Math.PI * CALIBRATION_HZ) / sampleRate;
|
|
429
|
+
for (let i = 0; i < length; i += 1) buffer[i] = amplitude * Math.sin(w * i);
|
|
430
|
+
const head = new HeadCircuit(settings, sampleRate * factor, factor);
|
|
431
|
+
runSection(buffer, factor, (x) => head.process(x));
|
|
432
|
+
let sum = 0;
|
|
433
|
+
for (let i = settle; i < length; i += 1) sum += buffer[i]! * buffer[i]!;
|
|
434
|
+
const rms = Math.sqrt(sum / (length - settle));
|
|
435
|
+
const target = 10 ** (CALIBRATION_DBFS / 20);
|
|
436
|
+
const makeup = rms > 1e-12 ? target / rms : 1;
|
|
437
|
+
makeups.set(key, makeup);
|
|
438
|
+
if (makeups.size > 4096) makeups.delete(makeups.keys().next().value!);
|
|
439
|
+
return makeup;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Automated makeup grid: gain and master in whole steps, 0..10. */
|
|
443
|
+
const GRID = 11;
|
|
444
|
+
const grids = new Map<string, Float64Array>();
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Makeup for automated gain and master: bilinear between whole-step
|
|
448
|
+
* calibration points of the same settings, computed lazily and cached per
|
|
449
|
+
* tone settings and rate. Smooth across blocks (no cache key per value),
|
|
450
|
+
* and a ramp calibrates at most a few dozen points once per process.
|
|
451
|
+
*/
|
|
452
|
+
export function headMakeupAt(
|
|
453
|
+
settings: HeadSettings,
|
|
454
|
+
sampleRate: number,
|
|
455
|
+
factor: 2 | 4,
|
|
456
|
+
): number {
|
|
457
|
+
const key = `${sampleRate}|${factor}|${settings.type}|${settings.bass}|${settings.mid}|${settings.treble}|${settings.presence}|${settings.sag ?? ""}`;
|
|
458
|
+
let grid = grids.get(key);
|
|
459
|
+
if (!grid) {
|
|
460
|
+
grid = new Float64Array(GRID * GRID).fill(Number.NaN);
|
|
461
|
+
grids.set(key, grid);
|
|
462
|
+
if (grids.size > 256) grids.delete(grids.keys().next().value!);
|
|
463
|
+
}
|
|
464
|
+
const g = clamp(settings.gain, 0, 10);
|
|
465
|
+
const m = clamp(settings.master, 0, 10);
|
|
466
|
+
const g0 = Math.min(GRID - 2, Math.floor(g));
|
|
467
|
+
const m0 = Math.min(GRID - 2, Math.floor(m));
|
|
468
|
+
const point = (gi: number, mi: number): number => {
|
|
469
|
+
const at = gi * GRID + mi;
|
|
470
|
+
let value = grid[at]!;
|
|
471
|
+
if (Number.isNaN(value)) {
|
|
472
|
+
value = headMakeup(
|
|
473
|
+
{ ...settings, gain: gi, master: mi },
|
|
474
|
+
sampleRate,
|
|
475
|
+
factor,
|
|
476
|
+
);
|
|
477
|
+
grid[at] = value;
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
};
|
|
481
|
+
const fg = g - g0;
|
|
482
|
+
const fm = m - m0;
|
|
483
|
+
const a = point(g0, m0) + (point(g0, m0 + 1) - point(g0, m0)) * fm;
|
|
484
|
+
const b =
|
|
485
|
+
point(g0 + 1, m0) + (point(g0 + 1, m0 + 1) - point(g0 + 1, m0)) * fm;
|
|
486
|
+
return a + (b - a) * fg;
|
|
487
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guitar rig stages of the track chain (`fx.stomp`, `fx.head` with its
|
|
3
|
+
* gate, `fx.cab`), mono, after `distort` and before `tremolo`. Each
|
|
4
|
+
* nonlinear section (stomp, head) runs inside one oversampler: 4x at
|
|
5
|
+
* 22.05 kHz, 2x at 44.1/48 kHz. Coefficients follow automation every 32
|
|
6
|
+
* samples, and only when a lane exists. Never per voice.
|
|
7
|
+
*/
|
|
8
|
+
import type { FxValues } from "../../../../core/fx.ts";
|
|
9
|
+
import type { Track } from "../../../../core/score.ts";
|
|
10
|
+
import { CONTROL_SAMPLES, fxReader, type EffectContext } from "../common.ts";
|
|
11
|
+
import { cabFilters } from "./cab.ts";
|
|
12
|
+
import { clamp, dbToGain } from "./filters.ts";
|
|
13
|
+
import { applyGate } from "./gate.ts";
|
|
14
|
+
import {
|
|
15
|
+
HeadCircuit,
|
|
16
|
+
headMakeup,
|
|
17
|
+
headMakeupAt,
|
|
18
|
+
type HeadSettings,
|
|
19
|
+
} from "./head.ts";
|
|
20
|
+
import { runSection, sectionFactor } from "./section.ts";
|
|
21
|
+
import {
|
|
22
|
+
stompCircuit,
|
|
23
|
+
stompMakeup,
|
|
24
|
+
stompMakeupAt,
|
|
25
|
+
type StompSettings,
|
|
26
|
+
} from "./stomp.ts";
|
|
27
|
+
|
|
28
|
+
function blend(
|
|
29
|
+
buffer: Float64Array,
|
|
30
|
+
dry: Float64Array,
|
|
31
|
+
mix: (index: number) => number,
|
|
32
|
+
): void {
|
|
33
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
34
|
+
const wet = clamp(mix(i), 0, 1);
|
|
35
|
+
if (wet < 1) buffer[i] = dry[i]! + (buffer[i]! - dry[i]!) * wet;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function applyStomp(
|
|
40
|
+
buffer: Float64Array,
|
|
41
|
+
track: Track,
|
|
42
|
+
values: FxValues,
|
|
43
|
+
context: EffectContext,
|
|
44
|
+
): void {
|
|
45
|
+
const read = fxReader(track, "stomp", values, context);
|
|
46
|
+
const gain = read.param("gain");
|
|
47
|
+
const tone = read.param("tone");
|
|
48
|
+
const mix = read.param("mix");
|
|
49
|
+
const settings: StompSettings = {
|
|
50
|
+
gain: gain.at(0),
|
|
51
|
+
tone: tone.at(0),
|
|
52
|
+
level: read.number("level"),
|
|
53
|
+
octave: read.number("octave"),
|
|
54
|
+
};
|
|
55
|
+
const factor = sectionFactor(context.sampleRate);
|
|
56
|
+
const circuit = stompCircuit(
|
|
57
|
+
read.text("type"),
|
|
58
|
+
settings,
|
|
59
|
+
context.sampleRate * factor,
|
|
60
|
+
);
|
|
61
|
+
const dry =
|
|
62
|
+
mix.automated || mix.at(0) < 1 ? Float64Array.from(buffer) : undefined;
|
|
63
|
+
const automated = gain.automated || tone.automated;
|
|
64
|
+
const type = read.text("type");
|
|
65
|
+
const blocks = automated
|
|
66
|
+
? new Float64Array(Math.ceil(buffer.length / CONTROL_SAMPLES) + 1)
|
|
67
|
+
: undefined;
|
|
68
|
+
runSection(
|
|
69
|
+
buffer,
|
|
70
|
+
factor,
|
|
71
|
+
(x) => circuit.process(x),
|
|
72
|
+
blocks
|
|
73
|
+
? (i) => {
|
|
74
|
+
if (i % CONTROL_SAMPLES !== 0) return;
|
|
75
|
+
settings.gain = gain.at(i);
|
|
76
|
+
settings.tone = tone.at(i);
|
|
77
|
+
circuit.set(settings);
|
|
78
|
+
blocks[i / CONTROL_SAMPLES] = stompMakeupAt(
|
|
79
|
+
type,
|
|
80
|
+
settings,
|
|
81
|
+
context.sampleRate,
|
|
82
|
+
factor,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
: undefined,
|
|
86
|
+
);
|
|
87
|
+
if (blocks)
|
|
88
|
+
for (let i = 0; i < buffer.length; i += 1)
|
|
89
|
+
buffer[i] = buffer[i]! * blocks[Math.floor(i / CONTROL_SAMPLES)]!;
|
|
90
|
+
else {
|
|
91
|
+
const makeup = stompMakeup(type, settings, context.sampleRate, factor);
|
|
92
|
+
for (let i = 0; i < buffer.length; i += 1) buffer[i] = buffer[i]! * makeup;
|
|
93
|
+
}
|
|
94
|
+
if (dry) blend(buffer, dry, (i) => mix.at(i));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function applyHead(
|
|
98
|
+
buffer: Float64Array,
|
|
99
|
+
track: Track,
|
|
100
|
+
values: FxValues,
|
|
101
|
+
context: EffectContext,
|
|
102
|
+
): void {
|
|
103
|
+
const read = fxReader(track, "head", values, context);
|
|
104
|
+
const sr = context.sampleRate;
|
|
105
|
+
const gain = read.param("gain");
|
|
106
|
+
const master = read.param("master");
|
|
107
|
+
const gate = values.gate as number | undefined;
|
|
108
|
+
if (gate !== undefined) applyGate(buffer, gate, sr);
|
|
109
|
+
const sag = values.sag as number | undefined;
|
|
110
|
+
const settings: HeadSettings & { gain: number; master: number } = {
|
|
111
|
+
type: read.text("type"),
|
|
112
|
+
gain: gain.at(0),
|
|
113
|
+
bass: read.number("bass"),
|
|
114
|
+
mid: read.number("mid"),
|
|
115
|
+
treble: read.number("treble"),
|
|
116
|
+
presence: read.number("presence"),
|
|
117
|
+
master: master.at(0),
|
|
118
|
+
...(sag === undefined ? {} : { sag }),
|
|
119
|
+
};
|
|
120
|
+
const factor = sectionFactor(sr);
|
|
121
|
+
const level = dbToGain(read.number("level"));
|
|
122
|
+
const head = new HeadCircuit(settings, sr * factor, factor);
|
|
123
|
+
const automated = gain.automated || master.automated;
|
|
124
|
+
if (!automated) {
|
|
125
|
+
const makeup = headMakeup(settings, sr, factor) * level;
|
|
126
|
+
runSection(buffer, factor, (x) => head.process(x));
|
|
127
|
+
for (let i = 0; i < buffer.length; i += 1) buffer[i] = buffer[i]! * makeup;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const blocks = new Float64Array(
|
|
131
|
+
Math.ceil(buffer.length / CONTROL_SAMPLES) + 1,
|
|
132
|
+
);
|
|
133
|
+
runSection(
|
|
134
|
+
buffer,
|
|
135
|
+
factor,
|
|
136
|
+
(x) => head.process(x),
|
|
137
|
+
(i) => {
|
|
138
|
+
if (i % CONTROL_SAMPLES !== 0) return;
|
|
139
|
+
settings.gain = gain.at(i);
|
|
140
|
+
settings.master = master.at(i);
|
|
141
|
+
head.set(settings);
|
|
142
|
+
blocks[i / CONTROL_SAMPLES] = headMakeupAt(settings, sr, factor) * level;
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
for (let i = 0; i < buffer.length; i += 1)
|
|
146
|
+
buffer[i] = buffer[i]! * blocks[Math.floor(i / CONTROL_SAMPLES)]!;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function applyCab(
|
|
150
|
+
buffer: Float64Array,
|
|
151
|
+
track: Track,
|
|
152
|
+
values: FxValues,
|
|
153
|
+
context: EffectContext,
|
|
154
|
+
): void {
|
|
155
|
+
const read = fxReader(track, "cab", values, context);
|
|
156
|
+
const mix = read.param("mix");
|
|
157
|
+
const chain = cabFilters(
|
|
158
|
+
read.text("type"),
|
|
159
|
+
read.number("mic"),
|
|
160
|
+
context.sampleRate,
|
|
161
|
+
);
|
|
162
|
+
const constant = !mix.automated ? clamp(mix.at(0), 0, 1) : undefined;
|
|
163
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
164
|
+
const dry = buffer[i]!;
|
|
165
|
+
let x = dry;
|
|
166
|
+
for (const filter of chain) x = filter.process(x);
|
|
167
|
+
const wet = constant ?? clamp(mix.at(i), 0, 1);
|
|
168
|
+
buffer[i] = wet === 1 ? x : dry + (x - dry) * wet;
|
|
169
|
+
}
|
|
170
|
+
}
|