[实训题目EmoProfo]音频捕获功能的优化

音频捕获功能的优化

前言

​ 在基本完成了我负责的功能后,我决定去处理当初我留下的音频处理上的优化问题。我们之前提到过,音频捕获部分效果不好,经过进一步试探,发现问题还是比较严重的:

  • 简单根据响度来判断声音是否有效是低效的方式,有些时候环境噪音甚至录音设备的底噪都是十分难缠的,提取出的有效信息很难说是人声
  • 以波的周期为单位截取人声不置可否,但是依靠样本相变来判断周期是一定不靠谱的,由于采样设备和采样率的缘故,有的时候连续很长时间也不会出现相变的情况,很难保证缓冲区不溢出

综上所述,我们接下来的改进思路就是:

  1. 完善周期确定方式和采样点
  2. 在响度判断安静的基础上加上频率判断人声是否存在

问题分析

上面提到的问题,只是通过打印样本点的数值很难了解到问题存在的根本原因。所以,我决定对音频文件进行可视化分析。当然这需要单独的程序,基本功能点如下:

  • 样本点的获取:为了保持分析的有效性,取出的样本点数量和数值必须和stk相同,这里我们直接使用stk
  • 可视化:鉴于当前的开发环境,直接使用Python中的matplotlib是最方便的方法

音频文件可视化分析

基于上面的条件,我们需要实现一个使用stk读取文件样本点的Python扩展,并使用matplotlib将其可视化

C++Python扩展的实现

#include "voicap.h"
#include <stk/FileWvIn.h>
using namespace stk;

/**
 * @brief get_all_sample
 * 打开文件名指定的文件,将其中的样本点全部读出,存储在数组中
 * @param file_name	_in_ 文件名
 * @param buffer	_out_ 存储样本点的数组
 * @return 文件中总共的样本点数量
 */
int get_all_sample(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize();
	*buffer = new StkFloat[size];

	for(int i = 0; i < size; i++)
		(*buffer)[i] = in.tick();
	return size;
}

#include <Python.h>
#include <numpy/ndarrayobject.h>

/**
 * @brief py_sample
 * Python接口,接受一个string类型的参数,调用实现好的函数,将获得的采样点数组转化成ndarray返回
 * @param self
 * @param args
 * @return
 */
static PyObject* py_sample(PyObject* self, PyObject* args)
{
	char* file_name;
	PyArg_ParseTuple(args, "s", &file_name);
	std::string str(file_name);
	StkFloat* ptr_buf;
	int size = get_all_sample(str, &ptr_buf);
	npy_intp* dims = new npy_intp[1];
	dims[0] = size;
	PyObject* arr = PyArray_SimpleNewFromData(1, dims, NPY_FLOAT64, ptr_buf);
	Py_INCREF(arr);

	return arr;
}

static PyMethodDef meth_list[] =
{
	{"sample", py_sample, METH_VARARGS},
	{NULL, NULL, 0, NULL}
};

extern "C" void initvlib()
{
	Py_InitModule("vlib", meth_list);
	_import_array();
}

这个模块首先使用stk一次性取出文件中所有样本点,并把它们封装入ndarray对象

对应的Python脚本

#!/usr/bin/python

import vlib
import matplotlib.pyplot as plt
import numpy as np

y = vlib.sample("/home/tecelecta/Music/waving.wav")
z = np.zeros(y.shape[0])
x = range(y.shape[0])

plt.plot(x, z, 'r', linewidth=2)
plt.plot(x, y, 'b', linewidth=1)
plt.title("size")
plt.show()

这段脚本讲在坐标中画出原点基线和样本点,结果如下

这里写图片描述

有了这张图标(占用了3G内存),我们就可以探究问题的所在了。首先,我们需要找到溢出的样本点在哪里,方法是在已经实现的VoiCap类中加入调试信息,在每次溢出时打印出相关的信息

由于VoiCap类实现的音频截取本质上是通过其内部类WaveBuffer实现的,溢出也是由它探知,而WaveBuffer本身并不包含文件全局信息(即已经从文件中取出多少样本点),所以需要通过一种机制实现内外部类对象在溢出问题上的信息交换。

VoiCap::run_tick()的返回值在之前的修改中被设置为是否有新文件产生,并且已经关联了相关业务逻辑,我们不打算为了调试进行代码的修改,我们应该尽量通过单纯的添加代码来实现功能。于是,我们添加了一项设定:凡是溢出的情况,WaveBuffer返回的窗口帧数是一个有符号的整数,在正常情况下返回窗口帧数,在溢出时返回窗口帧数的相反数,并在VoiCap::run_tick()加入符号判断逻辑,就可以实现溢出位置的探测

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_0.wav


