1
/***************************************************************************
2
 *   Copyright (C) 2004 Frederik Holljen <fh@ez.no>                        *
3
 *             (C) 2004, 2005 Max Howell <max.howell@methylblue.com>       *
4
 *             (C) 2004-2008 Mark Kretschmann <kretschmann@kde.org>        *
5
 *             (C) 2006, 2008 Ian Monroe <ian@monroe.nu>                   *
6
 *             (C) 2008 Jason A. Donenfeld <Jason@zx2c4.com>               *
7
 *                                                                         *
8
 *   This program is free software; you can redistribute it and/or modify  *
9
 *   it under the terms of the GNU General Public License as published by  *
10
 *   the Free Software Foundation; either version 2 of the License, or     *
11
 *   (at your option) any later version.                                   *
12
 *                                                                         *
13
 ***************************************************************************/
14
15
#define DEBUG_PREFIX "EngineController"
16
17
#include "EngineController.h"
18
19
#include "Amarok.h"
20
#include "amarokconfig.h"
21
#include "collection/CollectionManager.h"
22
#include "statusbar/StatusBar.h"
23
#include "Debug.h"
24
#include "MainWindow.h"
25
#include "MediaDeviceMonitor.h"
26
#include "meta/Meta.h"
27
#include "meta/MetaConstants.h"
28
#include "meta/capabilities/MultiPlayableCapability.h"
29
#include "meta/capabilities/MultiSourceCapability.h"
30
#include "playlist/PlaylistActions.h"
31
#include "playlistmanager/PlaylistManager.h"
32
#include "PluginManager.h"
33
34
#include <KFileItem>
35
#include <KIO/Job>
36
#include <KMessageBox>
37
#include <KRun>
38
39
#include <Phonon/AudioOutput>
40
#include <Phonon/BackendCapabilities>
41
#include <Phonon/MediaObject>
42
#include <Phonon/VolumeFaderEffect>
43
44
#include <QTimer>
45
46
#include <cmath>
47
48
namespace The {
49
    EngineController* engineController() { return EngineController::instance(); }
50
}
51
52
EngineController* EngineController::s_instance = 0;
53
54
EngineController*
55
EngineController::instance()
56
{
57
    return s_instance ? s_instance : new EngineController();
58
}
59
60
void
61
EngineController::destroy()
62
{
63
    delete s_instance;
64
    s_instance = 0;
65
}
66
67
EngineController::EngineController()
68
    : m_playWhenFetched( true )
69
    , m_fadeoutTimer( new QTimer( this ) )
70
    , m_volume( 0 )
71
{
72
    DEBUG_BLOCK
73
74
    initializePhonon();
75
76
    m_fadeoutTimer->setSingleShot( true );
77
78
    connect( m_fadeoutTimer, SIGNAL( timeout() ), SLOT( slotStopFadeout() ) );
79
80
    s_instance = this;
81
}
82
83
EngineController::~EngineController()
84
{
85
    DEBUG_BLOCK //we like to know when singletons are destroyed
86
87
    m_media->stop();
88
89
    delete m_media;
90
    delete m_audio;
91
}
92
93
void
94
EngineController::initializePhonon()
95
{
96
    DEBUG_BLOCK
97
98
    delete m_media;
99
    delete m_controller;
100
    delete m_audio;
101
    delete m_preamp;
102
103
    PERF_LOG( "EngineController: loading phonon objects" )
104
    m_media = new Phonon::MediaObject( this );
105
    m_audio = new Phonon::AudioOutput( Phonon::MusicCategory, this );
106
107
    m_path = Phonon::createPath( m_media, m_audio );
108
109
    m_controller = new Phonon::MediaController( m_media );
110
    
111
    // HACK we turn off replaygain manually on OSX, until the phonon coreaudio backend is fixed.
112
    // as the default is specified in the .cfg file, we can't just tell it to be a different default on OSX
113
#ifdef Q_WS_MAC
114
    AmarokConfig::setReplayGainMode( AmarokConfig::EnumReplayGainMode::Off );
115
#endif
116
117
    // only create pre-amp if we have replaygain on, preamp can cause phonon issues
118
    if( AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off )
119
    {   
120
        m_preamp = new Phonon::VolumeFaderEffect( this );
121
        m_path.insertEffect( m_preamp );
122
    }
123
124
    m_media->setTickInterval( 100 );
125
    debug() << "Tick Interval (actual): " << m_media->tickInterval();
126
    PERF_LOG( "EngineController: loaded phonon objects" )
127
128
    // Get the next track when there is 2 seconds left on the current one.
129
    m_media->setPrefinishMark( 2000 );
130
131
    connect( m_media, SIGNAL( finished() ), SLOT( slotQueueEnded() ) );
132
    connect( m_media, SIGNAL( aboutToFinish()), SLOT( slotAboutToFinish() ) );
133
    connect( m_media, SIGNAL( metaDataChanged() ), SLOT( slotMetaDataChanged() ) );
134
    connect( m_media, SIGNAL( stateChanged( Phonon::State, Phonon::State ) ), SLOT( slotStateChanged( Phonon::State, Phonon::State ) ) );
135
    connect( m_media, SIGNAL( tick( qint64 ) ), SLOT( slotTick( qint64 ) ) );
136
    connect( m_media, SIGNAL( totalTimeChanged( qint64 ) ), SLOT( slotTrackLengthChanged( qint64 ) ) );
137
    connect( m_media, SIGNAL( currentSourceChanged( const Phonon::MediaSource & ) ), SLOT( slotNewTrackPlaying( const Phonon::MediaSource & ) ) );
138
139
    connect( m_controller, SIGNAL( titleChanged( int ) ), SLOT( slotTitleChanged( int ) ) );
140
141
    
142
    //TODO: The xine engine does not support crossfading. Cannot get the gstreamer engine to work, will test this once I do.
143
#if 0
144
    if( AmarokConfig::trackDelayLength() > -1 )
145
        m_media->setTransitionTime( AmarokConfig::trackDelayLength() ); // Also Handles gapless.
146
    else if( AmarokConfig::crossfadeLength() > 0 )  // TODO: Handle the possible options on when to crossfade.. the values are not documented anywhere however
147
        m_media->setTransitionTime( -AmarokConfig::crossfadeLength() );
148
#endif
149
}
150
151
152
//////////////////////////////////////////////////////////////////////////////////////////
153
// PUBLIC
154
//////////////////////////////////////////////////////////////////////////////////////////
155
156
bool
157
EngineController::canDecode( const KUrl &url ) //static
158
{
159
   //NOTE this function must be thread-safe
160
161
    // We can't use playlists in the engine
162
    if( PlaylistManager::isPlaylist( url ) )
163
        return false;
164
165
    KFileItem item( KFileItem::Unknown, KFileItem::Unknown, url );
166
    // If file has 0 bytes, ignore it and return false
167
    if( !item.size() )
168
        return false;
169
170
    // We can't play directories, reguardless of what the engine says.
171
    if( item.isDir() )
172
        return false;
173
174
    // Accept non-local files, since we can't test them for validity at this point
175
    if( !item.isLocalFile() )
176
        return true;
177
178
    // Filter the available mime types to only include audio and video, as amarok does not intend to play photos
179
    static QStringList mimeTable = Phonon::BackendCapabilities::availableMimeTypes().filter( "audio/", Qt::CaseInsensitive ) +
180
                                   Phonon::BackendCapabilities::availableMimeTypes().filter( "video/", Qt::CaseInsensitive );
181
182
    const QString mimeType = item.mimetype();
183
    const bool valid = mimeTable.contains( mimeType, Qt::CaseInsensitive );
184
185
    //we special case this as otherwise users hate us
186
    if ( !valid && ( mimeType == "audio/mp3" || mimeType == "audio/x-mp3" ) && !installDistroCodec() )
187
        The::statusBar()->longMessage(
188
                i18n( "<p>Phonon claims it <b>cannot</b> play MP3 files. You may want to examine "
189
                      "the installation of the backend that phonon uses.</p>"
190
                      "<p>You may find useful information in the <i>FAQ</i> section of the <i>Amarok Handbook</i>.</p>" ), StatusBar::Error );
191
192
    return valid;
193
}
194
195
bool
196
EngineController::installDistroCodec()
197
{
198
    KService::List services = KServiceTypeTrader::self()->query( "Amarok/CodecInstall"
199
        , QString( "[X-KDE-Amarok-codec] == 'mp3' and [X-KDE-Amarok-engine] == 'phonon-%1'").arg( "xine" ) );
200
    //todo - figure out how to query Phonon for the current backend loaded
201
    if( !services.isEmpty() )
202
    {
203
        KService::Ptr service = services.first(); //list is not empty
204
        QString installScript = service->exec();
205
        if( !installScript.isNull() ) //just a sanity check
206
        {
207
            KGuiItem installButton( i18n( "Install MP3 Support" ) );
208
            if(KMessageBox::questionYesNo( The::mainWindow()
209
            , i18n("Amarok currently cannot play MP3 files. Do you want to install support for MP3?")
210
            , i18n( "No MP3 Support" )
211
            , installButton
212
            , KStandardGuiItem::no()
213
            , "codecInstallWarning" ) == KMessageBox::Yes )
214
            {
215
                    KRun::runCommand(installScript, 0);
216
                    return true;
217
            }
218
        }
219
    }
220
221
    return false;
222
}
223
224
void
225
EngineController::restoreSession()
226
{
227
    //here we restore the session
228
    //however, do note, this is always done, KDE session management is not involved
229
230
    if( AmarokConfig::resumePlayback() )
231
    {
232
        const KUrl url = AmarokConfig::resumeTrack();
233
234
        // Only resume local files, because resuming remote protocols can have weird side effects.
235
        // See: http://bugs.kde.org/show_bug.cgi?id=172897
236
        if( url.isLocalFile() )
237
        {
238
            Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
239
            play( track, AmarokConfig::resumeTime() );
240
        }
241
    }
242
}
243
244
void
245
EngineController::endSession()
246
{
247
    //only update song stats, when we're not going to resume it
248
    if ( !AmarokConfig::resumePlayback() && m_currentTrack )
249
    {
250
        playbackEnded( trackPosition(), m_currentTrack->length(), EngineObserver::EndedQuit );
251
        emit trackChanged( Meta::TrackPtr( 0 ) );
252
    }
253
}
254
255
//////////////////////////////////////////////////////////////////////////////////////////
256
// PUBLIC SLOTS
257
//////////////////////////////////////////////////////////////////////////////////////////
258
259
void
260
EngineController::play() //SLOT
261
{
262
    DEBUG_BLOCK
263
264
    if( m_media->state() == Phonon::PlayingState )
265
        return;
266
267
    if( m_fader )
268
        m_fader->deleteLater();
269
270
    if ( m_media->state() == Phonon::PausedState )
271
    {
272
        m_media->play();
273
    }
274
    else
275
    {
276
        The::playlistActions()->play();
277
    }
278
}
279
280
void
281
EngineController::play( const Meta::TrackPtr& track, uint offset )
282
{
283
    DEBUG_BLOCK
284
285
    if( !track ) // Guard
286
        return;
287
288
    m_currentTrack = track;
289
    delete m_boundedPlayback;
290
    delete m_multiPlayback;
291
    delete m_multiSource;
292
    m_currentTrack->create<Meta::BoundedPlaybackCapability>();
293
    m_multiPlayback = m_currentTrack->create<Meta::MultiPlayableCapability>();
294
    m_multiSource  = m_currentTrack->create<Meta::MultiSourceCapability>();
295
296
    m_nextTrack.clear();
297
    m_nextUrl.clear();
298
    m_media->clearQueue();
299
300
    m_currentTrack->prepareToPlay();
301
302
    if( m_multiPlayback )
303
    {
304
        m_media->stop();
305
        connect( m_multiPlayback, SIGNAL( playableUrlFetched( const KUrl & ) ), this, SLOT( slotPlayableUrlFetched( const KUrl & ) ) );
306
        m_multiPlayback->fetchFirst();
307
    }
308
    else if ( m_multiSource )
309
    {
310
        m_media->stop();
311
        debug() << "Got a MultiSource Track with " <<  m_multiSource->sources().count() << " sources";
312
        connect( m_multiSource, SIGNAL( urlChanged( const KUrl & ) ), this, SLOT( slotPlayableUrlFetched( const KUrl & ) ) );
313
        playUrl( m_currentTrack->playableUrl(), 0 );
314
    }
315
    else if ( m_boundedPlayback )
316
    {
317
        playUrl( m_currentTrack->playableUrl(), m_boundedPlayback->startPosition() );
318
    }
319
    else
320
    {
321
        playUrl( m_currentTrack->playableUrl(), offset );
322
    }
323
}
324
325
void
326
EngineController::playUrl( const KUrl &url, uint offset )
327
{
328
    DEBUG_BLOCK
329
330
    slotStopFadeout();
331
332
    debug() << "URL: " << url.url();
333
334
    if ( url.url().startsWith( "audiocd:/" ) )
335
    {
336
        //disconnect this signal for now or it will cause a loop that will cause a mutex lockup
337
        disconnect( m_controller, SIGNAL( titleChanged( int ) ), this, SLOT( slotTitleChanged( int ) ) );
338
        
339
        debug() << "play track from cd";
340
        QString trackNumberString = url.url();
341
        trackNumberString = trackNumberString.replace( "audiocd:/", QString() );
342
343
        QStringList parts = trackNumberString.split( "/" );
344
345
        if ( parts.count() != 2 )
346
            return;
347
        
348
        QString discId = parts.at( 0 );
349
        
350
        //we really only want to play it if it is the disc that is currently present.
351
        //In the case of cds for which we dont have any id, any "unknown" cds will
352
        //be considdered equal.
353
354
        if ( MediaDeviceMonitor::instance()->currentCdId() != discId )
355
            return;
356
357
        
358
        int trackNumber = parts.at( 1 ).toInt();
359
360
        debug() << "3.2.1...";
361
        m_media->clear();
362
        m_media->setCurrentSource( Phonon::Cd );
363
        debug() << "boom?";
364
        m_controller->setCurrentTitle( trackNumber );
365
        debug() << "no boom?";
366
367
        //reconnect it
368
        connect( m_controller, SIGNAL( titleChanged( int ) ), SLOT( slotTitleChanged( int ) ) );
369
        
370
    }
371
    else
372
    {
373
        m_media->setCurrentSource( url );
374
    }
375
376
    m_nextTrack.clear();
377
    m_nextUrl.clear();
378
    m_media->clearQueue();
379
380
    if( offset )
381
    {
382
        m_media->pause();
383
        m_media->seek( offset );
384
    }
385
    m_media->play();
386
}
387
388
void
389
EngineController::pause() //SLOT
390
{
391
    m_media->pause();
392
}
393
394
void
395
EngineController::stop( bool forceInstant ) //SLOT
396
{
397
    DEBUG_BLOCK
398
399
    // need to get a new instance of multi if played again
400
    delete m_multiPlayback;
401
    delete m_multiSource;
402
403
    m_mutex.lock();
404
    m_nextTrack.clear();
405
    m_nextUrl.clear();
406
    m_media->clearQueue();
407
    m_mutex.unlock();
408
409
    //let Amarok know that the previous track is no longer playing
410
    if( m_currentTrack )
411
    {
412
        debug() << "m_currentTrack != 0";
413
        const int pos = trackPosition();
414
        const int length = m_currentTrack->length();
415
        m_currentTrack->finishedPlaying( double(pos)/double(length) );
416
        playbackEnded( pos, length, EngineObserver::EndedStopped );
417
        emit trackChanged( Meta::TrackPtr( 0 ) );
418
    }
419
420
    // Stop instantly if fadeout is already running, or the media is paused (i.e. pressing Stop twice)
421
    if( m_fader || m_media->state() == Phonon::PausedState )
422
    {
423
        forceInstant = true;
424
    }
425
426
    if( AmarokConfig::fadeout() && AmarokConfig::fadeoutLength() && !forceInstant )
427
    {
428
        stateChangedNotify( Phonon::StoppedState, Phonon::PlayingState ); //immediately disable Stop action
429
430
        m_fader = new Phonon::VolumeFaderEffect( this );
431
        m_path.insertEffect( m_fader );
432
        m_fader->setFadeCurve( Phonon::VolumeFaderEffect::Fade9Decibel );
433
        m_fader->fadeOut( AmarokConfig::fadeoutLength() );
434
435
        m_fadeoutTimer->start( AmarokConfig::fadeoutLength() + 1000 ); //add 1s for good measure, otherwise seems to cut off early (buffering..)
436
    }
437
    else
438
        m_media->stop();
439
440
    emit trackFinished();
441
    m_currentTrack = 0;
442
}
443
444
bool
445
EngineController::isPaused() const
446
{
447
    return m_media->state() == Phonon::PausedState;
448
}
449
450
void
451
EngineController::playPause() //SLOT
452
{
453
    DEBUG_BLOCK
454
455
    //this is used by the TrayIcon, PlayPauseAction and DBus
456
    debug() << "PlayPause: phonon state" << m_media->state();
457
458
    if( m_media->state() == Phonon::PausedState ||
459
        m_media->state() == Phonon::StoppedState ||
460
        m_media->state() == Phonon::LoadingState )
461
        play();
462
    else
463
        pause();
464
}
465
466
void
467
EngineController::seek( int ms ) //SLOT
468
{
469
    DEBUG_BLOCK
470
471
    if( m_media->isSeekable() )
472
    {
473
474
        debug() << "ssek to: " << ms;
475
        int seekTo;
476
        
477
        if ( m_boundedPlayback )
478
            seekTo = m_boundedPlayback->startPosition() + ms;
479
        else
480
           seekTo = ms;
481
482
        m_media->seek( static_cast<qint64>( seekTo ) );
483
        trackPositionChangedNotify( seekTo, true ); /* User seek */
484
        emit trackSeeked( seekTo );
485
    }
486
    else
487
        debug() << "Stream is not seekable.";
488
}
489
490
491
void
492
EngineController::seekRelative( int ms ) //SLOT
493
{
494
    qint64 newPos = m_media->currentTime() + ms;
495
    seek( newPos <= 0 ? 0 : newPos );
496
}
497
498
void
499
EngineController::seekForward( int ms )
500
{
501
    seekRelative( ms );
502
}
503
504
void
505
EngineController::seekBackward( int ms )
506
{
507
    seekRelative( -ms );
508
}
509
510
int
511
EngineController::increaseVolume( int ticks ) //SLOT
512
{
513
    return setVolume( volume() + ticks );
514
}
515
516
int
517
EngineController::decreaseVolume( int ticks ) //SLOT
518
{
519
    return setVolume( volume() - ticks );
520
}
521
522
int
523
EngineController::setVolume( int percent ) //SLOT
524
{
525
    percent = qBound( 0, percent, 100 );
526
    m_volume = percent; 
527
    
528
    // Phonon stays completely mute if the volume is lower than 0.05, so we shift and limit the range 
529
    qreal newVolume =  ( percent + 4 ) / 100.0;
530
    newVolume = qBound( 0.04, newVolume, 1.0 );
531
    m_audio->setVolume( newVolume );
532
    
533
    AmarokConfig::setMasterVolume( percent );
534
    volumeChangedNotify( percent );
535
    emit volumeChanged( percent );
536
537
    return percent;
538
}
539
540
int
541
EngineController::volume() const
542
{
543
    return m_volume;
544
}
545
546
bool
547
EngineController::isMuted() const
548
{
549
    return m_audio->isMuted();
550
}
551
552
void
553
EngineController::setMuted( bool mute ) //SLOT
554
{
555
    m_audio->setMuted( mute ); // toggle mute
556
557
    AmarokConfig::setMuteState( mute );
558
    muteStateChangedNotify( mute );
559
560
    emit muteStateChanged( mute );
561
}
562
563
void
564
EngineController::toggleMute() //SLOT
565
{
566
    setMuted( !isMuted() );
567
}
568
569
Meta::TrackPtr
570
EngineController::currentTrack() const
571
{
572
    return m_currentTrack;
573
}
574
575
int
576
EngineController::trackLength() const
577
{
578
    const qint64 phononLength = m_media->totalTime(); //may return -1
579
580
    if( m_currentTrack && m_currentTrack->length() > 0 )   //When starting a last.fm stream, Phonon still shows the old track's length--trust Meta::Track over Phonon
581
        return m_currentTrack->length();
582
    else
583
        return static_cast<int>( phononLength / 1000 );
584
}
585
586
void
587
EngineController::setNextTrack( Meta::TrackPtr track )
588
{
589
    DEBUG_BLOCK
590
591
    debug() << "goin to lock mutex";
592
    QMutexLocker locker( &m_mutex );
593
    debug() << "locked!";
594
595
    if( !track )
596
        return;
597
598
    track->prepareToPlay();
599
    if( track->playableUrl().isEmpty() )
600
        return;
601
602
    if( m_media->state() == Phonon::PlayingState ||
603
        m_media->state() == Phonon::BufferingState )
604
    {
605
        m_media->clearQueue();
606
        if( track->playableUrl().isLocalFile() )
607
            m_media->enqueue( track->playableUrl() );
608
        m_nextTrack = track;
609
        m_nextUrl = track->playableUrl();
610
    }
611
    else
612
    {
613
        play( track );
614
    }
615
}
616
617
618
bool
619
EngineController::getAudioCDContents(const QString &device, KUrl::List &urls)
620
{
621
    Q_UNUSED( device ); Q_UNUSED( urls );
622
//since Phonon doesn't know anything about CD listings, there's actually no reason for this functionality to be here
623
//kept to keep things compiling, probably should be in its own class.
624
    return false;
625
}
626
627
bool
628
EngineController::isStream()
629
{
630
    DEBUG_BLOCK
631
632
    if( m_media )
633
        return m_media->currentSource().type() == Phonon::MediaSource::Stream;
634
    return false;
635
}
636
637
int
638
EngineController::trackPosition() const
639
{
640
//NOTE: there was a bunch of last.fm logic removed from here
641
//pretty sure it's irrelevant, if not, look back to mid-March 2008
642
    return static_cast<int>( m_media->currentTime() / 1000 );
643
}
644
645
int
646
EngineController::trackPositionMs() const
647
{
648
    return m_media->currentTime();
649
}
650
651
//////////////////////////////////////////////////////////////////////////////////////////
652
// PRIVATE SLOTS
653
//////////////////////////////////////////////////////////////////////////////////////////
654
655
void
656
EngineController::slotTick( qint64 position )
657
{
658
   
659
660
    if ( m_boundedPlayback )
661
    {
662
        trackPositionChangedNotify( static_cast<long>( position - m_boundedPlayback->startPosition() ), false );
663
        
664
        //dont go beyound the stop point
665
        if ( position >= m_boundedPlayback->endPosition() )
666
        {
667
            slotAboutToFinish();
668
        }
669
    }
670
    else
671
    {
672
        trackPositionChangedNotify( static_cast<long>( position ), false ); //it expects milliseconds
673
    }
674
}
675
676
677
void
678
EngineController::slotAboutToFinish()
679
{
680
    DEBUG_BLOCK
681
    debug() << "Track finished completely, updating statistics";
682
    if( m_currentTrack ) // not sure why this should not be the case, but sometimes happens. don't crash.
683
        m_currentTrack->finishedPlaying( 1.0 ); // If we reach aboutToFinish, the track is done as far as we are concerned.
684
    if( m_multiPlayback )
685
    {
686
        DEBUG_LINE_INFO
687
        m_mutex.lock();
688
        m_playWhenFetched = false;
689
        m_mutex.unlock();
690
        m_multiPlayback->fetchNext();
691
        debug() << "The queue has: " << m_media->queue().size() << " tracks in it";
692
    }
693
    else if( m_multiSource )
694
    {
695
        debug() << "source finished, lets get the next one";
696
        KUrl nextSource = m_multiSource->next();
697
698
        if ( !nextSource.isEmpty() )
699
        { //more sources
700
            m_mutex.lock();
701
            m_playWhenFetched = false;
702
            m_mutex.unlock();
703
            debug() << "playing next source: " << nextSource;
704
            slotPlayableUrlFetched( nextSource );
705
        }
706
        else if( m_media->queue().isEmpty() )
707
        { //go to next track
708
            The::playlistActions()->requestNextTrack();
709
            debug() << "no more sources, skip to next track";
710
        }
711
    }
712
    else if ( m_boundedPlayback )
713
    {
714
        debug() << "finished a track that consistst of part of another track, go to next track even if this url is technically not done yet";
715
716
        //stop this track, now, as the source track might go on and on, and
717
        //there might not be any more tracks in the playlist...
718
        stop( true );
719
        The::playlistActions()->requestNextTrack();
720
        slotQueueEnded();
721
    }
722
    else if ( m_currentTrack && m_currentTrack->playableUrl().url().startsWith( "audiocd:/" ) )
723
    {
724
        debug() << "finished a cd track, dont care if queue is not empty, just get new track...";
725
        //m_media->stop();
726
        The::playlistActions()->requestNextTrack();
727
        slotQueueEnded();
728
    }
729
    else if( m_media->queue().isEmpty() )
730
        The::playlistActions()->requestNextTrack();
731
}
732
733
void
734
EngineController::slotQueueEnded()
735
{
736
    DEBUG_BLOCK
737
738
    if( m_currentTrack && !m_multiPlayback && !m_multiSource )
739
    {
740
        m_currentTrack->finishedPlaying( 1.0 );
741
        emit trackFinished();
742
        playbackEnded( trackPosition(), m_currentTrack->length(), EngineObserver::EndedStopped );
743
        m_currentTrack = 0;
744
    }
745
746
    m_mutex.lock(); // in case setNextTrack is being handled right now.
747
748
    // Non-local urls are not enqueued so we must play them explicitly.
749
    if( m_nextTrack )
750
    {
751
        DEBUG_LINE_INFO
752
        play( m_nextTrack );
753
    }
754
    else if( !m_nextUrl.isEmpty() )
755
    {
756
        DEBUG_LINE_INFO
757
        playUrl( m_nextUrl, 0 );
758
    }
759
    else
760
        // possibly we are waiting for a fetch
761
        m_playWhenFetched = true;
762
763
    m_mutex.unlock();
764
}
765
766
static const qreal log10over20 = 0.1151292546497022842; // ln(10) / 20
767
768
void
769
EngineController::slotNewTrackPlaying( const Phonon::MediaSource &source )
770
{
771
    DEBUG_BLOCK
772
    Q_UNUSED( source );
773
774
    // the new track was taken from the queue, so clear these fields
775
    if( m_nextTrack )
776
    {
777
        m_currentTrack = m_nextTrack;
778
        m_nextTrack.clear();
779
    }
780
781
    if( !m_nextUrl.isEmpty() )
782
        m_nextUrl.clear();
783
784
    if ( m_currentTrack && AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off )
785
    {
786
        if( !m_preamp ) // replaygain was just turned on, and amarok was started with it off
787
        {
788
            m_preamp = new Phonon::VolumeFaderEffect( this );
789
            m_path.insertEffect( m_preamp );
790
        }
791
        
792
        Meta::Track::ReplayGainMode mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track)
793
                                         ? Meta::Track::TrackReplayGain
794
                                         : Meta::Track::AlbumReplayGain;
795
        // gain is usually negative (but may be positive)
796
        qreal gain = m_currentTrack->replayGain( mode );
797
        // peak is usually positive and smaller than gain (but may be negative)
798
        qreal peak = m_currentTrack->replayPeakGain( mode );
799
        if ( gain + peak > 0.0 )
800
        {
801
            debug() << "Gain of" << gain << "would clip at absolute peak of" << gain + peak;
802
            gain -= gain + peak;
803
        }
804
        debug() << "Using gain of" << gain << "with relative peak of" << peak;
805
        // we calculate the volume change ourselves, because m_preamp->setVolumeDecibel is
806
        // a little confused about minus signs
807
        m_preamp->setVolume( exp( gain * log10over20 ) );
808
    }
