Skip to content

Commit

Permalink
feat(dsp): add new operators
Browse files Browse the repository at this point in the history
Generators:

  - ADSR
  - SinCos (replaces old lfo())
  - PinkNoise
  - WhiteNoise

Processors:

  - Foldback
  - Mix
  - WaveShaper

Oscillators:

  - Discrete Summation (DSF, stateless)
  • Loading branch information
postspectacular committed Jan 21, 2020
1 parent 2854b09 commit 68a88e4
Show file tree
Hide file tree
Showing 8 changed files with 476 additions and 0 deletions.
158 changes: 158 additions & 0 deletions packages/dsp/src/gen/adsr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { IReset } from "../api";
import { AGen } from "./agen";
import { curve, MAdd } from "./madd";

const enum EnvPhase {
ATTACK,
DECAY,
SUSTAIN,
RELEASE,
IDLE
}

/**
* Time based ADSR envelope gen with customizable exponential attack,
* decay and release curves.
*
* @remarks
* The `attack`, `decay` and `release` args are to be given in samples
* (`num = time_in_seconds * sample_rate`). The release phase MUST be
* triggered manually by calling {@link ADSR.release}. If only attack &
* decay phases are required, initialize `sustain` to zero and configure
* `dcurve` to adjust falloff.
*
* The envelope can be re-used & restarted by calling
* {@link ADSR.reset}. This will move the internal state back to the
* attack phase and start producing a new envelope with current
* settings. Note: Any changes done to the envelope parameters are only
* guaranteed to be applied after reset.
*
* The `acurve` and `dcurve` args can be used to control the exponential
* curvature of the attack, decay and release phases. Recommended range
* [0.0001 - 100] (curved -> linear).
*
* @param attack - attack steps (default: 0)
* @param decay - decay steps (default: 0)
* @param sustain - sustain level (default: 1)
* @param release - release steps (default: 0)
* @param acurve - attack curvature (default: 0.1)
* @param dcurve - decay / release curvature (default: 0.001)
*/
export const adsr = (
attack?: number,
decay?: number,
sustain?: number,
release?: number,
acurve?: number,
dcurve?: number
) => new ADSR(attack, decay, sustain, release, acurve, dcurve);

export class ADSR extends AGen<number> implements IReset {
protected _phase!: EnvPhase;
protected _curve!: MAdd;
protected _atime!: number;
protected _dtime!: number;
protected _rtime!: number;
protected _acurve!: number;
protected _dcurve!: number;
protected _sustain!: number;

constructor(
attack = 0,
decay = 0,
sustain = 1,
release = 0,
acurve = 0.1,
dcurve = 0.001
) {
super(0);
this.setAttack(attack);
this.setDecay(decay);
this.setRelease(release);
this.setSustain(sustain);
this.setCurveA(acurve);
this.setCurveD(dcurve);
this.reset();
}

reset() {
this._phase = EnvPhase.ATTACK;
this._curve = curve(0, 1, this._atime, this._acurve);
this._val = 0;
}

release() {
if (this._phase < EnvPhase.RELEASE) {
this._phase = EnvPhase.RELEASE;
this._curve = curve(this._sustain, 0, this._rtime, this._dcurve);
}
}

isSustained() {
return this._phase === EnvPhase.SUSTAIN;
}

isDone() {
return this._phase === EnvPhase.IDLE;
}

next() {
let v = this._val;
switch (this._phase) {
case EnvPhase.IDLE:
case EnvPhase.SUSTAIN:
return v;
case EnvPhase.ATTACK:
v = this._curve.next();
if (v >= 1) {
v = 1;
this._phase = EnvPhase.DECAY;
this._curve = curve(
1,
this._sustain,
this._dtime,
this._dcurve
);
}
break;
case EnvPhase.DECAY:
v = this._curve.next();
if (v <= this._sustain) {
v = this._sustain;
this._phase = EnvPhase.SUSTAIN;
}
break;
case EnvPhase.RELEASE:
v = this._curve.next();
if (v < 0) {
v = 0;
this._phase = EnvPhase.IDLE;
}
}
return (this._val = v);
}

setAttack(rate: number) {
this._atime = Math.max(rate, 0);
}

setDecay(rate: number) {
this._dtime = Math.max(rate, 0);
}

setRelease(rate: number) {
this._rtime = Math.max(rate, 0);
}

setSustain(level: number) {
this._sustain = level;
}

setCurveA(ratio: number) {
this._acurve = Math.max(ratio, 1e-6);
}

setCurveD(ratio: number) {
this._dcurve = Math.max(ratio, 1e-6);
}
}
75 changes: 75 additions & 0 deletions packages/dsp/src/gen/pink-noise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Tuple } from "@thi.ng/api";
import { IRandom, SYSTEM } from "@thi.ng/random";
import { IReset } from "../api";
import { AGen } from "./agen";

type PNoiseCoeffs = Tuple<number, 5>;
const AMP = <PNoiseCoeffs>[3.8024, 2.9694, 2.597, 3.087, 3.4006];
const PROB = <PNoiseCoeffs>[0.00198, 0.0128, 0.049, 0.17, 0.682];

