dough.mjsannotateddough.mjssource1119 lines · 31.1 KB · raw

this is dough, the superdough without dependencies @ts-nocheck @ts-ignore ignore next line because sampleRate is unknown

4const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
5const PI_DIV_SR = Math.PI / SAMPLE_RATE;
6const ISR = 1 / SAMPLE_RATE;
8let gainCurveFunc = (val) => Math.pow(val, 2);
9const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
10
11function applyGainCurve(val) {
12  return gainCurveFunc(val);
13}

Equal Power Crossfade function. Smoothly transitions between signals A and B, maintaining consistent perceived loudness.

@param {number} a - Signal A (can be a single value or an array value in buffer processing). @param {number} b - Signal B (can be a single value or an array value in buffer processing). @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix). @tags internals @returns {number} Crossfaded output value.

25function crossfade(a, b, m) {
26  const aGain = Math.sin((1 - m) * 0.5 * Math.PI);
27  const bGain = Math.sin(m * 0.5 * Math.PI);
28  return a * aGain + b * bGain;
29}

function setGainCurve(newGainCurveFunc) { gainCurveFunc = newGainCurveFunc; } https://garten.salat.dev/audio-DSP/oscillators.html

35export class SineOsc {
36  phase = 0;
37  update(freq) {
38    const value = Math.sin(this.phase * 2 * Math.PI);
39    this.phase = (this.phase + freq / SAMPLE_RATE) % 1;
40    return value;
41  }
42}
44export class ZawOsc {
45  phase = 0;
46  update(freq) {
47    this.phase += ISR * freq;
48    return (this.phase % 1) * 2 - 1;
49  }
50}
51
52function polyBlep(t, dt) {
53  // 0 <= t < 1
54  if (t < dt) {
55    t /= dt;
56    // 2 * (t - t^2/2 - 0.5)
57    return t + t - t * t - 1;
58  }
59  // -1 < t < 0
60  if (t > 1 - dt) {
61    t = (t - 1) / dt;
62    // 2 * (t^2/2 + t + 0.5)
63    return t * t + t + t + 1;
64  }
65  // 0 otherwise
66  return 0;
67}
68
69export class SawOsc {
70  constructor(props = {}) {
71    this.phase = props.phase ?? 0;
72  }
73  update(freq) {
74    const dt = freq / SAMPLE_RATE;
75    let p = polyBlep(this.phase, dt);
76    let s = 2 * this.phase - 1 - p;
77    this.phase += dt;
78    if (this.phase > 1) {
79      this.phase -= 1;
80    }
81    return s;
82  }
83}
84
85function getUnisonDetune(unison, detune, voiceIndex) {
86  if (unison < 2) {
87    return 0;
88  }
89  const lerp = (a, b, n) => {
90    return n * (b - a) + a;
91  };
92  return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
93}
94function applySemitoneDetuneToFrequency(frequency, detune) {
95  return frequency * Math.pow(2, detune / 12);
96}
97export class SupersawOsc {
98  constructor(props = {}) {
99    //TODO: figure out a good way to pass in these params
100    this.voices = props.voices ?? 5;
101    this.freqspread = props.freqspread ?? 0.2;
102    this.panspread = props.panspread ?? 0.4;
103    this.phase = new Float32Array(this.voices).map(() => Math.random());
104  }
105  update(freq) {
106    const gain1 = Math.sqrt(1 - this.panspread);
107    const gain2 = Math.sqrt(this.panspread);
108    let sl = 0;
109    let sr = 0;
110    for (let n = 0; n < this.voices; n++) {
111      const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n));
112      const dt = freqAdjusted / SAMPLE_RATE;
113      const isOdd = (n & 1) == 1;
114      let gainL = gain1;
115      let gainR = gain2;
116      // invert right and left gain
117      if (isOdd) {
118        gainL = gain2;
119        gainR = gain1;
120      }
121      let p = polyBlep(this.phase[n], dt);
122      let s = 2 * this.phase[n] - 1 - p;
123      sl = sl + s * gainL;
124      sr = sr + s * gainL;
125
126      this.phase[n] += dt;
127      if (this.phase[n] > 1) {
128        this.phase[n] -= 1;
129      }
130    }
131
132    return sl + sr;
133    //TODO: make stereo
134    // return [sl, sr];
135  }
136}
137
138export class TriOsc {
139  phase = 0;
140  update(freq) {
141    this.phase += ISR * freq;
142    let phase = this.phase % 1;
143    let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5);
144    return value * 2 - 1;
145  }
146}
147
148export class TwoPoleFilter {
149  s0 = 0;
150  s1 = 0;
151  update(s, cutoff, resonance = 0) {
152    // Out of bound values can produce NaNs
153    resonance = Math.max(resonance, 0);
154
155    cutoff = Math.min(cutoff, 20000);
156    let c = 2 * Math.sin(cutoff * PI_DIV_SR);
157    c = clamp(c, 0, 1.14); // this line prevents instability TODO: test
158
159    const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
160    const mrc = 1 - r * c;
161
162    this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
163    this.s1 = mrc * this.s1 + c * this.s0; // lpf
164    return this.s1; // return lpf by default
165  }
166}
167
168class PulseOsc {
169  constructor(phase = 0) {
170    this.phase = phase;
171  }
172  saw(offset, dt) {
173    let phase = (this.phase + offset) % 1;
174    let p = polyBlep(phase, dt);
175    return 2 * phase - 1 - p;
176  }
177  update(freq, pw = 0.5) {
178    const dt = freq / SAMPLE_RATE;
179    let pulse = this.saw(0, dt) - this.saw(pw, dt);
180    this.phase = (this.phase + dt) % 1;
181    return pulse + pw * 2 - 1;
182  }
183}

