OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 3233|回复: 0

《I.MX6U 嵌入式Qt开发指南》第十二章 多媒体 (下)

[复制链接]

1130

主题

1141

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4746
金钱
4746
注册时间
2019-5-8
在线时间
1237 小时
发表于 2022-8-10 18:33:14 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2022-8-11 09:40 编辑

1)实验平台:正点原子阿尔法Linux开发板
2)  章节摘自【正点原子】《I.MX6U 嵌入式Qt开发指南》

3)购买链接:https://detail.tmall.com/item.htm?id=609033604451
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyz-i.mx6ull.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子阿尔法Linux交流群:1027879335






第十二章 多媒体
第十二章12.5  录音
Qt提供了QAudioRecorder类录制音频,继承于QmediaRecorder类,音频输入可以使用QAudioRecorder或者QAudioInput类实现。QAudioRecorder是高级的类,输入的音频数据直接保存为一个音频文件。而QAudioInput则是低层次的实现,从类的名称可以知道它是与输入输出流有关的,它可以将音频录制的数据写入一个流设备。本小节将介绍使用QAudioRecorder录制音频并保存成一个mp3文件。并使用QAudioProbe类探测音频数据缓冲区里的实时音量大小设计一个实用的录音应用程序。

第十三章12.5.1  应用实例
本例设计一个实用的录音界面,界面是编者原创界面。本例适用于正点原子ALPHA开发板,已经测试。Windows与Ubuntu下请读者使用Qt官方的audiorecorder例子自行测试,Windows系统上的声卡设置比较复杂,不详解,编者只确保正点原子I.MX6U ALPHA开发板正常运行此应用程序。Mini板没有声卡,请使用USB声卡插到正点原子I.MX6U开发板进行自行测试!!!
本例目的:录音程序的设计与使用。
例16_audiorecorder,录音程序(难度:难)。项目路径为Qt/2/16_audiorecorder。注意本例有用到qss样式文件,关于如何添加资源文件与qss文件请参考7.1.3小节。本例设计一个录音程序,录音功能部分直接参考Qt官方的audiorecorder例程,界面设计由编者设计。在Qt官方的audiorecorder例程里(自行在Qt官方的audiorecorder例程里打开查看),我们可以看到官方的例程录音设置都是通过QComboBox来选择的,当然这个只是一个Qt官方的例子,有很大的参考性。如果运用到实际项目里我们需要做一定的修改。如果是面向用户,我们就不需要暴露那么多信息给用户,同时也可以避免用户操作失败等等。所以编者参考Qt官方的例程重新设计了一个录音例程。源码如下,界面效果如12.5.2小节
项目文件16_audiorecorder文件第一行添加的代码部分如下。

  1. 16_audiorecorder.pro编程后的代码
  2. 1   QT       += core gui multimedia
  3. 2
  4. 3   greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
  5. 4
  6. 5   CONFIG += c++11
  7. 6
  8. 7   # The following define makes your compiler emit warnings if you use
  9. 8   # any Qt feature that has been marked deprecated (the exact warnings
  10. 9   # depend on your compiler). Please consult the documentation of the
  11. 10  # deprecated API in order to know how to port your code away from it.
  12. 11  DEFINES += QT_DEPRECATED_WARNINGS
  13. 12
  14. 13  # You can also make your code fail to compile if it uses deprecated APIs.
  15. 14  # In order to do so, uncomment the following line.
  16. 15  # You can also select to disable deprecated APIs only up to a certain version of Qt.
  17. 16  #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
  18. 17
  19. 18  SOURCES += \
  20. 19      main.cpp \
  21. 20      audiorecorder.cpp
  22. 21
  23. 22  HEADERS += \
  24. 23      audiorecorder.h
  25. 24
  26. 25  # Default rules for deployment.
  27. 26  qnx: target.path = /tmp/${TARGET}/bin
  28. 27  else: unix:!android: target.path = /opt/${TARGET}/bin
  29. 28  !isEmpty(target.path): INSTALLS += target
  30. 29
  31. 30  RESOURCES += \
  32. 31      res.qrc
