[Voice communications] 看得到的音頻流


上文介紹了 Web Audio API 的相關知識,以及如何在你的 web 程序中引入 音頻流,內容都是介紹性的,所以沒有寫太多 DEMO。本文重點講解如何利用 Web Audio API 中的中間節點拿到音頻信號的信息,並將信息轉化成信號圖繪制到 canvas 中。

從上文中我們了解到,AudioContext 是音頻播放和處理的一個環境,大概的流程是這個樣子:

  +---------------+------------------------------------+
  | AudioContext  |                                    |
  +---------------+                                    |
  |        +-------+    +-------+    +-------+         |
  |        |       |    |       |    |       |         |
==========>| Source|===>|Lots of|===>|  Dest | Output  |
Multi Input|       |    |       |    |       |===========>
==========>|  Node |===>| Nodes |===>|  Node |         |
  |        |       |    |       |    |       |         |
  |        +-------+    +-------+    +-------+         |
  |                         |                          |
  |                         |    Created By Barret Lee |
  +-------------------------|--------------------------+
                            |         +-------------+
                            +========>| Other Tools |
                              signal  +-------------+

在 AudioContext 中,通過一個結點(AudioNode)來接受輸入源,中間的一些結點可以過濾、放大、去雜等處理 Source Node 的信號,處理之后可以送到 AudioContext 的輸出結點,然后啟用 source.start() 播放音頻信息;也可以將處理的信息送到外部交給其他對象來處理,比如本文要談到的,將信號交給 Canvas 來處理,這樣就能看到音頻信號的波形圖了。

本文地址:http://www.cnblogs.com/hustskyking/p/webAudio-show-audio.html,轉載請注明源地址。

注:較新版 Google Chrome 和 Firefox 才能運行本文中的 DEMO。

一、節點的連接

先說一說,各個節點在 AudioContext 中的連接是如何用代碼實現的:

// 創建一個 AudioContext 環境
var context = new AudioContext();

function playSound() {
    // 創建一個 Source 節點
    var source = context.createBufferSource();
    // 拿到輸入源(狗吠)
    source.buffer = dogBarkingBuffer;
    // 將 source 節點鏈接到 destination 節點(輸出節點)
    source.connect(context.destination);
    // currentTime 設定為 0,開始播放
    source.start(0);
}

上面的連接十分簡單,直接將 source 節點連接到 destination 節點,相當於沒有經過人任何處理,直接輸出了,而下面的方式是創建一個中間節點,對信號做一些處理,不過在拿到 Source 的方式上跟上面有些不一樣:

var audio = document.getElementsByTagName("audio")[0];
// context 為一個 AudioContext 環境
// 從 audio 元素中拿到輸入源,也就是上圖所看到的 Mutil Input
var source = context.createMediaElementSource(audio);
// 建立一個處理延時節點
var delayNode = context.createDelay();
// sourceNode -> delayNode -> destinationNode
source.connect(delayNode);
delayNode.connect(context.destination);

這里需要注意的是,destination 是 AudioContext 實例的固有屬性,他就是信號的最終匯聚的位置,也是信號的輸出位置。下面是一個簡單的 DEMO 代碼:

<audio src="http://qianduannotes.duapp.com/file/tankWar.mp3" id="origin"></audio>
<audio src="http://qianduannotes.duapp.com/file/tankWar.mp3" id="audio"></audio>
<input type="button" onclick="origin.play()" value="原始音質 播放" />
<input type="button" onclick="origin.pause()" value="原始音源 暫停" /><br/>
<input type="button" onclick="audio.play()" value="濾波音質 播放" />
<input type="button" onclick="audio.pause()" value="濾波音源 暫停" />
<script type="text/javascript">
    var AudioContext = AudioContext || webkitAudioContext;
    var context = new AudioContext();
    var source = context.createMediaElementSource(audio);
    // 低通濾波節點(高頻信號被過濾,聽到的聲音會很沉悶)
    var FilterNode = context.createBiquadFilter("lowpass");
    // sourceNode -> FilterNode -> destinationNode
    source.connect(FilterNode);
    FilterNode.connect(context.destination);