non bandlimited (has aliasing)

186export class PulzeOsc {
187  phase = 0;
188  update(freq, duty = 0.5) {
189    this.phase += ISR * freq;
190    let cyclePos = this.phase % 1;
191    return cyclePos < duty ? 1 : -1;
192  }
193}
195export class Dust {
196  update = (density) => (Math.random() < density * ISR ? Math.random() : 0);
197}
198
199export class WhiteNoise {
200  update() {
201    return Math.random() * 2 - 1;
202  }
203}
204
205export class BrownNoise {
206  constructor() {
207    this.out = 0;
208  }
209  update() {
210    let white = Math.random() * 2 - 1;
211    this.out = (this.out + 0.02 * white) / 1.02;
212    return this.out;
213  }
214}
215
216export class PinkNoise {
217  constructor() {
218    this.b0 = 0;
219    this.b1 = 0;
220    this.b2 = 0;
221    this.b3 = 0;
222    this.b4 = 0;
223    this.b5 = 0;
224    this.b6 = 0;
225  }
226
227  update() {
228    const white = Math.random() * 2 - 1;
229
230    this.b0 = 0.99886 * this.b0 + white * 0.0555179;
231    this.b1 = 0.99332 * this.b1 + white * 0.0750759;
232    this.b2 = 0.969 * this.b2 + white * 0.153852;
233    this.b3 = 0.8665 * this.b3 + white * 0.3104856;
234    this.b4 = 0.55 * this.b4 + white * 0.5329522;
235    this.b5 = -0.7616 * this.b5 - white * 0.016898;
236
237    const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362;
238    this.b6 = white * 0.115926;
239
240    return pink * 0.11;
241  }
242}
243
244export class Impulse {
245  phase = 1;
246  update(freq) {
247    this.phase += ISR * freq;
248    let v = this.phase >= 1 ? 1 : 0;
249    this.phase = this.phase % 1;
250    return v;
251  }
252}
253
254export class ClockDiv {
255  inSgn = true;
256  outSgn = true;
257  clockCnt = 0;
258  update(clock, factor) {
259    let curSgn = clock > 0;
260    if (this.inSgn != curSgn) {
261      this.clockCnt++;
262      if (this.clockCnt >= factor) {
263        this.clockCnt = 0;
264        this.outSgn = !this.outSgn;
265      }
266    }
267
268    this.inSgn = curSgn;
269    return this.outSgn ? 1 : -1;
270  }
271}
272
273export class Hold {
274  value = 0;
275  trigSgn = false;
276  update(input, trig) {
277    if (!this.trigSgn && trig > 0) this.value = input;
278    this.trigSgn = trig > 0;
279    return this.value;
280  }
281}
282
283function lerp(x, y0, y1, exponent = 1) {
284  if (x <= 0) return y0;
285  if (x >= 1) return y1;
286
287  let curvedX;
288
289  if (exponent === 0) {
290    curvedX = x; // linear
291  } else if (exponent > 0) {
292    curvedX = Math.pow(x, exponent); // ease-in
293  } else {
294    curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out
295  }
296
297  return y0 + (y1 - y0) * curvedX;
298}
299
300export class ADSR {
301  constructor(props = {}) {
302    this.state = 'off';
303    this.startTime = 0;
304    this.startVal = 0;
305    this.decayCurve = props.decayCurve ?? 1;
306  }
307
308  update(curTime, gate, attack, decay, susVal, release) {
309    switch (this.state) {
310      case 'off': {
311        if (gate > 0) {
312          this.state = 'attack';
313          this.startTime = curTime;
314          this.startVal = 0;
315        }
316        return 0;
317      }
318      case 'attack': {
319        let time = curTime - this.startTime;
320        if (time > attack) {
321          this.state = 'decay';
322          this.startTime = curTime;
323          return 1;
324        }
325        return lerp(time / attack, this.startVal, 1, 1);
326      }
327      case 'decay': {
328        let time = curTime - this.startTime;
329        let curVal = lerp(time / decay, 1, susVal, -this.decayCurve);
330        if (gate <= 0) {
331          this.state = 'release';
332          this.startTime = curTime;
333          this.startVal = curVal;
334          return curVal;
335        }
336        if (time > decay) {
337          this.state = 'sustain';
338          this.startTime = curTime;
339          return susVal;
340        }
341        return curVal;
342      }
343      case 'sustain': {
344        if (gate <= 0) {
345          this.state = 'release';
346          this.startTime = curTime;
347          this.startVal = susVal;
348        }
349        return susVal;
350      }
351      case 'release': {
352        let time = curTime - this.startTime;
353
354        if (time > release) {
355          this.state = 'off';
356          return 0;
357        }
358        let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve);
359        if (gate > 0) {
360          this.state = 'attack';
361          this.startTime = curTime;
362          this.startVal = curVal;
363        }
364        return curVal;
365      }
366    }
367    throw 'invalid envelope state';
368  }
369}