复制代码
在头文件“audiorecorder.h”具体代码如下。
  1. audiorecorder.h编程后的代码
  2.     /******************************************************************
  3.     Copyright (C) 2017 The Qt Company Ltd.
  4.     Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
  5.     * @projectName   16_audiorecorder
  6.     * @brief         audiorecorder.h
  7.     * [url=home.php?mod=space&uid=90321]@Author[/url]        Deng Zhimao
  8.     * [url=home.php?mod=space&uid=55957]@EMAIL[/url]         1252699831@qq.com
  9.     * [url=home.php?mod=space&uid=28414]@net[/url]            www.openedv.com
  10.     * @date           2021-05-10
  11.     *******************************************************************/
  12. 1   #ifndef AUDIORECORDER_H
  13. 2   #define AUDIORECORDER_H
  14. 3  
  15. 4   #include <QMainWindow>
  16. 5   #include <QListWidget>
  17. 6   #include <QVBoxLayout>
  18. 7   #include <QPushButton>
  19. 8   #include <QLabel>
  20. 9   #include <QAudioRecorder>
  21. 10  #include <QAudioProbe>
  22. 11  #include <QAudioBuffer>
  23. 12  #include <QMediaPlaylist>
  24. 13  #include <QMediaPlayer>
  25. 14  #include <QProgressBar>
  26. 15
  27. 16  /* 媒体信息结构体 */
  28. 17  struct MediaObjectInfo {
  29. 18      /* 用于保存视频文件名 */
  30. 19      QString fileName;
  31. 20      /* 用于保存视频文件路径 */
  32. 21      QString filePath;
  33. 22  };
  34. 23
  35. 24  class AudioRecorder : public QMainWindow
  36. 25  {
  37. 26      Q_OBJECT
  38. 27
  39. 28  public:
  40. 29      AudioRecorder(QWidget *parent = nullptr);
  41. 30      ~AudioRecorder();
  42. 31
  43. 32  private:
  44. 33      /* 布局初始化 */
  45. 34      void layoutInit();
  46. 35
  47. 36      /* 主Widget*/
  48. 37      QWidget *mainWidget;
  49. 38
  50. 39      /* 录音列表 */
  51. 40      QListWidget *listWidget;
  52. 41
  53. 42      /* 底部的Widget,用于存放按钮 */
  54. 43      QWidget *bottomWidget;
  55. 44
  56. 45      /* 中间的显示录制时长的Widget容器 */
  57. 46      QWidget *centerWidget;
  58. 47
  59. 48      /* 垂直布局 */
  60. 49      QVBoxLayout *vBoxLayout;
  61. 50
  62. 51      /* 录音Level布局 */
  63. 52      QHBoxLayout *levelHBoxLayout;
  64. 53
  65. 54      /* 水平布局 */
  66. 55      QHBoxLayout *hBoxLayout;
  67. 56
  68. 57      /* 录音按钮 */
  69. 58      QPushButton *recorderBt;
  70. 59
  71. 60      /* 上一首按钮 */
  72. 61      QPushButton *previousBt;
  73. 62
  74. 63      /* 下一首按钮 */
  75. 64      QPushButton *nextBt;
  76. 65
  77. 66      /* 删除按钮 */
  78. 67      QPushButton *removeBt;
  79. 68
  80. 69      /* 录音类 */
  81. 70      QAudioRecorder *m_audioRecorder = nullptr;
  82. 71
  83. 72      /* 用于探测缓冲区的level */
  84. 73      QAudioProbe *m_probe = nullptr;
  85. 74
  86. 75      /* 扫描录音文件 */
  87. 76      void scanRecordFiles();
  88. 77
  89. 78      /* 录音设置容器,保存录音设备的可用信息,
  90. 79       * 本例使用默认的信息,即可录音 */
  91. 80      QList<QVariant>devicesVar;
  92. 81      QList<QVariant>codecsVar;
  93. 82      QList<QVariant>containersVar;
  94. 83      QList<QVariant>sampleRateVar;
  95. 84      QList<QVariant>channelsVar;
  96. 85      QList<QVariant>qualityVar;
  97. 86      QList<QVariant>bitratesVar;
  98. 87
  99. 88      /* 媒体播放器,用于播放视频 */
  100. 89      QMediaPlayer *recorderPlayer;
  101. 90
  102. 91      /* 媒体列表 */
  103. 92      QMediaPlaylist *mediaPlaylist;
  104. 93
  105. 94      /* 录音媒体信息存储 */
  106. 95      QVector<MediaObjectInfo> mediaObjectInfo;
  107. 96
  108. 97      /* 用于显示录音时长 */
  109. 98      QLabel *countLabel;
  110. 99
  111. 100     /* 用于显示录音level,最多四通道 */
  112. 101     QProgressBar *progressBar[4];
  113. 102
  114. 103     /* 清空录音level */
  115. 104     void clearAudioLevels();
  116. 105
  117. 106 private slots:
  118. 107     /* 点击录音按钮槽函数 */
  119. 108     void recorderBtClicked();
  120. 109
  121. 110     /* 播放列表点击 */
  122. 111     void listWidgetCliked(QListWidgetItem*);
  123. 112
  124. 113     /* 当前媒体状态改变 */
  125. 114     void mediaPlayerStateChanged(QMediaPlayer::State);
  126. 115
  127. 116     /* 媒体列表改变 */
  128. 117     void mediaPlaylistCurrentIndexChanged(int);
  129. 118
  130. 119     /* 当前列表项改变 */
  131. 120     void listWidgetCurrentItemChange(QListWidgetItem*,
  132. 121                                      QListWidgetItem*);
  133. 122
  134. 123     /* 上一首按钮点击 */
  135. 124     void previousBtClicked();
  136. 125
  137. 126     /* 下一首按钮点击 */
  138. 127     void nextBtClicked();
  139. 128
  140. 129     /* 删除按钮点击 */
  141. 130     void removeBtClicked();
  142. 131
  143. 132     /* 更新录音时长 */
  144. 133     void updateProgress(qint64);
  145. 134
  146. 135     /* 在列表里显示播放时间 */
  147. 136     void recorderPlayerPositionChanged(qint64);
  148. 137
  149. 138     /* 更新录音level */
  150. 139     void processBuffer(const QAudioBuffer&);
  151. 140 };
  152. 141 #endif // AUDIORECORDER_H
