<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Audio Distortion Tool</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
      <h1>Audio Distortion (Anti-Moderation) Tool</h1>
    <input type="file" id="audioFile" accept="audio/*" />

    <form id="audioControls">
      <label
        >Distortion Type:
        <select id="distType">
          <option value="none">None</option>
          <option value="soft">Soft Clipping</option>
          <option value="hard">Hard Clipping</option>
          <option value="bitcrush">Bitcrusher</option>
        </select>
      </label>

      <div class="slider-group">
        <span class="slider-label">Distortion Amount:</span
        ><input
          id="distAmount"
          type="range"
          min="0"
          max="1000"
          value="400"
        /><span id="distAmountVal" class="slider-value">400</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Bit Depth:</span
        ><input
          id="bitDepth"
          type="range"
          min="8"
          max="32"
          value="16"
          step="8"
        /><span id="bitDepthVal" class="slider-value">16</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Highpass Freq (Hz):</span
        ><input id="hpFreq" type="range" min="20" max="2000" value="100" /><span
          id="hpFreqVal"
          class="slider-value"
          >100</span
        >
      </div>
      <div class="slider-group">
        <span class="slider-label">Notch1 Freq (Hz):</span
        ><input
          id="notch1Freq"
          type="range"
          min="100"
          max="8000"
          value="500"
        /><span id="notch1FreqVal" class="slider-value">500</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Notch1 Q:</span
        ><input
          id="notch1Q"
          type="range"
          min="0.1"
          max="20"
          value="1"
          step="0.1"
        /><span id="notch1QVal" class="slider-value">1.0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Notch2 Freq (Hz):</span
        ><input
          id="notch2Freq"
          type="range"
          min="100"
          max="8000"
          value="2000"
        /><span id="notch2FreqVal" class="slider-value">2000</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Notch2 Q:</span
        ><input
          id="notch2Q"
          type="range"
          min="0.1"
          max="20"
          value="1"
          step="0.1"
        /><span id="notch2QVal" class="slider-value">1.0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Mid Gain:</span
        ><input
          id="midGain"
          type="range"
          min="0"
          max="2"
          value="1"
          step="0.01"
        /><span id="midGainVal" class="slider-value">1.00</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Side Gain:</span
        ><input
          id="sideGain"
          type="range"
          min="0"
          max="2"
          value="1"
          step="0.01"
        /><span id="sideGainVal" class="slider-value">1.00</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Mid EQ Freq (Hz):</span
        ><input
          id="midEqFreq"
          type="range"
          min="100"
          max="8000"
          value="1000"
        /><span id="midEqFreqVal" class="slider-value">1000</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Mid EQ Gain (dB):</span
        ><input id="midEqGain" type="range" min="-24" max="24" value="0" /><span
          id="midEqGainVal"
          class="slider-value"
          >0</span
        >
      </div>
      <div class="slider-group">
        <span class="slider-label">Side EQ Freq (Hz):</span
        ><input
          id="sideEqFreq"
          type="range"
          min="100"
          max="8000"
          value="1000"
        /><span id="sideEqFreqVal" class="slider-value">1000</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Side EQ Gain (dB):</span
        ><input
          id="sideEqGain"
          type="range"
          min="-24"
          max="24"
          value="0"
        /><span id="sideEqGainVal" class="slider-value">0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Formant1 Freq (Hz):</span
        ><input
          id="formant1Freq"
          type="range"
          min="200"
          max="4000"
          value="800"
        /><span id="formant1FreqVal" class="slider-value">800</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Formant1 Q:</span
        ><input
          id="formant1Q"
          type="range"
          min="0.1"
          max="20"
          value="1"
          step="0.1"
        /><span id="formant1QVal" class="slider-value">1.0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Formant1 Gain (dB):</span
        ><input
          id="formant1Gain"
          type="range"
          min="-24"
          max="24"
          value="0"
        /><span id="formant1GainVal" class="slider-value">0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Formant2 Freq (Hz):</span
        ><input
          id="formant2Freq"
          type="range"
          min="200"
          max="4000"
          value="1600"
        /><span id="formant2FreqVal" class="slider-value">1600</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Formant2 Q:</span
        ><input
          id="formant2Q"
          type="range"
          min="0.1"
          max="20"
          value="1"
          step="0.1"
        /><span id="formant2QVal" class="slider-value">1.0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Formant2 Gain (dB):</span
        ><input
          id="formant2Gain"
          type="range"
          min="-24"
          max="24"
          value="0"
        /><span id="formant2GainVal" class="slider-value">0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Comp Threshold (dB):</span
        ><input
          id="compThreshold"
          type="range"
          min="-60"
          max="0"
          value="-24"
        /><span id="compThresholdVal" class="slider-value">-24</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Comp Ratio:</span
        ><input id="compRatio" type="range" min="1" max="20" value="4" /><span
          id="compRatioVal"
          class="slider-value"
          >4</span
        >
      </div>
      <div class="slider-group">
        <span class="slider-label">Comp Attack (s):</span
        ><input
          id="compAttack"
          type="range"
          min="0.001"
          max="1"
          value="0.003"
          step="0.001"
        /><span id="compAttackVal" class="slider-value">0.003</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Comp Release (s):</span
        ><input
          id="compRelease"
          type="range"
          min="0.01"
          max="1"
          value="0.25"
          step="0.01"
        /><span id="compReleaseVal" class="slider-value">0.25</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Volume:</span>
        <input id="volume" type="range" min="0" max="2" value="1" step="0.01" />
        <span id="volumeVal" class="slider-value">1.00</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Bass (Low-Shelf Gain dB):</span>
        <input id="bassGain" type="range" min="-24" max="24" value="0" />
        <span id="bassGainVal" class="slider-value">0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Treble (High-Shelf Gain dB):</span>
        <input id="trebleGain" type="range" min="-24" max="24" value="0" />
        <span id="trebleGainVal" class="slider-value">0</span>
      </div>
      <div class="slider-group">
        <span class="slider-label">Low-Pass Freq (Hz):</span>
        <input id="lpFreq" type="range" min="20" max="20000" value="20000" />
        <span id="lpFreqVal" class="slider-value">20000</span>
      </div>
    </form>

    <button id="process">Process Audio</button>
    <audio id="player" controls></audio>
    <div id="fileSize"></div>
    <div id="recommendation"></div>
    <hr />
    <input id="presetName" placeholder="Preset name" />
    <button id="savePreset">Save Preset</button>
    <select id="presetList"></select>
    <button id="loadPreset">Load Preset</button>
    <button id="deletePreset">Delete Preset</button>

    <canvas id="spectrogramCanvas"></canvas>

    <script type="module" src="src/index.js"></script>
  </body>
