【Qt】FFmpeg提取音頻數據進行重采樣保存為pcm格式文件,并顯示提取進度

概念

這里有些重要的概念需要有必要先理解一下:

  1. 音頻重采樣
    音頻都是有個固定的采樣率,一般是44100Hz, 如果需要改變這個采樣率,比如8000Hz,那么就必須進行重采樣操作。
  2. 音頻雙通道
    雙通道的音頻數據在存儲的時候,需要交錯格式存儲,很多文章說明的是這樣形容的:LRLRLRLR...,這樣的,一開始我沒理解過來,其實L就是左聲道,R就是右聲道,就是說存儲的時候要先存一個左聲道數據,然后再存儲一個右聲道數據。

處理流程

由于需要異步處理音頻提取,并在界面展示進度,所以大致流程是這樣的:

  1. 開啟等待處理線程
  2. 輸入文件,通過ffmpeg讀取音頻幀數據,解碼音頻數據幀,進行數據重采樣處理,輸入寫入文件
  3. 異步通知當前進度,展示到進度條。

關鍵代碼

如下代碼都是基于Qt5,使用C++寫的。

頭文件定義


#include <QThread>
#include <QMutex>
#include <QWaitCondition>
extern "C" {
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libswresample/swresample.h>
}

class AudioPCMExtractor : public QThread
{
    Q_OBJECT
public:
    explicit AudioPCMExtractor(QObject *parent = nullptr);
    ~AudioPCMExtractor();

    void doExtract(int chan, int rate, QString src, QString dst);
    void stop();

signals:
    void procgress(int p);

protected:
    void run() override;

private:
    void setup();
    int setupOut();
    void process();
    void release();

    bool running;
    QWaitCondition wait_cond_;
    QMutex wait_lock_;
    QAtomicInt queued;

    int outChannel;           // 重采樣后輸出的通道
    AVSampleFormat outFormat; // 重采樣后輸出的格式
    int outSampleRate;        // 重采樣后輸出的采樣率
    QString outFileFormat;

    QString src_filePath; //目標源文件
    QString dst_filePath; //目的文件

    AVFormatContext *pAVFormatContext; // ffmpeg的全局上下文
    AVCodecContext *pAVCodecContext;   // ffmpeg編碼上下文
    AVFrame *pAVFrame;                 // ffmpeg單幀緩存
    AVPacket *pkt;
    AVCodec *pAVCodec;       // ffmpeg編碼器
    SwrContext *pSwrContext; // ffmpeg音頻轉碼
    int audio_index;
    float duration;
    int64_t bit_rate;

    AVFormatContext *pAVFormatContext_out;
    AVCodecContext *pAVCodecContext_out;
    AVStream *pAVStream_out;
    AVCodec *pAVCodec_out;
};
  1. 開啟異步線程,進程任務等待, 初始化對象之后,就進入死循環的等待狀態。
AudioPCMExtractor::AudioPCMExtractor(QObject *parent) : QThread(parent)
{
    pkt = nullptr;
    pSwrContext = nullptr;
    pAVFrame = nullptr;
    pAVCodecContext = nullptr;
    pAVFormatContext = nullptr;

    audio_index = 0;
    queued = false;
    running = true;
    
    //預設為16位采樣
    outFormat = AV_SAMPLE_FMT_S16P;
    
    //開啟等待線程
    start(QThread::NormalPriority);
}

void AudioPCMExtractor::stop()
{
    running = false;
    wait_cond_.wakeAll();
}

void AudioPCMExtractor::run()
{
    wait_lock_.lock();
    while (running)
    {
        //等待
        wait_cond_.wait(&wait_lock_);
        if (!running)
        {
            break;
        }

        process();

        queued = false;
    }

    wait_lock_.unlock();
}
  1. 外部調用接口,告訴當前需要進行提取的文件路徑,以及提取后的保存文件路徑, 并進行FFmpeg打開對應的解碼器等
void AudioPCMExtractor::doExtract(int chan, int rate, QString src, QString dst)
{
    if (queued)
    {
        return;
    }

    queued = true;
    src_filePath = src;
    dst_filePath = dst;
    outChannel = chan;
    outSampleRate = rate;
    setup();
    wait_cond_.wakeAll();
}

