-
Notifications
You must be signed in to change notification settings - Fork 993
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simplified Filter class without the "rolloff" param closes #686
- Loading branch information
Showing
3 changed files
with
270 additions
and
28 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { expect } from "chai"; | ||
import { BasicTests } from "test/helper/Basic"; | ||
import { Offline } from "test/helper/Offline"; | ||
import { PassAudio } from "test/helper/PassAudio"; | ||
import { Oscillator } from "../../source/oscillator/Oscillator"; | ||
import { BiquadFilter } from "./BiquadFilter"; | ||
|
||
describe("BiquadFilter", () => { | ||
|
||
BasicTests(BiquadFilter); | ||
|
||
context("BiquadFiltering", () => { | ||
|
||
it("can be constructed with a arguments", () => { | ||
const filter = new BiquadFilter(200, "highpass"); | ||
expect(filter.frequency.value).to.be.closeTo(200, 0.001); | ||
expect(filter.type).to.equal("highpass"); | ||
filter.dispose(); | ||
}); | ||
|
||
it("can be constructed with an object", () => { | ||
const filter = new BiquadFilter({ | ||
frequency: 340, | ||
type: "bandpass", | ||
}); | ||
expect(filter.frequency.value).to.be.closeTo(340, 0.001); | ||
expect(filter.type).to.equal("bandpass"); | ||
filter.dispose(); | ||
}); | ||
|
||
it("can set/get values as an Object", () => { | ||
const filter = new BiquadFilter(); | ||
const values = { | ||
Q: 2, | ||
frequency: 440, | ||
gain: -6, | ||
type: "lowshelf" as const | ||
}; | ||
filter.set(values); | ||
expect(filter.get()).to.include.keys(["type", "frequency", "Q", "gain"]); | ||
expect(filter.type).to.equal(values.type); | ||
expect(filter.frequency.value).to.equal(values.frequency); | ||
expect(filter.Q.value).to.equal(values.Q); | ||
expect(filter.gain.value).to.be.closeTo(values.gain, 0.04); | ||
filter.dispose(); | ||
}); | ||
|
||
it("can get the frequency response curve", () => { | ||
const filter = new BiquadFilter(); | ||
const curve = filter.getFrequencyResponse(32); | ||
expect(curve.length).to.equal(32); | ||
expect(curve[0]).be.closeTo(1, 0.01); | ||
expect(curve[5]).be.closeTo(0.5, 0.1); | ||
expect(curve[15]).be.closeTo(0, 0.01); | ||
expect(curve[31]).be.closeTo(0, 0.01); | ||
filter.dispose(); | ||
}); | ||
|
||
it("passes the incoming signal through", () => { | ||
return PassAudio(input => { | ||
const filter = new BiquadFilter().toDestination(); | ||
input.connect(filter); | ||
}); | ||
}); | ||
|
||
it("can set the basic filter types", () => { | ||
const filter = new BiquadFilter(); | ||
const types: BiquadFilterType[] = ["lowpass", "highpass", | ||
"bandpass", "lowshelf", "highshelf", "notch", "allpass", "peaking"]; | ||
for (const type of types) { | ||
filter.type = type; | ||
expect(filter.type).to.equal(type); | ||
} | ||
expect(() => { | ||
// @ts-ignore | ||
filter.type = "nontype"; | ||
}).to.throw(Error); | ||
filter.dispose(); | ||
}); | ||
|
||
it("attenuates the incoming signal", () => { | ||
return Offline(() => { | ||
const filter = new BiquadFilter(700, "lowpass").toDestination(); | ||
filter.Q.value = 0; | ||
const osc = new Oscillator(880).connect(filter); | ||
osc.start(0); | ||
}, 0.2).then((buffer) => { | ||
expect(buffer.getRmsAtTime(0.05)).to.be.within(0.37, 0.53); | ||
expect(buffer.getRmsAtTime(0.1)).to.be.within(0.37, 0.53); | ||
}); | ||
}); | ||
|
||
}); | ||
}); |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode"; | ||
import { Cents, Frequency, GainFactor, Positive } from "../../core/type/Units"; | ||
import { optionsFromArguments } from "../../core/util/Defaults"; | ||
import { Param } from "../../core/context/Param"; | ||
import { assert } from "../../core/util/Debug"; | ||
|
||
export interface BiquadFilterOptions extends ToneAudioNodeOptions { | ||
frequency: Frequency; | ||
detune: Cents; | ||
Q: number; | ||
type: BiquadFilterType; | ||
gain: GainFactor; | ||
} | ||
|
||
/** | ||
* Thin wrapper around the native Web Audio [BiquadFilterNode](https://webaudio.github.io/web-audio-api/#biquadfilternode). | ||
* BiquadFilter is similar to [[Filter]] but doesn't have the option to set the "rolloff" value. | ||
* @category Component | ||
*/ | ||
export class BiquadFilter extends ToneAudioNode<BiquadFilterOptions> { | ||
readonly name: string = "BiquadFilter"; | ||
|
||
readonly input: BiquadFilterNode; | ||
readonly output: BiquadFilterNode; | ||
|
||
/** | ||
* The frequency of the filter | ||
*/ | ||
readonly frequency: Param<"frequency">; | ||
|
||
/** | ||
* A detune value, in cents, for the frequency. | ||
*/ | ||
readonly detune: Param<"cents">; | ||
|
||
/** | ||
* The Q factor of the filter. | ||
* For lowpass and highpass filters the Q value is interpreted to be in dB. | ||
* For these filters the nominal range is [−𝑄𝑙𝑖𝑚,𝑄𝑙𝑖𝑚] where 𝑄𝑙𝑖𝑚 is the largest value for which 10𝑄/20 does not overflow. This is approximately 770.63678. | ||
* For the bandpass, notch, allpass, and peaking filters, this value is a linear value. | ||
* The value is related to the bandwidth of the filter and hence should be a positive value. The nominal range is | ||
* [0,3.4028235𝑒38], the upper limit being the most-positive-single-float. | ||
* This is not used for the lowshelf and highshelf filters. | ||
*/ | ||
readonly Q: Param<"number">; | ||
|
||
/** | ||
* The gain of the filter. Its value is in dB units. The gain is only used for lowshelf, highshelf, and peaking filters. | ||
*/ | ||
readonly gain: Param<"gain">; | ||
|
||
private readonly _filter: BiquadFilterNode; | ||
|
||
/** | ||
* @param frequency The cutoff frequency of the filter. | ||
* @param type The type of filter. | ||
*/ | ||
constructor(frequency?: Frequency, type?: BiquadFilterType); | ||
constructor(options?: Partial<BiquadFilterOptions>); | ||
constructor() { | ||
super(optionsFromArguments(BiquadFilter.getDefaults(), arguments, ["frequency", "type"])); | ||
const options = optionsFromArguments(BiquadFilter.getDefaults(), arguments, ["frequency", "type"]); | ||
|
||
this._filter = this.context.createBiquadFilter(); | ||
this.input = this.output = this._filter; | ||
|
||
this.Q = new Param({ | ||
context: this.context, | ||
units: "number", | ||
value: options.Q, | ||
param: this._filter.Q, | ||
}); | ||
|
||
this.frequency = new Param({ | ||
context: this.context, | ||
units: "frequency", | ||
value: options.frequency, | ||
param: this._filter.frequency, | ||
}); | ||
|
||
this.detune = new Param({ | ||
context: this.context, | ||
units: "cents", | ||
value: options.detune, | ||
param: this._filter.detune, | ||
}); | ||
|
||
this.gain = new Param({ | ||
context: this.context, | ||
units: "gain", | ||
value: options.gain, | ||
param: this._filter.gain, | ||
}); | ||
|
||
this.type = options.type; | ||
} | ||
|
||
static getDefaults(): BiquadFilterOptions { | ||
return Object.assign(ToneAudioNode.getDefaults(), { | ||
Q: 1, | ||
type: "lowpass" as const, | ||
frequency: 350, | ||
detune: 0, | ||
gain: 0, | ||
}); | ||
} | ||
|
||
/** | ||
* The type of this BiquadFilterNode. For a complete list of types and their attributes, see the | ||
* [Web Audio API](https://webaudio.github.io/web-audio-api/#dom-biquadfiltertype-lowpass) | ||
*/ | ||
get type(): BiquadFilterType { | ||
return this._filter.type; | ||
} | ||
set type(type) { | ||
const types: BiquadFilterType[] = ["lowpass", "highpass", "bandpass", | ||
"lowshelf", "highshelf", "notch", "allpass", "peaking"]; | ||
assert(types.indexOf(type) !== -1, `Invalid filter type: ${type}`); | ||
this._filter.type = type; | ||
} | ||
|
||
/** | ||
* Get the frequency response curve. This curve represents how the filter | ||
* responses to frequencies between 20hz-20khz. | ||
* @param len The number of values to return | ||
* @return The frequency response curve between 20-20kHz | ||
*/ | ||
getFrequencyResponse(len = 128): Float32Array { | ||
// start with all 1s | ||
const freqValues = new Float32Array(len); | ||
for (let i = 0; i < len; i++) { | ||
const norm = Math.pow(i / len, 2); | ||
const freq = norm * (20000 - 20) + 20; | ||
freqValues[i] = freq; | ||
} | ||
const magValues = new Float32Array(len); | ||
const phaseValues = new Float32Array(len); | ||
// clone the filter to remove any connections which may be changing the value | ||
const filterClone = this.context.createBiquadFilter(); | ||
filterClone.type = this.type; | ||
filterClone.Q.value = this.Q.value; | ||
filterClone.frequency.value = this.frequency.value as number; | ||
filterClone.gain.value = this.gain.value as number; | ||
filterClone.getFrequencyResponse(freqValues, magValues, phaseValues); | ||
return magValues; | ||
} | ||
|
||
dispose(): this { | ||
super.dispose(); | ||
this._filter.disconnect(); | ||
this.Q.dispose(); | ||
this.frequency.dispose(); | ||
this.gain.dispose(); | ||
this.detune.dispose(); | ||
return this; | ||
} | ||
} |
This file contains 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