音频录制流程
FFmpeg对各大平台的音频输入和输出的API进行了统一的封装,将输入API封装成AVInputFormat,将输出API封装成AVOutputFormat,并且将音频采集设备封装成一个音频文件进行处理,所以我们在采集音频数据的时候,可以将音频设备当做一个无限大的音频文件,不断的从音频文件中读取数据。
打开音频输入
注册音频设备
所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。
1
| avdevice_register_all();
|
创建输入上下文
FFmpeg将对音频文件的解封装和封装等操作都抽象到AVFormatContext中,所以音频文件的打开和输出都是要通道AVFormatContext来完成。
1 2 3 4 5 6 7 8 9 10 11
| AVInputFormat* format = av_find_input_format("avfoundation");
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; }
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
| 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;
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_ = 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; const int frame_size_out = codec_ctx_out_->frame_size; while (av_audio_fifo_size(fifo_) < frame_size_out) { if (ReadAndStore(&finished) < 0) { goto cleanup; } if (finished) { break; } }
while (av_audio_fifo_size(fifo_) >= frame_size_out || (finished && av_audio_fifo_size(fifo_) > 0)) { 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
| av_init_packet(&pkt);
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; }
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 { *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
| if ((error = av_audio_fifo_realloc( fifo_, av_audio_fifo_size(fifo_) + input_frame->nb_samples)) < 0) { goto cleanup; }
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();
frame->nb_samples = frame_size;
frame->channel_layout = codec_ctx_out_->channel_layout; frame->format = codec_ctx_out_->sample_fmt; frame->sample_rate = codec_ctx_out_->sample_rate;
if ((error = av_frame_get_buffer(frame, 0)) < 0) { av_frame_free(&frame); goto cleanup; }
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;
av_init_packet(&pkt);
if (frame) { frame->pts = pts; pts += frame->nb_samples; }
if ((error = avcodec_send_frame(codec_ctx_out_, frame)) < 0) { goto cleanup; }
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 { error = 0; *data_written = 1; }
|
写入到输出
1 2 3 4 5
| if (*data_written && (error = av_write_frame(fmt_ctx_out_, &pkt)) < 0) { goto cleanup; }
|