</html>
{
  "copyright": {
    "bitDepth": "8",
    "distAmount": "40",
    "hpFreq": "80",
    "notch1Freq": "1200",
    "notch1Q": "6",
    "notch2Freq": "3000",
    "notch2Q": "6",
    "midGain": "0.2",
    "sideGain": "0.8",
    "midEqFreq": "1500",
    "midEqGain": "-8",
    "sideEqFreq": "5000",
    "sideEqGain": "-4",
    "formant1Freq": "1000",
    "formant1Q": "4",
    "formant1Gain": "0",
    "formant2Freq": "2200",
    "formant2Q": "4",
    "formant2Gain": "0",
    "compThreshold": "-30",
    "compRatio": "6",
    "compAttack": "0.02",
    "compRelease": "0.15"
  },
  "swear": {
    "bitDepth": "8",
    "distAmount": "100",
    "hpFreq": "100",
    "notch1Freq": "800",
    "notch1Q": "4",
    "notch2Freq": "2000",
    "notch2Q": "4",
    "midGain": "0.15",
    "sideGain": "1.2",
    "midEqFreq": "1000",
    "midEqGain": "-12",
    "sideEqFreq": "6000",
    "sideEqGain": "-8",
    "formant1Freq": "1200",
    "formant1Q": "6",
    "formant1Gain": "-8",
    "formant2Freq": "2400",
    "formant2Q": "6",
    "formant2Gain": "-8",
    "compThreshold": "-25",
    "compRatio": "10",
    "compAttack": "0.005",
    "compRelease": "0.1"
  },
  "scvd": {
    "bitDepth": 16,
    "distAmount": 20,
    "hpFreq": 80,
    "notch1Freq": 1000,
    "notch1Q": 4,
    "notch2Freq": 3000,
    "notch2Q": 5,
    "midGain": 0.5,
    "sideGain": 1.5,
    "midEqFreq": 1800,
    "midEqGain": -8,
    "sideEqFreq": 8000,
    "sideEqGain": 4,
    "formant1Freq": 1200,
    "formant1Q": 1.2,
    "formant1Gain": -4,
    "formant2Freq": 2500,
    "formant2Q": 1.5,
    "formant2Gain": -3,
    "compThreshold": -18,
    "compRatio": 4,
    "compAttack": 0.003,
    "compRelease": 0.25
  }

}
body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #f9f9f9;
      max-width: 900px;
      margin: auto;
      padding: 2rem;
      color: #222;
    }

    h1 {
      text-align: center;
      margin-bottom: 2rem;
    }

    input[type="file"] {
      display: block;
      margin: 0 auto 1.5rem auto;
    }

    form {
      background-color: #fff;
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 1.5rem;
      box-shadow: 0 2px 6px rgba(0,0,0,0.05);
    }

    .slider-group {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 1rem;
      gap: 1rem;
    }

    .slider-label {
      flex: 1;
      font-weight: 500;
    }

    .slider-value {
      width: 50px;
      text-align: right;
      font-size: 0.9rem;
      color: #333;
    }

    input[type="range"] {
      flex: 2;
    }

    label select {
      width: 100%;
      margin-top: 0.5rem;
    }

    label {
      display: block;
      margin-bottom: 1.5rem;
      font-weight: 500;
    }

    button {
      background-color: #007BFF;
      color: white;
      padding: 0.6rem 1.2rem;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      margin-top: 1rem;
      margin-right: 0.5rem;
      transition: background 0.2s ease;
    }

    button:hover {
      background-color: #0056b3;
    }

    #player {
      display: block;
      margin: 2rem auto 1rem auto;
      width: 100%;
    }

    #fileSize, #recommendation {
      margin-top: 0.5rem;
      font-size: 0.95rem;
      color: #444;
    }

    hr {
      margin: 2rem 0;
    }

    #presetName {
      padding: 0.4rem;
      font-size: 1rem;
      margin-right: 0.5rem;
      width: calc(100% - 12rem);
    }

    #presetList {
      padding: 0.4rem;
      font-size: 1rem;
      margin-right: 0.5rem;
    }

    #spectrogramCanvas {
      width: 100%;
      height: 300px;
      background-color: black;
      display: block;
      margin-top: 20px;
    }

    @media (max-width: 600px) {
      .slider-group {
        flex-direction: column;
        align-items: flex-start;
      }

      input[type="range"] {
        width: 100%;
      }

      .slider-label,
      .slider-value {
        width: 100%;
        text-align: left;
      }

      #presetName {
        width: 100%;
        margin-bottom: 0.5rem;
      }

      #presetList {
        width: 100%;
        margin-bottom: 0.5rem;
      }

      button {
        width: 100%;
        margin-bottom: 0.5rem;
      }
