FFmpeg录制音频

音频录制流程

FFmpeg对各大平台的音频输入和输出的API进行了统一的封装,将输入API封装成AVInputFormat,将输出API封装成AVOutputFormat,并且将音频采集设备封装成一个音频文件进行处理,所以我们在采集音频数据的时候,可以将音频设备当做一个无限大的音频文件,不断的从音频文件中读取数据。

打开音频输入

注册音频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();

创建输入上下文

FFmpeg将对音频文件的解封装和封装等操作都抽象到AVFormatContext中,所以音频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
// 设置音频设备API Mac上为avfoundation
AVInputFormat* format = av_find_input_format("avfoundation");
// :0 为音频第一个设备,开启音频设备,FFmpeg中将音频设备封装为一个音频文件
// video:audio 如果视频设备为空,则写成 :音频设备
// 获取到输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, ":0", format, nullptr)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}

获取设备输入数据格式

FFmpeg将音频文件中的每一个轨道都封装成了AVStream对象,一个正常的音频文件一般都包含两个Stream,一个音频流和一个视频流。

对于这里,音频设备只有一个输入流,AVStream中包含了输入音频数据的采样率、采样位数、声道数等音频相关信息

1
2
// 音频设备只有一个输入流
stream_in_ = fmt_ctx_in_->streams[0];

创建解码上下文

由于FFmpeg是将音频设备封装为音频文件来处理的,所以我们从输入流中读取的是AVPacket对象,其data数据是PCM原始音频数据,虽然不解码也可以直接将AVPacket中数据copy到AVFrame中,但最好通过解码转换为AVFrame。

FFmpeg将对音频文件的编码和解码等操作都抽象到AVCodecContext中,所以音频文件的编码和解码都是要通过AVCodecContext来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取音频输入流的编码格式,进行解码
codec = avcodec_find_decoder(stream_in_->codecpar->codec_id);
if (!codec) {
std::cout << "Could not find input codec" << std::endl;
return AVERROR_EXIT;
}
// 创建解码上下文
codec_ctx_in_ = avcodec_alloc_context3(codec);
if (!codec_ctx_in_) {
std::cout << "Could not allocate input codec context" << std::endl;
return AVERROR(ENOMEM);
}
// 将输入流中的解码参数设置给解码上下文
if ((error = avcodec_parameters_to_context(codec_ctx_in_,
stream_in_->codecpar)) < 0) {
std::cout << "Could not copy parameters to input codec" << av_err2str(error)
<< std::endl;
return error;
}
// 开启解码器
if ((error = avcodec_open2(codec_ctx_in_, codec, nullptr)) < 0) {
std::cout << "Could not open input codec " << av_err2str(error)
<< std::endl;
return error;
}

打开音频输出

创建输出上下文

下面是创建了一个aac文件的输出上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
const char* filename = "/Users/gaozhenyu/Desktop/audio.aac";
AVOutputFormat* oformat = av_guess_format(nullptr, filename, nullptr);
// 创建输出上下文
if ((error = avformat_alloc_output_context2(&fmt_ctx_out_, oformat, nullptr,
filename)) < 0) {
std::cout << "Could not allocate output format context" << std::endl;
return error;
}
// 开启输出IO
if ((error = avio_open(&(fmt_ctx_out_->pb), filename, AVIO_FLAG_WRITE)) < 0) {
std::cout << "Could not open output file" << av_err2str(error) << std::endl;
return error;
}

创建指定编码器上下文

使用libfdk_aac编码器来进行编码,并设置音频编码的相关参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 找到libfdk_aac编码器
codec = avcodec_find_encoder_by_name("libfdk_aac");
if (!codec) {
std::cout << "Could not find libfdk_aac encoder" << std::endl;
return AVERROR_EXIT;
}
// 创建输出编码上下文
codec_ctx_out_ = avcodec_alloc_context3(codec);
if (!stream_out_) {
std::cout << "Could not allocate output codec context" << std::endl;
return AVERROR(ENOMEM);
}
// 设置输出编码参数
codec_ctx_out_->sample_fmt = AV_SAMPLE_FMT_S16;
codec_ctx_out_->sample_rate = 48000;
codec_ctx_out_->channel_layout = AV_CH_LAYOUT_STEREO;
codec_ctx_out_->channels = 2;
codec_ctx_out_->bit_rate = 128000;
// codec_ctx_out_->profile = FF_PROFILE_AAC_LD;

// 开启编码器
if ((error = avcodec_open2(codec_ctx_out_, codec, nullptr)) < 0) {
std::cout << "Could not open output codec " << av_err2str(error)
<< std::endl;
return error;
}

创建输出流

上面说过,音频文件中每一个轨道都被封装成一个AVStream,那么想要向输出一个音频文件,除了上面创建了AVFormatContext外,还需要对每一个轨道创建一个AVStream,并添加到对应的输出AVFormatContext上。

这里我们需要输出的轨道只有一个音频轨道,所以创建一个流就可以。创建完流后还需要将上面设置的音频编码相关参数设置给流。

1
2
3
4
5
6
7
8
9
10
// 创建指定编码器的输出音频流,添加到输出上下文中
stream_out_ = avformat_new_stream(fmt_ctx_out_, codec);

// 将编码上下文中的参数设置给输出流
if ((error = avcodec_parameters_from_context(stream_out_->codecpar,
codec_ctx_out_)) < 0) {
std::cout << "Could not initialize output stream parameters "
<< av_err2str(error) << std::endl;
return error;
}

初始化重采样、FIFO队列

重采样上下文

FFmpeg将对音频的重采样操作抽象到SwrContext中,根据输入的音频数据格式和重采样后的音频数据格式来创建一个SwrContext。

实际上决定音频数据格式的就是三要素:采样率、声道数、采样位数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建重采样上下文,设置重采样输入输出参数
swr_ctx_ = swr_alloc_set_opts(
nullptr, av_get_default_channel_layout(codec_ctx_out_->channels),
codec_ctx_out_->sample_fmt, codec_ctx_out_->sample_rate,
av_get_default_channel_layout(codec_ctx_in_->channels),
codec_ctx_in_->sample_fmt, codec_ctx_in_->sample_rate, 0, nullptr);
if (!swr_ctx_) {
std::cout << "Could not allocate swr context" << std::endl;
return AVERROR_EXIT;
}
// 初始化重采样上下文
if ((error = swr_init(swr_ctx_)) < 0) {
std::cout << "Could not open swr context" << av_err2str(error) << std::endl;
swr_free(&swr_ctx_);
return error;
}

FIFO队列

音频数据和视频数据在对一帧数据的定义上不太一样,对于视频来说一帧就是一张图像,每次从设备上采集来的都是一帧数据,而对于音频来说,都是散列的采样点,多少个采样点作为一帧需要看具体使用的编码器的要求。所以从设备上采集来的数据数量并不一定满足编码器需要的采样数量。

所以FFmpeg提供了AVAudioFifo队列来完成对数据的缓存。

1
2
3
4
5
6
7
8
// 创建音频FIFO队列,用来作为缓冲区,音频编码器需要一定的帧数才可以进行编码
// 所以将读取的音频帧存放在FIFO队列中,当大于音频编码器需要的帧数时,才进行编码
fifo_ = av_audio_fifo_alloc(codec_ctx_out_->sample_fmt,
codec_ctx_out_->channels, 1);
if (!fifo_) {
std::cout << "Could not allocate FIFO" << std::endl;
return AVERROR_EXIT;
}

录制

对于AVFormatContext来进行音频数据的输出,需要调用三个write函数,avformat_write_header、av_write_frame、av_write_trailer。

整体流程就是不断的从音频设备中读取数据,解码后进行重采样,将重采样结果放入到FIFO队列中,当FIFO中数据足够时,则从FIFO中取出数据进行编码,编码后写入到输出文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 if (avformat_write_header(fmt_ctx_out_, nullptr) < 0) {
std::cout << "Could not write output file header" << std::endl;
goto cleanup;
}

while (request_abort_) {
int finished = 0;
// 一个音频帧中每个通道的样本数。
// 编码:由libavcodec在avcodec_open2()中设置。
// 除最后一个帧外,每个提交的帧必须在每个通道中完全包含frame_size样本。
// 解码:可能由某些解码器设置以指示恒定的帧大小
const int frame_size_out = codec_ctx_out_->frame_size;
// 如果FIFO队列中音频样本数不足编码器需要的样本数,则循环读取数据,直到FIFO中数据满足需要的样本量
while (av_audio_fifo_size(fifo_) < frame_size_out) {
// 读取音频样本加入到FIFO队列中
if (ReadAndStore(&finished) < 0) {
// 数据读取失败,则直接退出
goto cleanup;
}
// 如果输入数据读取完了,则最后一帧的样本数可以小于指定的样本数
if (finished) {
// 跳出
break;
}
}

// 如果FIFO队列中音频样本数大于编码器需要的 或者
// 输入数据读取完但是FIFO中还有未处理的数据
// 则循环不断处理,直到FIFO中数据不足一帧 或者 数据全部处理完成 才退出
while (av_audio_fifo_size(fifo_) >= frame_size_out ||
(finished && av_audio_fifo_size(fifo_) > 0)) {
// 从FIFO中取出数据进行编码并写入到输出文件中
if (EncodeAndWrite() < 0) {
// 编码或写入失败,则直接退出
goto cleanup;
}
}

// 输入数据读取完,但是编码器中可能还有最后几帧数据还在编码,所以需要重新从编码器中读取
if (finished) {
int data_written;
do {
data_written = 0;
// 传入空的数据,进行编码,如果编码器中有数据,则继续
if (EncodeAudioFrame(nullptr, &data_written) < 0) {
goto cleanup;
}
} while (data_written);
break;
}
}

if (av_write_trailer(fmt_ctx_out_) < 0) {
std::cout << "Could not write output file trailer" << std::endl;
goto cleanup;
}

读取数据并解码

读取数据非常简单,创建一个AVPacket对象,通过av_read_frame就可以读取一帧数据

