- 基于 FFMPEG 的音频编解码(一):Hello FFMPEG,安装与编译
- 基于 FFMPEG 的音频编解码(二):音频解码
- 基于 FFMPEG 的音频编解码(三):音频编码
在前面文章中,我们了解如何用 FFMPEG 进行音频解码,今天我们来讨论音频编码
一个很重要的概念是,编码与解码是一对逆过程。 回想一下,解码的时候我们是如何做的?下面代码是解码的关键部分:
av_read_frame(ctx, packet);
avcodec_send_packet(ctx, packet);
avcodec_receive_frame(ctx, frame);
av_read_frame
从文件中读取一个 packet
avcodec_send_packet
将 packet
送去解码avcodec_receive_frame
取回解码好的数据编码的过程刚好是解码的逆过程,下面代码是编码的关键部分:
fill_data_to_frame(frame);
avcodec_send_frame(ctx, frame);
avcodec_receive_packet(ctx, packet);
fill_data_to_frame
将数据填充至 frame
中avcodec_send_frame
将 frame
送去编码avcodec_receive_packet
取回编码好的数据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