Fix creation of output file name
[skype-call-recorder.git] / call.cpp
blob663c1c1ef0ef37875f2f1b2539d254ed87a6a7fa
1 /*
2 Skype Call Recorder
3 Copyright 2008 - 2009 by jlh (jlh at gmx dot ch)
5 This program is free software; you can redistribute it and/or modify it
6 under the terms of the GNU General Public License as published by the
7 Free Software Foundation; either version 2 of the License, version 3 of
8 the License, or (at your option) any later version.
10 This program is distributed in the hope that it will be useful, but
11 WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 General Public License for more details.
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 The GNU General Public License version 2 is included with the source of
20 this program under the file name COPYING. You can also get a copy on
21 http://www.fsf.org/
24 #include <QStringList>
25 #include <QList>
26 #include <QTcpServer>
27 #include <QTcpSocket>
28 #include <QMessageBox>
29 #include <cstdlib>
30 #include <cmath>
31 #include <cstring>
33 #include "call.h"
34 #include "common.h"
35 #include "skype.h"
36 #include "wavewriter.h"
37 #include "mp3writer.h"
38 #include "vorbiswriter.h"
39 #include "preferences.h"
40 #include "gui.h"
42 // AutoSync - automatic resynchronization of the two streams. this class has a
43 // circular buffer that keeps track of the delay between the two streams. it
44 // calculates the running average and deviation and then tells if and how much
45 // correction should be applied.
47 AutoSync::AutoSync(int s, long p) :
48 size(s),
49 index(0),
50 sum(0),
51 sum2(0),
52 precision(p),
53 suppress(s)
55 delays = new long[size];
56 std::memset(delays, 0, sizeof(long) * size);
59 AutoSync::~AutoSync() {
60 delete[] delays;
63 void AutoSync::add(long d) {
64 long old = delays[index];
65 sum += d - old;
66 sum2 += (qint64)d * (qint64)d - (qint64)old * (qint64)old;
67 delays[index++] = d;
68 if (index >= size)
69 index = 0;
70 if (suppress)
71 suppress--;
74 long AutoSync::getSync() {
75 if (suppress)
76 return 0;
78 float avg = (float)sum / (float)size;
79 float dev = std::sqrt(((float)sum2 - (float)sum * (float)sum / (float)size) / (float)size);
81 if (std::fabs(avg) > (float)precision && dev < (float)precision)
82 return (long)avg;
84 return 0;
87 void AutoSync::reset() {
88 suppress = size;
91 // Call class
93 Call::Call(CallHandler *h, Skype *sk, CallID i) :
94 QObject(h),
95 skype(sk),
96 handler(h),
97 id(i),
98 status("UNKNOWN"),
99 writer(NULL),
100 isRecording(false),
101 shouldRecord(1),
102 sync(100 * 2 * 3, 320) // approx 3 seconds
104 debug(QString("Call %1: Call object contructed").arg(id));
106 // Call objects track calls even before they are in progress and also
107 // when they are not being recorded.
109 // TODO check if we actually should record this call here
110 // and ask if we're unsure
112 skypeName = skype->getObject(QString("CALL %1 PARTNER_HANDLE").arg(id));
113 if (skypeName.isEmpty()) {
114 debug(QString("Call %1: cannot get partner handle").arg(id));
115 skypeName = "UnknownCaller";
118 displayName = skype->getObject(QString("CALL %1 PARTNER_DISPNAME").arg(id));
119 if (displayName.isEmpty()) {
120 debug(QString("Call %1: cannot get partner display name").arg(id));
121 displayName = "Unnamed Caller";
124 // Skype does not properly send updates when the CONF_ID property
125 // changes. since we need this information, check it now on all calls
126 handler->updateConfIDs();
127 // this call isn't yet in the list of calls, thus we need to
128 // explicitely check its CONF_ID
129 updateConfID();
132 Call::~Call() {
133 debug(QString("Call %1: Call object destructed").arg(id));
135 if (isRecording)
136 stopRecording();
138 delete confirmation;
140 setStatus("UNKNOWN");
142 // QT takes care of deleting servers and sockets
145 void Call::updateConfID() {
146 confID = skype->getObject(QString("CALL %1 CONF_ID").arg(id)).toLong();
149 bool Call::okToDelete() const {
150 // this is used for checking whether past calls may now be deleted.
151 // when a past call hasn't been decided yet whether it should have been
152 // recorded, then it may not be deleted until the decision has been
153 // made by the user.
155 if (isRecording)
156 return false;
158 if (confirmation)
159 /* confirmation dialog still open */
160 return false;
162 return true;
165 bool Call::statusActive() const {
166 return status == "INPROGRESS" ||
167 status == "ONHOLD" ||
168 status == "LOCALHOLD" ||
169 status == "REMOTEHOLD";
172 void Call::setStatus(const QString &s) {
173 bool wasActive = statusActive();
174 status = s;
175 bool nowActive = statusActive();
177 if (!wasActive && nowActive) {
178 emit startedCall(id, skypeName);
179 startRecording();
180 } else if (wasActive && !nowActive) {
181 // don't stop recording when we get "FINISHED". just wait for
182 // the connections to close so that we really get all the data
183 emit stoppedCall(id);
187 bool Call::statusDone() const {
188 return status == "BUSY" ||
189 status == "CANCELLED" ||
190 status == "FAILED" ||
191 status == "FINISHED" ||
192 status == "MISSED" ||
193 status == "REFUSED" ||
194 status == "VM_FAILED";
195 // TODO: see what the deal is with REDIAL_PENDING (protocol 8)
198 QString Call::constructFileName() const {
199 return getFileName(skypeName, displayName, skype->getSkypeName(),
200 skype->getObject("PROFILE FULLNAME"), timeStartRecording);
203 QString Call::constructCommentTag() const {
204 QString str("Skype call between %1%2 and %3%4.");
205 QString dn1, dn2;
206 if (!displayName.isEmpty())
207 dn1 = QString(" (") + displayName + ")";
208 dn2 = skype->getObject("PROFILE FULLNAME");
209 if (!dn2.isEmpty())
210 dn2 = QString(" (") + dn2 + ")";
211 return str.arg(skypeName, dn1, skype->getSkypeName(), dn2);
214 void Call::setShouldRecord() {
215 // this sets shouldRecord based on preferences. shouldRecord is 0 if
216 // the call should not be recorded, 1 if we should ask and 2 if we
217 // should record
219 QStringList list = preferences.get(Pref::AutoRecordYes).toList();
220 if (list.contains(skypeName)) {
221 shouldRecord = 2;
222 return;
225 list = preferences.get(Pref::AutoRecordAsk).toList();
226 if (list.contains(skypeName)) {
227 shouldRecord = 1;
228 return;
231 list = preferences.get(Pref::AutoRecordNo).toList();
232 if (list.contains(skypeName)) {
233 shouldRecord = 0;
234 return;
237 QString def = preferences.get(Pref::AutoRecordDefault).toString();
238 if (def == "yes")
239 shouldRecord = 2;
240 else if (def == "ask")
241 shouldRecord = 1;
242 else if (def == "no")
243 shouldRecord = 0;
244 else
245 shouldRecord = 1;
248 void Call::ask() {
249 confirmation = new RecordConfirmationDialog(skypeName, displayName);
250 connect(confirmation, SIGNAL(yes()), this, SLOT(confirmRecording()));
251 connect(confirmation, SIGNAL(no()), this, SLOT(denyRecording()));
254 void Call::hideConfirmation(int should) {
255 if (confirmation) {
256 delete confirmation;
257 shouldRecord = should;
261 void Call::confirmRecording() {
262 shouldRecord = 2;
263 emit showLegalInformation();
266 void Call::denyRecording() {
267 // note that the call might already be finished by now
268 shouldRecord = 0;
269 stopRecording(true);
270 removeFile();
273 void Call::removeFile() {
274 debug(QString("Removing '%1'").arg(fileName));
275 QFile::remove(fileName);
278 void Call::startRecording(bool force) {
279 if (force)
280 hideConfirmation(2);
282 if (isRecording)
283 return;
285 if (handler->isConferenceRecording(confID)) {
286 debug(QString("Call %1: call is part of a conference that is already being recorded").arg(id));
287 return;
290 if (force) {
291 emit showLegalInformation();
292 } else {
293 setShouldRecord();
294 if (shouldRecord == 0)
295 return;
296 if (shouldRecord == 1)
297 ask();
298 else // shouldRecord == 2
299 emit showLegalInformation();
302 debug(QString("Call %1: start recording").arg(id));
304 // set up encoder for appropriate format
306 timeStartRecording = QDateTime::currentDateTime();
307 QString fn = constructFileName();
309 stereo = preferences.get(Pref::OutputStereo).toBool();
310 stereoMix = preferences.get(Pref::OutputStereoMix).toInt();
312 QString format = preferences.get(Pref::OutputFormat).toString();
314 if (format == "wav")
315 writer = new WaveWriter;
316 else if (format == "mp3")
317 writer = new Mp3Writer;
318 else /*if (format == "vorbis")*/
319 writer = new VorbisWriter;
321 if (preferences.get(Pref::OutputSaveTags).toBool())
322 writer->setTags(constructCommentTag(), timeStartRecording);
324 bool b = writer->open(fn, skypeSamplingRate, stereo);
325 fileName = writer->fileName();
327 if (!b) {
328 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
329 QString(PROGRAM_NAME " could not open the file %1. Please verify the output file pattern.").arg(fileName));
330 box->setWindowModality(Qt::NonModal);
331 box->setAttribute(Qt::WA_DeleteOnClose);
332 box->show();
333 removeFile();
334 delete writer;
335 return;
338 serverLocal = new QTcpServer(this);
339 serverLocal->listen();
340 connect(serverLocal, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
341 serverRemote = new QTcpServer(this);
342 serverRemote->listen();
343 connect(serverRemote, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
345 QString rep1 = skype->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id).arg(serverLocal->serverPort()));
346 QString rep2 = skype->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id).arg(serverRemote->serverPort()));
348 if (!rep1.startsWith("ALTER CALL ") || !rep2.startsWith("ALTER CALL")) {
349 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
350 QString(PROGRAM_NAME " could not obtain the audio streams from Skype and can thus not record this call.\n\n"
351 "The replies from Skype were:\n%1\n%2").arg(rep1, rep2));
352 box->setWindowModality(Qt::NonModal);
353 box->setAttribute(Qt::WA_DeleteOnClose);
354 box->show();
355 removeFile();
356 delete writer;
357 delete serverRemote;
358 delete serverLocal;
359 return;
362 if (preferences.get(Pref::DebugWriteSyncFile).toBool()) {
363 syncFile.setFileName(fn + ".sync");
364 syncFile.open(QIODevice::WriteOnly);
365 syncTime.start();
368 isRecording = true;
369 emit startedRecording(id);
372 void Call::acceptLocal() {
373 socketLocal = serverLocal->nextPendingConnection();
374 serverLocal->close();
375 // we don't delete the server, since it contains the socket.
376 // we could reparent, but that automatic stuff of QT is great
377 connect(socketLocal, SIGNAL(readyRead()), this, SLOT(readLocal()));
378 connect(socketLocal, SIGNAL(disconnected()), this, SLOT(checkConnections()));
381 void Call::acceptRemote() {
382 socketRemote = serverRemote->nextPendingConnection();
383 serverRemote->close();
384 connect(socketRemote, SIGNAL(readyRead()), this, SLOT(readRemote()));
385 connect(socketRemote, SIGNAL(disconnected()), this, SLOT(checkConnections()));
388 void Call::readLocal() {
389 bufferLocal += socketLocal->readAll();
390 if (isRecording)
391 tryToWrite();
394 void Call::readRemote() {
395 bufferRemote += socketRemote->readAll();
396 if (isRecording)
397 tryToWrite();
400 void Call::checkConnections() {
401 if (socketLocal->state() == QAbstractSocket::UnconnectedState && socketRemote->state() == QAbstractSocket::UnconnectedState) {
402 debug(QString("Call %1: both connections closed, stop recording").arg(id));
403 stopRecording();
407 void Call::mixToMono(long samples) {
408 qint16 *localData = reinterpret_cast<qint16 *>(bufferLocal.data());
409 qint16 *remoteData = reinterpret_cast<qint16 *>(bufferRemote.data());
411 for (long i = 0; i < samples; i++)
412 localData[i] = ((qint32)localData[i] + (qint32)remoteData[i]) / (qint32)2;
415 void Call::mixToStereo(long samples, int pan) {
416 qint16 *localData = reinterpret_cast<qint16 *>(bufferLocal.data());
417 qint16 *remoteData = reinterpret_cast<qint16 *>(bufferRemote.data());
419 qint32 fl = 100 - pan;
420 qint32 fr = pan;
422 for (long i = 0; i < samples; i++) {
423 qint16 newLocal = ((qint32)localData[i] * fl + (qint32)remoteData[i] * fr + (qint32)50) / (qint32)100;
424 qint16 newRemote = ((qint32)localData[i] * fr + (qint32)remoteData[i] * fl + (qint32)50) / (qint32)100;
425 localData[i] = newLocal;
426 remoteData[i] = newRemote;
430 long Call::padBuffers() {
431 // pads the shorter buffer with silence, so they are both the same
432 // length afterwards. returns the new number of samples in each buffer
434 long l = bufferLocal.size();
435 long r = bufferRemote.size();
437 if (l < r) {
438 long amount = r - l;
439 bufferLocal.append(QByteArray(amount, 0));
440 debug(QString("Call %1: padding %2 samples on local buffer").arg(id).arg(amount / 2));
441 return r / 2;
442 } else if (l > r) {
443 long amount = l - r;
444 bufferRemote.append(QByteArray(amount, 0));
445 debug(QString("Call %1: padding %2 samples on remote buffer").arg(id).arg(amount / 2));
446 return l / 2;
449 return l / 2;
452 void Call::doSync(long s) {
453 if (s > 0) {
454 bufferLocal.append(QByteArray(s * 2, 0));
455 debug(QString("Call %1: padding %2 samples on local buffer").arg(id).arg(s));
456 } else {
457 bufferRemote.append(QByteArray(s * -2, 0));
458 debug(QString("Call %1: padding %2 samples on remote buffer").arg(id).arg(-s));
462 void Call::tryToWrite(bool flush) {
463 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
465 long samples; // number of samples to write
467 if (flush) {
468 // when flushing, we pad the shorter buffer, so that all
469 // available data is written. this shouldn't usually be a
470 // significant amount, but it might be if there was an audio
471 // I/O error in Skype.
472 samples = padBuffers();
473 } else {
474 long l = bufferLocal.size() / 2;
475 long r = bufferRemote.size() / 2;
477 sync.add(r - l);
479 long syncAmount = sync.getSync();
480 syncAmount = (syncAmount / 160) * 160;
482 if (syncAmount) {
483 doSync(syncAmount);
484 sync.reset();
485 l = bufferLocal.size() / 2;
486 r = bufferRemote.size() / 2;
489 if (syncFile.isOpen())
490 syncFile.write(QString("%1 %2 %3\n").arg(syncTime.elapsed()).arg(r - l).arg(syncAmount).toAscii().constData());
492 if (std::labs(r - l) > skypeSamplingRate * 20) {
493 // more than 20 seconds out of sync, something went
494 // wrong. avoid eating memory by accumulating data
495 long s = (r - l) / skypeSamplingRate;
496 debug(QString("Call %1: WARNING: seriously out of sync by %2s; padding").arg(id).arg(s));
497 samples = padBuffers();
498 sync.reset();
499 } else {
500 samples = l < r ? l : r;
502 // skype usually sends new PCM data every 10ms (160
503 // samples at 16kHz). let's accumulate at least 100ms
504 // of data before bothering to write it to disk
505 if (samples < skypeSamplingRate / 10)
506 return;
510 // got new samples to write to file, or have to flush. note that we
511 // have to flush even if samples == 0
513 bool success;
515 if (!stereo) {
516 // mono
517 mixToMono(samples);
518 QByteArray dummy;
519 success = writer->write(bufferLocal, dummy, samples, flush);
520 bufferRemote.remove(0, samples * 2);
521 } else if (stereoMix == 0) {
522 // local left, remote right
523 success = writer->write(bufferLocal, bufferRemote, samples, flush);
524 } else if (stereoMix == 100) {
525 // local right, remote left
526 success = writer->write(bufferRemote, bufferLocal, samples, flush);
527 } else {
528 mixToStereo(samples, stereoMix);
529 success = writer->write(bufferLocal, bufferRemote, samples, flush);
532 if (!success) {
533 QMessageBox *box = new QMessageBox(QMessageBox::Critical, PROGRAM_NAME " - Error",
534 QString(PROGRAM_NAME " encountered an error while writing this call to disk. Recording terminated."));
535 box->setWindowModality(Qt::NonModal);
536 box->setAttribute(Qt::WA_DeleteOnClose);
537 box->show();
538 stopRecording(false);
539 return;
542 // the writer will remove the samples from the buffers
543 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
545 // TODO: handle the case where the two streams get out of sync (buffers
546 // not equally fulled by a significant amount). does skype document
547 // whether we always get two nice, equal, in-sync streams, even if
548 // there have been transmission errors? perl-script behavior: if out
549 // of sync by more than 6.4ms, then remove 1ms from the stream that's
550 // ahead.
553 void Call::stopRecording(bool flush) {
554 if (!isRecording)
555 return;
557 debug(QString("Call %1: stop recording").arg(id));
559 // NOTE: we don't delete the sockets here, because we may be here as a
560 // reaction to their disconnected() signals; and they don't like being
561 // deleted during their signals. we don't delete the servers either,
562 // since they own the sockets and we're too lazy to reparent. it's
563 // easiest to let QT handle all this on its own. there will be some
564 // memory wasted if you start/stop recording within the same call a few
565 // times, but unless you do it thousands of times, the waste is more
566 // than acceptable.
568 // flush data to writer
569 if (flush)
570 tryToWrite(true);
571 writer->close();
572 delete writer;
574 if (syncFile.isOpen())
575 syncFile.close();
577 // we must disconnect all signals from the sockets first, so that upon
578 // closing them it won't call checkConnections() and we don't land here
579 // recursively again
580 disconnect(socketLocal, 0, this, 0);
581 disconnect(socketRemote, 0, this, 0);
582 socketLocal->close();
583 socketRemote->close();
585 isRecording = false;
586 emit stoppedRecording(id);
589 // ---- CallHandler ----
591 CallHandler::CallHandler(QObject *parent, Skype *s) : QObject(parent), skype(s) {
594 CallHandler::~CallHandler() {
595 prune();
597 QList<Call *> list = calls.values();
598 if (!list.isEmpty()) {
599 debug(QString("Destroying CallHandler, these calls still exist:"));
600 for (int i = 0; i < list.size(); i++) {
601 Call *c = list.at(i);
602 debug(QString(" call %1, status=%2, okToDelete=%3").arg(c->getID()).arg(c->getStatus()).arg(c->okToDelete()));
606 delete legalInformationDialog;
609 void CallHandler::updateConfIDs() {
610 QList<Call *> list = calls.values();
611 for (int i = 0; i < list.size(); i++)
612 list.at(i)->updateConfID();
615 bool CallHandler::isConferenceRecording(CallID id) const {
616 QList<Call *> list = calls.values();
617 for (int i = 0; i < list.size(); i++) {
618 Call *c = list.at(i);
619 if (c->getConfID() == id && c->getIsRecording())
620 return true;
622 return false;
625 void CallHandler::callCmd(const QStringList &args) {
626 CallID id = args.at(0).toInt();
628 if (ignore.contains(id))
629 return;
631 bool newCall = false;
633 Call *call;
635 if (calls.contains(id)) {
636 call = calls[id];
637 } else {
638 call = new Call(this, skype, id);
639 calls[id] = call;
640 newCall = true;
642 connect(call, SIGNAL(startedCall(int, const QString &)), this, SIGNAL(startedCall(int, const QString &)));
643 connect(call, SIGNAL(stoppedCall(int)), this, SIGNAL(stoppedCall(int)));
644 connect(call, SIGNAL(startedRecording(int)), this, SIGNAL(startedRecording(int)));
645 connect(call, SIGNAL(stoppedRecording(int)), this, SIGNAL(stoppedRecording(int)));
646 connect(call, SIGNAL(showLegalInformation()), this, SLOT(showLegalInformation()));
649 QString subCmd = args.at(1);
651 if (subCmd == "STATUS")
652 call->setStatus(args.at(2));
653 else if (newCall && subCmd == "DURATION")
654 // this is where we start recording calls that are already
655 // running, for example if the user starts this program after
656 // the call has been placed
657 call->setStatus("INPROGRESS");
659 prune();
662 void CallHandler::prune() {
663 QList<Call *> list = calls.values();
664 for (int i = 0; i < list.size(); i++) {
665 Call *c = list.at(i);
666 if (c->statusDone() && c->okToDelete()) {
667 // we ignore this call from now on, because Skype might still send
668 // us information about it, like "SEEN" or "VAA_INPUT_STATUS"
669 calls.remove(c->getID());
670 ignore.insert(c->getID());
671 delete c;
676 void CallHandler::startRecording(int id) {
677 if (!calls.contains(id))
678 return;
680 calls[id]->startRecording(true);
683 void CallHandler::stopRecording(int id) {
684 if (!calls.contains(id))
685 return;
687 Call *call = calls[id];
688 call->stopRecording();
689 call->hideConfirmation(2);
692 void CallHandler::stopRecordingAndDelete(int id) {
693 if (!calls.contains(id))
694 return;
696 Call *call = calls[id];
697 call->stopRecording();
698 call->removeFile();
699 call->hideConfirmation(0);
702 void CallHandler::showLegalInformation() {
703 if (preferences.get(Pref::SuppressLegalInformation).toBool())
704 return;
706 if (!legalInformationDialog)
707 legalInformationDialog = new LegalInformationDialog;
709 legalInformationDialog->raise();
710 legalInformationDialog->activateWindow();