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