<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8" />

  <title>Spider Ballooning Sonification</title>

  <style>

    body {

      font-family: sans-serif;

      background: #0e0e11;

      color: #eaeaf0;

      display: flex;

      flex-direction: column;

      align-items: center;

      padding: 40px;

    }

    h1 {

      font-weight: 400;

      margin-bottom: 20px;

    }

    .control {

      margin: 15px 0;

      width: 300px;

    }

    label {

      display: flex;

      justify-content: space-between;

      font-size: 14px;

    }

    input[type="range"] {

      width: 100%;

    }

    button {

      margin-top: 30px;

      padding: 10px 20px;

      font-size: 15px;

      cursor: pointer;

    }

  </style>

</head>

<body>


<h1>Spider Ballooning — Sonification</h1>


<div class="control">

  <label>

    <span>Event rate</span>

    <span id="rateVal">1.0</span>

  </label>

  <input id="rate" type="range" min="0.2" max="5" step="0.1" value="1" />

</div>


<div class="control">

  <label>

    <span>Loudness</span>

    <span id="gainVal">0.3</span>

  </label>

  <input id="gain" type="range" min="0" max="1" step="0.01" value="0.3" />

</div>


<button id="toggle">Start</button>


<script>

let ctx;

let masterGain;

let intervalId;

let running = false;


const rateSlider = document.getElementById("rate");

const gainSlider = document.getElementById("gain");

const rateVal = document.getElementById("rateVal");

const gainVal = document.getElementById("gainVal");

const toggleBtn = document.getElementById("toggle");


rateVal.textContent = rateSlider.value;

gainVal.textContent = gainSlider.value;


rateSlider.oninput = () => rateVal.textContent = rateSlider.value;

gainSlider.oninput = () => {

  gainVal.textContent = gainSlider.value;

  if (masterGain) masterGain.gain.value = gainSlider.value;

};


function createNoiseBuffer(context) {

  const buffer = context.createBuffer(1, context.sampleRate, context.sampleRate);

  const data = buffer.getChannelData(0);

  for (let i = 0; i < data.length; i++) {

    data[i] = Math.random() * 2 - 1;

  }

  return buffer;

}


function triggerBallooningEvent() {

  const noise = ctx.createBufferSource();

  noise.buffer = createNoiseBuffer(ctx);


  const filter = ctx.createBiquadFilter();

  filter.type = "bandpass";

  filter.frequency.value = 800 + Math.random() * 1200;

  filter.Q.value = 2 + Math.random() * 4;


  const env = ctx.createGain();

  env.gain.setValueAtTime(0, ctx.currentTime);

  env.gain.linearRampToValueAtTime(1, ctx.currentTime + 0.02);

  env.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);


  noise.connect(filter);

  filter.connect(env);

  env.connect(masterGain);


  noise.start();

  noise.stop(ctx.currentTime + 0.5);

}


function start() {

  ctx = new (window.AudioContext || window.webkitAudioContext)();

  masterGain = ctx.createGain();

  masterGain.gain.value = gainSlider.value;

  masterGain.connect(ctx.destination);


  const schedule = () => {

    triggerBallooningEvent();

    const rate = parseFloat(rateSlider.value);

    intervalId = setTimeout(schedule, 1000 / rate);

  };

  schedule();

}


function stop() {

  clearTimeout(intervalId);

  ctx.close();

}


toggleBtn.onclick = async () => {

  if (!running) {

    await start();

    toggleBtn.textContent = "Stop";

  } else {

    stop();

    toggleBtn.textContent = "Start";

  }

  running = !running;

};

</script>


</body>

</html>