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