3 Copyright (C) 2008 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>
35 #include "wavewriter.h"
36 #include "mp3writer.h"
37 #include "preferences.h"
41 Call::Call(QObject
*p
, Skype
*sk
, CallID i
) :
50 debug(QString("Call %1: Call object contructed").arg(id
));
52 // Call objects track calls even before they are in progress and also
53 // when they are not being recorded.
55 // TODO check if we actually should record this call here
56 // and ask if we're unsure
58 skypeName
= skype
->getObject(QString("CALL %1 PARTNER_HANDLE").arg(id
));
59 if (skypeName
.isEmpty()) {
60 debug(QString("Call %1: cannot get partner handle").arg(id
));
61 skypeName
= "UnknownCaller";
64 displayName
= skype
->getObject(QString("CALL %1 PARTNER_DISPNAME").arg(id
));
65 if (displayName
.isEmpty()) {
66 debug(QString("Call %1: cannot get partner display name").arg(id
));
67 displayName
= "Unnamed Caller";
72 debug(QString("Call %1: Call object destructed").arg(id
));
81 // QT takes care of deleting servers and sockets
84 bool Call::okToDelete() const {
85 // this is used for checking whether past calls may now be deleted.
86 // when a past call hasn't been decided yet whether it should have been
87 // recorded, then it may not be deleted until the decision has been
94 /* confirmation dialog still open */
100 bool Call::statusActive() const {
101 return status
== "INPROGRESS" ||
102 status
== "ONHOLD" ||
103 status
== "LOCALHOLD" ||
104 status
== "REMOTEHOLD";
107 void Call::setStatus(const QString
&s
) {
108 bool wasActive
= statusActive();
110 bool nowActive
= statusActive();
112 if (!wasActive
&& nowActive
)
113 emit
startedCall(id
, skypeName
);
114 else if (wasActive
&& !nowActive
)
115 emit
stoppedCall(id
);
118 bool Call::statusDone() const {
119 return status
== "BUSY" ||
120 status
== "CANCELLED" ||
121 status
== "FAILED" ||
122 status
== "FINISHED" ||
123 status
== "MISSED" ||
125 // TODO: see what the deal is with REDIAL_PENDING (protocol 8)
128 QString
Call::constructFileName() const {
129 return getFileName(skypeName
, displayName
, skype
->getSkypeName(),
130 skype
->getObject("PROFILE FULLNAME"), timeStartRecording
);
133 QString
Call::constructCommentTag() const {
134 QString
str("Skype call between %1%2 and %3%4.");
136 if (!displayName
.isEmpty())
137 dn1
= QString(" (") + displayName
+ ")";
138 dn2
= skype
->getObject("PROFILE FULLNAME");
140 dn2
= QString(" (") + dn2
+ ")";
141 return str
.arg(skypeName
, dn1
, skype
->getSkypeName(), dn2
);
144 void Call::setShouldRecord() {
145 // this sets shouldRecord based on preferences. shouldRecord is 0 if
146 // the call should not be recorded, 1 if we should ask and 2 if we
149 QStringList list
= preferences
.get("autorecord.yes").toList();
150 if (list
.contains(skypeName
)) {
155 list
= preferences
.get("autorecord.ask").toList();
156 if (list
.contains(skypeName
)) {
161 list
= preferences
.get("autorecord.no").toList();
162 if (list
.contains(skypeName
)) {
167 QString def
= preferences
.get("autorecord.default").toString();
170 else if (def
== "ask")
172 else if (def
== "no")
179 confirmation
= new RecordConfirmationDialog(skypeName
, displayName
);
180 connect(confirmation
, SIGNAL(yes()), this, SLOT(confirmRecording()));
181 connect(confirmation
, SIGNAL(no()), this, SLOT(denyRecording()));
184 void Call::hideConfirmation(int should
) {
187 shouldRecord
= should
;
191 void Call::confirmRecording() {
193 emit
showLegalInformation();
196 void Call::denyRecording() {
197 // note that the call might already be finished by now
203 void Call::removeFile() {
204 debug(QString("Removing '%1'").arg(fileName
));
205 QFile::remove(fileName
);
208 void Call::startRecording(bool force
) {
216 emit
showLegalInformation();
219 if (shouldRecord
== 0)
221 if (shouldRecord
== 1)
223 else // shouldRecord == 2
224 emit
showLegalInformation();
227 debug(QString("Call %1: start recording").arg(id
));
229 // set up encoder for appropriate format
231 timeStartRecording
= QDateTime::currentDateTime();
232 QString fn
= constructFileName();
234 QString sm
= preferences
.get("output.channelmode").toString();
238 else if (sm
== "oerets")
240 else /* if (sm == "stereo") */
243 QString format
= preferences
.get("output.format").toString();
246 writer
= new WaveWriter
;
247 else /* if (format == "mp3") */
248 writer
= new Mp3Writer
;
250 if (preferences
.get("output.savetags").toBool())
251 writer
->setTags(constructCommentTag(), timeStartRecording
);
253 bool b
= writer
->open(fn
, 16000, channelMode
!= 0);
254 fileName
= writer
->fileName();
257 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
258 QString(PROGRAM_NAME
" could not open the file %1. Please verify the output file pattern.").arg(fileName
));
259 box
->setWindowModality(Qt::NonModal
);
260 box
->setAttribute(Qt::WA_DeleteOnClose
);
267 serverLocal
= new QTcpServer(this);
268 serverLocal
->listen();
269 connect(serverLocal
, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
270 serverRemote
= new QTcpServer(this);
271 serverRemote
->listen();
272 connect(serverRemote
, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
274 QString rep1
= skype
->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id
).arg(serverLocal
->serverPort()));
275 QString rep2
= skype
->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id
).arg(serverRemote
->serverPort()));
277 if (!rep1
.startsWith("ALTER CALL ") || !rep2
.startsWith("ALTER CALL")) {
278 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
279 QString(PROGRAM_NAME
" could not obtain the audio streams from Skype and can thus not record this call.\n\n"
280 "The replies from Skype were:\n%1\n%2").arg(rep1
, rep2
));
281 box
->setWindowModality(Qt::NonModal
);
282 box
->setAttribute(Qt::WA_DeleteOnClose
);
292 emit
startedRecording(id
);
295 void Call::acceptLocal() {
296 socketLocal
= serverLocal
->nextPendingConnection();
297 serverLocal
->close();
298 // we don't delete the server, since it contains the socket.
299 // we could reparent, but that automatic stuff of QT is great
300 connect(socketLocal
, SIGNAL(readyRead()), this, SLOT(readLocal()));
301 connect(socketLocal
, SIGNAL(disconnected()), this, SLOT(checkConnections()));
304 void Call::acceptRemote() {
305 socketRemote
= serverRemote
->nextPendingConnection();
306 serverRemote
->close();
307 connect(socketRemote
, SIGNAL(readyRead()), this, SLOT(readRemote()));
308 connect(socketRemote
, SIGNAL(disconnected()), this, SLOT(checkConnections()));
311 void Call::readLocal() {
312 bufferLocal
+= socketLocal
->readAll();
317 void Call::readRemote() {
318 bufferRemote
+= socketRemote
->readAll();
323 void Call::checkConnections() {
324 if (socketLocal
->state() == QAbstractSocket::UnconnectedState
&& socketRemote
->state() == QAbstractSocket::UnconnectedState
) {
325 debug(QString("Call %1: both connections closed, stop recording").arg(id
));
330 void Call::mixToMono(long samples
) {
331 long offset
= bufferMono
.size();
332 bufferMono
.resize(offset
+ samples
* 2);
334 qint16
*monoData
= reinterpret_cast<qint16
*>(bufferMono
.data()) + offset
;
335 qint16
*localData
= reinterpret_cast<qint16
*>(bufferLocal
.data());
336 qint16
*remoteData
= reinterpret_cast<qint16
*>(bufferRemote
.data());
338 for (long i
= 0; i
< samples
; i
++) {
339 long sum
= localData
[i
] + remoteData
[i
];
342 else if (sum
> 32767)
347 bufferLocal
.remove(0, samples
* 2);
348 bufferRemote
.remove(0, samples
* 2);
351 long Call::padBuffers() {
352 // pads the shorter buffer with silence, so they are both the same
353 // length afterwards. returns the new number of samples in each buffer
355 long l
= bufferLocal
.size();
356 long r
= bufferRemote
.size();
360 bufferLocal
.append(QByteArray(amount
, 0));
361 debug(QString("Call %1: padding %2 samples on local buffer").arg(id
).arg(amount
/ 2));
365 bufferRemote
.append(QByteArray(amount
, 0));
366 debug(QString("Call %1: padding %2 samples on remote buffer").arg(id
).arg(amount
/ 2));
373 void Call::tryToWrite(bool flush
) {
374 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
376 long samples
; // number of samples to write
379 // when flushing, we pad the shorter buffer, so that all
380 // available data is written. this shouldn't usually be a
381 // significant amount, but it might be if there was an audio
382 // I/O error in Skype.
383 samples
= padBuffers();
385 long l
= bufferLocal
.size() / 2;
386 long r
= bufferRemote
.size() / 2;
388 if (std::labs(l
- r
) > 16000 * 10) {
389 // more than 10 seconds out of sync, something went
390 // wrong. avoid eating memory by accumulating data
391 debug(QString("Call %1: WARNING: seriously out of sync!").arg(id
));
392 samples
= padBuffers();
394 samples
= l
< r
? l
: r
;
396 // skype usually sends new PCM data every 10ms (160
397 // samples at 16kHz). let's accumulate at least 100ms
398 // of data before bothering to write it to disk
404 // got new samples to write to file, or have to flush. note that we
405 // have to flush even if samples == 0
409 if (channelMode
== 0) {
413 success
= writer
->write(bufferMono
, dummy
, samples
, flush
);
414 } else if (channelMode
== 1) {
416 success
= writer
->write(bufferLocal
, bufferRemote
, samples
, flush
);
417 } else if (channelMode
== 2) {
419 success
= writer
->write(bufferRemote
, bufferLocal
, samples
, flush
);
425 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
426 QString(PROGRAM_NAME
" encountered an error while writing this call to disk. Recording terminated."));
427 box
->setWindowModality(Qt::NonModal
);
428 box
->setAttribute(Qt::WA_DeleteOnClose
);
430 stopRecording(false);
434 // the writer will remove the samples from the buffers
435 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
437 // TODO: handle the case where the two streams get out of sync (buffers
438 // not equally fulled by a significant amount). does skype document
439 // whether we always get two nice, equal, in-sync streams, even if
440 // there have been transmission errors? perl-script behavior: if out
441 // of sync by more than 6.4ms, then remove 1ms from the stream that's
445 void Call::stopRecording(bool flush
) {
449 debug(QString("Call %1: stop recording").arg(id
));
451 // NOTE: we don't delete the sockets here, because we may be here as a
452 // reaction to their disconnected() signals; and they don't like being
453 // deleted during their signals. we don't delete the servers either,
454 // since they own the sockets and we're too lazy to reparent. it's
455 // easiest to let QT handle all this on its own. there will be some
456 // memory wasted if you start/stop recording within the same call a few
457 // times, but unless you do it thousands of times, the waste is more
460 // flush data to writer
466 // we must disconnect all signals from the sockets first, so that upon
467 // closing them it won't call checkConnections() and we don't land here
469 disconnect(socketLocal
, 0, this, 0);
470 disconnect(socketRemote
, 0, this, 0);
471 socketLocal
->close();
472 socketRemote
->close();
475 emit
stoppedRecording(id
);
478 // ---- CallHandler ----
480 CallHandler::CallHandler(QObject
*parent
, Skype
*s
) : QObject(parent
), skype(s
) {
483 CallHandler::~CallHandler() {
486 QList
<Call
*> list
= calls
.values();
487 if (!list
.isEmpty()) {
488 debug(QString("Destroying CallHandler, these calls still exist:"));
489 for (int i
= 0; i
< list
.size(); i
++) {
490 Call
*c
= list
.at(i
);
491 debug(QString(" call %1, status=%2, okToDelete=%3").arg(c
->getID()).arg(c
->getStatus()).arg(c
->okToDelete()));
495 delete legalInformationDialog
;
498 void CallHandler::callCmd(const QStringList
&args
) {
499 CallID id
= args
.at(0).toInt();
501 if (ignore
.contains(id
))
504 bool newCall
= false;
508 if (calls
.contains(id
)) {
511 call
= new Call(this, skype
, id
);
515 connect(call
, SIGNAL(startedCall(int, const QString
&)), this, SIGNAL(startedCall(int, const QString
&)));
516 connect(call
, SIGNAL(stoppedCall(int)), this, SIGNAL(stoppedCall(int)));
517 connect(call
, SIGNAL(startedRecording(int)), this, SIGNAL(startedRecording(int)));
518 connect(call
, SIGNAL(stoppedRecording(int)), this, SIGNAL(stoppedRecording(int)));
519 connect(call
, SIGNAL(showLegalInformation()), this, SLOT(showLegalInformation()));
522 QString subCmd
= args
.at(1);
524 if (subCmd
== "STATUS") {
525 QString a
= args
.at(2);
528 if (a
== "INPROGRESS")
529 call
->startRecording();
531 // don't stop recording when we get "FINISHED". just wait for
532 // the connections to close so that we really get all the data
533 } else if (subCmd
== "DURATION") {
534 /* this is where we start recording calls that are already running, for
535 example if the user starts this program after the call has been placed */
536 call
->setStatus("INPROGRESS");
538 call
->startRecording();
544 void CallHandler::prune() {
545 QList
<Call
*> list
= calls
.values();
546 for (int i
= 0; i
< list
.size(); i
++) {
547 Call
*c
= list
.at(i
);
548 if (c
->statusDone() && c
->okToDelete()) {
549 // we ignore this call from now on, because Skype might still send
550 // us information about it, like "SEEN" or "VAA_INPUT_STATUS"
551 calls
.remove(c
->getID());
552 ignore
.insert(c
->getID());
558 void CallHandler::startRecording(int id
) {
559 if (!calls
.contains(id
))
562 calls
[id
]->startRecording(true);
565 void CallHandler::stopRecording(int id
) {
566 if (!calls
.contains(id
))
569 Call
*call
= calls
[id
];
570 call
->stopRecording();
571 call
->hideConfirmation(2);
574 void CallHandler::stopRecordingAndDelete(int id
) {
575 if (!calls
.contains(id
))
578 Call
*call
= calls
[id
];
579 call
->stopRecording();
581 call
->hideConfirmation(0);
584 void CallHandler::showLegalInformation() {
585 if (preferences
.get("suppress.legalinformation").toBool())
588 if (!legalInformationDialog
)
589 legalInformationDialog
= new LegalInformationDialog
;
591 legalInformationDialog
->raise();
592 legalInformationDialog
->activateWindow();