复制代码
头文件里主要是声明界面所使用的元素及一些槽函数。重点是QAudioRecorder与QAudioProbe对象的声明。
在源文件“audiorecorder.cpp”具体代码如下。
  1. audiorecorder.cpp编程后的代码
  2.     /******************************************************************
  3.     Copyright (C) 2017 The Qt Company Ltd.
  4.     Copyright &#169; Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
  5.     * @projectName   16_audiorecorder
  6.     * @brief         audiorecorder.cpp
  7.     * @author        Deng Zhimao
  8.     * @email         1252699831@qq.com
  9.     * @net            www.openedv.com
  10.     * @date           2021-05-10
  11.     *******************************************************************/
  12. 1   #include "audiorecorder.h"
  13. 2   #include <QDebug>
  14. 3   #include <QUrl>
  15. 4   #include <QDateTime>
  16. 5   #include <QDir>
  17. 6   #include <QCoreApplication>
  18. 7  
  19. 8   static qreal getPeakValue(const QAudioFormat &format);
  20. 9   static QVector<qreal> getBufferLevels(const QAudioBuffer &buffer);
  21. 10
  22. 11  template <class T>
  23. 12  static QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels);
  24. 13
  25. 14  AudioRecorder::AudioRecorder(QWidget *parent)
  26. 15      : QMainWindow(parent)
  27. 16  {
  28. 17      /* 初始化布局 */
  29. 18      layoutInit();
  30. 19
  31. 20      /* 录制音频的类 */
  32. 21      m_audioRecorder = new QAudioRecorder(this);
  33. 22
  34. 23      /* 用于探测缓冲区的数据 */
  35. 24      m_probe = new QAudioProbe(this);
  36. 25
  37. 26      /* 信号槽连接,更新录音level显示 */
  38. 27      connect(m_probe, &QAudioProbe::audioBufferProbed,
  39. 28              this, &AudioRecorder::processBuffer);
  40. 29
  41. 30      /* 设置探测的对象 */
  42. 31      m_probe->setSource(m_audioRecorder);
  43. 32
  44. 33      /* 播放器 */
  45. 34      recorderPlayer = new QMediaPlayer(this);
  46. 35
  47. 36      /* 播放列表 */
  48. 37      mediaPlaylist = new QMediaPlaylist(this);
  49. 38
  50. 39      recorderPlayer->setPlaylist(mediaPlaylist);
  51. 40
  52. 41      /* 设置播放模式,默认是列表播放 */
  53. 42      mediaPlaylist->setPlaybackMode(QMediaPlaylist::CurrentItemOnce);
  54. 43
  55. 44      /* 扫描本地声卡设备 */
  56. 45      devicesVar.append(QVariant(QString()));
  57. 46      for (auto &device: m_audioRecorder->audioInputs()) {
  58. 47          devicesVar.append(QVariant(device));
  59. 48          //qDebug()<<"本地声卡设备:"<<device<<endl;
  60. 49      }
  61. 50
  62. 51      /* 音频编码 */
  63. 52      codecsVar.append(QVariant(QString()));
  64. 53      for (auto &codecName: m_audioRecorder->supportedAudioCodecs()) {
  65. 54          codecsVar.append(QVariant(codecName));
  66. 55          //qDebug()<<"音频编码:"<<codecName<<endl;
  67. 56      }
  68. 57
  69. 58      /* 容器/支持的格式 */
  70. 59      containersVar.append(QVariant(QString()));
  71. 60      for (auto &containerName: m_audioRecorder->supportedContainers()) {
  72. 61          containersVar.append(QVariant(containerName));
  73. 62          //qDebug()<<"支持的格式:"<<containerName<<endl;
  74. 63      }
  75. 64
  76. 65      /* 采样率 */
  77. 66      sampleRateVar.append(QVariant(0));
  78. 67      for (int sampleRate: m_audioRecorder->supportedAudioSampleRates()) {
  79. 68          sampleRateVar.append(QVariant(sampleRate));
  80. 69          //qDebug()<<"采样率:"<<sampleRate<<endl;
  81. 70      }
  82. 71
  83. 72      /* 通道 */
  84. 73      channelsVar.append(QVariant(-1));
  85. 74      channelsVar.append(QVariant(1));
  86. 75      channelsVar.append(QVariant(2));
  87. 76      channelsVar.append(QVariant(4));
  88. 77
  89. 78      /* 质量 */
  90. 79      qualityVar.append(QVariant(int(QMultimedia::LowQuality)));
  91. 80      qualityVar.append(QVariant(int(QMultimedia::NormalQuality)));
  92. 81      qualityVar.append(QVariant(int(QMultimedia::HighQuality)));
  93. 82
  94. 83      /* 比特率 */
  95. 84      bitratesVar.append(QVariant(0));
  96. 85      bitratesVar.append(QVariant(32000));
  97. 86      bitratesVar.append(QVariant(64000));
  98. 87      bitratesVar.append(QVariant(96000));
  99. 88      bitratesVar.append(QVariant(128000));
  100. 89
  101. 90      /* 初始化时扫描已经录制的录音mp3文件 */
  102. 91      scanRecordFiles();
  103. 92
  104. 93      /* 录音类信号槽连接 */
  105. 94      connect(m_audioRecorder, &QAudioRecorder::durationChanged,
  106. 95              this, &AudioRecorder::updateProgress);
  107. 96
  108. 97      /* 列表信号槽连接 */
  109. 98      connect(listWidget, SIGNAL(itemClicked(QListWidgetItem*)),
  110. 99              this, SLOT(listWidgetCliked(QListWidgetItem*)));
  111. 100     connect(listWidget, SIGNAL(currentItemChanged(QListWidgetItem*,
  112. 101                                                   QListWidgetItem*)),
  113. 102             this, SLOT(listWidgetCurrentItemChange(QListWidgetItem*,
  114. 103                                                    QListWidgetItem*)));
  115. 104
  116. 105     /* 媒体连接信号槽 */
  117. 106     connect(recorderPlayer,
  118. 107             SIGNAL(stateChanged(QMediaPlayer::State)),
  119. 108             this,
  120. 109             SLOT(mediaPlayerStateChanged(QMediaPlayer::State)));
  121. 110     connect(mediaPlaylist,
  122. 111             SIGNAL(currentIndexChanged(int)),
  123. 112             this,
  124. 113             SLOT(mediaPlaylistCurrentIndexChanged(int)));
  125. 114     connect(recorderPlayer, SIGNAL(positionChanged(qint64)),
  126. 115             this,
  127. 116             SLOT(recorderPlayerPositionChanged(qint64)));
  128. 117
  129. 118     /* 按钮 */
  130. 119     connect(recorderBt, SIGNAL(clicked()), this, SLOT(recorderBtClicked()));
  131. 120     connect(nextBt, SIGNAL(clicked()), this, SLOT(nextBtClicked()));
  132. 121     connect(previousBt, SIGNAL(clicked()), this, SLOT(previousBtClicked()));
  133. 122     connect(removeBt, SIGNAL(clicked()), this, SLOT(removeBtClicked()));
  134. 123 }
  135. 124
  136. 125 AudioRecorder::~AudioRecorder()
  137. 126 {
  138. 127 }
  139. 128
  140. 129 void AudioRecorder::layoutInit()
  141. 130 {
  142. 131     this->setGeometry(0, 0, 800, 480);
  143. 132
  144. 133     mainWidget = new QWidget();
  145. 134     setCentralWidget(mainWidget);
  146. 135
  147. 136     vBoxLayout = new QVBoxLayout();
  148. 137     bottomWidget = new QWidget();
  149. 138     listWidget = new QListWidget();
  150. 139     listWidget->setFocusPolicy(Qt::NoFocus);
  151. 140     listWidget->setVerticalScrollBarPolicy(
  152. 141                 Qt::ScrollBarAlwaysOff);
  153. 142     listWidget->setHorizontalScrollBarPolicy(
  154. 143                 Qt::ScrollBarAlwaysOff);
  155. 144
  156. 145     /* 垂直布局 */
  157. 146     vBoxLayout->addWidget(listWidget);
  158. 147     vBoxLayout->addWidget(bottomWidget);
  159. 148     vBoxLayout->setContentsMargins(0, 0, 0, 0);
  160. 149     mainWidget->setLayout(vBoxLayout);
  161. 150
  162. 151     bottomWidget->setMinimumHeight(80);
  163. 152     bottomWidget->setMaximumHeight(80);
  164. 153     bottomWidget->setStyleSheet("background:#cccccc");
  165. 154
  166. 155     /* 水平布局 */
  167. 156     hBoxLayout = new QHBoxLayout();
  168. 157
  169. 158     /* 按钮,录音、上一首、下一首、删除项按钮 */
  170. 159     recorderBt = new QPushButton();
  171. 160     previousBt = new QPushButton();
  172. 161     nextBt = new QPushButton();
  173. 162     removeBt = new QPushButton();
  174. 163
  175. 164     recorderBt->setCheckable(true);
  176. 165     recorderBt->setObjectName("recorderBt");
  177. 166     recorderBt->setFocusPolicy(Qt::NoFocus);
  178. 167     recorderBt->setMaximumSize(60, 60);
  179. 168     recorderBt->setMinimumSize(60, 60);
  180. 169
  181. 170     hBoxLayout->setContentsMargins(0, 0, 0, 0);
  182. 171
  183. 172     bottomWidget->setLayout(hBoxLayout);
  184. 173     hBoxLayout->addWidget(recorderBt);
  185. 174     hBoxLayout->addWidget(previousBt);
  186. 175     hBoxLayout->addWidget(nextBt);
  187. 176     hBoxLayout->addWidget(removeBt);
  188. 177
  189. 178     nextBt->setMaximumSize(50, 50);
  190. 179     removeBt->setMaximumSize(50, 50);
  191. 180     previousBt->setMaximumSize(50, 50);
  192. 181
  193. 182     previousBt->setObjectName("previousBt");
  194. 183     removeBt->setObjectName("removeBt");
  195. 184     nextBt->setObjectName("nextBt");
  196. 185
  197. 186     previousBt->setFocusPolicy(Qt::NoFocus);
  198. 187     removeBt->setFocusPolicy(Qt::NoFocus);
  199. 188     nextBt->setFocusPolicy(Qt::NoFocus);
  200. 189
  201. 190     /* 显示录音时长与录音Level */
  202. 191     centerWidget = new QWidget(this);
  203. 192     centerWidget->setGeometry(width()/ 2 - 150,
  204. 193                               height() /2 - 100,
  205. 194                               300,
  206. 195                               200);
  207. 196     centerWidget->setStyleSheet("QWidget { background:#8823242a;"
  208. 197                                 "border-radius:10px}");
  209. 198     countLabel = new QLabel(centerWidget);
  210. 199     countLabel->setGeometry(0,
  211. 200                             0,
  212. 201                             300,
  213. 202                             50);
  214. 203     countLabel->setStyleSheet("QLabel {font-size: 30px;color:#eeeeee;"
  215. 204                               "font: blod;background:transparent}");
  216. 205     countLabel->setAlignment(Qt::AlignCenter);
  217. 206     levelHBoxLayout = new QHBoxLayout();
  218. 207
  219. 208     for (int i = 0; i < 4; i++) {
  220. 209         progressBar[i] = new QProgressBar();
  221. 210         progressBar[i]->setOrientation(Qt::Vertical);
  222. 211         progressBar[i]->setRange(0, 100);
  223. 212         progressBar[i]->setVisible(false);
  224. 213         progressBar[i]->setMaximumWidth(centralWidget()->width());
  225. 214         levelHBoxLayout->addWidget(progressBar[i]);
  226. 215         levelHBoxLayout->setContentsMargins(5, 50, 5, 5);
  227. 216         progressBar[i]->setStyleSheet("QWidget { background:#22eeeeee;"
  228. 217                                       "border-radius:0px}");
  229. 218     }
  230. 219     centerWidget->setLayout(levelHBoxLayout);
  231. 220     centerWidget->hide();
  232. 221     countLabel->raise();
  233. 222
  234. 223
  235. 224 }
  236. 225
  237. 226 void AudioRecorder::recorderBtClicked()
  238. 227 {
  239. 228     /* 录音前停止正在播放的媒体 */
  240. 229     if (recorderPlayer->state() != QMediaPlayer::StoppedState)
  241. 230         recorderPlayer->stop();
  242. 231     /* 如果录音已经停止,则开始录音 */
  243. 232     if (m_audioRecorder->state() == QMediaRecorder::StoppedState) {
  244. 233         /* 设置默认的录音设备 */
  245. 234         m_audioRecorder->setAudioInput(devicesVar.at(0).toString());
  246. 235
  247. 236         /* 下面的是录音设置,都是选择默认,可根据录音可用项,自行修改 */
  248. 237         QAudioEncoderSettings settings;
  249. 238         settings.setCodec(codecsVar.at(0).toString());
  250. 239         settings.setSampleRate(sampleRateVar[0].toInt());
  251. 240         settings.setBitRate(bitratesVar[0].toInt());
  252. 241         settings.setChannelCount(channelsVar[0].toInt());
  253. 242         settings.setQuality(QMultimedia::EncodingQuality(
  254. 243                                 qualityVar[0].toInt()));
  255. 244         /* 以恒定的质量录制,可选恒定的比特率 */
  256. 245         settings.setEncodingMode(QMultimedia::ConstantQualityEncoding);
  257. 246         QString container = containersVar.at(0).toString();
  258. 247         m_audioRecorder->setEncodingSettings(settings,
  259. 248                                              QVideoEncoderSettings(),
  260. 249                                              container);
  261. 250         m_audioRecorder->setOutputLocation(
  262. 251                     QUrl::fromLocalFile(tr("./Sounds/%1.mp3")
  263. 252                                         .arg(QDateTime::currentDateTime()
  264. 253                                              .toString())));
  265. 254         /* 开始录音 */
  266. 255         m_audioRecorder->record();
  267. 256         /* 显示录制时长标签 */
  268. 257         countLabel->clear();
  269. 258         centerWidget->show();
  270. 259     } else {
  271. 260         /* 停止录音 */
  272. 261         m_audioRecorder->stop();
  273. 262         /* 重设录音level */
  274. 263         clearAudioLevels();
  275. 264         /* 隐藏录制时长标签 */
  276. 265         centerWidget->hide();
  277. 266         /* 重新扫描录音文件 */
  278. 267         scanRecordFiles();
  279. 268     }
  280. 269 }
  281. 270
  282. 271 void AudioRecorder::scanRecordFiles()
  283. 272 {
  284. 273     mediaPlaylist->clear();
  285. 274     listWidget->clear();
  286. 275     mediaObjectInfo.clear();
  287. 276     /* 录音文件保存在当前Sounds文件夹下 */
  288. 277     QDir dir(QCoreApplication::applicationDirPath()
  289. 278              + "/Sounds");
  290. 279     QDir dirbsolutePath(dir.absolutePath());
  291. 280
  292. 281     /* 如果文件夹不存在,则创建一个 */
  293. 282     if(!dirbsolutePath.exists())
  294. 283         dirbsolutePath.mkdir(dirbsolutePath.absolutePath());
  295. 284
  296. 285     /* 定义过滤器 */
  297. 286     QStringList filter;
  298. 287     /* 包含所有xx后缀的文件 */
  299. 288     filter<<"*.mp3";
  300. 289     /* 获取该目录下的所有文件 */
  301. 290     QFileInfoList files =
  302. 291             dirbsolutePath.entryInfoList(filter, QDir::Files);
  303. 292     /* 遍历 */
  304. 293     for (int i = 0; i < files.count(); i++) {
  305. 294         MediaObjectInfo info;
  306. 295         /* 使用utf-8编码 */
  307. 296         info.fileName = QString::fromUtf8(files.at(i)
  308. 297                                           .fileName()
  309. 298                                           .toUtf8()
  310. 299                                           .data());
  311. 300         info.filePath = QString::fromUtf8(files.at(i)
  312. 301                                           .filePath()
  313. 302                                           .toUtf8()
  314. 303                                           .data());
  315. 304         /* 媒体列表添加音频 */
  316. 305         if (mediaPlaylist->addMedia(
  317. 306                     QUrl::fromLocalFile(info.filePath))) {
  318. 307             /* 添加到容器数组里储存 */
  319. 308             mediaObjectInfo.append(info);
  320. 309             /* 添加音频名字至列表 */
  321. 310             listWidget->addItem(
  322. 311                         new QListWidgetItem(QIcon(":/icons/play.png"),
  323. 312                                             info.fileName));
  324. 313         } else {
  325. 314             qDebug()<<
  326. 315                        mediaPlaylist->errorString()
  327. 316                        .toUtf8().data()
  328. 317                     << endl;
  329. 318             qDebug()<< "  Error number:"
  330. 319                     << mediaPlaylist->error()
  331. 320                     << endl;
  332. 321         }
  333. 322     }
  334. 323 }
  335. 324
  336. 325 void AudioRecorder::listWidgetCliked(QListWidgetItem *item)
  337. 326 {
  338. 327     /* item->setIcon 为设置列表里的图标状态 */
  339. 328     for (int i = 0; i < listWidget->count(); i++) {
  340. 329         listWidget->item(i)->setIcon(QIcon(":/icons/play.png"));
  341. 330     }
  342. 331
  343. 332     if (recorderPlayer->state() != QMediaPlayer::PlayingState) {
  344. 333         recorderPlayer->play();
  345. 334         item->setIcon(QIcon(":/icons/pause.png"));
  346. 335     } else {
  347. 336         recorderPlayer->pause();
  348. 337         item->setIcon(QIcon(":/icons/play.png"));
  349. 338     }
  350. 339 }
  351. 340
  352. 341 void AudioRecorder::listWidgetCurrentItemChange(
  353. 342         QListWidgetItem *currentItem,
  354. 343         QListWidgetItem *previousItem)
  355. 344 {
  356. 345     if (mediaPlaylist->mediaCount() == 0)
  357. 346         return;
  358. 347
  359. 348     if (listWidget->row(previousItem) != -1)
  360. 349         previousItem->setText(mediaObjectInfo
  361. 350                               .at(listWidget->row(previousItem))
  362. 351                               .fileName);
  363. 352
  364. 353     /* 先暂停播放媒体 */
  365. 354     if (recorderPlayer->state() == QMediaPlayer::PlayingState)
  366. 355         recorderPlayer->pause();
  367. 356
  368. 357     /* 设置当前媒体 */
  369. 358     mediaPlaylist->
  370. 359             setCurrentIndex(listWidget->row(currentItem));
  371. 360 }
  372. 361
  373. 362 void AudioRecorder::mediaPlayerStateChanged(
  374. 363         QMediaPlayer::State
  375. 364         state)
  376. 365 {
  377. 366     for (int i = 0; i < listWidget->count(); i++) {
  378. 367         listWidget->item(i)
  379. 368                 ->setIcon(QIcon(":/icons/play.png"));
  380. 369     }
  381. 370
  382. 371     /* 获取当前项,根据当前媒体的状态,然后设置不同的图标 */
  383. 372     if (mediaPlaylist->currentIndex() == -1)
  384. 373         return;
  385. 374     QListWidgetItem *item = listWidget->item(
  386. 375                 mediaPlaylist->currentIndex());
  387. 376
  388. 377     switch (state) {
  389. 378     case QMediaPlayer::PausedState:
  390. 379     case QMediaPlayer::PlayingState:
  391. 380         item->setIcon(QIcon(":/icons/pause.png"));
  392. 381         break;
  393. 382     case QMediaPlayer::StoppedState:
  394. 383         item->setIcon(QIcon(":/icons/play.png"));
  395. 384         break;
  396. 385     }
  397. 386 }
  398. 387
  399. 388 void AudioRecorder::mediaPlaylistCurrentIndexChanged(
  400. 389         int index)
  401. 390 {
  402. 391     if (-1 == index)
  403. 392         return;
  404. 393 }
  405. 394
  406. 395 void AudioRecorder::previousBtClicked()
  407. 396 {
  408. 397     /* 上一首操作 */
  409. 398     recorderPlayer->stop();
  410. 399     int count = listWidget->count();
  411. 400     if (0 == count)
  412. 401         return;
  413. 402     if (listWidget->currentRow() == -1)
  414. 403         listWidget->setCurrentRow(0);
  415. 404     else {
  416. 405         if (listWidget->currentRow() - 1 != -1)
  417. 406             listWidget->setCurrentRow(
  418. 407                         listWidget->currentRow() - 1);
  419. 408         else
  420. 409             listWidget->setCurrentRow(listWidget->count() - 1);
  421. 410     }
  422. 411     mediaPlaylist->setCurrentIndex(listWidget->currentRow());
  423. 412     recorderPlayer->play();
  424. 413 }
  425. 414
  426. 415 void AudioRecorder::nextBtClicked()
  427. 416 {
  428. 417     /* 下一首操作 */
  429. 418     recorderPlayer->stop();
  430. 419
  431. 420     /* 获取列表的总数目 */
  432. 421     int count = listWidget->count();
  433. 422
  434. 423     /* 如果列表的总数目为0则返回 */
  435. 424     if (0 == count)
  436. 425         return;
  437. 426
  438. 427     if (listWidget->currentRow() == -1)
  439. 428         listWidget->setCurrentRow(0);
  440. 429     else {
  441. 430         if (listWidget->currentRow() + 1 < listWidget->count())
  442. 431             listWidget->setCurrentRow(
  443. 432                         listWidget->currentRow() + 1);
  444. 433         else
  445. 434             listWidget->setCurrentRow(0);
  446. 435     }
  447. 436     mediaPlaylist->setCurrentIndex(listWidget->currentRow());
  448. 437     recorderPlayer->play();
  449. 438 }
  450. 439
  451. 440 void AudioRecorder::removeBtClicked()
  452. 441 {
  453. 442     int index = listWidget->currentRow();
  454. 443     if (index == -1)
  455. 444         return;
  456. 445
  457. 446     /* 移除媒体的项 */
  458. 447     mediaPlaylist->removeMedia(index);
  459. 448
  460. 449     /* 指向要删除的文件 */
  461. 450     QFile file(mediaObjectInfo.at(index).filePath);
  462. 451
  463. 452     /* 移除录音文件 */
  464. 453     file.remove();
  465. 454
  466. 455     /* 删除列表选中的项 */
  467. 456     listWidget->takeItem(index);
  468. 457
  469. 458     /* 删除后设置当前项为删除项的前一个 */
  470. 459     if (index - 1 != -1)
  471. 460         listWidget->setCurrentRow(index - 1);
  472. 461 }
  473. 462
  474. 463 void AudioRecorder::updateProgress(qint64 duration)
  475. 464 {
  476. 465     if (m_audioRecorder->error()
  477. 466             != QMediaRecorder::NoError)
  478. 467         return;
  479. 468
  480. 469     /* 显示录制时长 */
  481. 470     countLabel->setText(tr("已录制 %1 s")
  482. 471                         .arg(duration / 1000));
  483. 472 }
  484. 473
  485. 474 void AudioRecorder::recorderPlayerPositionChanged(
  486. 475         qint64 position)
  487. 476 {
  488. 477     /* 格式化时间 */
  489. 478     int p_second  = position / 1000;
  490. 479     int p_minute = p_second / 60;
  491. 480     p_second %= 60;
  492. 481
  493. 482     QString mediaPosition;
  494. 483     mediaPosition.clear();
  495. 484
  496. 485     if (p_minute >= 10)
  497. 486         mediaPosition = QString::number(p_minute, 10);
  498. 487     else
  499. 488         mediaPosition = "0" + QString::number(p_minute, 10);
  500. 489
  501. 490     if (p_second >= 10)
  502. 491         mediaPosition = mediaPosition
  503. 492                 + ":" + QString::number(p_second, 10);
  504. 493     else
  505. 494         mediaPosition = mediaPosition
  506. 495                 + ":0" + QString::number(p_second, 10);
  507. 496
  508. 497
  509. 498     int d_second  =  recorderPlayer->duration() / 1000;
  510. 499     int d_minute = d_second / 60;
  511. 500     d_second %= 60;
  512. 501
  513. 502     QString mediaDuration;
  514. 503     mediaDuration.clear();
  515. 504
  516. 505     if (d_minute >= 10)
  517. 506         mediaDuration = QString::number(d_minute, 10);
  518. 507     else
  519. 508         mediaDuration = "0" + QString::number(d_minute, 10);
  520. 509
  521. 510     if (d_second >= 10)
  522. 511         mediaDuration = mediaDuration
  523. 512                 + ":" + QString::number(d_second, 10);
  524. 513     else
  525. 514         mediaDuration = mediaDuration
  526. 515                 + ":0" + QString::number(d_second, 10);
  527. 516
  528. 517     QString fileNmae = mediaObjectInfo
  529. 518             .at(listWidget->currentRow()).fileName + "\t";
  530. 519     /* 显示媒体总长度时间与播放的当前位置 */
  531. 520     listWidget->currentItem()->setText(fileNmae
  532. 521                                        + mediaPosition
  533. 522                                        +"/" + mediaDuration);
  534. 523 }
  535. 524
  536. 525 void AudioRecorder::clearAudioLevels()
  537. 526 {
  538. 527     for (int i = 0; i < 4; i++)
  539. 528         progressBar[i]->setValue(0);
  540. 529 }
  541. 530
  542. 531 // This function returns the maximum possible sample value for a given audio format
  543. 532 qreal getPeakValue(const QAudioFormat& format)
  544. 533 {
  545. 534     // Note: Only the most common sample formats are supported
  546. 535     if (!format.isValid())
  547. 536         return qreal(0);
  548. 537
  549. 538     if (format.codec() != "audio/pcm")
  550. 539         return qreal(0);
  551. 540
  552. 541     switch (format.sampleType()) {
  553. 542     case QAudioFormat::Unknown:
  554. 543         break;
  555. 544     case QAudioFormat::Float:
  556. 545         if (format.sampleSize() != 32) // other sample formats are not supported
  557. 546             return qreal(0);
  558. 547         return qreal(1.00003);
  559. 548     case QAudioFormat::SignedInt:
  560. 549         if (format.sampleSize() == 32)
  561. 550             return qreal(INT_MAX);
  562. 551         if (format.sampleSize() == 16)
  563. 552             return qreal(SHRT_MAX);
  564. 553         if (format.sampleSize() == 8)
  565. 554             return qreal(CHAR_MAX);
  566. 555         break;
  567. 556     case QAudioFormat::UnSignedInt:
  568. 557         if (format.sampleSize() == 32)
  569. 558             return qreal(UINT_MAX);
  570. 559         if (format.sampleSize() == 16)
  571. 560             return qreal(USHRT_MAX);
  572. 561         if (format.sampleSize() == 8)
  573. 562             return qreal(UCHAR_MAX);
  574. 563         break;
  575. 564     }
  576. 565
  577. 566     return qreal(0);
  578. 567 }
  579. 568
  580. 569 // returns the audio level for each channel
  581. 570 QVector<qreal> getBufferLevels(const QAudioBuffer& buffer)
  582. 571 {
  583. 572     QVector<qreal> values;
  584. 573
  585. 574     if (!buffer.format().isValid() || buffer.format().byteOrder() != QAudioFormat::LittleEndian)
  586. 575         return values;
  587. 576
  588. 577     if (buffer.format().codec() != "audio/pcm")
  589. 578         return values;
  590. 579
  591. 580     int channelCount = buffer.format().channelCount();
  592. 581     values.fill(0, channelCount);
  593. 582     qreal peak_value = getPeakValue(buffer.format());
  594. 583     if (qFuzzyCompare(peak_value, qreal(0)))
  595. 584         return values;
  596. 585
  597. 586     switch (buffer.format().sampleType()) {
  598. 587     case QAudioFormat::Unknown:
  599. 588     case QAudioFormat::UnSignedInt:
  600. 589         if (buffer.format().sampleSize() == 32)
  601. 590             values = getBufferLevels(buffer.constData<quint32>(), buffer.frameCount(), channelCount);
  602. 591         if (buffer.format().sampleSize() == 16)
  603. 592             values = getBufferLevels(buffer.constData<quint16>(), buffer.frameCount(), channelCount);
  604. 593         if (buffer.format().sampleSize() == 8)
  605. 594             values = getBufferLevels(buffer.constData<quint8>(), buffer.frameCount(), channelCount);
  606. 595         for (int i = 0; i < values.size(); ++i)
  607. 596             values[i] = qAbs(values.at(i) - peak_value / 2) / (peak_value / 2);
  608. 597         break;
  609. 598     case QAudioFormat::Float:
  610. 599         if (buffer.format().sampleSize() == 32) {
  611. 600             values = getBufferLevels(buffer.constData<float>(), buffer.frameCount(), channelCount);
  612. 601             for (int i = 0; i < values.size(); ++i)
  613. 602                 values[i] /= peak_value;
  614. 603         }
  615. 604         break;
  616. 605     case QAudioFormat::SignedInt:
  617. 606         if (buffer.format().sampleSize() == 32)
  618. 607             values = getBufferLevels(buffer.constData<qint32>(), buffer.frameCount(), channelCount);
  619. 608         if (buffer.format().sampleSize() == 16)
  620. 609             values = getBufferLevels(buffer.constData<qint16>(), buffer.frameCount(), channelCount);
  621. 610         if (buffer.format().sampleSize() == 8)
  622. 611             values = getBufferLevels(buffer.constData<qint8>(), buffer.frameCount(), channelCount);
  623. 612         for (int i = 0; i < values.size(); ++i)
  624. 613             values[i] /= peak_value;
  625. 614         break;
  626. 615     }
  627. 616
  628. 617     return values;
  629. 618 }
  630. 619
  631. 620 template <class T>
  632. 621 QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels)
  633. 622 {
  634. 623     QVector<qreal> max_values;
  635. 624     max_values.fill(0, channels);
  636. 625
  637. 626     for (int i = 0; i < frames; ++i) {
  638. 627         for (int j = 0; j < channels; ++j) {
  639. 628             qreal value = qAbs(qreal(buffer[i * channels + j]));
  640. 629             if (value > max_values.at(j))
  641. 630                 max_values.replace(j, value);
  642. 631         }
  643. 632     }
  644. 633
  645. 634     return max_values;
  646. 635 }
  647. 636
  648. 637 void AudioRecorder::processBuffer(const QAudioBuffer& buffer)
  649. 638 {
  650. 639     /* 根据通道数目需要显示count个level */
  651. 640     int count = buffer.format().channelCount();
  652. 641     for (int i = 0; i < 4; i++) {
  653. 642         if (i < count)
  654. 643             progressBar[i]->setVisible(true);
  655. 644         else
  656. 645             progressBar[i]->setVisible(false);
  657. 646     }
  658. 647
  659. 648     /* 设置level的值 */
  660. 649     QVector<qreal> levels = getBufferLevels(buffer);
  661. 650     for (int i = 0; i < levels.count(); ++i)
  662. 651         progressBar[i]->setValue(levels.at(i) * 100);
  663. 652 }
