使用FFMpeg 解码音频文件

本篇文章将介绍使用FFMpeg解码音频文件为PCM的数据。
使用FFMpeg获取想要的音频数据的步骤如下:
解封装(MP3文件)->解码(MP3编码)->PCM数据重采样

1. 解封装

使用FFMpeg解封装的步骤如下:

  1. 使用函数 av_register_all() 注册所有的封装器和解封装器。
  2. 使用函数 avformat_open_input() 打开一个文件,可以为文件名也可以为一个URL。
  3. 使用函数 avformat_find_stream_info() 查找流信息,把它存入AVFormatContext中。
  4. 查找流信息,获取音频流的索引位置,获取解码器的codec_id
  5. 根据codec_id,使用函数 avcodec_find_decoder() 获取解码器AVCodec*。
  6. 使用函数 avcodec_open2() 打开解码器。

下面是关键部分代码:

bool MusicDecodecThread::openAudioFile(QString fileName)
{
    av_register_all();
    m_AvFrame = av_frame_alloc();
    
    // 打开文件
    int result = avformat_open_input(&m_AVFormatContext, fileName.toLocal8Bit().data(), nullptr, nullptr);
    if (result != 0 || m_AVFormatContext == nullptr)
        return false;

    // 查找流信息,把它存入AVFormatContext中
    if (avformat_find_stream_info(m_AVFormatContext, nullptr) < 0)
        return false;

    int streamsCount = m_AVFormatContext->nb_streams;

    // 读取详细信息
    AVDictionaryEntry *tag = nullptr;
    while (tag = av_dict_get(m_AVFormatContext->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))
    {
        QString keyString = tag->key;
        QString valueString = QString::fromUtf8(tag->value);
        m_InfoMap.insert(keyString, valueString);
    }

    // 查找音频流索引
    for (int i=0; i<streamsCount; ++i)
    {
        if (m_AVFormatContext->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
        {
            AVPacket pkt = m_AVFormatContext->streams[i]->attached_pic;
            m_InfoImage = QImage::fromData((uchar*)pkt.data, pkt.size);
        }
        if (m_AVFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            m_AudioIndex = i;
            continue;
        }
    }

    if (m_AudioIndex == -1)
        return false;

    // 获取总时间
    m_TotalTime = m_AVFormatContext->duration / AV_TIME_BASE * 1000;

    // 查找解码器
    m_AudioCodec = m_AVFormatContext->streams[m_AudioIndex]->codec;
    AVCodec *codec = avcodec_find_decoder(m_AudioCodec->codec_id);
    if (codec == nullptr)
        return false;

    // 打开音频解码器
    if (avcodec_open2(m_AudioCodec, codec, nullptr) != 0)
        return false;

    int rate = m_AudioCodec->sample_rate;
    int channel = m_AudioCodec->channels;
    m_AudioCodec->channel_layout = av_get_default_channel_layout(m_AudioCodec->channels);

    return true;
}

其中: AVFormatContext *m_AVFormatContext = nullptr;
AVCodecContext *m_AudioCodec = nullptr;

AVFormatContext中存储文件封装的信息,比如我们可以使用这个方法获取音频的总时间长度:
m_TotalTime = m_AVFormatContext->duration / AV_TIME_BASE * 1000;

AVCodecContext 中存储了与解码器相关的信息,如
sample_rate:表示采样率
channels: 表示通道数
sample_fmt: 表示采样的格式。(如AV_SAMPLE_FMT_S16、AV_SAMPLE_FMT_S32等)


2. 解码

  1. 使用函数 av_read_frame() 获取一包数据。
  2. 使用函数 avcodec_send_packet() 发送一包数据。
  3. 使用函数 avcodec_receive_frame() 获取一帧数据。

下面是关键部分代码:

while (!this->isInterruptionRequested())
{
    QMutexLocker locker(&m_Mutex);

    AVPacket pkt;
    int result = av_read_frame(m_AVFormatContext, &pkt);
    if (result != 0)
    {
        QThread::msleep(10);
        continue;
    }

    if (pkt.stream_index != m_AudioIndex)
        continue;

    // 解码音频帧, 发送音频包
    if (avcodec_send_packet(m_AudioCodec, &pkt))
        continue;

    // 解码音频帧,接收音频解码帧
    if (avcodec_receive_frame(m_AudioCodec, m_AvFrame))
        continue;

    // 释放包的内存
    av_packet_unref(&pkt);
}

m_AvFrame->data 就存储着解码后的数据。


3. 对解码后的数据重采样

这里使用的FFMpeg提供工具(SwrContext)对音频做重采样。使用方法如下:

  1. 使用方法 swr_alloc(); 创建一个 SwrContext* 类型的指针,并分配内存。
  2. 使用方法 swr_alloc_set_opts() 设置输入和输出的参数。
  3. 使用方法 swr_init() 初始化这个 SwrContext* 指针变量。
  4. 使用方法 swr_convert() 转换。
  5. 使用方法 swr_free() 释放内存。

下面是主要部分代码:

SwrContext *m_SWRtx = swr_alloc();
swr_alloc_set_opts(m_SWRtx, m_AudioCodec->channel_layout, AV_SAMPLE_FMT_S16, \
                           m_AudioCodec->sample_rate, m_AudioCodec->channels, m_AudioCodec->sample_fmt, \
                           m_AudioCodec->sample_rate, 0, 0);
swr_init(m_SWRtx);

uint8_t *array[1];
uint8_t arrays[10000] = {0};
array[0] = arrays;
int len = swr_convert(m_SWRtx, array, 10000, (const uint8_t **)m_AvFrame->data, \
                                                m_AvFrame->nb_samples);

swr_free(&m_SWRtx);

我这里使用线程解码,完整代码如下:
MusicDecodecThread.h

#ifndef MUSCI_DECODEC_THREAD_H
#define MUSCI_DECODEC_THREAD_H
#include <QThread>
#include <QObject>
#include <QMap>
#include <QImage>
#include <QMutex>
#include <QMutexLocker>
#include "AudioPlayThread.h"
extern "C"{
    #include <stdio.h>
    #include <stdlib.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/frame.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    #include <libavfilter/avfilter.h>
    #include <libavfilter/buffersink.h>
    #include <libavfilter/buffersrc.h>
    #include <libavutil/opt.h>
    #include <libavutil/error.h>
}

class MusicDecodecThread : public QThread
{
    Q_OBJECT

public:
    MusicDecodecThread(QObject *parent = nullptr);
    ~MusicDecodecThread();

    // 打开文件
    bool openAudioFile(QString fileName);

    void run(void) override;

    // 获取信息列表中的内容
    QMap<QString, QString> getInfoMap(void);
    // 获取音乐的头像
    QImage getMusicIcon(void);
    // 获取音乐的总时长
    int getTotalTime(void);

private:
    AVFormatContext *m_AVFormatContext = nullptr;
    AVCodecContext *m_AudioCodec = nullptr;
    AVFrame *m_AvFrame;

    int m_AudioIndex = -1;
    int m_TotalTime = 0;

    QMap<QString, QString> m_InfoMap;
    QImage m_InfoImage;

    QMutex m_Mutex;
};

#endif

MusicDecodecThread.cpp

#include "MusicDecodecThread.h"
#include <QDebug>
#include <QTime>

MusicDecodecThread::MusicDecodecThread(QObject *parent)
    :QThread(parent)
{
    av_register_all();
    avfilter_register_all();
    m_AvFrame = av_frame_alloc();
    g_AudioPlayThread->start();
}

MusicDecodecThread::~MusicDecodecThread()
{

}

bool MusicDecodecThread::openAudioFile(QString fileName)
{
    QMutexLocker locker(&m_Mutex);
    if (m_AVFormatContext)
        avformat_close_input(&m_AVFormatContext);

    // 打开文件
    int result = avformat_open_input(&m_AVFormatContext, fileName.toLocal8Bit().data(), nullptr, nullptr);
    if (result != 0 || m_AVFormatContext == nullptr)
        return false;

    // 查找流信息,把它存入AVFormatContext中
    if (avformat_find_stream_info(m_AVFormatContext, nullptr) < 0)
        return false;

    int streamsCount = m_AVFormatContext->nb_streams;

    // 读取详细信息
    AVDictionaryEntry *tag = nullptr;
    while (tag = av_dict_get(m_AVFormatContext->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))
    {
        QString keyString = tag->key;
        QString valueString = QString::fromUtf8(tag->value);
        m_InfoMap.insert(keyString, valueString);
    }

    // 查找音频流索引
    for (int i=0; i<streamsCount; ++i)
    {
        if (m_AVFormatContext->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
        {
            AVPacket pkt = m_AVFormatContext->streams[i]->attached_pic;
            m_InfoImage = QImage::fromData((uchar*)pkt.data, pkt.size);
        }
        if (m_AVFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            m_AudioIndex = i;
            continue;
        }
    }

    if (m_AudioIndex == -1)
        return false;

    // 获取总时间
    m_TotalTime = m_AVFormatContext->duration / AV_TIME_BASE * 1000;

    // 查找解码器
    m_AudioCodec = m_AVFormatContext->streams[m_AudioIndex]->codec;
    AVCodec *codec = avcodec_find_decoder(m_AudioCodec->codec_id);
    if (codec == nullptr)
        return false;

    // 打开音频解码器
    if (avcodec_open2(m_AudioCodec, codec, nullptr) != 0)
        return false;

    int rate = m_AudioCodec->sample_rate;
    int channel = m_AudioCodec->channels;
    m_AudioCodec->channel_layout = av_get_default_channel_layout(m_AudioCodec->channels);
    g_AudioPlayThread->cleanAllAudioBuffer();
    g_AudioPlayThread->setCurrentSampleInfo(rate, 16, channel);

    return true;
}

void MusicDecodecThread::run(void)
{
    QTime time;
    int count = 0;
    while (!this->isInterruptionRequested())
    {
        QMutexLocker locker(&m_Mutex);

        AVPacket pkt;
        int result = av_read_frame(m_AVFormatContext, &pkt);
        if (result != 0)
        {
            QThread::msleep(10);
            continue;
        }

        if (pkt.stream_index != m_AudioIndex)
            continue;

        // 解码视频帧, 发送视频包
        if (avcodec_send_packet(m_AudioCodec, &pkt))
            continue;

        // 解码视频帧,接收视频解码帧
        if (avcodec_receive_frame(m_AudioCodec, m_AvFrame))
            continue;

        SwrContext *m_SWRtx = swr_alloc();
        swr_alloc_set_opts(m_SWRtx, m_AudioCodec->channel_layout, AV_SAMPLE_FMT_S16, \
                           m_AudioCodec->sample_rate, m_AudioCodec->channels, m_AudioCodec->sample_fmt, \
                           m_AudioCodec->sample_rate, 0, 0);
        swr_init(m_SWRtx);

        uint8_t *array[1];
        uint8_t arrays[10000] = {0};
        array[0] = arrays;
        int len = swr_convert(m_SWRtx, array, 10000, (const uint8_t **)m_AvFrame->data, m_AvFrame->nb_samples);

        g_AudioPlayThread->addAudioBuffer((char*)arrays, m_AvFrame->linesize[0]);

        swr_free(&m_SWRtx);

        av_packet_unref(&pkt);
    }
}

QMap<QString, QString> MusicDecodecThread::getInfoMap(void)
{
    QMutexLocker locker(&m_Mutex);
    return m_InfoMap;
}

QImage MusicDecodecThread::getMusicIcon(void)
{
    QMutexLocker locker(&m_Mutex);
    return m_InfoImage;
}

int MusicDecodecThread::getTotalTime(void)
{
    return m_TotalTime;
}
不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:使用FFMpeg 获取MP3文件中的信息和图片