/**
* Pink noise generator with customizable frequency distribution. The
* default config produces a power spectrum roughly following the `1/f`
* pink characteristic.
*
* @remarks
* Custom frequency/power distributions can be obtained by providing
* `amp` and `prob`ability tuples for the 5 internal bins used to
* compute the noise. `amp` defines per-bin power contributions, the
* latter bin update probabilities.
*
* Resulting noise values are normalized to given `gain`, which itself
* is scale relative to the sum of given `amp` values.
*
* References:
* - http://web.archive.org/web/20160513114217/http://home.earthlink.net/~ltrammell/tech/newpink.htm
* - http://web.archive.org/web/20160515145318if_/http://home.earthlink.net/~ltrammell/tech/pinkalg.htm
* - https://www.musicdsp.org/en/latest/Synthesis/220-trammell-pink-noise-c-class.html
*
*/
export const pinkNoise = (
gain: number,
rnd: IRandom,
amp: PNoiseCoeffs,
prob: PNoiseCoeffs
) => new PinkNoise(gain, rnd, amp, prob);

export class PinkNoise extends AGen<number> implements IReset {
protected _bins: number[];
protected _psum: number[];

constructor(
protected _gain = 1,
protected _rnd: IRandom = SYSTEM,
protected _amp: PNoiseCoeffs = AMP,
prob: PNoiseCoeffs = PROB
) {
super(0);
this._gain /= _amp.reduce((acc, x) => acc + x, 0);
this._psum = prob.reduce(
(acc: number[], x, i) => (
acc.push(i > 0 ? acc[i - 1] + x : x), acc
),
[]
);
this._bins = [0, 0, 0, 0, 0];
}

reset() {
this._bins.fill(0);
}

next() {
const { _bins, _rnd, _amp, _psum } = this;
const bin = _rnd.float();
for (let i = 0; i < 5; i++) {
if (bin <= _psum[i]) {
_bins[i] = _rnd.norm(_amp[i]);
break;
}
}
return (this._val =
this._gain *
(_bins[0] + _bins[1] + _bins[2] + _bins[3] + _bins[4]));
}
}
61 changes: 61 additions & 0 deletions packages/dsp/src/gen/sincos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { TAU } from "@thi.ng/math";
import { AGen } from "./agen";

/**
* Generator of sine & cosine values of given frequency in the form of
* [sin,cos] tuples. Start phase always zero.
*
* @remarks
* Implementation based on a self-oscillating SVF (state-variable
* filter) without using any trig functions. Therefore, ~30% faster, but
* precision only useful for very low (< ~2Hz) frequencies. Due to
* floating point error accumulation, phase & amplitude drift will occur
* for higher frequencies.
*
* References:
* - http://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/
*
* @param freq - normalized freq
* @param amp - amplitude (default: 1)
*/
export class SinCos extends AGen<number[]> {
protected _f!: number;
protected _s!: number;
protected _c!: number;

constructor(protected _freq: number, protected _amp = 1) {
super([0, _amp]);
this.calcCoeffs();
}

next() {
this._val = [this._s, this._c];
this._s += this._f * this._c;
this._c -= this._f * this._s;
return this._val;
}

freq() {
return this._freq;
}

setFreq(freq: number) {
this._freq = freq;
this.calcCoeffs();
}

amp() {
return this._amp;
}

setAmp(amp: number) {
this._amp = amp;
this.calcCoeffs();
}

protected calcCoeffs() {
this._f = TAU * this._freq;
this._s = 0;
this._c = this._amp;
}
}
12 changes: 12 additions & 0 deletions packages/dsp/src/gen/white-noise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IRandom, SYSTEM } from "@thi.ng/random";
import { AGen } from "./agen";

export class WhiteNoise extends AGen<number> {
constructor(protected _gain = 1, protected _rnd: IRandom = SYSTEM) {
super(0);
}

next() {
return (this._val = this._rnd.norm(this._gain));
}
}
38 changes: 38 additions & 0 deletions packages/dsp/src/proc/foldback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { foldback as _foldback } from "@thi.ng/math";
import { AProc } from "./aproc";

/**
* Recursively folds input into `[-thresh .. +thresh]` interval and
* amplifies it with `amp` (default: 1/thresh).
*
* @param thresh - fold threshold
* @param amp - post amplifier
*/
export const foldback = (thresh?: number, amp?: number) =>
new Foldback(thresh, amp);

export class Foldback extends AProc<number, number> {
constructor(protected _thresh = 1, protected _amp = 1 / _thresh) {
super(0);
}

next(x: number) {
return (this._val = _foldback(this._thresh, x) * this._amp);
}

threshold() {
return this._thresh;
}

setThreshold(t: number) {
this._thresh = t;
}

amp() {
return this._amp;
}

setAmp(a: number) {
this._amp = a;
}
}
20 changes: 20 additions & 0 deletions packages/dsp/src/proc/mix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { clamp01 } from "@thi.ng/math";
import { AProc2 } from "./aproc";

export class Mix extends AProc2<number, number, number> {
constructor(protected _t: number) {
super(0);
}

get mix() {
return this._t;
}

set mix(x: number) {
this._t = clamp01(x);
}

next(a: number, b: number) {
return (this._val = a + (b - a) * this._t);
}
}
Loading

0 comments on commit 68a88e4

Please sign in to comment.