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

news2024/11/23 21:47:29

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

文章目录

  • Qt-FFmpeg开发-使用libavcodec API的音频解码示例(MP3转pcm)
    • 1、概述
    • 2、实现效果
    • 3、主要代码
    • 4、完整源代码

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

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 <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;             // 未解码的原始数据
        AVFrame*                m_frame         = nullptr;             // 解码后的数据帧
    };
    #endif // WIDGET_H
    
    
  • widget.cpp文件

    #include "widget.h"
    #include "ui_widget.h"
    #include <qfiledialog.h>
    #include <QDebug>
    #include <qthread.h>
    #include <qtimer.h>
    
    extern "C" {        // 用C规则编译指定的代码
    #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转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;
    }
    
    

4、完整源代码

  • github
  • gitee

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/65739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于 registerForActivityResult()的使用方法,不能说详细,只能说略懂得例子

目录 1.情况说明 2.registerForActivityResult()的使用方法 1.情况说明 startActivityForResult();函数过时 使用了 registerForActivityResult()进行了代替 2.registerForActivityResult()的使用方法 数据来源 (2条消息) registerForActivityResult()的使用方法例子_发狂…

西北工业大学算法实验机试复习

&#x1f600;大家好&#xff0c;我是白晨&#xff0c;一个不是很能熬夜&#x1f62b;&#xff0c;但是也想日更的人✈。如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下&#x1f440;白晨吧&#xff01;你的支持就是我最大的动力&#xff01;&#x1f4…

TC申请是否需要银行转账记录?

【TC申请是否需要银行转账记录&#xff1f;】 答案是毫无疑问的。 根据TE官网公开的文件CCS Certification Procedures V3.0 里面关于TC申请所需的文件指引E2.1.1f&#xff1a;&#xff08;如图&#xff09; 企业在申请与TE相关的认证项目&#xff08;例如GRS/RCS等等&#xf…

iptables 命令和 iptables.service 服务 有什么关系 ?

写在前面 关于iptables 命令 和 iptabls.service 的一些疑惑理解不足小伙伴帮忙指正 傍晚时分&#xff0c;你坐在屋檐下&#xff0c;看着天慢慢地黑下去&#xff0c;心里寂寞而凄凉&#xff0c;感到自己的生命被剥夺了。当时我是个年轻人&#xff0c;但我害怕这样生活下去&…

企业级大数据平台智能运维好帮手——星环科技多模数据平台监控软件Aquila Insight

Aquila Insight介绍 Aquila Insight是星环科技推出的一款多模数据平台监控软件&#xff0c;为企业运维团队提供了一套统一、完整、便捷的智能化运维解决方案。通过丰富的仪表盘管理、告警与通知管理、实时和历史查询语句运行分析、计算和存储引擎的统一监控、完整的日志收集过滤…

编程内功心法「底层原理系列」 回归与本质,让本文带你认识什么是计算机软件系统

前提概要 如果希望可以把计算机编程技术提升到另外一个高度&#xff0c;,那么想要搞清楚什么是软件设计&#xff0c;首先就要理解什么是计算机软件&#xff0c;在这之后&#xff0c;再去考虑为什么对软件进行设计&#xff0c;以及在通常情况下应该怎样设计软件。 什么是计算机…

计算机毕业设计:基于HTML学校后台用户登录界面模板源码

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

KT148A语音芯片SOP外挂功放芯片8002D的说明_V1

目录 一、简介 KT148A语音芯片内置8欧姆0.5W的功放&#xff0c;所以一般的应用场景&#xff0c;播放一些提示音音量是足够的。但是有的需求还需要驱动更大的扬声器&#xff0c;实现更大的播放音量&#xff0c;这里我们给出解决方案。KT148AHAA8002D的组合 二、硬件电路如下-K…

防抖和节流

使用场景&#xff1a; 防抖在连续的事件&#xff0c;只需触发一次回调的场景有&#xff1a; 搜索框搜索输入。只需用户最后一次输入完&#xff0c;再发送请求。 窗口大小resize。只需窗口调整完成后&#xff0c;计算窗口大小。防止重复渲染。 登录、发短信等按钮避免用户点击太…