import { initializeUI } from './ui/uiController.js';

let presets = {};

fetch('presets.json')
  .then(res => res.json())
  .then(data => { presets = data; })
  .catch(() => { presets = {}; })
  .then(() => initializeUI(presets));
export { bufferToWav } from './waveform/bufferToWav.js';
export function setupSpectrogram(player, canvas) {
  const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  const source = audioCtx.createMediaElementSource(player);
  const analyser = audioCtx.createAnalyser();
  analyser.fftSize = 512;
  source.connect(analyser);
  analyser.connect(audioCtx.destination);

  const bufferLength = analyser.frequencyBinCount;
  const spectHeight = 256;
  const waveHeight = 64;
  const axisWidth = 50;
  canvas.width = bufferLength + axisWidth;
  canvas.height = spectHeight + waveHeight;
  const ctx = canvas.getContext("2d");
  const freqLabels = 5;
  const sampleRate = audioCtx.sampleRate;

  ctx.font = "12px sans-serif";
  ctx.textAlign = 'right';
  ctx.textBaseline = 'middle';

  function drawAxis() {
    // Clear axis background
    ctx.fillStyle = '#fff';
    ctx.fillRect(0, 0, axisWidth, spectHeight);
    // Draw labels
    ctx.fillStyle = '#000';
    for (let i = 0; i <= freqLabels; i++) {
      const y = spectHeight - (i * spectHeight / freqLabels);
      const freq = Math.round(i * (sampleRate / 2) / freqLabels);
      ctx.fillText(freq + ' Hz', axisWidth - 5, y);
    }
  }

  let x = axisWidth;
  function draw() {
    if (player.paused) return;
    drawAxis();
    const freqData = new Uint8Array(bufferLength);
    analyser.getByteFrequencyData(freqData);
    let sum = 0;
    for (let i = 0; i < bufferLength; i++) {
      const v = freqData[i];
      sum += v;
      const norm = v / 255;
      const hue = (1 - norm) * 240;
      ctx.fillStyle = `hsl(${hue},100%,50%)`;
      const y = spectHeight - (i * spectHeight / bufferLength);
      const h = spectHeight / bufferLength + 1;
      ctx.fillRect(x, y - h/2, 1, h);
    }
    const vol = sum / bufferLength;
    const volHeight = (vol / 255) * waveHeight;
    ctx.fillStyle = '#444';
    ctx.fillRect(x, spectHeight + (waveHeight - volHeight), 1, volHeight);

    x++;
    if (x >= canvas.width) {
      x = axisWidth;
      ctx.clearRect(axisWidth, 0, canvas.width - axisWidth, canvas.height);
    }
    requestAnimationFrame(draw);
  }

  player.addEventListener("play", () => {
    if (audioCtx.state === "suspended") audioCtx.resume();
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    x = axisWidth;
    draw();
  });
}
// Converts an AudioBuffer to a WAV-formatted ArrayBuffer

