File indexing completed on 2024-05-05 04:48:33
0001 /**************************************************************************************** 0002 * Copyright (c) 2005 Gav Wood <gav@kde.org> * 0003 * Copyright (c) 2006 Joseph Rabinoff <rabinoff@post.harvard.edu> * 0004 * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org> * 0005 * Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org> * 0006 * * 0007 * This program is free software; you can redistribute it and/or modify it under * 0008 * the terms of the GNU General Public License as published by the Free Software * 0009 * Foundation; either version 2 of the License, or (at your option) any later * 0010 * version. * 0011 * * 0012 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0013 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0014 * PARTICULAR PURPOSE. See the GNU General Pulic License for more details. * 0015 * * 0016 * You should have received a copy of the GNU General Public License along with * 0017 * this program. If not, see <http://www.gnu.org/licenses/>. * 0018 ****************************************************************************************/ 0019 0020 #define DEBUG_PREFIX "MoodbarManager" 0021 0022 /* 0023 The mood file loading and rendering code is based on the Amarok 1.4 moodbar implementation 0024 by Gav Wood and Joseph Rabinoff, ported to Qt 4 with only a few modifications by me. 0025 0026 The moodbar generator seems to be running just fine on modern systems if gstreamer is 0027 installed, but it could none the less do with a major update, perhaps to use Phonon or 0028 even porting to qtscript so it could be run, as needed, by Amarok. 0029 0030 - Nikolaj 0031 */ 0032 0033 #include "MoodbarManager.h" 0034 0035 #include "amarokconfig.h" 0036 #include "core/meta/Meta.h" 0037 #include "core/support/Debug.h" 0038 #include "PaletteHandler.h" 0039 0040 #include <QFile> 0041 #include <QFileInfo> 0042 #include <QPainter> 0043 0044 #define NUM_HUES 12 0045 0046 namespace The 0047 { 0048 static MoodbarManager* s_MoodbarManager_instance = nullptr; 0049 0050 MoodbarManager* moodbarManager() 0051 { 0052 if( !s_MoodbarManager_instance ) 0053 s_MoodbarManager_instance = new MoodbarManager(); 0054 0055 return s_MoodbarManager_instance; 0056 } 0057 } 0058 0059 MoodbarManager::MoodbarManager() 0060 : m_cache( new KImageCache( "Amarok-moodbars", 10 * 1024 ) ) 0061 , m_lastPaintMode( 0 ) 0062 { 0063 connect( The::paletteHandler(), &PaletteHandler::newPalette, this, &MoodbarManager::paletteChanged ); 0064 } 0065 0066 MoodbarManager::~MoodbarManager() 0067 {} 0068 0069 bool MoodbarManager::hasMoodbar( Meta::TrackPtr track ) 0070 { 0071 0072 //check if we already checked this track: 0073 if ( m_hasMoodMap.contains( track ) ) 0074 { 0075 //debug() << "Cached value, returning: " << m_hasMoodMap.value( track ); 0076 return m_hasMoodMap.value( track ); 0077 } 0078 0079 0080 QUrl trackUrl = track->playableUrl(); 0081 //only supports local files for now. 0082 if ( !trackUrl.isLocalFile() ) 0083 { 0084 debug() << "non local file, no moodbar..."; 0085 m_hasMoodMap.insert( track, false ); 0086 return false; 0087 } 0088 0089 //do we already have a moodFile path for this track? 0090 QString moodFilePath; 0091 if ( m_moodFileMap.contains( track ) ) 0092 moodFilePath = m_moodFileMap.value( track ); 0093 else 0094 { 0095 //Now, lets see if there is a mood file that matches the track filename 0096 moodFilePath = moodPath( trackUrl.path() ); 0097 0098 } 0099 0100 debug() << "file path: " << trackUrl.path(); 0101 debug() << "mood file path: " << moodFilePath; 0102 0103 if( !QFile::exists( moodFilePath ) ) 0104 { 0105 debug() << "no such file"; 0106 //for fun, try without the leading '.' 0107 0108 QFileInfo fInfo( moodFilePath ); 0109 QString testName = fInfo.fileName(); 0110 testName.remove( 0, 1 ); 0111 0112 moodFilePath.replace( fInfo.fileName(), testName ); 0113 0114 debug() << "trying : " << moodFilePath; 0115 if( !QFile::exists( moodFilePath ) ) 0116 { 0117 debug() << "no luck removing the leading '.' either..."; 0118 m_hasMoodMap.insert( track, false ); 0119 return false; 0120 } 0121 0122 debug() << "whoops, missing leading '.', so mood file path: " << moodFilePath; 0123 } 0124 0125 //it is a local file with a matching .mood file. Good enough for now! 0126 0127 m_moodFileMap.insert( track, moodFilePath ); 0128 m_hasMoodMap.insert( track, true ); 0129 0130 return true; 0131 } 0132 0133 QPixmap MoodbarManager::getMoodbar( Meta::TrackPtr track, int width, int height, bool rtl ) 0134 { 0135 //if we have already marked this track as 0136 //not having a moodbar, don't even bother... 0137 if ( m_hasMoodMap.contains( track ) ) 0138 if( !m_hasMoodMap.value( track ) ) 0139 return QPixmap(); 0140 0141 0142 //first of all... Check if rendering settings have changed. If 0143 //so, clear data and pixmap caches. 0144 0145 if( m_lastPaintMode != AmarokConfig::moodbarPaintStyle() ) 0146 { 0147 m_lastPaintMode = AmarokConfig::moodbarPaintStyle(); 0148 m_cache->clear(); 0149 m_moodDataMap.clear(); 0150 Q_EMIT moodbarStyleChanged(); 0151 } 0152 0153 0154 //Do we already have this pixmap cached? 0155 const QString pixmapKey = QStringLiteral( "mood:%1-%2x%3%4" ).arg( track->uidUrl(), QString::number( width ), 0156 QString::number( height ), QString( rtl?"r":"" ) ); 0157 QPixmap moodbar; 0158 0159 if( m_cache->findPixmap( pixmapKey, &moodbar ) ) 0160 return moodbar; 0161 0162 //No? Ok, then create it reusing as much info as possible 0163 0164 MoodbarColorList data; 0165 0166 if ( m_moodDataMap.contains( track ) ) 0167 data = m_moodDataMap.value( track ); 0168 else 0169 { 0170 0171 QString moodFilePath; 0172 if ( m_moodFileMap.contains( track ) ) 0173 moodFilePath = m_moodFileMap.value( track ); 0174 else 0175 moodFilePath = moodPath( track->playableUrl().path() ); 0176 0177 data = readMoodFile( QUrl::fromUserInput(moodFilePath) ); 0178 0179 if ( data.size() > 10 ) 0180 m_moodDataMap.insert( track, data ); 0181 else 0182 { 0183 //likely a corrupt file, so mark this track as not having a moodbar 0184 m_hasMoodMap.insert( track, false ); 0185 } 0186 } 0187 0188 //assume that the readMoodFile function emits the proper error... 0189 if ( data.size() < 10 ) 0190 return moodbar; 0191 0192 moodbar = drawMoodbar( data, width, height, rtl ); 0193 m_cache->insertPixmap( pixmapKey, moodbar ); 0194 0195 return moodbar; 0196 } 0197 0198 MoodbarColorList MoodbarManager::readMoodFile( const QUrl &moodFileUrl ) 0199 { 0200 DEBUG_BLOCK 0201 0202 MoodbarColorList data; 0203 0204 const QString path = moodFileUrl.path(); 0205 if( path.isEmpty() ) 0206 return data; 0207 0208 debug() << "Trying to read " << path; 0209 0210 QFile moodFile( path ); 0211 0212 if( !moodFile.open( QIODevice::ReadOnly ) ) 0213 return data; 0214 0215 int r, g, b, samples = moodFile.size() / 3; 0216 debug() << "File" << path << "opened. Proceeding to read contents... s=" << samples; 0217 0218 // This would be bad. 0219 if( samples == 0 ) 0220 { 0221 debug() << "Filex " << moodFile.fileName() << "is corrupted, removing"; 0222 //TODO: notify the user somehow 0223 //moodFile.remove(); 0224 return data; 0225 } 0226 0227 int huedist[360]; // For alterMood 0228 int modalHue[NUM_HUES]; // For m_hueSort 0229 int h, s, v; 0230 0231 memset( modalHue, 0, sizeof( modalHue ) ); 0232 memset( huedist, 0, sizeof( huedist ) ); 0233 0234 // Read the file, keeping track of some histograms 0235 for( int i = 0; i < samples; ++i ) 0236 { 0237 0238 char rChar, gChar, bChar; 0239 moodFile.getChar( &rChar ); 0240 moodFile.getChar( &gChar ); 0241 moodFile.getChar( &bChar ); 0242 0243 r = qAbs( (int) rChar ); 0244 g = qAbs( (int) gChar ); 0245 b = qAbs( (int) bChar ); 0246 0247 data.append( QColor( qBound( 0, r, 255 ), 0248 qBound( 0, g, 255 ), 0249 qBound( 0, b, 255 ) ) ); 0250 0251 // Make a histogram of hues 0252 data.last().getHsv( &h, &s, &v ); 0253 modalHue[qBound( 0, h * NUM_HUES / 360, NUM_HUES - 1 )] += v; 0254 0255 if( h < 0 ) h = 0; else h = h % 360; 0256 huedist[h]++; 0257 } 0258 0259 // Make moodier -- copied straight from Gav Wood's code 0260 // Here's an explanation of the algorithm: 0261 // 0262 // The "input" hue for each bar is mapped to a hue between 0263 // rangeStart and (rangeStart + rangeDelta). The mapping is 0264 // determined by the hue histogram, huedist[], which is calculated 0265 // above by putting each sample into one of 360 hue bins. The 0266 // mapping is such that if your histogram is concentrated on a few 0267 // hues that are close together, then these hues are separated, 0268 // and the space between spikes in the hue histogram is 0269 // compressed. Here we consider a hue value to be a "spike" in 0270 // the hue histogram if the number of samples in that bin is 0271 // greater than the threshold variable. 0272 // 0273 // As an example, suppose we have 100 samples, and that 0274 // threshold = 10 rangeStart = 0 rangeDelta = 288 0275 // Suppose that we have 10 samples at each of 99,100,101, and 200. 0276 // Suppose that there are 20 samples < 99, 20 between 102 and 199, 0277 // and 20 above 201, with no spikes. There will be five hues in 0278 // the output, at hues 0, 72, 144, 216, and 288, containing the 0279 // following number of samples: 0280 // 0: 20 + 10 = 30 (range 0 - 99 ) 0281 // 72: 10 (range 100 - 100) 0282 // 144: 10 (range 101 - 101) 0283 // 216: 10 + 20 = 30 (range 102 - 200) 0284 // 288: 20 (range 201 - 359) 0285 // The hues are now much more evenly distributed. 0286 // 0287 // After the hue redistribution is calculated, the saturation and 0288 // value are scaled by sat and val, respectively, which are percentage 0289 // values. 0290 moodFile.close(); 0291 0292 const int paintStyle = AmarokConfig::moodbarPaintStyle(); 0293 0294 { 0295 MoodbarColorList modifiedData; 0296 // Explanation of the parameters: 0297 // 0298 // threshold: A hue value is considered to be a "spike" in the 0299 // histogram if it's above this value. Setting this value 0300 // higher will tend to make the hue distribution more uniform 0301 // 0302 // rangeStart, rangeDelta: output hues will be more or less 0303 // evenly spaced between rangeStart and (rangeStart + rangeDelta) 0304 // 0305 // sat, val: the saturation and value are scaled by these integral 0306 // percentage values 0307 0308 int threshold, rangeStart, rangeDelta, sat, val; 0309 int total = 0; 0310 memset( modalHue, 0, sizeof( modalHue ) ); // Recalculate this 0311 0312 switch( paintStyle ) 0313 { 0314 case Angry: // Angry 0315 threshold = samples / 360 * 9; 0316 rangeStart = 45; 0317 rangeDelta = -45; 0318 sat = 200; 0319 val = 100; 0320 break; 0321 0322 case Frozen: // Frozen 0323 threshold = samples / 360 * 1; 0324 rangeStart = 140; 0325 rangeDelta = 160; 0326 sat = 50; 0327 val = 100; 0328 break; 0329 0330 case Happy: // Happy 0331 threshold = samples / 360 * 2; 0332 rangeStart = 0; 0333 rangeDelta = 359; 0334 sat = 150; 0335 val = 250; 0336 break; 0337 0338 case Normal: // old "normal" mode, don't change moodfile's RGB values 0339 threshold = samples / 360 * 3; 0340 rangeStart = 0; 0341 rangeDelta = 359; 0342 sat = 100; 0343 val = 100; 0344 break; 0345 0346 case SystemColours: 0347 default: // Default (system colours) 0348 threshold = samples / 360 * 3; 0349 rangeStart = The::paletteHandler()->highlightColor().hsvHue(); 0350 rangeStart = (rangeStart - 20 + 360) % 360; 0351 rangeDelta = 20; 0352 sat = The::paletteHandler()->highlightColor().hsvSaturation(); 0353 val = The::paletteHandler()->highlightColor().value() / 2; 0354 } 0355 0356 //debug() << "ReadMood: Applying filter t=" << threshold 0357 // << ", rS=" << rangeStart << ", rD=" << rangeDelta 0358 // << ", s=" << sat << "%, v=" << val << "%" << Qt::endl; 0359 0360 // On average, huedist[i] = samples / 360. This counts the 0361 // number of samples over the threshold, which is usually 0362 // 1, 2, 9, etc. times the average samples in each bin. 0363 // The total determines how many output hues there are, 0364 // evenly spaced between rangeStart and rangeStart + rangeDelta. 0365 for( int i = 0; i < 360; i++ ) 0366 if( huedist[i] > threshold ) 0367 total++; 0368 0369 if( total < 360 && total > 0 ) 0370 { 0371 // Remap the hue values to be between rangeStart and 0372 // rangeStart + rangeDelta. Every time we see an input hue 0373 // above the threshold, increment the output hue by 0374 // (1/total) * rangeDelta. 0375 for( int i = 0, n = 0; i < 360; i++ ) 0376 huedist[i] = ( ( huedist[i] > threshold ? n++ : n ) 0377 * rangeDelta / total + rangeStart ) % 360; 0378 0379 // Now huedist is a hue mapper: huedist[h] is the new hue value 0380 // for a bar with hue h 0381 foreach( QColor color, data ) 0382 { 0383 color.getHsv( &h, &s, &v ); 0384 h = h < 0 ? 0 : h % 360; 0385 0386 color.setHsv( qBound( 0, huedist[h], 359 ), 0387 qBound( 0, s * sat / 100, 255 ), 0388 qBound( 0, v * val / 100, 255 ) ); 0389 0390 modalHue[qBound( 0, huedist[h] * NUM_HUES / 360, NUM_HUES - 1 )] += ( v * val / 100 ); 0391 modifiedData.append( color ); 0392 } 0393 } 0394 return modifiedData; 0395 } 0396 0397 // Calculate m_hueSort. This is a 3-digit number in base NUM_HUES, 0398 // where the most significant digit is the first strongest hue, the 0399 // second digit is the second strongest hue, and the third digit 0400 // is the third strongest. This code was written by Gav Wood. 0401 0402 /* 0403 m_hueSort = 0; 0404 mx = 0; 0405 for( int i = 1; i < NUM_HUES; i++ ) 0406 if( modalHue[i] > modalHue[mx] ) 0407 mx = i; 0408 m_hueSort = mx * NUM_HUES * NUM_HUES; 0409 modalHue[mx] = 0; 0410 0411 mx = 0; 0412 for( int i = 1; i < NUM_HUES; i++ ) 0413 if( modalHue[i] > modalHue[mx] ) 0414 mx = i; 0415 m_hueSort += mx * NUM_HUES; 0416 modalHue[mx] = 0; 0417 0418 mx = 0; 0419 for( int i = 1; i < NUM_HUES; i++ ) 0420 if( modalHue[i] > modalHue[mx] ) 0421 mx = i; 0422 m_hueSort += mx; 0423 */ 0424 //debug() << "All done."; 0425 return data; 0426 } 0427 0428 QPixmap MoodbarManager::drawMoodbar( const MoodbarColorList &data, int width, int height, bool rtl ) 0429 { 0430 0431 // First average the moodbar samples that will go into each 0432 // vertical bar on the screen. 0433 0434 if( data.isEmpty() ) // Play it safe -- see below 0435 return QPixmap(); 0436 0437 MoodbarColorList screenColors; 0438 QColor bar; 0439 float r, g, b; 0440 int h, s, v; 0441 0442 for( int i = 0; i < width; i++ ) 0443 { 0444 r = 0.f; g = 0.f; b = 0.f; 0445 0446 // data.size() needs to be at least 1 for this not to crash! 0447 uint start = i * data.size() / width; 0448 uint end = (i + 1) * data.size() / width; 0449 0450 if( start == end ) 0451 end = start + 1; 0452 0453 for( uint j = start; j < end; j++ ) 0454 { 0455 r += data[j].red(); 0456 g += data[j].green(); 0457 b += data[j].blue(); 0458 } 0459 0460 uint n = end - start; 0461 bar = QColor( int( r / float( n ) ), 0462 int( g / float( n ) ), 0463 int( b / float( n ) ) ); 0464 0465 // Snap to the HSV values for later 0466 bar.getHsv(&h, &s, &v); 0467 bar.setHsv(h, s, v); 0468 0469 screenColors.append( bar ); 0470 } 0471 0472 // Paint the bars. This is Gav's painting code -- it breaks up the 0473 // monotony of solid-color vertical bars by playing with the saturation 0474 // and value. 0475 0476 QPixmap pixmap = QPixmap( width, height ); 0477 QPainter paint( &pixmap ); 0478 0479 for( int x = 0; x < width; x++ ) 0480 { 0481 screenColors[x].getHsv( &h, &s, &v ); 0482 0483 for( int y = 0; y <= height / 2; y++ ) 0484 { 0485 float coeff = float( y ) / float( height / 2 ); 0486 float coeff2 = 1.f - ( ( 1.f - coeff ) * ( 1.f - coeff ) ); 0487 coeff = 1.f - ( 1.f - coeff ) / 2.f; 0488 coeff2 = 1.f - ( 1.f - coeff2 ) / 2.f; 0489 0490 QColor hsvColor; 0491 hsvColor.setHsv( h, 0492 qBound( 0, int( float( s ) * coeff ), 255 ), 0493 qBound( 0, int( 255.f - (255.f - float( v ) ) * coeff2 ), 255 ) ) ; 0494 paint.setPen( hsvColor ); 0495 paint.drawPoint( x, y ); 0496 paint.drawPoint( x, height - 1 - y ); 0497 } 0498 } 0499 paint.end(); 0500 0501 if ( rtl ) 0502 pixmap = QPixmap::fromImage( pixmap.toImage().mirrored( true, false ) ); 0503 0504 return pixmap; 0505 } 0506 0507 QString MoodbarManager::moodPath( const QString &trackPath ) const 0508 { 0509 QStringList parts = trackPath.split( QLatin1Char('.') ); 0510 parts.takeLast(); 0511 parts.append( "mood" ); 0512 QString moodPath = parts.join( "." ); 0513 0514 //now prepend the filename with . 0515 const QFileInfo fileInfo( moodPath ); 0516 const QString fileName = fileInfo.fileName(); 0517 0518 return moodPath.replace( fileName, '.' + fileName ); 0519 } 0520 0521 void MoodbarManager::paletteChanged( const QPalette &palette ) 0522 { 0523 Q_UNUSED( palette ) 0524 const int paintStyle = AmarokConfig::moodbarPaintStyle(); 0525 if( paintStyle == 0 ) // system default colour 0526 { 0527 m_cache->clear(); 0528 m_moodDataMap.clear(); 0529 } 0530 }