patterncppMinor
Raw PCM to FLAC conversion using QtGstreamer in C++
Viewed 0 times
conversionflacrawusingqtgstreamerpcm
Problem
This is part of my kynnaugh-cc TeamSpeak 3 plugin implementing speech recognition for deaf/hearing-impaired users. One of the self-contained classes in my code has the job of converting incoming PCM samples from TeamSpeak into FLAC format for transmission to the Google Cloud Speech API. The Cloud Speech API only accepts input in WAV or FLAC format, so obviously I prefer FLAC due to its superior compression (these files will be streamed in real time to Google's servers from an end-user's computer, so bandwidth is a concern).
The general mode of operation of this code from a caller's perspective is:
The general mode of operation of this code from a caller's perspective is:
- Call the constructor (from any thread).
- When you have some samples of raw PCM (which, by contract, must be Signed 16-bit Little Endian, interleaved, at 48000 Hz sample rate, and either 1 or 2 channels), call
convert::convertRawToFlac().
convertRawToFlac()will block until it completes the conversion, and then return.
- The
retvalmember of yourconvertinstance will have aQByteArrayfull of the FLAC-encoded bytes for you to pick up and carry on to the next part of the program.
- If you later on get more samples of PCM, just call the
convertRawToFlac()function again; you don't have to create a new instance ofconvert.
- The QtGstreamer and Qt stuff I use inside this class has "thread affinity" (they care which thread you call them on), but I actively expect
convertRawToFlac()to be called from any arbitrary thread, not necessarily the same thread every time. For this reason, I spawn and re-use the same thread each time for all of the GStreamer stuff, which prevents me from having to teardown theGstPipelinewith each call. It stays bound to the same mainloop thread.
- At a higher level architecturally (with code not visible directly in this question), each user (a real-life person with a microphone in a TeamSpeak 3 VOIP channel and something to say) will get their own
convertinstance. This allows us to use more than one CPU core to simu
Solution
Global namespace pollution
Don't do this in a header:
This will add many, many names to the global namespace of every source file that includes this header. It's much less harmful to import vast numbers of names in your implementation file (though I still recommend using just the names you need, in the smallest reasonable scope).
Includes
`
Don't do this in a header:
using namespace QGst;
using namespace QGst::Utils;This will add many, many names to the global namespace of every source file that includes this header. It's much less harmful to import vast numbers of names in your implementation file (though I still recommend using just the names you need, in the smallest reasonable scope).
Includes
`
is a big include, and will hit your compilation speed (again, for every translation unit that needs this header). Don't drag all of that in; include just what you need, and forward-declare what you can:
#include
#include
#include
#include
#include
#include
class QIODevice;
namespace QGst {
namespace Utils {
class ApplicationSource;
class ApplicationSink;
}
}
Naming
Qt naming convention is to use PascalCase nouns for class names:
class Converter
That's still very broad; consider alternatives such as Downsampler or SpeechCompressor.
Other identifiers I'd suggest changing:
retval -> flacData
dat -> pcmData
channes -> channels
The debugging macros can be written with the do while(0) idiom so that they can be used like statements:
#define SP() do { qDebug() << "convert::setupPipeline()" << i++; } while (0)
#define TS() do { qDebug() << "convert::threadStart()" << i++; } while (0)
#define CRTF() do { qDebug() << "convert::convertRawToFlac()" << i++; } while (0)
That said, I don't really like them at all - macros are always a bit suspect, and these touch a variable called i which is extremely likely to collide with the code. Also, I don't think this code needs to be quite so chatty.
Initialization
We don't need these:
bool convert::inited = false;
QMutex convert::initLock;
Instead, we can make the language work for us with an immediately-invoked initializer expression for a static local:
convert::convert(QObject *parent)
: QObject(parent),
thread(this),
lock(QReadWriteLock::Recursive),
data(NULL),
channels(0)
{
static const bool inited = []{
QGst::init();
qDebug() << "GStreamer has been inited in convert constructor.";
return true;
}();
(void)inited; // suppress "unused variable" warning
When you do need a lock, prefer to use a scoped lock-guard, rather than manual lock() and unlock(). It's much easier to ensure that locks and unlocks are balanced that way, without having to examine all execution paths.
Signal connection
I don't think you want a direct connection here:
connect(&thread, &QThread::started, this, &convert::threadStart, Qt::DirectConnection);
It defeats the point of having a thread if we execute this code directly in the owning thread. We should let that just default, so that threadStart() is executed in convert's thread:
connect(&thread, &QThread::started, this, &convert::threadStart);
Tricky timing
This code is suspect:
thread.start(QThread::NormalPriority);
this->moveToThread(&thread);
We want to move this object to the thread before we start the thread - remember that there's a signal emitted by thread start that's connected to this object. Yes, Qt won't deliver it until it gets back into the correct event loop, but why write code that's so hard to reason about when we can simply re-order these two lines?
Useless use of thread
Here, we do nothing while we wait for the thread we just started:
thread.start(QThread::NormalPriority);
this->moveToThread(&thread);
CRTF();
QThread::yieldCurrentThread();
CRTF();
thread.wait(); //TODO: Error handling
So there really was no point running that in its own thread, after all.
Magic numbers
appsrc->setMaxBytes(1073741824);
What's the significance of that number? It looks like 1 << 30 but it's hard to tell at a glance. Regardless, it's surprisingly big for 48kHz 16-bit input - over three hours' worth, even in stereo. It would be better to give it a good name near the start of the file.
Memory allocation
We prefer new/new[] and delete/delete[] over malloc() in C++ code. And we prefer self-reclaiming allocations using smart pointers over either of those:
if(sz > 0)
{
auto const raw = std::make_unique(sz);
bp->extract(0, raw.get(), sz);
retval.append(raw.get(), sz);
}
(you'll want to catch std::bad_alloc outside of the loop, I think).
I don't think you need a buffer for this at all, though - it should be straightforward to resize retval and extract directly to its data()` buffer without copying.Code Snippets
using namespace QGst;
using namespace QGst::Utils;#include <QByteArray>
#include <QMutex>
#include <QObject>
#include <QReadWriteLock>
#include <QThread>
#include <QGst/Element>
class QIODevice;
namespace QGst {
namespace Utils {
class ApplicationSource;
class ApplicationSink;
}
}class Converter#define SP() do { qDebug() << "convert::setupPipeline()" << i++; } while (0)
#define TS() do { qDebug() << "convert::threadStart()" << i++; } while (0)
#define CRTF() do { qDebug() << "convert::convertRawToFlac()" << i++; } while (0)bool convert::inited = false;
QMutex convert::initLock;Context
StackExchange Code Review Q#149902, answer score: 2
Revisions (0)
No revisions yet.