最近在做一个语音广播的项目,实现语音广播的过程主要是:音频采集、音频编码、音频发送、音频解码和音频播放,这个过程在这里不展开说明,本文主要讲述其中的音频重采样和音频降噪的问题和记录如何解决的思路。本文主要讲述重采样部分,降噪部分在下一篇文章讲述


一、重采样和降噪的关系

之所以降噪过程中需要借助重采样的力量来实现,原因是一般情况下我们从电脑端采集到的音频数据是44.1Khz或者48Khz的16位或者8位双通道的PCM数据,而对于目前一些通用开源的降噪库例如speex或者webrtc来说一般都是有对送入降噪音频频率特定的要求的,其中speex开源库的降噪模块可以对44.1Khz和48Khz等音频PCM数据进行降噪;webrtc则对输入的音频数据要求在32Khz及以下:8Khz、16Khz和32Khz。

对比降噪效果的话,就我在网络中找到的demo而言:

(1)speex虽然可以直接对44.1khz和48khz的PCM数据进行降噪,同时能够比较好的对麦克风的底噪进行了滤除,但是同时也在正常的声音部分引入了不可容忍的电流声,这种电流声噪点有点类似直接使用设定最低阈值和最高阈值一刀切得到的效果。

(2)webrtc虽然仅仅支持32Khz以下频率的音频数据进行降噪,即需要对正常采集到的PCM数据(44.1Khz或者48Khz)进行重采样为32Khz、16Khz或者8Khz的数据来进行降噪,然后再通过重采样为44.1Khz或者48Khz的音频数据才能正常播放。虽然对比speex的降噪步骤webrtc的降噪比较繁琐,但是webrtc的降噪效果对比speex的降噪效果是更好的,不仅底噪被滤除了,而且没有引入电流噪声。

二、重采样知识记录

参考链接:

理解重采样,需要真正的实现如下几个功能:

(1)实现重采样通道数、音频格式相同,但采样频率不同的音频;

(2)实现重采样音频格式相同、采样频率相同但通道数不同的音频;(目前一些usb转耳机就是单通道的)

1、重采样基础知识

(1)采样频率

采样频率在流媒体中是指每秒钟对音频的采样点数,单位为Hz(赫兹)。例如采样频率为44100hz是指每秒钟采集44100个样本点,其中每个样本点根据实际情况具有不同的内存空间,在下面将会讲述到。

(2)声道数

是指该段音频能够通过一个设备的多个发声位置进行发声,常见的声道数有:

单声道:mono

双声道:stereo,包含左右两声道

2.1声道:在双声道基础上增加了一个低音声道

5.1声道:包含一个正面声道、左前方声道、右前方声道、左环绕声道、右环绕声道、一个低音声道,最早应用于早期的电影院

7.1声道:在5.1声道的基础上,把左右的环绕声道拆分为左右环绕声道以及左右后置声道,主要应用于BD以及现代的电影院

(3)重采样样本数和空间大小

每重采样样本数:这个就是说在重采样时需要多少个样本数据,这个样本数会根据不同的声道数和音频格式会有所区别,例如对于1024个16位单声道PCM样本而言,它的大小为1024*2*1=2048字节;而对于1024个16位单声道PCM样本而言,它的样本大小为1024*2*2=4096字节,这两个数字也是在使用ffmpeg进行重采样时常常看到的,因为一般情况下ffmpeg进行重采样时一般采用输入1024个样本来进行重采样,而根据音频格式不同,所输入的音频具体空间大小不同。理解好这一点也是对于重采样知识理解的核心。

补充一点:

我们平时所看到的1024个样本点其实并不是绝对的,因为也可以是1152个样本点等,其中1024是对应一帧AAC音频的样本点数,而1152则一般对应MP3音频的样本点数。

(4)PCM音频格式

a)常见的PCM格式有8位和16位两种,8位每一个PCM数据的值由一个字节即8位来表示(0~255);而16位是指每一个PCM数据的值由两个字节即16位来表示,分为高8位和第8位(-32767~32767);

b)同时由于PCM音频格式还具有单声道和双声道等的区分,就衍生出如下4种自由组合的情况。

(5)重采样的目的

在了解完上述关于音频的基础知识之后,再来探讨一个问题就是为何需要进行重采样呢?其实重采样的真正原因是这样的,在我们进行音视频开发中,遇到的很多设备的对于音频的格式要求是不一样的,例如在我的一个项目中遇到过这么一个情形:正常的电脑播放声音的音频是2声道16位44.1khz或者48khz的PCM音频格式才能够播放,但是此时我的项目中需要使用的一款usb耳机它的格式如下图所示:单声道16位44.1khz或者48khz。

