mirror of
https://github.com/Tonejs/Tone.js
synced 2024-11-15 08:17:07 +00:00
adding OnePoleFilter
does a single pole highpass and lowpass addresses #531
This commit is contained in:
parent
fa13871bd2
commit
52c0b7d56f
5 changed files with 246 additions and 9 deletions
90
Tone/component/filter/OnePoleFilter.test.ts
Normal file
90
Tone/component/filter/OnePoleFilter.test.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { OnePoleFilter } from "./OnePoleFilter";
|
||||
import { BasicTests } from "test/helper/Basic";
|
||||
import { PassAudio } from "test/helper/PassAudio";
|
||||
import { Oscillator } from "Tone/source/oscillator/Oscillator";
|
||||
import { expect } from "chai";
|
||||
import { CompareToFile } from "test/helper/CompareToFile";
|
||||
import { atTime, Offline } from "test/helper/Offline";
|
||||
|
||||
describe("OnePoleFilter", () => {
|
||||
|
||||
BasicTests(OnePoleFilter);
|
||||
|
||||
it("matches a file when set to lowpass", () => {
|
||||
return CompareToFile(() => {
|
||||
const filter = new OnePoleFilter(300, "lowpass").toDestination();
|
||||
const osc = new Oscillator().connect(filter);
|
||||
osc.type = "square";
|
||||
osc.start(0).stop(0.1);
|
||||
}, "onePoleLowpass.wav", 0.05);
|
||||
});
|
||||
|
||||
it("matches a file when set to highpass", () => {
|
||||
return CompareToFile(() => {
|
||||
const filter = new OnePoleFilter(700, "highpass").toDestination();
|
||||
const osc = new Oscillator().connect(filter);
|
||||
osc.type = "square";
|
||||
osc.start(0).stop(0.1);
|
||||
}, "onePoleHighpass.wav", 0.05);
|
||||
});
|
||||
|
||||
context("Filtering", () => {
|
||||
|
||||
it("can set the frequency more than once", () => {
|
||||
return Offline(() => {
|
||||
const filter = new OnePoleFilter(200);
|
||||
filter.frequency = 300;
|
||||
return atTime(0.1, () => {
|
||||
filter.frequency = 400;
|
||||
});
|
||||
}, 1);
|
||||
});
|
||||
|
||||
it("can be constructed with an object", () => {
|
||||
const filter = new OnePoleFilter({
|
||||
frequency: 400,
|
||||
type: "lowpass"
|
||||
});
|
||||
expect(filter.frequency).to.be.closeTo(400, 0.1);
|
||||
expect(filter.type).to.equal("lowpass");
|
||||
filter.dispose();
|
||||
});
|
||||
|
||||
it("can be constructed with args", () => {
|
||||
const filter = new OnePoleFilter(120, "highpass");
|
||||
expect(filter.frequency).to.be.closeTo(120, 0.1);
|
||||
expect(filter.type).to.equal("highpass");
|
||||
filter.dispose();
|
||||
});
|
||||
|
||||
it("can be get and set through object", () => {
|
||||
const filter = new OnePoleFilter();
|
||||
filter.set({
|
||||
frequency: 200,
|
||||
type: "highpass"
|
||||
});
|
||||
expect(filter.get().type).to.equal("highpass");
|
||||
expect(filter.get().frequency).to.be.closeTo(200, 0.1);
|
||||
filter.dispose();
|
||||
});
|
||||
|
||||
it("passes the incoming signal through", () => {
|
||||
return PassAudio((input) => {
|
||||
const filter = new OnePoleFilter(5000).toDestination();
|
||||
input.connect(filter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context("Response Curve", () => {
|
||||
|
||||
it("can get the response curve", () => {
|
||||
const filter = new OnePoleFilter();
|
||||
const response = filter.getFrequencyResponse(128);
|
||||
expect(response.length).to.equal(128);
|
||||
response.forEach(v => expect(v).to.be.within(0, 1));
|
||||
filter.dispose();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
145
Tone/component/filter/OnePoleFilter.ts
Normal file
145
Tone/component/filter/OnePoleFilter.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { ToneAudioNode, ToneAudioNodeOptions } from "../../core/context/ToneAudioNode";
|
||||
import { Frequency, NormalRange } from "../../core/type/Units";
|
||||
import { optionsFromArguments } from "../../core/util/Defaults";
|
||||
import { Gain } from "../../core/context/Gain";
|
||||
|
||||
export type OnePoleFilterType = "highpass" | "lowpass";
|
||||
|
||||
export interface OnePoleFilterOptions extends ToneAudioNodeOptions {
|
||||
frequency: Frequency;
|
||||
type: OnePoleFilterType;
|
||||
}
|
||||
|
||||
/**
|
||||
* A one pole filter with 6db-per-octave rolloff. Either "highpass" or "lowpass".
|
||||
* Note that changing the type or frequency may result in a discontinuity which
|
||||
* can sound like a click or pop.
|
||||
* References:
|
||||
* * http://www.earlevel.com/main/2012/12/15/a-one-pole-filter/
|
||||
* * http://www.dspguide.com/ch19/2.htm
|
||||
* * https://github.com/vitaliy-bobrov/js-rocks/blob/master/src/app/audio/effects/one-pole-filters.ts
|
||||
*/
|
||||
export class OnePoleFilter extends ToneAudioNode<OnePoleFilterOptions> {
|
||||
|
||||
readonly name: string = "OnePoleFilter";
|
||||
|
||||
/**
|
||||
* Hold the current frequency
|
||||
*/
|
||||
private _frequency: Frequency;
|
||||
|
||||
/**
|
||||
* the current one pole type
|
||||
*/
|
||||
private _type: OnePoleFilterType;
|
||||
|
||||
/**
|
||||
* the current one pole filter
|
||||
*/
|
||||
private _filter!: IIRFilterNode;
|
||||
|
||||
readonly input: Gain;
|
||||
readonly output: Gain;
|
||||
|
||||
/**
|
||||
* @param frequency The frequency
|
||||
* @param type The filter type, either "lowpass" or "highpass"
|
||||
*/
|
||||
constructor(frequency?: Frequency, type?: OnePoleFilterType);
|
||||
constructor(options?: Partial<OnePoleFilterOptions>)
|
||||
constructor() {
|
||||
|
||||
super(optionsFromArguments(OnePoleFilter.getDefaults(), arguments, ["frequency", "type"]));
|
||||
const options = optionsFromArguments(OnePoleFilter.getDefaults(), arguments, ["frequency", "type"]);
|
||||
|
||||
this._frequency = options.frequency;
|
||||
this._type = options.type;
|
||||
this.input = new Gain({ context: this.context });
|
||||
this.output = new Gain({ context: this.context });
|
||||
this._createFilter();
|
||||
}
|
||||
|
||||
static getDefaults(): OnePoleFilterOptions {
|
||||
return Object.assign(ToneAudioNode.getDefaults(), {
|
||||
frequency: 0.5,
|
||||
type: "lowpass" as OnePoleFilterType
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter and dispose the old one
|
||||
*/
|
||||
private _createFilter() {
|
||||
const oldFilter = this._filter;
|
||||
const freq = this.toFrequency(this._frequency);
|
||||
const t = 1 / (2 * Math.PI * freq);
|
||||
if (this._type === "lowpass") {
|
||||
const a0 = 1 / (t * this.context.sampleRate);
|
||||
const b1 = a0 - 1;
|
||||
this._filter = this.context.createIIRFilter([a0, 0], [1, b1]);
|
||||
} else {
|
||||
const b1 = 1 / (t * this.context.sampleRate) - 1;
|
||||
this._filter = this.context.createIIRFilter([1, -1], [1, b1]);
|
||||
}
|
||||
|
||||
this.input.chain(this._filter, this.output);
|
||||
if (oldFilter) {
|
||||
// dispose it on the next block
|
||||
this.context.setTimeout(() => {
|
||||
if (!this.disposed) {
|
||||
this.input.disconnect(oldFilter);
|
||||
oldFilter.disconnect();
|
||||
}
|
||||
}, this.blockTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The frequency value.
|
||||
*/
|
||||
get frequency(): Frequency {
|
||||
return this._frequency;
|
||||
}
|
||||
set frequency(fq) {
|
||||
this._frequency = fq;
|
||||
this._createFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* The OnePole Filter type, either "highpass" or "lowpass"
|
||||
*/
|
||||
get type(): OnePoleFilterType {
|
||||
return this._type;
|
||||
}
|
||||
set type(t) {
|
||||
this._type = t;
|
||||
this._createFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: number = 128): Float32Array {
|
||||
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._filter.getFrequencyResponse(freqValues, magValues, phaseValues);
|
||||
return magValues;
|
||||
}
|
||||
|
||||
dispose(): this {
|
||||
super.dispose();
|
||||
this.input.dispose();
|
||||
this.output.dispose();
|
||||
this._filter.disconnect();
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
export { Analyser } from "./analysis/Analyser";
|
||||
export { CrossFade } from "./channel/CrossFade";
|
||||
export { Merge } from "./channel/Merge";
|
||||
export { Volume } from "./channel/Volume";
|
||||
export { AmplitudeEnvelope } from "./envelope/AmplitudeEnvelope";
|
||||
export { Envelope } from "./envelope/Envelope";
|
||||
export { EQ3 } from "./filter/EQ3";
|
||||
export { Filter } from "./filter/Filter";
|
||||
export { Compressor } from "./dynamics/Compressor";
|
||||
export * from "./analysis/Analyser";
|
||||
export * from "./channel/CrossFade";
|
||||
export * from "./channel/Merge";
|
||||
export * from "./channel/Volume";
|
||||
export * from "./channel/Panner";
|
||||
export * from "./envelope/AmplitudeEnvelope";
|
||||
export * from "./envelope/Envelope";
|
||||
export * from "./filter/EQ3";
|
||||
export * from "./filter/Filter";
|
||||
export * from "./dynamics/Compressor";
|
||||
export * from "./filter/OnePoleFilter";
|
||||
|
|
BIN
test/audio/compare/onePoleHighpass.wav
Normal file
BIN
test/audio/compare/onePoleHighpass.wav
Normal file
Binary file not shown.
BIN
test/audio/compare/onePoleLowpass.wav
Normal file
BIN
test/audio/compare/onePoleLowpass.wav
Normal file
Binary file not shown.
Loading…
Reference in a new issue