Merge pull request #2 from pljones/master

Add recording support with Reaper Project generation
This commit is contained in:
corrados 2019-04-10 19:33:30 +02:00 committed by GitHub
commit 703b458ee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1128 additions and 22 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")
}
}
@ -170,7 +170,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 +267,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 \

View File

@ -102,3 +102,11 @@ void CChatDlg::AddChatText ( QString strChatText )
#endif
);
}
void CChatDlg::keyPressEvent(QKeyEvent *e)
{
if (e->key() != Qt::Key_Escape)
{
QDialog::keyPressEvent(e);
}
}

View File

@ -48,6 +48,8 @@ public slots:
void OnLocalInputTextTextChanged ( const QString& strNewText );
void OnClearPressed();
void keyPressEvent(QKeyEvent *e);
signals:
void NewLocalInputText ( QString strNewText );
};

View File

@ -1200,3 +1200,16 @@ rbtReverbSelR->setStyleSheet ( "" );
// also apply GUI design to child GUI controls
MainMixerBoard->SetGUIDesign ( eNewDesign );
}
void CClientDlg::accept()
{
close();
}
void CClientDlg::keyPressEvent(QKeyEvent *e)
{
if (e->key() != Qt::Key_Escape)
{
QDialog::keyPressEvent(e);
}
}

View File

@ -205,4 +205,7 @@ public slots:
void OnAudioChannelsChanged() { UpdateRevSelection(); }
void OnNumClientsChanged ( int iNewNumClients );
void OnNewClientLevelChanged() { MainMixerBoard->iNewClientFaderLevel = pClient->iNewClientFaderLevel; }
void accept();
void keyPressEvent(QKeyEvent *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

@ -33,17 +33,41 @@
#include "testbench.h"
/******************************************************************************\
* Console logging *
\******************************************************************************/
// Try for a portable console --------------------------------------------------
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;
}
// 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;
@ -56,6 +80,7 @@ int main ( int argc, char** argv )
bool bDisconnectAllClients = false;
bool bShowAnalyzerConsole = false;
bool bCentServPingServerInList = false;
bool bEnableRecording = false;
int iNumServerChannels = DEFAULT_USED_NUM_CHANNELS;
int iCtrlMIDIChannel = INVALID_MIDI_CH;
quint16 iPortNumber = LLCON_DEFAULT_PORT_NUMBER;
@ -66,6 +91,8 @@ int main ( int argc, char** argv )
QString strServerName = "";
QString strLoggingFileName = "";
QString strHistoryFileName = "";
QString strRecordingDirName = "";
QString strSessionDirName = "";
QString strCentralServer = "";
QString strServerInfo = "";
QString strWelcomeMessage = "";
@ -157,6 +184,18 @@ int main ( int argc, char** argv )
}
// Enable recording at the server --------------------------------------
if ( GetFlagArgument ( argv,
i,
"-r",
"--enablerecording" ) )
{
bEnableRecording = true;
tsConsole << "- enabling recording" << endl;
continue;
}
// Show all registered servers in the server list ----------------------
// Undocumented debugging command line argument: Show all registered
// servers in the server list regardless if a ping to the server is
@ -292,6 +331,37 @@ 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;
}
// Convert a recording session to a Reaper Project ---------------------
if ( GetStringArgument ( tsConsole,
argc,
argv,
i,
"-T",
"--toreaper",
strArgument ) )
{
bUseGUI = false;
strSessionDirName = strArgument;
tsConsole << "- convert " << strSessionDirName << " to Reaper project (no GUI)" << endl;
continue;
}
// Central server ------------------------------------------------------
if ( GetStringArgument ( tsConsole,
argc,
@ -402,7 +472,15 @@ 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* _app = bUseGUI
? new QApplication ( argc, argv )
: new QCoreApplication ( argc, argv );
#define app (*_app)
#ifdef _WIN32
// set application priority class -> high priority
@ -425,7 +503,11 @@ int main ( int argc, char** argv )
try
{
if ( bIsClient )
if ( !strSessionDirName.isEmpty() )
{
CJamRecorder::SessionDirToReaper(strSessionDirName);
}
else if ( bIsClient )
{
// Client:
// actual client object
@ -473,10 +555,11 @@ int main ( int argc, char** argv )
strCentralServer,
strServerInfo,
strWelcomeMessage,
strRecordingDirName,
bEnableRecording,
bCentServPingServerInList,
bDisconnectAllClients,
eLicenceType );
if ( bUseGUI )
{
// special case for the GUI mode: as the default we want to use
@ -571,7 +654,11 @@ QString UsageArguments ( char **argv )
" [server1 country as QLocale ID]; ...\n"
" [server2 address]; ... (server only)\n"
" -p, --port local port number (server only)\n"
" -r --enablerecording create recordings of jam sessions (server only)\n"
" -R, --recordingdirectory\n"
" directory to contain recorded jams (server only)\n"
" -s, --server start server\n"
" -T, --toreaper create Reaper project from session in named directory\n"
" -u, --numchannels maximum number of channels (server only)\n"
" -w, --welcomemessage welcome message on connect (server only)\n"
" -y, --history enable connection history and set file\n"

107
src/recorder/creaperproject.cpp Executable file
View File

@ -0,0 +1,107 @@
#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();
}

70
src/recorder/creaperproject.h Executable file
View File

@ -0,0 +1,70 @@
#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

121
src/recorder/cwavestream.cpp Executable file
View File

@ -0,0 +1,121 @@
#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);
}

69
src/recorder/cwavestream.h Executable file
View File

@ -0,0 +1,69 @@
#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

418
src/recorder/jamrecorder.cpp Executable file
View File

@ -0,0 +1,418 @@
#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::CJamRecorder Create recording directory, if necessary, and connect signal handlers
* @param server Server object emiting signals
* @param recordingDirName Requested recording directory name
*/
CJamRecorder::CJamRecorder(const CServer* server, const QString recordingDirName) :
recordBaseDir (recordingDirName),
isRecording (false)
{
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 ( Frame(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);
}

156
src/recorder/jamrecorder.h Executable file
View File

@ -0,0 +1,156 @@
#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 CServer* server, const QString recordingDirName);
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,6 +206,8 @@ CServer::CServer ( const int iNewMaxNumChan,
const QString& strCentralServer,
const QString& strServerInfo,
const QString& strNewWelcomeMessage,
const QString& strRecordingDirName,
const bool bEnableRecording,
const bool bNCentServPingServerInList,
const bool bNDisconnectAllClients,
const ELicenceType eNLicenceType ) :
@ -357,6 +359,13 @@ CServer::CServer ( const int iNewMaxNumChan,
QString().number( static_cast<int> ( iPortNumber ) ) );
}
// Enable jam recording (if requested)
if ( bEnableRecording )
{
JamRecorder = new CJamRecorder(this, strRecordingDirName);
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 +748,10 @@ 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 )
{
emit ClientDisconnected(iCurChanID); //? do outside mutex lock?
bChannelIsNowDisconnected = true;
}
@ -825,6 +836,8 @@ JitterMeas.Measure();
// get number of audio channels of current channel
const int iCurNumAudChan = vecNumAudioChannels[i];
emit Frame(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

@ -41,6 +41,9 @@
#include "serverlogging.h"
#include "serverlist.h"
#include "recorder/jamrecorder.h"
using recorder::CJamRecorder;
/* Definitions ****************************************************************/
// no valid channel number
@ -127,6 +130,8 @@ public:
const QString& strCentralServer,
const QString& strServerInfo,
const QString& strNewWelcomeMessage,
const QString& strRecordingDirName,
const bool bEnableRecording,
const bool bNCentServPingServerInList,
const bool bNDisconnectAllClients,
const ELicenceType eNLicenceType );
@ -247,6 +252,9 @@ protected:
// logging
CServerLogging Logging;
// recording thread
CJamRecorder* JamRecorder;
// HTML file server status
bool bWriteStatusHTMLFile;
QString strServerHTMLFileListName;
@ -268,6 +276,8 @@ protected:
signals:
void Started();
void Stopped();
void ClientDisconnected(const int iChID);
void Frame(const int, const QString, const CHostAddress, const int, const CVector<int16_t>);
public slots:
void OnTimer();

View File

@ -588,3 +588,11 @@ void CServerDlg::changeEvent ( QEvent* pEvent )
}
}
}
void CServerDlg::keyPressEvent(QKeyEvent *e)
{
if (e->key() != Qt::Key_Escape)
{
QDialog::keyPressEvent(e);
}
}

