bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

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