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"
291 #include "ContextStatusBar.h"
293 #include <KStandardDirs>
295 #include <Q3ValueList>
296 #include <QDir> // For QDir::rename()
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 ///////////////////////////////////////////////////////////////////////////////
312 ///////////////////////////////////////////////////////////////////////////////
316 MoodServer::instance( void )
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.
349 MoodServer::queueJob( MetaBundle
*bundle
)
351 if( m_moodbarBroken
|| !AmarokConfig::showMoodbar() )
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();
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() )
373 debug() << "MoodServer::queueJob: Job for " << bundle
->url().path()
374 << " already in queue, increasing refcount to "
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.";
390 // New jobs *must* be started from the GUI thread!
391 QTimer::singleShot( 1000, this, SLOT( slotNewJob( void ) ) );
397 // Decrements the refcount of the job for the given URL
398 // and deletes that job if necessary.
400 MoodServer::deQueueJob( KUrl url
)
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 "
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
)
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
);
431 debug() << "MoodServer::deQueueJob: decrementing refcount of "
432 << (*it
).m_url
.path() << " to " << (*it
).m_refcount
440 debug() << "MoodServer::deQueueJob: tried to delete nonexistent job "
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!
451 MoodServer::slotNewJob( void )
453 if( m_moodbarBroken
)
458 // Are we already running a process?
459 if( m_jobQueue
.isEmpty() || m_currentProcess
!= 0 )
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;
505 // Extreme reentrancy pedatry :)
506 KUrl url
= m_currentData
.m_url
;
509 emit
jobEvent( url
, Moodbar::JobStateRunning
);
513 // This always run in the GUI thread. It is called
514 // when an analyzer process terminates
516 MoodServer::slotJobCompleted( int )
520 ReturnStatus returnval
;
521 if( m_currentProcess
->error() == Process::Crashed
)
524 returnval
= (ReturnStatus
) m_currentProcess
->exitStatus();
526 bool success
= (returnval
== Success
);
527 KUrl url
= m_currentData
.m_url
;
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
);
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";
548 emit
jobEvent( url
, Moodbar::JobStateFailed
);
556 debug() << "MoodServer::slotJobCompleted: job completed successfully";
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().
565 debug() << "MoodServer::slotJobCompleted: moodbar crashed on "
566 << m_currentData
.m_infile
;
572 debug() << "MoodServer::slotJobCompleted: moodbar had a problem with "
573 << m_currentData
.m_infile
;
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.
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
597 MoodServer::slotMoodbarPrefs( bool show
, bool moodier
, int alter
, bool withMusic
)
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();
614 // When a file is deleted, either manually using Organize Collection or
615 // automatically detected using AFT, delete the corresponding mood file.
617 MoodServer::slotFileDeleted( const QString
&path
)
619 QString mood
= Moodbar::moodFilename( KUrl( path
) );
620 if( mood
.isEmpty() || !QFile::exists( mood
) )
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.
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
) )
640 debug() << "MoodServer::slotFileMoved: moving " << srcMood
<< " to "
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.
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;
672 // Clear the job list and emit signals
674 MoodServer::clearJobs( void )
676 // We don't want to emit jobEvent (or really do anything
677 // external) while the mutex is locked.
679 Q3ValueList
<ProcData
> queueCopy
680 = Q3ValueList
<ProcData
> ( m_jobQueue
);
684 Q3ValueList
<ProcData
>::iterator it
;
685 for( it
= queueCopy
.begin(); it
!= queueCopy
.end(); ++it
)
686 emit
jobEvent( (*it
).m_url
, Moodbar::JobStateFailed
);
691 ///////////////////////////////////////////////////////////////////////////////
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
)
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
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.
729 Moodbar::operator=( const Moodbar
&mood
)
731 // Need to check this before locking both!
738 State oldState
= m_state
;
741 m_data
= mood
.m_data
;
742 m_pixmap
= mood
.m_pixmap
;
743 m_state
= mood
.m_state
;
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();
773 // Reset the moodbar to its Unloaded state. This is useful when
774 // the configuration is changed, and all the moodbars need to be
777 Moodbar::reset( void )
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
);
790 m_pixmap
= QPixmap();
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
804 Moodbar::dataExists( void )
806 // Put this first for efficiency
807 if( m_state
== Loaded
)
810 // Should we bother checking for the file?
811 if( m_state
== CantLoad
||
812 JOB_PENDING( m_state
) ||
813 m_state
== JobFailed
||
818 bool res
= readFile();
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.
831 Moodbar::canHaveMood( void )
833 if( m_state
== CantLoad
)
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() )
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.
857 Moodbar::load( void )
859 if( m_state
!= Unloaded
)
866 // State is now CantLoad
873 // State is now Loaded
878 if( MoodServer::instance()->moodbarBroken() )
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
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()
901 Moodbar::slotJobEvent( KUrl url
, int newState
)
903 // Is this job for us?
904 if( !JOB_PENDING( m_state
) || url
!= m_bundle
->url() )
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
;
918 // Disconnect the signal for efficiency's sake
919 MoodServer::instance()->disconnect( this, SLOT( slotJobEvent( KUrl
, int ) ) );
930 // m_state is now Loaded
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";
943 emit
jobEvent( newState
);
944 // This is a cheat for PlaylistItem so it doesn't have to
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.
954 Moodbar::draw( int width
, int height
)
956 if( m_state
!= Loaded
|| !AmarokConfig::showMoodbar() ) // Naughty caller!
961 // Do we have to repaint, or can we use the cache?
962 if( m_pixmap
.width() == width
&& m_pixmap
.height() == height
)
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
977 ColorList screenColors
;
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
;
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
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 ),
1029 paint
.drawPoint(x
, y
);
1030 paint
.drawPoint(x
, height
- 1 - y
);
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.
1053 Moodbar::readFile( void )
1055 if( !AmarokConfig::showMoodbar() )
1058 if( m_state
== Loaded
)
1061 QString path
= moodFilename( m_bundle
->url() );
1062 if( path
.isEmpty() )
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
1077 QString path2
= moodFilename( m_bundle
->url(),
1078 !AmarokConfig::moodsWithMusic() );
1079 moodFile
.setFileName( path2
);
1081 if( !QFile::exists( path2
) ||
1082 !moodFile
.open( QIODevice::ReadOnly
) )
1085 debug() << "Moodbar::readFile: Found a file at " << path2
1086 << " instead, using that and copying.";
1089 if( !copyFile( path2
, path
) )
1091 moodFile
.setFileName( path
);
1092 if( !moodFile
.open( QIODevice::ReadOnly
) )
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.
1103 debug() << "Moodbar::readFile: File " << moodFile
.name()
1104 << " is corrupted, removing.";
1109 int huedist
[360], mx
= 0; // For alterMood
1110 int modalHue
[NUM_HUES
]; // For m_hueSort
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 ),
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;
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
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
;
1183 memset( modalHue
, 0, sizeof( modalHue
) ); // Recalculate this
1185 switch( AmarokConfig::alterMood() )
1188 threshold
= samples
/ 360 * 9;
1196 threshold
= samples
/ 360 * 1;
1204 threshold
= samples
/ 360 * 2;
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
)
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)]
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.
1258 for( int i
= 1; i
< NUM_HUES
; i
++ )
1259 if( modalHue
[i
] > modalHue
[mx
] )
1261 m_hueSort
= mx
* NUM_HUES
* NUM_HUES
;
1265 for( int i
= 1; i
< NUM_HUES
; i
++ )
1266 if( modalHue
[i
] > modalHue
[mx
] )
1268 m_hueSort
+= mx
* NUM_HUES
;
1272 for( int i
= 1; i
< NUM_HUES
; i
++ )
1273 if( modalHue
[i
] > modalHue
[mx
] )
1278 debug() << "Moodbar::readFile: All done.";
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.
1292 Moodbar::moodFilename( const KUrl
&url
)
1294 return moodFilename( url
, AmarokConfig::moodsWithMusic() );
1298 Moodbar::moodFilename( const KUrl
&url
, bool withMusic
)
1300 // No need to lock the object
1307 path
.truncate(path
.lastIndexOf('.'));
1309 if (path
.isEmpty()) // Weird...
1313 int slash
= path
.lastIndexOf('/') + 1;
1314 QString dir
= path
.left(slash
);
1315 QString file
= path
.right(path
.length() - slash
);
1316 path
= dir
+ '.' + file
;
1321 // The moodbar file is {device id},{relative path}.mood}
1322 int deviceid
= MountPointManager::instance()->getIdForUrl( url
);
1324 MountPointManager::instance()->getRelativePath( deviceid
,
1325 url
, relativePath
);
1326 path
= relativePath
.path();
1327 path
.truncate(path
.lastIndexOf('.'));
1329 if (path
.isEmpty()) // Weird...
1332 path
= QString::number( deviceid
) + ','
1333 + path
.replace('/', ',') + ".mood";
1335 // Creates the path if necessary
1336 path
= KStandardDirs::locateLocal( "data", "amarok/moods/" + path
);
1343 // Quick-n-dirty -->synchronous<-- file copy (the GUI needs its
1344 // moodbars immediately!)
1346 Moodbar::copyFile( const QString
&srcPath
, const QString
&dstPath
)
1348 QFile
file( srcPath
);
1349 if( !file
.open( QIODevice::ReadOnly
) )
1351 QByteArray contents
= file
.readAll();
1353 file
.setFileName( dstPath
);
1354 if( !file
.open( QIODevice::WriteOnly
| QIODevice::Truncate
) )
1356 bool res
= ( uint( file
.write( contents
) ) == contents
.size() );
1363 // Can we find the moodbar program?
1365 Moodbar::executableExists( void )
1367 return !(KStandardDirs::findExe( "moodbar" ).isNull());
1371 #include "moodbar.moc"