transmission-processor.js (22508B)
1 /* eslint-disable no-undef */ 2 class XorShift32 { 3 constructor(seed) { 4 this.state = (seed >>> 0) || 0x12345678; 5 } 6 nextU32() { 7 let x = this.state >>> 0; 8 x ^= x << 13; 9 x ^= x >>> 17; 10 x ^= x << 5; 11 this.state = x >>> 0; 12 return this.state; 13 } 14 nextFloat() { 15 return (this.nextU32() >>> 0) / 0xffffffff; 16 } 17 nextSigned() { 18 return this.nextFloat() * 2 - 1; 19 } 20 } 21 function dbToLin(db) { 22 return Math.pow(10, db / 20); 23 } 24 function clamp(x, a, b) { 25 return Math.min(b, Math.max(a, x)); 26 } 27 function softClipTanh(x) { 28 return Math.tanh(x); 29 } 30 31 class TransmissionSatProcessor extends AudioWorkletProcessor { 32 static get parameterDescriptors() { 33 return [ 34 { name: "drive", defaultValue: 0.25, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 35 { name: "asym", defaultValue: 0.1, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 36 { name: "mix", defaultValue: 1, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 37 ]; 38 } 39 process(inputs, outputs, parameters) { 40 const input = inputs[0]; 41 const output = outputs[0]; 42 const out = output[0]; 43 const in0 = input?.[0]; 44 if (!out) return true; 45 const drive = parameters.drive[0] ?? 0.25; 46 const asym = parameters.asym[0] ?? 0.1; 47 const mix = parameters.mix[0] ?? 1; 48 const pre = 1 + drive * 10.5; 49 const bias = asym * 0.18; 50 const dryMix = 1 - clamp(mix, 0, 1); 51 const wetMix = clamp(mix, 0, 1); 52 for (let i = 0; i < out.length; i++) { 53 const x = in0 ? in0[i] : 0; 54 const y = softClipTanh((x + bias) * pre) - bias * 0.45; 55 out[i] = clamp(x * dryMix + y * wetMix, -1, 1); 56 } 57 return true; 58 } 59 } 60 class WalkieClickProcessor extends AudioWorkletProcessor { 61 static get parameterDescriptors() { 62 return [ 63 { name: "walkieEnable", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 64 { name: "thresholdDb", defaultValue: -45, minValue: -80, maxValue: -20, automationRate: "k-rate" }, 65 { name: "minSilenceMs", defaultValue: 220, minValue: 80, maxValue: 600, automationRate: "k-rate" }, 66 { name: "clickMs", defaultValue: 12, minValue: 5, maxValue: 200, automationRate: "k-rate" }, 67 { name: "clickLevel", defaultValue: 0.6, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 68 { name: "dispatchMode", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 69 ]; 70 } 71 constructor(options) { 72 super(); 73 const seed = (options?.processorOptions?.seed ?? 0xdecafbad) >>> 0; 74 this.prng = new XorShift32(seed); 75 this.rmsWindowSamples = Math.max(1, Math.floor(sampleRate * 0.01)); 76 this.ring = new Float32Array(this.rmsWindowSamples); 77 this.ringIndex = 0; 78 this.sumSquares = 0; 79 this.belowCount = 0; 80 this.inSilence = false; 81 this.clickRemaining = 0; 82 this.clickTotal = 0; 83 this.clickAmp = 0; 84 this.clickFreq = 1800; 85 this.clickPhase = 0; 86 this.noiseHpState = 0; 87 this.port.onmessage = (ev) => { 88 const msg = ev.data; 89 if (msg?.type === "reset") { 90 this.prng = new XorShift32((msg.seed ?? 0) >>> 0); 91 this.sumSquares = 0; 92 this.ring.fill(0); 93 this.ringIndex = 0; 94 this.belowCount = 0; 95 this.inSilence = false; 96 this.clickRemaining = 0; 97 this.noiseHpState = 0; 98 } 99 }; 100 } 101 _triggerClick(clickMs, clickLevel) { 102 this.clickTotal = Math.max(1, Math.floor((clickMs / 1000) * sampleRate)); 103 this.clickRemaining = this.clickTotal; 104 this.clickAmp = clickLevel * (0.55 + 0.55 * this.prng.nextFloat()); 105 this.clickFreq = 1300 + this.prng.nextFloat() * 1600; 106 this.clickPhase = this.prng.nextFloat() * Math.PI * 2; 107 } 108 process(inputs, outputs, parameters) { 109 const input = inputs[0]; 110 const output = outputs[0]; 111 const out = output[0]; 112 if (!out) return true; 113 const enable = (parameters.walkieEnable[0] ?? 0) >= 0.5; 114 const threshold = dbToLin(parameters.thresholdDb[0] ?? -45); 115 const minSilenceSamples = Math.max(1, Math.floor(((parameters.minSilenceMs[0] ?? 220) / 1000) * sampleRate)); 116 const dispatch = (parameters.dispatchMode[0] ?? 0) >= 0.5; 117 const clickMs = parameters.clickMs[0] ?? 12; 118 const clickLevel = parameters.clickLevel[0] ?? 0.6; 119 const inChans = input?.length ? input.length : 0; 120 const in0 = inChans > 0 ? input[0] : null; 121 const in1 = inChans > 1 ? input[1] : null; 122 for (let i = 0; i < out.length; i++) { 123 const dry = in0 ? (in1 ? 0.5 * (in0[i] + in1[i]) : in0[i]) : 0; 124 const old = this.ring[this.ringIndex]; 125 const x2 = dry * dry; 126 this.ring[this.ringIndex] = x2; 127 this.ringIndex = (this.ringIndex + 1) % this.rmsWindowSamples; 128 this.sumSquares += x2 - old; 129 const rms = Math.sqrt(Math.max(0, this.sumSquares / this.rmsWindowSamples)); 130 if (enable) { 131 if (rms < threshold) this.belowCount++; 132 else this.belowCount = 0; 133 const nowSilence = this.belowCount >= minSilenceSamples; 134 if (!this.inSilence && nowSilence) { 135 this.inSilence = true; 136 this._triggerClick(clickMs, clickLevel); 137 } else if (this.inSilence && rms >= threshold * 1.15) { 138 this.inSilence = false; 139 this.belowCount = 0; 140 this._triggerClick(clickMs, clickLevel); 141 } 142 } else { 143 this.inSilence = false; 144 this.belowCount = 0; 145 } 146 let y = dry; 147 if (this.clickRemaining > 0) { 148 const t = 1 - this.clickRemaining / this.clickTotal; 149 const n = this.prng.nextSigned() * 0.10; 150 const hp = (n - this.noiseHpState) + 0.995 * this.noiseHpState; 151 this.noiseHpState = n; 152 if (dispatch) { 153 // Dispatch-style two-tone beep (short "dee-doo"), still clipped and noisy. 154 const aHz = 1150 + this.prng.nextSigned() * 25; 155 const bHz = 820 + this.prng.nextSigned() * 18; 156 const split = 0.52; 157 const hz = t < split ? aHz : bHz; 158 const env = Math.sin(Math.min(1, t * 12) * Math.PI * 0.5) * Math.sin(Math.min(1, (1 - t) * 10) * Math.PI * 0.5); 159 this.clickPhase += (2 * Math.PI * hz) / sampleRate; 160 const s = Math.sin(this.clickPhase); 161 const beep = softClipTanh((s * 0.95 + hp * 0.35) * (this.clickAmp * 2.4)) * env; 162 y += beep; 163 } else { 164 // Click/squelch pop (very short transient). 165 const env = Math.exp(-t * 14); 166 const s = Math.sin(this.clickPhase); 167 this.clickPhase += (2 * Math.PI * this.clickFreq) / sampleRate; 168 const click = softClipTanh((s * 0.85 + hp) * (this.clickAmp * 3.2)) * env; 169 y += click; 170 } 171 this.clickRemaining--; 172 } 173 out[i] = clamp(y, -1, 1); 174 } 175 return true; 176 } 177 } 178 179 function clampFreq(hz) { 180 return clamp(hz, 40, sampleRate * 0.45); 181 } 182 183 class SvfBandpass { 184 constructor() { 185 this.ic1 = 0; 186 this.ic2 = 0; 187 } 188 reset() { 189 this.ic1 = 0; 190 this.ic2 = 0; 191 } 192 process(input, freqHz, q) { 193 const f = clampFreq(freqHz); 194 const g = Math.tan((Math.PI * f) / sampleRate); 195 const k = 1 / Math.max(0.08, q); 196 const v0 = input - this.ic2; 197 const v1 = (g * v0 + this.ic1) / (1 + g * (g + k)); 198 const v2 = this.ic2 + g * v1; 199 this.ic1 = 2 * v1 - this.ic1; 200 this.ic2 = 2 * v2 - this.ic2; 201 return v1; 202 } 203 } 204 205 class TuningNoiseProcessor extends AudioWorkletProcessor { 206 static get parameterDescriptors() { 207 return [ 208 { name: "enable", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 209 { name: "mode", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, // 0 edges, 1 search 210 { name: "source", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, // 0 synth, 1 sample 211 { name: "amount", defaultValue: 0.35, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 212 { name: "snippetMs", defaultValue: 140, minValue: 40, maxValue: 600, automationRate: "k-rate" }, 213 { name: "cutDepth", defaultValue: 0.55, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 214 ]; 215 } 216 217 constructor(options) { 218 super(); 219 const seed = (options?.processorOptions?.seed ?? 0x71c19e51) >>> 0; 220 this.prng = new XorShift32(seed); 221 const p = options?.processorOptions ?? {}; 222 const initialSample = p.sampleData; 223 const initialRate = p.sampleRate; 224 const leadEnd = Number(p.leadEnd); 225 const tailStart = Number(p.tailStart); 226 this.sampleData = 227 initialSample instanceof Float32Array 228 ? initialSample 229 : initialSample instanceof ArrayBuffer 230 ? new Float32Array(initialSample) 231 : null; 232 this.sampleRate = Number.isFinite(initialRate) ? Number(initialRate) : 0; 233 234 this.sampleIndex = 0; 235 this.leadEnd = Number.isFinite(leadEnd) ? Math.max(0, Math.floor(leadEnd)) : 0; 236 this.tailStart = Number.isFinite(tailStart) ? Math.max(0, Math.floor(tailStart)) : Number.POSITIVE_INFINITY; 237 238 this.eventRemaining = 0; 239 this.eventTotal = 0; 240 this.f0 = 1200; 241 this.f1 = 3200; 242 this.q = 6; 243 this.phase = 0; 244 this.playPos = 0; 245 this.playStep = 1; 246 this.svf = new SvfBandpass(); 247 248 this.port.onmessage = (ev) => { 249 const msg = ev.data; 250 if (msg?.type === "reset") { 251 this.prng = new XorShift32((msg.seed ?? 0) >>> 0); 252 this.sampleIndex = 0; 253 this.eventRemaining = 0; 254 this.eventTotal = 0; 255 this.phase = 0; 256 this.playPos = 0; 257 this.playStep = 1; 258 this.svf.reset(); 259 } else if (msg?.type === "setEdges") { 260 const leadEnd = Number(msg.leadEnd); 261 const tailStart = Number(msg.tailStart); 262 if (Number.isFinite(leadEnd)) this.leadEnd = Math.max(0, Math.floor(leadEnd)); 263 if (Number.isFinite(tailStart)) this.tailStart = Math.max(0, Math.floor(tailStart)); 264 } else if (msg?.type === "setSample") { 265 const sr = Number(msg.sampleRate); 266 const data = msg.data; 267 this.sampleData = data instanceof Float32Array ? data : null; 268 this.sampleRate = Number.isFinite(sr) ? sr : 0; 269 } 270 }; 271 } 272 273 _triggerEvent(snippetMs) { 274 this.eventTotal = Math.max(8, Math.floor((snippetMs / 1000) * sampleRate)); 275 this.eventRemaining = this.eventTotal; 276 const r = this.prng.nextFloat(); 277 const s = this.prng.nextFloat(); 278 this.f0 = 250 + r * 3500; 279 this.f1 = 600 + s * 7000; 280 if (this.prng.nextFloat() < 0.5) { 281 const tmp = this.f0; 282 this.f0 = this.f1; 283 this.f1 = tmp; 284 } 285 this.q = 4 + this.prng.nextFloat() * 8; 286 this.phase = this.prng.nextFloat() * Math.PI * 2; 287 this.svf.reset(); 288 } 289 290 _sampleAt(pos) { 291 const d = this.sampleData; 292 if (!d || d.length < 4) return 0; 293 let p = pos % d.length; 294 if (p < 0) p += d.length; 295 const i0 = p | 0; 296 const frac = p - i0; 297 const i1 = (i0 + 1) % d.length; 298 const a = d[i0]; 299 const b = d[i1]; 300 return a + (b - a) * frac; 301 } 302 303 _eventEnv(t) { 304 const a = Math.min(1, t / 0.08); 305 const b = Math.min(1, (1 - t) / 0.12); 306 return Math.sin(a * Math.PI * 0.5) * Math.sin(b * Math.PI * 0.5); 307 } 308 309 process(inputs, outputs, parameters) { 310 const input = inputs[0]; 311 const output = outputs[0]; 312 const out = output[0]; 313 const in0 = input?.[0]; 314 if (!out) return true; 315 316 const enable = (parameters.enable[0] ?? 0) >= 0.5; 317 const mode = (parameters.mode[0] ?? 0) >= 0.5 ? 1 : 0; 318 const srcMode = (parameters.source[0] ?? 0) >= 0.5 ? 1 : 0; 319 const amount = clamp(parameters.amount[0] ?? 0.35, 0, 1); 320 const snippetMs = parameters.snippetMs[0] ?? 140; 321 const cutDepth = clamp(parameters.cutDepth[0] ?? 0.55, 0, 1); 322 323 const ratePerSec = 0.08 + 5.5 * amount * amount; 324 const pPerSample = ratePerSec / sampleRate; 325 326 for (let i = 0; i < out.length; i++) { 327 const x = in0 ? in0[i] : 0; 328 329 if (enable && mode === 1 && this.eventRemaining <= 0 && this.prng.nextFloat() < pPerSample) { 330 this._triggerEvent(snippetMs); 331 } 332 if (enable && mode === 0 && this.eventRemaining <= 0) { 333 const inEdge = this.sampleIndex < this.leadEnd || this.sampleIndex >= this.tailStart; 334 if (inEdge) this._triggerEvent(snippetMs); 335 } 336 337 let y = x; 338 if (enable && this.eventRemaining > 0) { 339 const t = 1 - this.eventRemaining / this.eventTotal; 340 const env = this._eventEnv(t); 341 342 // If using a provided tuning sample, just cut it in (no extra resonant sweeping). 343 const chosen = srcMode === 1 && this.sampleData && this.sampleRate > 0 ? { sampleRate: this.sampleRate, data: this.sampleData } : null; 344 if (chosen && chosen.data instanceof Float32Array && Number.isFinite(chosen.sampleRate) && chosen.sampleRate > 0) { 345 if (this.eventRemaining === this.eventTotal) { 346 const start = Math.floor(this.prng.nextFloat() * chosen.data.length); 347 const ratio = chosen.sampleRate / sampleRate; 348 const varRatio = 0.92 + 0.16 * this.prng.nextFloat(); 349 this.playPos = start; 350 this.playStep = ratio * varRatio; 351 } 352 const prev = this.sampleData; 353 const prevSr = this.sampleRate; 354 this.sampleData = chosen.data; 355 this.sampleRate = chosen.sampleRate; 356 const s = this._sampleAt(this.playPos); 357 this.playPos += this.playStep; 358 this.sampleData = prev; 359 this.sampleRate = prevSr; 360 const hiss = this.prng.nextSigned() * (0.02 + 0.06 * amount); 361 const noise = softClipTanh((s * (0.9 + 1.8 * amount) + hiss) * 1.1) * env; 362 363 if (mode === 1) { 364 const duck = 1 - cutDepth * env; 365 y = x * duck + noise * (0.75 + 0.65 * amount); 366 } else { 367 y = x + noise * (0.55 + 0.75 * amount); 368 } 369 } else { 370 // Synth tuning: swept, bandpassed noise (intentionally resonant). 371 const f = this.f0 + (this.f1 - this.f0) * t; 372 const src = this.prng.nextSigned(); 373 const bp = this.svf.process(src, f, this.q); 374 const osc = Math.sin(this.phase) * 0.12; 375 this.phase += (2 * Math.PI * (40 + 70 * amount)) / sampleRate; 376 const noise = softClipTanh((bp * 2.2 + osc) * (0.6 + 1.6 * amount)) * env; 377 378 if (mode === 1) { 379 const duck = 1 - cutDepth * env; 380 y = x * duck + noise * (0.55 + 0.65 * amount); 381 } else { 382 y = x + noise * (0.45 + 0.75 * amount); 383 } 384 } 385 386 this.eventRemaining--; 387 } 388 389 out[i] = clamp(y, -1, 1); 390 this.sampleIndex++; 391 } 392 393 return true; 394 } 395 } 396 class TransmissionPostProcessor extends AudioWorkletProcessor { 397 static get parameterDescriptors() { 398 return [ 399 { name: "drive", defaultValue: 0.35, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 400 { name: "asym", defaultValue: 0.1, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 401 { name: "comp", defaultValue: 0.25, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 402 { name: "crush", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 403 { name: "badAmount", defaultValue: 0.25, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 404 { name: "wowDepth", defaultValue: 0.25, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 405 { name: "dropRate", defaultValue: 0.25, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 406 { name: "dropDepth", defaultValue: 0.35, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 407 { name: "crackle", defaultValue: 0.25, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 408 { name: "lfoRate", defaultValue: 0.7, minValue: 0.1, maxValue: 3, automationRate: "k-rate" }, 409 { name: "noise", defaultValue: 0.2, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 410 { name: "noiseColor", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 411 { name: "hiss", defaultValue: 0.2, minValue: 0, maxValue: 1, automationRate: "k-rate" }, 412 { name: "outGain", defaultValue: 0.9, minValue: 0, maxValue: 1.5, automationRate: "k-rate" }, 413 ]; 414 } 415 constructor(options) { 416 super(); 417 const seed = (options?.processorOptions?.seed ?? 0xdecafbad) >>> 0; 418 this.prng = new XorShift32(seed); 419 this.env = 0; 420 this.lfoPhase = 0; 421 this.dropoutRemaining = 0; 422 this.dropoutTotal = 0; 423 this.dropoutDepth = 1; 424 this.crackleRemaining = 0; 425 this.crackleTotal = 0; 426 this.p0 = 0; 427 this.p1 = 0; 428 this.p2 = 0; 429 this.p3 = 0; 430 this.p4 = 0; 431 this.p5 = 0; 432 this.p6 = 0; 433 this.prevNoise = 0; 434 this.crushHold = 0; 435 this.crushPhase = 0; 436 this.port.onmessage = (ev) => { 437 const msg = ev.data; 438 if (msg?.type === "reset") { 439 this.prng = new XorShift32((msg.seed ?? 0) >>> 0); 440 this.env = 0; 441 this.lfoPhase = 0; 442 this.dropoutRemaining = 0; 443 this.crackleRemaining = 0; 444 this.p0 = this.p1 = this.p2 = this.p3 = this.p4 = this.p5 = this.p6 = 0; 445 this.prevNoise = 0; 446 this.crushHold = 0; 447 this.crushPhase = 0; 448 } 449 }; 450 } 451 _pinkFromWhite(white) { 452 this.p0 = 0.99886 * this.p0 + white * 0.0555179; 453 this.p1 = 0.99332 * this.p1 + white * 0.0750759; 454 this.p2 = 0.969 * this.p2 + white * 0.153852; 455 this.p3 = 0.8665 * this.p3 + white * 0.3104856; 456 this.p4 = 0.55 * this.p4 + white * 0.5329522; 457 this.p5 = -0.7616 * this.p5 - white * 0.016898; 458 const pink = this.p0 + this.p1 + this.p2 + this.p3 + this.p4 + this.p5 + this.p6 + white * 0.5362; 459 this.p6 = white * 0.115926; 460 return pink * 0.11; 461 } 462 _maybeTriggerDropout(rateAmount, depthAmount) { 463 if (rateAmount <= 0) return; 464 if (this.dropoutRemaining > 0) return; 465 const ratePerSec = 0.15 + 1.9 * rateAmount * rateAmount; 466 const p = ratePerSec / sampleRate; 467 if (this.prng.nextFloat() < p) { 468 const ms = 18 + 140 * rateAmount; 469 this.dropoutTotal = Math.max(1, Math.floor((ms / 1000) * sampleRate)); 470 this.dropoutRemaining = this.dropoutTotal; 471 const minGain = 1 - clamp(depthAmount, 0, 1) * 0.95; 472 this.dropoutDepth = clamp(minGain * (0.78 + 0.22 * this.prng.nextFloat()), 0.02, 1); 473 } 474 } 475 _maybeTriggerCrackle(amount) { 476 if (amount <= 0) return; 477 if (this.crackleRemaining > 0) return; 478 const ratePerSec = 0.35 + 7.5 * amount * amount; 479 const p = ratePerSec / sampleRate; 480 if (this.prng.nextFloat() < p) { 481 const ms = 2 + 10 * amount; 482 this.crackleTotal = Math.max(1, Math.floor((ms / 1000) * sampleRate)); 483 this.crackleRemaining = this.crackleTotal; 484 } 485 } 486 process(inputs, outputs, parameters) { 487 const input = inputs[0]; 488 const output = outputs[0]; 489 const out = output[0]; 490 const in0 = input?.[0]; 491 if (!out) return true; 492 const drive = parameters.drive[0] ?? 0.35; 493 const asym = parameters.asym[0] ?? 0.1; 494 const comp = parameters.comp[0] ?? 0.25; 495 const crush = parameters.crush[0] ?? 0; 496 const badAmount = parameters.badAmount[0] ?? 0.25; 497 const wowCtrl = parameters.wowDepth[0] ?? badAmount; 498 const dropRateCtrl = parameters.dropRate[0] ?? badAmount; 499 const dropDepthCtrl = parameters.dropDepth[0] ?? badAmount; 500 const crackleCtrl = parameters.crackle[0] ?? badAmount; 501 const lfoRate = parameters.lfoRate[0] ?? 0.7; 502 const noise = parameters.noise[0] ?? 0.2; 503 const noiseColor = parameters.noiseColor[0] ?? 0; 504 const hiss = parameters.hiss[0] ?? 0.2; 505 const outGain = parameters.outGain[0] ?? 0.9; 506 const pre = 1 + drive * 12; 507 const bias = asym * 0.22; 508 const compAmt = comp * 0.75; 509 const thr = 0.18 + (1 - drive) * 0.16; 510 const attack = Math.exp(-1 / (sampleRate * (0.003 + 0.01 * (1 - drive)))); 511 const release = Math.exp(-1 / (sampleRate * (0.06 + 0.16 * (1 - drive)))); 512 const wowDepth = clamp(wowCtrl, 0, 1) * 0.45; 513 const dropRate = clamp(dropRateCtrl, 0, 1); 514 const dropDepth = clamp(dropDepthCtrl, 0, 1); 515 const crackleAmount = clamp(crackleCtrl, 0, 1); 516 517 const crushAmt = clamp(crush, 0, 1); 518 const bits = Math.round(16 - crushAmt * 12); 519 const quant = Math.pow(2, Math.max(1, bits - 1)); 520 const downsample = Math.max(1, Math.round(1 + crushAmt * 15)); 521 const noiseLevel = noise * 0.12; 522 const pinkMix = clamp(noiseColor, 0, 1); 523 const hissAmt = hiss * 0.6; 524 for (let i = 0; i < out.length; i++) { 525 const xIn = in0 ? in0[i] : 0; 526 let x = (xIn + bias) * pre; 527 const a = Math.abs(x); 528 const coeff = a > this.env ? attack : release; 529 this.env = a + coeff * (this.env - a); 530 let g = 1; 531 if (this.env > thr) g = 1 / (1 + compAmt * (this.env - thr) * 4.2); 532 x = softClipTanh(x * g) - bias * 0.5; 533 534 if (crushAmt > 0.0001) { 535 if (this.crushPhase === 0) { 536 this.crushHold = clamp(Math.round(x * quant) / quant, -1, 1); 537 } 538 this.crushPhase = (this.crushPhase + 1) % downsample; 539 x = this.crushHold; 540 } else { 541 this.crushPhase = 0; 542 this.crushHold = x; 543 } 544 545 this._maybeTriggerDropout(dropRate, dropDepth); 546 this._maybeTriggerCrackle(crackleAmount); 547 this.lfoPhase += (2 * Math.PI * lfoRate) / sampleRate; 548 if (this.lfoPhase > Math.PI * 2) this.lfoPhase -= Math.PI * 2; 549 const wow = 1 - wowDepth * (0.5 + 0.5 * Math.sin(this.lfoPhase)); 550 let drop = 1; 551 if (this.dropoutRemaining > 0) { 552 const t = 1 - this.dropoutRemaining / this.dropoutTotal; 553 const fade = t < 0.2 ? t / 0.2 : t > 0.85 ? (1 - t) / 0.15 : 1; 554 drop = 1 - (1 - this.dropoutDepth) * fade; 555 this.dropoutRemaining--; 556 } 557 let crackle = 0; 558 if (this.crackleRemaining > 0) { 559 const t = 1 - this.crackleRemaining / this.crackleTotal; 560 const env = (t < 0.15 ? t / 0.15 : 1) * (t > 0.7 ? (1 - t) / 0.3 : 1); 561 crackle = this.prng.nextSigned() * (0.10 + 0.30 * crackleAmount) * env; 562 this.crackleRemaining--; 563 } 564 const wn = this.prng.nextSigned(); 565 const pn = this._pinkFromWhite(wn); 566 const n = (1 - pinkMix) * wn + pinkMix * pn; 567 const hissHp = n - this.prevNoise; 568 this.prevNoise = n; 569 const noiseOut = n * noiseLevel + hissHp * (noiseLevel * hissAmt); 570 let y = x * wow * drop + crackle + noiseOut; 571 y *= outGain; 572 out[i] = clamp(y, -1, 1); 573 } 574 return true; 575 } 576 } 577 registerProcessor("walkie-click", WalkieClickProcessor); 578 registerProcessor("transmission-sat", TransmissionSatProcessor); 579 registerProcessor("transmission-post", TransmissionPostProcessor); 580 registerProcessor("tuning-noise", TuningNoiseProcessor);