Skip to content
Permalink
Browse files

Fix assumptions in alignment fix by reintroducing silence heuristic.

Previous fix assumed mix was called exactly once for every sample
output on the audio device including silence. This is not true
for all audio backends and hence can't be relied upon in the
recorder.

This fix removes the absolute sample counting and replaces it by
a sample count estimation done in a central place before adding
the buffers from mixing to make sure alignment isn't affected.

This solution accepts some drift between tracks of up to 100ms
due to silence insertion however it prevents larger absolute
drift over longer time. It is compatible with all audio backends.
  • Loading branch information...
hacst committed Feb 11, 2014
1 parent 8e22f9a commit 2ca559b29a363a9a71390df6e9cafa335ac870db
@@ -105,8 +105,6 @@ AudioOutput::AudioOutput()
, fSpeakerVolume(NULL)
, bSpeakerPositional(NULL)

, m_mixedSampleCount(0)
, m_mixingMutex()
, eSampleFormat(SampleFloat)

, bRunning(true)
@@ -364,13 +362,10 @@ void AudioOutput::initializeMixer(const unsigned int *chanmasks, bool forceheadp
}

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

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

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

@@ -419,6 +414,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {
if (recorder) {
recbuff = boost::shared_array<float>(new float[nsamp]);
memset(recbuff.get(), 0, sizeof(float) * nsamp);
recorder->prepareBufferAdds();
}

for (unsigned int i=0;i<iChannels;++i)
@@ -514,7 +510,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {

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

if (recorder && recorder->isInMixDownMode()) {
recorder->addBuffer(NULL, recbuff, nsamp, m_mixedSampleCount);
recorder->addBuffer(NULL, recbuff, nsamp);
}

// Clip
@@ -589,8 +585,6 @@ bool AudioOutput::mix(void *outbuff, unsigned int nsamp) {
foreach(AudioOutputUser *aop, qlDel)
removeBuffer(aop);

m_mixedSampleCount += nsamp;

return (! qlMix.isEmpty());
}

@@ -602,7 +596,3 @@ unsigned int AudioOutput::getMixerFreq() const {
return iMixerFreq;
}

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

#endif
@@ -48,17 +48,17 @@ VoiceRecorder::RecordBuffer::RecordBuffer(
quint64 absoluteStartSample_)

: recordInfoIndex(recordInfoIndex_)
, buffer(buffer_)
, samples(samples_)
, absoluteStartSample(absoluteStartSample_) {
, buffer(buffer_)
, samples(samples_)
, absoluteStartSample(absoluteStartSample_) {

// Nothing
}

VoiceRecorder::RecordInfo::RecordInfo(quint64 lastWrittenAbsoluteSample_, const QString& userName_)
VoiceRecorder::RecordInfo::RecordInfo(const QString& userName_)
: userName(userName_)
, soundFile(NULL)
, lastWrittenAbsoluteSample(lastWrittenAbsoluteSample_) {
, lastWrittenAbsoluteSample(0) {
}

VoiceRecorder::RecordInfo::~RecordInfo() {
@@ -75,7 +75,8 @@ VoiceRecorder::VoiceRecorder(QObject *parent_, const Config& config)
, m_config(config)
, m_recording(false)
, m_abort(false)
, m_recordingStartTime(QDateTime::currentDateTime()) {
, m_recordingStartTime(QDateTime::currentDateTime())
, m_absoluteSampleEstimation(0) {

// Nothing
}
@@ -352,17 +353,15 @@ void VoiceRecorder::run() {
return;
}

// Calculate the difference between the time of the current buffer and the time where we last wrote audio data for that user.
// Writes silence for up to maxSamplesPerIteration samples.
const qint64 missingSamples = rb->absoluteStartSample - ri->lastWrittenAbsoluteSample;
Q_ASSERT(missingSamples >= 0);

if (missingSamples > 0) {
static const qint64 heuristicSilenceThreshold = m_config.sampleRate / 10; // 100ms
if (missingSamples > heuristicSilenceThreshold) {
static const qint64 maxSamplesPerIteration = m_config.sampleRate * 1; // 1s

const bool requeue = missingSamples > maxSamplesPerIteration;

// Write |missingSamples| samples of silence.
// Write |missingSamples| samples of silence up to |maxSamplesPerIteration|
const float buffer[1024] = {};

const qint64 silenceToWrite = std::min(missingSamples, maxSamplesPerIteration);
@@ -374,18 +373,19 @@ void VoiceRecorder::run() {
if (rest > 0)
sf_write_float(ri->soundFile, buffer, rest);

ri->lastWrittenAbsoluteSample += silenceToWrite;

if (requeue) {
ri->lastWrittenAbsoluteSample += silenceToWrite;
// Requeue the writing for this buffer to keep thread responsive
QMutexLocker l(&m_bufferLock);
m_recordBuffer << rb;
m_recordBuffer.prepend(rb);
continue;
}
}

// Write the audio buffer and update the timestamp in |ri|.
sf_write_float(ri->soundFile, rb->buffer.get(), rb->samples);
ri->lastWrittenAbsoluteSample = rb->absoluteStartSample + rb->samples;
ri->lastWrittenAbsoluteSample += rb->samples;
}

m_sleepLock.unlock();
@@ -410,10 +410,15 @@ void VoiceRecorder::stop(bool force) {
m_sleepCondition.wakeAll();
}

void VoiceRecorder::prepareBufferAdds() {
// Should be ms accurat
m_absoluteSampleEstimation =
(m_timestamp->elapsed() / 1000) * (m_config.sampleRate / 1000);
}

void VoiceRecorder::addBuffer(const ClientUser *clientUser,
boost::shared_array<float> buffer,
int samples,
quint64 absoluteSampleCount) {
int samples) {

Q_ASSERT(!m_config.mixDownMode || clientUser == NULL);

@@ -422,7 +427,6 @@ void VoiceRecorder::addBuffer(const ClientUser *clientUser,

if (!m_recordInfo.contains(index)) {
boost::shared_ptr<RecordInfo> ri = boost::make_shared<RecordInfo>(
m_config.firstSampleAbsolute,
m_config.mixDownMode ? QLatin1String("Mixdown")
: clientUser->qsName);

@@ -433,7 +437,7 @@ void VoiceRecorder::addBuffer(const ClientUser *clientUser,
// Save the buffer in |qlRecordBuffer|.
QMutexLocker l(&m_bufferLock);
boost::shared_ptr<RecordBuffer> rb = boost::make_shared<RecordBuffer>(
index, buffer, samples, absoluteSampleCount);
index, buffer, samples, m_absoluteSampleEstimation);

m_recordBuffer << rb;
}
@@ -103,12 +103,6 @@ class VoiceRecorder : public QThread {

/// The current recording format.
VoiceRecorderFormat::Format recordingFormat;

/// The timestamp where the recording started.
QDateTime recordingStartTime;

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

/// Creates a new VoiceRecorder instance.
@@ -122,10 +116,14 @@ class VoiceRecorder : public QThread {
/// @param force If true buffers are discarded. Otherwise the thread will not stop before writing everything.
void stop(bool force = false);

/// Remembers the current time for a set of coming addBuffer calls
void prepareBufferAdds();

/// Adds an audio buffer which contains |samples| audio samples to the recorder.
/// The audio data will be aligned using the given |absoluteSampleCount|
/// The audio data will be assumed to be recorded at the time
/// prepareBufferAdds was last called.
/// @param clientUser User for which to add the audio data. NULL in mixdown mode.
void addBuffer(const ClientUser *clientUser, boost::shared_array<float> buffer, int samples, quint64 absoluteSampleCount);
void addBuffer(const ClientUser *clientUser, boost::shared_array<float> buffer, int samples);

/// Returns the elapsed time since the recording started.
quint64 getElapsedTime() const;
@@ -169,7 +167,7 @@ class VoiceRecorder : public QThread {

/// Stores the recording state for one user.
struct RecordInfo {
RecordInfo(quint64 lastWrittenAbsoluteSample_, const QString& userName_);
RecordInfo(const QString& userName_);
~RecordInfo();

/// Name of the user being recorded
@@ -230,6 +228,9 @@ class VoiceRecorder : public QThread {

/// The timestamp where the recording started.
const QDateTime m_recordingStartTime;

/// Absolute sample position to assume for buffer adds
quint64 m_absoluteSampleEstimation;
};

typedef boost::shared_ptr<VoiceRecorder> VoiceRecorderPtr;
@@ -194,7 +194,6 @@ void VoiceRecorderDialog::on_qpbStart_clicked() {
// Create the recorder
VoiceRecorder::Config config;
config.sampleRate = ao->getMixerFreq();
config.firstSampleAbsolute = ao->getMixedSampleCount();
config.fileName = dir.absoluteFilePath(basename + QLatin1Char('.') + suffix);
config.mixDownMode = qrbDownmix->isChecked();
config.recordingFormat = static_cast<VoiceRecorderFormat::Format>(ifm);

0 comments on commit 2ca559b

Please sign in to comment.
You can’t perform that action at this time.