1
0
mirror of https://github.com/tomahawk-player/tomahawk.git synced 2025-08-30 17:20:26 +02:00

implement Fingerprinting via LastFMInfoPlugin

This commit is contained in:
Stefan Derkits
2013-03-26 01:21:20 +01:00
parent 24ca843654
commit 8a1c4ecba0
10 changed files with 1050 additions and 49 deletions

View File

@@ -267,6 +267,10 @@ if( WIN32 )
macro_log_feature(QTSPARKLE_FOUND "qtsparkle" "Library for creating auto updaters written in Qt" "https://github.com/davidsansome/qtsparkle" FALSE "" "")
endif( WIN32 )
#Temporary only - we probably need either a CMake file for mad or something nicer than mad
include(LibFindMacros)
libfind_pkg_check_modules(mad_PKGCONF mad)
#TODO: support external qxt
set(QXTWEB_FOUND TRUE)
set(QXTWEB_LIBRARIES qxtweb-standalone)

View File

@@ -0,0 +1,99 @@
# Works the same as find_package, but forwards the "REQUIRED" and "QUIET" arguments
# used for the current package. For this to work, the first parameter must be the
# prefix of the current package, then the prefix of the new package etc, which are
# passed to find_package.
macro (libfind_package PREFIX)
set (LIBFIND_PACKAGE_ARGS ${ARGN})
if (${PREFIX}_FIND_QUIETLY)
set (LIBFIND_PACKAGE_ARGS ${LIBFIND_PACKAGE_ARGS} QUIET)
endif (${PREFIX}_FIND_QUIETLY)
if (${PREFIX}_FIND_REQUIRED)
set (LIBFIND_PACKAGE_ARGS ${LIBFIND_PACKAGE_ARGS} REQUIRED)
endif (${PREFIX}_FIND_REQUIRED)
find_package(${LIBFIND_PACKAGE_ARGS})
endmacro (libfind_package)
# CMake developers made the UsePkgConfig system deprecated in the same release (2.6)
# where they added pkg_check_modules. Consequently I need to support both in my scripts
# to avoid those deprecated warnings. Here's a helper that does just that.
# Works identically to pkg_check_modules, except that no checks are needed prior to use.
macro (libfind_pkg_check_modules PREFIX PKGNAME)
if (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4)
include(UsePkgConfig)
pkgconfig(${PKGNAME} ${PREFIX}_INCLUDE_DIRS ${PREFIX}_LIBRARY_DIRS ${PREFIX}_LDFLAGS ${PREFIX}_CFLAGS)
else (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4)
find_package(PkgConfig)
if (PKG_CONFIG_FOUND)
pkg_check_modules(${PREFIX} ${PKGNAME})
endif (PKG_CONFIG_FOUND)
endif (${CMAKE_MAJOR_VERSION} EQUAL 2 AND ${CMAKE_MINOR_VERSION} EQUAL 4)
endmacro (libfind_pkg_check_modules)
# Do the final processing once the paths have been detected.
# If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain
# all the variables, each of which contain one include directory.
# Ditto for ${PREFIX}_PROCESS_LIBS and library files.
# Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES.
# Also handles errors in case library detection was required, etc.
macro (libfind_process PREFIX)
# Skip processing if already processed during this run
if (NOT ${PREFIX}_FOUND)
# Start with the assumption that the library was found
set (${PREFIX}_FOUND TRUE)
# Process all includes and set _FOUND to false if any are missing
foreach (i ${${PREFIX}_PROCESS_INCLUDES})
if (${i})
set (${PREFIX}_INCLUDE_DIRS ${${PREFIX}_INCLUDE_DIRS} ${${i}})
mark_as_advanced(${i})
else (${i})
set (${PREFIX}_FOUND FALSE)
endif (${i})
endforeach (i)
# Process all libraries and set _FOUND to false if any are missing
foreach (i ${${PREFIX}_PROCESS_LIBS})
if (${i})
set (${PREFIX}_LIBRARIES ${${PREFIX}_LIBRARIES} ${${i}})
mark_as_advanced(${i})
else (${i})
set (${PREFIX}_FOUND FALSE)
endif (${i})
endforeach (i)
# Print message and/or exit on fatal error
if (${PREFIX}_FOUND)
if (NOT ${PREFIX}_FIND_QUIETLY)
message (STATUS "Found ${PREFIX} ${${PREFIX}_VERSION}")
endif (NOT ${PREFIX}_FIND_QUIETLY)
else (${PREFIX}_FOUND)
if (${PREFIX}_FIND_REQUIRED)
foreach (i ${${PREFIX}_PROCESS_INCLUDES} ${${PREFIX}_PROCESS_LIBS})
message("${i}=${${i}}")
endforeach (i)
message (FATAL_ERROR "Required library ${PREFIX} NOT FOUND.\nInstall the library (dev version) and try again. If the library is already installed, use ccmake to set the missing variables manually.")
endif (${PREFIX}_FIND_REQUIRED)
endif (${PREFIX}_FOUND)
endif (NOT ${PREFIX}_FOUND)
endmacro (libfind_process)
macro(libfind_library PREFIX basename)
set(TMP "")
if(MSVC80)
set(TMP -vc80)
endif(MSVC80)
if(MSVC90)
set(TMP -vc90)
endif(MSVC90)
set(${PREFIX}_LIBNAMES ${basename}${TMP})
if(${ARGC} GREATER 2)
set(${PREFIX}_LIBNAMES ${basename}${TMP}-${ARGV2})
string(REGEX REPLACE "\\." "_" TMP ${${PREFIX}_LIBNAMES})
set(${PREFIX}_LIBNAMES ${${PREFIX}_LIBNAMES} ${TMP})
endif(${ARGC} GREATER 2)
find_library(${PREFIX}_LIBRARY
NAMES ${${PREFIX}_LIBNAMES}
PATHS ${${PREFIX}_PKGCONF_LIBRARY_DIRS}
)
endmacro(libfind_library)

