Merge pull request #3 from corrados/integrate_recorder

Integrate recorder
This commit is contained in:
corrados 2019-04-12 23:00:41 +02:00 committed by GitHub
commit 9ac72ec357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1224 additions and 29 deletions

View file

@ -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 \

2
README
View file

@ -62,3 +62,5 @@ source:
- Some pixmaps are from the Open Clip Art Library (OCAL):
http://openclipart.org
- Server wave recording, coded by pljones

View file

@ -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 );
};

View file

@ -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 ); }
};

View file

@ -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 <QString>
#include <QEvent>
#include <QDebug>

View file

@ -31,19 +31,14 @@
#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
QTextStream& tsConsole = *( ( new ConsoleWriterFactory() )->get() );
QString strArgument;
double rDbleArgument;
@ -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"

130
src/recorder/creaperproject.cpp Executable file
View file

@ -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 "<ITEM>" 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 << " <ITEM " << endl;
sOut << " FADEIN 0 0 0 0 0 0" << endl;
sOut << " FADEOUT 0 0 0 0 0 0" << endl;
sOut << " POSITION " << secondsAt48K(trackItem.startFrame) << endl;
sOut << " LENGTH " << secondsAt48K(trackItem.frameCount) << endl;
sOut << " IGUID " << iguid.toString() << endl;
sOut << " IID " << iid << endl;
sOut << " NAME " << name << endl;
sOut << " GUID " << guid.toString() << endl;
sOut << " <SOURCE WAVE" << endl;
sOut << " FILE " << '"' << wavName << '"' << endl;
sOut << " >" << endl;
sOut << " >";
sOut.flush();
}
/**
* @brief CReaperTrack::CReaperTrack Construct a Reaper RPP "<TRACK>" 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<STrackItem> items)
{
QTextStream sOut(&out);
sOut << " <TRACK " << trackId.toString() << endl;
sOut << " NAME " << name << endl;
sOut << " TRACKID " << trackId.toString() << endl;
int ino = 1;
foreach (auto item, items) {
sOut << CReaperItem(name + " (" + QString::number(ino) + ")", item, iid).toString() << endl;
ino++;
iid++;
}
sOut << " >";
sOut.flush();
}
/**
* @brief CReaperProject::CReaperProject Construct a Reaper RPP "<REAPER_PROJECT>" for a given list of tracks
* @param tracks the list of tracks
*/
CReaperProject::CReaperProject(QMap<QString, QList<STrackItem>> tracks)
{
QTextStream sOut(&out);
sOut << "<REAPER_PROJECT 0.1 \"5.0\" 1551567848" << endl;
sOut << " RECORD_PATH \"\" \"\"" << endl;
sOut << " SAMPLERATE 48000 0 0" << endl;
sOut << " TEMPO 120 4 4" << endl;
qint32 iid = 0;
foreach(auto trackName, tracks.keys())
{
sOut << CReaperTrack(trackName, iid, tracks[trackName]).toString() << endl;
}
sOut << ">";
sOut.flush();
}

93
src/recorder/creaperproject.h Executable file
View file

@ -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 <QUuid>
#include <QFileInfo>
#include <QDir>
#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<double>(frames * SYSTEM_FRAME_SIZE_SAMPLES) / 48000, 'f', 14); }
};
class CReaperTrack : public QObject
{
Q_OBJECT
public:
CReaperTrack(QString name, qint32 &iid, QList<STrackItem> items);
QString toString() { return out; }
private:
QUuid trackId = QUuid::createUuid();
QString out;
};
class CReaperProject : public QObject
{
Q_OBJECT
public:
CReaperProject(QMap<QString, QList<STrackItem> > tracks);
QString toString() { return out; }
private:
QString out;
};
}
#endif // CREAPERPROJECT_H

144
src/recorder/cwavestream.cpp Executable file
View file

@ -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<uint32_t>(currentPos - initialPos);
QDataStream& out = static_cast<QDataStream&>(*this);
// Overwrite hdr_riff.chunkSize
this->device()->seek(initialPos + hdrRiffChunkSizeOffset);
out << static_cast<uint32_t>(fileLength - (hdrRiffChunkSizeOffset + sizeof (uint32_t)));
// Overwrite dataSubChunkHdr.chunkSize
this->device()->seek(initialPos + dataSubChunkHdrChunkSizeOffset);
out << static_cast<uint32_t>(fileLength - (dataSubChunkHdrChunkSizeOffset + sizeof (uint32_t)));
// and then restore the position and byte order
this->device()->seek(currentPos);
setByteOrder(initialByteOrder);
}

92
src/recorder/cwavestream.h Executable file
View file

@ -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 <QDataStream>
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

438
src/recorder/jamrecorder.cpp Executable file
View file

@ -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<uint16_t>(_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<int16_t>& 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<int16_t> 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<QString, QList<STrackItem>> CJamSession::Tracks()
{
QMap<QString, QList<STrackItem>> 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<QString, QList<STrackItem>> CJamSession::TracksFromSessionDir(const QString& sessionDirName)
{
QMap<QString, QList<STrackItem>> 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<CVector<int16_t>>();
QObject::connect((const QObject *)server, SIGNAL ( AudioFrame(const int, const QString, const CHostAddress, const int, const CVector<int16_t>) ),
this, SLOT( OnFrame(const int, const QString, const CHostAddress, const int, const CVector<int16_t>) ),
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<int16_t> data)
{
// Make sure we are ready
if (!isRecording)
{
OnStart();
}
currentSession->Frame(iChID, name, address, numAudioChannels, data);
}

181
src/recorder/jamrecorder.h Executable file
View file

@ -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 <QDir>
#include <QFile>
#include <QDateTime>
#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<int16_t>& 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<int16_t> data);
void End();
QVector<CJamClient*> Clients() { return vecptrJamClients; }
QMap<QString, QList<STrackItem>> Tracks();
QString Name() { return sessionDir.dirName(); }
const QDir SessionDir() { return sessionDir; }
void DisconnectClient(int iChID);
static QMap<QString, QList<STrackItem>> TracksFromSessionDir(const QString& name);
private:
CJamSession();
const QDir sessionDir;
qint64 currentFrame;
QVector<CJamClient*> vecptrJamClients;
QList<CJamClientConnection*> 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<int16_t> data);
private:
QDir recordBaseDir;
bool isRecording;
CJamSession* currentSession;
QTextStream& tsConsole = *((new ConsoleWriterFactory())->get());
};
}
Q_DECLARE_METATYPE(int16_t)
Q_DECLARE_METATYPE(CVector<int16_t>)
#endif // JAMRECORDER_H

View file

@ -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<int> ( 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,

View file

@ -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<int16_t> vecsData );
public slots:
void OnTimer();

View file

@ -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 ); }
};

View file

@ -385,6 +385,7 @@ CAboutDlg::CAboutDlg ( QWidget* parent ) : QDialog ( parent )
"<i><a href=""http://openclipart.org"">http://openclipart.org</a></i></li>"
"<li>Country flag icons from Mark James: "
"<i><a href=""http://www.famfamfam.com"">http://www.famfamfam.com</a></i></li>"
"<li>Server wave recording, coded by pljones</li>"
"</ul>"
"</center><br>");
@ -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 *
\******************************************************************************/

View file

@ -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 *
\******************************************************************************/

View file

@ -25,7 +25,7 @@
*
\******************************************************************************/
#include "Sound.h"
#include "sound.h"
/* Implementation *************************************************************/