Mở đầu: cao độ không sống trong miền tần số
Suốt Bài 6 tới Bài 11, cách đọc một tín hiệu là nhìn phổ — FFT ra một loạt đỉnh, đỉnh cao nhất "chắc chắn" là điều quan trọng nhất. Với cao độ (pitch) của một nốt nhạc hay giọng hát, trực giác này SAI: nhạc cụ thật không phát ra một tần số đơn — chúng phát ra cả một CHỒNG HÀI (harmonics), và đỉnh FFT to nhất có thể là hài bậc 2 hay bậc 3, không phải tần số cơ bản (fundamental) mà tai người thực sự nghe thành "nốt".
Bài này quay lại miền THỜI GIAN để định nghĩa cao độ đúng bản chất: chu kỳ lặp của dạng sóng. Công cụ chính — autocorrelation — hoá ra lại tính nhanh nhất bằng chính FFT ( Bài 6 trả cổ tức lần 2), rồi được vá lỗi bằng thuật toán YIN nổi tiếng trong xử lý tiếng nói/âm nhạc. Kết quả là một cái tuner thật, chạy bằng mic thật.
1. Pitch không phải đỉnh FFT to nhất
Một nốt nhạc cụ thật là tổng của tần số cơ bản $f_0$ cộng CHUỖI hài $2f_0, 3f_0, 4f_0, \ldots$ — mỗi nhạc cụ có một "vân tay" biên độ hài riêng (đàn guitar khác kèn khác giọng người), và HOÀN TOÀN có thể xảy ra hài bậc 2 hoặc 3 to hơn hài bậc 1. Hiện tượng cực đoan nhất gọi là missing fundamental — tần số cơ bản VẮNG MẶT HOÀN TOÀN khỏi tín hiệu (bị lọc mất, hay loa nhỏ không phát nổi tần số thấp) nhưng tai vẫn nghe ra đúng nốt trầm đó, vì bộ não tái tạo chu kỳ từ khoảng cách đều đặn giữa các hài còn lại.
Verified bằng chính hiện tượng này: dựng 1 tín hiệu CHỈ gồm hài bậc 2, 3, 4 của $f_0=150$Hz — không hề có
thành phần 150Hz nào trong tín hiệu — rồi chạy autocorrFFT(). Kết quả: đỉnh tự tương quan tại
$\tau=53$ mẫu, khớp CHÍNH XÁC $f_s/f_0 = 8000/150 \approx 53,33$ mẫu — công cụ phục hồi ĐÚNG chu kỳ của
một tần số KHÔNG TỒN TẠI trong tín hiệu, vì cao độ là thuộc tính của CHU KỲ, không phải của một đỉnh phổ
đơn lẻ.
2. Autocorrelation: tín hiệu tự so với chính nó
Autocorrelation so tín hiệu $x[n]$ với chính nó TRỄ ĐI $\tau$ mẫu: $R(\tau) = \sum_n x[n] \cdot x[n+\tau]$. Nếu $x$ có chu kỳ $T$ mẫu, thì $R(\tau)$ đạt đỉnh mỗi khi $\tau$ là BỘI SỐ của $T$ (tín hiệu trễ đúng 1 chu kỳ khớp lại với chính nó) — đỉnh đầu tiên sau $\tau=0$ chính là chu kỳ cơ bản. Autocorrelation là "họ hàng" của tích chập (Bài 4), nên tính trực tiếp tốn $O(N^2)$ — nhưng định lý Wiener-Khinchin cho phép tính qua FFT: $R = \text{IFFT}(|\text{FFT}(x)|^2)$, CÙNG mẹo "nhân trong miền tần số = tích chập trong miền thời gian" đã dùng ở Bài 6.
// Autocorrelation qua Wiener-Khinchin: FFT, nhan voi LIEN HOP chinh no (|X|^2), IFFT
function autocorrFFT(x) {
const N = x.length;
const paddedLen = nextPowerOfTwo(2 * N); // zero-pad tranh autocorrelation VONG
const X = fft(zeroPad(x, paddedLen));
const power = X.map((c) => ({ re: c.re * c.re + c.im * c.im, im: 0 }));
const r = ifft(power);
return r.slice(0, N).map((c) => c.re);
}
// Verified: tin hieu CHI co hai bac 2,3,4 cua f0=150Hz (missing fundamental)
// - dinh autocorrelation tai tau=53 mau, khop CHINH XAC fs/f0=8000/150=53,33.
3. YIN — bản vá cho nhạc thật
YIN sửa 2 điểm yếu của autocorrelation thô: (1) dùng difference function $d(\tau) = \sum_j (x[j]-x[j+\tau])^2$ thay vì TÍCH — tại đúng chu kỳ, $d(\tau)$ tiến về 0 (một CỰC TIỂU rõ ràng, dễ tìm hơn một cực đại lẫn giữa nhiều cực đại phụ); (2) chuẩn hoá trung bình tích luỹ (Cumulative Mean Normalized Difference — CMND): chia $d(\tau)$ cho trung bình cộng dồn của chính nó — dìm các cực tiểu giả ở $\tau$ nhỏ xuống gần 1, để một ngưỡng tuyệt đối (vd 0,1) hoạt động ổn định cho MỌI cao độ. Bước chọn $\tau$ quyết định: lấy điểm trũng ĐẦU TIÊN đủ tốt (dưới ngưỡng), KHÔNG PHẢI cực tiểu toàn cục — đây chính là cách né octave error.
function yinCMND(d) {
const dp = new Array(d.length).fill(1);
let runningSum = 0;
for (let tau = 1; tau < d.length; tau++) {
runningSum += d[tau];
dp[tau] = d[tau] / (runningSum / tau); // chuan hoa TICH LUY, khong phai tuc thoi
}
return dp;
}
function yinPickTau(dp, threshold, minTau, maxTau) {
for (let tau = minTau; tau < maxTau - 1; tau++) {
if (dp[tau] < threshold) { // DIEM TRUNG DAU TIEN, khong phai cuc tieu toan cuc
while (tau + 1 < maxTau && dp[tau + 1] < dp[tau]) tau++;
return tau;
}
}
/* ... fallback: cuc tieu toan cuc neu khong dip nao duoi nguong ... */
}
Verified bằng chính hiện tượng octave error: dựng 1 tín hiệu "gai" (hài bậc 2 MẠNH hơn cả $f_0$) — chọn
cực tiểu toàn cục của CMND (cách ngây thơ) rơi ĐÚNG vào $\tau=160$ (SAI,
ứng với $f \approx 50$Hz thay vì 150Hz thật) — trong khi yinPickTau()
(ngưỡng tuyệt đối + điểm trũng đầu tiên) chọn ĐÚNG $\tau=53$ (khớp $f_0=150$Hz). Cùng dữ liệu, 2 chiến
lược chọn đỉnh cho 2 kết quả khác hẳn nhau — YIN đúng, "chọn to nhất" sai.
4. Chính xác dưới-mẫu & đổi sang nốt
$\tau$ tìm được là một số NGUYÊN (chỉ số mẫu) — độ phân giải tần số bị giới hạn bởi lưới mẫu rời rạc. Nội suy parabol khớp một parabol qua 3 điểm CMND liền kề quanh $\tau$, tìm đáy THỰC (không nguyên) của parabol đó — cho độ chính xác vượt xa 1 mẫu mà không cần tăng tần số lấy mẫu. Sau đó đổi Hz sang đơn vị tai người cảm nhận: cent, $1200 \log_2(f/f_{ref})$ — 1 quãng tám (tần số gấp đôi) LUÔN đúng 1200 cent bất kể vị trí trên bàn phím (khác thang Hz tuyến tính, nơi 1 quãng tám ở âm trầm chỉ vài Hz còn ở âm cao là hàng trăm Hz).
Verified: trên tín hiệu đầy đủ hài của $f_0=150$Hz, yinPitch() tìm $\tau=53$ rồi nội suy
parabol — sai số tần số cuối cùng dưới 1 cent so với 150Hz thật. Một nửa cung (semitone,
khoảng cách 2 phím đàn piano liền kề) là 100 cent — sai số dưới 1 cent là hoàn toàn không thể nghe ra
được, ngay cả với tai nhạc sĩ chuyên nghiệp.
5. Thực hành: Guitar/Voice Tuner
Bật mic — engine chạy yinPitch() realtime trên từng khung âm thanh (khung xử lý theo kiểu
Bài 8), hiện tên nốt gần nhất + kim chỉ độ lệch cent. Bật "soi nội
tạng" để xem trực tiếp đường CMND và ngưỡng cắt ngay khi đang hát/đàn — thấy tận mắt điểm trũng nào được
chọn:
Đàn/hát một nốt ổn định — kim sẽ dịch về tâm khi đúng cao độ. Mọi xử lý chạy CỤC BỘ trong trình duyệt, không âm thanh nào được gửi đi đâu cả.
import { yinPitch, freqToNote } from './dsp-core.js';
async function startTuner(onFrame) {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
const buf = new Float32Array(analyser.fftSize);
function loop() {
analyser.getFloatTimeDomainData(buf);
const result = yinPitch(Array.from(buf), ctx.sampleRate, { maxTau: 800, minTau: 30, threshold: 0.15 });
if (result.clarity > 0.85) { // bo qua khung qua nhieu/im lang
const note = freqToNote(result.frequency);
onFrame({ note, frequency: result.frequency });
}
requestAnimationFrame(loop);
}
loop();
}
Tóm lược
-
✅ Pitch = chu kỳ lặp trong miền THỜI GIAN, không phải đỉnh phổ to nhất — verified: tín hiệu missing
fundamental (chỉ hài bậc 2,3,4) vẫn cho
autocorrFFT()phục hồi đúng chu kỳ $f_0$. - ✅ Autocorrelation tính nhanh qua FFT (Wiener-Khinchin) — cùng mẹo nhân-phổ của Bài 6.
- ✅ YIN (difference function + CMND + ngưỡng tuyệt đối, chọn điểm trũng ĐẦU TIÊN) né được octave error — verified: cực tiểu toàn cục chọn SAI $\tau=160$, YIN chọn ĐÚNG $\tau=53$.
- ✅ Nội suy parabol cho độ chính xác dưới-mẫu — verified sai số cuối cùng dưới 1 cent, không thể nghe ra.
- ✅ Cent dùng $\log_2$ vì tai cảm nhận cao độ theo TỈ LỆ, không theo hiệu số Hz.
- ✅ Guitar/Voice Tuner chạy mic thật, xử lý cục bộ hoàn toàn trong trình duyệt.
Trắc nghiệm ôn tập
Câu 1
Verified: tín hiệu CHỈ có hài bậc 2,3,4 của 150Hz (không có bậc 1) vẫn cho autocorrFFT() phục hồi đúng chu kỳ 150Hz. Điều này chứng minh điều gì?
Câu 2
YIN dùng difference function $d(\tau)=\sum(x[j]-x[j+\tau])^2$ thay vì tích như autocorrelation. Vì sao?
Câu 3
Verified: chọn cực tiểu TOÀN CỤC của CMND cho tau=160 (SAI, f≈50Hz), trong khi YIN (ngưỡng + điểm trũng ĐẦU TIÊN) cho tau=53 (ĐÚNG, 150Hz). Bài học là gì?
Câu 4
Vì sao công thức cent dùng $\log_2$ thay vì hiệu số Hz thông thường?
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 13 vừa thêm autocorrFFT(), yinDifference(),
yinCMND(), yinPickTau(), parabolicRefine(),
yinPitch(), centsFromFreq(), freqToNote(), 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ì):
Bình luận