Mở đầu: 14 khối rời, giờ ghép thành 1 dây chuyền

14 bài trước xây từng KHỐI riêng lẻ — cửa sổ, FFT, FIR, biquad, resampler, tuner, Q15. Bài capstone này KHÔNG dạy thuật toán DSP mới — nó dạy KIẾN TRÚC: cách ghép nhiều khối xử lý thành một dây chuyền (pipeline) hoàn chỉnh, đúng cách mọi phần mềm âm thanh thật (DAW, plugin, loa thông minh) tổ chức code. Trạm Âm Thanh DSP hôm nay: noise gate (Bài 11 + khái niệm FSM) → EQ 5 băng (biquad cascade, Bài 11) → soft-limiter (đối chứng Bài 3) → loa, với đồng hồ đo mức đúng chuẩn kỹ thuật.


📚 Điều kiện tiên quyết
Tổng hợp Bài 1–14 — dùng đậm nhất: Bài 8 (khung/block), Bài 10 (nhân H(z)), Bài 11 (biquad/cascade), Bài 13 (xử lý realtime theo khung).

1. Kiến trúc DSP Station

Đồ thị xử lý: nguồn (mic/nhạc) → noise gateEQ 5 băngsoft-limiter → loa, kèm 1 nhánh PHÂN TÍCH chạy song song (không nằm trên đường tín hiệu chính) tái dùng spectrogram của Bài 8 và tuner của Bài 13. Kiến trúc code: MỖI khối là một hàm/đối tượng nhận process(block) — nhận 1 mảng mẫu, trả về 1 mảng mẫu đã xử lý — để dây chuyền có thể GHÉP bất kỳ thứ tự nào, bật/tắt từng khối độc lập mà không phá vỡ khối khác:

dsp_station_architecture.js
import { noiseGate, cascadeBiquads, softClip } from './dsp-core.js';

// Moi khoi la 1 HAM process(block) -> block moi - de GHEP, de BAT/TAT rieng
function buildChain({ gateOn, eqOn, limiterOn, eqBands, fs }) {
  return function process(block) {
    let y = block;
    if (gateOn) y = noiseGate(y, fs, -35, 5, 150, 6);
    if (eqOn) y = cascadeBiquads(y, eqBands);
    if (limiterOn) y = y.map((v) => softClip(v, 0.9));
    return y;
  };
}
// Verified: cascadeBiquads() khop TUYET DOI voi goi biquadDF2T() lien tiep
// thu cong (Bai 11) - EQ 5 bang chi la 5 lan goi noi tiep, khong gi moi.

2. EQ 5 băng từ biquad cascade

EQ 5 băng chuẩn: low-shelf (điều khiển "đáy"), 3× peaking (điều khiển từng vùng tần số giữa), high-shelf (điều khiển "đỉnh") — mỗi băng là MỘT biquad từ RBJ cookbook (Bài 11), nối tiếp bằng cascadeBiquads(). Đáp ứng tần số TỔNG của cả chuỗi = TÍCH các $H(z)$ từng băng (đọc theo hình học của Bài 10) — hay cộng dồn theo dB, vì $20\log|H_1 H_2| = 20\log|H_1| + 20\log|H_2|$.

Verified bằng số thật: preset bass-boost (low-shelf +6dB @120Hz) cho tổng khuếch đại đúng +5,73dB tại 80Hz nhưng gần như 0dB tại 1kHz — đúng ý nghĩa "shelf": nâng MỘT VÙNG, giữ nguyên phần còn lại. Mỗi preset (bass-boost/vocal/hall) chỉ là MỘT bộ hệ số $(f_0, Q, gainDb)$ khác nhau cho 5 băng — không cần đổi code, chỉ đổi THAM SỐ.

Preset Low-shelf 3× Peaking High-shelf Vì sao hình dạng này
Bass-boost +6dB @120Hz 0dB 0dB Chỉ nâng đáy — nhạc điện tử/hip-hop cần lực trầm mà không đục âm
Vocal -3dB @150Hz (cắt bùn) +4dB @3kHz (presence) +2dB @8kHz (độ sáng) Cắt vùng "bùn" thấp, nâng vùng "hiện diện" giọng hát dễ nghe rõ lời
Hall +1dB @100Hz -2dB @1kHz (nhẹ) -3dB @10kHz Mô phỏng cách phòng lớn hấp thụ tần số cao, dội nhẹ tần số thấp

3. Noise gate: envelope follower & FSM hysteresis

Noise gate quyết định "im lặng thật" (nên câm) hay "có tín hiệu" (nên mở) bằng cách bám biên độ qua envelope follower (bộ lọc 1 cực bất đối xứng attack/release), rồi so với ngưỡng qua một FSM 2 trạng thái có hysteresis: mở khi vượt ngưỡng mở, chỉ đóng khi tụt XUỐNG DƯỚI một ngưỡng đóng THẤP HƠN — tránh "nhấp nháy" mở/đóng liên tục quanh một ngưỡng duy nhất (đúng tinh thần FSM chống dội đã học ở phần cứng nhúng).

