Skip to content

Commit 9bead8d

Browse files
committed
Add extra plots
1 parent e1d061f commit 9bead8d

13 files changed

Lines changed: 1274 additions & 306 deletions

plot/biquad-types.svg

Lines changed: 15 additions & 68 deletions
Loading

plot/butterworth-orders.svg

Lines changed: 20 additions & 21 deletions
Loading

plot/generate.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ write('moving-average', plotFir(impulse(dsp.movingAverage, {memory: 8}), 'Moving
8080
write('leaky-integrator', plotFir(impulse(dsp.leakyIntegrator, {lambda: 0.95}), 'Leaky integrator, λ=0.95'))
8181
write('gaussian-iir', plotFir(impulse(dsp.gaussianIir, {sigma: 5}), 'Gaussian IIR, σ=5'))
8282
write('dynamic-smoothing', plotFir(impulse(dsp.dynamicSmoothing, {minFc: 1, maxFc: 5000, sensitivity: 1, fs: FS}), 'Dynamic smoothing, fc=1–5kHz'))
83+
write('median', plotFir(impulse(dsp.median, {size: 5}), 'Median filter, size=5'))
84+
write('one-euro', plotFir(impulse(dsp.oneEuro, {minCutoff: 1, beta: 0.007, dCutoff: 1, fs: FS}), '1€ filter, minCutoff=1, β=0.007'))
85+
write('lattice', plotFir(impulse(dsp.lattice, {k: [0.5, -0.3, 0.2, -0.1]}), 'Lattice, k=[0.5, −0.3, 0.2, −0.1]'))
86+
write('warped-fir', plotFir(impulse(dsp.warpedFir, {coefs: new Float64Array([0.5, 0.3, 0.15, 0.05]), lambda: 0.7}), 'Warped FIR, λ=0.7'))
8387

8488
// ── Multirate ──
8589

plot/iir-comparison.svg

Lines changed: 22 additions & 28 deletions
Loading

plot/index.js

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -426,40 +426,89 @@ export function plotFir (h, title, opts = {}) {
426426
}
427427

428428
/**
429-
* Generate 4-panel SVG comparing multiple SOS filters overlaid.
430-
* @param {Array} filters - Array of [name, sos] or [name, sos, color]
429+
* Generate 4-panel SVG comparing multiple SOS filters or FIR impulse responses overlaid.
430+
* @param {Array} filters - Array of [name, sos|fir] or [name, sos|fir, color]
431+
* sos: [{b0,b1,b2,a1,a2}, ...] — IIR biquad cascade
432+
* fir: Float64Array|number[] — impulse response
431433
* @param {string} [title] - Plot title
432-
* @param {object} [opts] - Options: { fs, bins, irLength }
434+
* @param {object} [opts] - Options: { fs, bins, irLength, irMax, gdMin, gdMax }
433435
* @returns {string} SVG markup
434436
*/
435437
export function plotCompare (filters, title, opts = {}) {
436438
let fs = opts.fs || theme.fs, NF = opts.bins || theme.bins
437-
let irLen = opts.irLength || 128
439+
440+
function isFir (data) {
441+
return !(Array.isArray(data) && data.length > 0 && typeof data[0] === 'object' && 'b0' in data[0])
442+
}
443+
444+
function firFreqz (h) {
445+
let frequencies = new Float64Array(NF), magnitude = new Float64Array(NF), phase = new Float64Array(NF)
446+
for (let k = 0; k < NF; k++) {
447+
frequencies[k] = k * fs / (2 * NF)
448+
let re = 0, im = 0, w = Math.PI * k / NF
449+
for (let n = 0; n < h.length; n++) { re += h[n] * Math.cos(w * n); im -= h[n] * Math.sin(w * n) }
450+
magnitude[k] = Math.sqrt(re * re + im * im)
451+
phase[k] = Math.atan2(im, re)
452+
}
453+
let delay = new Float64Array(NF)
454+
let phDeg = Array.from(phase).map(v => v * 180 / Math.PI)
455+
for (let k = 1; k < NF - 1; k++) {
456+
let dp = phDeg[k + 1] - phDeg[k - 1]
457+
if (dp > 180) dp -= 360; if (dp < -180) dp += 360
458+
delay[k] = -dp / (2 * Math.PI / NF * 180 / Math.PI)
459+
}
460+
delay[0] = delay[1]
461+
return { frequencies, magnitude, phase, delay }
462+
}
463+
464+
// Pre-compute all responses for range detection
465+
let defaultIrLen = opts.irLength || 128
466+
let computed = filters.map(([, data]) => {
467+
if (isFir(data)) {
468+
let r = firFreqz(data)
469+
return { r, ir: data, gdFreqs: r.frequencies, gdDelay: r.delay }
470+
}
471+
let r = freqz(data, NF, fs)
472+
let gd = groupDelay(data, NF, fs)
473+
return { r, ir: impulseResponse(data, defaultIrLen), gdFreqs: gd.frequencies, gdDelay: gd.delay }
474+
})
475+
476+
// Auto-range: irMax and irLen from actual data
477+
let irLen = opts.irLength ?? Math.max(...computed.map(({ ir }) => ir.length))
478+
let irMax = opts.irMax ?? 0
479+
if (!opts.irMax) {
480+
for (let { ir } of computed) for (let i = 0; i < ir.length; i++) { let v = Math.abs(ir[i]); if (v > irMax) irMax = v }
481+
irMax = Math.max(irMax * 1.15, 1e-10)
482+
}
483+
484+
// Auto-range: group delay from actual data (unless overridden)
485+
let gdLo = opts.gdMin ?? null, gdHi = opts.gdMax ?? null
486+
if (gdLo == null || gdHi == null) {
487+
let allGd = []
488+
for (let { gdDelay } of computed) for (let v of gdDelay) if (isFinite(v)) allGd.push(v)
489+
let [lo, hi] = allGd.length ? robustRange(allGd) : [-25, 5]
490+
if (gdLo == null) gdLo = lo
491+
if (gdHi == null) gdHi = hi
492+
}
438493

439494
let s = svgOpen()
440495
if (title) s += ` <text x="${P2.x+P2.w}" y="${P2.y-5}" text-anchor="end" font-size="11" font-weight="600" fill="${theme.text}">${title}</text>\n`
441496

442497
let fc = parseFc(title)
443498
s += panel(P1, 'Hz', 'dB', -80, 20, 0) + logXTicks(P1, fTicks, 10, 20000) + dbGrid(P1) + fcLine(P1, fc)
444499
s += panel(P2, 'Hz', 'Phase (deg)', -180, 180, 0) + logXTicks(P2, fTicks, 10, 20000) + phaseGrid(P2) + fcLine(P2, fc)
445-
446-
let gdLo = opts.gdMin ?? -25, gdHi = opts.gdMax ?? 5
447500
s += panel(P3, 'Hz', 'Group delay', gdLo, gdHi, 0) + logXTicks(P3, fTicks, 10, 20000) + hTicks(P3, autoTicks(gdLo, gdHi, 4), gdLo, gdHi) + fcLine(P3, fc)
448-
449-
let irMax = opts.irMax ?? 0.35
450501
s += panel(P4, 'Samples', 'Impulse', -irMax, irMax, 0) + linXTicks(P4, autoTicks(0, irLen, 3).map(Math.round), 0, irLen) + hTicks(P4, autoTicks(-irMax, irMax, 3), -irMax, irMax)
451502

452503
for (let i = 0; i < filters.length; i++) {
453-
let [name, sos, c] = filters[i]
504+
let [,, c] = filters[i]
454505
c = c || theme.colors[i % theme.colors.length]
455-
let r = freqz(sos, NF, fs)
506+
let { r, ir, gdFreqs, gdDelay } = computed[i]
456507
let db = Array.from(mag2db(r.magnitude))
457508
let phase = Array.from(r.phase).map(v => v * 180 / Math.PI)
458-
let gd = groupDelay(sos, NF, fs)
459-
let ir = impulseResponse(sos, irLen)
460509
s += logPoly(P1, r.frequencies, db, 10, 20000, -80, 20, c, 1.3, false)
461510
s += logPoly(P2, r.frequencies, phase, 10, 20000, -180, 180, c, 1.3, false)
462-
s += logPoly(P3, gd.frequencies, Array.from(gd.delay), 10, 20000, gdLo, gdHi, c, 1.3, false)
511+
s += logPoly(P3, gdFreqs, Array.from(gdDelay), 10, 20000, gdLo, gdHi, c, 1.3, false)
463512
s += linPoly(P4, ir, 0, irLen, -irMax, irMax, c)
464513
}
465514

plot/lattice.svg

Lines changed: 191 additions & 0 deletions
Loading

plot/lms.svg

Lines changed: 40 additions & 38 deletions
Loading

plot/median.svg

Lines changed: 183 additions & 0 deletions
Loading

plot/nlms.svg

Lines changed: 33 additions & 28 deletions
Loading

plot/one-euro.svg

Lines changed: 197 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)