1 // Copyright 2005-2019 The Mumble Developers. All rights reserved.
2 // Use of this source code is governed by a BSD-style license
3 // that can be found in the LICENSE file at the root of the
4 // Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6 #include "mumble_pch.hpp"
7
8 #include "VoiceRecorder.h"
9
10 #include "AudioOutput.h"
11 #include "ClientUser.h"
12 #include "ServerHandler.h"
13
14 #include "../Timer.h"
15
16 // We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include.
17 #include "Global.h"
18
RecordBuffer(int recordInfoIndex_,boost::shared_array<float> buffer_,int samples_,quint64 absoluteStartSample_)19 VoiceRecorder::RecordBuffer::RecordBuffer(
20 int recordInfoIndex_,
21 boost::shared_array<float> buffer_,
22 int samples_,
23 quint64 absoluteStartSample_)
24
25 : recordInfoIndex(recordInfoIndex_)
26 , buffer(buffer_)
27 , samples(samples_)
28 , absoluteStartSample(absoluteStartSample_) {
29
30 // Nothing
31 }
32
RecordInfo(const QString & userName_)33 VoiceRecorder::RecordInfo::RecordInfo(const QString& userName_)
34 : userName(userName_)
35 , soundFile(NULL)
36 , lastWrittenAbsoluteSample(0) {
37 }
38
~RecordInfo()39 VoiceRecorder::RecordInfo::~RecordInfo() {
40 if (soundFile) {
41 // Close libsndfile's handle if we have one.
42 sf_close(soundFile);
43 }
44 }
45
VoiceRecorder(QObject * p,const Config & config)46 VoiceRecorder::VoiceRecorder(QObject *p, const Config& config)
47 : QThread(p)
48 , m_recordUser(new RecordUser())
49 , m_timestamp(new Timer())
50 , m_config(config)
51 , m_recording(false)
52 , m_abort(false)
53 , m_recordingStartTime(QDateTime::currentDateTime())
54 , m_absoluteSampleEstimation(0) {
55
56 // Nothing
57 }
58
~VoiceRecorder()59 VoiceRecorder::~VoiceRecorder() {
60 stop();
61 wait();
62 }
63
sanitizeFilenameOrPathComponent(const QString & str) const64 QString VoiceRecorder::sanitizeFilenameOrPathComponent(const QString &str) const {
65 // Trim leading/trailing whitespaces
66 QString res = str.trimmed();
67 if (res.isEmpty())
68 return QLatin1String("_");
69
70 #ifdef Q_OS_WIN
71 // Rules according to http://en.wikipedia.org/wiki/Filename#Comparison_of_file_name_limitations
72 // and http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
73
74 // Make sure name doesn't end in "."
75 if (res.at(res.length() - 1) == QLatin1Char('.')) {
76 if (res.length() == 255) { // Prevents possible infinite recursion later on
77 res[254] = QLatin1Char('_');
78 } else {
79 res = res.append(QLatin1Char('_'));
80 }
81 }
82
83 // Replace < > : " / \ | ? * as well as chr(0) to chr(31)
84 res = res.replace(QRegExp(QLatin1String("[<>:\"/\\\\|\\?\\*\\x00-\\x1F]")), QLatin1String("_"));
85
86 // Prepend reserved filenames CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9
87 res = res.replace(QRegExp(QLatin1String("^((CON|PRN|AUX|NUL|COM[1-9]|LPT1[1-9])(\\.|$))"), Qt::CaseInsensitive), QLatin1String("_\\1"));
88
89 // Make sure we do not exceed 255 characters
90 if (res.length() > 255) {
91 res.truncate(255);
92 // Call ourselves recursively to make sure we do not end up violating any of our rules because of this
93 res = sanitizeFilenameOrPathComponent(res);
94 }
95 #else
96 // For the rest just make sure the string doesn't contain a \0 or any forward-slashes
97 res = res.replace(QRegExp(QLatin1String("\\x00|/")), QLatin1String("_"));
98 #endif
99 return res;
100 }
101
expandTemplateVariables(const QString & path,const QString & userName) const102 QString VoiceRecorder::expandTemplateVariables(const QString &path, const QString& userName) const {
103 // Split path into components
104 QString res;
105 QStringList comp = path.split(QLatin1Char('/'));
106 Q_ASSERT(!comp.isEmpty());
107
108 // Create a readable representation of the start date.
109 QString date(m_recordingStartTime.date().toString(Qt::ISODate));
110 QString time(m_recordingStartTime.time().toString(QLatin1String("hh-mm-ss")));
111
112 QString hostname(QLatin1String("Unknown"));
113 if (g.sh && g.uiSession != 0) {
114 unsigned short port;
115 QString uname, pw;
116 g.sh->getConnectionInfo(hostname, port, uname, pw);
117 }
118
119 // Create hash which stores the names of the variables with the corresponding values.
120 // Valid variables are:
121 // %user Inserts the users name
122 // %date Inserts the current date
123 // %time Inserts the current time
124 // %host Inserts the hostname
125 QHash<const QString, QString> vars;
126 vars.insert(QLatin1String("user"), userName);
127 vars.insert(QLatin1String("date"), date);
128 vars.insert(QLatin1String("time"), time);
129 vars.insert(QLatin1String("host"), hostname);
130
131 // Reassemble and expand
132 bool first = true;
133 foreach(QString str, comp) {
134 bool replacements = false;
135 QString tmp;
136
137 tmp.reserve(str.length() * 2);
138 for (int i = 0; i < str.size(); ++i) {
139 bool replaced = false;
140 if (str[i] == QLatin1Char('%')) {
141 QHashIterator<const QString, QString> it(vars);
142 while (it.hasNext()) {
143 it.next();
144 if (str.midRef(i + 1, it.key().length()) == it.key()) {
145 i += it.key().length();
146 tmp += it.value();
147 replaced = true;
148 replacements = true;
149 break;
150 }
151 }
152 }
153
154 if (!replaced)
155 tmp += str[i];
156 }
157
158 str = tmp;
159
160 if (replacements)
161 str = sanitizeFilenameOrPathComponent(str);
162
163 if (first) {
164 first = false;
165 res.append(str);
166 } else {
167 res.append(QLatin1Char('/') + str);
168 }
169 }
170 return res;
171 }
172
indexForUser(const ClientUser * clientUser) const173 int VoiceRecorder::indexForUser(const ClientUser *clientUser) const {
174 Q_ASSERT(!m_config.mixDownMode || clientUser == NULL);
175
176 return (m_config.mixDownMode) ? 0 : clientUser->uiSession;
177 }
178
createSoundFileInfo() const179 SF_INFO VoiceRecorder::createSoundFileInfo() const {
180 Q_ASSERT(m_config.sampleRate != 0);
181
182 // When adding new formats make sure to properly configure needed additional
183 // behavior after opening the file handle (e.g. to enable clipping).
184
185 // Convert |fmFormat| to a SF_INFO structure for libsndfile.
186 SF_INFO sfinfo;
187 switch (m_config.recordingFormat) {
188 case VoiceRecorderFormat::WAV:
189 default:
190 sfinfo.frames = 0;
191 sfinfo.samplerate = m_config.sampleRate;
192 sfinfo.channels = 1;
193 sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_24;
194 sfinfo.sections = 0;
195 sfinfo.seekable = 0;
196 qWarning() << "VoiceRecorder: recording started to" << m_config.fileName << "@" << m_config.sampleRate << "hz in WAV format";
197 break;
198 #ifndef NO_VORBIS_RECORDING
199 case VoiceRecorderFormat::VORBIS:
200 sfinfo.frames = 0;
201 sfinfo.samplerate = m_config.sampleRate;
202 sfinfo.channels = 1;
203 sfinfo.format = SF_FORMAT_OGG | SF_FORMAT_VORBIS;
204 sfinfo.sections = 0;
205 sfinfo.seekable = 0;
206 qWarning() << "VoiceRecorder: recording started to" << m_config.fileName << "@" << m_config.sampleRate << "hz in OGG/Vorbis format";
207 break;
208 #endif
209 case VoiceRecorderFormat::AU:
210 sfinfo.frames = 0;
211 sfinfo.samplerate = m_config.sampleRate;
212 sfinfo.channels = 1;
213 sfinfo.format = SF_ENDIAN_CPU | SF_FORMAT_AU | SF_FORMAT_FLOAT;
214 sfinfo.sections = 0;
215 sfinfo.seekable = 0;
216 qWarning() << "VoiceRecorder: recording started to" << m_config.fileName << "@" << m_config.sampleRate << "hz in AU format";
217 break;
218 case VoiceRecorderFormat::FLAC:
219 sfinfo.frames = 0;
220 sfinfo.samplerate = m_config.sampleRate;
221 sfinfo.channels = 1;
222 sfinfo.format = SF_FORMAT_FLAC | SF_FORMAT_PCM_24;
223 sfinfo.sections = 0;
224 sfinfo.seekable = 0;
225 qWarning() << "VoiceRecorder: recording started to" << m_config.fileName << "@" << m_config.sampleRate << "hz in FLAC format";
226 break;
227 }
228
229 Q_ASSERT(sf_format_check(&sfinfo));
230 return sfinfo;
231 }
232
ensureFileIsOpenedFor(SF_INFO & soundFileInfo,boost::shared_ptr<RecordInfo> & ri)233 bool VoiceRecorder::ensureFileIsOpenedFor(SF_INFO& soundFileInfo, boost::shared_ptr<RecordInfo>& ri) {
234 if (ri->soundFile != NULL) {
235 // Nothing to do
236 return true;
237 }
238
239 QString filename = expandTemplateVariables(m_config.fileName, ri->userName);
240
241 // Try to find a unique filename.
242 {
243 int cnt = 1;
244 QString nf(filename);
245 QFileInfo tfi(filename);
246 while (QFile::exists(nf)) {
247 nf = tfi.path()
248 + QLatin1Char('/')
249 + tfi.completeBaseName()
250 + QString(QLatin1String(" (%1).")).arg(cnt)
251 + tfi.suffix();
252
253 ++cnt;
254 }
255 filename = nf;
256 }
257 qWarning() << "Recorder opens file" << filename;
258 QFileInfo fi(filename);
259
260 // Create the target path.
261 if (!QDir().mkpath(fi.absolutePath())) {
262 qWarning() << "Failed to create target directory: " << fi.absolutePath();
263 m_recording = false;
264 emit error(CreateDirectoryFailed, tr("Recorder failed to create directory '%1'").arg(fi.absolutePath()));
265 emit recording_stopped();
266 return false;
267 }
268
269 #ifdef Q_OS_WIN
270 // This is needed for unicode filenames on Windows.
271 ri->soundFile = sf_wchar_open(filename.toStdWString().c_str(), SFM_WRITE, &soundFileInfo);
272 #else
273 ri->soundFile = sf_open(qPrintable(filename), SFM_WRITE, &soundFileInfo);
274 #endif
275 if (ri->soundFile == NULL) {
276 qWarning() << "Failed to open file for recorder: "<< sf_strerror(NULL);
277 m_recording = false;
278 emit error(CreateFileFailed, tr("Recorder failed to open file '%1'").arg(filename));
279 emit recording_stopped();
280 return false;
281 }
282
283 // Store the username in the title attribute of the file (if supported by the format).
284 sf_set_string(ri->soundFile, SF_STR_TITLE, qPrintable(ri->userName));
285
286 // Enable hard-clipping for non-float formats to prevent wrapping
287 if ((soundFileInfo.format & SF_FORMAT_SUBMASK) != SF_FORMAT_FLOAT &&
288 (soundFileInfo.format & SF_FORMAT_SUBMASK) != SF_FORMAT_VORBIS) {
289
290 sf_command(ri->soundFile, SFC_SET_CLIPPING, NULL, SF_TRUE);
291 }
292
293 return true;
294 }
295
run()296 void VoiceRecorder::run() {
297 Q_ASSERT(!m_recording);
298
299 if (g.sh && g.sh->uiVersion < 0x010203)
300 return;
301
302 SF_INFO soundFileInfo = createSoundFileInfo();
303
304 m_recording = true;
305 emit recording_started();
306
307 forever {
308 // Sleep until there is new data for us to process.
309 m_sleepLock.lock();
310 m_sleepCondition.wait(&m_sleepLock);
311
312 if (!m_recording || m_abort || (g.sh && g.sh->uiVersion < 0x010203)) {
313 m_sleepLock.unlock();
314 break;
315 }
316
317 while (!m_abort && !m_recordBuffer.isEmpty()) {
318 boost::shared_ptr<RecordBuffer> rb;
319 {
320 QMutexLocker l(&m_bufferLock);
321 rb = m_recordBuffer.takeFirst();
322 }
323
324 // Create the file for this RecordInfo instance if it's not yet open.
325
326 Q_ASSERT(m_recordInfo.contains(rb->recordInfoIndex));
327 boost::shared_ptr<RecordInfo> ri = m_recordInfo.value(rb->recordInfoIndex);
328
329 if (!ensureFileIsOpenedFor(soundFileInfo, ri)) {
330 return;
331 }
332
333 const qint64 missingSamples = rb->absoluteStartSample - ri->lastWrittenAbsoluteSample;
334
335 static const qint64 heuristicSilenceThreshold = m_config.sampleRate / 10; // 100ms
336 if (missingSamples > heuristicSilenceThreshold) {
337 static const qint64 maxSamplesPerIteration = m_config.sampleRate * 1; // 1s
338
339 const bool requeue = missingSamples > maxSamplesPerIteration;
340
341 // Write |missingSamples| samples of silence up to |maxSamplesPerIteration|
342 const float buffer[1024] = {};
343
344 const qint64 silenceToWrite = std::min(missingSamples, maxSamplesPerIteration);
345 qint64 rest = silenceToWrite;
346
347 for (; rest > 1024; rest -= 1024)
348 sf_write_float(ri->soundFile, buffer, 1024);
349
350 if (rest > 0)
351 sf_write_float(ri->soundFile, buffer, rest);
352
353 ri->lastWrittenAbsoluteSample += silenceToWrite;
354
355 if (requeue) {
356 // Requeue the writing for this buffer to keep thread responsive
357 QMutexLocker l(&m_bufferLock);
358 m_recordBuffer.prepend(rb);
359 continue;
360 }
361 }
362
363 // Write the audio buffer and update the timestamp in |ri|.
364 sf_write_float(ri->soundFile, rb->buffer.get(), rb->samples);
365 ri->lastWrittenAbsoluteSample += rb->samples;
366 }
367
368 m_sleepLock.unlock();
369 }
370
371 m_recording = false;
372 {
373 QMutexLocker l(&m_bufferLock);
374 m_recordInfo.clear();
375 m_recordBuffer.clear();
376 }
377
378 emit recording_stopped();
379 qWarning() << "VoiceRecorder: recording stopped";
380 }
381
stop(bool force)382 void VoiceRecorder::stop(bool force) {
383 // Tell the main loop to terminate and wake up the sleep lock.
384 m_recording = false;
385 m_abort = force;
386
387 m_sleepCondition.wakeAll();
388 }
389
prepareBufferAdds()390 void VoiceRecorder::prepareBufferAdds() {
391 // Should be ms accurat
392 m_absoluteSampleEstimation =
393 (m_timestamp->elapsed() / 1000) * (m_config.sampleRate / 1000);
394 }
395
addBuffer(const ClientUser * clientUser,boost::shared_array<float> buffer,int samples)396 void VoiceRecorder::addBuffer(const ClientUser *clientUser,
397 boost::shared_array<float> buffer,
398 int samples) {
399
400 Q_ASSERT(!m_config.mixDownMode || clientUser == NULL);
401
402 if (!m_recording)
403 return;
404
405 // Create a new RecordInfo object if this is a new user.
406 const int index = indexForUser(clientUser);
407
408 if (!m_recordInfo.contains(index)) {
409 boost::shared_ptr<RecordInfo> ri = boost::make_shared<RecordInfo>(
410 m_config.mixDownMode ? QLatin1String("Mixdown")
411 : clientUser->qsName);
412
413 m_recordInfo.insert(index, ri);
414 }
415
416 {
417 // Save the buffer in |qlRecordBuffer|.
418 QMutexLocker l(&m_bufferLock);
419 boost::shared_ptr<RecordBuffer> rb = boost::make_shared<RecordBuffer>(
420 index, buffer, samples, m_absoluteSampleEstimation);
421
422 m_recordBuffer << rb;
423 }
424
425 // Tell the main loop that we have new audio data.
426 m_sleepCondition.wakeAll();
427 }
428
getElapsedTime() const429 quint64 VoiceRecorder::getElapsedTime() const {
430 return m_timestamp->elapsed();
431 }
432
getRecordUser() const433 RecordUser &VoiceRecorder::getRecordUser() const {
434 return *m_recordUser;
435 }
436
isInMixDownMode() const437 bool VoiceRecorder::isInMixDownMode() const {
438 return m_config.mixDownMode;
439 }
440
getFormatDescription(VoiceRecorderFormat::Format fm)441 QString VoiceRecorderFormat::getFormatDescription(VoiceRecorderFormat::Format fm) {
442 switch (fm) {
443 case VoiceRecorderFormat::WAV:
444 return VoiceRecorder::tr(".wav - Uncompressed");
445 #ifndef NO_VORBIS_RECORDING
446 case VoiceRecorderFormat::VORBIS:
447 return VoiceRecorder::tr(".ogg (Vorbis) - Compressed");
448 #endif
449 case VoiceRecorderFormat::AU:
450 return VoiceRecorder::tr(".au - Uncompressed");
451 case VoiceRecorderFormat::FLAC:
452 return VoiceRecorder::tr(".flac - Lossless compressed");
453 default:
454 return QString();
455 }
456 }
457
getFormatDefaultExtension(VoiceRecorderFormat::Format fm)458 QString VoiceRecorderFormat::getFormatDefaultExtension(VoiceRecorderFormat::Format fm) {
459 switch (fm) {
460 case VoiceRecorderFormat::WAV:
461 return QLatin1String("wav");
462 #ifndef NO_VORBIS_RECORDING
463 case VoiceRecorderFormat::VORBIS:
464 return QLatin1String("ogg");
465 #endif
466 case VoiceRecorderFormat::AU:
467 return QLatin1String("au");
468 case VoiceRecorderFormat::FLAC:
469 return QLatin1String("flac");
470 default:
471 return QString();
472 }
473 }
474