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

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。

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

// 若下方未出現演示頁面請刷新。

?你也可以點此全屏演示? 或者前往GitHub進行代碼下載然后本地運行。

你還可以?下載示例音樂(如果你手頭沒有音頻文件的話)

文件列表:
bbc_sherlock_openning.mp3
Neptune Illusion Dennis Kuo .mp3
單曲Remix ┃ 愛上這個女聲 放進專輯里私藏 夜電播音員.mp3
愛啦啦.mp3

最后,喜歡的朋友可以去GitHub星我(star me)叉我(fork me)。

?

這里將要介紹的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() {//這里判斷一下文件長度可以確定用戶是否真的選擇了文件,如果點了取消則文件長度為0if (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 buffervar 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用來做解碼ArrayBufferaudioContext.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數據賦值給sourceaudioBufferSouceNode.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軸,我們可以得到類似下面的圖形。

你也可以上傳文件自己看一下效果?http://sandbox.runjs.cn/show/1kn8nr4l?

所以,比如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);
復制代碼

?

查看效果?http://sandbox.runjs.cn/show/q1ng0jgp

繪制緩慢降落的帽頭

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

原理也很簡單,就是在繪制柱條的同時在同一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:

  1. A question about how to make an audio visualizer:?http://stackoverflow.com/questions/3351147/html5-audio-visualizer
  2. Web audio API:?http://www.html5rocks.com/en/tutorials/webaudio/intro/
  3. File reader in JavaScript:?https://developer.mozilla.org/en-US/docs/Web/API/FileReader
  4. Local audio visualizer source code:?http://cbrandolino.github.io/local-audio-visualizer/docs/local_audio_visualizer
  5. Audio context from MDN:?https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
  6. Window.requestAnimationFrame():https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame
  7. 3d visualizer with three.js :?http://do.adive.in/music/
  8. A CodePen example:?http://s.codepen.io/Wayou/fullpage/auCLE?
  9. Visualizer tutorial :?http://www.smartjava.org/content/exploring-html5-web-audio-visualizing-sound
  10. Web audio examples from Google code :?http://chromium.googlecode.com/svn/trunk/samples/audio/index.html

?

轉自:https://www.cnblogs.com/Wayou/p/3543577.html#top

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/448993.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/448993.shtml
英文地址,請注明出處:http://en.pswp.cn/news/448993.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Microsoft Visual Studio 2010(vs2010) 中文版安裝

Microsoft Visual Studio 2010(vs2010) 中文版安裝 日期&#xff1a;2019-05-12 時間&#xff1a;20:03:36 編輯&#xff1a;張國富 下載地址 基本簡介 Microsoft Visual Studio&#xff08;vs2010是簡稱&#xff09;是微軟公司推出的開發環境。visual studio 2010…

JVM的幾點性能優化

HotSpot&#xff0c;家喻戶曉的JVM&#xff0c;我們的Java和Scala程序就運行在它上面。年復一年&#xff0c;一次又一次的迭代&#xff0c;經過無數工程師的不斷優化&#xff0c;現在它的代碼執行的速度和效率已經逼近本地編譯的代碼了。 它的核心是一個JIT&#xff08;Just-I…

IDEA配置 及 快捷鍵

出處&#xff1a; https://www.cnblogs.com/hero123/p/10120552.html 快捷鍵&#xff1a; 格式化代碼 CtrlaltL 后退Ctrlalt <- 格式化代碼快捷鍵&#xff1a;Ctrl Alt L 刪除整行&#xff1a;CtrlX 實現類 ctrl alt CtrlN 查找類 CtrlShiftN 查找文件 CTRLSHIFTALTN 查找…

LeetCode Decode Ways

123123轉載于:https://www.cnblogs.com/ZHONGZHENHUA/p/10854545.html

SpringBoot 之集成 Spring AOP

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 在開始之前&#xff0c;我們先把需要的jar包添加到工程里。新增Maven依賴如下&#xff1a; <dependency><groupId>org.spri…

9件事把你從消極情緒中解救出來

也許你很難相信&#xff0c;但是情緒可以通過重復形成習慣。消極情緒甚至可以變成某種嵌入你每日生活的東西。 如何將它們趕跑? 你發現你不斷地埋怨世界和自己?你可以輕易地生氣并且對人變得刻薄?那憤怒又是否成為你對事情本能的回應了?如果你對所述問題中的一個回答了“是…

數據庫主鍵自增插入顯示值

版權聲明&#xff1a;本文為博主原創文章&#xff0c;未經博主同意不得轉載。 https://blog.csdn.net/nwsuaf2009012882/article/details/32703597 SQL Server 2008 數據庫主鍵自增插入顯示值 前幾天在工作的時候遇到在刪除數據庫中表的數據的時候。刪除之后&#xff0c;又一次…