void AudioPCMExtractor::setup()
{
    int ret = 0;
    char errors[1024] = {0};

    //打開媒體,并讀取頭部信息
    int errCode = avformat_open_input(&pAVFormatContext, src_filePath.toUtf8().data(), nullptr, nullptr);
    if (errCode != 0)
    {
        av_strerror(errCode, errors, 1024);
        qCritical() << "Could not open 1" << src_filePath << "-" << errors;
        return;
    }

    //找到音頻流
    audio_index = av_find_best_stream(pAVFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audio_index < 0)
    {
        av_strerror(errCode, errors, 1024);
        qCritical() << "Could not open 2" << src_filePath << "-" << errors;
        avformat_free_context(pAVFormatContext);
        return;
    }
    AVStream *in_stream = pAVFormatContext->streams[audio_index];
    duration = in_stream->duration;

    //找到對應的解碼器
    pAVCodec = avcodec_find_decoder(in_stream->codecpar->codec_id);
    if (pAVCodec == nullptr)
    {
        qDebug() << "avcodec_find_decoder fail";
        avformat_free_context(pAVFormatContext);
        return;
    }
    pAVCodecContext = avcodec_alloc_context3(pAVCodec);
    avcodec_parameters_to_context(pAVCodecContext, in_stream->codecpar);
    bit_rate = pAVCodecContext->bit_rate;

    //打開解碼器
    ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
    if (ret != 0)
    {
        qDebug() << "avcodec_open2 fail";
        avformat_free_context(pAVFormatContext);
        return;
    }
    pSwrContext = swr_alloc_set_opts(nullptr, // 輸入為空,則會分配
                                     av_get_default_channel_layout(outChannel),
                                     outFormat,     // 輸出的采樣頻率
                                     outSampleRate, // 輸出的格式
                                     av_get_default_channel_layout(pAVCodecContext->channels),
                                     pAVCodecContext->sample_fmt,  // 輸入的格式
                                     pAVCodecContext->sample_rate, // 輸入的采樣率
                                     0,
                                     nullptr);
    swr_init(pSwrContext);

    pkt = av_packet_alloc();
    av_init_packet(pkt);
    pAVFrame = av_frame_alloc();
}
  1. 喚醒線程,并讀取數據幀,解碼幀數據,并進行重采樣,保存音頻數據到文件
void AudioPCMExtractor::process()
{
    QFile file(dst_filePath);
    file.open(QIODevice::WriteOnly | QIODevice::Truncate);

    //獲取單次采樣的字節數
    int numBytes = av_get_bytes_per_sample(outFormat);

   //申請通道數據內存
    uint8_t *outData[outChannel];
    for (int i = 0; i < outChannel; i++)
    {
        outData[i] = (uint8_t *)av_malloc(192000);
    }

    float pcount = 0;
    int lastPersent = 0;
    //讀取音頻幀數據
    while (av_read_frame(pAVFormatContext, pkt) >= 0)
    {
        if (pkt->stream_index == audio_index)
        {

            //解碼音頻
            int ret = Decode(pAVCodecContext, pAVFrame, pkt);
            if (ret < 0)
            {
                break;
            }

            //重采樣應輸出采樣數
            int dstNbSamples = av_rescale_rnd(pAVFrame->nb_samples,
                                              outSampleRate,
                                              pAVCodecContext->sample_rate,
                                              AV_ROUND_ZERO);
            //進行轉碼,并返回實際采樣數
            int out_samples = swr_convert(pSwrContext,
                                          outData,
                                          dstNbSamples,
                                          (const uint8_t **)pAVFrame->data,
                                          pAVFrame->nb_samples);

            if (out_samples > 0)
            {
                //多聲道數據合并
                for (int index = 0; index < out_samples; index++)
                {
                    for (int channel = 0; channel < outChannel; channel++)
                        file.write((const char *)outData[channel] + numBytes * index, numBytes);
                }
            }

            pcount += pkt->duration;
        }

        int persent = qFloor(pcount * 100 / duration);
        if (persent > lastPersent && persent <= 100)
        {
            lastPersent = persent;

            //發送信號,通知當前提取進度
            emit procgress(persent);
        }

        av_packet_unref(pkt);
    }

    file.close();

    for (int i = 0; i < outChannel; i++)
        av_free(outData[i]);
    release();
    qDebug() << "extract finish";
}

//釋放資源
void AudioPCMExtractor::release()
{
    if (pkt)
        av_packet_free(&pkt);
    if (pSwrContext)
        swr_free(&pSwrContext);
    if (pAVFrame)
        av_frame_free(&pAVFrame);
    if (pAVCodecContext)
        avcodec_close(pAVCodecContext);
    if (pAVFormatContext)
        avformat_free_context(pAVFormatContext);

    pkt = nullptr;
    pSwrContext = nullptr;
    pAVFrame = nullptr;
    pAVCodecContext = nullptr;
    pAVFormatContext = nullptr;
}
  1. 音頻解碼
int Decode(AVCodecContext *pAVCodecContext, AVFrame* pAVFrame, AVPacket *pkt){
    int ret = 0;
    av_frame_unref(pAVFrame);

    while ((ret = avcodec_receive_frame(pAVCodecContext, pAVFrame)) == AVERROR(EAGAIN)){
        ret = avcodec_send_packet(pAVCodecContext, pkt);
        if (ret < 0) {
            qCritical() << "Failed to send packet to decoder." << ret;
            break;
        }
    }

    if(ret < 0 && ret != AVERROR_EOF){
        qDebug() << "Failed to receive packet from decoder." << ret;
    }

    return ret;
}

存儲的時候要特別注意這段數據合并的代碼,也就是LRLRLRLR的交錯存儲格式

//多聲道數據合并
for (int index = 0; index < out_samples; index++)
 {
     for (int channel = 0; channel < outChannel; channel++)
         file.write((const char *)outData[channel] + numBytes * index, numBytes);
 }

完整代碼

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容