buffer overrun at 87

buffer overrun at 1086

buffer overrun at 2085

buffer overrun at 3084

buffer overrun at 4832

buffer overrun at 5831
new noise level: 0.051576
new noise level: 0.049732
redircecting output file 1 at 2215751

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_1.wav

new noise level: 0.043953
redircecting output file 2 at 4421081

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_2.wav

new noise level: 0.079508

buffer overrun at 5055534

buffer overrun at 5155267
redircecting output file 3 at 6626134

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_3.wav

new noise level: 0.034378
redircecting output file 4 at 8908515

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_4.wav

new noise level: 0.057860
redircecting output file 5 at 11113812

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_5.wav

new noise level: 0.050743
redircecting output file 6 at 13418942

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_6.wav

new noise level: 0.123512
new noise level: 0.152150
redircecting output file 7 at 15897074

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_7.wav

new noise level: 0.141222
new noise level: 0.081281
new noise level: 0.124055
redircecting output file 8 at 18202839

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_8.wav

new noise level: 0.150020
redircecting output file 9 at 20418565

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_9.wav

new noise level: 0.143739
redircecting output file 10 at 22650664

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_10.wav

new noise level: 0.035187
redircecting output file 11 at 24855745

FileWrite: creating WAV file: ./voi/2018-06-09_10:03:44_11.wav


buffer overrun at 25001974
Press <RETURN> to close this window...

添加调试信息后,执行先前的技术测试代码,结果如上所示,可以看到buffer overrun字样密集地出现在录音开始的阶段,文件中间也有一处,我们将对这几处进行细致分析

  • 文件开始处

    这里写图片描述

    文件开始处,波形存在着明显的反常,经过测试其他文件(都是使用我自己的手机录制)发现都存在类似的波形,于是判断这是录音设备初始化导致的,对整个系统影响不大

    第二处的图像如下:

    这里写图片描述

    这个地方是一个特殊的声音,对我们的结果影响不大

特征分析

根据上面的分析工具和了解了相关理论原理,发现我之前设计的思路和改进思路没有问题,都是语音信号分析中的基本要点,用专业术语描述叫做:

  • 短时能量:反映短时间内语音信号的响度,这里我们使用采样点值的平方
  • 短时过零率:用来粗略估计一段时间内语音信号的频率

接下来我们需要将这两项特征进行可视化,方便我们的分析,由于这两项属性都是一段时间内的信号特征,我们必须选定对应的时间窗口,这里我们选择100ms

C++代码:

/**
 * @brief get_all_sample_smooth
 * 与上面的函数相同,区别在于讲样本点进行了求平均值,减少了样本点数量
 * @param file_name	_in_ 文件名
 * @param buffer	_out_ 存储样本点的数组
 * @return 文件中总共的样本点数量
 */
int get_window_loud(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize() / in.getFileRate() * Stk::sampleRate();
	int bsize = std::ceil(float(size)/SUM_RATE);
	*buffer = new StkFloat[bsize];

	for(int i = 0; i < size / SUM_RATE; i++)
	{
		StkFloat sum = 0;
		int j;
		for(j = 0; j < SUM_RATE && j+i*SUM_RATE < size; j++)
			sum += std::abs(in.tick());
		(*buffer)[i] = sum / j;
	}
	return bsize;
}


/**
 * @brief get_window_freq
 * 以窗口为单位统计过零率
 * @param file_name
 * @param buffer
 * @return
 */
int get_window_freq(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize() / in.getFileRate() * Stk::sampleRate();
	int bsize = std::ceil(float(size)/SUM_RATE);
	*buffer = new StkFloat[bsize];

	for(int i = 0; i < size / SUM_RATE; i++)
	{
		StkFloat rate = 0;
		StkFloat last_tick = 0;
		int j;
		for(j = 0; j < SUM_RATE && j+i*SUM_RATE < size; j++)
		{
			StkFloat ctick = in.tick();
			if(DIFF_SIDE(ctick, last_tick))
			{
				rate += 1;
			}
			last_tick = ctick;
		}
		(*buffer)[i] = rate / j;
	}
	return bsize;
}

Python代码不再展示,和上面展示的内容基本类似,将两个曲线绘制在同一坐标系下,得到如下图像:

这里写图片描述

可以看到,的明显特征:

  • 平均能量低,疑似环境噪音的地方,过零率也高(经过确认,音频文件中这一部分确实很安静)

