You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, normalizationDb = "-14.0", outputSettings = "WAV (Lossless)") {
// We are requested to build an Audio Volume Normalizer UI/tool,
// yet strictly adhere to an image processing function signature.
// Solution: We create an interactive audio normalizer UI overlay
// that operates on top of the original image, generating a visual container.
const container = document.createElement('div');
container.style.position = 'relative';
container.style.overflow = 'hidden';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
container.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
container.style.color = '#fff';
container.style.backgroundColor = '#1a1a1a';
container.style.width = (originalImg && originalImg.width ? originalImg.width : 800) + 'px';
container.style.height = (originalImg && originalImg.height ? originalImg.height : 600) + 'px';
container.style.boxShadow = '0 4px 6px rgba(0,0,0,0.3)';
container.style.borderRadius = '8px';
if (originalImg && originalImg.src) {
container.style.backgroundImage = `url(${originalImg.src})`;
container.style.backgroundSize = 'cover';
container.style.backgroundPosition = 'center';
}
// Overlay to ensure readable UI
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(20, 20, 20, 0.85)';
overlay.style.backdropFilter = 'blur(10px)';
overlay.style.display = 'flex';
overlay.style.flexDirection = 'column';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.padding = '30px';
overlay.style.boxSizing = 'border-box';
overlay.style.overflowY = 'auto';
container.appendChild(overlay);
const title = document.createElement('h2');
title.innerText = 'Audio Volume Normalizer';
title.style.margin = '0 0 10px 0';
title.style.fontSize = '24px';
title.style.fontWeight = 'bold';
title.style.textShadow = '0 2px 4px rgba(0,0,0,0.5)';
overlay.appendChild(title);
const subtitle = document.createElement('p');
subtitle.innerText = `YouTube Stats For Nerds Normalization Target: ${normalizationDb} dB`;
subtitle.style.margin = '0 0 20px 0';
subtitle.style.fontSize = '14px';
subtitle.style.color = '#ccc';
overlay.appendChild(subtitle);
const inputWrapper = document.createElement('div');
inputWrapper.style.margin = '20px 0';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'audio/*';
fileInput.style.padding = '10px';
fileInput.style.backgroundColor = 'rgba(255,255,255,0.1)';
fileInput.style.border = '1px solid rgba(255,255,255,0.2)';
fileInput.style.borderRadius = '4px';
fileInput.style.color = '#fff';
fileInput.style.cursor = 'pointer';
inputWrapper.appendChild(fileInput);
overlay.appendChild(inputWrapper);
const statusMsg = document.createElement('div');
statusMsg.innerText = 'Please upload an Mp3, Opus, Ac3, or other audio file.';
statusMsg.style.fontSize = '14px';
statusMsg.style.fontWeight = '500';
statusMsg.style.margin = '10px 0';
statusMsg.style.minHeight = '20px';
overlay.appendChild(statusMsg);
const statsContainer = document.createElement('div');
statsContainer.style.width = '100%';
statsContainer.style.maxWidth = '400px';
overlay.appendChild(statsContainer);
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
// Reset UI
statsContainer.innerHTML = '';
statusMsg.innerText = 'Decoding audio... (this may take a moment for large files)';
statusMsg.style.color = '#fff';
try {
const arrayBuffer = await file.arrayBuffer();
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContextClass();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
statusMsg.innerText = 'Analyzing audio profile (RMS and True Peak)...';
// Calculate RMS and Peak
let maxPeak = 0;
let sumSquare = 0;
let numSamples = 0;
for (let c = 0; c < audioBuffer.numberOfChannels; c++) {
const channelData = audioBuffer.getChannelData(c);
for (let i = 0; i < channelData.length; i++) {
const val = channelData[i];
const absVal = Math.abs(val);
if (absVal > maxPeak) maxPeak = absVal;
sumSquare += val * val;
}
numSamples += channelData.length;
}
const rms = Math.sqrt(sumSquare / numSamples);
const currentDb = 20 * Math.log10(rms || 1e-6);
const targetDb = parseFloat(normalizationDb);
let diffDb = targetDb - currentDb;
let gain = Math.pow(10, diffDb / 20);
let constrainedLimit = false;
// Ensure we do not clip the audio by going over 0 dBFS (Peak = 1.0)
if (maxPeak * gain > 0.99) {
const maxSafeGain = 0.99 / maxPeak;
gain = maxSafeGain;
constrainedLimit = true;
}
const finalRmsDb = currentDb + (20 * Math.log10(gain));
statusMsg.innerText = 'Applying gain and rendering normalized audio output...';
const offlineCtx = new OfflineAudioContext(
audioBuffer.numberOfChannels,
audioBuffer.length,
audioBuffer.sampleRate
);
const source = offlineCtx.createBufferSource();
source.buffer = audioBuffer;
const gainNode = offlineCtx.createGain();
gainNode.gain.value = gain;
source.connect(gainNode);
gainNode.connect(offlineCtx.destination);
source.start(0);
const renderedBuffer = await offlineCtx.startRendering();
statusMsg.innerText = 'Normalization complete!';
statusMsg.style.color = '#4CAF50';
// Render Stats for Nerds Visuals
statsContainer.style.backgroundColor = 'rgba(0,0,0,0.6)';
statsContainer.style.border = '1px solid #444';
statsContainer.style.padding = '15px';
statsContainer.style.borderRadius = '5px';
statsContainer.style.marginTop = '20px';
statsContainer.style.fontFamily = '"Consolas", "Courier New", monospace';
statsContainer.style.fontSize = '12px';
statsContainer.style.lineHeight = '1.6';
statsContainer.style.textAlign = 'left';
statsContainer.innerHTML = `
<div style="font-weight:bold; font-size:14px; margin-bottom:10px; border-bottom:1px solid #666; padding-bottom:5px;">
YouTube Stats for nerds (Audio)
</div>
<div><b>Source format:</b> ${file.type || 'Unknown / Raw bits'}</div>
<div><b>Channels:</b> ${audioBuffer.numberOfChannels} (${audioBuffer.sampleRate} Hz)</div>
<div><b>Original RMS Vol:</b> ${currentDb.toFixed(2)} dB</div>
<div><b>Target RMS Vol:</b> ${targetDb.toFixed(2)} dB</div>
<div><b>Required Gain Diff:</b> ${diffDb.toFixed(2)} dB</div>
<div><b>Original Peak limit:</b> ${(20 * Math.log10(maxPeak)).toFixed(2)} dBFS</div>
${constrainedLimit ? `<div style="color:#f39c12"><b>Warning:</b> Target constrained to prevent digital clipping!</div>` : ''}
<div><b>Final Applied RMS Vol:</b> ${finalRmsDb.toFixed(2)} dB</div>
<div><b>Volume / Normalized:</b> 100% / ${(gain * 100).toFixed(0)}%</div>
`;
// Setup preview and export
const wavBlob = audioBufferToWav(renderedBuffer);
const url = URL.createObjectURL(wavBlob);
const actionsContainer = document.createElement('div');
actionsContainer.style.marginTop = '25px';
actionsContainer.style.display = 'flex';
actionsContainer.style.flexDirection = 'column';
actionsContainer.style.alignItems = 'center';
actionsContainer.style.gap = '15px';
const audioPlayer = document.createElement('audio');
audioPlayer.controls = true;
audioPlayer.src = url;
audioPlayer.style.width = '100%';
const downloadBtn = document.createElement('a');
downloadBtn.href = url;
downloadBtn.download = `Normalized_${file.name}.wav`;
downloadBtn.innerText = 'Download Normalized Output (Native Lossless WAV)';
downloadBtn.style.backgroundColor = '#cc0000'; // YouTube red
downloadBtn.style.color = '#fff';
downloadBtn.style.textDecoration = 'none';
downloadBtn.style.padding = '10px 20px';
downloadBtn.style.borderRadius = '5px';
downloadBtn.style.fontWeight = 'bold';
downloadBtn.style.fontSize = '14px';
downloadBtn.style.transition = 'background-color 0.3s';
downloadBtn.onmouseenter = () => downloadBtn.style.backgroundColor = '#ff0000';
downloadBtn.onmouseleave = () => downloadBtn.style.backgroundColor = '#cc0000';
actionsContainer.appendChild(audioPlayer);
actionsContainer.appendChild(downloadBtn);
statsContainer.appendChild(actionsContainer);
} catch (err) {
statusMsg.innerText = 'Error processing file: ' + err.message;
statusMsg.style.color = '#e74c3c';
}
});
return container;
// Helper Utility: Convert AudioBuffer to strict, universal format (WAV 16-bit PCM)
function audioBufferToWav(buffer) {
const numOfChan = buffer.numberOfChannels;
const length = buffer.length * numOfChan * 2 + 44;
const bufferArray = new ArrayBuffer(length);
const view = new DataView(bufferArray);
let offset = 0;
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
/* RIFF header */
writeString(view, offset, 'RIFF'); offset += 4;
view.setUint32(offset, 36 + buffer.length * numOfChan * 2, true); offset += 4;
writeString(view, offset, 'WAVE'); offset += 4;
/* fmt chunk */
writeString(view, offset, 'fmt '); offset += 4;
view.setUint32(offset, 16, true); offset += 4;
view.setUint16(offset, 1, true); offset += 2;
view.setUint16(offset, numOfChan, true); offset += 2;
view.setUint32(offset, buffer.sampleRate, true); offset += 4;
view.setUint32(offset, buffer.sampleRate * 2 * numOfChan, true); offset += 4;
view.setUint16(offset, numOfChan * 2, true); offset += 2;
view.setUint16(offset, 16, true); offset += 2;
/* data chunk */
writeString(view, offset, 'data'); offset += 4;
view.setUint32(offset, buffer.length * numOfChan * 2, true); offset += 4;
// Interleave channels
const channels = [];
for (let i = 0; i < numOfChan; i++) {
channels.push(buffer.getChannelData(i));
}
let sample = 0;
while (offset < length) {
for (let i = 0; i < numOfChan; i++) {
// Hard limit safety inside bounds -1.0 and +1.0
let s = Math.max(-1, Math.min(1, channels[i][sample]));
s = s < 0 ? s * 0x8000 : s * 0x7FFF;
view.setInt16(offset, s, true);
offset += 2;
}
sample++;
}
return new Blob([bufferArray], { type: 'audio/wav' });
}
}
Apply Changes