import { writeString } from './writeString.js';

export function bufferToWav(buffer, bitDepth) {
  const numChannels = buffer.numberOfChannels;
  const bytesPerSample = bitDepth / 8;
  const dataLength = buffer.length * numChannels * bytesPerSample;
  const view = new DataView(new ArrayBuffer(44 + dataLength));

  writeString(view, 0, "RIFF");
  view.setUint32(4, 36 + dataLength, true);
  writeString(view, 8, "WAVE");
  writeString(view, 12, "fmt ");
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, numChannels, true);
  view.setUint32(24, buffer.sampleRate, true);
  view.setUint32(28, buffer.sampleRate * numChannels * bytesPerSample, true);
  view.setUint16(32, numChannels * bytesPerSample, true);
  view.setUint16(34, bitDepth, true);
  writeString(view, 36, "data");
  view.setUint32(40, dataLength, true);

  let offset = 44;
  for (let i = 0; i < buffer.length; i++) {
    for (let ch = 0; ch < numChannels; ch++) {
      const sample = Math.max(-1, Math.min(1, buffer.getChannelData(ch)[i]));
      if (bitDepth === 8) {
        view.setUint8(offset, Math.round((sample + 1) * 127.5));
        offset += 1;
      } else {
        view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
        offset += 2;
      }
    }
  }

  return view.buffer;
}
// Writes a string into a DataView at a specified offset