impulse(1).ad(.1).mul(sine(200)) .add(x=>x.delay(.1).mul(.8)) .out()

375const MAX_DELAY_TIME = 10;
376export class PitchDelay {
377  lpf = new TwoPoleFilter();
378  constructor(_props = {}) {
379    this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE);
380    this.writeIdx = 0;
381    this.readIdx = 0;
382    this.numSamples = 0;
383  }
384  write(s, delayTime) {
385    // Calculate how far in the past to read
386    this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
387    this.writeIdx = (this.writeIdx + 1) % this.numSamples;
388    this.buffer[this.writeIdx] = s;
389    this.readIdx = this.writeIdx - this.numSamples + 1;

If past the start of the buffer, wrap around (Q: is this possible?)

392    if (this.readIdx < 0) this.readIdx += this.numSamples;
393  }
394  update(input, delayTime, speed = 1) {
395    this.write(input, delayTime);
396    let index = this.readIdx;
397    if (speed < 0) {
398      index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples);
399    } else {
400      index = Math.floor(this.readIdx * speed) % this.numSamples;
401    }
402    const s = this.lpf.update(this.buffer[index], 0.9, 0);
404    return s;
405  }
406}
407
408export class Delay {
409  writeIdx = 0;
410  readIdx = 0;
411  buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0)
412  write(s, delayTime) {
413    this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
414    this.buffer[this.writeIdx] = s;
415    // Calculate how far in the past to read
416    let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
417    this.readIdx = this.writeIdx - numSamples;
418    // If past the start of the buffer, wrap around
419    if (this.readIdx < 0) this.readIdx += this.buffer.length;
420  }
421  update(input, delayTime) {
422    this.write(input, delayTime);
423    return this.buffer[this.readIdx];
424  }
425}

TODO: Figure out why clicking at the start off the buffer

