kde-extraapps/amarok/src/moodbar/MoodbarManager.cpp
2015-01-31 00:30:50 +00:00

529 lines
17 KiB
C++

/****************************************************************************************
* Copyright (c) 2005 Gav Wood <gav@kde.org> *
* Copyright (c) 2006 Joseph Rabinoff <rabinoff@post.harvard.edu> *
* Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org> *
* Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org> *
* *
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
* Foundation; either version 2 of the License, or (at your option) any later *
* version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
* PARTICULAR PURPOSE. See the GNU General Pulic License for more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/
#define DEBUG_PREFIX "MoodbarManager"
/*
The mood file loading and rendering code is based on the Amarok 1.4 moodbar implementation
by Gav Wood and Joseph Rabinoff, ported to Qt 4 with only a few modifications by me.
The moodbar generator seems to be running just fine on modern systems if gstreamer is
installed, but it could none the less do with a major update, perhaps to use Phonon or
even porting to qtscript so it could be run, as needed, by Amarok.
- Nikolaj
*/
#include "MoodbarManager.h"
#include "amarokconfig.h"
#include "core/meta/Meta.h"
#include "core/support/Debug.h"
#include "PaletteHandler.h"
#include <QFile>
#include <QFileInfo>
#include <QPainter>
#define NUM_HUES 12
namespace The
{
static MoodbarManager* s_MoodbarManager_instance = 0;
MoodbarManager* moodbarManager()
{
if( !s_MoodbarManager_instance )
s_MoodbarManager_instance = new MoodbarManager();
return s_MoodbarManager_instance;
}
}
MoodbarManager::MoodbarManager()
: m_cache( new KImageCache( "Amarok-moodbars", 10 * 1024 ) )
, m_lastPaintMode( 0 )
{
connect( The::paletteHandler(), SIGNAL(newPalette(QPalette)), SLOT(paletteChanged(QPalette)) );
}
MoodbarManager::~MoodbarManager()
{}
bool MoodbarManager::hasMoodbar( Meta::TrackPtr track )
{
//check if we already checked this track:
if ( m_hasMoodMap.contains( track ) )
{
//debug() << "Cached value, returning: " << m_hasMoodMap.value( track );
return m_hasMoodMap.value( track );
}
KUrl trackUrl = track->playableUrl();
//only supports local files for now.
if ( !trackUrl.isLocalFile() )
{
debug() << "non local file, no moodbar...";
m_hasMoodMap.insert( track, false );
return false;
}
//do we already have a moodFile path for this track?
QString moodFilePath;
if ( m_moodFileMap.contains( track ) )
moodFilePath = m_moodFileMap.value( track );
else
{
//Now, lets see if there is a mood file that matches the track filename
moodFilePath = moodPath( trackUrl.path() );
}
debug() << "file path: " << trackUrl.path();
debug() << "mood file path: " << moodFilePath;
if( !QFile::exists( moodFilePath ) )
{
debug() << "no such file";
//for fun, try without the leading '.'
QFileInfo fInfo( moodFilePath );
QString testName = fInfo.fileName();
testName.remove( 0, 1 );
moodFilePath.replace( fInfo.fileName(), testName );
debug() << "trying : " << moodFilePath;
if( !QFile::exists( moodFilePath ) )
{
debug() << "no luck removing the leading '.' either...";
m_hasMoodMap.insert( track, false );
return false;
}
debug() << "whoops, missing leading '.', so mood file path: " << moodFilePath;
}
//it is a local file with a matching .mood file. Good enough for now!
m_moodFileMap.insert( track, moodFilePath );
m_hasMoodMap.insert( track, true );
return true;
}
QPixmap MoodbarManager::getMoodbar( Meta::TrackPtr track, int width, int height, bool rtl )
{
//if we have already marked this track as
//not having a moodbar, don't even bother...
if ( m_hasMoodMap.contains( track ) )
if( !m_hasMoodMap.value( track ) )
return QPixmap();
//first of all... Check if rendering settings have changed. If
//so, clear data and pixmap caches.
if( m_lastPaintMode != AmarokConfig::moodbarPaintStyle() )
{
m_lastPaintMode = AmarokConfig::moodbarPaintStyle();
m_cache->clear();
m_moodDataMap.clear();
emit moodbarStyleChanged();
}
//Do we already have this pixmap cached?
const QString pixmapKey = QString( "mood:%1-%2x%3%4" ).arg( track->uidUrl() ).arg(width).arg(height).arg( rtl?"r":"" );
QPixmap moodbar;
if( m_cache->findPixmap( pixmapKey, &moodbar ) )
return moodbar;
//No? Ok, then create it reusing as much info as possible
MoodbarColorList data;
if ( m_moodDataMap.contains( track ) )
data = m_moodDataMap.value( track );
else
{
QString moodFilePath;
if ( m_moodFileMap.contains( track ) )
moodFilePath = m_moodFileMap.value( track );
else
moodFilePath = moodPath( track->playableUrl().path() );
data = readMoodFile( moodFilePath );
if ( data.size() > 10 )
m_moodDataMap.insert( track, data );
else
{
//likely a corrupt file, so mark this track as not having a moodbar
m_hasMoodMap.insert( track, false );
}
}
//assume that the readMoodFile function emits the proper error...
if ( data.size() < 10 )
return moodbar;
moodbar = drawMoodbar( data, width, height, rtl );
m_cache->insertPixmap( pixmapKey, moodbar );
return moodbar;
}
MoodbarColorList MoodbarManager::readMoodFile( const KUrl &moodFileUrl )
{
DEBUG_BLOCK
MoodbarColorList data;
const QString path = moodFileUrl.path();
if( path.isEmpty() )
return data;
debug() << "Trying to read " << path;
QFile moodFile( path );
if( !moodFile.open( QIODevice::ReadOnly ) )
return data;
int r, g, b, samples = moodFile.size() / 3;
debug() << "File" << path << "opened. Proceeding to read contents... s=" << samples;
// This would be bad.
if( samples == 0 )
{
debug() << "Filex " << moodFile.fileName() << "is corrupted, removing";
//TODO: notify the user somehow
//moodFile.remove();
return data;
}
int huedist[360]; // For alterMood
int modalHue[NUM_HUES]; // For m_hueSort
int h, s, v;
memset( modalHue, 0, sizeof( modalHue ) );
memset( huedist, 0, sizeof( huedist ) );
// Read the file, keeping track of some histograms
for( int i = 0; i < samples; ++i )
{
char rChar, gChar, bChar;
moodFile.getChar( &rChar );
moodFile.getChar( &gChar );
moodFile.getChar( &bChar );
r = qAbs( (int) rChar );
g = qAbs( (int) gChar );
b = qAbs( (int) bChar );
data.append( QColor( qBound( 0, r, 255 ),
qBound( 0, g, 255 ),
qBound( 0, b, 255 ) ) );
// Make a histogram of hues
data.last().getHsv( &h, &s, &v );
modalHue[qBound( 0, h * NUM_HUES / 360, NUM_HUES - 1 )] += v;
if( h < 0 ) h = 0; else h = h % 360;
huedist[h]++;
}
// Make moodier -- copied straight from Gav Wood's code
// Here's an explanation of the algorithm:
//
// The "input" hue for each bar is mapped to a hue between
// rangeStart and (rangeStart + rangeDelta). The mapping is
// determined by the hue histogram, huedist[], which is calculated
// above by putting each sample into one of 360 hue bins. The
// mapping is such that if your histogram is concentrated on a few
// hues that are close together, then these hues are separated,
// and the space between spikes in the hue histogram is
// compressed. Here we consider a hue value to be a "spike" in
// the hue histogram if the number of samples in that bin is
// greater than the threshold variable.
//
// As an example, suppose we have 100 samples, and that
// threshold = 10 rangeStart = 0 rangeDelta = 288
// Suppose that we have 10 samples at each of 99,100,101, and 200.
// Suppose that there are 20 samples < 99, 20 between 102 and 199,
// and 20 above 201, with no spikes. There will be five hues in
// the output, at hues 0, 72, 144, 216, and 288, containing the
// following number of samples:
// 0: 20 + 10 = 30 (range 0 - 99 )
// 72: 10 (range 100 - 100)
// 144: 10 (range 101 - 101)
// 216: 10 + 20 = 30 (range 102 - 200)
// 288: 20 (range 201 - 359)
// The hues are now much more evenly distributed.
//
// After the hue redistribution is calculated, the saturation and
// value are scaled by sat and val, respectively, which are percentage
// values.
moodFile.close();
const int paintStyle = AmarokConfig::moodbarPaintStyle();
{
MoodbarColorList modifiedData;
// Explanation of the parameters:
//
// threshold: A hue value is considered to be a "spike" in the
// histogram if it's above this value. Setting this value
// higher will tend to make the hue distribution more uniform
//
// rangeStart, rangeDelta: output hues will be more or less
// evenly spaced between rangeStart and (rangeStart + rangeDelta)
//
// sat, val: the saturation and value are scaled by these integral
// percentage values
int threshold, rangeStart, rangeDelta, sat, val;
int total = 0;
memset( modalHue, 0, sizeof( modalHue ) ); // Recalculate this
switch( paintStyle )
{
case Angry: // Angry
threshold = samples / 360 * 9;
rangeStart = 45;
rangeDelta = -45;
sat = 200;
val = 100;
break;
case Frozen: // Frozen
threshold = samples / 360 * 1;
rangeStart = 140;
rangeDelta = 160;
sat = 50;
val = 100;
break;
case Happy: // Happy
threshold = samples / 360 * 2;
rangeStart = 0;
rangeDelta = 359;
sat = 150;
val = 250;
break;
case Normal: // old "normal" mode, don't change moodfile's RGB values
threshold = samples / 360 * 3;
rangeStart = 0;
rangeDelta = 359;
sat = 100;
val = 100;
break;
case SystemColours:
default: // Default (system colours)
threshold = samples / 360 * 3;
rangeStart = The::paletteHandler()->highlightColor().hsvHue();
rangeStart = (rangeStart - 20 + 360) % 360;
rangeDelta = 20;
sat = The::paletteHandler()->highlightColor().hsvSaturation();
val = The::paletteHandler()->highlightColor().value() / 2;
}
//debug() << "ReadMood: Applying filter t=" << threshold
// << ", rS=" << rangeStart << ", rD=" << rangeDelta
// << ", s=" << sat << "%, v=" << val << "%" << endl;
// On average, huedist[i] = samples / 360. This counts the
// number of samples over the threshold, which is usually
// 1, 2, 9, etc. times the average samples in each bin.
// The total determines how many output hues there are,
// evenly spaced between rangeStart and rangeStart + rangeDelta.
for( int i = 0; i < 360; i++ )
if( huedist[i] > threshold )
total++;
if( total < 360 && total > 0 )
{
// Remap the hue values to be between rangeStart and
// rangeStart + rangeDelta. Every time we see an input hue
// above the threshold, increment the output hue by
// (1/total) * rangeDelta.
for( int i = 0, n = 0; i < 360; i++ )
huedist[i] = ( ( huedist[i] > threshold ? n++ : n )
* rangeDelta / total + rangeStart ) % 360;
// Now huedist is a hue mapper: huedist[h] is the new hue value
// for a bar with hue h
foreach( QColor color, data )
{
color.getHsv( &h, &s, &v );
h = h < 0 ? 0 : h % 360;
color.setHsv( qBound( 0, huedist[h], 359 ),
qBound( 0, s * sat / 100, 255 ),
qBound( 0, v * val / 100, 255 ) );
modalHue[qBound( 0, huedist[h] * NUM_HUES / 360, NUM_HUES - 1 )] += ( v * val / 100 );
modifiedData.append( color );
}
}
return modifiedData;
}
// Calculate m_hueSort. This is a 3-digit number in base NUM_HUES,
// where the most significant digit is the first strongest hue, the
// second digit is the second strongest hue, and the third digit
// is the third strongest. This code was written by Gav Wood.
/*
m_hueSort = 0;
mx = 0;
for( int i = 1; i < NUM_HUES; i++ )
if( modalHue[i] > modalHue[mx] )
mx = i;
m_hueSort = mx * NUM_HUES * NUM_HUES;
modalHue[mx] = 0;
mx = 0;
for( int i = 1; i < NUM_HUES; i++ )
if( modalHue[i] > modalHue[mx] )
mx = i;
m_hueSort += mx * NUM_HUES;
modalHue[mx] = 0;
mx = 0;
for( int i = 1; i < NUM_HUES; i++ )
if( modalHue[i] > modalHue[mx] )
mx = i;
m_hueSort += mx;
*/
//debug() << "All done.";
return data;
}
QPixmap MoodbarManager::drawMoodbar( const MoodbarColorList &data, int width, int height, bool rtl )
{
// First average the moodbar samples that will go into each
// vertical bar on the screen.
if( data.size() == 0 ) // Play it safe -- see below
return QPixmap();
MoodbarColorList screenColors;
QColor bar;
float r, g, b;
int h, s, v;
for( int i = 0; i < width; i++ )
{
r = 0.f; g = 0.f; b = 0.f;
// data.size() needs to be at least 1 for this not to crash!
uint start = i * data.size() / width;
uint end = (i + 1) * data.size() / width;
if( start == end )
end = start + 1;
for( uint j = start; j < end; j++ )
{
r += data[j].red();
g += data[j].green();
b += data[j].blue();
}
uint n = end - start;
bar = QColor( int( r / float( n ) ),
int( g / float( n ) ),
int( b / float( n ) ) );
// Snap to the HSV values for later
bar.getHsv(&h, &s, &v);
bar.setHsv(h, s, v);
screenColors.append( bar );
}
// Paint the bars. This is Gav's painting code -- it breaks up the
// monotony of solid-color vertical bars by playing with the saturation
// and value.
QPixmap pixmap = QPixmap( width, height );
QPainter paint( &pixmap );
for( int x = 0; x < width; x++ )
{
screenColors[x].getHsv( &h, &s, &v );
for( int y = 0; y <= height / 2; y++ )
{
float coeff = float( y ) / float( height / 2 );
float coeff2 = 1.f - ( ( 1.f - coeff ) * ( 1.f - coeff ) );
coeff = 1.f - ( 1.f - coeff ) / 2.f;
coeff2 = 1.f - ( 1.f - coeff2 ) / 2.f;
QColor hsvColor;
hsvColor.setHsv( h,
qBound( 0, int( float( s ) * coeff ), 255 ),
qBound( 0, int( 255.f - (255.f - float( v ) ) * coeff2 ), 255 ) ) ;
paint.setPen( hsvColor );
paint.drawPoint( x, y );
paint.drawPoint( x, height - 1 - y );
}
}
paint.end();
if ( rtl )
pixmap = QPixmap::fromImage( pixmap.toImage().mirrored( true, false ) );
return pixmap;
}
QString MoodbarManager::moodPath( const QString &trackPath ) const
{
QStringList parts = trackPath.split( '.' );
parts.takeLast();
parts.append( "mood" );
QString moodPath = parts.join( "." );
//now prepend the filename with .
const QFileInfo fileInfo( moodPath );
const QString fileName = fileInfo.fileName();
return moodPath.replace( fileName, '.' + fileName );
}
void MoodbarManager::paletteChanged( const QPalette &palette )
{
Q_UNUSED( palette )
const int paintStyle = AmarokConfig::moodbarPaintStyle();
if( paintStyle == 0 ) // system default colour
{
m_cache->clear();
m_moodDataMap.clear();
}
}