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