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);