Skip to content

Commit

Permalink
Fix multi-track recordings going out of alignment on long recordings
Browse files Browse the repository at this point in the history
Previously the wall clock was used to align speakers. As this didn't
allow for reliable detection of silence between outputs a heuristic
was used which only started adding silence after 100ms.

This patch makes the mixer keep track of the absolute number of samples
requested from it. This number is then forwarded to the recorder with
each addBuffer to allow for sample precise alignment.

To get the absolute sample number when starting the recording a thread-safe
way to retrieve this counter from the outside was added using a new
mixer lock.
  • Loading branch information
hacst committed Feb 13, 2014
1 parent afa6ee4 commit 9a47e05
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 44 deletions.
53 changes: 36 additions & 17 deletions src/mumble/AudioOutput.cpp
Expand Up @@ -100,18 +100,26 @@ bool AudioOutputRegistrar::canExclusive() const {
return false; return false;
} }


AudioOutput::AudioOutput() { AudioOutput::AudioOutput()
iFrameSize = SAMPLE_RATE / 100; : fSpeakers(NULL)
bRunning = true; , fSpeakerVolume(NULL)

, bSpeakerPositional(NULL)
iChannels = 0;
fSpeakers = NULL; , mixedSampleCount(0)
fSpeakerVolume = NULL; , mixingMutex()
bSpeakerPositional = NULL; , eSampleFormat(SampleFloat)


iMixerFreq = 0; , bRunning(true)
eSampleFormat = SampleFloat;
iSampleSize = 0; , iFrameSize(SAMPLE_RATE / 100)
, iMixerFreq(0)
, iChannels(0)
, iSampleSize(0)

, qrwlOutputs()
, qmOutputs() {

// Nothing
} }


