<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Simple Live Audio Editor with MP3 Download</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
  body { font-family: system-ui, -apple-system, Roboto, Arial; margin: 20px; color:#111 }
  h1 { font-size: 20px; margin-bottom: 8px; }
  .controls { display:flex; flex-wrap:wrap; gap:12px; }
  .control { min-width: 220px; flex:1 1 240px; background:#f7f7f7; padding:10px; border-radius:6px; box-shadow:0 1px 2px rgba(0,0,0,0.04); }
  label { display:block; font-size:13px; margin-bottom:6px; color:#333 }
  input[type="range"] { width:100%; }
  .row { display:flex; gap:8px; align-items:center; }
  .val { min-width:60px; text-align:right; font-family:monospace; font-size:13px; color:#222 }
  .topbar { display:flex; gap:8px; align-items:center; margin-bottom:12px; flex-wrap: wrap; }
  button { padding:8px 12px; border-radius:6px; border:1px solid #ccc; background:white; cursor:pointer; }
  input[type=file] { display:block; }
  .small { font-size:13px; color:#666; margin-top:8px; }
  footer { margin-top:14px; color:#666; font-size:13px; }
  select { padding:6px; border-radius:6px; }
</style>
</head>
<body>
  <h1>Simple Live Audio Editor</h1>
  <div class="topbar">
    <input id="file" type="file" accept="audio/*">
    <button id="play">Play</button>
    <button id="pause" disabled>Pause</button>
    <button id="stop" disabled>Stop</button>
    <select id="format">
      <option value="mp3">MP3 (if supported)</option>
      <option value="wav">WAV</option>
    </select>
    <button id="download" disabled>Download</button>
    <div id="info" style="margin-left:8px;font-size:13px;color:#444"></div>
  </div>

  <div class="controls">
    <div class="control">
      <label>High-pass: <span id="hp-val" class="val">20 Hz</span></label>
      <input id="hp" type="range" min="20" max="1200" value="20">
    </div>
    <div class="control">
      <label>Low-pass: <span id="lp-val" class="val">20000 Hz</span></label>
      <input id="lp" type="range" min="500" max="20000" value="20000">
    </div>
    <div class="control">
      <label>Bass (low shelf): <span id="bass-val" class="val">0 dB</span></label>
      <input id="bass" type="range" min="-15" max="15" value="0" step="0.5">
    </div>
    <div class="control">
      <label>Treble (high shelf): <span id="treb-val" class="val">0 dB</span></label>
      <input id="treb" type="range" min="-15" max="15" value="0" step="0.5">
    </div>

    <div class="control">
      <label>Volume: <span id="vol-val" class="val">1.00×</span></label>
      <input id="vol" type="range" min="0" max="2" value="1" step="0.01">
    </div>

    <div class="control">
      <label>Reverb mix: <span id="rv-mix-val" class="val">20%</span></label>
      <input id="rv-mix" type="range" min="0" max="100" value="20">
      <label style="margin-top:10px">Reverb decay: <span id="rv-decay-val" class="val">1.2 s</span></label>
      <input id="rv-decay" type="range" min="0.1" max="6" value="1.2" step="0.1">
    </div>

    <div class="control">
      <label>Compression</label>
      <div class="row" style="margin-bottom:6px">
        <label style="flex:1">Threshold: <span id="comp-th-val" class="val">-24 dB</span></label>
        <input id="comp-th" type="range" min="-100" max="0" value="-24">
      </div>
      <div class="row" style="margin-bottom:6px">
        <label style="flex:1">Ratio: <span id="comp-ratio-val" class="val">12:1</span></label>
        <input id="comp-ratio" type="range" min="1" max="20" value="12" step="0.1">
      </div>
      <div class="row" style="margin-bottom:6px">
        <label style="flex:1">Attack: <span id="comp-attack-val" class="val">0.003 s</span></label>
        <input id="comp-attack" type="range" min="0" max="1" value="0.003" step="0.001">
      </div>
      <div class="row">
        <label style="flex:1">Release: <span id="comp-release-val" class="val">0.25 s</span></label>
        <input id="comp-release" type="range" min="0" max="1" value="0.25" step="0.01">
      </div>
    </div>

  </div>

  <footer>Load audio, adjust sliders live, and click Download to export (MP3 if supported by your browser; otherwise WAV).</footer>

<script>
(async function(){
  // Elements
  const fileEl = document.getElementById('file');
  const playBtn = document.getElementById('play');
  const pauseBtn = document.getElementById('pause');
  const stopBtn = document.getElementById('stop');
  const downloadBtn = document.getElementById('download');
  const info = document.getElementById('info');
  const formatSelect = document.getElementById('format');

  const hp = document.getElementById('hp'), hpVal = document.getElementById('hp-val');
  const lp = document.getElementById('lp'), lpVal = document.getElementById('lp-val');
  const bass = document.getElementById('bass'), bassVal = document.getElementById('bass-val');
  const treb = document.getElementById('treb'), trebVal = document.getElementById('treb-val');
  const vol = document.getElementById('vol'), volVal = document.getElementById('vol-val');
  const rvMix = document.getElementById('rv-mix'), rvMixVal = document.getElementById('rv-mix-val');
  const rvDecay = document.getElementById('rv-decay'), rvDecayVal = document.getElementById('rv-decay-val');

  const compTh = document.getElementById('comp-th'), compThVal = document.getElementById('comp-th-val');
  const compRatio = document.getElementById('comp-ratio'), compRatioVal = document.getElementById('comp-ratio-val');
  const compAttack = document.getElementById('comp-attack'), compAttackVal = document.getElementById('comp-attack-val');
  const compRelease = document.getElementById('comp-release'), compReleaseVal = document.getElementById('comp-release-val');

  // Audio state
  let audioCtx = null, buffer = null, source = null;
  let startTime = 0, pausedAt = 0, isPlaying = false;
  // nodes (live)
  let hpFilter, lpFilter, bassShelf, trebleShelf, dryGain, wetGain, convolver, overallGain;
  let delayNode, feedbackGain, compressor, mediaStreamDestination;

  function ensureAudioContext() {
    if (!audioCtx) {
      audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      setupNodes();
    } else if (audioCtx.state === 'suspended') {
      audioCtx.resume();
    }
  }

  function setupNodes() {
    // Create the processing chain nodes (reusable for live playback)
    hpFilter = audioCtx.createBiquadFilter(); hpFilter.type = 'highpass'; hpFilter.frequency.value = hp.value;
    lpFilter = audioCtx.createBiquadFilter(); lpFilter.type = 'lowpass'; lpFilter.frequency.value = lp.value;
    bassShelf = audioCtx.createBiquadFilter(); bassShelf.type = 'lowshelf'; bassShelf.frequency.value = 200; bassShelf.gain.value = bass.value;
    trebleShelf = audioCtx.createBiquadFilter(); trebleShelf.type = 'highshelf'; trebleShelf.frequency.value = 3500; trebleShelf.gain.value = treb.value;

    // Reverb (convolver)
    convolver = audioCtx.createConvolver();
    convolver.buffer = createReverbImpulse(audioCtx, Number(rvDecay.value));

    // Slight echo (delay + feedback) mixed into the wet path
    delayNode = audioCtx.createDelay(5.0);
    delayNode.delayTime.value = 0.18; // slight echo
    feedbackGain = audioCtx.createGain();
    feedbackGain.gain.value = 0.25; // small feedback

    // feedback loop: delay -> feedback -> delay
    delayNode.connect(feedbackGain);
    feedbackGain.connect(delayNode);

    // Gains
    dryGain = audioCtx.createGain(); wetGain = audioCtx.createGain(); overallGain = audioCtx.createGain();
    dryGain.gain.value = 1; wetGain.gain.value = rvMix.value/100; overallGain.gain.value = vol.value;

    // Compressor
    compressor = audioCtx.createDynamicsCompressor();
    // default compressor params
    compressor.threshold.value = compTh.value;
    compressor.ratio.value = compRatio.value;
    compressor.attack.value = compAttack.value;
    compressor.release.value = compRelease.value;

    // routing (live default playback): source -> hp -> lp -> bass -> treb -> split -> dryGain -> compressor
    //                                                         treb -> convolver -> wetGain -> compressor
    //                                                         convolver -> delay -> wetGain (echo)
    // compressor -> overallGain -> audioCtx.destination
    // (when recording to MediaRecorder we will route to a mediaStreamDestination as well)

    // Keep nodes disconnected until createSource connects them — nodes created here for configuration and reuse.
  }

  function createReverbImpulse(ctx, decay) {
    const rate = ctx.sampleRate, length = Math.floor(rate * decay);
    // At least 1 sample
    const buf = ctx.createBuffer(2, Math.max(1, length), rate);
    for (let c = 0; c < 2; c++) {
      const data = buf.getChannelData(c);
      for (let i = 0; i < length; i++) {
        // white noise shaped by an exponential decay curve
        // reduce to avoid harshness: pow factor
        data[i] = (Math.random()*2 - 1) * Math.pow(1 - i/length, 2) * Math.exp(-i / (rate * decay));
      }
    }
    return buf;
  }

  function connectPlaybackChain(srcNode) {
    // Connect the prepared live nodes for audible playback.
    // ensure fresh state: disconnect previous connections
    try {
      hpFilter.disconnect();
    } catch(e){}
    // connect chain
    srcNode.connect(hpFilter);
    hpFilter.connect(lpFilter);
    lpFilter.connect(bassShelf);
    bassShelf.connect(trebleShelf);

    // dry path
    trebleShelf.connect(dryGain);

    // reverb path
    trebleShelf.connect(convolver);
    convolver.connect(wetGain);             // direct convolution reverb to wet
    convolver.connect(delayNode);           // also feed into delay for echo
    delayNode.connect(wetGain);             // delayed echo mixed into wet

    // mix dry + wet -> compressor -> overall -> destination
    dryGain.connect(compressor);
    wetGain.connect(compressor);
    compressor.connect(overallGain);
    overallGain.connect(audioCtx.destination);
  }

  function createSource(startOffset=0, connectToDestination=true) {
    if (!buffer) return null;
    // create a fresh BufferSource for each play
    const s = audioCtx.createBufferSource();
    s.buffer = buffer;
    s.onended = ()=>{
      isPlaying = false;
      pausedAt = 0;
      playBtn.disabled=false;
      pauseBtn.disabled=true;
      stopBtn.disabled=true;
    };

    if (connectToDestination) {
      connectPlaybackChain(s);
    } else {
      // If not connecting to destination (e.g. only recording into MediaRecorder), connect to chain but not to audioCtx.destination
      s.connect(hpFilter);
      hpFilter.connect(lpFilter);
      lpFilter.connect(bassShelf);
      bassShelf.connect(trebleShelf);

      trebleShelf.connect(dryGain);
      trebleShelf.connect(convolver);
      convolver.connect(wetGain);
      convolver.connect(delayNode);
      delayNode.connect(wetGain);

      dryGain.connect(compressor);
      wetGain.connect(compressor);
      compressor.connect(overallGain);
      // do not connect overallGain to audioCtx.destination here
    }
    // set initial dry/wet/vol values into nodes
    dryGain.gain.value = 1 - (rvMix.value/100);
    wetGain.gain.value = (rvMix.value/100);
    overallGain.gain.value = Number(vol.value);
    hpFilter.frequency.value = hp.value;
    lpFilter.frequency.value = lp.value;
    bassShelf.gain.value = bass.value;
    trebleShelf.gain.value = treb.value;
    convolver.buffer = createReverbImpulse(audioCtx, Number(rvDecay.value));
    return s;
  }

  // File loading
  fileEl.addEventListener('change', async () => {
    const f = fileEl.files[0]; if (!f) return;
    info.textContent = 'Loading...'; ensureAudioContext();
    const array = await f.arrayBuffer();
    // decodeAudioData with promise wrapper for cross-browser
    buffer = await audioCtx.decodeAudioData(array.slice(0));
    info.textContent = `${f.name} — ${Math.round(buffer.duration)}s`;
    pausedAt = 0;
    downloadBtn.disabled = false;
    // nudge defaults: make reverb audible by default (automated)
    rvMix.value = 20; rvMixVal.textContent = '20%';
  });

  // Playback controls
  playBtn.addEventListener('click', async ()=>{
    if (!buffer) return alert('Load a file first');
    ensureAudioContext();
    if (!isPlaying) {
      source = createSource(pausedAt, true);
      startTime = audioCtx.currentTime - pausedAt;
      source.start(0, pausedAt);
      isPlaying = true;
      playBtn.disabled = true; pauseBtn.disabled = false; stopBtn.disabled = false;
    }
  });

  pauseBtn.addEventListener('click', ()=>{
    if (!isPlaying) return;
    pausedAt = audioCtx.currentTime - startTime;
    if (source) source.stop();
    isPlaying=false; playBtn.disabled=false; pauseBtn.disabled=true;
  });

  stopBtn.addEventListener('click', ()=>{
    if (source) source.stop();
    pausedAt=0; isPlaying=false; playBtn.disabled=false; pauseBtn.disabled=true; stopBtn.disabled=true;
  });

  // Slider handlers (update live nodes if present)
  hp.oninput = ()=>{hpVal.textContent = hp.value+' Hz'; if(hpFilter) hpFilter.frequency.value=hp.value;};
  lp.oninput = ()=>{lpVal.textContent = lp.value+' Hz'; if(lpFilter) lpFilter.frequency.value=lp.value;};
  bass.oninput = ()=>{bassVal.textContent = bass.value+' dB'; if(bassShelf) bassShelf.gain.value=bass.value;};
  treb.oninput = ()=>{trebVal.textContent = treb.value+' dB'; if(trebleShelf) trebleShelf.gain.value=treb.value;};
  vol.oninput = ()=>{volVal.textContent = parseFloat(vol.value).toFixed(2)+'×'; if(overallGain) overallGain.gain.value=Number(vol.value);};
  rvMix.oninput = ()=>{rvMixVal.textContent = rvMix.value+'%'; if(dryGain&&wetGain){let m=rvMix.value/100; dryGain.gain.value=1-m; wetGain.gain.value=m;}};
  rvDecay.oninput = ()=>{rvDecayVal.textContent = rvDecay.value+' s'; if(convolver) convolver.buffer=createReverbImpulse(audioCtx, Number(rvDecay.value));};

  // Compressor UI handlers
  compTh.oninput = ()=>{compThVal.textContent = compTh.value+' dB'; if(compressor) compressor.threshold.value = compTh.value;};
  compRatio.oninput = ()=>{compRatioVal.textContent = Number(compRatio.value).toFixed(1)+':1'; if(compressor) compressor.ratio.value = Number(compRatio.value);};
  compAttack.oninput = ()=>{compAttackVal.textContent = Number(compAttack.value).toFixed(3)+' s'; if(compressor) compressor.attack.value = Number(compAttack.value);};
  compRelease.oninput = ()=>{compReleaseVal.textContent = Number(compRelease.value).toFixed(2)+' s'; if(compressor) compressor.release.value = Number(compRelease.value);};

  // ---- Download / Export ----
  downloadBtn.addEventListener('click', async ()=>{
    if (!buffer) return;
    const chosen = formatSelect.value;
    if (chosen === 'mp3') {
      // Try MediaRecorder with audio/mpeg if supported
      const mime = MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported('audio/mpeg') ? 'audio/mpeg' : null;
      if (mime) {
        try {
          await ensureAudioContext(); // ensure live audio context
          const recResult = await recordViaMediaRecorderMP3(audioCtx, buffer, mime);
          const blob = recResult;
          const filename = (fileEl.files[0]?.name?.replace(/\.[^/.]+$/, '') || 'output') + '_processed.mp3';
          downloadBlob(blob, filename);
          return;
        } catch(err) {
          console.warn('MediaRecorder MP3 failed, falling back to WAV:', err);
          // fallthrough to WAV fallback
        }
      } else {
        // not supported
        console.warn('MediaRecorder audio/mpeg not supported in this browser — falling back to WAV render.');
      }
    }

    // Fallback: render offline and produce WAV (works everywhere)
    const duration = buffer.duration;
    // Use OfflineAudioContext for deterministic rendering
    const offline = new OfflineAudioContext(buffer.numberOfChannels, Math.ceil(buffer.sampleRate * duration), buffer.sampleRate);

    // create nodes in offline context mirroring the live chain
    const src = offline.createBufferSource();
    src.buffer = buffer;

    const hpF = offline.createBiquadFilter(); hpF.type='highpass'; hpF.frequency.value=hp.value;
    const lpF = offline.createBiquadFilter(); lpF.type='lowpass'; lpF.frequency.value=lp.value;
    const bassF = offline.createBiquadFilter(); bassF.type='lowshelf'; bassF.frequency.value=200; bassF.gain.value=bass.value;
    const trebF = offline.createBiquadFilter(); trebF.type='highshelf'; trebF.frequency.value=3500; trebF.gain.value=treb.value;

    const conv = offline.createConvolver(); conv.buffer = createReverbImpulse(offline, Number(rvDecay.value));
    const delay = offline.createDelay(5.0); delay.delayTime.value = 0.18;
    const fb = offline.createGain(); fb.gain.value = 0.25;
    delay.connect(fb); fb.connect(delay);

    const dry = offline.createGain(); const wet = offline.createGain();
    const comp = offline.createDynamicsCompressor();
    comp.threshold.value = compTh.value;
    comp.ratio.value = compRatio.value;
    comp.attack.value = compAttack.value;
    comp.release.value = compRelease.value;

    const gain = offline.createGain(); gain.gain.value = vol.value;

    const mix = rvMix.value/100; dry.gain.value = 1-mix; wet.gain.value = mix;

    src.connect(hpF); hpF.connect(lpF); lpF.connect(bassF); bassF.connect(trebF);
    trebF.connect(dry); trebF.connect(conv);
    conv.connect(wet);
    conv.connect(delay);
    delay.connect(wet);
    dry.connect(comp); wet.connect(comp);
    comp.connect(gain);
    gain.connect(offline.destination);

    src.start(0);
    info.textContent = 'Rendering...';
    const rendered = await offline.startRendering();
    info.textContent = 'Rendering complete — preparing download...';

    const wav = bufferToWav(rendered);
    const blob = new Blob([wav], {type:'audio/wav'});
    const filename = (fileEl.files[0]?.name?.replace(/\.[^/.]+$/, '') || 'output') + '_processed.wav';
    downloadBlob(blob, filename);
    info.textContent = 'Download ready.';
  });

  // Use MediaRecorder to record processed audio from a live AudioContext into MP3 (if supported)
  async function recordViaMediaRecorderMP3(ctx, audioBuffer, mimeType) {
    return new Promise((resolve, reject)=>{
      // Create a dedicated set of nodes to avoid interfering with live playback nodes
      const src = ctx.createBufferSource();
      src.buffer = audioBuffer;

      // Create fresh nodes in the live context
      const hpF = ctx.createBiquadFilter(); hpF.type='highpass'; hpF.frequency.value=hp.value;
      const lpF = ctx.createBiquadFilter(); lpF.type='lowpass'; lpF.frequency.value=lp.value;
      const bassF = ctx.createBiquadFilter(); bassF.type='lowshelf'; bassF.frequency.value=200; bassF.gain.value=bass.value;
      const trebF = ctx.createBiquadFilter(); trebF.type='highshelf'; trebF.frequency.value=3500; trebF.gain.value=treb.value;

      const conv = ctx.createConvolver();
      conv.buffer = createReverbImpulse(ctx, Number(rvDecay.value));
      const delay = ctx.createDelay(5.0); delay.delayTime.value = 0.18;
      const fb = ctx.createGain(); fb.gain.value = 0.25;
      delay.connect(fb); fb.connect(delay);

      const dry = ctx.createGain(); const wet = ctx.createGain();
      const comp = ctx.createDynamicsCompressor();
      comp.threshold.value = compTh.value;
      comp.ratio.value = compRatio.value;
      comp.attack.value = compAttack.value;
      comp.release.value = compRelease.value;

      const gain = ctx.createGain(); gain.gain.value = vol.value;

      // MediaStreamDestination will be captured by MediaRecorder
      const dest = ctx.createMediaStreamDestination();

      // connect nodes (no audioCtx.destination connection, so this won't play through speakers)
      src.connect(hpF); hpF.connect(lpF); lpF.connect(bassF); bassF.connect(trebF);
      trebF.connect(dry); trebF.connect(conv);
      conv.connect(wet); conv.connect(delay); delay.connect(wet);
      dry.connect(comp); wet.connect(comp);
      comp.connect(gain);
      gain.connect(dest);

      const recordedChunks = [];
      let options = {};
      try { options = { mimeType }; } catch(e){}

      let recorder;
      try {
        recorder = new MediaRecorder(dest.stream, options);
      } catch(err) {
        // If creation fails, bail out
        cleanup();
        return reject(err);
      }

      recorder.ondataavailable = e => {
        if (e.data && e.data.size) recordedChunks.push(e.data);
      };
      recorder.onerror = err => {
        cleanup();
        reject(err);
      };
      recorder.onstop = () => {
        const blob = new Blob(recordedChunks, { type: mimeType });
        cleanup();
        resolve(blob);
      };

      // start recording, start playback; stop when buffer ends
      recorder.start(100); // timeslice optional
      const startAt = ctx.currentTime + 0.05; // small offset
      src.start(startAt);
      // schedule stop
      const stopTime = startAt + audioBuffer.duration + 0.05;
      // stop recorder after some margin
      setTimeout(()=>{
        try { recorder.stop(); } catch(e){ console.warn('recorder stop failed', e); }
      }, (audioBuffer.duration + 0.2) * 1000);

      // cleanup function to disconnect nodes
      function cleanup(){
        try { src.disconnect(); } catch(e){}
        try { hpF.disconnect(); } catch(e){}
        try { lpF.disconnect(); } catch(e){}
        try { bassF.disconnect(); } catch(e){}
        try { trebF.disconnect(); } catch(e){}
        try { conv.disconnect(); } catch(e){}
        try { delay.disconnect(); } catch(e){}
        try { fb.disconnect(); } catch(e){}
        try { dry.disconnect(); } catch(e){}
        try { wet.disconnect(); } catch(e){}
        try { comp.disconnect(); } catch(e){}
        try { gain.disconnect(); } catch(e){}
      }
    });
  }

  function downloadBlob(blob, filename) {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(()=>URL.revokeObjectURL(url), 2000);
  }

  // WAV encoding (same as before)
  function bufferToWav(buffer){
    const numOfChan = buffer.numberOfChannels, length = buffer.length * numOfChan * 2 + 44;
    const bufferArr = new ArrayBuffer(length), view = new DataView(bufferArr);
    const channels = [], sampleRate = buffer.sampleRate;
    let offset = 0, pos = 0;

    function writeString(s){ for (let i=0;i<s.length;i++) view.setUint8(pos+i,s.charCodeAt(i)); pos+=s.length; }

    writeString('RIFF'); view.setUint32(pos, length - 8, true); pos+=4;
    writeString('WAVE'); writeString('fmt '); view.setUint32(pos, 16, true); pos+=4;
    view.setUint16(pos, 1, true); pos+=2; view.setUint16(pos, numOfChan, true); pos+=2;
    view.setUint32(pos, sampleRate, true); pos+=4; view.setUint32(pos, sampleRate * 2 * numOfChan, true); pos+=4;
    view.setUint16(pos, numOfChan * 2, true); pos+=2; view.setUint16(pos, 16, true); pos+=2;
    writeString('data'); view.setUint32(pos, length - pos - 4, true); pos+=4;

    for (let i = 0; i < numOfChan; i++) channels.push(buffer.getChannelData(i));
    while (pos < length) {
      for (let i = 0; i < numOfChan; i++) {
        let sample = Math.max(-1, Math.min(1, channels[i][offset]));
        view.setInt16(pos, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
        pos += 2;
      }
      offset++;
    }
    return bufferArr;
  }

  // Utility: ensure AudioContext exists and nodes are ready (returns resolved Promise)
  async function ensureAudioContext() {
    if (!audioCtx) {
      audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      setupNodes();
    } else if (audioCtx.state === 'suspended') {
      await audioCtx.resume();
    }
  }

  // Initial UI defaults
  rvMixVal.textContent = rvMix.value+'%';
  rvDecayVal.textContent = rvDecay.value+' s';
  hpVal.textContent = hp.value+' Hz';
  lpVal.textContent = lp.value+' Hz';
  bassVal.textContent = bass.value+' dB';
  trebVal.textContent = treb.value+' dB';
  volVal.textContent = parseFloat(vol.value).toFixed(2)+'×';

  compThVal.textContent = compTh.value + ' dB';
  compRatioVal.textContent = compRatio.value + ':1';
  compAttackVal.textContent = Number(compAttack.value).toFixed(3)+' s';
  compReleaseVal.textContent = Number(compRelease.value).toFixed(2)+' s';

  // make download button enabled only when a file loaded (handled on file load)

})();
</script>
</body>
</html>
/* Add your styles here */

// Add your code here