这篇文章发布于 2020年07月27日,星期一,23:51,归类于 JS实例。 阅读 37263 次, 今日 16 次 23 条评论
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=9505
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。
背景是这样的,用户上传音频文件,可能只需要几十秒就够了,但是常规的音乐都要3~5分钟,80%的流量都是不需要的,要是就这么传上去,其实是流量的浪费,如果可以在前端就进行剪裁,也就是只取前面一段时间的音频,岂不是可以给公司省很多流量费用,前端的业务价值就体现了。
关键如何实现呢?
下面,就以“截取用户上传音频前3秒内容”的需求示意下如何借助Web Audio API实现音频的部分复制与播放功能。
一、不哔哔,直接正题
实现步骤如下。
1. File对象转ArrayBuffer
在Web网页中,用户选择的文件是个file对象,我们可以将这个文件对象转换成Blob、ArrayBuffer或者Base64。
在音频处理这里,都是使用ArrayBuffer这个数据类型。
代码如下所示,假设file类型的文件选择框的id是'file'
。
file.onchange = function (event) { var file = event.target.files[0]; // 开始识别 var reader = new FileReader(); reader.onload = function (event) { var arrBuffer = event.target.result; // arrBuffer就是包含音频数据的ArrayBuffer对象 }); reader.readAsArrayBuffer(file); };
使用的是readAsArrayBuffer()
方法,无论是MP3格式、OGG格式还是WAV格式,都可以转换成ArrayBuffer类型。
2. 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()
方法,代码如下:
var audioCtx = new AudioContext();
audioCtx.decodeAudioData(arrBuffer, function(audioBuffer) {
// audioBuffer就是AudioBuffer
});
3. 复制AudioBuffer前3秒数据
AudioBuffer对象是一个音频专用Buffer对象,包含很多音频信息,包括:
- 音频时长
duration
- 声道数量
numberOfChannels
- 采样率
sampleRate
等。
包括一些音频声道数据处理方法,例如:
- 获取通道数据
getChannelData()
- 复制通道数据
copyFromChannel()
- 写入通道数据
copyToChannel()
文档见这里:https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer
所以,实现的原理很简单,创建一个空的AudioBuffer,复制现有的通道数据前3秒的数据,然后复制的内容写入到这个空的AudioBuffer,于是我们就得到了一个剪裁后的音频Buffer数据了。
代码如下:
// 声道数量和采样率 var channels = audioBuffer.numberOfChannels; var rate = audioBuffer.sampleRate; // 截取前3秒 var startOffset = 0; var endOffset = rate * 3; // 3秒对应的帧数 var frameCount = endOffset - startOffset; // 创建同样采用率、同样声道数量,长度是前3秒的空的AudioBuffer var newAudioBuffer = new AudioContext().createBuffer(channels, endOffset - startOffset, rate); // 创建临时的Array存放复制的buffer数据 var anotherArray = new Float32Array(frameCount); // 声道的数据的复制和写入 var offset = 0; for (var channel = 0; channel < channels; channel++) { audioBuffer.copyFromChannel(anotherArray, channel, startOffset); newAudioBuffer.copyToChannel(anotherArray, channel, offset); } // newAudioBuffer就是全新的复制的3秒长度的AudioBuffer对象
上面JavaScript代码中的变量newAudioBuffer
就是全新的复制的3秒长度的AudioBuffer对象。
4. 使用newAudioBuffer做点什么?
其实应该是有了AudioBuffer对象后我们可以做点什么。
能做很多事情。
1) 如果希望直接播放
我们可以直接把AudioBuffer的数据作为音频数据进行播放
// 创建AudioBufferSourceNode对象 var source = audioCtx.createBufferSource(); // 设置AudioBufferSourceNode对象的buffer为复制的3秒AudioBuffer对象 source.buffer = newAudioBuffer; // 这一句是必须的,表示结束,没有这一句没法播放,没有声音 // 这里直接结束,实际上可以对结束做一些特效处理 source.connect(audioCtx.destination); // 资源开始播放 source.start();
2) 如果希望在<audio>元素中播放
这个还挺麻烦的。
从<audio>
的src属性获取音频资源,再进行处理是简单的,网上的案例也很多。
但是,想要处理后的AudioBuffer再变成src让<audio>
元素播放,嘿嘿,就没那么容易了。
我(张鑫旭)找了一圈,没有看到Web Audio API中有专门的“逆转录”方法。
唯一可行的路数就是根据AudioBuffer数据,重新构建原始的音频数据。研究了一番,转成WAV格式相对容易,想要转换成MP3格式比较麻烦,这里有个项目:https://github.com/higuma/mp3-lame-encoder-js 不过自己没验证过,不过看代码量,还挺惊人的。
因此,我们的目标还是转到WAV音频文件生成上吧,下面这段方法是从网上找的AudioBuffer转WAV文件的方法,以Blob数据格式返回。
// Convert AudioBuffer to a Blob using WAVE representation function bufferToWave(abuffer, len) { var numOfChan = abuffer.numberOfChannels, length = len * numOfChan * 2 + 44, buffer = new ArrayBuffer(length), view = new DataView(buffer), channels = [], i, sample, offset = 0, pos = 0; // write WAVE header // "RIFF" setUint32(0x46464952); // file length - 8 setUint32(length - 8); // "WAVE" setUint32(0x45564157); // "fmt " chunk setUint32(0x20746d66); // length = 16 setUint32(16); // PCM (uncompressed) setUint16(1); setUint16(numOfChan); setUint32(abuffer.sampleRate); // avg. bytes/sec setUint32(abuffer.sampleRate * 2 * numOfChan); // block-align setUint16(numOfChan * 2); // 16-bit (hardcoded in this demo) setUint16(16); // "data" - chunk setUint32(0x61746164); // chunk length setUint32(length - pos - 4); // write interleaved data for(i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i)); while(pos < length) { // interleave channels for(i = 0; i < numOfChan; i++) { // clamp sample = Math.max(-1, Math.min(1, channels[i][offset])); // scale to 16-bit signed int sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // write 16-bit sample view.setInt16(pos, sample, true); pos += 2; } // next source sample offset++ } // create Blob return new Blob([buffer], {type: "audio/wav"}); function setUint16(data) { view.setUint16(pos, data, true); pos += 2; } function setUint32(data) { view.setUint32(pos, data, true); pos += 4; } }
WAV格式的兼容性还是很6的,如下图所示:
凡事支持Web Audio API的浏览器都支持WAV格式,所以,技术上完全可行。
下面这段JS可以得到剪裁后的WAV音频的Blob数据格式:
var blob = bufferToWave(newAudioBuffer, frameCount);
有了Blob数据,接下来事情就简单了。
我们可以直接把Blob数据转换成URL,可以使用URL.createObjectURL()
生成一个Blob链接。
假设页面上有如下HTML代码:
<audio id="audio" controls=""></audio>
则如下设置,就可以点击上面的<audio>
元素进行播放了。
audio.src = URL.createObjectURL(blob);
如果要转换成Base64地址,可以这么处理:
var reader2 = new FileReader(); reader2.onload = function(event){ audio.src = event.target.result; }; reader2.readAsDataURL(blob);
3) 如果希望上传剪裁的音频
有了Blob数据,上传还不是洒洒水的事情。
可以使用FormData进行传输,例如:
var formData = new FormData(); formData.append('audio', blob); // 请求走起 var xhr = new XMLHttpRequest(); xhr.open('POST', this.cgiGetImg, true); // 请求成功 xhr.onload = function () { }; // 发送数据 xhr.send(formData);
有demo可以进行效果体验的,您可以狠狠地点击这里:用户上传的MP3音频剪裁并播放demo
使用截图示意如下:
二、不哔哔,直接结语
本文内容在圈子里算是稀缺的,文章质量还是不错的。
可惜,不太容易被用户发现。
即使有分享,也都是短时间的。
自己也没那么多精力去社交平台打理。
有些可惜。
可以找个人兼职打理自己的账号,做下类似运营的事情呢?
总之,感谢阅读,如果本文内容正好可以解决你的需求,记得在心里面点个赞!
本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
本文地址:https://www.zhangxinxu.com/wordpress/?p=9505
(本篇完)
- 使用JS提取视频中的音频资源 (0.489)
- 小tips: 纯前端JS读取与解析本地文本类文件 (0.347)
- JS改变AudioBuffer音量并下载为新audio音频 (0.344)
- 不改变音调情况下Audio音频的倍速合成JS实现 (0.344)
- 纯JS实现多个音频的拼接或者合并 (0.227)
- 利用HTML5 Web Audio API给网页JS交互增加声音 (0.164)
- 使用wavesurfer.js显示mp3 audio音频的波形图 (0.148)
- 独家,MP3音频淡入淡出播放和转换的JS实现 (0.129)
- 理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型 (0.098)
- 本地MP3封面图、时长等信息的JS读取 (0.084)
- 自己写的无图片版jQuery zxxbox弹出框插件 (RANDOM - 0.016)
大佬厉害
我试了几种不同sampleRate的音频,通过audioBuffer读出来的都是4.8k,楼主有没有遇到这个情况?
如果不是必须要求不需要过分介意。浏览器将对加载的音频自动重采样,从而sampleRate与源文件sampleRate无关。(与 AudioContext的相同)
当然new一个AudioBuffer例外。
请教一个问题,souce里面的音频数据,经过gainNode和pannerNode以后,输出到destinationNode,这个变换后的结果音频数据怎么能够获取到并保存下来?不知道这个结果数据保存在哪个变量里面了?
考虑 OfflineAudioContext (但我自己也没用过),实时获取用ScriptPrecessorNode(没用过可能拼写错误)
不错不错,这段解析很好,解决现在的问题,然后现在主要是拿到了blob数据或者audioBuffer,怎么转成wav或者mp3给后端去合成呢
博主 ,我剪辑过的音频体积会增大几倍,请问是在哪里处理这个问题啊
从 mp3 转换为 wav,体积自然会增大的
我这边文件是wav,剪辑后也是增大了好多倍,这是为啥呢
哈哈,可以帮大佬打理运营嘛,已经在从头看博客文章了
大佬真的强,比其他的文章方法简单好多还好用
要不,我来做你运营,顺便学习
如果希望在 中播放,不需要把 buffer 转 WAV 然后转 blob URL 再喂给 audio,AudioContext 默认的 destination 是你的声卡设备,它有个 createMediaStreamDestination 方法可以创建一个 mediaStream 当 destination。有了 mediaStream 就可以把它怼到 audio.srcObject 上啦连 URL.createObjectUrl 都省了。还有,拿到 mediaStream 还可以拿 MediaRecorder 把它录下来,或者怼到 webRTC 的 API 上播给远程的用户听。 https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamDestination
翻了下 API 这个 AudioContext 还有个 createMediaStreamSource 方法,意味着可以从声卡采集音频流,然后流过你设置的一堆 node, 最后流出到一个 medaiStream 再发给 webRTC 远端。这玩意儿不就是给咱做远程通话降噪销回音的嘛,实时变声也行哎。本地播放的话,做 KTV 混音还有游戏音效,比如实现游戏里的3D场景音。想想就激动,
文中的路径适合于简单的文件上传(原来的表单不用改)。不过想想,直播可是很刺激的。
webRTC在移动端上兼容太差,比如ios上只能在safari上使用,目前也没有polyfill的解决办法
一直订着你的RSS,会第一时间收到的。基本上每天早上上班前才有时间看。
可以做一期关于不同音频格式的文件压缩吗,找遍网上都没发现比较好的压缩办法
但是decodeAudioData的兼容性一般…
您可能不知道 咱订阅了你的博客 要不是每天10点半睡觉 估计昨天就看了
我是一个做government项目的前端,真的非常喜欢你这样的博文
请教博主一个问题。我想通过ImageData实时处理中播放的视频,请问一定要先把的视频帧写入,才可以获取ImageData吗?有没有其他优雅的方法?
…实时处理video标签中播放的视频…先把video标签的视频帧写入canvas…
不小心用了html标签,被吞了
视频流?stream试试~