Skip to content

Commit

Permalink
State Variable Filter initial implementation (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
danigb authored Jul 4, 2024
1 parent d35ec18 commit c65e4d4
Show file tree
Hide file tree
Showing 19 changed files with 546 additions and 5 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Synthlet
# synthlet

## 0.1.0

Initial implementation of:

- ADSR
- White Noise
- State Variable Filter
- Wavetable Oscillator
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Daniel Gómez
Copyright (c) 2023-2024 Daniel Gómez Blasco (danigb)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ npm i @synthlet/adsr

## Documentation

- [ADSR](/packages/adsr/README.md)
- [Noise](/packages/noise/README.md)
- [WavetableOscillator](/packages/wavetable-oscilllator/README.md)
- [ADSR](/packages/adsr)
- [Noise](/packages/noise)
- [StateVariableFilter](/packages/state-variable-filter)
- [WavetableOscillator](/packages/wavetable-oscilllator)
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/state-variable-filter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @synthlet/state-variable-filter

## 0.1.0

- Initial implementation
14 changes: 14 additions & 0 deletions packages/state-variable-filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# @synthlet/state-variable-filter

> State Variable Filter module for [synthlet](https://github.com/danigb/synthlet)
## Usage

If you're using synthlet, this filter is included by default and exposed by `patch.filter`:

```ts
import { Patch } from "synthlet";

const patch = new Patch();
await patch.create([patch.osc(), patch.filter({ type: "lowpass" })]);
```
35 changes: 35 additions & 0 deletions packages/state-variable-filter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@synthlet/state-variable-filter",
"version": "0.1.0",
"description": "State Variable Filter module for synthlet",
"keywords": [
"modular",
"synthesizer",
"filter",
"synthlet"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"author": "[email protected]",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"jest": "^29.7.0",
"ts-jest": "^29.1.1"
},
"jest": {
"preset": "ts-jest"
},
"scripts": {
"worklet": "esbuild src/worklet.ts --bundle --minify | sed -e 's/^/export const PROCESSOR = \\`/' -e 's/$/\\`;/' > src/processor.ts",
"lib": "tsup src/index.ts --sourcemap --dts --format esm,cjs",
"build": "npm run worklet && npm run lib"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SVFilter filters the signal 1`] = `
Float32Array [
-1,
1,
-1,
1,
-1,
1,
-1,
1,
-1,
1,
-1,
1,
-1,
1,
-1,
1,
-1,
1,
-1,
1,
]
`;

exports[`SVFilter filters the signal 2`] = `
Float32Array [
-7.058988571166992,
-96.1963882446289,
-933.3245239257812,
-7950.83740234375,
-63193.6796875,
-481030.5,
-3555095.5,
-25716442,
-183016032,
-1285889280,
-8941918208,
-61654106112,
-422081658880,
-2872091344896,
-19441708433408,
-131008733118464,
-879299460595712,
-5880895244337152,
-39209108632502270,
-260680291251650560,
]
`;

exports[`SVFilter filters the signal 3`] = `
Float32Array [
-1728730658831859700,
-11437933590730506000,
-75519349223376030000,
-497665280641740050000,
-3.2738075880989456e+21,
-2.150135330517005e+22,
-1.4100311966854957e+23,
-9.233990443912205e+23,
-6.03935217634656e+24,
-3.9452060773814655e+25,
-2.574315262255496e+26,
-1.6780219466165328e+27,
-1.0927115680685831e+28,
-7.109042947525591e+28,
-4.621019964675439e+29,
-3.001284075478557e+30,
-1.9477740115635436e+31,
-1.263136567931854e+32,
-8.18576013175832e+32,
-5.3012721012231e+33,
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ProcessorNode has parameter descriptors 1`] = `
[
{
"automationRate": "k-rate",
"defaultValue": 1000,
"maxValue": 20000,
"minValue": 16,
"name": "frequency",
},
{
"automationRate": "k-rate",
"defaultValue": 0.5,
"maxValue": 40,
"minValue": 0,
"name": "resonance",
},
]
`;
23 changes: 23 additions & 0 deletions packages/state-variable-filter/src/filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SVFilter, SVFilterType } from "./filter";

describe("SVFilter", () => {
it("filters the signal", () => {
const filter = SVFilter(20, SVFilterType.LowPass);
const input = new Float32Array(20);
for (let i = 0; i < input.length; i++) {
input[i] = i % 2 ? 1 : -1;
}
expect(input).toMatchSnapshot();
const output = new Float32Array(20);
filter.fill(input, output, {
frequency: [10],
resonance: [0.5],
});
expect(output).toMatchSnapshot();
filter.fill(input, output, {
frequency: [5],
resonance: [0.5],
});
expect(output).toMatchSnapshot();
});
});
83 changes: 83 additions & 0 deletions packages/state-variable-filter/src/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Limit the value between min and max
* @param value
* @param min
* @param max
* @returns
*/
export function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}

export enum SVFilterType {
LowPass = 1,
BandPass = 2,
HighPass = 3,
}

export type Inputs = {
frequency: number[];
resonance: number[];
};

/**
* A State Variable Filter following the Andy Simper's implementation described in http://www.cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf
*
* Various implementations:
* https://github.com/Ardour/ardour/blob/71e049202c017c0a546f39b455bdc9e4be182f06/libs/plugins/a-eq.lv2/a-eq.c
* https://github.com/SoundStacks/cmajor/blob/main/standard_library/std_library_filters.cmajor#L157
*/
export function SVFilter(sampleRate: number, type = SVFilterType.LowPass) {
// Params
let $frequency = 1000;
let $resonance = 0.5;

const period = 0.5 / sampleRate;
const pi2 = 2 * Math.PI;
const freqCoef = 2.0 * sampleRate * period;

let d = 0;
let a = 0;
let g1 = 0;
let z0 = 0;
let z1 = 0;
let high = 0;
let band = 0;
let low = 0;

function read(inputs: Inputs) {
if (
inputs.frequency[0] !== $frequency ||
inputs.resonance[0] !== $resonance
) {
$frequency = inputs.frequency[0];
$resonance = inputs.resonance[0];
const cutoffFreq = clamp($frequency, 16, sampleRate / 2);
const Q = clamp($resonance, 0.025, 40);
const invQ = 1.0 / Q;
// TODO: review - something weird here (sampleRate * period = 0.5)
a = 2.0 * sampleRate * Math.tan(pi2 * period * cutoffFreq) * period;
d = 1.0 / (1.0 + invQ * a + a * a);
g1 = a + invQ;
}
}

function fill(input: Float32Array, output: Float32Array, inputs: Inputs) {
read(inputs);
for (let i = 0; i < input.length; i++) {
const x = input[i];
high = (x - g1 * z0 - z1) * d;
band = a * high + z0;
low = a * band + z1;
z0 = a * high + band;
z1 = a * band + low;
output[i] =
type === SVFilterType.LowPass
? low
: type === SVFilterType.HighPass
? high
: band;
}
}
return { fill };
}
Loading

0 comments on commit c65e4d4

Please sign in to comment.