427export class Chorus {
428  delay = new Delay();
429  modulator = new TriOsc();
430  update(input, mix, delayTime, modulationFreq, modulationDepth) {
431    const m = this.modulator.update(modulationFreq) * modulationDepth;
432    const c = this.delay.update(input, delayTime * (1 + m));
433    return crossfade(input, c, mix);
434  }
435}
437export class Fold {
438  update(input = 0, rate = 0) {
439    if (rate < 0) rate = 0;
440    rate = rate + 1;
441    input = input * rate;
442    return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25);
443  }
444}
445
446export class Lag {
447  lagUnit = 4410;
448  s = 0;
449  update(input, rate) {
450    // Remap so the useful range is around [0, 1]
451    rate = rate * this.lagUnit;
452    if (rate < 1) rate = 1;
453    this.s += (1 / rate) * (input - this.s);
454    return this.s;
455  }
456}
457
458export class Slew {
459  last = 0;
460  update(input, up, dn) {
461    const upStep = up * ISR;
462    const downStep = dn * ISR;
463    let delta = input - this.last;
464    if (delta > upStep) {
465      delta = upStep;
466    } else if (delta < -downStep) {
467      delta = -downStep;
468    }
469    this.last += delta;
470    return this.last;
471  }
472}

overdrive style distortion (adapted from noisecraft) currently unused

475export function applyDistortion(x, amount) {
476  amount = Math.min(Math.max(amount, 0), 1);
477  amount -= 0.01;
478  var k = (2 * amount) / (1 - amount);
479  var y = ((1 + k) * x) / (1 + k * Math.abs(x));
480  return y;
481}
483export class Sequence {
484  clockSgn = true;
485  step = 0;
486  first = true;
487  update(clock, ...ins) {
488    if (!this.clockSgn && clock > 0) {
489      this.step = (this.step + 1) % ins.length;
490      this.clockSgn = clock > 0;
491      return 0; // set first sample to zero to retrigger gates on step change...
492    }
493    this.clockSgn = clock > 0;
494    return ins[this.step];
495  }
496}

sample rate bit crusher

499export class Coarse {
500  hold = 0;
501  t = 0;
502  update(input, coarse) {
503    if (this.t++ % coarse === 0) {
504      this.t = 0;
505      this.hold = input;
506    }
507    return this.hold;
508  }
509}

amplitude bit crusher

512export class Crush {
513  update(input, crush) {
514    crush = Math.max(1, crush);
515    const x = Math.pow(2, crush - 1);
516    return Math.round(input * x) / x;
517  }
518}

this is the distort from superdough

521export class Distort {
522  update(input, distort = 0, postgain = 1) {
523    postgain = Math.max(0.001, Math.min(1, postgain));
524    const shape = Math.expm1(distort);
525    return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain;
526  }
527}

distortion could be expressed as a function, because it's stateless

530export class BufferPlayer {
531  static samples = new Map(); // string -> { channels, sampleRate }
532  buffer; // Float32Array
533  sampleRate;
534  pos = 0;
535  sampleFreq = note2freq();
536  constructor(buffer, sampleRate, normalize) {
537    this.buffer = buffer;
538    this.sampleRate = sampleRate;
539    this.duration = this.buffer.length / this.sampleRate;
540    this.speed = SAMPLE_RATE / this.sampleRate;
541    if (normalize) {
542      // this will make the buffer last 1s if freq = sampleFreq
543      // it's useful to loop samples (e.g. fit function)
544      this.speed *= this.duration;
545    }
546  }
547  update(freq) {
548    if (this.pos >= this.buffer.length) {
549      return 0;
550    }
551    const speed = (freq / this.sampleFreq) * this.speed;
552    let s = this.buffer[Math.floor(this.pos)];
553    this.pos = this.pos + speed;
554    return s;
555  }
556}
558export function _rangex(sig, min, max) {
559  let logmin = Math.log(min);
560  let range = Math.log(max) - logmin;
561  const unipolar = (sig + 1) / 2;
562  return Math.exp(unipolar * range + logmin);
563}

duplicate

