1. 程式人生 > >開大你的音響,感受HTML5 Audio API帶來的視聽盛宴

開大你的音響,感受HTML5 Audio API帶來的視聽盛宴

話說HTML5的炫酷真的是讓我愛不釋手,即使在這個提到IE就傷心不完的年代。但話又說回來,追求卓越Web創造更美世界這樣高的追求什麼時候又與IE沾過邊兒呢?所以當你在看本文並且我們開始討論HTML5等前沿東西的時候,我們預設是把IE排除在外的。本文的例子可以工作在最新的Chrome及Firefox瀏覽器下,其他瀏覽器暫未測試。

若下方未出現演示頁面請重新整理。

 你也可以點此全屏演示  或者前往GitHub進行程式碼下載然後本地執行。

(如果你手頭沒有音訊檔案的話)

檔案列表:
bbc_sherlock_openning.mp3
Neptune Illusion Dennis Kuo .mp3


單曲Remix ┃ 愛上這個女聲 放進專輯裡私藏 夜電播音員.mp3
愛啦啦.mp3

這裡將要介紹的HTML5 音訊處理介面與Audio標籤是不一樣的。頁面上的Audio標籤只是HTML5更語義化的一個表現,而HTML5提供給JavaScript程式設計用的Audio API則讓我們有能力在程式碼中直接操作原始的音訊流資料,對其進行任意加工再造。

展示HTML5 Audio API 最典型直觀的一個例子就是跟隨音樂節奏變化的頻譜圖,也稱之為視覺化效果。本文便是以此為例子展示JavaScript中操作音訊資料的。

文中程式碼僅供參考,實際程式碼以下載的原始碼為準。

瞭解Audio API

一段音訊到達揚聲器進行播放之前,半路對其進行攔截,於是我們就得到了音訊資料了,這個攔截工作是由window.AudioContext來做的,我們所有對音訊的操作都基於這個物件。通過AudioContext可以建立不同各類的AudioNode,即音訊節點,不同節點作用不同,有的對音訊加上濾鏡比如提高音色(比如BiquadFilterNode),改變單調,有的音訊進行分割,比如將音源中的聲道分割出來得到左右聲道的聲音(ChannelSplitterNode),有的對音訊資料進行頻譜分析即本文要用到的(AnalyserNode)。

瀏覽器中的Audio API

統一字首

JavaScript中處理音訊首先需要例項化一個音訊上下文型別window.AudioContext。目前Chrome和Firefox對其提供了支援,但需要相應字首,Chrome中為window.webkitAudioContext,Firefox中為mozAudioContext。所以為了讓程式碼更通用,能夠同時工作在兩種瀏覽器中,只需要一句程式碼將字首進行統一即可。

window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;

這是一種常見的用法,或者操作符'||' 連線起來的表示式中,遇到真值即返回。比如在Chrome中,window.AudioContext為undefined,接著往下走,碰到window.webkitAudioContext不為undefined,表示式在此判斷為真值,所以將其返回,於是此時window.AudioContext =window.webkitAudioContext ,所以程式碼中我們就可以直接使用window.AudioContext 而不用擔心具體Chrome還是Firefox了。

var audioContext=new window.AudioContext();

考慮瀏覽器不支援的情況

但這還只是保證了在支援AudioContext的瀏覽器中能正常工作,如果是在IE中,上面例項化物件的操作會失敗,所以有必要加個try catch語句來避免報錯。

try {
    var audioContext = new window.AudioContext();
} catch (e) {
    Console.log('!Your browser does not support AudioContext');
}

這樣就安全多啦,媽媽再不擔心瀏覽器報錯了。

組織程式碼

為了更好地進行編碼,我們建立一個Visualizer物件,把所有相關屬性及方法寫到其中。按照慣例,物件的屬性直接寫在構造器裡面,物件的方法寫到原型中。物件內部使用的私有方法以短橫線開頭,不是必要但是種好的命名習慣。

其中設定了一些基本的屬性將在後續程式碼中使用,詳細的還請參見原始碼,這裡只簡單展示。

var Visualizer = function() {
    this.file = null, //要處理的檔案,後面會講解如何獲取檔案
    this.fileName = null, //要處理的檔案的名,檔名
    this.audioContext = null, //進行音訊處理的上下文,稍後會進行初始化
    this.source = null, //儲存音訊
};
Visualizer.prototype = {
    _prepareAPI: function() {
        //統一字首,方便呼叫
        window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
        //這裡順便也將requestAnimationFrame也打個補丁,後面用來寫動畫要用
        window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
        //安全地例項化一個AudioContext並賦值到Visualizer的audioContext屬性上,方便後面處理音訊使用
        try {
            this.audioContext = new AudioContext();
        } catch (e) {
            console.log('!妳的瀏覽器不支援AudioContext:(');
            console.log(e);
        }
    },
}

載入音訊檔案

不用說,你肯定得先在程式碼中獲取到音訊檔案,才能夠對其進一步加工。

檔案獲取的方法

讀取檔案到JavaScript可以有以下三種方法:

1.新開一個Ajax非同步請求來獲取檔案,如果是本地測試需要關掉瀏覽器的同源安全策略才能獲取成功,不然只能把網頁放到伺服器上才能正常工作。

具體說來,就是先開一個XMLHttpRequest請求,將檔案路徑作為請求的URL,並且設定請求返回型別為'ArrayBuffer',這種格式方便我們後續的處理。下面是一個例子。

loadSound("sample.mp3"); //呼叫
// 定義載入音訊檔案的函式
function loadSound(url) {
    var request = new XMLHttpRequest(); //建立一個請求
    request.open('GET', url, true); //配置好請求型別,檔案路徑等
    request.responseType = 'arraybuffer'; //配置資料返回型別
    // 一旦獲取完成,對音訊進行進一步操作,比如解碼
    request.onload = function() {
        var arraybuffer = request.response;
    }
    request.send();
}

2.通過檔案型別的input來進行檔案選擇,監聽input的onchnage事件,一擔檔案選中便開始在程式碼中進行獲取處理,此法方便,且不需要工作在伺服器上

3.通過拖拽的形式把檔案拖放到頁面進行獲取,比前面一種方法稍微繁雜一點(要監聽'dragenter','dragover','drop'等事件)但同樣可以很好地在本地環境下工作,無需伺服器支援。

更多在JavaScript中獲取及處理檔案的方法可以見這裡

不用說,方法2和3方便本地開發與測試,所以我們兩種方法都實現,既支援選擇檔案,也支援檔案拖拽。

通過選擇獲取

在頁面放一個file型別的input。然後在JavaScript中監聽它的onchange事件。此事件在input的值發生變化時觸發。

對於onchange事件,在Chrome與Firefox中還有一點小的區別,如果你已經選擇了一個檔案,此時Input就有值了,如果你再次選擇同一檔案,onchange事件不會觸發,但在Firefox中該事件會觸發。這裡只是提及一下,關係不大。

<label for="uploadedFile">Drag&drop or select a file to play:</label>
<input type="file" id="uploadedFile"></input>

當然,這裡同時也把最後我們要畫圖用的canvas也一起放上去吧,後面就不用多話了。所以下面就是最終的HTML了,頁面基本不會變,大量的工作是在JavaScript的編寫上。

<div id="wrapper">
    <div id="fileWrapper" class="file_wrapper">
        <div id="info">
            HTML5 Audio API showcase | An Audio Viusalizer
        </div>
        <label for="uploadedFile">Drag&drop or select a file to play:</label>
        <input type="file" id="uploadedFile"></input>
    </div>
    <div id="visualizer_wrapper">
        <canvas id='canvas' width="800" height="350"></canvas>
    </div>
</div>

再稍微寫一點樣式

#fileWrapper {
    transition: all 0.5s ease;
}
#fileWrapper: hover {
    opacity: 1!important;
}
#visualizer_wrapper {
    text-align: center;
}

向Visualizer物件的原型中新加一個方法,用於監聽檔案選擇既前面討論的onchange事件,並在事件中獲取選擇的檔案。

