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>
34 #include "wavewriter.h"
35 #include "mp3writer.h"
36 #include "preferences.h"
40 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 void Call::setStatus(const QString
&s
) {
101 bool wasInProgress
= status
== "INPROGRESS";
103 bool nowInProgress
= status
== "INPROGRESS";
105 if (!wasInProgress
&& nowInProgress
)
106 emit
startedCall(skypeName
);
107 else if (wasInProgress
&& !nowInProgress
)
112 QString
escape(const QString
&s
) {
114 out
.replace('%', "%%");
115 out
.replace('&', "&&");
120 QString
Call::constructFileName() const {
121 QString path
= getOutputPath();
122 QString fileName
= preferences
.get("output.pattern").toString();
124 fileName
.replace("&s", escape(skypeName
));
126 //fileName.replace("&f", escape(fullName));
127 //fileName.replace("&t", escape(mySkypeName));
128 //fileName.replace("&g", escape(myFullName));
129 fileName
.replace("&&", "&");
131 // TODO: uhm, does QT provide any time formatting the strftime() way?
132 char *buf
= new char[fileName
.size() + 1024];
133 time_t t
= std::time(NULL
);
134 struct tm
*tm
= std::localtime(&t
);
135 std::strftime(buf
, fileName
.size() + 1024, fileName
.toUtf8().constData(), tm
);
139 return path
+ '/' + fileName
;
142 void Call::setShouldRecord() {
143 // this sets shouldRecord based on preferences. shouldRecord is 0 if
144 // the call should not be recorded, 1 if we should ask and 2 if we
147 QStringList list
= preferences
.get("autorecord.yes").toList();
148 if (list
.contains(skypeName
)) {
153 list
= preferences
.get("autorecord.ask").toList();
154 if (list
.contains(skypeName
)) {
159 list
= preferences
.get("autorecord.no").toList();
160 if (list
.contains(skypeName
)) {
165 QString def
= preferences
.get("autorecord.default").toString();
168 else if (def
== "ask")
170 else if (def
== "no")
177 confirmation
= new RecordConfirmationDialog(skypeName
, displayName
);
178 connect(confirmation
, SIGNAL(yes()), this, SLOT(confirmRecording()));
179 connect(confirmation
, SIGNAL(no()), this, SLOT(denyRecording()));
182 void Call::hideConfirmation(int should
) {
187 shouldRecord
= should
;
190 void Call::confirmRecording() {
195 void Call::denyRecording() {
196 // 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
) {
217 if (shouldRecord
== 0)
219 if (shouldRecord
== 1)
223 debug(QString("Call %1: start recording").arg(id
));
225 // set up encoder for appropriate format
227 QString fn
= constructFileName();
229 QString sm
= preferences
.get("output.channelmode").toString();
233 else if (sm
== "oerets")
235 else /* if (sm == "stereo") */
238 QString format
= preferences
.get("output.format").toString();
241 writer
= new WaveWriter
;
242 else /* if (format == "mp3") */
243 writer
= new Mp3Writer
;
245 bool b
= writer
->open(fn
, 16000, channelMode
!= 0);
246 fileName
= writer
->fileName();
249 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
250 QString(PROGRAM_NAME
" could not open the file %1. Please verify the output file pattern.").arg(fileName
));
251 box
->setWindowModality(Qt::NonModal
);
252 box
->setAttribute(Qt::WA_DeleteOnClose
);
259 serverLocal
= new QTcpServer(this);
260 serverLocal
->listen();
261 connect(serverLocal
, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
262 serverRemote
= new QTcpServer(this);
263 serverRemote
->listen();
264 connect(serverRemote
, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
266 QString rep1
= skype
->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id
).arg(serverLocal
->serverPort()));
267 QString rep2
= skype
->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id
).arg(serverRemote
->serverPort()));
269 if (!rep1
.startsWith("ALTER CALL ") || !rep2
.startsWith("ALTER CALL")) {
270 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
271 QString(PROGRAM_NAME
" could not obtain the audio streams from Skype and can thus not record this call.\n\n"
272 "The replies from Skype were:\n%1\n%2").arg(rep1
, rep2
));
273 box
->setWindowModality(Qt::NonModal
);
274 box
->setAttribute(Qt::WA_DeleteOnClose
);
284 emit
startedRecording();
287 void Call::acceptLocal() {
288 socketLocal
= serverLocal
->nextPendingConnection();
289 serverLocal
->close();
290 // we don't delete the server, since it contains the socket.
291 // we could reparent, but that automatic stuff of QT is great
292 connect(socketLocal
, SIGNAL(readyRead()), this, SLOT(readLocal()));
293 connect(socketLocal
, SIGNAL(disconnected()), this, SLOT(checkConnections()));
296 void Call::acceptRemote() {
297 socketRemote
= serverRemote
->nextPendingConnection();
298 serverRemote
->close();
299 connect(socketRemote
, SIGNAL(readyRead()), this, SLOT(readRemote()));
300 connect(socketRemote
, SIGNAL(disconnected()), this, SLOT(checkConnections()));
303 void Call::readLocal() {
304 bufferLocal
+= socketLocal
->readAll();
309 void Call::readRemote() {
310 bufferRemote
+= socketRemote
->readAll();
315 void Call::checkConnections() {
316 if (socketLocal
->state() == QAbstractSocket::UnconnectedState
&& socketRemote
->state() == QAbstractSocket::UnconnectedState
) {
317 debug(QString("Call %1: both connections closed, stop recording").arg(id
));
322 void Call::mixToMono(int samples
) {
323 long offset
= bufferMono
.size();
324 bufferMono
.resize(offset
+ samples
* 2);
326 qint16
*monoData
= reinterpret_cast<qint16
*>(bufferMono
.data()) + offset
;
327 qint16
*localData
= reinterpret_cast<qint16
*>(bufferLocal
.data());
328 qint16
*remoteData
= reinterpret_cast<qint16
*>(bufferRemote
.data());
330 for (int i
= 0; i
< samples
; i
++) {
331 long sum
= localData
[i
] + remoteData
[i
];
334 else if (sum
> 32767)
339 bufferLocal
.remove(0, samples
* 2);
340 bufferRemote
.remove(0, samples
* 2);
343 void Call::tryToWrite(bool flush
) {
344 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
346 int l
= bufferLocal
.size();
347 int r
= bufferRemote
.size();
348 int samples
= (l
< r
? l
: r
) / 2;
350 // skype usually sends us more PCM data every 10ms, i.e. 160 samples.
351 // (skype operates at 16kHz) let's accumulate at least 100ms of data
352 // before bothering to write it to disk
353 if (samples
< 1600 && !flush
)
356 // got new samples to write to file, or have to flush
360 if (channelMode
== 0) {
363 success
= writer
->write(bufferMono
, bufferMono
, samples
, flush
);
364 } else if (channelMode
== 1) {
366 success
= writer
->write(bufferLocal
, bufferRemote
, samples
, flush
);
367 } else if (channelMode
== 2) {
369 success
= writer
->write(bufferRemote
, bufferLocal
, samples
, flush
);
375 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
376 QString(PROGRAM_NAME
" encountered an error while writing this call to disk. Recording terminated."));
377 box
->setWindowModality(Qt::NonModal
);
378 box
->setAttribute(Qt::WA_DeleteOnClose
);
380 stopRecording(false);
384 // the writer will remove the samples from the buffers
385 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
387 // TODO: handle the case where the two streams get out of sync (buffers
388 // not equally fulled by a significant amount). does skype document
389 // whether we always get two nice, equal, in-sync streams, even if
390 // there have been transmission errors? perl-script behavior: if out
391 // of sync by more than 6.4ms, then remove 1ms from the stream that's
395 void Call::stopRecording(bool flush
) {
399 debug(QString("Call %1: stop recording").arg(id
));
401 // NOTE: we don't delete the sockets here, because we may be here as a
402 // reaction to their disconnected() signals; and they don't like being
403 // deleted during their signals. we don't delete the servers either,
404 // since they own the sockets and we're too lazy to reparent. it's
405 // easiest to let QT handle all this on its own. there will be some
406 // memory wasted if you start/stop recording within the same call a few
407 // times, but unless you do it thousands of times, the waste is more
410 // flush data to writer
416 // we must disconnect all signals from the sockets first, so that upon
417 // closing them it won't call checkConnections() and we don't land here
419 disconnect(socketLocal
, 0, this, 0);
420 disconnect(socketRemote
, 0, this, 0);
421 socketLocal
->close();
422 socketRemote
->close();
425 emit
stoppedRecording();
428 // ---- CallHandler ----
430 CallHandler::CallHandler(Skype
*s
) : skype(s
), currentCall(-1) {
433 void CallHandler::callCmd(const QStringList
&args
) {
434 CallID id
= args
.at(0).toInt();
436 if (ignore
.contains(id
))
439 bool newCall
= false;
443 if (calls
.contains(id
)) {
446 call
= new Call(this, skype
, id
);
450 connect(call
, SIGNAL(startedCall(const QString
&)), this, SIGNAL(startedCall(const QString
&)));
451 connect(call
, SIGNAL(stoppedCall()), this, SIGNAL(stoppedCall()));
452 connect(call
, SIGNAL(startedRecording()), this, SIGNAL(startedRecording()));
453 connect(call
, SIGNAL(stoppedRecording()), this, SIGNAL(stoppedRecording()));
456 // this holds the current call. skype currently only allows for one
457 // call at any time, so this should work ok
460 QString subCmd
= args
.at(1);
462 if (subCmd
== "STATUS") {
463 QString a
= args
.at(2);
466 if (a
== "INPROGRESS")
467 call
->startRecording();
469 // don't stop recording when we get "FINISHED". just wait for
470 // the connections to close so that we really get all the data
471 } else if (subCmd
== "DURATION") {
472 /* this is where we start recording calls that are already running, for
473 example if the user starts this program after the call has been placed */
474 call
->setStatus("INPROGRESS");
476 call
->startRecording();
479 QList
<Call
*> list
= calls
.values();
480 for (int i
= 0; i
< list
.size(); i
++) {
481 Call
*c
= list
.at(i
);
482 QString status
= c
->getStatus();
483 if ((status
== "FINISHED" || status
== "CANCELLED") && c
->okToDelete()) {
484 calls
.remove(c
->getID());
490 void CallHandler::startRecording() {
491 if (!calls
.contains(currentCall
))
494 calls
[currentCall
]->startRecording(true);
497 void CallHandler::stopRecording() {
498 if (!calls
.contains(currentCall
))
501 Call
*call
= calls
[currentCall
];
502 call
->stopRecording();
503 call
->hideConfirmation(2);
506 void CallHandler::stopRecordingAndDelete() {
507 if (!calls
.contains(currentCall
))
510 Call
*call
= calls
[currentCall
];
511 call
->stopRecording();
513 call
->hideConfirmation(0);