export function writeString(view, offset, str) {
  for (let i = 0; i < str.length; i++) {
    view.setUint8(offset + i, str.charCodeAt(i));
  }
}
import { setupSliderLabels } from './elements/setupSliderLabels.js';
import { setupPresetHandlers } from './handlers/presetHandlers.js';
import { setupFileHandlers } from './handlers/fileHandlers.js';
import { setupProcessHandler } from './handlers/processHandler.js';

export function initializeUI(presets = {}) {
  const fileInput = document.getElementById("audioFile");
  const processBtn = document.getElementById("process");
  const player = document.getElementById("player");
  const presetName = document.getElementById("presetName");
  const savePreset = document.getElementById("savePreset");
  const presetList = document.getElementById("presetList");
  const loadPreset = document.getElementById("loadPreset");
  const deletePreset = document.getElementById("deletePreset");

  setupSliderLabels();
  setupPresetHandlers(presetName, savePreset, presetList, loadPreset, deletePreset);
  const bufferRef = setupFileHandlers(fileInput);
  setupProcessHandler(processBtn, bufferRef, player);
}
// Handles file selection and stores the arrayBuffer reference

export function setupFileHandlers(fileInput) {
  const ref = { buffer: null };

  fileInput.addEventListener("change", async () => {
    const file = fileInput.files[0];
    if (file) {
      ref.buffer = await file.arrayBuffer();
    }
  });

  return ref;
}
// Handles saving, loading, and deleting audio processing presets

import { getAllInputs, setAllInputs, saveAllPresets, loadAllPresets } from '../../presets/presetManager.js';

function refreshPresetList(presetList) {
  const store = loadAllPresets();
  presetList.innerHTML = "";
  Object.keys(store).forEach(name => {
    const opt = document.createElement("option");
    opt.value = name;
    opt.textContent = name;
    presetList.appendChild(opt);
  });
}

export function setupPresetHandlers(presetName, savePreset, presetList, loadPreset, deletePreset) {
  savePreset.addEventListener("click", () => {
    const name = presetName.value.trim();
    if (!name) return;
    const store = loadAllPresets();
    store[name] = getAllInputs();
    saveAllPresets(store);
    refreshPresetList(presetList);
    presetName.value = "";
  });

  loadPreset.addEventListener("click", () => {
    const name = presetList.value;
    const store = loadAllPresets();
    if (store[name]) setAllInputs(store[name]);
  });

  deletePreset.addEventListener("click", () => {
    const name = presetList.value;
    const store = loadAllPresets();
    delete store[name];
    saveAllPresets(store);
    refreshPresetList(presetList);
  });

  refreshPresetList(presetList);
}
import { getAllInputs } from '../../presets/presetManager.js';
import { processAudio } from '../../audio/audioProcessor.js';
import { setupSpectrogram } from '../../utils/spectrogram.js';

export function setupProcessHandler(processBtn, bufferRef, player) {
  processBtn.addEventListener("click", async () => {
    if (!bufferRef.buffer) return;
    processBtn.disabled = true;
    const params = getAllInputs();
    const { blob, size } = await processAudio(bufferRef.buffer, params);
    bufferRef.buffer = await blob.arrayBuffer();
    player.src = URL.createObjectURL(blob);
    document.getElementById("fileSize").innerText = `Processed file size: ${size} bytes`;
    document.getElementById("recommendation").innerText =
      size >= 20 * 1024 * 1024
        ? "File ≥ 20 MB which can't be uploaded to Roblox. Try lower bit depth or sample rate."
        : "";
    const canvas = document.getElementById("spectrogramCanvas");
    setupSpectrogram(player, canvas);
    processBtn.disabled = false;
  });
}
// Initializes slider label values and syncs them with the slider input

