音視頻/FFmpeg #Qt
Qt-FFmpeg開發(fā)-使用libavcodec API的音頻解碼示例(MP3轉(zhuǎn)pcm)
- 音視頻/FFmpeg #Qt
-
Qt-FFmpeg開發(fā)-使用libavcodec API的音頻解碼示例(MP3轉(zhuǎn)pcm)
- 1、概述
- 2、實現(xiàn)效果
- 3、主要代碼
- 4、完整源代碼
更多精彩內(nèi)容 |
---|
??個人內(nèi)容分類匯總 ?? |
??音視頻開發(fā) ?? |
1、概述
- 最近研究了一下FFmpeg開發(fā),功能實在是太強大了,網(wǎng)上ffmpeg3、4的文章還是很多的,但是學(xué)習(xí)嘛,最新的還是不能放過,就選了一個最新的ffmpeg n5.1.2版本,和3、4版本api變化還是挺大的;
- 這是一個libavcodec API示例;
- 這里主要是研究FFmpeg官方示例產(chǎn)生的一個程序,官方示例可以看Examples;
- 由于官方示例有一些小問題,編譯沒通過,并且是通過命令行執(zhí)行,不方便,這里通過修改為使用Qt實現(xiàn)這個音頻解碼為PCM文件的示例。
開發(fā)環(huán)境說明
- 系統(tǒng):Windows10、Ubuntu20.04
- Qt版本:V5.12.5
- 編譯器:MSVC2017-64、GCC/G++64
- FFmpeg版本:n5.1.2
- 官方下載
- 我使用的庫
2、實現(xiàn)效果
- 將.mp3文件解碼轉(zhuǎn)換為.pcm文件;(PCM數(shù)據(jù)時最原始的音頻數(shù)據(jù));
- 使用Qt重新實現(xiàn),方便操作,便于使用;
- 解決官方示例中解碼失敗程序會終止問題 ;
- 關(guān)鍵步驟加上詳細(xì)注釋,比官方示例更便于學(xué)習(xí)。
-
實現(xiàn)效果如下:
3、主要代碼
-
啥也不說了,直接上代碼,一切有注釋
-
widget.h文件文章來源:http://www.zghlxwxcb.cn/news/detail-405070.html
#ifndef WIDGET_H #define WIDGET_H #include <QFile> #include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE struct AVCodecParserContext; struct AVCodecContext; struct AVCodec; struct AVPacket; struct AVFrame; class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); private slots: void on_but_in_clicked(); void on_but_out_clicked(); void on_but_start_clicked(); private: int initDecode(); int decode(QFile& fileOut); void showError(int err); void showLog(const QString& log); private: Ui::Widget *ui; AVCodecParserContext* m_parserContex = nullptr; // 裸流解析器 AVCodecContext* m_context = nullptr; // 解碼器上下文 const AVCodec* m_codec = nullptr; // 音頻解碼器 AVPacket* m_packet = nullptr; // 未解碼的原始數(shù)據(jù) AVFrame* m_frame = nullptr; // 解碼后的數(shù)據(jù)幀 }; #endif // WIDGET_H
-
widget.cpp文件文章來源地址http://www.zghlxwxcb.cn/news/detail-405070.html
#include "widget.h" #include "ui_widget.h" #include <qfiledialog.h> #include <QDebug> #include <qthread.h> #include <qtimer.h> extern "C" { // 用C規(guī)則編譯指定的代碼 #include <libavutil/frame.h> #include <libavutil/mem.h> #include <libavcodec/avcodec.h> } #define AUDIO_INBUF_SIZE 20480 #define AUDIO_REFILL_THRESH 4096 Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); this->setWindowTitle(QString("使用libavcodec API的音頻解碼示例(mp3轉(zhuǎn)pcm) V%1").arg(APP_VERSION)); } Widget::~Widget() { delete ui; } /** * @brief 自定義非阻塞延時 * @param ms */ void msleep(int ms) { QEventLoop loop; QTimer::singleShot(ms, &loop, SLOT(quit())); loop.exec(); } void Widget::showLog(const QString &log) { ui->textEdit->append(log); } /** * @brief 顯示ffmpeg函數(shù)調(diào)用異常信息 * @param err */ void Widget::showError(int err) { static char m_error[1024]; memset(m_error, 0, sizeof (m_error)); // 將數(shù)組置零 av_strerror(err, m_error, sizeof (m_error)); showLog(QString("Error:%1 %2").arg(err).arg(m_error)); } /** * @brief 獲取輸入文件路徑 */ void Widget::on_but_in_clicked() { QString strName = QFileDialog::getOpenFileName(this, "選擇用于解碼的.mp3音頻文件~!", "/", "音頻 (*.mp3);"); if(strName.isEmpty()) { return; } ui->line_fileIn->setText(strName); } /** * @brief 獲取解碼后的原始音頻文件保存路徑 */ void Widget::on_but_out_clicked() { QString strName = QFileDialog::getSaveFileName(this, "解碼后數(shù)據(jù)保存到~!", "/", "原始音頻 (*.pcm);"); if(strName.isEmpty()) { return; } ui->line_fileOut->setText(strName); } void Widget::on_but_start_clicked() { int ret = initDecode(); if(ret < 0) { showError(ret); } avcodec_free_context(&m_context); // 釋放編解碼器上下文和與之相關(guān)的所有內(nèi)容,并將NULL寫入提供的指針。 av_parser_close(m_parserContex); av_frame_free(&m_frame); av_packet_free(&m_packet); } QString get_format_from_sample_fmt(int fmt) { typedef struct sample_fmt_entry { enum AVSampleFormat sample_fmt; QString fmt_be; // 大端模式指令 QString fmt_le; // 小端模式指令 }sample_fmt_entry; sample_fmt_entry sample_fmt_entryes[] = { { AV_SAMPLE_FMT_U8, "u8", "u8" }, { AV_SAMPLE_FMT_S16, "s16be", "s16le" }, { AV_SAMPLE_FMT_S32, "s32be", "s32le" }, { AV_SAMPLE_FMT_FLT, "f32be", "f32le" }, { AV_SAMPLE_FMT_DBL, "f64be", "f64le" }, }; for(int i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entryes); i++) { sample_fmt_entry entry = sample_fmt_entryes[i]; if(fmt == entry.sample_fmt) { return AV_NE(entry.fmt_be, entry.fmt_le); // AV_NE:判斷大小端 } } return QString(); } /** * @brief 開始解碼 * @return */ int Widget::initDecode() { QString strIn = ui->line_fileIn->text(); QString strOut = ui->line_fileOut->text(); if(strIn.isEmpty() || strOut.isEmpty()) { return AVERROR(ENOENT); // 返回文件不存在的錯誤碼 } m_packet = av_packet_alloc(); // 創(chuàng)建一個AVPacket if(!m_packet) { return AVERROR(ENOMEM); // 返回?zé)o法分配內(nèi)存的錯誤碼 } m_frame = av_frame_alloc(); // 創(chuàng)建一個AVFrame if(!m_frame) { return AVERROR(ENOMEM); // 返回?zé)o法分配內(nèi)存的錯誤碼 } // 通過ID查詢MPEG音頻解碼器 m_codec = avcodec_find_decoder(AV_CODEC_ID_MP2); if(!m_codec) { return AVERROR(ENXIO); // 找不到解碼器 } m_parserContex = av_parser_init(m_codec->id); if(!m_parserContex) { return AVERROR(ENOMEM); // 解析器初始化失敗 } m_context = avcodec_alloc_context3(m_codec); // 分配AVCodecContext并將其字段設(shè)置為默認(rèn)值 if(!m_context) { return AVERROR(ENOMEM); // 解碼器上下文創(chuàng)建失敗 } // 使用給定的AVCodec初始化AVCodecContext。 int ret = avcodec_open2(m_context, m_codec, nullptr); if(ret < 0) { return ret; } // 打開輸入文件 QFile fileIn(strIn); if(!fileIn.open(QIODevice::ReadOnly)) { return AVERROR(ENOENT); } // 打開輸出文件 QFile fileOut(strOut); if(!fileOut.open(QIODevice::WriteOnly)) { return AVERROR(ENOENT); } showLog("開始解碼!"); msleep(1); QByteArray buf = fileIn.readAll(); // 讀取所有數(shù)據(jù) char inbuf[AUDIO_INBUF_SIZE]; while(buf.count() > 0) { int len = (buf.count() <= AUDIO_INBUF_SIZE) ? buf.count() : AUDIO_INBUF_SIZE; memcpy(inbuf, buf.data(), len); // 解析數(shù)據(jù)包 ret = av_parser_parse2(m_parserContex, m_context, &m_packet->data, &m_packet->size, reinterpret_cast<const uchar*>(inbuf), // 這里不能直接使用buf.data(),否則會出現(xiàn)[mp2 @ 000001c8dbd40b00] Multiple frames in a packet. len, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); if(ret < 0) { break; } buf.remove(0, ret); // 移除已解析的數(shù)據(jù) if(m_packet->size) { ret = decode(fileOut); if(ret < 0) { // return ret; } } } m_packet->data = nullptr; m_packet->size = 0; decode(fileOut); // 需要傳入空的數(shù)據(jù)幀才可以將解碼器中所有數(shù)據(jù)讀取出來 enum AVSampleFormat sfmt = m_context->sample_fmt; // 檢查樣本格式是否為平面 if(av_sample_fmt_is_planar(sfmt)) { const char* name = av_get_sample_fmt_name(sfmt); // 獲取音頻樣本格式名稱 showLog(QString("警告:解碼器生成的樣本格式是平面格式(%1)。此示例將僅輸出第一個通道。").arg(name)); sfmt = av_get_packed_sample_fmt(sfmt); // 獲取樣本格式的替代格式 } // 音頻通道數(shù) #if FF_API_OLD_CHANNEL_LAYOUT int channels = m_context->channels; #else int channels = m_context->ch_layout.nb_channels; #endif QString strFmt = get_format_from_sample_fmt(sfmt); if(!strFmt.isEmpty()) { showLog(QString("使用下列命令播放輸出音頻文件!\n" "ffplay -f %1 -ac %2 -ar %3 %4\n") .arg(strFmt).arg(channels) .arg(m_context->sample_rate).arg(strOut)); } return 0; } /** * @brief 解碼并寫入文件 * @param fileOut * @return */ int Widget::decode(QFile &fileOut) { // 將包含壓縮數(shù)據(jù)的數(shù)據(jù)包發(fā)送到解碼器 int ret = avcodec_send_packet(m_context, m_packet); // 注意:官方Demo中這里如果返回值<0則終止程序,由于數(shù)據(jù)中有mp3文件頭,所以一開始會有返回值<0的情況 // 讀取所有輸出幀(通??梢杂腥我鈹?shù)量的輸出幀 while (ret >= 0) { // 讀取解碼后的數(shù)據(jù)幀 int ret = avcodec_receive_frame(m_context, m_frame); if(ret == AVERROR(EAGAIN) // 資源暫時不可用 || ret == AVERROR_EOF) // 文件末尾 { return 0; } else if(ret < 0) { return ret; } // 返回每個樣本的字節(jié)數(shù)。例如格式為AV_SAMPLE_FMT_U8,則字節(jié)數(shù)為1字節(jié) int size = av_get_bytes_per_sample(m_context->sample_fmt); // 返回值不會小于0 for(int i = 0; i < m_frame->nb_samples; ++i) // 音頻樣本數(shù)(采樣率) { #if FF_API_OLD_CHANNEL_LAYOUT for(int j = 0; j < m_context->channels; ++j) // 5.1.2以后版本會棄用channels #else for(int j = 0; j < m_context->ch_layout.nb_channels; ++j) #endif { fileOut.write((const char*)(m_frame->data[j] + size * i), size); } } } return 0; }
4、完整源代碼
- github
- gitee
到了這里,關(guān)于Qt-FFmpeg開發(fā)-音頻解碼為PCM文件(9)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!