Mở đầu: một hợp âm, biết trọn vẹn những nốt bên trong

Nghe một hợp âm piano — ba bốn nốt vang lên cùng lúc, hoà quyện thành MỘT dao động phức tạp duy nhất. Tai người tách được các nốt gần như tức thời, nhưng câu hỏi "trong TÍN HIỆU SỐ này (một dãy số thuần tuý) chính xác có những tần số nào, và mỗi tần số mạnh bao nhiêu?" cần trả lời bằng TOÁN, không phải bằng tai.

Bài này xây dựng công cụ trả lời câu hỏi đó — DFT (Discrete Fourier Transform, biến đổi Fourier rời rạc) — từ ý tưởng đơn giản nhất có thể: SO KHỚP. Tự viết DFT chỉ trong khoảng 15 dòng, verify trên tín hiệu biết trước tần số, rồi dùng chính nó xây một DFT Explorer: vẽ tín hiệu tự do bằng chuột, thấy phổ tần số hiện ra tức thì.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 1 (sine rời rạc, tần số lấy mẫu $f_s$), Bài 2 ($f_s$ và Nyquist). Nên học trước: Bài 4 (tích chập — DFT về bản chất cùng họ với phép "so khớp" — correlation — mà Bài 4 đã xây trực giác).

1. Câu hỏi "trong tín hiệu có những tần số nào?"

Ý tưởng nền tảng: so khớp (correlation). Muốn biết tín hiệu $x[n]$ có "chứa" một tần số cụ thể hay không, nhân từng mẫu $x[n]$ với một sinusoid ở đúng tần số đó rồi CỘNG DỒN kết quả lại — nếu tín hiệu thực sự dao động cùng nhịp với sinusoid dò, tổng sẽ LỚN (khớp nhau, không triệt tiêu); nếu không, các số dương/âm sẽ triệt tiêu lẫn nhau, tổng gần 0.

DFT chỉ đơn giản là làm phép so khớp đó cho $N$ sinusoid dò khác nhau — một bảng $N$ phép so khớp, mỗi phép cho ra một con số gọi là một bin tần số. Không có gì huyền bí: DFT là một BẢNG TÍNH, không phải phép biến hình bí ẩn.

2. Công thức DFT

Công thức đầy đủ:

$$X[k] = \sum_{n=0}^{N-1} x[n]\, e^{-j2\pi kn/N}$$

Giải phẫu từng ký hiệu:

  • $x[n]$: tín hiệu gốc, $N$ mẫu rời rạc (Bài 1) — đầu vào duy nhất.
  • $e^{-j2\pi kn/N}$: chính là sinusoid dò tần số bin $k$, viết dưới dạng số phức (số phức gói ĐỒNG THỜI cả biên độ lẫn pha vào một đối tượng duy nhất — không cần background toán phức, chỉ cần biết: $\mathrm{Re}(e^{-j\theta}) = \cos\theta$, $\mathrm{Im}(e^{-j\theta}) = -\sin\theta$, và một số phức $c = re + j\,im$ có biên độ $|c|=\sqrt{re^2+im^2}$, pha $\angle c = \mathrm{atan2}(im, re)$).
  • $X[k]$: kết quả so khớp cho bin thứ $k$ — một số phức duy nhất chứa cả biên độ VÀ pha của thành phần tần số đó trong tín hiệu gốc.
  • Bin $k$ ứng với tần số thật $f_k = k \cdot f_s/N$ (Hz) — chuyển từ chỉ số bin trừu tượng sang tần số vật lý có ý nghĩa.

Verify bằng số thật: sine 1000Hz lấy mẫu ở 8000Hz, đúng $N=8$ mẫu (1 chu kỳ trọn vẹn, Bài 1). Bin $k=1$ ứng tần số $f_1 = 1 \cdot 8000/8 = 1000$Hz — ĐÚNG tần số của sine gốc. Chạy dft() tự viết: bin $k=1$ có biên độ $|X[1]| = 4 = N/2$ (đúng công thức lý thuyết cho sine biên độ 1), pha $\angle X[1] = -\pi/2$ (đúng cho sine pha ban đầu 0, vì $\sin\theta = \cos(\theta - \pi/2)$). Mọi bin khác (trừ bin đối xứng, Mục 4) đều gần bằng 0 — không có tần số nào khác trong tín hiệu.

3. Độ phân giải tần số

Khoảng cách tần số giữa 2 bin liền kề — độ phân giải tần số — là $$\Delta f = \frac{f_s}{N}$$ Verify: $f_s=8000$Hz, $N=8$ mẫu $\Rightarrow \Delta f = 1000$Hz/bin (khớp đúng bin $k=1$ ở trên).

