音频倍速处理工具

yuze 发布于 6 天前 24 次阅读


前言

由于之前朋友玩词典笔发现词典笔的音乐倍速会改变音调,而现在网络上的工具均不会改变音调,故写了一个html倍速处理工具

(其实就是最原始的倍速处理方法,只是效果太有「戏剧性」被网友玩坏了)

源码

可前往https://audio.baigei.cc尝试

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>音频倍速处理器</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
    <style>
        /* 毛玻璃效果核心样式 */
        body {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .glass-container {
            background: rgba(255, 255, 255, 0.08);
            backdrop-filter: blur(15px);
            -webkit-backdrop-filter: blur(15px);
            border-radius: 20px;
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            padding: 2rem;
            margin: 2rem auto;
            max-width: 800px;
        }

        .glass-control {
            background: rgba(255, 255, 255, 0.1) !important;
            border: 1px solid rgba(255, 255, 255, 0.2) !important;
            color: white !important;
            backdrop-filter: blur(5px);
        }

        .glass-control:focus {
            background: rgba(255, 255, 255, 0.15) !important;
            box-shadow: none !important;
        }

        .glass-button {
            background: rgba(255, 255, 255, 0.15) !important;
            border: 1px solid rgba(255, 255, 255, 0.25) !important;
            color: white !important;
            transition: all 0.3s ease;
        }

        .glass-button:hover {
            background: rgba(255, 255, 255, 0.25) !important;
            transform: translateY(-1px);
        }

        #status {
            color: rgba(255, 255, 255, 0.8);
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            margin: 1rem 0;
            min-height: 24px;
        }

        /* 文件选择器美化 */
        .form-control::file-selector-button {
            background: rgba(255, 255, 255, 0.1);
            border: none;
            color: white;
            padding: 0.375rem 0.75rem;
            margin-right: 1rem;
        }
    </style>
