Mở đầu: một phép toán, biết trọn vẹn một cỗ máy

Tưởng tượng bạn vỗ tay MỘT LẦN trong một nhà thờ lớn — âm thanh dội lại kéo dài vài giây, đầy tiếng vang đặc trưng của không gian đó. Điều đáng kinh ngạc: chỉ với đúng MỘT tiếng vỗ tay đó (ghi âm lại tiếng vang), bạn có đủ thông tin để mô phỏng NHÀ THỜ ĐÓ tác động lên BẤT KỲ âm thanh nào khác — giọng nói, tiếng đàn guitar, bất cứ thứ gì — mà không cần quay lại nhà thờ lần nữa. Đây không phải phép màu; đó là hệ quả của một định lý toán học đẹp: với một lớp hệ thống đặc biệt gọi là LTI, đúng MỘT phép đo (đáp ứng với một xung duy nhất) mô tả TRỌN VẸN cách hệ đó phản ứng với MỌI tín hiệu đầu vào có thể.

Bài này xây dựng công cụ toán học đứng sau phép màu đó — tích chập (convolution) — từ nguyên lý xếp chồng cơ bản nhất, làm tay trên 4 số trước khi tổng quát hoá, rồi dùng chính công cụ đó để tạo hiệu ứng reverb thật: chập một tiếng vỗ tay tổng hợp với đáp ứng xung của một "căn phòng" toán học, và nghe âm thanh "bước vào" không gian đó.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 1 (xung đơn vị $\delta[n]$ — viên gạch xây nên toàn bộ bài này).

1. Hệ & hai tính chất vàng

Một "hệ" (system) trong DSP đơn giản là bất kỳ thứ gì nhận tín hiệu $x[n]$ vào và trả ra tín hiệu $y[n]$. Hai tính chất đặc biệt biến một hệ thành LTI (Linear Time-Invariant — tuyến tính bất biến thời gian):

  • Tuyến tính: scale đầu vào lên $a$ lần thì đầu ra cũng scale lên $a$ lần; cộng 2 đầu vào thì đầu ra là tổng 2 đầu ra riêng lẻ (nguyên lý xếp chồng — superposition).
  • Bất biến thời gian: hệ xử lý một tín hiệu giống hệt nhau bất kể tín hiệu đó đưa vào SỚM hay MUỘN — "hôm nay xử lý giống hệt hôm qua".

Một bài kiểm tra nhanh để cảm nhận trực giác — hệ nào LTI, hệ nào không:

Hệ thống LTI? Vì sao
Echo/vang (cộng bản trễ, scale) ✅ Có Chỉ cộng và scale — thoả cả 2 tính chất.
Distortion guitar (bóp méo phi tuyến) ❌ Không Phi tuyến — tăng gấp đôi đầu vào KHÔNG cho đầu ra gấp đôi (đã méo khác đi).
AGC (tự động điều chỉnh gain theo thời gian) ❌ Không Biến thời gian — cùng tín hiệu đưa vào lúc khác cho gain khác nhau.

Lý do LTI quan trọng đến vậy: đây là lớp hệ DUY NHẤT ta có công cụ phân tích TRỌN VẸN — không cần thử mọi tín hiệu đầu vào có thể, chỉ cần MỘT phép đo.

2. Đáp ứng xung h[n]

Đưa chính viên gạch $\delta[n]$ (xung đơn vị, Bài 1) vào một hệ LTI, xem nó trả lời gì — kết quả gọi là đáp ứng xung $h[n]$. Định lý trung tâm của toàn bộ lý thuyết hệ thống số: $h[n]$ đặc trưng trọn vẹn một hệ LTI — biết $h$ là biết TẤT CẢ cách hệ đó phản ứng với MỌI tín hiệu đầu vào, không có ngoại lệ, không cần biết thêm gì khác về cấu tạo bên trong hệ.

3. Tích chập

Công thức tích chập:

$$y[n] = \sum_{k} x[k]\,h[n-k]$$

