【前端週刊】20191101 初識 Web Audio

文章由再見落日餘暉投稿,感謝@人間最美四月天建哥審校。javascript

注:本文使用的Web Audio API遵循W3C在18年9月發佈的候選推薦版本,本文代碼在Chrome76中測試經過,請注意代碼兼容性。若有錯誤,請不吝指正。html

Web Audio Api的兼容性(數據來自can i use)

引言

每段緣起始都很簡單,或是擦肩而過的那陣清風,或是四目相對的那縷溫情,亦是共經患難的那份情誼……初春的一天,在我被工做中出現的問題折磨的不堪之際,經朋友引薦,她出如今個人視野中。我忘不了她解決問題時的颯爽英姿,那幹練的身影在我腦海中遲遲不能散去。我有意要了解她,卻總感受只在冰山一角。終於經歷了幾個月的徘徊後,我下定決心再也不等待,開一個系列來介紹個人理解。Why now? Why not?java

本文是《認識 Web Audio》系列文章的第一篇,主要涉及音頻上下文的一些簡單概念的介紹和使用。整個系列分爲如下文章:ios

  • 碰見你,很幸運——初識 Web Audio
  • 知其然,知其因此然——Web Audio原理探究
  • 我和音符有個約會——音頻可視化篇
  • 爲你彈奏肖邦的夜曲——音頻創做篇
  • 是誰帶來遠古的呼喚——音頻空間化篇

Web Audio背景

長期以來在網頁上播放音視頻都是一個痛點,雖然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

  1. 建立音頻上下文(AudioContext或OfflineAudioContext)
  2. 在上下文中建立聲音來源(如audio標籤、xmlhttprequest請求獲得的arraybuffer、oscillator振盪器產生的各類波形)
  3. 建立特效節點(如混響、各類濾波、平移、壓縮)
  4. 選擇音頻的最終產出地(如系統揚聲器)
  5. 未來源連到特效節點,將特效節點連到目的節點
    Web Audio Api使得咱們能夠更精確(時間上能夠作到無延遲、空間上能夠產生衰減效果模擬真實環境)的操做音頻,這是audio標籤所不具有的。舉個例子來講,若是要實現音頻的漸入漸出效果,原始方法須要使用定時器來不斷更改volume來實現,而使用web audio只需將源節點連到增益節點(GainNode),設置gainNode.gain.linearRampToValueAtTime(value, endTime)就能實如今endTime-currentTime時間段內,聲音值從原始到value值的變化,能夠說操縱很輕鬆了。

Web Audio基礎

在正式使用以前,首先了解一下Web Audio Api的部分接口。canvas

音頻上下文接口
  • BaseAudioContext是實際使用的音頻上下文AudioContext(用於實時渲染)和OfflineAudioContext(用於離線渲染)的基類,不能被直接實例化。音頻節點都在其內建立並相互鏈接在一塊兒,容許信號最終鏈接到AudioDestinationNode節點進行播放,造成一個音頻路由圖。其包含的部分屬性以下:瀏覽器

    • state屬性是一個枚舉屬性,取值爲能夠是suspended/running/closed,表示音頻上下文的當前狀態。前兩個可經過實例的resume()suspend()方法切換,調用close()以後音頻上下文將會釋放系統資源,不能再次使用。
    • currentTime屬性表示上下文的運行時間,當上下文處於running狀態時該值會以均勻的速度單調遞增,由渲染線程控制,不必定和處理的音頻時間同步。
    • destination屬性是一個AudioDestinationNode節點,一般是實際的音頻輸出設備。
    • sampleRate只讀屬性,表示採樣率,在處理過程當中保持不變,所以實時處理中不支持採樣率轉換。在不設置的狀況下默認爲音頻輸出設備的採樣率。若是處理時使用的採樣率和音頻輸出設備的採樣率不一致會進行重採樣。
  • 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的使用

接下來從加載音頻的角度講述如何使用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>
複製代碼

參考資料

  1. Web Audio API規範
  2. MDN Web_Audio_API專題
相關文章
相關標籤/搜索