Web Audio API 第5章 音頻的分析與可視化

到目前爲止,我們僅討論了音頻的合成與處理,但這僅是 Web Audio API 提供的一半功能。另一半功能則是音頻的分析,它播放起來應該是什麼樣子的。它最典型的例子就是音頻可視化,但其實有更多的其它應用場景,包括聲調檢測,節減檢測,語音識別等,這些已大大超出本書範圍。

對於遊戲或交互式應用開發者來說,這是一個重要的主題,原因有幾點。首先,一個好的可視化分析器可以用於類似調式工具(顯然這是除了你耳朵之外,良好的計量工具)用於調音。其次,對於某些關於音樂相關的遊戲或應用來說可視化是重點比如遊戲“吉它英雄”或者應用軟件 GarageBand (蘋果電腦上吉它教學軟件)

頻率分析

在 Web Audio API 分析聲音是最主要的方式利用 AnalyserNodes。這些節點不會對聲音本身做任何改變,可以在音頻上下文任意處調用。一旦在音頻圖中創建了這樣的節點,它就會提供兩種主要方式用於查看聲音波形:時域和頻域上

得到的結果是基於特定緩衝區大小的 FFT 分析。我們有一些定製化節點輸出的屬性可用:

  • fftSize
    定義緩衝區大小用於實現分析。大小一定是2的冪。較高的值將導致對信號進行更細粒度的分析,但代價是一些性能損失。

  • frequencyBinCount
    這個屬性是隻讀的,自動爲 fftSize / 2。

  • smoothingTimeConstant
    值範圍是 0 - 1. 值爲1會導致較大的移動平均平滑結果。值爲零意味着沒有移動平均線,結果波動很快。

最基本的設置就是把分析節點插到我們感興趣的音頻圖譜中:

// 假設節點A與B普普通通相連
var analyser = context.createAnalyser(); 
A.connect(analyser); 
analyser.connect(B);

然後我們就可以得到時域或頻域的數組了:

var freqDomain = new Float32Array(analyser.frequencyBinCount); 
analyser.getFloatFrequencyData(freqDomain);

在上面的代碼例子中,freqDomain 是一個頻域 32 位浮點數組。這些數組內存儲的值都被標準化爲 0-1。輸出的指標可以在0和奈奎斯特頻率之間線性映射,奈奎斯特頻率被定義爲採樣率的一半(在 Web Audio API 中通過 context.sampleRate 獲取)。下面的代碼片段將 frequency 映射到頻率數組中的正確位置:

奈奎斯特頻率是離散信號系統採樣頻率的一半也就是 1/2 fs,fs爲採樣頻率

function getFrequencyValue(frequency) {
  var nyquist = context.sampleRate/2;
  var index = Math.round(frequency/nyquist * freqDomain.length); 
  return freqDomain[index];
}

如果我們分析的是一個 1000 Hz 的正弦波,舉例,我們期望調用 getFrequencyValue(1000) 時返回圖像內的峯值,如圖 5-1。

頻域通過調用 getByteFrequencyData 使用8位無符號存儲也可以。 這些值就是無符號整型,在分析節點( analyzer node)它會縮放以適配在最大分貝和最小分貝之間(即在 dBFS中,decibels full scale)。因此可以根據需要調整這些參數以縮放輸出。

image

圖 5-1,一個 1000Hz 的可視化聲音(全頻域是從 0 至 22050Hz)

requestAnimationFrame 實現動畫

如果我們想要對我們的聲音進行可視化,我們需要週期性的查詢分析節點(analyzer node), 處理返回的結果,並渲染出來。我們可以利用 JavaScript 的定時器實現,setInterval, setTimeout, 但現在有更好用的:requestAnimationFrame。該 API 允許瀏覽器將你的自定義繪製函數合併到瀏覽器本地渲染循環中,這對性能來講會有很大提升。不同於指定固定繪製間隔需要等待瀏覽器空閒時纔來處理你的定時器不同,你只需要將它提交到隊列中,瀏覽器會以最快的速度執行它。

由於 requestAnimationFrame 還是處於實驗性質,你需要爲其指定瀏覽器前綴,且給它定一個相似功能的 setTimeout 來兜底。代碼如下:

window.requestAnimationFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
function(callback){
      window.setTimeout(callback, 1000 / 60);
    };
})();

