• Qt-FFmpeg开发-音频解码为PCM文件(9)


    Qt-FFmpeg开发-使用libavcodec API的音频解码示例(MP3转pcm)

    更多精彩内容
    👉个人内容分类汇总 👈
    👉音视频开发 👈

    1、概述

    • 最近研究了一下FFmpeg开发,功能实在是太强大了,网上ffmpeg3、4的文章还是很多的,但是学习嘛,最新的还是不能放过,就选了一个最新的ffmpeg n5.1.2版本,和3、4版本api变化还是挺大的;
    • 这是一个libavcodec API示例;
    • 这里主要是研究FFmpeg官方示例产生的一个程序,官方示例可以看Examples
    • 由于官方示例有一些小问题,编译没通过,并且是通过命令行执行,不方便,这里通过修改为使用Qt实现这个音频解码为PCM文件的示例。

    开发环境说明

    • 系统:Windows10、Ubuntu20.04
    • Qt版本:V5.12.5
    • 编译器:MSVC2017-64、GCC/G++64
    • FFmpeg版本:n5.1.2

    2、实现效果

    1. 将.mp3文件解码转换为.pcm文件;(PCM数据时最原始的音频数据);
    2. 使用Qt重新实现,方便操作,便于使用;
    3. 解决官方示例中解码失败程序会终止问题 ;
    4. 关键步骤加上详细注释,比官方示例更便于学习。
    • 实现效果如下:

      在这里插入图片描述

    3、主要代码

    • 啥也不说了,直接上代码,一切有注释

    • widget.h文件

      #ifndef WIDGET_H
      #define WIDGET_H
      
      #include 
      #include 
      
      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;             // 未解码的原始数据
          AVFrame*                m_frame         = nullptr;             // 解码后的数据帧
      };
      #endif // WIDGET_H
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
    • widget.cpp文件

      #include "widget.h"
      #include "ui_widget.h"
      #include 
      #include 
      #include 
      #include 
      
      extern "C" {        // 用C规则编译指定的代码
      #include 
      #include 
      #include 
      }
      
      #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转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函数调用异常信息
       * @param err
       */
      void Widget::showError(int err)
      {
          static char m_error[1024];
          memset(m_error, 0, sizeof (m_error));        // 将数组置零
          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, "解码后数据保存到~!", "/", "原始音频 (*.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);   // 释放编解码器上下文和与之相关的所有内容,并将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();      // 创建一个AVPacket
          if(!m_packet)
          {
              return AVERROR(ENOMEM);        // 返回无法分配内存的错误码
          }
      
          m_frame = av_frame_alloc();      // 创建一个AVFrame
          if(!m_frame)
          {
              return AVERROR(ENOMEM);        // 返回无法分配内存的错误码
          }
      
          // 通过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并将其字段设置为默认值
          if(!m_context)
          {
              return AVERROR(ENOMEM);        // 解码器上下文创建失败
          }
      
          // 使用给定的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();        // 读取所有数据
          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);
              // 解析数据包
              ret = av_parser_parse2(m_parserContex, m_context, &m_packet->data, &m_packet->size,
                                     reinterpret_cast<const uchar*>(inbuf),        // 这里不能直接使用buf.data(),否则会出现[mp2 @ 000001c8dbd40b00] Multiple frames in a packet.
                                     len,
                                     AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
              if(ret < 0)
              {
                  break;
              }
              buf.remove(0, ret);  // 移除已解析的数据
      
              if(m_packet->size)
              {
                  ret = decode(fileOut);
                  if(ret < 0)
                  {
      //                return ret;
                  }
              }
          }
          m_packet->data = nullptr;
          m_packet->size = 0;
          decode(fileOut);               // 需要传入空的数据帧才可以将解码器中所有数据读取出来
      
          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);   // 获取样本格式的替代格式
          }
      
          // 音频通道数
      #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)
      {
          // 将包含压缩数据的数据包发送到解码器
          int ret = avcodec_send_packet(m_context, m_packet);   // 注意:官方Demo中这里如果返回值<0则终止程序,由于数据中有mp3文件头,所以一开始会有返回值<0的情况
      
          // 读取所有输出帧(通常可以有任意数量的输出帧
          while (ret >= 0)
          {
              // 读取解码后的数据帧
              int ret = avcodec_receive_frame(m_context, m_frame);
              if(ret == AVERROR(EAGAIN)   // 资源暂时不可用
              || ret == AVERROR_EOF)      // 文件末尾
              {
                  return 0;
              }
              else if(ret < 0)
              {
                  return ret;
              }
      
              // 返回每个样本的字节数。例如格式为AV_SAMPLE_FMT_U8,则字节数为1字节
              int size = av_get_bytes_per_sample(m_context->sample_fmt);   // 返回值不会小于0
              for(int i = 0; i < m_frame->nb_samples; ++i)   // 音频样本数(采样率)
              {
      #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;
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182
      • 183
      • 184
      • 185
      • 186
      • 187
      • 188
      • 189
      • 190
      • 191
      • 192
      • 193
      • 194
      • 195
      • 196
      • 197
      • 198
      • 199
      • 200
      • 201
      • 202
      • 203
      • 204
      • 205
      • 206
      • 207
      • 208
      • 209
      • 210
      • 211
      • 212
      • 213
      • 214
      • 215
      • 216
      • 217
      • 218
      • 219
      • 220
      • 221
      • 222
      • 223
      • 224
      • 225
      • 226
      • 227
      • 228
      • 229
      • 230
      • 231
      • 232
      • 233
      • 234
      • 235
      • 236
      • 237
      • 238
      • 239
      • 240
      • 241
      • 242
      • 243
      • 244
      • 245
      • 246
      • 247
      • 248
      • 249
      • 250
      • 251
      • 252
      • 253
      • 254
      • 255
      • 256
      • 257
      • 258
      • 259
      • 260
      • 261
      • 262
      • 263
      • 264
      • 265
      • 266
      • 267
      • 268
      • 269
      • 270
      • 271
      • 272
      • 273
      • 274
      • 275
      • 276
      • 277
      • 278
      • 279
      • 280
      • 281
      • 282
      • 283
      • 284
      • 285
      • 286
      • 287
      • 288
      • 289
      • 290
      • 291

    4、完整源代码

  • 相关阅读:
    我们常说的网络资产,具体是如何定义的?
    HFI-脉振法
    PHP低版本安全问题
    Spring Cloud Alibaba Seata 实现分布式事物
    从可靠性的角度理解 tcp
    git命令
    车牌识别控制台 可快速整合二次开发
    在Windows环境下配置及安装Nacos
    《从菜鸟到大师之路 正则表达式 篇》
    【软考】-- 计算机组成体系结构(上)【我的1024】
  • 原文地址:https://blog.csdn.net/qq_43627907/article/details/128201598