Created
September 4, 2022 04:13
-
-
Save 44100hertz/c1641d07a0e2af726e5f03880ff90abc to your computer and use it in GitHub Desktop.
hand-rolled FIR sinc filter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const WINDOW_SIZE = 4; /* Determines how much of normalized sinc function is sampled | |
1 = Just the center spike (-Pi/2 thru Pi/2) | |
*/ | |
const SAMPLE_POINTS = 63; // Determines quality of output | |
const MIN_FREQ = 20; // Determines buffer length | |
const MAX_FREQ = 20000; // Determines shutoff frequency | |
const coefficients = Array(SAMPLE_POINTS).fill(0).map((_,i) => { | |
const thru = i / (SAMPLE_POINTS-1); | |
const pos = thru * WINDOW_SIZE - WINDOW_SIZE / 2.0; | |
const sinc = pos === 0 ? 1 : Math.sin(pos * Math.PI) / pos / Math.PI; // Sinc (lowpass FIR) | |
const tailoff = 1 - 2 * Math.abs(thru - 0.5); // Triangle | |
// console.log(thru, pos, sinc, tailoff, sinc * tailoff); | |
return sinc * tailoff; | |
}); | |
console.log(coefficients); | |
const windowDivisor = coefficients.reduce((acc, x) => acc + x); | |
export default class Filter { | |
buffer: WaveBuffer; | |
private scale: number = 0; // How much to scale sinc window for desired frequency | |
private underflow: number = 0; | |
private mute: boolean = false; | |
private disable: boolean = false; | |
constructor( | |
private srate: number, | |
) { | |
this.buffer = new WaveBuffer(WINDOW_SIZE * srate / MIN_FREQ); | |
} | |
setFrequency(frequency: number) { | |
this.mute = frequency < MIN_FREQ; | |
this.disable = frequency >= MAX_FREQ; | |
this.scale = this.srate / frequency; | |
} | |
push(sample: number) { | |
this.underflow = 0; | |
this.buffer.push(sample); | |
} | |
getSample(): number { | |
++this.underflow; | |
if (this.mute) return 0; // TODO: fix popping | |
if (this.disable) return this.buffer.get(0); | |
let acc = 0; | |
for (let i=0; i<SAMPLE_POINTS; ++i) { | |
const samplePos = i / SAMPLE_POINTS; | |
const scale = coefficients[i]; | |
acc += this.buffer.get(samplePos * this.scale * WINDOW_SIZE) * scale; | |
} | |
return acc / windowDivisor; | |
} | |
isStopped(): boolean { | |
return this.underflow >= this.scale * WINDOW_SIZE; | |
} | |
} | |
// A simple buffer for filter and delay effects | |
// | |
class WaveBuffer { | |
buf: number[]; | |
pos: number; | |
constructor( | |
public buflen: number | |
) { | |
this.buf = new Array(buflen).fill(0); | |
this.pos = 0; | |
} | |
// Add a value to the ring buffer | |
push(v: number) { | |
this.buf[this.pos] = v; | |
this.pos = (this.pos + 1) % this.buflen; | |
} | |
// Get a value from [index] values ago. | |
get(index: number) { | |
if (index > this.buflen) return -1; | |
const pos = (this.pos - index + this.buflen) % this.buflen; | |
const ipos = Math.floor(pos); | |
const ipos2 = (ipos + 1) % this.buflen; | |
const fpos = pos - ipos; | |
return this.buf[ipos] * (1-fpos) + this.buf[ipos2] * fpos; // Linear interpolation | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment