<!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>