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/include/jack/jack.h) {
|
||||||
!exists(/usr/local/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/soundbase.h \
|
||||||
src/testbench.h \
|
src/testbench.h \
|
||||||
src/util.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 \
|
HEADERS_OPUS = libs/opus/include/opus.h \
|
||||||
libs/opus/include/opus_multistream.h \
|
libs/opus/include/opus_multistream.h \
|
||||||
|
@ -264,7 +267,10 @@ SOURCES += src/audiomixerboard.cpp \
|
||||||
src/socket.cpp \
|
src/socket.cpp \
|
||||||
src/soundbase.cpp \
|
src/soundbase.cpp \
|
||||||
src/util.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 \
|
SOURCES_OPUS = libs/opus/src/opus.c \
|
||||||
libs/opus/src/opus_decoder.c \
|
libs/opus/src/opus_decoder.c \
|
||||||
|
|
|
@ -102,3 +102,11 @@ void CChatDlg::AddChatText ( QString strChatText )
|
||||||
#endif
|
#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 OnLocalInputTextTextChanged ( const QString& strNewText );
|
||||||
void OnClearPressed();
|
void OnClearPressed();
|
||||||
|
|
||||||
|
void keyPressEvent(QKeyEvent *e);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void NewLocalInputText ( QString strNewText );
|
void NewLocalInputText ( QString strNewText );
|
||||||
};
|
};
|
||||||
|
|
|
@ -1200,3 +1200,16 @@ rbtReverbSelR->setStyleSheet ( "" );
|
||||||
// also apply GUI design to child GUI controls
|
// also apply GUI design to child GUI controls
|
||||||
MainMixerBoard->SetGUIDesign ( eNewDesign );
|
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 OnAudioChannelsChanged() { UpdateRevSelection(); }
|
||||||
void OnNumClientsChanged ( int iNewNumClients );
|
void OnNumClientsChanged ( int iNewNumClients );
|
||||||
void OnNewClientLevelChanged() { MainMixerBoard->iNewClientFaderLevel = pClient->iNewClientFaderLevel; }
|
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_ )
|
#if !defined ( GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_ )
|
||||||
#define 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 <QString>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
107
src/main.cpp
107
src/main.cpp
|
@ -33,17 +33,41 @@
|
||||||
#include "testbench.h"
|
#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 **************************************************************
|
// Implementation **************************************************************
|
||||||
|
|
||||||
int main ( int argc, char** argv )
|
int main ( int argc, char** argv )
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
QTextStream& tsConsole = *((new ConsoleWriterFactory())->get());
|
||||||
// no console on windows -> just write in string and dump it
|
|
||||||
QString strDummySink;
|
|
||||||
QTextStream tsConsole ( &strDummySink );
|
|
||||||
#else
|
|
||||||
QTextStream tsConsole ( stdout );
|
|
||||||
#endif
|
|
||||||
QString strArgument;
|
QString strArgument;
|
||||||
double rDbleArgument;
|
double rDbleArgument;
|
||||||
|
|
||||||
|
@ -56,6 +80,7 @@ int main ( int argc, char** argv )
|
||||||
bool bDisconnectAllClients = false;
|
bool bDisconnectAllClients = false;
|
||||||
bool bShowAnalyzerConsole = false;
|
bool bShowAnalyzerConsole = false;
|
||||||
bool bCentServPingServerInList = false;
|
bool bCentServPingServerInList = false;
|
||||||
|
bool bEnableRecording = false;
|
||||||
int iNumServerChannels = DEFAULT_USED_NUM_CHANNELS;
|
int iNumServerChannels = DEFAULT_USED_NUM_CHANNELS;
|
||||||
int iCtrlMIDIChannel = INVALID_MIDI_CH;
|
int iCtrlMIDIChannel = INVALID_MIDI_CH;
|
||||||
quint16 iPortNumber = LLCON_DEFAULT_PORT_NUMBER;
|
quint16 iPortNumber = LLCON_DEFAULT_PORT_NUMBER;
|
||||||
|
@ -66,6 +91,8 @@ int main ( int argc, char** argv )
|
||||||
QString strServerName = "";
|
QString strServerName = "";
|
||||||
QString strLoggingFileName = "";
|
QString strLoggingFileName = "";
|
||||||
QString strHistoryFileName = "";
|
QString strHistoryFileName = "";
|
||||||
|
QString strRecordingDirName = "";
|
||||||
|
QString strSessionDirName = "";
|
||||||
QString strCentralServer = "";
|
QString strCentralServer = "";
|
||||||
QString strServerInfo = "";
|
QString strServerInfo = "";
|
||||||
QString strWelcomeMessage = "";
|
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 ----------------------
|
// Show all registered servers in the server list ----------------------
|
||||||
// Undocumented debugging command line argument: Show all registered
|
// Undocumented debugging command line argument: Show all registered
|
||||||
// servers in the server list regardless if a ping to the server is
|
// 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 ------------------------------------------------------
|
// Central server ------------------------------------------------------
|
||||||
if ( GetStringArgument ( tsConsole,
|
if ( GetStringArgument ( tsConsole,
|
||||||
argc,
|
argc,
|
||||||
|
@ -402,7 +472,15 @@ int main ( int argc, char** argv )
|
||||||
|
|
||||||
// Application/GUI setup ---------------------------------------------------
|
// Application/GUI setup ---------------------------------------------------
|
||||||
// Application object
|
// 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
|
#ifdef _WIN32
|
||||||
// set application priority class -> high priority
|
// set application priority class -> high priority
|
||||||
|
@ -425,7 +503,11 @@ int main ( int argc, char** argv )
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if ( bIsClient )
|
if ( !strSessionDirName.isEmpty() )
|
||||||
|
{
|
||||||
|
CJamRecorder::SessionDirToReaper(strSessionDirName);
|
||||||
|
}
|
||||||
|
else if ( bIsClient )
|
||||||
{
|
{
|
||||||
// Client:
|
// Client:
|
||||||
// actual client object
|
// actual client object
|
||||||
|
@ -473,10 +555,11 @@ int main ( int argc, char** argv )
|
||||||
strCentralServer,
|
strCentralServer,
|
||||||
strServerInfo,
|
strServerInfo,
|
||||||
strWelcomeMessage,
|
strWelcomeMessage,
|
||||||
|
strRecordingDirName,
|
||||||
|
bEnableRecording,
|
||||||
bCentServPingServerInList,
|
bCentServPingServerInList,
|
||||||
bDisconnectAllClients,
|
bDisconnectAllClients,
|
||||||
eLicenceType );
|
eLicenceType );
|
||||||
|
|
||||||
if ( bUseGUI )
|
if ( bUseGUI )
|
||||||
{
|
{
|
||||||
// special case for the GUI mode: as the default we want to use
|
// 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"
|
" [server1 country as QLocale ID]; ...\n"
|
||||||
" [server2 address]; ... (server only)\n"
|
" [server2 address]; ... (server only)\n"
|
||||||
" -p, --port local port number (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"
|
" -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"
|
" -u, --numchannels maximum number of channels (server only)\n"
|
||||||
" -w, --welcomemessage welcome message on connect (server only)\n"
|
" -w, --welcomemessage welcome message on connect (server only)\n"
|
||||||
" -y, --history enable connection history and set file\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& strCentralServer,
|
||||||
const QString& strServerInfo,
|
const QString& strServerInfo,
|
||||||
const QString& strNewWelcomeMessage,
|
const QString& strNewWelcomeMessage,
|
||||||
|
const QString& strRecordingDirName,
|
||||||
|
const bool bEnableRecording,
|
||||||
const bool bNCentServPingServerInList,
|
const bool bNCentServPingServerInList,
|
||||||
const bool bNDisconnectAllClients,
|
const bool bNDisconnectAllClients,
|
||||||
const ELicenceType eNLicenceType ) :
|
const ELicenceType eNLicenceType ) :
|
||||||
|
@ -357,6 +359,13 @@ CServer::CServer ( const int iNewMaxNumChan,
|
||||||
QString().number( static_cast<int> ( iPortNumber ) ) );
|
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
|
// enable all channels (for the server all channel must be enabled the
|
||||||
// entire life time of the software)
|
// entire life time of the software)
|
||||||
for ( i = 0; i < iMaxNumChannels; i++ )
|
for ( i = 0; i < iMaxNumChannels; i++ )
|
||||||
|
@ -739,8 +748,10 @@ JitterMeas.Measure();
|
||||||
|
|
||||||
// if channel was just disconnected, set flag that connected
|
// if channel was just disconnected, set flag that connected
|
||||||
// client list is sent to all other clients
|
// client list is sent to all other clients
|
||||||
|
// and emit the client disconnected signal
|
||||||
if ( eGetStat == GS_CHAN_NOW_DISCONNECTED )
|
if ( eGetStat == GS_CHAN_NOW_DISCONNECTED )
|
||||||
{
|
{
|
||||||
|
emit ClientDisconnected(iCurChanID); //? do outside mutex lock?
|
||||||
bChannelIsNowDisconnected = true;
|
bChannelIsNowDisconnected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -825,6 +836,8 @@ JitterMeas.Measure();
|
||||||
// get number of audio channels of current channel
|
// get number of audio channels of current channel
|
||||||
const int iCurNumAudChan = vecNumAudioChannels[i];
|
const int iCurNumAudChan = vecNumAudioChannels[i];
|
||||||
|
|
||||||
|
emit Frame(iCurChanID, vecChannels[iCurChanID].GetName(), vecChannels[iCurChanID].GetAddress(), iCurNumAudChan, vecvecsData[i]);
|
||||||
|
|
||||||
// generate a sparate mix for each channel
|
// generate a sparate mix for each channel
|
||||||
// actual processing of audio data -> mix
|
// actual processing of audio data -> mix
|
||||||
ProcessData ( vecvecsData,
|
ProcessData ( vecvecsData,
|
||||||
|
|
10
src/server.h
10
src/server.h
|
@ -41,6 +41,9 @@
|
||||||
#include "serverlogging.h"
|
#include "serverlogging.h"
|
||||||
#include "serverlist.h"
|
#include "serverlist.h"
|
||||||
|
|
||||||
|
#include "recorder/jamrecorder.h"
|
||||||
|
using recorder::CJamRecorder;
|
||||||
|
|
||||||
|
|
||||||
/* Definitions ****************************************************************/
|
/* Definitions ****************************************************************/
|
||||||
// no valid channel number
|
// no valid channel number
|
||||||
|
@ -127,6 +130,8 @@ public:
|
||||||
const QString& strCentralServer,
|
const QString& strCentralServer,
|
||||||
const QString& strServerInfo,
|
const QString& strServerInfo,
|
||||||
const QString& strNewWelcomeMessage,
|
const QString& strNewWelcomeMessage,
|
||||||
|
const QString& strRecordingDirName,
|
||||||
|
const bool bEnableRecording,
|
||||||
const bool bNCentServPingServerInList,
|
const bool bNCentServPingServerInList,
|
||||||
const bool bNDisconnectAllClients,
|
const bool bNDisconnectAllClients,
|
||||||
const ELicenceType eNLicenceType );
|
const ELicenceType eNLicenceType );
|
||||||
|
@ -247,6 +252,9 @@ protected:
|
||||||
// logging
|
// logging
|
||||||
CServerLogging Logging;
|
CServerLogging Logging;
|
||||||
|
|
||||||
|
// recording thread
|
||||||
|
CJamRecorder* JamRecorder;
|
||||||
|
|
||||||
// HTML file server status
|
// HTML file server status
|
||||||
bool bWriteStatusHTMLFile;
|
bool bWriteStatusHTMLFile;
|
||||||
QString strServerHTMLFileListName;
|
QString strServerHTMLFileListName;
|
||||||
|
@ -268,6 +276,8 @@ protected:
|
||||||
signals:
|
signals:
|
||||||
void Started();
|
void Started();
|
||||||
void Stopped();
|
void Stopped();
|
||||||
|
void ClientDisconnected(const int iChID);
|
||||||
|
void Frame(const int, const QString, const CHostAddress, const int, const CVector<int16_t>);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void OnTimer();
|
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 OnSysTrayMenuHide() { hide(); }
|
||||||
void OnSysTrayMenuExit() { close(); }
|
void OnSysTrayMenuExit() { close(); }
|
||||||
void OnSysTrayActivated ( QSystemTrayIcon::ActivationReason ActReason );
|
void OnSysTrayActivated ( QSystemTrayIcon::ActivationReason ActReason );
|
||||||
|
|
||||||
|
void keyPressEvent(QKeyEvent *e);
|
||||||
};
|
};
|
||||||
|
|
|
@ -342,10 +342,10 @@ void CServerLogging::AddNewConnection ( const QHostAddress& ClientInetAddr )
|
||||||
const QString strLogStr = CurTimeDatetoLogString() + ", " +
|
const QString strLogStr = CurTimeDatetoLogString() + ", " +
|
||||||
ClientInetAddr.toString() + ", connected";
|
ClientInetAddr.toString() + ", connected";
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
QTextStream tsConsoleStream ( stdout );
|
QTextStream* tsConsoleStream = (new ConsoleWriterFactory())->get();
|
||||||
tsConsoleStream << strLogStr << endl; // on console
|
(*tsConsoleStream) << strLogStr << endl; // on console
|
||||||
#endif
|
|
||||||
*this << strLogStr; // in log file
|
*this << strLogStr; // in log file
|
||||||
|
|
||||||
// add element to history
|
// add element to history
|
||||||
|
@ -357,10 +357,10 @@ void CServerLogging::AddServerStopped()
|
||||||
const QString strLogStr = CurTimeDatetoLogString() + ",, server stopped "
|
const QString strLogStr = CurTimeDatetoLogString() + ",, server stopped "
|
||||||
"-------------------------------------";
|
"-------------------------------------";
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
QTextStream tsConsoleStream ( stdout );
|
QTextStream* tsConsoleStream = (new ConsoleWriterFactory())->get();
|
||||||
tsConsoleStream << strLogStr << endl; // on console
|
(*tsConsoleStream) << strLogStr << endl; // on console
|
||||||
#endif
|
|
||||||
*this << strLogStr; // in log file
|
*this << strLogStr; // in log file
|
||||||
|
|
||||||
// add element to history and update on server stop
|
// add element to history and update on server stop
|
||||||
|
|
|
@ -1275,4 +1275,13 @@ protected:
|
||||||
bool bPreviousState;
|
bool bPreviousState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ConsoleWriterFactory
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConsoleWriterFactory() : ptsConsole ( nullptr ) { }
|
||||||
|
QTextStream* get();
|
||||||
|
private:
|
||||||
|
QTextStream* ptsConsole;
|
||||||
|
};
|
||||||
|
|
||||||
#endif /* !defined ( UTIL_HOIH934256GEKJH98_3_43445KJIUHF1912__INCLUDED_ ) */
|
#endif /* !defined ( UTIL_HOIH934256GEKJH98_3_43445KJIUHF1912__INCLUDED_ ) */
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
*
|
*
|
||||||
\******************************************************************************/
|
\******************************************************************************/
|
||||||
|
|
||||||
#include "Sound.h"
|
#include "sound.h"
|
||||||
|
|
||||||
|
|
||||||
/* Implementation *************************************************************/
|
/* Implementation *************************************************************/
|
||||||
|
|
Loading…
Reference in a new issue