echogarden
Version:
An easy-to-use speech toolset. Includes tools for synthesis, recognition, alignment, speech translation, language detection, source separation and more.
497 lines (413 loc) • 12.8 kB
text/typescript
// Based on the Chromium WebAudio source code at:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/web_tests/webaudio/resources/biquad-filters.js
//
// A biquad filter has a z-transform of
// H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
//
// The formulas for the various filters were taken from:
// https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
//
// MDN documentation:
// https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode
export function createLowpassFilter(sampleRate: number, cutoffFrequency: number, q = 0.7071) {
return createFilter('lowpass', sampleRate, cutoffFrequency, q, 0)
}
export function createHighpassFilter(sampleRate: number, cutoffFrequency: number, q = 0.7071) {
return createFilter('highpass', sampleRate, cutoffFrequency, q, 0)
}
export function createBandpassFilter(sampleRate: number, centerFrequency: number, q = 1) {
return createFilter('bandpass', sampleRate, centerFrequency, q, 0)
}
export function createLowshelfFilter(sampleRate: number, midpointFrequency: number, gain: number) {
return createFilter('lowshelf', sampleRate, midpointFrequency, 0, gain)
}
export function createHighshelfFilter(sampleRate: number, midpointFrequency: number, gain: number) {
return createFilter('highshelf', sampleRate, midpointFrequency, 0, gain)
}
export function createPeakingFilter(sampleRate: number, centerFrequency: number, q = 1, gain = 0) {
return createFilter('peaking', sampleRate, centerFrequency, q, gain)
}
export function createNotchFilter(sampleRate: number, centerFrequency: number, q = 1) {
return createFilter('notch', sampleRate, centerFrequency, q, 0)
}
export function createAllpassFilter(sampleRate: number, centerFrequency: number, q = 1) {
return createFilter('allpass', sampleRate, centerFrequency, q, 0)
}
export function createFilter(filterType: FilterType, sampleRate: number, frequency: number, q: number, gain: number) {
const coefficients = getFilterCoefficients(filterType, sampleRate, frequency, q, gain)
return new BiquadFilter(coefficients)
}
export class BiquadFilter {
private b0 = 0
private b1 = 0
private b2 = 0
private a1 = 0
private a2 = 0
private prevInput1 = 0
private prevInput2 = 0
private prevOutput1 = 0
private prevOutput2 = 0
constructor(coefficients: FilterCoefficients) {
this.setCoefficients(coefficients)
}
filter(sample: number): number {
const filteredSample =
(this.b0 * sample) +
(this.b1 * this.prevInput1) +
(this.b2 * this.prevInput2) -
(this.a1 * this.prevOutput1) -
(this.a2 * this.prevOutput2)
this.prevInput2 = this.prevInput1
this.prevInput1 = sample
this.prevOutput2 = this.prevOutput1
this.prevOutput1 = filteredSample
return filteredSample
}
filterSamplesInPlace(samples: Float32Array) {
for (let i = 0; i < samples.length; i++) {
samples[i] = this.filter(samples[i])
}
}
reset() {
this.prevInput1 = 0
this.prevInput2 = 0
this.prevOutput1 = 0
this.prevOutput2 = 0
}
setCoefficients(coefficients: FilterCoefficients) {
this.b0 = coefficients.b0
this.b1 = coefficients.b1
this.b2 = coefficients.b2
this.a1 = coefficients.a1
this.a2 = coefficients.a2
}
}
export function getFilterCoefficients(filterType: FilterType, sampleRate: number, centerFrequency: number, q: number, gain: number) {
const nyquistFrequency = sampleRate / 2
const freqRatio = clamp(centerFrequency / nyquistFrequency, 0, 1)
return filterCoefficientsFunction[filterType](freqRatio, q, gain) as FilterCoefficients
}
// Lowpass filter.
export function getLowpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
if (freqRatio == 1) {
// The formula below works, except for roundoff. When freqRatio = 1,
// the filter is just a wire, so hardwire the coefficients.
b0 = 1
b1 = 0
b2 = 0
a0 = 1
a1 = 0
a2 = 0
} else {
const theta = Math.PI * freqRatio
const alpha = Math.sin(theta) / (2 * Math.pow(10, q / 20))
const cosw = Math.cos(theta)
const beta = (1 - cosw) / 2
b0 = beta
b1 = 2 * beta
b2 = beta
a0 = 1 + alpha
a1 = -2 * cosw
a2 = 1 - alpha
}
return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
}
function getHighpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
if (freqRatio == 1) {
// The filter is 0
b0 = 0
b1 = 0
b2 = 0
a0 = 1
a1 = 0
a2 = 0
} else if (freqRatio == 0) {
// The filter is 1. Computation of coefficients below is ok, but
// there's a pole at 1 and a zero at 1, so round-off could make
// the filter unstable.
b0 = 1
b1 = 0
b2 = 0
a0 = 1
a1 = 0
a2 = 0
} else {
const theta = Math.PI * freqRatio
const alpha = Math.sin(theta) / (2 * Math.pow(10, q / 20))
const cosw = Math.cos(theta)
const beta = (1 + cosw) / 2
b0 = beta
b1 = -2 * beta
b2 = beta
a0 = 1 + alpha
a1 = -2 * cosw
a2 = 1 - alpha
}
return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
}
function getBandpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
let coefficients: FilterCoefficients
if (freqRatio > 0 && freqRatio < 1) {
const w0 = Math.PI * freqRatio
if (q > 0) {
const alpha = Math.sin(w0) / (2 * q)
const k = Math.cos(w0)
b0 = alpha
b1 = 0
b2 = -alpha
a0 = 1 + alpha
a1 = -2 * k
a2 = 1 - alpha
coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
} else {
// q = 0, and frequency ratio is not 0 or 1. The above formula has a
// divide by zero problem. The limit of the z-transform as q
// approaches 0 is 1, so set the filter that way.
coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
}
} else {
// When freqRatio = 0 or 1, the z-transform is identically 0,
// independent of q.
coefficients = { b0: 0, b1: 0, b2: 0, a1: 0, a2: 0 }
}
return coefficients
}
function getLowShelfFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
// q not used
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
let coefficients: FilterCoefficients
const S = 1
const A = Math.pow(10, gain / 40)
if (freqRatio == 1) {
// The filter is just a constant gain
coefficients = { b0: A * A, b1: 0, b2: 0, a1: 0, a2: 0 }
} else if (freqRatio == 0) {
// The filter is 1
coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
} else {
const w0 = Math.PI * freqRatio
const alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2)
const k = Math.cos(w0)
const k2 = 2 * Math.sqrt(A) * alpha
const Ap1 = A + 1
const Am1 = A - 1
b0 = A * (Ap1 - Am1 * k + k2)
b1 = 2 * A * (Am1 - Ap1 * k)
b2 = A * (Ap1 - Am1 * k - k2)
a0 = Ap1 + Am1 * k + k2
a1 = -2 * (Am1 + Ap1 * k)
a2 = Ap1 + Am1 * k - k2
coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
}
return coefficients
}
function getHighShelfFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
// q not used
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
let coefficients: FilterCoefficients
const A = Math.pow(10, gain / 40)
if (freqRatio == 1) {
// When freqRatio = 1, the z-transform is 1
coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
} else if (freqRatio > 0) {
const w0 = Math.PI * freqRatio
const S = 1
const alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2)
const k = Math.cos(w0)
const k2 = 2 * Math.sqrt(A) * alpha
const Ap1 = A + 1
const Am1 = A - 1
b0 = A * (Ap1 + Am1 * k + k2)
b1 = -2 * A * (Am1 + Ap1 * k)
b2 = A * (Ap1 + Am1 * k - k2)
a0 = Ap1 - Am1 * k + k2
a1 = 2 * (Am1 - Ap1 * k)
a2 = Ap1 - Am1 * k - k2
coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
} else {
// When freqRatio = 0, the filter is just a gain
coefficients = { b0: A * A, b1: 0, b2: 0, a1: 0, a2: 0 }
}
return coefficients
}
function getPeakingFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
let coefficients: FilterCoefficients
const A = Math.pow(10, gain / 40)
if (freqRatio > 0 && freqRatio < 1) {
if (q > 0) {
const w0 = Math.PI * freqRatio
const alpha = Math.sin(w0) / (2 * q)
const k = Math.cos(w0)
b0 = 1 + alpha * A
b1 = -2 * k
b2 = 1 - alpha * A
a0 = 1 + alpha / A
a1 = -2 * k
a2 = 1 - alpha / A
coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
} else {
// q = 0, we have a divide by zero problem in the formulas
// above. But if we look at the z-transform, we see that the
// limit as q approaches 0 is A^2.
coefficients = { b0: A * A, b1: 0, b2: 0, a1: 0, a2: 0 }
}
} else {
// freqRatio = 0 or 1, the z-transform is 1
coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
}
return coefficients
}
function getNotchFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
let coefficients: FilterCoefficients
if (freqRatio > 0 && freqRatio < 1) {
if (q > 0) {
const w0 = Math.PI * freqRatio
const alpha = Math.sin(w0) / (2 * q)
const k = Math.cos(w0)
b0 = 1
b1 = -2 * k
b2 = 1
a0 = 1 + alpha
a1 = -2 * k
a2 = 1 - alpha
coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
} else {
// When q = 0, we get a divide by zero above. The limit of the
// z-transform as q approaches 0 is 0, so set the coefficients
// appropriately.
coefficients = { b0: 0, b1: 0, b2: 0, a1: 0, a2: 0 }
}
} else {
// When freqRatio = 0 or 1, the z-transform is 1
coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
}
return coefficients
}
function getAllpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
let b0: number
let b1: number
let b2: number
let a0: number
let a1: number
let a2: number
let coefficients: FilterCoefficients
if (freqRatio > 0 && freqRatio < 1) {
if (q > 0) {
const w0 = Math.PI * freqRatio
const alpha = Math.sin(w0) / (2 * q)
const k = Math.cos(w0)
b0 = 1 - alpha
b1 = -2 * k
b2 = 1 + alpha
a0 = 1 + alpha
a1 = -2 * k
a2 = 1 - alpha
coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
} else {
// q = 0
coefficients = { b0: -1, b1: 0, b2: 0, a1: 0, a2: 0 }
}
} else {
coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
}
return coefficients
}
function normalizeFilterCoefficients(b0: number, b1: number, b2: number, a0: number, a1: number, a2: number): FilterCoefficients {
const scale = 1 / a0
return {
b0: b0 * scale,
b1: b1 * scale,
b2: b2 * scale,
a1: a1 * scale,
a2: a2 * scale
}
}
export function bandwidthToQFactor(bandwidth: number) {
const twoToBandwidth = 2 ** bandwidth
const q = Math.sqrt(twoToBandwidth) / (twoToBandwidth - 1)
return q
}
export function qFactorToBandwidth(q: number) {
const bandwidth = (2 / Math.log(2)) * Math.asinh(1 / (2 * q))
return bandwidth
}
export function clamp(num: number, min: number, max: number) {
return Math.max(min, Math.min(max, num))
}
// Map the filter type name to a function that computes the filter coefficents
// for the given filter type.
const filterCoefficientsFunction: { [name in FilterType]: Function } = {
'lowpass': getLowpassFilterCoefficients,
'highpass': getHighpassFilterCoefficients,
'bandpass': getBandpassFilterCoefficients,
'lowshelf': getLowShelfFilterCoefficients,
'highshelf': getHighShelfFilterCoefficients,
'peaking': getPeakingFilterCoefficients,
'notch': getNotchFilterCoefficients,
'allpass': getAllpassFilterCoefficients
}
export const filterTypeName: { [name in FilterType]: string } = {
'lowpass': 'Lowpass filter',
'highpass': 'Highpass filter',
'bandpass': 'Bandpass filter',
'lowshelf': 'Lowshelf filter',
'highshelf': 'Highshelf filter',
'peaking': 'Peaking filter',
'notch': 'Notch filter',
'allpass': 'Allpass filter'
}
export type FilterType = 'lowpass' | 'highpass' | 'bandpass' | 'lowshelf' | 'highshelf' | 'peaking' | 'notch' | 'allpass'
export type FilterCoefficients = {
b0: number
b1: number
b2: number
a1: number
a2: number
}
export const emptyBiquadCoefficients: FilterCoefficients = {
b0: 0,
b1: 0,
b2: 0,
a1: 0,
a2: 0,
}