纯JS实现多个音频的拼接或者合并

这篇文章发布于 2023年10月10日,星期二,22:53,归类于 JS实例。 阅读 16549 次, 今日 1 次 10 条评论

 

音频波形封面占位图

一、温故而知新

3年前有更新过JS对audio音频剪裁的实现,昨天更新了JS改变原始音频信息来实现音量的调整,今天我们再讲讲如何使用JS实现多个音频的拼接(下图1)与合并(下图2)。

拼接:拼接

合并:合并

这下子,纯JS操作MP3/WAV音频资源的活儿都齐全了。

操作过程本质上大同小异。

ArrayBuffer转AudioBuffer,然后读取音频信息,对采样信息进行处理。

话不多说,来看看如何实现的。

二、多个音频的拼接

方法如下:

// 拼接音频的方法
const concatAudio = (arrBufferList) => {
    // 获得 AudioBuffer
    const audioBufferList = arrBufferList;
    // 最大通道数
    const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels));
    // 总长度
    const totalLength = audioBufferList.map((buffer) => buffer.length).reduce((lenA, lenB) => lenA + lenB, 0);

    // 创建一个新的 AudioBuffer
    const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, totalLength, audioBufferList[0].sampleRate);
    // 将所有的 AudioBuffer 的数据拷贝到新的 AudioBuffer 中
    let offset = 0;

    audioBufferList.forEach((audioBuffer, index) => {
        for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
            newAudioBuffer.getChannelData(channel).set(audioBuffer.getChannelData(channel), offset);
        }

        offset += audioBuffer.length;
    });

    return newAudioBuffer;
}

语法:

concatAudio(arrBufferList);

其中:

arrBufferList
指需要合并的AudioBuffer数组。

那如何获得音频的AudioBuffer数据呢?

可以使用 new AudioContext().decodeAudioData(arrayBuffer) 方法转换。

那如何得到arrayBuffer数据呢?

本地资源可以使用FileReader对象读取为ArrayBuffer,在线资源可以使用fetch请求获取。

这里演示使用Fetch API得到音频的AudioBuffer数据。

// AudioContext
const audioContext = new AudioContext();
// 基于src地址获得 AudioBuffer 的方法
const getAudioBuffer = (src) => {
    return new Promise((resolve, reject) => {
        fetch(src).then(response => response.arrayBuffer()).then(arrayBuffer => {
            audioContext.decodeAudioData(arrayBuffer).then(buffer => {
                resolve(buffer);
            });
        })
    })
}

假设现在有下面两个音频地址:

const audioSrc = [
    './assets/1.wav',
    './assets/2.wav'
];

则使用下面几行JS就可以实现得到concat拼接后的新的音频的AudioBuffer数据了。

const arrBufferList = await Promise.all(audioSrc.map(src => getAudioBuffer(src)));

有了AudioBuffer数据,我们就可以播放音频,或者转为可下载的音频格式。

眼见为实,您可以狠狠地点击这里:纯JS实现音频的合并或拼接demo

点击演示页面中的“拼接”按钮,如下图所示,此时,就可以看到在下面的色块区域内显示了拼接后的音频了:

音频拼接demo示意

三、多个音频的合并

音频的合并指的是多个音频同时播放,但是是合在一个视频中。

我也弄了个JS函数方法:

// 合并音频的方法
const mergeAudio = (arrBufferList) => {
    // 获得 AudioBuffer
    const audioBufferList = arrBufferList;
    // 最大播放时长
    const maxDuration = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.duration));
    // 最大通道数
    const maxChannelNumber = Math.max(...audioBufferList.map(audioBuffer => audioBuffer.numberOfChannels));
    // 创建一个新的 AudioBuffer
    const newAudioBuffer = audioContext.createBuffer(maxChannelNumber, audioBufferList[0].sampleRate * maxDuration, audioBufferList[0].sampleRate);
    // 将所有的 AudioBuffer 的数据合并到新的 AudioBuffer 中
    audioBufferList.forEach((audioBuffer, index) => {
        for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
            const outputData = newAudioBuffer.getChannelData(channel);
            const bufferData = audioBuffer.getChannelData(channel);

            for (let i = audioBuffer.getChannelData(channel).length - 1; i >= 0; i--) {
                outputData[i] += bufferData[i];
            }

            newAudioBuffer.getChannelData(channel).set(outputData);
        }
    });

    return newAudioBuffer;
}

其中:

arrBufferList
指需要合并的AudioBuffer数组。

mergeAudio 方法的语法和具体的使用和上面的 concatAudio 方法类似,这里就不再赘述了。

demo也是同一个页面:纯JS实现音频的合并或拼接demo

点击“合并”按钮,就可以得到背景音乐和段落音频合二为一后的新的音频了(右键播放器可以下载此视频):

合并音频播放示意

完整的实现代码均在demo页面上。

相信国内没有比我这里更直观更容易上手的演示代码了。

四、使用开源的项目

如果你对原生实现不怎么感兴趣,也可以使用开源项目。

其实还挺多的,之前找了一个,没记住,忘记了。

那就用这个项目吧,名字奇奇怪怪的,叫做crunker:https://github.com/jaggad/crunker

使用示意:

let crunker = new Crunker();

crunker
  .fetchAudio('/song.mp3', '/another-song.mp3')
  .then((buffers) => {
    // => [AudioBuffer, AudioBuffer]
    return crunker.mergeAudio(buffers);
  })
  .then((merged) => {
    // => AudioBuffer
    return crunker.export(merged, 'audio/mp3');
  })
  .then((output) => {
    // => {blob, element, url}
    crunker.download(output.blob);
    document.body.append(output.element);
    console.log(output.url);
  })
  .catch((error) => {
    // => Error Message
  });

如果你已经有了 AudioBuffer 资源,上面的 fetchAudio() 方法也是可以省略的。

另外,此JS还支持使用静音填充视频……嗯,直接剪裁不香么~

五、结束啦下节预告

好了,正文结束。

最近项目有用到,记录下,即方便以后的自己,也方便遇到类似需求的大家,毕竟国内类似的教程资源可不多见。

然后,预告下下篇文章内容,我都已经想好了。

已知 Shader、frag、vert 等资源,如何使用类似 P5.js 这样的JS库给图片资源应用对应的滤镜效果。

技术的成长就是这样子的,一点一点积累出来的。

三五年前,我看Web Audio API的时候,感觉是天书,妈呀,这么多概念,哪个是哪个啊。

但是接触多了,一个需求一个需求慢慢实现下来,反复了解其整个API体系,自然就懂得多了,遇到需求也就知道大致的实现思路。

之前玩过JS解析 3D LUT 滤镜,不过那还是2D层面,虽然效果好,但是性能不太行,所以,这次试试WebGL,之前还没做过类似的实践,好期待哦。

你好坏哦,我好喜欢

(本篇完)

分享到:


发表评论(目前10 条评论)

  1. xhlmr说道:

    网络音频文件出现跨域问题怎么解决?

  2. 王一说道:

    合并音频的时候 如果第二个音频想从第2秒开始播放 怎么弄呢?

  3. JS不加瓦说道:

    哇正好有些小需求、正好又是前端出身,
    本来是在搜音频拼接app、突然就来到这里!还围观到了新鲜热乎的大佬O.o

  4. 不羡仙说道:

    最近做了一个需求,和你的实现99.99%是一样的,只有一点不一样:在小程序端实现。
    小程序一次录音最大只能10分钟,产品要一个60分钟的录音,于是录6个音频在小程序端合成,结果就是小程序闪退。。。[哭]
    大佬,有啥办法不?[可怜]