如何将PCM格式的原始音频采样数据编码为MP3格式或AAC格式的音频文件?
阅读原文时间:2023年09月01日阅读:1

一.打开和关闭输入文件和输出文件以及判断输入文件是否读取完毕

//io_data.cpp
static FILE* input_file= nullptr;
static FILE* output_file= nullptr;
int32_t open_input_output_files(const char* input_name,const char* output_name){
if(strlen(input_name)==0||strlen(output_name)==0){
cout<<"Error:empty input or output file name."<<endl;
return -1;
}
close_input_output_files();
input_file=fopen(input_name,"rb");//rb:读取一个二进制文件,该文件必须存在
if(input_file==nullptr){
cerr<<"Error:failed to open input file."<<endl;
return -1;
}
output_file=fopen(output_name,"wb");//wb:打开或新建一个二进制文件,只允许写
if(output_file== nullptr){
cout<<"Error:failed to open output file."<<endl;
return -1;
}
return 0;
}
void close_input_output_files(){
if(input_file!= nullptr){
fclose(input_file);
input_file= nullptr;
}
if(output_file!= nullptr){
fclose(output_file);
output_file= nullptr;
}
}
int32_t end_of_input_file(){
return feof(input_file);
}

二.音频编码器的初始化

//audio_encoder_core.cpp
static const AVCodec* codec= nullptr;
static AVCodecContext* codec_ctx= nullptr;
static AVFrame* frame= nullptr;
static AVPacket* pkt= nullptr;
static enum AVCodecID audio_codec_id;
int32_t init_audio_encoder(const char* codec_name){
if(strcasecmp(codec_name,"MP3")==0){
audio_codec_id=AV_CODEC_ID_MP3;
cout<<"Select codec id:MP3"<bit_rate=128000;
codec_ctx->sample_fmt=AV_SAMPLE_FMT_FLTP;
codec_ctx->sample_rate=44100;
codec_ctx->channel_layout=AV_CH_LAYOUT_STEREO;
codec_ctx->channels=2;

int32\_t result=avcodec\_open2(codec\_ctx,codec, nullptr);  
if(result<0){  
    cerr<<"Error:could not open codec."<<endl;  
    return -1;  
}  
frame=av\_frame\_alloc();  
if(!frame){  
    cerr<<"Error:could not alloc frame."<<endl;  
    return -1;  
}  
frame->nb\_samples=codec\_ctx->frame\_size;//采样点数量  
frame->format=codec\_ctx->sample\_fmt;  
frame->channel\_layout=codec\_ctx->channel\_layout;  
result= av\_frame\_get\_buffer(frame,0);  
if(result<0){  
    cerr<<"Error:AVFrame could not get buffer."<<endl;  
    return -1;  
}  
pkt=av\_packet\_alloc();  
if(!pkt){  
    cerr<<"Error:could not alloc packet."<<endl;  
    return -1;  
}  
return 0;  

}

三.编码循环体

  1.PCM文件的存储结构

    音频采样格式可以分为packed和planar两类。以packed格式保存的采样数据,各声道间按照采样值交替存储;以planar格式保存的采样数据,各个采样值按照不同声道连续存储

    下面以8bit为例展示planar和packed格式是如何保存音频采样数据的:

packed:

左声道0

右声道0

左声道1

右声道1

左声道2

右声道2

左声道3

右声道3

planar:

左声道0

左声道1

左声道2

左声道3

右声道0

右声道1

右声道2

右声道3

  2.读取PCM音频采样数据

    由于我们代码里设置了采样格式为fltp,即planar格式,而输入的PCM音频采样数据是packed格式的,因此我们需要将packed格式转化为planar格式进行保存:

//io_data.cpp
int32_t read_pcm_to_frame(AVFrame* frame,AVCodecContext* codec_ctx){
int data_size= av_get_bytes_per_sample(codec_ctx->sample_fmt);
if(data_size<0){ cerr<<"Error:Failed to calculate data size."<nb_samples;i++){
for(int ch=0;chchannels;ch++){
fread(frame->data[ch]+i*data_size,1,data_size,input_file);
}
}
return 0;
}

  3.编码音频采样数据

//audio_encoder_core.cpp
static int32_t encode_frame(bool flushing){
int32_t result=0;
if(!flushing){
cout<<"Send frame to encoder with pts:"<pts<=0){
result= avcodec_receive_packet(codec_ctx,pkt);
if(result==AVERROR(EAGAIN)||result==AVERROR_EOF){//尚未完成对新一帧的编码,要传入后续帧或编码器已完全输出内部缓存的码流
return 1;
}
else if(result<0){ cerr<<"Error:avcodec_receive_packet failed."<dts<<",pts:"<pts<<", "<<endl;
write_pkt_to_file(pkt);
}
return 0;
}

  4.写出码流数据  

//io_data.cpp
void write_pkt_to_file(AVPacket* pkt){
fwrite(pkt->data,1,pkt->size,output_file);
}

  5.实现编码循环

//audio_encoder_core.cpp
int32_t audio_encoding(){
int32_t result=0;
while(!end_of_input_file()){
result= read_pcm_to_frame(frame,codec_ctx);
if(result<0){
cerr<<"Error:read_pcm_to_frame failed."<<endl;
return -1;
}
result=encode_frame(false);
if(result<0){
cerr<<"Error:encode_frame failed."<<endl;
return result;
}
}
result=encode_frame(true);//刷新缓存区
if(result<0){
cerr<<"Error:flushing failed."<<endl;
return result;
}
return 0;
}

  6.关闭编码器

//audio_encoder_core.cpp
void destroy_audio_encoder(){
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
}

  7.最终main函数的实现如下:

int main(){
const char* input_file_name="../input.pcm";
const char* output_file_name="../output.mp3";
const char* codec_name="MP3";
int32_t result= open_input_output_files(input_file_name,output_file_name);
if(result<0){
return result;
}
result=init_audio_encoder(codec_name);
if(result<0){
return result;
}
result=audio_encoding();
if(result<0){
return result;
}
destroy_audio_encoder();
close_input_output_files();
return 0;
}

  与视频文件类似,可以使用ffplay播放输出的.mp3文件来测试效果。