复制代码
       布局与播放录音部分的代码编者不再解释,与音乐播放器和视频播放器的原理一样。只是换了个样式而已。
       第21行,初始化录音类对象,m_audioRecorder。录音的功能全靠这个类了,要完成录音的工作,我们只需要关注这个类。剩下的都是其他功能的实现。
       第44~88行,本例将录音设置的参数,参数可以从Qt提供的API里如supportedAudioCodecs()表示可用支持的编码方式,详细请参考代码,全部使用QVariant容器来储存。这样可以不必要暴露太多接口给用户修改,以免出错。
       第222~269,是录音功能的重要代码,一般是通过QAudioEncoderSettings来设置输入音频设置,主要是编码格式、采样率、通道数、音频质量等设置(音频格式Qt提供了如setCodes()等方式可以直接设置,音频相关知识不探讨,我们只需要知道这个流程即可。)。这些参考Qt官方的audiorecorder例程,使用默认的设置,也就是Default项,Qt自动选择系统音频输入设备输入,同时自动确定底层的采样参数等。在Linux里,Qt扫描声卡的设备项很多,让用户选择可能会导致出错。所以编者测试使用默认的设置,即可录音,无需用户自行选择和修改。同时设置了录音保存的文件名为xx.mp3。根据系统时间命名录音文件,如果不指定录音文件名,在Linux下一般保存为clip_0001.mov,clip_0002.mov等录音文件(mov格式为苹果操作系统常用音视频格式)。Windows一般保存为clip_0001.wav格式文件。
       设置完成录音项后,使用QAudioRecorder的record()、pause()和stop()函数即可完成录音。本例没有使用pause()也就是录音暂停,根据情景无需录音暂停。record()和stop()是开始录音和录音停止。