一但定義好了 requestAnimationFrame 函數,我們需要利用它來查詢分析節點得到音頻流的詳細信息。

requestAnimationFrame 現在早就加入肯德基豪華午餐了直接用就可以了

聲音可視化

把它們全組合在一起,設置一個渲染循環用於查詢和渲染之前用到的分析節點,將存進 freqDomain 數組:

var freqDomain = new Uint8Array(analyser.frequencyBinCount); 
analyser.getByteFrequencyData(freqDomain);
for (var i = 0; i < analyser.frequencyBinCount; i++) {
  var value = freqDomain[i];
  var percent = value / 256;
  var height = HEIGHT * percent;
  var offset = HEIGHT - height - 1;
  var barWidth = WIDTH/analyser.frequencyBinCount;
  var hue = i/analyser.frequencyBinCount * 360; 
  drawContext.fillStyle = 'hsl(' + hue + ', 100%, 50%)'; 
  drawContext.fillRect(i * barWidth, offset, barWidth, height);
}

對時域也可以進行類似的操作

var timeDomain = new Uint8Array(analyser.frequencyBinCount); 
analyser.getByteTimeDomainData(timeDomain);
for (var i = 0; i < analyser.frequencyBinCount; i++) {
  var value = timeDomain[i];
  var percent = value / 256;
  var height = HEIGHT * percent;
  var offset = HEIGHT - height - 1;
  var barWidth = WIDTH/analyser.frequencyBinCount; 
  drawContext.fillStyle = 'black'; 
  drawContext.fillRect(i * barWidth, offset, 1, 1);
}

此代碼將時域內的值利用 HTML5 canvas 繪製,創建一個簡單的可視化圖形,在代表頻域數據的彩色色條狀圖的頂部繪製了一個波形線條。

結果在 canvas 上繪製出來應該如圖 5-2

image

圖 5-2 某一時刻的可視化截圖

以上分析器節點的代碼實現 demo 可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch05/demo.html

我們處理可視化方案遺漏了很多數據。但對音樂的可視化來說足夠了。當然如果我們想綜合全面分析整個音頻緩衝區,我們需要看看其它的方法。

額外部分--顯示整個聲音文件的音量高低圖

這一部分並非 Web Audio API 書說中述,是譯者本人所述

這是我在項目中遇到的一個問題

網上一堆例子都是顯示實時音頻信號的,就像上一節中的那樣

可是如果我想要的是顯示整段 mp3 文件的音量高低圖呢?

即如何分析整斷音頻的數據?

原理是加載 mp3 文件後解碼分析音頻數據,獲取某一段音頻數據的採樣最高和最低點並繪製出來

首先就是從獲取音頻文件開始 利用 html 的 <input type="file" /> 標籤獲取 file 後:

const reader = new FileReader();
reader.onload = function (e) {
  const audioContext = new (window.AudioContext ||
    window.webkitAudioContext)();
  audioContext.decodeAudioData(e.target.result, function (buffer) {
    // 獲取音頻緩衝區數據
    const channelData = buffer.getChannelData(0);
    // 繪製波形
    drawWaveform(channelData);
  });
};
reader.readAsArrayBuffer(file);

利用 FileReader 以 ArrayBuffer 形式讀取文件內容

再使用 audioContext.decodeAudioData 解碼

解碼後獲取 channelData, 此時的 channelData 就包含該通道的音頻樣本數據,可以理解爲標準化後的 PCM 數據

如果忘記了什麼是 PCM 可以回顧第一章的內容

如圖 5-5 只要解析這個 channelData 內的 PCM 數據並繪製出來就行了 drawWaveform(channelData)

