@audio/filter-diode-ladder 1.0.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/diode-ladder.js +101 -0
- package/package.json +38 -0
package/diode-ladder.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diode ladder filter (Roland TB-303 / EMS VCS3 style).
|
|
3
|
+
* Zero-delay feedback (ZDF) via a tridiagonal solve of the 4 stages.
|
|
4
|
+
*
|
|
5
|
+
* Ref: Zavalishin, "The Art of VA Filter Design" (2012), §5.10 (unbuffered/
|
|
6
|
+
* loaded ladder networks).
|
|
7
|
+
* Pirkle, "Designing Audio Effect Plugins in C++" (2019), Ch. 10.
|
|
8
|
+
*
|
|
9
|
+
* Unlike the Moog transistor ladder — 4 stages isolated by unity-gain buffers,
|
|
10
|
+
* so each stage's output depends only on the stage before it — the diode
|
|
11
|
+
* ladder has no buffers: neighboring stages load each other, so each stage's
|
|
12
|
+
* drive depends on both its predecessor's output AND its successor's state.
|
|
13
|
+
* This couples all 4 stages into one tridiagonal system, solved once per
|
|
14
|
+
* sample (Thomas algorithm) instead of Moog's simple forward cascade. Each
|
|
15
|
+
* stage also carries its own tanh nonlinearity (the diode's I–V curve),
|
|
16
|
+
* closed by one global zero-delay resonance path around the whole chain.
|
|
17
|
+
*
|
|
18
|
+
* @module audio-filter/analog/diode-ladder
|
|
19
|
+
* @param {Float32Array|Float64Array} data - audio buffer (modified in place)
|
|
20
|
+
* @param {Object} params
|
|
21
|
+
* @param {number} [params.fc=1000] - cutoff frequency Hz
|
|
22
|
+
* @param {number} [params.resonance=0] - resonance 0–1 (self-oscillation ≈1.15–1.2, higher than moog-ladder's 1 — the per-stage tanh damps more; see readme)
|
|
23
|
+
* @param {number} [params.fs=44100] - sample rate
|
|
24
|
+
* @param {number} [params.drive=1] - input drive (saturation amount)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
let {tan, tanh, PI, min} = Math
|
|
28
|
+
|
|
29
|
+
// Inter-stage loading: diode stages have no buffers, so a fraction of each
|
|
30
|
+
// neighbor's signal bleeds across the stage boundary (unlike moog-ladder's 0).
|
|
31
|
+
let C = 0.1
|
|
32
|
+
|
|
33
|
+
export default function diodeLadder (data, params) {
|
|
34
|
+
let fc = params.fc || 1000
|
|
35
|
+
let res = params.resonance != null ? params.resonance : 0
|
|
36
|
+
let fs = params.fs || 44100
|
|
37
|
+
let drive = params.drive ?? 1
|
|
38
|
+
|
|
39
|
+
// Trapezoidal integrator coefficient
|
|
40
|
+
let g = tan(PI * min(fc, fs * 0.49) / fs)
|
|
41
|
+
let G = g / (1 + g)
|
|
42
|
+
let k = res * 4 // feedback coefficient, same convention as moog-ladder
|
|
43
|
+
|
|
44
|
+
// Tridiagonal system for the 4 bidirectionally-coupled stages:
|
|
45
|
+
// (1+a)y1 - a*y2 = d1
|
|
46
|
+
// -G*y1 + (1+a)y2 - a*y3 = d2
|
|
47
|
+
// -G*y2 + (1+a)y3 - a*y4 = d3
|
|
48
|
+
// -G*y3 + y4 = d4
|
|
49
|
+
// a = G*C couples each stage to its neighbor. Elimination coefficients
|
|
50
|
+
// (Thomas algorithm) depend only on G, C — precomputed once per block.
|
|
51
|
+
let a = G * C
|
|
52
|
+
let den1 = 1 + a
|
|
53
|
+
let cp1 = -a / den1
|
|
54
|
+
let den2 = den1 + G * cp1
|
|
55
|
+
let cp2 = -a / den2
|
|
56
|
+
let den3 = den1 + G * cp2
|
|
57
|
+
let cp3 = -a / den3
|
|
58
|
+
let den4 = 1 + G * cp3
|
|
59
|
+
|
|
60
|
+
// solve(d1..d4) -> [y1,y2,y3,y4]; reused for both the zero-delay estimate
|
|
61
|
+
// (from old state) and the final post-feedback pass.
|
|
62
|
+
let out = new Float64Array(4)
|
|
63
|
+
function solve (d1, d2, d3, d4) {
|
|
64
|
+
let dp1 = d1 / den1
|
|
65
|
+
let dp2 = (d2 + G * dp1) / den2
|
|
66
|
+
let dp3 = (d3 + G * dp2) / den3
|
|
67
|
+
let dp4 = (d4 + G * dp3) / den4
|
|
68
|
+
out[3] = dp4
|
|
69
|
+
out[2] = dp3 - cp3 * out[3]
|
|
70
|
+
out[1] = dp2 - cp2 * out[2]
|
|
71
|
+
out[0] = dp1 - cp1 * out[1]
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sensitivity of y4 to a unit change in the post-feedback drive (constant per block)
|
|
76
|
+
let sens = solve(G, 0, 0, 0)[3]
|
|
77
|
+
|
|
78
|
+
// State: 4 one-pole integrator states
|
|
79
|
+
if (!params._s) params._s = new Float64Array(4)
|
|
80
|
+
let s = params._s
|
|
81
|
+
|
|
82
|
+
for (let i = 0, n = data.length; i < n; i++) {
|
|
83
|
+
let t0 = tanh(s[0]), t1 = tanh(s[1]), t2 = tanh(s[2]), t3 = tanh(s[3])
|
|
84
|
+
let d0 = (1 - G) * t0, d1 = (1 - G) * t1, d2 = (1 - G) * t2, d3 = (1 - G) * t3
|
|
85
|
+
|
|
86
|
+
// Zero-input estimate (S) of stage-4 output, from old state only
|
|
87
|
+
let S = solve(d0, d1, d2, d3)[3]
|
|
88
|
+
|
|
89
|
+
// Implicit feedback solve, then input drive/saturation
|
|
90
|
+
let u = (data[i] - k * S) / (1 + k * sens)
|
|
91
|
+
u = tanh(u * drive)
|
|
92
|
+
|
|
93
|
+
// Final coupled solve with the resolved drive
|
|
94
|
+
let y = solve(d0 + G * u, d1, d2, d3)
|
|
95
|
+
for (let j = 0; j < 4; j++) s[j] = 2 * y[j] - s[j]
|
|
96
|
+
|
|
97
|
+
data[i] = y[3]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return data
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/filter-diode-ladder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Diode ladder filter (Roland TB-303 / EMS VCS3 style)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "diode-ladder.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./diode-ladder.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"diode-ladder.js"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"audio",
|
|
17
|
+
"dsp",
|
|
18
|
+
"filter",
|
|
19
|
+
"diode-ladder"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/audiojs/filter.git",
|
|
26
|
+
"directory": "packages/filter-diode-ladder"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/audiojs/filter/tree/main/packages/filter-diode-ladder",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/audiojs/filter/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|