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

相关推荐
Henrii_历小海23 分钟前
WhatsApp Web 不是手机镜像:多设备协议、本地数据流与出海获客流水线
前端·智能手机
ayqy贾杰30 分钟前
Claude Fable 5 提示词泄漏,抓紧学习下
前端·后端·面试
To_OC9 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic9 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy9 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志10 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble11 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯12 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜12 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司12 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能