1
2
3
4
// 创建AVPacket,用来从音频设备中接收一帧数据
av_init_packet(&pkt);
// 从音频设备中读取一帧数据到AVPacket中
av_read_frame(fmt_ctx_in_, &pkt);

通过avcodec_send_packet、avcodec_receive_frame来进行解码,avcodec_send_packet负责将一个AVPacket送给解码器,avcodec_receive_frame负责从解码器中取出一个已经解码好的AVFrame,如果返回AVERROR(EAGAIN)表明当前没有已经解码好的数据,返回AVERROR_EOF表明已经全部解码完成,返回大于0则表明获取解码数据成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 // 发送到解码器中进行解码
if ((error = avcodec_send_packet(codec_ctx_in_, &pkt)) < 0) {
goto cleanup;
}
// 从解码器中获取一帧解码后的数据,存到AVFrame中
error = avcodec_receive_frame(codec_ctx_in_, frame);
if (error == AVERROR(EAGAIN)) {
// 没有获取到数据
error = 0;
goto cleanup;
} else if (error == AVERROR_EOF) {
// 解码器中数据已经全部解码完成
error = 0;
*finished = 1;
} else if (error < 0) {
// 解码错误
goto cleanup;
} else {
// 数据成功解码到AVFrame中
*data_present = 1;
error = 0;
}

重采样

创建缓冲区,用来存放重采样后的数据

1
2
3
4
5
6
7
if ((error = av_samples_alloc_array_and_samples(
data, nullptr, codec_ctx_out_->channels, nb_samples,
codec_ctx_out_->sample_fmt, 0)) < 0) {
av_freep(*data[0]);
free(*data);
return error;
}

将AVFrame中的数据重采样到上面创建的缓冲区中

1
2
3
if ((error = swr_convert(swr_ctx_, dst, nb_samples, src, nb_samples)) < 0) {
return error;
}

入队列

将重采样后的数据放入到缓冲队列中

1
2
3
4
5
6
7
8
9
10
11
12
// 重新分配FIFO队列的大小,新的大小=已有的大小加上当前音频帧的样本量
if ((error = av_audio_fifo_realloc(
fifo_, av_audio_fifo_size(fifo_) + input_frame->nb_samples)) < 0) {
goto cleanup;
}
// 将重采样数据写入到FIFO中,如果写入的样本量小于当前帧的样本量,则表明写入有错误
if (av_audio_fifo_write(fifo_, (void**)converted_input_samples,
input_frame->nb_samples) <
input_frame->nb_samples) {
error = AVERROR_EXIT;
goto cleanup;
}

出队列

当队列中采样数据量满足编码器需要的大小后,从队列中取出需要的采样数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AVFrame* frame = av_frame_alloc();
// 用来接收FIFO中数据
// 将AVFrame的样本量,设置为当前会读取的样本量
frame->nb_samples = frame_size;
// 设置AVFrame的参数
frame->channel_layout = codec_ctx_out_->channel_layout;
frame->format = codec_ctx_out_->sample_fmt;
frame->sample_rate = codec_ctx_out_->sample_rate;

// 初始化AVFrame中的数据缓冲区
if ((error = av_frame_get_buffer(frame, 0)) < 0) {
av_frame_free(&frame);
goto cleanup;
}

// 从FIFO中读取数据到AVFrame的缓冲区中,如果读取的样本数小于预定的样本数,则说明读取出错
if (av_audio_fifo_read(fifo_, reinterpret_cast<void**>(frame->data),
frame_size) < frame_size) {
error = AVERROR_EXIT;
goto cleanup;
}

编码数据并输出

通过avcodec_send_frame、avcodec_receive_packet来进行编码。avcodec_send_frame负责将一个AVFrame帧发送给编码器,avcodec_receive_packet负责从编码器中取出一帧已经编码好的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
AVPacket pkt;
// 创建AVPacket,接收编码后的数据
av_init_packet(&pkt);

// 如果待编码数据不为空,则累计计算当前帧的pts
if (frame) {
frame->pts = pts;
// 这里用样本量的个数来计算pts
// 假设第一帧的样本量为1024,则第一帧的pts=1024,第二帧的样本量也是1024,则第二帧的pts=2048
pts += frame->nb_samples;
}
// 发送AVFrame到编码器中进行编码
if ((error = avcodec_send_frame(codec_ctx_out_, frame)) < 0) {
// 发送失败
goto cleanup;
}
// 接收编码后的数据到AVPacket中
error = avcodec_receive_packet(codec_ctx_out_, &pkt);
if (error == AVERROR(EAGAIN)) {
// 没有获取到编码后数据
error = 0;
goto cleanup;
} else if (error == AVERROR_EOF) {
// 编码器中所有数据编码完成
error = 0;
goto cleanup;
} else if (error < 0) {
// 编码出错
goto cleanup;
} else {
// 成功获取编码后数据到AVPacket中
error = 0;
*data_written = 1;
}

写入到输出

1
2
3
4
5
// 如果获取数据,则写入到输出上下文中
if (*data_written && (error = av_write_frame(fmt_ctx_out_, &pkt)) < 0) {
// 写入失败
goto cleanup;
}