基于 FFMPEG 的音频编解码(三):音频编码

音频编码

  • 基于 FFMPEG 的音频编解码(一):Hello FFMPEG,安装与编译
  • 基于 FFMPEG 的音频编解码(二):音频解码
  • 基于 FFMPEG 的音频编解码(三):音频编码

在前面文章中,我们了解如何用 FFMPEG 进行音频解码,今天我们来讨论音频编码

一个很重要的概念是,编码与解码是一对逆过程。 回想一下,解码的时候我们是如何做的?下面代码是解码的关键部分:

av_read_frame(ctx, packet);
avcodec_send_packet(ctx, packet);
avcodec_receive_frame(ctx, frame);
  1. av_read_frame 从文件中读取一个 packet
  2. avcodec_send_packetpacket 送去解码
  3. avcodec_receive_frame 取回解码好的数据

编码的过程刚好是解码的逆过程,下面代码是编码的关键部分:

fill_data_to_frame(frame);
avcodec_send_frame(ctx, frame);
avcodec_receive_packet(ctx, packet);
  1. fill_data_to_frame 将数据填充至 frame
  2. avcodec_send_frameframe 送去编码
  3. avcodec_receive_packet 取回编码好的数据

Show me the code

FFMPEG 官方 examples 中有一份关于编码的示例 encode_audio.c,这份代码中,向我们展示了如何编码音频数据为 MP2 格式。

在我折腾这份代码时,发现它并不能编码例如 aac、flac、wav 等格式,输出的文件播放器不认识,而 mp1/mp2/mp3 却是能够打开并正常播放的。搜索一番后才找到原因,aac 等格式缺少音频文件头,这些必要信息的缺失导致无法正常播放,而 mp3 等格式有一种叫 frame header 来记录每一帧数据的信息,因此 encode_audio.c 编码后的文件,只有 mp3 等格式才能正常播放。

非常遗憾 encode_audio.c 并不是我们想要的,但这份代码中,有很多细节值得我们学习,例如判断是否支持当前采样率、判断是否支持当前采样格式、如何编码一个 frame 等。

事情变得稍微复杂了一些,我们不仅要知道如何编码数据,还需要知道如何写文件头。又是一番苦苦搜索,最终锁定了一份雷霄骅大神的代码(愿天堂没有代码)最简单的基于FFMPEG的音频编码器(PCM编码为AAC,这份就是我们想要的,可以将音频数据编码为任意格式(只要 ffmpeg 支持)。这份代码年代有些久远了,里面使用到的 API 有些已经过时了,在其基础上,使用目前较新的 API 得到了下面这份代码:

//
// Created by William.Hua on 2020/9/30.
//
#if defined(__cplusplus)
extern "C"
{
#endif

#include 

#include 
#include 
#include 
#include 
#include 

#if defined(__cplusplus)
}
#endif

#include 
#include 
using namespace std;

static void encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt,
                   AVFormatContext* pFormatCtx, AVStream* audio_st)
{
    int ret;

    /* send the frame for encoding */
    ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        fprintf(stderr, "Error sending the frame to the encoder\n");
        exit(1);
    }

    /* read all the available output packets (in general there may be any
     * number of them */
    while (ret >= 0) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error encoding audio frame\n");
            exit(1);
        }

        pkt->stream_index = audio_st->index;
        if(frame){
            pkt->pts = frame->pts;
            frame->pts += 100;
        }
        av_write_frame(pFormatCtx, pkt);
        av_packet_unref(pkt);
    }
}

int main(int argc, char* argv[])
{
    if(argc < 2){
        cerr << "Usage: encode_audio /full/path/to/output_file\n";
        return -1;
    }

    const string output_file = argv[1];

    // open context
    AVFormatContext* format_ctx = NULL;
    avformat_alloc_output_context2(&format_ctx, NULL, NULL, output_file.c_str());
    if(!format_ctx){
        cerr << "Cannot alloc output context\n";
        return -1;
    }

    // open output file
    if(avio_open(&format_ctx->pb, output_file.c_str(), AVIO_FLAG_WRITE) < 0){
        cerr << "Cannot open output file\n";
        return -1;
    }

    // create new audio stream
    AVStream* audio_st = avformat_new_stream(format_ctx, 0);
    if(!audio_st){
        cerr << "Cannot create audio stream\n";
        return -1;
    }

    // set codec context parameters
    const int sample_rate = 44100;
    const int num_channels = 2;
    const int bit_rate = 64000;
    AVCodecContext* codec_ctx = audio_st->codec;
    codec_ctx->codec_id = format_ctx->oformat->audio_codec;
    codec_ctx->codec_type = AVMEDIA_TYPE_AUDIO;
    codec_ctx->sample_rate = sample_rate;
    codec_ctx->channels = num_channels;
    codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP; // planar float
    codec_ctx->channel_layout = av_get_default_channel_layout(num_channels);
    codec_ctx->bit_rate = bit_rate;

    // print detailed information about output format
    av_dump_format(format_ctx, 0, output_file.c_str(), 1);

    // find encode codec
    AVCodec* codec = avcodec_find_encoder(codec_ctx->codec_id);
    if(!codec){
        cerr << "Cannot find encode codec\n";
        return -1;
    }

    // open encode codec
    if(avcodec_open2(codec_ctx, codec, NULL) < 0){
        cerr << "Cannot open codec\n";
        return -1;
    }

    // alloc frame
    AVFrame* frame = av_frame_alloc();
    if(!frame){
        cerr << "Cannot alloc frame\n";
        return -1;
    }
    frame->nb_samples = codec_ctx->frame_size != 0 ? codec_ctx->frame_size : 1024;
    frame->format = codec_ctx->sample_fmt;
    frame->channel_layout = codec_ctx->channel_layout;
    // allocate buffer for frame
    if(av_frame_get_buffer(frame, 0) < 0){
        cerr << "Cannot allocate buffer for frame\n";
        return -1;
    }
    // make sure frame is writeable
    if(av_frame_make_writable(frame) < 0){
        cerr << "Cannot make frame writeable\n";
        return -1;
    }

    // alloc packet
    AVPacket* pkt = av_packet_alloc();
    if(!pkt){
        cerr << "Cannot allocate packet\n";
        return -1;
    }

    // write header
    if(avformat_write_header(format_ctx, NULL) < 0){
        cerr << "Cannot write header\n";
        return -1;
    }

    // write some
    const int num_output_frame = 500;
    float t = 0;
    float tincr = 2 * M_PI * 440.0f / sample_rate; // 440Hz sine wave
    auto* left_channel = reinterpret_cast<float*>(frame->data[0]);
    auto* right_channel = reinterpret_cast<float*>(frame->data[1]);
    frame->pts = 0;
    for(int i = 0; i < num_output_frame; ++i){
        // generate sine wave
        for(int j = 0; j < frame->nb_samples; ++j){
            left_channel[j] = sin(t);
            right_channel[j] = left_channel[j];

            t += tincr;
        }

        // encode sine wave, and send them to output file
        encode(codec_ctx, frame, pkt, format_ctx, audio_st);
    }

    // flush
    encode(codec_ctx, NULL, pkt, format_ctx, audio_st);

    av_packet_free(&pkt);
    av_frame_free(&frame);
    avcodec_close(audio_st->codec);
    avio_close(format_ctx->pb);
    avformat_free_context(format_ctx);
}

接下来对上述代码进行一些解释与说明

首先,申请并打开容器。avformat_alloc_output_context2 通过文件名后缀来猜测容器类型。

AVFormatContext* format_ctx = NULL;
avformat_alloc_output_context2(&format_ctx, NULL, NULL, output_file.c_str());

接着打开输出文件。

avio_open(&format_ctx->pb, output_file.c_str(), AVIO_FLAG_WRITE)

在容器中创建一条音频流,用于输出编码后的数据。

AVStream* audio_st = avformat_new_stream(format_ctx, 0);

设置编码器的各类参数,包括采样率、通道数、比特率等。需要注意的是,为了简化代码,这里采用 AV_SAMPLE_FMT_FLTP 作为采样格式,但是有些格式(例如 .flac )是不支持 AV_SAMPLE_FMT_FLTP 的,在实际项目中,应该判断当前容器类型是否支持采样格式,判断的代码大家可以在 encode_audio.c 找到灵感,这里不再展开说了。

const int sample_rate = 44100;
const int num_channels = 2;
const int bit_rate = 64000;
AVCodecContext* codec_ctx = audio_st->codec;
codec_ctx->codec_id = format_ctx->oformat->audio_codec;
codec_ctx->codec_type = AVMEDIA_TYPE_AUDIO;
codec_ctx->sample_rate = sample_rate;
codec_ctx->channels = num_channels;
codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP; // planar float
codec_ctx->channel_layout = av_get_default_channel_layout(num_channels);
codec_ctx->bit_rate = bit_rate;

找到并打开编码器。

AVCodec* codec = avcodec_find_encoder(codec_ctx->codec_id);
avcodec_open2(codec_ctx, codec, NULL);

申请 AVFrame 用于存放数据。av_frame_get_buffer 通过 nb_samples, sample_fmt, channel_layout 信息计算需要的内存大小,并进行申请。av_frame_make_writable 保证 AVFrame 是可写的。

AVFrame* frame = av_frame_alloc();
frame->nb_samples = codec_ctx->frame_size != 0 ? codec_ctx->frame_size : 1024;
frame->format = codec_ctx->sample_fmt;
frame->channel_layout = codec_ctx->channel_layout;
av_frame_get_buffer(frame, 0);
av_frame_make_writable(frame);

写文件头。

avformat_write_header(format_ctx, NULL);

生成正弦波,并写入文件。由于采样格式为 AV_SAMPLE_FMT_FLTP,且声道数为 2,因此 frame->data[0] 为左声道,frame->data[1] 为右声道。

auto* left_channel = reinterpret_cast<float*>(frame->data[0]);
auto* right_channel = reinterpret_cast<float*>(frame->data[1]);

for(int j = 0; j < frame->nb_samples; ++j){
    left_channel[j] = sin(t);
    right_channel[j] = left_channel[j];

    t += tincr;
}
encode(codec_ctx, frame, pkt, format_ctx, audio_st);

encode 函数中,将 frame 送去编码,接着通过 avcodec_receive_packet 取出编码后的 packet,最后将 packet 写入文件。

avcodec_send_frame(ctx, frame);
while (ret >= 0) {
    ret = avcodec_receive_packet(ctx, pkt);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        return;
    else if (ret < 0) {
        fprintf(stderr, "Error encoding audio frame\n");
        exit(1);
    }

    pkt->stream_index = audio_st->index;
    if(frame){
        pkt->pts = frame->pts;
        frame->pts += 100;
    }
    av_write_frame(pFormatCtx, pkt);
    av_packet_unref(pkt);
}

进行 flush,把剩余数据一网打尽。

encode(codec_ctx, NULL, pkt, format_ctx, audio_st);

最后别忘记释放内存。

av_packet_free(&pkt);
av_frame_free(&frame);
avcodec_close(audio_st->codec);
avio_close(format_ctx->pb);
avformat_free_context(format_ctx);

总结

我们介绍了如何利用 FFMPEG 进行音频编码。编码与解码是一对逆过程,理解这一概念后,我们给出了编码的具体代码,并对其进行了说明与解释,学会了如何写文件头,如何申请音频帧内存,以及如何编码数据等。

完整代码已上传至 github,欢迎 star:
https://github.com/jiemojiemo/ffmepg_audio_tutorial

转载请注明出处:https://blog.csdn.net/weiwei9363/article/details/109013232

参考资料

  • ffmpeg examples - encode_audio
  • 最简单的基于FFMPEG的音频编码器(PCM编码为AAC)

你可能感兴趣的:(音频处理,ffmpeg,音频编码解码)