使用FFmpeg API 做音频解码

419 阅读4分钟

开发环境

  • FFmpeg 3.4
  • window
  • Visual Studio 2022

开发过程

1. 使用VS 创建 FFmpegExamples 控制台工程

2. 在VS 里面将FFmpeg库加入依赖环境。

image.png
  • 加入.lib库 将静态库bin目录的.lib文件加入库文件

    1713867281580.jpg
  • 指定静态库bin目录

1713867339470.jpg
  • 拷贝静态库里bin目录里面的dll文件到运行目录
1713867513628.jpg

3. 创建 FileDecode.cpp FileDecode.h 文件

#include <string>
#include <iostream>

extern "C" {
	#include <libavcodec/avcodec.h>
	#include <libavformat/avformat.h>
	#include <libswresample/swresample.h>
	#include <libavutil/avutil.h>
}

class FileDecode
{
public:
	int OpenFile(std::string filename);
	int OpenAudioDecode();
	int Decode();
	void Close();
private:
	int DecodeAudio(AVPacket* originalPacket);
private:
	
	AVFormatContext* formatCtx = NULL;
	AVCodecContext* codecCtx = NULL;

	FILE* outdecodedfile;

	int audioStream;
};

注意上面引入ffmpeg头文件,时候要加入extern,因为ffmpeg是c语言编写的,错误话会找不到符号

3. 编写OpenFile 方法

int FileDecode::OpenFile(std::string filename)
{
    //这里用于保存后面解码后的音频pcm数据
    outdecodedfile = fopen("decode.pcm", "wb");
    if (!outdecodedfile) {
        std::cout << "open out put file failed";
    }
    //打开一个音频文件,这里可以返回一个format上下位
    int openInputResult = avformat_open_input(&formatCtx, filename.c_str(), NULL, NULL);
    if (openInputResult != 0) {
        std::cout << "open input failed" << std::endl;
        return -1;
    }
    //根据format去解复用流信息
    if (avformat_find_stream_info(formatCtx, NULL) < 0) {
        std::cout << "find stram info faild" << std::endl;
        return -1;
    }
    //打印format信息
    av_dump_format(formatCtx, 0, filename.c_str(), 0);


    //查找一些最优的音频流信息,返回音频流序号
    audioStream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audioStream < 0) {
        std::cout << "av find best stream failed" << std::endl;
        return -1;
    }

    return 0;
}

4. 编写OpenAudioDecode 方法

int FileDecode::OpenAudioDecode()
{ 
   //根据fomrat上下文返回音频流的编码山下文codecCtx
    codecCtx = formatCtx->streams[audioStream]->codec;
    
    //根据code上下文返回解码器
    AVCodec* codec = avcodec_find_decoder(codecCtx->codec_id);
    if (codec == NULL) {
        std::cout << "cannot find codec id: " << codecCtx->codec_id << std::endl;
        return -1;
    }

    // 打开解码器
    AVDictionary* dict = NULL;
    int codecOpenResult = avcodec_open2(codecCtx, codec, &dict);
    if (codecOpenResult < 0) {
        std::cout << "open decode faild" << std::endl;
        return -1;
    }
    return 0;
}

5. 编写Decode方法

int FileDecode::Decode()
{
    //创建一个AVPacket 用于放解码前数据
    AVPacket avpkt;
    do {
        
        av_init_packet(&avpkt);
        //读出一帧解码数据
        if (av_read_frame(formatCtx, &avpkt) < 0) {

            //没有读到数据,说明结束了
            return 0;
        }
        if (avpkt.stream_index == audioStream)
        {
            //std::cout << "read one audio frame" << std::endl;
            //送入音频解码方法
            DecodeAudio(&avpkt);
            av_packet_unref(&avpkt);
        }
        else {
            //暂时不处理其他针
            av_packet_unref(&avpkt);
            continue;
        }
    } while (avpkt.data == NULL);
}

6. 编写 DecodeAudio 方法

int FileDecode::DecodeAudio(AVPacket* originalPacket)
{
    //将解码数据送入解码器
    int ret = avcodec_send_packet(codecCtx, originalPacket);
    if (ret < 0)
    {
        return -1;
    }
    AVFrame* frame = av_frame_alloc();
    //从解码器获取解码后的数据
    ret = avcodec_receive_frame(codecCtx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
        return -2;
    }else if (ret < 0) {
        std::cout << "error decoding";
        return -1;
    }
    //返回每个采样占用几个字节
    int data_size = av_get_bytes_per_sample(codecCtx->sample_fmt);
    if (data_size < 0) {
        /* This should not occur, checking just for paranoia */
        std::cout << "Failed to calculate data size\n";
        return -1;
    }
    //将解码后数据写入文件
    for (int i = 0; i < frame->nb_samples; i++)
        for (int ch = 0; ch < codecCtx->channels; ch++)
            fwrite(frame->data[ch] + data_size * i, 1, data_size, outdecodedfile);


    av_frame_free(&frame);

    return 0;
}

7. 编写 Close 方法

void FileDecode::Close()
{
    fclose(outdecodedfile);
  

    avformat_close_input(&formatCtx);
    avcodec_free_context(&codecCtx);
}

8. 主程序调用

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
}
#include "FileDecode.h"

int main() {
    // 注册所有的编解码器、格式和协议
    av_register_all();

    FileDecode fileDecode;
    int ret = fileDecode.OpenFile("oceans.mp4");
    if (ret != 0)
    {
        std::cout << "OpenFile Faild";
    }
    ret = fileDecode.OpenAudioDecode();
    ret = fileDecode.Decode();
    if (ret == 0)
    {
        std::cout << "解码音频完成" << std::endl;
    }
    fileDecode.Close();
    return 0;
}

准备一个视频文件放到一个目录,我的这个oceans.mp4放在了解决方案里面,运行后会生成 decode.pcm文件

9. 可以使用 Audacity 测试这个pcm数据是否正常播放

1713869511496.jpg

这里的采样率,编码位数根据自己实际情况输入

打开后的波形图,点击 三角形开始按钮就可以播放

1713869586921.jpg

9. 总结

程序相对使用基础,但是avformat_open_input 在开开一些音频后,并不是一定能返回正确的媒体信息。在实际的使用场景中,很多是通过其他方式协商出音视频的信息,然后自己在去创建解码器。

av_register_all() 方法要全局注册一次,里面注册编码解码器,以及解复用协议

10. 其他