_addEventListner: function() {
    var that = this,
        audioInput = document.getElementById('uploadedFile'),
        dropContainer = document.getElementsByTagName("canvas")[0];
    //監聽是否有檔案被選中
    audioInput.onchange = function() {
        //這裡判斷一下檔案長度可以確定使用者是否真的選擇了檔案,如果點了取消則檔案長度為0
        if (audioInput.files.length !== 0) {
            that.file = audioInput.files[0]; //將檔案賦值到Visualizer物件的屬性上
            that.fileName = that.file.name;
            that._start(); //獲取到檔案後,開始程式,這個方法會在後面定義並實現
        };
    };
}

上面程式碼中,我們假設已經寫好了一個進一步處理檔案的方法_start(),在獲取到檔案後賦值給Visualizer物件的file屬性,之後在_start()方法裡我們就可以通過訪問this.file來得到該檔案了,當然你也可以直接讓_start()方法接收一個file引數,但將檔案賦值到Visualizer的屬性上的好處之一是我們可以在物件的任何方法中都能獲取該檔案 ,不用想怎麼用引數傳來傳去。同樣,將檔名賦值到Visualizer的fileName屬性當中進行儲存,也是為了方便之後在音樂播放過程中顯示當前播放的檔案。

通過拖拽獲取

我們把頁面中的canvas作為放置檔案的目標,在它身上監聽拖拽事件'dragenter','dragover','drop'等。

還是在上面已經新增好的_ addEventListner方法裡,接著寫三個事件監聽的程式碼。

dropContainer.addEventListener("dragenter", function() {
    that._updateInfo('Drop it on the page', true);
}, false);
dropContainer.addEventListener("dragover", function(e) {
    e.stopPropagation();
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy'; //設定檔案放置型別為拷貝
}, false);
dropContainer.addEventListener("dragleave", function() {
    that._updateInfo(that.info, false);
}, false);
dropContainer.addEventListener("drop", function(e) {
    e.stopPropagation();
    e.preventDefault();
    that.file = e.dataTransfer.files[0]; //獲取檔案並賦值到Visualizer物件
    that.fileName = that.file.name;
    that._start();
}, false);

注意到上面程式碼中我們在'dragover'時設定檔案拖放模式為'copy',既以複製的形式獲取檔案,如果不進行設定無法正確獲取檔案

然後在'drop'事件裡,我們獲得檔案以進行一下步操作。

用FileReader讀取檔案為ArrayBuffer

下面來看這個_start()方法,現在檔案得到 了,但首先需要將獲取的檔案轉換為ArrayBuffer格式,才能夠傳遞給AudioContext進行解碼,所以接下來_start()方法中要乾的事情就是例項化一個FileReader來將檔案讀取為ArrayBuffer格式。

_start: function() {
    //read and decode the file into audio array buffer
    var that = this, //當前this指代Visualizer物件,賦值給that以以便在其他地方使用
        file = this.file, //從Visualizer物件上獲取前面得到的檔案
        fr = new FileReader(); //例項化一個FileReader用於讀取檔案
    fr.onload = function(e) { //檔案讀取完後呼叫此函式
        var fileResult = e.target.result; //這是讀取成功得到的結果ArrayBuffer資料
        var audioContext = that.audioContext; //從Visualizer得到最開始例項化的AudioContext用來做解碼ArrayBuffer
        audioContext.decodeAudioData(fileResult, function(buffer) { //解碼成功則呼叫此函式,引數buffer為解碼後得到的結果
            that._visualize(audioContext, buffer); //呼叫_visualize進行下一步處理,此方法在後面定義並實現
        }, function(e) { //這個是解碼失敗會呼叫的函式
            console.log("!哎瑪,檔案解碼失敗:(");
        });
    };
    //將上一步獲取的檔案傳遞給FileReader從而將其讀取為ArrayBuffer格式
    fr.readAsArrayBuffer(file);
}

注意這裡我們把this賦值給了that,然後再 audioContext.decodeAudioData的回撥函式中使用that來指代我們的Visualizer物件。這是因為作用域的原因。我們知道JavaScript中無法通過花括號來建立程式碼塊級作用域,而唯一可以建立作用域的便是函式。一個函式就是一個作用域。函式內部的this指向的物件要視情況而定,就上面的程式碼來說,它是audioContext。所以如果想要在這個回撥函式中呼叫Visualizer身上方法或屬性,則需要通過另一個變數來傳遞,這裡是that,我們通過將外層this(指向的是我們的Viusalizer物件)賦值給新建的區域性變數that,此時that便可以傳遞到內層作用域中,而不會與內層作用域裡面原來的this相沖突。像這樣的用法在原始碼的其他地方也有使用,細心的你可以下載本文的原始碼慢慢研究。