第23~28行,QAudioProbe类型的对象用于探测缓冲区的数据。setSource()是指定探测的对象。
第525~652行,这部分代码是直接拷贝Qt官方的代码进行修改,代码比较复杂(闲时自行理解),我们只需要知道它是从缓冲区里获取通道数与音频的实时音量。
整个代码主要完成录音的功能是QAudioRecorder、QAudioEncoderSettings和QAudioProbe类。其他代码可以没有也可以完成本例录音的功能。QAudioRecorder负责录音,QAudioProbe类负责获取通道数,与输入的实时音量。只要掌握了这两个类,设计一个好看的录音应用界面不在话下。
       main       .cpp内容如下,主要是加载qss样式文件。
  1. 1   #include "audiorecorder.h"
  2. 2
  3. 3   #include <QApplication>
  4. 4   #include <QFile>
  5. 5
  6. 6   int main(int argc, char *argv[])
  7. 7   {
  8. 8       QApplication a(argc, argv);
  9. 9       /* 指定文件 */
  10. 10      QFile file(":/style.qss");
  11. 11
  12. 12      /* 判断文件是否存在 */
  13. 13      if (file.exists() ) {
  14. 14          /* 以只读的方式打开 */
  15. 15          file.open(QFile::ReadOnly);
  16. 16          /* 以字符串的方式保存读出的结果 */
  17. 17          QString styleSheet = QLatin1String(file.readAll());
  18. 18          /* 设置全局样式 */
  19. 19          qApp->setStyleSheet(styleSheet);
  20. 20          /* 关闭文件 */
  21. 21          file.close();
  22. 22      }
  23. 23
  24. 24      AudioRecorder w;
  25. 25      w.show();
  26. 26      return a.exec();
  27. 27  }
