Revert previous commit, was incorrect
[amarok.git] / src / moodbar.cpp
blob13f5f66d90e99984e6c8d43665d2ced2ac9b42de
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 "Process.h"
291 #include "ContextStatusBar.h"
293 #include <KStandardDirs>
295 #include <Q3ValueList>
296 #include <QDir> // For QDir::rename()
297 #include <QFile>
298 #include <QPainter>
299 #include <QPixmap>
300 #include <QTimer>
302 #include <string.h> // for memset()
305 #define CLAMP(n, v, x) ((v) < (n) ? (n) : (v) > (x) ? (x) : (v))
307 #define WEBPAGE "http://amarok.kde.org/wiki/Moodbar"
310 ///////////////////////////////////////////////////////////////////////////////
311 // MoodServer class
312 ///////////////////////////////////////////////////////////////////////////////
315 MoodServer *
316 MoodServer::instance( void )
318 static MoodServer m;
319 return &m;
323 MoodServer::MoodServer( void )
324 : m_moodbarBroken( false )
325 , m_currentProcess( 0 )
327 connect( App::instance(), SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
328 SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) );
329 connect( CollectionDB::instance(),
330 SIGNAL( fileMoved( const QString &, const QString & ) ),
331 SLOT( slotFileMoved( const QString &, const QString & ) ) );
332 connect( CollectionDB::instance(),
333 SIGNAL( fileMoved( const QString &, const QString &, const QString & ) ),
334 SLOT( slotFileMoved( const QString &, const QString & ) ) );
335 connect( CollectionDB::instance(),
336 SIGNAL( fileDeleted( const QString & ) ),
337 SLOT( slotFileDeleted( const QString & ) ) );
338 connect( CollectionDB::instance(),
339 SIGNAL( fileDeleted( const QString &, const QString & ) ),
340 SLOT( slotFileDeleted( const QString & ) ) );
344 // Queue a job, but not before checking if the moodbar is enabled
345 // in the config, if the moodbar analyzer appears to be working,
346 // and if a job for that URL isn't already queued. Returns true
347 // if the job is already running, false otherwise.
348 bool
349 MoodServer::queueJob( MetaBundle *bundle )
351 if( m_moodbarBroken || !AmarokConfig::showMoodbar() )
352 return false;
354 m_mutex.lock();
356 // Check if the currently running job is for that URL
357 if( m_currentProcess != 0 &&
358 m_currentData.m_url == bundle->url() )
360 debug() << "MoodServer::queueJob: Not re-queueing already-running job "
361 << bundle->url().path();
362 m_mutex.unlock();
363 return true;
366 // Check if there's already a job in the queue for that URL
367 Q3ValueList<ProcData>::iterator it;
368 for( it = m_jobQueue.begin(); it != m_jobQueue.end(); ++it )
370 if( (*it).m_url == bundle->url() )
372 (*it).m_refcount++;
373 debug() << "MoodServer::queueJob: Job for " << bundle->url().path()
374 << " already in queue, increasing refcount to "
375 << (*it).m_refcount;
376 m_mutex.unlock();
377 return false;
381 m_jobQueue.append( ProcData( bundle->url(),
382 bundle->url().path(),
383 bundle->moodbar().moodFilename( bundle->url() ) ) );
385 debug() << "MoodServer::queueJob: Queued job for " << bundle->url().path()
386 << ", " << m_jobQueue.size() << " jobs in queue.";
388 m_mutex.unlock();
390 // New jobs *must* be started from the GUI thread!
391 QTimer::singleShot( 1000, this, SLOT( slotNewJob( void ) ) );
393 return false;
397 // Decrements the refcount of the job for the given URL
398 // and deletes that job if necessary.
399 void
400 MoodServer::deQueueJob( KUrl url )
402 m_mutex.lock();
404 // Can't de-queue running jobs
405 if( m_currentProcess != 0 &&
406 m_currentData.m_url == url )
408 debug() << "MoodServer::deQueueJob: Not de-queueing already-running job "
409 << url.path();
410 m_mutex.unlock();
411 return;
414 // Check if there's already a job in the queue for that URL
415 Q3ValueList<ProcData>::iterator it;
416 for( it = m_jobQueue.begin(); it != m_jobQueue.end(); ++it )
418 if( (*it).m_url == url )
420 (*it).m_refcount--;
422 if( (*it).m_refcount == 0 )
424 debug() << "MoodServer::deQueueJob: nobody cares about "
425 << (*it).m_url.path()
426 << " anymore, deleting from queue";
427 m_jobQueue.erase( it );
430 else
431 debug() << "MoodServer::deQueueJob: decrementing refcount of "
432 << (*it).m_url.path() << " to " << (*it).m_refcount
435 m_mutex.unlock();
436 return;
440 debug() << "MoodServer::deQueueJob: tried to delete nonexistent job "
441 << url.path();
443 m_mutex.unlock();
447 // This slot exists so that jobs can be started from the GUI thread,
448 // just in case queueJob() is run from another thread. Only run
449 // directly if you're in the GUI thread!
450 void
451 MoodServer::slotNewJob( void )
453 if( m_moodbarBroken )
454 return;
456 m_mutex.lock();
458 // Are we already running a process?
459 if( m_jobQueue.isEmpty() || m_currentProcess != 0 )
461 m_mutex.unlock();
462 return;
465 m_currentData = m_jobQueue.first();
466 m_jobQueue.pop_front();
468 debug() << "MoodServer::slotNewJob: starting new analyzer process: "
469 << "moodbar -o " << m_currentData.m_outfile << ".tmp "
470 << m_currentData.m_infile;
471 debug() << "MoodServer::slotNewJob: " << m_jobQueue.size()
472 << " jobs left in queue.";
475 // Write to outfile.mood.tmp so that new Moodbar instances
476 // don't think the mood data exists while the analyzer is
477 // running. Then rename the file later.
478 m_currentProcess = new Process( this );
479 m_currentProcess->setLowPriority( true ); // Nice the process
480 *m_currentProcess << KStandardDirs::findExe( "moodbar" ) << "-o"
481 << (m_currentData.m_outfile + ".tmp")
482 << m_currentData.m_infile;
484 connect( m_currentProcess, SIGNAL( finished( int ) ),
485 SLOT( slotJobCompleted( int ) ) );
487 // We have to enable K3Process::Stdout (even though we don't monitor
488 // it) since otherwise the child process crashes every time in
489 // K3Process::start() (but only when started from the loader!). I
490 // have no idea why, but I imagine it's a bug in KDE.
491 m_currentProcess->setOutputChannelMode( ProcIO::MergedChannels );
492 m_currentProcess->start( );
493 if( m_currentProcess->error() == Process::FailedToStart )
495 // If we have an error starting the process, it's never
496 // going to work, so call moodbarBroken()
497 warning() << "Can't start moodbar analyzer process!";
498 delete m_currentProcess;
499 m_currentProcess = 0;
500 m_mutex.unlock();
501 setMoodbarBroken();
502 return;
505 // Extreme reentrancy pedatry :)
506 KUrl url = m_currentData.m_url;
507 m_mutex.unlock();
509 emit jobEvent( url, Moodbar::JobStateRunning );
513 // This always run in the GUI thread. It is called
514 // when an analyzer process terminates
515 void
516 MoodServer::slotJobCompleted( int )
518 m_mutex.lock();
520 ReturnStatus returnval;
521 if( m_currentProcess->error() == Process::Crashed )
522 returnval = Crash;
523 else
524 returnval = (ReturnStatus) m_currentProcess->exitStatus();
526 bool success = (returnval == Success);
527 KUrl url = m_currentData.m_url;
529 if( success )
531 QString file = m_currentData.m_outfile;
532 QString dir = file.left( file.lastIndexOf( '/' ) );
533 file = file.right( file.length() - file.lastIndexOf( '/' ) - 1 );
534 QDir( dir ).rename( file + ".tmp", file );
536 else
537 QFile::remove( m_currentData.m_outfile + ".tmp" );
539 delete m_currentProcess;
540 m_currentProcess = 0;
543 // If the moodbar was disabled, we killed the process
544 if( !AmarokConfig::showMoodbar() )
546 debug() << "MoodServer::slotJobCompleted: moodbar disabled, job killed";
547 m_mutex.unlock();
548 emit jobEvent( url, Moodbar::JobStateFailed );
549 return;
553 switch( returnval )
555 case Success:
556 debug() << "MoodServer::slotJobCompleted: job completed successfully";
557 m_mutex.unlock();
558 slotNewJob();
559 break;
561 // Crash and NoFile don't mean that moodbar is broken.
562 // Something bad happened, but it's probably a problem with this file
563 // Just log an error message and emit jobEvent().
564 case Crash:
565 debug() << "MoodServer::slotJobCompleted: moodbar crashed on "
566 << m_currentData.m_infile;
567 m_mutex.unlock();
568 slotNewJob();
569 break;
571 case NoFile:
572 debug() << "MoodServer::slotJobCompleted: moodbar had a problem with "
573 << m_currentData.m_infile;
574 m_mutex.unlock();
575 slotNewJob();
576 break;
578 // NoPlugin and CommandLine mean the moodbar is broken
579 // The moodbar analyzer is not likely to work ever, so let the
580 // user know about it and disable new jobs.
581 default:
582 m_mutex.unlock();
583 setMoodbarBroken();
584 break;
588 emit jobEvent( url, success ? Moodbar::JobStateSucceeded
589 : Moodbar::JobStateFailed );
593 // This is called whenever "Ok" or "Apply" is pressed on the configuration
594 // dialog. If the moodbar is disabled, kill the current process and
595 // clear the queue
596 void
597 MoodServer::slotMoodbarPrefs( bool show, bool moodier, int alter, bool withMusic )
599 if( show == true)
600 return;
602 (void) moodier; (void) alter; (void) withMusic;
604 // If we have a current process, kill it. Cleanup happens in
605 // slotJobCompleted() above. We do *not* want to lock the
606 // mutex when calling this!
607 if( m_currentProcess != 0 )
608 m_currentProcess->kill();
610 clearJobs();
614 // When a file is deleted, either manually using Organize Collection or
615 // automatically detected using AFT, delete the corresponding mood file.
616 void
617 MoodServer::slotFileDeleted( const QString &path )
619 QString mood = Moodbar::moodFilename( KUrl( path ) );
620 if( mood.isEmpty() || !QFile::exists( mood ) )
621 return;
623 debug() << "MoodServer::slotFileDeleted: deleting " << mood;
624 QFile::remove( mood );
628 // When a file is moved, either manually using Organize Collection or
629 // automatically using AFT, move the corresponding mood file.
630 void
631 MoodServer::slotFileMoved( const QString &srcPath, const QString &dstPath )
633 QString srcMood = Moodbar::moodFilename( KUrl( srcPath ) );
634 QString dstMood = Moodbar::moodFilename( KUrl( dstPath ) );
636 if( srcMood.isEmpty() || dstMood.isEmpty() ||
637 srcMood == dstMood || !QFile::exists( srcMood ) )
638 return;
640 debug() << "MoodServer::slotFileMoved: moving " << srcMood << " to "
641 << dstMood;
643 Moodbar::copyFile( srcMood, dstMood );
644 QFile::remove( srcMood );
648 // This is called when we decide that the moodbar analyzer is
649 // never going to work. Disable further jobs, and let the user
650 // know about it. This should only be called when m_currentProcess == 0.
651 void
652 MoodServer::setMoodbarBroken( void )
654 warning() << "Uh oh, it looks like the moodbar analyzer is not going to work"
657 Amarok::ContextStatusBar::instance()->longMessage( i18n(
658 "The Amarok moodbar analyzer program seems to be broken. "
659 "This is probably because the moodbar package is not installed "
660 "correctly. The moodbar package, installation instructions, and "
661 "troubleshooting help can be found on the wiki page at <a href='"
662 WEBPAGE "'>" WEBPAGE "</a>. "
663 "When the problem is fixed, please restart Amarok."),
664 KDE::StatusBar::Error );
667 m_moodbarBroken = true;
668 clearJobs();
672 // Clear the job list and emit signals
673 void
674 MoodServer::clearJobs( void )
676 // We don't want to emit jobEvent (or really do anything
677 // external) while the mutex is locked.
678 m_mutex.lock();
679 Q3ValueList<ProcData> queueCopy
680 = Q3ValueList<ProcData> ( m_jobQueue );
681 m_jobQueue.clear();
682 m_mutex.unlock();
684 Q3ValueList<ProcData>::iterator it;
685 for( it = queueCopy.begin(); it != queueCopy.end(); ++it )
686 emit jobEvent( (*it).m_url, Moodbar::JobStateFailed );
691 ///////////////////////////////////////////////////////////////////////////////
692 // Moodbar class
693 ///////////////////////////////////////////////////////////////////////////////
696 // The moodbar behavior is nearly identical in the JobQueued and
697 // JobRunning states, but we have to keep track anyway so the
698 // PlaylistItem knows what do display
700 #define JOB_PENDING(state) ((state)==JobQueued||(state)==JobRunning)
703 // The passed MetaBundle _must_ be non-NULL, and the pointer must be valid
704 // as long as this instance is alive. The Moodbar is only meant to be a
705 // member of a MetaBundle, in other words.
707 Moodbar::Moodbar( MetaBundle *mb )
708 : QObject ( )
709 , m_bundle ( mb )
710 , m_hueSort ( 0 )
711 , m_state ( Unloaded )
716 // If we have any pending jobs, de-queue them. The use case I
717 // have in mind is if the user has the moodbar column displayed
718 // and adds all his/her tracks to the playlist, then deletes
719 // them again.
720 Moodbar::~Moodbar( void )
722 if( JOB_PENDING( m_state ) )
723 MoodServer::instance()->deQueueJob( m_url );
727 // MetaBundle's are often assigned using operator=, so so are we.
728 Moodbar&
729 Moodbar::operator=( const Moodbar &mood )
731 // Need to check this before locking both!
732 if( &mood == this )
733 return *this;
735 m_mutex.lock();
736 mood.m_mutex.lock();
738 State oldState = m_state;
739 KUrl oldURL = m_url;
741 m_data = mood.m_data;
742 m_pixmap = mood.m_pixmap;
743 m_state = mood.m_state;
744 m_url = mood.m_url;
745 // DO NOT overwrite m_bundle! That should never change.
747 // Signal connections and job queues are part of our "state",
748 // so those should be updated too.
749 if( JOB_PENDING( m_state ) && !JOB_PENDING( oldState ) )
751 connect( MoodServer::instance(),
752 SIGNAL( jobEvent( KUrl, int ) ),
753 SLOT( slotJobEvent( KUrl, int ) ) );
754 // Increase the refcount for this job. Use mood.m_bundle
755 // since that one's already initialized.
756 MoodServer::instance()->queueJob( mood.m_bundle );
759 // If we had a job pending, de-queue it
760 if( !JOB_PENDING( m_state ) && JOB_PENDING( oldState ) )
762 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl, int ) ) );
763 MoodServer::instance()->deQueueJob( oldURL );
766 mood.m_mutex.unlock();
767 m_mutex.unlock();
769 return *this;
773 // Reset the moodbar to its Unloaded state. This is useful when
774 // the configuration is changed, and all the moodbars need to be
775 // reloaded.
776 void
777 Moodbar::reset( void )
779 m_mutex.lock();
781 debug() << "Resetting moodbar: " << m_bundle->url().path();
783 if( JOB_PENDING( m_state ) )
785 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl, int ) ) );
786 MoodServer::instance()->deQueueJob( m_url );
789 m_data.clear();
790 m_pixmap = QPixmap();
791 m_url = KUrl();
792 m_hueSort = 0;
793 m_state = Unloaded;
795 m_mutex.unlock();
799 // If possible, try to open the bundle's .mood file. When this method
800 // returns true, this instance must be able to draw(). This may
801 // change the state to CantLoad, but usually leaves the state
802 // untouched.
803 bool
804 Moodbar::dataExists( void )
806 // Put this first for efficiency
807 if( m_state == Loaded )
808 return true;
810 // Should we bother checking for the file?
811 if( m_state == CantLoad ||
812 JOB_PENDING( m_state ) ||
813 m_state == JobFailed ||
814 !canHaveMood() )
815 return false;
817 m_mutex.lock();
818 bool res = readFile();
819 m_mutex.unlock();
821 return res;
825 // If m_bundle is not a local file or for some other reason cannot
826 // have mood data, return false, and set the state to CantLoad to
827 // save future checks. Note that MoodServer::m_moodbarBroken == true
828 // does not mean we can't have a mood file; it just means that we
829 // can't generate new ones.
830 bool
831 Moodbar::canHaveMood( void )
833 if( m_state == CantLoad )
834 return false;
836 // Don't try to analyze it if we can't even determine it has a length
837 // If for some reason we can't determine a file name, give up
838 // If the moodbar is disabled, set to CantLoad -- if the user re-enables
839 // the moodbar, we'll be reset() anyway.
840 if( !AmarokConfig::showMoodbar() ||
841 !m_bundle->url().isLocalFile() ||
842 !m_bundle->length() ||
843 moodFilename( m_bundle->url() ).isEmpty() )
845 m_state = CantLoad;
846 return false;
849 return true;
853 // Ask MoodServer to queue an analyzer job for us if necessary. This
854 // method will only do something the first time it's called, as it's
855 // guaranteed to change the state from Unloaded.
856 void
857 Moodbar::load( void )
859 if( m_state != Unloaded )
860 return;
862 m_mutex.lock();
864 if( !canHaveMood() )
866 // State is now CantLoad
867 m_mutex.unlock();
868 return;
871 if( readFile() )
873 // State is now Loaded
874 m_mutex.unlock();
875 return;
878 if( MoodServer::instance()->moodbarBroken() )
880 m_state = JobFailed;
881 m_mutex.unlock();
882 return;
885 // Ok no more excuses, we have to queue a job
886 connect( MoodServer::instance(),
887 SIGNAL( jobEvent( KUrl, int ) ),
888 SLOT( slotJobEvent( KUrl, int ) ) );
889 bool isRunning = MoodServer::instance()->queueJob( m_bundle );
890 m_state = isRunning ? JobRunning : JobQueued;
891 m_url = m_bundle->url(); // Use this URL for MoodServer::deQueueJob
893 m_mutex.unlock();
897 // This is called by MoodServer when our moodbar analyzer job starts
898 // or finishes. It may change the state from JobQueued / JobRunning
899 // to JobRunning, Loaded, or JobFailed. It may emit a jobEvent()
900 void
901 Moodbar::slotJobEvent( KUrl url, int newState )
903 // Is this job for us?
904 if( !JOB_PENDING( m_state ) || url != m_bundle->url() )
905 return;
907 bool success = ( newState == JobStateSucceeded );
909 // We don't really care about this, but our listeners might
910 if( newState == JobStateRunning )
912 m_state = JobRunning;
913 goto out;
916 m_mutex.lock();
918 // Disconnect the signal for efficiency's sake
919 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl, int ) ) );
921 if( !success )
923 m_state = JobFailed;
924 m_mutex.unlock();
925 goto out;
928 if( readFile() )
930 // m_state is now Loaded
931 m_mutex.unlock();
932 goto out;
935 // If we get here it means the analyzer job went wrong, but
936 // somehow the MoodServer didn't know about it
937 debug() << "WARNING: Failed to open file " << moodFilename( m_bundle->url() )
938 << " -- something is very wrong";
939 m_state = JobFailed;
940 m_mutex.unlock();
942 out:
943 emit jobEvent( newState );
944 // This is a cheat for PlaylistItem so it doesn't have to
945 // use signals
946 m_bundle->moodbarJobEvent( newState );
950 // Draw the moodbar onto a pixmap of the given dimensions and return
951 // it. This is mostly Gav's original code, cut and pasted from
952 // various places. This will not change the state.
953 QPixmap
954 Moodbar::draw( int width, int height )
956 if( m_state != Loaded || !AmarokConfig::showMoodbar() ) // Naughty caller!
957 return QPixmap();
959 m_mutex.lock();
961 // Do we have to repaint, or can we use the cache?
962 if( m_pixmap.width() == width && m_pixmap.height() == height )
964 m_mutex.unlock();
965 return m_pixmap;
968 m_pixmap = QPixmap( width, height );
969 QPainter paint( &m_pixmap );
971 // First average the moodbar samples that will go into each
972 // vertical bar on the screen.
974 if( m_data.size() == 0 ) // Play it safe -- see below
975 return QPixmap();
977 ColorList screenColors;
978 QColor bar;
979 float r, g, b;
980 int h, s, v;
982 for( int i = 0; i < width; i++ )
984 r = 0.f; g = 0.f; b = 0.f;
986 // m_data.size() needs to be at least 1 for this not to crash!
987 uint start = i * m_data.size() / width;
988 uint end = (i + 1) * m_data.size() / width;
989 if( start == end )
990 end = start + 1;
992 for( uint j = start; j < end; j++ )
994 r += m_data[j].red();
995 g += m_data[j].green();
996 b += m_data[j].blue();
999 uint n = end - start;
1000 bar = QColor( int( r / float( n ) ),
1001 int( g / float( n ) ),
1002 int( b / float( n ) ), QColor::Rgb );
1004 /* Snap to the HSV values for later */
1005 bar.getHsv(&h, &s, &v);
1006 bar.setHsv(h, s, v);
1008 screenColors.push_back( bar );
1011 // Paint the bars. This is Gav's painting code -- it breaks up the
1012 // monotony of solid-color vertical bars by playing with the saturation
1013 // and value.
1015 for( int x = 0; x < width; x++ )
1017 screenColors[x].getHsv( &h, &s, &v );
1019 for( int y = 0; y <= height / 2; y++ )
1021 float coeff = float(y) / float(height / 2);
1022 float coeff2 = 1.f - ((1.f - coeff) * (1.f - coeff));
1023 coeff = 1.f - (1.f - coeff) / 2.f;
1024 coeff2 = 1.f - (1.f - coeff2) / 2.f;
1025 paint.setPen( QColor( h,
1026 CLAMP( 0, int( float( s ) * coeff ), 255 ),
1027 CLAMP( 0, int( 255.f - (255.f - float( v )) * coeff2), 255 ),
1028 QColor::Hsv ) );
1029 paint.drawPoint(x, y);
1030 paint.drawPoint(x, height - 1 - y);
1034 m_mutex.unlock();
1036 return m_pixmap;
1040 #define NUM_HUES 12
1042 // Read the .mood file. Returns true if the read was successful
1043 // and changes the state to Loaded; returns false and leaves the
1044 // state untouched otherwise.
1046 // This is based on Gav's original code. We do the mood altering
1047 // (AmarokConfig::AlterMood()) here, as well as calculating the
1048 // hue-based sort. All displayed moodbars will be reset() when
1049 // the config is changed, so there's no harm in doing it here.
1051 // This method must be called with the instance locked.
1052 bool
1053 Moodbar::readFile( void )
1055 if( !AmarokConfig::showMoodbar() )
1056 return false;
1058 if( m_state == Loaded )
1059 return true;
1061 QString path = moodFilename( m_bundle->url() );
1062 if( path.isEmpty() )
1063 return false;
1065 debug() << "Moodbar::readFile: Trying to read " << path;
1067 QFile moodFile( path );
1069 if( !QFile::exists( path ) ||
1070 !moodFile.open( QIODevice::ReadOnly ) )
1072 // If the user has changed his/her preference about where to
1073 // store the mood files, he/she might have the .mood file
1074 // in the other place, so we should check there before giving
1075 // up.
1077 QString path2 = moodFilename( m_bundle->url(),
1078 !AmarokConfig::moodsWithMusic() );
1079 moodFile.setFileName( path2 );
1081 if( !QFile::exists( path2 ) ||
1082 !moodFile.open( QIODevice::ReadOnly ) )
1083 return false;
1085 debug() << "Moodbar::readFile: Found a file at " << path2
1086 << " instead, using that and copying.";
1088 moodFile.close();
1089 if( !copyFile( path2, path ) )
1090 return false;
1091 moodFile.setFileName( path );
1092 if( !moodFile.open( QIODevice::ReadOnly ) )
1093 return false;
1096 int r, g, b, samples = moodFile.size() / 3;
1097 debug() << "Moodbar::readFile: File " << path
1098 << " opened. Proceeding to read contents... s=" << samples;
1100 // This would be bad.
1101 if( samples == 0 )
1103 debug() << "Moodbar::readFile: File " << moodFile.name()
1104 << " is corrupted, removing.";
1105 moodFile.remove();
1106 return false;
1109 int huedist[360], mx = 0; // For alterMood
1110 int modalHue[NUM_HUES]; // For m_hueSort
1111 int h, s, v;
1113 memset( modalHue, 0, sizeof( modalHue ) );
1114 memset( huedist, 0, sizeof( huedist ) );
1116 // Read the file, keeping track of some histograms
1117 for( int i = 0; i < samples; i++ )
1119 r = moodFile.getch();
1120 g = moodFile.getch();
1121 b = moodFile.getch();
1123 m_data.push_back( QColor( CLAMP( 0, r, 255 ),
1124 CLAMP( 0, g, 255 ),
1125 CLAMP( 0, b, 255 ), QColor::Rgb ) );
1127 // Make a histogram of hues
1128 m_data.last().getHsv( &h, &s, &v );
1129 modalHue[CLAMP( 0, h * NUM_HUES / 360, NUM_HUES - 1 )] += v;
1131 if( h < 0 ) h = 0; else h = h % 360;
1132 huedist[h]++;
1135 // Make moodier -- copied straight from Gav Wood's code
1136 // Here's an explanation of the algorithm:
1138 // The "input" hue for each bar is mapped to a hue between
1139 // rangeStart and (rangeStart + rangeDelta). The mapping is
1140 // determined by the hue histogram, huedist[], which is calculated
1141 // above by putting each sample into one of 360 hue bins. The
1142 // mapping is such that if your histogram is concentrated on a few
1143 // hues that are close together, then these hues are separated,
1144 // and the space between spikes in the hue histogram is
1145 // compressed. Here we consider a hue value to be a "spike" in
1146 // the hue histogram if the number of samples in that bin is
1147 // greater than the threshold variable.
1149 // As an example, suppose we have 100 samples, and that
1150 // threshold = 10 rangeStart = 0 rangeDelta = 288
1151 // Suppose that we have 10 samples at each of 99,100,101, and 200.
1152 // Suppose that there are 20 samples < 99, 20 between 102 and 199,
1153 // and 20 above 201, with no spikes. There will be five hues in
1154 // the output, at hues 0, 72, 144, 216, and 288, containing the
1155 // following number of samples:
1156 // 0: 20 + 10 = 30 (range 0 - 99 )
1157 // 72: 10 (range 100 - 100)
1158 // 144: 10 (range 101 - 101)
1159 // 216: 10 + 20 = 30 (range 102 - 200)
1160 // 288: 20 (range 201 - 359)
1161 // The hues are now much more evenly distributed.
1163 // After the hue redistribution is calculated, the saturation and
1164 // value are scaled by sat and val, respectively, which are percentage
1165 // values.
1167 if( AmarokConfig::makeMoodier() )
1169 // Explanation of the parameters:
1171 // threshold: A hue value is considered to be a "spike" in the
1172 // histogram if it's above this value. Setting this value
1173 // higher will tend to make the hue distribution more uniform
1175 // rangeStart, rangeDelta: output hues will be more or less
1176 // evenly spaced between rangeStart and (rangeStart + rangeDelta)
1178 // sat, val: the saturation and value are scaled by these integral
1179 // percentage values
1181 int threshold, rangeStart, rangeDelta, sat, val;
1182 int total = 0;
1183 memset( modalHue, 0, sizeof( modalHue ) ); // Recalculate this
1185 switch( AmarokConfig::alterMood() )
1187 case 1: // Angry
1188 threshold = samples / 360 * 9;
1189 rangeStart = 45;
1190 rangeDelta = -45;
1191 sat = 200;
1192 val = 100;
1193 break;
1195 case 2: // Frozen
1196 threshold = samples / 360 * 1;
1197 rangeStart = 140;
1198 rangeDelta = 160;
1199 sat = 50;
1200 val = 100;
1201 break;
1203 default: // Happy
1204 threshold = samples / 360 * 2;
1205 rangeStart = 0;
1206 rangeDelta = 359;
1207 sat = 150;
1208 val = 250;
1211 debug() << "ReadMood: Applying filter t=" << threshold
1212 << ", rS=" << rangeStart << ", rD=" << rangeDelta
1213 << ", s=" << sat << "%, v=" << val << "%";
1215 // On average, huedist[i] = samples / 360. This counts the
1216 // number of samples over the threshold, which is usually
1217 // 1, 2, 9, etc. times the average samples in each bin.
1218 // The total determines how many output hues there are,
1219 // evenly spaced between rangeStart and rangeStart + rangeDelta.
1220 for( int i = 0; i < 360; i++ )
1221 if( huedist[i] > threshold )
1222 total++;
1224 if( total < 360 && total > 0 )
1226 // Remap the hue values to be between rangeStart and
1227 // rangeStart + rangeDelta. Every time we see an input hue
1228 // above the threshold, increment the output hue by
1229 // (1/total) * rangeDelta.
1230 for( int i = 0, n = 0; i < 360; i++ )
1231 huedist[i] = ( ( huedist[i] > threshold ? n++ : n )
1232 * rangeDelta / total + rangeStart ) % 360;
1234 // Now huedist is a hue mapper: huedist[h] is the new hue value
1235 // for a bar with hue h
1237 for(uint i = 0; i < m_data.size(); i++)
1239 m_data[i].getHsv( &h, &s, &v );
1240 if( h < 0 ) h = 0; else h = h % 360;
1241 m_data[i].setHsv( CLAMP( 0, huedist[h], 359 ),
1242 CLAMP( 0, s * sat / 100, 255 ),
1243 CLAMP( 0, v * val / 100, 255 ) );
1245 modalHue[CLAMP(0, huedist[h] * NUM_HUES / 360, NUM_HUES - 1)]
1246 += (v * val / 100);
1251 // Calculate m_hueSort. This is a 3-digit number in base NUM_HUES,
1252 // where the most significant digit is the first strongest hue, the
1253 // second digit is the second strongest hue, and the third digit
1254 // is the third strongest. This code was written by Gav Wood.
1256 m_hueSort = 0;
1257 mx = 0;
1258 for( int i = 1; i < NUM_HUES; i++ )
1259 if( modalHue[i] > modalHue[mx] )
1260 mx = i;
1261 m_hueSort = mx * NUM_HUES * NUM_HUES;
1262 modalHue[mx] = 0;
1264 mx = 0;
1265 for( int i = 1; i < NUM_HUES; i++ )
1266 if( modalHue[i] > modalHue[mx] )
1267 mx = i;
1268 m_hueSort += mx * NUM_HUES;
1269 modalHue[mx] = 0;
1271 mx = 0;
1272 for( int i = 1; i < NUM_HUES; i++ )
1273 if( modalHue[i] > modalHue[mx] )
1274 mx = i;
1275 m_hueSort += mx;
1278 debug() << "Moodbar::readFile: All done.";
1280 moodFile.close();
1281 m_state = Loaded;
1283 return true;
1287 // Returns where the mood file for this bundle should be located,
1288 // based on the user preferences. If no location can be determined,
1289 // return QString::null.
1291 QString
1292 Moodbar::moodFilename( const KUrl &url )
1294 return moodFilename( url, AmarokConfig::moodsWithMusic() );
1297 QString
1298 Moodbar::moodFilename( const KUrl &url, bool withMusic )
1300 // No need to lock the object
1302 QString path;
1304 if( withMusic )
1306 path = url.path();
1307 path.truncate(path.lastIndexOf('.'));
1309 if (path.isEmpty()) // Weird...
1310 return QString();
1312 path += ".mood";
1313 int slash = path.lastIndexOf('/') + 1;
1314 QString dir = path.left(slash);
1315 QString file = path.right(path.length() - slash);
1316 path = dir + '.' + file;
1319 else
1321 // The moodbar file is {device id},{relative path}.mood}
1322 int deviceid = MountPointManager::instance()->getIdForUrl( url );
1323 KUrl relativePath;
1324 MountPointManager::instance()->getRelativePath( deviceid,
1325 url, relativePath );
1326 path = relativePath.path();
1327 path.truncate(path.lastIndexOf('.'));
1329 if (path.isEmpty()) // Weird...
1330 return QString();
1332 path = QString::number( deviceid ) + ','
1333 + path.replace('/', ',') + ".mood";
1335 // Creates the path if necessary
1336 path = KStandardDirs::locateLocal( "data", "amarok/moods/" + path );
1339 return path;
1343 // Quick-n-dirty -->synchronous<-- file copy (the GUI needs its
1344 // moodbars immediately!)
1345 bool
1346 Moodbar::copyFile( const QString &srcPath, const QString &dstPath )
1348 QFile file( srcPath );
1349 if( !file.open( QIODevice::ReadOnly ) )
1350 return false;
1351 QByteArray contents = file.readAll();
1352 file.close();
1353 file.setFileName( dstPath );
1354 if( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
1355 return false;
1356 bool res = ( uint( file.write( contents ) ) == contents.size() );
1357 file.close();
1358 return res;
1363 // Can we find the moodbar program?
1364 bool
1365 Moodbar::executableExists( void )
1367 return !(KStandardDirs::findExe( "moodbar" ).isNull());
1371 #include "moodbar.moc"