所以,在 audioContext.decodeAudioData的回撥函式裡,當解碼完成得到audiobuffer檔案(buffer引數)後,再把audioContext和buffer傳遞給Visualizer的_visualize()方法進一步處理:播放音樂和繪製頻譜圖。當然此時_visualize()方法還沒有下,下面便開始實現它。

建立Analyser分析器及播放音訊

上面已經將獲取的檔案進行解碼,得到了audio buffer資料。接下來是設定我們的AudioContext以及獲取頻譜能量資訊的Analyser節點。向Visualizer物件新增_visualize方法,我們在這個方法裡完成這些工作。

播放音訊

首先將buffer賦值給audioContext。AudioContext只相當於一個容器,要讓它真正豐富起來需要將實際的音樂資訊傳遞給它的。也就是將audio buffer資料傳遞給它的BufferSource屬性。

其實到了這裡你應該有點暈了,不過沒關係,看程式碼就會更明白一些,程式設計師是理解程式碼優於文字的一種生物。

var audioBufferSouceNode = audioContext.createBufferSource();
audioBufferSouceNode.buffer = buffer;

就這麼兩名,把音訊檔案的內容裝進了AudioContext。

這時已經可以開始播放我們的音訊了。

audioBufferSouceNode.start(0);

這裡引數是時間,表示從這段音訊的哪個時刻開始播放。

注意:在舊版本的瀏覽器裡是使用onteOn()來進行播放的,引數一樣,指開始時刻。

但此時是聽不到聲音的,因為還差一步,需要將audioBufferSouceNode連線到audioContext.destination,這個AudioContext的destination也就相關於speaker(揚聲器)。

audioBufferSouceNode.connect(audioContext.destination);
audioBufferSouceNode.start(0);

此刻就能夠聽到揚聲器傳過來動聽的聲音了。

_visualize: function(audioContext, buffer) {
    var audioBufferSouceNode = audioContext.createBufferSource();
    audioBufferSouceNode.connect(audioContext.destination);
    audioBufferSouceNode.buffer = buffer;
    audioBufferSouceNode.start(0);
}

建立分析器

建立獲取頻譜能量值的analyser節點。

var analyser = audioContext.createAnalyser();

上面一步我們是直接將audioBufferSouceNode與audioContext.destination相連的,音訊就直接輸出到揚聲器開始播放了,現在為了將音訊在播放前擷取,所以要把analyser插在audioBufferSouceNode與audioContext.destination之間。明白了這個道理,程式碼也就很簡單了,audioBufferSouceNode連線到analyser,analyser連線destination。

audioBufferSouceNode.connect(analyser);
analyser.connect(audioContext.destination);

然後再開始播放,此刻所有音訊資料都會經過analyser,我們再從analyser中獲取頻譜的能量資訊,將其畫出到Canvas即可。

假設我們已經寫好了畫頻譜圖的方法_drawSpectrum(analyser);

_visualize: function(audioContext, buffer) {
    var audioBufferSouceNode = audioContext.createBufferSource(),
        analyser = audioContext.createAnalyser();
    //將source與分析器連線
    audioBufferSouceNode.connect(analyser);
    //將分析器與destination連線,這樣才能形成到達揚聲器的通路
    analyser.connect(audioContext.destination);
    //將上一步解碼得到的buffer資料賦值給source
    audioBufferSouceNode.buffer = buffer;
    //播放
    audioBufferSouceNode.start(0);
    //音樂響起後,把analyser傳遞到另一個方法開始繪製頻譜圖了,因為繪圖需要的資訊要從analyser裡面獲取
    this._drawSpectrum(analyser);
}

繪製精美的頻譜圖

接下來的工作,也是最後一步,也就是實現_drawSpectrum()方法,將跟隨音樂而靈動的柱狀頻譜圖畫出到頁面。

