Qt图表绘制(QtCharts)-麦克风输入数据波形绘制(3) 原创

Qt图表绘制(QtCharts)-麦克风输入数据波形绘制(3)🐒

更多精彩内容
👉个人内容分类汇总 👈
注意:我使用的QtCharts是基于Qt 5.12.12版本

1、概述🐶

Qt Charts 模块提供了一组易于使用的图表组件,能够创建时尚、交互式、以数据为中心的用户界面。

它使用 Qt Graphics View 框架,因此可以轻松地将图表集成到现代用户界面中。

Qt Charts 可以用作 QWidgets、QGraphicsWidget 或 QML 类型。 用户可以通过选择图表主题之一轻松创建令人印象深刻的图表。

这一个Demo干了啥:🤏🤏🤏

  • 调用麦克风输入设备录取音频;
  • 将音频数据波形绘制显示;
  • 在源码中包含使用的说明注释和注意事项 ;
  • 去除了Qt示例中的警告信息、一些过时的用法和危险的用法;
  • 将UI和代码分离,使实现图表绘制的代码更单一简洁,便于学习。

2、实现步骤🐕

  1. 打开.pro文件,输入Qt += charts,引入Qt Charts模,由于需要使用音频输入设备录制音频,所以需要音频多媒体模块QT += multimedia

    在这里插入图片描述

  2. 打开ui设计器,选择一个Graphics View控件,鼠标右键提升为

    在这里插入图片描述

    在这里插入图片描述

  3. 输入QChartView,点击添加、提升;

    在这里插入图片描述

  4. 在窗口右上角就可以看见控件类型变成QChartView了,然后将控件命名为chartView; 注意:由于QChartView需要引入命名空间,所以这里提升后编译时不通过的。

    在这里插入图片描述

  5. 在代码中添加QtCharts头文件,引入命名空间;一般我们最好在cpp文件中添加头文件和引入命名空间,尽量避免在.h文件中引入(这里为了方便,代码简洁直接在头文件中引入命名空间),而在cpp文件中引入命名空间需要在ui_widget.h文件前,否则编译会失败,因为在ui文件中会用到QChartView类。

    #include <QtCharts>         // 导入QtCharts所有的头文件,也可以单独导入某一个
    
    // 引入qchart命名空间(注意:如果是在ui中提升为QChartView,
    // 则QT_CHARTS_USE_NAMESPACE一定要放在#include "ui_widget.h"前面,否则编译会失败)
    QT_CHARTS_USE_NAMESPACE
    
  6. 创建一个折线图对象QLineSeries,用于绘制音频数据波形;

    m_series = new QLineSeries();                       // 创建一个折线图对象
    
  7. 获取一个QChart对象,QChart类似于容器,可以加载不同的图表(series),并通过QChartView显示,这里有两种获取QChart对象的方法,QChartView显示QChart的视图类;

    #if 1
        QChart *chart = ui->chartView->chart();                    // 方法1:可以直接获取QChartView中的QChart
    #else
        QChart *chart = new QChart();                              // 方法2:创建用于管理不同类型Series和其他其他图表相关对象(如图例和轴)的chart
        ui->chartView->setChart(chart);                            // 将包含series的QChart对象添加进ui中的chartView对象中
    #endif
    
  8. 将准备好的区域面积图对象QAreaSeries添加进QChart对象中,用于显示;

    chart->addSeries(series);
    
  9. 设置图表的XY轴范围、标签;

    QValueAxis *axisX = new QValueAxis;                 // 创建X轴
    axisX->setRange(0, 10000);                          // 设置显示同时10000个音频数据点
    axisX->setLabelFormat("%g");
    axisX->setTitleText("采样");
    QValueAxis *axisY = new QValueAxis;                 // 创建Y轴
    axisY->setRange(0, 255);                            // 因为显示的使uchar类型数据,所以Y轴设置为0~255就可以
    axisY->setTitleText("输入音频电平");
    
  10. 开启OpenGL加速,因为数据量比较大时如果不开启opengl渲染,则会有比较卡的感觉;

    m_series->setUseOpenGL(true);                     // 开启OpenGL加速,仅QLineSeries和QScatterSeries支持使用OpenGL进行加速
    
  11. 使用Qt自带的QAudioInput类开始录制音频数据,获取的音频数据由于直接输出到IO设备接口,所以我们需要继承QIODevice,重写writeData虚函数来获取音频数据;

    m_audioInput = new QAudioInput(m_inputDevice, audio, this);  // 打开音频录音设备
    m_device = new XYSeriesIODevice(m_series, this);             // 将录制的音频数据输出到XYSeriesIODevice
    m_device->open(QIODevice::WriteOnly);
    m_audioInput->start(m_device);                               // 开始录制
    
  12. 重写writeData虚函数获取到音频数据后由于数据量比较大,这里每4个音频数据只显示1个,并且数据由右往左逐渐移动;

    qint64 XYSeriesIODevice::writeData(const char *data, qint64 len)
    {
        if(m_buffer.isEmpty()) return -1;                        // 如果未初始化数组则不显示
    
        static const int resolution = 4;                         // 每四个数据显示一个
        const int availableSamples = int(len) / resolution;      // 需要显示的数据个数
        int start = 0;
        if(availableSamples < m_buffer.count())                  // 如果需要显示的数据个数 < 数组个数,则每次的数据都需要往前移动
        {
            start = m_buffer.count() - availableSamples;         // 需要向前移动的数据个数
            for(int i = 0; i < start; i++)
            {
                m_buffer[i].setY(m_buffer.at(i + availableSamples).y());
            }
        }
    
        for(int i = start; i < m_buffer.count(); i++)           // 每隔4个取出1个数据加入绘制的数组
        {
            m_buffer[i].setY(uchar(*data));
            data += resolution;
        }
    
        m_series->replace(m_buffer);                           // 绘制数组中的数据
        return len;
    }
    