Xây trực giác từ nguyên lý xếp chồng: mọi tín hiệu $x[n]$ có thể viết thành tổng của nhiều xung đơn vị đã scale và dịch ($x[n] = \sum_k x[k]\,\delta[n-k]$). Vì hệ là TUYẾN TÍNH, đầu ra là tổng các phản hồi riêng lẻ; vì hệ BẤT BIẾN THỜI GIAN, phản hồi với $x[k]\,\delta[n-k]$ chính là $x[k]\,h[n-k]$ (đáp ứng xung, scale theo $x[k]$, dịch theo $k$). Cộng tất cả các bản sao đó lại — chính là công thức tích chập.

Thuật toán thực hành: lật-dịch-nhân-cộng. Làm tay trên ví dụ đơn giản $x = [1, 2, 3]$, $h = [1, 1]$ (verify từng bước bằng convolve() tự viết):

tich_chap_lam_tay.txt
x = [1, 2, 3],  h = [1, 1]

y[0] = x[0]*h[0]                    = 1*1             = 1
y[1] = x[0]*h[1] + x[1]*h[0]        = 1*1 + 2*1       = 3
y[2] = x[1]*h[1] + x[2]*h[0]        = 2*1 + 3*1       = 5
y[3] = x[2]*h[1]                    = 3*1             = 3

y = [1, 3, 5, 3]   <- verify: convolve([1,2,3],[1,1]) tra ve DUNG day nay

Độ phức tạp thuật toán trực tiếp là $O(N \cdot M)$ — với $N$ mẫu tín hiệu và $M$ mẫu đáp ứng xung, mỗi mẫu $x[n]$ cần nhân với toàn bộ $h$. Bài 6 (FFT) sẽ gieo mầm một thuật toán nhanh hơn nhiều cho tích chập dài.

4. Tính chất & hệ quả kiến trúc

Tích chập có 3 tính chất đại số quan trọng — giao hoán ($x*h = h*x$), kết hợp, và phân phối. Verify giao hoán: convolve([1,2,3],[1,1])convolve([1,1],[1,2,3]) cho ra CHÍNH XÁC cùng một dãy số.

Tính kết hợp mang một hệ quả kiến trúc cực kỳ thực dụng: ghép nối tiếp 2 bộ lọc LTI (tín hiệu qua bộ lọc 1 rồi qua bộ lọc 2) tương đương với việc chập tín hiệu gốc với TÍCH CHẬP CỦA 2 ĐÁP ỨNG XUNG đó gộp lại thành một bộ lọc DUY NHẤT. Verify bằng số thật: chập từng bước convolve(convolve(tín_hiệu, h1), h2) và chập một lần convolve(tín_hiệu, convolve(h1, h2)) cho ra kết quả khớp nhau với sai số chỉ khoảng $10^{-16}$ (sai số làm tròn số dấu phẩy động, về bản chất toán học là BẰNG NHAU TUYỆT ĐỐI). Ghép song song (cộng 2 đầu ra) tương đương cộng trực tiếp 2 đáp ứng xung lại với nhau.

⚠️ Pitfall: độ dài kết quả luôn là N+M-1
Verify: $x$ có 3 mẫu, $h$ có 2 mẫu — kết quả tích chập luôn có đúng 3+2-1 = 4 mẫu, KHÔNG PHẢI 3 hay bất kỳ độ dài nào khác. Đây là lỗi cấp phát bộ nhớ kinh điển trong lập trình DSP thực tế: cấp mảng kết quả chỉ bằng độ dài tín hiệu gốc sẽ CẮT CỤT phần đuôi — với reverb, đó chính xác là phần "tiếng vang kéo dài" bị mất, hậu quả nghe được rõ ràng nhất trong mọi lỗi tích chập.

5. Thực hành: Convolution Stepper & reverb thật

Demo dưới đây chạy đúng thuật toán lật-dịch-nhân-cộng từng bước, và một hiệu ứng reverb thật: chập một tiếng vỗ tay tổng hợp với đáp ứng xung "phòng" cũng tổng hợp bằng synthesizeRoomIR():

🌊 Convolution Stepper & Reverb — DSPJS thật

1. Convolution Stepper — x=[1,2,3,2,1], h=[0,5,1,0,3]

