web端实现音频波形分析以及音频截取

背景是这样的,最近接到这么一个需求,用户上传音频文件后,想要实现音乐编辑器那种效果,展示音频波形,截取其中的音频片段,网上查询了一些常用的实现方式,中间也遇到了一些坑,不过最终还是实现了项目需求。下面记录下我的实现过程。

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插件的主要功能:

  1. 区域选择:允许用户在音频波形上选择特定的时间段,创建可交互的区域。
  2. 可视化标记:在波形上高亮显示选定的区域,便于用户识别裁切范围。
  3. 交互操作:支持拖拽、调整大小等操作来修改选中的区域。
  4. 事件监听 :提供各种事件回调,如region-updatedregion-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中,copyFromChannelcopyToChannel是用于操作音频缓冲区通道数据的方法。

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的官方文档。

相关推荐
前端那点事1 小时前
别再乱用Vue3路由!useRoute/useRouter传参、跳转、避坑最全实战指南
前端
LIO2 小时前
深度解析 localStorage 与 sessionStorage:用法、区别与最佳实践
前端
Amy_yang2 小时前
uni-app 中 web-view 的使用与 App 端全屏问题处理
前端·javascript·vue.js
闲坐含香咀翠2 小时前
Electron 加载原生模块总崩溃?搞懂这两行配置就够了
前端·electron·客户端
拉拉肥_King2 小时前
pc端视频压缩:FFmpeg.wasm 实战指南
前端
0x862 小时前
基于 Dio 实现 SSE 流式通信
前端
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_40:(DOMImplementation 接口完全解析)
前端·ui·html·媒体
Highcharts.js3 小时前
Highcharts 纯 JavaScript 图表库深度使用评测
开发语言·前端·javascript·功能测试·ecmascript·highcharts·技术评测