3、主要使用的类🐪

类名作用
QAudioDeviceInfo获取默认的音频输入设备(麦克风)
QValueAxis将数值添加到图表的轴(X轴Y轴)
QAudioFormat存储音频流参数信息(如采样率、音频通道、编解码器等)
QAudioInput提供用于从音频输入设备接收音频数据的接口(打开麦克风)
QLineSeries用于创建一条折线

4、主要代码🦄

  • widget.h文件
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QChartGlobal>
#include <QAudioDeviceInfo>

QT_CHARTS_BEGIN_NAMESPACE    // QtCharts命名空间
class QLineSeries;           // 在头文件中声明要用到的QtCharts类,而不直接引入头文件
class QChart;
QT_CHARTS_END_NAMESPACE

QT_CHARTS_USE_NAMESPACE      // 引入QtCharts命名空间

class XYSeriesIODevice;
class QAudioInput;

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    void initChart();         // 初始化绘制图表
    void audioSample();       // 采样绘制录音音频波形

private:
    Ui::Widget *ui;

    QAudioDeviceInfo m_inputDevice;                // 音频输入设备信息对象
    XYSeriesIODevice* m_device = nullptr;          // IO接口,用于获取音频数据并显示
    QLineSeries* m_series = nullptr;               // 折线图对象
    QAudioInput* m_audioInput = nullptr;           // 录音设备对象
};
#endif // WIDGET_H

  • widget.cpp文件
#include "widget.h"
#include "ui_widget.h"
#include "xyseriesiodevice.h"
#include <QAudioDeviceInfo>
#include <QAudioInput>
#include <QtCharts>
#include <qdebug.h>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->setWindowTitle("QtCharts绘图-动态数据的绘制(麦克风输入)Demo");
    initChart();
    audioSample();
}

Widget::~Widget()
{
    m_audioInput->stop();    // 停止录制
    m_device->close();       // 关闭IO设备
    delete ui;
}

/**
 * @brief 初始化绘制图表
 */
void Widget::initChart()
{
    m_inputDevice = QAudioDeviceInfo::defaultInputDevice();   // 获取默认的音频输入设备(麦克风)

    QChart* chart = ui->chartView->chart();             // 这里直接获取QChartView包含的chart,不自己创建
    chart->legend()->hide();                            // 隐藏折线图的图例
    chart->setTitle(QString("来自麦克风的数据:%1")        // 设置图表的标题和输入设备名称
                    .arg(m_inputDevice.deviceName()));

    m_series = new QLineSeries();                       // 创建一个折线图对象
    chart->addSeries(m_series);

    QValueAxis *axisX = new QValueAxis;                 // 创建X轴
    axisX->setRange(0, 10000);
    axisX->setLabelFormat("%g");
    axisX->setTitleText("采样");
    QValueAxis *axisY = new QValueAxis;                 // 创建Y轴
    axisY->setRange(0, 255);
    axisY->setTitleText("输入音频电平");

    chart->addAxis(axisX, Qt::AlignBottom);           // 在chart中添加创建的X轴,指定在图表底部
    m_series->attachAxis(axisX);                      // 将X轴关联到创建的折线图上
    chart->addAxis(axisY, Qt::AlignLeft);             // 添加创建的Y轴,指定在左侧
    m_series->attachAxis(axisY);
    m_series->setUseOpenGL(true);                     // 开启OpenGL加速,仅QLineSeries和QScatterSeries支持使用OpenGL进行加速

}

/**
 * @brief 采样音频波形显示
 */