云原生周刊 | 美国国防部发布零信任战略与路线图

美国国防部发布了到 2027 年截止的零信任战略与路线图&#xff0c;总共有 45 项能力&#xff0c;最终目标是“安全的国防部信息企业”。 开源项目推荐 Extism Extism 是一个 WebAssembly 插件实现框架&#xff0c;它可以给你的应用开发出各种各样的 WebAssembly 插件&#xf…

HTML网页设计——轮滑运动体育类人物介绍主题12页面毕业设计网页

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

[附源码]JAVA毕业设计商店管理系统(系统+LW)

[附源码]JAVA毕业设计商店管理系统&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

电池供电遥测终端RTU 遥测终端机 低功耗遥测采集终端 智能远传 防水IP68

平升电子电池供电遥测终端RTU/遥测终端机/低功耗遥测采集终端是基于4G、5G、NB-IoT网络实现数据采集、远程传输、分析计算、越限报警的智能设备&#xff0c;具有功耗低、IP68防水等特点。特别适合用在无供电条件、防水防尘要求高的监测现场。 随着通信网络更迭、产品持续改进&…

在线文档协同办公-开箱即用demo

在线文档协同办公-开箱即用demo服务安装&#xff08;略&#xff09;下面开始集成打开文件保存文件共同编辑展示一下集成后的效果图服务安装&#xff08;略&#xff09; 这里可以参考前几篇博客内容 Linux版 windows版 下面开始集成 打开文件 1.创建一个空的html文件。 添加…

python匿名函数和高阶函数总结

bilibili 千峰python 学习笔记 匿名函数 lambda函数的语法只包含一个语句&#xff0c;如下&#xff1a; lambda 参数列表: 运算表达式Lambda函数能接收任何数量的参数但只能返回一个表达式的值匿名函数可以执行任意表达式(甚至print函数)&#xff0c;但是一般认为表达式应该有…

UDP-B-L-阿拉伯糖二钠盐,UDP-b-L-arabinopyranose disodium salt,15839-78-8

产品名称&#xff1a;UDP-B-L-阿拉伯糖二钠盐 英文名称&#xff1a;UDP-b-L-arabinopyranose disodium salt CAS号&#xff1a;15839-78-8 分子式&#xff1a;C14H22N2O16P2 分子量&#xff1a;536.27600 产地&#xff1a;西安 规格&#xff1a;1mg 5mg 10mg 纯度&#xff1…

西门子CT重建算法

相对于MR&#xff0c;CT在参数及扫描方面并不是太难&#xff0c;但是CT的图像重建及各种算法则是非常难的&#xff0c;也是比较抽象的。这篇文章介绍CT图像重建算法等相关内容。 CT技术是CT诊断的基础&#xff0c;帮助医务工作者充分掌握CT技术是我们的责任和义务&#xff01; …

Redis原理 - 对象的数据结构(SDS、Inset、Dict、ZipList、QuickList、SkipList、RedisObject)

Redis数据结构 1. SDS Redis 是用 C 语言写的&#xff0c;但是对于 Redis 的字符串&#xff0c;却不是 C 语言中的字符串&#xff08;即以空字符’\0’结尾的字符数组&#xff09;&#xff0c;它是自己构建了一种名为 简单动态字符串&#xff08;simple dynamic string,SDS&am…

如何在ios成功上架android tv?​

如果你是新手&#xff0c;将在这里清晰发布到App Store整个上架流程&#xff0c;还有相应的流程解决方案。​ 如果你上架过iOS APP&#xff0c;这里会了解到有更快捷的上架过程。​ 上架iOS最基本需要一个付费的开发者账号&#xff0c;还没有的话申请一个或者借用。​ 通常也…

7.springboot中整合Jpa多数据源

Springboot 整合spring data jpa多数据源 在使用Mybatis时会涉及多数据源的问题&#xff0c;同样&#xff0c;当我们使用Jpa作为持久化处理方式时&#xff0c;也会涉及多数据源的问题&#xff0c;本节我们来讲解一下Springboot如何整合Spring data Jpa多数据源。 1.创建项目 …