View File

@@ -371,6 +371,8 @@ IF(LIBLASTFM_FOUND)
accounts/lastfm/LastFmAccount.cpp
accounts/lastfm/LastFmConfig.cpp
accounts/lastfm/LastFmInfoPlugin.cpp
#Temporary only - requires libmad - can we do this anyhow with libs we already use?
accounts/lastfm/MadSource.cpp
)
ENDIF(LIBLASTFM_FOUND)
@@ -521,6 +523,11 @@ TARGET_LINK_LIBRARIES( tomahawklib
${QT_QTCORE_LIBRARY}
${OS_SPECIFIC_LINK_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
#Temporary only
${mad_PKGCONF_LDFLAGS}
#CMake should also find fp library
lastfm_fingerprint
${LINK_LIBRARIES}
)

View File

@@ -230,6 +230,8 @@ namespace Tomahawk
InfoUnLove = 91,
InfoShareTrack = 92,
InfoFingerprintTrack = 95,
InfoNotifyUser = 100,
InfoInboxReceived = 101,

View File

@@ -36,8 +36,14 @@
#include <lastfm/ws.h>
#include <lastfm/XmlQuery.h>
#include <lastfm/Track.h>
#include <lastfm/Fingerprint.h>
#include <lastfm/FingerprintableSource.h>
#include "MadSource.h"
#include <qjson/parser.h>
#include <boost/concept_check.hpp>
using namespace Tomahawk::Accounts;
using namespace Tomahawk::InfoSystem;
@@ -48,7 +54,7 @@ LastFmInfoPlugin::LastFmInfoPlugin( LastFmAccount* account )
, m_account( account )
, m_scrobbler( 0 )
{
m_supportedGetTypes << InfoAlbumCoverArt << InfoArtistImages << InfoArtistSimilars << InfoArtistSongs << InfoArtistBiography << InfoChart << InfoChartCapabilities << InfoTrackSimilars;
m_supportedGetTypes << InfoAlbumCoverArt << InfoArtistImages << InfoArtistSimilars << InfoArtistSongs << InfoArtistBiography << InfoChart << InfoChartCapabilities << InfoTrackSimilars << InfoFingerprintTrack;
m_supportedPushTypes << InfoSubmitScrobble << InfoSubmitNowPlaying << InfoLove << InfoUnLove;
}
@@ -137,6 +143,10 @@ LastFmInfoPlugin::getInfo( Tomahawk::InfoSystem::InfoRequestData requestData )
fetchSimilarTracks( requestData );
break;
case InfoFingerprintTrack:
fetchFingerprint( requestData );
break;
default:
dataError( requestData );
}
@@ -408,6 +418,78 @@ LastFmInfoPlugin::fetchAlbumInfo( Tomahawk::InfoSystem::InfoRequestData requestD
emit getCachedInfo( criteria, Q_INT64_C(2419200000), requestData );
}
void
LastFmInfoPlugin::fetchFingerprint( Tomahawk::InfoSystem::InfoRequestData requestData )
{
if ( !requestData.input.canConvert< Tomahawk::InfoSystem::InfoStringHash >() )
{
dataError( requestData );
return;
}
InfoStringHash hash = requestData.input.value< Tomahawk::InfoSystem::InfoStringHash >();
if ( !hash.contains( "file" ) )
{
dataError( requestData );
return;
}
QFileInfo fi( hash["file"] );
lastfm::MutableTrack track;
track.setUrl( QUrl::fromLocalFile( fi.canonicalFilePath() ) );
try
{
lastfm::Fingerprint* fp = new lastfm::Fingerprint( track );
if ( fp->id().isNull() )
{
//TODO: atm let's only fp mp3, make it later nicer and fp others too
if ( fi.fileName().endsWith( "mp3" ) || fi.fileName().endsWith( "MP3" ) )
{
lastfm::FingerprintableSource* fs = new MadSource( ); //TODO: this should become a PhononSource later
fp->generate( fs ); //TODO: put this into own Thread cause it could take long
QNetworkReply* reply = fp->submit();
QPair< QFileInfo, lastfm::Fingerprint* > pair;
pair.first = fi;
pair.second = fp;
m_fingerprintMap.insert( reply, pair );
reply->setProperty( "requestData", QVariant::fromValue< Tomahawk::InfoSystem::InfoRequestData >( requestData ) );
connect( reply, SIGNAL( finished() ), this, SLOT( fingerprintReturned() ) );
}
}
else
{
int id = fp->id();
delete fp;
fetchTrackInfo( requestData, fi, id );
}
}
catch ( lastfm::Fingerprint::Error e )
{
tDebug() << "FP Error: " << e;
}
}
void
LastFmInfoPlugin::fetchTrackInfo( Tomahawk::InfoSystem::InfoRequestData requestData, const QFileInfo& fi, int id )
{
QMap< QString, QString > query;
query["method"] = "track.getFingerprintMetadata";
query["fingerprintid"] = QString::number( id );
QNetworkReply* trackInfoReply = lastfm::ws::get( query );
trackInfoReply->setProperty( "requestData", QVariant::fromValue< Tomahawk::InfoSystem::InfoRequestData >( requestData ) );
trackInfoReply->setProperty( "file", QVariant::fromValue< QString >( fi.canonicalFilePath() ) );
connect( trackInfoReply, SIGNAL( finished() ), this, SLOT( trackInfoReturned() ) );
}
void
LastFmInfoPlugin::notInCacheSlot( QHash<QString, QString> criteria, Tomahawk::InfoSystem::InfoRequestData requestData )
@@ -895,6 +977,54 @@ LastFmInfoPlugin::artistImagesReturned()
}
void
LastFmInfoPlugin::fingerprintReturned()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>( sender() );
QFileInfo fi = m_fingerprintMap.value( reply ).first;
lastfm::Fingerprint* fp = m_fingerprintMap.value( reply ).second;
m_fingerprintMap.remove( reply );
fp->decode( reply );
int id = fp->id();
delete fp;
reply->deleteLater();
fetchTrackInfo( reply->property( "requestData" ).value< Tomahawk::InfoSystem::InfoRequestData >(), fi, id );
}
void
LastFmInfoPlugin::trackInfoReturned()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>( sender() );
QList< lastfm::Track > tracks = parseTrackList( reply );
Tomahawk::InfoSystem::InfoRequestData requestData = reply->property( "requestData" ).value< Tomahawk::InfoSystem::InfoRequestData >();
QFileInfo fi( reply->property( "file" ).value< QString >() );
QVariantMap returnedData;
returnedData["file"] = fi.canonicalFilePath();
if ( !tracks.isEmpty() )
{
lastfm::Track track = tracks.at( 0 );
returnedData["artist"] = track.artist().toString();
returnedData["title"] = track.title();
returnedData["album"] = track.album().toString();
returnedData["albumartist"] = track.albumArtist().toString();
}
else
{
tDebug() << "FP: we didn't find a result for " << fi.canonicalFilePath();
}
emit info( requestData, returnedData );
}
void
LastFmInfoPlugin::settingsChanged()
{

View File

@@ -27,10 +27,18 @@
#include <lastfm/Track.h>
#include <lastfm/Audioscrobbler.h>
#include <lastfm/ScrobblePoint.h>
#include <lastfm/Fingerprint.h>
#include <QObject>
class QNetworkReply;
class QFileInfo;
/*namespace lastm
{
class Fingerprint;
class FingerprintId;
}*/
namespace Tomahawk
{
@@ -65,6 +73,8 @@ public slots:
void albumInfoReturned();
void chartReturned();
void similarTracksReturned();
void fingerprintReturned();
void trackInfoReturned();
protected slots:
virtual void init();
@@ -81,6 +91,9 @@ private:
void fetchChart( Tomahawk::InfoSystem::InfoRequestData requestData );
void fetchChartCapabilities( Tomahawk::InfoSystem::InfoRequestData requestData );
void fetchSimilarTracks( Tomahawk::InfoSystem::InfoRequestData requestData );
void fetchFingerprint( Tomahawk::InfoSystem::InfoRequestData requestData );
void fetchTrackInfo( Tomahawk::InfoSystem::InfoRequestData requestData, const QFileInfo& fi, int id );
void createScrobbler();
void nowPlaying( const QVariant& input );
@@ -97,6 +110,8 @@ private:
QString m_pw;
QList< QUrl > m_badUrls;
QMap< QNetworkReply* , QPair< QFileInfo, lastfm::Fingerprint* > > m_fingerprintMap;
};
}

