通過Web Audio API可視化輸出MP3音樂頻率波形
Web Audio API(網絡音頻API)過去幾年中已經改進很多,通過網頁播放聲音和音樂已經成為了可能。但這還不夠,不同瀏覽器的行為方式還有不同。但至少已經實現了.
在這篇文章中,我們將通過DOM和Web Audio API創建一個可視化音頻的例子。由于Firefox還不能正確處理CORS,Safari瀏覽器存在一些字節處理問題,這個演示只能在Chrome上使用。 注* 形狀會波形而變化.
Audio 組件
首先我們需要創建一個audio組件,通過預加載(preloading)和流式(streaming)播放時時處理.
創建Audio上下文Context
AudioContext是Web Audio API的基石,我們將創建一個全局的AudioContext對象,然后用它"線性"處理字節流.
/ 創建一個 AudioContext / var context;/ 嘗試初始化一個新的 AudioContext, 如果失敗拋出 error / try { / 創建 AudioContext. / context = new AudioContext(); } catch(e) { throw new Error('The Web Audio API is unavailable'); }</pre>
通過XHR預加載MP3(AJAX)
通過XMLHttpRequest的代理,我們能從服務器獲取數據時做一些時髦而有用的處理。在這種情況下,我們將.mp3音頻文件轉化為數組緩沖區ArrayBuffer,這使得它能更容易地與Web Audio API交互。
/*一個新的 XHR 對象 */ var xhr = new XMLHttpRequest(); /* 通過 GET 請連接到 .mp3 */ xhr.open('GET', '/path/to/audio.mp3', true); /* 設置響應類型為字節流 arraybuffer */ xhr.responseType = 'arraybuffer'; xhr.onload = function() { /* arraybuffer 可以在 xhr.response 訪問到 */ }; xhr.send();
在 XHR的onload處理函數中,該文件的數組緩沖區將在 response 屬性中,而不是通常的responseText。現在,我們有array buffer,我們可以繼續將其作為音頻的緩沖區源。首先,我們將需要使用decodeAudioData異步地轉換ArrayBuffer到 AudioBuffer。
/ demo的音頻緩沖緩沖源 / var sound;xhr.onload = function() { sound = context.createBufferSource();
context.decodeAudioData(xhr.response, function(buffer) { / 將 buffer 傳入解碼 AudioBuffer. / sound.buffer = buffer; /連接 AudioBufferSourceNode 到 AudioContext / sound.connect(context.destination); }); };</pre>
通過XHR預加載文件的方法對小文件很實用,但也許我們不希望用戶要等到整個文件下載完才開始播放。這里我們將使用一個稍微不同點的方法,它能讓我們使用HTMLMediaElement的流媒體功能。
通過HTML Media元素流式加載
我們可以使用<audio>元素流式加載音樂文件, 在JavaScript中調用createMediaElementSource方式, 直接操作HTMLMediaElement, 像play()和pause()的方法均可調用.
/ 聲明我們的 MediaElementAudioSourceNode 變量 / var sound, / 新建一個<audio>
元素. Chrome 支持通過new Audio()
創建, Firefox 需要通過createElement
方法創建. */ audio = new Audio();/ 添加
canplay
事件偵聽當文件可以被播放時. / audio.addEventListener('canplay', function() { / 現在這個文件可以canplay
了, 從<audio>
元素創建一個 MediaElementAudioSourceNode(媒體元素音頻源結點) . / sound = context.createMediaElementSource(audio); / 將 MediaElementAudioSourceNode 與 AudioContext 關聯 / sound.connect(context.destination); /通過我們可以play
<audio>
元素了 */ audio.play(); }); audio.src = '/path/to/audio.mp3';</pre>
這個方法減少了大量的代碼,而且對于我們的示例來說更加合適,現在讓我們整理一下代碼用promise模式來定義一個Sound的Class類.
/ Hoist some variables. / var audio, context;/ Try instantiating a new AudioContext, throw an error if it fails. / try { / Setup an AudioContext. / context = new AudioContext(); } catch(e) { throw new Error('The Web Audio API is unavailable'); }
/ Define a
Sound
Class / var Sound = { / Give the sound an element property initially undefined. / element: undefined, / Define a class method of play which instantiates a new Media Element Source each time the file plays, once the file has completed disconnect and destroy the media element source. / play: function() { var sound = context.createMediaElementSource(this.element); this.element.onended = function() { sound.disconnect(); sound = null; } sound.connect(context.destination);/ Call
play
on the MediaElement. / this.element.play(); } };/ Create an async function which returns a promise of a playable audio element. / function loadAudioElement(url) { return new Promise(function(resolve, reject) { var audio = new Audio(); audio.addEventListener('canplay', function() { / Resolve the promise, passing through the element. / resolve(audio); }); / Reject the promise on an error. / audio.addEventListener('error', reject); audio.src = url; }); }
/ Let's load our file. / loadAudioElement('/path/to/audio.mp3').then(function(elem) { / Instantiate the Sound class into our hoisted variable. / audio = Object.create(Sound); / Set the element of
audio
to our MediaElement. / audio.element = elem; / Immediately play the file. / audio.play(); }, function(elem) { / Let's throw an the error from the MediaElement if it fails. / throw elem.error; });</pre>
現在我們能播放音樂文件,我們將繼續嘗試來獲取audio的頻率數據.
處理Audio音頻數據
在開始從audio context獲取實時數據前,我們要連線兩個獨立的音頻節點。這些節點可以從一開始定義時就進行連接。
/ 聲明變量 / var audio, context = new (window.AudioContext || window.webAudioContext || window.webkitAudioContext)(), / 創建一個1024長度的緩沖區bufferSize
/ processor = context.createScriptProcessor(1024), /創建一個分析節點 analyser node / analyser = context.createAnalyser();/ 將 processor 和 audio 連接 / processor.connect(context.destination); / 將 processor 和 analyser 連接 / analyser.connect(processor);
/ 定義一個 Uint8Array 字節流去接收分析后的數據 / var data = new Uint8Array(analyser.frequencyBinCount);</pre>
現在我們定義好了analyser節點和數據流,我們需要略微更改一下Sound類的定義,除了將音頻源和audio context連接,我們還需要將其與analyser連接.我們同樣需要添加一個audioprocess處理processor節點.當播放結束時再移除.
play: function() { var sound = context.createMediaElementSource(this.element); this.element.onended = function() { sound.disconnect(); sound = null; / 當文件結束時置空事件處理 / processor.onaudioprocess = function() {}; } / 連接到 analyser. / sound.connect(analyser); sound.connect(context.destination);processor.onaudioprocess = function() { / 產生頻率數據 / analyser.getByteTimeDomainData(data); }; / 調用 MediaElement 的
play
方法. / this.element.play(); }</pre>
這也意味著我們的連接關系大致是這樣的:
MediaElementSourceNode \=> AnalyserNode => ScriptProcessorNode /=> AudioContext
\_____________________________________/
為了獲取頻率數據, 我們只需要簡單地將audioprocess處理函數改成這樣:
analyser.getByteFrequencyData(data);
可視化組件
現在所有的關于audio的東西都已經解決了, 現在我們需要將波形可視化地輸出來,在這個例子中我們將使用DOM節點和 requestAnimationFrame. 這也意味著我們將從輸出中獲取更多的功能. 在這個功能中,我們將借助CSS的一些屬性如:transofrm和opacity.
初始步驟
我們先在文檔中添加一些css和logo.
<div class="logo-container"> <img class="logo" src="/path/to/image.svg"/> </div>.logo-container, .logo, .container, .clone { width: 300px; height: 300px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; }
.logo-container, .clone { background: black; border-radius: 200px; }
.mask { overflow: hidden; will-change: transform; position: absolute; transform: none; top: 0; left: 0; }</pre>
現在,最重要的一點,我們將會把圖片切成很多列.這是通過JavaScript完成的.
/ 開始可視化組件, 讓我們定義一些參數. / var NUM_OF_SLICES = 300, /STEP
步長值, 影響我們將數據切成多少列 / STEP = Math.floor(data.length / NUM_OF_SLICES), / 當 analyser 不再接收數據時, array中的所有值都值是 128. */ NO_SIGNAL = 128;/ 獲取我們將切片的元素 / var logo = document.querySelector('.logo-container');
/ 我們稍微會將切好的圖片與數據交互 / var slices = [] rect = logo.getBoundingClientRect(), / 感謝 Thankfully在
TextRectangle
中給我們提供了寬度和高度屬性 / width = rect.width, height = rect.height, widthPerSlice = width / NUM_OF_SLICES;/ 為切好的列,創建一個容器 / var container = document.createElement('div'); container.className = 'container'; container.style.width = width + 'px'; container.style.height = height + 'px';</pre>
創建 'slices' 切片
我們需要為每一列添加一個遮罩層,然后按x軸進行偏移.
/ Let's create our 'slices'. / for (var i = 0; i < NUM_OF_SLICES; i++) { / Calculate theoffset
for each individual 'slice'. / var offset = i * widthPerSlice;/ Create a mask
<div>
for this 'slice'. / var mask = document.createElement('div'); mask.className = 'mask'; mask.style.width = widthPerSlice + 'px'; / For the best performance, and to prevent artefacting when we usescale
we instead use a 2dmatrix
that is in the form: matrix(scaleX, 0, 0, scaleY, translateX, translateY). We initially translate by theoffset
on the x-axis. */ mask.style.transform = 'matrix(1,0,0,1,' + offset + '0)';/ Clone the original element. / var clone = logo.cloneNode(true); clone.className = 'clone'; clone.style.width = width + 'px'; / We won't be changing this transform so we don't need to use a matrix. / clone.style.transform = 'translate3d(' + -offset + 'px,0,0)'; clone.style.height = mask.style.height = height + 'px';
mask.appendChild(clone); container.appendChild(mask);
/ We need to maintain the
offset
for when we alter the transform inrequestAnimationFrame
. */ slices.push({ offset: offset, elem: mask }); }/ Replace the original element with our new container of 'slices'. / document.body.replaceChild(container, logo);</pre>
定義我們的渲染函數
每當audioprocess處理函數接收到數據,我們就需要重新渲染,這時 requestAnimationFrame 就派上用場了.
/ Create ourrender
function to be called every available frame. / function render() { / Request arender
on the next available frame. No need to polyfill because we are in Chrome. */ requestAnimationFrame(render);/ Loop through our 'slices' and use the STEP(n) data from the analysers data. */ for (var i = 0, n = 0; i < NUM_OF_SLICES; i++, n+=STEP) { var slice = slices[i], elem = slice.elem, offset = slice.offset;
/ Make sure the val is positive and divide it by
NO_SIGNAL
to get a value suitable for use on the Y scale. / var val = Math.abs(data[n]) / NO_SIGNAL; / Change the scaleY value of our 'slice', while keeping it's original offset on the x-axis. / elem.style.transform = 'matrix(1,0,0,' + val + ',' + offset + ',0)'; elem.style.opacity = val; } }/ Call the
render
function initially. / render();</pre>
現在我們完成了所有的DOM構建, 完整的在線示例 , 完整的源碼文件同樣在此DEMO中.
原文地址: fourthof5.com
來自:http://ourjs.com/detail/54d48406232227083e000029