Merge pull request #3 from corrados/integrate_recorder
Integrate recorder
This commit is contained in:
commit
9ac72ec357
18 changed files with 1224 additions and 29 deletions
15
Jamulus.pro
15
Jamulus.pro
|
@ -113,7 +113,7 @@ win32 {
|
|||
|
||||
!exists(/usr/include/jack/jack.h) {
|
||||
!exists(/usr/local/include/jack/jack.h) {
|
||||
message(Warning: jack.h was not found at the usual place, maybe the jack dev packet is missing)
|
||||
message("Warning: jack.h was not found at the usual place, maybe the jack dev packet is missing")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,9 +123,6 @@ win32 {
|
|||
LIBS += -ljack
|
||||
}
|
||||
|
||||
# enable to following line to support compilation on the Raspberry Pi
|
||||
#LIBS += -lrt
|
||||
|
||||
# Linux is our source distribution, include sources from other OSs
|
||||
DISTFILES += mac/sound.h \
|
||||
mac/sound.cpp \
|
||||
|
@ -170,7 +167,10 @@ HEADERS += src/audiomixerboard.h \
|
|||
src/soundbase.h \
|
||||
src/testbench.h \
|
||||
src/util.h \
|
||||
src/analyzerconsole.h
|
||||
src/analyzerconsole.h \
|
||||
src/recorder/jamrecorder.h \
|
||||
src/recorder/creaperproject.h \
|
||||
src/recorder/cwavestream.h
|
||||
|
||||
HEADERS_OPUS = libs/opus/include/opus.h \
|
||||
libs/opus/include/opus_multistream.h \
|
||||
|
@ -264,7 +264,10 @@ SOURCES += src/audiomixerboard.cpp \
|
|||
src/socket.cpp \
|
||||
src/soundbase.cpp \
|
||||
src/util.cpp \
|
||||
src/analyzerconsole.cpp
|
||||
src/analyzerconsole.cpp \
|
||||
src/recorder/jamrecorder.cpp \
|
||||
src/recorder/creaperproject.cpp \
|
||||
src/recorder/cwavestream.cpp
|
||||
|
||||
SOURCES_OPUS = libs/opus/src/opus.c \
|
||||
libs/opus/src/opus_decoder.c \
|
||||
|
|
2
README
2
README
|
@ -62,3 +62,5 @@ source:
|
|||
|
||||
- Some pixmaps are from the Open Clip Art Library (OCAL):
|
||||
http://openclipart.org
|
||||
|
||||
- Server wave recording, coded by pljones
|
||||
|
|
|
@ -48,6 +48,9 @@ public slots:
|
|||
void OnLocalInputTextTextChanged ( const QString& strNewText );
|
||||
void OnClearPressed();
|
||||
|
||||
void keyPressEvent ( QKeyEvent *e ) // block escape key
|
||||
{ if ( e->key() != Qt::Key_Escape ) QDialog::keyPressEvent ( e ); }
|
||||
|
||||
signals:
|
||||
void NewLocalInputText ( QString strNewText );
|
||||
};
|
||||
|
|
|
@ -79,8 +79,8 @@ public:
|
|||
const QString& strConnOnStartupAddress,
|
||||
const bool bNewShowComplRegConnList,
|
||||
const bool bShowAnalyzerConsole,
|
||||
QWidget* parent = 0,
|
||||
Qt::WindowFlags f = 0 );
|
||||
QWidget* parent = nullptr,
|
||||
Qt::WindowFlags f = nullptr );
|
||||
|
||||
protected:
|
||||
void SetGUIDesign ( const EGUIDesign eNewDesign );
|
||||
|
@ -205,4 +205,9 @@ public slots:
|
|||
void OnAudioChannelsChanged() { UpdateRevSelection(); }
|
||||
void OnNumClientsChanged ( int iNewNumClients );
|
||||
void OnNewClientLevelChanged() { MainMixerBoard->iNewClientFaderLevel = pClient->iNewClientFaderLevel; }
|
||||
|
||||
void accept() { close(); } // introduced by pljones
|
||||
|
||||
void keyPressEvent ( QKeyEvent *e ) // block escape key
|
||||
{ if ( e->key() != Qt::Key_Escape ) QDialog::keyPressEvent ( e ); }
|
||||
};
|
||||
|
|
|
@ -50,6 +50,10 @@ LED bar: lbr
|
|||
#if !defined ( GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_ )
|
||||
#define GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_
|
||||
|
||||
#if _WIN32
|
||||
# define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
|
||||
#include <QString>
|
||||
#include <QEvent>
|
||||
#include <QDebug>
|
||||
|
|
56
src/main.cpp
56
src/main.cpp
|
@ -31,19 +31,14 @@
|
|||
#include "serverdlg.h"
|
||||
#include "settings.h"
|
||||
#include "testbench.h"
|
||||
#include "util.h"
|
||||
|
||||
|
||||
// Implementation **************************************************************
|
||||
|
||||
int main ( int argc, char** argv )
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// no console on windows -> just write in string and dump it
|
||||
QString strDummySink;
|
||||
QTextStream tsConsole ( &strDummySink );
|
||||
#else
|
||||
QTextStream tsConsole ( stdout );
|
||||
#endif
|
||||
QTextStream& tsConsole = *( ( new ConsoleWriterFactory() )->get() );
|
||||
QString strArgument;
|
||||
double rDbleArgument;
|
||||
|
||||
|
@ -66,6 +61,7 @@ int main ( int argc, char** argv )
|
|||
QString strServerName = "";
|
||||
QString strLoggingFileName = "";
|
||||
QString strHistoryFileName = "";
|
||||
QString strRecordingDirName = "";
|
||||
QString strCentralServer = "";
|
||||
QString strServerInfo = "";
|
||||
QString strWelcomeMessage = "";
|
||||
|
@ -292,6 +288,21 @@ int main ( int argc, char** argv )
|
|||
}
|
||||
|
||||
|
||||
// Recording directory -------------------------------------------------
|
||||
if ( GetStringArgument ( tsConsole,
|
||||
argc,
|
||||
argv,
|
||||
i,
|
||||
"-R",
|
||||
"--recordingdirectory",
|
||||
strArgument ) )
|
||||
{
|
||||
strRecordingDirName = strArgument;
|
||||
tsConsole << "- recording directory name: " << strRecordingDirName << endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Central server ------------------------------------------------------
|
||||
if ( GetStringArgument ( tsConsole,
|
||||
argc,
|
||||
|
@ -402,7 +413,14 @@ int main ( int argc, char** argv )
|
|||
|
||||
// Application/GUI setup ---------------------------------------------------
|
||||
// Application object
|
||||
QApplication app ( argc, argv, bUseGUI );
|
||||
if ( !bUseGUI && !strHistoryFileName.isEmpty() )
|
||||
{
|
||||
tsConsole << "Qt5 requires a windowing system to paint a JPEG image; disabling history graph" << endl;
|
||||
strHistoryFileName = "";
|
||||
}
|
||||
QCoreApplication* pApp = bUseGUI
|
||||
? new QApplication ( argc, argv )
|
||||
: new QCoreApplication ( argc, argv );
|
||||
|
||||
#ifdef _WIN32
|
||||
// set application priority class -> high priority
|
||||
|
@ -412,7 +430,7 @@ int main ( int argc, char** argv )
|
|||
// be located in the install directory of the software by the installer.
|
||||
// Here, we set the path to our application path.
|
||||
QDir ApplDir ( QApplication::applicationDirPath() );
|
||||
app.addLibraryPath ( QString ( ApplDir.absolutePath() ) );
|
||||
pApp->addLibraryPath ( QString ( ApplDir.absolutePath() ) );
|
||||
#endif
|
||||
|
||||
// init resources
|
||||
|
@ -445,19 +463,19 @@ int main ( int argc, char** argv )
|
|||
strConnOnStartupAddress,
|
||||
bShowComplRegConnList,
|
||||
bShowAnalyzerConsole,
|
||||
0,
|
||||
nullptr,
|
||||
Qt::Window );
|
||||
|
||||
// show dialog
|
||||
ClientDlg.show();
|
||||
app.exec();
|
||||
pApp->exec();
|
||||
}
|
||||
else
|
||||
{
|
||||
// only start application without using the GUI
|
||||
tsConsole << CAboutDlg::GetVersionAndNameStr ( false ) << endl;
|
||||
|
||||
app.exec();
|
||||
pApp->exec();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -473,10 +491,10 @@ int main ( int argc, char** argv )
|
|||
strCentralServer,
|
||||
strServerInfo,
|
||||
strWelcomeMessage,
|
||||
strRecordingDirName,
|
||||
bCentServPingServerInList,
|
||||
bDisconnectAllClients,
|
||||
eLicenceType );
|
||||
|
||||
if ( bUseGUI )
|
||||
{
|
||||
// special case for the GUI mode: as the default we want to use
|
||||
|
@ -496,7 +514,7 @@ int main ( int argc, char** argv )
|
|||
CServerDlg ServerDlg ( &Server,
|
||||
&Settings,
|
||||
bStartMinimized,
|
||||
0,
|
||||
nullptr,
|
||||
Qt::Window );
|
||||
|
||||
// show dialog (if not the minimized flag is set)
|
||||
|
@ -505,7 +523,7 @@ int main ( int argc, char** argv )
|
|||
ServerDlg.show();
|
||||
}
|
||||
|
||||
app.exec();
|
||||
pApp->exec();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -515,7 +533,7 @@ int main ( int argc, char** argv )
|
|||
// only start application without using the GUI
|
||||
tsConsole << CAboutDlg::GetVersionAndNameStr ( false ) << endl;
|
||||
|
||||
app.exec();
|
||||
pApp->exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -525,11 +543,11 @@ int main ( int argc, char** argv )
|
|||
// show generic error
|
||||
if ( bUseGUI )
|
||||
{
|
||||
QMessageBox::critical ( 0,
|
||||
QMessageBox::critical ( nullptr,
|
||||
APP_NAME,
|
||||
generr.GetErrorText(),
|
||||
"Quit",
|
||||
0 );
|
||||
nullptr );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -571,6 +589,8 @@ QString UsageArguments ( char **argv )
|
|||
" [server1 country as QLocale ID]; ...\n"
|
||||
" [server2 address]; ... (server only)\n"
|
||||
" -p, --port local port number (server only)\n"
|
||||
" -R, --recording enables recording and sets directory to contain\n"
|
||||
" recorded jams (server only)\n"
|
||||
" -s, --server start server\n"
|
||||
" -u, --numchannels maximum number of channels (server only)\n"
|
||||
" -w, --welcomemessage welcome message on connect (server only)\n"
|
||||
|
|
130
src/recorder/creaperproject.cpp
Executable file
130
src/recorder/creaperproject.cpp
Executable file
|
@ -0,0 +1,130 @@
|
|||
/******************************************************************************\
|
||||
*
|
||||
* Author(s):
|
||||
* pljones
|
||||
*
|
||||
******************************************************************************
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#include "creaperproject.h"
|
||||
|
||||
/**
|
||||
* @brief operator << Write details of the STrackItem to the QTextStream
|
||||
* @param os the QTextStream
|
||||
* @param trackItem the STrackItem
|
||||
* @return the QTextStream
|
||||
*
|
||||
* Note: unused?
|
||||
*/
|
||||
QTextStream& operator<<(QTextStream& os, const recorder::STrackItem& trackItem)
|
||||
{
|
||||
os << "_track( "
|
||||
<< "numAudioChannels(" << trackItem.numAudioChannels << ")"
|
||||
<< ", startFrame(" << trackItem.startFrame << ")"
|
||||
<< ", frameCount(" << trackItem.frameCount << ")"
|
||||
<< ", fileName(" << trackItem.fileName << ")"
|
||||
<< " );";
|
||||
return os;
|
||||
}
|
||||
|
||||
/******************************************************************************\
|
||||
* recorder methods *
|
||||
\******************************************************************************/
|
||||
using namespace recorder;
|
||||
|
||||
// Reaper Project writer -------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief CReaperItem::CReaperItem Construct a Reaper RPP "<ITEM>" for a given RIFF WAVE file
|
||||
* @param name the item name
|
||||
* @param trackItem the details of where the item is in the track, along with the RIFF WAVE filename
|
||||
* @param iid the sequential item id
|
||||
*/
|
||||
CReaperItem::CReaperItem(const QString& name, const STrackItem& trackItem, const qint32& iid)
|
||||
{
|
||||
QString wavName = trackItem.fileName; // assume RPP in same location...
|
||||
|
||||
QTextStream sOut(&out);
|
||||
|
||||
sOut << " <ITEM " << endl;
|
||||
sOut << " FADEIN 0 0 0 0 0 0" << endl;
|
||||
sOut << " FADEOUT 0 0 0 0 0 0" << endl;
|
||||
sOut << " POSITION " << secondsAt48K(trackItem.startFrame) << endl;
|
||||
sOut << " LENGTH " << secondsAt48K(trackItem.frameCount) << endl;
|
||||
sOut << " IGUID " << iguid.toString() << endl;
|
||||
sOut << " IID " << iid << endl;
|
||||
sOut << " NAME " << name << endl;
|
||||
sOut << " GUID " << guid.toString() << endl;
|
||||
|
||||
sOut << " <SOURCE WAVE" << endl;
|
||||
sOut << " FILE " << '"' << wavName << '"' << endl;
|
||||
sOut << " >" << endl;
|
||||
|
||||
sOut << " >";
|
||||
|
||||
sOut.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CReaperTrack::CReaperTrack Construct a Reaper RPP "<TRACK>" for a given list of track items
|
||||
* @param name the track name
|
||||
* @param iid the sequential track id
|
||||
* @param items the list of items in the track
|
||||
*/
|
||||
CReaperTrack::CReaperTrack(QString name, qint32& iid, QList<STrackItem> items)
|
||||
{
|
||||
QTextStream sOut(&out);
|
||||
|
||||
sOut << " <TRACK " << trackId.toString() << endl;
|
||||
sOut << " NAME " << name << endl;
|
||||
sOut << " TRACKID " << trackId.toString() << endl;
|
||||
|
||||
int ino = 1;
|
||||
foreach (auto item, items) {
|
||||
sOut << CReaperItem(name + " (" + QString::number(ino) + ")", item, iid).toString() << endl;
|
||||
ino++;
|
||||
iid++;
|
||||
}
|
||||
sOut << " >";
|
||||
|
||||
sOut.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CReaperProject::CReaperProject Construct a Reaper RPP "<REAPER_PROJECT>" for a given list of tracks
|
||||
* @param tracks the list of tracks
|
||||
*/
|
||||
CReaperProject::CReaperProject(QMap<QString, QList<STrackItem>> tracks)
|
||||
{
|
||||
QTextStream sOut(&out);
|
||||
|
||||
sOut << "<REAPER_PROJECT 0.1 \"5.0\" 1551567848" << endl;
|
||||
sOut << " RECORD_PATH \"\" \"\"" << endl;
|
||||
sOut << " SAMPLERATE 48000 0 0" << endl;
|
||||
sOut << " TEMPO 120 4 4" << endl;
|
||||
|
||||
qint32 iid = 0;
|
||||
foreach(auto trackName, tracks.keys())
|
||||
{
|
||||
sOut << CReaperTrack(trackName, iid, tracks[trackName]).toString() << endl;
|
||||
}
|
||||
|
||||
sOut << ">";
|
||||
|
||||
sOut.flush();
|
||||
}
|
93
src/recorder/creaperproject.h
Executable file
93
src/recorder/creaperproject.h
Executable file
|
@ -0,0 +1,93 @@
|
|||
/******************************************************************************\
|
||||
*
|
||||
* Author(s):
|
||||
* pljones
|
||||
*
|
||||
******************************************************************************
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#ifndef CREAPERPROJECT_H
|
||||
#define CREAPERPROJECT_H
|
||||
|
||||
#include <QUuid>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include "util.h"
|
||||
|
||||
#include "cwavestream.h"
|
||||
|
||||
namespace recorder {
|
||||
|
||||
struct STrackItem
|
||||
{
|
||||
STrackItem(int numAudioChannels, qint64 startFrame, qint64 frameCount, QString fileName) :
|
||||
numAudioChannels(numAudioChannels),
|
||||
startFrame(startFrame),
|
||||
frameCount(frameCount),
|
||||
fileName(fileName)
|
||||
{
|
||||
}
|
||||
int numAudioChannels;
|
||||
qint64 startFrame;
|
||||
qint64 frameCount;
|
||||
QString fileName;
|
||||
};
|
||||
|
||||
class CReaperItem : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CReaperItem(const QString& name, const STrackItem& trackItem, const qint32& iid);
|
||||
QString toString() { return out; }
|
||||
|
||||
private:
|
||||
const QUuid iguid = QUuid::createUuid();
|
||||
const QUuid guid = QUuid::createUuid();
|
||||
QString out;
|
||||
|
||||
inline QString secondsAt48K(const qint64 frames) { return QString::number(static_cast<double>(frames * SYSTEM_FRAME_SIZE_SAMPLES) / 48000, 'f', 14); }
|
||||
};
|
||||
|
||||
class CReaperTrack : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CReaperTrack(QString name, qint32 &iid, QList<STrackItem> items);
|
||||
QString toString() { return out; }
|
||||
|
||||
private:
|
||||
QUuid trackId = QUuid::createUuid();
|
||||
QString out;
|
||||
};
|
||||
|
||||
class CReaperProject : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CReaperProject(QMap<QString, QList<STrackItem> > tracks);
|
||||
QString toString() { return out; }
|
||||
|
||||
private:
|
||||
QString out;
|
||||
};
|
||||
|
||||
}
|
||||
#endif // CREAPERPROJECT_H
|
144
src/recorder/cwavestream.cpp
Executable file
144
src/recorder/cwavestream.cpp
Executable file
|
@ -0,0 +1,144 @@
|
|||
/******************************************************************************\
|
||||
*
|
||||
* Author(s):
|
||||
* pljones
|
||||
*
|
||||
******************************************************************************
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#include "cwavestream.h"
|
||||
|
||||
/******************************************************************************\
|
||||
* Overrides in global namespace *
|
||||
\******************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief operator << Emit a hdr_riff object to a QDataStream
|
||||
* @param os a QDataStream
|
||||
* @param obj a hdr_riff object
|
||||
* @return the QDataStream passed
|
||||
*/
|
||||
recorder::CWaveStream& operator<<(recorder::CWaveStream& os, const recorder::HdrRiff& obj)
|
||||
{
|
||||
(QDataStream&)os << obj.chunkId << obj.chunkSize << obj.format;
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief operator << Emit a fmtSubChunk object to a QDataStream
|
||||
* @param os a QDataStream
|
||||
* @param obj a fmtSubChunk object
|
||||
* @return the QDataStream passed
|
||||
*/
|
||||
recorder::CWaveStream& operator<<(recorder::CWaveStream& os, const recorder::FmtSubChunk& obj)
|
||||
{
|
||||
(QDataStream&)os << obj.chunkId
|
||||
<< obj.chunkSize
|
||||
<< obj.audioFormat
|
||||
<< obj.numChannels
|
||||
<< obj.sampleRate
|
||||
<< obj.byteRate
|
||||
<< obj.blockAlign
|
||||
<< obj.bitsPerSample
|
||||
;
|
||||
return os;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief operator << Emit a dataSubChunkHdr object to a QDataStream
|
||||
* @param os a QDataStream
|
||||
* @param obj a dataSubChunkHdr object
|
||||
* @return the QDataStream passed
|
||||
*/
|
||||
recorder::CWaveStream& operator<<(recorder::CWaveStream& os, const recorder::DataSubChunkHdr& obj)
|
||||
{
|
||||
(QDataStream&)os << obj.chunkId << obj.chunkSize;
|
||||
return os;
|
||||
}
|
||||
|
||||
/******************************************************************************\
|
||||
* Implementations of recorder.CWaveStream methods *
|
||||
\******************************************************************************/
|
||||
|
||||
using namespace recorder;
|
||||
|
||||
CWaveStream::CWaveStream(const uint16_t numChannels) :
|
||||
QDataStream(),
|
||||
numChannels (numChannels),
|
||||
initialPos (device()->pos()),
|
||||
initialByteOrder (byteOrder())
|
||||
{
|
||||
waveStreamHeaders();
|
||||
}
|
||||
CWaveStream::CWaveStream(QIODevice *iod, const uint16_t numChannels) :
|
||||
QDataStream(iod),
|
||||
numChannels (numChannels),
|
||||
initialPos (device()->pos()),
|
||||
initialByteOrder (byteOrder())
|
||||
{
|
||||
waveStreamHeaders();
|
||||
}
|
||||
CWaveStream::CWaveStream(QByteArray *iod, QIODevice::OpenMode flags, const uint16_t numChannels) :
|
||||
QDataStream(iod, flags),
|
||||
numChannels (numChannels),
|
||||
initialPos (device()->pos()),
|
||||
initialByteOrder (byteOrder())
|
||||
{
|
||||
waveStreamHeaders();
|
||||
}
|
||||
CWaveStream::CWaveStream(const QByteArray &ba, const uint16_t numChannels) :
|
||||
QDataStream(ba),
|
||||
numChannels (numChannels),
|
||||
initialPos (device()->pos()),
|
||||
initialByteOrder (byteOrder())
|
||||
{
|
||||
waveStreamHeaders();
|
||||
}
|
||||
|
||||
void CWaveStream::waveStreamHeaders()
|
||||
{
|
||||
static const HdrRiff scHdrRiff;
|
||||
const FmtSubChunk cFmtSubChunk (numChannels);
|
||||
static const DataSubChunkHdr scDataSubChunkHdr;
|
||||
|
||||
setByteOrder(LittleEndian);
|
||||
*this << scHdrRiff << cFmtSubChunk << scDataSubChunkHdr;
|
||||
}
|
||||
|
||||
CWaveStream::~CWaveStream()
|
||||
{
|
||||
static const uint32_t hdrRiffChunkSizeOffset = sizeof(uint32_t);
|
||||
static const uint32_t dataSubChunkHdrChunkSizeOffset = sizeof(HdrRiff) + sizeof(FmtSubChunk) + sizeof (uint32_t) + sizeof (uint32_t);
|
||||
|
||||
const int64_t currentPos = this->device()->pos();
|
||||
const uint32_t fileLength = static_cast<uint32_t>(currentPos - initialPos);
|
||||
|
||||
QDataStream& out = static_cast<QDataStream&>(*this);
|
||||
|
||||
// Overwrite hdr_riff.chunkSize
|
||||
this->device()->seek(initialPos + hdrRiffChunkSizeOffset);
|
||||
out << static_cast<uint32_t>(fileLength - (hdrRiffChunkSizeOffset + sizeof (uint32_t)));
|
||||
|
||||
// Overwrite dataSubChunkHdr.chunkSize
|
||||
this->device()->seek(initialPos + dataSubChunkHdrChunkSizeOffset);
|
||||
out << static_cast<uint32_t>(fileLength - (dataSubChunkHdrChunkSizeOffset + sizeof (uint32_t)));
|
||||
|
||||
// and then restore the position and byte order
|
||||
this->device()->seek(currentPos);
|
||||
setByteOrder(initialByteOrder);
|
||||
}
|
92
src/recorder/cwavestream.h
Executable file
92
src/recorder/cwavestream.h
Executable file
|
@ -0,0 +1,92 @@
|
|||
/******************************************************************************\
|
||||
*
|
||||
* Author(s):
|
||||
* pljones
|
||||
*
|
||||
******************************************************************************
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#ifndef CWAVESTREAM_H
|
||||
#define CWAVESTREAM_H
|
||||
|
||||
#include <QDataStream>
|
||||
|
||||
namespace recorder {
|
||||
|
||||
class HdrRiff
|
||||
{
|
||||
public:
|
||||
HdrRiff() {}
|
||||
|
||||
static const uint32_t chunkId = 0x46464952; // RIFF
|
||||
static const uint32_t chunkSize = 0xffffffff; // (will be overwritten) Size of file in bytes - 8 = size of data + 36
|
||||
static const uint32_t format = 0x45564157; // WAVE
|
||||
};
|
||||
|
||||
class FmtSubChunk
|
||||
{
|
||||
public:
|
||||
FmtSubChunk(const uint16_t _numChannels) :
|
||||
numChannels (_numChannels)
|
||||
, byteRate (sampleRate * numChannels * bitsPerSample/8)
|
||||
, blockAlign (numChannels * bitsPerSample/8)
|
||||
{
|
||||
}
|
||||
static const uint32_t chunkId = 0x20746d66; // "fmt "
|
||||
static const uint32_t chunkSize = 16; // bytes in fmtSubChunk after chunkSize
|
||||
static const uint16_t audioFormat = 1; // PCM
|
||||
const uint16_t numChannels; // 1 for mono, 2 for joy... uh, stereo
|
||||
static const uint32_t sampleRate = 48000; // because it's Jamulus
|
||||
const uint32_t byteRate; // sampleRate * numChannels * bitsPerSample/8
|
||||
const uint16_t blockAlign; // numChannels * bitsPerSample/8
|
||||
static const uint16_t bitsPerSample = 16;
|
||||
};
|
||||
|
||||
class DataSubChunkHdr
|
||||
{
|
||||
public:
|
||||
DataSubChunkHdr() {}
|
||||
|
||||
static const uint32_t chunkId = 0x61746164; // "data"
|
||||
static const uint32_t chunkSize = 0xffffffff; // (will be overwritten) Size of data
|
||||
};
|
||||
|
||||
class CWaveStream : public QDataStream
|
||||
{
|
||||
public:
|
||||
CWaveStream(const uint16_t numChannels);
|
||||
explicit CWaveStream(QIODevice *iod, const uint16_t numChannels);
|
||||
CWaveStream(QByteArray *iod, QIODevice::OpenMode flags, const uint16_t numChannels);
|
||||
CWaveStream(const QByteArray &ba, const uint16_t numChannels);
|
||||
~CWaveStream();
|
||||
|
||||
private:
|
||||
void waveStreamHeaders();
|
||||
|
||||
const uint16_t numChannels;
|
||||
const int64_t initialPos;
|
||||
const ByteOrder initialByteOrder;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
recorder::CWaveStream& operator<<(recorder::CWaveStream& out, recorder::HdrRiff& hdrRiff);
|
||||
recorder::CWaveStream& operator<<(recorder::CWaveStream& out, recorder::FmtSubChunk& fmtSubChunk);
|
||||
recorder::CWaveStream& operator<<(recorder::CWaveStream& out, recorder::DataSubChunkHdr& dataSubChunkHdr);
|
||||
|
||||
#endif // CWAVESTREAM_H
|
438
src/recorder/jamrecorder.cpp
Executable file
438
src/recorder/jamrecorder.cpp
Executable file
|
@ -0,0 +1,438 @@
|
|||
/******************************************************************************\
|
||||
*
|
||||
* Author(s):
|
||||
* pljones
|
||||
*
|
||||
******************************************************************************
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#include "jamrecorder.h"
|
||||
|
||||
using namespace recorder;
|
||||
|
||||
/* ********************************************************************************************************
|
||||
* CJamClient
|
||||
* ********************************************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief CJamClient::CJamClient
|
||||
* @param frame Start frame of the client within the session
|
||||
* @param numChannels 1 for mono, 2 for stereo
|
||||
* @param name The client's current name
|
||||
* @param address IP and Port
|
||||
* @param recordBaseDir Session recording directory
|
||||
*
|
||||
* Creates a file for the raw PCM data and sets up a QDataStream to which to write received frames.
|
||||
* The data is stored Little Endian.
|
||||
*/
|
||||
CJamClient::CJamClient(const qint64 frame, const int _numChannels, const QString name, const CHostAddress address, const QDir recordBaseDir) :
|
||||
startFrame (frame),
|
||||
numChannels (static_cast<uint16_t>(_numChannels)),
|
||||
name (name),
|
||||
address (address)
|
||||
{
|
||||
// At this point we may not have much of a name
|
||||
QString fileName = ClientName() + "-" + QString::number(frame) + "-" + QString::number(_numChannels);
|
||||
QString affix = "";
|
||||
while (recordBaseDir.exists(fileName + affix + ".wav"))
|
||||
{
|
||||
affix = affix.length() == 0 ? "_1" : "_" + QString::number(affix.remove(0, 1).toInt() + 1);
|
||||
}
|
||||
fileName = fileName + affix + ".wav";
|
||||
|
||||
wavFile = new QFile(recordBaseDir.absoluteFilePath(fileName));
|
||||
if (!wavFile->open(QFile::OpenMode(QIODevice::OpenModeFlag::ReadWrite))) // need to allow rewriting headers
|
||||
{
|
||||
throw new std::runtime_error( ("Could not write to WAV file " + wavFile->fileName()).toStdString() );
|
||||
}
|
||||
out = new CWaveStream(wavFile, numChannels);
|
||||
|
||||
filename = wavFile->fileName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamClient::Frame Handle a frame of PCM data from a client connected to the server
|
||||
* @param _name The client's current name
|
||||
* @param pcm The PCM data
|
||||
*/
|
||||
void CJamClient::Frame(const QString _name, const CVector<int16_t>& pcm)
|
||||
{
|
||||
name = _name;
|
||||
|
||||
for(int i = 0; i < numChannels * SYSTEM_FRAME_SIZE_SAMPLES; i++)
|
||||
{
|
||||
*out << pcm[i];
|
||||
}
|
||||
|
||||
frameCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamClient::Disconnect Clean up after a disconnected client
|
||||
*/
|
||||
void CJamClient::Disconnect()
|
||||
{
|
||||
delete out;
|
||||
out = nullptr;
|
||||
|
||||
wavFile->close();
|
||||
|
||||
delete wavFile;
|
||||
wavFile = nullptr;
|
||||
}
|
||||
|
||||
/* ********************************************************************************************************
|
||||
* CJamSession
|
||||
* ********************************************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief CJamSession::CJamSession Construct a new jam recording session
|
||||
* @param recordBaseDir The recording base directory
|
||||
*
|
||||
* Each session is stored into its own subdirectory of the recording base directory.
|
||||
*/
|
||||
CJamSession::CJamSession(QDir recordBaseDir) :
|
||||
sessionDir (QDir(recordBaseDir.absoluteFilePath("Jam-" + QDateTime().currentDateTimeUtc().toString("yyyyMMdd-HHmmsszzz")))),
|
||||
currentFrame (0),
|
||||
vecptrJamClients (MAX_NUM_CHANNELS),
|
||||
jamClientConnections()
|
||||
{
|
||||
const QFileInfo fi(sessionDir.absolutePath());
|
||||
|
||||
if (!fi.exists() && !QDir().mkpath(sessionDir.absolutePath()))
|
||||
{
|
||||
throw std::runtime_error( (sessionDir.absolutePath() + " does not exist but could not be created").toStdString() );
|
||||
}
|
||||
if (!fi.isDir())
|
||||
{
|
||||
throw std::runtime_error( (sessionDir.absolutePath() + " exists but is not a directory").toStdString() );
|
||||
}
|
||||
if (!fi.isWritable())
|
||||
{
|
||||
throw std::runtime_error( (sessionDir.absolutePath() + " is a directory but cannot be written to").toStdString() );
|
||||
}
|
||||
|
||||
// Explicitly set all the pointers to "empty"
|
||||
vecptrJamClients.fill(nullptr);
|
||||
|
||||
currentFrame = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamSession::DisconnectClient Capture details of the departing client's connection
|
||||
* @param iChID the channel id of the client that disconnected
|
||||
*/
|
||||
void CJamSession::DisconnectClient(int iChID)
|
||||
{
|
||||
vecptrJamClients[iChID]->Disconnect();
|
||||
|
||||
jamClientConnections.append(new CJamClientConnection(vecptrJamClients[iChID]->NumAudioChannels(),
|
||||
vecptrJamClients[iChID]->StartFrame(),
|
||||
vecptrJamClients[iChID]->FrameCount(),
|
||||
vecptrJamClients[iChID]->ClientName(),
|
||||
vecptrJamClients[iChID]->FileName()));
|
||||
|
||||
delete vecptrJamClients[iChID];
|
||||
vecptrJamClients[iChID] = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamSession::Frame Process a frame emited for a client by the server
|
||||
* @param iChID the client channel id
|
||||
* @param name the client name
|
||||
* @param address the client IP and port number
|
||||
* @param numAudioChannels the client number of audio channels
|
||||
* @param data the frame data
|
||||
*
|
||||
* Manages changes that affect how the recording is stored - i.e. if the number of audio channels changes, we need a new file.
|
||||
* Files are grouped by IP and port number, so if either of those change for a connection, we also start a new file.
|
||||
*
|
||||
* Also manages the overall current frame counter for the session.
|
||||
*/
|
||||
void CJamSession::Frame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector<int16_t> data)
|
||||
{
|
||||
if (vecptrJamClients[iChID] == nullptr)
|
||||
{
|
||||
// then we have not seen this client this session
|
||||
vecptrJamClients[iChID] = new CJamClient(currentFrame, numAudioChannels, name, address, sessionDir);
|
||||
}
|
||||
else if (numAudioChannels != vecptrJamClients[iChID]->NumAudioChannels()
|
||||
|| address.InetAddr != vecptrJamClients[iChID]->ClientAddress().InetAddr
|
||||
|| address.iPort != vecptrJamClients[iChID]->ClientAddress().iPort)
|
||||
{
|
||||
DisconnectClient(iChID);
|
||||
if (numAudioChannels == 0)
|
||||
{
|
||||
vecptrJamClients[iChID] = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
vecptrJamClients[iChID] = new CJamClient(currentFrame, numAudioChannels, name, address, sessionDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (vecptrJamClients[iChID] == nullptr)
|
||||
{
|
||||
// Frame allegedly from iChID but unable to establish client details
|
||||
return;
|
||||
}
|
||||
|
||||
vecptrJamClients[iChID]->Frame(name, data);
|
||||
|
||||
// If _any_ connected client frame steps past currentFrame, increase currentFrame
|
||||
if (vecptrJamClients[iChID]->StartFrame() + vecptrJamClients[iChID]->FrameCount() > currentFrame)
|
||||
{
|
||||
currentFrame++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamSession::End Clean up any "hanging" clients when the server thinks they all left
|
||||
*/
|
||||
void CJamSession::End()
|
||||
{
|
||||
for (int iChID = 0; iChID < vecptrJamClients.size(); iChID++)
|
||||
{
|
||||
if (vecptrJamClients[iChID] != nullptr)
|
||||
{
|
||||
DisconnectClient(iChID);
|
||||
vecptrJamClients[iChID] = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamSession::Tracks Retrieve a map of (latest) client name to connection items
|
||||
* @return a map of (latest) client name to connection items
|
||||
*/
|
||||
QMap<QString, QList<STrackItem>> CJamSession::Tracks()
|
||||
{
|
||||
QMap<QString, QList<STrackItem>> tracks;
|
||||
|
||||
for (int i = 0; i < jamClientConnections.count(); i++ )
|
||||
{
|
||||
STrackItem track (
|
||||
jamClientConnections[i]->Format(),
|
||||
jamClientConnections[i]->StartFrame(),
|
||||
jamClientConnections[i]->Length(),
|
||||
jamClientConnections[i]->FileName()
|
||||
);
|
||||
|
||||
if (!tracks.contains(jamClientConnections[i]->Name()))
|
||||
{
|
||||
tracks.insert(jamClientConnections[i]->Name(), { });
|
||||
}
|
||||
|
||||
tracks[jamClientConnections[i]->Name()].append(track);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamSession::TracksFromSessionDir Replica of CJamSession::Tracks but using the directory contents to construct the track item map
|
||||
* @param sessionDirName the directory name to scan
|
||||
* @return a map of (latest) client name to connection items
|
||||
*/
|
||||
QMap<QString, QList<STrackItem>> CJamSession::TracksFromSessionDir(const QString& sessionDirName)
|
||||
{
|
||||
QMap<QString, QList<STrackItem>> tracks;
|
||||
|
||||
const QDir sessionDir(sessionDirName);
|
||||
foreach(auto entry, sessionDir.entryList({ "*.pcm" }))
|
||||
{
|
||||
|
||||
auto split = entry.split(".")[0].split("-");
|
||||
QString name = split[0];
|
||||
QString hostPort = split[1];
|
||||
QString frame = split[2];
|
||||
QString tail = split[3]; //numChannels may have _nnn
|
||||
QString numChannels = tail.count("_") > 0 ? tail.split("_")[0] : tail;
|
||||
|
||||
QString trackName = name + "-" + hostPort;
|
||||
if (!tracks.contains(trackName))
|
||||
{
|
||||
tracks.insert(trackName, { });
|
||||
}
|
||||
|
||||
QFileInfo fiEntry(sessionDir.absoluteFilePath(entry));
|
||||
qint64 length = fiEntry.size() / numChannels.toInt() / SYSTEM_FRAME_SIZE_SAMPLES;
|
||||
|
||||
STrackItem track (
|
||||
numChannels.toInt(),
|
||||
frame.toLongLong(),
|
||||
length,
|
||||
sessionDir.absoluteFilePath(entry)
|
||||
);
|
||||
|
||||
tracks[trackName].append(track);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/* ********************************************************************************************************
|
||||
* CJamRecorder
|
||||
* ********************************************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief CJamRecorder::Init Create recording directory, if necessary, and connect signal handlers
|
||||
* @param server Server object emiting signals
|
||||
*/
|
||||
void CJamRecorder::Init(const CServer* server)
|
||||
{
|
||||
const QFileInfo fi(recordBaseDir.absolutePath());
|
||||
|
||||
if (!fi.exists() && !QDir().mkpath(recordBaseDir.absolutePath()))
|
||||
{
|
||||
throw std::runtime_error( (recordBaseDir.absolutePath() + " does not exist but could not be created").toStdString() );
|
||||
}
|
||||
if (!fi.isDir())
|
||||
{
|
||||
throw std::runtime_error( (recordBaseDir.absolutePath() + " exists but is not a directory").toStdString() );
|
||||
}
|
||||
if (!fi.isWritable())
|
||||
{
|
||||
throw std::runtime_error( (recordBaseDir.absolutePath() + " is a directory but cannot be written to").toStdString() );
|
||||
}
|
||||
|
||||
QObject::connect((const QObject *)server, SIGNAL ( Stopped() ),
|
||||
this, SLOT( OnEnd() ),
|
||||
Qt::ConnectionType::QueuedConnection);
|
||||
|
||||
QObject::connect((const QObject *)server, SIGNAL ( ClientDisconnected(int) ),
|
||||
this, SLOT( OnDisconnected(int) ),
|
||||
Qt::ConnectionType::QueuedConnection);
|
||||
|
||||
qRegisterMetaType<CVector<int16_t>>();
|
||||
QObject::connect((const QObject *)server, SIGNAL ( AudioFrame(const int, const QString, const CHostAddress, const int, const CVector<int16_t>) ),
|
||||
this, SLOT( OnFrame(const int, const QString, const CHostAddress, const int, const CVector<int16_t>) ),
|
||||
Qt::ConnectionType::QueuedConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamRecorder::OnStart Start up tasks when the first client connects
|
||||
*/
|
||||
void CJamRecorder::OnStart() {
|
||||
// Ensure any previous cleaning up has been done.
|
||||
OnEnd();
|
||||
|
||||
currentSession = new CJamSession(recordBaseDir);
|
||||
isRecording = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamRecorder::OnEnd Finalise the recording and emit the Reaper RPP file
|
||||
*/
|
||||
void CJamRecorder::OnEnd()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
isRecording = false;
|
||||
currentSession->End();
|
||||
|
||||
QString reaperProjectFileName = currentSession->SessionDir().filePath(currentSession->Name().append(".rpp"));
|
||||
const QFileInfo fi(reaperProjectFileName);
|
||||
|
||||
if (fi.exists())
|
||||
{
|
||||
tsConsole << "CJamRecorder::OnEnd() - " << fi.absolutePath() << " exists and will not be overwritten." << endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
QFile outf (reaperProjectFileName);
|
||||
outf.open(QFile::WriteOnly);
|
||||
QTextStream out(&outf);
|
||||
out << CReaperProject(currentSession->Tracks()).toString() << endl;
|
||||
tsConsole << "Session RPP: " << reaperProjectFileName << endl;
|
||||
}
|
||||
|
||||
delete currentSession;
|
||||
currentSession = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamRecorder::SessionDirToReaper Replica of CJamRecorder::OnEnd() but using the directory contents to construct the CReaperProject object
|
||||
* @param strSessionDirName
|
||||
*/
|
||||
void CJamRecorder::SessionDirToReaper(QString& strSessionDirName)
|
||||
{
|
||||
const QFileInfo fiSessionDir(QDir::cleanPath(strSessionDirName));
|
||||
if (!fiSessionDir.exists() || !fiSessionDir.isDir())
|
||||
{
|
||||
throw std::runtime_error( (fiSessionDir.absoluteFilePath() + " does not exist or is not a directory. Aborting.").toStdString() );
|
||||
}
|
||||
|
||||
const QDir dSessionDir(fiSessionDir.absoluteFilePath());
|
||||
const QString reaperProjectFileName = dSessionDir.absoluteFilePath(fiSessionDir.baseName().append(".rpp"));
|
||||
const QFileInfo fiRPP(reaperProjectFileName);
|
||||
if (fiRPP.exists())
|
||||
{
|
||||
throw std::runtime_error( (fiRPP.absoluteFilePath() + " exists and will not be overwritten. Aborting.").toStdString() );
|
||||
}
|
||||
|
||||
QFile outf (fiRPP.absoluteFilePath());
|
||||
if (!outf.open(QFile::WriteOnly)) {
|
||||
throw std::runtime_error( (fiRPP.absoluteFilePath() + " could not be written. Aborting.").toStdString() );
|
||||
}
|
||||
QTextStream out(&outf);
|
||||
|
||||
out << CReaperProject(CJamSession::TracksFromSessionDir(fiSessionDir.absoluteFilePath())).toString() << endl;
|
||||
|
||||
(*(new ConsoleWriterFactory())->get()) << "Session RPP: " << reaperProjectFileName << endl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamRecorder::OnDisconnected Handle disconnection of a client
|
||||
* @param iChID the client channel id
|
||||
*/
|
||||
void CJamRecorder::OnDisconnected(int iChID)
|
||||
{
|
||||
if (!isRecording)
|
||||
{
|
||||
tsConsole << "CJamRecorder::OnDisconnected: channel " << iChID << " disconnected but not recording" << endl;
|
||||
}
|
||||
if (currentSession == nullptr)
|
||||
{
|
||||
tsConsole << "CJamRecorder::OnDisconnected: channel " << iChID << " disconnected but no currentSession" << endl;
|
||||
return;
|
||||
}
|
||||
currentSession->DisconnectClient(iChID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CJamRecorder::OnFrame Handle a frame emited for a client by the server
|
||||
* @param iChID the client channel id
|
||||
* @param name the client name
|
||||
* @param address the client IP and port number
|
||||
* @param numAudioChannels the client number of audio channels
|
||||
* @param data the frame data
|
||||
*
|
||||
* Ensures recording has started.
|
||||
*/
|
||||
void CJamRecorder::OnFrame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector<int16_t> data)
|
||||
{
|
||||
// Make sure we are ready
|
||||
if (!isRecording)
|
||||
{
|
||||
OnStart();
|
||||
}
|
||||
|
||||
currentSession->Frame(iChID, name, address, numAudioChannels, data);
|
||||
}
|
181
src/recorder/jamrecorder.h
Executable file
181
src/recorder/jamrecorder.h
Executable file
|
@ -0,0 +1,181 @@
|
|||
/******************************************************************************\
|
||||
*
|
||||
* Author(s):
|
||||
* pljones
|
||||
*
|
||||
******************************************************************************
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 2 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#ifndef JAMRECORDER_H
|
||||
#define JAMRECORDER_H
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "../util.h"
|
||||
#include "../channel.h"
|
||||
|
||||
#include "creaperproject.h"
|
||||
#include "cwavestream.h"
|
||||
|
||||
namespace recorder {
|
||||
|
||||
class CJamClientConnection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CJamClientConnection(const int _numAudioChannels, const qint64 _startFrame, const qint64 _length, const QString _name, const QString _fileName) :
|
||||
numAudioChannels (_numAudioChannels),
|
||||
startFrame (_startFrame),
|
||||
length (_length),
|
||||
name (_name),
|
||||
fileName (_fileName)
|
||||
{
|
||||
}
|
||||
|
||||
int Format() { return numAudioChannels; }
|
||||
qint64 StartFrame() { return startFrame; }
|
||||
qint64 Length() { return length; }
|
||||
QString Name() { return name; }
|
||||
QString FileName() { return fileName; }
|
||||
|
||||
private:
|
||||
const int numAudioChannels;
|
||||
const qint64 startFrame;
|
||||
const qint64 length;
|
||||
const QString name;
|
||||
const QString fileName;
|
||||
};
|
||||
|
||||
class CJamClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CJamClient(const qint64 frame, const int numChannels, const QString name, const CHostAddress address, const QDir recordBaseDir);
|
||||
|
||||
void Frame(const QString name, const CVector<int16_t>& pcm);
|
||||
|
||||
void Disconnect();
|
||||
|
||||
qint64 StartFrame() { return startFrame; }
|
||||
qint64 FrameCount() { return frameCount; }
|
||||
uint16_t NumAudioChannels() { return numChannels; }
|
||||
QString ClientName() { return name.leftJustified(4, '_', false).replace(QRegExp("[-.:/\\ ]"), "_")
|
||||
.append("-")
|
||||
.append(address.toString(CHostAddress::EStringMode::SM_IP_NO_LAST_BYTE_PORT).replace(QRegExp("[-.:/\\ ]"), "_"))
|
||||
;
|
||||
}
|
||||
CHostAddress ClientAddress() { return address; }
|
||||
|
||||
QString FileName() { return filename; }
|
||||
|
||||
private:
|
||||
const qint64 startFrame;
|
||||
const uint16_t numChannels;
|
||||
QString name;
|
||||
const CHostAddress address;
|
||||
|
||||
QString filename;
|
||||
QFile* wavFile;
|
||||
QDataStream* out;
|
||||
qint64 frameCount = 0;
|
||||
};
|
||||
|
||||
class CJamSession : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
CJamSession(QDir recordBaseDir);
|
||||
|
||||
void Frame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector<int16_t> data);
|
||||
|
||||
void End();
|
||||
|
||||
QVector<CJamClient*> Clients() { return vecptrJamClients; }
|
||||
|
||||
QMap<QString, QList<STrackItem>> Tracks();
|
||||
|
||||
QString Name() { return sessionDir.dirName(); }
|
||||
|
||||
const QDir SessionDir() { return sessionDir; }
|
||||
|
||||
void DisconnectClient(int iChID);
|
||||
|
||||
static QMap<QString, QList<STrackItem>> TracksFromSessionDir(const QString& name);
|
||||
|
||||
private:
|
||||
CJamSession();
|
||||
|
||||
const QDir sessionDir;
|
||||
|
||||
qint64 currentFrame;
|
||||
QVector<CJamClient*> vecptrJamClients;
|
||||
QList<CJamClientConnection*> jamClientConnections;
|
||||
};
|
||||
|
||||
class CJamRecorder : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CJamRecorder(const QString recordingDirName) :
|
||||
recordBaseDir (recordingDirName), isRecording (false) {}
|
||||
void Init(const CServer* server);
|
||||
|
||||
static void SessionDirToReaper(QString& strSessionDirName);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Raised when first client joins the server, triggering a new recording.
|
||||
*/
|
||||
void OnStart();
|
||||
|
||||
/**
|
||||
* @brief Raised when last client leaves the server, ending the recording.
|
||||
*/
|
||||
void OnEnd();
|
||||
|
||||
/**
|
||||
* @brief Raised when an existing client leaves the server.
|
||||
* @param iChID channel number of client
|
||||
*/
|
||||
void OnDisconnected(int iChID);
|
||||
|
||||
/**
|
||||
* @brief Raised when a frame of data fis available to process
|
||||
*/
|
||||
void OnFrame(const int iChID, const QString name, const CHostAddress address, const int numAudioChannels, const CVector<int16_t> data);
|
||||
|
||||
private:
|
||||
QDir recordBaseDir;
|
||||
|
||||
bool isRecording;
|
||||
CJamSession* currentSession;
|
||||
QTextStream& tsConsole = *((new ConsoleWriterFactory())->get());
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(int16_t)
|
||||
Q_DECLARE_METATYPE(CVector<int16_t>)
|
||||
#endif // JAMRECORDER_H
|
|
@ -206,11 +206,14 @@ CServer::CServer ( const int iNewMaxNumChan,
|
|||
const QString& strCentralServer,
|
||||
const QString& strServerInfo,
|
||||
const QString& strNewWelcomeMessage,
|
||||
const QString& strRecordingDirName,
|
||||
const bool bNCentServPingServerInList,
|
||||
const bool bNDisconnectAllClients,
|
||||
const ELicenceType eNLicenceType ) :
|
||||
iMaxNumChannels ( iNewMaxNumChan ),
|
||||
Socket ( this, iPortNumber ),
|
||||
JamRecorder ( strRecordingDirName ),
|
||||
bEnableRecording ( !strRecordingDirName.isEmpty() ),
|
||||
bWriteStatusHTMLFile ( false ),
|
||||
ServerListManager ( iPortNumber,
|
||||
strCentralServer,
|
||||
|
@ -357,6 +360,13 @@ CServer::CServer ( const int iNewMaxNumChan,
|
|||
QString().number( static_cast<int> ( iPortNumber ) ) );
|
||||
}
|
||||
|
||||
// Enable jam recording (if requested)
|
||||
if ( bEnableRecording )
|
||||
{
|
||||
JamRecorder.Init ( this );
|
||||
JamRecorder.start();
|
||||
}
|
||||
|
||||
// enable all channels (for the server all channel must be enabled the
|
||||
// entire life time of the software)
|
||||
for ( i = 0; i < iMaxNumChannels; i++ )
|
||||
|
@ -739,8 +749,14 @@ JitterMeas.Measure();
|
|||
|
||||
// if channel was just disconnected, set flag that connected
|
||||
// client list is sent to all other clients
|
||||
// and emit the client disconnected signal
|
||||
if ( eGetStat == GS_CHAN_NOW_DISCONNECTED )
|
||||
{
|
||||
if ( bEnableRecording )
|
||||
{
|
||||
emit ClientDisconnected ( iCurChanID ); // TODO do this outside the mutex lock?
|
||||
}
|
||||
|
||||
bChannelIsNowDisconnected = true;
|
||||
}
|
||||
|
||||
|
@ -825,6 +841,16 @@ JitterMeas.Measure();
|
|||
// get number of audio channels of current channel
|
||||
const int iCurNumAudChan = vecNumAudioChannels[i];
|
||||
|
||||
// export the audio data for recording purpose
|
||||
if ( bEnableRecording )
|
||||
{
|
||||
emit AudioFrame ( iCurChanID,
|
||||
vecChannels[iCurChanID].GetName(),
|
||||
vecChannels[iCurChanID].GetAddress(),
|
||||
iCurNumAudChan,
|
||||
vecvecsData[i] );
|
||||
}
|
||||
|
||||
// generate a sparate mix for each channel
|
||||
// actual processing of audio data -> mix
|
||||
ProcessData ( vecvecsData,
|
||||
|
|
12
src/server.h
12
src/server.h
|
@ -40,6 +40,7 @@
|
|||
#include "util.h"
|
||||
#include "serverlogging.h"
|
||||
#include "serverlist.h"
|
||||
#include "recorder/jamrecorder.h"
|
||||
|
||||
|
||||
/* Definitions ****************************************************************/
|
||||
|
@ -127,6 +128,7 @@ public:
|
|||
const QString& strCentralServer,
|
||||
const QString& strServerInfo,
|
||||
const QString& strNewWelcomeMessage,
|
||||
const QString& strRecordingDirName,
|
||||
const bool bNCentServPingServerInList,
|
||||
const bool bNDisconnectAllClients,
|
||||
const ELicenceType eNLicenceType );
|
||||
|
@ -247,6 +249,10 @@ protected:
|
|||
// logging
|
||||
CServerLogging Logging;
|
||||
|
||||
// recording thread
|
||||
recorder::CJamRecorder JamRecorder;
|
||||
bool bEnableRecording;
|
||||
|
||||
// HTML file server status
|
||||
bool bWriteStatusHTMLFile;
|
||||
QString strServerHTMLFileListName;
|
||||
|
@ -268,6 +274,12 @@ protected:
|
|||
signals:
|
||||
void Started();
|
||||
void Stopped();
|
||||
void ClientDisconnected ( const int iChID );
|
||||
void AudioFrame ( const int iChID,
|
||||
const QString stChName,
|
||||
const CHostAddress RecHostAddr,
|
||||
const int iNumAudChan,
|
||||
const CVector<int16_t> vecsData );
|
||||
|
||||
public slots:
|
||||
void OnTimer();
|
||||
|
|
|
@ -97,4 +97,7 @@ public slots:
|
|||
void OnSysTrayMenuHide() { hide(); }
|
||||
void OnSysTrayMenuExit() { close(); }
|
||||
void OnSysTrayActivated ( QSystemTrayIcon::ActivationReason ActReason );
|
||||
|
||||
void keyPressEvent ( QKeyEvent *e ) // block escape key
|
||||
{ if ( e->key() != Qt::Key_Escape ) QDialog::keyPressEvent ( e ); }
|
||||
};
|
||||
|
|
26
src/util.cpp
26
src/util.cpp
|
@ -385,6 +385,7 @@ CAboutDlg::CAboutDlg ( QWidget* parent ) : QDialog ( parent )
|
|||
"<i><a href=""http://openclipart.org"">http://openclipart.org</a></i></li>"
|
||||
"<li>Country flag icons from Mark James: "
|
||||
"<i><a href=""http://www.famfamfam.com"">http://www.famfamfam.com</a></i></li>"
|
||||
"<li>Server wave recording, coded by pljones</li>"
|
||||
"</ul>"
|
||||
"</center><br>");
|
||||
|
||||
|
@ -1209,6 +1210,31 @@ QString CCountyFlagIcons::GetResourceReference ( const QLocale::Country eCountry
|
|||
}
|
||||
|
||||
|
||||
// Console writer factory ------------------------------------------------------
|
||||
QTextStream* ConsoleWriterFactory::get()
|
||||
{
|
||||
if ( ptsConsole == nullptr )
|
||||
{
|
||||
#if _WIN32
|
||||
if ( !AttachConsole ( ATTACH_PARENT_PROCESS ) )
|
||||
{
|
||||
// Not run from console, dump logging to nowhere
|
||||
static QString conout;
|
||||
ptsConsole = new QTextStream ( &conout );
|
||||
}
|
||||
else
|
||||
{
|
||||
freopen ( "CONOUT$", "w", stdout );
|
||||
ptsConsole = new QTextStream ( stdout );
|
||||
}
|
||||
#else
|
||||
ptsConsole = new QTextStream ( stdout );
|
||||
#endif
|
||||
}
|
||||
return ptsConsole;
|
||||
}
|
||||
|
||||
|
||||
/******************************************************************************\
|
||||
* Global Functions Implementation *
|
||||
\******************************************************************************/
|
||||
|
|
13
src/util.h
13
src/util.h
|
@ -482,6 +482,19 @@ public slots:
|
|||
};
|
||||
|
||||
|
||||
// Console writer factory ------------------------------------------------------
|
||||
// this class was written by pljones
|
||||
class ConsoleWriterFactory
|
||||
{
|
||||
public:
|
||||
ConsoleWriterFactory() : ptsConsole ( nullptr ) { }
|
||||
QTextStream* get();
|
||||
|
||||
private:
|
||||
QTextStream* ptsConsole;
|
||||
};
|
||||
|
||||
|
||||
/******************************************************************************\
|
||||
* Other Classes/Enums *
|
||||
\******************************************************************************/
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
*
|
||||
\******************************************************************************/
|
||||
|
||||
#include "Sound.h"
|
||||
#include "sound.h"
|
||||
|
||||
|
||||
/* Implementation *************************************************************/
|
||||
|
|
Loading…
Reference in a new issue