繪製柱狀能量槽

首先你要對數字訊號處理有一定了解,嚇人的,不瞭解也沒多大關係。頻譜反應的是聲音各頻率上能量的分佈,所以叫能量槽也沒有硬要跟遊戲聯絡起來的嫌疑,是將輸入的訊號經過傅立葉變化得到的(大學裡的知識終於還是可以派得上用場了)。但特麼我知道這些又怎樣呢,僅僅為了裝逼顯擺而以。真實的頻譜圖是頻率上連續的,不是我們看到的最終效果那樣均勻分開鋸齒狀的。

通過下面的程式碼我們可以從analyser中得到此刻的音訊中各頻率的能量值。

var array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);

此刻array中儲存了從低頻0Hz到高頻~Hz的所有資料。頻率做為X軸,能量值做為Y軸,我們可以得到類似下面的圖形。

所以,比如array[0]=100,我們就知道在x=0處畫一個高為100單位長度的長條,array[1]=50,然後在x=1畫一個高為50單位長度的柱條,從此類推,如果用一個for迴圈遍歷array將其全部畫出的話,便是你看到的上圖。

取樣

但我們要的不是那樣的效果,我們只需在所有資料中進行抽樣,比如設定一個步長100,進度抽取,來畫出整個頻譜圖中的部分柱狀條。

或者先根據畫面的大小,設計好每根柱條的寬度,以及他們的間隔,從而計算出畫面中一共需要共多少根,再來推算出這個取樣步長該取多少,本例便是這樣實現的。說來還是有點暈,下面看簡單的程式碼:

var canvas = document.getElementById('canvas'),
    meterWidth = 10, //能量條的寬度
    gap = 2, //能量條間的間距
    meterNum = 800 / (10 + 2); //計算當前畫布上能畫多少條
var step = Math.round(array.length / meterNum); //計算從analyser中的取樣步長

我們的畫布即Canvas寬800px,同時我們設定柱條寬10px , 柱與柱間間隔為2px,所以得到meterNum為總共可以畫的柱條數。再用陣列總長度除以這個數目就得到取樣的步長,即在遍歷array時每隔step這麼長一段我們從陣列中取一個值出來畫,這個值為array[i*step]。這樣就均勻地取出meterNum個值,從而正確地反應了原來頻譜圖的形狀。

var canvas = document.getElementById('canvas'),
    cwidth = canvas.width,
    cheight = canvas.height - 2,
    meterWidth = 10, //能量條的寬度
    gap = 2, //能量條間的間距
    meterNum = 800 / (10 + 2), //計算當前畫布上能畫多少條
    ctx = canvas.getContext('2d'),
    array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
var step = Math.round(array.length / meterNum);計算從
analyser中的取樣步長
ctx.clearRect(0, 0, cwidth, cheight); //清理畫布準備畫畫
//定義一個漸變樣式用於畫圖
gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(1, '#0f0');
gradient.addColorStop(0.5, '#ff0');
gradient.addColorStop(0, '#f00');
ctx.fillStyle = gradient;
//對信源陣列進行抽樣遍歷,畫出每個頻譜條
for (var i = 0; i < meterNum; i++) {
    var value = array[i * step];
    ctx.fillRect(i * 12 /*頻譜條的寬度+條間間距*/ , cheight - value + capHeight, meterWidth, cheight);
}

使用requestAnimationFrame讓柱條動起來

但上面繪製的僅僅是某一刻的頻譜,要讓整個畫面動起來,我們需要不斷更新畫面,window.requestAnimationFrame()正好提供了更新畫面得到動畫效果的功能,關於requestAnimationFrame的使用及更多資訊可以從我的上一篇博文<requestAnimationFrame,Web中寫動畫的另一種選擇>中瞭解,這裡直接給出簡單改造後的程式碼,即得到我們要的效果了:跟隨音樂而靈動的頻譜柱狀圖。

var canvas = document.getElementById('canvas'),
    cwidth = canvas.width,
    cheight = canvas.height - 2,
    meterWidth = 10, //能量條的寬度
    gap = 2, //能量條間的間距
    meterNum = 800 / (10 + 2), //計算當前畫布上能畫多少條
    ctx = canvas.getContext('2d');
