【FFmpeg(2016)】SwrContext 转换PCM音频位数



【相关博客】

【FFmpeg(2016)】PCM编码AAC


【FFmpeg(2016)】SwrContext重采样结构体


【前言】

这两天在做一些音频的编码,但FFmpeg的编码库avcodec有20M这么大,所以决定使用其他库进行编码。网上发现faac体积小,直接编解码,于是决定使用faac库作为编码模块。


但是从faac的源码发现,它只支持如下格式的PCM编码:


PCM Sample Input Format
0 FAAC_INPUT_NULL invalid, signifies a misconfigured config
1 FAAC_INPUT_16BIT native endian 16bit
2 FAAC_INPUT_24BIT native endian 24bit in 24 bits (not implemented)
3 FAAC_INPUT_32BIT native endian 24bit in 32 bits (DEFAULT)
4 FAAC_INPUT_FLOAT 32bit floating point

我要编码的PCM 是32bit float的,使用faac编码时却总是出错,于是决定使用ffmpeg的转码库进行位数转换。


【FFmpeg及音频相关概念】

enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8,          ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16,         ///< signed 16 bits
    AV_SAMPLE_FMT_S32,         ///< signed 32 bits
    AV_SAMPLE_FMT_FLT,         ///< float
    AV_SAMPLE_FMT_DBL,         ///< double

    AV_SAMPLE_FMT_U8P,         ///< unsigned 8 bits, planar
    AV_SAMPLE_FMT_S16P,        ///< signed 16 bits, planar
    AV_SAMPLE_FMT_S32P,        ///< signed 32 bits, planar
    AV_SAMPLE_FMT_FLTP,        ///< float, planar
    AV_SAMPLE_FMT_DBLP,        ///< double, planar
    AV_SAMPLE_FMT_S64,         ///< signed 64 bits
    AV_SAMPLE_FMT_S64P,        ///< signed 64 bits, planar

    AV_SAMPLE_FMT_NB           ///< Number of sample formats. DO NOT USE if linking dynamically
};
这是ffmpeg罗列的音频格式,带P的表示是 平面数据。

在介绍音频存储格式的两个概念: 交错立体声存储 和 双单声道。

交错立体声存储,即对于两声道的音频存储格式是:L(一个采样点)RLRLRLR.....

双单声道,即是:L(一个采样点)LLLLLLLLLLLLRRRRRRRRRRRRLLLLLLLLLLLLLLLRRRRRRRRRRRRRR。。。


目前为止,FFmpeg API中,我发现只有sample_fmt为 AV_SAMPLE_FMT_FLTP 的音频编码器能打开,也就说它只支持编码格式为AV_SAMPLE_FMT_FLTP的PCM音频。


它虽然并不支持其他格式PCM数据的编码,但是它支持所有格式的相互转换,所以不管你拥有什么格式的音频,只要使用SwrContext转换到AV_SAMPLE_FMT_FLTP,即可使用FFmpeg编码。


【代码】


原始PCM数据:

sample_rate = 48000

channels = 2

sample_fmt = AV_SAMPLE_FMT_FLT

目标PCM数据:

sample_rate = 48000

channels = 2

sample_fmt = AV_SAMPLE_FMT_U8


原始数据 及 目标数据 均是交错立体声存储(必须注意:FFmpeg PCM格式枚举中带P的后缀表明用此参数初始化的编码器,源数据必须是交错立体声存储)


	SwrContext *swr_ctx = NULL;

    FILE *fp_in = NULL;
	fp_in = fopen("in.pcm", "rb");
    FILE *outpcm = NULL;
	outpcm = fopen("out.pcm", "wb");

	uint8_t **convert_data;		
	uint8_t* frame_buf;
    int framenum = 100000;

    int nb_samples = 1024;
    int channels = 2;
    int samplerate = 48000;

	swr_ctx = swr_alloc_set_opts(
		NULL,
		AV_CH_LAYOUT_STEREO,
		AV_SAMPLE_FMT_U8,
		samplerate,
		AV_CH_LAYOUT_STEREO,
		AV_SAMPLE_FMT_FLT,
		samplerate,
		0, NULL);

	swr_init(swr_ctx);

	/* 分配空间 ,存储结果*/
	convert_data = (uint8_t**)calloc(
        channels,
		sizeof(*convert_data));
	int sizes = av_samples_alloc(
        convert_data, NULL,
		channels, nb_samples,
		AV_SAMPLE_FMT_U8, 0);

    /* 存储原始数据 */
	size = av_samples_get_buffer_size(NULL, channels, nb_samples, AV_SAMPLE_FMT_FLT, 0);

	frame_buf = (uint8_t *)av_malloc(size);

	ret = avcodec_fill_audio_frame(pFrame, channels, AV_SAMPLE_FMT_FLT, (const uint8_t*)frame_buf, size, 0);


	if (ret < 0)
	{
		qDebug() << "avcodec_fill_audio_frame error ";
		return 0;
	}


	for (i = 0; i < framenum; i++) {
		av_init_packet(&pkt);
		pkt.data = NULL;    // packet data will be allocated by the encoder
		pkt.size = 0;
		//Read raw data
		if (fread(frame_buf, 1, size, fp_in) <= 0) {
			printf("Failed to read raw data! \n");
			return -1;
		}
		else if (feof(fp_in)) {
			break;
		}
		/* 转换数据,令各自声道的音频数据存储在不同的数组(分别由不同指针指向)*/
		swr_convert(swr_ctx, convert_data, nb_samples ,
			(const uint8_t**)pFrame->data, nb_samples );



		fwrite(convert_data[0], nb_samples * channels * av_get_bytes_per_sample(AV_SAMPLE_FMT_U8), 1, outpcm);

    }


【代码解析】


FFmpeg 在处理  交错立体声存储 以及  双声单声道 中结构体的存储方式稍有不同。


由于是由AV_SAMPLE_FMT_FLT 转 AV_SAMPLE_FMT_U8,那么目标数据的内存空间只需要源数据的1/4.



下面先看 av_sample_alloc 发生什么事,

int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
                     int nb_samples, enum AVSampleFormat sample_fmt, int align)
{
    uint8_t *buf;
    //计算总共内存大小
    int size = av_samples_get_buffer_size(NULL, nb_channels, nb_samples,
                                          sample_fmt, align);
    if (size < 0)
        return size;

    buf = av_malloc(size);
    if (!buf)
        return AVERROR(ENOMEM);

    size = av_samples_fill_arrays(audio_data, linesize, buf, nb_channels,
                                  nb_samples, sample_fmt, align);
    if (size < 0) {
        av_free(buf);
        return size;
    }

    //省略...

    return size;
}

int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
                           const uint8_t *buf, int nb_channels, int nb_samples,
                           enum AVSampleFormat sample_fmt, int align)
{
    int ch, planar, buf_size, line_size;

    planar   = av_sample_fmt_is_planar(sample_fmt);
    buf_size = av_samples_get_buffer_size(&line_size, nb_channels, nb_samples,
                                          sample_fmt, align);
    if (buf_size < 0)
        return buf_size;

    audio_data[0] = (uint8_t *)buf;     // 这里对平面数据做多一些操作
    for (ch = 1; planar && ch < nb_channels; ch++)
        audio_data[ch] = audio_data[ch-1] + line_size;

    if (linesize)
        *linesize = line_size;

    return buf_size;
}


以上调用av_samples_get_buffer_size的第一个参数返回每个声道所占的字节数。

可以发现,如果是此PCM属于平面数据,那么内存将平均分配给每一个声道,此时有如audio_data[0] audio_data[1]....;

对于非平面数据,那么只需要将第一个元素audio_data[0]指向首指针


int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
                               enum AVSampleFormat sample_fmt, int align)
{
    int line_size;
    int sample_size = av_get_bytes_per_sample(sample_fmt);
    int planar      = av_sample_fmt_is_planar(sample_fmt);

    /* validate parameter ranges */
    if (!sample_size || nb_samples <= 0 || nb_channels <= 0)
        return AVERROR(EINVAL);

    /* auto-select alignment if not specified */
    if (!align) {
        if (nb_samples > INT_MAX - 31)
            return AVERROR(EINVAL);
        align = 1;
        nb_samples = FFALIGN(nb_samples, 32);
    }

    /* check for integer overflow */
    if (nb_channels > INT_MAX / align ||
        (int64_t)nb_channels * nb_samples > (INT_MAX - (align * nb_channels)) / sample_size)
        return AVERROR(EINVAL);

    line_size = planar ? FFALIGN(nb_samples * sample_size,               align) :   // 这里根据是否是平面数据
                         FFALIGN(nb_samples * sample_size * nb_channels, align);
    if (linesize)
        *linesize = line_size;

    return planar ? line_size * nb_channels : line_size;
}


由于我们使用的是AV_SAMPLES_FMT_U8,所以处理结果数据时只需用 convert_data[0] 指针。


avcodec_fill_audio_frame 调用同理,由于是AV_SAMPLES_FMT_FLT非平面格式,pFrame->data[0]指向所有数据(左右声道)


其实它们的内存空间一样大,区别就是

非planr : data[0] -> [8192字节]

planr: data[0]->[4096字节] data[1]->[4096字节]


【对于平面与非平面格式的相互转换】


平面数据中,有多少声道,那么data二维指针中就有多少元素指向内存,打个比方,如果是三声道

那么AVFrame中的 data[0] data[1] data[2] 分别指向响应位置,以提供内存给每个声道(顺便说下,每个声道的数据大小是一样的)


如下是FFmpeg支持的layout


/**
 * @}
 * @defgroup channel_mask_c Audio channel layouts
 * @{
 * */
#define AV_CH_LAYOUT_MONO              (AV_CH_FRONT_CENTER)
#define AV_CH_LAYOUT_STEREO            (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
#define AV_CH_LAYOUT_2POINT1           (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_2_1               (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_SURROUND          (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
#define AV_CH_LAYOUT_3POINT1           (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_4POINT0           (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_4POINT1           (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_2_2               (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
#define AV_CH_LAYOUT_QUAD              (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_5POINT0           (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
#define AV_CH_LAYOUT_5POINT1           (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_5POINT0_BACK      (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_5POINT1_BACK      (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_6POINT0           (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT0_FRONT     (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_HEXAGONAL         (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT1           (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT1_BACK      (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
#define AV_CH_LAYOUT_6POINT1_FRONT     (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
#define AV_CH_LAYOUT_7POINT0           (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_7POINT0_FRONT     (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_7POINT1           (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_7POINT1_WIDE      (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
#define AV_CH_LAYOUT_OCTAGONAL         (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
#define AV_CH_LAYOUT_HEXADECAGONAL     (AV_CH_LAYOUT_OCTAGONAL|AV_CH_WIDE_LEFT|AV_CH_WIDE_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT)
#define AV_CH_LAYOUT_STEREO_DOWNMIX    (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)




你可能感兴趣的:(C/C++,FFmpeg)