為什麼數位音訊只要記錄振幅就可以了?

最近想要持續改進 ownCloud Music app ,現在在 Chrome 上面跑的很順,但是我常用的 Firefox 播 FLAC 音樂還是經常會有 glitch (我的 Firefox 本來就已經很頓了,雖然我有 150+ 個分頁,但是我有裝自動 unload 的 plugin 啊,任一時間同時開啟的分頁應該不會超過 10 個,為什麼會這麼慢我也搞不懂),為了要改善效能,我先研究了一下 web worker ,看看有沒有可能把 codec 跑在 web worker 裡面,這樣就不會受到 main thread 的影響了,看看 web worker ,發現有 AudioWorker 這東西,就又再挖進去探究一下(筆記在這)。看 AudioWorker 發現自己實在不懂數位音效處理,就又開始找資源研讀。

關於數位音訊處理,我先前有看過 Xiph.org 講得非常好的技術理論的影片。這次找到 Indiana University 清楚簡明的介紹

我之前一直不太懂數位格式音訊(振幅)是怎麼轉換成實際的聲音的,我不懂為什麼數位音訊裡面不包含頻率資料(譬如說某個頻率的聲音在某個時間小段的振幅),卻還是可以還原本來的聲音?看了這段之後我懂了:

Frequencies will be recreated later by playing back the sequential sample amplitudes at a specified rate. It is important to remember that frequency, phase, waveshape, etc. are not recorded in each discrete sample measurement, but will be reconstructed during the playback of the stored sequential amplitudes.

然後加上這張圖:

d-to-a

Each sample is “clocked" into the DAC’s register. A ‘1’ in a register place will add a voltage to the sum of that sample proportionate to its binary value. In this hypothetical case, we have a sample whose binary value is 5. The gates or switches for the binary places of ‘4’ and ‘1’ are closed, and the value of 5 mvolts is sent out the DAC and held until the next sample is clocked into the register.

所以說,為什麼只要收集一個時間小段裡面的振幅資訊就可以還原本來的音效呢?

對於 DAC (Digital to Analog Converter) 來說,它做的事情就是在每 1/44100 秒內(數字依照取樣率決定), 輸出音訊檔指定的電壓大小,並在那一小段時間維持電壓大小。每個時間小段的振幅資訊在音訊檔裡面叫做一個 frame ,但一個 frame 可能有很多值,一個聲道 (channel) 一個值,譬如說雙聲道有 2 個值,5.1 環繞音效就有 6 個值。

每一個電壓值都是那個時間之內喇叭振動體的位置,電壓值不變,振動體位置就不變,但聲音是空氣的震動,振動體位置不變,空氣就不會被震動,也就沒有聲音。所以舉例來說,如果要產生一個特定頻率、音量的 sine 聲波,做法不是讓 DAC 在時間內輸出某個固定的電壓值,而是讓 DAC 輸出一個不斷變大,到了極大又逐漸變小的電壓值,從極大到極小的速度(譬如說每個極大到下一個極大值的差距是 2205 個 frame),決定了輸出的 sine 波的頻率。

更複雜一點來說,所有的聲音都是不同頻率、音量(最大振幅)、相位的 sine 波疊加出來的,所以,取樣所得的資訊,即是在那個取樣時間間隔內,附近聲音的所有波的振幅疊加。下一個取樣,每個波又會依照自己本來的規律而有不同的振幅。所有波,不同頻率、音量、相位的波,在那小段時間內的振幅資訊,都是疊加在一起被收集起來的,所以不需要「把每個頻率的波的資訊分開來收集」。

註:我記得國中理化好像有提到,振幅本來就有「最大」的意思,就是波的峰和谷的高度差,不過我忘了那個描述某個時間點介質的位置的名詞是什麼了,不過我想你知道我的意思。

不過你或許也注意到了,這邊有 2 個頻率:取樣頻率DAC 變換值的頻率,如果兩個頻率不一樣,還原出來的聲音的頻率就會有變化。

If samples are clocked into the DAC at the rate they were sampled, then the original frequencies will be reproduced up to the Nyquist frequency. If the samples are clocked in at twice the rate, then the frequency will be doubled.

如果取樣頻率和 DAC 變換頻率相同,原始錄音時小於 Nyquist frequency 的頻率就會被重現,如果 DAC 變換頻率是取樣頻率的兩倍,還原出來的頻率也會是原本錄音頻率的兩倍。

懂了這點之後,原本似懂非懂的 Nyquist frequency 也懂啦。

(我不是這方面專業,本文有任何錯誤還請指正)

Web Audio API

Web Audio 是在瀏覽器裡面合成、filter、輸出音效的 API 。

音訊處理可以視為一系列的函數,有輸入和輸出,這個概念在 Web Audio 裡面稱作 Node ,有:

  • 音訊來源的 node ,像是直接從程式產生 sine wave ,或是從 buffer 裡面讀取 PCM 資料的 AudioBufferSourceNode
  • 中間處理、形變的 node ,像是改變音量的 GainNode
  • 輸出的 node, 像是輸出至喇叭的 AudioDestinationNode

以上這些瀏覽器內建的常用音效功能實際上都是在獨立的 thread 執行的,但是若用自訂的 Javascript 操作音訊資料,就要使用 ScriptProcessorNode ,但是糟糕的是,它和 DOM 使用同一個 thread ,所以會影響畫面的 rendering ,反之亦然。

因此,若要用自訂的 Javascript 產生音效,像是 Aurora.js 一系列的各種音訊格式的解碼器,內建的簡單 sine wave generator 應該不夠,就必須使用效能差的 ScriptProcessorNode (但是我沒有找到 source code 裡面有那個?可能在解碼器裡面,但他們絕對不會是使用 web worker ,那樣效能會更差,因為 web worker 在不同 thread 之間傳遞資料的效率很差)。

AudioWorker comes to the rescue!

AudioWorker 是基於 web worker ,加強 web worker 資料傳遞功能,成為專門用來在瀏覽器裡面處理音訊的 worker 。

現在 (2015/1/9) AudioWorker 的 spec 還沒完全確定,但似乎已經在最後修改文字的階段了。