View File

@@ -0,0 +1,514 @@
/*
Copyright 2009 Last.fm Ltd.
Copyright 2009 John Stamp <jstamp@users.sourceforge.net>
This file is part of liblastfm.
liblastfm 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 3 of the License, or
(at your option) any later version.
liblastfm 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 Public License for more details.
You should have received a copy of the GNU General Public License
along with liblastfm. If not, see <http://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <fstream>
#include <limits>
#include <climits>
#include <cstdlib>
#include <sstream>
#include <cassert>
#include <stdexcept>
#include "MadSource.h"
#undef max // was definded in mad
using namespace std;
// -----------------------------------------------------------
MadSource::MadSource()
: m_pMP3_Buffer ( new unsigned char[m_MP3_BufferSize+MAD_BUFFER_GUARD] )
{}
// -----------------------------------------------------------
MadSource::~MadSource()
{
if ( m_inputFile.isOpen() )
{
m_inputFile.close();
mad_synth_finish(&m_mad_synth);
mad_frame_finish(&m_mad_frame);
mad_stream_finish(&m_mad_stream);
}
if (m_pMP3_Buffer) delete[] m_pMP3_Buffer;
}
// ---------------------------------------------------------------------
inline short f2s(mad_fixed_t f)
{
/* A fixed point number is formed of the following bit pattern:
*
* SWWWFFFFFFFFFFFFFFFFFFFFFFFFFFFF
* MSB LSB
* S ==> Sign (0 is positive, 1 is negative)
* W ==> Whole part bits
* F ==> Fractional part bits
*
* This pattern contains MAD_F_FRACBITS fractional bits, one
* should alway use this macro when working on the bits of a fixed
* point number. It is not guaranteed to be constant over the
* different platforms supported by libmad.
*
* The signed short value is formed, after clipping, by the least
* significant whole part bit, followed by the 15 most significant
* fractional part bits. Warning: this is a quick and dirty way to
* compute the 16-bit number, madplay includes much better
* algorithms.
*/
/* Clipping */
if(f >= MAD_F_ONE)
return(SHRT_MAX);
if(f <= -MAD_F_ONE)
return(-SHRT_MAX);
/* Conversion. */
f = f >> (MAD_F_FRACBITS-15);
return (signed short)f;
}
// ---------------------------------------------------------------------
string MadSource::MadErrorString(const mad_error& error)
{
switch(error)
{
/* Generic unrecoverable errors. */
case MAD_ERROR_BUFLEN:
return("input buffer too small (or EOF)");
case MAD_ERROR_BUFPTR:
return("invalid (null) buffer pointer");
case MAD_ERROR_NOMEM:
return("not enough memory");
/* Frame header related unrecoverable errors. */
case MAD_ERROR_LOSTSYNC:
return("lost synchronization");
case MAD_ERROR_BADLAYER:
return("reserved header layer value");
case MAD_ERROR_BADBITRATE:
return("forbidden bitrate value");
case MAD_ERROR_BADSAMPLERATE:
return("reserved sample frequency value");
case MAD_ERROR_BADEMPHASIS:
return("reserved emphasis value");
/* Recoverable errors */
case MAD_ERROR_BADCRC:
return("CRC check failed");
case MAD_ERROR_BADBITALLOC:
return("forbidden bit allocation value");
case MAD_ERROR_BADSCALEFACTOR:
return("bad scalefactor index");
case MAD_ERROR_BADFRAMELEN:
return("bad frame length");
case MAD_ERROR_BADBIGVALUES:
return("bad big_values count");
case MAD_ERROR_BADBLOCKTYPE:
return("reserved block_type");
case MAD_ERROR_BADSCFSI:
return("bad scalefactor selection info");
case MAD_ERROR_BADDATAPTR:
return("bad main_data_begin pointer");
case MAD_ERROR_BADPART3LEN:
return("bad audio data length");
case MAD_ERROR_BADHUFFTABLE:
return("bad Huffman table select");
case MAD_ERROR_BADHUFFDATA:
return("Huffman data overrun");
case MAD_ERROR_BADSTEREO:
return("incompatible block_type for JS");
/* Unknown error. This switch may be out of sync with libmad's
* defined error codes.
*/
default:
return("Unknown error code");
}
}
// -----------------------------------------------------------------------------
bool MadSource::isRecoverable(const mad_error& error, bool log)
{
if (MAD_RECOVERABLE (error))
{
/* Do not print a message if the error is a loss of
* synchronization and this loss is due to the end of
* stream guard bytes. (See the comments marked {3}
* supra for more informations about guard bytes.)
*/
if (error != MAD_ERROR_LOSTSYNC /*|| mad_stream.this_frame != pGuard */ && log)
{
cerr << "Recoverable frame level error: "
<< MadErrorString(error) << endl;
}
return true;
}
else
{
if (error == MAD_ERROR_BUFLEN)
return true;
else
{
stringstream ss;
ss << "Unrecoverable frame level error: "
<< MadErrorString (error) << endl;
throw ss.str();
}
}
return false;
}
// -----------------------------------------------------------
void MadSource::init(const QString& fileName)
{
m_inputFile.setFileName( m_fileName = fileName );
bool fine = m_inputFile.open( QIODevice::ReadOnly );
if ( !fine )
{
throw std::runtime_error ("Cannot load mp3 file!");
}
mad_stream_init(&m_mad_stream);
mad_frame_init (&m_mad_frame);
mad_synth_init (&m_mad_synth);
mad_timer_reset(&m_mad_timer);
m_pcmpos = m_mad_synth.pcm.length;
}
// -----------------------------------------------------------------------------
/*QString MadSource::getMbid()
{
char out[MBID_BUFFER_SIZE];
int const r = getMP3_MBID( QFile::encodeName( m_fileName ), out );
if (r == 0)
return QString::fromLatin1( out );
return QString();
}*/
void MadSource::getInfo(int& lengthSecs, int& samplerate, int& bitrate, int& nchannels )
{
// get the header plus some other stuff..
QFile inputFile(m_fileName);
bool fine = inputFile.open( QIODevice::ReadOnly );
if ( !fine )
{
throw std::runtime_error ("ERROR: Cannot load file for getInfo!");
return;
}
unsigned char* pMP3_Buffer = new unsigned char[m_MP3_BufferSize+MAD_BUFFER_GUARD];
mad_stream madStream;
mad_header madHeader;
mad_timer_t madTimer;
mad_stream_init(&madStream);
mad_timer_reset(&madTimer);
double avgSamplerate = 0;
double avgBitrate = 0;
double avgNChannels = 0;
int nFrames = 0;
while ( fetchData( inputFile, pMP3_Buffer, m_MP3_BufferSize, madStream) )
{
if ( mad_header_decode(&madHeader, &madStream) != 0 )
{
if ( isRecoverable(madStream.error) )
continue;
else
break;
}
mad_timer_add(&madTimer, madHeader.duration);
avgSamplerate += madHeader.samplerate;
avgBitrate += madHeader.bitrate;
if ( madHeader.mode == MAD_MODE_SINGLE_CHANNEL )
++avgNChannels;
else
avgNChannels += 2;
++nFrames;
}
inputFile.close();
mad_stream_finish(&madStream);
mad_header_finish(&madHeader);
delete[] pMP3_Buffer;
lengthSecs = static_cast<int>(madTimer.seconds);
samplerate = static_cast<int>( (avgSamplerate/nFrames) + 0.5 );
bitrate = static_cast<int>( (avgBitrate/nFrames) + 0.5 );
nchannels = static_cast<int>( (avgNChannels/nFrames) + 0.5 );
}
// -----------------------------------------------------------
bool MadSource::fetchData( QFile& mp3File,
unsigned char* pMP3_Buffer,
const int MP3_BufferSize,
mad_stream& madStream )
{
unsigned char *pReadStart = NULL;
unsigned char *pGuard = NULL;
if ( madStream.buffer == NULL ||
madStream.error == MAD_ERROR_BUFLEN )
{
size_t readSize;
size_t remaining;
/* {2} libmad may not consume all bytes of the input
* buffer. If the last frame in the buffer is not wholly
* contained by it, then that frame's start is pointed by
* the next_frame member of the Stream structure. This
* common situation occurs when mad_frame_decode() fails,
* sets the stream error code to MAD_ERROR_BUFLEN, and
* sets the next_frame pointer to a non NULL value. (See
* also the comment marked {4} bellow.)
*
* When this occurs, the remaining unused bytes must be
* put back at the beginning of the buffer and taken in
* account before refilling the buffer. This means that
* the input buffer must be large enough to hold a whole
* frame at the highest observable bit-rate (currently 448
* kb/s). XXX=XXX Is 2016 bytes the size of the largest
* frame? (448000*(1152/32000))/8
*/
if (madStream.next_frame != NULL)
{
remaining = madStream.bufend - madStream.next_frame;
memmove (pMP3_Buffer, madStream.next_frame, remaining);
pReadStart = pMP3_Buffer + remaining;
readSize = MP3_BufferSize - remaining;
}
else
{
readSize = MP3_BufferSize;
pReadStart = pMP3_Buffer;
remaining = 0;
}
readSize = mp3File.read( reinterpret_cast<char*>(pReadStart), readSize );
// nothing else to read!
if (readSize <= 0)
return false;
if ( mp3File.atEnd() )
{
pGuard = pReadStart + readSize;
memset (pGuard, 0, MAD_BUFFER_GUARD);
readSize += MAD_BUFFER_GUARD;
}
// Pipe the new buffer content to libmad's stream decoder facility.
mad_stream_buffer( &madStream, pMP3_Buffer,
static_cast<unsigned int>(readSize + remaining));
madStream.error = MAD_ERROR_NONE;
}
return true;
}
// -----------------------------------------------------------------------------
void MadSource::skipSilence(double silenceThreshold /* = 0.0001 */)
{
mad_frame madFrame;
mad_synth madSynth;
mad_frame_init(&madFrame);
mad_synth_init (&madSynth);
silenceThreshold *= static_cast<double>( numeric_limits<short>::max() );
for (;;)
{
if ( !fetchData( m_inputFile, m_pMP3_Buffer, m_MP3_BufferSize, m_mad_stream) )
break;
if ( mad_frame_decode(&madFrame, &m_mad_stream) != 0 )
{
if ( isRecoverable(m_mad_stream.error) )
continue;
else
break;
}
mad_synth_frame (&madSynth, &madFrame);
double sum = 0;
switch (madSynth.pcm.channels)
{
case 1:
for (size_t j = 0; j < madSynth.pcm.length; ++j)
sum += abs(f2s(madSynth.pcm.samples[0][j]));
break;
case 2:
for (size_t j = 0; j < madSynth.pcm.length; ++j)
sum += abs(f2s(
(madSynth.pcm.samples[0][j] >> 1)
+ (madSynth.pcm.samples[1][j] >> 1)));
break;
}
if ( (sum >= silenceThreshold * madSynth.pcm.length) )
break;
}
mad_frame_finish(&madFrame);
}
// -----------------------------------------------------------------------------
void MadSource::skip(const int mSecs)
{
if ( mSecs <= 0 )
return;
mad_header madHeader;
mad_header_init(&madHeader);
for (;;)
{
if (!fetchData( m_inputFile, m_pMP3_Buffer, m_MP3_BufferSize, m_mad_stream))
break;
if ( mad_header_decode(&madHeader, &m_mad_stream) != 0 )
{
if ( isRecoverable(m_mad_stream.error) )
continue;
else
break;
}
mad_timer_add(&m_mad_timer, madHeader.duration);
if ( mad_timer_count(m_mad_timer, MAD_UNITS_MILLISECONDS) >= mSecs )
break;
}
mad_header_finish(&madHeader);
}
// -----------------------------------------------------------
int MadSource::updateBuffer(signed short* pBuffer, size_t bufferSize)
{
size_t nwrit = 0; //number of samples written to the output buffer
for (;;)
{
// get a (valid) frame
// m_pcmpos == 0 could mean two things
// - we have completely decoded a frame, but the output buffer is still
// not full (it would make more sense for pcmpos == pcm.length(), but
// the loop assigns pcmpos = 0 at the end and does it this way!
// - we are starting a stream
if ( m_pcmpos == m_mad_synth.pcm.length )
{
if ( !fetchData( m_inputFile, m_pMP3_Buffer, m_MP3_BufferSize, m_mad_stream) )
{
break; // nothing else to read
}
// decode the frame
if (mad_frame_decode (&m_mad_frame, &m_mad_stream))
{
if ( isRecoverable(m_mad_stream.error) )
continue;
else
break;
} // if (mad_frame_decode (&madFrame, &madStream))
mad_timer_add (&m_mad_timer, m_mad_frame.header.duration);
mad_synth_frame (&m_mad_synth, &m_mad_frame);
m_pcmpos = 0;
}
size_t samples_for_mp3 = m_mad_synth.pcm.length - m_pcmpos;
size_t samples_for_buf = bufferSize - nwrit;
signed short* pBufferIt = pBuffer + nwrit;
size_t i = 0, j = 0;
switch( m_mad_synth.pcm.channels )
{
case 1:
{
size_t samples_to_use = min (samples_for_mp3, samples_for_buf);
for (i = 0; i < samples_to_use; ++i )
pBufferIt[i] = f2s( m_mad_synth.pcm.samples[0][i+m_pcmpos] );
}
j = i;
break;
case 2:
for (; i < samples_for_mp3 && j < samples_for_buf ; ++i, j+=2 )
{
pBufferIt[j] = f2s( m_mad_synth.pcm.samples[0][i+m_pcmpos] );
pBufferIt[j+1] = f2s( m_mad_synth.pcm.samples[1][i+m_pcmpos] );
}
break;
default:
cerr << "wtf kind of mp3 has " << m_mad_synth.pcm.channels << " channels??\n";
break;
}
m_pcmpos += i;
nwrit += j;
assert( nwrit <= bufferSize );
if (nwrit == bufferSize)
return static_cast<int>(nwrit);
}
return static_cast<int>(nwrit);
}
// -----------------------------------------------------------------------------

