1// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
2// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
3// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
4
5import OLAProcessor from './ola-processor';
6import FFT from './fft.js';
7import { getDistortionAlgorithm } from './helpers.mjs';
8import * as ugens from '@kabelsalat/lib/src/ugens.js';
9
10const UGENS = new Map(Object.entries(ugens));
11
12const blockSize = 128;
13const PI = Math.PI;
14const TWO_PI = 2 * PI;
15const INVSR = 1 / sampleRate;
16
17const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t);
18const dbToLin = (db) => Math.pow(10, db / 20);
19
20const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
21const mod = (n, m) => ((n % m) + m) % m;
22const lerp = (a, b, n) => n * (b - a) + a;
23const pv = (arr, n) => arr[n] ?? arr[0];
24const frac = (x) => x - Math.floor(x);
25
26// Fast integer ops for non-negative values
27const ffloor = (x) => x | 0;
28const fround = (x) => ffloor(x + 0.5);
29const fceil = (x) => ffloor(x + 1);
30const ffrac = (x) => x - ffloor(x);
31
32const fast_tanh = (x) => {
33  const x2 = x ** 2;
34  return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
35};
36
37// Optimized per-voice detuner which precomputes constants
38const getDetuner = (unison, detune) => {
39  if (unison < 2) {
40    return (_voiceIdx) => 0;
41  }
42  const scale = detune / (unison - 1);
43  const center = detune * 0.5;
44  return (voiceIdx) => voiceIdx * scale - center;
45};
46
47const applySemitoneDetuneToFrequency = (frequency, detune) => {
48  return frequency * Math.pow(2, detune / 12);
49};
50
51// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
52// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
53function polyBlep(phase, dt) {
54  dt = Math.min(dt, 1 - dt);
55  const invdt = 1 / dt;
56  // Start of cycle
57  if (phase < dt) {
58    phase *= invdt;
59    return 2 * phase - phase ** 2 - 1;
60  }
61  // End of cycle
62  else if (phase > 1 - dt) {
63    phase = (phase - 1) * invdt;
64    return phase ** 2 + 2 * phase + 1;
65  }
66  // 0 otherwise
67  else {
68    return 0;
69  }
70}
71// The order is important for dough integration
72const waveshapes = {
73  tri(phase, skew = 0.5) {
74    const x = 1 - skew;
75    if (phase >= skew) {
76      return 1 / x - phase / x;
77    }
78    return phase / skew;
79  },
80  sine(phase) {
81    return Math.sin(TWO_PI * phase) * 0.5 + 0.5;
82  },
83  ramp(phase) {
84    return phase;
85  },
86  saw(phase) {
87    return 1 - phase;
88  },
89
90  square(phase, skew = 0.5) {
91    if (phase >= skew) {
92      return 0;
93    }
94    return 1;
95  },
96  custom(phase, values = [0, 1]) {
97    const numParts = values.length - 1;
98    const currPart = Math.floor(phase * numParts);
99
100    const partLength = 1 / numParts;
101    const startVal = clamp(values[currPart], 0, 1);
102    const endVal = clamp(values[currPart + 1], 0, 1);
103    const y2 = endVal;
104    const y1 = startVal;
105    const x1 = 0;
106    const x2 = partLength;
107    const slope = (y2 - y1) / (x2 - x1);
108    return slope * (phase - partLength * currPart) + startVal;
109  },
110  sawblep(phase, dt) {
111    const v = 2 * phase - 1;
112    return v - polyBlep(phase, dt);
113  },
114};
115
116const waveShapeNames = Object.keys(waveshapes);
117class LFOProcessor extends AudioWorkletProcessor {
118  static get parameterDescriptors() {
119    return [
120      { name: 'begin', defaultValue: 0 },
121      { name: 'time', defaultValue: 0 },
122      { name: 'end', defaultValue: 0 },
123      { name: 'frequency', defaultValue: 0.5 },
124      { name: 'skew', defaultValue: 0.5 },
125      { name: 'depth', defaultValue: 1 },
126      { name: 'phaseoffset', defaultValue: 0 },
127      { name: 'shape', defaultValue: 0 },
128      { name: 'curve', defaultValue: 1 },
129      { name: 'dcoffset', defaultValue: 0 },
130      { name: 'min', defaultValue: -1e9 },
131      { name: 'max', defaultValue: 1e9 },
132    ];
133  }
134
135  constructor() {
136    super();
137    this.phase;
138  }
139
140  incrementPhase(dt) {
141    this.phase += dt;
142    if (this.phase > 1.0) {
143      this.phase = this.phase - 1;
144    }
145  }
146
147  process(_inputs, outputs, parameters) {
148    const begin = parameters['begin'][0];
149    const end = parameters['end'][0];
150    if (currentTime >= end) {
151      return false;
152    }
153    if (currentTime <= begin) {
154      return true;
155    }
156
157    const output = outputs[0];
158    const frequency = parameters['frequency'][0];
159
160    const time = parameters['time'][0];
161    const depth = parameters['depth'][0];
162    const skew = parameters['skew'][0];
163    const phaseoffset = parameters['phaseoffset'][0];
164
165    const curve = parameters['curve'][0];
166
167    const dcoffset = parameters['dcoffset'][0];
168
169    const min = parameters['min'][0];
170    const max = parameters['max'][0];
171    const shape = waveShapeNames[parameters['shape'][0]];
172
173    const blockSize = output[0].length ?? 0;
174
175    if (this.phase == null) {
176      this.phase = ffrac(time * frequency + phaseoffset);
177    }
178    const dt = frequency * INVSR;
179    for (let n = 0; n < blockSize; n++) {
180      for (let i = 0; i < output.length; i++) {
181        let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
182        modval = Math.pow(modval, curve);
183        output[i][n] = clamp(modval, min, max);
184      }
185      this.incrementPhase(dt);
186    }
187
188    return true;
189  }
190}
191registerProcessor('lfo-processor', LFOProcessor);
192
193class CoarseProcessor extends AudioWorkletProcessor {
194  static get parameterDescriptors() {
195    return [{ name: 'coarse', defaultValue: 1 }];
196  }
197
198  constructor() {
199    super();
200    this.started = false;
201  }
202
203  process(inputs, outputs, parameters) {
204    const input = inputs[0];
205    const output = outputs[0];
206
207    const hasInput = !(input[0] === undefined);
208    if (this.started && !hasInput) {
209      return false;
210    }
211    this.started = hasInput;
212
213    let coarse = parameters.coarse[0] ?? 0;
214    coarse = Math.max(1, coarse);
215    for (let n = 0; n < blockSize; n++) {
216      for (let i = 0; i < input.length; i++) {
217        output[i][n] = n % coarse < 1 ? input[i][n] : output[i][n - 1];
218      }
219    }
220    return true;
221  }
222}
223registerProcessor('coarse-processor', CoarseProcessor);
224
225class CrushProcessor extends AudioWorkletProcessor {
226  static get parameterDescriptors() {
227    return [{ name: 'crush', defaultValue: 0 }];
228  }
229
230  constructor() {
231    super();
232    this.started = false;
233  }
234
235  process(inputs, outputs, parameters) {
236    const input = inputs[0];
237    const output = outputs[0];
238
239    const hasInput = !(input[0] === undefined);
240    if (this.started && !hasInput) {
241      return false;
242    }
243    this.started = hasInput;
244
245    let crush = parameters.crush[0] ?? 8;
246    crush = Math.max(1, crush);
247
248    for (let n = 0; n < blockSize; n++) {
249      for (let i = 0; i < input.length; i++) {
250        const x = Math.pow(2, crush - 1);
251        output[i][n] = Math.round(input[i][n] * x) / x;
252      }
253    }
254    return true;
255  }
256}
257registerProcessor('crush-processor', CrushProcessor);
258
259class ShapeProcessor extends AudioWorkletProcessor {
260  static get parameterDescriptors() {
261    return [
262      { name: 'shape', defaultValue: 0 },
263      { name: 'postgain', defaultValue: 1 },
264    ];
265  }
266
267  constructor() {
268    super();
269    this.started = false;
270  }
271
272  process(inputs, outputs, parameters) {
273    const input = inputs[0];
274    const output = outputs[0];
275
276    const hasInput = !(input[0] === undefined);
277    if (this.started && !hasInput) {
278      return false;
279    }
280    this.started = hasInput;
281
282    let shape = parameters.shape[0];
283    shape = shape < 1 ? shape : 1.0 - 4e-10;
284    shape = (2.0 * shape) / (1.0 - shape);
285    const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
286
287    for (let n = 0; n < blockSize; n++) {
288      for (let i = 0; i < input.length; i++) {
289        output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
290      }
291    }
292    return true;
293  }
294}
295registerProcessor('shape-processor', ShapeProcessor);
296
297class TwoPoleFilter {
298  s0 = 0;
299  s1 = 0;
300  update(s, cutoff, resonance = 0) {
301    // Out of bound values can produce NaNs
302    resonance = clamp(resonance, 0, 1);
303    cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
304    const c = clamp(2 * Math.sin(cutoff * PI * INVSR), 0, 1.14);
305    const r = Math.pow(0.5, 8 * resonance + 1);
306    const mrc = 1 - r * c;
307    this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
308    this.s1 = mrc * this.s1 + c * this.s0; // lpf
309    return this.s1; // return lpf by default
310  }
311}
312
313class DJFProcessor extends AudioWorkletProcessor {
314  static get parameterDescriptors() {
315    return [{ name: 'value', defaultValue: 0.5 }];
316  }
317
318  constructor() {
319    super();
320    this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
321  }
322
323  process(inputs, outputs, parameters) {
324    const input = inputs[0];
325    const output = outputs[0];
326
327    const hasInput = !(input[0] === undefined);
328    this.started = hasInput;
329
330    const value = clamp(parameters.value[0], 0, 1);
331    let filterType = 'none';
332    let cutoff;
333    let v = 1;
334    if (value > 0.51) {
335      filterType = 'hipass';
336      v = (value - 0.5) * 2;
337    } else if (value < 0.49) {
338      filterType = 'lopass';
339      v = value * 2;
340    }
341    cutoff = Math.pow(v * 11, 4);
342
343    for (let i = 0; i < input.length; i++) {
344      for (let n = 0; n < blockSize; n++) {
345        if (filterType == 'none') {
346          output[i][n] = input[i][n];
347        } else {
348          this.filters[i].update(input[i][n], cutoff, 0.1);
349          if (filterType === 'lopass') {
350            output[i][n] = this.filters[i].s1;
351          } else if (filterType === 'hipass') {
352            output[i][n] = input[i][n] - this.filters[i].s1;
353          } else {
354            output[i][n] = input[i][n];
355          }
356        }
357      }
358    }
359    return true;
360  }
361}
362registerProcessor('djf-processor', DJFProcessor);
363
364//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
365class LadderProcessor extends AudioWorkletProcessor {
366  static get parameterDescriptors() {
367    return [
368      { name: 'frequency', defaultValue: 500 },
369      { name: 'q', defaultValue: 1 },
370      { name: 'drive', defaultValue: 0.69 },
371    ];
372  }
373
374  constructor() {
375    super();
376    this.started = false;
377    this.p0 = [0, 0];
378    this.p1 = [0, 0];
379    this.p2 = [0, 0];
380    this.p3 = [0, 0];
381    this.p32 = [0, 0];
382    this.p33 = [0, 0];
383    this.p34 = [0, 0];
384  }
385
386  process(inputs, outputs, parameters) {
387    const input = inputs[0];
388    const output = outputs[0];
389
390    const hasInput = !(input[0] === undefined);
391    if (this.started && !hasInput) {
392      return false;
393    }
394
395    this.started = hasInput;
396
397    const resonance = parameters.q[0];
398    const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
399
400    let cutoff = parameters.frequency[0];
401    cutoff = cutoff * TWO_PI * INVSR;
402    cutoff = cutoff > 1 ? 1 : cutoff;
403
404    const k = Math.min(8, resonance * 0.13);
405    //               drive makeup  * resonance volume loss makeup
406    let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
407
408    for (let n = 0; n < blockSize; n++) {
409      for (let i = 0; i < input.length; i++) {
410        const out = this.p3[i] * 0.360891 + this.p32[i] * 0.41729 + this.p33[i] * 0.177896 + this.p34[i] * 0.0439725;
411
412        this.p34[i] = this.p33[i];
413        this.p33[i] = this.p32[i];
414        this.p32[i] = this.p3[i];
415
416        this.p0[i] += (fast_tanh(input[i][n] * drive - k * out) - fast_tanh(this.p0[i])) * cutoff;
417        this.p1[i] += (fast_tanh(this.p0[i]) - fast_tanh(this.p1[i])) * cutoff;
418        this.p2[i] += (fast_tanh(this.p1[i]) - fast_tanh(this.p2[i])) * cutoff;
419        this.p3[i] += (fast_tanh(this.p2[i]) - fast_tanh(this.p3[i])) * cutoff;
420
421        output[i][n] = out * makeupgain;
422      }
423    }
424    return true;
425  }
426}
427registerProcessor('ladder-processor', LadderProcessor);
428
429class DistortProcessor extends AudioWorkletProcessor {
430  static get parameterDescriptors() {
431    return [
432      { name: 'distort', defaultValue: 0 },
433      { name: 'postgain', defaultValue: 1 },
434    ];
435  }
436
437  constructor({ processorOptions }) {
438    super();
439    this.started = false;
440    this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
441  }
442
443  process(inputs, outputs, parameters) {
444    const input = inputs[0];
445    const output = outputs[0];
446
447    const hasInput = !(input[0] === undefined);
448    if (this.started && !hasInput) {
449      return false;
450    }
451    this.started = hasInput;
452    for (let n = 0; n < blockSize; n++) {
453      const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
454      const shape = Math.expm1(pv(parameters.distort, n));
455      for (let ch = 0; ch < input.length; ch++) {
456        const x = input[ch][n];
457        output[ch][n] = postgain * this.algorithm(x, shape);
458      }
459    }
460    return true;
461  }
462}
463registerProcessor('distort-processor', DistortProcessor);
464
465// SUPERSAW
466class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
467  constructor() {
468    super();
469    this.port.onmessage = (e) => {
470      const { type, payload } = e.data || {};
471      if (type === 'initialize') {
472        this.initialize(payload);
473      }
474    };
475    this.initialize();
476  }
477  initialize(_options) {
478    this.phase = [];
479  }
480  static get parameterDescriptors() {
481    return [
482      {
483        name: 'begin',
484        defaultValue: -1,
485        max: Number.POSITIVE_INFINITY,
486        min: -1,
487      },
488
489      {
490        name: 'end',
491        defaultValue: -1,
492        max: Number.POSITIVE_INFINITY,
493        min: -1,
494      },
495
496      {
497        name: 'frequency',
498        defaultValue: 440,
499        min: Number.EPSILON,
500      },
501
502      {
503        name: 'panspread',
504        defaultValue: 0.4,
505        min: 0,
506        max: 1,
507      },
508      {
509        name: 'freqspread',
510        defaultValue: 0.2,
511        min: 0,
512      },
513      {
514        name: 'detune',
515        defaultValue: 0,
516        min: 0,
517      },
518
519      {
520        name: 'voices',
521        defaultValue: 5,
522        min: 1,
523        automationRate: 'k-rate',
524      },
525    ];
526  }
527  process(_input, outputs, params) {
528    const begin = params.begin[0];
529    const end = params.end[0];
530    const beginDefined = begin >= 0;
531    const endDefined = end >= 0;
532    // We give a 0.5s grace period (for node pooling) before termination
533    const shouldTerminate = endDefined && currentTime >= end + 0.5;
534    const ended = endDefined && currentTime >= end;
535    const notStarted = currentTime <= begin;
536    if (shouldTerminate) {
537      return false;
538    } else if (ended || notStarted || !beginDefined) {
539      return true;
540    }
541    const output = outputs[0];
542    const voices = params.voices[0]; // k-rate
543    for (let i = 0; i < output[0].length; i++) {
544      const detune = pv(params.detune, i);
545      const freqspread = pv(params.freqspread, i);
546      const panspread = pv(params.panspread, i) * 0.5 + 0.5;
547      let gainL = Math.sqrt(1 - panspread);
548      let gainR = Math.sqrt(panspread);
549      let freq = pv(params.frequency, i);
550      // Main detuning
551      freq = applySemitoneDetuneToFrequency(freq, detune / 100);
552      const detuner = getDetuner(voices, freqspread);
553      for (let n = 0; n < voices; n++) {
554        // Individual voice detuning
555        const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
556        // We must wrap this here because it is passed into sawblep below which
557        // has domain [0, 1]
558        const dt = frac(freqVoice * INVSR);
559        this.phase[n] = this.phase[n] ?? Math.random();
560        const v = waveshapes.sawblep(this.phase[n], dt);
561
562        output[0][i] += v * gainL;
563        output[1][i] += v * gainR;
564
565        let pn = this.phase[n] + dt;
566        if (pn >= 1.0) pn -= 1.0;
567        this.phase[n] = pn;
568        // invert right and left gain
569        const tmp = gainL;
570        gainL = gainR;
571        gainR = tmp;
572      }
573    }
574    return true;
575  }
576}
577
578registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
579
580// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
581const BUFFERED_BLOCK_SIZE = 2048;
582
583const hannCache = new Map();
584function genHannWindow(length) {
585  if (!hannCache.has(length)) {
586    const win = new Float32Array(length);
587    for (let i = 0; i < length; i++) {
588      win[i] = 0.5 * (1 - Math.cos((TWO_PI * i) / length));
589    }
590    hannCache.set(length, win);
591  }
592  return hannCache.get(length);
593}
594
595class PhaseVocoderProcessor extends OLAProcessor {
596  static get parameterDescriptors() {
597    return [
598      {
599        name: 'pitchFactor',
600        defaultValue: 1.0,
601      },
602    ];
603  }
604
605  constructor(options) {
606    options.processorOptions = {
607      blockSize: BUFFERED_BLOCK_SIZE,
608    };
609    super(options);
610    this.timeCursor = 0;
611    this.fftSize = this.blockSize;
612    this.invfftSize = 1 / this.fftSize;
613    this.hannWindow = genHannWindow(this.fftSize);
614    // prepare FFT and pre-allocate buffers
615    this.fft = new FFT(this.fftSize);
616    this.freqComplexBuffer = this.fft.createComplexArray();
617    this.freqComplexBufferShifted = this.fft.createComplexArray();
618    this.timeComplexBuffer = this.fft.createComplexArray();
619    this.magnitudes = new Float32Array(this.fftSize / 2 + 1);
620    this.peakIndexes = new Int32Array(this.magnitudes.length);
621    this.nbPeaks = 0;
622  }
623
624  processOLA(inputs, outputs, parameters) {
625    // no automation, take last value
626    let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
627    if (pitchFactor < 0) {
628      pitchFactor = pitchFactor * 0.25;
629    }
630    pitchFactor = Math.max(0, pitchFactor + 1);
631    for (let i = 0; i < this.nbInputs; i++) {
632      for (let j = 0; j < inputs[i].length; j++) {
633        const input = inputs[i][j];
634        const output = outputs[i][j];
635        this.applyHannWindow(input);
636        this.fft.realTransform(this.freqComplexBuffer, input);
637        this.computeMagnitudes();
638        this.findPeaks();
639        this.shiftPeaks(pitchFactor);
640        this.fft.completeSpectrum(this.freqComplexBufferShifted);
641        this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
642        this.fft.fromComplexArray(this.timeComplexBuffer, output);
643        this.applyHannWindow(output);
644      }
645    }
646    this.timeCursor += this.hopSize;
647  }
648
649  /** Apply Hann window in-place
650   * @tags internals
651   */
652  applyHannWindow(input) {
653    for (let i = 0; i < this.blockSize; i++) {
654      input[i] *= this.hannWindow[i] * 1.62;
655    }
656  }
657
658  /** Compute squared magnitudes for peak finding
659   * @tags internals
660   **/
661  computeMagnitudes() {
662    let i = 0,
663      j = 0;
664    while (i < this.magnitudes.length) {
665      const real = this.freqComplexBuffer[j];
666      const imag = this.freqComplexBuffer[j + 1];
667      // no need to sqrt for peak finding
668      this.magnitudes[i] = real ** 2 + imag ** 2;
669      i += 1;
670      j += 2;
671    }
672  }
673
674  /** Find peaks in spectrum magnitudes
675   * @tags internals
676   **/
677  findPeaks() {
678    this.nbPeaks = 0;
679    let i = 2;
680    const end = this.magnitudes.length - 2;
681    while (i < end) {
682      const mag = this.magnitudes[i];
683      if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
684        i++;
685        continue;
686      }
687      if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {
688        i++;
689        continue;
690      }
691      this.peakIndexes[this.nbPeaks] = i;
692      this.nbPeaks++;
693      i += 2;
694    }
695  }
696
697  /** Shift peaks and regions of influence by pitchFactor into new specturm
698   * @tags internals
699   */
700  shiftPeaks(pitchFactor) {
701    // zero-fill new spectrum
702    this.freqComplexBufferShifted.fill(0);
703    for (let i = 0; i < this.nbPeaks; i++) {
704      const peakIndex = this.peakIndexes[i];
705      const peakIndexShifted = fround(peakIndex * pitchFactor);
706      if (peakIndexShifted > this.magnitudes.length) {
707        break;
708      }
709      // find region of influence
710      let startIndex = 0;
711      let endIndex = this.fftSize;
712      if (i > 0) {
713        startIndex = peakIndex - fround((peakIndex - this.peakIndexes[i - 1]) / 2);
714      }
715      if (i < this.nbPeaks - 1) {
716        endIndex = peakIndex + fceil((this.peakIndexes[i + 1] - peakIndex) / 2);
717      }
718      // shift whole region of influence around peak to shifted peak
719      const startOffset = startIndex - peakIndex;
720      const endOffset = endIndex - peakIndex;
721      const omegaDelta = TWO_PI * this.invfftSize * (peakIndexShifted - peakIndex);
722      const phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
723      const phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
724      for (let j = startOffset; j < endOffset; j++) {
725        const binIndex = peakIndex + j;
726        const binIndexShifted = peakIndexShifted + j;
727        if (binIndexShifted >= this.magnitudes.length) {
728          break;
729        }
730        // apply phase correction
731        const indexReal = 2 * binIndex;
732        const indexImag = indexReal + 1;
733        const valueReal = this.freqComplexBuffer[indexReal];
734        const valueImag = this.freqComplexBuffer[indexImag];
735
736        const valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
737        const valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
738
739        const indexShiftedReal = 2 * binIndexShifted;
740        const indexShiftedImag = indexShiftedReal + 1;
741        this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
742        this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
743      }
744    }
745  }
746}
747
748registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
749
750// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
751class PulseOscillatorProcessor extends AudioWorkletProcessor {
752  constructor() {
753    super();
754    this.phi = -PI; // phase
755    this.Y0 = 0; // feedback memories
756    this.Y1 = 0;
757    this.PW = PI; // pulse width
758    this.B = 2.3; // feedback coefficient
759    this.dphif = 0; // filtered phase increment
760    this.envf = 0; // filtered envelope
761  }
762
763  static get parameterDescriptors() {
764    return [
765      {
766        name: 'begin',
767        defaultValue: 0,
768        max: Number.POSITIVE_INFINITY,
769        min: 0,
770      },
771
772      {
773        name: 'end',
774        defaultValue: 0,
775        max: Number.POSITIVE_INFINITY,
776        min: 0,
777      },
778
779      {
780        name: 'frequency',
781        defaultValue: 440,
782        min: Number.EPSILON,
783      },
784      {
785        name: 'detune',
786        defaultValue: 0,
787        min: Number.NEGATIVE_INFINITY,
788        max: Number.POSITIVE_INFINITY,
789      },
790      {
791        name: 'pulsewidth',
792        defaultValue: 1,
793        min: 0,
794        max: Number.POSITIVE_INFINITY,
795      },
796    ];
797  }
798
799  process(inputs, outputs, params) {
800    if (this.disconnected) {
801      return false;
802    }
803    if (currentTime <= params.begin[0]) {
804      return true;
805    }
806    if (currentTime >= params.end[0]) {
807      return false;
808    }
809    const output = outputs[0];
810    let env = 1,
811      dphi;
812
813    for (let i = 0; i < (output[0].length ?? 0); i++) {
814      const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
815      const detune = pv(params.detune, i);
816      const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
817
818      dphi = freq * TWO_PI * INVSR; // phase increment
819      this.dphif += 0.1 * (dphi - this.dphif);
820
821      env *= 0.9998; // exponential decay envelope
822      this.envf += 0.1 * (env - this.envf);
823
824      // Feedback coefficient control
825      this.B = 2.3 * (1 - 0.0001 * freq); // feedback limitation
826      if (this.B < 0) this.B = 0;
827
828      // Waveform generation (half-Tomisawa oscillators)
829      this.phi += this.dphif; // phase increment
830      if (this.phi >= PI) this.phi -= TWO_PI; // phase wrapping
831
832      // First half-Tomisawa generator
833      let out0 = Math.cos(this.phi + this.B * this.Y0); // self-phase modulation
834      this.Y0 = 0.5 * (out0 + this.Y0); // anti-hunting filter
835
836      // Second half-Tomisawa generator (with phase offset for pulse width)
837      let out1 = Math.cos(this.phi + this.B * this.Y1 + pw);
838      this.Y1 = 0.5 * (out1 + this.Y1); // anti-hunting filter
839
840      for (let o = 0; o < output.length; o++) {
841        // Combination of both oscillators with envelope applied
842        output[o][i] = 0.15 * (out0 - out1) * this.envf;
843      }
844    }
845
846    return true; // keep the audio processing going
847  }
848}
849
850registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
851
852/**  BYTE BEATS
853 * @tags internals
854 */
855const chyx = {
856  /*bit*/ bitC: function (x, y, z) {
857    return x & y ? z : 0;
858  },
859  /*bit reverse*/ br: function (x, size = 8) {
860    if (size > 32) {
861      throw new Error('br() Size cannot be greater than 32');
862    }
863    let result = 0;
864    for (let idx = 0; idx < size; idx++) {
865      result |= chyx.bitC(x, 1 << idx, 1 << (size - (idx + 1)));
866    }
867    return result;
868  },
869  /*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
870    return Math.sin((x * PI) / 128);
871  },
872  /*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
873    return Math.cos((x * PI) / 128);
874  },
875  /*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
876    return Math.tan((x * PI) / 128);
877  },
878  /*converts t into a string composed of its bits; regexes that*/ regG: function (t, X) {
879    return X.test(t.toString(2));
880  },
881};
882
883// Create shortened Math functions
884let mathParams, byteBeatHelperFuncs;
885function getByteBeatFunc(codetext) {
886  if (mathParams == null) {
887    mathParams = Object.getOwnPropertyNames(Math);
888    byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
889    const chyxNames = Object.getOwnPropertyNames(chyx);
890    const chyxFuncs = chyxNames.map((k) => chyx[k]);
891    mathParams.push('int', 'window', ...chyxNames);
892    byteBeatHelperFuncs.push(Math.floor, globalThis, ...chyxFuncs);
893  }
894  return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
895}
896
897class ByteBeatProcessor extends AudioWorkletProcessor {
898  constructor() {
899    super();
900    this.port.onmessage = (event) => {
901      let { codeText } = event.data;
902      const { byteBeatStartTime } = event.data;
903      if (byteBeatStartTime != null) {
904        this.t = 0;
905        this.initialOffset = Math.floor(byteBeatStartTime);
906      }
907
908      //Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
909      //Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
910      codeText = codeText
911        .trim()
912        .replace(
913          /^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
914          (match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
915        );
916
917      this.func = getByteBeatFunc(codeText);
918    };
919    this.initialOffset = 0;
920    this.t = null;
921    this.func = null;
922  }
923
924  static get parameterDescriptors() {
925    return [
926      {
927        name: 'begin',
928        defaultValue: 0,
929        max: Number.POSITIVE_INFINITY,
930        min: 0,
931      },
932      {
933        name: 'frequency',
934        defaultValue: 440,
935        min: Number.EPSILON,
936      },
937      {
938        name: 'detune',
939        defaultValue: 0,
940        min: Number.NEGATIVE_INFINITY,
941        max: Number.POSITIVE_INFINITY,
942      },
943      {
944        name: 'end',
945        defaultValue: 0,
946        max: Number.POSITIVE_INFINITY,
947        min: 0,
948      },
949    ];
950  }
951
952  process(inputs, outputs, params) {
953    if (this.disconnected) {
954      return false;
955    }
956    if (currentTime <= params.begin[0]) {
957      return true;
958    }
959    if (currentTime >= params.end[0]) {
960      return false;
961    }
962    if (this.t == null) {
963      this.t = params.begin[0] * sampleRate;
964    }
965    const output = outputs[0];
966    const scale = 256 * INVSR;
967    for (let i = 0; i < output[0].length; i++) {
968      const detune = pv(params.detune, i);
969      const freq = applySemitoneDetuneToFrequency(pv(params.frequency, i), detune / 100);
970      const local_t = scale * freq * this.t + this.initialOffset;
971      const funcValue = this.func(local_t);
972      const signal = (funcValue & 255) / 127.5 - 1;
973      //prevent speaker blowout via clipping if threshold exceeds
974      const out = clamp(signal * 0.2, -0.4, 0.4);
975      for (let c = 0; c < output.length; c++) {
976        output[c][i] = out;
977      }
978      this.t++;
979    }
980
981    return true; // keep the audio processing going
982  }
983}
984
985registerProcessor('byte-beat-processor', ByteBeatProcessor);
986
987class EnvelopeProcessor extends AudioWorkletProcessor {
988  static get parameterDescriptors() {
989    return [
990      { name: 'begin', defaultValue: 0 },
991      { name: 'end', defaultValue: 0 },
992      { name: 'attack', defaultValue: 0.005, minValue: 0 },
993      { name: 'decay', defaultValue: 0.14, minValue: 0 },
994      { name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
995      { name: 'release', defaultValue: 0.1, minValue: 0 },
996      { name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
997      { name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
998      { name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
999      { name: 'depth', defaultValue: 1 },
1000      { name: 'min', defaultValue: -1e9 },
1001      { name: 'max', defaultValue: 1e9 },
1002      { name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
1003    ];
1004  }
1005
1006  constructor() {
1007    super();
1008    this.val = 0;
1009    this.segIdx = 0;
1010    this.state = 0;
1011    this.beginTime = 0;
1012    this.endTime = 0;
1013    this.attackStart = 0;
1014  }
1015
1016  _warp(phase, curvature, strength = 8) {
1017    if (phase === 0 || phase === 1) return phase; // fast exit
1018    if (curvature > 0) {
1019      // snappier
1020      const exp = 1 + strength * curvature;
1021      return 1 - Math.pow(1 - phase, exp);
1022    } else {
1023      // more calm
1024      const exp = 1 - strength * curvature;
1025      return Math.pow(phase, exp);
1026    }
1027  }
1028
1029  _advance(start, target, time, curvature) {
1030    if (time === 0 || start === target) {
1031      this.val = target;
1032    } else {
1033      // We compute our progress through this section of the envelope in time
1034      // as a `phase` value, which is warped by the curvature, and then used
1035      // to compute the value of the envelope at that time
1036      const phase = Math.min(1, (currentTime - this.beginTime) / time);
1037      const phaseWarped = this._warp(phase, curvature);
1038      this.val = start + (target - start) * phaseWarped;
1039    }
1040  }
1041
1042  process(_inputs, outputs, params) {
1043    const begin = params['begin'][0];
1044    const end = params['end'][0];
1045    if (currentTime >= end) {
1046      return false;
1047    }
1048    if (currentTime <= begin) {
1049      return true;
1050    }
1051    const out = outputs[0][0];
1052    const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
1053    if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
1054      // triggered
1055      this.beginTime = begin;
1056      this.state = 1;
1057      this.endTime = pv(params.end, 0);
1058      this.attackStart = this.val;
1059    }
1060    const susTime = this.endTime - this.beginTime;
1061    for (let i = 0; i < out.length; i++) {
1062      const attack = pv(params.attack, i);
1063      const decay = pv(params.decay, i);
1064      const sustain = pv(params.sustain, i);
1065      const release = pv(params.release, i);
1066      const aCurve = pv(params.attackCurve, i);
1067      const dCurve = pv(params.decayCurve, i);
1068      const rCurve = pv(params.releaseCurve, i);
1069      const depth = pv(params.depth, i);
1070      const min = pv(params.min, i);
1071      const max = pv(params.max, i);
1072      const states = [
1073        { time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
1074        { time: attack, start: this.attackStart, target: 1, curve: aCurve },
1075        { time: attack + decay, start: 1, target: sustain, curve: dCurve },
1076        { time: susTime, start: sustain, target: sustain },
1077        { time: susTime + release, start: sustain, target: 0, curve: rCurve },
1078      ];
1079      let { time, start, target, curve } = states[this.state];
1080      this._advance(start, target, time, curve);
1081      while (currentTime - this.beginTime >= time) {
1082        this.state = (this.state + 1) % states.length;
1083        time = states[this.state].time;
1084      }
1085      out[i] = clamp(this.val * depth, min, max);
1086    }
1087    return true;
1088  }
1089}
1090
1091registerProcessor('envelope-processor', EnvelopeProcessor);
1092
1093export const WarpMode = Object.freeze({
1094  NONE: 0,
1095  ASYM: 1,
1096  MIRROR: 2,
1097  BENDP: 3,
1098  BENDM: 4,
1099  BENDMP: 5,
1100  SYNC: 6,
1101  QUANT: 7,
1102  FOLD: 8,
1103  PWM: 9,
1104  ORBIT: 10,
1105  SPIN: 11,
1106  CHAOS: 12,
1107  PRIMES: 13,
1108  BINARY: 14,
1109  BROWNIAN: 15,
1110  RECIPROCAL: 16,
1111  WORMHOLE: 17,
1112  LOGISTIC: 18,
1113  SIGMOID: 19,
1114  FRACTAL: 20,
1115  FLIP: 21,
1116});
1117
1118function hash32(u) {
1119  u = u + 0x7ed55d16 + (u << 12);
1120  u = u ^ 0xc761c23c ^ (u >>> 19);
1121  u = u + 0x165667b1 + (u << 5);
1122  u = (u + 0xd3a2646c) ^ (u << 9);
1123  u = u + 0xfd7046c5 + (u << 3);
1124  u = u ^ 0xb55a4f09 ^ (u >>> 16);
1125  return u >>> 0;
1126}
1127const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000;
1128
1129function bitReverse(i, n) {
1130  let r = 0;
1131  for (let b = 0; b < n; b++) {
1132    r = (r << 1) | (i & 1);
1133    i >>>= 1;
1134  }
1135  return r;
1136}
1137
1138function noise(x) {
1139  const i = Math.floor(x),
1140    f = x - i;
1141  const a = hash01(i),
1142    b = hash01(i + 1);
1143  return a + (b - a) * f;
1144}
1145
1146function brownian(x, oct = 4) {
1147  let amp = 0.5,
1148    sum = 0,
1149    norm = 0,
1150    freq = 1;
1151  for (let o = 0; o < oct; o++) {
1152    sum += amp * noise(x * freq);
1153    norm += amp;
1154    amp *= 0.5;
1155    freq *= 2;
1156  }
1157  return (sum / norm) * 2 - 1;
1158}
1159
1160const tablesCache = {};
1161class WavetableOscillatorProcessor extends AudioWorkletProcessor {
1162  static get parameterDescriptors() {
1163    return [
1164      { name: 'begin', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
1165      { name: 'end', defaultValue: -1, min: -1, max: Number.POSITIVE_INFINITY },
1166      { name: 'frequency', defaultValue: 440, min: Number.EPSILON },
1167      { name: 'detune', defaultValue: 0 },
1168      { name: 'freqspread', defaultValue: 0.18, min: 0 },
1169      { name: 'position', defaultValue: 0, min: 0, max: 1 },
1170      { name: 'warp', defaultValue: 0, min: 0, max: 1 },
1171      { name: 'warpMode', defaultValue: 0 },
1172      { name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
1173      { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
1174      { name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
1175    ];
1176  }
1177
1178  constructor(options) {
1179    super(options);
1180    this.port.onmessage = (e) => {
1181      const { type, payload } = e.data || {};
1182      if (type === 'initialize') {
1183        this.initialize(payload);
1184      }
1185    };
1186    this.initialize();
1187  }
1188  initialize(options) {
1189    this.table = null;
1190    this.frameLen = null;
1191    this.numFrames = null;
1192    this.phase = [];
1193    if (options?.key) {
1194      const key = options.key;
1195      this.frameLen = options.frameLen;
1196      if (!tablesCache[key]) {
1197        tablesCache[key] = options.frames;
1198      }
1199      this.table = tablesCache[key];
1200      this.numFrames = this.table.length;
1201    }
1202  }
1203
1204  _mirror(x) {
1205    return 1 - Math.abs(2 * x - 1);
1206  }
1207
1208  _toBits(amt, min = 2, max = 12) {
1209    const b = max + (min - max) * amt;
1210    return { b, n: fround(Math.pow(2, b)) };
1211  }
1212
1213  _warpPhase(phase, amt, mode) {
1214    switch (mode) {
1215      case WarpMode.NONE: {
1216        return phase;
1217      }
1218      case WarpMode.ASYM: {
1219        const a = 0.01 + 0.99 * amt;
1220        return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a);
1221      }
1222      case WarpMode.MIRROR: {
1223        // Asym, then mirror
1224        return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM));
1225      }
1226      case WarpMode.BENDP: {
1227        return Math.pow(phase, 1 + 3 * amt);
1228      }
1229      case WarpMode.BENDM: {
1230        return Math.pow(phase, 1 / (1 + 3 * amt));
1231      }
1232      case WarpMode.BENDMP: {
1233        return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
1234      }
1235      case WarpMode.SYNC: {
1236        const syncRatio = Math.pow(16, amt ** 2);
1237        return (phase * syncRatio) % 1;
1238      }
1239      case WarpMode.QUANT: {
1240        const { n } = this._toBits(amt);
1241        return ffloor(phase * n) / n;
1242      }
1243      case WarpMode.FOLD: {
1244        const K = 7;
1245        const k = 1 + Math.max(1, fround(K * amt));
1246        return Math.abs(ffrac(k * phase) - 0.5) * 2;
1247      }
1248      case WarpMode.PWM: {
1249        const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
1250        if (phase < w) return (phase / w) * 0.5;
1251        return 0.5 + ((phase - w) / (1 - w)) * 0.5;
1252      }
1253      case WarpMode.ORBIT: {
1254        const depth = 0.5 * amt;
1255        const n = 3;
1256        return frac(phase + depth * Math.sin(TWO_PI * n * phase));
1257      }
1258      case WarpMode.SPIN: {
1259        const depth = 0.5 * amt;
1260        const { n } = this._toBits(amt, 1, 6);
1261        return frac(phase + depth * Math.sin(TWO_PI * n * phase));
1262      }
1263      case WarpMode.CHAOS: {
1264        const r = 3.7 + 0.3 * amt;
1265        const logistic = r * phase * (1 - phase);
1266        return clamp((1 - amt) * phase + amt * logistic, 0, 1);
1267      }
1268      case WarpMode.PRIMES: {
1269        const isPrime = (n) => {
1270          if (n < 2) return false;
1271          if (n % 2 === 0) return n === 2;
1272          for (let d = 3; d ** 2 <= n; d += 2) if (n % d === 0) return false;
1273          return true;
1274        };
1275        let { n } = this._toBits(amt, 3);
1276        while (!isPrime(n)) n++;
1277        return ffloor(phase * n) / n;
1278      }
1279      case WarpMode.BINARY: {
1280        let { b } = this._toBits(amt, 3);
1281        b = fround(b);
1282        const n = 1 << b;
1283        const idx = ffloor(phase * n);
1284        const ridx = bitReverse(idx, b);
1285        return ridx / n;
1286      }
1287      case WarpMode.BROWNIAN: {
1288        const disp = 0.25 * amt * brownian(64 * phase, 4);
1289        return frac(phase + disp);
1290      }
1291      case WarpMode.RECIPROCAL: {
1292        const g = 2 + 4 * amt;
1293        const num = phase * g;
1294        const den = phase + (1 - phase) * g;
1295        const y = den > 1e-12 ? num / den : 0;
1296        return clamp(y, 0, 1);
1297      }
1298      case WarpMode.WORMHOLE: {
1299        const gap = clamp(0.8 * amt, 0, 1);
1300        const a = 0.5 * (1 - gap);
1301        const b = 0.5 * (1 + gap);
1302        if (phase < a) return (phase / a) * 0.5;
1303        if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b));
1304        return 0.5;
1305      }
1306      case WarpMode.LOGISTIC: {
1307        let x = phase;
1308        const r = 3.6 + 0.4 * amt;
1309        const iters = 1 + fround(2 * amt);
1310        for (let i = 0; i < iters; i++) x = r * x * (1 - x);
1311        return clamp(x, 0, 1);
1312      }
1313      case WarpMode.SIGMOID: {
1314        const k = 1 + 10 * amt;
1315        const x = phase - 0.5;
1316        const y = 1 / (1 + Math.exp(-k * x));
1317        const y0 = 1 / (1 + Math.exp(0.5 * k));
1318        const y1 = 1 / (1 + Math.exp(-0.5 * k));
1319        return (y - y0) / (y1 - y0);
1320      }
1321      case WarpMode.FRACTAL: {
1322        const d = 0.5 * Math.sin(TWO_PI * phase) * amt;
1323        return frac(phase + d);
1324      }
1325      case WarpMode.FLIP: {
1326        return phase;
1327      }
1328      default:
1329        return phase;
1330    }
1331  }
1332
1333  _sampleFrame(frame, phase) {
1334    const len = frame.length;
1335    const pos = phase * len;
1336    let i = pos | 0;
1337    if (i >= len) i = 0; // fast wrap
1338    const frac = pos - i;
1339    const a = frame[i];
1340    let i1 = i + 1;
1341    if (i1 >= len) i1 = 0;
1342    const b = frame[i1];
1343    return a + (b - a) * frac;
1344  }
1345
1346  process(_inputs, outputs, parameters) {
1347    const begin = parameters.begin[0];
1348    const end = parameters.end[0];
1349    const beginDefined = begin >= 0;
1350    const endDefined = end >= 0;
1351    // We give a 0.5s grace period (for node pooling) before termination
1352    const shouldTerminate = endDefined && currentTime >= end + 0.5;
1353    const ended = endDefined && currentTime >= end;
1354    const notStarted = currentTime <= begin;
1355    if (shouldTerminate) {
1356      return false;
1357    } else if (ended || notStarted || !beginDefined) {
1358      return true;
1359    }
1360    const outL = outputs[0][0];
1361    const outR = outputs[0][1] || outputs[0][0];
1362    if (!this.table) {
1363      outL.fill(0);
1364      if (outR !== outL) outR.set(outL);
1365      return true;
1366    }
1367    const voices = parameters.voices[0]; // k-rate
1368    for (let i = 0; i < outL.length; i++) {
1369      const detune = pv(parameters.detune, i);
1370      const freqspread = pv(parameters.freqspread, i);
1371      const tablePos = clamp(pv(parameters.position, i), 0, 1);
1372      const idx = tablePos * (this.numFrames - 1);
1373      const fIdx = idx | 0;
1374      const interpT = idx - fIdx;
1375      const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
1376      const warpMode = pv(parameters.warpMode, i);
1377      const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
1378      const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
1379      const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
1380      const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
1381      let f = pv(parameters.frequency, i);
1382      f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
1383      const normalizer = 1 / Math.sqrt(voices);
1384      const detuner = getDetuner(voices, freqspread);
1385      for (let n = 0; n < voices; n++) {
1386        const isOdd = (n & 1) == 1;
1387        let gainL = gain1;
1388        let gainR = gain2;
1389        // invert right and left gain
1390        if (isOdd) {
1391          gainL = gain2;
1392          gainR = gain1;
1393        }
1394        const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
1395        const dPhase = fVoice * INVSR;
1396
1397        // warp phase then sample
1398        this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
1399        const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
1400        const s0 = this._sampleFrame(this.table[fIdx], ph);
1401        const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
1402        let s = lerp(s0, s1, interpT);
1403        if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
1404          s = -s;
1405        }
1406        outL[i] += s * gainL * normalizer;
1407        outR[i] += s * gainR * normalizer;
1408        this.phase[n] = frac(this.phase[n] + dPhase);
1409      }
1410    }
1411    return true;
1412  }
1413}
1414
1415registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
1416
1417class TransientProcessor extends AudioWorkletProcessor {
1418  static get parameterDescriptors() {
1419    return [];
1420  }
1421
1422  constructor(options) {
1423    super();
1424    this.gainCoeff = timeToCoeff(0.2);
1425    this.avgGain = 1;
1426    let {
1427      attackTime = 0.003,
1428      sustainTime = 0.08,
1429      attack = 0,
1430      sustain = 0,
1431      sensitivity = 0.1,
1432      mix = 1,
1433      begin = 0,
1434      end = 0,
1435    } = options.processorOptions;
1436    attackTime = clamp(attackTime, 0.0005, 0.05);
1437    sustainTime = clamp(sustainTime, 0.01, 0.5);
1438    this.attackCoeff = timeToCoeff(attackTime);
1439    this.sustainCoeff = timeToCoeff(sustainTime);
1440    this.attackAmt = clamp(attack, -1, 1);
1441    this.sustainAmt = clamp(sustain, -1, 1);
1442    this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1);
1443    this.mix = clamp(mix, 0, 1);
1444    this.begin = begin;
1445    this.end = end;
1446    this.attackEnv = new Float32Array(2); // assume stereo
1447    this.sustainEnv = new Float32Array(2);
1448  }
1449
1450  process(inputs, outputs, _params) {
1451    const input = inputs[0];
1452    const output = outputs[0];
1453    if (currentTime >= this.end) {
1454      return false;
1455    }
1456    if (currentTime <= this.begin) {
1457      return true;
1458    }
1459    const channels = input.length;
1460    if (channels > this.attackEnv.length) {
1461      this.attackEnv = new Float32Array(channels);
1462      this.sustainEnv = new Float32Array(channels);
1463    }
1464    let avgGain = this.avgGain;
1465    for (let ch = 0; ch < channels; ch++) {
1466      let attEnv = this.attackEnv[ch];
1467      let susEnv = this.sustainEnv[ch];
1468      for (let n = 0; n < blockSize; n++) {
1469        const sample = input[ch][n];
1470        const x = Math.abs(sample);
1471        attEnv = lerp(attEnv, x, this.attackCoeff);
1472        susEnv = lerp(susEnv, x, this.sustainCoeff);
1473        const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5);
1474        const attScale = peakiness > 0 ? peakiness : 0;
1475        const susScale = peakiness < 0 ? -peakiness : 0;
1476        const attackGain = dbToLin(this.attackAmt * attScale * 18);
1477        const sustainGain = dbToLin(this.sustainAmt * susScale * 36);
1478        const gain = clamp(attackGain * sustainGain, 0, 8);
1479        avgGain = lerp(avgGain, gain, this.gainCoeff);
1480        const makeup = avgGain > 1e-3 ? 1 / avgGain : 1;
1481        const wet = sample * gain * makeup;
1482        let y = lerp(sample, wet, this.mix);
1483        y /= 1 + Math.abs(y); // soft clip
1484        output[ch][n] = y;
1485      }
1486      this.attackEnv[ch] = attEnv;
1487      this.sustainEnv[ch] = susEnv;
1488    }
1489    this.avgGain = avgGain;
1490    return true;
1491  }
1492}
1493
1494registerProcessor('transient-processor', TransientProcessor);
1495
1496class GenericProcessor extends AudioWorkletProcessor {
1497  constructor() {
1498    super();
1499    this.playPos = 0;
1500    const channels = 16;
1501    this.outputs = new Array(channels).fill(0);
1502    this.sources = new Array(channels).fill(0);
1503    this.gateEnded = false;
1504    this.started = false;
1505    this.port.onmessage = (event) => {
1506      let {
1507        src,
1508        schema: { ugens, registers },
1509        start,
1510        gateEnd,
1511        end,
1512      } = event.data;
1513      this.start = start;
1514      this.gateEnd = gateEnd;
1515      this.end = end;
1516      this.registers = new Array(registers).fill(0);
1517      this.src = `o.fill(0); // reset outputs\n${src}`;
1518      this.nodes = [];
1519      for (let i = 0; i < ugens.length; i++) {
1520        const ugen = ugens[i];
1521        const nodeClass = UGENS.get(ugen.type);
1522        const node = new nodeClass(i, ugen, sampleRate);
1523        if (node.type === 'cc' && ugen.inputs?.[0]?.includes('strudel-gate')) {
1524          node.setValue(1);
1525          this.gateNode = node;
1526        }
1527        this.nodes[i] = node;
1528      }
1529      this.genSample = new Function(
1530        'time',
1531        'nodes',
1532        'input',
1533        'r', // registers
1534        'o', // outputs
1535        's', // sources
1536        this.src,
1537      );
1538    };
1539  }
1540  process(inputs, outputs) {
1541    const input = inputs[0]?.[0];
1542    if (currentTime >= this.end) {
1543      return false;
1544    } else if (this.genSample === undefined || currentTime < this.start) {
1545      // pending
1546      return true;
1547    }
1548    this.started = true;
1549    if (!this.gateEnded && currentTime > this.gateEnd) {
1550      this.gateNode?.setValue(0);
1551      this.gateEnded = true;
1552    }
1553    const output = outputs[0];
1554    const outL = output[0];
1555    const outR = output[1];
1556    for (let n = 0; n < blockSize; n++) {
1557      this.genSample(this.playPos, this.nodes, input ? input[n] : 0, this.registers, this.outputs, this.sources);
1558      const left = this.outputs[0];
1559      const right = this.outputs[1];
1560      // Spread to stereo if possible; else mixdown to mono
1561      if (outR) {
1562        outL[n] = left;
1563        outR[n] = right;
1564      } else {
1565        outL[n] = 0.5 * (left + right);
1566      }
1567      this.playPos += 1 / sampleRate;
1568    }
1569    return true;
1570  }
1571}
1572registerProcessor('generic-processor', GenericProcessor);
1573
1574// jevstrudel: the master limiter, the last stage before the speakers
1575// (superdoughoutput.mjs). A lookahead brickwall: the output's true peak
1576// (4× interpolated, as ITU-R BS.1770 measures it) stays at or under
1577// `ceilingDb`, and below that the gain is exactly 1, so a mix that never
1578// reaches the ceiling passes untouched, only delayed by the lookahead.
1579//
1580// Per sample: the true peak of the interval ahead (Lanczos-6 interpolation at
1581// ¼, ½ and ¾ between samples, linked across channels) gives the gain it
1582// needs; that is held for the lookahead, released exponentially, then
1583// averaged over the lookahead. The average of held values reaches the peak's
1584// gain exactly when the peak leaves the delay line, so there is no overshoot
1585// to measure: the guarantee is by construction, not by tuning. The attack is
1586// the lookahead (a linear ramp that long); the release is `release` seconds.
1587//
1588// A DynamicsCompressorNode was the other candidate. It is stereo only (its
1589// channelCount cannot exceed 2, so multichannel orbits would be downmixed),
1590// adds its own makeup gain (+0.57 dB at -1 dBFS and ratio 20, per the Web
1591// Audio spec), and can overshoot its threshold during its attack.
1592class LimiterProcessor extends AudioWorkletProcessor {
1593  constructor({ processorOptions = {} } = {}) {
1594    super();
1595    const { ceilingDb = -1, lookahead = 0.002, release = 0.1 } = processorOptions;
1596    this.ceiling = dbToLin(ceilingDb);
1597    this.release = timeToCoeff(release);
1598    const H = 6; // interpolation half-width: taps m-5 … m+6
1599    this.H = H;
1600    this.L = Math.max(1, Math.round(lookahead * sampleRate));
1601    // the interpolation kernels, one per fraction between samples
1602    const sinc = (u) => (u === 0 ? 1 : Math.sin(PI * u) / (PI * u));
1603    this.kernels = [0.25, 0.5, 0.75].map((f) =>
1604      Float64Array.from({ length: 2 * H }, (_, i) => {
1605        const u = f - (i - H + 1);
1606        return sinc(u) * sinc(u / H);
1607      }),
1608    );
1609    let size = 1;
1610    while (size < this.L + 2 * H + 4) size *= 2;
1611    this.size = size;
1612    this.mask = size - 1;
1613    this.hist = []; // per channel, the input's recent samples
1614    this.t = 0; // index of the newest input sample
1615    this.prevPeak = 0;
1616    this.required = new Float64Array(this.L).fill(1); // the last L required gains
1617    this.env = new Float64Array(this.L).fill(1); // the last L released gains
1618    this.envNow = 1;
1619    this.sum = this.L; // sum of env
1620  }
1621
1622  process(inputs, outputs) {
1623    const input = inputs[0] ?? [];
1624    const output = outputs[0];
1625    const channels = output.length;
1626    while (this.hist.length < channels) this.hist.push(new Float32Array(this.size));
1627    const { H, L, mask, hist, kernels, ceiling, release } = this;
1628    const frames = output[0].length;
1629    for (let n = 0; n < frames; n++) {
1630      const t = this.t++;
1631      for (let c = 0; c < channels; c++) hist[c][t & mask] = input[c]?.[n] ?? 0;
1632      // the true peak of [m, m+1), all of whose taps have arrived
1633      const m = t - H;
1634      let peak = 0;
1635      for (let c = 0; c < channels; c++) {
1636        const h = hist[c];
1637        const x = h[m & mask];
1638        let p = x < 0 ? -x : x;
1639        for (let k = 0; k < 3; k++) {
1640          const kernel = kernels[k];
1641          let y = 0;
1642          for (let i = 0; i < 2 * H; i++) y += h[(m - H + 1 + i) & mask] * kernel[i];
1643          if (y < 0) y = -y;
1644          if (y > p) p = y;
1645        }
1646        if (p > peak) peak = p;
1647      }
1648      // sample m bounds the intervals either side of it
1649      const worst = peak > this.prevPeak ? peak : this.prevPeak;
1650      this.prevPeak = peak;
1651      const slot = m % L < 0 ? (m % L) + L : m % L;
1652      this.required[slot] = worst > ceiling ? ceiling / worst : 1;
1653      // held over the lookahead
1654      let held = 1;
1655      for (let i = 0; i < L; i++) if (this.required[i] < held) held = this.required[i];
1656      // released, never above what is held
1657      const released = this.envNow + (1 - this.envNow) * release;
1658      this.envNow = released < held ? released : held;
1659      this.sum += this.envNow - this.env[slot];
1660      this.env[slot] = this.envNow;
1661      const gain = this.sum / L;
1662      // the sample whose peak this gain has fully ramped to
1663      const out = m - L + 1;
1664      for (let c = 0; c < channels; c++) output[c][n] = hist[c][out & mask] * gain;
1665    }
1666    return true;
1667  }
1668}
1669registerProcessor('limiter-processor', LimiterProcessor);