前言
還有這種技巧。今日早讀文章由@張鑫旭授權分享。
正文從這開始~~
背景是這樣的,用戶上傳音頻文件,可能只需要幾十秒就夠了,但是常規的音樂都要3~5分鐘,80%的流量都是不需要的,要是就這麼傳上去,其實是流量的浪費,如果可以在前端就進行剪裁,也就是只取前面一段時間的音頻,豈不是可以給公司省很多流量費用,前端的業務價值就體現了。
關鍵如何實現呢?
下面,就以「截取用戶上傳音頻前3秒內容」的需求示意下如何藉助Web Audio API實現音頻的部分複製與播放功能。
不嗶嗶,直接正題實現步驟如下。
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類型。
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
});
複製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對象。
使用newAudioBuffer做點什麼?其實應該是有了AudioBuffer對象後我們可以做點什麼。
能做很多事情。
如果希望直接播放我們可以直接把AudioBuffer的數據作為音頻數據進行播放
// 創建AudioBufferSourceNode對象
var source = audioCtx.createBufferSource();
// 設置AudioBufferSourceNode對象的buffer為複製的3秒AudioBuffer對象
source.buffer = newAudioBuffer;
// 這一句是必須的,表示結束,沒有這一句沒法播放,沒有聲音
// 這裡直接結束,實際上可以對結束做一些特效處理
source.connect(audioCtx.destination);
// 資源開始播放
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數據格式返回。
// 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.src = URL.createObjectURL(blob);
如果要轉換成Base64地址,可以這麼處理:
var reader2 = new FileReader();
reader2.onload = function(event){
audio.src = event.target.result;
};
reader2.readAsDataURL(blob);
如果希望上傳剪裁的音頻有了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音頻剪裁併播放demohttps://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 媒體控制效果
歡迎自薦投稿,前端早讀課等你來