复制代码
style.qss样式文件如下。素材已经在源码处提供。注意下面的style.qss不能有注释!
  1. 1   QTabBar:tab {
  2. 2   height:0; width:0;
  3. 3   }
  4. 4
  5. 5   QWidget {
  6. 6   background:#e6e6e6;
  7. 7   }
  8. 8
  9. 9   QListWidget {
  10. 10  border:none;
  11. 11  }
  12. 12
  13. 13  QPushButton#recorderBt {
  14. 14  border-image:url(:/icons/recorder_stop1.png);
  15. 15  background:transparent;
  16. 16  }
  17. 17
  18. 18  QPushButton#recorderBt:hover {
  19. 19  border-image:url(:/icons/recorder_stop2.png);
  20. 20  }
  21. 21
  22. 22  QPushButton#recorderBt:checked {
  23. 23  border-image:url(:/icons/recorder_start1.png);
  24. 24  }
  25. 25
  26. 26  QPushButton#recorderBt:checked:hover {
  27. 27  border-image:url(:/icons/recorder_start2.png);
  28. 28  }
  29. 29
  30. 30  QListWidget {
  31. 31  color:black;
  32. 32  font-size: 20px;
  33. 33  border:none;
  34. 34  icon-size:40px;
  35. 35  }
  36. 36
  37. 37  QListWidget:item:active {
  38. 38  background: transparent;
  39. 39  }
  40. 40
  41. 41  QListWidget:item {
  42. 42  background: transparent;
  43. 43  height:60;
  44. 44  }
  45. 45
  46. 46  QListWidget:item:selected {
  47. 47  color:red;
  48. 48  background: transparent;
  49. 49  }
  50. 50
  51. 51  QListWidget:item:hover {
  52. 52  background: transparent;
  53. 53  color:red;
  54. 54  border:none;
  55. 55  }
  56. 56
  57. 57  QPushButton#nextBt {
  58. 58  border-image:url(:/icons/btn_next1.png);
  59. 59  }
  60. 60
  61. 61  QPushButton#nextBt:hover {
  62. 62  border-image:url(:/icons/btn_next2.png);
  63. 63  }
  64. 64
  65. 65  QPushButton#previousBt {
  66. 66  border-image:url(:/icons/btn_previous1.png);
  67. 67  }
  68. 68
  69. 69  QPushButton#previousBt:hover {
  70. 70  border-image:url(:/icons/btn_previous2.png);
  71. 71  }
  72. 72
  73. 73  QPushButton#removeBt {
  74. 74  border-image:url(:/icons/remove1.png);
  75. 75  }
  76. 76
  77. 77  QPushButton#removeBt:hover {
  78. 78  border-image:url(:/icons/remove2.png);
  79. 79  }
  80. 80
  81. 81  QProgressBar::chunk {
  82. 82  background-color: #f6f6f6;
  83. 83  height: 8px;
  84. 84  margin: 0.5px;
  85. 85  border-radius:0px;
  86. 86  }
  87. 87
  88. 88  QProgressBar {
  89. 89  color:transparent;
  90. 90  }
