前言
由于之前朋友玩词典笔发现词典笔的音乐倍速会改变音调,而现在网络上的工具均不会改变音调,故写了一个html倍速处理工具
(其实就是最原始的倍速处理方法,只是效果太有「戏剧性」被网友玩坏了)
源码
<!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>
Comments NOTHING