noise_gate_fsm.js (trích engine dsp-core.js)
function noiseGate(x, fs, thresholdDb, attackMs, releaseMs, hysteresisDb) {
  const decisionEnv = envelopeFollower(x, fs, 0.5, 5); // envelope QUYET DINH, rieng biet
  const openThresh = thresholdDb, closeThresh = thresholdDb - hysteresisDb;
  let isOpen = false;
  const targetGain = new Array(x.length);
  for (let n = 0; n < x.length; n++) {
    const envDb = amplitudeToDbfs(decisionEnv[n] + 1e-9);
    if (!isOpen && envDb > openThresh) isOpen = true;        // FSM: mo
    else if (isOpen && envDb < closeThresh) isOpen = false;  // FSM: dong (nguong THAP hon)
    targetGain[n] = isOpen ? 1 : 0;
  }
  const smoothGain = envelopeFollower(targetGain, fs, attackMs, releaseMs); // "toc do" nghe duoc
  return x.map((v, n) => v * smoothGain[n]);
}
⚠️ Cạm bẫy kép: attack chậm "nuốt" phụ âm, release nhanh gây "thở"
Verified thật bằng số: attack CHẬM (50ms) trên 1 phụ âm/transient đột ngột nuốt mất hơn 90% năng lượng trong 10ms đầu tiên (gate mở KHÔNG KỊP) — trong khi attack NHANH (1ms) giữ lại hơn 75%. Ngược lại, release quá nhanh khiến gate bám sát MỌI dao động biên độ tự nhiên của tín hiệu (kể cả reverb tail đang tắt dần) — verify bằng chính envelope follower: release 5ms giảm về nửa biên độ chỉ sau ~166 mẫu, release 200ms cần hơn 40 lần lâu hơn (~6654 mẫu) — release quá nhanh làm âm lượng "phập phồng" theo từng dao động nhỏ, nghe như tiếng "thở".

4. Gain staging & chống clip xuyên chuỗi

Mỗi khối trong chuỗi có thể LÀM TO tín hiệu (EQ boost, ví dụ) — nếu không theo dõi headroom, tổng tín hiệu tới khối SAU có thể vượt $\pm 1,0$ và bị clip trước khi tới softClip(). Chuỗi này dùng soft-limiter ($\tanh$, Bài 11 không có nhưng Bài 3 đã có hardClip() để đối chứng) thay vì clip cứng ở khâu cuối cùng.

Verified: trên tín hiệu overdrive mạnh, hardClip() khiến hơn 30% số mẫu FLATLINE CHÍNH XÁC tại biên $\pm 1,0$ — đạo hàm gián đoạn, tạo góc nhọn trong dạng sóng (nguồn gốc của méo hài bậc cao, khó nghe); softClip() ngược lại KHÔNG MẪU NÀO chạm đúng ngưỡng (0/4096) — chỉ tiệm cận, đường cong luôn liên tục mượt.

gain_staging_meter.js (trích engine dsp-core.js)
function softClip(x, threshold = 1) {
  return threshold * Math.tanh(x / threshold); // MUOT, khong bao gio "gay" dot ngot
}

// Do muc dung chuan: Peak (bien do tuc thoi) va RMS (cong suat trung binh, gan voi
// do to CAM NHAN duoc hon Peak) - tai dung amplitudeToDbfs()/signalPower() cua Bai 1, 3
function meterDb(x) {
  const peak = Math.max(...x.map(Math.abs));
  const rms = Math.sqrt(signalPower(x));
  return { peakDb: amplitudeToDbfs(peak), rmsDb: amplitudeToDbfs(rms) };
}
💡 Vì sao đo cả Peak LẪN RMS, không chỉ một trong hai
Peak báo hiệu clip TỨC THỜI (dù chỉ 1 mẫu vượt ngưỡng cũng méo) — cần cho việc canh headroom kỹ thuật. RMS phản ánh đúng hơn độ to mà tai CẢM NHẬN được, vì tai nhạy với công suất trung bình theo thời gian hơn là 1 đỉnh tức thời đơn lẻ — 2 tín hiệu có Peak bằng nhau có thể nghe to khác hẳn nhau nếu RMS khác nhau (một cái đầy đặn liên tục, một cái chỉ có vài đỉnh nhọn hiếm hoi).
Đại lượng Ý nghĩa Dùng khi nào
Peak (dBFS) Biên độ tuyệt đối lớn nhất — amplitudeToDbfs() của Bài 3 Canh headroom, tránh clip tức thời
RMS (dBFS) Căn bậc 2 công suất trung bình — signalPower() của Bài 1 Ước lượng độ to CẢM NHẬN được (tai nhạy công suất trung bình hơn đỉnh tức thời)

5. Thực hành: DSP Station hoàn chỉnh

