PageRenderTime 142ms CodeModel.GetById 19ms app.highlight 113ms RepoModel.GetById 1ms app.codeStats 1ms

/src/libtomahawk/audio/AudioEngine.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 1390 lines | 1049 code | 287 blank | 54 comment | 217 complexity | d2f9fab5bf67ed1d0aa57c8ecd20e49d MD5 | raw file
   1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
   2 *
   3 *   Copyright 2010-2016, Christian Muehlhaeuser <muesli@tomahawk-player.org>
   4 *   Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
   5 *   Copyright 2013,      Teo Mrnjavac <teo@kde.org>
   6 *
   7 *   Tomahawk is free software: you can redistribute it and/or modify
   8 *   it under the terms of the GNU General Public License as published by
   9 *   the Free Software Foundation, either version 3 of the License, or
  10 *   (at your option) any later version.
  11 *
  12 *   Tomahawk is distributed in the hope that it will be useful,
  13 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15 *   GNU General Public License for more details.
  16 *
  17 *   You should have received a copy of the GNU General Public License
  18 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
  19 */
  20
  21#include "AudioEngine.h"
  22#include "AudioEngine_p.h"
  23
  24#include "config.h"
  25
  26#include "audio/Qnr_IoDeviceStream.h"
  27#include "filemetadata/MusicScanner.h"
  28#include "jobview/JobStatusView.h"
  29#include "jobview/JobStatusModel.h"
  30#include "jobview/ErrorStatusMessage.h"
  31#include "network/Servent.h"
  32#include "playlist/SingleTrackPlaylistInterface.h"
  33#include "utils/Closure.h"
  34#include "utils/Logger.h"
  35#include "utils/NetworkReply.h"
  36#include "utils/NetworkAccessManager.h"
  37
  38#include "Album.h"
  39#include "Artist.h"
  40#include "Pipeline.h"
  41#include "PlaylistEntry.h"
  42#include "SourceList.h"
  43#include "TomahawkSettings.h"
  44#include "UrlHandler.h"
  45#include "resolvers/ScriptJob.h"
  46
  47#include <QDir>
  48
  49using namespace Tomahawk;
  50
  51#define AUDIO_VOLUME_STEP 5
  52
  53static const uint_fast8_t UNDERRUNTHRESHOLD = 2;
  54
  55static QString s_aeInfoIdentifier = QString( "AUDIOENGINE" );
  56
  57
  58void
  59AudioEnginePrivate::onStateChanged( AudioOutput::AudioState newState, AudioOutput::AudioState oldState )
  60{
  61    tDebug() << Q_FUNC_INFO << oldState << newState << q_ptr->state();
  62    AudioEngine::AudioState previousState = q_ptr->state();
  63
  64    if ( newState == AudioOutput::Loading )
  65    {
  66        // We don't emit this state to listeners - yet.
  67        state = AudioEngine::Loading;
  68    }
  69    else if ( newState == AudioOutput::Buffering )
  70    {
  71        if ( underrunCount > UNDERRUNTHRESHOLD && !underrunNotified )
  72        {
  73            underrunNotified = true;
  74            //FIXME: Actually notify
  75        }
  76        else
  77            underrunCount++;
  78    }
  79    else if ( newState == AudioOutput::Error )
  80    {
  81        q_ptr->setState( AudioEngine::Stopped );
  82        tDebug() << Q_FUNC_INFO << "AudioOutput Error";
  83        emit q_ptr->error( AudioEngine::UnknownError );
  84        q_ptr->setState( AudioEngine::Error );
  85    }
  86    else if ( newState == AudioOutput::Playing )
  87    {
  88        bool emitSignal = false;
  89        if ( q_ptr->state() != AudioEngine::Paused && q_ptr->state() != AudioEngine::Playing )
  90        {
  91            underrunCount = 0;
  92            underrunNotified = false;
  93            emitSignal = true;
  94        }
  95        q_ptr->setState( AudioEngine::Playing );
  96        audioRetryCounter = 0;
  97
  98        if ( emitSignal )
  99            emit q_ptr->started( currentTrack );
 100    }
 101    else if ( newState == AudioOutput::Paused )
 102    {
 103        q_ptr->setState( AudioEngine::Paused );
 104    }
 105    else if ( newState == AudioOutput::Stopped )
 106    {
 107        q_ptr->setState( AudioEngine::Stopped );
 108    }
 109
 110    if ( previousState != AudioEngine::Stopped &&
 111         ( oldState == AudioOutput::Playing || oldState == AudioOutput::Loading ) )
 112    {
 113        bool retry = false;
 114        if ( newState == AudioOutput::Error )
 115        {
 116            retry = ( audioRetryCounter < 2 );
 117            audioRetryCounter++;
 118
 119            if ( !retry )
 120            {
 121                q_ptr->stop( AudioEngine::UnknownError );
 122            }
 123        }
 124
 125        if ( newState == AudioOutput::Stopped || retry )
 126        {
 127            tDebug() << Q_FUNC_INFO << "Finding next track." << oldState << newState;
 128            if ( q_ptr->canGoNext() )
 129            {
 130                q_ptr->loadNextTrack();
 131            }
 132            else
 133            {
 134                if ( !playlist.isNull() && playlist.data()->retryMode() == Tomahawk::PlaylistModes::Retry )
 135                    waitingOnNewTrack = true;
 136
 137                q_ptr->stop();
 138            }
 139        }
 140    }
 141}
 142
 143
 144AudioEngine* AudioEnginePrivate::s_instance = 0;
 145
 146
 147AudioEngine*
 148AudioEngine::instance()
 149{
 150    return AudioEnginePrivate::s_instance;
 151}
 152
 153
 154AudioEngine::AudioEngine()
 155    : QObject()
 156    , d_ptr( new AudioEnginePrivate( this ) )
 157{
 158    Q_D( AudioEngine );
 159
 160    d->timeElapsed = 0;
 161    d->waitingOnNewTrack = false;
 162    d->state = Stopped;
 163    d->coverTempFile = 0;
 164
 165    d->s_instance = this;
 166    tDebug() << "Init AudioEngine";
 167
 168    d->audioOutput = new AudioOutput( this );
 169
 170    connect( d->audioOutput, SIGNAL( initialized() ), this, SIGNAL( initialized() ) );
 171    connect( d->audioOutput, SIGNAL( stateChanged( AudioOutput::AudioState, AudioOutput::AudioState ) ), d_func(), SLOT( onStateChanged( AudioOutput::AudioState, AudioOutput::AudioState ) ) );
 172    connect( d->audioOutput, SIGNAL( tick( qint64 ) ), SLOT( timerTriggered( qint64 ) ) );
 173    connect( d->audioOutput, SIGNAL( positionChanged( float ) ), SLOT( onPositionChanged( float ) ) );
 174    connect( d->audioOutput, SIGNAL( volumeChanged( qreal ) ), SLOT( onVolumeChanged( qreal ) ) );
 175    connect( d->audioOutput, SIGNAL( mutedChanged( bool ) ), SIGNAL( mutedChanged( bool ) ) );
 176
 177    if ( TomahawkSettings::instance()->muted() )
 178    {
 179        mute();
 180    }
 181    setVolume( TomahawkSettings::instance()->volume() );
 182
 183    qRegisterMetaType< AudioErrorCode >("AudioErrorCode");
 184    qRegisterMetaType< AudioState >("AudioState");
 185}
 186
 187
 188AudioEngine::~AudioEngine()
 189{
 190    tDebug() << Q_FUNC_INFO;
 191
 192    TomahawkSettings::instance()->setVolume( volume() );
 193    TomahawkSettings::instance()->setMuted( isMuted() );
 194
 195    delete d_ptr;
 196}
 197
 198
 199void
 200AudioEngine::playPause()
 201{
 202    if ( QThread::currentThread() != thread() )
 203    {
 204        QMetaObject::invokeMethod( this, "playPause", Qt::QueuedConnection );
 205        return;
 206    }
 207
 208    if ( isPlaying() )
 209        pause();
 210    else
 211        play();
 212}
 213
 214
 215void
 216AudioEngine::play()
 217{
 218    if ( QThread::currentThread() != thread() )
 219    {
 220        QMetaObject::invokeMethod( this, "play", Qt::QueuedConnection );
 221        return;
 222    }
 223
 224    Q_D( AudioEngine );
 225
 226    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
 227
 228    if ( isPaused() )
 229    {
 230        d->audioOutput->play();
 231        emit resumed();
 232
 233        sendNowPlayingNotification( Tomahawk::InfoSystem::InfoNowResumed );
 234    }
 235    else
 236    {
 237        if ( !d->currentTrack && d->playlist && d->playlist->nextResult() )
 238        {
 239            loadNextTrack();
 240        }
 241        else
 242            next();
 243    }
 244}
 245
 246
 247void
 248AudioEngine::pause()
 249{
 250    if ( QThread::currentThread() != thread() )
 251    {
 252        QMetaObject::invokeMethod( this, "pause", Qt::QueuedConnection );
 253        return;
 254    }
 255
 256    Q_D( AudioEngine );
 257
 258    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
 259
 260    d->audioOutput->pause();
 261    emit paused();
 262
 263    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo( Tomahawk::InfoSystem::InfoPushData( s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoNowPaused, QVariant(), Tomahawk::InfoSystem::PushNoFlag ) );
 264}
 265
 266
 267void
 268AudioEngine::stop( AudioErrorCode errorCode )
 269{
 270    if ( QThread::currentThread() != thread() )
 271    {
 272        QMetaObject::invokeMethod( this, "stop", Qt::QueuedConnection );
 273        return;
 274    }
 275
 276    Q_D( AudioEngine );
 277
 278    tDebug() << Q_FUNC_INFO << errorCode << isStopped();
 279
 280    if ( errorCode == NoError )
 281        setState( Stopped );
 282    else
 283        setState( Error );
 284
 285    if ( d->audioOutput->state() != AudioOutput::Stopped )
 286        d->audioOutput->stop();
 287
 288    emit stopped();
 289
 290    if ( !d->playlist.isNull() )
 291        d->playlist.data()->reset();
 292    if ( !d->currentTrack.isNull() )
 293        emit timerPercentage( ( (double)d->timeElapsed / (double)d->currentTrack->track()->duration() ) * 100.0 );
 294
 295    setCurrentTrack( Tomahawk::result_ptr() );
 296
 297    if ( d->waitingOnNewTrack )
 298        sendWaitingNotification();
 299
 300    if ( d->audioOutput->isInitialized() )
 301    {
 302        Tomahawk::InfoSystem::InfoPushData pushData( s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoNowStopped, QVariant(), Tomahawk::InfoSystem::PushNoFlag );
 303        Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo( pushData );
 304    }
 305}
 306
 307
 308void
 309AudioEngine::previous()
 310{
 311    if ( QThread::currentThread() != thread() )
 312    {
 313        QMetaObject::invokeMethod( this, "previous", Qt::QueuedConnection );
 314        return;
 315    }
 316
 317    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
 318
 319    if ( canGoPrevious() )
 320        loadPreviousTrack();
 321}
 322
 323
 324void
 325AudioEngine::next()
 326{
 327    if ( QThread::currentThread() != thread() )
 328    {
 329        QMetaObject::invokeMethod( this, "next", Qt::QueuedConnection );
 330        return;
 331    }
 332
 333    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
 334
 335    if ( canGoNext() )
 336        loadNextTrack();
 337}
 338
 339
 340bool
 341AudioEngine::canGoNext()
 342{
 343    Q_D( AudioEngine );
 344
 345    tDebug( LOGVERBOSE ) << Q_FUNC_INFO;
 346
 347    if ( d->queue && d->queue->trackCount() )
 348        return true;
 349
 350    if ( d->playlist.isNull() )
 351        return false;
 352
 353    if ( d->playlist.data()->skipRestrictions() == PlaylistModes::NoSkip ||
 354         d->playlist.data()->skipRestrictions() == PlaylistModes::NoSkipForwards )
 355    {
 356        return false;
 357    }
 358
 359    if ( !d->currentTrack.isNull() && !d->playlist->hasNextResult() &&
 360       ( d->playlist->currentItem().isNull() || ( d->currentTrack->id() == d->playlist->currentItem()->id() ) ) )
 361    {
 362        //For instance, when doing a catch-up while listening along, but the person
 363        //you're following hasn't started a new track yet...don't do anything
 364        tDebug( LOGEXTRA ) << Q_FUNC_INFO << "Catch up, but same track or can't move on because don't have next track or it wasn't resolved";
 365        return false;
 366    }
 367
 368    return ( d->currentTrack && d->playlist.data()->hasNextResult() &&
 369             !d->playlist.data()->nextResult().isNull() &&
 370             d->playlist.data()->nextResult()->isOnline() );
 371}
 372
 373
 374bool
 375AudioEngine::canGoPrevious()
 376{
 377    Q_D( AudioEngine );
 378
 379    if ( d->playlist.isNull() )
 380        return false;
 381
 382    if ( d->playlist.data()->skipRestrictions() == PlaylistModes::NoSkip ||
 383        d->playlist.data()->skipRestrictions() == PlaylistModes::NoSkipBackwards )
 384        return false;
 385
 386    return ( d->currentTrack && d->playlist.data()->hasPreviousResult() && d->playlist.data()->previousResult()->isOnline() );
 387}
 388
 389
 390bool
 391AudioEngine::canSeek()
 392{
 393    Q_D( AudioEngine );
 394
 395    return !d->playlist.isNull() && ( d->playlist.data()->seekRestrictions() != PlaylistModes::NoSeek );
 396}
 397
 398
 399void
 400AudioEngine::seek( qint64 ms )
 401{
 402    Q_D( AudioEngine );
 403
 404    /*if ( !canSeek() )
 405    {
 406        tDebug( LOGEXTRA ) << "Could not seek!";
 407        return;
 408    }*/
 409
 410    if ( isPlaying() || isPaused() )
 411    {
 412        tDebug( LOGVERBOSE ) << Q_FUNC_INFO << ms;
 413        d->audioOutput->seek( ms );
 414        emit seeked( ms );
 415    }
 416}
 417
 418
 419void
 420AudioEngine::seek( int ms )
 421{
 422    seek( (qint64) ms );
 423}
 424
 425
 426void
 427AudioEngine::setVolume( int percentage )
 428{
 429    Q_D( AudioEngine );
 430
 431    tDebug() << Q_FUNC_INFO << percentage;
 432
 433    percentage = qBound( 0, percentage, 100 );
 434    d->audioOutput->setVolume( (qreal)percentage / 100.0 );
 435
 436    if ( percentage > 0 && d->audioOutput->isMuted() )
 437        d->audioOutput->setMuted( false );
 438
 439    emit volumeChanged( percentage );
 440}
 441
 442
 443void
 444AudioEngine::lowerVolume()
 445{
 446    setVolume( volume() - AUDIO_VOLUME_STEP );
 447}
 448
 449
 450void
 451AudioEngine::raiseVolume()
 452{
 453    setVolume( volume() + AUDIO_VOLUME_STEP );
 454}
 455
 456
 457bool
 458AudioEngine::isMuted() const
 459{
 460    return d_func()->audioOutput->isMuted();
 461}
 462
 463
 464void
 465AudioEngine::mute()
 466{
 467    Q_D( AudioEngine );
 468    d->audioOutput->setMuted( true );
 469
 470    emit volumeChanged( volume() );
 471}
 472
 473
 474void
 475AudioEngine::toggleMute()
 476{
 477    Q_D( AudioEngine );
 478    d->audioOutput->setMuted( !d->audioOutput->isMuted() );
 479
 480    emit volumeChanged( volume() );
 481}
 482
 483
 484void
 485AudioEngine::sendWaitingNotification() const
 486{
 487    tDebug( LOGVERBOSE ) << Q_FUNC_INFO;
 488    //since it's async, after this is triggered our result could come in, so don't show the popup in that case
 489    if ( d_func()->playlist && d_func()->playlist->nextResult() && d_func()->playlist->nextResult()->isOnline() )
 490        return;
 491
 492    Tomahawk::InfoSystem::InfoPushData pushData (
 493        s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoTrackUnresolved,
 494        QVariant(),
 495        Tomahawk::InfoSystem::PushNoFlag );
 496
 497    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo( pushData );
 498}
 499
 500
 501void
 502AudioEngine::sendNowPlayingNotification( const Tomahawk::InfoSystem::InfoType type )
 503{
 504    Q_D( AudioEngine );
 505
 506    if ( d->currentTrack.isNull() )
 507        return;
 508
 509    if ( d->currentTrack->track()->coverLoaded() )
 510    {
 511        onNowPlayingInfoReady( type );
 512    }
 513    else
 514    {
 515        NewClosure( d->currentTrack->track().data(), SIGNAL( coverChanged() ), const_cast< AudioEngine* >( this ), SLOT( sendNowPlayingNotification( const Tomahawk::InfoSystem::InfoType ) ), type );
 516        d->currentTrack->track()->cover( QSize( 0, 0 ), true );
 517    }
 518}
 519
 520
 521void
 522AudioEngine::onNowPlayingInfoReady( const Tomahawk::InfoSystem::InfoType type )
 523{
 524    Q_D( AudioEngine );
 525
 526    if ( d->currentTrack.isNull() ||
 527         d->currentTrack->track()->artist().isEmpty() )
 528        return;
 529
 530    QVariantMap playInfo;
 531
 532    QImage cover;
 533    cover = d->currentTrack->track()->cover( QSize( 0, 0 ) ).toImage();
 534    if ( !cover.isNull() )
 535    {
 536        playInfo["cover"] = cover;
 537
 538        delete d->coverTempFile;
 539        d->coverTempFile = new QTemporaryFile( QDir::toNativeSeparators( QDir::tempPath() + "/" + d->currentTrack->track()->artist() + "_" + d->currentTrack->track()->album() + "_tomahawk_cover.png" ) );
 540        if ( !d->coverTempFile->open() )
 541        {
 542            tDebug() << Q_FUNC_INFO << "WARNING: could not write temporary file for cover art!";
 543        }
 544        else
 545        {
 546            // Finally, save the image to the new temp file
 547            if ( cover.save( d->coverTempFile, "PNG" ) )
 548            {
 549                tDebug() <<  Q_FUNC_INFO << "Saving cover image to:" << QFileInfo( *d->coverTempFile ).absoluteFilePath();
 550                playInfo["coveruri"] = QFileInfo( *d->coverTempFile ).absoluteFilePath();
 551            }
 552            else
 553                tDebug() << Q_FUNC_INFO << "Failed to save cover image!";
 554        }
 555    }
 556    else
 557        tDebug() << Q_FUNC_INFO << "Cover from query is null!";
 558
 559    Tomahawk::InfoSystem::InfoStringHash trackInfo;
 560    trackInfo["title"] = d->currentTrack->track()->track();
 561    trackInfo["artist"] = d->currentTrack->track()->artist();
 562    trackInfo["album"] = d->currentTrack->track()->album();
 563    trackInfo["duration"] = QString::number( d->currentTrack->track()->duration() );
 564    trackInfo["albumpos"] = QString::number( d->currentTrack->track()->albumpos() );
 565
 566    playInfo["trackinfo"] = QVariant::fromValue< Tomahawk::InfoSystem::InfoStringHash >( trackInfo );
 567    playInfo["private"] = TomahawkSettings::instance()->privateListeningMode();
 568
 569    Tomahawk::InfoSystem::InfoPushData pushData( s_aeInfoIdentifier, type, playInfo, Tomahawk::InfoSystem::PushShortUrlFlag );
 570    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo( pushData );
 571}
 572
 573
 574void
 575AudioEngine::loadTrack( const Tomahawk::result_ptr& result )
 576{
 577    Q_D( AudioEngine );
 578    tDebug( LOGEXTRA ) << Q_FUNC_INFO << ( result.isNull() ? QString() : result->url() );
 579
 580
 581    if ( !d->audioOutput->isInitialized() )
 582    {
 583        return;
 584    }
 585
 586    if ( !result )
 587    {
 588        stop();
 589        return;
 590    }
 591
 592    // We do this to stop the audio as soon as a user activated another track
 593    // If we don't block the audioOutput signals, the state change will trigger
 594    // loading yet another track
 595    d->audioOutput->blockSignals( true );
 596    d->audioOutput->stop();
 597    d->audioOutput->blockSignals( false );
 598
 599    setCurrentTrack( result );
 600
 601    ScriptJob* job = result->resolvedBy()->getStreamUrl( result );
 602    connect( job, SIGNAL( done( QVariantMap ) ), SLOT( gotStreamUrl( QVariantMap ) ) );
 603    job->setProperty( "result", QVariant::fromValue( result ) );
 604    job->start();
 605}
 606
 607
 608void
 609AudioEngine::gotStreamUrl( const QVariantMap& data )
 610{
 611    QString streamUrl = data[ "url" ].toString();
 612    QVariantMap headers = data[ "headers" ].toMap();
 613    Tomahawk::result_ptr result = sender()->property( "result" ).value<result_ptr>();
 614
 615    if ( streamUrl.isEmpty() || headers.isEmpty() ||
 616         !( TomahawkUtils::isHttpResult( streamUrl ) || TomahawkUtils::isHttpsResult( streamUrl ) ) )
 617    {
 618        // We can't supply custom headers to VLC - but prefer using its HTTP streaming due to improved seeking ability
 619        // Not an RTMP or HTTP-with-headers URL, get IO device
 620        QSharedPointer< QIODevice > sp;
 621        performLoadIODevice( result, streamUrl );
 622    }
 623    else
 624    {
 625        // We need our own QIODevice for streaming
 626        // TODO: just make this part of the http(s) IoDeviceFactory (?)
 627        QUrl url = QUrl::fromEncoded( streamUrl.toUtf8() );
 628        QNetworkRequest req( url );
 629
 630        QMap<QString, QString> parsedHeaders;
 631        foreach ( const QString& key, headers.keys() )
 632        {
 633            Q_ASSERT_X( headers[key].canConvert( QVariant::String ), Q_FUNC_INFO, "Expected a Map of string for additional headers" );
 634            if ( headers[key].canConvert( QVariant::String ) )
 635            {
 636                parsedHeaders.insert( key, headers[key].toString() );
 637            }
 638        }
 639
 640        foreach ( const QString& key, parsedHeaders.keys() )
 641        {
 642            req.setRawHeader( key.toLatin1(), parsedHeaders[key].toLatin1() );
 643        }
 644
 645        tDebug() << "Creating a QNetworkReply with url:" << req.url().toString();
 646        NetworkReply* reply = new NetworkReply( Tomahawk::Utils::nam()->get( req ) );
 647        NewClosure( reply, SIGNAL( finalUrlReached() ), this, SLOT( gotRedirectedStreamUrl( Tomahawk::result_ptr, NetworkReply* ) ), result, reply );
 648    }
 649
 650    sender()->deleteLater();
 651}
 652
 653
 654void
 655AudioEngine::gotRedirectedStreamUrl( const Tomahawk::result_ptr& result, NetworkReply* reply )
 656{
 657    // std::functions cannot accept temporaries as parameters
 658    QSharedPointer< QIODevice > sp ( reply->reply(), &QObject::deleteLater );
 659    QString url = reply->reply()->url().toString();
 660    reply->disconnectFromReply();
 661    reply->deleteLater();
 662
 663    performLoadTrack( result, url, sp );
 664}
 665
 666
 667void
 668AudioEngine::onPositionChanged( float new_position )
 669{
 670//    tDebug() << Q_FUNC_INFO << new_position << state();
 671    emit trackPosition( new_position );
 672}
 673
 674
 675void
 676AudioEngine::performLoadIODevice( const result_ptr& result, const QString& url )
 677{
 678    tDebug( LOGEXTRA ) << Q_FUNC_INFO << ( result.isNull() ? QString() : url );
 679
 680    if ( !TomahawkUtils::isLocalResult( url ) && !TomahawkUtils::isHttpResult( url )
 681         && !TomahawkUtils::isRtmpResult( url ) )
 682    {
 683        std::function< void ( const QString, QSharedPointer< QIODevice > ) > callback =
 684                std::bind( &AudioEngine::performLoadTrack, this, result,
 685                           std::placeholders::_1,
 686                           std::placeholders::_2 );
 687        Tomahawk::UrlHandler::getIODeviceForUrl( result, url, callback );
 688    }
 689    else
 690    {
 691        QSharedPointer< QIODevice > io;
 692        performLoadTrack( result, url, io );
 693    }
 694}
 695
 696
 697void
 698AudioEngine::performLoadTrack( const Tomahawk::result_ptr result, const QString& url, QSharedPointer< QIODevice > io )
 699{
 700    if ( QThread::currentThread() != thread() )
 701    {
 702        QMetaObject::invokeMethod( this, "performLoadTrack", Qt::QueuedConnection,
 703                                   Q_ARG( const Tomahawk::result_ptr, result ),
 704                                   Q_ARG( const QString, url ),
 705                                   Q_ARG( QSharedPointer< QIODevice >, io )
 706                                   );
 707        return;
 708    }
 709
 710    Q_D( AudioEngine );
 711    if ( currentTrack() != result )
 712    {
 713        tLog( LOGVERBOSE ) << Q_FUNC_INFO << "Track loaded too late, skip.";
 714        return;
 715    }
 716    tDebug( LOGEXTRA ) << Q_FUNC_INFO << ( result.isNull() ? QString() : result->url() );
 717    QSharedPointer< QIODevice > ioToKeep = io;
 718
 719    bool err = false;
 720    {
 721        if ( !( TomahawkUtils::isLocalResult( url ) || TomahawkUtils::isHttpResult( url ) || TomahawkUtils::isRtmpResult( url )  )
 722             && ( !io || io.isNull() ) )
 723        {
 724            tLog() << Q_FUNC_INFO << "Error getting iodevice for" << result->url();
 725            err = true;
 726        }
 727
 728        if ( !err )
 729        {
 730            tLog() << Q_FUNC_INFO << "Starting new song:" << url;
 731            d->state = Loading;
 732            emit loading( d->currentTrack );
 733
 734            if ( !TomahawkUtils::isLocalResult( url )
 735                 && !( TomahawkUtils::isHttpResult( url ) && io.isNull() )
 736                 && !TomahawkUtils::isRtmpResult( url ) )
 737            {
 738                QSharedPointer<QNetworkReply> qnr = io.objectCast<QNetworkReply>();
 739                if ( !qnr.isNull() )
 740                {
 741                    d->audioOutput->setCurrentSource( new QNR_IODeviceStream( qnr, this ) );
 742                    // We keep track of the QNetworkReply in QNR_IODeviceStream
 743                    // and AudioOutput handles the deletion of the
 744                    // QNR_IODeviceStream object
 745                    ioToKeep.clear();
 746                    d->audioOutput->setAutoDelete( true );
 747                }
 748                else
 749                {
 750                    d->audioOutput->setCurrentSource( io.data() );
 751                    // We handle the deletion via tracking in d->input
 752                    d->audioOutput->setAutoDelete( false );
 753                }
 754            }
 755            else
 756            {
 757                /*
 758                 * TODO: Do we need this anymore as we now do HTTP streaming ourselves?
 759                 * Maybe this can be useful for letting VLC do other protocols?
 760                 */
 761                if ( !TomahawkUtils::isLocalResult( url ) )
 762                {
 763                    QUrl furl = url;
 764                    if ( url.contains( "?" ) )
 765                    {
 766                        furl = QUrl( url.left( url.indexOf( '?' ) ) );
 767                        TomahawkUtils::urlSetQuery( furl, QString( url.mid( url.indexOf( '?' ) + 1 ) ) );
 768                    }
 769
 770                    tLog( LOGVERBOSE ) << Q_FUNC_INFO << "Passing to VLC:" << furl;
 771                    d->audioOutput->setCurrentSource( furl );
 772                }
 773                else
 774                {
 775                    QString furl = url;
 776                    if ( furl.startsWith( "file://" ) )
 777                        furl = furl.right( furl.length() - 7 );
 778
 779                    tLog( LOGVERBOSE ) << Q_FUNC_INFO << "Passing to VLC:" << QUrl::fromLocalFile( furl );
 780                    d->audioOutput->setCurrentSource( QUrl::fromLocalFile( furl ) );
 781                }
 782
 783                d->audioOutput->setAutoDelete( true );
 784            }
 785
 786            if ( !d->input.isNull() )
 787            {
 788                d->input->close();
 789                d->input.clear();
 790            }
 791            d->input = ioToKeep;
 792            d->audioOutput->play();
 793
 794            if ( TomahawkSettings::instance()->privateListeningMode() != TomahawkSettings::FullyPrivate )
 795            {
 796                d->currentTrack->track()->startPlaying();
 797            }
 798
 799            sendNowPlayingNotification( Tomahawk::InfoSystem::InfoNowPlaying );
 800        }
 801    }
 802
 803    if ( err )
 804    {
 805        stop();
 806        return;
 807    }
 808
 809    d->waitingOnNewTrack = false;
 810    return;
 811}
 812
 813
 814void
 815AudioEngine::loadPreviousTrack()
 816{
 817    if ( QThread::currentThread() != thread() )
 818    {
 819        QMetaObject::invokeMethod( this, "loadPreviousTrack", Qt::QueuedConnection );
 820        return;
 821    }
 822
 823    Q_D( AudioEngine );
 824
 825    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
 826
 827    if ( d->playlist.isNull() )
 828    {
 829        stop();
 830        return;
 831    }
 832
 833    Tomahawk::result_ptr result;
 834    if ( d->playlist.data()->previousResult() )
 835    {
 836        result = d->playlist.data()->setSiblingResult( -1 );
 837        setCurrentTrackPlaylist( d->playlist );
 838    }
 839
 840    if ( result )
 841        loadTrack( result );
 842    else
 843        stop();
 844}
 845
 846
 847void
 848AudioEngine::loadNextTrack()
 849{
 850    if ( QThread::currentThread() != thread() )
 851    {
 852        QMetaObject::invokeMethod( this, "loadNextTrack", Qt::QueuedConnection );
 853        return;
 854    }
 855
 856    Q_D( AudioEngine );
 857
 858    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
 859
 860    Tomahawk::result_ptr result;
 861
 862    if ( d->stopAfterTrack && d->currentTrack )
 863    {
 864        if ( d->stopAfterTrack->track()->equals( d->currentTrack->track() ) )
 865        {
 866            d->stopAfterTrack.clear();
 867            stop();
 868            return;
 869        }
 870    }
 871
 872    if ( d->queue && d->queue->trackCount() )
 873    {
 874        query_ptr query = d->queue->tracks().first();
 875        if ( query && query->numResults() )
 876            result = query->results().first();
 877    }
 878
 879    if ( !d->playlist.isNull() && result.isNull() )
 880    {
 881        tDebug( LOGVERBOSE ) << Q_FUNC_INFO << "Loading playlist's next item" << d->playlist.data() << d->playlist->shuffled();
 882
 883        if ( d->playlist.data()->nextResult() )
 884        {
 885            result = d->playlist.data()->setSiblingResult( 1 );
 886            setCurrentTrackPlaylist( d->playlist );
 887        }
 888    }
 889
 890    if ( result )
 891    {
 892        tDebug( LOGVERBOSE ) << Q_FUNC_INFO << "Got next item, loading track";
 893        loadTrack( result );
 894    }
 895    else
 896    {
 897        if ( !d->playlist.isNull() && d->playlist.data()->retryMode() == Tomahawk::PlaylistModes::Retry )
 898            d->waitingOnNewTrack = true;
 899
 900        stop();
 901    }
 902}
 903
 904
 905void
 906AudioEngine::play( const QUrl& url )
 907{
 908    tDebug() << Q_FUNC_INFO << url;
 909
 910    const QVariantMap tags = MusicScanner::readTags( QFileInfo( url.toLocalFile() ) ).toMap();
 911
 912    track_ptr t;
 913    if ( !tags.isEmpty() )
 914    {
 915        t = Track::get( tags["artist"].toString(), tags["track"].toString(), tags["album"].toString(),
 916                        tags["albumArtist"].toString(), tags["duration"].toInt(), tags["composer"].toString(),
 917                        tags["albumpos"].toUInt(), tags["discnumber"].toUInt() );
 918    }
 919    else
 920    {
 921        t = Tomahawk::Track::get( "Unknown Artist", "Unknown Track" );
 922    }
 923
 924    result_ptr result = Result::get( url.toString(), t );
 925
 926    if ( !tags.isEmpty() )
 927    {
 928        result->setSize( tags["size"].toUInt() );
 929        result->setBitrate( tags["bitrate"].toUInt() );
 930        result->setModificationTime( tags["mtime"].toUInt() );
 931        result->setMimetype( tags["mimetype"].toString() );
 932    }
 933
 934    result->setResolvedByCollection( SourceList::instance()->getLocal()->collections().first(), false );
 935
 936    //    Tomahawk::query_ptr qry = Tomahawk::Query::get( t );
 937    playItem( playlistinterface_ptr(), result, query_ptr() );
 938}
 939
 940
 941void
 942AudioEngine::playItem( Tomahawk::playlistinterface_ptr playlist, const Tomahawk::result_ptr& result, const Tomahawk::query_ptr& fromQuery )
 943{
 944    Q_D( AudioEngine );
 945
 946    tDebug( LOGEXTRA ) << Q_FUNC_INFO << ( result.isNull() ? QString() : result->url() );
 947
 948    if ( !d->playlist.isNull() )
 949        d->playlist.data()->reset();
 950
 951    setPlaylist( playlist );
 952
 953    if ( !playlist && fromQuery )
 954    {
 955        setCurrentTrackPlaylist( playlistinterface_ptr( new SingleTrackPlaylistInterface( fromQuery ) ) );
 956    }
 957    else
 958    {
 959        setCurrentTrackPlaylist( playlist );
 960    }
 961
 962    if ( result )
 963    {
 964        loadTrack( result );
 965    }
 966    else if ( !d->playlist.isNull() && d->playlist.data()->retryMode() == PlaylistModes::Retry )
 967    {
 968        d->waitingOnNewTrack = true;
 969        if ( isStopped() )
 970            emit sendWaitingNotification();
 971        else
 972            stop();
 973    }
 974}
 975
 976
 977void
 978AudioEngine::playItem( Tomahawk::playlistinterface_ptr playlist, const Tomahawk::query_ptr& query )
 979{
 980    if ( query->resolvingFinished() || query->numResults( true ) )
 981    {
 982        if ( query->numResults( true ) )
 983        {
 984            playItem( playlist, query->results().first(), query );
 985            return;
 986        }
 987
 988        JobStatusView::instance()->model()->addJob(
 989            new ErrorStatusMessage( tr( "Sorry, %applicationName couldn't find the track '%1' by %2" ).arg( query->queryTrack()->track() ).arg( query->queryTrack()->artist() ), 15 ) );
 990
 991        if ( isStopped() )
 992            emit stopped(); // we do this so the original caller knows we couldn't find this track
 993    }
 994    else
 995    {
 996        Pipeline::instance()->resolve( query );
 997
 998        NewClosure( query.data(), SIGNAL( resultsChanged() ),
 999                    const_cast<AudioEngine*>(this), SLOT( playItem( Tomahawk::playlistinterface_ptr, Tomahawk::query_ptr ) ), playlist, query );
1000    }
1001}
1002
1003
1004void
1005AudioEngine::playItem( const Tomahawk::artist_ptr& artist )
1006{
1007    playlistinterface_ptr pli = artist->playlistInterface( Mixed );
1008    if ( pli->isFinished() )
1009    {
1010        if ( pli->tracks().isEmpty() )
1011        {
1012            JobStatusView::instance()->model()->addJob(
1013                new ErrorStatusMessage( tr( "Sorry, %applicationName couldn't find the artist '%1'" ).arg( artist->name() ), 15 ) );
1014
1015            if ( isStopped() )
1016                emit stopped(); // we do this so the original caller knows we couldn't find this track
1017        }
1018        else
1019            playPlaylistInterface( pli );
1020    }
1021    else
1022    {
1023        NewClosure( artist.data(), SIGNAL( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ),
1024                    const_cast<AudioEngine*>(this), SLOT( playItem( Tomahawk::artist_ptr ) ), artist );
1025        pli->tracks();
1026    }
1027}
1028
1029
1030void
1031AudioEngine::playItem( const Tomahawk::album_ptr& album )
1032{
1033    playlistinterface_ptr pli = album->playlistInterface( Mixed );
1034    if ( pli->isFinished() )
1035    {
1036        if ( pli->tracks().isEmpty() )
1037        {
1038            JobStatusView::instance()->model()->addJob(
1039                new ErrorStatusMessage( tr( "Sorry, %applicationName couldn't find the album '%1' by %2" ).arg( album->name() ).arg( album->artist()->name() ), 15 ) );
1040
1041            if ( isStopped() )
1042                emit stopped(); // we do this so the original caller knows we couldn't find this track
1043        }
1044        else
1045            playPlaylistInterface( pli );
1046    }
1047    else
1048    {
1049        NewClosure( album.data(), SIGNAL( tracksAdded( QList<Tomahawk::query_ptr>, Tomahawk::ModelMode, Tomahawk::collection_ptr ) ),
1050                    const_cast<AudioEngine*>(this), SLOT( playItem( Tomahawk::album_ptr ) ), album );
1051        pli->tracks();
1052    }
1053}
1054
1055
1056void
1057AudioEngine::playPlaylistInterface( const Tomahawk::playlistinterface_ptr& playlist )
1058{
1059    if ( !playlist->hasFirstPlayableTrack() )
1060    {
1061        NewClosure( playlist.data(), SIGNAL( foundFirstPlayableTrack() ),
1062                    const_cast<AudioEngine*>(this), SLOT( playPlaylistInterface( Tomahawk::playlistinterface_ptr ) ), playlist );
1063        return;
1064    }
1065
1066    foreach ( const Tomahawk::query_ptr& query, playlist->tracks() )
1067    {
1068        if ( query->playable() )
1069        {
1070            playItem( playlist, query );
1071            return;
1072        }
1073    }
1074
1075    // No playable track found
1076    JobStatusView::instance()->model()->addJob( new ErrorStatusMessage( tr( "Sorry, couldn't find any playable tracks" ), 15 ) );
1077}
1078
1079
1080void
1081AudioEngine::onPlaylistNextTrackAvailable()
1082{
1083    Q_D( AudioEngine );
1084
1085    tDebug() << Q_FUNC_INFO;
1086
1087    {
1088        // If in real-time and you have a few seconds left, you're probably lagging -- finish it up
1089        if ( d->playlist && d->playlist->latchMode() == PlaylistModes::RealTime && ( d->waitingOnNewTrack || d->currentTrack.isNull() || d->currentTrack->id() == 0 || ( currentTrackTotalTime() - currentTime() > 6000 ) ) )
1090        {
1091            d->waitingOnNewTrack = false;
1092            loadNextTrack();
1093            return;
1094        }
1095
1096        if ( !d->waitingOnNewTrack )
1097            return;
1098
1099        d->waitingOnNewTrack = false;
1100        loadNextTrack();
1101    }
1102}
1103
1104
1105void
1106AudioEngine::timerTriggered( qint64 time )
1107{
1108    Q_D( AudioEngine );
1109
1110    emit timerMilliSeconds( time );
1111
1112    if ( d->timeElapsed != time / 1000 )
1113    {
1114        d->timeElapsed = time / 1000;
1115        emit timerSeconds( d->timeElapsed );
1116
1117        if ( !d->currentTrack.isNull() )
1118        {
1119            if ( d->currentTrack->track()->duration() == 0 )
1120            {
1121                emit timerPercentage( 0 );
1122            }
1123            else
1124            {
1125                emit timerPercentage( ( (double) d->timeElapsed / (double) d->currentTrack->track()->duration() ) * 100.0 );
1126            }
1127        }
1128    }
1129}
1130
1131
1132void
1133AudioEngine::setQueue( const playlistinterface_ptr& queue )
1134{
1135    Q_D( AudioEngine );
1136
1137    if ( d->queue )
1138    {
1139        disconnect( d->queue.data(), SIGNAL( previousTrackAvailable( bool ) ), this, SIGNAL( controlStateChanged() ) );
1140        disconnect( d->queue.data(), SIGNAL( nextTrackAvailable( bool ) ), this, SIGNAL( controlStateChanged() ) );
1141    }
1142
1143    d->queue = queue;
1144
1145    if ( d->queue )
1146    {
1147        connect( d->queue.data(), SIGNAL( previousTrackAvailable( bool ) ), SIGNAL( controlStateChanged() ) );
1148        connect( d->queue.data(), SIGNAL( nextTrackAvailable( bool ) ), SIGNAL( controlStateChanged() ) );
1149    }
1150}
1151
1152
1153void
1154AudioEngine::setPlaylist( Tomahawk::playlistinterface_ptr playlist )
1155{
1156    Q_D( AudioEngine );
1157
1158    if ( d->playlist == playlist )
1159        return;
1160
1161    if ( !d->playlist.isNull() )
1162    {
1163        if ( d->playlist.data() )
1164        {
1165            disconnect( d->playlist.data(), SIGNAL( previousTrackAvailable( bool ) ) );
1166            disconnect( d->playlist.data(), SIGNAL( nextTrackAvailable( bool ) ) );
1167            disconnect( d->playlist.data(), SIGNAL( shuffleModeChanged( bool ) ) );
1168            disconnect( d->playlist.data(), SIGNAL( repeatModeChanged( Tomahawk::PlaylistModes::RepeatMode ) ) );
1169        }
1170
1171        d->playlist.data()->reset();
1172    }
1173
1174    if ( playlist.isNull() )
1175    {
1176        d->playlist.clear();
1177        emit playlistChanged( playlist );
1178        return;
1179    }
1180
1181    d->playlist = playlist;
1182    d->stopAfterTrack.clear();
1183
1184    if ( !d->playlist.isNull() )
1185    {
1186        connect( d->playlist.data(), SIGNAL( nextTrackAvailable( bool ) ), SLOT( onPlaylistNextTrackAvailable() ) );
1187
1188        connect( d->playlist.data(), SIGNAL( previousTrackAvailable( bool ) ), SIGNAL( controlStateChanged() ) );
1189        connect( d->playlist.data(), SIGNAL( nextTrackAvailable( bool ) ), SIGNAL( controlStateChanged() ) );
1190
1191        connect( d->playlist.data(), SIGNAL( shuffleModeChanged( bool ) ), SIGNAL( shuffleModeChanged( bool ) ) );
1192        connect( d->playlist.data(), SIGNAL( repeatModeChanged( Tomahawk::PlaylistModes::RepeatMode ) ), SIGNAL( repeatModeChanged( Tomahawk::PlaylistModes::RepeatMode ) ) );
1193
1194        emit shuffleModeChanged( d->playlist.data()->shuffled() );
1195        emit repeatModeChanged( d->playlist.data()->repeatMode() );
1196    }
1197
1198    emit playlistChanged( playlist );
1199}
1200
1201
1202void
1203AudioEngine::setRepeatMode( Tomahawk::PlaylistModes::RepeatMode mode )
1204{
1205    Q_D( AudioEngine );
1206
1207    if ( !d->playlist.isNull() )
1208    {
1209        d->playlist.data()->setRepeatMode( mode );
1210    }
1211}
1212
1213
1214void
1215AudioEngine::setShuffled( bool enabled )
1216{
1217    Q_D( AudioEngine );
1218
1219    if ( !d->playlist.isNull() )
1220    {
1221        d->playlist.data()->setShuffled( enabled );
1222    }
1223}
1224
1225
1226void
1227AudioEngine::setStopAfterTrack( const query_ptr& query )
1228{
1229    Q_D( AudioEngine );
1230
1231    if ( d->stopAfterTrack != query )
1232    {
1233        d->stopAfterTrack = query;
1234        emit stopAfterTrackChanged();
1235    }
1236}
1237
1238
1239void
1240AudioEngine::setCurrentTrack( const Tomahawk::result_ptr& result )
1241{
1242    Q_D( AudioEngine );
1243
1244    if ( !d->currentTrack.isNull() )
1245    {
1246        if ( d->state != Error && TomahawkSettings::instance()->privateListeningMode() == TomahawkSettings::PublicListening )
1247        {
1248            d->currentTrack->track()->finishPlaying( d->timeElapsed );
1249        }
1250
1251        emit finished( d->currentTrack );
1252    }
1253
1254    d->currentTrack = result;
1255
1256    if ( result )
1257    {
1258        if ( d->playlist && d->playlist->currentItem() != result )
1259        {
1260            d->playlist->setCurrentIndex( d->playlist->indexOfResult( result ) );
1261        }
1262    }
1263}
1264
1265
1266void
1267AudioEngine::setState( AudioState state )
1268{
1269    Q_D( AudioEngine );
1270
1271    AudioState oldState = (AudioState) d->state;
1272    d->state = state;
1273
1274    emit stateChanged( state, oldState );
1275}
1276
1277
1278qint64
1279AudioEngine::currentTime() const
1280{
1281    return d_func()->audioOutput->currentTime();
1282}
1283
1284
1285qint64
1286AudioEngine::currentTrackTotalTime() const
1287{
1288    Q_D( const AudioEngine );
1289
1290    // FIXME : This is too hacky. The problem is that I don't know why
1291    //         libVLC doesn't report total duration for stream data (imem://)
1292    // But it's not a real problem for playback, since EndOfStream is emitted by libVLC itself
1293    // This value is only used by AudioOutput to evaluate if it's close to end of stream
1294    if ( d->audioOutput->totalTime() <= 0 && d->currentTrack && d->currentTrack->track() ) {
1295        return d->currentTrack->track()->duration() * 1000 + 1000;
1296    }
1297    return d->audioOutput->totalTime();
1298}
1299
1300
1301unsigned int
1302AudioEngine::volume() const
1303{
1304    return d_func()->audioOutput->volume() * 100.0;
1305}
1306
1307
1308AudioEngine::AudioState
1309AudioEngine::state() const
1310{
1311    return (AudioState) d_func()->state;
1312}
1313
1314
1315bool
1316AudioEngine::isPlaying() const
1317{
1318    return d_func()->state == Playing;
1319}
1320
1321
1322bool
1323AudioEngine::isPaused() const
1324{
1325    return d_func()->state == Paused;
1326}
1327
1328
1329bool
1330AudioEngine::isStopped() const
1331{
1332    return d_func()->state == Stopped;
1333}
1334
1335
1336playlistinterface_ptr
1337AudioEngine::currentTrackPlaylist() const
1338{
1339    return d_func()->currentTrackPlaylist;
1340}
1341
1342
1343playlistinterface_ptr
1344AudioEngine::playlist() const
1345{
1346    return d_func()->playlist;
1347}
1348
1349
1350result_ptr
1351AudioEngine::currentTrack() const
1352{
1353    return d_func()->currentTrack;
1354}
1355
1356
1357query_ptr
1358AudioEngine::stopAfterTrack() const
1359{
1360    return d_func()->stopAfterTrack;
1361}
1362
1363
1364void
1365AudioEngine::onVolumeChanged( qreal volume )
1366{
1367    emit volumeChanged( volume * 100 );
1368}
1369
1370
1371void
1372AudioEngine::setCurrentTrackPlaylist( const playlistinterface_ptr& playlist )
1373{
1374    Q_D( AudioEngine );
1375
1376    if ( d->currentTrackPlaylist != playlist )
1377    {
1378        d->currentTrackPlaylist = playlist;
1379        emit currentTrackPlaylistChanged( d->currentTrackPlaylist );
1380    }
1381}
1382
1383
1384void
1385AudioEngine::setDspCallback( std::function< void( int state, int frameNumber, float* samples, int nb_channels, int nb_samples ) > cb )
1386{
1387    Q_D( AudioEngine );
1388
1389    d->audioOutput->setDspCallback( cb );
1390}