566export const getADSR = (params, curve = 'linear', defaultValues) => {
567  const envmin = curve === 'exponential' ? 0.001 : 0.001;
568  const releaseMin = 0.01;
569  const envmax = 1;
570  const [a, d, s, r] = params;
571  if (a == null && d == null && s == null && r == null) {
572    return defaultValues ?? [envmin, envmin, envmax, releaseMin];
573  }
574  const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
575  return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
576};
578let shapes = {
579  sine: SineOsc,
580  saw: SawOsc,
581  zaw: ZawOsc,
582  sawtooth: SawOsc,
583  zawtooth: ZawOsc,
584  supersaw: SupersawOsc,
585  tri: TriOsc,
586  triangle: TriOsc,
587  pulse: PulseOsc,
588  square: PulseOsc,
589  pulze: PulzeOsc,
590  dust: Dust,
591  crackle: Dust,
592  impulse: Impulse,
593  white: WhiteNoise,
594  brown: BrownNoise,
595  pink: PinkNoise,
596};
597
598const defaultDefaultValues = {
599  chorus: 0,
600  note: 48,
601  s: 'triangle',
602  bank: '',
603  gain: 1,
604  postgain: 1,
605  velocity: 1,
606  density: '.03',
607  ftype: '12db',
608  fanchor: 0,
609  //resonance: 1, // superdough resonance is scaled differently
610  resonance: 0,
611  //hresonance: 1, // superdough resonance is scaled differently
612  hresonance: 0,
613  // bandq: 1,  // superdough resonance is scaled differently
614  bandq: 0,
615  channels: [1, 2],
616  phaserdepth: 0.75,
617  shapevol: 1,
618  distortvol: 1,
619  delay: 0,
620  byteBeatExpression: '0',
621  delayfeedback: 0.5,
622  delayspeed: 1,
623  delaytime: 0.25,
624  orbit: 1,
625  i: 1,
626  fft: 8,
627  z: 'triangle',
628  pan: 0.5,
629  fmh: 1,
630  fmenv: 0, // differs from superdough
631  speed: 1,
632  pw: 0.5,
633};
634
635let getDefaultValue = (key) => defaultDefaultValues[key];
636
637const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
638const accs = { '#': 1, b: -1, s: 1, f: -1 };
639const note2midi = (note, defaultOctave = 3) => {
640  let [pc, acc = '', oct = ''] =
641    String(note)
642      .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)
643      ?.slice(1) || [];
644  if (!pc) {
645    throw new Error('not a note: "' + note + '"');
646  }
647  const chroma = chromas[pc.toLowerCase()];
648  const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
649  const octave = Number(oct || defaultOctave);
650  return (octave + 1) * 12 + chroma + offset;
651};
652const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
653const note2freq = (note) => {
654  note = note || getDefaultValue('note');
655  if (typeof note === 'string') {
656    note = note2midi(note, 3); // e.g. c3 => 48
657  }
658  return midi2freq(note);
659};
660
661export class DoughVoice {
662  /** @type {number} */
663  id = 0;
664  /** @type {number[]} */
665  out = [0, 0];

@type {number | undefined}

668  attack;
669  /** @type {number | undefined} */
670  decay;
671  /** @type {number | undefined} */
672  sustain;
673  /** @type {number} */
674  release;
675  /** @type {number} */
676  _begin;
677  /** @type {number} */
678  _duration;

@type {any}

681  _sound;
682  /** @type {number} */
683  _channels = 1;
684  /** @type {BufferPlayer[] | undefined} */
685  _buffers;
686  /** @type {string | undefined} */
687  unit;

@type {ADSR | undefined}

690  _penv;
691  /** @type {number | undefined} */
692  penv;
693  /** @type {number | undefined} */
694  pattack;
695  /** @type {number | undefined} */
696  pdecay;
697  /** @type {number | undefined} */
698  psustain;
699  /** @type {number | undefined} */
700  prelease;

@type {number | undefined}

703  vib;
705  _vib;
706  /** @type {number | undefined} */
707  vibmod;

@type {SineOsc | undefined}

710  _fm;
711  /** @type {number | undefined} */
712  fmh;
713  /** @type {number | undefined} */
714  fmi;

@type {ADSR | undefined}

717  _fmenv;
718  /** @type {number | undefined} */
719  fmattack;
720  /** @type {number | undefined} */
721  fmdecay;
722  /** @type {number | undefined} */
723  fmsustain;
724  /** @type {number | undefined} */
725  fmrelease;

@type {ADSR | undefined}

728  _lpenv;
729  lpenv;
730  /** @type {number | undefined} */
731  lpattack;
732  /** @type {number | undefined} */
733  lpdecay;
734  /** @type {number | undefined} */
735  lpsustain;
736  /** @type {number | undefined} */
737  lprelease;

@type {ADSR | undefined}

740  _hpenv;
741  /** @type {number | undefined} */
742  hpenv;
743  /** @type {number | undefined} */
744  hpattack;
745  /** @type {number | undefined} */
746  hpdecay;
747  /** @type {number | undefined} */
748  hpsustain;
749  /** @type {number | undefined} */
750  hprelease;

@type {ADSR | undefined}

753  _bpenv;
754  /** @type {number | undefined} */
755  bpenv;
756  /** @type {number | undefined} */
757  bpattack;
758  /** @type {number | undefined} */
759  bpdecay;
760  /** @type {number | undefined} */
761  bpsustain;
762  /** @type {number | undefined} */
763  bprelease;

@type {number | undefined}

766  cutoff;
767  /** @type {number | undefined} */
768  hcutoff;
769  /** @type {number | undefined} */
770  bandf;
771  /** @type {number | undefined} */
772  coarse;
773  /** @type {number | undefined} */
774  crush;
775  /** @type {number | undefined} */
776  distort;

@type {number}

779  freq;
780  /** @type {string | undefined} */
781  note;

@type {TwoPoleFilter[] | null | undefined}

784  _lpf;
785  /** @type {TwoPoleFilter[] | null | undefined} */
786  _hpf;
787  /** @type {TwoPoleFilter[] | null | undefined} */
788  _bpf;
789  /** @type {Chorus[] | null | undefined} */
790  _chorus;
791  /** @type {Coarse[] | null | undefined} */
792  _coarse;
793  /** @type {Crush[] | null | undefined} */
794  _crush;
795  /** @type {Distort[] | null | undefined} */
796  _distort;

@param {DoughVoice} value

801  constructor(value) {
802    // mandatory controls
803    this.freq ??= note2freq(value.note);
804    this._begin = value._begin;
805    this._duration = value._duration;
806    this.release = value.release ?? 0;
807    // the rest.. we use $ for readability
808    let $ = this;
809    Object.assign($, value);
810    $.s = $.s ?? getDefaultValue('s');
811    $.gain = applyGainCurve($.gain ?? getDefaultValue('gain'));
812    $.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity'));
813    $.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain'));
814    $.density = $.density ?? getDefaultValue('density');
815    $.fanchor = $.fanchor ?? getDefaultValue('fanchor');
816    $.drive = $.drive ?? 0.69;
817    $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth');
818    $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol'));
819    $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol'));
820    $.i = $.i ?? getDefaultValue('i');
821    $.chorus = $.chorus ?? getDefaultValue('chorus');
822    $.fft = $.fft ?? getDefaultValue('fft');
823    $.pan = $.pan ?? getDefaultValue('pan');
824    $.orbit = $.orbit ?? getDefaultValue('orbit');
825    $.fmenv = $.fmenv ?? getDefaultValue('fmenv');
826    $.resonance = $.resonance ?? getDefaultValue('resonance');
827    $.hresonance = $.hresonance ?? getDefaultValue('hresonance');
828    $.bandq = $.bandq ?? getDefaultValue('bandq');
829    $.speed = $.speed ?? getDefaultValue('speed');
830    $.pw = $.pw ?? getDefaultValue('pw');
831
832    [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]);
833
834    $._holdEnd = $._begin + $._duration; // needed for gate
835    $._end = $._holdEnd + $.release + 0.01; // needed for despawn
836
837    if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) {
838      $.s = 'zaw'; // polyblepped saw when fm is applied
839    }
840
841    if (shapes[$.s]) {
842      const SourceClass = shapes[$.s];
843      $._sound = new SourceClass();
844      $._channels = 1;
845    } else if (BufferPlayer.samples.has($.s)) {
846      const sample = BufferPlayer.samples.get($.s);
847      $._buffers = [];
848      $._channels = sample.channels.length;
849      for (let i = 0; i < $._channels; i++) {
850        $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c'
851      }
852    } else {
853      console.warn('sound not loaded', $.s);
854    }
855
856    if ($.penv) {
857      $._penv = new ADSR({ decayCurve: 4 });
858      [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]);
859    }
860
861    if ($.vib) {
862      $._vib = new SineOsc();
863      $.vibmod = $.vibmod ?? getDefaultValue('vibmod');
864    }
865
866    if ($.fmi) {
867      $._fm = new SineOsc();
868      $.fmh = $.fmh ?? getDefaultValue('fmh');
869      if ($.fmenv) {
870        $._fmenv = new ADSR({ decayCurve: 2 });
871        [$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]);
872      }
873    }

