From 450886432055b6aab4e00879f7b46e235f064745 Mon Sep 17 00:00:00 2001 From: Klaus Basan Date: Mon, 10 Feb 2014 23:23:16 +0100 Subject: [PATCH] refs #129, added methods to play notification sounds Further testing revealed shortcomings in sound generation, which were fixed: * Sounds can be played in background (own thread) * Tones can be pushed and pulled http://qt-project.org/doc/qt-5.0/qtmultimedia/audiooverview.html#push-and-pull --- src/blacksound/soundgenerator.cpp | 141 ++++++++++++++++++++++++++---- src/blacksound/soundgenerator.h | 89 ++++++++++++++----- 2 files changed, 191 insertions(+), 39 deletions(-) diff --git a/src/blacksound/soundgenerator.cpp b/src/blacksound/soundgenerator.cpp index 853e97c61..675f7ef08 100644 --- a/src/blacksound/soundgenerator.cpp +++ b/src/blacksound/soundgenerator.cpp @@ -2,8 +2,12 @@ #include #include #include +#include #include -#include +#include +#include +#include +#include using namespace BlackMisc::Aviation; using namespace BlackMisc::PhysicalQuantities; @@ -14,7 +18,8 @@ namespace BlackSound CSoundGenerator::CSoundGenerator(const QAudioDeviceInfo &device, const QAudioFormat &format, const QList &tones, PlayMode mode, QObject *parent) : QIODevice(parent), m_tones(tones), m_position(0), m_playMode(mode), m_endReached(false), m_oneCycleDurationMs(calculateDurationMs(tones)), - m_device(device), m_audioFormat(format), m_audioOutput(new QAudioOutput(format)) + m_device(device), m_audioFormat(format), m_audioOutput(new QAudioOutput(format)), + m_pushTimer(nullptr), m_pushModeIODevice(nullptr), m_ownThread(nullptr) { Q_ASSERT(tones.size() > 0); } @@ -23,7 +28,8 @@ namespace BlackSound : QIODevice(parent), m_tones(tones), m_position(0), m_playMode(mode), m_endReached(false), m_oneCycleDurationMs(calculateDurationMs(tones)), m_device(QAudioDeviceInfo::defaultOutputDevice()), m_audioFormat(CSoundGenerator::defaultAudioFormat()), - m_audioOutput(new QAudioOutput(CSoundGenerator::defaultAudioFormat())) + m_audioOutput(new QAudioOutput(CSoundGenerator::defaultAudioFormat())), + m_pushTimer(nullptr), m_pushModeIODevice(nullptr), m_ownThread(nullptr) { Q_ASSERT(tones.size() > 0); } @@ -31,33 +37,97 @@ namespace BlackSound CSoundGenerator::~CSoundGenerator() { this->stop(true); + if (this->m_ownThread) this->m_ownThread->deleteLater(); } - void CSoundGenerator::start(int volume) + void CSoundGenerator::start(int volume, bool pull) { - if (volume < 1) return; if (this->m_buffer.isEmpty()) this->generateData(); this->open(QIODevice::ReadOnly); this->m_audioOutput->setVolume(qreal(0.01 * volume)); - this->m_audioOutput->start(this); // pull + + if (pull) + { + // For an output device, the QAudioOutput class will pull data from the QIODevice + // (using QIODevice::read()) when more audio data is required. + this->m_audioOutput->start(this); // pull + } + else + { + // In push mode, the audio device provides a QIODevice instance that can be + // written or read to as needed. Typically this results in simpler code but more buffering, which may affect latency. + if (!this->m_pushTimer) + { + this->m_pushTimer = new QTimer(this); + bool connect = this->connect(this->m_pushTimer, &QTimer::timeout, this, &CSoundGenerator::pushTimerExpired); + Q_ASSERT(connect); + this->m_pushTimer->start(20); + } + this->m_pushModeIODevice = this->m_audioOutput->start(); // push, IO device not owned + } + } + + void CSoundGenerator::startInOwnThread(int volume) + { + this->m_ownThread = new QThread(); // deleted by signals, hence no parent + this->moveToThread(this->m_ownThread); + connect(this, &CSoundGenerator::startThread, this, &CSoundGenerator::start); + connect(this, &CSoundGenerator::stopping, this->m_ownThread, &QThread::quit); + + // in auto delete mode force deleteLater when thread is finished + if (this->m_playMode == SingleWithAutomaticDeletion) + connect(this->m_ownThread, &QThread::finished, this, &CSoundGenerator::deleteLater); + + // start thread and begin processing by calling start via signal startThread + this->m_ownThread->start(); + emit startThread(volume, false); // this signal will trigger start in own thread } void CSoundGenerator::stop(bool destructor) { // this->m_audioOutput->setVolume(0); // Bug or feature, killing the applicaions volume? - this->m_audioOutput->stop(); if (this->isOpen()) { // 1. isOpen avoids redundant signals // 2. OK in destructor, see http://stackoverflow.com/a/14024955/356726 - emit this->stopped(); this->close(); // close IO Device + this->m_audioOutput->stop(); + if (this->m_pushTimer) this->m_pushTimer->stop(); + emit this->stopped(); } this->m_position = 0; if (destructor) return; // trigger own termination - if (this->m_playMode == SingleWithAutomaticDeletion) this->deleteLater(); + if (this->m_playMode == SingleWithAutomaticDeletion) + { + emit this->stopping(); + if (!this->m_ownThread) this->deleteLater(); // with own thread, thread signal will call deleteLater + } + } + + void CSoundGenerator::pushTimerExpired() + { + if (this->m_pushModeIODevice && !this->m_endReached && this->m_audioOutput->state() != QAudio::StoppedState) + { + int chunks = this->m_audioOutput->bytesFree() / this->m_audioOutput->periodSize(); + while (chunks) + { + // periodSize-> Returns the period size in bytes. + const qint64 len = this->read(m_buffer.data(), this->m_audioOutput->periodSize()); + if (len) + this->m_pushModeIODevice->write(m_buffer.data(), len); + if (len != this->m_audioOutput->periodSize()) + break; + --chunks; + } + } + else + { + if (this->m_pushTimer) this->m_pushTimer->stop(); + this->m_pushTimer->disconnect(this); + if (this->m_playMode == SingleWithAutomaticDeletion) this->stop(); + } } void CSoundGenerator::generateData() @@ -91,15 +161,15 @@ namespace BlackSound double amplitude = 0; // silence if (t.m_frequencyHz > 10) { + // the combination of two frequencies actually would have 2*amplitude, + // but I have to normalize with amplitude -1 -> +1 + amplitude = t.m_secondaryFrequencyHz == 0 ? qSin(2 * M_PI * t.m_frequencyHz * pseudoTime) : qSin(M_PI * (t.m_frequencyHz + t.m_secondaryFrequencyHz) * pseudoTime) * qCos(M_PI * (t.m_frequencyHz - t.m_secondaryFrequencyHz) * pseudoTime); } - // the combination of two frequencies actually would have 2*amplitude, - // but I have to normalize with amplitude -1 -> +1 - for (int i = 0; i < this->m_audioFormat.channelCount(); ++i) { if (this->m_audioFormat.sampleSize() == 8 && this->m_audioFormat.sampleType() == QAudioFormat::UnSignedInt) @@ -178,7 +248,6 @@ namespace BlackSound { Q_UNUSED(data); Q_UNUSED(len); - return 0; } @@ -241,12 +310,24 @@ namespace BlackSound if (volume < 1) return generator; if (generator->singleCyleDurationMs() < 10) return generator; // unable to hear - // top and clean uo when done + // play, and maybe clean up when done generator->start(volume); return generator; } - CSoundGenerator *CSoundGenerator::playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, QAudioDeviceInfo device) + CSoundGenerator *CSoundGenerator::playSignalInBackground(qint32 volume, const QList &tones, QAudioDeviceInfo device) + { + CSoundGenerator *generator = new CSoundGenerator(device, CSoundGenerator::defaultAudioFormat(), tones, CSoundGenerator::SingleWithAutomaticDeletion); + if (tones.isEmpty()) return generator; // that was easy + if (volume < 1) return generator; + if (generator->singleCyleDurationMs() < 10) return generator; // unable to hear + + // play, and maybe clean up when done + generator->startInOwnThread(volume); + return generator; + } + + void CSoundGenerator::playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, QAudioDeviceInfo device) { QList tones; if (selcal.isValid()) @@ -259,12 +340,36 @@ namespace BlackSound Tone t3(frequencies.at(2), frequencies.at(3), oneSec); tones << t1 << t2 << t3; } - return CSoundGenerator::playSignal(volume, tones, device); + CSoundGenerator::playSignalInBackground(volume, tones, device); } - CSoundGenerator *CSoundGenerator::playSelcal(qint32 volume, const CSelcal &selcal, const CAudioDevice &audioDevice) + void CSoundGenerator::playSelcal(qint32 volume, const CSelcal &selcal, const CAudioDevice &audioDevice) { - return CSoundGenerator::playSelcal(volume, selcal, CSoundGenerator::findClosestOutputDevice(audioDevice)); + CSoundGenerator::playSelcal(volume, selcal, CSoundGenerator::findClosestOutputDevice(audioDevice)); + } + + void CSoundGenerator::playNotificationSound(qint32 volume, CSoundGenerator::Notification notification) + { + static QMediaPlayer *mediaPlayer = new QMediaPlayer(); + if (mediaPlayer->state() == QMediaPlayer::PlayingState) return; + QMediaPlaylist *playlist = mediaPlayer->playlist(); + if (!playlist || playlist->isEmpty()) + { + // order here is crucial, needs to be the same as in CSoundGenerator::Notification + if (!playlist) playlist = new QMediaPlaylist(mediaPlayer); + bool success = true; + success = playlist->addMedia(QUrl::fromLocalFile(QCoreApplication::applicationDirPath().append("/sounds/error.wav"))) && success; + success = playlist->addMedia(QUrl::fromLocalFile(QCoreApplication::applicationDirPath().append("/sounds/login.wav"))) && success; + success = playlist->addMedia(QUrl::fromLocalFile(QCoreApplication::applicationDirPath().append("/sounds/logoff.wav"))) && success; + success = playlist->addMedia(QUrl::fromLocalFile(QCoreApplication::applicationDirPath().append("/sounds/privatemessage.wav"))) && success; + Q_ASSERT(success); + playlist->setPlaybackMode(QMediaPlaylist::CurrentItemOnce); + mediaPlayer->setPlaylist(playlist); + } + int index = static_cast(notification); + playlist->setCurrentIndex(index); + mediaPlayer->setVolume(volume); // 0-100 + mediaPlayer->play(); } } // namespace diff --git a/src/blacksound/soundgenerator.h b/src/blacksound/soundgenerator.h index 753aa76c8..c6efa84f3 100644 --- a/src/blacksound/soundgenerator.h +++ b/src/blacksound/soundgenerator.h @@ -11,6 +11,7 @@ #include "blackmisc/pqtime.h" #include +#include #include #include #include @@ -37,6 +38,17 @@ namespace BlackSound EndlessLoop }; + /*! + * \brief Play notification + */ + enum Notification + { + NotificationError = 0, + NotificationLogin, + NotificationLogoff, + NotificationTextMessage, + }; + /*! * \brief Tone to be played */ @@ -47,7 +59,7 @@ namespace BlackSound private: int m_frequencyHz; /*!< first tone's frequency, use 0 for silence */ int m_secondaryFrequencyHz; /*!< second tone's frequency, or 0 */ - qint64 m_durationMs; /*!< How long to play */ + qint64 m_durationMs; /*!< How long to play (duration) */ public: /*! @@ -87,14 +99,6 @@ namespace BlackSound */ CSoundGenerator(const QList &tones, PlayMode mode, QObject *parent = nullptr); - /*! - * \brief Constructor for dummy device - * \param device - * \param format - * \param parent - */ - CSoundGenerator(const QAudioDeviceInfo &device, const QAudioFormat &format, QObject *parent); - /*! * Destructor */ @@ -115,30 +119,30 @@ namespace BlackSound void stop(bool destructor = false); /*! - * \brief sDuration of one cycle + * \brief duration of one cycle */ qint64 singleCyleDurationMs() const { return calculateDurationMs(this->m_tones); } /*! * \copydoc QIODevice::readData() */ - virtual qint64 readData(char *data, qint64 maxlen); + virtual qint64 readData(char *data, qint64 maxlen) override; /*! * \copydoc QIODevice::writeData() * \remarks NOT(!) used here */ - virtual qint64 writeData(const char *data, qint64 len); + virtual qint64 writeData(const char *data, qint64 len) override; /*! * \copydoc QIODevice::bytesAvailable() */ - virtual qint64 bytesAvailable() const; + virtual qint64 bytesAvailable() const override; /*! * \copydoc QIODevice::seek() */ - virtual bool seek(qint64 pos) + virtual bool seek(qint64 pos) override { return this->m_endReached ? false : QIODevice::seek(pos); } @@ -146,7 +150,7 @@ namespace BlackSound /*! * \copydoc QIODevice::atEnd() */ - virtual bool atEnd() const + virtual bool atEnd() const override { return this->m_endReached ? true : QIODevice::atEnd(); } @@ -169,29 +173,43 @@ namespace BlackSound * \param volume 0-100 * \param tones list of tones * \param device device to be used - * \return + * \return generator used, important with SingleWithAutomaticDeletion automatically deleted */ static CSoundGenerator *playSignal(qint32 volume, const QList &tones, QAudioDeviceInfo device = QAudioDeviceInfo::defaultOutputDevice()); + /*! + * \brief Play signal of tones once + * \param volume 0-100 + * \param tones list of tones + * \param device device to be used + * \return generator used, important with SingleWithAutomaticDeletion automatically deleted + */ + static CSoundGenerator *playSignalInBackground(qint32 volume, const QList &tones, QAudioDeviceInfo device); + /*! * \brief Play SELCAL tone * \param volume 0-100 * \param selcal * \param device device to be used - * \return * \see BlackMisc::Aviation::CSelcal */ - static CSoundGenerator *playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, QAudioDeviceInfo device = QAudioDeviceInfo::defaultOutputDevice()); + static void playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, QAudioDeviceInfo device = QAudioDeviceInfo::defaultOutputDevice()); /*! * \brief Play SELCAL tone * \param volume 0-100 * \param selcal * \param audioDevice device to be used - * \return * \see BlackMisc::Aviation::CSelcal */ - static CSoundGenerator *playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, const BlackMisc::Voice::CAudioDevice &audioDevice); + static void playSelcal(qint32 volume, const BlackMisc::Aviation::CSelcal &selcal, const BlackMisc::Voice::CAudioDevice &audioDevice); + + /*! + * \brief Play notification + * \param volume 0-100 + * \param notification + */ + static void playNotificationSound(qint32 volume, Notification notification); /*! * \brief One cycle of tones takes t milliseconds @@ -212,8 +230,34 @@ namespace BlackSound /*! * \brief Play sound, open device * \param volume 0..100 + * \param pull, if false push mode */ - void start(int volume); + void start(int volume, bool pull = true); + + /*! + * \brief Play sound in own thread, open device + * \remarks always push mode + * \param volume 0..100 + */ + void startInOwnThread(int volume); + + signals: + /*! + * \brief Used to start in own thread + * \param volume 0..100 + * \param pull + * \remarks only works with push, but signature has to be identical with CSoundGenerator::start + */ + void startThread(int volume, bool pull); + + //! \brief Generator is stopping + void stopping(); + + private slots: + /*! + * \brief Push mode, timer expired + */ + void pushTimerExpired(); private: /*! @@ -231,6 +275,9 @@ namespace BlackSound QAudioDeviceInfo m_device; /*!< audio device */ QAudioFormat m_audioFormat; /*!< used format */ QScopedPointer m_audioOutput; + QTimer *m_pushTimer; /*!< Push mode timer */ + QIODevice *m_pushModeIODevice; /*!< IO device when used in push mode */ + QThread *m_ownThread; /*! * \brief Duration of these tones