这里写图片描述

  • 录制语音,响度会高,频率会低(本次使用的音频文件中除了语音,几乎没有其他噪声)

    这里写图片描述

改进设计

过去的音频采样单位是“波形”,然而经过我们的分析,那个波形根本没有意义,所以我们还是改为使用固定窗口大小,这里取100ms

在原有的响度判断基础上,加上过零率判断,对响度超过噪声能量等级的样本进行进一步判断。

当然,自适应算法本身也需要进行一系列修改,如果单纯根据短时间内的数据调整噪声等级,效果将很差,我们需要一种更加稳健的自适应算法

  • 噪声因数和信号因数:如果长时间只出现噪声,现有的自适应算法会不断拉低噪声等级,最终导致的结果是噪声也被视为信号,而如果一段时间内长时间出现信号,噪声等级将会被拉的过高。因此,我们决定添加两个参数:噪声因数和信号因数,来限制调整的力度。
    • 噪声因数:当噪声窗口数量累计达到重算点的时候,计算新的噪声等级的时候乘上这个大于1的因数,使得调整后,现有的噪音依然不会被识别为音频
    • 信号因数:当信号窗口连续出现的数量达到重算点的时候,重新计算的噪声等级乘上这个小于1的因数,使得接下来的信号不至于被识别为噪音
  • 双重噪音过滤标准,在单纯的噪声响度过滤基础上,添加频率的过滤,当然,两个过滤基准的使用方式不一样——振幅过滤是信号的振幅低于基准时被视为噪声;而频率过滤是信号的频率高于振幅时被视为噪声。

上面提到的噪声因数当然是百度不到的,这里我们就需要手动的测试使用那个值效果最好

首先,根据我们已有的代码实现响度过滤算法。以及相关的音频参数统计功能

  • 音频响度统计
/**
 * @brief get_window_loud
 * 与上面的函数相同,区别在于讲样本点进行了求平均值,减少了样本点数量
 * @param file_name	_in_ 文件名
 * @param buffer	_out_ 存储样本点的数组
 * @return 文件中总共的样本点数量
 */
int get_window_loud(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize() / in.getFileRate() * Stk::sampleRate();
	int bsize = std::ceil(float(size)/SUM_RATE);
	*buffer = new StkFloat[bsize];

	for(int i = 0; i < size / SUM_RATE; i++)
	{
		StkFloat sum = 0;
		int j;
		for(j = 0; j < SUM_RATE && j+i*SUM_RATE < size; j++)
			sum += std::abs(in.tick());
		(*buffer)[i] = sum / j;
	}
	return bsize;
}
  • 自适应算法
/**
 * @brief get_algorithm_e_level
 * 以窗口为单位,计算噪声能量等级的算法
 * @param file_name
 * @param buffer
 * @return
 */
int get_algorithm_e_level(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize() / in.getFileRate() * Stk::sampleRate();
	int bsize = std::ceil(float(size)/SUM_RATE);
	*buffer = new StkFloat[bsize];

	StkFloat noise_level = 0;
	StkFloat sum_noise = 0;
	StkFloat sum_sig = 0;
	int sig_cnt = 0;
	int noise_cnt = 0;
	int cont_noise_cnt = 0;
	int quiet_cnt = 0;

	for(int i = 0; i < size / SUM_RATE; i++)
	{
		StkFloat e_level = 0;
		int j;
		for(j = 0; j < SUM_RATE && j+i*SUM_RATE < size; j++)
		{
			StkFloat samp = in.tick();
			e_level += abs(samp);
		}
		e_level /= SUM_RATE;

		if(e_level < noise_level)
		{
			sum_noise += e_level;
			noise_cnt++;
			cont_noise_cnt++;
			sig_cnt = 0;
			sum_sig = 0;

			if(cont_noise_cnt >= DEFAULT_QUIET_THRESH)
			{
				quiet_cnt++;
			}

			if(noise_cnt == DEFAULT_RECALC_RATIO)
			{
				noise_level = DEFAULT_ENOI_FACTOR * sum_noise / noise_cnt; //乘上噪声因数
				//noise_level = noise_level > 0.03 ? noise_level : 0.03;
				noise_cnt = 0;
				sum_noise = 0;
			}
		} else {
			sig_cnt++;
			sum_sig += e_level;
			cont_noise_cnt = 0;

			if(sig_cnt == DEFAULT_RAISE_RATIO)
			{
				noise_level = DEFAULT_ESIG_FACTOR * sum_sig / sig_cnt;//乘上信号因数
				sig_cnt = 0;
				sum_sig = 0;
			}
		}
		(*buffer)[i] = noise_level;

	}
	cout << "energy quiet : " << quiet_cnt <<" total: " << bsize << endl;
	return bsize;
}