复制代码
第十四章12.5.2 程序运行效果
本例适用于正点原子I.MX6U ALPHA开发板!请使用正点原子I.MX6U的出厂系统进行测试!
请使用正点原子的I.MX6U的出厂时的系统测试!
请使用正点原子的I.MX6U的出厂时的系统测试!
请使用正点原子的I.MX6U的出厂时的系统测试!
重要的事情是说三遍!
开始录音前,需要根据正点原子I.MX6U用户快速体验手册,第3.15小节进行测试板子的录音功能。确保能正常录音,再交叉编译此Qt应用程序到开发板上运行。如何交叉编译Qt应用程序到开发板,请看【正点原子】I.MX6U 出厂系统Qt交叉编译环境搭建V1.x版本
在正点原子I.MX6U开发板上运行此录音程序,需要先配置是麦克风(板子上的麦头)或者是Line_in输入方式。
如果是麦头录音,则在板子上运行开启麦头录音的脚本。
  1. /home/root/shell/audio/mic_in_config.sh
复制代码
如果是Line_in的方式录音,请使用一条3.5mm的两头公头的音频线,一头对板子上Line_in接口。另一头连接手机或者电脑的音频输出设备。手机或者电脑开始播放音乐,音乐尽量调高!执行下面的脚本,开启板子以line_in的方式录音。
  1. /home/root/shell/audio/line_in_config.sh
复制代码
点击左下角的录音按钮开始录音(Ubuntu上模拟效果图)。可以看到两个通道的实时音量柱状图,用于显示实时音量输入的大小。
image012.jpg
开如播放录音。(Ubuntu上模拟效果图)
image014.jpg

正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-1-19 07:59

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表