Bỏ qua

Xử lý tín hiệu số

Xử lý tín hiệu số (DSP) chuyển đổi dạng sóng âm thanh thô thành các biểu diễn có cấu trúc mà các mô hình học máy có thể học được. File này bao quát vật lý âm thanh, lấy mẫu và lượng tử hóa, biến đổi Fourier (DFT, FFT), phổ spectrogram, dải lọc mel, MFCC, và cửa sổ hóa — tức toàn bộ quy trình trích xuất đặc trưng cho mọi hệ thống AI về tiếng nói và âm thanh.

  • Âm thanh là một sóng áp suất lan truyền qua một môi trường (không khí, nước, chất rắn). Một vật dao động (dây thanh âm, dây đàn ghita, màng loa) đẩy và kéo các phân tử không khí, tạo ra các vùng luân phiên có áp suất cao (nén) và áp suất thấp (giãn nở).

  • Những biến thiên áp suất này lan truyền ra ngoài với tốc độ khoảng 343 m/s trong không khí và đến tai bạn, nơi chúng làm rung màng nhĩ và được chuyển đổi thành các tín hiệu thần kinh.

  • Hãy hình dung âm thanh giống như thả một hòn đá vào một hồ nước tĩnh: hòn đá là nguồn dao động, các gợn sóng là sóng áp suất, và một chiếc nút bần nổi lên xuống trên mặt nước chính là microphone hoặc màng nhĩ phản ứng khi sóng tới.

  • Độ cao của sự nhấp nhô của nút bần chính là biên độ (amplitude), tần suất nó nhấp nhô mỗi giây là tần số (frequency), và việc nó bắt đầu ở đỉnh hay đáy của nhịp nhấp nhô khi sóng tới chính là pha (phase).

  • Dạng sóng (waveform) là một đồ thị biểu diễn áp suất (hoặc điện áp, sau khi microphone chuyển âm thanh thành tín hiệu điện) theo thời gian. Dạng sóng đơn giản nhất là một âm thuần (pure tone), tức một hàm sin đơn:

\[x(t) = A \sin(2\pi f t + \phi)\]
  • trong đó:

    • \(A\) là biên độ (độ lệch đỉnh so với không, quyết định độ to),
    • \(f\) là tần số tính bằng Hz (chu kỳ mỗi giây, quyết định cao độ),
    • \(\phi\) là pha tính bằng radian (độ lệch thời gian của sóng).
  • Chu kỳ\(T = 1/f\), tức thời lượng của một chu kỳ hoàn chỉnh.

Một sóng sine với chú thích biên độ, chu kỳ, tần số và độ lệch pha

  • Biên độ quyết định độ to mà ta cảm nhận. Tăng gấp đôi biên độ sẽ tăng gấp bốn lần công suất (vì công suất tỉ lệ với bình phương biên độ).

  • Thính giác con người trải dài trên một dải biên độ cực lớn, nên ta dùng một thang logarit: đó là decibel (dB). Mức áp suất âm thanh là:

\[L = 20 \log_{10}\left(\frac{A}{A_\text{ref}}\right) \text{ dB}\]
  • trong đó \(A_\text{ref}\) là biên độ tham chiếu (thường là ngưỡng nghe, \(20 \mu\text{Pa}\)). Tiếng thì thầm khoảng 30 dB, trò chuyện bình thường 60 dB, hòa nhạc rock 110 dB. Mỗi lần tăng 6 dB xấp xỉ tăng gấp đôi biên độ; mỗi lần tăng 10 dB xấp xỉ tăng gấp đôi độ to cảm nhận được. Hàm logarit ở đây chính là hàm đã gặp ở chương 03.

  • Tần số quyết định cao độ. Tần số thấp (20–250 Hz) nghe trầm; tần số cao (2000–20000 Hz) nghe bổng. Thính giác người trải rộng từ khoảng 20 Hz đến 20 kHz. Nốt La hòa tấu là 440 Hz. Tăng gấp đôi tần số sẽ nâng cao độ lên một quãng tám (octave).

  • Hầu hết âm thanh tự nhiên không phải âm thuần mà là hỗn hợp phức tạp của nhiều tần số, đó là lý do piano và violin chơi cùng một nốt nghe khác nhau: chúng chia sẻ cùng tần số cơ bản (fundamental frequency) nhưng khác nhau ở hài âm (harmonics — các bội số nguyên của tần số cơ bản) và biên độ tương đối (tức âm sắc — timbre).

  • Pha quyết định sóng bắt đầu ở đâu trong chu kỳ của nó. Hai sóng có cùng biên độ và tần số nhưng khác pha có thể giao thoa cộng (pha trùng khớp, biên độ cộng dồn) hoặc giao thoa trừ (pha ngược nhau, biên độ triệt tiêu).

  • Pha rất quan trọng trong âm thanh stereo và beamforming, nhưng lại bị bỏ qua phần lớn trong các quy trình xử lý tiếng nói vì nhận thức cao độ và âm sắc của con người hầu như không phụ thuộc vào pha.

  • Tín hiệu âm thanh thực tế là các hàm liên tục theo thời gian, nhưng máy tính làm việc với các số rời rạc. Lấy mẫu (sampling) chuyển đổi một tín hiệu liên tục thành một dãy rời rạc bằng cách đo giá trị của tín hiệu ở những khoảng thời gian đều đặn.

  • Tần số lấy mẫu \(f_s\) là số lần đo mỗi giây. Âm thanh CD dùng \(f_s = 44{,}100\) Hz; điện thoại dùng 8000 Hz; các mô hình tiếng nói hiện đại thường dùng 16000 Hz.

  • Định lý lấy mẫu Nyquist-Shannon phát biểu rằng một tín hiệu liên tục có thể được tái tạo hoàn hảo từ các mẫu của nó khi và chỉ khi tần số lấy mẫu ít nhất gấp đôi tần số cao nhất có mặt trong tín hiệu:

\[f_s \geq 2 f_\text{max}\]
  • Tần số \(f_s / 2\) được gọi là tần số Nyquist. Nếu tín hiệu chứa các tần số cao hơn tần số Nyquist, những tần số đó sẽ gấp lại (fold back) vào dải hợp lệ và xuất hiện như các thành phần tần số thấp giả. Hiện tượng này gọi là nhiễu alias (aliasing). Aliasing là không thể đảo ngược: một khi đã xảy ra, tín hiệu gốc không thể khôi phục từ các mẫu.

  • Phép loại suy đời thường cho aliasing là hiệu ứng bánh xe ngựa trong phim: một bánh xe quay nhanh hơn tốc độ khung hình một chút sẽ trông như quay chậm ngược lại vì máy quay lấy mẫu vòng quay quá thưa. Trong âm thanh, một âm 15 kHz được lấy mẫu ở 16 kHz (\(f_\text{Nyquist} = 8\) kHz) bị alias xuống \(16 - 15 = 1\) kHz, một cao độ hoàn toàn khác.

Lấy mẫu đúng tín hiệu so với lấy mẫu bị nhiễu alias khi tần số lấy mẫu quá thấp

  • Để ngăn aliasing, một bộ lọc chống alias (anti-aliasing filter — một bộ lọc thông thấp) loại bỏ mọi tần số cao hơn \(f_s/2\) trước khi lấy mẫu. Bộ lọc này được phần cứng chuyển đổi tương tự-số (ADC) áp dụng trước khi tín hiệu được số hóa.

  • Lượng tử hóa (quantisation) ánh xạ mỗi mẫu có giá trị liên tục vào giá trị gần nhất trong một tập hữu hạn các mức. Bộ lượng tử hóa \(n\) bit có \(2^n\) mức. Âm thanh CD dùng lượng tử hóa 16 bit (\(2^{16} = 65{,}536\) mức); điện thoại thường dùng 8 bit với companding \(\mu\)-law hoặc A-law (một ánh xạ phi tuyến dành nhiều mức hơn cho biên độ nhỏ, phù hợp với nhận thức của con người). Lượng tử hóa sinh ra nhiễu lượng tử hóa (quantisation noise), một dạng sai số làm tròn với phương sai \(\Delta^2/12\), trong đó \(\Delta\) là bước giữa các mức.

  • Phân tích miền thời gian (time-domain analysis) trích xuất đặc trưng trực tiếp từ dạng sóng mà không chuyển đổi sang miền khác. Những đặc trưng này đơn giản, tính nhanh, và nắm bắt được các tính chất cơ bản của tín hiệu.

  • Năng lượng (energy) của một khung gồm \(N\) mẫu đo độ to tổng thể:

\[E = \sum_{n=0}^{N-1} x[n]^2\]
  • Các đoạn tiếng nói có năng lượng cao; khoảng lặng có năng lượng thấp. Năng lượng chính là chuẩn \(\ell_2\) bình phương từ chương 01 áp dụng lên một vector tín hiệu.

  • Tỷ lệ qua không (zero-crossing rate — ZCR) đếm tín hiệu đổi dấu bao nhiêu lần trong một khung:

\[\text{ZCR} = \frac{1}{2(N-1)} \sum_{n=1}^{N-1} |\text{sign}(x[n]) - \text{sign}(x[n-1])|\]
  • ZCR cao chỉ ra thành phần tần số cao hoặc nhiễu; ZCR thấp chỉ ra tần số thấp hoặc tiếng nói có thanh (voiced — nơi dây thanh âm dao động tuần hoàn). ZCR là một bộ ước lượng tần số thô: một âm thuần ở \(f\) Hz qua không \(2f\) lần mỗi giây.

  • Tự tương quan (autocorrelation) đo xem một tín hiệu giống bản sao lệch thời gian của chính nó đến mức nào:

\[R[k] = \sum_{n=0}^{N-1-k} x[n] \cdot x[n+k]\]
  • Tại độ trễ \(k = 0\), tự tương quan bằng năng lượng. Với tín hiệu tuần hoàn, tự tương quan có các đỉnh tại các độ trễ bằng chu kỳ và bội của nó. Đây là kỹ thuật chuẩn cho phát hiện cao độ (pitch detection): tìm đỉnh đáng kể đầu tiên trong \(R[k]\) sau \(k=0\), và cao độ là \(f_s / k_\text{peak}\). Tự tương quan liên quan đến tích vô hướng từ chương 01: \(R[k]\) là tích vô hướng của tín hiệu với phiên bản lệch \(k\) của nó.

  • Phân tích miền tần số (frequency-domain analysis) bộc lộ nội dung phổ của tín hiệu, thông tin không nhìn thấy được trong dạng sóng. Công cụ then chốt là Biến đổi Fourier rời rạc (Discrete Fourier Transform — DFT), phân rã một tín hiệu gồm \(N\) mẫu thành \(N\) thành phần tần số có giá trị phức:

\[X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j 2\pi k n / N}, \quad k = 0, 1, \ldots, N-1\]
  • Mỗi \(X[k]\) là một số phức có độ lớn \(|X[k]|\) cho biên độ của thành phần tần số tại \(f_k = k \cdot f_s / N\) Hz, và có pha \(\angle X[k]\) cho độ lệch pha. DFT là một phép đổi cơ sở từ cơ sở miền thời gian (các xung đơn vị) sang cơ sở miền tần số (các hàm mũ phức), là một ứng dụng trực tiếp của khái niệm cơ sở từ chương 02. DFT có thể viết dưới dạng nhân ma trận \(\mathbf{X} = W \mathbf{x}\), với \(W\) là ma trận DFT \(N \times N\) có các phần tử \(W_{kn} = e^{-j2\pi kn/N}\).

  • Biến đổi Fourier nhanh (Fast Fourier Transform — FFT) là một thuật toán tính DFT trong \(O(N \log N)\) phép toán thay vì \(O(N^2)\) ngây thơ, bằng cách đệ quy chia bài toán thành các bài toán con chỉ số chẵn và chỉ số lẻ (thuật toán Cooley-Tukey). Sự tăng tốc này làm cho phân tích phổ thời gian thực trở nên khả thi. FFT là một trong những thuật toán quan trọng nhất của toàn bộ ngành khoa học máy tính.

  • Phổ công suất (power spectrum) \(|X[k]|^2\) cho thấy sự phân bố năng lượng theo tần số. Phổ biên độ (magnitude spectrum) \(|X[k]|\) cho thấy biên độ. Vẽ các phổ này bộc lộ tần số nào chiếm ưu thế trong tín hiệu: một nguyên âm có các hài âm mạnh tại các bội nguyên của tần số cơ bản; một âm xát (như "s") có năng lượng tần số cao trải rộng.

  • Phổ spectrogram là một biểu diễn trực quan về cách nội dung tần số của tín hiệu thay đổi theo thời gian. Nó được tính bằng cách chặt tín hiệu thành các khung ngắn chồng lấp, tính FFT của mỗi khung, và xếp các phổ biên độ kết quả cạnh nhau. Trục ngang là thời gian, trục dọc là tần số, và màu sắc (hoặc độ sáng) tại mỗi điểm biểu diễn biên độ. Spectrogram là hình ảnh trực quan quan trọng nhất trong xử lý âm thanh.

Phổ spectrogram với trục ngang là thời gian, trục dọc là tần số, và cường độ biểu diễn bằng màu

  • Thang mel là một thang tần số cảm quan phản ánh cách con người nhận thức cao độ. Con người nhận thức các tỉ lệ tần số bằng nhau như những khoảng cao độ bằng nhau (cũng giống như ta nhận thức các tỉ lệ cường độ bằng nhau như những khoảng độ to bằng nhau). Dưới khoảng 1000 Hz, thang mel xấp xỉ tuyến tính; trên 1000 Hz, nó trở nên xấp xỉ logarit:
\[m = 2595 \log_{10}\left(1 + \frac{f}{700}\right)\]
  • Nghịch đảo là \(f = 700(10^{m/2595} - 1)\). Thang mel là lý do các nửa cung (semitone) âm nhạc cách đều nhau trên một trục log-tần số: từ A4 (440 Hz) lên A5 (880 Hz) và từ A5 lên A6 (1760 Hz) đều nghe như "nâng một quãng tám" dù các khoảng cách Hz lần lượt là 440 và 880.

  • Dải lọc mel (mel filterbank) là một tập các bộ lọc thông dải hình tam giác cách đều nhau trên thang mel. Mỗi bộ lọc bao phủ một dải tần số và cộng năng lượng phổ trong dải đó, cho ra một số duy nhất. Các hệ thống tiếng nói điển hình dùng 40–80 bộ lọc mel. Các bộ lọc tần số thấp hẹp (độ phân giải tần số cao, nơi ta nhạy cảm về mặt cảm quan) và các bộ lọc tần số cao rộng (độ phân giải thấp, nơi ta kém nhạy hơn). Điều này bắt chước độ phân giải tần số của ốc tai người.

Dải lọc mel hình tam giác phủ trên trục tần số tuyến tính, cho thấy bộ lọc hẹp ở tần số thấp và rộng ở tần số cao

  • Hệ số cepstral tần số mel (Mel-Frequency Cepstral Coefficients — MFCC) là biểu diễn đặc trưng kinh điển cho tiếng nói và âm thanh. Chúng nén phổ mel thành một số lượng nhỏ các hệ số không tương quan, nắm bắt được hình dạng của bao phổ (vốn mã hóa cấu hình đường dẫn giọng nói và do đó mã hóa bản sắc âm vị) đồng thời loại bỏ chi tiết phổ tinh tế (vốn mã hóa cao độ và pha).

  • Quy trình MFCC:

    1. Tiền nhấn (Pre-emphasis): áp dụng bộ lọc thông cao bậc nhất \(y[n] = x[n] - \alpha x[n-1]\) (thường \(\alpha = 0.97\)) để tăng cường tần số cao bị suy giảm bởi đường dẫn giọng nói.
    2. Phân khung (Framing): chặt tín hiệu thành các khung chồng lấp (thường dài 25 ms, bước nhảy 10 ms).
    3. Cửa sổ hóa (Windowing): nhân mỗi khung với một hàm cửa sổ (Hamming) để giảm rò rỉ phổ (xem bên dưới).
    4. FFT: tính phổ công suất của mỗi khung đã cửa sổ hóa.
    5. Dải lọc mel: áp dụng dải lọc mel hình tam giác lên phổ công suất, sinh ra năng lượng các dải mel.
    6. Log: lấy logarithm của năng lượng các dải mel. Phép log nén dải động và chuyển phép nhân (của các thành phần phổ) thành phép cộng, phù hợp với nhận thức độ to của con người.
    7. DCT: áp dụng Biến đổi cosin rời rạc lên năng lượng log-mel. DCT bất tương quan hóa các dải mel (vì các dải liền kề highly correlated) và cô đặc năng lượng vào vài hệ số đầu. Giữ 13 hệ số đầu (MFCC-0 đến MFCC-12).

