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