gain envelope

876    $._adsr = new ADSR({ decayCurve: 2 });
877    // delay
878    $.delay = applyGainCurve($.delay ?? getDefaultValue('delay'));
879    $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback');
880    $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed');
881    $.delaytime = $.delaytime ?? getDefaultValue('delaytime');

filter setup

884    if ($.lpenv) {
885      $._lpenv = new ADSR({ decayCurve: 4 });
886      [$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]);
887    }
888    if ($.hpenv) {
889      $._hpenv = new ADSR({ decayCurve: 4 });
890      [$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]);
891    }
892    if ($.bpenv) {
893      $._bpenv = new ADSR({ decayCurve: 4 });
894      [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]);
895    }

channelwise effects setup

898    $._chorus = $.chorus ? [] : null;
899    $._lpf = $.cutoff ? [] : null;
900    $._hpf = $.hcutoff ? [] : null;
901    $._bpf = $.bandf ? [] : null;
902    $._coarse = $.coarse ? [] : null;
903    $._crush = $.crush ? [] : null;
904    $._distort = $.distort ? [] : null;
905    for (let i = 0; i < this._channels; i++) {
906      $._lpf?.push(new TwoPoleFilter());
907      $._hpf?.push(new TwoPoleFilter());
908      $._bpf?.push(new TwoPoleFilter());
909      $._chorus?.push(new Chorus());
910      $._coarse?.push(new Coarse());
911      $._crush?.push(new Crush());
912      $._distort?.push(new Distort());
913    }
914  }
915  update(t) {
916    if (!this._sound && !this._buffers) {
917      return 0;
918    }
919    let gate = Number(t >= this._begin && t <= this._holdEnd);
921    let freq = this.freq * this.speed;

frequency modulation

924    if (this._fm && this.fmh !== undefined && this.fmi !== undefined) {
925      let fmi = this.fmi;
926      if (this._fmenv) {
927        const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
928        fmi = this.fmenv * env * fmi;
929      }
930      const modfreq = freq * this.fmh;
931      const modgain = modfreq * fmi;
932      freq = freq + this._fm.update(modfreq) * modgain;
933    }

vibrato

936    if (this._vib && this.vibmod !== undefined) {
937      freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
938    }

pitch envelope

941    if (this._penv && this.penv !== undefined) {
942      const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
943      freq = freq + env * this.penv;
944    }

filters

947    let lpf = this.cutoff;
948    if (lpf !== undefined && this._lpenv) {
949      const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
950      lpf = this.lpenv * env * lpf + lpf;
951    }
952    let hpf = this.hcutoff;
953    if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) {
954      const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
955      hpf = 2 ** this.hpenv * env * hpf + hpf;
956    }
957    let bpf = this.bandf;
958    if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) {
959      const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
960      bpf = 2 ** this.bpenv * env * bpf + bpf;
961    }
962    // gain envelope
963    const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);

