本文中使用的语言为c++,使用的三方库为fftw,libsndfile
音频的时域转频域
这一部分主要使用傅里叶变换,将时域转成频域。这一块的帖子已经很多,这里不再赘述了。主要注意点如下:
- fftw库的使用,请参考 fftw官网
- libsndfile库的使用,请参考 libsndfile官网
- 窗体采用汉宁窗,宽度为512ms,处理音频采样率为8k,16bit,单声道,窗体移动为1/2窗体宽度
汉宁窗函数如上所示
实现逻辑如下:
- 使用libsndfile读取wav文件,获取到double数组
- 制定窗宽度为512ms,sample长度为N=512*8
- 每个窗载入N个sample的数组,然后做傅里叶变换,调用fftw_plan_dft_r2c_1d函数,获取到复数数组。
// HERE IS pseudocode
double *in = (double *) fftw_malloc(N* sizeof(double));
fftw_complex *out = (fftw_complex *) fftw_malloc(N* sizeof(fftw_complex));
... ...
for (int i = 0; i < N; ++i) {
double multiplier = 0.5 * (1 - cos(2*M_PI*i/(N-1)));//Hanning Window
in[i] = wav[i]*multiplier;
}
... ...
plan = fftw_plan_dft_r2c_1d(N, in, out, FFTW_ESTIMATE);
fftw_execute(plan);
- 窗体移动1/2窗体宽度,载入N个sample,再做一次步骤3
- 重复步骤4,直到窗体移动到文件末尾
从频域恢复到时域
一般来说,在频域中我们会做一些处理,比如说滤波,将50HZ以下的频率去掉。可以在频域中轻松处理。处理后再使用傅里叶逆变换将频域数据恢复成时域数据。
如下将低频部分频率滤掉
// HERE IS pseudocode
for (int i = 0; i < N; ++i) {
if (i<=50) {
out[i][0] = 0;
out[i][1] = 0;
}
}
再转回时域
double *out2 = (double *) fftw_malloc(N* sizeof(double));
fftw_plan backword_plan = fftw_plan_dft_c2r_1d(N, out, out2, FFTW_ESTIMATE);
fftw_execute(backword_plan);
for (int i=0; i<N; ++i) {
out2[i] = out2[i]/N; // normalization
}
最后将数据写入wav文件。
写的时候注意。因为是有重复数据的(窗体是有一半重复的),所以重复的部分是要相乘处理的。
一些常见问题
为什么窗体宽度为512ms?
这个主要看主要分析的频谱范围,因为本人只关心2kHZ以下部分,为了达到这个频率,窗里的sample数必须达到4k以上。512*8=4096,达到要求即可。如果需要更高频率,需要按此计算。
频域转时域是否需要加窗?
上面的例子是没有的,但是实际上是需要的。
窗其实分为两种, 分析窗和合成窗
只是巧合的是位移1/2的汉宁窗(分析窗)+ 矩形窗(合成窗)正好可以还原信号。
还有一种组合是 1/2的正弦窗+ 正弦窗(合成窗)也是可以还原信号的。
为什么窗体位移为1/2?
原始信号乘以窗函数后,无论是哪一种,都会存在前后部分的信号衰减。这主要是因为窗函数的意义——它就是为了将窗体部分的信号趋近于周期的。
1/2的汉宁窗移动有个好处是,前一个窗体的后半部分乘以下一个窗体的前半部分,正好可以还原衰减信号。利用这个特性,所以选取位移为1/2.
为何还原出来的音频听上去一段一段的?
这个问题的根因正如上一个问题的解答。那么为什么会出现这个现象?
一般来说,原始信号没有使用窗函数,在还原的时候会出现这个现象。
因为在做傅里叶变换时,对变换的信号是由周期性要求的。但是真实环境中的声音是不可能的。所以我们需要加窗处理,将窗中的信号趋近于周期性。
如果非周期行函数做处理会发生什么?能量的泄露。主频率的能量会被分散到其他频率上。这种泄露的频谱再逆变换回来的时候,原信号必然发生失真。而傅里叶变换是一段一段处理的,所以最终的音频也会出现一段一段的失真。
如果您觉得有所帮助,请给与一定的支持和鼓励。感谢。