Mở đầu: một tone sạch, một phổ "bẩn"

Cho một sine đơn tần thật sạch vào dft()/fft() đã verify ở 2 bài trước — kỳ vọng thấy đúng MỘT vạch nhọn tại đúng tần số đó. Nhưng nếu tần số sine không rơi đúng vào một bin (ví dụ hoàn thành đúng 10,5 chu kỳ trong cả khung, thay vì một số nguyên chu kỳ), phổ không còn sạch nữa — năng lượng "loang" ra khắp mọi bin xung quanh, có khi che mất hẳn một tín hiệu yếu hơn ở gần đó.

Bài này truy nguyên hiện tượng đó tận gốc — gọi là rò rỉ phổ (spectral leakage) — bằng đúng một hình ảnh toán học, rồi giới thiệu bộ công cụ khắc phục: các hàm cửa sổ (rect/Hann/Hamming/Blackman), mỗi loại đánh đổi độ phân giải lấy dải động theo cách khác nhau.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 5 (DFT, bin tần số) và Bài 6 (FFT — dùng để tính phổ nhanh trong demo).

1. Hiện tượng lạ: một sine, cả phổ "sáng"

Truy nguyên gốc rễ: dft() ngầm giả định khung tín hiệu đang phân tích LẶP LẠI vô hạn — nối đuôi khung này với chính nó tạo thành một tín hiệu tuần hoàn tưởng tượng. Nếu sine hoàn thành đúng MỘT SỐ NGUYÊN chu kỳ trong khung, mép nối liền mạch — không có vấn đề gì. Nhưng nếu không (ví dụ 10,5 chu kỳ), mép nối bị GÃY — tạo ra một bước nhảy giả tạo không hề có trong tín hiệu gốc. Bước nhảy giả đó chính là nguồn năng lượng "rò rỉ" lan ra khắp phổ.

2. Nhìn qua lăng kính toán: cửa sổ = chập với sinc

Cắt một khung hữu hạn từ tín hiệu vô hạn, về bản chất toán học, là NHÂN tín hiệu đó với một cửa sổ chữ nhật (1 bên trong khung, 0 bên ngoài). Theo định lý nhân-chập của biến đổi Fourier, nhân trong miền thời gian tương đương với chập trong miền tần số — phổ của tín hiệu gốc bị chập với phổ của chính cửa sổ chữ nhật, vốn có dạng hàm sinc ($\text{sinc}(x) = \sin(\pi x)/(\pi x)$): một thuỳ chính (main lobe) hẹp ở giữa, và vô số thuỳ phụ (side lobes) nhỏ dần lan ra hai bên. Đây là lời giải thích TRỌN VẸN cho hiện tượng leakage — không có gì bí ẩn, chỉ là hệ quả của việc cắt khung hữu hạn KHÔNG THỂ tránh khỏi khi xử lý số.

3. Bộ sưu tập cửa sổ: đánh đổi thuỳ chính ↔ thuỳ phụ

Nếu cửa sổ chữ nhật là "cắt cứng" (0/1 đột ngột ở biên), các cửa sổ khác làm biên độ GIẢM DẦN MƯỢT về 0 ở hai đầu khung — đổi lấy thuỳ chính rộng hơn (phân giải tần số kém hơn) nhưng thuỳ phụ thấp hơn NHIỀU (ít rò rỉ hơn hẳn). Số đo thật từ self-test (node dsp-core.js, $N=1024$):

Cửa sổ Mức thuỳ phụ (side lobe) Coherent gain Khi nào dùng
Rect (chữ nhật) ≈ −13,5dB (rất cao — rò rỉ nhiều) 1,00 Chỉ khi tần số RƠI ĐÚNG bin (hiếm khi thực tế)
Hann ≈ −32,2dB 0,50 Đa dụng hằng ngày — cân bằng tốt độ phân giải/dải động
Hamming ≈ −42,7dB 0,54 Tách 2 tone GẦN nhau (thuỳ chính hẹp hơn Hann)
Blackman ≈ −58,3dB (thấp NHẤT) 0,42 Tìm tone YẾU cạnh tone MẠNH (side lobe thấp nhất)