</script>

上面的代碼,AudioContext 獲取 audio 源的原理是這樣的:

  1. audio有一個內置的輸出通道
  2. AudioContext 通過 createMediaElementSource 將 audio 的輸出直接拉去到新的環境中,之前 audio 環境被破壞
  3. 拉去的 source 沒有 start 函數,他會一直監聽 audio 的操控,當 play 函數被觸發的時候,開始播放音頻。也可以認為,play 函數觸發了 start (老版瀏覽器中是 noteOn)

下面是一個演示圖:

 +----------+------------------------------+
 | audio    |                              |
 +----------+                              |
 |     +--------+      //  +-------------+ |
 |     | Source |=====//==>| Destination | |
 |     +--------+  | //    +-------------+ |
 |                 |                       |
 +-----------------|-----------------------+
                   |   Created By Barret Lee
          +--------|-----+--------------------------+
          |        ↓                                |
          |     +--------+    +-------+    +------+ |
          |     | Source |===>| Nodes |===>| Dest | |
          |     +--------+    +-------+    +------+ |
          |                          +--------------+
          |                          | AudioContext |
          +--------------------------+--------------+

二、兩個中間節點的介紹

1. ScriptProcessorNode

我們可以直接使用 JavaScript 操控這個節點,他的作用是產生、傳遞、分析一段音頻。他有一個 bufferSize 屬性和一個 onaudioprocess 事件。初始化一個 ScriptProcessorNode:

var processor=context.createScriptProcessor(4096, 1, 1);

他接收三個參數,第一個是 bufferSize 的大小,取值范圍是 Math.pow(2, N) ( 8≤N≤14 ),第二個參數是送入的 channel 數,第三個參數是輸出的 channel 數。信息不會自動通過這個節點需要我們自己將輸入的信號復制到輸出位置去:

processor.onaudioprocess = function(e){
    //獲取輸入和輸出的數據緩沖區
    var input = e.inputBuffer.getChannelData(0);
    var output = e.outputBuffer.getChannelData(0);

    //將輸入數緩沖復制到輸出緩沖上
    for(var i = 0, len = input.length; i < len; i++){
        output[i] = input[i];
    }
}

這樣處理的原因是因為多個輸入要對應對個輸出,也有可能是多對一或者一對多,所以這些信息的設定必須要人為去控制。

關於 ScriptProcessorNode 的介紹,具體請移步http://www.w3.org/TR/webaudio/#ScriptProcessorNode-section

2. AnalyserNode

通過這個節點我們可以對信號進行頻域和時域上的分析,學過 通信原理 的同學對這些屬於應該是十分熟悉的。

interface AnalyserNode : AudioNode {

    // Real-time frequency-domain data 
    void getFloatFrequencyData(Float32Array array);
    void getByteFrequencyData(Uint8Array array);

    // Real-time waveform data 
    void getByteTimeDomainData(Uint8Array array);

    attribute unsigned long fftSize;
    readonly attribute unsigned long frequencyBinCount;

    attribute double minDecibels;
    attribute double maxDecibels;

    attribute double smoothingTimeConstant;

};

上面是這個節點的接口信息,不要感到奇怪,對接口的描述,都是使用這種方式,從上面我們可以看到,他有三個方法,四個屬性。fftSize 是指頻率分析下的快速傅里葉變換大小,他的值被限定在 32-2048 的 2 的整數次方。

關於 AnalyserNode 的介紹,具體請移步http://www.w3.org/TR/webaudio/#AnalyserNode-section

三、音頻信息的提取

利用上面介紹的兩個節點可以十分輕松的提取到音頻信息,如使用 ScriptProcessorNode,在他的 onaudioprocess 觸發的時候,可以拿到 input 信息,此時也就是音頻信息流。

