Mercurial > hg > svapp
diff audio/AudioCallbackRecordTarget.cpp @ 582:b2d49e7c4149
Merge from branch 3.0-integration
author | Chris Cannam |
---|---|
date | Fri, 13 Jan 2017 10:29:55 +0000 |
parents | 8cc291b13f2b |
children | 96b605673585 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audio/AudioCallbackRecordTarget.cpp Fri Jan 13 10:29:55 2017 +0000 @@ -0,0 +1,322 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + + 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "AudioCallbackRecordTarget.h" + +#include "base/ViewManagerBase.h" +#include "base/TempDirectory.h" + +#include "data/model/WritableWaveFileModel.h" + +#include <QDir> +#include <QTimer> + +AudioCallbackRecordTarget::AudioCallbackRecordTarget(ViewManagerBase *manager, + QString clientName) : + m_viewManager(manager), + m_clientName(clientName.toUtf8().data()), + m_recording(false), + m_recordSampleRate(44100), + m_recordChannelCount(2), + m_frameCount(0), + m_model(0), + m_buffers(0), + m_bufferCount(0), + m_inputLeft(0.f), + m_inputRight(0.f), + m_levelsSet(false) +{ + m_viewManager->setAudioRecordTarget(this); + + connect(this, SIGNAL(recordStatusChanged(bool)), + m_viewManager, SLOT(recordStatusChanged(bool))); + + recreateBuffers(); +} + +AudioCallbackRecordTarget::~AudioCallbackRecordTarget() +{ + m_viewManager->setAudioRecordTarget(0); + + QMutexLocker locker(&m_bufPtrMutex); + for (int c = 0; c < m_bufferCount; ++c) { + delete m_buffers[c]; + } + delete[] m_buffers; +} + +void +AudioCallbackRecordTarget::recreateBuffers() +{ + static int bufferSize = 441000; + + int count = m_recordChannelCount; + + if (count > m_bufferCount) { + + RingBuffer<float> **newBuffers = new RingBuffer<float> *[count]; + for (int c = 0; c < m_bufferCount; ++c) { + newBuffers[c] = m_buffers[c]; + } + for (int c = m_bufferCount; c < count; ++c) { + newBuffers[c] = new RingBuffer<float>(bufferSize); + } + + // This is the only place where m_buffers is rewritten and + // should be the only possible source of contention against + // putSamples for this mutex (as the model-updating code is + // supposed to run in the same thread as this) + QMutexLocker locker(&m_bufPtrMutex); + delete[] m_buffers; + m_buffers = newBuffers; + m_bufferCount = count; + } +} + +int +AudioCallbackRecordTarget::getApplicationSampleRate() const +{ + return 0; // don't care +} + +int +AudioCallbackRecordTarget::getApplicationChannelCount() const +{ + return m_recordChannelCount; +} + +void +AudioCallbackRecordTarget::setSystemRecordBlockSize(int) +{ +} + +void +AudioCallbackRecordTarget::setSystemRecordSampleRate(int n) +{ + m_recordSampleRate = n; +} + +void +AudioCallbackRecordTarget::setSystemRecordLatency(int) +{ +} + +void +AudioCallbackRecordTarget::setSystemRecordChannelCount(int c) +{ + m_recordChannelCount = c; + recreateBuffers(); +} + +void +AudioCallbackRecordTarget::putSamples(const float *const *samples, int, int nframes) +{ + // This may be called from RT context, and in a different thread + // from everything else in this class. It takes a mutex that + // should almost never be contended (see recreateBuffers()) + if (!m_recording) return; + + QMutexLocker locker(&m_bufPtrMutex); + if (m_buffers && m_bufferCount >= m_recordChannelCount) { + for (int c = 0; c < m_recordChannelCount; ++c) { + m_buffers[c]->write(samples[c], nframes); + } + } +} + +void +AudioCallbackRecordTarget::updateModel() +{ + bool secChanged = false; + sv_frame_t frameToEmit = 0; + + int nframes = 0; + for (int c = 0; c < m_recordChannelCount; ++c) { + if (c == 0 || m_buffers[c]->getReadSpace() < nframes) { + nframes = m_buffers[c]->getReadSpace(); + } + } + + if (nframes == 0) { + return; + } + + float **samples = new float *[m_recordChannelCount]; + for (int c = 0; c < m_recordChannelCount; ++c) { + samples[c] = new float[nframes]; + m_buffers[c]->read(samples[c], nframes); + } + + m_model->addSamples(samples, nframes); + + for (int c = 0; c < m_recordChannelCount; ++c) { + delete[] samples[c]; + } + delete[] samples; + + sv_frame_t priorFrameCount = m_frameCount; + m_frameCount += nframes; + + RealTime priorRT = + RealTime::frame2RealTime(priorFrameCount, m_recordSampleRate); + + RealTime postRT = + RealTime::frame2RealTime(m_frameCount, m_recordSampleRate); + + secChanged = (postRT.sec > priorRT.sec); + if (secChanged) { + m_model->updateModel(); + frameToEmit = m_frameCount; + } + + if (secChanged) { + emit recordDurationChanged(frameToEmit, m_recordSampleRate); + } + + if (m_recording) { + QTimer::singleShot(1000, this, SLOT(updateModel())); + } +} + +void +AudioCallbackRecordTarget::setInputLevels(float left, float right) +{ + if (left > m_inputLeft) m_inputLeft = left; + if (right > m_inputRight) m_inputRight = right; + m_levelsSet = true; +} + +bool +AudioCallbackRecordTarget::getInputLevels(float &left, float &right) +{ + left = m_inputLeft; + right = m_inputRight; + bool valid = m_levelsSet; + m_inputLeft = 0.f; + m_inputRight = 0.f; + m_levelsSet = false; + return valid; +} + +void +AudioCallbackRecordTarget::modelAboutToBeDeleted() +{ + if (sender() == m_model) { + m_model = 0; + m_recording = false; + } +} + +QString +AudioCallbackRecordTarget::getRecordContainerFolder() +{ + QDir parent(TempDirectory::getInstance()->getContainingPath()); + QString subdirname("recorded"); + + if (!parent.mkpath(subdirname)) { + SVCERR << "ERROR: AudioCallbackRecordTarget::getRecordContainerFolder: Failed to create recorded dir in \"" << parent.canonicalPath() << "\"" << endl; + return ""; + } else { + return parent.filePath(subdirname); + } +} + +QString +AudioCallbackRecordTarget::getRecordFolder() +{ + QDir parent(getRecordContainerFolder()); + QDateTime now = QDateTime::currentDateTime(); + QString subdirname = QString("%1").arg(now.toString("yyyyMMdd")); + + if (!parent.mkpath(subdirname)) { + SVCERR << "ERROR: AudioCallbackRecordTarget::getRecordFolder: Failed to create recorded dir in \"" << parent.canonicalPath() << "\"" << endl; + return ""; + } else { + return parent.filePath(subdirname); + } +} + +WritableWaveFileModel * +AudioCallbackRecordTarget::startRecording() +{ + if (m_recording) { + SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: We are already recording" << endl; + return 0; + } + + m_model = 0; + m_frameCount = 0; + + QString folder = getRecordFolder(); + if (folder == "") return 0; + QDir recordedDir(folder); + + QDateTime now = QDateTime::currentDateTime(); + + // Don't use QDateTime::toString(Qt::ISODate) as the ":" character + // isn't permitted in filenames on Windows + QString nowString = now.toString("yyyyMMdd-HHmmss-zzz"); + + QString filename = tr("recorded-%1.wav").arg(nowString); + QString label = tr("Recorded %1").arg(nowString); + + m_audioFileName = recordedDir.filePath(filename); + + m_model = new WritableWaveFileModel(m_recordSampleRate, + m_recordChannelCount, + m_audioFileName); + + if (!m_model->isOK()) { + SVCERR << "ERROR: AudioCallbackRecordTarget::startRecording: Recording failed" + << endl; + //!!! and throw? + delete m_model; + m_model = 0; + return 0; + } + + m_model->setObjectName(label); + m_recording = true; + + emit recordStatusChanged(true); + + QTimer::singleShot(1000, this, SLOT(updateModel())); + + return m_model; +} + +void +AudioCallbackRecordTarget::stopRecording() +{ + if (!m_recording) { + SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: Not recording" << endl; + return; + } + + m_recording = false; + + m_bufPtrMutex.lock(); + m_bufPtrMutex.unlock(); + + // buffers should now be up-to-date + updateModel(); + + m_model->writeComplete(); + m_model = 0; + + emit recordStatusChanged(false); + emit recordCompleted(); +} + +