View File

@@ -0,0 +1,69 @@
/*
Copyright 2009 Last.fm Ltd.
Copyright 2009 John Stamp <jstamp@users.sourceforge.net>
This file is part of liblastfm.
liblastfm 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 3 of the License, or
(at your option) any later version.
liblastfm 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 Public License for more details.
You should have received a copy of the GNU General Public License
along with liblastfm. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __MP3_SOURCE_H__
#define __MP3_SOURCE_H__
#include <lastfm/FingerprintableSource.h>
#include <QFile>
#include <string>
#include <vector>
#include <fstream>
#include <mad.h>
class MadSource : public lastfm::FingerprintableSource
{
public:
MadSource();
~MadSource();
virtual void getInfo(int& lengthSecs, int& samplerate, int& bitrate, int& nchannels);
virtual void init(const QString& fileName);
virtual int updateBuffer(signed short* pBuffer, size_t bufferSize);
virtual void skip(const int mSecs);
virtual void skipSilence(double silenceThreshold = 0.0001);
virtual bool eof() const { return m_inputFile.atEnd(); }
private:
static bool fetchData( QFile& mp3File,
unsigned char* pMP3_Buffer,
const int MP3_BufferSize,
mad_stream& madStream );
static bool isRecoverable(const mad_error& error, bool log = false);
static std::string MadErrorString(const mad_error& error);
struct mad_stream m_mad_stream;
struct mad_frame m_mad_frame;
mad_timer_t m_mad_timer;
struct mad_synth m_mad_synth;
QFile m_inputFile;
unsigned char* m_pMP3_Buffer;
static const int m_MP3_BufferSize = (5*8192);
QString m_fileName;
size_t m_pcmpos;
};
#endif

View File

@@ -35,7 +35,6 @@
#include <QCoreApplication>
using namespace Tomahawk;
void
@@ -101,7 +100,7 @@ DirLister::scanDir( QDir dir, int depth )
}
DirListerThreadController::DirListerThreadController( QObject *parent )
DirListerThreadController::DirListerThreadController( QObject* parent )
: QThread( parent )
{
tDebug() << Q_FUNC_INFO;
@@ -139,6 +138,7 @@ MusicScanner::MusicScanner( MusicScanner::ScanMode scanMode, const QStringList&
, m_paths( paths )
, m_batchsize( bs )
, m_dirListerThreadController( 0 )
, m_fingerprint( true )
{
m_ext2mime.insert( "mp3", TomahawkUtils::extensionToMimetype( "mp3" ) );
m_ext2mime.insert( "ogg", TomahawkUtils::extensionToMimetype( "ogg" ) );
@@ -174,8 +174,9 @@ void
MusicScanner::startScan()
{
tDebug( LOGVERBOSE ) << "Loading mtimes...";
m_scanned = m_skipped = m_cmdQueue = 0;
m_scanned = m_skipped = m_fingerprinting = m_cmdQueue = 0;
m_skippedFiles.clear();
m_fingerprintingFiles.clear();
SourceList::instance()->getLocal()->scanningProgress( m_scanned );
@@ -183,9 +184,9 @@ MusicScanner::startScan()
//FIXME: For multiple collection support make sure the right prefix gets passed in...or not...
//bear in mind that simply passing in the top-level of a defined collection means it will not return items that need
//to be removed that aren't in that root any longer -- might have to do the filtering in setMTimes based on strings
DatabaseCommand_FileMtimes *cmd = new DatabaseCommand_FileMtimes();
DatabaseCommand_FileMtimes* cmd = new DatabaseCommand_FileMtimes();
connect( cmd, SIGNAL( done( QMap< QString, QMap< unsigned int, unsigned int > > ) ),
SLOT( setFileMtimes( QMap< QString, QMap< unsigned int, unsigned int > > ) ) );
SLOT( setFileMtimes( QMap< QString, QMap< unsigned int, unsigned int > > ) ) );
Database::instance()->enqueue( dbcmd_ptr( cmd ) );
return;
@@ -207,7 +208,7 @@ MusicScanner::scan()
tDebug( LOGEXTRA ) << "Num saved file mtimes from last scan:" << m_filemtimes.size();
connect( this, SIGNAL( batchReady( QVariantList, QVariantList ) ),
SLOT( commitBatch( QVariantList, QVariantList ) ), Qt::DirectConnection );
SLOT( commitBatch( QVariantList, QVariantList ) ), Qt::DirectConnection );
if ( m_scanMode == MusicScanner::FileScan )
{
@@ -225,7 +226,7 @@ void
MusicScanner::scanFilePaths()
{
tDebug( LOGVERBOSE ) << Q_FUNC_INFO;
foreach( QString path, m_paths )
foreach ( QString path, m_paths )
{
QFileInfo fi( path );
if ( fi.exists() && fi.isReadable() )
@@ -244,7 +245,7 @@ MusicScanner::postOps()
if ( m_scanMode == MusicScanner::DirScan )
{
// any remaining stuff that wasnt emitted as a batch:
foreach( const QString& key, m_filemtimes.keys() )
foreach ( const QString& key, m_filemtimes.keys() )
{
if ( !m_filemtimes[ key ].keys().isEmpty() )
m_filesToDelete << m_filemtimes[ key ].keys().first();
@@ -281,8 +282,13 @@ MusicScanner::cleanup()
m_dirListerThreadController = 0;
}
tDebug() << Q_FUNC_INFO << "emitting finished!";
emit finished();
tDebug() << "FP: m_fingerprinting: " << m_fingerprinting;
if ( m_fingerprinting == 0 )
{
tDebug() << Q_FUNC_INFO << "emitting finished!";
emit finished();
}
}
@@ -323,6 +329,64 @@ MusicScanner::commandFinished()
}
void
MusicScanner::infoSystemInfo( Tomahawk::InfoSystem::InfoRequestData requestData, QVariant output )
{
if ( requestData.caller != infoid() )
return;
QVariantMap returnedData = output.value< QVariantMap >();
QFileInfo fi( returnedData["file"].value< QString >() );
switch ( requestData.type )
{
case InfoSystem::InfoFingerprintTrack:
if ( --m_fingerprinting == 0 )
{
tDebug() << "FP: got info for all files, disconnecting info slots";
disconnect( Tomahawk::InfoSystem::InfoSystem::instance(), SIGNAL( info( Tomahawk::InfoSystem::InfoRequestData, QVariant ) ),
this, SLOT( infoSystemInfo( Tomahawk::InfoSystem::InfoRequestData, QVariant ) ) );
disconnect( Tomahawk::InfoSystem::InfoSystem::instance(), SIGNAL( finished( QString ) ),
this, SLOT( infoSystemFinished( QString ) ) );
}
if ( fi.isFile() && returnedData.contains( "artist" ) && returnedData.contains( "title" ) )
{
tDebug() << "received valid metadata: " << returnedData["artist"].toString() << " - " << returnedData["title"].toString();
QVariantMap m = readAdditionalMetadata( fi );
m["artist"] = returnedData["artist"];
m["track"] = returnedData["title"];
m["album"] = returnedData["album"];
m["albumartist"] = returnedData["albumartist"];
commitFile( m );
}
else
{
tDebug() << "data is not valid";
m_skipped++;
m_skippedFiles << fi.canonicalFilePath();
}
break;
default:
Q_ASSERT( false );
}
}
void
MusicScanner::infoSystemFinished( QString target )
{
if ( target != infoid() )
return;
QMetaObject::invokeMethod( this, "postOps", Qt::QueuedConnection );
}
void
MusicScanner::scanFile( const QFileInfo& fi )
{
@@ -345,13 +409,7 @@ MusicScanner::scanFile( const QFileInfo& fi )
if ( m.toMap().isEmpty() )
return;
m_scannedfiles << m;
if ( m_batchsize != 0 && (quint32)m_scannedfiles.length() >= m_batchsize )
{
emit batchReady( m_scannedfiles, m_filesToDelete );
m_scannedfiles.clear();
m_filesToDelete.clear();
}
commitFile( m );
}
@@ -371,31 +429,16 @@ MusicScanner::readFile( const QFileInfo& fi )
if ( m_scanned % 100 == 0 )
tDebug( LOGINFO ) << "Scan progress:" << m_scanned << fi.canonicalFilePath();
#ifdef COMPLEX_TAGLIB_FILENAME
const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( fi.canonicalFilePath().utf16() );
#else
QByteArray fileName = QFile::encodeName( fi.canonicalFilePath() );
const char *encodedName = fileName.constData();
#endif
TagLib::FileRef f = getTagLibFileRef( fi );
TagLib::FileRef f( encodedName );
if ( f.isNull() || !f.tag() )
if ( !m_fingerprint && ( f.isNull() || !f.tag() ) )
{
m_skippedFiles << fi.canonicalFilePath();
m_skipped++;
return QVariantMap();
}
int bitrate = 0;
int duration = 0;
Tag *tag = Tag::fromFile( f );
if ( f.audioProperties() )
{
TagLib::AudioProperties *properties = f.audioProperties();
duration = properties->length();
bitrate = properties->bitrate();
}
QString artist, album, track;
if ( tag )
@@ -406,33 +449,129 @@ MusicScanner::readFile( const QFileInfo& fi )
}
if ( !tag || artist.isEmpty() || track.isEmpty() )
{
// FIXME: do some clever filename guessing
m_skippedFiles << fi.canonicalFilePath();
m_skipped++;
if ( m_fingerprint )
{
m_fingerprintingFiles << fi.canonicalFilePath();
m_fingerprinting++;
fingerprintFile( fi );
}
else
{
m_skippedFiles << fi.canonicalFilePath();
m_skipped++;
}
return QVariantMap();
}
QString mimetype = m_ext2mime.value( suffix );
QString url( "file://%1" );
m_scanned++;
QVariantMap m = readAdditionalMetadata( fi );
QVariantMap m;
m["url"] = url.arg( fi.canonicalFilePath() );
m["mtime"] = fi.lastModified().toUTC().toTime_t();
m["size"] = (unsigned int)fi.size();
m["mimetype"] = mimetype;
m["duration"] = duration;
m["bitrate"] = bitrate;
m["artist"] = artist;
m["album"] = album;
m["track"] = track;
m["albumpos"] = tag->track();
m["year"] = tag->year();
m["albumartist"] = tag->albumArtist();
m["composer"] = tag->composer();
m["discnumber"] = tag->discNumber();
m["hash"] = ""; // TODO
m_scanned++;
return m;
}
void
MusicScanner::commitFile( const QVariant& m )
{
m_scannedfiles << m;
if ( m_batchsize != 0 && ( quint32 )m_scannedfiles.length() >= m_batchsize )
{
emit batchReady( m_scannedfiles, m_filesToDelete );
m_scannedfiles.clear();
m_filesToDelete.clear();
}
}
TagLib::FileRef
MusicScanner::getTagLibFileRef(const QFileInfo& fi) const
{
#ifdef COMPLEX_TAGLIB_FILENAME
const wchar_t* encodedName = reinterpret_cast< const wchar_t* >( fi.canonicalFilePath().utf16() );
#else
QByteArray fileName = QFile::encodeName( fi.canonicalFilePath() );
const char* encodedName = fileName.constData();
#endif
TagLib::FileRef f( encodedName );
return f;
}
void
MusicScanner::fingerprintFile( const QFileInfo& fi )
{
tDebug() << "Fingerprinting track: " << fi.canonicalFilePath();
Tomahawk::InfoSystem::InfoStringHash hash;
hash["file"] = fi.canonicalFilePath();
Tomahawk::InfoSystem::InfoRequestData requestData;
requestData.caller = infoid();
requestData.input = QVariant::fromValue< Tomahawk::InfoSystem::InfoStringHash >( hash );
requestData.type = Tomahawk::InfoSystem::InfoFingerprintTrack;
requestData.allSources = true;
if ( m_fingerprinting == 1 )
{
connect( Tomahawk::InfoSystem::InfoSystem::instance(),
SIGNAL( info( Tomahawk::InfoSystem::InfoRequestData, QVariant ) ),
SLOT( infoSystemInfo( Tomahawk::InfoSystem::InfoRequestData, QVariant ) ), Qt::UniqueConnection );
connect( Tomahawk::InfoSystem::InfoSystem::instance(),
SIGNAL( finished( QString ) ),
SLOT( infoSystemFinished( QString ) ), Qt::UniqueConnection );
}
Tomahawk::InfoSystem::InfoSystem::instance()->getInfo( requestData );
}
QVariantMap
MusicScanner::readAdditionalMetadata( const QFileInfo& fi ) const
{
int bitrate = 0;
int duration = 0;
QString mimetype = m_ext2mime.value( fi.suffix().toLower() );
QString url( "file://%1" );
TagLib::FileRef f = getTagLibFileRef( fi );
if ( f.audioProperties() )
{
TagLib::AudioProperties* properties = f.audioProperties();
duration = properties->length();
bitrate = properties->bitrate();
}
QVariantMap m;
m["url"] = url.arg( fi.canonicalFilePath() );
m["mtime"] = fi.lastModified().toUTC().toTime_t();
m["size"] = ( unsigned int )fi.size();
m["mimetype"] = mimetype;
m["duration"] = duration;
m["bitrate"] = bitrate;
m["hash"] = ""; // TODO
return m;
}
QString
MusicScanner::infoid() const
{
return "MusicScanner";
}

View File

@@ -39,6 +39,14 @@
#include <QTimer>
#include <QVariantMap>
namespace Tomahawk
{
namespace InfoSystem
{
class InfoRequestData;
}
}
// descend dir tree comparing dir mtimes to last known mtime
// emit signal for any dir with new content, so we can scan it.
// finally, emit the list of new mtimes we observed.
@@ -113,6 +121,7 @@ signals:
private:
QVariant readFile( const QFileInfo& fi );
void executeCommand( Tomahawk::dbcmd_ptr cmd );
void fingerprintFile ( const QFileInfo& fi );
private slots:
void postOps();
@@ -124,16 +133,27 @@ private slots:
void commitBatch( const QVariantList& tracks, const QVariantList& deletethese );
void commandFinished();
void infoSystemInfo( Tomahawk::InfoSystem::InfoRequestData requestData, QVariant output );
void infoSystemFinished( QString target );
private:
TagLib::FileRef getTagLibFileRef( const QFileInfo& fi ) const;
void scanFilePaths();
void commitFile( const QVariant& m );
QString infoid() const;
QVariantMap readAdditionalMetadata( const QFileInfo& fi ) const;
MusicScanner::ScanMode m_scanMode;
QStringList m_paths;
QMap<QString, QString> m_ext2mime; // eg: mp3 -> audio/mpeg
unsigned int m_scanned;
unsigned int m_skipped;
unsigned int m_fingerprinting;
QList<QString> m_skippedFiles;
QList<QString> m_fingerprintingFiles;
QMap<QString, QMap< unsigned int, unsigned int > > m_filemtimes;
unsigned int m_cmdQueue;
@@ -143,6 +163,8 @@ private:
quint32 m_batchsize;
DirListerThreadController* m_dirListerThreadController;
bool m_fingerprint;
};
#endif