channelwise dsp

966    for (let i = 0; i < this._channels; i++) {
967      // sound source
968      if (this._sound && this.s === 'pulse') {
969        this.out[i] = this._sound.update(freq, this.pw);
970      } else if (this._sound) {
971        this.out[i] = this._sound.update(freq);
972      } else if (this._buffers) {
973        this.out[i] = this._buffers[i].update(freq);
974      }
975      this.out[i] = this.out[i] * this.gain * this.velocity;
976      if (this._chorus) {
977        const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11);
978        this.out[i] = c + this.out[i];
979      }
980
981      if (this._lpf) {
982        this._lpf[i].update(this.out[i], lpf, this.resonance);
983        this.out[i] = this._lpf[i].s1;
984      }
985      if (this._hpf) {
986        this._hpf[i].update(this.out[i], hpf, this.hresonance);
987        this.out[i] = this.out[i] - this._hpf[i].s1;
988      }
989      if (this._bpf) {
990        this._bpf[i].update(this.out[i], bpf, this.bandq);
991        this.out[i] = this._bpf[i].s0;
992      }
993      if (this._coarse) {
994        this.out[i] = this._coarse[i].update(this.out[i], this.coarse);
995      }
996      if (this._crush) {
997        this.out[i] = this._crush[i].update(this.out[i], this.crush);
998      }
999      if (this._distort) {
1000        this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol);
1001      }
1002      this.out[i] = this.out[i] * env;
1003      this.out[i] = this.out[i] * this.postgain;
1004      if (!this._buffers) {
1005        this.out[i] = this.out[i] * 0.2; // turn down waveform
1006      }
1007    }
1008    if (this._channels === 1) {
1009      this.out[1] = this.out[0];
1010    }
1011    if (this.pan !== 0.5) {
1012      const panpos = (this.pan * Math.PI) / 2;
1013      this.out[0] = this.out[0] * Math.cos(panpos);
1014      this.out[1] = this.out[1] * Math.sin(panpos);
1015    }
1016  }
1017}

