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 }