| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2017 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the examples of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:BSD$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** BSD License Usage |
| 18 | ** Alternatively, you may use this file under the terms of the BSD license |
| 19 | ** as follows: |
| 20 | ** |
| 21 | ** "Redistribution and use in source and binary forms, with or without |
| 22 | ** modification, are permitted provided that the following conditions are |
| 23 | ** met: |
| 24 | ** * Redistributions of source code must retain the above copyright |
| 25 | ** notice, this list of conditions and the following disclaimer. |
| 26 | ** * Redistributions in binary form must reproduce the above copyright |
| 27 | ** notice, this list of conditions and the following disclaimer in |
| 28 | ** the documentation and/or other materials provided with the |
| 29 | ** distribution. |
| 30 | ** * Neither the name of The Qt Company Ltd nor the names of its |
| 31 | ** contributors may be used to endorse or promote products derived |
| 32 | ** from this software without specific prior written permission. |
| 33 | ** |
| 34 | ** |
| 35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
| 46 | ** |
| 47 | ** $QT_END_LICENSE$ |
| 48 | ** |
| 49 | ****************************************************************************/ |
| 50 | |
| 51 | #include "engine.h" |
| 52 | #include "levelmeter.h" |
| 53 | #include "mainwidget.h" |
| 54 | #include "waveform.h" |
| 55 | #include "progressbar.h" |
| 56 | #include "settingsdialog.h" |
| 57 | #include "spectrograph.h" |
| 58 | #include "tonegeneratordialog.h" |
| 59 | #include "utils.h" |
| 60 | |
| 61 | #include <QLabel> |
| 62 | #include <QPushButton> |
| 63 | #include <QHBoxLayout> |
| 64 | #include <QVBoxLayout> |
| 65 | #include <QStyle> |
| 66 | #include <QMenu> |
| 67 | #include <QFileDialog> |
| 68 | #include <QTimerEvent> |
| 69 | #include <QMessageBox> |
| 70 | |
| 71 | const int NullTimerId = -1; |
| 72 | |
| 73 | MainWidget::MainWidget(QWidget *parent) |
| 74 | : QWidget(parent) |
| 75 | , m_mode(NoMode) |
| 76 | , m_engine(new Engine(this)) |
| 77 | #ifndef DISABLE_WAVEFORM |
| 78 | , m_waveform(new Waveform(this)) |
| 79 | #endif |
| 80 | , m_progressBar(new ProgressBar(this)) |
| 81 | , m_spectrograph(new Spectrograph(this)) |
| 82 | , m_levelMeter(new LevelMeter(this)) |
| 83 | , m_modeButton(new QPushButton(this)) |
| 84 | , m_recordButton(new QPushButton(this)) |
| 85 | , m_pauseButton(new QPushButton(this)) |
| 86 | , m_playButton(new QPushButton(this)) |
| 87 | , m_settingsButton(new QPushButton(this)) |
| 88 | , m_infoMessage(new QLabel(tr(s: "Select a mode to begin" ), this)) |
| 89 | , m_infoMessageTimerId(NullTimerId) |
| 90 | , m_settingsDialog(new SettingsDialog( |
| 91 | m_engine->availableAudioInputDevices(), |
| 92 | m_engine->availableAudioOutputDevices(), |
| 93 | this)) |
| 94 | , m_toneGeneratorDialog(new ToneGeneratorDialog(this)) |
| 95 | , m_modeMenu(new QMenu(this)) |
| 96 | , m_loadFileAction(0) |
| 97 | , m_generateToneAction(0) |
| 98 | , m_recordAction(0) |
| 99 | { |
| 100 | m_spectrograph->setParams(numBars: SpectrumNumBands, lowFreq: SpectrumLowFreq, highFreq: SpectrumHighFreq); |
| 101 | |
| 102 | createUi(); |
| 103 | connectUi(); |
| 104 | } |
| 105 | |
| 106 | MainWidget::~MainWidget() |
| 107 | { |
| 108 | |
| 109 | } |
| 110 | |
| 111 | |
| 112 | //----------------------------------------------------------------------------- |
| 113 | // Public slots |
| 114 | //----------------------------------------------------------------------------- |
| 115 | |
| 116 | void MainWidget::stateChanged(QAudio::Mode mode, QAudio::State state) |
| 117 | { |
| 118 | Q_UNUSED(mode); |
| 119 | |
| 120 | updateButtonStates(); |
| 121 | |
| 122 | if (QAudio::ActiveState != state && |
| 123 | QAudio::SuspendedState != state && |
| 124 | QAudio::InterruptedState != state) { |
| 125 | m_levelMeter->reset(); |
| 126 | m_spectrograph->reset(); |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | void MainWidget::formatChanged(const QAudioFormat &format) |
| 131 | { |
| 132 | infoMessage(message: formatToString(format), timeoutMs: NullMessageTimeout); |
| 133 | |
| 134 | #ifndef DISABLE_WAVEFORM |
| 135 | if (QAudioFormat() != format) { |
| 136 | m_waveform->initialize(format, audioBufferSize: WaveformTileLength, |
| 137 | windowDurationUs: WaveformWindowDuration); |
| 138 | } |
| 139 | #endif |
| 140 | } |
| 141 | |
| 142 | void MainWidget::spectrumChanged(qint64 position, qint64 length, |
| 143 | const FrequencySpectrum &spectrum) |
| 144 | { |
| 145 | m_progressBar->windowChanged(position, length); |
| 146 | m_spectrograph->spectrumChanged(spectrum); |
| 147 | } |
| 148 | |
| 149 | void MainWidget::infoMessage(const QString &message, int timeoutMs) |
| 150 | { |
| 151 | m_infoMessage->setText(message); |
| 152 | |
| 153 | if (NullTimerId != m_infoMessageTimerId) { |
| 154 | killTimer(id: m_infoMessageTimerId); |
| 155 | m_infoMessageTimerId = NullTimerId; |
| 156 | } |
| 157 | |
| 158 | if (NullMessageTimeout != timeoutMs) |
| 159 | m_infoMessageTimerId = startTimer(interval: timeoutMs); |
| 160 | } |
| 161 | |
| 162 | void MainWidget::errorMessage(const QString &heading, const QString &detail) |
| 163 | { |
| 164 | QMessageBox::warning(parent: this, title: heading, text: detail, buttons: QMessageBox::Close); |
| 165 | } |
| 166 | |
| 167 | void MainWidget::timerEvent(QTimerEvent *event) |
| 168 | { |
| 169 | Q_ASSERT(event->timerId() == m_infoMessageTimerId); |
| 170 | Q_UNUSED(event) // suppress warnings in release builds |
| 171 | killTimer(id: m_infoMessageTimerId); |
| 172 | m_infoMessageTimerId = NullTimerId; |
| 173 | m_infoMessage->setText("" ); |
| 174 | } |
| 175 | |
| 176 | void MainWidget::audioPositionChanged(qint64 position) |
| 177 | { |
| 178 | #ifndef DISABLE_WAVEFORM |
| 179 | m_waveform->audioPositionChanged(position); |
| 180 | #else |
| 181 | Q_UNUSED(position) |
| 182 | #endif |
| 183 | } |
| 184 | |
| 185 | void MainWidget::bufferLengthChanged(qint64 length) |
| 186 | { |
| 187 | m_progressBar->bufferLengthChanged(length); |
| 188 | } |
| 189 | |
| 190 | |
| 191 | //----------------------------------------------------------------------------- |
| 192 | // Private slots |
| 193 | //----------------------------------------------------------------------------- |
| 194 | |
| 195 | void MainWidget::showFileDialog() |
| 196 | { |
| 197 | const QString dir; |
| 198 | const QStringList fileNames = QFileDialog::getOpenFileNames(parent: this, caption: tr(s: "Open WAV file" ), dir, filter: "*.wav" ); |
| 199 | if (fileNames.count()) { |
| 200 | reset(); |
| 201 | setMode(LoadFileMode); |
| 202 | m_engine->loadFile(fileName: fileNames.front()); |
| 203 | updateButtonStates(); |
| 204 | } else { |
| 205 | updateModeMenu(); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | void MainWidget::showSettingsDialog() |
| 210 | { |
| 211 | m_settingsDialog->exec(); |
| 212 | if (m_settingsDialog->result() == QDialog::Accepted) { |
| 213 | m_engine->setAudioInputDevice(m_settingsDialog->inputDevice()); |
| 214 | m_engine->setAudioOutputDevice(m_settingsDialog->outputDevice()); |
| 215 | m_engine->setWindowFunction(m_settingsDialog->windowFunction()); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | void MainWidget::showToneGeneratorDialog() |
| 220 | { |
| 221 | m_toneGeneratorDialog->exec(); |
| 222 | if (m_toneGeneratorDialog->result() == QDialog::Accepted) { |
| 223 | reset(); |
| 224 | setMode(GenerateToneMode); |
| 225 | const qreal amplitude = m_toneGeneratorDialog->amplitude(); |
| 226 | if (m_toneGeneratorDialog->isFrequencySweepEnabled()) { |
| 227 | m_engine->generateSweptTone(amplitude); |
| 228 | } else { |
| 229 | const qreal frequency = m_toneGeneratorDialog->frequency(); |
| 230 | const Tone tone(frequency, amplitude); |
| 231 | m_engine->generateTone(tone); |
| 232 | updateButtonStates(); |
| 233 | } |
| 234 | } else { |
| 235 | updateModeMenu(); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | void MainWidget::initializeRecord() |
| 240 | { |
| 241 | reset(); |
| 242 | setMode(RecordMode); |
| 243 | if (m_engine->initializeRecord()) |
| 244 | updateButtonStates(); |
| 245 | } |
| 246 | |
| 247 | |
| 248 | //----------------------------------------------------------------------------- |
| 249 | // Private functions |
| 250 | //----------------------------------------------------------------------------- |
| 251 | |
| 252 | void MainWidget::createUi() |
| 253 | { |
| 254 | createMenus(); |
| 255 | |
| 256 | setWindowTitle(tr(s: "Spectrum Analyser" )); |
| 257 | |
| 258 | QVBoxLayout *windowLayout = new QVBoxLayout(this); |
| 259 | |
| 260 | m_infoMessage->setSizePolicy(hor: QSizePolicy::Preferred, ver: QSizePolicy::Fixed); |
| 261 | m_infoMessage->setAlignment(Qt::AlignHCenter); |
| 262 | windowLayout->addWidget(m_infoMessage); |
| 263 | |
| 264 | #ifdef SUPERIMPOSE_PROGRESS_ON_WAVEFORM |
| 265 | QScopedPointer<QHBoxLayout> waveformLayout(new QHBoxLayout); |
| 266 | waveformLayout->addWidget(m_progressBar); |
| 267 | m_progressBar->setMinimumHeight(m_waveform->minimumHeight()); |
| 268 | waveformLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0); |
| 269 | m_waveform->setLayout(waveformLayout.data()); |
| 270 | waveformLayout.take(); |
| 271 | windowLayout->addWidget(m_waveform); |
| 272 | #else |
| 273 | #ifndef DISABLE_WAVEFORM |
| 274 | windowLayout->addWidget(m_waveform); |
| 275 | #endif // DISABLE_WAVEFORM |
| 276 | windowLayout->addWidget(m_progressBar); |
| 277 | #endif // SUPERIMPOSE_PROGRESS_ON_WAVEFORM |
| 278 | |
| 279 | // Spectrograph and level meter |
| 280 | |
| 281 | QScopedPointer<QHBoxLayout> analysisLayout(new QHBoxLayout); |
| 282 | analysisLayout->addWidget(m_spectrograph); |
| 283 | analysisLayout->addWidget(m_levelMeter); |
| 284 | windowLayout->addLayout(layout: analysisLayout.data()); |
| 285 | analysisLayout.take(); |
| 286 | |
| 287 | // Button panel |
| 288 | |
| 289 | const QSize buttonSize(30, 30); |
| 290 | |
| 291 | m_modeButton->setText(tr(s: "Mode" )); |
| 292 | |
| 293 | m_recordIcon = QIcon(":/images/record.png" ); |
| 294 | m_recordButton->setIcon(m_recordIcon); |
| 295 | m_recordButton->setEnabled(false); |
| 296 | m_recordButton->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Fixed); |
| 297 | m_recordButton->setMinimumSize(buttonSize); |
| 298 | |
| 299 | m_pauseIcon = style()->standardIcon(standardIcon: QStyle::SP_MediaPause); |
| 300 | m_pauseButton->setIcon(m_pauseIcon); |
| 301 | m_pauseButton->setEnabled(false); |
| 302 | m_pauseButton->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Fixed); |
| 303 | m_pauseButton->setMinimumSize(buttonSize); |
| 304 | |
| 305 | m_playIcon = style()->standardIcon(standardIcon: QStyle::SP_MediaPlay); |
| 306 | m_playButton->setIcon(m_playIcon); |
| 307 | m_playButton->setEnabled(false); |
| 308 | m_playButton->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Fixed); |
| 309 | m_playButton->setMinimumSize(buttonSize); |
| 310 | |
| 311 | m_settingsIcon = QIcon(":/images/settings.png" ); |
| 312 | m_settingsButton->setIcon(m_settingsIcon); |
| 313 | m_settingsButton->setEnabled(true); |
| 314 | m_settingsButton->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Fixed); |
| 315 | m_settingsButton->setMinimumSize(buttonSize); |
| 316 | |
| 317 | QScopedPointer<QHBoxLayout> buttonPanelLayout(new QHBoxLayout); |
| 318 | buttonPanelLayout->addStretch(); |
| 319 | buttonPanelLayout->addWidget(m_modeButton); |
| 320 | buttonPanelLayout->addWidget(m_recordButton); |
| 321 | buttonPanelLayout->addWidget(m_pauseButton); |
| 322 | buttonPanelLayout->addWidget(m_playButton); |
| 323 | buttonPanelLayout->addWidget(m_settingsButton); |
| 324 | |
| 325 | QWidget *buttonPanel = new QWidget(this); |
| 326 | buttonPanel->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Fixed); |
| 327 | buttonPanel->setLayout(buttonPanelLayout.data()); |
| 328 | buttonPanelLayout.take(); // ownership transferred to buttonPanel |
| 329 | |
| 330 | QScopedPointer<QHBoxLayout> bottomPaneLayout(new QHBoxLayout); |
| 331 | bottomPaneLayout->addWidget(buttonPanel); |
| 332 | windowLayout->addLayout(layout: bottomPaneLayout.data()); |
| 333 | bottomPaneLayout.take(); // ownership transferred to windowLayout |
| 334 | |
| 335 | // Apply layout |
| 336 | |
| 337 | setLayout(windowLayout); |
| 338 | } |
| 339 | |
| 340 | void MainWidget::connectUi() |
| 341 | { |
| 342 | connect(sender: m_recordButton, signal: &QPushButton::clicked, |
| 343 | receiver: m_engine, slot: &Engine::startRecording); |
| 344 | |
| 345 | connect(sender: m_pauseButton, signal: &QPushButton::clicked, |
| 346 | receiver: m_engine, slot: &Engine::suspend); |
| 347 | |
| 348 | connect(sender: m_playButton, signal: &QPushButton::clicked, |
| 349 | receiver: m_engine, slot: &Engine::startPlayback); |
| 350 | |
| 351 | connect(sender: m_settingsButton, signal: &QPushButton::clicked, |
| 352 | receiver: this, slot: &MainWidget::showSettingsDialog); |
| 353 | |
| 354 | connect(sender: m_engine, signal: &Engine::stateChanged, |
| 355 | receiver: this, slot: &MainWidget::stateChanged); |
| 356 | |
| 357 | connect(sender: m_engine, signal: &Engine::formatChanged, |
| 358 | receiver: this, slot: &MainWidget::formatChanged); |
| 359 | |
| 360 | m_progressBar->bufferLengthChanged(length: m_engine->bufferLength()); |
| 361 | |
| 362 | connect(sender: m_engine, signal: &Engine::bufferLengthChanged, |
| 363 | receiver: this, slot: &MainWidget::bufferLengthChanged); |
| 364 | |
| 365 | connect(sender: m_engine, signal: &Engine::dataLengthChanged, |
| 366 | receiver: this, slot: &MainWidget::updateButtonStates); |
| 367 | |
| 368 | connect(sender: m_engine, signal: &Engine::recordPositionChanged, |
| 369 | receiver: m_progressBar, slot: &ProgressBar::recordPositionChanged); |
| 370 | |
| 371 | connect(sender: m_engine, signal: &Engine::playPositionChanged, |
| 372 | receiver: m_progressBar, slot: &ProgressBar::playPositionChanged); |
| 373 | |
| 374 | connect(sender: m_engine, signal: &Engine::recordPositionChanged, |
| 375 | receiver: this, slot: &MainWidget::audioPositionChanged); |
| 376 | |
| 377 | connect(sender: m_engine, signal: &Engine::playPositionChanged, |
| 378 | receiver: this, slot: &MainWidget::audioPositionChanged); |
| 379 | |
| 380 | connect(sender: m_engine, signal: &Engine::levelChanged, |
| 381 | receiver: m_levelMeter, slot: &LevelMeter::levelChanged); |
| 382 | |
| 383 | connect(sender: m_engine, signal: QOverload<qint64, qint64, const FrequencySpectrum&>::of(ptr: &Engine::spectrumChanged), |
| 384 | receiver: this, slot: QOverload<qint64, qint64, const FrequencySpectrum&>::of(ptr: &MainWidget::spectrumChanged)); |
| 385 | |
| 386 | connect(sender: m_engine, signal: &Engine::infoMessage, |
| 387 | receiver: this, slot: &MainWidget::infoMessage); |
| 388 | |
| 389 | connect(sender: m_engine, signal: &Engine::errorMessage, |
| 390 | receiver: this, slot: &MainWidget::errorMessage); |
| 391 | |
| 392 | connect(sender: m_spectrograph, signal: &Spectrograph::infoMessage, |
| 393 | receiver: this, slot: &MainWidget::infoMessage); |
| 394 | |
| 395 | #ifndef DISABLE_WAVEFORM |
| 396 | connect(sender: m_engine, signal: &Engine::bufferChanged, |
| 397 | receiver: m_waveform, slot: &Waveform::bufferChanged); |
| 398 | #endif |
| 399 | } |
| 400 | |
| 401 | void MainWidget::createMenus() |
| 402 | { |
| 403 | m_modeButton->setMenu(m_modeMenu); |
| 404 | |
| 405 | m_generateToneAction = m_modeMenu->addAction(text: tr(s: "Play generated tone" )); |
| 406 | m_recordAction = m_modeMenu->addAction(text: tr(s: "Record and play back" )); |
| 407 | m_loadFileAction = m_modeMenu->addAction(text: tr(s: "Play file" )); |
| 408 | |
| 409 | m_loadFileAction->setCheckable(true); |
| 410 | m_generateToneAction->setCheckable(true); |
| 411 | m_recordAction->setCheckable(true); |
| 412 | |
| 413 | connect(sender: m_loadFileAction, signal: &QAction::triggered, receiver: this, slot: &MainWidget::showFileDialog); |
| 414 | connect(sender: m_generateToneAction, signal: &QAction::triggered, receiver: this, slot: &MainWidget::showToneGeneratorDialog); |
| 415 | connect(sender: m_recordAction, signal: &QAction::triggered, receiver: this, slot: &MainWidget::initializeRecord); |
| 416 | } |
| 417 | |
| 418 | void MainWidget::updateButtonStates() |
| 419 | { |
| 420 | const bool recordEnabled = ((QAudio::AudioOutput == m_engine->mode() || |
| 421 | (QAudio::ActiveState != m_engine->state() && |
| 422 | QAudio::IdleState != m_engine->state())) && |
| 423 | RecordMode == m_mode); |
| 424 | m_recordButton->setEnabled(recordEnabled); |
| 425 | |
| 426 | const bool pauseEnabled = (QAudio::ActiveState == m_engine->state() || |
| 427 | QAudio::IdleState == m_engine->state()); |
| 428 | m_pauseButton->setEnabled(pauseEnabled); |
| 429 | |
| 430 | const bool playEnabled = (/*m_engine->dataLength() &&*/ |
| 431 | (QAudio::AudioOutput != m_engine->mode() || |
| 432 | (QAudio::ActiveState != m_engine->state() && |
| 433 | QAudio::IdleState != m_engine->state() && |
| 434 | QAudio::InterruptedState != m_engine->state()))); |
| 435 | m_playButton->setEnabled(playEnabled); |
| 436 | } |
| 437 | |
| 438 | void MainWidget::reset() |
| 439 | { |
| 440 | #ifndef DISABLE_WAVEFORM |
| 441 | m_waveform->reset(); |
| 442 | #endif |
| 443 | m_engine->reset(); |
| 444 | m_levelMeter->reset(); |
| 445 | m_spectrograph->reset(); |
| 446 | m_progressBar->reset(); |
| 447 | } |
| 448 | |
| 449 | void MainWidget::setMode(Mode mode) |
| 450 | { |
| 451 | m_mode = mode; |
| 452 | updateModeMenu(); |
| 453 | } |
| 454 | |
| 455 | void MainWidget::updateModeMenu() |
| 456 | { |
| 457 | m_loadFileAction->setChecked(LoadFileMode == m_mode); |
| 458 | m_generateToneAction->setChecked(GenerateToneMode == m_mode); |
| 459 | m_recordAction->setChecked(RecordMode == m_mode); |
| 460 | } |
| 461 | |