1. Máy tính thực chất đọc file âm thanh như thế nào?
Khi bạn mở một đoạn ghi âm lên nghe, thứ bạn cảm nhận là âm thanh. Nhưng đối với máy tính, một file audio (như .wav hay .mp3) thực chất chỉ là một mảng (array) chứa rất nhiều con số.
Âm thanh ngoài đời thực là những làn sóng liên tục. Nhưng vì RAM và ổ cứng có giới hạn, máy tính phải áp dụng cơ chế lấy mẫu (sampling) để chuyển sóng âm thành các con số rời rạc. Cứ mỗi giây trôi qua, máy tính sẽ đo độ lớn âm thanh hàng chục ngàn lần và lưu thành số. Ví dụ, mức lấy mẫu 44.100 Hz nghĩa là 1 giây âm thanh sẽ tương ứng với một mảng chứa đúng 44.100 con số.
Trong thực tế, file ghi âm rất hay bị lẫn tạp âm như tiếng xì xèo đài radio hay tiếng quạt máy. Micro thu tất cả cùng lúc và trộn lẫn giọng người lẫn tiếng ồn vào chung một mảng số.
Vấn đề là nếu chỉ nhìn mảng số này theo chiều thời gian (Time-domain), chúng ta không thể viết code kiểu if/else để xóa tạp âm được. Tại bất kỳ mốc thời gian nào, giọng nói và tiếng ồn đã bị cộng dồn thành một con số duy nhất. Để giải quyết sự "bất lực" này, chúng ta phải tìm đến một mảng kiến thức lớn hơn: Xử lý tín hiệu số (DSP).
2. Xử lý tín hiệu số (DSP) & "Chiếc lăng kính" FFT
Xử lý tín hiệu số (DSP - Digital Signal Processing) thực chất là tập hợp các kỹ thuật toán học giúp chúng ta biến đổi, phân tích hoặc lọc các mảng dữ liệu số. Nó chính là bộ não đằng sau tính năng chống ồn của AirPods, bộ lọc giọng trên Discord hay thuật toán nhận diện bài hát Shazam.
Trong DSP, nếu một bài toán quá khó để giải ở Miền thời gian (Time-domain), cách duy nhất là chuyển nó sang Miền tần số (Frequency-domain).
Tại sao phải chuyển sang miền tần số?
Một nguyên lý nền tảng của lý thuyết tín hiệu là: Mọi tín hiệu phức tạp trong tự nhiên đều có thể được cấu tạo bằng cách cộng nhiều sóng đơn lẻ ở các tần số khác nhau lại với nhau.
- Ở miền thời gian: Mảng dữ liệu chỉ thể hiện biên độ (độ lớn) tại từng mốc thời điểm ...tại từng mốc thời điểm t1, t2, t3... Mọi thành phần cấu thành nên tín hiệu đều bị nén chặt thành một con số duy nhất tại mỗi mốc thời gian.
- Ở miền tần số: Mảng dữ liệu được bóc tách theo trục tần số (Hz). Lúc này, dữ liệu thể hiện rõ tín hiệu đang chứa những tần số nào và cường độ của từng tần số đó là bao nhiêu.
Tác dụng của việc chuyển đổi này là khả năng tách biệt dữ liệu. Những thành phần vốn bị trộn lẫn không thể gỡ ra ở miền thời gian sẽ nằm ở các vị trí hoàn toàn độc lập trên trục tần số. Việc lọc bớt một thành phần không mong muốn lúc này chỉ đơn giản là đặt giá trị tại dải tần đó về 0.
Từ lý thuyết Fourier đến thuật toán FFT
Để làm được cú chuyển đổi kỳ diệu này, các nhà toán học đã nghĩ ra Biến đổi Fourier (Fourier Transform).
Ý tưởng cốt lõi là: Mọi tín hiệu phức tạp, dù trông ngoằn ngoèo hỗn loạn đến đâu ở miền thời gian, đều chỉ là tổng của nhiều sóng sin (và cosine) đơn giản ghép lại.
Giống như một hợp âm phức tạp được tạo thành từ nhiều nốt nhạc riêng lẻ, mảng dữ liệu audio của bạn thực chất là tập hợp của hàng ngàn sóng sin dao động ở các tần số khác nhau. Phép biến đổi Fourier đóng vai trò "bóc tách" hợp âm đó, giúp bạn biết chính xác:
- Tín hiệu đang chứa những tần số nào?
- Mỗi tần số đó mạnh hay yếu (biên độ bao nhiêu)?
Toán học lý thuyết dùng tích phân cho tín hiệu liên tục. Còn trên máy tính, vì dữ liệu là mảng số rời rạc, ta phải dùng Biến đổi Fourier rời rạc (DFT - Discrete Fourier Transform).
Vấn đề là cách tính DFT thuần túy tương đương với việc chạy 2 vòng lặp for lồng nhau để nhân bản mảng dữ liệu. Với mảng N phần tử, máy tính phải tốn O(N²) phép tính.
Một file audio 44.1 kHz chỉ kéo dài 10 giây đã chứa hơn 440.000 phần tử. Nếu chạy độ phức tạp O(N²), máy tính phải thực hiện gần 200 tỷ phép tính — CPU sẽ bị nghẽn ngay lập tức và không bao giờ xử lý nổi theo thời gian thực.
Năm 1965, John Wilder Tukey (cùng với James Cooley) công bố thuật toán Fast Fourier Transform (FFT). Bằng cách áp dụng tư tưởng "Chia để trị" (Divide and Conquer), FFT phân rã mảng dữ liệu lớn thành các mảng con nhỏ hơn để tính toán tối ưu.
FFT giảm độ phức tạp từ O(N²) xuống còn O(N log N).
Sự chênh lệch này khủng khiếp ở chỗ: Với một mảng 1 triệu phần tử, DFT thuần túy mất 1.000.000.000.000 (1 ngàn tỷ) phép tính, trong khi FFT chỉ mất khoảng 20.000.000 phép tính — nhanh hơn 50.000 lần.
Chính phát minh này của John Tukey đã đưa phép biến đổi Fourier từ lý thuyết nằm trên giấy trở thành công cụ chạy thực tế trên mọi thiết bị số ngày nay. Và trong bài toán của chúng ta, FFT chính là chiếc lăng kính giúp bóc tách dải tiếng rít ra khỏi giọng nói.
3. Thực hành: Xây dựng bộ lọc nhiễu bằng Python
Đặt bài toán thực tế
Giả sử bạn có một file âm thanh radio_sample.wav. Đoạn ghi âm này chứa giọng nói con người, nhưng bị lẫn một tiếng rít kéo dài do nhiễu sóng tín hiệu.
- Đặc tính giọng nói: Thường nằm ở dải tần số thấp và trung (khoảng dưới 1000 Hz).
- Đặc tính tiếng rít: Thường là dải nhiễu tần số cao (khoảng trên 1000 Hz).
Mục tiêu của chúng ta: Loại bỏ dải tần số cao chứa tiếng rít mà vẫn giữ nguyên dải tần số thấp chứa giọng nói.
Luồng xử lý 3 bước (Pipeline)
Để giải quyết bài toán trên, chương trình Python sẽ chạy theo quy trình:
- Chuyển đổi (FFT): Đọc file audio từ miền thời gian và dùng
rfftđể chuyển mảng số sang miền tần số. - Lọc dữ liệu (Low-Pass Filter): Tìm các vị trí tần số lớn hơn 1000 Hz trên mảng dữ liệu và gán giá trị của chúng về
0. - Tái tạo (IFFT): Dùng
irfftđể chuyển mảng dữ liệu đã lọc sạch từ miền tần số quay trở lại miền thời gian, sau đó xuất ra file.wavmới.
Code thực thi bằng Python
Chúng ta sẽ sử dụng hai thư viện standard trong xử lý dữ liệu: numpy (thao tác mảng và tính FFT) cùng scipy (đọc/ghi file âm thanh).
import numpy as np
from scipy.io import wavfile
# Bước 1: Đọc file âm thanh đầu vào
sample_rate, data = wavfile.read('radio_sample.wav')
# Chuyển kênh Stereo (2 kênh) về Mono (1 kênh) nếu cần để làm việc với mảng 1 chiều
if len(data.shape) > 1:
data = data.mean(axis=1)
# Bước 2: Chuyển mảng dữ liệu sang miền tần số bằng FFT
# Dùng rfft (Real FFT) vì tín hiệu âm thanh đầu vào là tập hợp các số thực
fft_spectrum = np.fft.rfft(data)
frequencies = np.fft.rfftfreq(len(data), d=1/sample_rate)
# Bước 3: Áp dụng bộ lọc thông thấp (Low-Pass Filter)
# Gán toàn bộ dải tần số trên 1000 Hz (vùng chứa tiếng rít) về 0
cutoff_frequency = 1000
fft_spectrum[frequencies > cutoff_frequency] = 0
# Bước 4: Chuyển dữ liệu đã làm sạch ngược về miền thời gian bằng IFFT
filtered_data = np.fft.irfft(fft_spectrum)
# Ép kiểu dữ liệu về int16 để tương thích với định dạng file audio gốc
filtered_data = np.int16(filtered_data)
# Bước 5: Ghi kết quả ra file âm thanh mới
wavfile.write('radio_cleaned.wav', sample_rate, filtered_data)
print("Đã lọc nhiễu thành công!")
Bản chất các hàm được sử dụng
np.fft.rfft: Chuyển mảng biên độ theo thời gian thành mảng số phức biểu diễn biên độ và pha trên miền tần số.np.fft.rfftfreq: Tạo ra mảng chứa giá trị các tần số tương ứng (tính bằng Hz) để chúng ta biết chính xác chỉ số mảng nào ứng với tần số nào.fft_spectrum[frequencies > cutoff_frequency] = 0: Dòng lệnh can thiệp cốt lõi. Nhờ dữ liệu đã được phân tách trên miền tần số, ta chỉ cần dùng điều kiện mảng đơn giản để cắt bỏ hoàn toàn dải nhiễu.np.fft.irfft: Biến đổi FFT ngược (Inverse FFT), đóng vai trò tổng hợp lại sóng âm từ các dải tần còn giữ lại, đưa mảng dữ liệu về dạng biên độ thời gian để loa có thể phát ra âm thanh.
4. Hạn chế thực tế và Hướng phát triển
Cách lọc nhiễu bằng bộ lọc thông thấp (Low-pass filter) thô sơ ở trên hoạt động rất tốt với các file ghi âm bị dính tiếng rít tần số cao. Tuy nhiên, trong môi trường thực tế, bài toán xử lý âm thanh sẽ gặp hai thách thức lớn:
1. Tạp âm đè lên dải tần giọng nói
Nếu tiếng ồn không nằm ở tần số cao mà là tiếng quạt máy, tiếng còi xe hay tiếng nói chuyện rì rầm xung quanh (vốn nằm cùng dải tần từ 300 Hz đến 3000 Hz với giọng người), việc gán dải tần về 0 sẽ làm mất luôn giọng nói.
- Giải pháp: Lúc này ta không thể cắt tần số cố định, mà phải dùng các thuật toán như Trừ phổ (Spectral Subtraction) hay Bộ lọc Wiener (Wiener Filter) để ước lượng mô hình tiếng ồn và trừ đi.
2. Hiện tượng méo tiếng do cắt gọt đột ngột (Gibbs Phenomenon)
Việc gán lẹm toàn bộ giá trị tần số trên 1000 Hz về đúng số 0 (gọi là bộ lọc "bức tường gạch" - Brick-wall filter) sẽ tạo ra các biến đổi biên độ quá gắt. Điều này gây ra hiện tượng méo tiếng nhẹ hoặc có tiếng vang lạ (ringing artifact) ở ranh giới dải cắt.
- Giải pháp: Trong thực tế, người ta sử dụng các bộ lọc mượt hơn như Butterworth Filter hoặc nhân mảng dữ liệu với các hàm cửa sổ (Window Functions như Hann, Hamming) để hạ dần tần số về 0 một cách êm ái.
Lời kết
Xử lý tín hiệu số (DSP) nghe qua có vẻ đầy toán học và phức tạp, nhưng về bản chất lập trình, nó chỉ xoay quanh một tư duy: Chuyển đổi góc nhìn mảng dữ liệu sang miền không gian phù hợp để bài toán trở nên dễ giải nhất.
Nguồn tham khảo & Đọc thêm
- Cooley, J. W., & Tukey, J. W. (1965): An algorithm for the machine calculation of complex Fourier series — Bài báo lịch sử công bố thuật toán FFT.
- SciPy Documentation: Fourier Transforms (
scipy.fft/numpy.fft) — Tài liệu chính thức của Python về các hàm FFT.
