Audio Enhancer
<div id="main-container" class="container mx-auto p-4 md:p-8">
<!-- Header -->
<header class="text-center mb-12">
<h1 class="text-4xl md:text-5xl font-bold text-teal-400">Audio Alter</h1>
<p class="text-gray-400 mt-2">A collection of powerful and easy-to-use web tools for your audio files.</p>
</header>
<!-- Tool Selection Grid -->
<div id="tool-selection" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6">
<!-- Tool cards will be dynamically inserted here -->
</div>
<!-- Tool Pages Container -->
<div id="tool-pages">
<!-- Bass Booster Page -->
<div id="bass-booster-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Bass Booster</h2>
<p class="text-gray-400 mb-6">Amplify the low-end frequencies of your audio.</p>
<div class="space-y-4">
<label for="bass-boost-gain" class="block text-lg">Bass Gain (dB)</label>
<input type="range" id="bass-boost-gain" min="0" max="20" value="10" class="w-full">
<span id="bass-boost-gain-value" class="text-teal-300">10 dB</span>
</div>
</div>
<!-- Vocal Remover Page -->
<div id="vocal-remover-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Vocal Remover</h2>
<p class="text-gray-400 mb-6">Attempt to remove vocals from a song using phase cancellation. Works best on stereo tracks with centered vocals.</p>
</div>
<!-- Pitch Shifter Page -->
<div id="pitch-shifter-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Pitch Shifter</h2>
<p class="text-gray-400 mb-6">Change the pitch of the audio without changing the tempo. (Note: Simple version also affects tempo).</p>
<div class="space-y-4">
<label for="pitch-shift-rate" class="block text-lg">Pitch (Playback Rate)</label>
<input type="range" id="pitch-shift-rate" min="0.5" max="2.0" value="1.0" step="0.05" class="w-full">
<span id="pitch-shift-rate-value" class="text-teal-300">1.0x</span>
</div>
</div>
<!-- 8D Audio Page -->
<div id="8d-audio-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">8D Audio Effect</h2>
<p class="text-gray-400 mb-6">Simulate audio moving around your head using panning and reverb.</p>
</div>
<!-- Visualizer Page -->
<div id="visualizer-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Audio Visualizer</h2>
<p class="text-gray-400 mb-6">Generate a waveform from your audio file.</p>
<canvas id="visualizer-canvas" class="w-full h-64 bg-gray-900 rounded-lg"></canvas>
</div>
<!-- NEW: Audio Enhancer Page -->
<div id="audio-enhancer-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Audio Enhancer</h2>
<p class="text-gray-400 mb-6">Improve audio clarity and loudness using a compressor and equalizer.</p>
<div class="space-y-4">
<label for="enhancer-amount" class="block text-lg">Enhancement Amount</label>
<input type="range" id="enhancer-amount" min="0" max="1" value="0.5" step="0.1" class="w-full">
<span id="enhancer-amount-value" class="text-teal-300">Medium</span>
</div>
</div>
<!-- NEW: Format Converter Page -->
<div id="format-converter-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Format Converter</h2>
<p class="text-gray-400 mb-6">Convert your audio file to a different format.</p>
<div class="space-y-4">
<label for="format-select" class="block text-lg">Target Format</label>
<select id="format-select" class="w-full bg-gray-700 border border-gray-600 rounded-lg p-2">
<option value="wav">WAV (Lossless)</option>
<option value="mp3">MP3 (Compressed)</option>
</select>
</div>
</div>
<!-- NEW: Audio Cutter Page -->
<div id="audio-cutter-page" class="tool-page hidden bg-gray-800 p-6 md:p-8 rounded-lg shadow-xl">
<button class="back-btn mb-6 bg-teal-500 hover:bg-teal-600 px-4 py-2 rounded-lg">← Back to Tools</button>
<h2 class="text-3xl font-bold mb-4 text-teal-400">Audio Cutter</h2>
<p class="text-gray-400 mb-6">Cut a section of your audio. Drag the markers on the waveform or enter times below.</p>
<canvas id="cutter-canvas" class="w-full h-64 bg-gray-900 rounded-lg cursor-ew-resize"></canvas>
<div class="flex justify-between mt-4">
<div class="w-1/2 pr-2">
<label for="start-time" class="block text-sm">Start Time (s)</label>
<input type="number" id="start-time" class="w-full bg-gray-700 rounded p-2" step="0.1" min="0">
</div>
<div class="w-1/2 pl-2">
<label for="end-time" class="block text-sm">End Time (s)</label>
<input type="number" id="end-time" class="w-full bg-gray-700 rounded p-2" step="0.1" min="0">
</div>
</div>
</div>
</div>
<!-- Shared UI for all tools -->
<div id="shared-tool-ui" class="hidden mt-8 bg-gray-800/50 p-6 rounded-lg">
<div class="flex flex-col items-center space-y-6">
<!-- File Input -->
<div id="file-input-container" class="w-full max-w-lg">
<label for="audio-file-input" class="w-full cursor-pointer bg-gray-700 hover:bg-gray-600 text-white font-bold py-3 px-4 rounded-lg inline-flex items-center justify-center transition">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-4-4V7a4 4 0 014-4h5l5 5v7a4 4 0 01-4 4H7z"></path></svg>
<span id="file-input-label">Select Audio File</span>
</label>
<input id="audio-file-input" type="file" class="hidden" accept="audio/*">
</div>
<!-- Audio Player -->
<audio id="audio-player" controls class="w-full max-w-lg rounded-lg"></audio>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4 w-full max-w-lg">
<button id="process-btn" class="w-full bg-teal-500 hover:bg-teal-600 text-white font-bold py-3 px-4 rounded-lg transition disabled:bg-gray-600 disabled:cursor-not-allowed" disabled>Process Audio</button>
<a id="download-link" class="w-full text-center bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-4 rounded-lg transition hidden">Download Processed Audio</a>
</div>
<!-- Loader and Status -->
<div id="status-container" class="w-full max-w-lg text-center h-12 flex items-center justify-center">
<div id="loader" class="loader hidden"></div>
<p id="status-message" class="text-gray-400"></p>
</div>
</div>
</div>
</div>
<script type="module">
// --- Core Application Logic ---
const tools = [
{ id: 'bass-booster', name: 'Bass Booster', icon: '🔊' },
{ id: 'vocal-remover', name: 'Vocal Remover', icon: '🎤' },
{ id: 'pitch-shifter', name: 'Pitch Shifter', icon: '튜' },
{ id: '8d-audio', name: '8D Audio', icon: '🎧' },
{ id: 'audio-enhancer', name: 'Audio Enhancer', icon: '✨' },
{ id: 'format-converter', name: 'Format Converter', icon: '🔄' },
{ id: 'audio-cutter', name: 'Audio Cutter', icon: '✂️' },
{ id: 'visualizer', name: 'Visualizer', icon: '📊' },
];
// --- DOM Elements ---
const toolSelectionGrid = document.getElementById('tool-selection');
const sharedUi = document.getElementById('shared-tool-ui');
const backButtons = document.querySelectorAll('.back-btn');
const fileInput = document.getElementById('audio-file-input');
const fileInputLabel = document.getElementById('file-input-label');
const audioPlayer = document.getElementById('audio-player');
const processBtn = document.getElementById('process-btn');
const downloadLink = document.getElementById('download-link');
const loader = document.getElementById('loader');
const statusMessage = document.getElementById('status-message');
const cutterCanvas = document.getElementById('cutter-canvas');
const startTimeInput = document.getElementById('start-time');
const endTimeInput = document.getElementById('end-time');
// --- App State ---
let audioContext;
let originalBuffer;
let activeToolId = null;
let cutterState = {
startRatio: 0,
endRatio: 1,
dragging: null // 'start', 'end', or null
};
// --- Initialization ---
function initialize() {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
try {
audioContext = new AudioContext();
} catch (e) {
alert('Web Audio API is not supported in this browser');
return;
}
renderToolGrid();
setupEventListeners();
}
function renderToolGrid() {
toolSelectionGrid.innerHTML = tools.map(tool => `
<div id="${tool.id}" class="tool-card bg-gray-800 p-4 rounded-lg text-center cursor-pointer flex flex-col items-center justify-center aspect-square">
<div class="text-4xl mb-2">${tool.icon}</div>
<h3 class="font-semibold text-sm md:text-base">${tool.name}</h3>
</div>
`).join('');
}
// --- Event Listeners ---
function setupEventListeners() {
toolSelectionGrid.addEventListener('click', (e) => {
const card = e.target.closest('.tool-card');
if (card) navigateToTool(card.id);
});
backButtons.forEach(btn => btn.addEventListener('click', navigateHome));
fileInput.addEventListener('change', handleFileSelect);
processBtn.addEventListener('click', handleProcess);
// Slider value displays
document.getElementById('bass-boost-gain').addEventListener('input', e => {
document.getElementById('bass-boost-gain-value').textContent = `${e.target.value} dB`;
});
document.getElementById('pitch-shift-rate').addEventListener('input', e => {
document.getElementById('pitch-shift-rate-value').textContent = `${parseFloat(e.target.value).toFixed(2)}x`;
});
document.getElementById('enhancer-amount').addEventListener('input', e => {
const val = parseFloat(e.target.value);
let label = 'Medium';
if (val < 0.3) label = 'Low';
else if (val > 0.7) label = 'High';
document.getElementById('enhancer-amount-value').textContent = label;
});
// Cutter listeners
setupCutterListeners();
}
// --- Navigation ---
function navigateToTool(toolId) {
activeToolId = toolId;
toolSelectionGrid.classList.add('hidden');
document.getElementById(`${toolId}-page`).classList.remove('hidden');
sharedUi.classList.remove('hidden');
resetToolState();
}
function navigateHome() {
activeToolId = null;
document.querySelectorAll('.tool-page').forEach(page => page.classList.add('hidden'));
toolSelectionGrid.classList.remove('hidden');
sharedUi.classList.add('hidden');
resetToolState();
}
function resetToolState() {
originalBuffer = null;
fileInput.value = '';
fileInputLabel.textContent = 'Select Audio File';
audioPlayer.src = '';
processBtn.disabled = true;
downloadLink.classList.add('hidden');
statusMessage.textContent = 'Please select an audio file to begin.';
// Reset canvases
[document.getElementById('visualizer-canvas'), cutterCanvas].forEach(canvas => {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
// Reset cutter state
cutterState = { startRatio: 0, endRatio: 1, dragging: null };
startTimeInput.value = 0;
endTimeInput.value = 0;
}
// --- File Handling ---
async function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
fileInputLabel.textContent = file.name.length > 25 ? file.name.substring(0, 22) + '...' : file.name;
audioPlayer.src = URL.createObjectURL(file);
processBtn.disabled = true;
downloadLink.classList.add('hidden');
setStatus('Loading audio...', true);
try {
const arrayBuffer = await file.arrayBuffer();
originalBuffer = await audioContext.decodeAudioData(arrayBuffer);
processBtn.disabled = false;
setStatus('Audio loaded. Ready to process.', false);
// If visualizer or cutter is active, draw immediately
if (activeToolId === 'visualizer') {
drawWaveform(originalBuffer, document.getElementById('visualizer-canvas'));
}
if (activeToolId === 'audio-cutter') {
startTimeInput.value = 0;
endTimeInput.value = originalBuffer.duration.toFixed(3);
cutterState.endRatio = 1;
drawCutterWaveform();
}
} catch (e) {
console.error('Error decoding audio data:', e);
setStatus('Error: Could not decode audio file.', false);
}
}
// --- Audio Processing ---
async function handleProcess() {
if (!originalBuffer || !activeToolId) {
setStatus('Error: No audio data or tool selected.', false);
return;
}
setStatus('Processing...', true);
downloadLink.classList.add('hidden');
try {
let bufferToProcess = originalBuffer;
// For cutter, we first slice the buffer
if (activeToolId === 'audio-cutter') {
const startSample = Math.floor(startTimeInput.value * originalBuffer.sampleRate);
const endSample = Math.floor(endTimeInput.value * originalBuffer.sampleRate);
const durationSamples = endSample - startSample;
if (durationSamples <= 0) throw new Error("End time must be after start time.");
const slicedBuffer = audioContext.createBuffer(
originalBuffer.numberOfChannels,
durationSamples,
originalBuffer.sampleRate
);
for (let i = 0; i < originalBuffer.numberOfChannels; i++) {
const channelData = originalBuffer.getChannelData(i);
const slicedData = channelData.subarray(startSample, endSample);
slicedBuffer.copyToChannel(slicedData, i);
}
bufferToProcess = slicedBuffer;
}
const offlineCtx = new OfflineAudioContext(
bufferToProcess.numberOfChannels,
bufferToProcess.length,
bufferToProcess.sampleRate
);
const source = offlineCtx.createBufferSource();
source.buffer = bufferToProcess;
let processorNode = source;
// --- TOOL-SPECIFIC LOGIC ---
switch (activeToolId) {
case 'bass-booster':
const gain = parseFloat(document.getElementById('bass-boost-gain').value);
const bassBooster = offlineCtx.createBiquadFilter();
bassBooster.type = 'lowshelf';
bassBooster.frequency.setValueAtTime(300, 0);
bassBooster.gain.setValueAtTime(gain, 0);
processorNode.connect(bassBooster);
processorNode = bassBooster;
break;
case 'vocal-remover':
if (bufferToProcess.numberOfChannels < 2) throw new Error("Vocal remover requires a stereo track.");
const splitter = offlineCtx.createChannelSplitter(2);
const merger = offlineCtx.createChannelMerger(1);
const gainNode = offlineCtx.createGain();
gainNode.gain.value = -1;
source.connect(splitter);
splitter.connect(merger, 0, 0);
splitter.connect(gainNode, 1);
gainNode.connect(merger, 0, 0);
processorNode = merger;
break;
case 'pitch-shifter':
source.playbackRate.value = parseFloat(document.getElementById('pitch-shift-rate').value);
break;
case '8d-audio':
if (offlineCtx.destination.maxChannelCount < 2) offlineCtx.destination.channelCount = 2;
const panner = offlineCtx.createStereoPanner();
const lfo = offlineCtx.createOscillator();
const lfoGain = offlineCtx.createGain();
lfo.type = 'sine';
lfo.frequency.value = 0.2;
lfoGain.gain.value = 1.0;
lfo.connect(lfoGain);
lfoGain.connect(panner.pan);
lfo.start(0);
const reverb = await createReverb(offlineCtx);
source.connect(panner);
panner.connect(reverb);
reverb.connect(offlineCtx.destination);
processorNode = null;
break;
case 'audio-enhancer':
const amount = parseFloat(document.getElementById('enhancer-amount').value);
// Compressor to even out dynamics
const compressor = offlineCtx.createDynamicsCompressor();
compressor.threshold.setValueAtTime(-50 + (20 * (1-amount)), 0); // -50 to -30
compressor.knee.setValueAtTime(40, 0);
compressor.ratio.setValueAtTime(12 - (8 * (1-amount)), 0); // 12 to 4
compressor.attack.setValueAtTime(0.003, 0);
compressor.release.setValueAtTime(0.25, 0);
// EQ for "smile" curve - boost lows and highs
const lowShelf = offlineCtx.createBiquadFilter();
lowShelf.type = "lowshelf";
lowShelf.frequency.value = 250;
lowShelf.gain.value = 3 * amount;
const highShelf = offlineCtx.createBiquadFilter();
highShelf.type = "highshelf";
highShelf.frequency.value = 3000;
highShelf.gain.value = 3 * amount;
source.connect(compressor);
compressor.connect(lowShelf);
lowShelf.connect(highShelf);
processorNode = highShelf;
break;
case 'format-converter':
case 'audio-cutter':
// No extra processing needed, just format conversion at the end
break;
case 'visualizer':
drawWaveform(bufferToProcess, document.getElementById('visualizer-canvas'));
setStatus('Visualization generated.', false);
return;
}
if (processorNode) {
processorNode.connect(offlineCtx.destination);
}
source.start(0);
const processedBuffer = await offlineCtx.startRendering();
// --- Output Generation ---
const format = document.getElementById('format-select')?.value || 'wav';
let blob, extension;
if (activeToolId === 'format-converter' && format === 'mp3') {
blob = bufferToMp3(processedBuffer);
extension = 'mp3';
} else {
blob = bufferToWave(processedBuffer);
extension = 'wav';
}
const url = URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.download = `processed_${activeToolId}.${extension}`;
downloadLink.classList.remove('hidden');
setStatus('Processing complete. Ready for download.', false);
} catch (e) {
console.error('Processing error:', e);
setStatus(`Error: ${e.message}`, false);
}
}
// --- Utility Functions ---
function setStatus(message, isLoading) {
statusMessage.textContent = message;
loader.classList.toggle('hidden', !isLoading);
}
async function createReverb(context) {
const convolver = context.createConvolver();
const length = context.sampleRate * 2;
const impulse = context.createBuffer(2, length, context.sampleRate);
for (let i = 0; i < impulse.numberOfChannels; i++) {
const channel = impulse.getChannelData(i);
for (let j = 0; j < length; j++) {
channel[j] = (Math.random() * 2 - 1) * Math.pow(1 - j / length, 2.5);
}
}
convolver.buffer = impulse;
return convolver;
}
function drawWaveform(buffer, canvas, options = {}) {
const {
color = '#38b2ac',
bgColor = '#1a202c',
selectionColor = 'rgba(56, 178, 172, 0.3)'
} = options;
const ctx = canvas.getContext('2d');
const data = buffer.getChannelData(0);
const width = canvas.width;
const height = canvas.height;
const step = Math.ceil(data.length / width);
const amp = height / 2;
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
// Draw selection
if (options.startRatio !== undefined && options.endRatio !== undefined) {
ctx.fillStyle = selectionColor;
ctx.fillRect(options.startRatio * width, 0, (options.endRatio - options.startRatio) * width, height);
}
ctx.lineWidth = 2;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(0, amp);
for (let i = 0; i < width; i++) {
let min = 1.0, max = -1.0;
for (let j = 0; j < step; j++) {
const datum = data[(i * step) + j];
if (datum < min) min = datum;
if (datum > max) max = datum;
}
ctx.lineTo(i, (1 + min) * amp);
}
ctx.lineTo(width, amp);
ctx.stroke();
}
// --- Cutter Logic ---
function setupCutterListeners() {
cutterCanvas.addEventListener('mousedown', e => {
if (!originalBuffer) return;
const rect = cutterCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const clickRatio = x / rect.width;
const startHandlePos = cutterState.startRatio * rect.width;
const endHandlePos = cutterState.endRatio * rect.width;
if (Math.abs(x - startHandlePos) < 10) {
cutterState.dragging = 'start';
} else if (Math.abs(x - endHandlePos) < 10) {
cutterState.dragging = 'end';
}
});
window.addEventListener('mousemove', e => {
if (!cutterState.dragging || !originalBuffer) return;
const rect = cutterCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
let ratio = x / rect.width;
ratio = Math.max(0, Math.min(1, ratio)); // Clamp between 0 and 1
if (cutterState.dragging === 'start' && ratio < cutterState.endRatio) {
cutterState.startRatio = ratio;
} else if (cutterState.dragging === 'end' && ratio > cutterState.startRatio) {
cutterState.endRatio = ratio;
}
updateCutterUI();
});
window.addEventListener('mouseup', () => {
cutterState.dragging = null;
});
startTimeInput.addEventListener('change', () => {
if (!originalBuffer) return;
const newStart = parseFloat(startTimeInput.value);
if (newStart >= 0 && newStart < endTimeInput.value) {
cutterState.startRatio = newStart / originalBuffer.duration;
drawCutterWaveform();
} else {
startTimeInput.value = (cutterState.startRatio * originalBuffer.duration).toFixed(3);
}
});
endTimeInput.addEventListener('change', () => {
if (!originalBuffer) return;
const newEnd = parseFloat(endTimeInput.value);
if (newEnd > startTimeInput.value && newEnd <= originalBuffer.duration) {
cutterState.endRatio = newEnd / originalBuffer.duration;
drawCutterWaveform();
} else {
endTimeInput.value = (cutterState.endRatio * originalBuffer.duration).toFixed(3);
}
});
}
function updateCutterUI() {
startTimeInput.value = (cutterState.startRatio * originalBuffer.duration).toFixed(3);
endTimeInput.value = (cutterState.endRatio * originalBuffer.duration).toFixed(3);
drawCutterWaveform();
}
function drawCutterWaveform() {
if (!originalBuffer) return;
drawWaveform(originalBuffer, cutterCanvas, {
startRatio: cutterState.startRatio,
endRatio: cutterState.endRatio
});
}
// --- Format Conversion Functions ---
function bufferToWave(abuffer) {
let numOfChan = abuffer.numberOfChannels,
length = abuffer.length * numOfChan * 2 + 44,
buffer = new ArrayBuffer(length),
view = new DataView(buffer),
channels = [], i, sample, offset = 0, pos = 0;
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); setUint16(1); setUint16(numOfChan);
setUint32(abuffer.sampleRate);
setUint32(abuffer.sampleRate * 2 * numOfChan);
setUint16(numOfChan * 2); setUint16(16);
setUint32(0x61746164); // "data" - chunk
setUint32(length - pos - 4);
for (i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i));
while (pos < length) {
for (i = 0; i < numOfChan; i++) {
sample = Math.max(-1, Math.min(1, channels[i][offset]));
sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0;
view.setInt16(pos, sample, true); pos += 2;
}
offset++;
}
return new Blob([buffer], { type: "audio/wav" });
function setUint16(data) { view.setUint16(pos, data, true); pos += 2; }
function setUint32(data) { view.setUint32(pos, data, true); pos += 4; }
}
function bufferToMp3(abuffer) {
const mp3encoder = new lamejs.Mp3Encoder(abuffer.numberOfChannels, abuffer.sampleRate, 128); // 128 kbps
const samples = new Int16Array(abuffer.length);
// This assumes mono for simplicity, a stereo implementation would interleave channels.
const channelData = abuffer.getChannelData(0);
let sampleIndex = 0;
for (let i = 0; i < abuffer.length; i++) {
samples[sampleIndex++] = channelData[i] * 32767;
}
let mp3Data = [];
const sampleBlockSize = 1152;
for (let i = 0; i < samples.length; i += sampleBlockSize) {
const sampleChunk = samples.subarray(i, i + sampleBlockSize);
const mp3buf = mp3encoder.encodeBuffer(sampleChunk);
if (mp3buf.length > 0) {
mp3Data.push(mp3buf);
}
}
const mp3buf = mp3encoder.flush();
if (mp3buf.length > 0) {
mp3Data.push(mp3buf);
}
return new Blob(mp3Data, { type: 'audio/mp3' });
}
// --- Start the App ---
initialize();
</script>