//定義一個漸變樣式用於畫圖
gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(1, '#0f0');
gradient.addColorStop(0.5, '#ff0');
gradient.addColorStop(0, '#f00');
ctx.fillStyle = gradient;
var drawMeter = function() {
    var array = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(array);
    var step = Math.round(array.length / meterNum); //計算取樣步長
    ctx.clearRect(0, 0, cwidth, cheight); //清理畫布準備畫畫
    for (var i = 0; i < meterNum; i++) {
        var value = array[i * step];
        ctx.fillRect(i * 12 /*頻譜條的寬度+條間間距*/ , cheight - value + capHeight, meterWidth, cheight);
    }
    requestAnimationFrame(drawMeter);
}
requestAnimationFrame(drawMeter);

繪製緩慢降落的帽頭

到上面一步,主要工作已經完成。最後為了美觀,再實現一下柱條上方緩慢降落的帽頭。

原理也很簡單,就是在繪製柱條的同時在同一X軸的位置再繪製一個短的柱條,並且其開始和結束位置都要比頻譜中的柱條高。難的地方便是如何實現緩慢降落。

首先要搞清楚的一點是,我們拿一根柱條來說明問題,當此刻柱條高度高於前一時刻時,我們看到的是往上衝的一根頻譜,所以這時帽頭是緊貼著正文柱條的,這個好畫。考慮相反的情況,當此刻高度要低於前一時刻的高度時,下方柱條是立即縮下去的,同時我們需要記住上一時刻帽頭的高度位置,此刻畫的時候就按照前一時刻的位置將Y-1來畫。如果下一時刻頻譜柱條還是沒有超過帽頭的位置,繼續讓它下降,Y-1畫出帽頭。

通過上面的分析,所以我們在每次畫頻譜的時刻,需要將此刻頻譜及帽頭的Y值(即垂直方向的位置)記到一個迴圈外的變數中,在下次繪製的時刻從這個變數中讀取,將此刻的值與變數中儲存的上一刻的值進行比較,然後按照上面的分析作圖。

最後給出實現的程式碼:

_drawSpectrum: function(analyser) {
    var canvas = document.getElementById('canvas'),
        cwidth = canvas.width,
        cheight = canvas.height - 2,
        meterWidth = 10, //頻譜條寬度
        gap = 2, //頻譜條間距
        capHeight = 2,
        capStyle = '#fff',
        meterNum = 800 / (10 + 2), //頻譜條數量
        capYPositionArray = []; //將上一畫面各帽頭的位置儲存到這個陣列
    ctx = canvas.getContext('2d'),
    gradient = ctx.createLinearGradient(0, 0, 0, 300);
    gradient.addColorStop(1, '#0f0');
    gradient.addColorStop(0.5, '#ff0');
    gradient.addColorStop(0, '#f00');
    var drawMeter = function() {
        var array = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
        var step = Math.round(array.length / meterNum); //計算取樣步長
        ctx.clearRect(0, 0, cwidth, cheight);
        for (var i = 0; i < meterNum; i++) {
            var value = array[i * step]; //獲取當前能量值
            if (capYPositionArray.length < Math.round(meterNum)) {
                capYPositionArray.push(value); //初始化儲存帽頭位置的陣列,將第一個畫面的資料壓入其中
            };
            ctx.fillStyle = capStyle;
            //開始繪製帽頭
            if (value < capYPositionArray[i]) { //如果當前值小於之前值
                ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight); //則使用前一次儲存的值來繪製帽頭
            } else {
                ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); //否則使用當前值直接繪製
                capYPositionArray[i] = value;
            };
            //開始繪製頻譜條
            ctx.fillStyle = gradient;
            ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight);
        }
        requestAnimationFrame(drawMeter);
    }
    requestAnimationFrame(drawMeter);
}

Reference:

相關推薦

音響感受HTML5 Audio API帶來視聽盛宴

話說HTML5的炫酷真的是讓我愛不釋手,即使在這個提到IE就傷心不完的年代。但話又說回來,追求卓越Web創造更美世界這樣高的追求什麼時候又與IE沾過邊兒呢?所以當你在看本文並且我們開始討論HTML5等前沿東西的時候,我們預設是把IE排除在外的。本文的例子可以工作在最新的Chrome及Firefox瀏覽器下,其