processor.onaudioprocess = function(e){
    //獲取輸入和輸出的數據緩沖區
    var input = e.inputBuffer.getChannelData(0);

    doSomething(input);
};

上面這種方式拿到數據的效率是比較低的,一般可以直接使用 AnalyserNode 節點。這個節點中一個獲取緩沖數據區的方法叫做 getByteTimeDomainData,這個方法的設計是十分偏底層的,或者對 JSer 來說,這個借口的設計並不合理,可以看看:

//以fftSize為長度創建一個字節數組作為數據緩沖區
var output = new Uint8Array(analyser.fftSize);
// 將獲取得到的數據賦值給 output
analyser.getByteTimeDomainData(output);

這里是把 output 作為引用傳進 getByteTimeDomainData 函數中,相信大家應該沒有在 JS 中遇到過這樣的寫法吧~ (我覺得在該 web 標准定稿的時候,這里一定會做修改!)

四、信號圖的繪制

上面我們已經拿到了信號數據了,繪制工作其實就是 canvas 的事情啦~

var width = canvas.width,
    height = canvas.height,
    g = canvas.getContext("2d");

// 將坐標原點移動到(0.5, height / 2 + 0.5)的位置
g.translate(0.5, height / 2 + 0.5);

然后繪制圖形:

processor.onaudioprocess=function(e){
    //獲取輸入和輸出的數據緩沖區
    var input = e.inputBuffer.getChannelData(0);
    var output = e.outputBuffer.getChannelData(0);
    //將輸入數緩沖復制到輸出緩沖上
    for(var i=0; i<input.length; i++)
        output[i]=input[i];
    //將緩沖區的數據繪制到Canvas上
    g.clearRect(-0.5, -height/2 - 0.5, width, height);

    g.beginPath();
    for(i = 0; i < width; i++)
    g.lineTo(i, height / 2 * output[output.length * i / width|0]);
    g.stroke();
};

下面是整個 DEMO 的代碼,效果預覽:

代碼:

<canvas id="canvas" width="400" height="100"></canvas>
<audio id="audio" autoplay src="http://qianduannotes.duapp.com/file/tankWar.mp3"></audio>
<br/>
<input type="button" onclick="audio.play()" value="播放" />
<input type="button" onclick="audio.pause()" value="暫停" />
<script>
var AudioContext=AudioContext||webkitAudioContext;
var context=new AudioContext;
//從元素創建媒體節點
var media=context.createMediaElementSource(audio);
//創建腳本處理節點
var processor=context.createScriptProcessor(4096,1,1);
//Canvas初始化
var width=canvas.width,height=canvas.height;
var g=canvas.getContext("2d");
g.translate(0.5,height/2+0.5);
//連接:媒體節點→控制節點→輸出源
media.connect(processor);
processor.connect(context.destination);
//控制節點的過程處理
processor.onaudioprocess=function(e){
  //獲取輸入和輸出的數據緩沖區
  var input=e.inputBuffer.getChannelData(0);
  var output=e.outputBuffer.getChannelData(0);
  //將輸入數緩沖復制到輸出緩沖上
  for(var i=0;i<input.length;i++)output[i]=input[i];
  //將緩沖區的數據繪制到Canvas上
  g.clearRect(-0.5,-height/2-0.5,width,height);
  g.beginPath();
  for(var i=0;i<width;i++)
    g.lineTo(i,height/2*output[output.length*i/width|0]);
  g.stroke();
};
</script>
DEMO

該段代碼引自次碳酸鈷的博客

五、小結

本文着重講述了 AudioContext 內部節點之間的交互原理,以及如何使用節點獲取數據,關於圖形的繪制是 canvas 的操作,不是本系列文章的重點,所以一筆帶過。

如果文中有敘述不正確的地方,還請斧正!

六、參考資料

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM