3.3 頻譜圖與時頻分析#
概念說明#
單一 FFT 只告訴我們「這一段訊號裡有哪些頻率」,卻丟掉了「何時發生」。音樂與語音的頻率內容隨時間劇烈變化——旋律在走、音節在換——我們需要同時有時間與頻率解析度的表示法。
短時傅立葉轉換(STFT, short-time Fourier transform)正是答案:把訊號切成一個個短窗框,逐框做 FFT,把結果排成一個「時間 × 頻率」的二維矩陣:
其中 \(m\) 是框索引、 \(H\) 是移步(hop)、 \(w\) 是窗函式。取 \(|X[m,k]|\) 畫成圖,以顏色(或灰階)表示能量,就是頻譜圖(spectrogram)——聲音的「指紋照片」,語音學、音樂分析、鳥類研究都靠它。
STFT 面臨根本的時頻取捨(time–frequency trade-off),由測不準原理限制:
- 窗框越長 → 頻率解析度越好( \(\Delta f = f_s/N\) 小),但時間解析度差,快速變化被抹平;
- 窗框越短 → 時間解析度好,能捕捉瞬態(鼓點、子音),但頻率糊成一片。
實務經驗:語音常用 20–40 ms 窗(時間結構細),音樂和聲分析可用 40–90 ms(要分辨低音音高)。移步 \(H\) 通常取窗長的 \(1/4\) 到 \(1/2\) (75%–50% 重疊),確保相鄰框之間的變化不被漏接;窗函式幾乎總是 Hann 或 Hamming。
以 dB 刻度顯示( \(20\log_{10}|X|\) )是標準做法,因為人耳對強度的感知是對數的,且動態範圍高達 100 dB 以上。
演算法步驟:計算 STFT 頻譜圖#
- 選定窗長 \(N\) (2 的冪方便 FFT)、窗型、移步 \(H\) 。
- 對框 \(m = 0, 1, 2, \dots\) (含訊號兩端的補零): a. 取 \(x[mH : mH+N]\) ,乘上窗函式; b. 做 \(N\) 點 FFT,保留前 \(N/2+1\) 個頻點; c. 存下 \(|X[m, k]|\) (或其 dB 值)。
- 將所有框的結果堆疊成矩陣 \(S[m, k]\) (時間軸 × 頻率軸)。
- 以固定動態範圍(如峰值下 70 dB)量化後輸出影像或文字視覺化。
Python 範例#
import numpy as np
fs = 8000
t = np.arange(0, 1.5, 1/fs)
# 模擬「一隻小鳥」:0-0.5s 2 kHz 上滑,0.5-1.0s 3 kHz 下滑,其後靜音
f_inst = np.where(t < 0.5, 2000 + 2000*t/0.5, np.where(t < 1.0, 3000 - 2000*(t-0.5)/0.5, 0))
x = 0.5 * np.sin(2*np.pi * np.cumsum(f_inst)/fs)
def stft_db(x, fs, n_fft=512, hop=128):
w = np.hanning(n_fft)
n_frames = 1 + (len(x) - n_fft) // hop
S = np.empty((n_frames, n_fft//2 + 1))
for m in range(n_frames):
seg = x[m*hop : m*hop + n_fft] * w
S[m] = np.abs(np.fft.rfft(seg))
db = 20*np.log10(S / (n_fft/4) + 1e-9)
return db, n_fft, hop
db, n_fft, hop = stft_db(x, fs)
print(f"頻譜圖尺寸: {db.shape[0]} 框 × {db.shape[1]} 頻點,"
f"時間解析 {hop/fs*1000:.0f} ms,頻率解析 {fs/n_fft:.1f} Hz\n")
# 文字視覺化:頻率由高到低,* 表該頻帶能量最強處
t_frames = db.shape[0]
rows = 14
for r in range(rows):
f_hi = fs/2 * (1 - r/rows)
f_lo = fs/2 * (1 - (r+1)/rows)
band = db[:, int(f_lo/(fs/n_fft)) : max(int(f_lo/(fs/n_fft))+1, int(f_hi/(fs/n_fft)))]
line = ""
for m in range(0, t_frames, t_frames//50):
col = band[m]
line += "*" if col.max() > -40 else ("." if col.max() > -70 else " ")
print(f"{(f_hi+f_lo)/2:6.0f} Hz |{line}|")
print(" 時間 0s" + " " * 46 + "1.5s")
輸出:
頻譜圖尺寸: 92 框 × 257 頻點,時間解析 16 ms,頻率解析 15.6 Hz
3692 Hz | |
3164 Hz | *********** |
2636 Hz | ********** *********** |
2108 Hz | ******** ** |
1580 Hz | |
...
時間 0s 1.5s
文字圖清楚呈現:能量帶在 0–0.5 秒從 2 kHz 爬升到 4 kHz,0.5–1.0 秒再滑回 1 kHz——這就是鳥叫聲(或警笛)在頻譜圖上的典型「雪茄狀」軌跡。真實的鳥鳴、鯨歌、語音共振峰在頻譜圖上都呈現類似的可辨識圖案,這也是 8.3 節音訊指紋的視覺基礎。
複雜度分析#
設訊號長 \(N_s\) 、窗長 \(N\) 、移步 \(H\) ,共 \(M \approx N_s/H\) 框。每框 FFT 為 \(O(N \log N)\) ,總時間 \(O\!\left(\frac{N_s}{H} N \log N\right)\) ,空間 \(O(M \cdot N/2)\) 存頻譜矩陣。以 8 kHz、10 秒訊號、 \(N=512\) 、 \(H=128\) 為例: \(M \approx 625\) 、矩陣約 32 萬個浮點數(1.3 MB),總運算約千萬級——筆電毫秒到數十毫秒即可完成。若 \(H = N/4\) ,時間與 \(4 N_s \log N\) 成正比,僅為單次全長 FFT 的數倍,這正是 STFT 能即時運作的原因。時頻取捨則無法靠算力解決: \(N\) 的選擇是應用問題,不是效能問題。
練習#
- 對同一訊號分別以 \(N=128\) 與 \(N=2048\) ( \(H\) 不變)計算頻譜圖,觀察「時間清晰、頻率糊」與「頻率清晰、時間糊」的差異,並說明哪一種適合分析鼓刷聲、哪一種適合分析管風琴低音。
- 合成一段「先 440 Hz 純音、後 442 Hz 純音」的訊號,找出能分辨兩音的最短窗長,驗證與理論 \(\Delta f = f_s/N\) 的關係。
- 在頻譜圖上實作「峰值偵測」:對每框找出能量前 5 強的頻點,輸出 (時間, 頻率) 座標列表,並思考這份「星座圖」如何用於 8.3 節的音訊指紋比對。