通常情况下在双声道16位的音频设备需要输入的肯定是对应双声道16位的音频数据才能够正确播放,这个时候来了个单声道16位的数据,播放时声音肯定不是我们想要的了。这个时候想要解决这个问题就是需要对这个麦克风的音频数据进行重采样由原来的单声道进行重采样为双声道的数据,以此能够在正常的扬声器中播放音频。

一般情况下重采样能够满足如下需求:

1.采样频率转换:例如44.1khz变为48khz等。

2.声道数变换:例如单双声道的数据转换等。

3.音频格式变换:例如16位转8位等。

(6)重采样流程:

在使用ffmpeg或者其他的重采样方式时,无外乎都是需要注意上面所介绍过的采样频率、声道数以及音频格式的变化等,同时还有一个特别需要注意的是送入样本点和输出样本点的所占的字节数以及保存方式,这些都是需要考虑的,因此每次进行重采样时对输入和输出的样本字节数的计算是特别需要注意的,否则会导致重采样得到的结果不是我们所期望的。

由于网络中很多帖子都仅仅是讲述如何使用字符串操作的方式来进行声道的转换,同时对双声道采样为单声道的讲述尤为多,而对于单声道转为双声道的讲述特别是通过ffmpeg进行的声道转换的重采样尤为的少,在接下来将给出一个demo是关于如何使用ffmpeg来进行对一段44.1khz16位单声道的pcm进行重采样为双声道的16位44.1khz的音频数据。

对于下面的代码有几个需要注意的地方:

(1)在函数pcm_resample_init()中“pcm->nb_samples = 1024;”,选择1024个音频样本数是因为我有一个项目是在重采样完后需要进行编码为aac格式的,因此选择1024个样本数比较方便后面的操作,如果是后面编码为mp3的话建议可以选择1152个样本数。

(2)"max_dst_nb_samples = dst_nb_samples =

av_rescale_rnd(src_nb_samples, outSamplerate, inSamplerate, AV_ROUND_UP);"这一段是为了能够计算出根据采样频率不同时,根据输入样本点数计算出输出的样本点数,其中: max_dst_nb_samples = src_nb_samples*outSamplerate/inSamplerate。

(3)"dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels,

len_swr, outSamplefmt, 1);"这一个函数是用于计算重采样完成之后实际得到的数据的字节大小,算法是dst_bufsize = len_swr*2*2 = 1024*2*2 = 4096 字节

其中len_swr是根据实际调用swr_convert()后返回1024。

(4)输入数据字节大小的计算

src_bufsize = av_samples_get_buffer_size(NULL, 1, 1024, inSamplefmt, 1);

if (dst_bufsize < 0) {

printf("av_samples_get_buffer_size error\n");

return -1;

}

其中src_bufsie = 1024 * 1 * 2 = 2048(字节),1为单声道,2为16位的意思,因此也说明了为什么在main()函数中需要传入的是2048字节的样本数而不是4096,如果还是传4096字节的样本的话,最后得到的仍然是4096而不是8192字节的数据,因此需要输入2048字节数据才能够正确的重采样。

(5)重采样参数设置方式有两种:

1.参数设置方式一:swr_alloc_set_opts()

2.参数设置方式二:av_opt_set_int()

这两种方式设置效果是一致的,可以自由选用,本文的demo是为了给大家作为参考都写进去了,读者可以自由选用。

2、重采样demo(单通道转双通道)

