diff --git a/Jamulus.pro b/Jamulus.pro index eb4d1382..a4f21112 100755 --- a/Jamulus.pro +++ b/Jamulus.pro @@ -113,7 +113,7 @@ win32 { !exists(/usr/include/jack/jack.h) { !exists(/usr/local/include/jack/jack.h) { - message(Warning: jack.h was not found at the usual place, maybe the jack dev packet is missing) + message("Warning: jack.h was not found at the usual place, maybe the jack dev packet is missing") } } @@ -123,9 +123,6 @@ win32 { LIBS += -ljack } - # enable to following line to support compilation on the Raspberry Pi - #LIBS += -lrt - # Linux is our source distribution, include sources from other OSs DISTFILES += mac/sound.h \ mac/sound.cpp \ @@ -170,7 +167,10 @@ HEADERS += src/audiomixerboard.h \ src/soundbase.h \ src/testbench.h \ src/util.h \ - src/analyzerconsole.h + src/analyzerconsole.h \ + src/recorder/jamrecorder.h \ + src/recorder/creaperproject.h \ + src/recorder/cwavestream.h HEADERS_OPUS = libs/opus/include/opus.h \ libs/opus/include/opus_multistream.h \ @@ -264,7 +264,10 @@ SOURCES += src/audiomixerboard.cpp \ src/socket.cpp \ src/soundbase.cpp \ src/util.cpp \ - src/analyzerconsole.cpp + src/analyzerconsole.cpp \ + src/recorder/jamrecorder.cpp \ + src/recorder/creaperproject.cpp \ + src/recorder/cwavestream.cpp SOURCES_OPUS = libs/opus/src/opus.c \ libs/opus/src/opus_decoder.c \ diff --git a/README b/README index 4f696650..c95eea89 100755 --- a/README +++ b/README @@ -62,3 +62,5 @@ source: - Some pixmaps are from the Open Clip Art Library (OCAL): http://openclipart.org + +- Server wave recording, coded by pljones diff --git a/src/chatdlg.h b/src/chatdlg.h index 11704933..87014f25 100755 --- a/src/chatdlg.h +++ b/src/chatdlg.h @@ -48,6 +48,9 @@ public slots: void OnLocalInputTextTextChanged ( const QString& strNewText ); void OnClearPressed(); + void keyPressEvent ( QKeyEvent *e ) // block escape key + { if ( e->key() != Qt::Key_Escape ) QDialog::keyPressEvent ( e ); } + signals: void NewLocalInputText ( QString strNewText ); }; diff --git a/src/clientdlg.h b/src/clientdlg.h index 4f636a07..65326f05 100755 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -79,8 +79,8 @@ public: const QString& strConnOnStartupAddress, const bool bNewShowComplRegConnList, const bool bShowAnalyzerConsole, - QWidget* parent = 0, - Qt::WindowFlags f = 0 ); + QWidget* parent = nullptr, + Qt::WindowFlags f = nullptr ); protected: void SetGUIDesign ( const EGUIDesign eNewDesign ); @@ -205,4 +205,9 @@ public slots: void OnAudioChannelsChanged() { UpdateRevSelection(); } void OnNumClientsChanged ( int iNewNumClients ); void OnNewClientLevelChanged() { MainMixerBoard->iNewClientFaderLevel = pClient->iNewClientFaderLevel; } + + void accept() { close(); } // introduced by pljones + + void keyPressEvent ( QKeyEvent *e ) // block escape key + { if ( e->key() != Qt::Key_Escape ) QDialog::keyPressEvent ( e ); } }; diff --git a/src/global.h b/src/global.h index e957a693..c0924836 100755 --- a/src/global.h +++ b/src/global.h @@ -50,6 +50,10 @@ LED bar: lbr #if !defined ( GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_ ) #define GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_ +#if _WIN32 +# define _CRT_SECURE_NO_WARNINGS +#endif + #include #include #include diff --git a/src/main.cpp b/src/main.cpp index b03eb296..91fea668 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,21 +31,16 @@ #include "serverdlg.h" #include "settings.h" #include "testbench.h" +#include "util.h" // Implementation ************************************************************** int main ( int argc, char** argv ) { -#ifdef _WIN32 - // no console on windows -> just write in string and dump it - QString strDummySink; - QTextStream tsConsole ( &strDummySink ); -#else - QTextStream tsConsole ( stdout ); -#endif - QString strArgument; - double rDbleArgument; + QTextStream& tsConsole = *( ( new ConsoleWriterFactory() )->get() ); + QString strArgument; + double rDbleArgument; // initialize all flags and string which might be changed by command line // arguments @@ -66,6 +61,7 @@ int main ( int argc, char** argv ) QString strServerName = ""; QString strLoggingFileName = ""; QString strHistoryFileName = ""; + QString strRecordingDirName = ""; QString strCentralServer = ""; QString strServerInfo = ""; QString strWelcomeMessage = ""; @@ -292,6 +288,21 @@ int main ( int argc, char** argv ) } + // Recording directory ------------------------------------------------- + if ( GetStringArgument ( tsConsole, + argc, + argv, + i, + "-R", + "--recordingdirectory", + strArgument ) ) + { + strRecordingDirName = strArgument; + tsConsole << "- recording directory name: " << strRecordingDirName << endl; + continue; + } + + // Central server ------------------------------------------------------ if ( GetStringArgument ( tsConsole, argc, @@ -402,7 +413,14 @@ int main ( int argc, char** argv ) // Application/GUI setup --------------------------------------------------- // Application object - QApplication app ( argc, argv, bUseGUI ); + if ( !bUseGUI && !strHistoryFileName.isEmpty() ) + { + tsConsole << "Qt5 requires a windowing system to paint a JPEG image; disabling history graph" << endl; + strHistoryFileName = ""; + } + QCoreApplication* pApp = bUseGUI + ? new QApplication ( argc, argv ) + : new QCoreApplication ( argc, argv ); #ifdef _WIN32 // set application priority class -> high priority @@ -412,7 +430,7 @@ int main ( int argc, char** argv ) // be located in the install directory of the software by the installer. // Here, we set the path to our application path. QDir ApplDir ( QApplication::applicationDirPath() ); - app.addLibraryPath ( QString ( ApplDir.absolutePath() ) ); + pApp->addLibraryPath ( QString ( ApplDir.absolutePath() ) ); #endif // init resources @@ -445,19 +463,19 @@ int main ( int argc, char** argv ) strConnOnStartupAddress, bShowComplRegConnList, bShowAnalyzerConsole, - 0, + nullptr, Qt::Window ); // show dialog ClientDlg.show(); - app.exec(); + pApp->exec(); } else { // only start application without using the GUI tsConsole << CAboutDlg::GetVersionAndNameStr ( false ) << endl; - app.exec(); + pApp->exec(); } } else @@ -473,10 +491,10 @@ int main ( int argc, char** argv ) strCentralServer, strServerInfo, strWelcomeMessage, + strRecordingDirName, bCentServPingServerInList, bDisconnectAllClients, eLicenceType ); - if ( bUseGUI ) { // special case for the GUI mode: as the default we want to use @@ -496,7 +514,7 @@ int main ( int argc, char** argv ) CServerDlg ServerDlg ( &Server, &Settings, bStartMinimized, - 0, + nullptr, Qt::Window ); // show dialog (if not the minimized flag is set) @@ -505,7 +523,7 @@ int main ( int argc, char** argv ) ServerDlg.show(); } - app.exec(); + pApp->exec(); } else { @@ -515,7 +533,7 @@ int main ( int argc, char** argv ) // only start application without using the GUI tsConsole << CAboutDlg::GetVersionAndNameStr ( false ) << endl; - app.exec(); + pApp->exec(); } } } @@ -525,11 +543,11 @@ int main ( int argc, char** argv ) // show generic error if ( bUseGUI ) { - QMessageBox::critical ( 0, + QMessageBox::critical ( nullptr, APP_NAME, generr.GetErrorText(), "Quit", - 0 ); + nullptr ); } else { @@ -571,6 +589,8 @@ QString UsageArguments ( char **argv ) " [server1 country as QLocale ID]; ...\n" " [server2 address]; ... (server only)\n" " -p, --port local port number (server only)\n" + " -R, --recording enables recording and sets directory to contain\n" + " recorded jams (server only)\n" " -s, --server start server\n" " -u, --numchannels maximum number of channels (server only)\n" " -w, --welcomemessage welcome message on connect (server only)\n" diff --git a/src/recorder/creaperproject.cpp b/src/recorder/creaperproject.cpp new file mode 100755 index 00000000..412f61e1 --- /dev/null +++ b/src/recorder/creaperproject.cpp @@ -0,0 +1,130 @@ +/******************************************************************************\ + * + * Author(s): + * pljones + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "creaperproject.h" + +/** + * @brief operator << Write details of the STrackItem to the QTextStream + * @param os the QTextStream + * @param trackItem the STrackItem + * @return the QTextStream + * + * Note: unused? + */ +QTextStream& operator<<(QTextStream& os, const recorder::STrackItem& trackItem) +{ + os << "_track( " + << "numAudioChannels(" << trackItem.numAudioChannels << ")" + << ", startFrame(" << trackItem.startFrame << ")" + << ", frameCount(" << trackItem.frameCount << ")" + << ", fileName(" << trackItem.fileName << ")" + << " );"; + return os; +} + +/******************************************************************************\ +* recorder methods * +\******************************************************************************/ +using namespace recorder; + +// Reaper Project writer ------------------------------------------------------- + +/** + * @brief CReaperItem::CReaperItem Construct a Reaper RPP "" for a given RIFF WAVE file + * @param name the item name + * @param trackItem the details of where the item is in the track, along with the RIFF WAVE filename + * @param iid the sequential item id + */ +CReaperItem::CReaperItem(const QString& name, const STrackItem& trackItem, const qint32& iid) +{ + QString wavName = trackItem.fileName; // assume RPP in same location... + + QTextStream sOut(&out); + + sOut << " " << endl; + + sOut << " >"; + + sOut.flush(); +} + +/** + * @brief CReaperTrack::CReaperTrack Construct a Reaper RPP "" for a given list of track items + * @param name the track name + * @param iid the sequential track id + * @param items the list of items in the track + */ +CReaperTrack::CReaperTrack(QString name, qint32& iid, QList items) +{ + QTextStream sOut(&out); + + sOut << " "; + + sOut.flush(); +} + +/** + * @brief CReaperProject::CReaperProject Construct a Reaper RPP "" for a given list of tracks + * @param tracks the list of tracks + */ +CReaperProject::CReaperProject(QMap> tracks) +{ + QTextStream sOut(&out); + + sOut << ""; + + sOut.flush(); +} diff --git a/src/recorder/creaperproject.h b/src/recorder/creaperproject.h new file mode 100755 index 00000000..78a6f05e --- /dev/null +++ b/src/recorder/creaperproject.h @@ -0,0 +1,93 @@ +/******************************************************************************\ + * + * Author(s): + * pljones + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#ifndef CREAPERPROJECT_H +#define CREAPERPROJECT_H + +#include +#include +#include +#include "util.h" + +#include "cwavestream.h" + +namespace recorder { + +struct STrackItem +{ + STrackItem(int numAudioChannels, qint64 startFrame, qint64 frameCount, QString fileName) : + numAudioChannels(numAudioChannels), + startFrame(startFrame), + frameCount(frameCount), + fileName(fileName) + { + } + int numAudioChannels; + qint64 startFrame; + qint64 frameCount; + QString fileName; +}; + +class CReaperItem : public QObject +{ + Q_OBJECT + +public: + CReaperItem(const QString& name, const STrackItem& trackItem, const qint32& iid); + QString toString() { return out; } + +private: + const QUuid iguid = QUuid::createUuid(); + const QUuid guid = QUuid::createUuid(); + QString out; + + inline QString secondsAt48K(const qint64 frames) { return QString::number(static_cast(frames * SYSTEM_FRAME_SIZE_SAMPLES) / 48000, 'f', 14); } +}; + +class CReaperTrack : public QObject +{ + Q_OBJECT + +public: + CReaperTrack(QString name, qint32 &iid, QList items); + QString toString() { return out; } + +private: + QUuid trackId = QUuid::createUuid(); + QString out; +}; + +class CReaperProject : public QObject +{ + Q_OBJECT + +public: + CReaperProject(QMap > tracks); + QString toString() { return out; } + +private: + QString out; +}; + +} +#endif // CREAPERPROJECT_H diff --git a/src/recorder/cwavestream.cpp b/src/recorder/cwavestream.cpp new file mode 100755 index 00000000..7f6e7437 --- /dev/null +++ b/src/recorder/cwavestream.cpp @@ -0,0 +1,144 @@ +/******************************************************************************\ + * + * Author(s): + * pljones + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "cwavestream.h" + +/******************************************************************************\ +* Overrides in global namespace * +\******************************************************************************/ + +/** + * @brief operator << Emit a hdr_riff object to a QDataStream + * @param os a QDataStream + * @param obj a hdr_riff object + * @return the QDataStream passed + */ +recorder::CWaveStream& operator<<(recorder::CWaveStream& os, const recorder::HdrRiff& obj) +{ + (QDataStream&)os << obj.chunkId << obj.chunkSize << obj.format; + return os; +} + +/** + * @brief operator << Emit a fmtSubChunk object to a QDataStream + * @param os a QDataStream + * @param obj a fmtSubChunk object + * @return the QDataStream passed + */ +recorder::CWaveStream& operator<<(recorder::CWaveStream& os, const recorder::FmtSubChunk& obj) +{ + (QDataStream&)os << obj.chunkId + << obj.chunkSize + << obj.audioFormat + << obj.numChannels + << obj.sampleRate + << obj.byteRate + << obj.blockAlign + << obj.bitsPerSample + ; + return os; +} + +/** + * @brief operator << Emit a dataSubChunkHdr object to a QDataStream + * @param os a QDataStream + * @param obj a dataSubChunkHdr object + * @return the QDataStream passed + */ +recorder::CWaveStream& operator<<(recorder::CWaveStream& os, const recorder::DataSubChunkHdr& obj) +{ + (QDataStream&)os << obj.chunkId << obj.chunkSize; + return os; +} + +/******************************************************************************\ +* Implementations of recorder.CWaveStream methods * +\******************************************************************************/ + +using namespace recorder; + +CWaveStream::CWaveStream(const uint16_t numChannels) : + QDataStream(), + numChannels (numChannels), + initialPos (device()->pos()), + initialByteOrder (byteOrder()) +{ + waveStreamHeaders(); +} +CWaveStream::CWaveStream(QIODevice *iod, const uint16_t numChannels) : + QDataStream(iod), + numChannels (numChannels), + initialPos (device()->pos()), + initialByteOrder (byteOrder()) +{ + waveStreamHeaders(); +} +CWaveStream::CWaveStream(QByteArray *iod, QIODevice::OpenMode flags, const uint16_t numChannels) : + QDataStream(iod, flags), + numChannels (numChannels), + initialPos (device()->pos()), + initialByteOrder (byteOrder()) +{ + waveStreamHeaders(); +} +CWaveStream::CWaveStream(const QByteArray &ba, const uint16_t numChannels) : + QDataStream(ba), + numChannels (numChannels), + initialPos (device()->pos()), + initialByteOrder (byteOrder()) +{ + waveStreamHeaders(); +} + +void CWaveStream::waveStreamHeaders() +{ + static const HdrRiff scHdrRiff; + const FmtSubChunk cFmtSubChunk (numChannels); + static const DataSubChunkHdr scDataSubChunkHdr; + + setByteOrder(LittleEndian); + *this << scHdrRiff << cFmtSubChunk << scDataSubChunkHdr; +} + +CWaveStream::~CWaveStream() +{ + static const uint32_t hdrRiffChunkSizeOffset = sizeof(uint32_t); + static const uint32_t dataSubChunkHdrChunkSizeOffset = sizeof(HdrRiff) + sizeof(FmtSubChunk) + sizeof (uint32_t) + sizeof (uint32_t); + + const int64_t currentPos = this->device()->pos(); + const uint32_t fileLength = static_cast(currentPos - initialPos); + + QDataStream& out = static_cast(*this); + + // Overwrite hdr_riff.chunkSize + this->device()->seek(initialPos + hdrRiffChunkSizeOffset); + out << static_cast(fileLength - (hdrRiffChunkSizeOffset + sizeof (uint32_t))); + + // Overwrite dataSubChunkHdr.chunkSize + this->device()->seek(initialPos + dataSubChunkHdrChunkSizeOffset); + out << static_cast(fileLength - (dataSubChunkHdrChunkSizeOffset + sizeof (uint32_t))); + + // and then restore the position and byte order + this->device()->seek(currentPos); + setByteOrder(initialByteOrder); +} diff --git a/src/recorder/cwavestream.h b/src/recorder/cwavestream.h new file mode 100755 index 00000000..7538e173 --- /dev/null +++ b/src/recorder/cwavestream.h @@ -0,0 +1,92 @@ +/******************************************************************************\ + * + * Author(s): + * pljones + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#ifndef CWAVESTREAM_H +#define CWAVESTREAM_H + +#include + +namespace recorder { + +class HdrRiff +{ +public: + HdrRiff() {} + + static const uint32_t chunkId = 0x46464952; // RIFF + static const uint32_t chunkSize = 0xffffffff; // (will be overwritten) Size of file in bytes - 8 = size of data + 36 + static const uint32_t format = 0x45564157; // WAVE +}; + +class FmtSubChunk +{ +public: + FmtSubChunk(const uint16_t _numChannels) : + numChannels (_numChannels) + , byteRate (sampleRate * numChannels * bitsPerSample/8) + , blockAlign (numChannels * bitsPerSample/8) + { + } + static const uint32_t chunkId = 0x20746d66; // "fmt " + static const uint32_t chunkSize = 16; // bytes in fmtSubChunk after chunkSize + static const uint16_t audioFormat = 1; // PCM + const uint16_t numChannels; // 1 for mono, 2 for joy... uh, stereo + static const uint32_t sampleRate = 48000; // because it's Jamulus + const uint32_t byteRate; // sampleRate * numChannels * bitsPerSample/8 + const uint16_t blockAlign; // numChannels * bitsPerSample/8 + static const uint16_t bitsPerSample = 16; +}; + +class DataSubChunkHdr +{ +public: + DataSubChunkHdr() {} + + static const uint32_t chunkId = 0x61746164; // "data" + static const uint32_t chunkSize = 0xffffffff; // (will be overwritten) Size of data +}; + +class CWaveStream : public QDataStream +{ +public: + CWaveStream(const uint16_t numChannels); + explicit CWaveStream(QIODevice *iod, const uint16_t numChannels); + CWaveStream(QByteArray *iod, QIODevice::OpenMode flags, const uint16_t numChannels); + CWaveStream(const QByteArray &ba, const uint16_t numChannels); + ~CWaveStream(); + +private: + void waveStreamHeaders(); + + const uint16_t numChannels; + const int64_t initialPos; + const ByteOrder initialByteOrder; +}; + +} + +recorder::CWaveStream& operator<<(recorder::CWaveStream& out, recorder::HdrRiff& hdrRiff); +recorder::CWaveStream& operator<<(recorder::CWaveStream& out, recorder::FmtSubChunk& fmtSubChunk); +recorder::CWaveStream& operator<<(recorder::CWaveStream& out, recorder::DataSubChunkHdr& dataSubChunkHdr); + +#endif // CWAVESTREAM_H diff --git a/src/recorder/jamrecorder.cpp b/src/recorder/jamrecorder.cpp new file mode 100755 index 00000000..503e147a --- /dev/null +++ b/src/recorder/jamrecorder.cpp @@ -0,0 +1,438 @@ +/******************************************************************************\ + * + * Author(s): + * pljones + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "jamrecorder.h" + +using namespace recorder; + +/* ******************************************************************************************************** + * CJamClient + * ********************************************************************************************************/ + +/** + * @brief CJamClient::CJamClient + * @param frame Start frame of the client within the session + * @param numChannels 1 for mono, 2 for stereo + * @param name The client's current name + * @param address IP and Port + * @param recordBaseDir Session recording directory + * + * Creates a file for the raw PCM data and sets up a QDataStream to which to write received frames. + * The data is stored Little Endian. + */ +CJamClient::CJamClient(const qint64 frame, const int _numChannels, const QString name, const CHostAddress address, const QDir recordBaseDir) : + startFrame (frame), + numChannels (static_cast(_numChannels)), + name (name), + address (address) +{ + // At this point we may not have much of a name + QString fileName = ClientName() + "-" + QString::number(frame) + "-" + QString::number(_numChannels); + QString affix = ""; + while (recordBaseDir.exists(fileName + affix + ".wav")) + { + affix = affix.length() == 0 ? "_1" : "_" + QString::number(affix.remove(0, 1).toInt() + 1); + } + fileName = fileName + affix + ".wav"; + + wavFile = new QFile(recordBaseDir.absoluteFilePath(fileName)); + if (!wavFile->open(QFile::OpenMode(QIODevice::OpenModeFlag::ReadWrite))) // need to allow rewriting headers + { + throw new std::runtime_error( ("Could not write to WAV file " + wavFile->fileName()).toStdString() ); + } + out = new CWaveStream(wavFile, numChannels); + + filename = wavFile->fileName(); +} + +/** + * @brief CJamClient::Frame Handle a frame of PCM data from a client connected to the server + * @param _name The client's current name + * @param pcm The PCM data + */ +void CJamClient::Frame(const QString _name, const CVector& pcm) +{ + name = _name; + + for(int i = 0; i < numChannels * SYSTEM_FRAME_SIZE_SAMPLES; i++) + { + *out << pcm[i]; + } + + frameCount++; +} + +/** + * @brief CJamClient::Disconnect Clean up after a disconnected client + */ +void CJamClient::Disconnect() +{ + delete out; + out = nullptr; + + wavFile->close(); + + delete wavFile; + wavFile = nullptr; +} + +/* ******************************************************************************************************** + * CJamSession + * ********************************************************************************************************/ + +/** + * @brief CJamSession::CJamSession Construct a new jam recording session + * @param recordBaseDir The recording base directory + * + * Each session is stored into its own subdirectory of the recording base directory. + */ +CJamSession::CJamSession(QDir recordBaseDir) : + sessionDir (QDir(recordBaseDir.absoluteFilePath("Jam-" + QDateTime().currentDateTimeUtc().toString("yyyyMMdd-HHmmsszzz")))), + currentFrame (0), + vecptrJamClients (MAX_NUM_CHANNELS), + jamClientConnections() +{ + const QFileInfo fi(sessionDir.absolutePath()); + + if (!fi.exists() && !QDir().mkpath(sessionDir.absolutePath())) + { + throw std::runtime_error( (sessionDir.absolutePath() + " does not exist but could not be created").toStdString() ); + } + if (!fi.isDir()) + { + throw std::runtime_error( (sessionDir.absolutePath() + " exists but is not a directory").toStdString() ); + } + if (!fi.isWritable()) + { + throw std::runtime_error( (sessionDir.absolutePath() + " is a directory but cannot be written to").toStdString() ); + } + + // Explicitly set all the pointers to "empty" + vecptrJamClients.fill(nullptr); + + currentFrame = 0; +} + +/** + * @brief CJamSession::DisconnectClient Capture details of the departing client's connection + * @param iChID the channel id of the client that disconnected + */ +void CJamSession::DisconnectClient(int iChID) +{ + vecptrJamClients[iChID]->Disconnect(); + + jamClientConnections.append(new CJamClientConnection(vecptrJamClients[iChID]->NumAudioChannels(), + vecptrJamClients[iChID]->StartFrame(), + vecptrJamClients[iChID]->FrameCount(), + vecptrJamClients[iChID]->ClientName(), + vecptrJamClients[iChID]->FileName())); + + delete vecptrJamClients[iChID]; + vecptrJamClients[iChID] = nullptr; +} + +/** + * @brief CJamSession::Frame Process a frame emited for a client by the server + * @param iChID the client channel id + * @param name the client name + * @param address the client IP and port number + * @param numAudioChannels the client number of audio channels + * @param data the frame data + * + * Manages changes that affect how the recording is stored - i.e. if the number of audio channels changes, we need a new file. + * Files are grouped by IP and port number, so if either of those change for a connection, we also start a new file. + * + * Also manages the overall current frame counter for the session. + */ +void CJamSession::Frame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector data) +{ + if (vecptrJamClients[iChID] == nullptr) + { + // then we have not seen this client this session + vecptrJamClients[iChID] = new CJamClient(currentFrame, numAudioChannels, name, address, sessionDir); + } + else if (numAudioChannels != vecptrJamClients[iChID]->NumAudioChannels() + || address.InetAddr != vecptrJamClients[iChID]->ClientAddress().InetAddr + || address.iPort != vecptrJamClients[iChID]->ClientAddress().iPort) + { + DisconnectClient(iChID); + if (numAudioChannels == 0) + { + vecptrJamClients[iChID] = nullptr; + } + else + { + vecptrJamClients[iChID] = new CJamClient(currentFrame, numAudioChannels, name, address, sessionDir); + } + } + + if (vecptrJamClients[iChID] == nullptr) + { + // Frame allegedly from iChID but unable to establish client details + return; + } + + vecptrJamClients[iChID]->Frame(name, data); + + // If _any_ connected client frame steps past currentFrame, increase currentFrame + if (vecptrJamClients[iChID]->StartFrame() + vecptrJamClients[iChID]->FrameCount() > currentFrame) + { + currentFrame++; + } +} + +/** + * @brief CJamSession::End Clean up any "hanging" clients when the server thinks they all left + */ +void CJamSession::End() +{ + for (int iChID = 0; iChID < vecptrJamClients.size(); iChID++) + { + if (vecptrJamClients[iChID] != nullptr) + { + DisconnectClient(iChID); + vecptrJamClients[iChID] = nullptr; + } + } +} + +/** + * @brief CJamSession::Tracks Retrieve a map of (latest) client name to connection items + * @return a map of (latest) client name to connection items + */ +QMap> CJamSession::Tracks() +{ + QMap> tracks; + + for (int i = 0; i < jamClientConnections.count(); i++ ) + { + STrackItem track ( + jamClientConnections[i]->Format(), + jamClientConnections[i]->StartFrame(), + jamClientConnections[i]->Length(), + jamClientConnections[i]->FileName() + ); + + if (!tracks.contains(jamClientConnections[i]->Name())) + { + tracks.insert(jamClientConnections[i]->Name(), { }); + } + + tracks[jamClientConnections[i]->Name()].append(track); + } + + return tracks; +} + +/** + * @brief CJamSession::TracksFromSessionDir Replica of CJamSession::Tracks but using the directory contents to construct the track item map + * @param sessionDirName the directory name to scan + * @return a map of (latest) client name to connection items + */ +QMap> CJamSession::TracksFromSessionDir(const QString& sessionDirName) +{ + QMap> tracks; + + const QDir sessionDir(sessionDirName); + foreach(auto entry, sessionDir.entryList({ "*.pcm" })) + { + + auto split = entry.split(".")[0].split("-"); + QString name = split[0]; + QString hostPort = split[1]; + QString frame = split[2]; + QString tail = split[3]; //numChannels may have _nnn + QString numChannels = tail.count("_") > 0 ? tail.split("_")[0] : tail; + + QString trackName = name + "-" + hostPort; + if (!tracks.contains(trackName)) + { + tracks.insert(trackName, { }); + } + + QFileInfo fiEntry(sessionDir.absoluteFilePath(entry)); + qint64 length = fiEntry.size() / numChannels.toInt() / SYSTEM_FRAME_SIZE_SAMPLES; + + STrackItem track ( + numChannels.toInt(), + frame.toLongLong(), + length, + sessionDir.absoluteFilePath(entry) + ); + + tracks[trackName].append(track); + } + + return tracks; +} + +/* ******************************************************************************************************** + * CJamRecorder + * ********************************************************************************************************/ + +/** + * @brief CJamRecorder::Init Create recording directory, if necessary, and connect signal handlers + * @param server Server object emiting signals + */ +void CJamRecorder::Init(const CServer* server) +{ + const QFileInfo fi(recordBaseDir.absolutePath()); + + if (!fi.exists() && !QDir().mkpath(recordBaseDir.absolutePath())) + { + throw std::runtime_error( (recordBaseDir.absolutePath() + " does not exist but could not be created").toStdString() ); + } + if (!fi.isDir()) + { + throw std::runtime_error( (recordBaseDir.absolutePath() + " exists but is not a directory").toStdString() ); + } + if (!fi.isWritable()) + { + throw std::runtime_error( (recordBaseDir.absolutePath() + " is a directory but cannot be written to").toStdString() ); + } + + QObject::connect((const QObject *)server, SIGNAL ( Stopped() ), + this, SLOT( OnEnd() ), + Qt::ConnectionType::QueuedConnection); + + QObject::connect((const QObject *)server, SIGNAL ( ClientDisconnected(int) ), + this, SLOT( OnDisconnected(int) ), + Qt::ConnectionType::QueuedConnection); + + qRegisterMetaType>(); + QObject::connect((const QObject *)server, SIGNAL ( AudioFrame(const int, const QString, const CHostAddress, const int, const CVector) ), + this, SLOT( OnFrame(const int, const QString, const CHostAddress, const int, const CVector) ), + Qt::ConnectionType::QueuedConnection); +} + +/** + * @brief CJamRecorder::OnStart Start up tasks when the first client connects + */ +void CJamRecorder::OnStart() { + // Ensure any previous cleaning up has been done. + OnEnd(); + + currentSession = new CJamSession(recordBaseDir); + isRecording = true; +} + +/** + * @brief CJamRecorder::OnEnd Finalise the recording and emit the Reaper RPP file + */ +void CJamRecorder::OnEnd() +{ + if (isRecording) + { + isRecording = false; + currentSession->End(); + + QString reaperProjectFileName = currentSession->SessionDir().filePath(currentSession->Name().append(".rpp")); + const QFileInfo fi(reaperProjectFileName); + + if (fi.exists()) + { + tsConsole << "CJamRecorder::OnEnd() - " << fi.absolutePath() << " exists and will not be overwritten." << endl; + } + else + { + QFile outf (reaperProjectFileName); + outf.open(QFile::WriteOnly); + QTextStream out(&outf); + out << CReaperProject(currentSession->Tracks()).toString() << endl; + tsConsole << "Session RPP: " << reaperProjectFileName << endl; + } + + delete currentSession; + currentSession = nullptr; + } +} + +/** + * @brief CJamRecorder::SessionDirToReaper Replica of CJamRecorder::OnEnd() but using the directory contents to construct the CReaperProject object + * @param strSessionDirName + */ +void CJamRecorder::SessionDirToReaper(QString& strSessionDirName) +{ + const QFileInfo fiSessionDir(QDir::cleanPath(strSessionDirName)); + if (!fiSessionDir.exists() || !fiSessionDir.isDir()) + { + throw std::runtime_error( (fiSessionDir.absoluteFilePath() + " does not exist or is not a directory. Aborting.").toStdString() ); + } + + const QDir dSessionDir(fiSessionDir.absoluteFilePath()); + const QString reaperProjectFileName = dSessionDir.absoluteFilePath(fiSessionDir.baseName().append(".rpp")); + const QFileInfo fiRPP(reaperProjectFileName); + if (fiRPP.exists()) + { + throw std::runtime_error( (fiRPP.absoluteFilePath() + " exists and will not be overwritten. Aborting.").toStdString() ); + } + + QFile outf (fiRPP.absoluteFilePath()); + if (!outf.open(QFile::WriteOnly)) { + throw std::runtime_error( (fiRPP.absoluteFilePath() + " could not be written. Aborting.").toStdString() ); + } + QTextStream out(&outf); + + out << CReaperProject(CJamSession::TracksFromSessionDir(fiSessionDir.absoluteFilePath())).toString() << endl; + + (*(new ConsoleWriterFactory())->get()) << "Session RPP: " << reaperProjectFileName << endl; +} + +/** + * @brief CJamRecorder::OnDisconnected Handle disconnection of a client + * @param iChID the client channel id + */ +void CJamRecorder::OnDisconnected(int iChID) +{ + if (!isRecording) + { + tsConsole << "CJamRecorder::OnDisconnected: channel " << iChID << " disconnected but not recording" << endl; + } + if (currentSession == nullptr) + { + tsConsole << "CJamRecorder::OnDisconnected: channel " << iChID << " disconnected but no currentSession" << endl; + return; + } + currentSession->DisconnectClient(iChID); +} + +/** + * @brief CJamRecorder::OnFrame Handle a frame emited for a client by the server + * @param iChID the client channel id + * @param name the client name + * @param address the client IP and port number + * @param numAudioChannels the client number of audio channels + * @param data the frame data + * + * Ensures recording has started. + */ +void CJamRecorder::OnFrame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector data) +{ + // Make sure we are ready + if (!isRecording) + { + OnStart(); + } + + currentSession->Frame(iChID, name, address, numAudioChannels, data); +} diff --git a/src/recorder/jamrecorder.h b/src/recorder/jamrecorder.h new file mode 100755 index 00000000..42443509 --- /dev/null +++ b/src/recorder/jamrecorder.h @@ -0,0 +1,181 @@ +/******************************************************************************\ + * + * Author(s): + * pljones + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#ifndef JAMRECORDER_H +#define JAMRECORDER_H + +#include +#include +#include + +#include "../util.h" +#include "../channel.h" + +#include "creaperproject.h" +#include "cwavestream.h" + +namespace recorder { + +class CJamClientConnection : public QObject +{ + Q_OBJECT + +public: + CJamClientConnection(const int _numAudioChannels, const qint64 _startFrame, const qint64 _length, const QString _name, const QString _fileName) : + numAudioChannels (_numAudioChannels), + startFrame (_startFrame), + length (_length), + name (_name), + fileName (_fileName) + { + } + + int Format() { return numAudioChannels; } + qint64 StartFrame() { return startFrame; } + qint64 Length() { return length; } + QString Name() { return name; } + QString FileName() { return fileName; } + +private: + const int numAudioChannels; + const qint64 startFrame; + const qint64 length; + const QString name; + const QString fileName; +}; + +class CJamClient : public QObject +{ + Q_OBJECT + +public: + CJamClient(const qint64 frame, const int numChannels, const QString name, const CHostAddress address, const QDir recordBaseDir); + + void Frame(const QString name, const CVector& pcm); + + void Disconnect(); + + qint64 StartFrame() { return startFrame; } + qint64 FrameCount() { return frameCount; } + uint16_t NumAudioChannels() { return numChannels; } + QString ClientName() { return name.leftJustified(4, '_', false).replace(QRegExp("[-.:/\\ ]"), "_") + .append("-") + .append(address.toString(CHostAddress::EStringMode::SM_IP_NO_LAST_BYTE_PORT).replace(QRegExp("[-.:/\\ ]"), "_")) + ; + } + CHostAddress ClientAddress() { return address; } + + QString FileName() { return filename; } + +private: + const qint64 startFrame; + const uint16_t numChannels; + QString name; + const CHostAddress address; + + QString filename; + QFile* wavFile; + QDataStream* out; + qint64 frameCount = 0; +}; + +class CJamSession : public QObject +{ + Q_OBJECT + +public: + + CJamSession(QDir recordBaseDir); + + void Frame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector data); + + void End(); + + QVector Clients() { return vecptrJamClients; } + + QMap> Tracks(); + + QString Name() { return sessionDir.dirName(); } + + const QDir SessionDir() { return sessionDir; } + + void DisconnectClient(int iChID); + + static QMap> TracksFromSessionDir(const QString& name); + +private: + CJamSession(); + + const QDir sessionDir; + + qint64 currentFrame; + QVector vecptrJamClients; + QList jamClientConnections; +}; + +class CJamRecorder : public QThread +{ + Q_OBJECT + +public: + CJamRecorder(const QString recordingDirName) : + recordBaseDir (recordingDirName), isRecording (false) {} + void Init(const CServer* server); + + static void SessionDirToReaper(QString& strSessionDirName); + +public slots: + /** + * @brief Raised when first client joins the server, triggering a new recording. + */ + void OnStart(); + + /** + * @brief Raised when last client leaves the server, ending the recording. + */ + void OnEnd(); + + /** + * @brief Raised when an existing client leaves the server. + * @param iChID channel number of client + */ + void OnDisconnected(int iChID); + + /** + * @brief Raised when a frame of data fis available to process + */ + void OnFrame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector data); + +private: + QDir recordBaseDir; + + bool isRecording; + CJamSession* currentSession; + QTextStream& tsConsole = *((new ConsoleWriterFactory())->get()); +}; + +} + +Q_DECLARE_METATYPE(int16_t) +Q_DECLARE_METATYPE(CVector) +#endif // JAMRECORDER_H diff --git a/src/server.cpp b/src/server.cpp index 9ed2c4e2..4437d242 100755 --- a/src/server.cpp +++ b/src/server.cpp @@ -206,11 +206,14 @@ CServer::CServer ( const int iNewMaxNumChan, const QString& strCentralServer, const QString& strServerInfo, const QString& strNewWelcomeMessage, + const QString& strRecordingDirName, const bool bNCentServPingServerInList, const bool bNDisconnectAllClients, const ELicenceType eNLicenceType ) : iMaxNumChannels ( iNewMaxNumChan ), Socket ( this, iPortNumber ), + JamRecorder ( strRecordingDirName ), + bEnableRecording ( !strRecordingDirName.isEmpty() ), bWriteStatusHTMLFile ( false ), ServerListManager ( iPortNumber, strCentralServer, @@ -357,6 +360,13 @@ CServer::CServer ( const int iNewMaxNumChan, QString().number( static_cast ( iPortNumber ) ) ); } + // Enable jam recording (if requested) + if ( bEnableRecording ) + { + JamRecorder.Init ( this ); + JamRecorder.start(); + } + // enable all channels (for the server all channel must be enabled the // entire life time of the software) for ( i = 0; i < iMaxNumChannels; i++ ) @@ -739,8 +749,14 @@ JitterMeas.Measure(); // if channel was just disconnected, set flag that connected // client list is sent to all other clients + // and emit the client disconnected signal if ( eGetStat == GS_CHAN_NOW_DISCONNECTED ) { + if ( bEnableRecording ) + { + emit ClientDisconnected ( iCurChanID ); // TODO do this outside the mutex lock? + } + bChannelIsNowDisconnected = true; } @@ -825,6 +841,16 @@ JitterMeas.Measure(); // get number of audio channels of current channel const int iCurNumAudChan = vecNumAudioChannels[i]; + // export the audio data for recording purpose + if ( bEnableRecording ) + { + emit AudioFrame ( iCurChanID, + vecChannels[iCurChanID].GetName(), + vecChannels[iCurChanID].GetAddress(), + iCurNumAudChan, + vecvecsData[i] ); + } + // generate a sparate mix for each channel // actual processing of audio data -> mix ProcessData ( vecvecsData, diff --git a/src/server.h b/src/server.h index 26e56499..76dfb215 100755 --- a/src/server.h +++ b/src/server.h @@ -40,6 +40,7 @@ #include "util.h" #include "serverlogging.h" #include "serverlist.h" +#include "recorder/jamrecorder.h" /* Definitions ****************************************************************/ @@ -127,6 +128,7 @@ public: const QString& strCentralServer, const QString& strServerInfo, const QString& strNewWelcomeMessage, + const QString& strRecordingDirName, const bool bNCentServPingServerInList, const bool bNDisconnectAllClients, const ELicenceType eNLicenceType ); @@ -247,6 +249,10 @@ protected: // logging CServerLogging Logging; + // recording thread + recorder::CJamRecorder JamRecorder; + bool bEnableRecording; + // HTML file server status bool bWriteStatusHTMLFile; QString strServerHTMLFileListName; @@ -268,6 +274,12 @@ protected: signals: void Started(); void Stopped(); + void ClientDisconnected ( const int iChID ); + void AudioFrame ( const int iChID, + const QString stChName, + const CHostAddress RecHostAddr, + const int iNumAudChan, + const CVector vecsData ); public slots: void OnTimer(); diff --git a/src/serverdlg.h b/src/serverdlg.h index 642037ea..1c8fdc01 100755 --- a/src/serverdlg.h +++ b/src/serverdlg.h @@ -97,4 +97,7 @@ public slots: void OnSysTrayMenuHide() { hide(); } void OnSysTrayMenuExit() { close(); } void OnSysTrayActivated ( QSystemTrayIcon::ActivationReason ActReason ); + + void keyPressEvent ( QKeyEvent *e ) // block escape key + { if ( e->key() != Qt::Key_Escape ) QDialog::keyPressEvent ( e ); } }; diff --git a/src/util.cpp b/src/util.cpp index 50160cde..5430bd8f 100755 --- a/src/util.cpp +++ b/src/util.cpp @@ -385,6 +385,7 @@ CAboutDlg::CAboutDlg ( QWidget* parent ) : QDialog ( parent ) "http://openclipart.org" "
  • Country flag icons from Mark James: " "http://www.famfamfam.com
  • " + "
  • Server wave recording, coded by pljones
  • " "" "
    "); @@ -1209,6 +1210,31 @@ QString CCountyFlagIcons::GetResourceReference ( const QLocale::Country eCountry } +// Console writer factory ------------------------------------------------------ +QTextStream* ConsoleWriterFactory::get() +{ + if ( ptsConsole == nullptr ) + { +#if _WIN32 + if ( !AttachConsole ( ATTACH_PARENT_PROCESS ) ) + { + // Not run from console, dump logging to nowhere + static QString conout; + ptsConsole = new QTextStream ( &conout ); + } + else + { + freopen ( "CONOUT$", "w", stdout ); + ptsConsole = new QTextStream ( stdout ); + } +#else + ptsConsole = new QTextStream ( stdout ); +#endif + } + return ptsConsole; +} + + /******************************************************************************\ * Global Functions Implementation * \******************************************************************************/ diff --git a/src/util.h b/src/util.h index 340a37f3..5eb2c1fa 100755 --- a/src/util.h +++ b/src/util.h @@ -482,6 +482,19 @@ public slots: }; +// Console writer factory ------------------------------------------------------ +// this class was written by pljones +class ConsoleWriterFactory +{ +public: + ConsoleWriterFactory() : ptsConsole ( nullptr ) { } + QTextStream* get(); + +private: + QTextStream* ptsConsole; +}; + + /******************************************************************************\ * Other Classes/Enums * \******************************************************************************/ diff --git a/windows/sound.cpp b/windows/sound.cpp index 4950b856..308ba367 100755 --- a/windows/sound.cpp +++ b/windows/sound.cpp @@ -25,7 +25,7 @@ * \******************************************************************************/ -#include "Sound.h" +#include "sound.h" /* Implementation *************************************************************/