如何将PCM格式的原始音频采样数据编码为MP3格式或AAC格式的音频文件?

2023-10-22 13:48:07 浏览数 (3)

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

代码语言:javascript复制
//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);
}

二.音频编码器的初始化

代码语言:javascript复制
//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"<<endl;
    }
    else if(strcasecmp(codec_name,"AAC")==0){
        audio_codec_id=AV_CODEC_ID_AAC;
        cout<<"Select codec id:AAC"<<endl;
    }
    else{
        cerr<<"Error:invalid audio format."<<endl;
        return -1;
    }
    codec=avcodec_find_encoder(audio_codec_id);
    if(!codec){
        cerr<<"Error:could not find codec."<<endl;
        return -1;
    }
    codec_ctx=avcodec_alloc_context3(codec);
    if(!codec_ctx){
        cerr<<"Error:could not alloc codec_ctx."<<endl;
        return -1;
    }
    //设置音频编码器的参数
    codec_ctx->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格式进行保存:

代码语言:javascript复制
//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."<<endl;
        return -1;
    }
    for(int i=0;i<frame->nb_samples;i  ){
        for(int ch=0;ch<codec_ctx->channels;ch  ){
            fread(frame->data[ch] i*data_size,1,data_size,input_file);
        }
    }
    return 0;
}

  3.编码音频采样数据

代码语言:javascript复制
//audio_encoder_core.cpp
static int32_t encode_frame(bool flushing){
    int32_t result=0;
    if(!flushing){
        cout<<"Send frame to encoder with pts:"<<frame->pts<<endl;
    }
    result=avcodec_send_frame(codec_ctx,flushing? nullptr:frame);
    if(result<0){
        cerr<<"Error:avcodec_send_frame failed."<<endl;
        return result;
    }
    while(result>=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."<<endl;
            return result;
        }
        if(flushing){
            cout<<"Flushing:";
        }
        cout<<"Got encoded package with dts:"<<pkt->dts<<",pts:"<<pkt->pts<<", "<<endl;
        write_pkt_to_file(pkt);
    }
    return 0;
}

  4.写出码流数据  

代码语言:javascript复制
//io_data.cpp
void write_pkt_to_file(AVPacket* pkt){
    fwrite(pkt->data,1,pkt->size,output_file);
}

  5.实现编码循环

代码语言:javascript复制
//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.关闭编码器

代码语言:javascript复制
//audio_encoder_core.cpp
void destroy_audio_encoder(){
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&codec_ctx);
}

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

代码语言:javascript复制
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文件来测试效果。

0 人点赞