diff --git a/ScStwLibraries/headers/ScStw.hpp b/ScStwLibraries/headers/ScStw.hpp new file mode 100644 index 0000000..6657fc7 --- /dev/null +++ b/ScStwLibraries/headers/ScStw.hpp @@ -0,0 +1,174 @@ +#ifndef SCSTW_HPP +#define SCSTW_HPP + +#include +#include + +/** + * @mainpage ScStw Libraries documentation + * + * @section intro_sec Introduction + * + * This library is meant for usage with the Speed climbing stopwatch project. + * It contains some helper classes to build a client application for the ScStw basestation with Qt. + * + * @section section Installation + * @code{.sh} + * cd yourRepo + * git submodule add https://git.itsblue.de/ScStw/shared-libraries/ + * git submodule update --init --recursive + * @endcode + * + * Add to the list of libraries for the Qt-Secret assembly. For an example you can create Main.Pro in which connect Qt-Secret and your project.pro files as subprojects. + * + * Main.pro: + * @code{.pro} + * TEMPLATE = subdirs + * CONFIG += ordered + * + * SUBDIRS += \ + * ScStwLibraries \ + * MyProject + * + * ScStwLibraries.file = shared-libraries/ScStwLibraries/ScStwLibraries.pro + * @endcode + * + * And in your MyProject.pro include the .pri file: + * @code{.pro} + * include($$PWD/../shared-libraries/ScStwLibraries/ScStwLibraries.pri) + * @endcode + */ + +/** + * @brief The ScStw class provides some shared functions and enums for use in the ScStw project. + */ +class ScStw : public QObject { + Q_OBJECT +public: + + /** + * @brief The RaceState enum contains all states a race can have + */ +<<<<<<< HEAD:ScStwLibraries/ScStw.hpp + enum RaceState { IDLE, STARTING, WAITING, RUNNING, STOPPED }; + Q_ENUM(RaceState) + + /** + * @brief The SignalKey enum contains all signal keys a client can subscribe to + * + * @see ScStw::signalKeyFromInt() + */ +======= + +>>>>>>> - some changes to file structure:ScStwLibraries/headers/ScStw.hpp + enum SignalKey { + InvalidSignal = -1, + RaceStateChanged = 9000, + TimersChanged = 9001, + ExtensionsChanged = 9002, + NextStartActionChanged = 9003 /*, ProfilesChanged*/ + }; + Q_ENUM(SignalKey) + +<<<<<<< HEAD:ScStwLibraries/ScStw.hpp + /** + * @brief The NextStartAction enum contains all action that occur before a race is started + */ + enum NextStartAction { AtYourMarks, Ready, Start, None }; + Q_ENUM(NextStartAction) + + /** + * @brief The BaseStationSetting enum contains all settings of the base station that can be changed by a client + * + * @see ScStw::baseStationSettingFromInt() + * @see ScStw::baseStationSettingToString() + * @see ScStw::baseStationSettingFromString() + * @see ScStw::baseStationSettings + */ +======= +>>>>>>> - some changes to file structure:ScStwLibraries/headers/ScStw.hpp + enum BaseStationSetting { + InvalidSetting = -1, + ReadySoundEnableSetting, + ReadySoundDelaySetting, + AtYourMarksSoundEnableSetting, + AtYourMarksSoundDelaySetting, + SoundVolumeSetting + }; + Q_ENUM(BaseStationSetting) + + /** + * @brief The ErrorCode enum contains all error codes that can occur when sending a command to the basestation + */ + enum ErrorCode { + Success = 200, + + Error = 900, + NotConnectedError = 910, + TimeoutError = 911, + SettingNotAccessibleError = 901 + }; + Q_ENUM(ErrorCode) + + /** + * @brief SOCKET_MESSAGE_START_KEY contains the key, a message is supposed to start with + */ + static const char* SOCKET_MESSAGE_START_KEY; + + /** + * @brief SOCKET_MESSAGE_END_KEY contains the key, a message is supposed to end with + */ + static const char* SOCKET_MESSAGE_END_KEY; + + /** + * @brief baseStationSettings contains a string with reference to all BaseStationSetting values + * + * @see ScStw::BaseStationSetting + * @see ScStw::baseStationSettingToString() + * @see ScStw::baseStationSettingFromString() + */ + static const QMap baseStationSettings; + + /** + * @brief Function to convert an int to a BaseStationSetting + * @param i the int to convert + * @return a BaseStationSetting + * + * @see ScStw::BaseStationSetting + */ + static BaseStationSetting baseStationSettingfromInt(int i); + + /** + * @brief Function to convert a QString to a BaseStationSetting + * @param s the string to convert + * @return a BaseStationSetting + * + * @see ScStw::BaseStationSetting + * @see ScStw::baseStationSettingToString() + */ + static BaseStationSetting baseStationSettingFromString(QString s); + + /** + * @brief Function to convert BaseStationSetting to a QString + * @param s the BaseStationSetting to convert + * @return a QString + * + * @see ScStw::BaseStationSetting + * @see ScStw::baseStationSettingFromString() + */ + static QString baseStationSettingToString(BaseStationSetting s); + + /** + * @brief Function to convert an int to a SignalKey + * @param i the int to convert + * @return a SignalKey + * + * @see ScStw::SignalKey + */ + static SignalKey signalKeyFromInt(int i); + +private: + ScStw() : QObject(nullptr) {}; +}; + +#endif // SCSTW_HPP diff --git a/ScStwLibraries/headers/scstwsoundplayer.h b/ScStwLibraries/headers/scstwsoundplayer.h new file mode 100644 index 0000000..8ba1f8f --- /dev/null +++ b/ScStwLibraries/headers/scstwsoundplayer.h @@ -0,0 +1,41 @@ +#ifndef SCSTWSTARTSOUNDPLAYER_H +#define SCSTWSTARTSOUNDPLAYER_H + +#include +#include +#include +#include +#include +#include +#include + +class ScStwSoundPlayer : public QObject +{ + Q_OBJECT +public: + explicit ScStwSoundPlayer(QObject *parent = nullptr); + +private: + QMap soundFiles; + QFile *falseStartSoundFile; + + QAudioOutput *audioOutput; + QEventLoop *waitLoop; + QTimer *waitTimer; + int currentlyPlayingAction; + +public slots: + bool play(int action, double volume, double *timeOfStop = nullptr); + bool cancel(double volume); + bool waitForSoundFinish(double *timeOfStop = nullptr); + //int interrupt(); + +private slots: + void handleStateChanged(QAudio::State newState); + +signals: + void playbackStarted(); + +}; + +#endif // SCSTWSTARTSOUNDPLAYER_H diff --git a/ScStwLibraries/sources/scstwsoundplayer.cpp b/ScStwLibraries/sources/scstwsoundplayer.cpp new file mode 100644 index 0000000..21ae82e --- /dev/null +++ b/ScStwLibraries/sources/scstwsoundplayer.cpp @@ -0,0 +1,159 @@ +#include "../headers/scstwsoundplayer.h" + +ScStwSoundPlayer::ScStwSoundPlayer(QObject *parent) : QObject(parent) +{ + this->waitLoop = new QEventLoop(this); + this->waitTimer = new QTimer(this); + + this->soundFiles.insert(0, new QFile(":/sound/AtYourMarksSound.wav", this)); + this->soundFiles.insert(1, new QFile(":/sound/ReadySound.wav", this)); + this->soundFiles.insert(2, new QFile(":/sound/StartsignalSound.wav", this)); + this->falseStartSoundFile = new QFile(":/sound/FalseStartSound.wav", this); + + // init sound format + QAudioFormat format; + format.setSampleRate(44100); + format.setChannelCount(2); + format.setSampleSize(16); + format.setCodec("audio/pcm"); + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + + // check if format is valid + QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); + if (!info.isFormatSupported(format)) { + qWarning() << "Raw audio format not supported by backend, cannot play audio."; + return; + } + + this->audioOutput = new QAudioOutput(format, this); + this->audioOutput->setCategory("ScStw start sound"); + + connect(this->audioOutput, SIGNAL(stateChanged(QAudio::State)), this, SLOT(handleStateChanged(QAudio::State))); + connect(this, &ScStwSoundPlayer::playbackStarted, this->waitLoop, &QEventLoop::quit); +} + +bool ScStwSoundPlayer::play(int action, double volume, double *timeOfStart) { + if(!this->soundFiles.contains(action)) + return false; + + // stop playback + this->audioOutput->stop(); + + // update currently playing action + this->currentlyPlayingAction = action; + + QFile *playbackFile = this->soundFiles[action]; + + // close soundfile + if(playbackFile->isOpen()) + playbackFile->close(); + + // open soundfile + if(!playbackFile->open(QFile::ReadOnly)) { + qWarning() << "[ERROR][SOUNDS] Could not open sound file " << action << " !!"; + return false; + } + + // update volume + this->audioOutput->setVolume(volume); + + // start + this->audioOutput->start(playbackFile); + + qDebug() << "playback stared, took " << this->audioOutput->elapsedUSecs(); + + // pass the time of start if requested + if(timeOfStart != nullptr) { + *timeOfStart = QDateTime::currentMSecsSinceEpoch() - (this->audioOutput->elapsedUSecs() / 1000); + } + + if(action < 2) + return this->waitForSoundFinish(); + + return true; +} + +bool ScStwSoundPlayer::cancel(double volume) { + if(!QList{QAudio::ActiveState}.contains(this->audioOutput->state()) ) + return false; + + // stop playback + this->audioOutput->stop(); + + if(this->currentlyPlayingAction != 2) + return true; + + // close soundfile + if(this->falseStartSoundFile->isOpen()) + this->falseStartSoundFile->close(); + + // open sound file + if(!this->falseStartSoundFile->open(QFile::ReadOnly)) { + qWarning() << "[ERROR][SOUNDS] Could not open false start sound file!!"; + return false; + } + + // update volume + this->audioOutput->setVolume(volume); + + // start + this->audioOutput->start(this->falseStartSoundFile); + + return true; +} + +bool ScStwSoundPlayer::waitForSoundFinish(double *timeOfStop) { + if(this->audioOutput->state() != QAudio::ActiveState) + return false; + + // wait until the audio output reports the sound is over + waitLoop->exec(); + + qDebug() << "waitLoop exited!! elapsed: " << this->audioOutput->elapsedUSecs() << " processed: " << this->audioOutput->processedUSecs(); + // wait until the sound is actually over + // the timeOffset is the buffer time before the audio started! + int timeOffset = ((this->audioOutput->processedUSecs() - this->audioOutput->elapsedUSecs()) / 1000); + + if(timeOffset > 0) { + QTimer timer; + timer.singleShot(timeOffset, this->waitLoop, &QEventLoop::quit); + this->waitLoop->exec(); + } + + // calculate the point in time where the sound playback actually ended + if(timeOfStop != nullptr) { + int latency = (this->audioOutput->processedUSecs() - this->audioOutput->elapsedUSecs()) / 1000; + *timeOfStop = QDateTime::currentMSecsSinceEpoch() - latency; + } + + qDebug() << "finished now"; + + return true; +} + +void ScStwSoundPlayer::handleStateChanged(QAudio::State newState) +{ + switch (newState) { + case QAudio::IdleState: + // Finished playing (no more data) + qDebug() << "sound reported over!! elapsed: " << this->audioOutput->elapsedUSecs() << " processed: " << this->audioOutput->processedUSecs(); + waitLoop->exit(); + break; + + case QAudio::StoppedState: + // Stopped for other reasons + if (this->audioOutput->error() != QAudio::NoError) { + // Error handling + } + break; + + case QAudio::ActiveState: + emit this->playbackStarted(); + break; + + default: + // ... other cases as appropriate + break; + } +}