爬取抖音甜曲《好喜歡感受荷爾蒙的氣息

最近發現一首很火的歌,瞬間讓你感受到濃濃的青春懵懂感,這就是王廣允的《好喜歡你》。說實話,爬這種愛意濃濃的歌曲似乎不是我們這種單身XX應有的想法,但是還是想體會一下那些青春歲月裡的小幸福,話不多說,程式碼走起來。   本來想這裡直接貼上歌曲的連結,但是由於版權問題,大家可以去網易雲

IoT 10 安全挑戰來過招

人員 lips eclipse 文章 工作組 com developer china all 現今,越來越多的 IoT 設備部署在無法控制、復雜且通常惡劣的環境中,保護 IoT 系統,我們面臨著大量獨特的挑戰!據 Eclipse IoT 工作組 2017 年的調查,安全是

大數據:數據合集想要的這裏或許會有

數據資源大數據時代,用數據做出理性分析顯然更為有力。做數據分析前,能夠找到合適的的數據源是一件非常重要的事情,獲取數據的方式有很多種,不必局限。下面將從公開的數據集、爬蟲、數據采集工具、付費API等等介紹。給大家推薦一些能夠用得上的數據獲取方式。 一、公開數據庫 1.常用數據公開網站 UCI:經典的機器學習、

少聽忽悠的AI萬能論:不打四道鎖企業永遠無法享用AI

華為雲如果你是一位科技和AI愛好者,想必會在各種信息渠道看到“人工智能又能幹什麽了”、“人工智能又在某領域超過人類了”,這類消息近乎於每天都在我們的眼球前搖晃。久而久之,我們似乎會習慣性地認為AI已經可以拿下一切問題,甚至覺得AI已經是萬能的。這種想象假如只存在於普通消費者腦中,那麽可能還好;假如企業家和行業

微信好友揭秘使用Python抓取朋友圈數據通過人臉識別全面分析好友一起看透的“朋友圈”

類型 get ads pid 地圖 文本文 .json image pack 微信:一個提供即時通訊服務的應用程序,更是一種生活方式,超過數十億的使用者,越來越多的人選擇使用它來溝通交流。 不知從何時起,我們的生活離不開微信,每天睜開眼的第一件事就是打開微信,關註著朋友圈裏

面試資料工程師必須洞察HR的心思這些面試技巧懂麼?

很多面試大資料工程師職位的抱怨,為什麼面試的時候老是要考什麼演算法呀,還要現場寫程式碼?弄得大家天天去刷面試題,這些有什麼用?本文是由科多大資料的就業指導老師總結的面試的經驗和技巧。      那麼,今天就來聊聊這麼大資料工程師面試後面這麼做的原委。  

AI框架比拼喜歡哪一個?

轉載自https://baijiahao.baidu.com/s?id=1589649119274801302&wfr=spider&for=pc 人工智慧(AI)已經存在很長時間了。然而,由於這一領域的巨大進步,近年來它已成為一個流行語。人工智慧曾經被稱為一個完整的書呆子和天才

資料時代的角色是什麼?

大資料時代,不懂點資料分析都不好意思告訴別人你混網際網路、混大都市的,在大資料的環境下,我把市場上的分析師分為幾類   一、資料變現者   這類人一直在公司從事這業務分析的角色,他們一直在嘗試用資料去改變業務決策的流程變更和機遇,驅動這企業的北極星指標,更多做的事

3利器推薦寫出規範漂亮的python程式碼

新建Python軟體開發測試技術交友群QQ:952490269(加群備註software) Python學了好久,但是拿出來review的程式碼好像總是長的不夠俊美,不夠工整!因此標準化的程式碼規範就顯得尤為重要。今天就來推薦3個利器,python界廣泛認同的程式碼風格規範PEP8和兩個超牛的工

必須知道的8資料結構程式設計師面試

