視覺化的音樂播放器,可戳我觀看效果
瞭解Web-Audio-Api
- 基礎知識
<audio>
標籤是HTML5的新標籤,通過新增src
屬性實現音樂播放。
AudioContext
是音訊播放環境,原理與canvas的繪製環境類似,都是需要建立環境上下文,通過上下文的呼叫相關的建立音訊節點,控制音訊流播放暫停操作等操作,這一些操作都需要發生在這個環境之中。
try{
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}catch(e){
alert(`Web Audio API is not supported in this browser`);
}
複製程式碼
AudioNode
介面是一個處理音訊的通用模組,它可以是音訊音源模組,音訊播放裝置模組,也可以是中間音訊處理模組。不同的音訊節點的連線(通過AudioContext.connect()
),以及終點連線AudioContext.destination
(可以看作是連線到耳機或揚聲器裝置)完成後,才能輸出音樂。
常見的音訊節點:
AudioBufferSourceNode: 播放和處理音訊資料
AnalyserNode: 顯示音訊時間和頻率資料 (通過分析頻率資料可以繪製出波形圖之類的檢視,視覺化的主要途徑)
GainNode: 音量節點,控制音訊的總音量
MediaElementAudioSourceNode: 關聯HTMLMediaElement,播放和處理來自<video>和<audio>元素的音訊
OscillatorNode: 一個週期性波形,只建立一個音調
...
複製程式碼
- 執行模式
- 建立音訊上下文
- 在上下文中,建立音訊源
- 建立音訊節點,處理音訊資料並連線
- 輸出裝置
建立音訊上下文
try{
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}catch(e){
alert(`Web Audio API is not supported in this browser`);
}
複製程式碼
建立音訊源
由於音訊檔案的資料是二進位制(非文字),所以要設定請求頭的responseType
為arraybuffer
,將.mp3音訊檔案轉換成陣列緩衝區ArrayBuffer
當AudioContext.decodeAudioData
解碼成功之後獲取buffer
,執行回撥函式,將資料放入AudioBufferSourceNode
中
方法一採用流式載入音樂檔案,簡單易懂,缺點是通過createMediaElementSource
載入的src檔案必須是同源,不允許跨域
下面步驟主要根據方法2。
- 方法一:通過HTMLMediaElement流式載入
<audio src="1.mp3"></audio>
<script>
let audio = document.querySelector(`audio`);
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
audio.addEventListener(`canplay`, function () {
let source = audioCtx.createMediaElementSource(audio);
source.connect(audioCtx.destination);
audio.play()
})
</script>
複製程式碼
- 方法二:通過XMLHttpRequest獲取資源
let xhr = new XMLHttpRequest();
xhr.open(`GET`, `1.mp3`, true);
xhr.responseType = `arraybuffer`;
xhr.onload = function () {
audioCtx.decodeAudioData(xhr.response, function (buffer) {
getBufferSuccess(buffer)
})
}
複製程式碼
- 方法三:通過input file獲取
let input = document.querySelector(`input`);
input.addEventListener(`change`, function () {
if (this.files.length !== 0) {
let file = this.files[0];
let fr = new FileReader();
fr.onload = function () {
let fileRet = e.target.result;
audioCtx.decodeAudioData(fileRet, function (buffer) {
getBufferSuccess(buffer);
}, function (err) {
console.log(err)
})
}
fr.readAsArrayBuffer(file);
}
})
複製程式碼
處理音訊資料
function getBufferSuccess(buffer) {
// 建立頻率分析節點
let analyser = audioCtx.createAnalyser();
// 確定頻域的快速傅立葉變換大小
analyser.fftSize = 2048;
// 這個屬性可以讓最後一個分析幀的資料隨時間使值之間的過渡更平滑。
analyser.smoothingTimeConstant = 0.6;
// 建立播放物件節點
let source = audioCtx.createBufferSource();
// 填充音訊buffer資料
source.buffer = buffer;
// 建立音量節點(如果你需要用調整音量大小的話)
let gainNode = audioCtx.createGain();
// 連線節點物件
source.connect(gainNode);
gainNode.connect(analyser);
analyser.connect(audioCtx.destination);
}
複製程式碼
獲取音訊頻率
- 方法一:用js的方法獲取(通過監聽audioprocess事件,由於效能問題,將會被棄用,不做詳細說明,感興趣的可以瞭解一下)
// 此方法需要補充節點的連線
let javascriptNode = audioCtx.createScriptProcessor(2048, 1, 1);
javascriptNode.connect(audioCtx.destination);
analyser.connect(javascriptNode);
this.javascriptNode.onaudioprocess = function () {
currData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(currData);
}
複製程式碼
- 方法二:用AnalyserNode獲取
獲取AnalyserNode
節點裡的頻率長度frequencyBinCount
,例項化長度為8位的整型陣列,通過AnalyserNode.getByteFrequencyData
將節點中的頻率資料拷貝到陣列中去,值的大小在0 - 256
之間,數值越高表明頻率越高;AnalyserNode.getByteTimeDomainData
原理一樣,不過獲取的是頻率大小,兩種方法根據需求選一種即可。
function getData () {
// analyser.frequencyBinCount 視覺化值的數量,是前面fftSize的一半
let currData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(currData);
analyser.getByteTimeDomainData(currData);
}
複製程式碼
輸出裝置
AudioBufferSourceNode.start(n)
n表示開始的時間,預設為0,開始播放音訊
AudioBufferSourceNode.stop(n)
音訊在第n秒時間停止,若沒有傳值表示立即停止
其他api
AudioContext.resume()
控制音訊的播放
AudioContext.suspend()
控制音訊的暫停
AudioContext.currentTime
獲取當前音訊播放時間
AudioBufferSourceNode.buffer.duration
獲取音訊的播放總時長
GainNode.gain.value
控制音量大小 [0, 1]
GainNode.gain.linearRampToValueAtTime
實現音量的漸入漸出
Canvas繪製視覺化效果
瞭解上面的api,就可以來著手繪製啦~,你想繪啥就繪啥,頻繁的呼叫canvas的api很耗效能問題,這裡講下我在測試中提高效能的小技巧。
- 多分層canvas,一些不需要頻繁改動的繪製,例如背景,固定的裝飾繪製,可以採用另一個canvas的上下文來繪製
- 離屏繪製,原理是生成一個沒有出現在頁面的canvas,在這個快取的canvas中繪製,而真正展示的canvas只需要通過drawImage這個api將畫面繪製出來即可,參考此博文
- 固定好lineWidth的長度,而不是每繪製一個就設定一次lineWidth
- 繪製區域提前計算好,不要讓canvas邊繪製同時還要計算位置(canvas:好累哦~)
總而言之,少呼叫canvas api,可是也不要為了提高效能而拋棄你的一些天馬星空的想法哦
遇到的問題
在切換歌曲中,遇到了這個報錯Failed to set the `buffer` property on `AudioBufferSourceNode`: Cannot set buffer to non-null after it has been already been set to a non-null buffer at AudioContext
,大致是講AudioBufferSourceNode
的buffer屬性在之前我已經設定過了,不能被重新設定新的buffer值,由於播放歌曲主要是通過其陣列緩衝區ArrayBuffer來進行,可看看issue,解決辦法就是當需要切換歌曲情況下,將當前的AudioBufferSourceNode
銷燬,重新建立上下文環境,音訊節點,連線等操作。
原始碼在這,互動部分寫得有點亂,因為當時原來只是想練練視覺化,之後想到啥功能就加,所以導致程式碼看起來冗餘繁瑣,大家可以參考看看audio
實現,主要在MusicPlay
物件。
小白第一次發表博文,發現寫博文比寫一個demo還要時間長,怕寫出來的東西有錯誤會誤導大家(有錯誤請大家評論指出~),所以會去查很多相關資料,這個過程也是學習的過程,以後會經常寫寫博文滴!最後,希望大家通過這篇文章也能學會自己做這種視覺化的效果,配合一些視覺化庫還能做出很酷炫的效果呢,一起互相學習進步吧,加油!(。・д・。)