Hệ quả thực dụng — không có bữa trưa miễn phí: muốn phân biệt 2 tone chỉ cách nhau 1Hz, cần $\Delta f \le 1$Hz, tức $N \ge f_s$ mẫu — ĐÚNG BẰNG $f_s$ giây dữ liệu, ở BẤT KỲ tần số lấy mẫu nào (verify: $f_s=8000$Hz cần $N=8000$ mẫu = 1 giây; $f_s=44100$Hz cần $N=44100$ mẫu = CŨNG ĐÚNG 1 giây). Muốn phân giải tần số tốt hơn buộc phải quan sát tín hiệu LÂU HƠN — đây chính là mầm mống của trade-off dài–mịn khai thác đầy đủ ở STFT (Bài 8): cửa sổ dài cho phổ mịn nhưng mất độ phân giải THỜI GIAN, cửa sổ ngắn cho độ phân giải thời gian tốt nhưng phổ thô.

4. Phổ của tín hiệu thực

Với tín hiệu thực (không phải số phức — mọi tín hiệu âm thanh/cảm biến đều vậy), phổ có tính đối xứng liên hợp (conjugate symmetry): bin $k$ và bin $N-k$ luôn là 2 số phức LIÊN HỢP của nhau — CÙNG biên độ, NGƯỢC dấu pha. Verify ở ví dụ trên: $|X[1]|=|X[7]|=4$ (cùng biên độ), nhưng $\angle X[1] = -\pi/2$ còn $\angle X[7] = +\pi/2$ (ngược dấu). Hệ quả thực dụng: chỉ cần lưu/hiển thị đúng $N/2$ bin đầu (từ $k=0$ đến $k=N/2$) — nửa còn lại KHÔNG mang thông tin mới, chỉ là bản sao gương.

Một bin $X[k]$ mang 2 phổ khác nhau: phổ biên độ $|X[k]|$ (tần số đó MẠNH bao nhiêu) và phổ pha $\angle X[k]$ (tần số đó bắt đầu ở ĐÂU trong chu kỳ). Cả 2 đều bắt buộc để dựng lại đúng dạng sóng gốc — verify: idft(dft(x)) (biến đổi ngược, dùng cả biên độ LẪN pha) khớp lại tín hiệu gốc với sai số chỉ khoảng $10^{-9}$.

⚠️ Pitfall: vứt pha đi là mất dạng sóng — pha mới mang cấu trúc
Trực giác sai phổ biến: "chỉ cần biên độ là đủ, pha không quan trọng lắm". Thí nghiệm verified (demo Mục 6 bên dưới): lấy 2 tín hiệu chirp quét băng thông rộng ngược chiều nhau ($A$: 10→150Hz, $B$: 150→10Hz), ghép biên độ của $A$ với pha của $B$ rồi dựng lại tín hiệu bằng idft() — kết quả tương quan > 0,999 với $B$ (tín hiệu CHO PHA), nhưng tương quan < 0,1 với $A$ (tín hiệu cho biên độ, gần như KHÔNG liên quan). Pha — không phải biên độ — mới là thứ mang cấu trúc/hình dạng thời gian của tín hiệu.

5. Tự viết DFT 15 dòng + verify

Cài đặt trực tiếp theo đúng định nghĩa — không có "phép màu", chỉ 2 vòng lặp lồng nhau:

dft.js (trích từ dsp-core.js)
function dft(x) {
  const N = x.length;
  const X = new Array(N);
  for (let k = 0; k < N; k++) {
    let re = 0, im = 0;
    for (let n = 0; n < N; n++) {
      const angle = -2 * Math.PI * k * n / N;
      re += x[n] * Math.cos(angle);
      im += x[n] * Math.sin(angle);
    }
    X[k] = { re, im };
  }
  return X;
}

Verify: chạy trên sine 1000Hz/8000Hz (8 mẫu) — bin $k=1$ ra đúng biên độ 4, pha $-\pi/2$, mọi bin khác gần 0 — khớp CHÍNH XÁC những gì Mục 2 tính tay. Nhưng độ phức tạp thuật toán là $O(N^2)$: gấp đôi $N$ thì khối lượng tính tăng GẤP BỐN. Với $N=4096$ (một khung phổ âm thanh thực tế phổ biến), đó là hơn 16 triệu phép nhân-cộng — bấm nút bên dưới đo trực tiếp trên máy của bạn:

Bấm nút để đo thời gian thật.

Vài trăm mili-giây cho ĐÚNG MỘT khung 4096 mẫu — với âm thanh thời gian thực cần xử lý HÀNG CHỤC khung mỗi giây, đây là "chậm không thể chấp nhận được". Bài 6 (FFT) giải quyết chính xác vấn đề này: cùng một kết quả, nhanh hơn hàng trăm lần.

6. Thực hành: DFT Explorer & tráo pha

Demo dưới đây chạy đúng dft()/idft() vừa verify: vẽ tín hiệu tự do bằng chuột thấy phổ hiện ra tức thì, click một bin thấy đúng sinusoid nó đại diện overlay lên tín hiệu gốc; và thí nghiệm tráo pha Mục 4 nghe được trực tiếp.