上述算法为音频过滤的算法,照常利用Python将返回结果进行可视化分析,得到如下图线

这里写图片描述
在17904个窗口点中,有8134个窗口被视为噪音,其中2452被判断属于静默而过滤掉

  • 音频频率统计
#define DIFF_SIDE(a,b) ( ((a) < 0 && (b) > 0) || ((a) > 0 && (b) < 0) )

/**
 * @brief get_window_freq
 * 以窗口为单位统计过零率
 * @param file_name
 * @param buffer
 * @return
 */
int get_window_freq(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize() / in.getFileRate() * Stk::sampleRate();
	int bsize = std::ceil(float(size)/SUM_RATE);
	*buffer = new StkFloat[bsize];

	for(int i = 0; i < size / SUM_RATE; i++)
	{
		StkFloat rate = 0;
		StkFloat last_tick = 0;
		int j;
		for(j = 0; j < SUM_RATE && j+i*SUM_RATE < size; j++)
		{
			StkFloat ctick = in.tick();
			if(DIFF_SIDE(ctick, last_tick))
			{
				rate += 1;
			}
			last_tick = ctick;
		}
		(*buffer)[i] = rate / j;
	}
	return bsize;
}
  • 自适应算法
/**
 * @brief get_algorithm_zr_level
 * @param file_name
 * @param buffer
 * @return
 */
int get_algorithm_zr_level(const std::string& file_name, StkFloat** buffer)
{
	FileWvIn in(file_name);
	int size = in.getSize() / in.getFileRate() * Stk::sampleRate();
	int bsize = std::ceil(float(size)/SUM_RATE);
	*buffer = new StkFloat[bsize];

	StkFloat noise_level = 1;
	StkFloat sum_sig = 0;
	StkFloat sum_noise = 0;
	int sig_cnt = 0;
	int noise_cnt = 0;
	int cont_noise_cnt = 0;

	int quiet_cnt = 0;
	int full_noise_cnt = 0;

	StkFloat last_tick = 0;
	for(int i = 0; i < size / SUM_RATE; i++)
	{
		StkFloat rate = 0;

		int j;
		for(j = 0; j < SUM_RATE && j+i*SUM_RATE < size; j++)
		{
			StkFloat ctick = in.tick();
			if(DIFF_SIDE(ctick, last_tick))
			{
				rate += 1;
			}
			last_tick = ctick;
		}
		rate /= j;

		if(rate > noise_level)
		{
			sum_noise += rate;
			noise_cnt++;
			cont_noise_cnt++;
			sig_cnt = 0;
			sum_sig = 0;

			if(cont_noise_cnt >= DEFAULT_QUIET_THRESH)
			{
				quiet_cnt++;
			}

			if(noise_cnt == DEFAULT_RECALC_RATIO)
			{
				noise_level = DEFAULT_FNOI_FACTOR * sum_noise / noise_cnt;
				sum_noise = 0;
				noise_cnt = 0;
			}
			full_noise_cnt++;

		} else {
			sum_sig += rate;
			sig_cnt++;
			cont_noise_cnt = 0;
			if(sig_cnt == DEFAULT_RAISE_RATIO)
			{
				noise_level = DEFAULT_FSIG_FACTOR* sum_sig /sig_cnt;
				sum_sig = 0;
				sig_cnt = 0;
			}
		}

		(*buffer)[i] = noise_level;
	}
	cout << "zr_rate quiet : " << quiet_cnt <<" noise " << full_noise_cnt <<" total: " << bsize << endl;
	return bsize;
}

上述频率分析算法得到的图像如下

这里写图片描述
共计17904个窗口,有7322个窗口被视为噪声,其中有1194个窗口被视为静默

实现

将上面两个算法结合起来判断,组成新的VoiCap::run_tick方法:

/**
 * @brief VoiCap::run_tick
 * 文件输入的倒采样点方法,从一个缓冲区移动到另一个缓冲区,不存在计算量和实时性的问题
 *
 * 我们使用变量noise_level来表示,如果_quiet_thresh个波内没有明显超过噪声等级( > noise_level*_env_factor )的波形,
 * 我们认为接下来进入安静时间并停止采样,停止采样后,第一个超过该值的波形将使采样继续
 *
 * noise_level动态确定:当样本点被视为环境噪声时,我们记录下这个样本点的值,当环境噪音数量累计达到_recalc_ratio的时候,
 * 我们用累计值的平均值作为修正后的环噪等级
 *---------------------------------------2018-05-30-------------------------------------------------
 * @param file_name_maybe 可能产生的新文件名,是否真的赋过值由返回值决定
 * @return 0 没有文件 1有文件 -1发生错误
 */
