前言
測試環境:
- ffmpeg的4.3.2自行編譯版本
- windows環境
- qt5.12
AAC編碼是MP3格式的后繼產品,通常在相同的比特率下可以獲得比MP3更高的聲音質量,是iPhone、iPod、iPad、iTunes的標準音頻格式。
AAC相較于MP3的改進包含:
- 更多的采樣率選擇:8kHz ~ 96kHz,MP3為16kHz ~ 48kHz
- 更高的聲道數上限:48個,MP3在MPEG-1模式下為最多雙聲道,MPEG-2模式下5.1聲道
- 改進的壓縮功能:以較小的文件大小提供更高的質量
- 改進的解碼效率:需要較少的處理能力進行解碼
- …
AAC編碼為了使用不同場景的需求,設計了很多規格
- MPEG-2 AAC LC:低復雜度規格(Low Complexity)
- MPEG-2 AAC Main:主規格
- MPEG-2 AAC SSR:可變采樣率規格(Scaleable Sample Rate)
- MPEG-4 AAC LC:低復雜度規格(Low Complexity)
- 現在的手機比較常見的MP4文件中的音頻部分使用了該規格
- MPEG-4 AAC Main:主規格
- MPEG-4 AAC SSR:可變采樣率規格(Scaleable Sample Rate)
- MPEG-4 AAC LTP:長時期預測規格(Long Term Predicition)
- MPEG-4 AAC LD:低延遲規格(Low Delay)
- MPEG-4 AAC HE:高效率規格(High Efficiency)
眾多規格中只需關注LC和HE
pcm與aac的轉換需要AAC編解碼器(如下列舉幾種常用的AAC編解碼器)
- Nero AAC
- 支持LC/HE規格
- 目前已經停止開發維護
- FFmpeg AAC
- 支持LC規格
- FFmpeg官方內置的AAC編解碼器,在libavcodec庫中
- 編解碼器名字叫做aac
- 在開發過程中通過這個名字找到編解碼器
- FAAC(Freeware Advanced Audio Coder)
- 支持LC規格
- 可以集成到FFmpeg的libavcodec中
- 編解碼器名字叫做libfaac
- 在開發過程中通過這個名字找到編解碼器,最后調用FAAC庫的功能
- 從2016年開始,FFmpeg已經移除了對FAAC的支持
- Fraunhofer FDK AAC
- 支持LC/HE規格
- 目前質量最高的AAC編解碼器
- 可以集成到FFmpeg的libavcodec中
- 編解碼器名字叫做libfdk_aac
- 在開發過程中通過這個名字找到編解碼器,最后調用FDK AAC庫的功能
編碼質量排名:Fraunhofer FDK AAC > FFmpeg AAC > FAAC。
由于libfdk_aac最好,但是網上下載好的ffmpeg編譯好的版本不帶libfdk_aac編解碼器。所以我們只能自行編譯ffmpeg。
如下命令可以查看FFmpeg目前集成的AAC編解碼器
ffmpeg -codecs | findstr aac
自己手動編譯FFmpeg源碼,將libfdk_aac集成到FFmpeg中,這種方式最好,但在windows環境下較為麻煩。
因為編譯源碼需要在類Unix系統上的(Linux、Mac等),默認無法直接用在Windows上。所以必須先用MSYS2軟件在Windows上模擬出Linux環境,然后再在其中用MinGW軟件對FFmpeg進行編譯。
鏈接:windows下msys2編譯64位的ffmpeg源碼
編譯好源碼后,需要把.pro文件配置成新編譯的源碼。
fdk-aac對需要編解碼的pcm音頻有一定的格式要求
- 采樣格式必須為16位整數PCM
- 采樣率只支持:8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、88200、96000
命令行將pcm和wav文件編碼成aac音頻
# pcm -> aac
ffmpeg -ar 44100 -ac 2 -f s16le -i in.pcm -c:a libfdk_aac out.aac
-ar 44100 -ac 2 -f s16le --PCM輸入數據的參數
-c:a 設置音頻編碼器,c表示codec(編解碼器),a表示audio(音頻)。 等價寫法 -codec:a或-acodec# wav -> aac
ffmpeg -i in.wav -c:a libfdk_aac out.aac
默認生成的aac文件是LC規格的。aac文件比之前的pcm文件小了很多很多。
aac的縮寫還可以是m4a和mp4。雖然現在都只認為mp4是視頻文件
首先是pcm編碼為aac
完整代碼
AacEncodeThread.h
#ifndef AACENCODETHREAD_H
#define AACENCODETHREAD_H#include <QFile>
#include <QObject>
#include <QThread>extern "C" {
#include <libavformat/avformat.h>
}typedef struct {const char *filename;int sampleRate;AVSampleFormat sampleFmt;int chLayout;
} AudioEncodeSpec;class AacEncodeThread : public QThread
{Q_OBJECT
public:explicit AacEncodeThread(QObject *parent = nullptr);~AacEncodeThread();static int check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt);static int encode(AVCodecContext *ctx,AVFrame *frame,AVPacket *pkt,QFile &outFile);static void aacEncode(AudioEncodeSpec &in,const char *outFilename);signals:// QThread interface
protected:virtual void run() override;
};#endif // AACENCODETHREAD_H
AacEncodeThread.cpp
#include "aacencodethread.h"#include <QDebug>
#include <QFile>extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}#define ERROR_BUF(ret) \char errbuf[1024]; \av_strerror(ret, errbuf, sizeof (errbuf));AacEncodeThread::AacEncodeThread(QObject *parent) : QThread(parent)
{// 當監聽到線程結束時(finished),就調用deleteLater回收內存connect(this, &AacEncodeThread::finished,this, &AacEncodeThread::deleteLater);
}AacEncodeThread::~AacEncodeThread()
{// 斷開所有的連接disconnect();// 內存回收之前,正常結束線程requestInterruption();// 安全退出quit();wait();qDebug() << this << "析構(內存被回收)";
}// 檢查采樣格式
int AacEncodeThread::check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt) {const enum AVSampleFormat *p = codec->sample_fmts;while (*p != AV_SAMPLE_FMT_NONE) {
// qDebug() << av_get_sample_fmt_name(*p);if (*p == sample_fmt) return 1;p++;}return 0;
}// 音頻編碼
// 返回負數:中途出現了錯誤
// 返回0:編碼操作正常完成
int AacEncodeThread::AacEncodeThread::encode(AVCodecContext *ctx,AVFrame *frame,AVPacket *pkt,QFile &outFile) {// 發送數據到編碼器int ret = avcodec_send_frame(ctx, frame);if (ret < 0) {ERROR_BUF(ret);qDebug() << "avcodec_send_frame error" << errbuf;return ret;}// 不斷從編碼器中取出編碼后的數據// while (ret >= 0)while (true) {ret = avcodec_receive_packet(ctx, pkt);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {// 繼續讀取數據到frame,然后送到編碼器return 0;} else if (ret < 0) { // 其他錯誤return ret;}// 成功從編碼器拿到編碼后的數據// 將編碼后的數據寫入文件outFile.write((char *) pkt->data, pkt->size);// 釋放pkt內部的資源av_packet_unref(pkt);}
}void AacEncodeThread::aacEncode(AudioEncodeSpec &in, const char *outFilename)
{// 文件QFile inFile(in.filename);QFile outFile(outFilename);// 返回結果int ret = 0;// 編碼器AVCodec *codec = nullptr;// 編碼上下文AVCodecContext *ctx = nullptr;// 存放編碼前的數據(pcm)AVFrame *frame = nullptr;// 存放編碼后的數據(aac)AVPacket *pkt = nullptr;// 獲取編碼器
// codec = avcodec_find_encoder(AV_CODEC_ID_AAC);codec = avcodec_find_encoder_by_name("libfdk_aac");if (!codec) {qDebug() << "encoder not found";return;}// libfdk_aac對輸入數據的要求:采樣格式必須是16位整數// 檢查輸入數據的采樣格式if (!check_sample_fmt(codec, in.sampleFmt)) {qDebug() << "unsupported sample format"<< av_get_sample_fmt_name(in.sampleFmt);return;}// 創建編碼上下文ctx = avcodec_alloc_context3(codec);if (!ctx) {qDebug() << "avcodec_alloc_context3 error";return;}// 設置PCM參數ctx->sample_rate = in.sampleRate;ctx->sample_fmt = in.sampleFmt;ctx->channel_layout = in.chLayout;// 比特率ctx->bit_rate = 32000;// 規格ctx->profile = FF_PROFILE_AAC_HE_V2;// 打開編碼器
// AVDictionary *options = nullptr;
// av_dict_set(&options, "vbr", "5", 0);
// ret = avcodec_open2(ctx, codec, &options);ret = avcodec_open2(ctx, codec, nullptr);if (ret < 0) {ERROR_BUF(ret);qDebug() << "avcodec_open2 error" << errbuf;goto end;}// 創建AVFrameframe = av_frame_alloc();if (!frame) {qDebug() << "av_frame_alloc error";goto end;}// frame緩沖區中的樣本幀數量(由ctx->frame_size決定)frame->nb_samples = ctx->frame_size;frame->format = ctx->sample_fmt;frame->channel_layout = ctx->channel_layout;// 利用nb_samples、format、channel_layout創建緩沖區ret = av_frame_get_buffer(frame, 0);if (ret < 0) {ERROR_BUF(ret);qDebug() << "av_frame_get_buffer error" << errbuf;goto end;}// 創建AVPacketpkt = av_packet_alloc();if (!pkt) {qDebug() << "av_packet_alloc error";goto end;}// 打開文件if (!inFile.open(QFile::ReadOnly)) {qDebug() << "file open error" << in.filename;goto end;}if (!outFile.open(QFile::WriteOnly)) {qDebug() << "file open error" << outFilename;goto end;}// 讀取數據到frame中while ((ret = inFile.read((char *) frame->data[0],frame->linesize[0])) > 0) {// 從文件中讀取的數據,不足以填滿frame緩沖區if (ret < frame->linesize[0]) {int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);int ch = av_get_channel_layout_nb_channels(frame->channel_layout);// 設置真正有效的樣本幀數量// 防止編碼器編碼了一些冗余數據frame->nb_samples = ret / (bytes * ch);}// 進行編碼if (encode(ctx, frame, pkt, outFile) < 0) {goto end;}}// 刷新緩沖區encode(ctx, nullptr, pkt, outFile);end:// 關閉文件inFile.close();outFile.close();// 釋放資源av_frame_free(&frame);av_packet_free(&pkt);avcodec_free_context(&ctx);qDebug() << "線程正常結束";
}void AacEncodeThread::run()
{AudioEncodeSpec in;in.filename = "E:/media/test.pcm";in.sampleRate = 44100;in.sampleFmt = AV_SAMPLE_FMT_S16;in.chLayout = AV_CH_LAYOUT_STEREO;aacEncode(in, "E:/media/test.aac");
}
線程調用:
void MainWindow::on_pushButton_aac_encode_clicked()
{m_pAacEncodeThread=new AacEncodeThread(this);m_pAacEncodeThread->start();
}
注意:.h文件中提前聲明了以下全局變量
AacEncodeThread *m_pAacEncodeThread=nullptr;
下面是aac解碼成pcm
完整代碼
AacDecodeThread.h
#ifndef AACDECODETHREAD_H
#define AACDECODETHREAD_H#include <QFile>
#include <QObject>
#include <QThread>extern "C" {
#include <libavformat/avformat.h>
}typedef struct {const char *filename;int sampleRate;AVSampleFormat sampleFmt;int chLayout;
} AudioDecodeSpec;class AacDecodeThread : public QThread
{Q_OBJECT
public:explicit AacDecodeThread(QObject *parent = nullptr);~AacDecodeThread();static int decode(AVCodecContext *ctx,AVPacket *pkt,AVFrame *frame,QFile &outFile);static void aacDecode(const char *inFilename,AudioDecodeSpec &out);signals:// QThread interface
protected:virtual void run() override;
};#endif // AACDECODETHREAD_H
AacDecodeThread.cpp
#include "aacdecodethread.h"#include <QDebug>extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}#define ERROR_BUF(ret) \char errbuf[1024]; \av_strerror(ret, errbuf, sizeof (errbuf));// 輸入緩沖區的大小
#define IN_DATA_SIZE 20480
// 需要再次讀取輸入文件數據的閾值
#define REFILL_THRESH 4096AacDecodeThread::AacDecodeThread(QObject *parent) : QThread(parent)
{// 當監聽到線程結束時(finished),就調用deleteLater回收內存connect(this, &AacDecodeThread::finished,this, &AacDecodeThread::deleteLater);
}AacDecodeThread::~AacDecodeThread()
{// 斷開所有的連接disconnect();// 內存回收之前,正常結束線程requestInterruption();// 安全退出quit();wait();qDebug() << this << "析構(內存被回收)";
}int AacDecodeThread::decode(AVCodecContext *ctx,AVPacket *pkt,AVFrame *frame,QFile &outFile) {// 發送壓縮數據到解碼器int ret = avcodec_send_packet(ctx, pkt);if (ret < 0) {ERROR_BUF(ret);qDebug() << "avcodec_send_packet error" << errbuf;return ret;}while (true) {// 獲取解碼后的數據ret = avcodec_receive_frame(ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;} else if (ret < 0) {ERROR_BUF(ret);qDebug() << "avcodec_receive_frame error" << errbuf;return ret;}// for (int i = 0; i < frame->channels; i++) {
// frame->data[i];
// }// 將解碼后的數據寫入文件outFile.write((char *) frame->data[0], frame->linesize[0]);}
}void AacDecodeThread::aacDecode(const char *inFilename, AudioDecodeSpec &out)
{// 返回結果int ret = 0;// 用來存放讀取的輸入文件數據(aac)// 加上AV_INPUT_BUFFER_PADDING_SIZE是為了防止某些優化過的reader一次性讀取過多導致越界char inDataArray[IN_DATA_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];char *inData = inDataArray;// 每次從輸入文件中讀取的長度(aac)int inLen;// 是否已經讀取到了輸入文件的尾部int inEnd = 0;// 文件QFile inFile(inFilename);QFile outFile(out.filename);// 解碼器AVCodec *codec = nullptr;// 上下文AVCodecContext *ctx = nullptr;// 解析器上下文AVCodecParserContext *parserCtx = nullptr;// 存放解碼前的數據(aac)AVPacket *pkt = nullptr;// 存放解碼后的數據(pcm)AVFrame *frame = nullptr;// 獲取解碼器codec = avcodec_find_decoder_by_name("libfdk_aac");if (!codec) {qDebug() << "decoder not found";return;}// 初始化解析器上下文parserCtx = av_parser_init(codec->id);if (!parserCtx) {qDebug() << "av_parser_init error";return;}// 創建上下文ctx = avcodec_alloc_context3(codec);if (!ctx) {qDebug() << "avcodec_alloc_context3 error";goto end;}// 創建AVPacketpkt = av_packet_alloc();if (!pkt) {qDebug() << "av_packet_alloc error";goto end;}// 創建AVFrameframe = av_frame_alloc();if (!frame) {qDebug() << "av_frame_alloc error";goto end;}// 打開解碼器ret = avcodec_open2(ctx, codec, nullptr);if (ret < 0) {ERROR_BUF(ret);qDebug() << "avcodec_open2 error" << errbuf;goto end;}// 打開文件if (!inFile.open(QFile::ReadOnly)) {qDebug() << "file open error:" << inFilename;goto end;}if (!outFile.open(QFile::WriteOnly)) {qDebug() << "file open error:" << out.filename;goto end;}while ((inLen = inFile.read(inDataArray, IN_DATA_SIZE)) > 0) {inData = inDataArray;while (inLen > 0) {// 經過解析器解析// 內部調用的核心邏輯是:ff_aac_ac3_parseret = av_parser_parse2(parserCtx, ctx,&pkt->data, &pkt->size,(uint8_t *) inData, inLen,AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);if (ret < 0) {ERROR_BUF(ret);qDebug() << "av_parser_parse2 error" << errbuf;goto end;}// 跳過已經解析過的數據inData += ret;// 減去已經解析過的數據大小inLen -= ret;// 解碼if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {goto end;}}}decode(ctx, nullptr, frame, outFile);// 賦值輸出參數out.sampleRate = ctx->sample_rate;out.sampleFmt = ctx->sample_fmt;out.chLayout = ctx->channel_layout;end:inFile.close();outFile.close();av_packet_free(&pkt);av_frame_free(&frame);av_parser_close(parserCtx);avcodec_free_context(&ctx);
}void AacDecodeThread::run()
{AudioDecodeSpec out;out.filename = "E:/media/test.pcm";aacDecode("E:/media/test.aac", out);qDebug() << "采樣率:" << out.sampleRate;qDebug() << "采樣格式:" << av_get_sample_fmt_name(out.sampleFmt);qDebug() << "聲道數:" << av_get_channel_layout_nb_channels(out.chLayout);
}
注意:本文為個人記錄,新手照搬可能會出現各種問題,請謹慎使用
碼字不易,如果這篇博客對你有幫助,麻煩點贊收藏,非常感謝!有不對的地方