解決: This application has no explicit mapping for /error, so you are seeing this as a fallback.

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 報錯如題&#xff0c;出現這個異常說明了跳轉頁面的url無對應的值. 原因1: Application啟動類的位置不對.要將Application類放在最外側…

Selenium自動化獲取WebSocket信息

性能日志 ChromeDriver支持性能日志記錄&#xff0c;您可以從中獲取域“時間軸”&#xff0c;“網絡”和“頁面”的事件&#xff0c;以及指定跟蹤類別的跟蹤數據。啟用性能日志 默認情況下不啟用性能日志記錄。因此&#xff0c;在創建新會話時&#xff0c;您必須啟用它。 Desir…

零負債之人的10個習慣

無論你是已下定決心要于今年實現零負債&#xff0c;還是距離這個目標的實現有很長的路要走&#xff0c;能受到啟發總是好事。 看看你認識的已經過上“無債一身輕”生活的人──朋友、家人、同事或是你認為可能與其他無負債之人具有類似品質的人。 下文為無負債之人的10個共同…

《App后臺開發運維與架構實踐》第3章 App后臺核心技術

2019獨角獸企業重金招聘Python工程師標準>>> 3.1 用戶驗證方案 3.1.1 使用HTTPS協議 HTTPS協議是“HTTP協議”和“SSL/TLS”的組合。SSL&#xff08;Secure Sockets Layer&#xff09;&#xff0c;即安全套接層&#xff0c;是為了解決因HTTP協議是明文而導致傳輸內容…

IntelliJ IDEA 配置 JDK

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 提前安裝jdk&#xff0c;配置環境變量 一、配置jdk 1、依次點開File -->Project Structure&#xff0c;點擊左側標簽頁&#xff0c…

xml編輯無提示?這么破!

在學習testng這個單元測試框架時&#xff0c;如果咱們碰到了編輯測試套件xml&#xff0c;不提示的情況&#xff08;有提示方便咱們學習&#xff0c;并且testng的測試套件定義必須按照他的dtd文件約束來&#xff09;&#xff0c;咱們可以按照下面的步驟去解決這個問題。 1.檢查t…

“云棲直播”升級為“公開課”

直播平臺是面向廣大開發者的視頻學習平臺&#xff0c;幫助廣大開發者學習最新技術&#xff0c;了解最新阿里云產品以及最新技術發展趨勢&#xff0c;幫助開發者們不斷學習與成長。截止到2019年3月&#xff0c;直播共進行800余場&#xff0c;觀看人次100萬。  社區將對“云棲直…

遭銀行賬號詐騙最快最有效自救法

銀行卡或賬戶詐騙案件層出不窮&#xff0c;當匯錯款時該怎么做&#xff0c;切記以下方法&#xff1a; 一、當匯錯款或被騙匯款后&#xff0c;最快最有效的緊急自救法&#xff1a;當你把自己的錢不小心匯到了不該匯的人卡上&#xff0c;或者被騙子忽悠而把錢匯給了騙子&#xf…

SQL 判斷非空 NULL :IFNUL( ) 、COALESCE( ) 、ISNULL( ) 、NVL( )

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. mysql 支持&#xff1a; IFNULL&#xff08;&#xff09;、COALESCE&#xff08;&#xff09; 如 IFNULL(UnitsOnOrder, 0) 或者 CO…

navigator.geolocation的應用 - 將定位信息顯示在百度地圖上

在學習navigator.geolocation的時候&#xff0c;有一個實例是獲取坐標后顯示在谷歌地圖上。眾所周知&#xff0c;谷歌地圖國內并不能直接訪問&#xff0c;得用特殊手段&#xff0c;那我要測試的時候還要開著梯子挺麻煩的&#xff0c;想給別人用也得那個人能訪問谷歌地圖先。 地…

centos7 mysql數據庫安裝和配置

2019獨角獸企業重金招聘Python工程師標準>>> 一、系統環境 yum update升級以后的系統版本為 [rootyl-web yl]# cat /etc/redhat-release CentOS Linux release 7.1.1503 (Core) 二、mysql安裝 一般網上給出的資料都是 #yum install mysql #yum install mysql-serve…

5種聰明工作法

1、每天最多做三件事 請拿出你落落長的待辦清單&#xff0c;圈出最重要的一~三件事&#xff0c;然后給自己一天的時間&#xff0c;卯足全力解決它! 你不需要因為還有很多事要做而焦慮&#xff0c;只需要專注今天、當下、以及最重要的問題。 《與成功有約》作者史蒂芬.柯維(Step…

【Quartz】Quartz概述及入門實例

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Quartz 在開源任務調度框架中的翹首&#xff0c;它提供了強大任務調度機制&#xff0c;難能可貴的是它同時保持了使用的簡單性。Quartz 允…