掌握音频开发基础知识

文章目录

      • 基本概念
      • 几种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值评价方法。

实时调度相关

会议系统中的采集,编码,解码,混音等部分,是不同的线程,需要配合起来才能工作。这些线程之间就是通过环形缓冲区连接起来实现实时调度的。如图

掌握音频开发基础知识_第1张图片
这样,每个线程只需要处理自己的事情并从缓冲区读取/写入数据即可。(缓冲区满时写入,以及缓冲区空时读取,处理不好依然会造成阻塞)。

缓冲区

两种类型
  • 块缓存:缓冲区分配的读写单位为固定大小的块,用以读取或者写入一帧音频数据(一帧原始音频数据长度,由采样周期(处理周期)决定),这里按照最大分配,一块缓存可以存放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);      //写入完后归还缓存块,内部实际为写指针移动
/**
 * 点缓冲区接口示例
 */
 
/** 
 * 初始化点缓存区,根据采样率和帧数,以及默认帧长度确定大小,
 * 如传入 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,需要再次获取 */

遇到的问题
  1. 获取缓冲区指针陷入死循环问题
    通常需要获取到缓冲区的数据才能进行下一步的处理和操作,这么做必然少不了循环操作,在获取到时返回。这么做的潜在风险是,获取不到时卡在循环中出不来,无法响应其他模块发出的消息等。有下面几点
  • 在每次获取失败后都要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,包长度。

  1. 两次收到包序号的连续与否决定是否使能FEC(如果有该能力的话)
  2. 根据解码能力Index来重新初始化解码器,同时改变采样周期时间,写数据长度,重置点缓冲区以及通知混音模块能力变化等
  3. 包长度决定了块缓存中读取多少有效数据

混音模块

组成: 重采样 ->混音->重采样
混音模块需要获取多路缓冲区的数据并处理后放入后级缓冲区中,这里需要做好处理,如,使用采集驱动混音工作,这里死等采集数据后才能工作,对于其他路的音频,如果获取不到默认为0来处理,对于后期缓冲区,同样,能获取到则写缓冲区,获取不到则不写。通过这种方式保证混音模块不会阻塞在某一路上。

  • 大循环中使用消息队列接收来自DEC与ENC的消息,如DEC/ENC能力的变化(只有采样率变化才会影响到MIX重采样处理)

  • 多进多出的具体处理(16进16出为例):

  1. 使能一路
  2. 获取音频数据
    其中mic数据为死等处理,以MIC驱动Mix模块工作,其他路音频等待3次,获取不到读缓存则为0数据,获取不到写缓存则不写。
  3. 混音处理
    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模块,以实现处理。
流程图为:

mic ptBuf_in 3A ptBuf_ref speaker ptBuf_out 采集 送采集信号 送参考信号 3A参考信号缓存 输出处理后的信号 mic ptBuf_in 3A ptBuf_ref speaker ptBuf_out

能量统计

这里已经有了简单测试过的代码,用来统计出多路情况下语音能量较大的几路(能量排序),将来用于多画面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;

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_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);
}
  • 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错误,比较好的办法就是按照设定的周期读/写设备(必须及时)。

你可能感兴趣的:(AVCODEC)