FFmpeg连载7-mp3转码aac及AVAudioFifo的使用

简介: ffmpeg连载系列

前言

如今以抖音、快手为代表的短视频秀无处不在,比如它们一个很普通的功能就是使用流行音乐替换作为视频的背景音乐。而在视频中音频一般都是以AAC的形成存在,但流行音乐大多以mp3的格式传播,
因此需要完成背景音乐替换这个功能,其中的一个步骤就需要完成mp3转aac这样的一个音频转转码的过程。

按照我们以往的经验,转码的大致流程应该是这样的:

解封装->提取音频流->解码成PCM->重新编码成AAC

流程是这样没错,但是内部的出来细节是怎样的呢?是mp3解码出来后的AVFrame可以通过函数avcodec_send_frame送进aac编码器即可吗?
很明显这是不行的,因为mp3每帧是1152个采样点,而aac每帧是1024个采样点。它们每帧的采样点数不同,所以不能直接通过avcodec_send_frame进行编码。

AVAudioFifo·

AVAudioFifo是一个音频缓冲区,是一个先进先出的队列。使用它可以很方便地储存我们的音频缓冲数据,例如在mp3转码aac的过程中,因为它们的采样点数不同,我们就可以把mp3解码出来的
pcm数据放入到AVAudioFifo中去,然后每次从AVAudioFifo中获取1024个采样点送进aac编码器,这样的做法让我们的音频转码变得非常的方便灵活。AVAudioFifo让我们在采样层面做操作,而不用关心底层的字节层面;而且它支持多种格式的单次采样,如支持planar或packed的采样格式,支持不同的通道数等等。

AVAudioFifo的API使用也非常简单,主要包含分配释放、获取可读写空间长度、写入音频数据、读取音频数据等相关函数:

首先是分配和释放操作:

//分配一个AVAudioFifo。
//sample_fmt指定采样格式
//nb_samples则指定AVAudioFifo的缓冲区大小,可以通过av_audio_fifo_realloc重新分配
AVAudioFifo *av_audio_fifo_alloc(enum AVSampleFormat sample_fmt, int channels,int nb_samples);
 
//重新分配缓冲区大小
//成功返回0,失败返回负的错误值
int av_audio_fifo_realloc(AVAudioFifo *af, int nb_samples);
 
//释放AVAudioFifo
void av_audio_fifo_free(AVAudioFifo *af);

查询操作:

//返回fifo中当前存储的采样数量
int av_audio_fifo_size(AVAudioFifo *af);
 
//返回fifo中当前可写的采样数量,即尚未使用的空间数量
int av_audio_fifo_space(AVAudioFifo *af);
 
// 以上两个函数的返回值之和等于AVAudioFifo的缓冲区大小

读取操作:

//将采样写入到AVAudioFifo
//成功则返回实际写入的采样数,如果写入成功,返回值必定等于nb_samples,失败返回负的错误值
int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples);
 
//peek:读取数据,但读到的数据并不会从fifo中删除
int av_audio_fifo_peek(AVAudioFifo *af, void **data, int nb_samples);
 
//从指定的偏移位置peek数据
int av_audio_fifo_peek_at(AVAudioFifo *af, void **data, int nb_samples, int offset);
 
//读取数据,读到的数据会从fifo中删除
int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples);
 
//从fifo中删除nb_samples个采样
int av_audio_fifo_drain(AVAudioFifo *af, int nb_samples);
 
//删除fifo中的所有采样,清空
void av_audio_fifo_reset(AVAudioFifo *af);

音频转码

有了AVAudioFifo,那么我们音频的转码流程就变成了以下这样子:

解封装 -> 提取音频流 -> 解码成PCM->将PCM数据写入AVAudioFifo -> 每次从AVAudioFifo获取1024个采样点送进aac编码器 -> 重新编码成AAC

如果到了最后没有可输入的PCM数据了,但是AVAudioFifo中可读取的采样点数依然不满足aac的1024个采样点的话,可以通过填充静音的方式补充...

上代码:

#include <iostream>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/audio_fifo.h>
#include <libavutil/channel_layout.h>
}

class Mp3ToAAC {
public:
    void mp3_to_aac(const char *mp3, const char *aac) {
        avFormatContext = avformat_alloc_context();
        int ret = avformat_open_input(&avFormatContext, mp3, nullptr, nullptr);
        if (ret < 0) {
            std::cout << "打开mp3输入流失败" << std::endl;
            return;
        }
        av_dump_format(avFormatContext, 0, mp3, 0);

        std::cout << "流的类型:" << avFormatContext->streams[0]->codecpar->codec_type << std::endl;

        int audio_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
        if (audio_index < 0) {
            std::cout << "没有找到音频流,换一个方式查找" << std::endl;
            for (int i = 0; i < avFormatContext->nb_streams; ++i) {
                if (AVMEDIA_TYPE_AUDIO == avFormatContext->streams[i]->codecpar->codec_type) {
                    audio_index = i;
                    std::cout << "找到音频流,audio_index:" << audio_index << std::endl;
                    break;
                }
            }

            if (audio_index < 0) {
                return;
            }
        }

        // 初始化输出
        out_format_context = avformat_alloc_context();
        const AVOutputFormat *avOutputFormat = av_guess_format(nullptr, aac, nullptr);
        out_format_context->oformat = avOutputFormat;
        const AVCodec *aac_encoder = avcodec_find_encoder(AV_CODEC_ID_AAC);
        encode_CodecContext = avcodec_alloc_context3(aac_encoder);

        encode_CodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
        encode_CodecContext->channels = av_get_channel_layout_nb_channels(encode_CodecContext->channel_layout);
        // 如果解码出来的pcm不是44100的则需要进行重采样,重采样需要主要音频时长不变
        encode_CodecContext->sample_rate = 44100;
        // 比如使用 22050的采样率进行编码,编码后的时长明显是比实际音频长的
//        encode_CodecContext->sample_rate = 22050;
        encode_CodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
        encode_CodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
        encode_CodecContext->profile = FF_PROFILE_AAC_LOW;
        //ffmpeg默认的aac是不带adts,而fdk_aac默认带adts,这里我们强制不带
        encode_CodecContext->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
        // 打开编码器
        ret = avcodec_open2(encode_CodecContext, aac_encoder, nullptr);
        if (ret < 0) {
            char error[1024];
            av_strerror(ret, error, 1024);
            std::cout << "编码器打开失败:" << error << std::endl;
            return;
        }

        AVStream *aac_stream = avformat_new_stream(out_format_context, aac_encoder);
        aac_index = aac_stream->index;
        avcodec_parameters_from_context(aac_stream->codecpar, encode_CodecContext);
        ret = avio_open(&out_format_context->pb, aac, AVIO_FLAG_WRITE);
        if (ret < 0) {
            std::cout << "输出流打开失败" << std::endl;
            return;
        }
        ret = avformat_write_header(out_format_context, nullptr);
        if (ret < 0) {
            std::cout << "文件头写入失败" << std::endl;
            return;
        }

        // 解码相关
        const AVCodec *decoder = avcodec_find_decoder(avFormatContext->streams[audio_index]->codecpar->codec_id);
        avCodecContext = avcodec_alloc_context3(decoder);
        avcodec_parameters_to_context(avCodecContext, avFormatContext->streams[audio_index]->codecpar);
        ret = avcodec_open2(avCodecContext, decoder, nullptr);
        if (ret < 0) {
            std::cout << "解码器打开失败" << std::endl;
            return;
        }
        // 分配frame和pack
        AVPacket *avPacket = av_packet_alloc();
        avFrame = av_frame_alloc();
        out_pack = av_packet_alloc();

        encode_frame = av_frame_alloc();
        encode_frame->nb_samples = encode_CodecContext->frame_size;
        encode_frame->sample_rate = encode_CodecContext->sample_rate;
        encode_frame->channel_layout = encode_CodecContext->channel_layout;
        encode_frame->channels = encode_CodecContext->channels;
        encode_frame->format = encode_CodecContext->sample_fmt;
        av_frame_get_buffer(encode_frame, 0);

        // 初始化audiofifo
        audiofifo = av_audio_fifo_alloc(encode_CodecContext->sample_fmt, encode_CodecContext->channels,
                                        encode_CodecContext->frame_size);
        while (true) {
            ret = av_read_frame(avFormatContext, avPacket);
            if (ret < 0) {
                std::cout << "read end" << std::endl;
                break;
            } else if (avPacket->stream_index == audio_index) {
                decode_to_pcm(avPacket);
            }
            av_packet_unref(avPacket);
        }

        // todo 需要冲刷数据,不然可能会漏掉几帧数据 [aac @ 0x125e05f50] 2 frames left in the queue on closing

        ret = av_write_trailer(out_format_context);
        if (ret < 0) {
            std::cout << "文件尾写入失败" << std::endl;
        }
    }

    ~Mp3ToAAC() {
        if (nullptr != avFormatContext) {
            avformat_close_input(&avFormatContext);
        }
        avcodec_free_context(&avCodecContext);

        if (nullptr != out_format_context) {
            avformat_close_input(&out_format_context);
        }
        avcodec_free_context(&encode_CodecContext);
    }

private:
    // 解码
    AVFormatContext *avFormatContext = nullptr;
    AVCodecContext *avCodecContext = nullptr;
    AVFrame *avFrame = nullptr;

    // 编码
    AVFormatContext *out_format_context = nullptr;
    AVCodecContext *encode_CodecContext = nullptr;
    AVPacket *out_pack = nullptr;
    AVFrame *encode_frame = nullptr;
    AVAudioFifo *audiofifo = nullptr;

    int aac_index = 0;
    int64_t cur_pts = 0;

    void encode_to_aac() {
        av_frame_make_writable(encode_frame);
        // todo 如果是冲刷最后几帧数据,不够的可以填充静音  av_samples_set_silence
        while (av_audio_fifo_size(audiofifo) > encode_CodecContext->frame_size) {
            int ret = av_audio_fifo_read(audiofifo, reinterpret_cast<void **>(encode_frame->data),
                                         encode_CodecContext->frame_size);
            if (ret < 0) {
                std::cout << "audiofifo 读取数据失败" << std::endl;
                return;
            }
            cur_pts += encode_frame->nb_samples;
            encode_frame->pts = cur_pts;
            ret = avcodec_send_frame(encode_CodecContext, encode_frame);
            if (ret < 0) {
                std::cout << "发送编码失败" << std::endl;
                return;
            }
            while (true) {
                ret = avcodec_receive_packet(encode_CodecContext, out_pack);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    std::cout << "avcodec_receive_packet end:" << ret << std::endl;
                    break;
                } else if (ret < 0) {
                    std::cout << "avcodec_receive_packet fail:" << ret << std::endl;
                    return;
                } else {
                    out_pack->stream_index = aac_index;
                    ret = av_write_frame(out_format_context, out_pack);
                    if (ret < 0) {
                        std::cout << "av_write_frame fail:" << ret << std::endl;
                        return;
                    } else {
                        std::cout << "av_write_frame success:" << ret << std::endl;
                    }
                }
            }
        }
    }

    void decode_to_pcm(const AVPacket *avPacket) {
        int ret = avcodec_send_packet(avCodecContext, avPacket);
        if (ret < 0) {
            char error[1024];
            av_strerror(ret, error, 1024);
            std::cout << "发送解码失败:" << error << std::endl;
        }
        while (true) {
            ret = avcodec_receive_frame(avCodecContext, avFrame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                std::cout << "avcodec_receive_frame end:" << ret << std::endl;
                break;
            } else if (ret < 0) {
                std::cout << "获取解码数据失败" << std::endl;
                return;
            } else {
                std::cout << "获取解码数据成功 nb_samples:" << avFrame->nb_samples << std::endl;
                /**
                 * mp3解码出来的pcm无法直接编码成aac,
                 * 因为mp3每帧是1152个采样点,而aac每帧需要1024个采样点
                 * 解决方案是使用AVAudioFifo缓冲起来
                 */

                int cache_size = av_audio_fifo_size(audiofifo);
                std::cout << "cache_size:" << cache_size << std::endl;
                av_audio_fifo_realloc(audiofifo, cache_size + avFrame->nb_samples);
                av_audio_fifo_write(audiofifo, reinterpret_cast<void **>(avFrame->data), avFrame->nb_samples);
                encode_to_aac();
            }
        }
    }
};

系列推荐

FFmpeg连载1-开发环境搭建
FFmpeg连载2-分离视频和音频
FFmpeg连载3-视频解码
FFmpeg连载4-音频解码
FFmpeg连载5-音视频编码
FFmpeg连载6-音频重采样

目录
相关文章
|
7月前
|
编解码
项目实战——Qt实现FFmpeg音视频转码器(二)
项目实战——Qt实现FFmpeg音视频转码器(二)
141 0
|
7月前
|
编解码 编译器
项目实战——Qt实现FFmpeg音视频转码器(一)
项目实战——Qt实现FFmpeg音视频转码器(一)
208 0
|
2月前
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
81 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
4月前
|
编解码 NoSQL Java
使用Spring Boot + Redis 队列实现视频文件上传及FFmpeg转码的技术分享
【8月更文挑战第30天】在当前的互联网应用中,视频内容的处理与分发已成为不可或缺的一部分。对于视频平台而言,高效、稳定地处理用户上传的视频文件,并对其进行转码以适应不同设备的播放需求,是提升用户体验的关键。本文将围绕使用Spring Boot结合Redis队列技术来实现视频文件上传及FFmpeg转码的过程,分享一系列技术干货。
247 3
|
4月前
|
编解码 Linux
CentOS安装ffmpeg并转码视频为mp4
CentOS安装ffmpeg并转码视频为mp4
155 0
|
6月前
|
编解码
FFmpeg之转码
FFmpeg之转码
|
7月前
|
编解码 开发工具
获取ffmpeg转码的实时进度
获取ffmpeg转码的实时进度
251 0
|
编解码 前端开发 Java
SpringBoot集成ffmpeg实现视频转码播放
之前构建过文件预览服务,对于视频部分前端播放组件限制只能为mp4格式,为了支持更多视频格式决定对方案进行升级,由于视频格式较多,针对每一种格式定制选择播放器不太现实,决定对视频源统一转码,转码后的格式为mp4,兼容性稳定且前后端改造工作较小
468 0
|
编解码 前端开发
vue+ffmpeg实现前端视频转码
如何使用ffmpeg的wasm文件在浏览器实现视频转码
|
编解码 vr&ar Android开发
Android FFmpeg 转换MP3格式
Android FFmpeg 转换MP3格式
270 0