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
<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>