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