Chọn nguồn (tín hiệu test dựng sẵn hoặc ghi 2 giây từ mic thật), bật/tắt từng khối (gate/EQ/limiter), chọn preset EQ — nghe kết quả VÀ xem đồng hồ Peak/RMS trước/sau cùng đáp ứng tần số tổng của EQ đang chọn:

🎚️ DSP Station — DSPJS thật

Đáp ứng tần số TỔNG của EQ (dB):

Peak/RMS: —
Sẵn sàng.

Tín hiệu test = 1 tông 300Hz + khoảng lặng + nhiễu nhỏ, để nghe rõ hiệu ứng gate. Tắt từng khối để so sánh A/B trực tiếp.

dsp_station_demo.js (đúng logic đang chạy ở tab Xem trước)
import { noiseGate, cascadeBiquads, softClip, biquadCoeffsRBJ, signalPower, amplitudeToDbfs } from './dsp-core.js';

const PRESETS = {
  bass: [['lowshelf', 120, 0.9, 6], ['peaking', 500, 1, 0], ['peaking', 1500, 1, 0], ['peaking', 5000, 1, 0], ['highshelf', 8000, 0.9, 0]],
  vocal: [['lowshelf', 150, 0.9, -3], ['peaking', 800, 1, 0], ['peaking', 3000, 1.2, 4], ['peaking', 5000, 1, 0], ['highshelf', 8000, 0.9, 2]],
  hall: [['lowshelf', 100, 0.9, 1], ['peaking', 500, 1, 0], ['peaking', 1000, 1, -2], ['peaking', 4000, 1, 0], ['highshelf', 10000, 0.9, -3]],
};

function processChain(x, fs, { gateOn, eqOn, limiterOn, presetKey }) {
  let y = x;
  if (gateOn) y = noiseGate(y, fs, -35, 5, 150, 6);
  if (eqOn) {
    const bands = PRESETS[presetKey].map(([type, f0, Q, gainDb]) => biquadCoeffsRBJ(type, f0, fs, Q, gainDb));
    y = cascadeBiquads(y, bands);
  }
  if (limiterOn) y = y.map((v) => softClip(v, 0.9));
  return y;
}

function meterDb(x) {
  const peak = Math.max(...x.map(Math.abs));
  const rms = Math.sqrt(signalPower(x));
  return { peakDb: amplitudeToDbfs(peak), rmsDb: amplitudeToDbfs(rms) };
}

Checklist tự đánh giá

Trước khi coi Series 14 hoàn thành, tự kiểm tra:

  • ☑️ Giải thích được vì sao aliasing xảy ra và cách chống nó (Bài 2, 12).
  • ☑️ Tự tay viết được FFT lặp (không đệ quy) và biết khi nào dùng DFT trực tiếp là đủ (Bài 6).
  • ☑️ Phân biệt được FIR và IIR, biết khi nào chọn loại nào (Bài 9, 11).
  • ☑️ Đọc được đáp ứng tần số bằng HÌNH HỌC từ vị trí pole/zero (Bài 10).
  • ☑️ Biết chính xác vì sao Q15 tồn tại và cái giá của nó (Bài 14).
  • ☑️ Ghép được nhiều khối DSP thành 1 dây chuyền có thể bật/tắt độc lập (Bài 15 — bài này).

Bản đồ học tiếp sau Series 14: adaptive filter (hệ số filter tự điều chỉnh theo tín hiệu, dùng trong khử tiếng vọng/khử ồn chủ động), wavelet (phân tích đa độ phân giải — thay STFT có cửa sổ cố định bằng cửa sổ đổi theo tần số), và audio ML — spectrogram của Bài 8 chính là input chuẩn cho CNN nhận diện giọng nói/âm nhạc (nối thẳng sang Series 12 — AI).

Trắc nghiệm ôn tập

Câu 1

Vì sao mỗi khối trong DSP Station được viết dạng hàm process(block) độc lập?

Câu 2

Verified: preset bass-boost cho +5,73dB tại 80Hz nhưng ~0dB tại 1kHz. Vì sao đúng nghĩa "shelf"?

Câu 3

Vì sao noise gate cần HYSTERESIS (ngưỡng mở khác ngưỡng đóng) thay vì chỉ 1 ngưỡng duy nhất?

Câu 4

Verified: hardClip() flatline chính xác tại biên hơn 30% số mẫu trên tín hiệu overdrive; softClip() không mẫu nào chạm đúng ngưỡng. Ý nghĩa thực tế là 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 15 vừa thêm cascadeBiquads(), envelopeFollower(), noiseGate(), softClip() — 34 hàm cộng dồn từ Bài 1, kèm self-test đối chiếu đúng mọi hành vi trong toàn bộ series (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 14: DSP thời gian thực & trên phần cứng nhúng 🎉 Hoàn thành Series! Quay lại Lộ trình Series Xử Lý Tín Hiệu Số

Bình luận