View File

@ -97,4 +97,6 @@ public slots:
void OnSysTrayMenuHide() { hide(); }
void OnSysTrayMenuExit() { close(); }
void OnSysTrayActivated ( QSystemTrayIcon::ActivationReason ActReason );
void keyPressEvent(QKeyEvent *e);
};

View File

@ -342,10 +342,10 @@ void CServerLogging::AddNewConnection ( const QHostAddress& ClientInetAddr )
const QString strLogStr = CurTimeDatetoLogString() + ", " +
ClientInetAddr.toString() + ", connected";
#ifndef _WIN32
QTextStream tsConsoleStream ( stdout );
tsConsoleStream << strLogStr << endl; // on console
#endif
QTextStream* tsConsoleStream = (new ConsoleWriterFactory())->get();
(*tsConsoleStream) << strLogStr << endl; // on console
*this << strLogStr; // in log file
// add element to history
@ -357,10 +357,10 @@ void CServerLogging::AddServerStopped()
const QString strLogStr = CurTimeDatetoLogString() + ",, server stopped "
"-------------------------------------";
#ifndef _WIN32
QTextStream tsConsoleStream ( stdout );
tsConsoleStream << strLogStr << endl; // on console
#endif
QTextStream* tsConsoleStream = (new ConsoleWriterFactory())->get();
(*tsConsoleStream) << strLogStr << endl; // on console
*this << strLogStr; // in log file
// add element to history and update on server stop

View File

@ -1275,4 +1275,13 @@ protected:
bool bPreviousState;
};
class ConsoleWriterFactory
{
public:
ConsoleWriterFactory() : ptsConsole ( nullptr ) { }
QTextStream* get();
private:
QTextStream* ptsConsole;
};
#endif /* !defined ( UTIL_HOIH934256GEKJH98_3_43445KJIUHF1912__INCLUDED_ ) */

View File

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