Quy trình MFCC từ âm thanh thô qua các khung có cửa sổ, FFT, dải lọc mel, nén log, và DCT đến đặc trưng MFCC cuối

  • DCT ở bước 7 về cơ bản là "biến đổi Fourier của phổ" (do đó có tên cepstrum = đảo chữ của spectrum). Các hệ số cepstral bậc thấp nắm bắt hình dạng phổ rộng (cộng hưởng đường dẫn giọng nói, gọi là formant), trong khi các hệ số bậc cao nắm bắt chi tiết phổ tinh tế (hài âm cao độ). Bằng cách chỉ giữ 13 hệ số đầu, ta giữ lại thông tin formant và loại bỏ chi tiết cao độ.

  • Deltadelta-delta MFCC (đạo hàm bậc nhất và bậc hai theo thời gian của MFCC, tính bằng sai phân hữu hạn qua các khung liền kề) nắm bắt động thái của hình dạng phổ, bổ sung ngữ cảnh thời gian. Một vector đặc trưng MFCC đầy đủ thường có 39 chiều: 13 tĩnh + 13 delta + 13 delta-delta.

  • Các mô hình mạng nơ-ron hiện đại (chương 06) đã thay thế phần lớn MFCC bằng các đặc trưng được học: log-mel spectrogram (đầu ra của bước 6, bỏ qua DCT) là đầu vào chuẩn cho ASR học sâu và phân lớp âm thanh. Mô hình tự học sự bất tương quan của nó. Dù vậy, MFCC vẫn quan trọng trong các thiết lập ít tài nguyên, quy trình ML cổ điển, và để hiểu nền tảng xử lý tín hiệu.

  • Cửa sổ hóa (windowing) là quá trình nhân một khung tín hiệu với một hàm cửa sổ mượt trước khi tính FFT. Nếu không cửa sổ hóa, FFT giả định khung lặp lại vô hạn; sự bắt đầu và kết thúc đột ngột của khung tạo ra các bất liên tục nhân tạo lan tỏa năng lượng ra mọi tần số, một artifacts gọi là rò rỉ phổ (spectral leakage).

  • Cửa sổ chữ nhật (rectangular window) \(w[n] = 1\) với mọi \(n\): không vát, rò rỉ tối đa, nhưng chóp chính rộng nhất (độ phân giải tần số tốt nhất cho một độ dài khung cho trước). Hiếm khi dùng trong thực tế.

  • Cửa sổ Hamming: \(w[n] = 0.54 - 0.46 \cos(2\pi n / (N-1))\). Vát gần về không ở các cạnh, giảm mạnh rò rỉ. Lựa chọn chuẩn cho xử lý tiếng nói.

  • Cửa sổ Hann (cũng gọi là Hanning): \(w[n] = 0.5 - 0.5 \cos(2\pi n / (N-1))\). Vát chính xác về không ở các cạnh. Rất giống Hamming nhưng có triệt sóng phụ (sidelobe suppression) tốt hơn một chút.

  • Cửa sổ Blackman: \(w[n] = 0.42 - 0.5 \cos(2\pi n / (N-1)) + 0.08 \cos(4\pi n / (N-1))\). Triệt sóng phụ còn tốt hơn nhưng chóp chính rộng hơn (độ phân giải tần số tệ hơn). Dùng khi các artifacts sóng phụ đặc biệt phiền toái.

  • Có một đánh đổi cơ bản: các cửa sổ ít rò rỉ hơn thì chóp chính rộng hơn, nghĩa là chúng không thể phân giải hai tần số gần nhau. Đây là sự đánh đổi độ phân giải phổ so với rò rỉ, hệ quả của nguyên lý bất định từ chương 03.

  • Chồng lấp-cộng (overlap-add — OLA) là một kỹ thuật tái tạo tín hiệu từ các khung đã cửa sổ và xử lý. Các khung chồng lấp (thường 50–75%), và sau khi xử lý, các đầu ra có cửa sổ được cộng lại. Nếu cửa sổ và độ chồng lấp được chọn đúng (ví dụ, cửa sổ Hann với chồng lấp 50%), các cửa sổ chồng lấp cộng lại thành một hằng số, cho tái tạo hoàn hảo. Điều này thiết yếu cho mọi sự sửa đổi âm thanh theo khung (giảm nhiễu, dịch cao độ, kéo dãn thời gian).

  • Biến đổi Fourier thời gian ngắn (Short-Time Fourier Transform — STFT) là khung formal làm nền cho spectrogram. Nó áp dụng DFT lên mỗi khung có cửa sổ của tín hiệu:

\[ \text{STFT}\{x[n]\}(m, k) = \sum_{n=0}^{N-1} x[n + mH] \cdot w[n] \cdot e^{-j 2\pi k n / N} \]
  • trong đó \(m\) là chỉ số khung, \(H\) là kích thước bước nhảy (hop size — số mẫu giữa các khung liên tiếp), \(w[n]\) là hàm cửa sổ, và \(N\) là kích thước FFT. Đầu ra là một ma trận phức 2D: biểu diễn thời gian-tần số của tín hiệu.

  • STFT hiện thân một sự đánh đổi thời gian-tần số cơ bản:

    • Khung dài (N lớn): độ phân giải tần số cao (phân biệt được các tần số gần nhau) nhưng độ phân giải thời gian kém (không xác định được chính xác khi nào một tần số thay đổi).
    • Khung ngắn (N nhỏ): độ phân giải thời gian cao nhưng độ phân giải tần số kém.
    • Tích của độ phân giải thời gian và độ phân giải tần số bị chặn dưới: \(\Delta t \cdot \Delta f \geq \frac{1}{4\pi}\). Đây là giới hạn Gabor, phép loại suy xử lý tín hiệu của nguyên lý bất định Heisenberg từ vật lý.
  • Các tham số STFT điển hình cho tiếng nói: độ dài khung 25 ms (\(N = 400\) ở 16 kHz), bước nhảy 10 ms (\(H = 160\)), cửa sổ Hamming, FFT 512 điểm (zero-padding từ 400 để hiệu quả và nội suy phổ mượt hơn).

  • Lọc (filtering) sửa đổi nội dung tần số của tín hiệu bằng cách khuếch đại một số tần số và suy giảm các tần số khác. Một bộ lọc (filter) là một hệ thống nhận tín hiệu đầu vào và sinh tín hiệu đầu ra. Các bộ lọc được đặc trưng bởi đáp ứng tần số (frequency response) \(H(f)\), mô tả độ lợi và độ lệch pha áp dụng lên mỗi tần số.

  • Bộ lọc thông thấp (low-pass filter): cho qua các tần số dưới tần số cắt \(f_c\) và suy giảm các tần số trên đó. Loại bỏ nhiễu và chi tiết tần số cao. Bộ lọc chống alias trước khi lấy mẫu là một bộ lọc thông thấp.

  • Bộ lọc thông cao (high-pass filter): cho qua các tần số trên \(f_c\) và suy giảm bên dưới. Loại bỏ tiếng ầm tần số thấp và độ lệch DC. Bộ lọc tiền nhấn trong trích xuất MFCC (\(y[n] = x[n] - 0.97 x[n-1]\)) là một bộ lọc thông cao đơn giản.

  • Bộ lọc thông dải (band-pass filter): cho qua các tần số trong khoảng \([f_1, f_2]\) và suy giảm bên ngoài. Mỗi tam giác trong dải lọc mel là một bộ lọc thông dải.

  • Bộ lọc dừng dải (band-stop / notch filter): suy giảm một dải tần số hẹp cụ thể. Dùng để loại bỏ nhiễu điện cụ thể (ví dụ tiếng rè đường dây điện 50/60 Hz).

  • Một bộ lọc Đáp ứng xung hữu hạn (Finite Impulse Response — FIR) tính mỗi mẫu đầu ra như một tổng có trọng số của các mẫu đầu vào hiện tại và quá khứ:

\[y[n] = \sum_{k=0}^{M} b_k \cdot x[n-k]\]
  • Các trọng số \(b_k\)hệ số bộ lọc (filter coefficients — còn gọi là taps). Bộ lọc có bậc \(M\). Bộ lọc FIR luôn ổn định (đầu ra không bao giờ phân kỳ) và có thể thiết kế để có pha hoàn toàn tuyến tính (mọi tần số bị trễ cùng một lượng, bảo toàn hình dạng dạng sóng). Nhược điểm là đạt được độ cắt sắc nét đòi hỏi nhiều taps (M lớn), tăng tính toán. Đầu ra là một phép tích chập của đầu vào với vector hệ số, chính xác là phép tích chập 1D từ chương 06.

  • Một bộ lọc Đáp ứng xung vô hạn (Infinite Impulse Response — IIR) dùng hồi tiếp: đầu ra phụ thuộc vào cả đầu vào quá khứ và đầu ra quá khứ:

\[ y[n] = \sum_{k=0}^{M} b_k \cdot x[n-k] - \sum_{k=1}^{L} a_k \cdot y[n-k] \]
  • Các số hạng hồi tiếp \(a_k\) tạo ra một cấu trúc đệ quy có đáp ứng xung về lý thuyết kéo dài vô hạn. Bộ lọc IIR đạt được độ cắt sắc nét với số hệ số ít hơn nhiều so với FIR, nhưng chúng có thể không ổn định (đầu ra tăng không bound nếu các cực của hàm truyền nằm ngoài đường tròn đơn vị — một khái niệm từ biến đổi \(z\)). Chúng cũng có pha phi tuyến, có thể làm méo hình dạng dạng sóng. Các thiết kế bộ lọc cổ điển (Butterworth, Chebyshev, elliptic) là IIR.

  • Hàm truyền (transfer function) của một bộ lọc thời gian rời rạc thu được qua biến đổi \(z\):

\[H(z) = \frac{\sum_{k=0}^{M} b_k z^{-k}}{1 + \sum_{k=1}^{L} a_k z^{-k}}\]
  • Các nghiệm của tử số gọi là zeros và các nghiệm của mẫu số gọi là poles. Đồ thị pole-zero (zero và cực) đặc trưng hoàn toàn hành vi của bộ lọc. Các cực gần đường tròn đơn vị khuếch đại các tần số lân cận; các zeros gần đường tròn đơn vị suy giảm chúng. Bộ lọc FIR chỉ có zeros (mẫu số bằng 1). Điều này kết nối với các khái niệm trị riêng và tìm nghiệm từ chương 02 và chương 03.

  • Định lý tích chập (convolution theorem): tích chập trong miền thời gian bằng phép nhân từng phần tử trong miền tần số. Điều này nghĩa là lọc có thể thực hiện bằng cách tích chập trực tiếp tín hiệu với đáp ứng xung của bộ lọc, hoặc nhân các biến đổi Fourier của chúng rồi nghịch biến đổi kết quả. Với các bộ lọc dài, cách tiếp cận miền tần số (dùng FFT) nhanh hơn: \(O(N \log N)\) so với \(O(NM)\).

  • STFT nghịch đảo (inverse STFT — iSTFT) tái tạo một tín hiệu miền thời gian từ biểu diễn STFT của nó. Điều này thiết yếu cho mọi hệ thống sửa đổi âm thanh trong miền tần số (giảm nhiễu, tách nguồn, chuyển đổi giọng nói). Phép tái tạo dùng chồng lấp-cộng:

\[ x[n] = \frac{\sum_{m} w[n - mH] \cdot \text{IDFT}\{X(m, k)\}[n - mH]}{\sum_{m} w[n - mH]^2} \]
  • Mẫu số chuẩn hóa cho sự chồng lấp cửa sổ, đảm bảo tái tạo hoàn hảo khi cửa sổ tổng hợp khớp với cửa sổ phân tích và độ chồng lấp đủ lớn.

  • Tóm tắt chuỗi DSP cho tiếng nói: âm thanh thô được lấy mẫu ở 16 kHz, tiền nhấn, chặt thành các khung 25 ms có cửa sổ Hamming với bước nhảy 10 ms, mỗi khung được biến đổi FFT, qua dải lọc mel, nén log, và hoặc giữ lại thành đặc trưng log-mel (cho các mô hình mạng nơ-ron) hoặc biến đổi DCT để sinh MFCC (cho các mô hình cổ điển). Toàn bộ chuỗi này chuyển đổi một tín hiệu 1D miền thời gian thành một biểu diễn thời gian-tần số 2D phù hợp cho học máy hạ nguồn, sẽ là chủ đề của file 02.

Bài tập lập trình (dùng CoLab hoặc notebook)

  1. Sinh một sóng sine, lấy mẫu nó ở các tốc độ khác nhau, và minh họa aliasing. Vẽ tín hiệu liên tục, phiên bản lấy mẫu đúng, và phiên bản lấy mẫu thưa (bị alias).

    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # Parameters
    f_signal = 5.0  # 5 Hz signal
    duration = 1.0  # 1 second
    
    # "Continuous" signal (very high sample rate)
    t_cont = jnp.linspace(0, duration, 10000)
    x_cont = jnp.sin(2 * jnp.pi * f_signal * t_cont)
    
    # Properly sampled (fs = 50 Hz, well above Nyquist = 10 Hz)
    fs_good = 50
    t_good = jnp.arange(0, duration, 1.0 / fs_good)
    x_good = jnp.sin(2 * jnp.pi * f_signal * t_good)
    
    # Under-sampled (fs = 7 Hz, below Nyquist = 10 Hz) -> aliasing
    fs_bad = 7
    t_bad = jnp.arange(0, duration, 1.0 / fs_bad)
    x_bad = jnp.sin(2 * jnp.pi * f_signal * t_bad)
    
    # The aliased frequency: |f_signal - fs_bad| = |5 - 7| = 2 Hz
    f_alias = abs(f_signal - fs_bad)
    x_alias_cont = jnp.sin(2 * jnp.pi * f_alias * t_cont)
    
    fig, axes = plt.subplots(3, 1, figsize=(12, 9))
    
    # Plot 1: original signal
    axes[0].plot(t_cont, x_cont, color='#3498db', linewidth=1.5, label=f'Original {f_signal} Hz')
    axes[0].set_title(f'Original {f_signal} Hz Signal')
    axes[0].set_xlabel('Time (s)'); axes[0].set_ylabel('Amplitude')
    axes[0].legend(); axes[0].grid(True, alpha=0.3)
    
    # Plot 2: proper sampling
    axes[1].plot(t_cont, x_cont, color='#3498db', linewidth=1, alpha=0.4, label='Original')
    axes[1].stem(t_good, x_good, linefmt='#27ae60', markerfmt='o', basefmt='k-',
                 label=f'Sampled at {fs_good} Hz (above Nyquist)')
    axes[1].set_title(f'Proper Sampling: fs = {fs_good} Hz > 2 x {f_signal} Hz')
    axes[1].set_xlabel('Time (s)'); axes[1].set_ylabel('Amplitude')
    axes[1].legend(); axes[1].grid(True, alpha=0.3)
    
    # Plot 3: aliased sampling
    axes[2].plot(t_cont, x_cont, color='#3498db', linewidth=1, alpha=0.4, label='Original')
    axes[2].stem(t_bad, x_bad, linefmt='#e74c3c', markerfmt='o', basefmt='k-',
                 label=f'Sampled at {fs_bad} Hz (below Nyquist)')
    axes[2].plot(t_cont, x_alias_cont, color='#f39c12', linewidth=1.5, linestyle='--',
                 label=f'Aliased signal appears as {f_alias} Hz')
    axes[2].set_title(f'Aliased Sampling: fs = {fs_bad} Hz < 2 x {f_signal} Hz')
    axes[2].set_xlabel('Time (s)'); axes[2].set_ylabel('Amplitude')
    axes[2].legend(); axes[2].grid(True, alpha=0.3)
    
    plt.tight_layout(); plt.show()
    

  2. Tính và trực quan hóa FFT của một tín hiệu gồm nhiều hàm sin. Hiển thị phổ biên độ và xác định các tần số thành phần.

    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # Create a composite signal: 220 Hz + 440 Hz + 880 Hz (A3 + A4 + A5)
    fs = 8000  # 8 kHz sample rate
    duration = 0.1  # 100 ms
    t = jnp.arange(0, duration, 1.0 / fs)
    n_samples = len(t)
    
    # Three frequency components with different amplitudes
    x = 1.0 * jnp.sin(2 * jnp.pi * 220 * t) + \
        0.6 * jnp.sin(2 * jnp.pi * 440 * t) + \
        0.3 * jnp.sin(2 * jnp.pi * 880 * t)
    
    # Compute FFT
    X = jnp.fft.fft(x)
    freqs = jnp.fft.fftfreq(n_samples, d=1.0 / fs)
    magnitude = jnp.abs(X) / n_samples  # normalise
    
    # Only plot positive frequencies
    pos_mask = freqs >= 0
    freqs_pos = freqs[pos_mask]
    mag_pos = magnitude[pos_mask] * 2  # double to account for negative freq energy
    
    fig, axes = plt.subplots(2, 1, figsize=(12, 7))
    
    # Time domain
    axes[0].plot(t * 1000, x, color='#3498db', linewidth=1)
    axes[0].set_title('Composite Signal: 220 Hz + 440 Hz + 880 Hz')
    axes[0].set_xlabel('Time (ms)'); axes[0].set_ylabel('Amplitude')
    axes[0].grid(True, alpha=0.3)
    
    # Frequency domain
    axes[1].plot(freqs_pos, mag_pos, color='#e74c3c', linewidth=1.5)
    axes[1].set_title('Magnitude Spectrum (FFT)')
    axes[1].set_xlabel('Frequency (Hz)'); axes[1].set_ylabel('Magnitude')
    axes[1].set_xlim(0, 1500)
    # Annotate peaks
    for f_peak, amp in [(220, 1.0), (440, 0.6), (880, 0.3)]:
        axes[1].annotate(f'{f_peak} Hz', xy=(f_peak, amp), fontsize=10,
                         ha='center', va='bottom', color='#9b59b6',
                         arrowprops=dict(arrowstyle='->', color='#9b59b6'))
    axes[1].grid(True, alpha=0.3)
    
    plt.tight_layout(); plt.show()
    

  3. Xây dựng toàn bộ quy trình MFCC từ đầu trong JAX: tiền nhấn, phân khung, cửa sổ hóa, FFT, dải lọc mel, log, DCT. Trực quan hóa dải lọc mel và các MFCC kết quả dưới dạng bản đồ nhiệt.

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # --- Generate a synthetic speech-like signal ---
    key = jax.random.PRNGKey(42)
    fs = 16000
    duration = 1.0
    t = jnp.arange(0, duration, 1.0 / fs)
    
    # Simulate voiced speech: fundamental + harmonics with amplitude decay
    f0 = 150.0  # fundamental frequency
    x = sum(jnp.sin(2 * jnp.pi * f0 * k * t) / k for k in range(1, 8))
    # Add some noise
    x = x + 0.1 * jax.random.normal(key, t.shape)
    x = x / jnp.max(jnp.abs(x))  # normalise
    
    # --- Step 1: Pre-emphasis ---
    alpha = 0.97
    x_pre = jnp.concatenate([x[:1], x[1:] - alpha * x[:-1]])
    
    # --- Step 2: Framing ---
    frame_len = int(0.025 * fs)   # 25 ms = 400 samples
    hop_len = int(0.010 * fs)     # 10 ms = 160 samples
    n_frames = (len(x_pre) - frame_len) // hop_len + 1
    frames = jnp.stack([x_pre[i * hop_len : i * hop_len + frame_len]
                         for i in range(n_frames)])
    
    # --- Step 3: Hamming window ---
    hamming = 0.54 - 0.46 * jnp.cos(2 * jnp.pi * jnp.arange(frame_len) / (frame_len - 1))
    windowed = frames * hamming
    
    # --- Step 4: FFT ---
    n_fft = 512
    spectra = jnp.fft.rfft(windowed, n=n_fft)
    power_spectra = jnp.abs(spectra) ** 2 / n_fft
    
    # --- Step 5: Mel filterbank ---
    n_mels = 40
    f_min, f_max = 0.0, fs / 2.0
    
    def hz_to_mel(f):
        return 2595 * jnp.log10(1 + f / 700)
    
    def mel_to_hz(m):
        return 700 * (10 ** (m / 2595) - 1)
    
    mel_min = hz_to_mel(f_min)
    mel_max = hz_to_mel(f_max)
    mel_points = jnp.linspace(mel_min, mel_max, n_mels + 2)
    hz_points = mel_to_hz(mel_points)
    
    freq_bins = jnp.floor((n_fft + 1) * hz_points / fs).astype(jnp.int32)
    n_freqs = n_fft // 2 + 1
    filterbank = jnp.zeros((n_mels, n_freqs))
    
    for m in range(n_mels):
        f_left = freq_bins[m]
        f_center = freq_bins[m + 1]
        f_right = freq_bins[m + 2]
        # Rising slope
        for k in range(int(f_left), int(f_center)):
            if f_center != f_left:
                filterbank = filterbank.at[m, k].set((k - f_left) / (f_center - f_left))
        # Falling slope
        for k in range(int(f_center), int(f_right)):
            if f_right != f_center:
                filterbank = filterbank.at[m, k].set((f_right - k) / (f_right - f_center))
    
    # Apply filterbank
    mel_spectra = jnp.dot(power_spectra, filterbank.T)
    
    # --- Step 6: Log ---
    log_mel = jnp.log(mel_spectra + 1e-10)
    
    # --- Step 7: DCT (type-II) ---
    n_mfcc = 13
    n_mel_channels = log_mel.shape[1]
    dct_matrix = jnp.zeros((n_mfcc, n_mel_channels))
    for i in range(n_mfcc):
        for j in range(n_mel_channels):
            dct_matrix = dct_matrix.at[i, j].set(
                jnp.cos(jnp.pi * i * (j + 0.5) / n_mel_channels)
            )
    mfccs = jnp.dot(log_mel, dct_matrix.T)
    
    # --- Visualisation ---
    fig, axes = plt.subplots(3, 1, figsize=(14, 11))
    
    # Mel filterbank
    freq_axis = jnp.linspace(0, fs / 2, n_freqs)
    for m in range(n_mels):
        color = '#3498db' if m % 2 == 0 else '#e74c3c'
        axes[0].plot(freq_axis, filterbank[m], color=color, alpha=0.6, linewidth=0.8)
    axes[0].set_title(f'Mel Filterbank ({n_mels} filters)')
    axes[0].set_xlabel('Frequency (Hz)'); axes[0].set_ylabel('Weight')
    axes[0].grid(True, alpha=0.3)
    
    # Log-mel spectrogram
    im1 = axes[1].imshow(log_mel.T, aspect='auto', origin='lower',
                          extent=[0, duration, 0, n_mels], cmap='viridis')
    axes[1].set_title('Log-Mel Spectrogram')
    axes[1].set_xlabel('Time (s)'); axes[1].set_ylabel('Mel Band')
    plt.colorbar(im1, ax=axes[1], label='Log Energy')
    
    # MFCCs
    im2 = axes[2].imshow(mfccs.T, aspect='auto', origin='lower',
                          extent=[0, duration, 0, n_mfcc], cmap='coolwarm')
    axes[2].set_title(f'MFCCs (first {n_mfcc} coefficients)')
    axes[2].set_xlabel('Time (s)'); axes[2].set_ylabel('MFCC Index')
    plt.colorbar(im2, ax=axes[2], label='Coefficient Value')
    
    plt.tight_layout(); plt.show()
    

  4. Cài đặt các bộ lọc FIR thông thấp và thông cao, và trực quan hóa tác động của chúng lên một tín hiệu chứa cả thành phần tần số thấp và cao. Hiển thị cả góc nhìn miền thời gian và miền tần số.

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # Create a signal with low (100 Hz) and high (2000 Hz) components
    fs = 8000
    duration = 0.05  # 50 ms for clear visualisation
    t = jnp.arange(0, duration, 1.0 / fs)
    
    x_low = jnp.sin(2 * jnp.pi * 100 * t)
    x_high = 0.5 * jnp.sin(2 * jnp.pi * 2000 * t)
    x = x_low + x_high
    
    # Design a simple FIR low-pass filter using windowed sinc
    def fir_lowpass(cutoff_hz, fs, n_taps=51):
        """Design FIR low-pass filter using windowed sinc method."""
        fc = cutoff_hz / fs  # normalised cutoff
        n = jnp.arange(n_taps)
        mid = (n_taps - 1) / 2.0
        # Sinc function (ideal low-pass impulse response)
        h = jnp.where(n == mid, 2 * fc,
                      jnp.sin(2 * jnp.pi * fc * (n - mid)) / (jnp.pi * (n - mid)))
        # Apply Hamming window
        window = 0.54 - 0.46 * jnp.cos(2 * jnp.pi * n / (n_taps - 1))
        h = h * window
        h = h / jnp.sum(h)  # normalise to unity gain at DC
        return h
    
    def apply_filter(x, h):
        """Apply FIR filter via convolution."""
        return jnp.convolve(x, h, mode='same')
    
    # Low-pass filter at 500 Hz (passes 100 Hz, blocks 2000 Hz)
    h_lp = fir_lowpass(500, fs, n_taps=51)
    x_lp = apply_filter(x, h_lp)
    
    # High-pass = delta - low-pass (spectral inversion)
    delta = jnp.zeros(51)
    delta = delta.at[25].set(1.0)
    h_hp = delta - h_lp
    x_hp = apply_filter(x, h_hp)
    
    # Compute spectra for all signals
    def compute_spectrum(signal, fs):
        X = jnp.fft.rfft(signal)
        freqs = jnp.fft.rfftfreq(len(signal), d=1.0 / fs)
        mag = jnp.abs(X) / len(signal) * 2
        return freqs, mag
    
    fig, axes = plt.subplots(3, 2, figsize=(14, 10))
    
    # Time domain plots
    for i, (sig, title, color) in enumerate([
        (x, 'Original (100 Hz + 2000 Hz)', '#3498db'),
        (x_lp, 'Low-pass filtered (< 500 Hz)', '#27ae60'),
        (x_hp, 'High-pass filtered (> 500 Hz)', '#e74c3c')
    ]):
        axes[i, 0].plot(t * 1000, sig[:len(t)], color=color, linewidth=1)
        axes[i, 0].set_title(f'Time Domain: {title}')
        axes[i, 0].set_xlabel('Time (ms)'); axes[i, 0].set_ylabel('Amplitude')
        axes[i, 0].grid(True, alpha=0.3)
    
    # Frequency domain plots
    for i, (sig, title, color) in enumerate([
        (x, 'Original', '#3498db'),
        (x_lp, 'Low-pass', '#27ae60'),
        (x_hp, 'High-pass', '#e74c3c')
    ]):
        freqs, mag = compute_spectrum(sig, fs)
        axes[i, 1].plot(freqs, mag, color=color, linewidth=1.5)
        axes[i, 1].set_title(f'Spectrum: {title}')
        axes[i, 1].set_xlabel('Frequency (Hz)'); axes[i, 1].set_ylabel('Magnitude')
        axes[i, 1].set_xlim(0, 3000)
        axes[i, 1].axvline(x=500, color='#f39c12', linestyle='--', alpha=0.7,
                            label='Cutoff (500 Hz)')
        axes[i, 1].legend(); axes[i, 1].grid(True, alpha=0.3)
    
    plt.tight_layout(); plt.show()