有些面試題會明確提及某種資料結構,例如,“給定一個二叉樹。”而另一些則隱含在面試題中,例如,“我們希望記錄每個作者相關的書籍數量。” 什麼是資料結構? 簡單地說,資料結構是以某種特定的佈局方式儲存資料的容器。這種“佈局方式”決定了資料結構對於某些操作是高效的,而對於其他操作則是低效的

資料時代誰的眼神鎖定

資料時代當前,歡迎來到楚門的世界。 雙十一餘韻未歇,剛處理完一波售後及退件等“剁手後遺症”的各方人馬也已經為再戰雙十二做好了準備。截至 12 日零點,天貓雙十一成交額達 2135 億元。與此同時,據國家郵政局監測資料顯示,主要電商企業 11 日全天共產生快遞物流訂單

新手如何進入資料領域500k神帶零基礎到精通路線

  總體而言,我們大資料人才劃分為三個大類: 一、 大資料開發工程師: 圍繞大資料系平臺系統級的研發人員, 熟練Hadoop、Spark、Storm等主流大資料平臺的核心框架。深入掌握如何編寫MapReduce的作業及作業流的管理完成對資料的計算,並能夠使用Ha

六個做PPT離不的輔助外掛一秒讓的PPT逼格滿滿!

不會做PPT? 一做PPT就頭疼? PPT做來做去沒新意? 別急!那是因為你還不知道這6個實用的PPT外掛,每一個都超實用! 1.PA口袋動畫 PA口袋動畫是一個致力於簡化PPT動畫設計的外掛,完善了PPT各種動畫相關功能,以高效簡潔的方式,改變了傳統PPT動畫的設計模式,讓PPT更加生動起

新手程式設計師必備的5開發工具看看都有了嗎?

“工欲善其事,必先利其器!”作為入門級別的程式設計師,幾款趁手的程式設計軟體是最需要的。除了Git、Visual Basic……等等,其實還有很多很很酷的程式設計工具。接下來就給大家看7款不一樣的程式設計工具,如有心動,純屬巧合。      

2018年最流行的十程式語言其中包括學的語言嗎?

對於程式設計界的初學者來說,最大的困難是決定從何處入手,或者應掌握哪種語言才能在職場上平步青雲。有時,專業程式設計師也面臨學習一門新語言似乎更卓有成效的情形。 2018年最流行的十大程式語言,其中包括你用的語言嗎? 無論是什麼原因,下面列出了世界上最流行的程式語言,以便了解哪些語言占主導地

雲合同電子合同:支付寶紅包簡訊真相起底有人領的比還多

今天,昨天,前天...你收到【支付寶紅包】了嘛一條條簡訊轟炸有沒有中了錦鯉的感覺什麼時候支付寶的營銷也走簡訊路線了 這種簡訊轟炸的風格很不支付寶,要知道,之前再大的活動,不管是春節集五福還是錦鯉,所有活動可都是在支付寶平臺內部發出通告,簡訊營銷難道不是100

顛覆!小小理髮店3年店800家征服2000萬人!

正在耗光消費者信任的理髮行業,橫空殺出一個程咬金! 10塊錢搞定“項上人頭”,3年開店800家,輕鬆斬獲2000萬粉絲! 老闆放話,未來每個人將免費理髮,每家店零元送給有心人! 山雨欲來風滿樓,一場行業大顛覆即將拉開序幕! 理髮行業正變成欺詐重災區 12315統

百業洗牌如何重生?揭祕微信通訊錄打群代加好友不為人知的營銷套路!?

百業大洗牌,你如何重生?銅板平哥–揭祕微信通訊錄打群代加好友不為人知的營銷套路 什麼是通訊錄打群,通訊錄拉群,微信通訊錄拉人進群? 就是你提供客戶的手機號和你建的微信群的二維碼,把你提供的手機號轉化成微信之後直接拉到你建的微信群裡面! 銅板平哥 告訴你看懂微信者看懂方法,得微信流量者得天下!

Python幹貨派送!一千個Python庫只有想不到沒有查不到!

媒體庫 反序列化 高質量 統計 操作類 圖數據庫 ret 應用服務 pcs 環境管理 管理 Python 版本和環境的工具 p – 非常簡單的交互式 python 版本管理工具。 pyenv – 簡單的 Python 版本管理工具。 Vex – 可以在虛擬環境中執行