</head>
<body>
    <div class="glass-container">
        <!-- 状态提示 -->
        <div id="status" class="small mb-3">准备就绪</div>

        <!-- 文件上传 -->
        <div class="mb-4">
            <input type="file" 
                   class="form-control glass-control" 
                   id="audioInput" 
                   accept="audio/*">
        </div>

        <!-- 控制面板 -->
        <div class="row g-3 align-items-center mb-4">
            <div class="col-md-5">
                <div class="input-group">
                    <input type="number" 
                           id="speed" 
                           class="form-control glass-control"
                           min="0.5" 
                           max="4" 
                           step="0.1" 
                           value="1.0" 
                           required>
                    <span class="input-group-text glass-control">倍速</span>
                </div>
            </div>
            
            <div class="col-md-7 d-flex gap-2">
                <button class="btn glass-button flex-grow-1" 
                        onclick="togglePlayback()">
                    ▶ 播放/暂停
                </button>
                <button id="downloadBtn" 
                        class="btn glass-button flex-grow-1" 
                        onclick="downloadProcessed()" 
                        disabled>
                    ⬇ 下载
                </button>
            </div>
        </div>
    </div>
    <div class="glass-container"><img src="./furinatips.png" width="18%" style="color:white;">全部音频只在本地处理</div>
    
    <script>
        // 音频处理核心逻辑
        let audioContext;
        let audioBuffer;
        let activeSource;
        let isPlaying = false;
        let originalExtension = '';
        let originalFilename = '';

        // 状态提示
        function showStatus(text, isError = false) {
            const statusEl = document.getElementById('status');
            statusEl.textContent = text;
            statusEl.style.color = isError ? '#ff6b6b' : 'rgba(255,255,255,0.8)';
        }

        // 文件处理
        document.getElementById('audioInput').addEventListener('change', async (e) => {
            const file = e.target.files[0];
            if (!file) return;

            showStatus('文件加载中...');
            try {
                originalFilename = file.name.replace(/\.[^/.]+$/, "");
                originalExtension = file.name.split('.').pop().toLowerCase();

                if (!audioContext) {
                    audioContext = new (window.AudioContext || window.webkitAudioContext)();
                }

                const arrayBuffer = await file.arrayBuffer();
                audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
                
                document.getElementById('downloadBtn').disabled = false;
                showStatus(`${file.name} 已加载`);
            } catch (error) {
                console.error('文件处理失败:', error);
                showStatus(`错误: ${error.message}`, true);
                document.getElementById('downloadBtn').disabled = true;
            }
        });

        // 播放控制
        async function togglePlayback() {
            if (!audioBuffer) {
                showStatus('请先选择音频文件', true);
                return;
            }

            try {
                if (isPlaying) {
                    stopPlayback();
                    return;
                }

                if (audioContext.state === 'suspended') {
                    await audioContext.resume();
                }

                activeSource = audioContext.createBufferSource();
                activeSource.buffer = audioBuffer;
                activeSource.playbackRate.value = parseFloat(document.getElementById('speed').value);
                activeSource.connect(audioContext.destination);
                
                activeSource.onended = () => {
                    isPlaying = false;
                    showStatus('播放结束');
                };

                activeSource.start();
                isPlaying = true;
                showStatus('播放中...');
            } catch (error) {
                console.error('播放失败:', error);
                showStatus(`播放错误: ${error.message}`, true);
                isPlaying = false;
            }
        }

        function stopPlayback() {
            if (activeSource) {
                activeSource.stop();
                activeSource.disconnect();
                activeSource = null;
            }
            isPlaying = false;
            showStatus('已停止');
        }

        // 下载处理
        async function downloadProcessed() {
            if (!audioBuffer) {
                showStatus('请先选择音频文件', true);
                return;
            }

            showStatus('处理中...');
            try {
                const speed = parseFloat(document.getElementById('speed').value);
                const offlineContext = new OfflineAudioContext(
                    audioBuffer.numberOfChannels,
                    Math.ceil(audioBuffer.length / speed),
                    audioBuffer.sampleRate
                );

                const source = offlineContext.createBufferSource();
                source.buffer = audioBuffer;
                source.playbackRate.value = speed;
                source.connect(offlineContext.destination);
                source.start();

                const renderedBuffer = await offlineContext.startRendering();
                const wavData = audioBufferToWav(renderedBuffer);
                
                const blob = new Blob([wavData], { type: 'audio/wav' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `${originalFilename}_${speed}x.${originalExtension}`;
                a.click();
                
                setTimeout(() => {
                    URL.revokeObjectURL(url);
                    showStatus('下载完成');
                }, 100);
            } catch (error) {
                console.error('下载失败:', error);
                showStatus(`处理失败: ${error.message}`, true);
            }
        }

        // WAV转换工具
        function audioBufferToWav(buffer) {
            const numChannels = buffer.numberOfChannels;
            const sampleRate = buffer.sampleRate;
            const length = buffer.length;
            const bytesPerSample = 2;
            const blockAlign = numChannels * bytesPerSample;

            const arrayBuffer = new ArrayBuffer(44 + (length * blockAlign));
            const view = new DataView(arrayBuffer);

            // 头部信息
            writeString(view, 0, 'RIFF');
            view.setUint32(4, 36 + length * blockAlign, true);
            writeString(view, 8, 'WAVE');
            writeString(view, 12, 'fmt ');
            view.setUint32(16, 16, true);
            view.setUint16(20, 1, true);
            view.setUint16(22, numChannels, true);
            view.setUint32(24, sampleRate, true);
            view.setUint32(28, sampleRate * blockAlign, true);
            view.setUint16(32, blockAlign, true);
            view.setUint16(34, bytesPerSample * 8, true);
            writeString(view, 36, 'data');
            view.setUint32(40, length * blockAlign, true);

            // PCM数据写入
            let offset = 44;
            for (let i = 0; i < length; i++) {
                for (let ch = 0; ch < numChannels; ch++) {
                    const sample = Math.max(-1, Math.min(1, buffer.getChannelData(ch)[i]));
                    view.setInt16(offset, sample * 0x7FFF, true);
                    offset += 2;
                }
            }

            return arrayBuffer;
        }

        function writeString(view, offset, string) {
            for (let i = 0; i < string.length; i++) {
                view.setUint8(offset + i, string.charCodeAt(i));
            }
        }
    </script>
</body>
</html>
此作者没有提供个人介绍。
最后更新于 2025-04-13