diff --git a/CMakeLists.txt b/CMakeLists.txt index 116c3367f..4dc32e66e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/CMakeModules/LibFindMacros.cmake b/CMakeModules/LibFindMacros.cmake new file mode 100644 index 000000000..69975c51b --- /dev/null +++ b/CMakeModules/LibFindMacros.cmake @@ -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) + diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt index 2c8b25ab8..ea30cdd52 100644 --- a/src/libtomahawk/CMakeLists.txt +++ b/src/libtomahawk/CMakeLists.txt @@ -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} ) diff --git a/src/libtomahawk/Typedefs.h b/src/libtomahawk/Typedefs.h index b044dc23d..7332a1311 100644 --- a/src/libtomahawk/Typedefs.h +++ b/src/libtomahawk/Typedefs.h @@ -230,6 +230,8 @@ namespace Tomahawk InfoUnLove = 91, InfoShareTrack = 92, + InfoFingerprintTrack = 95, + InfoNotifyUser = 100, InfoInboxReceived = 101, diff --git a/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.cpp b/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.cpp index 6ca02f615..5b81be209 100644 --- a/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.cpp +++ b/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.cpp @@ -36,8 +36,14 @@ #include #include +#include +#include +#include + +#include "MadSource.h" #include +#include 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 criteria, Tomahawk::InfoSystem::InfoRequestData requestData ) @@ -895,6 +977,54 @@ LastFmInfoPlugin::artistImagesReturned() } +void +LastFmInfoPlugin::fingerprintReturned() +{ + QNetworkReply* reply = qobject_cast( 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( 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() { diff --git a/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.h b/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.h index 6e202b216..896f7150c 100644 --- a/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.h +++ b/src/libtomahawk/accounts/lastfm/LastFmInfoPlugin.h @@ -27,10 +27,18 @@ #include #include #include +#include #include 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; }; } diff --git a/src/libtomahawk/accounts/lastfm/MadSource.cpp b/src/libtomahawk/accounts/lastfm/MadSource.cpp new file mode 100644 index 000000000..00e725ae6 --- /dev/null +++ b/src/libtomahawk/accounts/lastfm/MadSource.cpp @@ -0,0 +1,514 @@ +/* + Copyright 2009 Last.fm Ltd. + Copyright 2009 John Stamp + + 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 . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#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(madTimer.seconds); + samplerate = static_cast( (avgSamplerate/nFrames) + 0.5 ); + bitrate = static_cast( (avgBitrate/nFrames) + 0.5 ); + nchannels = static_cast( (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(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(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( numeric_limits::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(nwrit); + } + + return static_cast(nwrit); +} + +// ----------------------------------------------------------------------------- + diff --git a/src/libtomahawk/accounts/lastfm/MadSource.h b/src/libtomahawk/accounts/lastfm/MadSource.h new file mode 100644 index 000000000..4e9515307 --- /dev/null +++ b/src/libtomahawk/accounts/lastfm/MadSource.h @@ -0,0 +1,69 @@ +/* + Copyright 2009 Last.fm Ltd. + Copyright 2009 John Stamp + + 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 . +*/ + +#ifndef __MP3_SOURCE_H__ +#define __MP3_SOURCE_H__ + +#include +#include +#include +#include +#include +#include + + +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 diff --git a/src/libtomahawk/filemetadata/MusicScanner.cpp b/src/libtomahawk/filemetadata/MusicScanner.cpp index 9e9650e65..a275c67a6 100644 --- a/src/libtomahawk/filemetadata/MusicScanner.cpp +++ b/src/libtomahawk/filemetadata/MusicScanner.cpp @@ -35,7 +35,6 @@ #include - 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"; +} diff --git a/src/libtomahawk/filemetadata/MusicScanner.h b/src/libtomahawk/filemetadata/MusicScanner.h index 8e038f514..46596004d 100644 --- a/src/libtomahawk/filemetadata/MusicScanner.h +++ b/src/libtomahawk/filemetadata/MusicScanner.h @@ -39,6 +39,14 @@ #include #include +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 m_ext2mime; // eg: mp3 -> audio/mpeg unsigned int m_scanned; unsigned int m_skipped; + unsigned int m_fingerprinting; QList m_skippedFiles; + QList m_fingerprintingFiles; QMap > m_filemtimes; unsigned int m_cmdQueue; @@ -143,6 +163,8 @@ private: quint32 m_batchsize; DirListerThreadController* m_dirListerThreadController; + + bool m_fingerprint; }; #endif