简介:这份MATLAB代码资源聚焦梅尔倒谱系数(MFCC)的完整实现,面向语音识别、情感分析与语音合成方向的学习者和开发者,尤其适合希望从零理解特征提取流程的初学者,也可作为专业人士的可靠工具。代码按预加重、分帧、加窗、FFT、梅尔滤波器组、对数运算、DCT及动态特征计算等步骤逐步展开,注释详细,便于对照原理逐段调试。资源包共23个文件,以m脚本与函数文件为主,辅以mfc特征数据、png示意图、pdf说明文档、txt文本、wav示例音频及conf配置文件,压缩包约291KB,结构紧凑、便于快速上手。目前已有3029人学习下载,读者可据此掌握帧长、帧移、滤波器数量、DCT系数等参数调整方法,并借助示例音频与对比结果验证特征提取效果,为后续语音处理任务打下基础。
1. 从一段“听起来不对劲”的语音说起:MFCC 到底在算什么
有个做语音采集的朋友找我,说设备录回来的音频人耳听着正常,可丢进识别模型里准确率就是上不去,换了几版分类器也没用。我让他把特征提取那段代码发过来,一看,MFCC 的滤波器组数量、预加重系数、帧移全是拍脑袋填的,连采样率都没跟音频对齐。问题根本不在模型,在特征。梅尔倒谱系数(MFCC)这套东西,本质上是把一段时域波形压成几十维、贴近人耳感知的特征向量,前端提得糙,后端再花哨也是白搭。这份 MATLAB 代码资源,就是把 MFCC 从分帧、加窗、FFT、梅尔滤波到 DCT 取倒谱的完整链路摊开给你看,适合正在做语音识别、声纹比对、音频分类,又不想被工具箱黑盒糊弄的开发者。下面我按“它怎么算 → 怎么跑 → 怎么改”的顺序拆一遍。
2. 核心链路拆解:一帧语音是怎么变成 13 维倒谱的
2.1 预加重、分帧与加窗:先把连续波形切成可分析的短片段
语音信号整体是非平稳的,但 20~30 毫秒的短窗内可以近似看成平稳,所以第一步永远是切帧。切之前通常先做预加重,用一个一阶高通把高频抬起来,补偿发音时声门脉冲带来的高频衰减。常见做法是y[n] = x[n] - 0.97 * x[n-1],这个 0.97 就是预加重系数,取值一般在 0.9~0.98 之间。
% preprocess.m —— 预加重 + 分帧 + 汉明窗 function frames = preprocess(x, fs, frame_ms, hop_ms, preemph) % x: 单声道列向量, fs: 采样率 frame_len = round(frame_ms * fs / 1000); % 帧长采样点数 hop_len = round(hop_ms * fs / 1000); % 帧移采样点数 % 1) 预加重:一阶高通,抬高频 x = [x(1); x(2:end) - preemph * x(1:end-1)]; % 2) 分帧:不足一帧的尾部直接丢弃,避免补零引入假能量 n_frames = floor((length(x) - frame_len) / hop_len) + 1; frames = zeros(frame_len, n_frames); for i = 1:n_frames start_idx = (i-1) * hop_len + 1; frames(:, i) = x(start_idx : start_idx + frame_len - 1); end % 3) 加汉明窗,减小频谱泄漏 win = hamming(frame_len); frames = frames .* win; end这段是整个 MFCC 的地基。frame_ms一般取 25,hop_ms取 10,也就是帧长 25 毫秒、帧移 10 毫秒,相邻帧有 15 毫秒重叠,保证特征在时间上平滑过渡。preemph我一般固定 0.97,除非音频本身高频噪声很重才降到 0.95。注意分帧时尾部不足一帧直接丢掉,比补零更干净——补零会在频谱上引入虚假的低频能量,后面梅尔滤波一压就污染了倒谱。加窗用汉明窗是标配,如果你做的是端点检测前的能量分析,可以换成矩形窗保留真实能量,但做 MFCC 还是汉明窗稳。
2.2 FFT 与功率谱:把时域帧搬到频域
加完窗的每一帧做 N 点 FFT,N 通常取 512 或 1024,要大于帧长,多出来的部分补零。补零在这里是合理的,它让频域采样更密,方便后面梅尔滤波器组对齐。取模平方得到功率谱,只保留正频率那一半。
% powerspectrum.m —— 逐帧 FFT 取功率谱 function P = powerspectrum(frames, nfft) frame_len = size(frames, 1); n_frames = size(frames, 2); % 补零到 nfft 点 X = fft(frames, nfft, 1); % 只取正频率部分 (nfft/2 + 1 个点) half = floor(nfft / 2) + 1; X = X(1:half, :); % 功率谱 = 幅度平方 / nfft,归一化避免数值随 nfft 漂移 P = (abs(X).^2) / nfft; endnfft取 512 时,正频率有 257 个点,对应 0 到 fs/2。这里除以nfft做归一化,是为了让不同nfft设置下功率谱量级一致,否则你换一次 FFT 点数,后面所有阈值都得重调。这一步没什么玄学,但很多人漏掉归一化,导致滤波器组输出的对数能量整体偏移,DCT 之后第一维(能量维)直接失真。
2.3 梅尔滤波器组:按人耳感知重新分配频带
人耳对低频的分辨能力远强于高频,梅尔刻度就是模拟这个特性:低频密、高频疏。滤波器组一般用 24~40 个三角滤波器,覆盖 0 到 fs/2,中心频率在梅尔刻度上等间距分布,再映射回赫兹。
% melbank.m —— 构造梅尔三角滤波器组 function [fb, fb_centers] = melbank(n_filters, nfft, fs, low_freq, high_freq) % 赫兹转梅尔 hz2mel = @(f) 1125 * log(1 + f / 700); mel2hz = @(m) 700 * (exp(m / 1125) - 1); mel_low = hz2mel(low_freq); mel_high = hz2mel(high_freq); % 滤波器中心在梅尔刻度上等间距 mel_points = linspace(mel_low, mel_high, n_filters + 2); hz_points = mel2hz(mel_points); % 映射到 FFT bin 索引 bin = floor((nfft + 1) * hz_points / fs); fb = zeros(n_filters, floor(nfft/2) + 1); for m = 2:n_filters + 1 f_m_minus = bin(m - 1); f_m = bin(m); f_m_plus = bin(m + 1); for k = f_m_minus:f_m fb(m-1, k+1) = (k - f_m_minus) / (f_m - f_m_minus); end for k = f_m:f_m_plus fb(m-1, k+1) = (f_m_plus - k) / (f_m_plus - f_m); end end fb_centers = hz_points(2:end-1); endn_filters是滤波器个数,语音识别里 26 是经典值,声纹场景常用 40。low_freq一般设 0 或 20Hz,high_freq设 fs/2。这里有个容易翻车的点:bin索引可能重复,当滤波器个数太多、nfft太小时,相邻中心会落到同一个 FFT bin 上,三角滤波器退化成一条竖线,特征直接废掉。所以nfft至少要是滤波器个数的 4 倍以上,26 个滤波器配 512 点 FFT 是安全组合。滤波器组矩阵fb是n_filters × (nfft/2+1),每行是一个三角窗,乘上功率谱就得到每个频带的能量。
2.4 对数能量与 DCT:压成低维倒谱,去掉频带相关性
滤波器组输出取对数,再做离散余弦变换(DCT),把能量集中到前几维,同时去掉各频带之间的相关性。通常取前 12~13 维,加上一维对数总能量,凑成 13 维特征向量。
% mfcc.m —— 主流程:从波形到 13 维 MFCC function C = mfcc(x, fs, varargin) p = inputParser; addParameter(p, 'frame_ms', 25); addParameter(p, 'hop_ms', 10); addParameter(p, 'nfft', 512); addParameter(p, 'n_filters',26); addParameter(p, 'n_ceps', 13); addParameter(p, 'preemph', 0.97); parse(p, varargin{:}); opt = p.Results; frames = preprocess(x, fs, opt.frame_ms, opt.hop_ms, opt.preemph); P = powerspectrum(frames, opt.nfft); fb = melbank(opt.n_filters, opt.nfft, fs, 0, fs/2); % 滤波器组加权 + 取对数 mel_energy = fb * P; log_energy = log(mel_energy + eps); % eps 防止 log(0) % DCT-II,取前 n_ceps 维 C = dct(log_energy); C = C(1:opt.n_ceps, :); % 用第 0 维(总能量)替换第 1 维,是常见做法 C(1, :) = log(sum(frames.^2, 1) + eps); endn_ceps取 13 是语音识别的惯例,取 12 维倒谱加 1 维能量。log_energy里加eps是防止静音帧能量为零导致log(0)变-Inf,这个坑我踩过,模型训练时 loss 直接 NaN。DCT 之后第一维是直流分量,代表整体能量,有些实现会把它替换成帧的对数总能量,两种做法都行,看你后端模型怎么接。到这里,一帧 25 毫秒的波形就变成了 13 个数,整段音频输出一个13 × n_frames的矩阵,这就是喂给识别模型的特征。
3. 跑起来:环境、数据与第一次出图
3.1 环境与依赖:MATLAB 版本和工具箱要求
这份代码只依赖基础 MATLAB 和信号处理工具箱里的hamming、dct、fft,没有用到 Audio Toolbox 的mfcc函数——这点很关键,用自带函数就失去拆解意义了。MATLAB R2016b 及以上都能跑,inputParser那套参数解析在更老的版本里也支持。如果你只有基础版没装信号处理工具箱,hamming可以自己用0.54 - 0.46*cos(2*pi*(0:N-1)'/(N-1))替代,dct也能用 FFT 手动实现,不影响主流程。
3.2 准备一段测试音频并加载
拿一段 16kHz 单声道 wav 就行,自己用手机录一段“一二三四五”也可以,但记得转成单声道、16bit PCM。MATLAB 里读音频:
% run_demo.m —— 最小可运行示例 [x, fs] = audioread('test.wav'); % 多声道转单声道 if size(x, 2) > 1 x = mean(x, 2); end % 归一化到 [-1, 1],避免不同录音设备增益差异 x = x / max(abs(x)); C = mfcc(x, fs, 'n_filters', 26, 'n_ceps', 13); % 画出倒谱图:横轴时间,纵轴倒谱维 imagesc(C); xlabel('帧序号'); ylabel('倒谱维'); title('MFCC 特征'); colorbar;audioread返回的fs一定要传给mfcc,采样率错了,帧长和滤波器中心频率全错位。归一化那步不是必须,但强烈建议做,否则不同设备录的音频能量量级差几个数量级,第一维能量特征会主导距离计算。跑完imagesc(C)应该能看到一张横条纹理图,低频维(上面几行)变化平缓,高频维(下面几行)噪声感更强,这是正常形态。如果整张图几乎全黑或全白,多半是log那步出了问题,检查mel_energy是不是有零。
3.3 参数怎么调:一张对照表
| 参数 | 常用值 | 调大后果 | 调小后果 |
|---|---|---|---|
| frame_ms | 25 | 频率分辨率高,时间分辨率低 | 时间分辨率高,频谱模糊 |
| hop_ms | 10 | 特征帧数少,可能丢动态 | 帧数多,计算量和冗余上升 |
| nfft | 512 | 频域采样密,计算量增 | 滤波器可能重叠到同一 bin |
| n_filters | 26 | 频带细,特征维度高 | 频带粗,区分度下降 |
| n_ceps | 13 | 保留更多倒谱细节 | 信息压缩过度 |
| preemph | 0.97 | 高频抬升强 | 高频补偿不足 |
这张表不是让你全调一遍,而是出问题时知道往哪个方向拧。语音识别任务里,frame_ms=25、hop_ms=10、nfft=512、n_filters=26、n_ceps=13这套组合是经过大量实践验证的起点,先跑通再谈优化。
4. 避坑与排查:MFCC 提取里最容易翻车的五件事
4.1 现象:特征图全黑或全白,模型完全不收敛
原因基本锁定在log(mel_energy + eps)这一步。如果音频开头有长段静音,mel_energy接近零,即使加了eps,log出来的值也可能在 -30 以下,把整张图的动态范围拉爆。解决方法是做端点检测,把静音段裁掉再提特征,或者对mel_energy设一个下限,比如max(mel_energy, 1e-10)。我一般会在mfcc里加一个可选的能量门限,低于门限的帧直接标记为静音帧,不参与后续距离计算。
4.2 现象:换一段音频后特征维度对不上
原因通常是采样率变了但代码里fs没更新,或者音频是立体声没转单声道。audioread读立体声返回N×2矩阵,直接丢进preprocess会按列分帧,结果完全错乱。解决方法是加载后强制检查size(x, 2),大于 1 就取均值。另外,如果音频采样率和mfcc调用时传的fs不一致,滤波器中心频率会整体偏移,特征分布完全变样,这种错误不会报错,只会让模型悄悄变差。
4.3 现象:相邻帧特征几乎一样,看不出动态
帧移设得太大,或者分帧时重叠算错了。hop_ms如果等于frame_ms,相邻帧没有重叠,特征在时间上就是跳变的;如果hop_ms大于frame_ms,还会丢样本。标准做法是hop_ms取frame_ms的 40% 左右,25 毫秒帧长配 10 毫秒帧移。检查n_frames的计算公式,floor((length(x) - frame_len) / hop_len) + 1这个写法在length(x)刚好等于frame_len时返回 1,不会越界。
4.4 现象:DCT 之后第一维数值异常大
第一维是直流分量,等于所有滤波器组对数能量之和,量级本来就比后面几维大。如果你直接拿这 13 维算欧氏距离,第一维会主导结果。常见做法是对每一维做均值方差归一化(CMVN),或者把第一维替换成帧对数能量后单独处理。我在声纹比对项目里习惯对倒谱系数做倒谱均值归一化(CMN),减去每维的帧均值,能有效消除信道影响。
4.5 现象:滤波器组矩阵出现全零行
n_filters太大而nfft太小,某些三角滤波器的中心频率映射到同一个 FFT bin,导致该行全零。log(0)即使加了eps也会得到一个极小的常数,该维特征恒定不变,等于浪费一个维度。解决办法是保证nfft >= 4 * n_filters,26 个滤波器至少配 128 点 FFT,实际用 512 更稳。构造完fb后可以加一句assert(all(sum(fb, 2) > 0))做自检。
5. 二次开发:从能跑到好用,我改过的三个地方
5.1 加一阶、二阶差分,补上动态信息
原始 MFCC 只描述当前帧的静态频谱,语音里的动态变化(比如辅音到元音的过渡)靠差分捕捉。标准做法是算一阶差分(Delta)和二阶差分(Delta-Delta),和原始 13 维拼成 39 维。差分公式不复杂,但边界处理有讲究:
% deltas.m —— 一阶差分,边界用复制填充 function D = deltas(C, N) % C: n_ceps × n_frames, N: 差分窗口半宽,通常取 2 if nargin < 2, N = 2; end n_ceps = size(C, 1); n_frames = size(C, 2); % 边界复制填充,避免差分在首尾失真 C_pad = [repmat(C(:,1), 1, N), C, repmat(C(:,end), 1, N)]; D = zeros(n_ceps, n_frames); denom = 2 * sum((1:N).^2); for t = 1:n_frames acc = zeros(n_ceps, 1); for n = 1:N acc = acc + n * (C_pad(:, t + N + n) - C_pad(:, t + N - n)); end D(:, t) = acc / denom; end endN取 2 是惯例,窗口太宽会过度平滑,太窄对噪声敏感。边界用复制填充而不是补零,是因为补零会让首尾帧的差分值异常大,模型会学到虚假的边界模式。拼成 39 维后,特征矩阵变成39 × n_frames,后端模型输入维度记得同步改。
5.2 用 CMVN 做特征归一化,消除信道差异
同一句话用不同麦克风录,MFCC 分布会整体平移,这是信道卷积效应。倒谱域里信道影响表现为加性偏置,减去均值就能抵消大部分。我一般做倒谱均值归一化(CMN),对每一维减去该维在所有帧上的均值;如果数据够多,再做方差归一化(CVN),除以标准差。实现就三行:
% cmvn.m —— 倒谱均值方差归一化 function C_norm = cmvn(C) mu = mean(C, 2); sigma = std(C, 0, 2); sigma(sigma < 1e-8) = 1; % 防止除零 C_norm = (C - mu) ./ sigma; end注意std第二个参数 0 表示按 N-1 归一化,第三个参数 2 表示沿帧维度算。归一化要在差分之前做还是之后做,两种流派都有,我习惯先对静态 MFCC 做 CMVN,再算差分,这样差分反映的是归一化后的动态,更干净。如果你的数据里静音帧很多,均值会被拉偏,最好先做端点检测再归一化。
5.3 把主流程封装成可配置函数,方便批量处理
原始代码如果是一段脚本从头跑到尾,换一批音频就得改路径重跑,效率很低。我一般把它重构成一个extract_mfcc_dir(input_dir, output_dir, config)函数,遍历目录下所有 wav,逐个提特征存成 mat 或 csv,配置项用结构体传。这样调参时只改 config,不用动主逻辑。验证方法也简单:拿同一段音频,分别用原始脚本和封装函数跑,对比输出矩阵的max(abs(diff)),应该为零。改完记得用两三段不同采样率、不同时长的音频各跑一遍,确认没有维度错位或越界。
从那以后我每次接手语音特征相关的代码,都强制先拿一段已知内容的音频跑通全流程,把中间每一步的矩阵尺寸打印出来核对一遍,再谈调参和优化。特征提取这层不出错,后面的模型才有得谈。希望帮到你。
本文还有配套的精品资源,点击获取