Đúng thứ tự kinh điển verified bằng self-test: rect > Hann > Hamming > Blackman (số càng âm, càng ít rò rỉ). Không có cửa sổ nào "tốt nhất tuyệt đối" — Hamming thắng khi cần TÁCH 2 tần số gần nhau, Blackman thắng khi cần THẤY một tín hiệu yếu bên cạnh một tín hiệu mạnh, Hann là lựa chọn cân bằng mặc định khi không chắc cần gì.

Số đo thật minh hoạ trực tiếp mức độ khác biệt: với một sine hoàn thành 10,5 chu kỳ trong khung 256 mẫu, đo năng lượng "rò rỉ" tại một bin XA (bin 40, cách xa vị trí tín hiệu thật ở bin ~10-11) — cửa sổ rect cho ra biên độ rò rỉ 0,62, trong khi Hann chỉ còn 0,0006 — chênh nhau hơn 1000 lần.

4. Pitfall đo lường: cửa sổ "ăn" mất năng lượng

Mọi cửa sổ (trừ rect) đều làm GIẢM biên độ trung bình của tín hiệu — vì phần lớn cửa sổ có giá trị nhỏ hơn 1 (đặc biệt gần 2 đầu khung). Đại lượng đo mức giảm này gọi là coherent gain — chính là giá trị TRUNG BÌNH của cửa sổ (bảng Mục 3: Hann≈0,50, Hamming≈0,54, Blackman≈0,42).

coherent_gain_compensate.js (trích engine dsp-core.js)
function coherentGain(window) {
  return window.reduce((sum, w) => sum + w, 0) / window.length;
}

// Bu lai bien do da bi cua so lam "tut" - QUEN buoc nay la sai he thong
function compensatedMagnitude(magnitude, window) {
  const cg = coherentGain(window);
  return magnitude.map((m) => m / cg);
}
⚠️ Cạm bẫy: quên bù coherent gain là sai biên độ có hệ thống
Nếu đo biên độ một tín hiệu qua cửa sổ Hann mà KHÔNG chia lại cho coherent gain (0,50), kết quả đọc được sẽ THẤP HƠN giá trị thật đúng một hệ số 2 lần — MỌI phép đo biên độ sau đó (đo mức âm lượng, hiệu chỉnh EQ, so sánh trước/sau xử lý) đều sai một hằng số CỐ ĐỊNH, không ngẫu nhiên, rất dễ bị bỏ qua vì "trông vẫn hợp lý" — chỉ phát hiện được khi đối chiếu với một phép đo tham chiếu độc lập (vd đo bằng rect window, coherent gain=1, không cần bù gì).
🔬 Đào sâu: vì sao cửa sổ giảm dần mượt lại có main lobe rộng hơn?
Đây là hệ quả của chính định lý nhân-chập (Mục 2): một hàm càng "mượt/rộng" trong miền thời gian (không có góc gãy đột ngột như rect) thì biến đổi Fourier của nó càng "hẹp và gọn" về NĂNG LƯỢNG SIDE LOBE nhưng lại RỘNG hơn ở MAIN LOBE — đây là một dạng của nguyên lý bất định thời gian-tần số sẽ gặp lại ở Bài 8 sắp tới (STFT). Không có cách nào "vừa hẹp main lobe, vừa thấp side lobe" cùng lúc — mọi cửa sổ chỉ là một điểm cụ thể trên đường đánh đổi đó.

5. Thực hành: hai tone chênh 40dB — hiện rồi lặn

Demo dưới đây dựng 2 tone: một tone MẠNH tại 1004Hz (cố tình KHÔNG rơi đúng bin để có leakage thật) và một tone YẾU HƠN 40dB (biên độ chỉ bằng 1%) tại 1031,25Hz (cách main tone vài bin). Đổi cửa sổ để xem tone yếu bị leakage của tone mạnh CHE MẤT hay HIỆN RA rõ:

🪟 So sánh cửa sổ — DSPJS thật
Đang khởi tạo…

Trục ngang: bin tần số quanh 2 tone. Cột xanh = tone mạnh (1004Hz), cột cam = tone yếu -40dB (1031,25Hz). Với Rect, "sàn rò rỉ" của tone mạnh cao hơn hẳn tone yếu — tone yếu gần như biến mất. Đổi sang Hann/Hamming/Blackman xem sàn rò rỉ hạ xuống, tone yếu dần lộ rõ.

window_demo.js (đúng logic đang chạy ở tab Xem trước)
import { rectWindow, hannWindow, hammingWindow, blackmanWindow, applyWindow, fft, dftMagnitude } from './dsp-core.js';

const fs = 8000, N = 1024;
const strongFreq = 1004, weakFreq = 1031.25, weakAmp = Math.pow(10, -40 / 20); // -40dB

const signal = Array.from({ length: N }, (_, n) =>
  Math.sin((2 * Math.PI * strongFreq * n) / fs) + weakAmp * Math.sin((2 * Math.PI * weakFreq * n) / fs)
);

const windows = { rect: rectWindow(N), hann: hannWindow(N), hamming: hammingWindow(N), blackman: blackmanWindow(N) };
const spectrum = dftMagnitude(fft(applyWindow(signal, windows[selectedWindow])));
// bin ~128 = tone manh, bin ~132 = tone yeu - xem spectrum[132] doi the nao theo tung cua so

Tóm lược

  • Leakage: DFT ngầm coi khung là tuần hoàn — nếu tần số không hoàn thành số nguyên chu kỳ, mép nối "gãy" tạo năng lượng giả lan khắp phổ.
  • ✅ Toán học trọn vẹn: cắt khung = nhân với cửa sổ chữ nhật = chập phổ với sinc (main lobe + side lobes).
  • ✅ Verified ($N=1024$): side lobe rect≈−13,5dB, Hann≈−32,2dB, Hamming≈−42,7dB, Blackman≈−58,3dB — đúng thứ tự kinh điển.
  • ✅ Verified: sine không-nguyên-chu-kỳ rò rỉ tới 1 bin xa — biên độ 0,62 (rect) so 0,0006 (Hann), chênh >1000 lần.
  • ✅ Coherent gain (rect=1,0 / Hann≈0,50 / Hamming≈0,54 / Blackman≈0,42): cửa sổ "ăn" năng lượng — quên bù là sai biên độ có hệ thống.
  • ✅ Không cửa sổ nào thắng tuyệt đối: Hamming tách 2 tone gần, Blackman tìm tone yếu cạnh tone mạnh, Hann là lựa chọn cân bằng mặc định.

Trắc nghiệm ôn tập

Câu 1

Nguyên nhân gốc rễ của hiện tượng rò rỉ phổ (spectral leakage) là gì?

Câu 2

Về mặt toán học, việc cắt một khung tín hiệu hữu hạn tương đương với phép toán nào?

Câu 3

Verified: Blackman có side lobe thấp nhất (≈−58dB) nhưng main lobe RỘNG nhất trong 4 loại cửa sổ. Khi nào nên chọn Blackman?

Câu 4

Quên bù coherent gain khi đo biên độ qua cửa sổ Hann (coherent gain≈0,50) gây ra hậu quả gì?

Tải file code thực hành minh họa bài học

File JavaScript DSPJS (dsp-core.js) — thư viện DSP tự viết dùng xuyên suốt cả 15 bài, Bài 7 vừa thêm 4 hàm cửa sổ (rect/Hann/Hamming/Blackman) + coherent gain + đo side lobe, kèm self-test đối chiếu đúng mọi hành vi trong bài (chạy node dsp-core.js, không cần cài thêm gì):

Tải về dsp-core.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 6: FFT: thuật toán thay đổi thế giới Bài 8: STFT & Spectrogram Quay lại Lộ trình Series Xử Lý Tín Hiệu Số

Bình luận