Merge pull request #2 from pljones/master
Add recording support with Reaper Project generation
This commit is contained in:
commit
703b458ee9
20 changed files with 1128 additions and 22 deletions
12
Jamulus.pro
12
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 \
|
||||
|
|
|
@ -102,3 +102,11 @@ void CChatDlg::AddChatText ( QString strChatText )
|
|||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
void CChatDlg::keyPressEvent(QKeyEvent *e)
|
||||
{
|
||||
if (e->key() != Qt::Key_Escape)
|
||||
{
|
||||
QDialog::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ public slots:
|
|||
void OnLocalInputTextTextChanged ( const QString& strNewText );
|
||||
void OnClearPressed();
|
||||
|
||||
void keyPressEvent(QKeyEvent *e);
|
||||
|
||||
signals:
|
||||
void NewLocalInputText ( QString strNewText );
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
107
src/main.cpp
107
src/main.cpp
|
@ -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
107
src/recorder/creaperproject.cpp
Executable 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
70
src/recorder/creaperproject.h
Executable 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
121
src/recorder/cwavestream.cpp
Executable 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
69
src/recorder/cwavestream.h
Executable 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
418
src/recorder/jamrecorder.cpp
Executable 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
156
src/recorder/jamrecorder.h
Executable 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
|
|
@ -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,
|
||||
|
|
10
src/server.h
10
src/server.h
|
@ -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();
|
||||
|
|
|
@ -588,3 +588,11 @@ void CServerDlg::changeEvent ( QEvent* pEvent )
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CServerDlg::keyPressEvent(QKeyEvent *e)
|
||||
{
|
||||
if (e->key() != Qt::Key_Escape)
|
||||
{
|
||||
QDialog::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,4 +97,6 @@ public slots:
|
|||
void OnSysTrayMenuHide() { hide(); }
|
||||
void OnSysTrayMenuExit() { close(); }
|
||||
void OnSysTrayActivated ( QSystemTrayIcon::ActivationReason ActReason );
|
||||
|
||||
void keyPressEvent(QKeyEvent *e);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_ ) */
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#include "Sound.h"
|
||||
#include "sound.h"
|
||||
|
||||
|
||||
/* Implementation *************************************************************/
|
||||
|
|
Loading…
Reference in a new issue