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、实现步骤🐕
-
打开
.pro
文件,输入Qt += charts
,引入Qt Charts模,由于需要使用音频输入设备录制音频,所以需要音频多媒体模块QT += multimedia
; -
打开ui设计器,选择一个
Graphics View
控件,鼠标右键提升为; -
输入
QChartView
,点击添加、提升; -
在窗口右上角就可以看见控件类型变成
QChartView
了,然后将控件命名为chartView; 注意:由于QChartView需要引入命名空间,所以这里提升后编译时不通过的。 -
在代码中添加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
-
创建一个折线图对象
QLineSeries
,用于绘制音频数据波形;m_series = new QLineSeries(); // 创建一个折线图对象
-
获取一个
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
-
将准备好的区域面积图对象
QAreaSeries
添加进QChart
对象中,用于显示;chart->addSeries(series);
-
设置图表的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("输入音频电平");
-
开启OpenGL加速,因为数据量比较大时如果不开启opengl渲染,则会有比较卡的感觉;
m_series->setUseOpenGL(true); // 开启OpenGL加速,仅QLineSeries和QScatterSeries支持使用OpenGL进行加速
-
使用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); // 开始录制
-
重写
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、源代码🐱
🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞🤞