view audio/AudioCallbackRecordTarget.cpp @ 588:d122d3595a32

Store aggregate models in the document and release them when they are invalidated (because their components have been released). They're no longer leaked, but we still don't save them in the session file.
author Chris Cannam
date Mon, 27 Feb 2017 16:26:37 +0000
parents 8cc291b13f2b
children 96b605673585
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>

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();
}