📊 DFT Explorer & Tráo Pha — DSPJS thật

1. DFT Explorer — vẽ tín hiệu (kéo chuột), click 1 cột phổ để overlay sinusoid

Click một cột trong phổ để xem chi tiết bin đó.

2. Tráo pha — nghe "ma quái": biên độ A ghép với pha B

Đang tính tương quan…
dft_explorer_demo.js (đúng logic đang chạy ở tab Xem trước)
import { dft, idft, complexMagnitude, complexPhase, chirp, dftMagnitude, dftPhase } from './dsp-core.js';

// --- DFT Explorer: N=64 mau, fs=64 (bin k <-> k Hz, de doc) ---
const N = 64;
const X = dft(signalSamples); // signalSamples: mang 64 so, ve bang chuot

// Click bin k: dung lai DUNG sinusoid no dai dien bang idft() tren pho
// chi giu bin k va bin doi xung lien hop N-k, con lai = 0.
function binComponent(X, k) {
  const spectrum = Array.from({ length: N }, () => ({ re: 0, im: 0 }));
  spectrum[k] = X[k];
  spectrum[(N - k) % N] = X[(N - k) % N];
  return idft(spectrum); // overlay: dung sinusoid, verify sai so ~1e-14
}

// --- Trao pha: 2 chirp bang thong rong quet nguoc chieu ---
const N3 = 400, fs3 = 400;
const sigA = Array.from({ length: N3 }, (_, n) => chirp(n, 10, 150, N3, fs3, 0.9));
const sigB = Array.from({ length: N3 }, (_, n) => chirp(n, 150, 10, N3, fs3, 0.9));
const magA = dftMagnitude(dft(sigA));
const phaseB = dftPhase(dft(sigB));
const hybrid = magA.map((m, k) => ({ re: m * Math.cos(phaseB[k]), im: m * Math.sin(phaseB[k]) }));
const hybridSignal = idft(hybrid);
// verify: correlate(hybridSignal, sigB) > 0.999, correlate(hybridSignal, sigA) < 0.1
// - pha (khong phai bien do) moi la thu mang cau truc/dang song.

Tóm lược

  • ✅ DFT = $N$ phép so khớp (correlation) tín hiệu với $N$ sinusoid dò tần số $f_k = k\cdot f_s/N$ — công thức $X[k] = \sum_n x[n]e^{-j2\pi kn/N}$, mỗi bin là 1 số phức chứa cả biên độ lẫn pha.
  • ✅ Verify: sine 1000Hz/8000Hz (N=8) cho bin $k=1$ biên độ $|X[1]|=N/2=4$, pha $-\pi/2$ — khớp đúng công thức lý thuyết.
  • ✅ Độ phân giải tần số $\Delta f = f_s/N$: muốn phân biệt 2 tone cách 1Hz cần $N=f_s$ mẫu (1 giây dữ liệu, BẤT KỲ $f_s$ nào) — không có bữa trưa miễn phí.
  • ✅ Đối xứng liên hợp: bin $k$ và $N-k$ cùng biên độ, ngược pha — chỉ $N/2$ bin đầu mang tin mới.
  • ✅ Verified: tráo pha (biên độ A + pha B) cho kết quả tương quan >0,999 với B, <0,1 với A — pha mới mang cấu trúc/dạng sóng, không phải biên độ.
  • ✅ DFT trực tiếp là $O(N^2)$ — chậm không chấp nhận được ở $N$ lớn — cliffhanger sang FFT (Bài 6).

Trắc nghiệm ôn tập

Câu 1

Về bản chất, DFT đang làm phép toán gì với tín hiệu đầu vào?

Câu 2

Verified: muốn phân biệt 2 tone cách nhau 1Hz, cần $N \ge f_s$ mẫu — đúng $f_s$ giây dữ liệu, ở BẤT KỲ tần số lấy mẫu nào ($f_s=8000$Hz cũng như $f_s=44100$Hz đều cần đúng 1 giây). Điều này nói lên điều gì?

Câu 3

Verified: với tín hiệu thực, bin $k$ và bin $N-k$ luôn có CÙNG biên độ nhưng NGƯỢC dấu pha (đối xứng liên hợp). Hệ quả thực dụng của tính chất này là gì?

Câu 4

Verified: ghép biên độ của tín hiệu A với pha của tín hiệu B rồi dựng lại (idft), kết quả tương quan >0,999 với B nhưng <0,1 với A. Kết luận rút ra từ thí nghiệm này là gì?

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

File JavaScript DSPJS — thư viện DSP mini dùng xuyên suốt cả 15 bài, Bài 5 vừa thêm dft, idft và các complex helpers, 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 4: Hệ LTI, tích chập & đáp ứng xung Bài 6: FFT: thuật toán thay đổi thế giới Quay lại Lộ trình Series Xử Lý Tín Hiệu Số

Bình luận