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