annotate audio/AudioCallbackRecordTarget.cpp @ 679:1f18e0f64af8

Defer alignment for recorded audio until recording is complete
author Chris Cannam
date Thu, 13 Jun 2019 20:36:10 +0100
parents 6fd0ebfd2bbe
children aee03ad6d3f6
rev   line source
Chris@476 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@476 2
Chris@476 3 /*
Chris@476 4 Sonic Visualiser
Chris@476 5 An audio file viewer and annotation editor.
Chris@476 6 Centre for Digital Music, Queen Mary, University of London.
Chris@476 7
Chris@476 8 This program is free software; you can redistribute it and/or
Chris@476 9 modify it under the terms of the GNU General Public License as
Chris@476 10 published by the Free Software Foundation; either version 2 of the
Chris@476 11 License, or (at your option) any later version. See the file
Chris@476 12 COPYING included with this distribution for more information.
Chris@476 13 */
Chris@476 14
Chris@574 15 #include "AudioCallbackRecordTarget.h"
Chris@476 16
Chris@476 17 #include "base/ViewManagerBase.h"
Chris@620 18 #include "base/RecordDirectory.h"
Chris@641 19 #include "base/Debug.h"
Chris@476 20
Chris@477 21 #include "data/model/WritableWaveFileModel.h"
Chris@476 22
Chris@476 23 #include <QDir>
Chris@575 24 #include <QTimer>
Chris@476 25
Chris@609 26 //#define DEBUG_AUDIO_CALLBACK_RECORD_TARGET 1
Chris@609 27
Chris@609 28 static const int recordUpdateTimeout = 200; // ms
Chris@609 29
Chris@574 30 AudioCallbackRecordTarget::AudioCallbackRecordTarget(ViewManagerBase *manager,
Chris@574 31 QString clientName) :
Chris@476 32 m_viewManager(manager),
Chris@476 33 m_clientName(clientName.toUtf8().data()),
Chris@476 34 m_recording(false),
Chris@476 35 m_recordSampleRate(44100),
Chris@546 36 m_recordChannelCount(2),
Chris@485 37 m_frameCount(0),
Chris@636 38 m_model(nullptr),
Chris@636 39 m_buffers(nullptr),
Chris@575 40 m_bufferCount(0),
Chris@574 41 m_inputLeft(0.f),
Chris@580 42 m_inputRight(0.f),
Chris@580 43 m_levelsSet(false)
Chris@476 44 {
Chris@574 45 m_viewManager->setAudioRecordTarget(this);
Chris@574 46
Chris@574 47 connect(this, SIGNAL(recordStatusChanged(bool)),
Chris@574 48 m_viewManager, SLOT(recordStatusChanged(bool)));
Chris@575 49
Chris@575 50 recreateBuffers();
Chris@476 51 }
Chris@476 52
Chris@574 53 AudioCallbackRecordTarget::~AudioCallbackRecordTarget()
Chris@476 54 {
Chris@641 55 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
Chris@641 56 cerr << "AudioCallbackRecordTarget dtor" << endl;
Chris@641 57 #endif
Chris@641 58
Chris@636 59 m_viewManager->setAudioRecordTarget(nullptr);
Chris@575 60
Chris@575 61 QMutexLocker locker(&m_bufPtrMutex);
Chris@575 62 for (int c = 0; c < m_bufferCount; ++c) {
Chris@575 63 delete m_buffers[c];
Chris@575 64 }
Chris@575 65 delete[] m_buffers;
Chris@476 66 }
Chris@476 67
Chris@575 68 void
Chris@575 69 AudioCallbackRecordTarget::recreateBuffers()
Chris@575 70 {
Chris@575 71 static int bufferSize = 441000;
Chris@575 72
Chris@575 73 int count = m_recordChannelCount;
Chris@575 74
Chris@575 75 if (count > m_bufferCount) {
Chris@575 76
Chris@575 77 RingBuffer<float> **newBuffers = new RingBuffer<float> *[count];
Chris@575 78 for (int c = 0; c < m_bufferCount; ++c) {
Chris@575 79 newBuffers[c] = m_buffers[c];
Chris@575 80 }
Chris@575 81 for (int c = m_bufferCount; c < count; ++c) {
Chris@575 82 newBuffers[c] = new RingBuffer<float>(bufferSize);
Chris@575 83 }
Chris@575 84
Chris@575 85 // This is the only place where m_buffers is rewritten and
Chris@575 86 // should be the only possible source of contention against
Chris@575 87 // putSamples for this mutex (as the model-updating code is
Chris@575 88 // supposed to run in the same thread as this)
Chris@575 89 QMutexLocker locker(&m_bufPtrMutex);
Chris@575 90 delete[] m_buffers;
Chris@575 91 m_buffers = newBuffers;
Chris@575 92 m_bufferCount = count;
Chris@575 93 }
Chris@575 94 }
Chris@575 95
Chris@559 96 int
Chris@574 97 AudioCallbackRecordTarget::getApplicationSampleRate() const
Chris@559 98 {
Chris@559 99 return 0; // don't care
Chris@559 100 }
Chris@559 101
Chris@559 102 int
Chris@574 103 AudioCallbackRecordTarget::getApplicationChannelCount() const
Chris@559 104 {
Chris@559 105 return m_recordChannelCount;
Chris@559 106 }
Chris@559 107
Chris@476 108 void
Chris@574 109 AudioCallbackRecordTarget::setSystemRecordBlockSize(int)
Chris@476 110 {
Chris@476 111 }
Chris@476 112
Chris@476 113 void
Chris@574 114 AudioCallbackRecordTarget::setSystemRecordSampleRate(int n)
Chris@476 115 {
Chris@476 116 m_recordSampleRate = n;
Chris@476 117 }
Chris@476 118
Chris@476 119 void
Chris@574 120 AudioCallbackRecordTarget::setSystemRecordLatency(int)
Chris@476 121 {
Chris@476 122 }
Chris@476 123
Chris@476 124 void
Chris@574 125 AudioCallbackRecordTarget::setSystemRecordChannelCount(int c)
Chris@546 126 {
Chris@546 127 m_recordChannelCount = c;
Chris@575 128 recreateBuffers();
Chris@546 129 }
Chris@546 130
Chris@546 131 void
Chris@574 132 AudioCallbackRecordTarget::putSamples(const float *const *samples, int, int nframes)
Chris@476 133 {
Chris@575 134 // This may be called from RT context, and in a different thread
Chris@575 135 // from everything else in this class. It takes a mutex that
Chris@575 136 // should almost never be contended (see recreateBuffers())
Chris@575 137 if (!m_recording) return;
Chris@575 138
Chris@575 139 QMutexLocker locker(&m_bufPtrMutex);
Chris@575 140 if (m_buffers && m_bufferCount >= m_recordChannelCount) {
Chris@575 141 for (int c = 0; c < m_recordChannelCount; ++c) {
Chris@575 142 m_buffers[c]->write(samples[c], nframes);
Chris@575 143 }
Chris@575 144 }
Chris@575 145 }
Chris@575 146
Chris@575 147 void
Chris@575 148 AudioCallbackRecordTarget::updateModel()
Chris@575 149 {
Chris@611 150 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
Chris@611 151 cerr << "AudioCallbackRecordTarget::updateModel" << endl;
Chris@611 152 #endif
Chris@611 153
Chris@485 154 sv_frame_t frameToEmit = 0;
Chris@485 155
Chris@575 156 int nframes = 0;
Chris@575 157 for (int c = 0; c < m_recordChannelCount; ++c) {
Chris@575 158 if (c == 0 || m_buffers[c]->getReadSpace() < nframes) {
Chris@575 159 nframes = m_buffers[c]->getReadSpace();
Chris@575 160 }
Chris@575 161 }
Chris@485 162
Chris@575 163 if (nframes == 0) {
Chris@609 164 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
Chris@609 165 cerr << "AudioCallbackRecordTarget::updateModel: no frames available" << endl;
Chris@609 166 #endif
Chris@609 167 if (m_recording) {
Chris@609 168 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel()));
Chris@609 169 }
Chris@575 170 return;
Chris@575 171 }
Chris@485 172
Chris@611 173 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
Chris@611 174 cerr << "AudioCallbackRecordTarget::updateModel: have " << nframes << " frames" << endl;
Chris@611 175 #endif
Chris@611 176
Chris@641 177 if (!m_model) {
Chris@641 178 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
Chris@641 179 cerr << "AudioCallbackRecordTarget::updateModel: have no model to update; I am hoping there is a good reason for this" << endl;
Chris@641 180 #endif
Chris@641 181 return;
Chris@641 182 }
Chris@641 183
Chris@575 184 float **samples = new float *[m_recordChannelCount];
Chris@575 185 for (int c = 0; c < m_recordChannelCount; ++c) {
Chris@575 186 samples[c] = new float[nframes];
Chris@575 187 m_buffers[c]->read(samples[c], nframes);
Chris@575 188 }
Chris@485 189
Chris@575 190 m_model->addSamples(samples, nframes);
Chris@485 191
Chris@575 192 for (int c = 0; c < m_recordChannelCount; ++c) {
Chris@575 193 delete[] samples[c];
Chris@575 194 }
Chris@575 195 delete[] samples;
Chris@575 196
Chris@575 197 m_frameCount += nframes;
Chris@575 198
Chris@611 199 m_model->updateModel();
Chris@611 200 frameToEmit = m_frameCount;
Chris@611 201 emit recordDurationChanged(frameToEmit, m_recordSampleRate);
Chris@575 202
Chris@575 203 if (m_recording) {
Chris@611 204 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel()));
Chris@575 205 }
Chris@476 206 }
Chris@476 207
Chris@476 208 void
Chris@574 209 AudioCallbackRecordTarget::setInputLevels(float left, float right)
Chris@476 210 {
Chris@574 211 if (left > m_inputLeft) m_inputLeft = left;
Chris@574 212 if (right > m_inputRight) m_inputRight = right;
Chris@580 213 m_levelsSet = true;
Chris@574 214 }
Chris@574 215
Chris@574 216 bool
Chris@574 217 AudioCallbackRecordTarget::getInputLevels(float &left, float &right)
Chris@574 218 {
Chris@574 219 left = m_inputLeft;
Chris@574 220 right = m_inputRight;
Chris@581 221 bool valid = m_levelsSet;
Chris@574 222 m_inputLeft = 0.f;
Chris@574 223 m_inputRight = 0.f;
Chris@581 224 m_levelsSet = false;
Chris@581 225 return valid;
Chris@476 226 }
Chris@476 227
Chris@477 228 void
Chris@574 229 AudioCallbackRecordTarget::modelAboutToBeDeleted()
Chris@477 230 {
Chris@477 231 if (sender() == m_model) {
Chris@641 232 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
Chris@641 233 cerr << "AudioCallbackRecordTarget::modelAboutToBeDeleted: taking note" << endl;
Chris@641 234 #endif
Chris@636 235 m_model = nullptr;
Chris@477 236 m_recording = false;
Chris@642 237 } else if (m_model) {
Chris@642 238 SVCERR << "WARNING: AudioCallbackRecordTarget::modelAboutToBeDeleted: this is not my model!" << endl;
Chris@477 239 }
Chris@477 240 }
Chris@477 241
Chris@477 242 WritableWaveFileModel *
Chris@574 243 AudioCallbackRecordTarget::startRecording()
Chris@476 244 {
Chris@575 245 if (m_recording) {
Chris@575 246 SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: We are already recording" << endl;
Chris@636 247 return nullptr;
Chris@477 248 }
Chris@477 249
Chris@636 250 m_model = nullptr;
Chris@575 251 m_frameCount = 0;
Chris@575 252
Chris@620 253 QString folder = RecordDirectory::getRecordDirectory();
Chris@636 254 if (folder == "") return nullptr;
Chris@575 255 QDir recordedDir(folder);
Chris@575 256
Chris@575 257 QDateTime now = QDateTime::currentDateTime();
Chris@575 258
Chris@575 259 // Don't use QDateTime::toString(Qt::ISODate) as the ":" character
Chris@575 260 // isn't permitted in filenames on Windows
Chris@575 261 QString nowString = now.toString("yyyyMMdd-HHmmss-zzz");
Chris@575 262
Chris@575 263 QString filename = tr("recorded-%1.wav").arg(nowString);
Chris@575 264 QString label = tr("Recorded %1").arg(nowString);
Chris@575 265
Chris@575 266 m_audioFileName = recordedDir.filePath(filename);
Chris@575 267
Chris@621 268 m_model = new WritableWaveFileModel
Chris@621 269 (m_audioFileName,
Chris@621 270 m_recordSampleRate,
Chris@621 271 m_recordChannelCount,
Chris@621 272 WritableWaveFileModel::Normalisation::None);
Chris@575 273
Chris@575 274 if (!m_model->isOK()) {
Chris@575 275 SVCERR << "ERROR: AudioCallbackRecordTarget::startRecording: Recording failed"
Chris@575 276 << endl;
Chris@575 277 //!!! and throw?
Chris@575 278 delete m_model;
Chris@636 279 m_model = nullptr;
Chris@636 280 return nullptr;
Chris@575 281 }
Chris@575 282
Chris@641 283 connect(m_model, SIGNAL(aboutToBeDeleted()),
Chris@641 284 this, SLOT(modelAboutToBeDeleted()));
Chris@641 285
Chris@575 286 m_model->setObjectName(label);
Chris@575 287 m_recording = true;
Chris@575 288
Chris@477 289 emit recordStatusChanged(true);
Chris@575 290
Chris@611 291 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel()));
Chris@575 292
Chris@477 293 return m_model;
Chris@476 294 }
Chris@476 295
Chris@476 296 void
Chris@574 297 AudioCallbackRecordTarget::stopRecording()
Chris@476 298 {
Chris@575 299 if (!m_recording) {
Chris@575 300 SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: Not recording" << endl;
Chris@575 301 return;
Chris@477 302 }
Chris@477 303
Chris@575 304 m_recording = false;
Chris@575 305
Chris@575 306 m_bufPtrMutex.lock();
Chris@575 307 m_bufPtrMutex.unlock();
Chris@575 308
Chris@575 309 // buffers should now be up-to-date
Chris@575 310 updateModel();
Chris@575 311
Chris@575 312 m_model->writeComplete();
Chris@636 313 m_model = nullptr;
Chris@575 314
Chris@477 315 emit recordStatusChanged(false);
Chris@497 316 emit recordCompleted();
Chris@476 317 }
Chris@476 318
Chris@476 319