809
    else if( m_preamp )
810
        m_preamp->setVolumeDecibel( 0.0 );
811
812
    // state never changes if tracks are queued, but we need this to update the caption
813
    stateChangedNotify( m_media->state(), m_media->state() );
814
815
    emit trackChanged( m_currentTrack );
816
    newTrackPlaying();
817
}
818
819
void
820
EngineController::slotStateChanged( Phonon::State newState, Phonon::State oldState ) //SLOT
821
{
822
    DEBUG_BLOCK
823
824
    // Sanity checks:
825
    if( newState == oldState )
826
        return;
827
828
    // The following check is an attempt to fix http://bugs.kde.org/show_bug.cgi?id=180339
829
    // ("amarok stops playing tracks") and other issues with Phonon.
830
    // The theory:
831
    // It has been observed that Phonon will sometimes emit a stateChanged() with
832
    // _both_ oldState and newState == 0, which makes little sense. After that it goes
833
    // berserk, until you restart Amarok. 
834
    // Now we try to detect this weird state, and then try to destroy and recreate all
835
    // Phonon objects, in the hope of fixing the situation. Fingers crossed.
836
    if( newState == Phonon::LoadingState && oldState == Phonon::LoadingState )
837
    {
838
        warning() << "Trying to re-initialize Phonon. Fingers crossed.";
839
        initializePhonon();
840
        newState = Phonon::ErrorState;  // Indicate error
841
    }
842
843
    if( newState == Phonon::ErrorState )  // If media is borked, skip to next track
844
    {
845
        warning() << "Phonon failed to play this URL. Error: " << m_media->errorString();
846
        if( m_multiPlayback )
847
        {
848
            DEBUG_LINE_INFO
849
            m_mutex.lock();
850
            m_playWhenFetched = false;
851
            m_mutex.unlock();
852
            m_multiPlayback->fetchNext();
853
            debug() << "The queue has: " << m_media->queue().size() << " tracks in it";
854
        }
855
        else if( m_multiSource )
856
        {
857
            debug() << "source error, lets get the next one";
858
            KUrl nextSource = m_multiSource->next();
859
860
            if ( !nextSource.isEmpty() )
861
            { //more sources
862
                m_mutex.lock();
863
                m_playWhenFetched = false;
864
                m_mutex.unlock();
865
                debug() << "playing next source: " << nextSource;
866
                slotPlayableUrlFetched( nextSource );
867
            }
868
            else if( m_media->queue().isEmpty() )
869
                The::playlistActions()->requestNextTrack();
870
        }
871
872
        else if( m_media->queue().isEmpty() )
873
            The::playlistActions()->requestNextTrack();
874
    }
875
876
    stateChangedNotify( newState, oldState );
877
878
    if( newState == Phonon::PlayingState )
879
        emit trackPlayPause( Playing );
880
    else if( newState == Phonon::PausedState )
881
        emit trackPlayPause( Paused );
882
}
883
884
void
885
EngineController::slotPlayableUrlFetched( const KUrl &url )
886
{
887
    DEBUG_BLOCK
888
    debug() << "Fetched url: " << url;
889
    if( url.isEmpty() )
890
    {
891
        DEBUG_LINE_INFO
892
        The::playlistActions()->requestNextTrack();
893
        return;
894
    }
895
896
    if( !m_playWhenFetched )
897
    {
898
        DEBUG_LINE_INFO
899
        m_mutex.lock();
900
        m_media->clearQueue();
901
        if( url.isLocalFile() )
902
            m_media->enqueue( url );
903
        m_nextTrack.clear();
904
        m_nextUrl = url;
905
        debug() << "The next url we're playing is: " << m_nextUrl;
906
        // reset this flag each time
907
        m_playWhenFetched = true;
908
        m_mutex.unlock();
909
    }
910
    else
911
    {
912
        DEBUG_LINE_INFO
913
        m_mutex.lock();
914
        playUrl( url, 0 );
915
        m_mutex.unlock();
916
    }
917
}
918
919
void
920
EngineController::slotTrackLengthChanged( qint64 milliseconds )
921
{
922
    DEBUG_BLOCK
923
924
    //Phonon reports bad info on streams, so call trackLength() to verify
925
    milliseconds = trackLength() * 1000;
926
927
    if( milliseconds != 0 ) //don't notify for 0 seconds, it's probably just a stream
928
        trackLengthChangedNotify( static_cast<long>( milliseconds ) / 1000 );
929
}
930
931
void
932
EngineController::slotMetaDataChanged()
933
{
934
    DEBUG_BLOCK
935
936
    QHash<qint64, QString> meta;
937
938
    meta.insert( Meta::valUrl, m_media->currentSource().url().toString() );
939
940
    QStringList artist = m_media->metaData( "ARTIST" );
941
    debug() << "Artist     : " << artist;
942
    if( !artist.isEmpty() )
943
        meta.insert( Meta::valArtist, artist.first() );
944
945
    QStringList album = m_media->metaData( "ALBUM" );
946
    debug() << "Album      : " << album;
947
    if( !album.isEmpty() )
948
        meta.insert( Meta::valAlbum, album.first() );
949
950
    QStringList title = m_media->metaData( "TITLE" );
951
    debug() << "Title      : " << title;
952
    if( !title.isEmpty() )
953
        meta.insert( Meta::valTitle, title.first() );
954
955
    QStringList genre = m_media->metaData( "GENRE" );
956
    debug() << "Genre      : " << genre;
957
    if( !genre.isEmpty() )
958
        meta.insert( Meta::valGenre, genre.first() );
959
960
    QStringList tracknum = m_media->metaData( "TRACKNUMBER" );
961
    debug() << "Tracknumber: " << tracknum;
962
    if( !tracknum.isEmpty() )
963
        meta.insert( Meta::valTrackNr, tracknum.first() );
964
965
    QStringList length = m_media->metaData( "LENGTH" );
966
    debug() << "Length     : " << length;
967
    if( !length.isEmpty() )
968
        meta.insert( Meta::valLength, length.first() );
969
970
    bool trackChanged = false;
971
    if( m_lastTrack != m_currentTrack )
972
    {
973
        trackChanged = true;
974
        m_lastTrack = m_currentTrack;
975
    }
976
    debug() << "Track changed: " << trackChanged;
977
    newMetaDataNotify( meta, trackChanged );
978
}
979
980
void
981
EngineController::slotStopFadeout() //SLOT
982
{
983
    DEBUG_BLOCK
984
985
    // Make sure the timer won't call this method again
986
    m_fadeoutTimer->stop();
987
988
    if ( m_fader ) {
989
        m_fader->deleteLater();
990
        m_media->stop();
991
    }
992
}
993
994
void EngineController::slotTitleChanged( int titleNumber )
995
{
996
    DEBUG_BLOCK
997
    Q_UNUSED( titleNumber );
998
    slotAboutToFinish();
999
}
1000
1001
#include "EngineController.moc"