Add recording support with Reaper Project generation
Includes the following changes * Initial .gitignore Administrative * Fix up warning message * Not all Windows file systems are case insensitive Bugfixes * (Qt5) Use QCoreApplication for headless Possible solution to get the application to run as a headless server but it loses the nice history graph, so not ideal. * Avoid ESC closing chat Because ESC shouldn't close the chat window. Or the main app window. * Add console logging support for Windows Whilst looking for the headless support, I found this idea for Windows logging. New improved version. This makes far fewer changes. ---- * Add recording support with Reaper Project generation The main feature! * New -r option to enable recording of PCM files and conversion to Reaper RPP with WAV files * New -R option to set the directory in which to create recording sessions You need to specify the -R option, there's no default... so I guess -r and -R could be combined. * New -T option to convert a session directory with PCM files into a Reaper RPP with WAV files You can use -T on "failed" sessions, if the -r option captures the PCMs but the RPP converter doesn't run for some reaon. (It was useful during development, maybe less so once things seem stable.) The recorder is implemented as a new thread with queuing from the main "real time" server thread. When a new client connects or if its audio format changes (e.g. mono to stereo), a new RIFF WAVE file is started. Each frame of decompressed audio for each client written out as LPCM to the file. When the client disconnects, the RIFF WAVE headers are updated to reflect the file length. Once all clients disconnect, the session is considered ended and a Reaper RPP file is written.
This commit is contained in:
parent
3609332754
commit
8c1deffda7
21 changed files with 1145 additions and 22 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.qmake.stash
|
||||||
|
Jamulus
|
||||||
|
Makefile
|
||||||
|
*.pro.user*
|
||||||
|
**.cppe
|
||||||
|
**.he
|
||||||
|
.cproject
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
*.user
|
||||||
|
*.user.*
|
||||||
|
*.o
|
||||||
|
moc_*.cpp
|
||||||
|
ui_*.h
|
||||||
|
moc_predefs.h
|
||||||
|
src/res/qrc_resources.cpp
|
||||||
|
windows/ASIOSDK2
|
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