文章目录
基本概念 几种CODEC介绍 实时调度相关 缓冲区
解码能力的自适应 混音模块 回声消除的延时控制 能量统计 双声道支持 ALSA设备
代码相关
基本概念
采样率(Hz) : 每秒去取样本的个数,eg: 48000Hz也就是每秒取样本48000个 比特率(bps) :一秒钟传送多少个bit 采样位数 : 每个样本占多少个bit,一般都是16bit也就是两个字节 编/解码周期 : 多久编/解码一次,对应不同的数据量 编解码能力 : 就同视频的h264,h265,VP8,VP9一样,音频也有多种编解码能力,主要使用到的有 alaw,ulaw,g722,aacld,opus等
几种CODEC介绍
alaw/ulaw : 即是g711a与g711u,主要用在电话中,采样率为8k,主要是将16位数据压缩为8位,可得比特率为64kbps g722 : 采样率16K,相对于g711来说能处理的音频信号带宽更大,比特率仍为64kbps aacld : (Advanced Audio Coding - Low Delay)多种采样率,我们主要用32k,这个编码主要特点在于高压缩比与低延迟上,比特率可以自行控制 opus : Opus编码是由silk编码和celt编码合并在一起,可以实现低延迟也可以实现高保真,我们主要使用48k采样率,libopus中同样有inbandfec功能,可以在丢包情况下预测还原语音,功能十分强大 对于不同能力的评估,可以在编码成相同码率的文件后解码回来,然后使用PESQ与编码前的原始文件比较
PESQ(Perceptual evaluation of speech quality) 即:客观语音质量评估。 ITU-T P.862建议书提供的客观MOS值评价方法。
实时调度相关
会议系统中的采集,编码,解码,混音等部分,是不同的线程,需要配合起来才能工作。这些线程之间就是通过环形缓冲区连接起来实现实时调度的。如图
这样,每个线程只需要处理自己的事情并从缓冲区读取/写入数据即可。(缓冲区满时写入,以及缓冲区空时读取,处理不好依然会造成阻塞)。
缓冲区
两种类型
块缓存:缓冲区分配的读写单位为固定大小的块,用以读取或者写入一帧音频数据(一帧原始音频数据长度,由采样周期(处理周期)决定),这里按照最大分配,一块缓存可以存放48K 16bit 50ms长度的数据,即采样周期50ms. 点缓存:缓冲区分配的读写大小不固定,可以精确到字节,主要用来处理写入与读取缓冲区大小不匹配的情况,例如,AACLD的编解码周期为16ms,但是混音周期为10ms或者20ms(统一)。如果用上面的块缓冲很难处理这种情况
编写要点
封装好初始化,读取,写入,以及调试四个部分的函数
初始化 : 主要是给定参数:缓冲区大小(缓冲块的个数,块的大小等),主要是分配空间,初始化计数变量,指针 读取/写入 : 获取缓冲区buf 以及归还缓冲区buf(主要是指针操作,以及计数变量累加) 调试 : 主要是用于日后出问题时的调试,主要就是打印当前环形缓冲区的读写指针位置,读写次数等等
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) ;
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) ;
遇到的问题
获取缓冲区指针陷入死循环问题 通常需要获取到缓冲区的数据才能进行下一步的处理和操作,这么做必然少不了循环操作,在获取到时返回。这么做的潜在风险是,获取不到时卡在循环中出不来,无法响应其他模块发出的消息等。有下面几点
在每次获取失败后都要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 ) ;
gettimeofday ( & aft_sleep, NULL ) ;
}
while ( param-> cycle_enable)
{
outbuf = ( u8* ) getWritingLoopBuffer ( param-> blkbuff) ;
if ( outbuf)
{
break ;
}
usleep ( 1000 ) ;
}
解码能力的自适应
这里解码器的解码能力可以在运行过程中动态变化而不用重启程序,再RTP传输的数据中添加了5字节Header,包含了三个内容:包序号,解码能力Index,包长度。
两次收到包序号的连续与否决定是否使能FEC(如果有该能力的话) 根据解码能力Index来重新初始化解码器,同时改变采样周期时间,写数据长度,重置点缓冲区以及通知混音模块能力变化等 包长度决定了块缓存中读取多少有效数据
混音模块
组成: 重采样 ->混音->重采样 混音模块需要获取多路缓冲区的数据并处理后放入后级缓冲区中,这里需要做好处理,如,使用采集驱动混音工作,这里死等采集数据后才能工作,对于其他路的音频,如果获取不到默认为0来处理,对于后期缓冲区,同样,能获取到则写缓冲区,获取不到则不写。通过这种方式保证混音模块不会阻塞在某一路上。
使能一路 获取音频数据 其中mic数据为死等处理,以MIC驱动Mix模块工作,其他路音频等待3次,获取不到读缓存则为0数据,获取不到写缓存则不写。 混音处理 ret = Audio_Scheduler_TransOneFrame(s8 **inaddr,s8 **outaddr);
int Audio_Sheduler_enable_one ( u8 index, u16 decSmprate, u16 encSmprate)
{
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模块,以实现处理。 流程图为:
mic
ptBuf_in
3A
ptBuf_ref
speaker
ptBuf_out
采集
送采集信号
送参考信号
3A参考信号缓存
输出处理后的信号
mic
ptBuf_in
3A
ptBuf_ref
speaker
ptBuf_out
能量统计
这里已经有了简单测试过的代码,用来统计出多路情况下语音能量较大的几路(能量排序),将来用于多画面MCU会议时动态选择显示出声音最大的几路画面,同时混音也可以只混入显示出画面那些路音频。 这里的能量计算取自MIX模块一路音频中一帧绝对值和的均值。
双声道支持
目前的音频模块处理都是单声道的,将来需要支持双声道,只需要在缓冲区中再申请一块相同大小的空间。
typedef struct {
int size;
int number;
int readid;
int writeid;
int readcnt;
int writecnt;
void * * paddr;
void * * paddr_chn2;
pthread_mutex_t mutex;
} T_LoopBuffer;
ALSA设备
为了避免驱动出现XRUN错误,对于此类设备一定要按照设定的周期及时读写.同时程序中要对USB-ALSA设备进行热插拔检测,设备不存在时往缓冲区写零以保证混音模块继续运行(采集驱动混音)
代码相关
为了日后扩展某种音频能力方便,所有的编解码器采用同一套接口,内部通过增加switch分支即可。
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_FUNC decode;
DEC_DESTROY_FUNC destroy;
} T_decoder_func;
typedef struct encoder_func{
void * _handle;
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)
{
p_dec_init = & local_decoder_init;
p_stfunc-> decode = & local_decoder_decoding;
p_stfunc-> destroy = & local_decoder_destroy;
}
else
{
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) ;
}
alsa设备操作: 检测USB-ALSA设备是否存在
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错误,比较好的办法就是按照设定的周期读/写设备(必须及时)。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)