coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
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);
Fast integer ops for non-negative values
Optimized per-voice detuner which precomputes constants
Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing 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}
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};
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);
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);
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);
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}
578registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
581const BUFFERED_BLOCK_SIZE = 2048;
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 }
Apply Hann window in-place @tags internals
Compute squared magnitudes for peak finding @tags internals
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 }
Find peaks in spectrum magnitudes @tags internals
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 }
Shift peaks and regions of influence by pitchFactor into new specturm @tags internals
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}
748registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
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);
Feedback coefficient control
Waveform generation (half-Tomisawa oscillators)
First half-Tomisawa generator
Second half-Tomisawa generator (with phase offset for pulse width)
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);
BYTE BEATS @tags internals
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};
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}
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 }
Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
Optimize code like eval(unescape(escapeXXXX.replace(/u(..)/g,"$1%")))
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);
jevstrudel: the master limiter, the last stage before the speakers
(superdoughoutput.mjs). A lookahead brickwall: the output's true peak
(4× interpolated, as ITU-R BS.1770 measures it) stays at or under
ceilingDb, and below that the gain is exactly 1, so a mix that never
reaches the ceiling passes untouched, only delayed by the lookahead.
Per sample: the true peak of the interval ahead (Lanczos-6 interpolation at
¼, ½ and ¾ between samples, linked across channels) gives the gain it
needs; that is held for the lookahead, released exponentially, then
averaged over the lookahead. The average of held values reaches the peak's
gain exactly when the peak leaves the delay line, so there is no overshoot
to measure: the guarantee is by construction, not by tuning. The attack is
the lookahead (a linear ramp that long); the release is release seconds.
A DynamicsCompressorNode was the other candidate. It is stereo only (its channelCount cannot exceed 2, so multichannel orbits would be downmixed), adds its own makeup gain (+0.57 dB at -1 dBFS and ratio 20, per the Web 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);