背景是这样的,最近接到这么一个需求,用户上传音频文件后,想要实现音乐编辑器那种效果,展示音频波形,截取其中的音频片段,网上查询了一些常用的实现方式,中间也遇到了一些坑,不过最终还是实现了项目需求。下面记录下我的实现过程。
1、使用到的插件
wavesurfer.js。这个轻量级但功能强大的JavaScript库,能让你在短短几分钟内,将一个静态的音频文件链接,变成一个交互式、可定制的音频可视化组件。无论你是想为播客网站添加一个精致的播放器,还是为音乐教学应用集成频谱分析,甚至是构建一个内部的音频审核工具,wavesurfer.js都能成为你项目中的得力助手。
2、快速引入
wavesurfer可以通过npm安装方式,也可以直接在html引入,我采用的是html页面直接引入
js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>音频波形裁切工具</title>
<!-- 核心依赖 -->
<script src="https://unpkg.com/wavesurfer.js@7"></script>
<!-- 选区插件 -->
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
<!-- 时间线插件 -->
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/timeline.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/hover.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
max-width: 1000px;
margin: 30px auto;
padding: 0 20px;
font-family: Arial, sans-serif;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
#waveform {
width: 100%;
height: 200px;
border: 1px solid #eee;
border-radius: 8px;
}
#cut-waveform {
width: 50%;
height: 100px;
border: 1px solid #eee;
border-radius: 8px;
}
.btn-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
button {
padding: 10px 18px;
border: none;
border-radius: 6px;
background: #409eff;
color: white;
font-size: 14px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.tip {
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h2>音频波形可视化 & 裁切工具</h2>
<input type="file" id="audioFile" accept="audio/*" />
<div id="waveform"></div>
<div class="tip">👉 鼠标拖动波形即可选择裁切区域</div>
<div>裁切音频:</div>
<div id="cut-waveform"></div>
<div class="btn-group">
<button id="playBtn">播放/暂停</button>
<button id="cutBtn" disabled>裁切选中区域</button>
<button id="exportBtn" disabled>导出音频</button>
</div>
</div>
<script>
</script>
</body>
</html>
3、初始化波形播放器
js
const regions = WaveSurfer.Regions.create()
const timeLine = WaveSurfer.Timeline.create()
const hover = WaveSurfer.Hover.create()
// 初始化波形播放器
const wavesurfer = WaveSurfer.create({
container: '#waveform',
waveColor: '#8b9eff',
progressColor: '#409eff',
cursorColor: '#ff4d4f',
responsive: true,
barWidth: 2,
barRadius: 3,
cursorWidth: 2,
height: 200,
barGap: 2,
dragToSeek: true, // 是否允许拖动拖动播放头
plugins: [
regions,
timeLine,
hover
]
})
// 裁切音频
const cutWaveform = WaveSurfer.create({
container: '#cut-waveform',
waveColor: '#8b9eff',
progressColor: '#409eff',
cursorColor: '#ff4d4f',
responsive: true,
barWidth: 2,
barRadius: 3,
cursorWidth: 2,
height: 100,
barGap: 2,
dragToSeek: true, // 是否允许拖动拖动播放头
plugins: [
WaveSurfer.Timeline.create()
]
})
4、上传音频文件,利用wavesurfer实现音频加载
js
document.getElementById('audioFile').addEventListener('change', (e) => {
const file = e.target.files[0]
if (!file) return
audioBlob = file // 全局保存,方便其他地方使用音频数据
wavesurfer.loadBlob(file)
})
// 音频加载完成
wavesurfer.on('ready', () => {
document.getElementById('cutBtn').disabled = false
document.getElementById('exportBtn').disabled = false
// Regions
regions.addRegion({
start: 0,
end: 1,
content: '',
color: 'rgba(64, 158, 255, 0.2)',
drag: false, // 是否允许拖动选区
resize: true, // 是否允许调整选区大小
loop: true // 是否循环播放选区
})
})
上面使用了regions插件,这个插件是wavesurfer的扩展插件。
Regions插件的主要功能:
- 区域选择:允许用户在音频波形上选择特定的时间段,创建可交互的区域。
- 可视化标记:在波形上高亮显示选定的区域,便于用户识别裁切范围。
- 交互操作:支持拖拽、调整大小等操作来修改选中的区域。
- 事件监听 :提供各种事件回调,如
region-updated、region-double-clicked等,方便开发者处理用户的交互行为。
当用户选择好要裁切的音频时间段后,可以监听regions的region-updated事件,用于获取选择音频的信息。
js
regions.on('region-updated', (region) => {
currentRegion = currentRegion || region
})
5、裁切音频
js
document.getElementById('cutBtn').onclick = async () => {
if (!currentRegion) {
alert('请先拖动选择裁切区域!')
return
}
regions.clearRegions()
const { start, end } = currentRegion
audioBlob = await cutAudio(audioBlob, start, end)
cutWaveform.loadBlob(audioBlob)
cutWaveform.playPause()
// alert('裁切成功!')
}
async function cutAudio(blob, startTime, endTime) {
const arrayBuffer = await blob.arrayBuffer()
console.log('arrayBuffer', arrayBuffer)
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
const sampleRate = audioBuffer.sampleRate
const startOffset = startTime * sampleRate
const endOffset = endTime * sampleRate
const channelCount = audioBuffer.numberOfChannels
const frameCount = endOffset - startOffset
console.log('采样率', sampleRate)
// 创建新的音频缓冲区
const newAudioBuffer = audioContext.createBuffer(
channelCount,
frameCount,
sampleRate
)
// 创建临时的Array存放复制的buffer数据
var anotherArray = new Float32Array(frameCount);
// 声道的数据的复制和写入
var offset = 0;
for (var channel = 0; channel < channelCount; channel++) {
audioBuffer.copyFromChannel(anotherArray, channel, startOffset);
newAudioBuffer.copyToChannel(anotherArray, channel, offset);
}
return audioBufferToWavBlob(newAudioBuffer)
}
// 修复后的音频转 WAV Blob 函数
function audioBufferToWavBlob(audioBuffer) {
const length = audioBuffer.length;
const channels = audioBuffer.numberOfChannels;
const sampleRate = audioBuffer.sampleRate;
// 计算数据块大小和总文件大小
const bytesPerSample = 2; // 16位采样
const totalSamples = length * channels;
const totalDataBytes = totalSamples * bytesPerSample;
// 创建包含WAV头部和数据的数组
const buffer = new ArrayBuffer(44 + totalDataBytes);
const view = new DataView(buffer);
// 写入WAV头部
const writeString = (offset, str) => {
for (let i = 0; i < str.length; i++) {
view.setUint8(offset + i, str.charCodeAt(i));
}
};
const writeUint32 = (offset, value) => {
view.setUint32(offset, value, true); // true表示小端字节序
};
const writeUint16 = (offset, value) => {
view.setUint16(offset, value, true);
};
// RIFF块描述符
writeString(0, 'RIFF');
writeUint32(4, 36 + totalDataBytes); // 文件总大小 - 8
writeString(8, 'WAVE');
// fmt子块
writeString(12, 'fmt ');
writeUint32(16, 16); // 子块大小
writeUint16(20, 1); // 音频格式 (PCM)
writeUint16(22, channels); // 声道数
writeUint32(24, sampleRate); // 采样率
writeUint32(28, sampleRate * channels * bytesPerSample); // 字节率
writeUint16(32, channels * bytesPerSample); // 块对齐
writeUint16(34, 16); // 位深度 (修正为16位)
// data子块
writeString(36, 'data');
writeUint32(40, totalDataBytes); // 数据块大小
// 写入音频数据
let offset = 44;
for (let i = 0; i < length; i++) {
for (let channel = 0; channel < channels; channel++) {
const sample = Math.max(-1, Math.min(1, audioBuffer.getChannelData(channel)[i]));
const intSample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(offset, intSample, true);
offset += 2;
}
}
return new Blob([buffer], { type: 'audio/wav' });
}
上面截取音频里面用到了copyFromChannel和copyToChannel,在Web Audio API中,copyFromChannel和copyToChannel是用于操作音频缓冲区通道数据的方法。
copyFromChannel 方法
scss
audioBuffer.copyFromChannel(destination, channelNumber, startInChannel)
参数说明:
destination: 一个Float32Array数组,用于接收从音频缓冲区通道复制的数据channelNumber: 要复制的通道索引(从0开始)startInChannel: 可选参数,指定从通道的哪个位置开始复制,默认为0
作用: 将音频缓冲区中指定通道的数据复制到目标Float32Array数组中
copyToChannel 方法
bash
audioBuffer.copyToChannel(source, channelNumber, startInChannel)
参数说明:
source: 一个Float32Array数组,包含要复制到音频缓冲区通道的数据channelNumber: 目标通道索引(从0开始)startInChannel: 可选参数,指定从通道的哪个位置开始写入,默认为0
作用: 将Float32Array数组中的数据复制到音频缓冲区的指定通道
结语
上面这个是我这边在接到产品需求后通过调研踩坑捣鼓出来的这个demo,但是里面想要实现的功能大体都已经实现了,剩下的样式微调以及功能扩展可以直接参考waveSurfer的官方文档。