1 /***************************************************************************
2 moodbar.cpp - description
5 copyright : (C) 2006 by Joseph Rabinoff
6 copyright : (C) 2005 by Gav Wood
7 email : bobqwatson@yahoo.com
8 ***************************************************************************/
10 /***************************************************************************
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. *
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.
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 )
41 // // This only needs to be done once!
42 // connect( &m_bundle.moodbar(), SIGNAL( jobEvent( int ) ),
43 // SLOT( newMoodData( int ) ) );
46 // void MyClass::newMetaBundle( const MetaBundle &b )
50 // if( !m_bundle.moodbar().dataExists() )
51 // m_bundle.moodbar().load();
56 // void MyClass::draw( void )
59 // if( m_bundle.moodbar().dataExists() )
60 // toDraw = m_bundle.moodbar().draw( width(), height() );
61 // // else draw something else...
64 // void MyClass::newMoodData( int newState )
66 // if( newState == Moodbar::JobStateSucceeded )
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.
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
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
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()
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
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
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.
277 #define DEBUG_PREFIX "Moodbar"
281 #include "config-amarok.h"
283 #include "amarokconfig.h"
286 #include "collectiondb.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()
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 ///////////////////////////////////////////////////////////////////////////////
311 ///////////////////////////////////////////////////////////////////////////////
315 MoodServer::instance( void )
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.
348 MoodServer::queueJob( MetaBundle
*bundle
)
350 if( m_moodbarBroken
|| !AmarokConfig::showMoodbar() )
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
;
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() )
372 debug() << "MoodServer::queueJob: Job for " << bundle
->url().path()
373 << " already in queue, increasing refcount to "
374 << (*it
).m_refcount
<< endl
;
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
;
389 // New jobs *must* be started from the GUI thread!
390 QTimer::singleShot( 1000, this, SLOT( slotNewJob( void ) ) );
396 // Decrements the refcount of the job for the given URL
397 // and deletes that job if necessary.
399 MoodServer::deQueueJob( KUrl url
)
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
;
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
)
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
);
430 debug() << "MoodServer::deQueueJob: decrementing refcount of "
431 << (*it
).m_url
.path() << " to " << (*it
).m_refcount
439 debug() << "MoodServer::deQueueJob: tried to delete nonexistent job "
440 << url
.path() << endl
;
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!
450 MoodServer::slotNewJob( void )
452 if( m_moodbarBroken
)
457 // Are we already running a process?
458 if( m_jobQueue
.isEmpty() || m_currentProcess
!= 0 )
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;
502 // Extreme reentrancy pedatry :)
503 KUrl url
= m_currentData
.m_url
;
506 emit
jobEvent( url
, Moodbar::JobStateRunning
);
510 // This always run in the GUI thread. It is called
511 // when an analyzer process terminates
513 MoodServer::slotJobCompleted( K3Process
*proc
)
518 if( proc
!= m_currentProcess
)
519 warning() << "MoodServer::slotJobCompleted: proc != m_currentProcess!" << endl
;
521 ReturnStatus returnval
;
522 if( !m_currentProcess
->normalExit() )
525 returnval
= (ReturnStatus
) m_currentProcess
->exitStatus();
527 bool success
= (returnval
== Success
);
528 KUrl url
= m_currentData
.m_url
;
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
);
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";
549 emit
jobEvent( url
, Moodbar::JobStateFailed
);
557 debug() << "MoodServer::slotJobCompleted: job completed successfully";
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().
566 debug() << "MoodServer::slotJobCompleted: moodbar crashed on "
567 << m_currentData
.m_infile
<< endl
;
573 debug() << "MoodServer::slotJobCompleted: moodbar had a problem with "
574 << m_currentData
.m_infile
<< endl
;
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.
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
598 MoodServer::slotMoodbarPrefs( bool show
, bool moodier
, int alter
, bool withMusic
)
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();
615 // When a file is deleted, either manually using Organize Collection or
616 // automatically detected using AFT, delete the corresponding mood file.
618 MoodServer::slotFileDeleted( const QString
&path
)
620 QString mood
= Moodbar::moodFilename( KUrl( path
) );
621 if( mood
.isEmpty() || !QFile::exists( mood
) )
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.
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
) )
641 debug() << "MoodServer::slotFileMoved: moving " << srcMood
<< " to "
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.
653 MoodServer::setMoodbarBroken( void )
655 warning() << "Uh oh, it looks like the moodbar analyzer is not going to work"
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;
673 // Clear the job list and emit signals
675 MoodServer::clearJobs( void )
677 // We don't want to emit jobEvent (or really do anything
678 // external) while the mutex is locked.
680 Q3ValueList
<ProcData
> queueCopy
681 = Q3ValueList
<ProcData
> ( m_jobQueue
);
685 Q3ValueList
<ProcData
>::iterator it
;
686 for( it
= queueCopy
.begin(); it
!= queueCopy
.end(); ++it
)
687 emit
jobEvent( (*it
).m_url
, Moodbar::JobStateFailed
);
692 ///////////////////////////////////////////////////////////////////////////////
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
)
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
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.
730 Moodbar::operator=( const Moodbar
&mood
)
732 // Need to check this before locking both!
739 State oldState
= m_state
;
742 m_data
= mood
.m_data
;
743 m_pixmap
= mood
.m_pixmap
;
744 m_state
= mood
.m_state
;
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();
774 // Reset the moodbar to its Unloaded state. This is useful when
775 // the configuration is changed, and all the moodbars need to be
778 Moodbar::reset( void )
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
);
791 m_pixmap
= QPixmap();
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
805 Moodbar::dataExists( void )
807 // Put this first for efficiency
808 if( m_state
== Loaded
)
811 // Should we bother checking for the file?
812 if( m_state
== CantLoad
||
813 JOB_PENDING( m_state
) ||
814 m_state
== JobFailed
||
819 bool res
= readFile();
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.
832 Moodbar::canHaveMood( void )
834 if( m_state
== CantLoad
)
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() )
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.
858 Moodbar::load( void )
860 if( m_state
!= Unloaded
)
867 // State is now CantLoad
874 // State is now Loaded
879 if( MoodServer::instance()->moodbarBroken() )
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
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()
902 Moodbar::slotJobEvent( KUrl url
, int newState
)
904 // Is this job for us?
905 if( !JOB_PENDING( m_state
) || url
!= m_bundle
->url() )
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
;
919 // Disconnect the signal for efficiency's sake
920 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl
, int ) ) );
931 // m_state is now Loaded
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
;
944 emit
jobEvent( newState
);
945 // This is a cheat for PlaylistItem so it doesn't have to
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.
955 Moodbar::draw( int width
, int height
)
957 if( m_state
!= Loaded
|| !AmarokConfig::showMoodbar() ) // Naughty caller!
962 // Do we have to repaint, or can we use the cache?
963 if( m_pixmap
.width() == width
&& m_pixmap
.height() == height
)
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
978 ColorList screenColors
;
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
;
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
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 ),
1030 paint
.drawPoint(x
, y
);
1031 paint
.drawPoint(x
, height
- 1 - y
);
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.
1054 Moodbar::readFile( void )
1056 if( !AmarokConfig::showMoodbar() )
1059 if( m_state
== Loaded
)
1062 QString path
= moodFilename( m_bundle
->url() );
1063 if( path
.isEmpty() )
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
1078 QString path2
= moodFilename( m_bundle
->url(),
1079 !AmarokConfig::moodsWithMusic() );
1080 moodFile
.setFileName( path2
);
1082 if( !QFile::exists( path2
) ||
1083 !moodFile
.open( QIODevice::ReadOnly
) )
1086 debug() << "Moodbar::readFile: Found a file at " << path2
1087 << " instead, using that and copying." << endl
;
1090 if( !copyFile( path2
, path
) )
1092 moodFile
.setFileName( path
);
1093 if( !moodFile
.open( QIODevice::ReadOnly
) )
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.
1104 debug() << "Moodbar::readFile: File " << moodFile
.name()
1105 << " is corrupted, removing." << endl
;
1110 int huedist
[360], mx
= 0; // For alterMood
1111 int modalHue
[NUM_HUES
]; // For m_hueSort
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 ),
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;
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
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
;
1184 memset( modalHue
, 0, sizeof( modalHue
) ); // Recalculate this
1186 switch( AmarokConfig::alterMood() )
1189 threshold
= samples
/ 360 * 9;
1197 threshold
= samples
/ 360 * 1;
1205 threshold
= samples
/ 360 * 2;
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
)
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)]
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.
1259 for( int i
= 1; i
< NUM_HUES
; i
++ )
1260 if( modalHue
[i
] > modalHue
[mx
] )
1262 m_hueSort
= mx
* NUM_HUES
* NUM_HUES
;
1266 for( int i
= 1; i
< NUM_HUES
; i
++ )
1267 if( modalHue
[i
] > modalHue
[mx
] )
1269 m_hueSort
+= mx
* NUM_HUES
;
1273 for( int i
= 1; i
< NUM_HUES
; i
++ )
1274 if( modalHue
[i
] > modalHue
[mx
] )
1279 debug() << "Moodbar::readFile: All done.";
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.
1293 Moodbar::moodFilename( const KUrl
&url
)
1295 return moodFilename( url
, AmarokConfig::moodsWithMusic() );
1299 Moodbar::moodFilename( const KUrl
&url
, bool withMusic
)
1301 // No need to lock the object
1308 path
.truncate(path
.lastIndexOf('.'));
1310 if (path
.isEmpty()) // Weird...
1314 int slash
= path
.lastIndexOf('/') + 1;
1315 QString dir
= path
.left(slash
);
1316 QString file
= path
.right(path
.length() - slash
);
1317 path
= dir
+ '.' + file
;
1322 // The moodbar file is {device id},{relative path}.mood}
1323 int deviceid
= MountPointManager::instance()->getIdForUrl( url
);
1325 MountPointManager::instance()->getRelativePath( deviceid
,
1326 url
, relativePath
);
1327 path
= relativePath
.path();
1328 path
.truncate(path
.lastIndexOf('.'));
1330 if (path
.isEmpty()) // Weird...
1333 path
= QString::number( deviceid
) + ','
1334 + path
.replace('/', ',') + ".mood";
1336 // Creates the path if necessary
1337 path
= KStandardDirs::locateLocal( "data", "amarok/moods/" + path
);
1344 // Quick-n-dirty -->synchronous<-- file copy (the GUI needs its
1345 // moodbars immediately!)
1347 Moodbar::copyFile( const QString
&srcPath
, const QString
&dstPath
)
1349 QFile
file( srcPath
);
1350 if( !file
.open( QIODevice::ReadOnly
) )
1352 QByteArray contents
= file
.readAll();
1354 file
.setFileName( dstPath
);
1355 if( !file
.open( QIODevice::WriteOnly
| QIODevice::Truncate
) )
1357 bool res
= ( uint( file
.write( contents
) ) == contents
.size() );
1364 // Can we find the moodbar program?
1366 Moodbar::executableExists( void )
1368 return !(KStandardDirs::findExe( "moodbar" ).isNull());
1372 #include "moodbar.moc"