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