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>
29 #include <QHBoxLayout>
30 #include <QVBoxLayout>
31 #include <QPushButton>
35 #include <QApplication>
43 #include "wavewriter.h"
44 #include "mp3writer.h"
45 #include "preferences.h"
48 Call::Call(Skype
*sk
, CallID i
) :
54 debug(QString("Call %1: Call object contructed").arg(id
));
56 // Call objects track calls even before they are in progress and also
57 // when they are not being recorded.
59 // TODO check if we actually should record this call here
60 // and ask if we're unsure
62 skypeName
= skype
->getObject(QString("CALL %1 PARTNER_HANDLE").arg(id
));
63 if (skypeName
.isEmpty()) {
64 debug(QString("Call %1: cannot get partner handle").arg(id
));
65 skypeName
= "UnknownCaller";
68 displayName
= skype
->getObject(QString("CALL %1 PARTNER_DISPNAME").arg(id
));
69 if (displayName
.isEmpty()) {
70 debug(QString("Call %1: cannot get partner display name").arg(id
));
71 displayName
= "Unnamed Caller";
76 debug(QString("Call %1: Call object destructed").arg(id
));
81 // QT takes care of deleting servers and sockets
85 QString
escape(const QString
&s
) {
87 out
.replace('%', "%%");
88 out
.replace('&', "&&");
93 QString
Call::getFileName() const {
94 QString path
= preferences
.get("output.path").toString();
95 QString fileName
= preferences
.get("output.pattern").toString();
97 path
.replace('~', QDir::homePath());
99 fileName
.replace("&s", escape(skypeName
));
101 //fileName.replace("&f", escape(fullName));
102 //fileName.replace("&t", escape(mySkypeName));
103 //fileName.replace("&g", escape(myFullName));
104 fileName
.replace("&&", "&");
106 // TODO: uhm, does QT provide any time formatting the strftime() way?
107 char *buf
= new char[fileName
.size() + 1024];
108 time_t t
= std::time(NULL
);
109 struct tm
*tm
= std::localtime(&t
);
110 std::strftime(buf
, fileName
.size() + 1024, fileName
.toUtf8().constData(), tm
);
114 return path
+ '/' + fileName
;
117 void Call::startRecording() {
121 debug(QString("Call %1: start recording").arg(id
));
123 // set up encoder for appropriate format
125 QString fileName
= getFileName();
127 QString sm
= preferences
.get("output.channelmode").toString();
131 else if (sm
== "oerets")
133 else /* if (sm == "stereo") */
136 QString format
= preferences
.get("output.format").toString();
139 writer
= new WaveWriter
;
140 else /* if (format == "mp3") */
141 writer
= new Mp3Writer
;
143 bool b
= writer
->open(fileName
, 16000, channelMode
!= 0);
146 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
147 QString(PROGRAM_NAME
" could not open the file %1. Please verify the output file pattern.").arg(fileName
));
148 box
->setWindowModality(Qt::NonModal
);
149 box
->setAttribute(Qt::WA_DeleteOnClose
);
156 serverLocal
= new QTcpServer(this);
157 serverLocal
->listen();
158 connect(serverLocal
, SIGNAL(newConnection()), this, SLOT(acceptLocal()));
159 serverRemote
= new QTcpServer(this);
160 serverRemote
->listen();
161 connect(serverRemote
, SIGNAL(newConnection()), this, SLOT(acceptRemote()));
163 QString rep1
= skype
->sendWithReply(QString("ALTER CALL %1 SET_CAPTURE_MIC PORT=\"%2\"").arg(id
).arg(serverLocal
->serverPort()));
164 QString rep2
= skype
->sendWithReply(QString("ALTER CALL %1 SET_OUTPUT SOUNDCARD=\"default\" PORT=\"%2\"").arg(id
).arg(serverRemote
->serverPort()));
166 if (!rep1
.startsWith("ALTER CALL ") || !rep2
.startsWith("ALTER CALL")) {
167 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
168 QString(PROGRAM_NAME
" could not obtain the audio streams from Skype and can thus not record this call.\n\n"
169 "The replies from Skype were:\n%1\n%2").arg(rep1
).arg(rep2
));
170 box
->setWindowModality(Qt::NonModal
);
171 box
->setAttribute(Qt::WA_DeleteOnClose
);
183 void Call::acceptLocal() {
184 socketLocal
= serverLocal
->nextPendingConnection();
185 serverLocal
->close();
186 // we don't delete the server, since it contains the socket.
187 // we could reparent, but that automatic stuff of QT is great
188 connect(socketLocal
, SIGNAL(readyRead()), this, SLOT(readLocal()));
189 connect(socketLocal
, SIGNAL(disconnected()), this, SLOT(checkConnections()));
192 void Call::acceptRemote() {
193 socketRemote
= serverRemote
->nextPendingConnection();
194 serverRemote
->close();
195 connect(socketRemote
, SIGNAL(readyRead()), this, SLOT(readRemote()));
196 connect(socketRemote
, SIGNAL(disconnected()), this, SLOT(checkConnections()));
199 void Call::readLocal() {
200 bufferLocal
+= socketLocal
->readAll();
205 void Call::readRemote() {
206 bufferRemote
+= socketRemote
->readAll();
211 void Call::checkConnections() {
212 if (socketLocal
->state() == QAbstractSocket::UnconnectedState
&& socketRemote
->state() == QAbstractSocket::UnconnectedState
) {
213 debug(QString("Call %1: both connections closed, stop recording").arg(id
));
218 void Call::mixToMono(int samples
) {
219 long offset
= bufferMono
.size();
220 bufferMono
.resize(offset
+ samples
* 2);
222 qint16
*monoData
= reinterpret_cast<qint16
*>(bufferMono
.data()) + offset
;
223 qint16
*localData
= reinterpret_cast<qint16
*>(bufferLocal
.data());
224 qint16
*remoteData
= reinterpret_cast<qint16
*>(bufferRemote
.data());
226 for (int i
= 0; i
< samples
; i
++) {
227 long sum
= localData
[i
] + remoteData
[i
];
230 else if (sum
> 32767)
235 bufferLocal
.remove(0, samples
* 2);
236 bufferRemote
.remove(0, samples
* 2);
239 void Call::tryToWrite(bool flush
) {
240 //debug(QString("Situation: %3, %4").arg(bufferLocal.size()).arg(bufferRemote.size()));
242 int l
= bufferLocal
.size();
243 int r
= bufferRemote
.size();
244 int samples
= (l
< r
? l
: r
) / 2;
246 if (!samples
&& !flush
)
249 // got new samples to write to file, or have to flush
253 if (channelMode
== 0) {
256 success
= writer
->write(bufferMono
, bufferMono
, samples
, flush
);
257 } else if (channelMode
== 1) {
259 success
= writer
->write(bufferLocal
, bufferRemote
, samples
, flush
);
260 } else if (channelMode
== 2) {
262 success
= writer
->write(bufferRemote
, bufferLocal
, samples
, flush
);
268 QMessageBox
*box
= new QMessageBox(QMessageBox::Critical
, PROGRAM_NAME
" - Error",
269 QString(PROGRAM_NAME
" encountered an error while writing this call to disk. Recording terminated."));
270 box
->setWindowModality(Qt::NonModal
);
271 box
->setAttribute(Qt::WA_DeleteOnClose
);
273 stopRecording(false);
277 // the writer will remove the samples from the buffers
278 //debug(QString("Call %1: wrote %2 samples").arg(id).arg(samples));
280 // TODO: handle the case where the two streams get out of sync (buffers
281 // not equally fulled by a significant amount). does skype document
282 // whether we always get two nice, equal, in-sync streams, even if
283 // there have been transmission errors? perl-script behavior: if out
284 // of sync by more than 6.4ms, then remove 1ms from the stream that's
288 void Call::stopRecording(bool flush
, bool removeFile
) {
292 debug(QString("Call %1: stop recording").arg(id
));
294 // NOTE: we don't delete the sockets here, because we may be here as a
295 // reaction to their disconnected() signals; and they don't like being
296 // deleted during their signals. we don't delete the servers either,
297 // since they own the sockets and we're too lazy to reparent. it's
298 // easiest to let QT handle all this on its own. there will be some
299 // memory wasted if you start/stop recording within the same call a few
300 // times, but unless you do it thousands of times, the waste is more
303 // flush data to writer
317 CallHandler::CallHandler(Skype
*s
) : skype(s
) {
320 void CallHandler::closeAll() {
321 debug("closing all calls");
322 QList
<Call
*> list
= calls
.values();
323 for (int i
= 0; i
< list
.size(); i
++)
324 list
.at(i
)->stopRecording();
327 // ---- CallHandler ----
329 void CallHandler::callCmd(const QStringList
&args
) {
330 CallID id
= args
.at(0).toInt();
332 if (ignore
.contains(id
))
335 bool newCall
= false;
337 if (!calls
.contains(id
)) {
338 calls
[id
] = new Call(skype
, id
);
342 Call
*call
= calls
[id
];
344 QString subCmd
= args
.at(1);
346 if (subCmd
== "STATUS") {
347 QString a
= args
.at(2);
348 if (a
== "INPROGRESS")
349 call
->startRecording();
350 else if (a
== "DURATION") {
351 /* this is where we start recording calls that are already running, for
352 example if the user starts this program after the call has been placed */
354 call
->startRecording();
357 // don't stop recording when we get "FINISHED". just wait for
358 // the connections to close so that we really get all the data
362 // ---- RecordConfirmationDialog ----
364 RecordConfirmationDialog::RecordConfirmationDialog(const QString
&skypeName
, const QString
&displayName
) {
365 setWindowTitle(PROGRAM_NAME
);
366 setAttribute(Qt::WA_DeleteOnClose
);
368 QHBoxLayout
*bighbox
= new QHBoxLayout(this);
369 bighbox
->setSizeConstraint(QLayout::SetFixedSize
);
372 int iconSize
= QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize
);
373 QIcon icon
= QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion
);
374 QLabel
*iconLabel
= new QLabel
;
375 iconLabel
->setPixmap(icon
.pixmap(iconSize
, iconSize
));
376 bighbox
->addWidget(iconLabel
, 0, Qt::AlignTop
);
378 bighbox
->addSpacing(10);
380 QVBoxLayout
*vbox
= new QVBoxLayout
;
381 bighbox
->addLayout(vbox
);
382 QLabel
*label
= new QLabel(QString("Do you wish to record this call with <b>%1</b> (%2)?").arg(skypeName
).arg(displayName
));
383 vbox
->addWidget(label
);
384 QCheckBox
*check
= new QCheckBox("Automatically perform this action on the next call with this person");
385 check
->setEnabled(false);
386 vbox
->addWidget(check
);
387 QHBoxLayout
*hbox
= new QHBoxLayout
;
388 QPushButton
*button
= new QPushButton("Yes, record this call");
389 connect(button
, SIGNAL(clicked()), this, SLOT(yesClicked()));
390 hbox
->addWidget(button
);
391 button
= new QPushButton("Do not record this call");
392 connect(button
, SIGNAL(clicked()), this, SLOT(noClicked()));
393 hbox
->addWidget(button
);
394 vbox
->addLayout(hbox
);
396 connect(this, SIGNAL(rejected()), this, SIGNAL(no()));
403 void RecordConfirmationDialog::yesClicked() {
405 // TODO update preferences depending on checkbox
409 void RecordConfirmationDialog::noClicked() {
411 // TODO update preferences depending on checkbox