export function setupSliderLabels() {
  const sliders = document.querySelectorAll("input[type=range]");
  sliders.forEach(slider => {
    const valEl = document.getElementById(slider.id + "Val");
    if (valEl) {
      slider.addEventListener("input", () => {
        valEl.textContent = slider.value;
      });
      valEl.textContent = slider.value;
    }
  });
}
export function getAllInputs() {
  const keys = [
    'distType',
    'distAmount',
    'bitDepth',
    'hpFreq',
    'notch1Freq',
    'notch1Q',
    'notch2Freq',
    'notch2Q',
    'midGain',
    'sideGain',
    'midEqFreq',
    'midEqGain',
    'sideEqFreq',
    'sideEqGain',
    'formant1Freq',
    'formant1Q',
    'formant1Gain',
    'formant2Freq',
    'formant2Q',
    'formant2Gain',
    'compThreshold',
    'compRatio',
    'compAttack',
    'compRelease',
    'volume',
    'bassGain',
    'trebleGain',
    'lpFreq'
  ];
  const o = {};
  keys.forEach(k => {
    o[k] = document.getElementById(k).value;
  });
  return o;
}

export function setAllInputs(o) {
  Object.keys(o).forEach(k => {
    const el = document.getElementById(k);
    if (el) el.value = o[k];
    const valEl = document.getElementById(k + 'Val');
    if (valEl) valEl.textContent = o[k];
  });
}

export function saveAllPresets(store) {
  localStorage.setItem('audioPresets', JSON.stringify(store));
}

export function loadAllPresets() {
  const raw = localStorage.getItem('audioPresets');
  return raw ? JSON.parse(raw) : {};
}
export function makeBitcrusher(ctx, bits = 4) {
  const node = ctx.createScriptProcessor(4096, 1, 1);
  const step = Math.pow(0.5, bits);
  node.onaudioprocess = function(e) {
    const input = e.inputBuffer.getChannelData(0);
    const output = e.outputBuffer.getChannelData(0);
    for (let i = 0; i < input.length; i++) {
      output[i] = Math.round(input[i] / step) * step;
    }
  };
  return node;
}
export function makeHardClippingCurve(amount) {
  const n_samples = 44100;
  const curve = new Float32Array(n_samples);
  const threshold = 1 - (amount / 1000);
  for (let i = 0; i < n_samples; ++i) {
    const x = (i * 2) / n_samples - 1;
    curve[i] = Math.max(-threshold, Math.min(threshold, x));
  }
  return curve;
}
export function makeDistortionCurve(amount) {
  const k = typeof amount === "number" ? amount : 50;
  const n_samples = 44100;
  const curve = new Float32Array(n_samples);
  const deg = Math.PI / 180;
  for (let i = 0; i < n_samples; ++i) {
    const x = (i * 2) / n_samples - 1;
    curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
  }
  return curve;
}
export { makeDistortionCurve } from './distortion/softClip.js';
export { makeHardClippingCurve } from './distortion/hardClip.js';
export { makeBitcrusher } from './distortion/bitcrush.js';
import { makeDistortionCurve, makeHardClippingCurve, makeBitcrusher } from './distortionUtils.js';
import { bufferToWav } from '../utils/fileConverter.js';

