Mercurial > hg > svapp
comparison audio/AudioCallbackRecordTarget.cpp @ 775:56a81812c131 smoother-recording
Use ModelId for recording model; add mix-to-mono option
author | Chris Cannam |
---|---|
date | Tue, 16 Jun 2020 15:17:50 +0100 |
parents | aee03ad6d3f6 |
children |
comparison
equal
deleted
inserted
replaced
774:7bded7599874 | 775:56a81812c131 |
---|---|
15 #include "AudioCallbackRecordTarget.h" | 15 #include "AudioCallbackRecordTarget.h" |
16 | 16 |
17 #include "base/ViewManagerBase.h" | 17 #include "base/ViewManagerBase.h" |
18 #include "base/RecordDirectory.h" | 18 #include "base/RecordDirectory.h" |
19 #include "base/Debug.h" | 19 #include "base/Debug.h" |
20 #include "base/Preferences.h" | |
20 | 21 |
21 #include "data/model/WritableWaveFileModel.h" | 22 #include "data/model/WritableWaveFileModel.h" |
22 | 23 |
23 #include <QDir> | 24 #include <QDir> |
24 #include <QTimer> | 25 #include <QTimer> |
32 QString clientName) : | 33 QString clientName) : |
33 m_viewManager(manager), | 34 m_viewManager(manager), |
34 m_clientName(clientName.toUtf8().data()), | 35 m_clientName(clientName.toUtf8().data()), |
35 m_recording(false), | 36 m_recording(false), |
36 m_recordSampleRate(44100), | 37 m_recordSampleRate(44100), |
37 m_recordChannelCount(2), | 38 m_systemRecordChannelCount(2), |
39 m_recordMono(false), | |
38 m_frameCount(0), | 40 m_frameCount(0), |
39 m_model(nullptr), | |
40 m_buffers(nullptr), | 41 m_buffers(nullptr), |
41 m_bufferCount(0), | 42 m_bufferCount(0), |
42 m_inputLeft(0.f), | 43 m_inputLeft(0.f), |
43 m_inputRight(0.f), | 44 m_inputRight(0.f), |
44 m_levelsSet(false) | 45 m_levelsSet(false) |
69 void | 70 void |
70 AudioCallbackRecordTarget::recreateBuffers() | 71 AudioCallbackRecordTarget::recreateBuffers() |
71 { | 72 { |
72 static int bufferSize = 441000; | 73 static int bufferSize = 441000; |
73 | 74 |
74 int count = m_recordChannelCount; | 75 int count = m_systemRecordChannelCount; |
75 | 76 |
76 if (count > m_bufferCount) { | 77 if (count > m_bufferCount) { |
77 | 78 |
78 RingBuffer<float> **newBuffers = new RingBuffer<float> *[count]; | 79 RingBuffer<float> **newBuffers = new RingBuffer<float> *[count]; |
79 for (int c = 0; c < m_bufferCount; ++c) { | 80 for (int c = 0; c < m_bufferCount; ++c) { |
101 } | 102 } |
102 | 103 |
103 int | 104 int |
104 AudioCallbackRecordTarget::getApplicationChannelCount() const | 105 AudioCallbackRecordTarget::getApplicationChannelCount() const |
105 { | 106 { |
106 return m_recordChannelCount; | 107 // Pretend to just have as many as the system expects - we do our |
108 // own mixing-down optionally in the m_recordMono case | |
109 return m_systemRecordChannelCount; | |
107 } | 110 } |
108 | 111 |
109 void | 112 void |
110 AudioCallbackRecordTarget::setSystemRecordBlockSize(int) | 113 AudioCallbackRecordTarget::setSystemRecordBlockSize(int) |
111 { | 114 { |
112 } | 115 } |
113 | 116 |
114 void | 117 void |
115 AudioCallbackRecordTarget::setSystemRecordSampleRate(int n) | 118 AudioCallbackRecordTarget::setSystemRecordSampleRate(int n) |
116 { | 119 { |
120 SVCERR << "AudioCallbackRecordTarget: system sample rate is " << n << endl; | |
117 m_recordSampleRate = n; | 121 m_recordSampleRate = n; |
118 } | 122 } |
119 | 123 |
120 void | 124 void |
121 AudioCallbackRecordTarget::setSystemRecordLatency(int) | 125 AudioCallbackRecordTarget::setSystemRecordLatency(int) |
123 } | 127 } |
124 | 128 |
125 void | 129 void |
126 AudioCallbackRecordTarget::setSystemRecordChannelCount(int c) | 130 AudioCallbackRecordTarget::setSystemRecordChannelCount(int c) |
127 { | 131 { |
128 m_recordChannelCount = c; | 132 SVCERR << "AudioCallbackRecordTarget: system channel count is " << c << endl; |
133 m_systemRecordChannelCount = c; | |
129 recreateBuffers(); | 134 recreateBuffers(); |
130 } | 135 } |
131 | 136 |
132 void | 137 void |
133 AudioCallbackRecordTarget::putSamples(const float *const *samples, int, int nframes) | 138 AudioCallbackRecordTarget::putSamples(const float *const *samples, int, int nframes) |
136 // from everything else in this class. It takes a mutex that | 141 // from everything else in this class. It takes a mutex that |
137 // should almost never be contended (see recreateBuffers()) | 142 // should almost never be contended (see recreateBuffers()) |
138 if (!m_recording) return; | 143 if (!m_recording) return; |
139 | 144 |
140 QMutexLocker locker(&m_bufPtrMutex); | 145 QMutexLocker locker(&m_bufPtrMutex); |
141 if (m_buffers && m_bufferCount >= m_recordChannelCount) { | 146 if (m_buffers && m_bufferCount >= m_systemRecordChannelCount) { |
142 for (int c = 0; c < m_recordChannelCount; ++c) { | 147 for (int c = 0; c < m_systemRecordChannelCount; ++c) { |
143 m_buffers[c]->write(samples[c], nframes); | 148 m_buffers[c]->write(samples[c], nframes); |
144 } | 149 } |
145 } | 150 } |
146 } | 151 } |
147 | 152 |
153 #endif | 158 #endif |
154 | 159 |
155 sv_frame_t frameToEmit = 0; | 160 sv_frame_t frameToEmit = 0; |
156 | 161 |
157 int nframes = 0; | 162 int nframes = 0; |
158 for (int c = 0; c < m_recordChannelCount; ++c) { | 163 for (int c = 0; c < m_systemRecordChannelCount; ++c) { |
159 if (c == 0 || m_buffers[c]->getReadSpace() < nframes) { | 164 if (c == 0 || m_buffers[c]->getReadSpace() < nframes) { |
160 nframes = m_buffers[c]->getReadSpace(); | 165 nframes = m_buffers[c]->getReadSpace(); |
161 } | 166 } |
162 } | 167 } |
163 | 168 |
173 | 178 |
174 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET | 179 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET |
175 cerr << "AudioCallbackRecordTarget::updateModel: have " << nframes << " frames" << endl; | 180 cerr << "AudioCallbackRecordTarget::updateModel: have " << nframes << " frames" << endl; |
176 #endif | 181 #endif |
177 | 182 |
178 if (!m_model) { | 183 auto model = ModelById::getAs<WritableWaveFileModel>(m_modelId); |
184 if (!model) { | |
179 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET | 185 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET |
180 cerr << "AudioCallbackRecordTarget::updateModel: have no model to update; I am hoping there is a good reason for this" << endl; | 186 cerr << "AudioCallbackRecordTarget::updateModel: have no model to update; I am hoping there is a good reason for this" << endl; |
181 #endif | 187 #endif |
182 return; | 188 return; |
183 } | 189 } |
184 | 190 |
185 float **samples = new float *[m_recordChannelCount]; | 191 float **samples = new float *[m_systemRecordChannelCount]; |
186 for (int c = 0; c < m_recordChannelCount; ++c) { | 192 for (int c = 0; c < m_systemRecordChannelCount; ++c) { |
187 samples[c] = new float[nframes]; | 193 samples[c] = new float[nframes]; |
188 m_buffers[c]->read(samples[c], nframes); | 194 m_buffers[c]->read(samples[c], nframes); |
189 } | 195 } |
190 | 196 |
191 m_model->addSamples(samples, nframes); | 197 if (m_recordMono) { |
192 | 198 breakfastquay::v_reconfigure_channels_inplace(samples, 1, |
193 for (int c = 0; c < m_recordChannelCount; ++c) { | 199 m_systemRecordChannelCount, |
200 nframes); | |
201 } | |
202 | |
203 model->addSamples(samples, nframes); | |
204 | |
205 for (int c = 0; c < m_systemRecordChannelCount; ++c) { | |
194 delete[] samples[c]; | 206 delete[] samples[c]; |
195 } | 207 } |
196 delete[] samples; | 208 delete[] samples; |
197 | 209 |
198 m_frameCount += nframes; | 210 m_frameCount += nframes; |
199 | 211 |
200 m_model->updateModel(); | 212 model->updateModel(); |
201 frameToEmit = m_frameCount; | 213 frameToEmit = m_frameCount; |
202 emit recordDurationChanged(frameToEmit, m_recordSampleRate); | 214 emit recordDurationChanged(frameToEmit, m_recordSampleRate); |
203 | 215 |
204 if (m_recording) { | 216 if (m_recording) { |
205 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel())); | 217 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel())); |
224 m_inputRight = 0.f; | 236 m_inputRight = 0.f; |
225 m_levelsSet = false; | 237 m_levelsSet = false; |
226 return valid; | 238 return valid; |
227 } | 239 } |
228 | 240 |
229 void | 241 ModelId |
230 AudioCallbackRecordTarget::modelAboutToBeDeleted() | |
231 { | |
232 if (sender() == m_model) { | |
233 #ifdef DEBUG_AUDIO_CALLBACK_RECORD_TARGET | |
234 cerr << "AudioCallbackRecordTarget::modelAboutToBeDeleted: taking note" << endl; | |
235 #endif | |
236 m_model = nullptr; | |
237 m_recording = false; | |
238 } else if (m_model) { | |
239 SVCERR << "WARNING: AudioCallbackRecordTarget::modelAboutToBeDeleted: this is not my model!" << endl; | |
240 } | |
241 } | |
242 | |
243 WritableWaveFileModel * | |
244 AudioCallbackRecordTarget::startRecording() | 242 AudioCallbackRecordTarget::startRecording() |
245 { | 243 { |
246 if (m_recording) { | 244 if (m_recording) { |
247 SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: We are already recording" << endl; | 245 SVCERR << "WARNING: AudioCallbackRecordTarget::startRecording: We are already recording" << endl; |
248 return nullptr; | 246 return {}; |
249 } | 247 } |
250 | 248 |
251 m_model = nullptr; | 249 m_modelId = {}; |
252 m_frameCount = 0; | 250 m_frameCount = 0; |
253 | 251 |
254 QString folder = RecordDirectory::getRecordDirectory(); | 252 QString folder = RecordDirectory::getRecordDirectory(); |
255 if (folder == "") return nullptr; | 253 if (folder == "") return {}; |
256 QDir recordedDir(folder); | 254 QDir recordedDir(folder); |
257 | 255 |
258 QDateTime now = QDateTime::currentDateTime(); | 256 QDateTime now = QDateTime::currentDateTime(); |
259 | 257 |
260 // Don't use QDateTime::toString(Qt::ISODate) as the ":" character | 258 // Don't use QDateTime::toString(Qt::ISODate) as the ":" character |
264 QString filename = tr("recorded-%1.wav").arg(nowString); | 262 QString filename = tr("recorded-%1.wav").arg(nowString); |
265 QString label = tr("Recorded %1").arg(nowString); | 263 QString label = tr("Recorded %1").arg(nowString); |
266 | 264 |
267 m_audioFileName = recordedDir.filePath(filename); | 265 m_audioFileName = recordedDir.filePath(filename); |
268 | 266 |
269 m_model = new WritableWaveFileModel | 267 m_recordMono = Preferences::getInstance()->getRecordMono(); |
268 | |
269 int modelChannelCount = m_systemRecordChannelCount; | |
270 if (m_recordMono) { | |
271 modelChannelCount = 1; | |
272 } | |
273 | |
274 SVCERR << "AudioCallbackRecordTarget::startRecording: Recording to \"" | |
275 << m_audioFileName << "\", sample rate " << m_recordSampleRate | |
276 << ", system channel count " << m_systemRecordChannelCount | |
277 << ", model channel count " << modelChannelCount | |
278 << " (recordMono = " << m_recordMono << ")" << endl; | |
279 | |
280 auto model = std::make_shared<WritableWaveFileModel> | |
270 (m_audioFileName, | 281 (m_audioFileName, |
271 m_recordSampleRate, | 282 m_recordSampleRate, |
272 m_recordChannelCount, | 283 modelChannelCount, |
273 WritableWaveFileModel::Normalisation::None); | 284 WritableWaveFileModel::Normalisation::None); |
274 | 285 |
275 if (!m_model->isOK()) { | 286 if (!model->isOK()) { |
276 SVCERR << "ERROR: AudioCallbackRecordTarget::startRecording: Recording failed" | 287 SVCERR << "ERROR: AudioCallbackRecordTarget::startRecording: Recording failed" |
277 << endl; | 288 << endl; |
278 //!!! and throw? | 289 m_recording = false; |
279 delete m_model; | 290 return {}; |
280 m_model = nullptr; | 291 } |
281 return nullptr; | 292 |
282 } | 293 m_modelId = ModelById::add(model); |
283 | 294 |
284 connect(m_model, SIGNAL(aboutToBeDeleted()), | 295 model->setObjectName(label); |
285 this, SLOT(modelAboutToBeDeleted())); | |
286 | |
287 m_model->setObjectName(label); | |
288 m_recording = true; | 296 m_recording = true; |
289 | 297 |
290 emit recordStatusChanged(true); | 298 emit recordStatusChanged(true); |
291 | 299 |
292 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel())); | 300 QTimer::singleShot(recordUpdateTimeout, this, SLOT(updateModel())); |
293 | 301 |
294 return m_model; | 302 return m_modelId; |
295 } | 303 } |
296 | 304 |
297 void | 305 void |
298 AudioCallbackRecordTarget::stopRecording() | 306 AudioCallbackRecordTarget::stopRecording() |
299 { | 307 { |
308 m_bufPtrMutex.unlock(); | 316 m_bufPtrMutex.unlock(); |
309 | 317 |
310 // buffers should now be up-to-date | 318 // buffers should now be up-to-date |
311 updateModel(); | 319 updateModel(); |
312 | 320 |
313 m_model->writeComplete(); | 321 auto model = ModelById::getAs<WritableWaveFileModel>(m_modelId); |
314 m_model = nullptr; | 322 if (model) { |
323 model->writeComplete(); | |
324 } | |
325 | |
326 m_modelId = {}; | |
315 | 327 |
316 emit recordStatusChanged(false); | 328 emit recordStatusChanged(false); |
317 emit recordCompleted(); | 329 emit recordCompleted(); |
318 } | 330 } |
319 | 331 |
320 |