PESQ(Perceptual evaluation of speech quality) 即:客观语音质量评估。 ITU-T P.862建议书提供的客观MOS值评价方法。
会议系统中的采集,编码,解码,混音等部分,是不同的线程,需要配合起来才能工作。这些线程之间就是通过环形缓冲区连接起来实现实时调度的。如图
这样,每个线程只需要处理自己的事情并从缓冲区读取/写入数据即可。(缓冲区满时写入,以及缓冲区空时读取,处理不好依然会造成阻塞)。
封装好初始化,读取,写入,以及调试四个部分的函数
/**
* 块缓冲区接口示例
*/
BlkBuffHandle createLoopBuffer(int size, int number); //初始化缓存块大小与缓存块个数
int destroyLoopBuffer(BlkBuffHandle bfhandle); //销毁
void *getReadingLoopBuffer(BlkBuffHandle bfhandle); //尝试获取缓冲区内容,返回一个缓存块指针
int putReadingLoopBuffer(BlkBuffHandle bfhandle); //读取完后归还缓存块,内部实际为读指针移动
void *getWritingLoopBuffer(BlkBuffHandle bfhandle); //尝试获取缓存区未用空间,返回一个缓存块指针
int putWritingLoopBuffer(BlkBuffHandle bfhandle); //写入完后归还缓存块,内部实际为写指针移动
/**
* 点缓冲区接口示例
*/
/**
* 初始化点缓存区,根据采样率和帧数,以及默认帧长度确定大小,
* 如传入 48000,10,帧长度为10ms,每个音频样本为2bytes,
* 则缓冲区总大小为 48000Hz*(10ms/1000ms)*10*2 (bytes)
*/
PtBuffHandle createPointBuffer(int smprate,int frame_num);
int destroyPointBuffer(PtBuffHandle bfhandle); //销毁缓冲区
void *getReadingPointBuffer(PtBuffHandle bfhandle,int readsize);//尝试读取指定长度,返回起始地址
int putReadingPointBuffer(PtBuffHandle bfhandle, int readsize);//读取完后归还,传入实际读取的长度以更新指针
void *getWritingPointBuffer(PtBuffHandle bfhandle, int writesize);//尝试写入指定长度,返回起始地址
int putWritingPointBuffer(PtBuffHandle bfhandle, int writesize);//写入完后归还,传入实际写入长度以更新指针
/* 在获取失败的情况下,返回指针都为 NULL,需要再次获取 */
在每次获取失败后都要usleep(1000)一下,让出时间片使其他线程能够获取到锁,否则死循环快速执行其他线程也无法读取/写入数据
获取缓冲区要有超时次数限制,再超过指定的次数后需要进行某种处理并退出循环,不能无限卡在某个小循环中
在某些需要死等的情况下要给循环使用标志位,如while(bEnableFlag){...}
来避免死循环。
/* 设置超时次数 */
while (1)
{
outbuf = (u8 *)getWritingPointBuffer(param->ptbuff,dec_samples*2);
if(outbuf || try_put_time <= 0)
{
break;
}
try_put_time--;
gettimeofday(&bf_sleep, NULL); //为了测试usleep(1000)到底能不能达到1MS
usleep(1000);
gettimeofday(&aft_sleep, NULL);//为了测试usleep(1000)到底能不能达到1MS
/* 这里当初无论如何都无法实时调度,调试时候发现usleep(1000)也达到了10MS,达不到实时调度要求
最后重新编译内核改变时钟频率(100Hz-->1000Hz)后才解决的问题 */
}
/* 设置循环条件 */
while (param->cycle_enable)
{
outbuf = (u8*)getWritingLoopBuffer(param->blkbuff);
if (outbuf)
{
break;
}
usleep(1000);
}
这里解码器的解码能力可以在运行过程中动态变化而不用重启程序,再RTP传输的数据中添加了5字节Header,包含了三个内容:包序号,解码能力Index,包长度。
组成: 重采样 ->混音->重采样
混音模块需要获取多路缓冲区的数据并处理后放入后级缓冲区中,这里需要做好处理,如,使用采集驱动混音工作,这里死等采集数据后才能工作,对于其他路的音频,如果获取不到默认为0来处理,对于后期缓冲区,同样,能获取到则写缓冲区,获取不到则不写。通过这种方式保证混音模块不会阻塞在某一路上。
大循环中使用消息队列接收来自DEC与ENC的消息,如DEC/ENC能力的变化(只有采样率变化才会影响到MIX重采样处理)
多进多出的具体处理(16进16出为例):
ret = Audio_Scheduler_TransOneFrame(s8 **inaddr,s8 **outaddr);
//from mix_thread_func()
/* 使能一路的主要操作 */
int Audio_Sheduler_enable_one(u8 index,u16 decSmprate,u16 encSmprate)
{
//pthread_mutex_lock(&Audio_Scheduler_Mutex);
/*由传输模块决定直接使能 index 路*/
if (!sg_stInfo.enable_flag_arr[index])
{
sg_stInfo.enable_flag_arr[index] = 1; //置位,在混音的时候循环扫描标志位决定是否参与混音
Audio_Sheduler_Change_decSmprate(index, decSmprate);//初始化相应的重采样实例句柄
Audio_Sheduler_Change_encSmprate(index, encSmprate);
return 0;
}
回声消除的原理是:将采集信号(已经录入了远端声音)与参考信号(播放出来的远端声音)传入后,做一些处理,还原为只有本地声音的过程,这里,参考信号和采集信号采集到回声的时间一定要对应上,否则回声消除会出现问题,我们需要将声音播放出来到被MIC采集到的时间做一下记录,统计一下延迟需要多久,记为时间T,然后就将延迟了T时间的参考信号发送给3A模块,以实现处理。
流程图为:
这里已经有了简单测试过的代码,用来统计出多路情况下语音能量较大的几路(能量排序),将来用于多画面MCU会议时动态选择显示出声音最大的几路画面,同时混音也可以只混入显示出画面那些路音频。
这里的能量计算取自MIX模块一路音频中一帧绝对值和的均值。
目前的音频模块处理都是单声道的,将来需要支持双声道,只需要在缓冲区中再申请一块相同大小的空间。
//例如该实例中,再添加void **paddr_chn2
typedef struct {
int size; // 队列中每个buffer的长度
int number; // 队列中的buffer总数
int readid; // 读指针(buffer序号)
int writeid; // 写指针(buffer序号)
int readcnt; // 读操作计数器
int writecnt; // 写操作计数器
void **paddr; // 队列中每个buffer的内存起始地址
/**
* 例如此处添加void **paddr_chn2;用来存储声道2数据,他们共享上面所有的计数变量,以及指针偏移量等等,
*/
void **paddr_chn2;
/**************************************/
pthread_mutex_t mutex; // 读写指针的互斥锁
}T_LoopBuffer;
为了避免驱动出现XRUN错误,对于此类设备一定要按照设定的周期及时读写.同时程序中要对USB-ALSA设备进行热插拔检测,设备不存在时往缓冲区写零以保证混音模块继续运行(采集驱动混音)
typedef void *(*DEC_INIT_FUNC)(const T_decoder_param *);
typedef s32 (*DEC_FUNC)(void*, u8*, s32, u8*, s32*);
typedef void (*DEC_DESTROY_FUNC)(void*);
typedef void *(*ENC_INIT_FUNC)(const T_encoder_param *);
typedef s32 (*ENC_FUNC)(void*, u8*, s32, u8*, s32*);
typedef void (*ENC_DESTROY_FUNC)(void*);
typedef struct decoder_func{
void* _handle;
//DEC_INIT_FUNC audio_decoder_init;
DEC_FUNC decode;
DEC_DESTROY_FUNC destroy;
} T_decoder_func;
typedef struct encoder_func{
void* _handle;
//ENC_INIT_FUNC audio_encoder_init;
ENC_FUNC encode;
ENC_DESTROY_FUNC destroy;
} T_encoder_func;
void* audio_decoder_init(const T_decoder_param* dec_param)
{
T_decoder_func *p_stfunc = NULL;
p_stfunc = (T_decoder_func *)malloc(sizeof(T_decoder_func));
if(!p_stfunc)
{
printf("MALLOC FOR T_decoder_func error\n");
return NULL;
}
memset(p_stfunc, 0, sizeof(T_decoder_func));
DEC_INIT_FUNC p_dec_init;
if(dec_param->decoder_index == CODEC_INDEX_G722) //g722
{
//local coder in ffmpeg
p_dec_init = &local_decoder_init;
p_stfunc->decode =&local_decoder_decoding;
p_stfunc->destroy = &local_decoder_destroy;
}
else
{
//third coder
switch(dec_param->decoder_index)
{
case CODEC_INDEX_AACLD:
{
p_dec_init = &aacld_decoder_init;
p_stfunc->decode =&aacld_decoder_decoding;
p_stfunc->destroy = &aacld_decoder_destroy;
}break;
case CODEC_INDEX_OPUS:
{
p_dec_init = &OpusDec_init;
p_stfunc->decode = &OpusDec_decoding;
p_stfunc->destroy = &OpusDec_destroy;
}break;
case CODEC_INDEX_ULAW:
case CODEC_INDEX_ALAW:
{
p_dec_init = &g711_decoder_init;
p_stfunc->decode =&g711_decoder_decoding;
p_stfunc->destroy = &g711_decoder_destroy;
} break;
default:
break;
}
}
p_stfunc->_handle = p_dec_init(dec_param);
if(!p_stfunc->_handle)
{
free(p_stfunc);
p_stfunc =NULL;
}
return p_stfunc;
}
s32 audio_decoder_decoding(void *handle,u8 *inbuf, s32 length, u8 *outbuf, s32 *outlen )
{
T_decoder_func *phandle = (T_decoder_func *)handle;
return phandle->decode(phandle->_handle,inbuf, length, outbuf,outlen);
}
void audio_decoder_destroy(void *handle)
{
T_decoder_func *phandle = (T_decoder_func *)handle;
phandle->destroy(phandle->_handle);
free(phandle);
}
int soundcard_exist()
{
const char *dev_name = "/dev/snd/controlC0";
if(!access(dev_name,F_OK))
{
return 1;
}
else
return 0;
}
用于在程序中实现usb-alsa设备的动态热插拔检测,插上设备后无需重启程序即可使用。
ALSA设备特别容易出现XRUN错误,比较好的办法就是按照设定的周期读/写设备(必须及时)。