export async function processAudio(arrayBuffer, params) {
  const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  const decoded = await audioCtx.decodeAudioData(arrayBuffer);
  const oCtx = new OfflineAudioContext(
    decoded.numberOfChannels,
    decoded.length,
    decoded.sampleRate
  );
  const src = oCtx.createBufferSource();
  src.buffer = decoded;

  const volumeNode = oCtx.createGain();
  volumeNode.gain.value = +params.volume;

  const bass = oCtx.createBiquadFilter();
  bass.type = 'lowshelf';
  bass.frequency.value = 200;
  bass.gain.value = +params.bassGain;

  const treble = oCtx.createBiquadFilter();
  treble.type = 'highshelf';
  treble.frequency.value = 3000;
  treble.gain.value = +params.trebleGain;

  const lp = oCtx.createBiquadFilter();
  lp.type = 'lowpass';
  lp.frequency.value = +params.lpFreq;

  const hp = oCtx.createBiquadFilter();
  hp.type = 'highpass';
  hp.frequency.value = +params.hpFreq;

  const notch1 = oCtx.createBiquadFilter();
  notch1.type = 'notch';
  notch1.frequency.value = +params.notch1Freq;
  notch1.Q.value = +params.notch1Q;

  const notch2 = oCtx.createBiquadFilter();
  notch2.type = 'notch';
  notch2.frequency.value = +params.notch2Freq;
  notch2.Q.value = +params.notch2Q;

  const inverter = oCtx.createGain();
  inverter.gain.value = -1;

  const splitter = oCtx.createChannelSplitter(2);
  const merger = oCtx.createChannelMerger(2);

  const midGain = oCtx.createGain();
  midGain.gain.value = +params.midGain;
  const sideGain = oCtx.createGain();
  sideGain.gain.value = +params.sideGain;

  const midEQ = oCtx.createBiquadFilter();
  midEQ.type = 'peaking';
  midEQ.frequency.value = +params.midEqFreq;
  midEQ.gain.value = +params.midEqGain;

  const sideEQ = oCtx.createBiquadFilter();
  sideEQ.type = 'peaking';
  sideEQ.frequency.value = +params.sideEqFreq;
  sideEQ.gain.value = +params.sideEqGain;

  const form1 = oCtx.createBiquadFilter();
  form1.type = 'peaking';
  form1.frequency.value = +params.formant1Freq;
  form1.Q.value = +params.formant1Q;
  form1.gain.value = +params.formant1Gain;

  const form2 = oCtx.createBiquadFilter();
  form2.type = 'peaking';
  form2.frequency.value = +params.formant2Freq;
  form2.Q.value = +params.formant2Q;
  form2.gain.value = +params.formant2Gain;

  const compressor = oCtx.createDynamicsCompressor();
  compressor.threshold.value = +params.compThreshold;
  compressor.ratio.value = +params.compRatio;
  compressor.attack.value = +params.compAttack;
  compressor.release.value = +params.compRelease;

  let distortionNode;
  if (params.distType === 'soft') {
    distortionNode = oCtx.createWaveShaper();
    distortionNode.curve = makeDistortionCurve(+params.distAmount);
    distortionNode.oversample = '4x';
  } else if (params.distType === 'hard') {
    distortionNode = oCtx.createWaveShaper();
    distortionNode.curve = makeHardClippingCurve(+params.distAmount);
    distortionNode.oversample = 'none';
  } else if (params.distType === 'bitcrush') {
    distortionNode = makeBitcrusher(oCtx, 4 + Math.floor((+params.distAmount/1000)*12));
  } else {
    distortionNode = oCtx.createGain();
    distortionNode.gain.value = 1;
  }

  src.connect(volumeNode);
  volumeNode.connect(bass);
  bass.connect(treble);
  treble.connect(lp);
  lp.connect(hp);
  hp.connect(notch1);
  notch1.connect(notch2);
  notch2.connect(inverter);

  inverter.connect(splitter);
  splitter.connect(midGain, 0);
  splitter.connect(sideGain, 1);

  midGain.connect(midEQ);
  sideGain.connect(sideEQ);

  midEQ.connect(merger, 0, 0);
  sideEQ.connect(merger, 0, 1);

  merger.connect(form1);
  form1.connect(form2);
  form2.connect(compressor);
  compressor.connect(distortionNode);
  distortionNode.connect(oCtx.destination);

  const script = oCtx.createScriptProcessor(256, 1, 1);
  compressor.connect(script);
  script.connect(oCtx.destination);
  script.onaudioprocess = () => {
    const duckGain = distortionNode.context.createGain();
    duckGain.gain.value = 1 - Math.min(1, compressor.reduction / 20);
  };

  src.start();
  const renderedBuffer = await oCtx.startRendering();
  const bitDepth = +params.bitDepth;
  const wav = bufferToWav(renderedBuffer, bitDepth);
  const blob = new Blob([wav], { type: 'audio/wav' });
  return { blob, size: blob.size, renderedBuffer };
}