AudioContext技術和音樂視覺化(1)
寫在最前,測試部落格在這裡,直接欣賞完成視覺化效果。程式碼不日在github公開,效能目前巨爛,RadialGradient損耗巨大,優化正在提上日程。
轉載註明來源。
扒掉網頁上js的煩請留下js裡的頂端註釋謝謝。。雖然我程式碼是寫的挺爛的。如果轉發到別的地方了能註明一下作者和來源的話我會很開心的。
https://th-zxj.club 這是你從未體驗過的船新版本
Intro
因為自己搭了個部落格,一時興起,就想寫個動態的部落格背景。畢竟用django後端渲染,前端只有jquery和bootstrap已經夠low了,雖說極簡風格也很棒,但是多少有點亮眼的東西才好和別人吹牛不是嗎。
為了方便講解,整個思路分為兩個部分:音樂播放和背景繪製。
一、音樂播放
1.1 AudioContext
概述部分懶得自己寫,參考MDN的描述。
AudioContext介面表示由音訊模組連線而成的音訊處理圖,每個模組對應一個
AudioNode
。AudioContext可以控制它所包含的節點的建立,以及音訊處理、解碼操作的執行。做任何事情之前都要先建立AudioContext物件,因為一切都發生在這個環境之中。
1.2 瀏覽器支援狀況
AudioContext標準
目前還是草案,不過新chrome已經實現了。我使用的chrome版本如下。
版本 70.0.3538.77(正式版本) (64 位)
如果發現console報錯或者其他問題請檢查瀏覽器版本,所有支援的瀏覽器可以在這個連結檢視。
1.3 AudioContext和音訊處理圖
關於AudioContext
我的瞭解不是很深入,所以只在需要用到的部分進行概述。
首先,關於音訊處理圖的概念。
這個名詞不甚直觀,我用過虛幻,所以用虛幻的Blueprint
來類比理解。音訊處理圖,其實是一系列音訊處理的模組,連線構成一張資料結構中的“圖”,從一般使用的角度來講,一個播放音訊的圖,就是AudioSource -> AudioContext.destination
,兩個節點構成的圖。其中有很多特殊的節點可以對音訊進行處理,比如音訊增益節點GainNode
對於音訊處理的部分介紹就到這裡為止,畢竟真的瞭解不多,不過從MDN的文件看,可用的處理節點還是非常多的,就等標準制訂完成了。
1.4 載入音訊檔案並播放
音訊檔案載入使用典型的JavaScript
介面FileReader
實現。
一個非常簡單的例項是這樣
首先是html裡寫上input
<html>
<body>
<input type="file" accept="audio/*" onchange="onInputChange">
</body>
</html>
然後在javascript裡讀檔案內容。
function onInputChange(files){
const reader = new FileReader();
reader.onload = (event) => {
// event.target.result 就是我們的檔案內容了
}
reader.readAsArrayBuffer(files[0])
}
檔案讀取就是這麼簡單,所以回到那個問題:說了那麼多,音樂到底怎麼放?
答案是用AudioContext
的decodeAudioData
方法。
所以從上面的js裡做少許修改——
// 建立一個新的 AudioContext
const ctx = new AudioContext();
function onInputChange(files){
const reader = new FileReader();
reader.onload = (event) => {
// event.target.result 就是我們的檔案內容了
// 解碼它
ctx.decodeAudioData(event.target.result).then(decoded => {
// 解碼後的音訊資料作為音訊源
const audioBufferSourceNode = ctx.createBufferSource();
audioBufferSourceNode.buffer = decoded;
// 把音源 node 和輸出 node 連線,boom——
audioBufferSourceNode.connect(ctx.destination);
audioBufferSourceNode.start(0);
// 收工。
});
}
reader.readAsArrayBuffer(files[0])
}
1.5 分析頻譜
頻譜的概念我建議搜一下傅立葉變換,關於時域和頻域轉換的計算過程和數學原理直接略(因為不懂),至今我還只理解到時域和頻域的概念以及傅立葉變換的實現接受取樣返回取樣數一半長的頻域資料......
不班門弄斧了。
以前寫python
的時候用的numpy
來進行傅立葉變換取得頻域資料,現在在瀏覽器上用js著實有些難受。不過幸好,AudioContext
直接支援了一個音訊分析的node,叫做AudioAnalyserNode
。
這個Node處於音源Node和播放輸出Node之間,想象一道資料流,音源Node把離散的取樣資料交給Analyser,Analyser再交給輸出Node。
直接看程式碼例項。
// 建立一個新的 AudioContext
const ctx = new AudioContext();
// 解碼後的音訊資料作為音訊源
// 為了方便管理,將這些Node都放置在回撥函式外部
const audioBufferSourceNode = ctx.createBufferSource();
// 建立音訊分析Node!
const audioAnalyser = ctx.createAnalyser();
// 注意注意!這裡配置傅立葉變換使用的取樣視窗大小!比如說,我們要256個頻域資料,那麼取樣就應該是512。
// 具體對應頻率請自行搜傅立葉變換相關博文。
audioAnalyser.fftSize = 512;
function onInputChange(files){
const reader = new FileReader();
reader.onload = (event) => {
// event.target.result 就是我們的檔案內容了
// 解碼它
ctx.decodeAudioData(event.target.result).then(decoded => {
// 停止原先的音訊源
audioBufferSourceNode.stop();
// 先把音訊源Node和Analyser連線。
audioBufferSourceNode.connect(audioAnalyser);
// 然後把Analyser和destination連線。
audioAnalyser.connect(ctx.destination);
// 修改音訊源資料
audioBufferSourceNode.buffer = decoded;
audioBufferSourceNode.start(0);
// 收工。
});
}
reader.readAsArrayBuffer(files[0])
}
window.requestAnimationFrame(function() {
// 讀取頻域資料
const freqData = new Uint8Array(audioAnalyser.frequencyBinCount);
console.log(freqData);
})
頻域資料是二維的,頻率(陣列下標)和能量(下標對應值)。悄悄補一句,數學上應該說是該頻率函式影象的振幅?
其實獲得了這個頻域資料,繼續畫出我們常見的條狀頻域圖就很容易了。參考我一朋友的部落格。misuzu.moe,可以看看效果。
關於AudioContext
的介紹先到此為止,等我找時間繼續寫。
PS:程式碼不保證複製貼上就能執行,領會精神,遇到問題查查文件。MDN比我這部落格詳細多了。