function drawWaveform(data) {
  const canvas = document.getElementById("waveform"); // 獲取canvas元素
  const ctx = canvas.getContext("2d"); // 獲取2D繪圖上下文
  const width = canvas.width; // canvas的寬度
  const height = canvas.height; // canvas的高度
  const step = Math.ceil(data.length / width); // 計算每個畫布像素對應的音頻樣本數
  const amp = height / 2; // 放大因子,用於控制波形在畫布上的高度

  ctx.fillStyle = "#fff"; // 設置填充顏色爲白色
  ctx.fillRect(0, 0, width, height); // 填充整個畫布爲白色

  ctx.beginPath(); // 開始繪製新的路徑
  ctx.moveTo(0, amp); // 將繪圖遊標移動到畫布中央的起始點

  // 繪製波形
  for (let i = 0; i < width; i += 4) {
    // 遍歷畫布的每一個像素
    let min = 1.0; // 初始化最小值
    let max = -1.0; // 初始化最大值
    for (let j = 0; j < step; j++) {
      // 遍歷與當前像素對應的音頻樣本

      const datum = data[i * step + j]; // 獲取單個音頻樣本
      if (datum < min) min = datum; // 更新最小值
      if (datum > max) max = datum; // 更新最大值
    }

    ctx.lineTo(i, (1 + min) * amp); // 繪製從當前位置到最小值的線
    
    ctx.lineTo(i, (1 + max) * amp); // 繪製從當前位置到最大值的線
    
  }
  ctx.stroke(); // 根據路徑繪製線條
  
}

image

圖 5-5 加載 test1.mp3 後顯示的圖

可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch05/volume-visualization1.html

步驟:

  1. 根據 canvas 的 width 確定採樣數據範圍()寬度
    const step = Math.ceil(data.length / width);

  2. 在 step 採樣數據範圍循環找出最高與最低音量

for (let j = 0; j < step; j++) {
    // 遍歷與當前像素對應的音頻樣本

    const datum = data[i * step + j]; // 獲取單個音頻樣本
    if (datum < min) min = datum; // 更新最小值
    if (datum > max) max = datum; // 更新最大值
  }
  1. 有了音量高低的值,直接繪製線條或柱型就可以了
ctx.lineTo(i, (1 + min) * amp); // 繪製從當前位置到最小值的線
ctx.lineTo(i, (1 + max) * amp); // 繪製從當前位置到最大值的線

把線條獨立開後加點色彩或許更好看

function drawWaveform(data) {
  const canvas = document.getElementById("waveform"); // 獲取canvas元素
  const ctx = canvas.getContext("2d"); // 獲取2D繪圖上下文
  const width = canvas.width; // canvas的寬度
  const height = canvas.height; // canvas的高度
  const step = Math.ceil(data.length / width); // 計算每個畫布像素對應的音頻樣本數
  const amp = height / 2; // 放大因子,用於控制波形在畫布上的高度

  ctx.fillStyle = "#fff"; // 設置填充顏色爲白色
  ctx.fillRect(0, 0, width, height); // 填充整個畫布爲白色

  ctx.beginPath(); // 開始繪製新的路徑
  ctx.moveTo(0, amp); // 將繪圖遊標移動到畫布中央的起始點

  // 繪製波形
  for (let i = 0; i < width; i += 4) {
    // 根據 i 遍歷畫布的寬度
    let min = 1.0; // 初始化最小值
    let max = -1.0; // 初始化最大值
    ctx.moveTo(i, amp);
    for (let j = 0; j < step; j++) {
      // 遍歷與當前像素對應的音頻樣本

      const datum = data[i * step + j]; // 獲取單個音頻樣本
      if (datum < min) min = datum; // 更新最小值
      if (datum > max) max = datum; // 更新最大值
    }
    var hue = (i / width) * 360;

    ctx.beginPath()
    ctx.strokeStyle = "hsl(" + hue + ", 100%, 50%)";
    ctx.moveTo(i, amp);
    ctx.lineTo(i, (1 + min) * amp); // 繪製從當前位置到最小值的線
    ctx.stroke(); // 根據路徑繪製線條

    ctx.beginPath()
    ctx.moveTo(i, amp);
    ctx.lineTo(i, (1 + max) * amp); // 繪製從當前位置到最大值的線
    ctx.stroke(); // 根據路徑繪製線條
  }
}

image

圖 5-3

可參考 https://github.com/willian12345/WebAudioAPI/tree/master/examples/ch05/volume-visualization2.html

還可以把 i 的間隔縮小,再把 step 的大小也縮小試試,我得到了下面 圖 5-4 效果

image

圖 5-4

在網上見過有人用扇形或者螺旋形來可視化音頻。效果也是相當的酷

請自行搜索,只要得到數據了實現起來還是比較簡單的


注:轉載請註明出處博客園:王二狗Sheldon池中物 ([email protected])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章