查看原文
其他

【第2019期】JS纯前端实现audio音频剪裁剪切复制播放与上传

张鑫旭 前端早读课 2022-07-31

前言

还有这种技巧。今日早读文章由@张鑫旭授权分享。

正文从这开始~~

背景是这样的,用户上传音频文件,可能只需要几十秒就够了,但是常规的音乐都要3~5分钟,80%的流量都是不需要的,要是就这么传上去,其实是流量的浪费,如果可以在前端就进行剪裁,也就是只取前面一段时间的音频,岂不是可以给公司省很多流量费用,前端的业务价值就体现了。

关键如何实现呢?

下面,就以“截取用户上传音频前3秒内容”的需求示意下如何借助Web Audio API实现音频的部分复制与播放功能。

不哔哔,直接正题

实现步骤如下。

File对象转ArrayBuffer

在Web网页中,用户选择的文件是个file对象,我们可以将这个文件对象转换成Blob、ArrayBuffer或者Base64。

在音频处理这里,都是使用ArrayBuffer这个数据类型。

代码如下所示,假设file类型的文件选择框的id是'file'。

  1. file.onchange = function (event) {

  2. var file = event.target.files[0];

  3. // 开始识别

  4. var reader = new FileReader();

  5. reader.onload = function (event) {

  6. var arrBuffer = event.target.result;

  7. // arrBuffer就是包含音频数据的ArrayBuffer对象

  8. });

  9. reader.readAsArrayBuffer(file);

  10. };

使用的是readAsArrayBuffer()方法,无论是MP3格式、OGG格式还是WAV格式,都可以转换成ArrayBuffer类型。

ArrayBuffer转AudioBuffer

这里的ArrayBuffer相对于把音频文件数组化了,大家可以理解为把音频文件分解成一段一段的,塞进了一个一个有地址的小屋子里,在计算机领域称为“缓冲区”,就是单词Buffer的意思。

所谓音频的剪裁,其实就是希望可以复制音频前面一段时间的内容。

但是问题来了,ArrayBuffer里面的数据并没有分类,统一分解了,想要准确提取某一截音频数据,提取不出来。

所以,才需要转换成AudioBuffer,纯粹的音频数据,方便提取。

AudioBuffer是一个仅仅包含音频数据的数据对象,是Web Audio API中的一个概念。

既然说到了Web Audio API,那我们就顺便……顺便……,想了想,还是不展开,因为太庞杂了,这Web Audio API至少比Web Animation API复杂了10倍,API之多,体量之大,世间罕见,想要完全吃透了,没有三年五载,啃不下来。

如果大家不是想要立志成为音视频处理专家,仅仅是临时解决一点小毛小病的问题,则不必深入,否则脑坑疼,使用MDN文档中的一些案例东拼西凑,基本的效果也能弄出来。

扯远了,回到这里。

AudioBuffer大家可以理解为音乐数据,那为什么叫AudioBuffer,不叫AudioData呢?

因为Buffer是个专有名词,直译为缓冲区,大家可以理解为高速公路,AudioBuffer处理数据更快,而且还有很多延伸的API,就像是高速公路上的服务区,有吃有喝还有加油的地方。

AudioData一看名字就是乡下土鳖,虽然接地气,但是,处理好几兆的数据的时候,就有些带不动了,就好像骑小电驴,在公速公路和乡道县道没多大区别,但是如果是开跑车,啧啧,乡下路就带不动了。

如何才能转换成AudioBuffer呢?

使用AudioContext对象的decodeAudioData()方法,代码如下:

  1. var audioCtx = new AudioContext();


  2. audioCtx.decodeAudioData(arrBuffer, function(audioBuffer) {

  3. // audioBuffer就是AudioBuffer

  4. });

复制AudioBuffer前3秒数据

AudioBuffer对象是一个音频专用Buffer对象,包含很多音频信息,包括:

  • 音频时长 duration

  • 声道数量 numberOfChannels

  • 采样率 sampleRate

等。

包括一些音频声道数据处理方法,例如:

  • 获取通道数据 getChannelData()

  • 复制通道数据 copyFromChannel()

  • 写入通道数据 copyToChannel()

文档见这里:https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer

所以,实现的原理很简单,创建一个空的AudioBuffer,复制现有的通道数据前3秒的数据,然后复制的内容写入到这个空的AudioBuffer,于是我们就得到了一个剪裁后的音频Buffer数据了。

代码如下:

  1. // 声道数量和采样率

  2. var channels = audioBuffer.numberOfChannels;

  3. var rate = audioBuffer.sampleRate;


  4. // 截取前3秒

  5. var startOffset = 0;

  6. var endOffset = rate * 3;

  7. // 3秒对应的帧数

  8. var frameCount = endOffset - startOffset;


  9. // 创建同样采用率、同样声道数量,长度是前3秒的空的AudioBuffer

  10. var newAudioBuffer = new AudioContext().createBuffer(channels, endOffset - startOffset, rate);

  11. // 创建临时的Array存放复制的buffer数据

  12. var anotherArray = new Float32Array(frameCount);

  13. // 声道的数据的复制和写入

  14. var offset = 0;

  15. for (var channel = 0; channel < channels; channel++) {

  16. audioBuffer.copyFromChannel(anotherArray, channel, startOffset);

  17. newAudioBuffer.copyToChannel(anotherArray, channel, offset);

  18. }


  19. // newAudioBuffer就是全新的复制的3秒长度的AudioBuffer对象

上面JavaScript代码中的变量newAudioBuffer就是全新的复制的3秒长度的AudioBuffer对象。

使用newAudioBuffer做点什么?

其实应该是有了AudioBuffer对象后我们可以做点什么。

能做很多事情。

如果希望直接播放

我们可以直接把AudioBuffer的数据作为音频数据进行播放

  1. // 创建AudioBufferSourceNode对象

  2. var source = audioCtx.createBufferSource();

  3. // 设置AudioBufferSourceNode对象的buffer为复制的3秒AudioBuffer对象

  4. source.buffer = newAudioBuffer;

  5. // 这一句是必须的,表示结束,没有这一句没法播放,没有声音

  6. // 这里直接结束,实际上可以对结束做一些特效处理

  7. source.connect(audioCtx.destination);

  8. // 资源开始播放

  9. source.start();

如果希望在 <audio>元素中播放

这个还挺麻烦的。

<audio>的src属性获取音频资源,再进行处理是简单的,网上的案例也很多。

但是,想要处理后的AudioBuffer再变成src让 <audio>元素播放,嘿嘿,就没那么容易了。

我找了一圈,没有看到Web Audio API中有专门的“逆转录”方法。

唯一可行的路数就是根据AudioBuffer数据,重新构建原始的音频数据。研究了一番,转成WAV格式相对容易,想要转换成MP3格式比较麻烦,这里有个项目:https://github.com/higuma/mp3-lame-encoder-js 不过自己没验证过,不过看代码量,还挺惊人的。

因此,我们的目标还是转到WAV音频文件生成上吧,下面这段方法是从网上找的AudioBuffer转WAV文件的方法,以Blob数据格式返回。

  1. // Convert AudioBuffer to a Blob using WAVE representation

  2. function bufferToWave(abuffer, len) {

  3. var numOfChan = abuffer.numberOfChannels,

  4. length = len * numOfChan * 2 + 44,

  5. buffer = new ArrayBuffer(length),

  6. view = new DataView(buffer),

  7. channels = [], i, sample,

  8. offset = 0,

  9. pos = 0;


  10. // write WAVE header

  11. // "RIFF"

  12. setUint32(0x46464952);

  13. // file length - 8

  14. setUint32(length - 8);

  15. // "WAVE"

  16. setUint32(0x45564157);

  17. // "fmt " chunk

  18. setUint32(0x20746d66);

  19. // length = 16

  20. setUint32(16);

  21. // PCM (uncompressed)

  22. setUint16(1);

  23. setUint16(numOfChan);

  24. setUint32(abuffer.sampleRate);

  25. // avg. bytes/sec

  26. setUint32(abuffer.sampleRate * 2 * numOfChan);

  27. // block-align

  28. setUint16(numOfChan * 2);

  29. // 16-bit (hardcoded in this demo)

  30. setUint16(16);

  31. // "data" - chunk

  32. setUint32(0x61746164);

  33. // chunk length

  34. setUint32(length - pos - 4);


  35. // write interleaved data

  36. for(i = 0; i < abuffer.numberOfChannels; i++)

  37. channels.push(abuffer.getChannelData(i));


  38. while(pos < length) {

  39. // interleave channels

  40. for(i = 0; i < numOfChan; i++) {

  41. // clamp

  42. sample = Math.max(-1, Math.min(1, channels[i][offset]));

  43. // scale to 16-bit signed int

  44. sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0;

  45. // write 16-bit sample

  46. view.setInt16(pos, sample, true);

  47. pos += 2;

  48. }

  49. // next source sample

  50. offset++

  51. }


  52. // create Blob

  53. return new Blob([buffer], {type: "audio/wav"});


  54. function setUint16(data) {

  55. view.setUint16(pos, data, true);

  56. pos += 2;

  57. }


  58. function setUint32(data) {

  59. view.setUint32(pos, data, true);

  60. pos += 4;

  61. }

  62. }

WAV格式的兼容性还是很6的,如下图所示:

凡事支持Web Audio API的浏览器都支持WAV格式,所以,技术上完全可行。

下面这段JS可以得到剪裁后的WAV音频的Blob数据格式:

  1. var blob = bufferToWave(newAudioBuffer, frameCount);

有了Blob数据,接下来事情就简单了。

我们可以直接把Blob数据转换成URL,可以使用URL.createObjectURL()生成一个Blob链接。

假设页面上有如下HTML代码:

  1. <audio id="audio" controls=""></audio>

则如下设置,就可以点击上面的

  1. audio.src = URL.createObjectURL(blob);

如果要转换成Base64地址,可以这么处理:

  1. var reader2 = new FileReader();

  2. reader2.onload = function(event){

  3. audio.src = event.target.result;

  4. };

  5. reader2.readAsDataURL(blob);

如果希望上传剪裁的音频

有了Blob数据,上传还不是洒洒水的事情。

可以使用FormData进行传输,例如:

  1. var formData = new FormData();

  2. formData.append('audio', blob);

  3. // 请求走起

  4. var xhr = new XMLHttpRequest();

  5. xhr.open('POST', this.cgiGetImg, true);

  6. // 请求成功

  7. xhr.onload = function () {

  8. };

  9. // 发送数据

  10. xhr.send(formData);

有demo可以进行效果体验的,您可以狠狠地点击这里:用户上传的MP3音频剪裁并播放demo https://www.zhangxinxu.com/study/202007/upload-audio-clip-play-demo.php

使用截图示意如下:

关于本文 作者:@张鑫旭 原文:https://www.zhangxinxu.com/wordpress/2020/07/js-audio-clip-copy-upload/

为你推荐


【第1829期】复制黏贴上传图片和跨浏览器自动化测试


【第2009期】实现 Bilibili 视频播放Chrome 媒体控制效果


欢迎自荐投稿,前端早读课等你来

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存