UNPKG

biquadjs

Version:

Create arbitrary biquad filters to apply to signals. Created for real time biosensing, works in most general cases.

311 lines (261 loc) 10.8 kB
<html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js"></script> </head> <body> F12 to open console and see results <h3 id="string1"></h3> Sine wave in, 20Hz with 60Hz interference, each at amplitude = 1: <div id="sinechart"></div> <h3 id="string2"></h3> 60Hz Biquad Single Notch Filtered: <div id="zchart"></div> 3-45Hz Biquad Quadruple Band Pass Filtered: <div id="zchart2"></div> 40Hz Biquad Quadruple Low Pass Filtered. Loss due to butterworth falloff not well suited for low frequencies: <div id="zchart3"></div> <script> // https://arachnoid.com/phase_locked_loop/resources/biquad_module.py //Translated to JS by Josh Brewster class Biquad { constructor(type,freq,sps,Q=1/Math.sqrt(2),dbGain=0) { let types = ['lowpass','highpass','bandpass','notch','peak','lowshelf','highshelf']; if(types.indexOf(type) < 0) { console.error("Valid types: 'lowpass','highpass','bandpass','notch','peak','lowshelf','highshelf'"); return false; } this.type = type; this.freq = freq; this.sps = sps; this.Q = Q; this.dbGain = dbGain; this.a0 = 0,this.a1 = 0,this.a2 = 0, this.b0 = 0,this.b1 = 0,this.b2 = 0; this.x1 = 0,this.x2 = 0, this.y1 = 0,this.y2 = 0; let A = Math.pow(10,dbGain/40); let omega = 2*Math.PI*freq/sps; let sn = Math.sin(omega) let cs = Math.cos(omega); let alpha = sn/(2*Q); let beta = Math.sqrt(A+A); this[type](A,sn,cs,alpha,beta); //scale constants this.b0 /= this.a0; this.b1 /= this.a0; this.b2 /= this.a0; this.a1 /= this.a0; this.a2 /= this.a0; } lowpass(A,sn,cs,alpha,beta) { //Stop upper frequencies this.b0 = (1-cs)*.5; this.b1 = 1-cs; this.b2 = (1-cs)*.5; this.a0 = 1+alpha; this.a1 = -2*cs; this.a2 = 1-alpha; } highpass(A,sn,cs,alpha,beta) { //Stop lower frequencies this.b0 = (1+cs)*.5; this.b1 = -(1+cs); this.b2 = (1+cs)*.5; this.a0 = 1 + alpha; this.a1 = -2*cs; this.a2 = 1-alpha; } bandpass(A,sn,cs,alpha,beta) { //Stop lower and upper frequencies. Q = frequency_resonant / Bandwidth(to 3db cutoff line); frequency_resonant = Math.sqrt(f_low * f_high); So for 60Hz with 0.5Hz bandwidth: Fr = Math.sqrt(59.5*60.5). Q = Fr/0.5 = 120; this.b0 = alpha; this.b1 = 0; this.b2 = -alpha; this.a0 = 1+alpha; this.a1 = -2*cs; this.a2 = 1-alpha; } notch(A,sn,cs,alpha,beta) { //Stop a specific frequency this.b0 = 1; this.b1 = -2*cs; this.b2 = 1; this.a0 = 1+alpha; this.a1 = -2*cs; this.a2 = 1-alpha; } peak(A,sn,cs,alpha,beta) { //Opposite of a notch filter, stop all but one frequency this.b0 = 1+(alpha*A); this.b1 = -2*cs; this.b2 = 1-(alpha*A); this.a0 = 1+(alpha/A); this.a1 = -2*cs; this.a2 = 1-(alpha/A); } lowshelf(A,sn,cs,alpha,beta) { //Amplify signals below the cutoff this.b0 = A*((A+1) - (A-1)*cs + beta*sn); this.b1 = 2*A*((A-1)-(A+1)*cs); this.b2 = A*((A+1) - (A-1)*cs - beta*sn); this.a0 = (A+1) + (A+1)*cs + beta*sn; this.a1 = 2*((A-1) + (A+1)*cs); this.a2 = (A+1) + (A-1)*cs - beta*sn; } highshelf(A,sn,cs,alpha,beta) { //Amplify signals above the cutoff this.b0 = A*((A+1) + (A-1)*cs + beta*sn); this.b1 = 2*A*((A-1) + (A+1)*cs); this.b2 = A*((A+1) - (A-1)*cs - beta*sn); this.a0 = (A+1) - (A+1)*cs - beta*sn; this.a1 = 2*((A-1) - (A+1)*cs); this.a2 = (A+1) - (A-1)*cs - beta*sn; } applyFilter(signal_step) { //Step the filter forward, return modulated signal amplitude let y = this.b0*signal_step + this.b1*this.x1 + this.b2*this.x2 - this.a1*this.y1 - this.a2*this.y2; this.x2 = this.x1; this.x1 = signal_step; this.y2 = this.y1; this.y1 = y; return y; } zResult(freq) { //This should return the z-transfer function values. Max freq = sps/2 try{ let phi = Math.pow((Math.sin(Math.PI*freq*2/(2*this.sps))),2); let result = (Math.pow(this.b0+this.b1+this.b2,2) - 4*(this.b0*this.b1+4*this.b0*this.b2 + this.b1*this.b2)*phi + 16*this.b0*this.b2*phi*phi) / (Math.pow(1+this.a1+this.a2,2) - 4*(this.a1 + 4*this.a2 + this.a1*this.a2)*phi + 16*this.a2*phi*phi) return result; } catch(err) { return -200; } } //Get the center frequency for your bandpass filter static calcCenterFrequency(freqStart,freqEnd) { return (freqStart+freqEnd) / 2; } static calcBandwidth(freqStart,freqEnd) { return (freqEnd-this.calcCenterFrequency(freqStart,freqEnd)); } //Use for bandpass or peak filter //Q gets sharper as resonance approaches infinity. Set to 500 for example for a more precise filter. Recommended r: 10 static calcBandpassQ (frequency, bandwidth, resonance=Math.pow(10,Math.floor(Math.log10(frequency)))) { //Use Math.sqrt(0.5) for low pass, high pass, and shelf filters let Q = resonance*Math.sqrt((frequency-bandwidth)*(frequency+bandwidth))/(2*bandwidth); //Could just do f/bw return Q; } static calcNotchQ (frequency, bandwidth, resonance=Math.pow(10,Math.floor(Math.log10(frequency)))) { //Q gets sharper as resonance approaches infinity. Recommended r: 10 let Q = resonance*frequency*bandwidth/Math.sqrt((frequency-bandwidth)*(frequency+bandwidth)); // bw/f return Q; } } class DCBlocker { constructor(r=0.995) { this.r = r; this.y1=this.y2=this.x1=this.x2=0; } applyFilter(signal_step) { this.x2=this.x1; this.x1 = signal_step let y = this.x1 - this.x2 + this.r*this.y1; this.y2 = this.y1; this.y1 = y; return y; } } function genSineWave(freq=20,peakAmp=1,nSec=1,fs=512,freq2=0,peakAmp2=1){ var sineWave = []; var t = []; var increment = 1/fs; //x-axis time increment based on sample rate for (var ti = 0; ti < nSec; ti+=increment){ var amplitude = Math.sin(2*Math.PI*freq*ti)*peakAmp; amplitude += Math.sin(2*Math.PI*freq2*ti)*peakAmp2; //Add interference sineWave.push(amplitude); t.push(ti); } return [t,sineWave]; // [[times],[amplitudes]] } let freq_in = 20; let freq_intf = 60; let nSec = 2; let fs = 512; let wave = genSineWave(freq_in,1,nSec,fs,freq_intf); let wave_amps = wave[1]; let wave_t = wave[0]; console.log("amplitudes in", wave_amps); let notch = new Biquad('notch',freq_intf,512,Biquad.calcNotchQ(freq_intf,1),0); let notch2 = new Biquad('notch',freq_intf,512,Biquad.calcNotchQ(freq_intf,1),0); let notch3 = new Biquad('notch',freq_intf,512,Biquad.calcNotchQ(freq_intf,1),0); let bandpass = new Biquad('bandpass', Biquad.calcCenterFrequency(3,45), fs, Biquad.calcBandpassQ(Biquad.calcCenterFrequency(3,45),Biquad.calcBandwidth(3,45),9.75), 0); let bandpass2 = new Biquad('bandpass', Biquad.calcCenterFrequency(3,45), fs, Biquad.calcBandpassQ(Biquad.calcCenterFrequency(3,45),Biquad.calcBandwidth(3,45),9.75), 0); let bandpass3 = new Biquad('bandpass', Biquad.calcCenterFrequency(3,45), fs, Biquad.calcBandpassQ(Biquad.calcCenterFrequency(3,45),Biquad.calcBandwidth(3,45),9.75), 0); let bandpass4 = new Biquad('bandpass', Biquad.calcCenterFrequency(3,45), fs, Biquad.calcBandpassQ(Biquad.calcCenterFrequency(3,45),Biquad.calcBandwidth(3,45),9.75), 0); let lowpass = new Biquad('lowpass', 40, 512); let lowpass2 = new Biquad('lowpass', 40, 512); let lowpass3 = new Biquad('lowpass', 40, 512); let lowpass4 = new Biquad('lowpass', 40, 512); let wave_filtered_notch = []; let wave_filtered_bp = []; let wave_filtered_lp = []; wave_amps.forEach((amp,i) => { wave_filtered_notch.push(notch.applyFilter(amp)); //wave_filtered_bp.push(bandpass.applyFilter(amp)); //Need to rescale the outputs for some reason but otherwise it's accurate wave_filtered_bp.push(4*bandpass4.applyFilter(bandpass3.applyFilter(bandpass2.applyFilter(bandpass.applyFilter(amp))))); //Need to rescale the outputs for some reason but otherwise it's accurate wave_filtered_lp.push(lowpass4.applyFilter(lowpass3.applyFilter(lowpass2.applyFilter(lowpass.applyFilter(amp))))); }); //console.log("filtered: ",wave_filtered_notch) let data = [ [{ x: wave_t, y: wave_amps, type: 'line', marker: { color: "rgba(255,100,100, 1)" }, name: "Sine", //xbins: { size: 0.01 } }] ,[{ x: wave_t, y: wave_filtered_notch, type: 'line', marker: { color: "rgba(100,255,100, 1)" }, name: "Filtered", //xbins: { size: 0.01 } }] ,[{ x: wave_t, y: wave_filtered_bp, type: 'line', marker: { color: "rgba(100,255,100, 1)" }, name: "Filtered2", //xbins: { size: 0.01 } }],[{ x: wave_t, y: wave_filtered_lp, type: 'line', marker: { color: "rgba(100,255,100, 1)" }, name: "Filtered3", //xbins: { size: 0.01 } }] ]; let config = { scrollZoom: true, responsive: true, } Plotly.newPlot("sinechart",data[0],undefined,config); Plotly.newPlot("zchart",data[1],undefined,config); Plotly.newPlot("zchart2",data[2],undefined,config); Plotly.newPlot("zchart3",data[3],undefined,config); </script> </body> </html>