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@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@477
|
225 WritableWaveFileModel *
|
Chris@574
|
226 AudioCallbackRecordTarget::startRecording()
|
Chris@476
|
227 {
|
Chris@575
|
228 if (m_recording) {
|
Chris@575
|
229 SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: We are already recording" << endl;
|
Chris@575
|
230 return 0;
|
Chris@477
|
231 }
|
Chris@477
|
232
|
Chris@575
|
233 m_model = 0;
|
Chris@575
|
234 m_frameCount = 0;
|
Chris@575
|
235
|
Chris@620
|
236 QString folder = RecordDirectory::getRecordDirectory();
|
Chris@575
|
237 if (folder == "") return 0;
|
Chris@575
|
238 QDir recordedDir(folder);
|
Chris@575
|
239
|
Chris@575
|
240 QDateTime now = QDateTime::currentDateTime();
|
Chris@575
|
241
|
Chris@575
|
242 // Don't use QDateTime::toString(Qt::ISODate) as the ":" character
|
Chris@575
|
243 // isn't permitted in filenames on Windows
|
Chris@575
|
244 QString nowString = now.toString("yyyyMMdd-HHmmss-zzz");
|
Chris@575
|
245
|
Chris@575
|
246 QString filename = tr("recorded-%1.wav").arg(nowString);
|
Chris@575
|
247 QString label = tr("Recorded %1").arg(nowString);
|
Chris@575
|
248
|
Chris@575
|
249 m_audioFileName = recordedDir.filePath(filename);
|
Chris@575
|
250
|
Chris@621
|
251 m_model = new WritableWaveFileModel
|
Chris@621
|
252 (m_audioFileName,
|
Chris@621
|
253 m_recordSampleRate,
|
Chris@621
|
254 m_recordChannelCount,
|
Chris@621
|
255 WritableWaveFileModel::Normalisation::None);
|
Chris@575
|
256
|
Chris@575
|
257 if (!m_model->isOK()) {
|
Chris@575
|
258 SVCERR << "ERROR: AudioCallbackRecordTarget::startRecording: Recording failed"
|
Chris@575
|
259 << endl;
|
Chris@575
|
260 //!!! and throw?
|
Chris@575
|
261 delete m_model;
|
Chris@575
|
262 m_model = 0;
|
Chris@575
|
263 return 0;
|
Chris@575
|
264 }
|
Chris@575
|
265
|
Chris@575
|
266 m_model->setObjectName(label);
|
Chris@575
|
267 m_recording = true;
|
Chris@575
|
268
|
Chris@477
|
269 emit recordStatusChanged(true);
|
Chris@575
|
270
|
Chris@611
|
271 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel()));
|
Chris@575
|
272
|
Chris@477
|
273 return m_model;
|
Chris@476
|
274 }
|
Chris@476
|
275
|
Chris@476
|
276 void
|
Chris@574
|
277 AudioCallbackRecordTarget::stopRecording()
|
Chris@476
|
278 {
|
Chris@575
|
279 if (!m_recording) {
|
Chris@575
|
280 SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: Not recording" << endl;
|
Chris@575
|
281 return;
|
Chris@477
|
282 }
|
Chris@477
|
283
|
Chris@575
|
284 m_recording = false;
|
Chris@575
|
285
|
Chris@575
|
286 m_bufPtrMutex.lock();
|
Chris@575
|
287 m_bufPtrMutex.unlock();
|
Chris@575
|
288
|
Chris@575
|
289 // buffers should now be up-to-date
|
Chris@575
|
290 updateModel();
|
Chris@575
|
291
|
Chris@575
|
292 m_model->writeComplete();
|
Chris@575
|
293 m_model = 0;
|
Chris@575
|
294
|
Chris@477
|
295 emit recordStatusChanged(false);
|
Chris@497
|
296 emit recordCompleted();
|
Chris@476
|
297 }
|
Chris@476
|
298
|
Chris@476
|
299
|