UNPKG

dsp-filter-library

Version:

A comprehensive DSP library with 23 window functions, advanced FIR/IIR filter design, biquad combination, and interactive visualization

79 lines (72 loc) 3.82 kB
// IIR filter designer import { BLT } from "../digital/BLT.js"; import { Response } from "../digital/Response.js"; import { SOS } from "../digital/SOS.js"; import { Prototypes } from "../analog/Prototypes.js"; import { IIRFilter } from "../model/IIRFilter.js"; function divC(a,b){ const d=b.re*b.re+b.im*b.im||1e-300; return {re:(a.re*b.re+0*b.im)/d, im:(0*b.re-a.re*b.im)/d}; } export class IIRDesigner { constructor(spec){ this.spec = spec; // {family, kind, N, Rp?, Rs?, Fs, f1, f2?} } design(){ const { family, kind, Fs } = this.spec; let { N, f1, f2 } = this.spec; const Rp=this.spec.Rp ?? 1, Rs=this.spec.Rs ?? 60; // Prototype let proto; switch(family){ case 'butter': proto=Prototypes.butter(N); break; case 'cheby1': proto=Prototypes.cheby1(N, Rp); break; case 'cheby2': proto=Prototypes.cheby2(N, Rs); break; case 'ellip': proto=Prototypes.ellipHybrid(N, Rp, Rs); break; case 'linkwitz': { const lr=Prototypes.linkwitzRiley(N); N=lr.enforcedOrder ?? N; proto={poles:lr.poles, zeros:[]}; break; } case 'bessel': proto=Prototypes.bessel(N); break; default: proto=Prototypes.butter(N); } proto.family=family; proto.order=N; // Map + BLT let zPoles=[], zZeros=[]; if(kind==='lowpass'||kind==='highpass'){ const Oc=BLT.prewarp(f1,Fs); const sPoles=proto.poles.map(p=> kind==='lowpass'? {re:p.re*Oc, im:p.im*Oc} : divC({re:Oc,im:0}, p)); const sZeros=proto.zeros.map(z=> kind==='lowpass'? {re:z.re*Oc, im:z.im*Oc} : divC({re:Oc,im:0}, z)); zPoles=sPoles.map(s=>BLT.sToZ(s,Fs)); zZeros=sZeros.map(s=>BLT.sToZ(s,Fs)); if((['butter','cheby1','bessel','linkwitz'].includes(family)) && kind==='highpass'){ for(let k=0;k<N;k++) zZeros.push({re:1,im:0}); } } else { if(!f2) throw new Error('bandpass/bandstop require f2'); if(f2<f1){ const t=f1; f1=f2; f2=t; } const O1=BLT.prewarp(f1,Fs), O2=BLT.prewarp(f2,Fs), B=O2-O1, O0=Math.sqrt(O1*O2); const sPoles=[], sZeros=[]; if(kind==='bandpass'){ for(const p of proto.poles){ const [r1,r2]=BLT.quad({re:1,im:0},{re:-(p.re*B),im:-(p.im*B)},{re:O0*O0,im:0}); sPoles.push(r1,r2); } for(const z of proto.zeros){ const [r1,r2]=BLT.quad({re:1,im:0},{re:-(z.re*B),im:-(z.im*B)},{re:O0*O0,im:0}); sZeros.push(r1,r2); } if(['butter','cheby1','bessel','linkwitz'].includes(family)){ for(let k=0;k<N;k++) sZeros.push({re:0,im:0}); } zPoles=sPoles.map(s=>BLT.sToZ(s,Fs)); zZeros=sZeros.map(s=>BLT.sToZ(s,Fs)); if(['butter','cheby1','bessel','linkwitz'].includes(family)){ for(let k=0;k<N;k++) zZeros.push({re:-1,im:0}); } else if(family==='cheby2' && (N%2===1)){ zZeros.push({re:1,im:0},{re:-1,im:0}); } } else { for(const p of proto.poles){ const [r1,r2]=BLT.quad({re:p.re,im:p.im},{re:-B,im:0},{re:p.re*O0*O0,im:p.im*O0*O0}); sPoles.push(r1,r2); } for(const z of proto.zeros){ const [r1,r2]=BLT.quad({re:z.re,im:z.im},{re:-B,im:0},{re:z.re*O0*O0,im:z.im*O0*O0}); sZeros.push(r1,r2); } for(let k=0;k<N;k++){ sZeros.push({re:0,im:O0},{re:0,im:-O0}); } zPoles=sPoles.map(s=>BLT.sToZ(s,Fs)); zZeros=sZeros.map(s=>BLT.sToZ(s,Fs)); } } // SOS + gain normalization const sections = SOS.fromZPK(zZeros, zPoles, 1); let wRef=0; if(kind==='highpass') wRef=Math.PI; if(kind==='bandpass'){ const f0=Math.sqrt(f1*f2); wRef=2*Math.PI*(f0/Fs); } const Href=Response.H_w_IIR(sections, wRef), g=1/(Math.hypot(Href.re,Href.im)||1e-12); sections[0].b=[sections[0].b[0]*g, sections[0].b[1]*g, sections[0].b[2]*g]; return new IIRFilter({sections, Fs, zPoles, zZeros}); } }