通過Web Audio API可視化輸出MP3音樂頻率波形

jopen 9年前發布 | 143K 次閱讀 前端技術 Web Audio API

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,
    / 新建一個 &lt;audio&gt; 元素. Chrome 支持通過 new Audio() 創建,
      Firefox 需要通過 createElement 方法創建. */
    audio = new Audio();

/ 添加 canplay 事件偵聽當文件可以被播放時. / audio.addEventListener('canplay', function() {     / 現在這個文件可以 canplay 了, 從 &lt;audio&gt; 元素創建一個      MediaElementAudioSourceNode(媒體元素音頻源結點) . /     sound = context.createMediaElementSource(audio);     / 將 MediaElementAudioSourceNode 與 AudioContext 關聯 /     sound.connect(context.destination);     /通過我們可以 play &lt;audio&gt; 元素了 */     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 the offset for each individual 'slice'. /
    var offset = i * widthPerSlice;

    / Create a mask &lt;div&gt; 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      use scale we instead use a 2d matrix that is in the form:      matrix(scaleX, 0, 0, scaleY, translateX, translateY). We initially      translate by the offset 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 in requestAnimationFrame. */     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 our render function to be called every available frame. /
function render() {
    / Request a render 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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!