1/*
2 Copyright (c) 1997 Christian Esken (esken@kde.org)
3 2000 Charles Samuels (charles@kde.org)
4 2000 Stefan Schimanski (1Stein@gmx.de)
5 2000 Matthias Ettrich (ettrich@kde.org)
6 2000 Waldo Bastian <bastian@kde.org>
7 2000-2003 Carsten Pfeiffer <pfeiffer@kde.org>
8 2005 Allan Sandfeld Jensen <kde@carewolf.com>
9 2005-2006 by Olivier Goffart <ogoffart at kde.org>
10
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2, or (at your option)
14 any later version.
15
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24*/
25
26
27#include "notifybysound.h"
28
29// QT headers
30#include <QHash>
31#include <QtCore/QBasicTimer>
32#include <QtCore/QQueue>
33#include <QtCore/QTimer>
34#include <QtCore/QTimerEvent>
35#include <QtCore/QStack>
36#include <QSignalMapper>
37
38// KDE headers
39#include <kdebug.h>
40#include <klocale.h>
41#include <kprocess.h>
42#include <kstandarddirs.h>
43#include <kconfiggroup.h>
44#include <kurl.h>
45#include <config-runtime.h>
46#include <kcomponentdata.h>
47#include <knotifyconfig.h>
48
49// Phonon headers
50#include <phonon/mediaobject.h>
51#include <phonon/path.h>
52#include <phonon/audiooutput.h>
53
54struct Player
55{
56 Player()
57 : media(new Phonon::MediaObject),
58 output(new Phonon::AudioOutput(Phonon::NotificationCategory))
59 {
60 Phonon::createPath(media, output);
61 }
62
63 inline void play(const QString &file) { media->setCurrentSource(file); media->enqueue(Phonon::MediaSource()); media->play(); }
64 inline void stop() { media->stop(); }
65 inline void setVolume(float volume) { output->setVolume(volume); }
66
67 ~Player()
68 {
69 output->deleteLater();
70 media->deleteLater();
71 }
72
73 Phonon::MediaObject *const media;
74 Phonon::AudioOutput *const output;
75};
76
77class PlayerPool
78{
79 public:
80 PlayerPool() : m_idlePlayer(0), m_volume(1.0) {}
81
82 Player *getPlayer();
83 void returnPlayer(Player *);
84 void clear();
85
86 void setVolume(float volume);
87
88 private:
89 Player *m_idlePlayer;
90 QList<Player *> m_playersInUse;
91 float m_volume;
92};
93
94Player *PlayerPool::getPlayer()
95{
96 Player *p = 0;
97 if (!m_idlePlayer) {
98 p = new Player;
99 } else {
100 p = m_idlePlayer;
101 m_idlePlayer = 0;
102 }
103 p->setVolume(m_volume);
104 m_playersInUse << p;
105 return p;
106}
107
108void PlayerPool::returnPlayer(Player *p)
109{
110 m_playersInUse.removeAll(p);
111 if (m_idlePlayer) {
112 delete p;
113 } else {
114 m_idlePlayer = p;
115 }
116}
117
118void PlayerPool::clear()
119{
120 delete m_idlePlayer;
121 m_idlePlayer = 0;
122}
123
124void PlayerPool::setVolume(float v)
125{
126 m_volume = v;
127 foreach (Player *p, m_playersInUse) {
128 p->setVolume(v);
129 }
130}
131
132class NotifyBySound::Private
133{
134 public:
135 enum { NoSound, UsePhonon, ExternalPlayer } playerMode;
136 QString externalPlayer;
137
138 QHash<int, KProcess *> processes;
139 QHash<int, Player*> playerObjects;
140 QSignalMapper *signalmapper;
141 PlayerPool playerPool;
142 QBasicTimer poolTimer;
143 QQueue<int> closeQueue;
144
145 int volume;
146
147};
148
149NotifyBySound::NotifyBySound(QObject *parent) : KNotifyPlugin(parent),d(new Private)
150{
151 d->signalmapper = new QSignalMapper(this);
152 connect(d->signalmapper, SIGNAL(mapped(int)), this, SLOT(slotSoundFinished(int)));
153
154 loadConfig();
155}
156
157
158NotifyBySound::~NotifyBySound()
159{
160 delete d;
161}
162
163
164void NotifyBySound::loadConfig()
165{
166 // load external player settings
167 KSharedConfig::Ptr kc = KGlobal::config();
168 KConfigGroup cg(kc, "Sounds");
169
170 d->playerMode = Private::UsePhonon;
171 if(cg.readEntry( "Use external player", false ))
172 {
173 d->playerMode = Private::ExternalPlayer;
174 d->externalPlayer = cg.readPathEntry("External player", QString());
175 // try to locate a suitable player if none is configured
176 if ( d->externalPlayer.isEmpty() ) {
177 QStringList players;
178 players << "wavplay" << "aplay" << "auplay" << "artsplay" << "akodeplay";
179 QStringList::const_iterator it = players.constBegin();
180 while ( d->externalPlayer.isEmpty() && it != players.constEnd() ) {
181 d->externalPlayer = KStandardDirs::findExe( *it );
182 ++it;
183 }
184 }
185 }
186 else if(cg.readEntry( "No sound" , false ))
187 {
188 d->playerMode = Private::NoSound;
189 }
190 // load default volume
191 setVolume( cg.readEntry( "Volume", 100 ) );
192}
193
194
195
196
197void NotifyBySound::notify( int eventId, KNotifyConfig * config )
198{
199 if(d->playerMode == Private::NoSound)
200 {
201 finish( eventId );
202 return;
203 }
204
205 if(d->playerObjects.contains(eventId) || d->processes.contains(eventId) )
206 {
207 //a sound is already playing for this notification, we don't support playing two sounds.
208 finish( eventId );
209 return;
210 }
211
212 KUrl soundFileURL = config->readEntry( "Sound" , true );
213 QString soundFile = soundFileURL.toLocalFile();
214
215 if (soundFile.isEmpty())
216 {
217 finish( eventId );
218 return;
219 }
220
221 // get file name
222 if ( KUrl::isRelativeUrl(soundFile) )
223 {
224 QString search = QString("%1/sounds/%2").arg(config->appname).arg(soundFile);
225 search = KGlobal::mainComponent().dirs()->findResource("data", search);
226 if ( search.isEmpty() )
227 soundFile = KStandardDirs::locate( "sound", soundFile );
228 else
229 soundFile = search;
230 }
231 if ( soundFile.isEmpty() )
232 {
233 finish( eventId );
234 return;
235 }
236
237 kDebug() << " going to play " << soundFile;
238 d->poolTimer.stop();
239
240 if(d->playerMode == Private::UsePhonon)
241 {
242 Player *player = d->playerPool.getPlayer();
243 connect(player->media, SIGNAL(finished()), d->signalmapper, SLOT(map()));
244 d->signalmapper->setMapping(player->media, eventId);
245 player->play(soundFile);
246 d->playerObjects.insert(eventId, player);
247 }
248 else if (d->playerMode == Private::ExternalPlayer && !d->externalPlayer.isEmpty())
249 {
250 // use an external player to play the sound
251 KProcess *proc = new KProcess( this );
252 connect( proc, SIGNAL(finished(int, QProcess::ExitStatus)),
253 d->signalmapper, SLOT(map()) );
254 d->signalmapper->setMapping( proc , eventId );
255
256 (*proc) << d->externalPlayer << soundFile;
257 proc->start();
258 }
259}
260
261
262void NotifyBySound::setVolume( int volume )
263{
264 if ( volume<0 ) volume=0;
265 if ( volume>=100 ) volume=100;
266 d->volume = volume;
267 d->playerPool.setVolume(d->volume / 100.0);
268}
269
270
271void NotifyBySound::timerEvent(QTimerEvent *e)
272{
273 if (e->timerId() == d->poolTimer.timerId()) {
274 d->poolTimer.stop();
275 d->playerPool.clear();
276 return;
277 }
278 KNotifyPlugin::timerEvent(e);
279}
280
281void NotifyBySound::slotSoundFinished(int id)
282{
283 kDebug() << id;
284 if(d->playerObjects.contains(id))
285 {
286 Player *player=d->playerObjects.take(id);
287 disconnect(player->media, SIGNAL(finished()), d->signalmapper, SLOT(map()));
288 d->playerPool.returnPlayer(player);
289 //d->poolTimer.start(1000, this);
290 }
291 if(d->processes.contains(id))
292 {
293 d->processes[id]->deleteLater();
294 d->processes.remove(id);
295 }
296 finish(id);
297}
298
299void NotifyBySound::close(int id)
300{
301 // close in 1 min - ugly workaround for sounds getting cut off because the close call in kdelibs
302 // is hardcoded to 6 seconds
303 d->closeQueue.enqueue(id);
304 QTimer::singleShot(60000, this, SLOT(closeNow()));
305}
306
307void NotifyBySound::closeNow()
308{
309 const int id = d->closeQueue.dequeue();
310 if(d->playerObjects.contains(id))
311 {
312 Player *p = d->playerObjects.take(id);
313 p->stop();
314 d->playerPool.returnPlayer(p);
315 //d->poolTimer.start(1000, this);
316 }
317 if(d->processes.contains(id))
318 {
319 d->processes[id]->kill();
320 d->processes[id]->deleteLater();
321 d->processes.remove(id);
322 }
323}
324
325#include "notifybysound.moc"
326// vim: ts=4 noet
327