之前负责的一个项目是音频关键词识别,就是要从音频中提取出关键词,用于给视频打标签等功能。然而在实际的应用场景中,音频的质量参差不齐,各种噪音会影响语音识别的效果。因此,在做语音识别之前,首先要做的是音频降噪,FFmpeg是使用最广泛的音视频处理工具,这篇文章就是记录一下在C++中用FFmpeg进行音频降噪的处理流程。
关于FFmpeg
什么是FFmpeg
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案(摘自百度百科)。
FFmpeg应该是音视频领域用的最多的一款开源工具了,几乎你需要的所有功能都能在FFmpeg中找到,关于FFmpeg的教程,推荐在CSDN上看雷霄骅的专栏,写的非常好,可惜天妒英才,雷霄骅于2016年过世,愿天堂没有劳累,Respect
在Windows平台下使用FFmpeg(C++)
FFmpeg是在Linux下开发的工具,所以在Linux平台下使用很简单,在Windows平台中使用FFmpeg主要分为两种方式,一种方式是将FFmpeg编译成lib或者dll进行调用,另一种方式是通过cmd启动ffmpeg.exe以输入命令的方式调用,本文介绍第二种方式。
首先下载FFmpeg,可通过官网进行下载,选择Windows版本,下载好会得到FFmpeg.exe,要在C++中通过cmd命令行使用FFmpeg,需要以下代码
void RunProcess(string parameters, string ffmpeg_path){ STARTUPINFO si = { sizeof(STARTUPINFO) }; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; ZeroMemory(&pi, sizeof(pi)); parameters = "cmd /C " + ffmpeg_path + parameters; wstring parameters_wstr = StringToWstring(parameters); LPTSTR szCmdLine = const_cast<LPTSTR>(parameters_wstr.c_str()); if (!CreateProcess(L"c:\\Windows\\System32\\cmd.exe", szCmdLine, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi) ) { WriteErrorLog("命令行执行失败!" + to_string(long long(GetLastError()))); } WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
|
然后就是使用了,使用的代码如下
string parameter = "-i audio.mp3" RunProcess(parameter.c_str(), "C:\\ffmpeg.exe");
|
利用FFmpeg进行音频降噪
噪声对于语音识别来说是一个巨大的干扰,且噪声的建模较为复杂,根据声源和麦克风阵列距离的远近,声场模型可分为近场模型和远场模型,我们的应用场景比较固定,声源离麦克风不会特别远,因此只考虑近场模型。
在降噪之前,先对应用场景中可能出现的噪声进行分析,其中部分噪声类型为麦克风底噪(宽频)、电源噪音(50Hz)、喷麦噪音(60-100Hz)、环境底噪(如空调声音/风扇声音等,<200Hz)、人群背景音(和讲话人声音频谱重叠,85-1100Hz),处理这些类型的噪声常用的方法为频域滤波、时域滤波和幅值均衡。
频域滤波
基于噪声类型的频率特点,采用带通滤波器(60-3000Hz,根据应用场景进行调整)滤除低频、高频噪音,频域滤波的代码如下
string parameter = "-y -i \"" + audio_path + "\" -ac 1 -ar 16000 -acodec pcm_s16le -filter \"bandpass=frequency=1470:width_type=h:width=2940\" " + "\"" + return_path + "\""; RunProcess(parameter.c_str(), "C:\\ffmpeg.exe");
|
时域滤波+幅值均衡
相较于频域滤波,时域滤波就有点复杂了,若以讲话人的麦克风为中心,则收声距离造成音量分贝大小关系为讲话人>> 人群> 环境噪音,基于此,我们提出了低分贝静音时域降噪法,即低于某分贝的时间段将其音量设为静音,低分贝静音时域降噪法的实现原理如下:
-
对频域降噪后的音频采样点的幅值求绝对值
-
采用低通滤波器降低毛刺等高频信号的影响;(巴特沃斯低通滤波器,截止频率1000Hz,根据应用场景进行调整)
-
对滤波后的音频求包络面(峰值包络面,即峰值点相连获得的包络面)
-
求静音门限值,并将经频域降噪后的音频中采样点幅值的绝对值低于此门限值的部分设为静音,求静音门限值的公式如下,其中,n需要通过大量测试来确定一个比较好的值,我这里默认为4
静音门限值=n1×包络面均值
那么如何求包络面均值呢,包络面的图形如下所示
- 如中蓝线为经过低通滤波器后的音频信号(幅值上归一化处理),黄线为包络线
- 包络线用峰值法求得,步长为100,即每100个点中取峰值后连接成的曲线为包络线
计算包络面均值的代码如下,大致原理是计算峰值包络面,即每一百个点中取最大的值作为这个区间的包络值
string parameter = "-y -i " + freq_filter_file + " -filter \"lowpass=frequency=1000:width_type=h\" \"" + time_filter_file + "\" 2>> " + audio_filter_log_path; RunProcess(parameter.c_str()); LOG(INFO) << "时域滤波完成";
|
ifstream infile; vector<int> array_list;
infile.open(temp_url_2, ios::in | ios::binary); infile.seekg(44);
if (!infile) { WriteErrorLog("静音降噪时,文件打开失败!"); return -16; } else { short int byt = 0; while (true) { infile.read((char*)&byt, sizeof(byt)); array_list.push_back(byt); if (infile.eof()) { break; } } LOG(INFO) << "音频已转换为数组,大小为" << array_list.size(); } infile.close();
audio_duration = array_list.size() / (16000.0);
for (int i = 0; i < (int)array_list.size(); i++) { array_list[i] = abs(array_list[i]); }
double array_list_mean = 0; vector<int> temp_list; vector<int> max_temp_list; for (int i = 0; i < (int)array_list.size() - 100; i = i + 100) { temp_list.assign(array_list.begin() + i, array_list.begin() + i + 100); sort(temp_list.begin(), temp_list.end()); max_temp_list.push_back(temp_list[temp_list.size() - 1]); array_list_mean = (max_temp_list[max_temp_list.size() - 1]) + array_list_mean; temp_list.clear(); }
array_list_mean = array_list_mean / max_temp_list.size(); double threshold_value = array_list_mean*threshold_factor; double dBFS_mean_value_1 = 20 * log10(abs(threshold_value) / 32768.0); dBFS_mean_value = (int)round(dBFS_mean_value_1); LOG(INFO) << "门限值为" << dBFS_mean_value << "dBFS";
double abscissa_3_1 = dBFS_mean_value + (90 / equilibrium_factor);
int abscissa_3 = (int)round(abscissa_3_1); string dBFS_mean_value_string = to_string(dBFS_mean_value); string abscissa_3_string = to_string(abscissa_3);
string para_threshold = "-y -i " + temp_url_1 + " -af \"compand=0.1:0.1:-90/-90 " + dBFS_mean_value_string + "/-90 " + abscissa_3_string + "/0:.02:0.01:-90:0\" " + temp_url_3 + " 2>> " + audio_filter_log_path; RunProcess(para_threshold.c_str(), "C:\\ffmpeg.exe");
vector<int>().swap(array_list);
|