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