跳轉到

2.1 音量與能量計算#

概念說明#

時域分析最基礎的問題是:「這段聲音有多大聲?」數位訊號 \(x[n]\) 的大小可以用幾種統計量描述,各有不同的物理意義與用途。

訊號能量(energy)定義為訊號取值平方的總和:

\[ E = \sum_{n=0}^{N-1} x[n]^2 \]

對持續存在的訊號(如穩定的音樂),能量會隨長度線性成長,因此改用單位時間的平均,即功率(power):

\[ P = \frac{1}{N}\sum_{n=0}^{N-1} x[n]^2 \]

均方根(RMS, root mean square)是功率的平方根:

\[ x_{rms} = \sqrt{\frac{1}{N}\sum_{n=0}^{N-1} x[n]^2} \]

RMS 與人耳感知到的「音量」高度相關,比「最大振幅(peak)」更能反映主觀響度。例如一個峰值很高但只出現一瞬間的訊號,peak 很大、RMS 卻很小,聽起來仍然不大聲。音響工程中的「峰值對均值比(crest factor)」 \(= x_{peak}/x_{rms}\) ,正是在描述這個差異。

對正弦波 \(A\sin(2\pi f t)\) 而言,理論 RMS 值為 \(A/\sqrt{2} \approx 0.707A\) 。這是檢驗實作是否正確的好基準。

若要把 RMS 轉成分貝(dB),習慣以滿格振幅 1.0 為參考(dBFS):

\[ dBFS = 20\log_{10}(x_{rms}) \]

純音滿格正弦波約為 \(-3\) dBFS;接近 \(0\) dBFS 就幾乎要削波(clipping)失真了。

實務上音量會隨時間變化(例如語音有音節的強弱),因此我們把長訊號切成短窗框(frame),逐框計算 RMS,得到「音量包絡(envelope)」。這是語音活動偵測、音訊分段、自動混音的第一步。

演算法步驟:逐框 RMS 包絡#

  1. 選定框長 \(L\) (例如 20 ms)與移步(hop) \(H\) (例如 10 ms,即 50% 重疊)。
  2. 取第 \(i\) 框的樣本 \(x[iH : iH+L]\) 。
  3. 計算該框 RMS: \(r_i = \sqrt{\frac{1}{L}\sum x[n]^2}\) 。
  4. 將 \(r_i\) 轉為 dBFS: \(20\log_{10}(r_i + \epsilon)\) (加 \(\epsilon\) 避免取 \(\log\) 出現 \(-\infty\) )。
  5. 輸出逐框的 dB 序列,即音量包絡,可再以門檻值做分段或事件偵測。

Python 範例#

import numpy as np

fs = 8000
t = np.arange(0, 1.0, 1/fs)
# 一段由小聲漸大、中間靜音的訊號
amp = np.interp(t, [0, 0.3, 0.45, 0.6, 1.0], [0.02, 0.5, 0.5, 0.0, 0.0])
x = amp * np.sin(2*np.pi*440*t)

frame, hop = int(0.020*fs), int(0.010*fs)   # 20ms 框、10ms 移步
n_frames = 1 + (len(x) - frame) // hop
env = np.array([
    20*np.log10(np.sqrt(np.mean(x[i*hop:i*hop+frame]**2)) + 1e-12)
    for i in range(n_frames)
])

# 文字視覺化:每個 * 代表約 3 dB
for i in range(0, n_frames, 5):
    db = env[i]
    bar = "#" * max(0, int((db + 60) / 3))
    print(f"t={i*hop/fs:5.2f}s  {db:7.1f} dBFS  {bar}")

輸出(節錄):

t= 0.00s    -52.0 dBFS  #
t= 0.05s    -37.3 dBFS  ########
t= 0.10s    -27.3 dBFS  ##############
t= 0.20s    -14.9 dBFS  ##############################
t= 0.30s     -3.1 dBFS  #############################################
t= 0.40s     -3.1 dBFS  #############################################
t= 0.55s    -47.2 dBFS  ####
t= 0.60s   -240.0 dBFS 
t= 0.90s   -240.0 dBFS 

包絡清楚地呈現「漸強 → 穩定 → 靜音」的結構。若在包絡上設定門檻(例如 \(-40\) dBFS),即可偵測出語音起點與終點,這正是第 7 章 VAD 的雛形。

複雜度分析#

對長度 \(N\) 的整段訊號計算單一 RMS 需要 \(O(N)\) 時間、 \(O(1)\) 額外空間。逐框包絡共約 \(N/H\) 框、每框 \(L\) 次乘加,總時間 \(O(N \cdot L / H)\) ;若 \(L=2H\) (50% 重疊),即 \(O(2N) = O(N)\) 。逐框版本需儲存 \(O(N/H)\) 個包絡值。若用滑動總和(sliding window sum of squares,以累進方式加減移入移出的 \(x[n]^2\) )可將重疊計算降到嚴格 \(O(N)\) 。

練習#

  1. 產生振幅 \(A=0.5\) 的 1 kHz 正弦波,驗證其 RMS 是否接近 \(0.5/\sqrt{2}\) ,並算出對應的 dBFS。
  2. 修改範例,把框長改成 5 ms 與 100 ms 各算一次包絡,觀察時間解析度與平滑度之間的取捨,並說明哪一種較適合偵測鼓聲的瞬態。
  3. 寫一個函式 normalize(x, target_dbfs),將訊號整體 RMS 縮放到目標音量(例如 \(-16\) dBFS,串流平台常用的響度),並用 np.clip 處理縮放後可能超過 \(\pm 1\) 的削波問題。