101
// There are two new classes, namely the Moodbar (a member of
101
// There are two new classes, namely the Moodbar (a member of
102
102
// MetaBundle), and the MoodServer. The former is the only public
103
103
// class. In a nutshell, the Moodbar is responsible for reading
104
// and drawing mood data, and the MoodServer is in charge of
104
// and drawing mood data, and the MoodServer is in charge of
105
105
// queueing analyzer jobs and notifying interested Moodbar's when
106
106
// their job is done.
125
125
// not reentrant (from what I understand), so we don't want that being
126
126
// called every time a Moodbar is destroyed! For the same reason, the
127
127
// PlaylistItem does not listen for the jobEvent() signal; instead it
128
// reimplements the MetaBundle::moodbarJobEvent() virtual method.
128
// reimplements the MetaBundle::moodbarJobEvent() virtual method.
130
130
// Again for this reason, the individual Moodbar's don't listen for
131
131
// the App::moodbarPrefs() signal (which is emitted every time the
147
147
// immediately in this state.
148
148
// JobQueued: At some point load() was called, so we queued a job with
149
149
// the MoodServer which hasn't started yet. In this state,
150
// ~Moodbar(), reset(), etc. knows to dequeue jobs and
150
// ~Moodbar(), reset(), etc. knows to dequeue jobs and
151
151
// disconnect signals.
152
152
// JobRunning: Our analyzer job is actually running. The moodbar behaves
153
153
// basically the same as in the JobQueued state; this state
156
156
// trying), and came up empty. This state behaves basically
157
157
// the same as CantLoad.
158
158
// Loaded: This is the only state in which draw() will work.
161
161
// Note that nothing is done to load until dataExists() is called; this
162
162
// is because there may very well be MetaBundle's floating around that
163
163
// aren't displayed in the GUI.
165
165
// Important members:
166
166
// m_bundle: link to the parent bundle
167
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
168
// m_pixmap: the last time draw() was called, we cached what we drew
170
170
// m_url: cache the URL of our queued job for de-queueing
171
171
// m_state: our current state
199
199
// (private) readFile(): When we think there's a file available, this
200
// method tries to load it. We also do the display-independent
200
// method tries to load it. We also do the display-independent
201
201
// analysis here, namely, calculating the sorting index (for sort-
202
202
// by-hue in the Playlist), and Making Moodier.
205
205
// The MoodServer class --
207
207
// This is a singleton class. It is responsible for queueing analyzer
208
208
// jobs requested by Moodbar's, running them, and notifying the
209
209
// Moodbar's when the job has started and completed, successful or no.
218
218
// instead, each queued job has a refcount, which is increased. This
219
219
// is to support the de-queueing of jobs when Moodbar's are destroyed;
220
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
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
224
// passes the URL of the job that was completed.
226
226
// The analyzer is actually run using a KProcess. ThreadWeaver::Job
227
227
// is not a good solution, since we need more flexibility in the
228
228
// queuing process, and in addition, KProcess'es must be started from
231
231
// Important members:
232
232
// m_jobQueue: this is a list of MoodServer::ProcData structures,
233
233
// which contain the data needed to start and reference
234
234
// a process, as well as a refcount.
235
235
// m_currentProcess: the currently-running KProcess, if any.
236
// m_currentData: the ProcData structure for the currently-running
236
// m_currentData: the ProcData structure for the currently-running
238
238
// m_moodbarBroken: this is set when there's an error running the analyzer
239
239
// that indicates the analyzer will never be able to run.
255
255
// (private slot) slotJobCompleted(): Called when a job finishes. Do some
256
256
// cleanup, and notify the interested parties. Set m_moodbarBroken if
257
257
// necessary; otherwise call slotNewJob().
259
259
// (private slot) slotNewJob(): Called by slotJobCompleted() and queueJob().
260
260
// Take a job off the queue and start the KProcess.
262
262
// (private slot) slotMoodbarPrefs(): Called when the Amarok config changes.
263
263
// If the moodbar has been disabled completely, kill the current job
264
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
266
272
// TODO: off-color single bars in dark areas -- do some interpolation when
267
273
// averaging. Big jumps in hues when near black.
311
319
: m_moodbarBroken( false )
312
320
, m_currentProcess( 0 )
314
connect( App::instance(), SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
322
connect( App::instance(), SIGNAL( moodbarPrefs( bool, bool, int, bool ) ),
315
323
SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) );
324
connect( CollectionDB::instance(),
325
SIGNAL( fileMoved( const QString &, const QString & ) ),
326
SLOT( slotFileMoved( const QString &, const QString & ) ) );
327
connect( CollectionDB::instance(),
328
SIGNAL( fileMoved( const QString &, const QString &, const QString & ) ),
329
SLOT( slotFileMoved( const QString &, const QString & ) ) );
330
connect( CollectionDB::instance(),
331
SIGNAL( fileDeleted( const QString & ) ),
332
SLOT( slotFileDeleted( const QString & ) ) );
333
connect( CollectionDB::instance(),
334
SIGNAL( fileDeleted( const QString &, const QString & ) ),
335
SLOT( slotFileDeleted( const QString & ) ) );
356
m_jobQueue.append( ProcData( bundle->url(),
357
bundle->url().path(),
358
bundle->moodbar().moodFilename() ) );
376
m_jobQueue.append( ProcData( bundle->url(),
377
bundle->url().path(),
378
bundle->moodbar().moodFilename( bundle->url() ) ) );
360
debug() << "MoodServer::queueJob: Queued job for " << bundle->url().path()
380
debug() << "MoodServer::queueJob: Queued job for " << bundle->url().path()
361
381
<< ", " << m_jobQueue.size() << " jobs in queue." << endl;
363
383
m_mutex.unlock();
450
470
// Write to outfile.mood.tmp so that new Moodbar instances
451
471
// don't think the mood data exists while the analyzer is
452
472
// running. Then rename the file later.
453
m_currentProcess = new amaroK::Process( this );
473
m_currentProcess = new Amarok::Process( this );
454
474
m_currentProcess->setPriority( 19 ); // Nice the process
455
*m_currentProcess << KStandardDirs::findExe( "moodbar" ) << "-o"
475
*m_currentProcess << KStandardDirs::findExe( "moodbar" ) << "-o"
456
476
<< (m_currentData.m_outfile + ".tmp")
457
477
<< m_currentData.m_infile;
611
// When a file is deleted, either manually using Organize Collection or
612
// automatically detected using AFT, delete the corresponding mood file.
614
MoodServer::slotFileDeleted( const QString &path )
616
QString mood = Moodbar::moodFilename( KURL::fromPathOrURL( path ) );
617
if( mood.isEmpty() || !QFile::exists( mood ) )
620
debug() << "MoodServer::slotFileDeleted: deleting " << mood << endl;
621
QFile::remove( mood );
625
// When a file is moved, either manually using Organize Collection or
626
// automatically using AFT, move the corresponding mood file.
628
MoodServer::slotFileMoved( const QString &srcPath, const QString &dstPath )
630
QString srcMood = Moodbar::moodFilename( KURL::fromPathOrURL( srcPath ) );
631
QString dstMood = Moodbar::moodFilename( KURL::fromPathOrURL( dstPath ) );
633
if( srcMood.isEmpty() || dstMood.isEmpty() ||
634
srcMood == dstMood || !QFile::exists( srcMood ) )
637
debug() << "MoodServer::slotFileMoved: moving " << srcMood << " to "
640
Moodbar::copyFile( srcMood, dstMood );
641
QFile::remove( srcMood );
591
645
// This is called when we decide that the moodbar analyzer is
592
646
// never going to work. Disable further jobs, and let the user
593
647
// know about it. This should only be called when m_currentProcess == 0.
595
649
MoodServer::setMoodbarBroken( void )
597
warning() << "Uh oh, it looks like the moodbar analyzer is not going to work"
651
warning() << "Uh oh, it looks like the moodbar analyzer is not going to work"
600
amaroK::StatusBar::instance()->longMessage( i18n(
654
Amarok::StatusBar::instance()->longMessage( i18n(
601
655
"The Amarok moodbar analyzer program seems to be broken. "
602
656
"This is probably because the moodbar package is not installed "
603
657
"correctly. The moodbar package, installation instructions, and "
604
"troubleshooting help can be found on the wiki page at <a href='"
658
"troubleshooting help can be found on the wiki page at <a href='"
605
659
WEBPAGE "'>" WEBPAGE "</a>. "
606
660
"When the problem is fixed, please restart Amarok."),
607
661
KDE::StatusBar::Error );
691
745
// so those should be updated too.
692
746
if( JOB_PENDING( m_state ) && !JOB_PENDING( oldState ) )
694
connect( MoodServer::instance(),
748
connect( MoodServer::instance(),
695
749
SIGNAL( jobEvent( KURL, int ) ),
696
750
SLOT( slotJobEvent( KURL, int ) ) );
697
751
// Increase the refcount for this job. Use mood.m_bundle
796
850
// Don't try to analyze it if we can't even determine it has a length
797
851
// If for some reason we can't determine a file name, give up
798
852
// If the moodbar is disabled, set to CantLoad -- if the user re-enables
799
// the moodbar, we'll be reset() anyway.
853
// the moodbar, we'll be reset() anyway.
800
854
if( !AmarokConfig::showMoodbar() ||
801
855
!m_bundle->url().isLocalFile() ||
802
856
!m_bundle->length() ||
803
moodFilename().isEmpty() )
857
moodFilename( m_bundle->url() ).isEmpty() )
805
859
m_state = CantLoad;
982
1036
float coeff2 = 1.f - ((1.f - coeff) * (1.f - coeff));
983
1037
coeff = 1.f - (1.f - coeff) / 2.f;
984
1038
coeff2 = 1.f - (1.f - coeff2) / 2.f;
985
paint.setPen( QColor( h,
986
CLAMP( 0, int( float( s ) * coeff ), 255 ),
987
CLAMP( 0, int( 255.f - (255.f - float( v )) * coeff2), 255 ),
1039
paint.setPen( QColor( h,
1040
CLAMP( 0, int( float( s ) * coeff ), 255 ),
1041
CLAMP( 0, int( 255.f - (255.f - float( v )) * coeff2), 255 ),
988
1042
QColor::Hsv ) );
989
1043
paint.drawPoint(x, y);
990
1044
paint.drawPoint(x, height - 1 - y);
1027
1081
QFile moodFile( path );
1029
if( !QFile::exists( path ) ||
1083
if( !QFile::exists( path ) ||
1030
1084
!moodFile.open( IO_ReadOnly ) )
1032
1086
// If the user has changed his/her preference about where to
1033
1087
// store the mood files, he/she might have the .mood file
1034
1088
// in the other place, so we should check there before giving
1037
QString path2 = moodFilename( !AmarokConfig::moodsWithMusic() );
1091
QString path2 = moodFilename( m_bundle->url(),
1092
!AmarokConfig::moodsWithMusic() );
1038
1093
moodFile.setName( path2 );
1040
1095
if( !QFile::exists( path2 ) ||
1041
1096
!moodFile.open( IO_ReadOnly ) )
1044
debug() << "Moodbar::readFile: Found a file at " << path2
1099
debug() << "Moodbar::readFile: Found a file at " << path2
1045
1100
<< " instead, using that and copying." << endl;
1047
QByteArray contents = moodFile.readAll();
1048
1102
moodFile.close();
1103
if( !copyFile( path2, path ) )
1049
1105
moodFile.setName( path );
1050
if( !moodFile.open( IO_WriteOnly ) )
1052
bool res = ( uint( moodFile.writeBlock( contents ) ) == contents.size() );
1056
1106
if( !moodFile.open( IO_ReadOnly ) )
1060
1110
int r, g, b, samples = moodFile.size() / 3;
1061
debug() << "Moodbar::readFile: File " << path
1111
debug() << "Moodbar::readFile: File " << path
1062
1112
<< " opened. Proceeding to read contents... s=" << samples << endl;
1064
1114
// This would be bad.
1065
1115
if( samples == 0 )
1067
debug() << "Moodbar::readFile: File " << moodFile.name()
1117
debug() << "Moodbar::readFile: File " << moodFile.name()
1068
1118
<< " is corrupted, removing." << endl;
1069
1119
moodFile.remove();
1107
1157
// hues that are close together, then these hues are separated,
1108
1158
// and the space between spikes in the hue histogram is
1109
1159
// compressed. Here we consider a hue value to be a "spike" in
1110
// the hue histogram if the number of samples in that bin is
1160
// the hue histogram if the number of samples in that bin is
1111
1161
// greater than the threshold variable.
1113
1163
// As an example, suppose we have 100 samples, and that
1114
1164
// threshold = 10 rangeStart = 0 rangeDelta = 288
1115
1165
// Suppose that we have 10 samples at each of 99,100,101, and 200.
1149
1199
switch( AmarokConfig::alterMood() )
1151
1201
case 1: // Angry
1152
threshold = samples / 360 * 9;
1202
threshold = samples / 360 * 9;
1159
1209
case 2: // Frozen
1160
threshold = samples / 360 * 1;
1210
threshold = samples / 360 * 1;
1167
1217
default: // Happy
1168
threshold = samples / 360 * 2;
1218
threshold = samples / 360 * 2;
1175
debug() << "ReadMood: Appling filter t=" << threshold
1176
<< ", rS=" << rangeStart << ", rD=" << rangeDelta
1225
debug() << "ReadMood: Appling filter t=" << threshold
1226
<< ", rS=" << rangeStart << ", rD=" << rangeDelta
1177
1227
<< ", s=" << sat << "%, v=" << val << "%" << endl;
1179
// On average, huedist[i] = samples / 360. This counts the
1229
// On average, huedist[i] = samples / 360. This counts the
1180
1230
// number of samples over the threshold, which is usually
1181
1231
// 1, 2, 9, etc. times the average samples in each bin.
1182
1232
// The total determines how many output hues there are,
1183
1233
// evenly spaced between rangeStart and rangeStart + rangeDelta.
1184
for( int i = 0; i < 360; i++ )
1185
if( huedist[i] > threshold )
1234
for( int i = 0; i < 360; i++ )
1235
if( huedist[i] > threshold )
1188
1238
if( total < 360 && total > 0 )
1190
1240
// Remap the hue values to be between rangeStart and
1191
1241
// rangeStart + rangeDelta. Every time we see an input hue
1192
// above the threshold, increment the output hue by
1242
// above the threshold, increment the output hue by
1193
1243
// (1/total) * rangeDelta.
1194
1244
for( int i = 0, n = 0; i < 360; i++ )
1195
huedist[i] = ( ( huedist[i] > threshold ? n++ : n )
1245
huedist[i] = ( ( huedist[i] > threshold ? n++ : n )
1196
1246
* rangeDelta / total + rangeStart ) % 360;
1198
// Now huedist is a hue mapper: huedist[h] is the new hue value
1248
// Now huedist is a hue mapper: huedist[h] is the new hue value
1199
1249
// for a bar with hue h
1201
1251
for(uint i = 0; i < m_data.size(); i++)
1203
1253
m_data[i].getHsv( &h, &s, &v );
1204
1254
if( h < 0 ) h = 0; else h = h % 360;
1205
m_data[i].setHsv( CLAMP( 0, huedist[h], 359 ),
1206
CLAMP( 0, s * sat / 100, 255 ),
1255
m_data[i].setHsv( CLAMP( 0, huedist[h], 359 ),
1256
CLAMP( 0, s * sat / 100, 255 ),
1207
1257
CLAMP( 0, v * val / 100, 255 ) );
1209
modalHue[CLAMP(0, huedist[h] * NUM_HUES / 360, NUM_HUES - 1)]
1259
modalHue[CLAMP(0, huedist[h] * NUM_HUES / 360, NUM_HUES - 1)]
1210
1260
+= (v * val / 100);
1215
1265
// Calculate m_hueSort. This is a 3-digit number in base NUM_HUES,
1216
1266
// where the most significant digit is the first strongest hue, the
1217
// second digit is the second strongest hue, and the third digit
1267
// second digit is the second strongest hue, and the third digit
1218
1268
// is the third strongest. This code was written by Gav Wood.
1222
for( int i = 1; i < NUM_HUES; i++ )
1223
if( modalHue[i] > modalHue[mx] )
1272
for( int i = 1; i < NUM_HUES; i++ )
1273
if( modalHue[i] > modalHue[mx] )
1225
1275
m_hueSort = mx * NUM_HUES * NUM_HUES;
1226
1276
modalHue[mx] = 0;
1229
for( int i = 1; i < NUM_HUES; i++ )
1230
if( modalHue[i] > modalHue[mx] )
1279
for( int i = 1; i < NUM_HUES; i++ )
1280
if( modalHue[i] > modalHue[mx] )
1232
1282
m_hueSort += mx * NUM_HUES;
1233
1283
modalHue[mx] = 0;
1236
for( int i = 1; i < NUM_HUES; i++ )
1237
if( modalHue[i] > modalHue[mx] )
1286
for( int i = 1; i < NUM_HUES; i++ )
1287
if( modalHue[i] > modalHue[mx] )
1239
1289
m_hueSort += mx;
1242
1292
debug() << "Moodbar::readFile: All done." << endl;
1253
1303
// return QString::null.
1256
Moodbar::moodFilename( void )
1306
Moodbar::moodFilename( const KURL &url )
1258
return moodFilename( AmarokConfig::moodsWithMusic() );
1308
return moodFilename( url, AmarokConfig::moodsWithMusic() );
1262
Moodbar::moodFilename( bool withMusic )
1312
Moodbar::moodFilename( const KURL &url, bool withMusic )
1264
1314
// No need to lock the object
1266
QString path = m_bundle->url().path();
1267
path.truncate(path.findRev('.'));
1269
if (path.isEmpty()) // Weird...
1270
return QString::null;
1272
1318
if( withMusic )
1321
path.truncate(path.findRev('.'));
1323
if (path.isEmpty()) // Weird...
1324
return QString::null;
1274
1326
path += ".mood";
1275
1327
int slash = path.findRev('/') + 1;
1276
1328
QString dir = path.left(slash);
1277
1329
QString file = path.right(path.length() - slash);
1278
path = dir + "." + file;
1330
path = dir + '.' + file;
1282
path.replace('/', ',');
1335
// The moodbar file is {device id},{relative path}.mood}
1336
int deviceid = MountPointManager::instance()->getIdForUrl( url );
1338
MountPointManager::instance()->getRelativePath( deviceid,
1339
url, relativePath );
1340
path = relativePath.path();
1341
path.truncate(path.findRev('.'));
1343
if (path.isEmpty()) // Weird...
1344
return QString::null;
1346
path = QString::number( deviceid ) + ','
1347
+ path.replace('/', ',') + ".mood";
1283
1349
// Creates the path if necessary
1284
path = ::locateLocal("data", "amarok/moods/" + path + ".mood");
1350
path = ::locateLocal( "data", "amarok/moods/" + path );
1357
// Quick-n-dirty -->synchronous<-- file copy (the GUI needs its
1358
// moodbars immediately!)
1360
Moodbar::copyFile( const QString &srcPath, const QString &dstPath )
1362
QFile file( srcPath );
1363
if( !file.open( IO_ReadOnly ) )
1365
QByteArray contents = file.readAll();
1367
file.setName( dstPath );
1368
if( !file.open( IO_WriteOnly | IO_Truncate ) )
1370
bool res = ( uint( file.writeBlock( contents ) ) == contents.size() );
1291
1377
// Can we find the moodbar program?
1293
1379
Moodbar::executableExists( void )
1295
1381
return !(KStandardDirs::findExe( "moodbar" ).isNull());