1.1 聲音與數位訊號#
概念說明#
聲音的本質是壓力波。 當物體振動時(例如吉他的弦、揚聲器的振膜),它會推擠周圍的空氣分子,造成空氣壓力在平均值附近起伏。這個壓力擾動以約 343 m/s 的速度在空氣中傳播,抵達耳膜後,推動耳膜振動,最終由大腦解讀為「聽到的聲音」。所以從演算法的角度看,一個聲音訊號就是「壓力對時間的函數」:
電腦裡我們通常把這個函數減去平均值,記成一個訊號 \(x(t)\) ,即「相對於環境壓力的擾動量」,單位是帕斯卡(Pa)。
最簡單也最重要的模型是純音(pure tone),也就是正弦波:
IEEE 在 1980 年代採用標準名稱 pure tone 指單一頻率的振盪。它有三個參數,各自對應一種聽覺感知:
| 物理參數 | 數學量 | 聽覺感知 |
|---|---|---|
| 頻率 \(f\) (Hz) | 正弦波每秒振盪次數 | 音調高低(pitch) |
| 振幅 \(A\) (Pa 或 dB) | 波峰的高度 | 音量大小(loudness) |
| 相位 \(\phi\) | 起始角度 | (人耳對相位大多不敏感) |
真實的聲音幾乎從來不是單一正弦波。鋼琴彈一個 A4(440 Hz)時,實際上同時發出 440、880、1320、1760 Hz……等一整組泛音(overtones/harmonics),相對強度由樂器的材質與發聲方式決定。人耳把「基頻 f0」解讀為音調,把泛音的能量分布解讀為音色(timbre)——這就是為什麼小提琴與長號吹同一個音,聽起來完全不同。整本書的任務,基本上就是不斷地把這三個概念(頻率結構、能量、頻譜形狀)轉成可計算的演算法。
從聲音到數字:類比數位轉換#
電腦無法儲存連續函數,只能儲存離散的數字。把連續訊號 \(x(t)\) 變成數列 \(x[0], x[1], x[2], \ldots\) 的過程稱為類比數位轉換(ADC, Analog-to-Digital Conversion),包含三個步驟:
步驟 1:抗混疊低通濾波(anti-aliasing filter) 先以低通濾波器把輸入訊號頻率限制在取樣率的一半以下(見 1.2 節)。這一步在 ADC 晶片內部完成,使用者通常看不到,但若省略,高頻成分會折疊成假的低頻,且事後無法救回。
步驟 2:取樣(sampling) 每 \(T_s\) 秒量測一次訊號的瞬時值, \(T_s\) 的倒數就是取樣率(sampling rate) \(f_s\) :
常見取樣率:語音 8 kHz(電話)、16 kHz(語音辨識)、CD 音質 44.1 kHz、專業錄音 48/96 kHz。
步驟 3:量化與編碼(quantization & coding) 把量測到的實數捨入到有限的位數。例如 16-bit 錄音把振幅對應到 65536 個整數階層,8-bit 只有 256 個。量化引入的誤差就是量化雜訊(見 1.3 節)。
反方向 DAC(數位類比轉換)則以重建濾波器把階梯狀的數字序列平滑回連續波形。
數位訊號的三大座標系#
拿到一段數位音訊,我們可以用三種互相等價的表示法觀察它:
- 波形(時域):直接畫 \(x[n]\) 對 \(n\) 的圖,看包絡與週期性。
- 頻譜(頻域):用傅立葉轉換(第 3 章)看頻率成分。
- 時頻表示:STFT 頻譜圖(3.3 節),同時看時間與頻率。
一個好用的類比:錄音檔等於「樣本數 \(N\) × 每樣本位元數 \(B\) × 聲道數 \(C\) × 取樣率 \(f_s\) 」的資料矩陣。一切演算法的輸入輸出都是對這個矩陣做運算。
演算法步驟:合成與觀察一段數位音訊#
- 選定取樣率 \(f_s\) 與時長 \(D\) ,樣本數 \(N = f_s \times D\) 。
- 建立時間陣列 \(t[n] = n / f_s\) 。
- 以解析式生成訊號(純音、和絃、掃頻)。
- 計算基本統計量(峰值、RMS、週期),驗證與理論一致。
Python 範例#
下面的程式不需要任何聲音硬體,只用 numpy 就能「造出聲音」並做數值分析:
import numpy as np
fs = 8000 # 取樣率(Hz)
t = np.arange(0, 1, 1/fs) # 時間陣列:0 到 1 秒,共 8000 點
# (1) 純音:440 Hz 的 A4
f0 = 440.0
x = 0.8 * np.sin(2*np.pi*f0*t)
# (2) 三個音疊成簡單和絃(A 大三和弦:A4, C#5, E5)
chord = (0.4*np.sin(2*np.pi*440*t) +
0.4*np.sin(2*np.pi*554.37*t) +
0.4*np.sin(2*np.pi*659.26*t))
def stats(name, s):
peak = np.max(np.abs(s))
rms = np.sqrt(np.mean(s**2))
print(f"{name:8s} peak={peak:.4f} rms={rms:.4f} n={len(s)}")
stats("A4", x)
stats("chord", chord)
# (3) 驗證週期:找第一個零上坡穿越點,兩次穿越的間隔 = 一個週期
cross = np.where((x[:-1] <= 0) & (x[1:] > 0))[0][0]
period_samples = np.where((x[cross:] <= 0) & (x[cross+1:] > 0))[0][0]
print("估得週期 =", period_samples/fs*1000, "ms, 理論 =", 1000/f0, "ms")
# (4) 文字波形(取前 64 點,以字元高度表示振幅)
def draw(s, n=64, w=7):
for v in s[:n]:
k = int((v/w + 0.5) * 10)
k = max(0, min(9, k))
print("|" + "."*k + "*"*(9-k))
執行輸出(節錄):
A4 peak=0.8000 rms=0.5657 n=8000
chord peak=1.0585 rms=0.4750 n=8000
估得週期 = 2.272727272727273 ms, 理論 = 2.2727272727272727 ms
(週期與理論值一致到小數點後十位以上,說明合成是正確的。)
複雜度分析#
- 合成長度 \(N\) 的訊號: \(O(N)\) (每個樣本做一次三角函數)。
- 計算峰值/RMS 統計: \(O(N)\) 、記憶體 \(O(1)\) (串流一次走完)。
- 若用查表法避免即時計算 sin(即時音效引擎常用),每樣本僅 \(O(1)\) 。
練習#
- 修改程式,合成 523.25 Hz(C5)的純音,驗證程式估出的週期是否等於理論值 \(1/523.25\) 秒。
- 把和弦的相對振幅改成 1.0、0.2、0.2,重新執行 RMS 的計算。RMS 掉到約 half power 嗎?請以振幅平方的角度解釋。
- 試把 440 Hz 與 441 Hz 兩個音疊加,觀察文字波形會出現什麼現象(拍頻 beat,見第三章的正式解釋)。