view audio/AudioCallbackRecordTarget.cpp @ 725:16f6737fa557 spectrogram-export

Rework OSC handler so as to consume all available messages rather than having to wait for the timeout in between them. Pause to process events, and also wait for file loads and transforms to complete. (Should only certain kinds of OSC command wait for transforms?)
author Chris Cannam
date Wed, 08 Jan 2020 15:33:17 +0000
parents aee03ad6d3f6
children 56a81812c131
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/RecordDirectory.h"
#include "base/Debug.h"

#include "data/model/WritableWaveFileModel.h"

#include <QDir>
#include <QTimer>
#include <QDateTime>

//#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(nullptr),
    m_buffers(nullptr),
    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()
{
#ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
    cerr << "AudioCallbackRecordTarget dtor" << endl;
#endif
    
    m_viewManager->setAudioRecordTarget(nullptr);

    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()
{
#ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
    cerr << "AudioCallbackRecordTarget::updateModel" << endl;
#endif
    
    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;
    }

#ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
    cerr << "AudioCallbackRecordTarget::updateModel: have " << nframes << " frames" << endl;
#endif

    if (!m_model) {
#ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
        cerr << "AudioCallbackRecordTarget::updateModel: have no model to update; I am hoping there is a good reason for this" << endl;
#endif
        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;
    
    m_frameCount += nframes;
    
    m_model->updateModel();
    frameToEmit = m_frameCount;
    emit recordDurationChanged(frameToEmit, m_recordSampleRate);

    if (m_recording) {
        QTimer::singleShot(recordUpdateTimeout, 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) {
#ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET
        cerr << "AudioCallbackRecordTarget::modelAboutToBeDeleted: taking note" << endl;
#endif
        m_model = nullptr;
        m_recording = false;
    } else if (m_model) {
        SVCERR << "WARNING: AudioCallbackRecordTarget::modelAboutToBeDeleted: this is not my model!" << endl;
    }
}

WritableWaveFileModel *
AudioCallbackRecordTarget::startRecording()
{
    if (m_recording) {
        SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: We are already recording" << endl;
        return nullptr;
    }

    m_model = nullptr;
    m_frameCount = 0;

    QString folder = RecordDirectory::getRecordDirectory();
    if (folder == "") return nullptr;
    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_audioFileName,
         m_recordSampleRate,
         m_recordChannelCount,
         WritableWaveFileModel::Normalisation::None);

    if (!m_model->isOK()) {
        SVCERR << "ERROR: AudioCallbackRecordTarget::startRecording: Recording failed"
               << endl;
        //!!! and throw?
        delete m_model;
        m_model = nullptr;
        return nullptr;
    }

    connect(m_model, SIGNAL(aboutToBeDeleted()), 
            this, SLOT(modelAboutToBeDeleted()));

    m_model->setObjectName(label);
    m_recording = true;

    emit recordStatusChanged(true);

    QTimer::singleShot(recordUpdateTimeout, 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 = nullptr;
    
    emit recordStatusChanged(false);
    emit recordCompleted();
}