本文有兩個關鍵詞:音頻可視化
和Web Audio
。前者是實踐,後者是其背後的技術支持。 Web Audio 是很大的知識點,本文會將重點放在如何獲取音頻數據這塊,對於其 API 的更多內容,能夠查看 MDN。javascript
另外,要將音頻數據轉換成可視化圖形,除了瞭解 Web Audio 以外,還須要對 Canvas (特指2D,下同),甚至 WebGL (可選)有必定了解。若是讀者對它們沒有任何學習基礎,能夠先從如下資源入手:前端
經過獲取頻率、波形和其餘來自聲源的數據,將其轉換成圖形或圖像在屏幕上顯示出來,再進行交互處理。java
雲音樂有很多跟音頻動效相關的案例,但其中有些過於複雜,又或者太偏業務。所以這裏就現找了兩個相對簡單,但有表明性的例子。c++
第一個是用 Canvas 實現的音頻柱形圖。git
↑點擊播放↑github
第二個是用 WebGL 實現的粒子效果。web
↑點擊播放↑算法
在具體實踐中,除了這些基本圖形(矩形、圓形等)的變換,還能夠把音頻和天然運動、3D 圖形結合到一塊兒。數組
Web Audio 是 Web 端處理和分析音頻的一套 API 。它能夠設置不一樣的音頻來源(包括
<audio>
節點、 ArrayBuffer 、用戶設備等),對音頻添加音效,生成可視化圖形等。
接下來重點介紹 Web Audio 在可視化中扮演的角色,見下圖。
簡單來講,就是取數據 + 映射數據兩個過程。咱們先把「取數據」這個問題解決,能夠按如下5步操做。
在音頻的任何操做以前,都必須先建立 AudioContext 。它的做用是關聯音頻輸入,對音頻進行解碼、控制音頻的播放暫停等基礎操做。
建立方式以下:
const AudioContext = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioContext();
複製代碼
AnalyserNode 用於獲取音頻的頻率數據( FrequencyData )和時域數據( TimeDomainData )。從而實現音頻的可視化。
它只會對音頻進行讀取,而不會對音頻進行任何改變。
const analyser = ctx.createAnalyser();
analyser.fftSize = 512;
複製代碼
關於 fftSize ,在 MDN 上的介紹可能很難理解,說是快速傅里葉變換的一個參數。
能夠從如下角度理解:
1. 它的取值是什麼?
fftSize 的要求是 2 的冪次方,好比 256 、 512 等。數字越大,獲得的結果越精細。
對於移動端網頁來講,自己音頻的比特率大可能是 128Kbps ,沒有必要用太大的頻率數組去存儲自己就不夠精細的源數據。另外,手機屏幕的尺寸比桌面端小,所以最終展現圖形也不須要每一個頻率都採到。只須要體現節奏便可,所以 512 是較爲合理的值。
2. 它的做用是什麼?
fftSize 決定了 frequencyData 的長度,具體爲 fftSize 的一半。
至於爲何是 1 / 2,感興趣的能夠看下這篇文章:Why is the FFT 「mirrored」?
如今,咱們須要將音頻節點,關聯到 AudioContext 上,做爲整個音頻分析過程的輸入。
在 Web Audio 中,有三種類型的音頻源:
<audio>
節點直接做爲輸入,可作到流式播放。navigator.getUserMedia
獲取用戶的音頻或視頻流後,生成音頻源。這 3 種音頻源中,除了 MediaStreamAudioSourceNode 有它不可替代的使用場景(好比語音或視頻直播)以外。 MediaElementAudioSourceNode 和 AudioBufferSourceNode 相對更容易混用,所以這裏着重介紹一下。
MediaElementAudioSourceNode 將<audio>
標籤做爲音頻源。它的 API 調用很是簡單。
// 獲取<audio>節點
const audio = document.getElementById('audio');
// 經過<audio>節點建立音頻源
const source = ctx.createMediaElementSource(audio);
// 將音頻源關聯到分析器
source.connect(analyser);
// 將分析器關聯到輸出設備(耳機、揚聲器)
analyser.connect(ctx.destination);
複製代碼
有一種狀況是,在安卓端,測試了在Chrome/69
(不含)如下的版本,用 MediaElementAudioSourceNode 時,獲取到的 frequencyData 是全爲 0 的數組。
所以,想要兼容這類機器,就須要換一種預加載的方式,即便用 AudioBufferSourceNode ,加載方式以下:
// 建立一個xhr
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/audio.mp3', true);
// 設置響應類型爲 arraybuffer
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
var source = ctx.createBufferSource();
// 對響應內容進行解碼
ctx.decodeAudioData(xhr.response, function(buffer) {
// 將解碼後獲得的值賦給buffer
source.buffer = buffer;
// 完成。將source綁定到ctx。也能夠鏈接AnalyserNode
source.connect(ctx.destination);
});
};
xhr.send();
複製代碼
若是將 AnalyserNode 類比中間件,會不會好理解一些?
能夠對比一下常規的<audio>
播放,和 Web Audio 中的播放流程:
對於<audio>
節點,即便用 MediaElementAudioSourceNode 的話,播放相對比較熟悉:
audio.play();
複製代碼
但若是是 AudioBufferSourceNode ,它不存在 play 方法,而是:
// 建立AudioBufferSourceNode
const source = ctx.createBufferSource();
// buffer是經過xhr獲取的音頻文件
source.buffer = buffer;
// 調用start方法進行播放
source.start(0);
複製代碼
到此,咱們已經將音頻輸入關聯到一個 AnalyserNode ,而且開始播放音頻。對於 Web Audio 這部分來講,它只剩最後一個任務:獲取頻率數據。
關於頻率, Web Audio 提供了兩個相關的 API,分別是:
analyser.getByteFrequencyData
analyser.getFloatFrequencyData
二者都是返回 TypedArray ,惟一的區別是精度不一樣。
getByteFrequencyData 返回的是 0 - 255 的 Uint8Array 。而 getFloatFrequencyData 返回的是 0 - 22050 的 Float32Array 。
相比較而言,若是項目中對性能的要求高於精度,那建議使用 getByteFrequencyData 。下圖展現了一個具體例子:
關於數組的長度( 256 ),在上文已經解釋過,它是 fftSize 的一半。
如今,咱們來看下如何獲取頻率數組:
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
複製代碼
須要注意的是, getByteFrequencyData 是對已有的數組元素進行賦值,而不是建立後返回新的數組。
它的好處是,在代碼中只會有一個 dataArray 的引用,不用經過函數調用和參數傳遞的方式來從新取值。
在瞭解 Web Audio 以後,已經能用 getByteFrequencyData 取到一個 Uint8Array 的數組,暫時命名爲 dataArray 。
從原理上講,可視化所依賴的數據能夠是音頻,也能夠是溫度變化,甚至能夠是隨機數。因此,接下來的內容,咱們只須要關心如何將 dataArray 映射爲圖形數據,不用再考慮 Web Audio 的操做。
(爲了簡化 Canvas 和 WebGL 的描述,下文提到 Canvas 特指 Canvas 2D
。)
Canvas 自己是一個序列幀的播放。它在每一幀中,都要先清空 Canvas ,再從新繪製。
如下是從示例代碼中摘取的一段:
function renderFrame() {
requestAnimationFrame(renderFrame);
// 更新頻率數據
analyser.getByteFrequencyData(dataArray);
// bufferLength表示柱形圖中矩形的個數
for (var i = 0, x = 0; i < bufferLength; i++) {
// 根據頻率映射一個矩形高度
barHeight = dataArray[i];
// 根據每一個矩形高度映射一個背景色
var r = barHeight + 25 * (i / bufferLength);
var g = 250 * (i / bufferLength);
var b = 50;
// 繪製一個矩形,並填充背景色
ctx.fillStyle = "rgb(" + r + "," + g + "," + b + ")";
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
renderFrame();
複製代碼
對於可視化來講,核心邏輯在於:如何把頻率數據映射成圖形參數。在上例中,只是簡單地改變了柱形圖中每個矩形的高度和顏色。
Canvas 提供了豐富的繪製API,僅從 2D 的角度考慮,它也能實現不少酷炫的效果。類比 DOM 來講,若是隻是<div>
的組合就能作出豐富多彩的頁面,那麼 Canvas 同樣能夠。
Canvas 是 CPU 計算,對於 for 循環計算 10000 次,並且每一幀都要重複計算, CPU 是負載不了的。因此咱們不多看到用 Canvas 2D 去實現粒子效果。取而代之的,是使用 WebGL ,藉助 GPU 的計算能力。
在 WebGL 中,有一個概念相對比較陌生——着色器。它是運行在 GPU 中負責渲染算法的一類總稱。它使用 GLSL( OpenGL Shading Language )編寫,簡單來講是一種類 C 風格的語言。如下是簡單的示例:
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
複製代碼
關於着色器更詳細的介紹,能夠查看這篇文章。
WebGL 的原生 API 是很是複雜的,所以咱們使用Three.js
做爲基礎庫,它會讓業務邏輯的編寫變得簡單。
先來看下整個開發流程中作的事情,以下圖:
在這個過程當中, uniforms 的類型是簡單 Object ,咱們會將音頻數組做爲 uniforms 的一個屬性,傳到着色器中。至於着色器作的事情,能夠簡單理解爲,它將 uniforms 中定義的一系列屬性,映射爲屏幕上的頂點和顏色。
頂點着色器和片元着色器的編寫每每不須要前端開發參與,對於學過 Unity3D 等技術的遊戲同窗可能會熟悉一些。讀者能夠到 ShaderToy 上尋找現成的着色器。
而後介紹如下3個 Three.js 中的類:
1. THREE.Geometry
能夠理解爲形狀。也就是說,最後展現的物體是球體、仍是長方體、仍是其餘不規則的形狀,是由這個類決定的。
所以,你須要給它傳入一些頂點的座標。好比三角形,有3個頂點,則傳入3個頂點座標。
固然, Three.js 內置了不少經常使用的形狀,好比 BoxGeometry 、 CircleGeometry 等。
2. THREE.ShaderMaterial
能夠理解爲顏色。仍是以三角形爲例,一個三角形能夠是黑色、白色、漸變色等,這些顏色是由 ShaderMaterial 決定的。
ShaderMaterial 是 Material 的一種,它由頂點着色器和片元着色器進行定義。
3. THREE.Mesh
定義好物體的形狀和顏色後,須要把它們組合在一塊兒,稱做 Mesh (網格)。有了 Mesh 以後,即可以將它添加到畫布中。而後就是常規的 requestAnimationFrame 的流程。
一樣的,咱們摘取了示例中比較關鍵的代碼,並作了標註。
i. 建立 Geometry (這是從 THREE.BufferGeometry 繼承的類):
var geometry = ParticleBufferGeometry({
// TODO 一些參數
});
複製代碼
ii. 定義 uniforms :
var uniforms = {
dataArray: {
value: null,
type: 't' // 對應THREE.DataTexture
},
// TODO 其餘屬性
};
複製代碼
iii. 建立 ShaderMaterial :
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: '', // TODO 傳入頂點着色器
fragmentShader: '', // TODO 傳入片元着色器
// TODO 其餘參數
});
複製代碼
iv. 建立 Mesh :
var mesh = new THREE.Mesh(geometry, material);
複製代碼
v. 建立 Three.js 中一些必須的渲染對象,包括場景和攝像頭:
var scene, camera, renderer;
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
camera = new THREE.PerspectiveCamera(45, 1, .1, 1e3);
scene = new THREE.Scene();
複製代碼
vi. 常規的渲染邏輯:
function animate() {
requestAnimationFrame(animate);
// TODO 此處能夠觸發事件,用於更新頻率數據
renderer.render(scene, camera);
}
複製代碼
本文首先介紹瞭如何經過 Web Audio 的相關 API 獲取音頻的頻率數據。
而後介紹了 Canvas 和 WebGL 兩種可視化方案,將頻率數據映射爲圖形數據的一些經常使用方式。
另外,雲音樂客戶端上線鯨雲動效已經有一段時間,看過本文以後,有沒有同窗想嘗試實現一個本身的音頻動效呢?
最後附上文中提到的兩段 codepen 示例:
本文發佈自 網易雲音樂前端團隊,歡迎自由轉載,轉載請保留出處。咱們一直在招人,若是你剛好準備換工做,又剛好喜歡雲音樂,那就 加入咱們!