Free Audio Enhancer And Formet Converter



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">&larr; 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">&larr; 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">&larr; 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">&larr; 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">&larr; 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">&larr; 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">&larr; 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">&larr; 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>