Mercurial > hg > svapp
view audio/AudioCallbackRecordTarget.cpp @ 609:96b605673585
Fix failure to call updateModel() ever again after no frames available
author | Chris Cannam |
---|---|
date | Wed, 08 Aug 2018 15:17:36 +0100 |
parents | 8cc291b13f2b |
children | 37b23e50832b |
line wrap: on
line source
/* -*- 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> //#define DEBUG_AUDIO_CALLBACK_RECORD_TARGET 1 static const int recordUpdateTimeout = 200; // ms 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) { #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET cerr << "AudioCallbackRecordTarget::updateModel: no frames available" << endl; #endif if (m_recording) { QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel())); } 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(); }