a)初始化函数

	/*函数名称:pcm_resample_init
	*函数功能:初始化pcm重采样参数
	*传入参数:int insample:输入数据采样频率, int outsample:输出采样频率
	*函数返回值:成功返回0,失败返回-1
	*函数作者:吴豪乐
	*日期:2021年1月5日
	*/
	int pcm_resample::pcm_resample_init(int insample, int outsample)
	{
	        //音频重采样 上下文初始化
	        inSamplerate = insample;
	        outSamplerate = outsample;
	        int ret = 0;
	        inSamplefmt = AV_SAMPLE_FMT_S16;
	        outSamplefmt = AV_SAMPLE_FMT_S16;
	        //channels = 2;
	        in_channels = 1;
	        out_channels = 2;
	        dst_nb_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
	        printf("dst_nb_channels:%d\n", dst_nb_channels);
	        //outs_new[0] = (uint8_t *)malloc(4096);//len 为4096
	        //outs_new[1] = (uint8_t *)malloc(4096);
	
	        //设置参数方式一
	        asc = swr_alloc_set_opts(asc, av_get_default_channel_layout(out_channels), outSamplefmt, outSamplerate,
	                av_get_default_channel_layout(in_channels), inSamplefmt, inSamplerate, 0, 0);
	        if (!asc)
	        {
	                printf("swr_alloc_set_opts failed\n");
	                return -1;
	        }
	
	        //设置参数方式二:
	        /* set options */
	        int64_t src_ch_layout = AV_CH_LAYOUT_MONO, dst_ch_layout = AV_CH_LAYOUT_STEREO;//AV_CH_LAYOUT_SURROUND;
	        av_opt_set_int(asc, "in_channel_layout", src_ch_layout, 0);
	        av_opt_set_int(asc, "in_sample_rate", inSamplerate, 0);
	        av_opt_set_sample_fmt(asc, "in_sample_fmt", inSamplefmt, 0);
	        av_opt_set_int(asc, "in_channel_count", in_channels, 0);
	
	        av_opt_set_int(asc, "out_channel_layout", dst_ch_layout, 0);
	        av_opt_set_int(asc, "out_sample_rate", outSamplerate, 0);
	        av_opt_set_sample_fmt(asc, "out_sample_fmt", outSamplefmt, 0);
	        av_opt_set_int(asc, "out_channel_count", out_channels, 0);
	
	        ret = swr_init(asc);
	        if (ret != 0)
	        {
	                printf("swr_init failed\n");
	                return -1;
	        }
	
	        indata[0] = (const uint8_t*)indata1;
	
	        //重采样输出分配空间
	        pcm = av_frame_alloc();
	        pcm->format = outSamplefmt;
	        pcm->channels = out_channels;
	        pcm->channel_layout = av_get_default_channel_layout(out_channels);
	        pcm->nb_samples = 1024;	//一帧音频一通道的采样数量
	        ret = av_frame_get_buffer(pcm, 0);//给pcm分配存储空间
	        if (ret != 0)
	        {
	                printf("重采样分配输出空间失败\n");
	                return -1;
	        }
	
	        printf("pcm_resample_init 重采样初始化成功, inSamplerate:%d, outSamplerate:%d\n", inSamplerate, outSamplerate);
	        
	        src_nb_samples = 1024;
	        //计算理论重采样后的数据量
	        /* compute the number of converted samples: buffering is avoided
	        * ensuring that the output buffer will contain at least all the
	        * converted input samples */
	        //e=av_rescale_rnd(a,b,c,d) => e=a*b/c
	        max_dst_nb_samples = dst_nb_samples =
	                av_rescale_rnd(src_nb_samples, outSamplerate, inSamplerate, AV_ROUND_UP);//这是按比例来求取的目标通道的采样数据:src_num/src_rate = dst_num/dst_rate => dst_num = src_num * dst_rate/src_rate
	
	        printf("111max_dst_nb_samples:%d, dst_nb_samples:%d\n", max_dst_nb_samples, dst_nb_samples);
	
	        //为重采样后的接收数据申请缓存
	        ret = av_samples_alloc_array_and_samples(&dst_data, &dst_linesize, 2,
	                dst_nb_samples, outSamplefmt, 0);
	        if (ret < 0) {
	                printf("Could not allocate destination samples\n");
	                return -1;
	        }
	
	        return 0;
	}