Đang tải…

2. Reverb thật — tiếng vỗ tay + đáp ứng xung phòng

Đường xanh dương là tiếng vỗ tay gốc (rất ngắn, tắt nhanh); đường cam là kết quả sau khi chập với đáp ứng xung phòng (dài hơn HẲN, đúng độ dài N+M-1, kéo dài đúng như "tiếng vang" thật).

convolution_demo.js (đúng logic đang chạy ở tab Xem trước)
import { convolve, synthesizeRoomIR, whiteNoise } from './dsp-core.js';

console.log(convolve([1, 2, 3], [1, 1])); // [1, 3, 5, 3] - verify lam tay Muc 3

// Tiec vo tay tong hop: nhieu trang tat dan RAT NHANH (~15ms)
const SAMPLE_RATE = 8000;
const clap = Array.from({length: 120}, (_, n) => whiteNoise(n, 1, 9) * Math.exp(-n / 15));

// Dap ung xung "phong" tong hop: nhieu trang tat dan CHAM HON (0.4s)
const roomIR = synthesizeRoomIR(0.4, SAMPLE_RATE, 0.15, 5);

// Reverb = chap tieng vo tay voi dap ung xung phong
const wet = convolve(clap, roomIR);
console.log('do dai clap:', clap.length, '- do dai IR:', roomIR.length, '- do dai wet:', wet.length);
// wet.length = clap.length + roomIR.length - 1 (dung Muc 4 pitfall)

const ctx = new AudioContext();
const buffer = ctx.createBuffer(1, wet.length, SAMPLE_RATE);
buffer.copyToChannel(new Float32Array(wet), 0);
const src = ctx.createBufferSource();
src.buffer = buffer;
src.connect(ctx.destination);
src.start();

Tóm lược

  • ✅ Hệ LTI (tuyến tính + bất biến thời gian) là lớp hệ DUY NHẤT phân tích trọn vẹn được bằng đáp ứng xung $h[n]$ — biết $h$ là biết TẤT CẢ.
  • ✅ Tích chập $y[n] = \sum_k x[k]h[n-k]$ xây từ nguyên lý xếp chồng — verify tay: convolve([1,2,3],[1,1]) = [1,3,5,3], độ dài luôn $N+M-1$.
  • ✅ Tính giao hoán và kết hợp verified: ghép nối tiếp 2 bộ lọc = chập với tích chập của 2 đáp ứng xung (sai số chỉ $10^{-16}$, về bản chất bằng nhau tuyệt đối).
  • ✅ Pitfall: cấp mảng kết quả thiếu (không đủ $N+M-1$) cắt cụt đuôi tín hiệu — với reverb, đó là phần tiếng vang kéo dài bị mất.

Trắc nghiệm ôn tập

Câu 1

Vì sao một bộ distortion guitar (bóp méo phi tuyến) KHÔNG phải hệ LTI?

Câu 2

Vì sao đáp ứng xung $h[n]$ của một hệ LTI lại "đặc trưng trọn vẹn" hệ đó — biết $h$ là biết được cách hệ phản ứng với MỌI tín hiệu đầu vào?

Câu 3

Verified: ghép nối tiếp 2 bộ lọc LTI (h1 rồi h2) cho kết quả khớp với việc chập tín hiệu gốc với TÍCH CHẬP của h1 và h2 (sai số chỉ ~$10^{-16}$). Hệ quả kiến trúc thực tế của tính chất này là gì?

Câu 4

Verified: convolve của một tín hiệu 3 mẫu với một đáp ứng xung 2 mẫu luôn cho ra đúng 4 mẫu (N+M-1). Nếu lập trình viên cấp phát mảng kết quả chỉ có 3 mẫu (bằng độ dài tín hiệu gốc), hậu quả gì xảy ra trong một demo reverb thật?

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 4 vừa thêm convolvesynthesizeRoomIR, 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 3: Lượng tử hoá & dải động Bài 5: DFT: cửa sổ nhìn sang miền tần số Quay lại Lộ trình Series Xử Lý Tín Hiệu Số

Bình luận