this class is the interface to the "outer world" it handles spawning and despawning of DoughVoice's

1021export class Dough {
1022  voices = []; // DoughVoice[]
1023  vid = 0;
1024  q = [];
1025  out = [0, 0];
1026  delaysend = [0, 0];
1027  delaytime = getDefaultValue('delaytime');
1028  delayfeedback = getDefaultValue('delayfeedback');
1029  delayspeed = getDefaultValue('delayspeed');
1030  t = 0;
1031  // sampleRate: number, currentTime: number (seconds)
1032  constructor(sampleRate = 48000, currentTime = 0) {
1033    this.sampleRate = sampleRate;
1034    this.t = Math.floor(currentTime * sampleRate); // samples
1035    // console.log('init dough', this.sampleRate, this.t);
1036    this._delayL = new Delay();
1037    this._delayR = new Delay();
1038  }
1039  loadSample(name, channels, sampleRate) {
1040    BufferPlayer.samples.set(name, { channels, sampleRate });
1041  }
1042  scheduleSpawn(value) {
1043    if (value._begin === undefined) {
1044      throw new Error('[dough]: scheduleSpawn expected _begin to be set');
1045    }
1046    if (value._duration === undefined) {
1047      throw new Error('[dough]: scheduleSpawn expected _duration to be set');
1048    }
1049    value.sampleRate = this.sampleRate;
1050    // convert seconds to samples
1051    const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs
1052    this.schedule({ time, type: 'spawn', arg: value });
1053  }
1054  spawn(value) {
1055    value.id = this.vid++;
1056    const voice = new DoughVoice(value);
1057    this.voices.push(voice);
1058    // console.log('spawn', voice.id, 'voices:', this.voices.length);
1059    // schedule removal
1060    const endTime = Math.ceil(voice._end * this.sampleRate);
1061    this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id });
1062  }
1063  despawn(vid) {
1064    this.voices = this.voices.filter((v) => v.id !== vid);
1065    // console.log('despawn', vid, 'voices:', this.voices.length);
1066  }
1067  // schedules a function call with a single argument
1068  // msg = {time:number,type:string, arg: any}
1069  // the Dough method "type" will be called with "arg" at "time"
1070  schedule(msg) {
1071    if (!this.q.length) {
1072      // if empty, just push
1073      this.q.push(msg);
1074      return;
1075    }
1076    // not empty
1077    // find index where msg.time fits in
1078    let i = 0;
1079    while (i < this.q.length && this.q[i].time < msg.time) {
1080      i++;
1081    }
1082    // this ensures q stays sorted by time, so we only need to check q[0]
1083    this.q.splice(i, 0, msg);
1084  }
1085  // maybe update should be called once per block instead for perf reasons?
1086  update() {
1087    // go over q
1088    while (this.q.length > 0 && this.q[0].time <= this.t) {
1089      // console.log('schedule', this.q[0]);
1090      // trigger due messages. q is sorted, so we only need to check q[0]
1091      this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method
1092      this.q.shift();
1093    }
1094    // add active voices
1095    this.out[0] = 0;
1096    this.out[1] = 0;
1097    for (let v = 0; v < this.voices.length; v++) {
1098      this.voices[v].update(this.t / this.sampleRate);
1099      this.out[0] += this.voices[v].out[0];
1100      this.out[1] += this.voices[v].out[1];
1101      if (this.voices[v].delay) {
1102        this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay;
1103        this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay;
1104        this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice
1105        this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice
1106        this.delayfeedback = this.voices[v].delayfeedback;
1107      }
1108    }
1109    // todo: how to change delaytime / delayfeedback from a voice?
1110    const delayL = this._delayL.update(this.delaysend[0], this.delaytime);
1111    const delayR = this._delayR.update(this.delaysend[1], this.delaytime);
1112
1113    this.delaysend[0] = delayL * this.delayfeedback;
1114    this.delaysend[1] = delayR * this.delayfeedback;
1115    this.out[0] += delayL;
1116    this.out[1] += delayR;
1117    this.t++;
1118  }
1119}