<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Simple Live Audio Editor with 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; }
</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>
<button id="download" disabled>Download WAV</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">0%</span></label>
<input id="rv-mix" type="range" min="0" max="100" value="0">
<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>
<footer>Load audio, adjust sliders live, and click Download to export.</footer>
<script>
(async function(){
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 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');
let audioCtx = null, buffer = null, source = null;
let startTime = 0, pausedAt = 0, isPlaying = false;
let hpFilter, lpFilter, bassShelf, trebleShelf, dryGain, wetGain, convolver, overallGain;
function ensureAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
setupNodes();
} else if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function setupNodes() {
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;
convolver = audioCtx.createConvolver();
dryGain = audioCtx.createGain(); wetGain = audioCtx.createGain(); overallGain = audioCtx.createGain();
dryGain.gain.value = 1; wetGain.gain.value = 0; overallGain.gain.value = vol.value;
convolver.buffer = createReverbImpulse(audioCtx, rvDecay.value);
}
function createReverbImpulse(ctx, decay) {
const rate = ctx.sampleRate, length = Math.floor(rate * decay);
const buf = ctx.createBuffer(2, length, rate);
for (let c = 0; c < 2; c++) {
const data = buf.getChannelData(c);
for (let i = 0; i < length; i++) {
data[i] = (Math.random()*2-1)*Math.pow(1 - i/length, 3)*Math.exp(-i/(rate*decay));
}
}
return buf;
}
function createSource(offset=0) {
if (!buffer) return null;
const s = audioCtx.createBufferSource();
s.buffer = buffer;
s.onended = ()=>{isPlaying=false; pausedAt=0; playBtn.disabled=false; pauseBtn.disabled=true; stopBtn.disabled=true;};
s.connect(hpFilter);
hpFilter.connect(lpFilter);
lpFilter.connect(bassShelf);
bassShelf.connect(trebleShelf);
trebleShelf.connect(dryGain);
trebleShelf.connect(convolver);
convolver.connect(wetGain);
dryGain.connect(overallGain);
wetGain.connect(overallGain);
overallGain.connect(audioCtx.destination);
return s;
}
fileEl.addEventListener('change', async () => {
const f = fileEl.files[0]; if (!f) return;
info.textContent = 'Loading...'; ensureAudioContext();
const array = await f.arrayBuffer();
buffer = await audioCtx.decodeAudioData(array);
info.textContent = `${f.name} — ${Math.round(buffer.duration)}s`;
pausedAt = 0; downloadBtn.disabled = false;
});
playBtn.addEventListener('click', async ()=>{
if (!buffer) return alert('Load a file first');
ensureAudioContext();
if (!isPlaying) {
source = createSource(pausedAt);
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
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=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, rvDecay.value);};
// ---- Download ----
downloadBtn.addEventListener('click', async ()=>{
if (!buffer) return;
const duration = buffer.duration;
const offline = new OfflineAudioContext(buffer.numberOfChannels, buffer.sampleRate*duration, buffer.sampleRate);
// nodes
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, rvDecay.value);
const dry = offline.createGain(); const wet = offline.createGain();
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);
dry.connect(gain); wet.connect(gain); gain.connect(offline.destination);
src.start();
const rendered = await offline.startRendering();
const wav = bufferToWav(rendered);
const blob = new Blob([wav], {type:'audio/wav'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = (fileEl.files[0]?.name?.replace(/\.[^/.]+$/, '') || 'output') + '_processed.wav';
a.click();
URL.revokeObjectURL(url);
});
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;
}
})();
</script>
</body>
</html>
/* Add your styles here */
// Add your code here