Make playlist items use the full width available to them and adjust correctly when...
[amarok.git] / src / moodbar.cpp
blobdc8c91e1beb123daa8e88218bb859b1dc267c1e6
1 /***************************************************************************
2 moodbar.cpp - description
3 -------------------
4 begin : 6th Nov 2005
5 copyright : (C) 2006 by Joseph Rabinoff
6 copyright : (C) 2005 by Gav Wood
7 email : bobqwatson@yahoo.com
8 ***************************************************************************/
10 /***************************************************************************
11 * *
12 * This program is free software; you can redistribute it and/or modify *
13 * it under the terms of the GNU General Public License as published by *
14 * the Free Software Foundation; either version 2 of the License, or *
15 * (at your option) any later version. *
16 * *
17 ***************************************************************************/
19 // Although the current incarnation of moodbar.cpp shares bits and
20 // pieces of code with Gav Wood's original, it has been completely
21 // rewritten -- the only code I kept was purely algorithmic. Also
22 // lots of Moodbar-related functionality has been moved from other
23 // places to here (all of it really).
25 // The Moodbar is used by small amounts of code in playlistitem.cpp
26 // and sliderwidget.cpp. There are also trivial amounts of support
27 // code in other places.
29 // Moodbar usage
30 // -------------
32 // The Moodbar is part of the track's metadata, so it's held by a
33 // MetaBundle. The actual Moodbar object is only used to draw a
34 // QPixmap, which it does efficiently -- it caches a pixmap of the
35 // last thing it drew, and just copies that pixmap if the dimensions
36 // have not changed. To use the moodbar, one just needs a few lines of
37 // code, such as the following, based on PrettySlider:
39 // void MyClass::MyClass( void )
40 // {
41 // // This only needs to be done once!
42 // connect( &m_bundle.moodbar(), SIGNAL( jobEvent( int ) ),
43 // SLOT( newMoodData( int ) ) );
44 // }
46 // void MyClass::newMetaBundle( const MetaBundle &b )
47 // {
48 // m_bundle = b;
50 // if( !m_bundle.moodbar().dataExists() )
51 // m_bundle.moodbar().load();
52 // else
53 // update();
54 // }
56 // void MyClass::draw( void )
57 // {
58 // QPixmap toDraw;
59 // if( m_bundle.moodbar().dataExists() )
60 // toDraw = m_bundle.moodbar().draw( width(), height() );
61 // // else draw something else...
62 // }
64 // void MyClass::newMoodData( int newState )
65 // {
66 // if( newState == Moodbar::JobStateSucceeded )
67 // update();
68 // }
70 // Explanation:
72 // * In the constructor we listen for the jobEvent() signal from the
73 // Moodbar. The Moodbar emits this signal when an analyzer process
74 // has started or completed and it has loaded its moodbar data.
75 // (This connection will exist for the lifetime of the instance of
76 // MyClass and hence only needs to be created once.)
78 // * Whenever the MetaBundle associated with this instance of MyClass
79 // is changed, so does the moodbar, so we should reload it. The
80 // dataExists() method is meant to return whether the mood has
81 // already been analyzed for that track (it will always return false
82 // for streaming bundles and the like). If it returns true then the
83 // moodbar has already loaded its data, and can draw it.
85 // * Otherwise we run the Moodbar's load() method. This method may
86 // be called many times; it will only actually do anything the first
87 // time it's called (unless the moodbar is reset()). Hence it's
88 // totally reasonable to call load() in the draw() method too; this
89 // is in fact what the PlaylistItem does. When load() has completed,
90 // it emits a jobEvent() signal.
92 // * Note that jobEvent() will also be emitted if there is an error
93 // in analyzing or loading the data, with a state indicating failure.
94 // In this case, subsequent calls to dataExists() will still return
95 // false, and subsequent calls to load() will do nothing.
98 // Implementation
99 // --------------
101 // There are two new classes, namely the Moodbar (a member of
102 // MetaBundle), and the MoodServer. The former is the only public
103 // class. In a nutshell, the Moodbar is responsible for reading
104 // and drawing mood data, and the MoodServer is in charge of
105 // queueing analyzer jobs and notifying interested Moodbar's when
106 // their job is done.
109 // The Moodbar class --
111 // The only public interface to the moodbar system. An unloaded
112 // Moodbar is meant to have a very small footprint, since there are
113 // lots of MetaBundle's floating around that aren't going to be
114 // displayed. Most of the data in loaded Moodbars is implicitly
115 // shared anyway, so it's reasonable to
116 // pass them around by value.
118 // Much care has been taken to absolutely minimize the amount of time
119 // a Moodbar is listening for a signal. The only signal a Moodbar
120 // will connect to is MoodServer::jobEvent; this connection is made
121 // when MoodServer::queueJob() is called, and is disconnected in
122 // slotJobEvent(). The reason for this care is because MetaBundle's,
123 // and hence Moodbar's, are copied around and passed-by-value all the
124 // time, so I wanted to reduce overhead; also QObject::disconnect() is
125 // not reentrant (from what I understand), so we don't want that being
126 // called every time a Moodbar is destroyed! For the same reason, the
127 // PlaylistItem does not listen for the jobEvent() signal; instead it
128 // reimplements the MetaBundle::moodbarJobEvent() virtual method.
130 // Again for this reason, the individual Moodbar's don't listen for
131 // the App::moodbarPrefs() signal (which is emitted every time the
132 // configuration is changed); thus Moodbar's aren't automatically
133 // updated when the AlterMood variable is changed, for instance. This
134 // is a small annoyance, as the owner of the Moodbar has to listen for
135 // that signal and call reset(). This happens in sliderwidget.cpp and
136 // playlist.cpp.
138 // A moodbar is always in one of the following states:
140 // Unloaded: A newly-created (or newly reset()) Moodbar is in this
141 // state. The Moodbar remains in this state until
142 // dataExists() or load() is called. Note that load()
143 // will return immediately unless the state is Unloaded.
144 // CantLoad: For some reason we know that we'll never be able to
145 // load the Moodbar, for instance if the parent bundle
146 // describes a streaming source. Most methods will return
147 // immediately in this state.
148 // JobQueued: At some point load() was called, so we queued a job with
149 // the MoodServer which hasn't started yet. In this state,
150 // ~Moodbar(), reset(), etc. knows to dequeue jobs and
151 // disconnect signals.
152 // JobRunning: Our analyzer job is actually running. The moodbar behaves
153 // basically the same as in the JobQueued state; this state
154 // exists so the PlaylistItem knows the difference.
155 // JobFailed: The MoodServer has tried to run our job (or gave up before
156 // trying), and came up empty. This state behaves basically
157 // the same as CantLoad.
158 // Loaded: This is the only state in which draw() will work.
161 // Note that nothing is done to load until dataExists() is called; this
162 // is because there may very well be MetaBundle's floating around that
163 // aren't displayed in the GUI.
165 // Important members:
166 // m_bundle: link to the parent bundle
167 // m_data: if we are loaded, this is the contents of the .mood file
168 // m_pixmap: the last time draw() was called, we cached what we drew
169 // here
170 // m_url: cache the URL of our queued job for de-queueing
171 // m_state: our current state
172 // m_mutex: lock for the entire object. The Moodbar object should
173 // be entirely reentrant (but see below), so most methods lock the
174 // object before doing anything. (Of course the calling code has to
175 // be threadsafe for this to mean anything.)
177 // Important methods:
179 // dataExists(): When this is called, we check if the .mood file
180 // exists for our bundle. If so, we load the corresponding file,
181 // and if all goes well, return true. If our bundle is a streaming
182 // track, or is otherwise unloadable, always return false.
184 // load(): First run readFile() to see if we can load. If not, then
185 // ask MoodServer to run a job for us. Always changes the state
186 // from Unloaded so subsequent calls to load() do nothing.
188 // draw(): Draw the moodbar onto a QPixmap. Cache what we drew
189 // so that if draw() is called again with the same dimensions
190 // we don't have to redraw.
192 // reset(): Reset to the unloaded state. This is basically the same
193 // as calling moodbar = Moodbar().
195 // (protected) slotJobEvent(): Only run by MoodServer, to notify us
196 // when a job is started or completed. Emits the jobEvent()
197 // signal.
199 // (private) readFile(): When we think there's a file available, this
200 // method tries to load it. We also do the display-independent
201 // analysis here, namely, calculating the sorting index (for sort-
202 // by-hue in the Playlist), and Making Moodier.
205 // The MoodServer class --
207 // This is a singleton class. It is responsible for queueing analyzer
208 // jobs requested by Moodbar's, running them, and notifying the
209 // Moodbar's when the job has started and completed, successful or no.
210 // This class is also responsible for remembering if the moodbar
211 // system is totally broken (e.g. if the GStreamer plugins are
212 // missing), notifying the user if such is the case, and refusing to
213 // queue any more jobs. MoodServer should be threadsafe, in that you
214 // should be able to run queueJob() from any thread.
216 // Jobs are referenced by URL. If a Moodbar tries to queue a job
217 // with the same URL as an existing job, the job will not be re-queued;
218 // instead, each queued job has a refcount, which is increased. This
219 // is to support the de-queueing of jobs when Moodbar's are destroyed;
220 // the use case I have in mind is if the user has the moodbar column
221 // displayed in the playlist, he/she adds 1000 tracks to the playlist
222 // (at which point all the displayed tracks queue moodbar jobs), and
223 // then decides to clear the playlist again. The jobEvent() signal
224 // passes the URL of the job that was completed.
226 // The analyzer is actually run using a K3Process. ThreadManager::Job
227 // is not a good solution, since we need more flexibility in the
228 // queuing process, and in addition, K3Process'es must be started from
229 // the GUI thread!
231 // Important members:
232 // m_jobQueue: this is a list of MoodServer::ProcData structures,
233 // which contain the data needed to start and reference
234 // a process, as well as a refcount.
235 // m_currentProcess: the currently-running K3Process, if any.
236 // m_currentData: the ProcData structure for the currently-running
237 // process.
238 // m_moodbarBroken: this is set when there's an error running the analyzer
239 // that indicates the analyzer will never be able to run.
240 // When m_moodbarBroken == true, the MoodServer will refuse
241 // to queue new jobs.
242 // m_mutex: you should be able to run queueJob() from any thread,
243 // so most methods lock the object.
245 // Important methods:
247 // queueJob(): Add a job to the queue. If the job is being run, do nothing;
248 // if the job is already queued, increase its refcount, and if
249 // m_moodbarBroken == true, do nothing.
251 // deQueueJob(): Called from ~Moodbar(), for instance. Decreases
252 // the refcount of a job, removing it from the queue when the
253 // refcount hits zero. This won't kill a running process.
255 // (private slot) slotJobCompleted(): Called when a job finishes. Do some
256 // cleanup, and notify the interested parties. Set m_moodbarBroken if
257 // necessary; otherwise call slotNewJob().
259 // (private slot) slotNewJob(): Called by slotJobCompleted() and queueJob().
260 // Take a job off the queue and start the K3Process.
262 // (private slot) slotMoodbarPrefs(): Called when the Amarok config changes.
263 // If the moodbar has been disabled completely, kill the current job
264 // (if any), clear the queue, and notify the interested Moodbar's.
266 // (private slot) slotFileDeleted(): Called when a music file is deleted, so
267 // we can delete the associated moodbar
269 // (private slot) slotFileMoved(): Called when a music file is moved, so
270 // we can move the associated moodbar
272 // TODO: off-color single bars in dark areas -- do some interpolation when
273 // averaging. Big jumps in hues when near black.
275 // BUGS:
277 #define DEBUG_PREFIX "Moodbar"
279 #include "moodbar.h"
281 #include "config-amarok.h"
283 #include "amarokconfig.h"
284 #include "amarok.h"
285 #include "app.h"
286 #include "collectiondb.h"
287 #include "debug.h"
288 #include "metabundle.h"
289 #include "mountpointmanager.h"
290 #include "statusbar.h"
292 #include <KStandardDirs>
294 #include <Q3ValueList>
295 #include <QDir> // For QDir::rename()
296 #include <QFile>
297 #include <QPainter>
298 #include <QPixmap>
299 #include <QTimer>
301 #include <string.h> // for memset()
304 #define CLAMP(n, v, x) ((v) < (n) ? (n) : (v) > (x) ? (x) : (v))
306 #define WEBPAGE "http://amarok.kde.org/wiki/Moodbar"
309 ///////////////////////////////////////////////////////////////////////////////
310 // MoodServer class
311 ///////////////////////////////////////////////////////////////////////////////
314 MoodServer *
315 MoodServer::instance( void )
317 static MoodServer m;
318 return &m;
322 MoodServer::MoodServer( void )
323 : m_moodbarBroken( false )
324 , m_currentProcess( 0 )
326 connect( App::instance(), SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
327 SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) );
328 connect( CollectionDB::instance(),
329 SIGNAL( fileMoved( const QString &, const QString & ) ),
330 SLOT( slotFileMoved( const QString &, const QString & ) ) );
331 connect( CollectionDB::instance(),
332 SIGNAL( fileMoved( const QString &, const QString &, const QString & ) ),
333 SLOT( slotFileMoved( const QString &, const QString & ) ) );
334 connect( CollectionDB::instance(),
335 SIGNAL( fileDeleted( const QString & ) ),
336 SLOT( slotFileDeleted( const QString & ) ) );
337 connect( CollectionDB::instance(),
338 SIGNAL( fileDeleted( const QString &, const QString & ) ),
339 SLOT( slotFileDeleted( const QString & ) ) );
343 // Queue a job, but not before checking if the moodbar is enabled
344 // in the config, if the moodbar analyzer appears to be working,
345 // and if a job for that URL isn't already queued. Returns true
346 // if the job is already running, false otherwise.
347 bool
348 MoodServer::queueJob( MetaBundle *bundle )
350 if( m_moodbarBroken || !AmarokConfig::showMoodbar() )
351 return false;
353 m_mutex.lock();
355 // Check if the currently running job is for that URL
356 if( m_currentProcess != 0 &&
357 m_currentData.m_url == bundle->url() )
359 debug() << "MoodServer::queueJob: Not re-queueing already-running job "
360 << bundle->url().path() << endl;
361 m_mutex.unlock();
362 return true;
365 // Check if there's already a job in the queue for that URL
366 Q3ValueList<ProcData>::iterator it;
367 for( it = m_jobQueue.begin(); it != m_jobQueue.end(); ++it )
369 if( (*it).m_url == bundle->url() )
371 (*it).m_refcount++;
372 debug() << "MoodServer::queueJob: Job for " << bundle->url().path()
373 << " already in queue, increasing refcount to "
374 << (*it).m_refcount << endl;
375 m_mutex.unlock();
376 return false;
380 m_jobQueue.append( ProcData( bundle->url(),
381 bundle->url().path(),
382 bundle->moodbar().moodFilename( bundle->url() ) ) );
384 debug() << "MoodServer::queueJob: Queued job for " << bundle->url().path()
385 << ", " << m_jobQueue.size() << " jobs in queue." << endl;
387 m_mutex.unlock();
389 // New jobs *must* be started from the GUI thread!
390 QTimer::singleShot( 1000, this, SLOT( slotNewJob( void ) ) );
392 return false;
396 // Decrements the refcount of the job for the given URL
397 // and deletes that job if necessary.
398 void
399 MoodServer::deQueueJob( KUrl url )
401 m_mutex.lock();
403 // Can't de-queue running jobs
404 if( m_currentProcess != 0 &&
405 m_currentData.m_url == url )
407 debug() << "MoodServer::deQueueJob: Not de-queueing already-running job "
408 << url.path() << endl;
409 m_mutex.unlock();
410 return;
413 // Check if there's already a job in the queue for that URL
414 Q3ValueList<ProcData>::iterator it;
415 for( it = m_jobQueue.begin(); it != m_jobQueue.end(); ++it )
417 if( (*it).m_url == url )
419 (*it).m_refcount--;
421 if( (*it).m_refcount == 0 )
423 debug() << "MoodServer::deQueueJob: nobody cares about "
424 << (*it).m_url.path()
425 << " anymore, deleting from queue" << endl;
426 m_jobQueue.erase( it );
429 else
430 debug() << "MoodServer::deQueueJob: decrementing refcount of "
431 << (*it).m_url.path() << " to " << (*it).m_refcount
432 << endl;
434 m_mutex.unlock();
435 return;
439 debug() << "MoodServer::deQueueJob: tried to delete nonexistent job "
440 << url.path() << endl;
442 m_mutex.unlock();
446 // This slot exists so that jobs can be started from the GUI thread,
447 // just in case queueJob() is run from another thread. Only run
448 // directly if you're in the GUI thread!
449 void
450 MoodServer::slotNewJob( void )
452 if( m_moodbarBroken )
453 return;
455 m_mutex.lock();
457 // Are we already running a process?
458 if( m_jobQueue.isEmpty() || m_currentProcess != 0 )
460 m_mutex.unlock();
461 return;
464 m_currentData = m_jobQueue.first();
465 m_jobQueue.pop_front();
467 debug() << "MoodServer::slotNewJob: starting new analyzer process: "
468 << "moodbar -o " << m_currentData.m_outfile << ".tmp "
469 << m_currentData.m_infile << endl;
470 debug() << "MoodServer::slotNewJob: " << m_jobQueue.size()
471 << " jobs left in queue." << endl;
474 // Write to outfile.mood.tmp so that new Moodbar instances
475 // don't think the mood data exists while the analyzer is
476 // running. Then rename the file later.
477 m_currentProcess = new K3Process( this );
478 m_currentProcess->setPriority( 19 ); // Nice the process
479 *m_currentProcess << KStandardDirs::findExe( "moodbar" ) << "-o"
480 << (m_currentData.m_outfile + ".tmp")
481 << m_currentData.m_infile;
483 connect( m_currentProcess, SIGNAL( processExited( K3Process* ) ),
484 SLOT( slotJobCompleted( K3Process* ) ) );
486 // We have to enable K3Process::Stdout (even though we don't monitor
487 // it) since otherwise the child process crashes every time in
488 // K3Process::start() (but only when started from the loader!). I
489 // have no idea why, but I imagine it's a bug in KDE.
490 if( !m_currentProcess->start( K3Process::NotifyOnExit, K3Process::AllOutput ) )
492 // If we have an error starting the process, it's never
493 // going to work, so call moodbarBroken()
494 warning() << "Can't start moodbar analyzer process!" << endl;
495 delete m_currentProcess;
496 m_currentProcess = 0;
497 m_mutex.unlock();
498 setMoodbarBroken();
499 return;
502 // Extreme reentrancy pedatry :)
503 KUrl url = m_currentData.m_url;
504 m_mutex.unlock();
506 emit jobEvent( url, Moodbar::JobStateRunning );
510 // This always run in the GUI thread. It is called
511 // when an analyzer process terminates
512 void
513 MoodServer::slotJobCompleted( K3Process *proc )
515 m_mutex.lock();
517 // Pedantry
518 if( proc != m_currentProcess )
519 warning() << "MoodServer::slotJobCompleted: proc != m_currentProcess!" << endl;
521 ReturnStatus returnval;
522 if( !m_currentProcess->normalExit() )
523 returnval = Crash;
524 else
525 returnval = (ReturnStatus) m_currentProcess->exitStatus();
527 bool success = (returnval == Success);
528 KUrl url = m_currentData.m_url;
530 if( success )
532 QString file = m_currentData.m_outfile;
533 QString dir = file.left( file.lastIndexOf( '/' ) );
534 file = file.right( file.length() - file.lastIndexOf( '/' ) - 1 );
535 QDir( dir ).rename( file + ".tmp", file );
537 else
538 QFile::remove( m_currentData.m_outfile + ".tmp" );
540 delete m_currentProcess;
541 m_currentProcess = 0;
544 // If the moodbar was disabled, we killed the process
545 if( !AmarokConfig::showMoodbar() )
547 debug() << "MoodServer::slotJobCompleted: moodbar disabled, job killed";
548 m_mutex.unlock();
549 emit jobEvent( url, Moodbar::JobStateFailed );
550 return;
554 switch( returnval )
556 case Success:
557 debug() << "MoodServer::slotJobCompleted: job completed successfully";
558 m_mutex.unlock();
559 slotNewJob();
560 break;
562 // Crash and NoFile don't mean that moodbar is broken.
563 // Something bad happened, but it's probably a problem with this file
564 // Just log an error message and emit jobEvent().
565 case Crash:
566 debug() << "MoodServer::slotJobCompleted: moodbar crashed on "
567 << m_currentData.m_infile << endl;
568 m_mutex.unlock();
569 slotNewJob();
570 break;
572 case NoFile:
573 debug() << "MoodServer::slotJobCompleted: moodbar had a problem with "
574 << m_currentData.m_infile << endl;
575 m_mutex.unlock();
576 slotNewJob();
577 break;
579 // NoPlugin and CommandLine mean the moodbar is broken
580 // The moodbar analyzer is not likely to work ever, so let the
581 // user know about it and disable new jobs.
582 default:
583 m_mutex.unlock();
584 setMoodbarBroken();
585 break;
589 emit jobEvent( url, success ? Moodbar::JobStateSucceeded
590 : Moodbar::JobStateFailed );
594 // This is called whenever "Ok" or "Apply" is pressed on the configuration
595 // dialog. If the moodbar is disabled, kill the current process and
596 // clear the queue
597 void
598 MoodServer::slotMoodbarPrefs( bool show, bool moodier, int alter, bool withMusic )
600 if( show == true)
601 return;
603 (void) moodier; (void) alter; (void) withMusic;
605 // If we have a current process, kill it. Cleanup happens in
606 // slotJobCompleted() above. We do *not* want to lock the
607 // mutex when calling this!
608 if( m_currentProcess != 0 )
609 m_currentProcess->kill();
611 clearJobs();
615 // When a file is deleted, either manually using Organize Collection or
616 // automatically detected using AFT, delete the corresponding mood file.
617 void
618 MoodServer::slotFileDeleted( const QString &path )
620 QString mood = Moodbar::moodFilename( KUrl( path ) );
621 if( mood.isEmpty() || !QFile::exists( mood ) )
622 return;
624 debug() << "MoodServer::slotFileDeleted: deleting " << mood;
625 QFile::remove( mood );
629 // When a file is moved, either manually using Organize Collection or
630 // automatically using AFT, move the corresponding mood file.
631 void
632 MoodServer::slotFileMoved( const QString &srcPath, const QString &dstPath )
634 QString srcMood = Moodbar::moodFilename( KUrl( srcPath ) );
635 QString dstMood = Moodbar::moodFilename( KUrl( dstPath ) );
637 if( srcMood.isEmpty() || dstMood.isEmpty() ||
638 srcMood == dstMood || !QFile::exists( srcMood ) )
639 return;
641 debug() << "MoodServer::slotFileMoved: moving " << srcMood << " to "
642 << dstMood << endl;
644 Moodbar::copyFile( srcMood, dstMood );
645 QFile::remove( srcMood );
649 // This is called when we decide that the moodbar analyzer is
650 // never going to work. Disable further jobs, and let the user
651 // know about it. This should only be called when m_currentProcess == 0.
652 void
653 MoodServer::setMoodbarBroken( void )
655 warning() << "Uh oh, it looks like the moodbar analyzer is not going to work"
656 << endl;
658 Amarok::StatusBar::instance()->longMessage( i18n(
659 "The Amarok moodbar analyzer program seems to be broken. "
660 "This is probably because the moodbar package is not installed "
661 "correctly. The moodbar package, installation instructions, and "
662 "troubleshooting help can be found on the wiki page at <a href='"
663 WEBPAGE "'>" WEBPAGE "</a>. "
664 "When the problem is fixed, please restart Amarok."),
665 KDE::StatusBar::Error );
668 m_moodbarBroken = true;
669 clearJobs();
673 // Clear the job list and emit signals
674 void
675 MoodServer::clearJobs( void )
677 // We don't want to emit jobEvent (or really do anything
678 // external) while the mutex is locked.
679 m_mutex.lock();
680 Q3ValueList<ProcData> queueCopy
681 = Q3ValueList<ProcData> ( m_jobQueue );
682 m_jobQueue.clear();
683 m_mutex.unlock();
685 Q3ValueList<ProcData>::iterator it;
686 for( it = queueCopy.begin(); it != queueCopy.end(); ++it )
687 emit jobEvent( (*it).m_url, Moodbar::JobStateFailed );
692 ///////////////////////////////////////////////////////////////////////////////
693 // Moodbar class
694 ///////////////////////////////////////////////////////////////////////////////
697 // The moodbar behavior is nearly identical in the JobQueued and
698 // JobRunning states, but we have to keep track anyway so the
699 // PlaylistItem knows what do display
701 #define JOB_PENDING(state) ((state)==JobQueued||(state)==JobRunning)
704 // The passed MetaBundle _must_ be non-NULL, and the pointer must be valid
705 // as long as this instance is alive. The Moodbar is only meant to be a
706 // member of a MetaBundle, in other words.
708 Moodbar::Moodbar( MetaBundle *mb )
709 : QObject ( )
710 , m_bundle ( mb )
711 , m_hueSort ( 0 )
712 , m_state ( Unloaded )
717 // If we have any pending jobs, de-queue them. The use case I
718 // have in mind is if the user has the moodbar column displayed
719 // and adds all his/her tracks to the playlist, then deletes
720 // them again.
721 Moodbar::~Moodbar( void )
723 if( JOB_PENDING( m_state ) )
724 MoodServer::instance()->deQueueJob( m_url );
728 // MetaBundle's are often assigned using operator=, so so are we.
729 Moodbar&
730 Moodbar::operator=( const Moodbar &mood )
732 // Need to check this before locking both!
733 if( &mood == this )
734 return *this;
736 m_mutex.lock();
737 mood.m_mutex.lock();
739 State oldState = m_state;
740 KUrl oldURL = m_url;
742 m_data = mood.m_data;
743 m_pixmap = mood.m_pixmap;
744 m_state = mood.m_state;
745 m_url = mood.m_url;
746 // DO NOT overwrite m_bundle! That should never change.
748 // Signal connections and job queues are part of our "state",
749 // so those should be updated too.
750 if( JOB_PENDING( m_state ) && !JOB_PENDING( oldState ) )
752 connect( MoodServer::instance(),
753 SIGNAL( jobEvent( KUrl, int ) ),
754 SLOT( slotJobEvent( KUrl, int ) ) );
755 // Increase the refcount for this job. Use mood.m_bundle
756 // since that one's already initialized.
757 MoodServer::instance()->queueJob( mood.m_bundle );
760 // If we had a job pending, de-queue it
761 if( !JOB_PENDING( m_state ) && JOB_PENDING( oldState ) )
763 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl, int ) ) );
764 MoodServer::instance()->deQueueJob( oldURL );
767 mood.m_mutex.unlock();
768 m_mutex.unlock();
770 return *this;
774 // Reset the moodbar to its Unloaded state. This is useful when
775 // the configuration is changed, and all the moodbars need to be
776 // reloaded.
777 void
778 Moodbar::reset( void )
780 m_mutex.lock();
782 debug() << "Resetting moodbar: " << m_bundle->url().path();
784 if( JOB_PENDING( m_state ) )
786 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl, int ) ) );
787 MoodServer::instance()->deQueueJob( m_url );
790 m_data.clear();
791 m_pixmap = QPixmap();
792 m_url = KUrl();
793 m_hueSort = 0;
794 m_state = Unloaded;
796 m_mutex.unlock();
800 // If possible, try to open the bundle's .mood file. When this method
801 // returns true, this instance must be able to draw(). This may
802 // change the state to CantLoad, but usually leaves the state
803 // untouched.
804 bool
805 Moodbar::dataExists( void )
807 // Put this first for efficiency
808 if( m_state == Loaded )
809 return true;
811 // Should we bother checking for the file?
812 if( m_state == CantLoad ||
813 JOB_PENDING( m_state ) ||
814 m_state == JobFailed ||
815 !canHaveMood() )
816 return false;
818 m_mutex.lock();
819 bool res = readFile();
820 m_mutex.unlock();
822 return res;
826 // If m_bundle is not a local file or for some other reason cannot
827 // have mood data, return false, and set the state to CantLoad to
828 // save future checks. Note that MoodServer::m_moodbarBroken == true
829 // does not mean we can't have a mood file; it just means that we
830 // can't generate new ones.
831 bool
832 Moodbar::canHaveMood( void )
834 if( m_state == CantLoad )
835 return false;
837 // Don't try to analyze it if we can't even determine it has a length
838 // If for some reason we can't determine a file name, give up
839 // If the moodbar is disabled, set to CantLoad -- if the user re-enables
840 // the moodbar, we'll be reset() anyway.
841 if( !AmarokConfig::showMoodbar() ||
842 !m_bundle->url().isLocalFile() ||
843 !m_bundle->length() ||
844 moodFilename( m_bundle->url() ).isEmpty() )
846 m_state = CantLoad;
847 return false;
850 return true;
854 // Ask MoodServer to queue an analyzer job for us if necessary. This
855 // method will only do something the first time it's called, as it's
856 // guaranteed to change the state from Unloaded.
857 void
858 Moodbar::load( void )
860 if( m_state != Unloaded )
861 return;
863 m_mutex.lock();
865 if( !canHaveMood() )
867 // State is now CantLoad
868 m_mutex.unlock();
869 return;
872 if( readFile() )
874 // State is now Loaded
875 m_mutex.unlock();
876 return;
879 if( MoodServer::instance()->moodbarBroken() )
881 m_state = JobFailed;
882 m_mutex.unlock();
883 return;
886 // Ok no more excuses, we have to queue a job
887 connect( MoodServer::instance(),
888 SIGNAL( jobEvent( KUrl, int ) ),
889 SLOT( slotJobEvent( KUrl, int ) ) );
890 bool isRunning = MoodServer::instance()->queueJob( m_bundle );
891 m_state = isRunning ? JobRunning : JobQueued;
892 m_url = m_bundle->url(); // Use this URL for MoodServer::deQueueJob
894 m_mutex.unlock();
898 // This is called by MoodServer when our moodbar analyzer job starts
899 // or finishes. It may change the state from JobQueued / JobRunning
900 // to JobRunning, Loaded, or JobFailed. It may emit a jobEvent()
901 void
902 Moodbar::slotJobEvent( KUrl url, int newState )
904 // Is this job for us?
905 if( !JOB_PENDING( m_state ) || url != m_bundle->url() )
906 return;
908 bool success = ( newState == JobStateSucceeded );
910 // We don't really care about this, but our listeners might
911 if( newState == JobStateRunning )
913 m_state = JobRunning;
914 goto out;
917 m_mutex.lock();
919 // Disconnect the signal for efficiency's sake
920 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl, int ) ) );
922 if( !success )
924 m_state = JobFailed;
925 m_mutex.unlock();
926 goto out;
929 if( readFile() )
931 // m_state is now Loaded
932 m_mutex.unlock();
933 goto out;
936 // If we get here it means the analyzer job went wrong, but
937 // somehow the MoodServer didn't know about it
938 debug() << "WARNING: Failed to open file " << moodFilename( m_bundle->url() )
939 << " -- something is very wrong" << endl;
940 m_state = JobFailed;
941 m_mutex.unlock();
943 out:
944 emit jobEvent( newState );
945 // This is a cheat for PlaylistItem so it doesn't have to
946 // use signals
947 m_bundle->moodbarJobEvent( newState );
951 // Draw the moodbar onto a pixmap of the given dimensions and return
952 // it. This is mostly Gav's original code, cut and pasted from
953 // various places. This will not change the state.
954 QPixmap
955 Moodbar::draw( int width, int height )
957 if( m_state != Loaded || !AmarokConfig::showMoodbar() ) // Naughty caller!
958 return QPixmap();
960 m_mutex.lock();
962 // Do we have to repaint, or can we use the cache?
963 if( m_pixmap.width() == width && m_pixmap.height() == height )
965 m_mutex.unlock();
966 return m_pixmap;
969 m_pixmap = QPixmap( width, height );
970 QPainter paint( &m_pixmap );
972 // First average the moodbar samples that will go into each
973 // vertical bar on the screen.
975 if( m_data.size() == 0 ) // Play it safe -- see below
976 return QPixmap();
978 ColorList screenColors;
979 QColor bar;
980 float r, g, b;
981 int h, s, v;
983 for( int i = 0; i < width; i++ )
985 r = 0.f; g = 0.f; b = 0.f;
987 // m_data.size() needs to be at least 1 for this not to crash!
988 uint start = i * m_data.size() / width;
989 uint end = (i + 1) * m_data.size() / width;
990 if( start == end )
991 end = start + 1;
993 for( uint j = start; j < end; j++ )
995 r += m_data[j].red();
996 g += m_data[j].green();
997 b += m_data[j].blue();
1000 uint n = end - start;
1001 bar = QColor( int( r / float( n ) ),
1002 int( g / float( n ) ),
1003 int( b / float( n ) ), QColor::Rgb );
1005 /* Snap to the HSV values for later */
1006 bar.getHsv(&h, &s, &v);
1007 bar.setHsv(h, s, v);
1009 screenColors.push_back( bar );
1012 // Paint the bars. This is Gav's painting code -- it breaks up the
1013 // monotony of solid-color vertical bars by playing with the saturation
1014 // and value.
1016 for( int x = 0; x < width; x++ )
1018 screenColors[x].getHsv( &h, &s, &v );
1020 for( int y = 0; y <= height / 2; y++ )
1022 float coeff = float(y) / float(height / 2);
1023 float coeff2 = 1.f - ((1.f - coeff) * (1.f - coeff));
1024 coeff = 1.f - (1.f - coeff) / 2.f;
1025 coeff2 = 1.f - (1.f - coeff2) / 2.f;
1026 paint.setPen( QColor( h,
1027 CLAMP( 0, int( float( s ) * coeff ), 255 ),
1028 CLAMP( 0, int( 255.f - (255.f - float( v )) * coeff2), 255 ),
1029 QColor::Hsv ) );
1030 paint.drawPoint(x, y);
1031 paint.drawPoint(x, height - 1 - y);
1035 m_mutex.unlock();
1037 return m_pixmap;
1041 #define NUM_HUES 12
1043 // Read the .mood file. Returns true if the read was successful
1044 // and changes the state to Loaded; returns false and leaves the
1045 // state untouched otherwise.
1047 // This is based on Gav's original code. We do the mood altering
1048 // (AmarokConfig::AlterMood()) here, as well as calculating the
1049 // hue-based sort. All displayed moodbars will be reset() when
1050 // the config is changed, so there's no harm in doing it here.
1052 // This method must be called with the instance locked.
1053 bool
1054 Moodbar::readFile( void )
1056 if( !AmarokConfig::showMoodbar() )
1057 return false;
1059 if( m_state == Loaded )
1060 return true;
1062 QString path = moodFilename( m_bundle->url() );
1063 if( path.isEmpty() )
1064 return false;
1066 debug() << "Moodbar::readFile: Trying to read " << path;
1068 QFile moodFile( path );
1070 if( !QFile::exists( path ) ||
1071 !moodFile.open( QIODevice::ReadOnly ) )
1073 // If the user has changed his/her preference about where to
1074 // store the mood files, he/she might have the .mood file
1075 // in the other place, so we should check there before giving
1076 // up.
1078 QString path2 = moodFilename( m_bundle->url(),
1079 !AmarokConfig::moodsWithMusic() );
1080 moodFile.setFileName( path2 );
1082 if( !QFile::exists( path2 ) ||
1083 !moodFile.open( QIODevice::ReadOnly ) )
1084 return false;
1086 debug() << "Moodbar::readFile: Found a file at " << path2
1087 << " instead, using that and copying." << endl;
1089 moodFile.close();
1090 if( !copyFile( path2, path ) )
1091 return false;
1092 moodFile.setFileName( path );
1093 if( !moodFile.open( QIODevice::ReadOnly ) )
1094 return false;
1097 int r, g, b, samples = moodFile.size() / 3;
1098 debug() << "Moodbar::readFile: File " << path
1099 << " opened. Proceeding to read contents... s=" << samples << endl;
1101 // This would be bad.
1102 if( samples == 0 )
1104 debug() << "Moodbar::readFile: File " << moodFile.name()
1105 << " is corrupted, removing." << endl;
1106 moodFile.remove();
1107 return false;
1110 int huedist[360], mx = 0; // For alterMood
1111 int modalHue[NUM_HUES]; // For m_hueSort
1112 int h, s, v;
1114 memset( modalHue, 0, sizeof( modalHue ) );
1115 memset( huedist, 0, sizeof( huedist ) );
1117 // Read the file, keeping track of some histograms
1118 for( int i = 0; i < samples; i++ )
1120 r = moodFile.getch();
1121 g = moodFile.getch();
1122 b = moodFile.getch();
1124 m_data.push_back( QColor( CLAMP( 0, r, 255 ),
1125 CLAMP( 0, g, 255 ),
1126 CLAMP( 0, b, 255 ), QColor::Rgb ) );
1128 // Make a histogram of hues
1129 m_data.last().getHsv( &h, &s, &v );
1130 modalHue[CLAMP( 0, h * NUM_HUES / 360, NUM_HUES - 1 )] += v;
1132 if( h < 0 ) h = 0; else h = h % 360;
1133 huedist[h]++;
1136 // Make moodier -- copied straight from Gav Wood's code
1137 // Here's an explanation of the algorithm:
1139 // The "input" hue for each bar is mapped to a hue between
1140 // rangeStart and (rangeStart + rangeDelta). The mapping is
1141 // determined by the hue histogram, huedist[], which is calculated
1142 // above by putting each sample into one of 360 hue bins. The
1143 // mapping is such that if your histogram is concentrated on a few
1144 // hues that are close together, then these hues are separated,
1145 // and the space between spikes in the hue histogram is
1146 // compressed. Here we consider a hue value to be a "spike" in
1147 // the hue histogram if the number of samples in that bin is
1148 // greater than the threshold variable.
1150 // As an example, suppose we have 100 samples, and that
1151 // threshold = 10 rangeStart = 0 rangeDelta = 288
1152 // Suppose that we have 10 samples at each of 99,100,101, and 200.
1153 // Suppose that there are 20 samples < 99, 20 between 102 and 199,
1154 // and 20 above 201, with no spikes. There will be five hues in
1155 // the output, at hues 0, 72, 144, 216, and 288, containing the
1156 // following number of samples:
1157 // 0: 20 + 10 = 30 (range 0 - 99 )
1158 // 72: 10 (range 100 - 100)
1159 // 144: 10 (range 101 - 101)
1160 // 216: 10 + 20 = 30 (range 102 - 200)
1161 // 288: 20 (range 201 - 359)
1162 // The hues are now much more evenly distributed.
1164 // After the hue redistribution is calculated, the saturation and
1165 // value are scaled by sat and val, respectively, which are percentage
1166 // values.
1168 if( AmarokConfig::makeMoodier() )
1170 // Explanation of the parameters:
1172 // threshold: A hue value is considered to be a "spike" in the
1173 // histogram if it's above this value. Setting this value
1174 // higher will tend to make the hue distribution more uniform
1176 // rangeStart, rangeDelta: output hues will be more or less
1177 // evenly spaced between rangeStart and (rangeStart + rangeDelta)
1179 // sat, val: the saturation and value are scaled by these integral
1180 // percentage values
1182 int threshold, rangeStart, rangeDelta, sat, val;
1183 int total = 0;
1184 memset( modalHue, 0, sizeof( modalHue ) ); // Recalculate this
1186 switch( AmarokConfig::alterMood() )
1188 case 1: // Angry
1189 threshold = samples / 360 * 9;
1190 rangeStart = 45;
1191 rangeDelta = -45;
1192 sat = 200;
1193 val = 100;
1194 break;
1196 case 2: // Frozen
1197 threshold = samples / 360 * 1;
1198 rangeStart = 140;
1199 rangeDelta = 160;
1200 sat = 50;
1201 val = 100;
1202 break;
1204 default: // Happy
1205 threshold = samples / 360 * 2;
1206 rangeStart = 0;
1207 rangeDelta = 359;
1208 sat = 150;
1209 val = 250;
1212 debug() << "ReadMood: Applying filter t=" << threshold
1213 << ", rS=" << rangeStart << ", rD=" << rangeDelta
1214 << ", s=" << sat << "%, v=" << val << "%" << endl;
1216 // On average, huedist[i] = samples / 360. This counts the
1217 // number of samples over the threshold, which is usually
1218 // 1, 2, 9, etc. times the average samples in each bin.
1219 // The total determines how many output hues there are,
1220 // evenly spaced between rangeStart and rangeStart + rangeDelta.
1221 for( int i = 0; i < 360; i++ )
1222 if( huedist[i] > threshold )
1223 total++;
1225 if( total < 360 && total > 0 )
1227 // Remap the hue values to be between rangeStart and
1228 // rangeStart + rangeDelta. Every time we see an input hue
1229 // above the threshold, increment the output hue by
1230 // (1/total) * rangeDelta.
1231 for( int i = 0, n = 0; i < 360; i++ )
1232 huedist[i] = ( ( huedist[i] > threshold ? n++ : n )
1233 * rangeDelta / total + rangeStart ) % 360;
1235 // Now huedist is a hue mapper: huedist[h] is the new hue value
1236 // for a bar with hue h
1238 for(uint i = 0; i < m_data.size(); i++)
1240 m_data[i].getHsv( &h, &s, &v );
1241 if( h < 0 ) h = 0; else h = h % 360;
1242 m_data[i].setHsv( CLAMP( 0, huedist[h], 359 ),
1243 CLAMP( 0, s * sat / 100, 255 ),
1244 CLAMP( 0, v * val / 100, 255 ) );
1246 modalHue[CLAMP(0, huedist[h] * NUM_HUES / 360, NUM_HUES - 1)]
1247 += (v * val / 100);
1252 // Calculate m_hueSort. This is a 3-digit number in base NUM_HUES,
1253 // where the most significant digit is the first strongest hue, the
1254 // second digit is the second strongest hue, and the third digit
1255 // is the third strongest. This code was written by Gav Wood.
1257 m_hueSort = 0;
1258 mx = 0;
1259 for( int i = 1; i < NUM_HUES; i++ )
1260 if( modalHue[i] > modalHue[mx] )
1261 mx = i;
1262 m_hueSort = mx * NUM_HUES * NUM_HUES;
1263 modalHue[mx] = 0;
1265 mx = 0;
1266 for( int i = 1; i < NUM_HUES; i++ )
1267 if( modalHue[i] > modalHue[mx] )
1268 mx = i;
1269 m_hueSort += mx * NUM_HUES;
1270 modalHue[mx] = 0;
1272 mx = 0;
1273 for( int i = 1; i < NUM_HUES; i++ )
1274 if( modalHue[i] > modalHue[mx] )
1275 mx = i;
1276 m_hueSort += mx;
1279 debug() << "Moodbar::readFile: All done.";
1281 moodFile.close();
1282 m_state = Loaded;
1284 return true;
1288 // Returns where the mood file for this bundle should be located,
1289 // based on the user preferences. If no location can be determined,
1290 // return QString::null.
1292 QString
1293 Moodbar::moodFilename( const KUrl &url )
1295 return moodFilename( url, AmarokConfig::moodsWithMusic() );
1298 QString
1299 Moodbar::moodFilename( const KUrl &url, bool withMusic )
1301 // No need to lock the object
1303 QString path;
1305 if( withMusic )
1307 path = url.path();
1308 path.truncate(path.lastIndexOf('.'));
1310 if (path.isEmpty()) // Weird...
1311 return QString();
1313 path += ".mood";
1314 int slash = path.lastIndexOf('/') + 1;
1315 QString dir = path.left(slash);
1316 QString file = path.right(path.length() - slash);
1317 path = dir + '.' + file;
1320 else
1322 // The moodbar file is {device id},{relative path}.mood}
1323 int deviceid = MountPointManager::instance()->getIdForUrl( url );
1324 KUrl relativePath;
1325 MountPointManager::instance()->getRelativePath( deviceid,
1326 url, relativePath );
1327 path = relativePath.path();
1328 path.truncate(path.lastIndexOf('.'));
1330 if (path.isEmpty()) // Weird...
1331 return QString();
1333 path = QString::number( deviceid ) + ','
1334 + path.replace('/', ',') + ".mood";
1336 // Creates the path if necessary
1337 path = KStandardDirs::locateLocal( "data", "amarok/moods/" + path );
1340 return path;
1344 // Quick-n-dirty -->synchronous<-- file copy (the GUI needs its
1345 // moodbars immediately!)
1346 bool
1347 Moodbar::copyFile( const QString &srcPath, const QString &dstPath )
1349 QFile file( srcPath );
1350 if( !file.open( QIODevice::ReadOnly ) )
1351 return false;
1352 QByteArray contents = file.readAll();
1353 file.close();
1354 file.setFileName( dstPath );
1355 if( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
1356 return false;
1357 bool res = ( uint( file.write( contents ) ) == contents.size() );
1358 file.close();
1359 return res;
1364 // Can we find the moodbar program?
1365 bool
1366 Moodbar::executableExists( void )
1368 return !(KStandardDirs::findExe( "moodbar" ).isNull());
1372 #include "moodbar.moc"