Skip to content

Commit

Permalink
feat: adding BiquadFilter
Browse files Browse the repository at this point in the history
Simplified Filter class without the "rolloff" param

closes #686
  • Loading branch information
tambien committed Jun 8, 2020
1 parent fdb306b commit 75617d3
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 28 deletions.
94 changes: 94 additions & 0 deletions Tone/component/filter/BiquadFilter.test.ts
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);
});
});

});
});
157 changes: 157 additions & 0 deletions Tone/component/filter/BiquadFilter.ts
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;
}
}
47 changes: 19 additions & 28 deletions Tone/component/filter/Filter.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import { Gain } from "../../core/context/Gain";
import { connectSeries, ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode";
import { Cents, Frequency, GainFactor, Positive } from "../../core/type/Units";
import { connectSeries, ToneAudioNode } from "../../core/context/ToneAudioNode";
import { Frequency } from "../../core/type/Units";
import { optionsFromArguments } from "../../core/util/Defaults";
import { readOnly, writable } from "../../core/util/Interface";
import { isNumber } from "../../core/util/TypeCheck";
import { Signal } from "../../signal/Signal";
import { assert } from "../../core/util/Debug";
import { BiquadFilter, BiquadFilterOptions } from "./BiquadFilter";

export type FilterRollOff = -12 | -24 | -48 | -96;

export interface FilterOptions extends ToneAudioNodeOptions {
type: BiquadFilterType;
frequency: Frequency;
export type FilterOptions = BiquadFilterOptions & {
rolloff: FilterRollOff;
Q: Positive;
detune: Cents;
gain: GainFactor;
}

/**
Expand All @@ -35,7 +31,7 @@ export class Filter extends ToneAudioNode<FilterOptions> {

readonly input = new Gain({ context: this.context });
readonly output = new Gain({ context: this.context });
private _filters: BiquadFilterNode[] = [];
private _filters: BiquadFilter[] = [];

/**
* the rolloff value of the filter
Expand Down Expand Up @@ -149,7 +145,9 @@ export class Filter extends ToneAudioNode<FilterOptions> {

this._filters = new Array(cascadingCount);
for (let count = 0; count < cascadingCount; count++) {
const filter = this.context.createBiquadFilter();
const filter = new BiquadFilter({
context: this.context,
});
filter.type = this._type;
this.frequency.connect(filter.frequency);
this.detune.connect(filter.detune);
Expand All @@ -168,27 +166,20 @@ export class Filter extends ToneAudioNode<FilterOptions> {
* @return The frequency response curve between 20-20kHz
*/
getFrequencyResponse(len = 128): Float32Array {
const filterClone = new BiquadFilter({
frequency: this.frequency.value,
gain: this.gain.value,
Q: this.Q.value,
type: this._type,
detune: this.detune.value,
});
// start with all 1s
const totalResponse = new Float32Array(len).map(() => 1);
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);
this._filters.forEach(() => {
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);
magValues.forEach((val, i) => {
totalResponse[i] *= val;
});
const response = filterClone.getFrequencyResponse(len);
response.forEach((val, i) => totalResponse[i] *= val);
});
filterClone.dispose();
return totalResponse;
}

Expand All @@ -198,7 +189,7 @@ export class Filter extends ToneAudioNode<FilterOptions> {
dispose(): this {
super.dispose();
this._filters.forEach(filter => {
filter.disconnect();
filter.dispose();
});
writable(this, ["detune", "frequency", "gain", "Q"]);
this.frequency.dispose();
Expand Down

0 comments on commit 75617d3

Please sign in to comment.