AudioOutput::~AudioOutput() { AudioOutput::~AudioOutput() {
Expand Down Expand Up @@ -356,11 +364,15 @@ void AudioOutput::initializeMixer(const unsigned int *chanmasks, bool forceheadp
} }


bool AudioOutput::mix(void *outbuff, unsigned int nsamp) { bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {
QMutexLocker mixingMutexLocker(&mixingMutex);

QList<AudioOutputUser *> qlMix; QList<AudioOutputUser *> qlMix;
QList<AudioOutputUser *> qlDel; QList<AudioOutputUser *> qlDel;


if (g.s.fVolume < 0.01f) if (g.s.fVolume < 0.01f) {
mixedSampleCount += nsamp;
return false; return false;
}


const float adjustFactor = std::pow(10, -18.f / 20); const float adjustFactor = std::pow(10, -18.f / 20);
const float mul = g.s.fVolume; const float mul = g.s.fVolume;
Expand Down Expand Up @@ -502,7 +514,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {


if (!recorder->getMixDown()) { if (!recorder->getMixDown()) {
if (aos) { if (aos) {
recorder->addBuffer(aos->p, recbuff, nsamp); recorder->addBuffer(aos->p, recbuff, nsamp, mixedSampleCount);
} else { } else {
// this should be unreachable // this should be unreachable
Q_ASSERT(false); Q_ASSERT(false);
Expand Down Expand Up @@ -560,7 +572,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {
} }


if (recorder && recorder->getMixDown()) { if (recorder && recorder->getMixDown()) {
recorder->addBuffer(NULL, recbuff, nsamp); recorder->addBuffer(NULL, recbuff, nsamp, mixedSampleCount);
} }


// Clip // Clip
Expand All @@ -576,7 +588,9 @@ bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {


foreach(AudioOutputUser *aop, qlDel) foreach(AudioOutputUser *aop, qlDel)
removeBuffer(aop); removeBuffer(aop);


mixedSampleCount += nsamp;

return (! qlMix.isEmpty()); return (! qlMix.isEmpty());
} }


Expand All @@ -587,3 +601,8 @@ bool AudioOutput::isAlive() const {
unsigned int AudioOutput::getMixerFreq() const { unsigned int AudioOutput::getMixerFreq() const {
return iMixerFreq; return iMixerFreq;
} }

quint64 AudioOutput::getMixedSampleCount() const {
QMutexLocker mixingMutexLocker(&mixingMutex);
return mixedSampleCount;
}
6 changes: 6 additions & 0 deletions src/mumble/AudioOutput.h
Expand Up @@ -102,6 +102,10 @@ class AudioOutput : public QThread {
float *fSpeakers; float *fSpeakers;
float *fSpeakerVolume; float *fSpeakerVolume;
bool *bSpeakerPositional; bool *bSpeakerPositional;
/// Total number of samples mixed by this thread
quint64 mixedSampleCount;
/// Locked while mixing to protect mixedSampleCount
mutable QMutex mixingMutex;
protected: protected:
enum { SampleShort, SampleFloat } eSampleFormat; enum { SampleShort, SampleFloat } eSampleFormat;
volatile bool bRunning; volatile bool bRunning;
Expand Down Expand Up @@ -129,6 +133,8 @@ class AudioOutput : public QThread {
const float *getSpeakerPos(unsigned int &nspeakers); const float *getSpeakerPos(unsigned int &nspeakers);
static float calcGain(float dotproduct, float distance); static float calcGain(float dotproduct, float distance);
unsigned int getMixerFreq() const; unsigned int getMixerFreq() const;
/// Returns the total number of samples mixed by this output thread
quint64 getMixedSampleCount() const;
}; };


#endif #endif
64 changes: 45 additions & 19 deletions src/mumble/VoiceRecorder.cpp
Expand Up @@ -41,12 +41,23 @@


#include "../Timer.h" #include "../Timer.h"


VoiceRecorder::RecordBuffer::RecordBuffer(const ClientUser *cu, VoiceRecorder::RecordBuffer::RecordBuffer(
boost::shared_array<float> buffer, int samples, quint64 timestamp) : const ClientUser *cu,
cuUser(cu), fBuffer(buffer), iSamples(samples), uiTimestamp(timestamp) { boost::shared_array<float> buffer,
int samples,
quint64 absoluteStartSample)

: cuUser(cu)
, fBuffer(buffer)
, iSamples(samples)
, absoluteStartSample(absoluteStartSample) {

// Nothing
} }


VoiceRecorder::RecordInfo::RecordInfo() : sf(NULL), uiLastPosition(0) { VoiceRecorder::RecordInfo::RecordInfo(quint64 lastWrittenAbsoluteSample)
: sf(NULL)
, lastWrittenAbsoluteSample(lastWrittenAbsoluteSample) {
} }


VoiceRecorder::RecordInfo::~RecordInfo() { VoiceRecorder::RecordInfo::~RecordInfo() {
Expand All @@ -56,9 +67,17 @@ VoiceRecorder::RecordInfo::~RecordInfo() {
} }
} }


VoiceRecorder::VoiceRecorder(QObject *p) : QThread(p), recordUser(new RecordUser()), VoiceRecorder::VoiceRecorder(QObject *p)
tTimestamp(new Timer()), iSampleRate(0), bRecording(false), bMixDown(false), : QThread(p)
fmFormat(VoiceRecorderFormat::WAV), qdtRecordingStart(QDateTime::currentDateTime()) { , recordUser(new RecordUser())
, tTimestamp(new Timer())
, iSampleRate(0)
, bRecording(false)
, bMixDown(false)
, fmFormat(VoiceRecorderFormat::WAV)
, qdtRecordingStart(QDateTime::currentDateTime()) {

// Nothing
} }


VoiceRecorder::~VoiceRecorder() { VoiceRecorder::~VoiceRecorder() {
Expand Down Expand Up @@ -260,7 +279,7 @@ void VoiceRecorder::run() {


// Create the file for this RecordInfo instance if it's not yet open. // Create the file for this RecordInfo instance if it's not yet open.
boost::shared_ptr<RecordInfo> ri = qhRecordInfo.value(index); boost::shared_ptr<RecordInfo> ri = qhRecordInfo.value(index);
if (!ri->sf) { if (ri->sf == NULL) {
QString filename = expandTemplateVariables(qsFileName, rb); QString filename = expandTemplateVariables(qsFileName, rb);


// Try to find a unique filename. // Try to find a unique filename.
Expand Down Expand Up @@ -306,23 +325,23 @@ void VoiceRecorder::run() {
} }


// Calculate the difference between the time of the current buffer and the time where we last wrote audio data for that user. // Calculate the difference between the time of the current buffer and the time where we last wrote audio data for that user.
// Writes silence if the number of |missingSamples| is larger than a threshold of 100ms (to account for processing delay). // Writes silence for all the missing samples.
qint64 missingSamples = ((rb->uiTimestamp - ri->uiLastPosition) * iSampleRate) / 1000000 - rb->iSamples; const qint64 missingSamples = rb->absoluteStartSample - ri->lastWrittenAbsoluteSample;
if (missingSamples > iSampleRate / 10) { if (missingSamples > 0) {
// Write |missingSamples| samples of silence. // Write |missingSamples| samples of silence.
boost::scoped_array<float> buffer(new float[1024]); const float buffer[1024] = {};
memset(buffer.get(), 0, sizeof(float) * 1024);
qint64 rest = missingSamples; qint64 rest = missingSamples;
for (; rest > 1024; rest -= 1024) for (; rest > 1024; rest -= 1024)
sf_write_float(ri->sf, buffer.get(), 1024); sf_write_float(ri->sf, buffer, 1024);


if (rest > 0) if (rest > 0)
sf_write_float(ri->sf, buffer.get(), rest); sf_write_float(ri->sf, buffer, rest);
} }


// Write the audio buffer and update the timestamp in |ri|. // Write the audio buffer and update the timestamp in |ri|.
sf_write_float(ri->sf, rb->fBuffer.get(), rb->iSamples); sf_write_float(ri->sf, rb->fBuffer.get(), rb->iSamples);
ri->uiLastPosition = rb->uiTimestamp; ri->lastWrittenAbsoluteSample = rb->absoluteStartSample + rb->iSamples;
} }


qmSleepLock.unlock(); qmSleepLock.unlock();
Expand All @@ -338,20 +357,21 @@ void VoiceRecorder::stop() {
qwcSleep.wakeAll(); qwcSleep.wakeAll();
} }


void VoiceRecorder::addBuffer(const ClientUser *cu, boost::shared_array<float> buffer, int samples) { void VoiceRecorder::addBuffer(const ClientUser *cu, boost::shared_array<float> buffer, int samples, quint64 absoluteSampleCount) {
Q_ASSERT(!bMixDown || cu == NULL); Q_ASSERT(!bMixDown || cu == NULL);


// Create a new RecordInfo object if this is a new user. // Create a new RecordInfo object if this is a new user.
int index = bMixDown ? 0 : cu->uiSession; int index = bMixDown ? 0 : cu->uiSession;
if (!qhRecordInfo.contains(index)) { if (!qhRecordInfo.contains(index)) {
boost::shared_ptr<RecordInfo> ri = boost::make_shared<RecordInfo>(); boost::shared_ptr<RecordInfo> ri = boost::make_shared<RecordInfo>(m_firstSampleAbsolute);

qhRecordInfo.insert(index, ri); qhRecordInfo.insert(index, ri);
} }


{ {
// Save the buffer in |qlRecordBuffer|. // Save the buffer in |qlRecordBuffer|.
QMutexLocker l(&qmBufferLock); QMutexLocker l(&qmBufferLock);
boost::shared_ptr<RecordBuffer> rb = boost::make_shared<RecordBuffer>(cu, buffer, samples, tTimestamp->elapsed()); boost::shared_ptr<RecordBuffer> rb = boost::make_shared<RecordBuffer>(cu, buffer, samples, absoluteSampleCount);
qlRecordBuffer << rb; qlRecordBuffer << rb;
} }


Expand All @@ -365,6 +385,12 @@ void VoiceRecorder::setSampleRate(int sampleRate) {
iSampleRate = sampleRate; iSampleRate = sampleRate;
} }


void VoiceRecorder::setFirstSampleAbsolute(quint64 firstSampleAbsolute) {
Q_ASSERT(!bRecording);

m_firstSampleAbsolute = firstSampleAbsolute;
}

int VoiceRecorder::getSampleRate() const { int VoiceRecorder::getSampleRate() const {
return iSampleRate; return iSampleRate;
} }
Expand Down
25 changes: 17 additions & 8 deletions src/mumble/VoiceRecorder.h
Expand Up @@ -81,7 +81,10 @@ class VoiceRecorder : public QThread {
// Stores information about a recording buffer. // Stores information about a recording buffer.
struct RecordBuffer { struct RecordBuffer {
// Constructs a new RecordBuffer object. // Constructs a new RecordBuffer object.
explicit RecordBuffer(const ClientUser *cu, boost::shared_array<float> buffer, int samples, quint64 timestamp); explicit RecordBuffer(const ClientUser *cu,
boost::shared_array<float> buffer,
int samples,
quint64 absoluteStartSample);


// The user to which this buffer belongs. // The user to which this buffer belongs.
const ClientUser *cuUser; const ClientUser *cuUser;
Expand All @@ -92,20 +95,20 @@ class VoiceRecorder : public QThread {
// The number of samples in the buffer. // The number of samples in the buffer.
int iSamples; int iSamples;


// Timestamp for the buffer. /// Absolute sample number at the start of this buffer
quint64 uiTimestamp; quint64 absoluteStartSample;
}; };


// Keep the recording state for one user. // Keep the recording state for one user.
struct RecordInfo { struct RecordInfo {
explicit RecordInfo(); explicit RecordInfo(quint64 lastWrittenAbsoluteSample);
~RecordInfo(); ~RecordInfo();


// libsndfile's handle. // libsndfile's handle.
SNDFILE *sf; SNDFILE *sf;


// The timestamp where we last wrote audio data for this user. /// The last absolute sample we wrote for this users
quint64 uiLastPosition; quint64 lastWrittenAbsoluteSample;
}; };


// Hash which maps the |uiSession| of all users for which we have to keep a recording state to the corresponding RecordInfo object. // Hash which maps the |uiSession| of all users for which we have to keep a recording state to the corresponding RecordInfo object.
Expand Down Expand Up @@ -145,6 +148,8 @@ class VoiceRecorder : public QThread {
// The timestamp where the recording started. // The timestamp where the recording started.
const QDateTime qdtRecordingStart; const QDateTime qdtRecordingStart;


/// Absolute sample number which to consider the start of the recording
quint64 m_firstSampleAbsolute;


// Removes invalid characters in a path component. // Removes invalid characters in a path component.
QString sanitizeFilenameOrPathComponent(const QString &str) const; QString sanitizeFilenameOrPathComponent(const QString &str) const;
Expand All @@ -166,11 +171,15 @@ class VoiceRecorder : public QThread {
// Stops the main loop. // Stops the main loop.
void stop(); void stop();


// Adds an audio buffer which contains |samples| audio samples to the recorder. /// Adds an audio buffer which contains |samples| audio samples to the recorder.
void addBuffer(const ClientUser *cu, boost::shared_array<float> buffer, int samples); /// The audio data will be aligned using the given |absoluteSampleCount|
void addBuffer(const ClientUser *cu, boost::shared_array<float> buffer, int samples, quint64 absoluteSampleCount);


// Sets the sample rate of the recorder. The sample rate can't change while the recoder is active. // Sets the sample rate of the recorder. The sample rate can't change while the recoder is active.
void setSampleRate(int sampleRate); void setSampleRate(int sampleRate);

/// Sets the absolute sample number considered the first sample of the recording
void setFirstSampleAbsolute(quint64 firstSampleAbsolute);


// Returns the current sample rate of the encoder. // Returns the current sample rate of the encoder.
int getSampleRate() const; int getSampleRate() const;
Expand Down
1 change: 1 addition & 0 deletions src/mumble/VoiceRecorderDialog.cpp
Expand Up @@ -202,6 +202,7 @@ void VoiceRecorderDialog::on_qpbStart_clicked() {


// Configure it // Configure it
recorder->setSampleRate(ao->getMixerFreq()); recorder->setSampleRate(ao->getMixerFreq());
recorder->setFirstSampleAbsolute(ao->getMixedSampleCount());
recorder->setFileName(dir.absoluteFilePath(basename + QLatin1Char('.') + suffix)); recorder->setFileName(dir.absoluteFilePath(basename + QLatin1Char('.') + suffix));
recorder->setMixDown(qrbDownmix->isChecked()); recorder->setMixDown(qrbDownmix->isChecked());
recorder->setFormat(static_cast<VoiceRecorderFormat::Format>(ifm)); recorder->setFormat(static_cast<VoiceRecorderFormat::Format>(ifm));
Expand Down

0 comments on commit 9a47e05

Please sign in to comment.