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
24 #include <QStringList>
28 #include <QMessageBox>
36 #include "wavewriter.h"
37 #include "mp3writer.h"
38 #include "vorbiswriter.h"
39 #include "preferences.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
) :
55 delays
= new long[size
];
56 std::memset(delays
, 0, sizeof(long) * size
);
59 AutoSync::~AutoSync() {
63 void AutoSync::add(long d
) {
64 long old
= delays
[index
];
66 sum2
+= (qint64
)d
* (qint64
)d
- (qint64
)old
* (qint64
)old
;
74 long AutoSync::getSync() {
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
)
87 void AutoSync::reset() {
93 Call::Call(CallHandler
*h
, Skype
*sk
, CallID i
) :
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
133 debug(QString("Call %1: Call object destructed").arg(id
));
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
159 /* confirmation dialog still open */
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();
175 bool nowActive
= statusActive();
177 if (!wasActive
&& nowActive
) {
178 emit
startedCall(id
, skypeName
);
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.");
206 if (!displayName
.isEmpty())
207 dn1
= QString(" (") + displayName
+ ")";
208 dn2
= skype
->getObject("PROFILE FULLNAME");
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
219 QStringList list
= preferences
.get(Pref::AutoRecordYes
).toList();
220 if (list
.contains(skypeName
)) {
225 list
= preferences
.get(Pref::AutoRecordAsk
).toList();
226 if (list
.contains(skypeName
)) {
231 list
= preferences
.get(Pref::AutoRecordNo
).toList();
232 if (list
.contains(skypeName
)) {
237 QString def
= preferences
.get(Pref::AutoRecordDefault
).toString();
240 else if (def
== "ask")
242 else if (def
== "no")
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
) {
257 shouldRecord
= should
;
261 void Call::confirmRecording() {
263 emit
showLegalInformation();
266 void Call::denyRecording() {
267 // note that the call might already be finished by now
273 void Call::removeFile() {
274 debug(QString("Removing '%1'").arg(fileName
));
275 QFile::remove(fileName
);
278 void Call::startRecording(bool force
) {
285 if (handler
->isConferenceRecording(confID
)) {
286 debug(QString("Call %1: call is part of a conference that is already being recorded").arg(id
));
291 emit
showLegalInformation();
294 if (shouldRecord
== 0)
296 if (shouldRecord
== 1)
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();
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();
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
);
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
);
362 if (preferences
.get(Pref::DebugWriteSyncFile
).toBool()) {
363 syncFile
.setFileName(fn
+ ".sync");
364 syncFile
.open(QIODevice::WriteOnly
);
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();
394 void Call::readRemote() {
395 bufferRemote
+= socketRemote
->readAll();
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
));
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
;
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();
439 bufferLocal
.append(QByteArray(amount
, 0));
440 debug(QString("Call %1: padding %2 samples on local buffer").arg(id
).arg(amount
/ 2));
444 bufferRemote
.append(QByteArray(amount
, 0));
445 debug(QString("Call %1: padding %2 samples on remote buffer").arg(id
).arg(amount
/ 2));
452 void Call::doSync(long s
) {
454 bufferLocal
.append(QByteArray(s
* 2, 0));
455 debug(QString("Call %1: padding %2 samples on local buffer").arg(id
).arg(s
));
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
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();
474 long l
= bufferLocal
.size() / 2;
475 long r
= bufferRemote
.size() / 2;
479 long syncAmount
= sync
.getSync();
480 syncAmount
= (syncAmount
/ 160) * 160;
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();
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)
510 // got new samples to write to file, or have to flush. note that we
511 // have to flush even if samples == 0
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
);
528 mixToStereo(samples
, stereoMix
);
529 success
= writer
->write(bufferLocal
, bufferRemote
, samples
, flush
);
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
);
538 stopRecording(false);
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
553 void Call::stopRecording(bool flush
) {
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
568 // flush data to writer
574 if (syncFile
.isOpen())
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
580 disconnect(socketLocal
, 0, this, 0);
581 disconnect(socketRemote
, 0, this, 0);
582 socketLocal
->close();
583 socketRemote
->close();
586 emit
stoppedRecording(id
);
589 // ---- CallHandler ----
591 CallHandler::CallHandler(QObject
*parent
, Skype
*s
) : QObject(parent
), skype(s
) {
594 CallHandler::~CallHandler() {
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())
625 void CallHandler::callCmd(const QStringList
&args
) {
626 CallID id
= args
.at(0).toInt();
628 if (ignore
.contains(id
))
631 bool newCall
= false;
635 if (calls
.contains(id
)) {
638 call
= new Call(this, skype
, id
);
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");
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());
676 void CallHandler::startRecording(int id
) {
677 if (!calls
.contains(id
))
680 calls
[id
]->startRecording(true);
683 void CallHandler::stopRecording(int id
) {
684 if (!calls
.contains(id
))
687 Call
*call
= calls
[id
];
688 call
->stopRecording();
689 call
->hideConfirmation(2);
692 void CallHandler::stopRecordingAndDelete(int id
) {
693 if (!calls
.contains(id
))
696 Call
*call
= calls
[id
];
697 call
->stopRecording();
699 call
->hideConfirmation(0);
702 void CallHandler::showLegalInformation() {
703 if (preferences
.get(Pref::SuppressLegalInformation
).toBool())
706 if (!legalInformationDialog
)
707 legalInformationDialog
= new LegalInformationDialog
;
709 legalInformationDialog
->raise();
710 legalInformationDialog
->activateWindow();