-
-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generators: - ADSR - SinCos (replaces old lfo()) - PinkNoise - WhiteNoise Processors: - Foldback - Mix - WaveShaper Oscillators: - Discrete Summation (DSF, stateless)
- Loading branch information
1 parent
2854b09
commit 68a88e4
Showing
8 changed files
with
476 additions
and
0 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,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); | ||
} | ||
} |
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,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])); | ||
} | ||
} |
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,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; | ||
} | ||
} |
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,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)); | ||
} | ||
} |
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,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; | ||
} | ||
} |
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,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); | ||
} | ||
} |
Oops, something went wrong.