void Widget::audioSample()
{
    QAudioFormat audio;                               // 存储音频流参数信息的对象
    audio.setSampleRate(8000);                        // 设置音频采样率
    audio.setChannelCount(1);                         // 设置通道数
    audio.setSampleSize(8);                           // 设置样本大小
    audio.setCodec("audio/pcm");                      // 设置编解码器
    audio.setByteOrder(QAudioFormat::LittleEndian);   // 采用小端模式
    audio.setSampleType(QAudioFormat::UnSignedInt);   // 设置样本数据类型

    m_audioInput = new QAudioInput(m_inputDevice, audio, this);  // 打开音频录音设备
    m_device = new XYSeriesIODevice(m_series, this);             // 将录制的音频数据输出到XYSeriesIODevice
    m_device->open(QIODevice::WriteOnly);
    m_audioInput->start(m_device);                               // 开始录制
}

  • xyseriesiodevice.h文件
/******************************************************************************
 * @文件名     xyseriesiodevice.h
 * @功能       通过QAudioInput录制的音频数据因为需要用于绘制波形,而不是保存为音频文件,
 *             所以需要重写QIODevice类,获取录制的音频数据
 *
 * @开发者     mhf
 * @邮箱       1603291350@qq.com
 * @时间       2022/04/24
 * @备注
 *****************************************************************************/
#ifndef XYSERIESIODEVICE_H
#define XYSERIESIODEVICE_H

#include <QIODevice>
#include <QChartGlobal>
#include <QVector>
#include <QElapsedTimer>

QT_CHARTS_BEGIN_NAMESPACE
class QXYSeries;
QT_CHARTS_END_NAMESPACE

QT_CHARTS_USE_NAMESPACE

class XYSeriesIODevice : public QIODevice
{
    Q_OBJECT
public:
    explicit XYSeriesIODevice(QXYSeries *series, QObject *parent = nullptr);

protected:
    qint64 readData(char *data, qint64 maxlen) override;
    qint64 writeData(const char *data, qint64 len) override;

private:
    QXYSeries* m_series= nullptr;      // 用于显示音频数据的对象
    QVector<QPointF> m_buffer;
};

#endif // XYSERIESIODEVICE_H

  • xyseriesiodevice.cpp文件
#include "xyseriesiodevice.h"
#include <QLineSeries>
#include <QPointF>
#include <qdebug.h>
#include <qvalueaxis.h>

XYSeriesIODevice::XYSeriesIODevice(QXYSeries *series, QObject *parent) :
    QIODevice(parent),
    m_series(series)
{
    const QList<QAbstractAxis*> axiss = m_series->attachedAxes();   // 获取关联的X轴
    for(auto axis : axiss)
    {
        if(axis->alignment() == Qt::AlignBottom)  // 判断是否是X轴
        {
            QValueAxis * axisX = (QValueAxis *)m_series->attachedAxes().at(0);
            int count = axisX->max() - axisX->min();   // 获取轴范围
            m_buffer.reserve(count);                   // 初始化绘制点的数组大小
            for(int i = 0; i < count; i++)
            {
                m_buffer.append(QPointF(i, 127));
            }
        }
    }
}

/**
 * @brief          readData是纯虚函数,需要重写才能实例化
 * @param data
 * @param maxlen
 * @return
 */
qint64 XYSeriesIODevice::readData(char *data, qint64 maxlen)
{
    Q_UNUSED(data)
    Q_UNUSED(maxlen)
    return -1;
}

/**
 * @brief        重写IO接口,获取输入设备录制的音频数据
 * @param data   音频数据
 * @param len    数据长度
 * @return
 */
qint64 XYSeriesIODevice::writeData(const char *data, qint64 len)
{
    if(m_buffer.isEmpty()) return -1;                        // 如果未初始化数组则不显示

    static const int resolution = 4;                         // 每四个数据显示一个
    const int availableSamples = int(len) / resolution;      // 需要显示的数据个数
    int start = 0;
    if(availableSamples < m_buffer.count())                  // 如果需要显示的数据个数 < 数组个数,则每次的数据都需要往前移动
    {
        start = m_buffer.count() - availableSamples;         // 需要向前移动的数据个数
        for(int i = 0; i < start; i++)
        {
            m_buffer[i].setY(m_buffer.at(i + availableSamples).y());
        }
    }

    for(int i = start; i < m_buffer.count(); i++)           // 每隔4个取出1个数据加入绘制的数组
    {
        m_buffer[i].setY(uchar(*data));
        data += resolution;
    }

    m_series->replace(m_buffer);                           // 绘制数组中的数据
    return len;
}

5、实现效果🦊

在这里插入图片描述

6、源代码🐱

gitee
github

🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞

posted @ 2022-04-25 22:39  mahuifa  阅读(0)  评论(0编辑  收藏  举报  来源