文章由再見落日餘暉投稿,感謝@人間最美四月天建哥審校。javascript
注:本文使用的Web Audio API遵循W3C在18年9月發佈的候選推薦版本,本文代碼在Chrome76中測試經過,請注意代碼兼容性。若有錯誤,請不吝指正。html
每段緣起始都很簡單,或是擦肩而過的那陣清風,或是四目相對的那縷溫情,亦是共經患難的那份情誼……初春的一天,在我被工做中出現的問題折磨的不堪之際,經朋友引薦,她出如今個人視野中。我忘不了她解決問題時的颯爽英姿,那幹練的身影在我腦海中遲遲不能散去。我有意要了解她,卻總感受只在冰山一角。終於經歷了幾個月的徘徊後,我下定決心再也不等待,開一個系列來介紹個人理解。Why now? Why not?java
本文是《認識 Web Audio》系列文章的第一篇,主要涉及音頻上下文的一些簡單概念的介紹和使用。整個系列分爲如下文章:ios
長期以來在網頁上播放音視頻都是一個痛點,雖然HTML5引入了audio、video標籤來實現基本的音視頻播放,但卻不足以應付更復雜狀況如遊戲引擎、實時交互等場景下的音頻處理。因而,Web Audio應運而生。但要說明的是,它之於audio標籤並非替代關係,而是相似img和canvas的一種補充的關係。Web Audio播放音頻能實現安卓、ios的一致性,消除諸如播放有延遲、不能循環播放等問題,如howler.js(固然不只於此);其次能夠利用振盪器、濾波器等創造本身的樂器,進行交互式音樂的構建、實現歌曲的演奏,如Tone.js;還能夠對音頻播放進行可視化,如Pts.js……git
Web Audio是應在web瀏覽器中處理音頻的需求而產生的,它早已不是一個新鮮的概念,但並無大規模使用倒是一個不爭的事實。除去不值一提的兼容性問題,我想緣由大概有幾個,首先是API居多,由於它要實如今瀏覽器產生、處理音頻必需要有相應的接口,但和專業音頻製做程序仍有必定差距,不是特別能吸引專業人士;而後就是要實現實時WebRTC、遊戲級引擎的音頻特效等所須要的專業知識(數字信號處理、通訊原理等)所帶來的學習成本也是讓不少人望而卻步。不過大勢所趨,它還在處於不斷髮展中,相信將來前景仍是不錯的。github
Web Audio提供的合成聲音、添加特效、音頻可視化功能,是在音頻上下文完成的,它將不一樣的操做細化爲對應的節點實現了模塊化(Modular routing),各個節點又能夠經過connect方法相鏈接,進行必要的處理後,最終輸出到目標節點(音頻上下文的destination屬性,一般是聲音輸出設備如揚聲器)。整個流程鏈接在一塊兒造成一個音頻路由圖(audio routing graph)。web
Web Audio Api簡單工做流程爲:ajax
gainNode.gain.linearRampToValueAtTime(value, endTime)
就能實如今endTime-currentTime時間段內,聲音值從原始到value值的變化,能夠說操縱很輕鬆了。在正式使用以前,首先了解一下Web Audio Api的部分接口。canvas
BaseAudioContext是實際使用的音頻上下文AudioContext(用於實時渲染)和OfflineAudioContext(用於離線渲染)的基類,不能被直接實例化。音頻節點都在其內建立並相互鏈接在一塊兒,容許信號最終鏈接到AudioDestinationNode節點進行播放,造成一個音頻路由圖。其包含的部分屬性以下:瀏覽器
resume()
和suspend()
方法切換,調用close()
以後音頻上下文將會釋放系統資源,不能再次使用。AudioContext實時音頻上下文,來直接爲用戶產生信號。AudioContext初始化時state默認爲suspended,由於自動播放策略限制,必須通過用戶操做後才容許處於運行狀態,這可經過resume()
恢復音頻上下文,或者調用AudioBufferSourceNode的start()
時來恢復上下文。主動暫停上下文使用suspend()
,關閉使用close()
。但要注意的是雖然兩者均可以釋放包括線程,進程和音頻流等系統資源,但前者可經過resume()恢復運行,然後者意味着釋放全部資源,此後將沒法使用或再次恢復它。構造函數中可傳入contextOptions的對象,若是選項中包含sampleRate屬性,則採樣率設置爲該屬性,不然使用默認輸出設備的採樣率。
OfflineAudioContext離線音頻上下文,雖然有destination屬性,但實際並不會渲染到音頻輸出硬件中,但會盡量快的渲染(通常來講要比實時渲染更快),經過startRendering()
返回AudioBuffer,適用於那些能夠在後臺進行音頻處理的場景。使用OfflineAudioContext(numberOfChannels, length, sampleRate)
構造函數進行初始化,參數必填,不過也能夠將一個包含這三個屬性的對象(sampleRate/length必須屬性)做爲參數傳遞。由於構造時就已經肯定了長度,因此在渲染length/smapleRate時間以後其狀態就會變爲closed,不能繼續使用了。和AudioContext不一樣的是,離線音頻上下文只有suspend(suspendTime)
方法,沒有close()
方法,在音頻數據渲染完以後主動關閉。終止時必須傳入終止時間,以在指定時刻終止,該方法通常來講只有在同步操做音頻數據時纔有用。
AudioNode:音頻節點,是全部展現在音頻路由圖中節點模塊(如音頻源節點、目的節點、過濾器節點、增益節點等)的基類,不能直接實例化,提供connect方法實現節點間的鏈接。
AudioBuffer:音頻緩衝區,由createBuffer(numberOfChannels, length, sampleRate)
或構造器建立而來,參數都是必填,生成指定長度的音頻buffer,該buffer默認初始化爲0。該buffer包含duration(=length/sampleRate)、length、numberOfChannels、sampleRate屬性。可經過getChannelData(channel)
方法獲取某一通道下的音頻數據,返回一個Float32Array類型的數據。它只是保存數據源,不是真正的音頻節點,只有賦值給相應的AudioNode纔有做用。像AudioBufferSourceNode的buffer就是AudioBuffer類型。
AudioParam:音頻參數接口,控制AudioNode的某個方面,好比音量。該接口除了value屬性,其餘屬性都是隻讀的。能夠直接爲參數的value賦值,也可使用AudioParam的方法實現預先設定。setValueAtTime(value, startTime)
能夠作到的當AudioContext的currentTime走到給定的startTime時間後進行賦值爲value。linearRampToValueAtTime(value, endTime)
能夠作到從當前時間到endTime從當前值線性變化到value值。經常使用場景是實現聲音播放的漸隱漸現,來避免使用定時器來控制音量大小。setTargetAtTime(target, startTime, timeConstant)
可用於實現聲音信號的衰變。
AudioBufferSourceNode:音頻數據源節點,由音頻上下文的createBufferSourceNode()
方法或構造器建立獲得。它接受一個AudioBuffer做爲buffer屬性來提供數據源,數據源必須來自內存。
MediaElementAudioSourceNode:媒體元素節點,由指定的HTMLMediaElement(一般爲audio或video標籤)建立獲得,調用該方法後,音頻播放的控制權移交給當前音頻上下文的路由圖。即只有當前AudioContext狀態爲running時才能播放,而且要鏈接到AudioContext的destination節點才能播放出聲音。它容許咱們操做audio、video聲音軌道數據。
MediaStreamAudioSourceNode:媒體流音頻節點,獲取實時媒體流的音頻源。使用音頻上下文的createMediaStreamSource(mediaStream)
方法或new MediaStreanAudioSourceNode(context, {mediaStream})
建立而來
MediaStreamAudioDestinationNode:音頻流輸出目的節點,數據存儲在stream屬性中,作臨時存儲用。
AudioDestinationNode:音頻目的節點,每一個AudioContext只有惟一的一個該節點,由BaseAudioContext的destination屬性提供,通常爲對應的音頻輸出硬件,如揚聲器。
接下來從加載音頻的角度講述如何使用Web Audio。加載音頻方式主要有四種:
使用HTMLMediaElement元素(如audio/video)做爲數據源,適合應用於音頻數據量很大的場景
下面是一個接管頁面audio標籤音頻播放的示例,並使用gainNode節點來控制音頻的音量
<audio src="./1.mp3" controls></audio>
<button>播放</button>
<input type="range" value="1" min="0" max="3" step=".1" />
<script> const ac = new AudioContext() const range = document.querySelector('input[type="range"]') const btn = document.querySelector('button') const audio = document.querySelector('audio') const ms = ac.createMediaElementSource(audio) // 等價於如下語句 // const bf = new MediaElementAudioSourceNode(ac, { mediaElement: audio }) const gainNode = ac.createGain() gainNode.gain.value = 1 ms.connect(gainNode).connect(ac.destination) btn.onclick = function() { // AudioContext若處於suspended狀態,須要恢復後才能正常播放 if (ac.state === 'suspended') ac.resume() audio.play() } range.onchange = e => { gainNode.gain.value = e.target.value } </script>
複製代碼
ajax異步獲取數據:由於只有當所有加載到音頻數據後才能使用,因此通常應用在少許音頻數據場景中
下面是一個先使用離線音頻上下文異步獲取到音頻數據再傳給音頻上下文進行播放的例子
const ac = new AudioContext()
// 這裏只是爲展現OfflineAudioContext的使用,實際上徹底能夠直接用AudioContext異步獲取到數據進行播放
const oc = new OfflineAudioContext(2, 44000 * 100, 44000)
async function fetchAudio(src) {
const file = await fetch(src)
const fileBuffer = await file.arrayBuffer()
const buffer = await oc.decodeAudioData(fileBuffer)
const bufferSource = oc.createBufferSource()
bufferSource.buffer = buffer
bufferSource.connect(oc.destination)
bufferSource.start(0)
oc.startRendering()
.then(buffer => {
// 這裏對a、b進行賦值是爲了給第三個例子的buffer創造來源
window.a = buffer.getChannelData(0)
window.b = buffer.getChannelData(1)
const bs = new AudioBufferSourceNode(ac, { buffer })
bs.connect(ac.destination)
bs.start(0)
})
.catch(e => {
console.log(e)
})
}
fetchAudio('1.mp3')
複製代碼
自定義聲音:使用OscillatorNode產生相似正弦、鋸齒形波形或者建立buffer並使用數據填充
下面展現了一個使用自定義數據填充AudioBuffer的例子,在建立buffer時須要傳入信道數、採樣率、採樣長度,使用AudioBuffer的copyToChannel()
方法能夠實現將一個Float32Arrays類型數據複製到指定信道。固然數據也能夠本身產生,mdn官網上有一個使用隨機數生成噪聲的例子。
const ac = new AudioContext()
const buffer2 = ac.createBuffer(2, 10 * 41000, 41000)
// 這裏使用了上面第二個例子代碼中的數據來源
buffer2.copyToChannel(a, 0, 0)
buffer2.copyToChannel(b, 1, 0)
const bf = ac.createBufferSource()
bf.buffer = buffer2
bf.connect(ac.destination)
bf.start()
複製代碼
經過Media Stream API獲取相機和麥克風的數據:使用MediaSteamAudioSourceNode來實現,適用於WebRTC或想要錄製音頻的場景
下面是一個使用getUserMedia和MediaRecorder進行錄音的例子。getUserMedia獲取用戶錄音的權限,由於在錄音過程當中不須要輸出,因此將音頻流保存至MediaStreamAudioDestinationNode節點,而且在音頻流傳遞過程當中還使用了低通濾波器實現低音加強,錄完音保存至audio標籤,使用原生控件可進行播放和下載。一樣,此例子只作展現用,其實直接將mediaStream做爲參數傳遞給MediaRecorder構造器也是能實現錄音。不過要注意的是MediaRecorder目前還僅在高版本PC瀏覽器中受支持,不太適合應用於生產環境。
<audio controls></audio>
<button>開始錄音</button>
<script>
const ac = new AudioContext()
const biquadFilter = ac.createBiquadFilter()
const msa = new MediaStreamAudioDestinationNode(ac)
const mediaRecorder = new MediaRecorder(msa.stream)
let chunks = [],
start = false
const btn = document.querySelector('button')
const audio = document.querySelector('audio')
// 麥克風->低音濾波->音頻流目的節點 在這個過程當中進行錄製->audio標籤播放
navigator.mediaDevices
.getUserMedia({
audio: true
})
.then(mediaStream => {
const ms = ac.createMediaStreamSource(mediaStream)
biquadFilter.type = 'lowshelf'
biquadFilter.frequency.value = 40000
biquadFilter.gain.value = 1
ms.connect(biquadFilter).connect(msa)
})
.catch(e => console.log(e))
// 點擊按鈕開始錄音或結束錄音
btn.onclick = () => {
if (!start) {
mediaRecorder.start()
btn.innerText = '結束錄音'
} else {
mediaRecorder.stop()
btn.innerText = '開始錄音'
}
start = !start
}
// 把獲得的錄音流保存至chunk
mediaRecorder.ondataavailable = e => {
chunks.push(e.data)
}
mediaRecorder.onstop = e => {
const blob = new Blob(chunks, { type: 'audio/ogg; codecs=opus' })
audio.src = URL.createObjectURL(blob)
}
</script>
複製代碼