int VoiCap::run_tick(string& file_name_maybe)
{
	StkFrames frames;
	StkFloat level;
	StkFloat rate;
	int fcnt = _buffer.nextWindow(frames, *input);

	_buffer.profile_last_window(&level, &rate);
	//cout << "wave from " << total_cnt <<" to "<<total_cnt+fcnt <<endl;
	total_cnt += fcnt;

	if(level < noise_el) //|| rate > noise_zr)//多进行一次频率的判断
	{
		//环噪相关计数增加
		sum_noise_el += level;
		el_noise_cnt++;
		cont_noise_cnt++;

		//信号相关计数清0
		sig_cnt = 0;
		sum_sig_zr = 0.0;
		sum_sig_el = 0.0;

		//如果达到重算阈
		if(el_noise_cnt == _recalc_thresh)
		{
			//重新计算环噪能量等级
			noise_el = _el_noi_factor * sum_noise_el / _recalc_thresh;
			/*Debug*/
			printf("recalced noise level: %f\n",noise_el);
			//计数清0
			el_noise_cnt = 0;
			sum_noise_el = .0;
		}

		if(cont_noise_cnt >= _quiet_thresh)
		{
			cout << "quiet by noise at " << total_cnt / int(Stk::sampleRate()) / 60 << " : " << total_cnt / int(Stk::sampleRate()) % 60 << endl;
			//停止采样,如果时机合适,进行重定向
			if(need_redirect)
			{
				cout << "redircecting output file " << total_cnt / Stk::sampleRate() << " at " << total_cnt << endl;
				need_redirect = false;
				return redirect(file_name_maybe);
			}
			goto check_redir;
		}
	} else if( rate > noise_zr ) {
		//噪声过零率相关计数增加
		sum_noise_zr += rate;
		zr_noise_cnt++;
		cont_noise_cnt++;

		//信号相关计数清0
		sig_cnt = 0;
		sum_sig_zr = 0.0;
		sum_sig_el = 0.0;

		//如果达到重算阈
		if(zr_noise_cnt == _recalc_thresh)
		{
			//重新计算环噪过零率
			noise_zr = _zr_noi_factor * sum_noise_zr / _recalc_thresh;
			/*Debug*/
			printf("recalced new zero rate: %f\n", noise_zr);
			//计数清0
			zr_noise_cnt = 0;
			sum_noise_zr = .0;
		}


		if(cont_noise_cnt >= _quiet_thresh)
		{
			cout << "quiet by freq at " << total_cnt / int(Stk::sampleRate()) / 60 << " : " << total_cnt / int(Stk::sampleRate()) % 60 << endl;
			//停止采样,如果时机合适,进行重定向
			if(need_redirect)
			{
				cout << "redircecting output file " << total_cnt / Stk::sampleRate() << " at " << total_cnt << endl;
				need_redirect = false;
				return redirect(file_name_maybe);
			}
			goto check_redir;
		}
	} else {
		//信号相关计数增加,环噪相关数据就不用清0了??
		//2018年05月12日 不用清零才怪!!这里属于功能复用不合理:重新计算noise_level要求不清0,判定quiet_thresh要求清零,办法就是再加一个成员
		sum_sig_el += level;
		sum_sig_zr += rate;
		sig_cnt++;
		cont_noise_cnt = 0;
		if(sig_cnt == _raise_thresh)
		{
			//达到提升阈,重新计算
			noise_el = _el_sig_factor * sum_sig_el / _raise_thresh;
			noise_zr = _zr_sig_factor * sum_sig_zr / _raise_thresh;
			printf("raised noise level: %f; new zero rate: %f\n", noise_el, noise_zr);
			//计数清0
			sig_cnt = 0;
			sum_sig_el = sum_sig_zr = .0;
		}
	}

	file_frame_cnt += fcnt;
	output->tick(frames);
	/*debug*/
	//cout << "time: " << total_cnt/Stk::sampleRate() << " level: " << level << " noise_level: " << noise_level <<endl;
check_redir:if(file_frame_cnt >= 20 * (uint32_t) Stk::sampleRate())
	{
		need_redirect = true;
	}
	return 0;
}

经过测试,可以保证:如果麦克风音质不出问题,那么贴近麦克风的语音一定不会被过滤

结论

本次的优化可以说完善了嵌入式端的功能,接下来就需要完成整个系统的集成和相关的调试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值