简介:这份资源面向音频处理与信号分析方向的学习者和开发者,提供短时傅立叶变换(STFT)与逆短时傅立叶变换(ISTFT)的C++实现,支持wav音频文件与麦克风输入,并给出25%、50%两种重叠比例的STFT处理方式,可用于语音分析、频谱可视化及信号处理模块的搭建。压缩包共13个文件,以h头文件为核心,涵盖STFT、Ooura_FFT、HannWindow、PostProcessor等模块,另含cpp示例、txt构建说明、md文档、license及示例wav音频,整体约1.7MB,结构紧凑便于直接集成。资源采用单头文件FFT实现,速度较快,但作者也提示其输出与MATLAB FFT在个别情况下存在差异,需要完全一致结果时可替换其他FFT库。目前已有2330人学习下载,适合希望快速上手STFT/ISTFT并理解重叠分帧与逆变换流程的读者参考。
1. 从一段“听不清”的录音说起:STFT 与 ISTFT 到底解决什么问题
有人丢给我一段现场录音,说里面有人说话,但背景噪声大、声音忽远忽近,想先做点处理再听。我打开波形一看,整段信号在时域上糊成一团,直接做频谱分析只能得到“整段录音的平均频率分布”,谁在什么时候说了什么,完全看不出来。这就是傅里叶变换的经典局限:它告诉你信号里有哪些频率,却不告诉你这些频率是什么时候出现的。短时傅里叶变换(STFT)就是冲着这个矛盾去的——把长信号切成一小段一小段,每段加窗后做 FFT,得到一张“时间—频率”的二维图,也就是语谱图。而逆短时傅里叶变换(ISTFT)负责把这张图还原回波形,让处理后的信号还能被播放、被保存成 wav。
这份 MATLAB 代码资源围绕的正是这条链路:wav 文件读入、麦克风实时输入、STFT 分帧变换、频域处理、ISTFT 重叠相加还原。它适合两类人:一类是信号处理课程或项目里需要一套能跑通的 STFT/ISTFT 参考实现的学生;另一类是想在 MATLAB 里快速验证降噪、滤波、时频分析想法,不想从零推导重叠相加公式的开发者。系统开源的好处在于,你可以直接把分帧、加窗、重叠相加这几块拆出来,塞进自己的处理流程里。
2. 工程结构与信号链路:从 wav 读入到 ISTFT 还原的完整走向
2.1 目录与文件职责划分
这类 MATLAB 信号处理工程,常见做法是按“入口脚本 + 算法函数 + 辅助工具”三层来组织。虽然具体文件名会因作者习惯不同而有差异,但职责边界基本是固定的,读代码时先按这个框架对号入座,能省不少时间。
| 层级 | 典型文件 | 职责 |
|---|---|---|
| 入口层 | 主脚本 / demo 脚本 | 选择输入源(wav 或 mic)、设定参数、调用处理链路、播放与绘图 |
| 算法层 | STFT 函数、ISTFT 函数 | 分帧、加窗、FFT、重叠相加,纯计算,不碰 IO |
| 输入输出层 | wav 读写、录音回调 | audioread / audiowrite、audiorecorder 或录音对象 |
| 辅助层 | 窗函数生成、帧数计算、绘图 | 汉宁窗/汉明窗、帧移换算、语谱图显示 |
我一般会先找入口脚本,看它调用了哪几个函数、参数是怎么传的,再顺着往下读算法层。这样读的好处是,你能立刻知道“哪些参数是暴露给使用者的、哪些是函数内部写死的”,二次开发时改哪里一目了然。
2.2 一条信号从进入到返回的链路
把整条链路摊开,大概是这么几步:读入信号并统一成单声道列向量 → 按帧长和帧移切分 → 每帧乘窗函数 → 逐帧 FFT 得到复数频谱矩阵 → 在频域做你想要的处理(滤波、阈值降噪、相位保留)→ 逐帧 IFFT → 乘同样的窗做重叠相加 → 归一化输出。这里有个容易被忽略的点:STFT 和 ISTFT 必须用同一套帧长、帧移和窗函数,否则还原出来的波形会有幅度畸变或者周期性咔哒声。
% 入口脚本的典型骨架(示意,参数按需调整) [x, fs] = audioread('input.wav'); % 读入 wav x = x(:,1); % 多声道取第一路,统一成列向量 winLen = 1024; % 帧长,决定频率分辨率 hop = winLen / 4; % 帧移,75% 重叠,还原更平滑 win = hann(winLen, 'periodic'); % 周期汉宁窗,适合重叠相加 X = stft_forward(x, win, hop); % 正变换:返回 [帧数 x 频点数] 复数矩阵 Y = process_spectrum(X); % 频域处理,比如置零低能量频点 y = stft_inverse(Y, win, hop, numel(x));% 逆变换,指定原长度便于裁剪 audiowrite('output.wav', y, fs); % 写回 wav这段骨架里,winLen和hop是最关键的两个参数。帧长越大,频率分辨率越高,但时间分辨率越差,语谱图上的瞬态会被抹平;帧移越小,重叠越多,ISTFT 还原越平滑,但计算量成倍上升。工程上 1024 点帧长配 256 点帧移是很常见的折中,采样率 16 kHz 时对应 64 ms 窗、16 ms 步进,语音场景够用。hann(winLen,'periodic')里的'periodic'不是可有可无的:做 STFT 时用周期窗,重叠相加的归一化系数才对得上,用对称窗会在帧边界引入轻微的能量偏差,短信号上不明显,长信号累积起来就能听出来。
2.3 麦克风实时输入的特殊处理
mic 输入和 wav 输入最大的差别在于“数据是流式来的、长度不确定”。常见做法是用audiorecorder录一段固定时长再处理,或者用录音对象配合回调,攒够一帧就送进 STFT。前者简单、适合离线验证,后者才是真正的实时。实时路径上要注意两点:一是缓冲区要按帧移对齐,不能随便切;二是 ISTFT 的重叠相加需要保留上一帧的尾部,跨回调传递状态,否则每帧独立还原会在拼接处产生断裂。
recObj = audiorecorder(fs, 16, 1); % 16 bit 单声道 recordblocking(recObj, 3); % 先录 3 秒做验证 x = getaudiodata(recObj); % 拿到列向量 % 后续与 wav 路径完全一致,复用同一套 stft_forward / stft_inverse先用recordblocking把实时问题降级成离线问题,是排查链路是否正确的稳妥办法。等离线路径的还原波形和原波形对得上,再换成回调式流式处理,出问题时也容易定位是算法错还是缓冲管理错。
3. 核心算法拆解:分帧、加窗与重叠相加的代码实现
3.1 STFT 正变换:分帧与加窗
正变换的核心是把一维信号变成二维复数矩阵。实现上有两种写法:一种用循环逐帧处理,直观但慢;一种用索引矩阵一次性取出所有帧,快但吃内存。教学和二次开发场景下,循环版更好读,也方便你在帧循环里插调试输出。
function X = stft_forward(x, win, hop) % x : 输入列向量 % win : 窗函数列向量,长度即帧长 % hop : 帧移(样本数) % X : [帧数 x 频点数] 复数矩阵 winLen = numel(win); nFrames = floor((numel(x) - winLen) / hop) + 1; % 完整帧数 nFreq = floor(winLen / 2) + 1; % 单边频谱点数 X = zeros(nFrames, nFreq); for k = 1:nFrames idx = (k-1)*hop + (1:winLen); % 当前帧样本索引 frame = x(idx) .* win; % 加窗 spec = fft(frame); % 整段 FFT X(k,:) = spec(1:nFreq).'; % 只取单边谱 end end逐段说明:nFrames用floor而不是ceil,意味着末尾不足一帧的样本被丢弃,这是简化处理;如果你要严格无损还原,得在入口处先补零到整帧边界,ISTFT 后再裁掉。nFreq = winLen/2 + 1是实信号 FFT 的单边谱长度,包含直流和奈奎斯特点,丢掉负频率半边不影响还原(前提是 ISTFT 里正确补回共轭对称)。spec(1:nFreq).'的转置是为了让矩阵按“帧在前、频率在后”排列,和后续处理、绘图的习惯一致。这里只取单边谱会损失相位信息的完整性吗?不会,实信号的负频率分量是正频率的共轭,ISTFT 里重建即可,但前提是你没有在频域对单边谱做破坏共轭对称的操作。
3.2 ISTFT 逆变换:共轭重建与重叠相加
逆变换比正变换多两步:把单边谱补成完整谱,以及重叠相加后做归一化。归一化系数来自窗函数的平方和,这一步算错,输出要么整体偏小,要么帧边界出现周期性起伏。
function y = stft_inverse(Y, win, hop, outLen) % Y : [帧数 x 频点数] 单边复数谱 % win : 与正变换相同的窗 % hop : 与正变换相同的帧移 % outLen : 期望输出长度,用于裁剪补零部分 winLen = numel(win); nFrames = size(Y, 1); nFreq = size(Y, 2); yBuf = zeros((nFrames-1)*hop + winLen, 1); wsum = zeros(size(yBuf)); % 窗平方和累加 for k = 1:nFrames full = [Y(k,:), conj(Y(k,end-1:-1:2))]; % 补共轭对称 frame = real(ifft(full)); % 回到时域 idx = (k-1)*hop + (1:winLen); yBuf(idx) = yBuf(idx) + frame .* win; % 加窗后叠加 wsum(idx) = wsum(idx) + win.^2; % 记录权重 end yBuf = yBuf ./ max(wsum, eps); % 归一化,防除零 y = yBuf(1:outLen); % 裁剪到原长度 endconj(Y(k,end-1:-1:2))是补共轭对称的关键:单边谱去掉直流和奈奎斯特点后,倒序取共轭拼到尾部,长度正好回到winLen。real(ifft(...))取实部是因为数值误差会留下极小的虚部,不取实部后续播放会报警告。wsum累加窗平方、最后逐点相除,是重叠相加法(overlap-add)的标准归一化,max(wsum, eps)防止信号首尾权重为零时除出 Inf。yBuf(1:outLen)裁掉补零尾巴,保证输出和输入等长,方便直接对比误差。
3.3 频域处理接口:在 X 和 Y 之间插入你的逻辑
这套代码真正的价值不在变换本身,而在于X和Y之间那个可以随便改的处理环节。降噪、带通滤波、谐波增强,都是对Y这个矩阵做逐元素或逐列操作。比如一个最朴素的谱减降噪:估计每帧的噪声底,低于阈值的频点幅度压小、相位保留。
function Y = process_spectrum(X, threshold) % X : STFT 输出复数谱 % threshold : 幅度阈值,低于它的频点被衰减 mag = abs(X); phase = angle(X); mag(mag < threshold) = mag(mag < threshold) * 0.1; % 衰减而非置零 Y = mag .* exp(1i * phase); % 保留相位重建 end这里刻意用“衰减”而不是“置零”,是因为直接置零会破坏帧间的相位连续性,ISTFT 还原后容易出现金属感的“音乐噪声”。保留相位、只压幅度,是谱减法里血泪经验换来的做法。阈值怎么定?可以先对静音段做一次 STFT,取幅度均值乘个系数,比拍脑袋定一个固定值靠谱得多。
4. 跑起来与调参:从示例脚本到可听结果的验证路径
4.1 最小可运行流程
拿到这套代码,第一步不是急着改算法,而是先让原始链路跑通、听到“未经处理但正确还原”的声音。具体顺序是:准备一段 16 kHz 单声道 wav,确认 MATLAB 版本支持hann(...,'periodic')(R2016b 之后都行),把入口脚本里的文件名改成你的文件,运行,然后对比输入和输出的波形。
% 验证还原正确性的最小脚本 [x, fs] = audioread('input.wav'); x = x(:,1); win = hann(1024, 'periodic'); hop = 256; X = stft_forward(x, win, hop); y = stft_inverse(X, win, hop, numel(x)); err = norm(x - y) / norm(x); % 相对误差 fprintf('相对还原误差: %.2e\n', err); soundsc(y, fs); % 听一下如果err在 1e-12 量级,说明变换链路数学上是对的;如果到了 1e-2 甚至更大,先别怀疑算法,去查帧移是否整除、窗函数是否两边一致、outLen是否传对。这个误差指标是我每次改完 STFT/ISTFT 代码后必跑的“后悔药”,比肉耳听可靠得多。
4.2 参数怎么调:帧长、帧移、窗型的取舍
| 参数 | 增大后的影响 | 适用场景 |
|---|---|---|
| 帧长 winLen | 频率分辨率升高,时间分辨率下降 | 分析稳态音、乐器音高 |
| 帧移 hop | 重叠减少,计算量下降,还原平滑度下降 | 实时性优先的流式处理 |
| 窗型 | 汉宁窗旁瓣低、汉明窗主瓣窄 | 降噪用汉宁,频率估计用汉明 |
帧移一般取帧长的 1/4 到 1/2。取 1/4 时重叠相加的权重曲线最平坦,还原质量最好;取 1/2 时计算量减半,但帧边界附近的归一化权重起伏变大,处理过的信号更容易听出周期性。窗型方面,汉宁窗两端归零,重叠相加天然平滑,是 STFT 的默认选择;汉明窗两端不为零,主瓣更窄,适合需要精确定位频率成分的场合,但重叠相加时要更小心归一化。
4.3 结果怎么看:语谱图与波形双验证
只看波形不够,还要看语谱图。正确的 STFT 结果,语谱图上语音的共振峰应该清晰、随时间连续;如果出现整帧全黑或者横向条纹错位,多半是分帧索引算错或者帧移没对齐。绘图时用imagesc加对数幅度,比线性幅度更容易看出结构。
imagesc(20*log10(abs(X.'))); % 转置后频率在纵轴、时间在横轴 axis xy; colorbar; xlabel('帧序号'); ylabel('频点'); title('STFT 幅度谱(dB)');X.'的转置是因为我们的矩阵是“帧 x 频点”,而imagesc默认把行当纵轴,转置后时间才落在横轴上。axis xy让纵轴从下往上递增,符合频率轴的直觉。这两行看着琐碎,但少了任何一个,图的方向都是反的,排查问题时会被误导。
5. 避坑与排查:五个真实会翻车的点
5.1 还原波形幅度整体偏小或偏大
现象:ISTFT 输出听感正常,但和原信号比幅度差了一截,或者归一化后个别位置削顶。原因:重叠相加的归一化用了窗函数本身而不是窗平方和,或者正逆变换用了不同窗型的窗。解决:确认wsum累加的是win.^2,且正逆变换传入的是同一个win变量,不要在两处分别调用hann生成。
5.2 帧边界出现周期性咔哒声
现象:处理后的音频每隔固定时间“哒”一下,周期正好等于帧移。原因:频域处理破坏了帧间的相位连续性,或者帧移没有整除帧长导致重叠区域权重不连续。解决:频域操作尽量保留相位、只改幅度;帧移取帧长的整数分之一;必要时在重叠相加后加一个短时平滑。
5.3 末尾样本丢失或输出比输入短
现象:numel(y) < numel(x),最后几十毫秒被截掉。原因:nFrames用floor计算,末尾不足一帧的样本没进循环。解决:入口处把x补零到(nFrames-1)*hop + winLen长度,ISTFT 后再裁回原长度;或者改用ceil并在分帧时对越界索引做边界处理。
5.4 单边谱处理后再还原出现虚部警告
现象:ifft结果虚部不为零,播放时报“忽略虚部”警告。原因:在单边谱上做了不保持共轭对称的操作,比如只改了正频率的相位。解决:要么在完整双边谱上处理,要么处理完单边谱后确保补共轭那一步用的是处理后的值,最后统一real()。
5.5 麦克风实时路径延迟越来越大
现象:流式处理跑一会儿,输出比输入越来越滞后。原因:每帧处理耗时超过帧移对应的时间,缓冲区持续堆积。解决:减小帧长或帧移、把频域处理改成向量化、或者接受延迟但定期丢弃积压的旧帧。实时场景下,宁可牺牲一点频率分辨率,也不能让缓冲无限增长。
6. 二次开发:把 STFT 模块嵌进自己的处理链
这套代码最值得带走的部分,是stft_forward和stft_inverse这一对函数——它们不依赖任何全局变量,输入输出都是明确的矩阵和向量,天然适合当积木。我一般的改造顺序是:先把这两个函数单独抽成一个文件或一个包,用第 4 章的相对误差脚本验证一遍;再把process_spectrum换成自己的处理逻辑;最后才动入口脚本的 IO 部分。
改造时有几个具体技巧。第一,把帧长、帧移、窗型收进一个结构体参数,避免函数签名越来越长:
params.winLen = 1024; params.hop = 256; params.win = hann(params.winLen, 'periodic'); X = stft_forward(x, params.win, params.hop);第二,给process_spectrum留一个“旁路”开关,处理前后都能一键对比,调试降噪参数时非常省事。第三,如果要做多通道,别急着改算法层,先在入口处对每个通道独立跑一遍单通道链路,确认无误后再考虑共享窗和帧索引的批量实现。第四,验证方法固定成三步:相对误差小于 1e-10、语谱图结构连续、盲听无咔哒声,三步都过才算改对。
有个教训我印象很深:早期我为了省事,在 ISTFT 里直接用了hann的对称版本,短测试音频听着没问题,换到几分钟的录音后,帧边界累积的能量偏差让整段声音发闷,查了大半天才定位到窗型不一致。从那以后我每次改 STFT/ISTFT,都强制先跑一遍“正变换再逆变换”的误差对比,再谈任何频域处理。希望这套拆解能帮到你,少走我走过的弯路。
本文还有配套的精品资源,点击获取