b)开始重采样函数

	/*函数名称:pcm_resample_run
	*函数功能:开始进行重采样,每次重采样数据大小为2048字节
	*传入参数:char *in_data:输入数据, int len_in:输入数据的大小(字节), char *out_data:输出数据, int len_out:输出数据的大小(字节)
	*函数返回值:成功返回0,失败返回-1
	*函数作者:吴豪乐
	*日期:2021年1月5日
	*/
	int pcm_resample::pcm_resample_run(char *in_data, int len_in, char *out_data, int *len_out)
	{
	        if (in_data == NULL || out_data == NULL || len_out == NULL)
	        {
	                printf("pcm_resample_run in_data or out_data error\n");
	                getchar();
	                return -1;
	        }
	
	        if (len_in < 2048)
	        {
	                printf("pcm_resample_run len_in or len_out error\n");
	                getchar();
	                return -1;
	        }
	
	        int ret = 0;
	        //拷贝输入数据
	        memcpy(indata1, in_data, 2048);
	
	        //计算实际重采样后得到的数据大小以及重新分配缓存
	        /* compute destination number of samples */
	        dst_nb_samples = av_rescale_rnd(swr_get_delay(asc, inSamplerate) +
	                src_nb_samples, outSamplerate, inSamplerate, AV_ROUND_UP);//这里将重采样时间也计算进去的意思
	        if (dst_nb_samples > max_dst_nb_samples) {
	                av_freep(&dst_data[0]);
	                ret = av_samples_alloc(dst_data, &dst_linesize, 2,
	                        dst_nb_samples, outSamplefmt, 1);
	                if (ret < 0)
	                {
	                        printf("av_samples_alloc error\n");
	                        return -1;
	                }
	                max_dst_nb_samples = dst_nb_samples;
	        }
	        printf("222max_dst_nb_samples:%d, dst_nb_samples:%d\n", max_dst_nb_samples, dst_nb_samples);
	
	
	        int len_swr = 0;
	        len_swr = swr_convert(asc, dst_data, dst_nb_samples, (const uint8_t**)(&indata), pcm->nb_samples);
	        if (len_swr < 0)
	        {
	                printf("swr_convert error\n");
	                return -1;
	        }
	
	        src_bufsize = av_samples_get_buffer_size(NULL, 1, 1024, inSamplefmt, 1);
	        if (dst_bufsize < 0) {
	                printf("av_samples_get_buffer_size error\n");
	                return -1;
	        }
	        printf("src_bufsize:%d\n", src_bufsize);
	        
	        dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels,
	                len_swr, outSamplefmt, 1);
	        if (dst_bufsize < 0) {
	                printf("av_samples_get_buffer_size error\n");
	                return -1;
	        }
	
	        printf("dst_bufsize:%d, dst_nb_samples:%d, pcm->nb_samples:%d\n", dst_bufsize, dst_nb_samples, pcm->nb_samples);
	        printf("outSamplerate:%d, inSamplerate:%d\n", outSamplerate, inSamplerate);
	        //getchar();
	        *len_out = dst_bufsize;
	        memcpy(out_data, dst_data[0], dst_bufsize);
	        return 0;
	}

c)重采样相关资源释放函数

	/*函数名称:pcm_resample_release
	*函数功能:释放资源
	*传入参数:无
	*函数返回值:无
	*函数作者:吴豪乐
	*日期:2021年1月5日
	*/
	void pcm_resample::pcm_resample_release(void)
	{
	        if (pcm != NULL)
	        {
	                av_frame_free(&pcm);
	                pcm = NULL;
	        }
	
	        if (asc != NULL)
	        {
	                swr_free(&asc);
	                asc = NULL;
	        }
	
	        if (dst_data != NULL)
	        {
	                av_freep(&dst_data[0]);
	                dst_data = NULL;
	        }
	
	        return;
	}

d)main函数运行

	int main()
	{
	        FILE *fb_in = fopen("in_datamono.pcm", "rb");
	        FILE *fb_out = fopen("out_datastereo.pcm", "wb");
	        int nRead = 0, len_swr = 0;
	
	        //初始化重采样
	        pcm_resample obj_pcm_resample;
	        obj_pcm_resample.pcm_resample_init(44100, 44100);
	
	        //进行重采样数据
	        char data_read[8192] = {0};
	
	        while (1)
	        {
	                nRead = fread(data_read, 1, 2048, fb_in);
	                if (nRead == 0)
	                        break;
	                //printf("1nRead:%d\n", nRead);
	                obj_pcm_resample.pcm_resample_run(data_read, 2048, data_read, &len_swr);
	                //printf("len_swr:%d\n", len_swr);
	                //len_swr = single2Double(data_read, 4096);
	                printf("lenswr:%d\n", len_swr);
	                fwrite(data_read, 1, len_swr, fb_out);
	        }
	
	        obj_pcm_resample.pcm_resample_release();
	        getchar();
	        return 0;
	}

下面将提供本文重采样(通道转换的demo)和频率变换的demo链接:

1、单通道转双通道demo

2、频率转换demo


每写一篇文章都不容易,尊重别人的知识产权才是对自己和技术的尊重。为了避免发生知识产权被侵权的情况,我决定做出以下声明:

1.博客中标注原创的文章,版权归原作者 吴豪乐工作室 所有;

2.未经原作者允许不得转载本文内容,否则将视为侵权;

3.转载或者引用本文内容请注明来源及原作者;

4.对于不遵守此声明或者其他违法使用本文内容者,本人依法保留追究权等。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