1
/****************************************************************************************
2
 * Copyright (c) 2004 Frederik Holljen <fh@ez.no>                                       *
3
 * Copyright (c) 2004,2005 Max Howell <max.howell@methylblue.com>                       *
4
 * Copyright (c) 2004-2010 Mark Kretschmann <kretschmann@kde.org>                       *
5
 * Copyright (c) 2006,2008 Ian Monroe <ian@monroe.nu>                                   *
6
 * Copyright (c) 2008 Jason A. Donenfeld <Jason@zx2c4.com>                              *
7
 * Copyright (c) 2009 Nikolaj Hald Nielsen <nhn@kde.org>                                *
8
 * Copyright (c) 2009 Artur Szymiec <artur.szymiec@gmail.com>                           *
9
 *                                                                                      *
10
 * This program is free software; you can redistribute it and/or modify it under        *
11
 * the terms of the GNU General Public License as published by the Free Software        *
12
 * Foundation; either version 2 of the License, or (at your option) any later           *
13
 * version.                                                                             *
14
 *                                                                                      *
15
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
16
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
17
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
18
 *                                                                                      *
19
 * You should have received a copy of the GNU General Public License along with         *
20
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
21
 ****************************************************************************************/
22
23
#define DEBUG_PREFIX "EngineController"
24
25
#include "EngineController.h"
26
27
#include "core/support/Amarok.h"
28
#include "amarokconfig.h"
29
#include "core-impl/collections/support/CollectionManager.h"
30
#include "core/support/Components.h"
31
#include "core/interfaces/Logger.h"
32
#include "core/support/Debug.h"
33
#include "MainWindow.h"
34
#include "MediaDeviceMonitor.h"
35
#include "core/meta/Meta.h"
36
#include "core/meta/support/MetaConstants.h"
37
#include "core/meta/support/MetaUtility.h"
38
#include "core/capabilities/MultiPlayableCapability.h"
39
#include "core/capabilities/MultiSourceCapability.h"
40
#include "core/capabilities/SourceInfoCapability.h"
41
#include "playlist/PlaylistActions.h"
42
#include "core-impl/playlists/types/file/PlaylistFileSupport.h"
43
#include "core/plugins/PluginManager.h"
44
#include "core/playlists/PlaylistFormat.h"
45
46
#include <KFileItem>
47
#include <KIO/Job>
48
#include <KMessageBox>
49
#include <KRun>
50
51
#include <Phonon/AudioOutput>
52
#include <Phonon/BackendCapabilities>
53
#include <Phonon/MediaObject>
54
#include <Phonon/VolumeFaderEffect>
55
56
#include <QTextDocument>
57
#include <QTimer>
58
59
#include <cmath>
60
61
namespace The {
62
    EngineController* engineController() { return EngineController::instance(); }
63
}
64
65
EngineController*
66
EngineController::instance()
67
{
68
    return Amarok::Components::engineController();
69
}
70
71
void
72
EngineController::destroy()
73
{
74
    //nothing to do?
75
}
76
77
EngineController::EngineController()
78
    : m_playWhenFetched( true )
79
    , m_fadeoutTimer( new QTimer( this ) )
80
    , m_volume( 0 )
81
    , m_currentIsAudioCd( false )
82
    , m_ignoreVolumeChangeAction ( false )
83
    , m_ignoreVolumeChangeObserve ( false )
84
    , m_tickInterval( 0 )
85
    , m_lastTickPosition( -1 )
86
    , m_lastTickCount( 0 )
87
{
88
    DEBUG_BLOCK
89
90
    m_fadeoutTimer->setSingleShot( true );
91
92
    connect( m_fadeoutTimer, SIGNAL( timeout() ), SLOT( slotStopFadeout() ) );
93
}
94
95
EngineController::~EngineController()
96
{
97
    DEBUG_BLOCK //we like to know when singletons are destroyed
98
99
    // don't do any of the after-processing that normally happens when
100
    // the media is stopped - that's what endSession() is for
101
    m_media->blockSignals(true);
102
    m_media->stop();
103
104
    delete m_media;
105
    delete m_audio;
106
}
107
108
void
109
EngineController::createFadeoutEffect()
110
{
111
    m_fader = new Phonon::VolumeFaderEffect( this );
112
    m_path.insertEffect( m_fader );
113
    m_fader->setFadeCurve( Phonon::VolumeFaderEffect::Fade9Decibel );
114
}
115
116
void
117
EngineController::initializePhonon()
118
{
119
    DEBUG_BLOCK
120
121
    m_path.disconnect();
122
    delete m_media;
123
    delete m_controller;
124
    delete m_audio;
125
    delete m_preamp;
126
    delete m_equalizer;
127
    delete m_fader;
128
129
    PERF_LOG( "EngineController: loading phonon objects" )
130
    m_media = new Phonon::MediaObject( this );
131
    m_audio = new Phonon::AudioOutput( Phonon::MusicCategory, this );
132
133
    m_path = Phonon::createPath( m_media, m_audio );
134
135
    m_controller = new Phonon::MediaController( m_media );
136
137
    //Add an equalizer effect if available
138
    QList<Phonon::EffectDescription> mEffectDescriptions = Phonon::BackendCapabilities::availableAudioEffects();
139
    foreach ( const Phonon::EffectDescription &mDescr, mEffectDescriptions ) {
140
        if ( mDescr.name() == QLatin1String( "KEqualizer" ) ) {
141
            m_equalizer = new Phonon::Effect( mDescr, this );
142
            eqUpdate();
143
            }
144
    }
145
146
    // HACK we turn off replaygain manually on OSX, until the phonon coreaudio backend is fixed.
147
    // as the default is specified in the .cfg file, we can't just tell it to be a different default on OSX
148
#ifdef Q_WS_MAC
149
    AmarokConfig::setReplayGainMode( AmarokConfig::EnumReplayGainMode::Off );
150
    AmarokConfig::setFadeout( false );
151
#endif
152
153
    // only create pre-amp if we have replaygain on, VolumeFaderEffect can cause phonon issues
154
    if( AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off )
155
    {
156
        m_preamp = new Phonon::VolumeFaderEffect( this );
157
        m_path.insertEffect( m_preamp );
158
    }
159
160
    // only create fader if we have fadeout on, VolumeFaderEffect can cause phonon issues
161
    if( AmarokConfig::fadeout() && AmarokConfig::fadeoutLength() )
162
    {
163
        createFadeoutEffect();
164
    }
165
166
    m_media->setTickInterval( 100 );
167
    m_tickInterval = m_media->tickInterval();
168
    debug() << "Tick Interval (actual): " << m_tickInterval;
169
    PERF_LOG( "EngineController: loaded phonon objects" )
170
171
    // Get the next track when there is 2 seconds left on the current one.
172
    m_media->setPrefinishMark( 2000 );
173
174
    connect( m_media, SIGNAL( finished() ), SLOT( slotQueueEnded() ) );
175
    connect( m_media, SIGNAL( aboutToFinish() ), SLOT( slotAboutToFinish() ) );
176
    connect( m_media, SIGNAL( metaDataChanged() ), SLOT( slotMetaDataChanged() ) );
177
    connect( m_media, SIGNAL( stateChanged( Phonon::State, Phonon::State ) ), SLOT( slotStateChanged( Phonon::State, Phonon::State ) ) );
178
    connect( m_media, SIGNAL( tick( qint64 ) ), SLOT( slotTick( qint64 ) ) );
179
    connect( m_media, SIGNAL( totalTimeChanged( qint64 ) ), SLOT( slotTrackLengthChanged( qint64 ) ) );
180
    connect( m_media, SIGNAL( currentSourceChanged( const Phonon::MediaSource & ) ), SLOT( slotNewTrackPlaying( const Phonon::MediaSource & ) ) );
181
182
    connect( m_audio, SIGNAL( volumeChanged( qreal ) ), SLOT( slotVolumeChanged( qreal ) ) );
183
    connect( m_audio, SIGNAL( mutedChanged( bool ) ), SLOT( slotMutedChanged( bool ) ) );
184
185
    connect( m_controller, SIGNAL( titleChanged( int ) ), SLOT( slotTitleChanged( int ) ) );
186
187
    // Read the volume from phonon
188
    m_volume = qBound<qreal>( 0, qRound(m_audio->volume()*100), 100 );
189
190
    if( AmarokConfig::trackDelayLength() > -1 )
191
        m_media->setTransitionTime( AmarokConfig::trackDelayLength() ); // Also Handles gapless.
192
    else if( AmarokConfig::crossfadeLength() > 0 )  // TODO: Handle the possible options on when to crossfade.. the values are not documented anywhere however
193
        m_media->setTransitionTime( -AmarokConfig::crossfadeLength() );
194
}
195
196
197
//////////////////////////////////////////////////////////////////////////////////////////
198
// PUBLIC
199
//////////////////////////////////////////////////////////////////////////////////////////
200
201
bool
202
EngineController::canDecode( const KUrl &url ) //static
203
{
204
   //NOTE this function must be thread-safe
205
206
    // We can't use playlists in the engine
207
    if( Playlists::isPlaylist( url ) )
208
        return false;
209
210
    KFileItem item( KFileItem::Unknown, KFileItem::Unknown, url );
211
    // If file has 0 bytes, ignore it and return false
212
    if( !item.size() )
213
        return false;
214
215
    // We can't play directories, regardless of what the engine says.
216
    if( item.isDir() )
217
        return false;
218
219
    // Accept non-local files, since we can't test them for validity at this point
220
    if( !item.isLocalFile() )
221
        return true;
222
223
    // Filter the available mime types to only include audio and video, as amarok does not intend to play photos
224
    static QStringList mimeTable = supportedMimeTypes();
225
226
    const KMimeType::Ptr mimeType = item.mimeTypePtr();
227
    
228
    bool valid = false;
229
    foreach( const QString &type, mimeTable )
230
    {
231
        if( mimeType->is( type ) )
232
        {
233
            valid = true;
234
            break;
235
        }
236
    }
237
238
    return valid;
239
}
240
241
QStringList
242
EngineController::supportedMimeTypes()
243
{
244
    //NOTE this function must be thread-safe
245
    // Filter the available mime types to only include audio and video, as amarok does not intend to play photos
246
    static QStringList mimeTable = Phonon::BackendCapabilities::availableMimeTypes().filter( "audio/", Qt::CaseInsensitive ) +
247
                                   Phonon::BackendCapabilities::availableMimeTypes().filter( "video/", Qt::CaseInsensitive );
248
249
    // Add whitelist hacks
250
    mimeTable << "audio/x-m4b"; // MP4 Audio Books have a different extension that KFileItem/Phonon don't grok
251
252
    // We special case this, as otherwise the users would hate us
253
    if( ( !mimeTable.contains( "audio/mp3" ) && !mimeTable.contains( "audio/x-mp3" ) ) && !installDistroCodec() )
254
    {
255
        Amarok::Components::logger()->longMessage(
256
                i18n( "<p>Phonon claims it <b>cannot</b> play MP3 files. You may want to examine "
257
                      "the installation of the backend that phonon uses.</p>"
258
                      "<p>You may find useful information in the <i>FAQ</i> section of the <i>Amarok Handbook</i>.</p>" ), Amarok::Logger::Error );
259
        mimeTable << "audio/mp3" << "audio/x-mp3";
260
    }
261
262
    return mimeTable;
263
}
264
265
bool
266
EngineController::installDistroCodec()
267
{
268
    KService::List services = KServiceTypeTrader::self()->query( "Amarok/CodecInstall"
269
        , QString( "[X-KDE-Amarok-codec] == 'mp3' and [X-KDE-Amarok-engine] == 'phonon-%1'").arg( "xine" ) );
270
    //todo - figure out how to query Phonon for the current backend loaded
271
    if( !services.isEmpty() )
272
    {
273
        KService::Ptr service = services.first(); //list is not empty
274
        QString installScript = service->exec();
275
        if( !installScript.isNull() ) //just a sanity check
276
        {
277
            KGuiItem installButton( i18n( "Install MP3 Support" ) );
278
            if(KMessageBox::questionYesNo( The::mainWindow()
279
            , i18n("Amarok currently cannot play MP3 files. Do you want to install support for MP3?")
280
            , i18n( "No MP3 Support" )
281
            , installButton
282
            , KStandardGuiItem::no()
283
            , "codecInstallWarning" ) == KMessageBox::Yes )
284
            {
285
                    KRun::runCommand(installScript, 0);
286
                    return true;
287
            }
288
        }
289
    }
290
291
    return false;
292
}
293
294
void
295
EngineController::restoreSession()
296
{
297
    //here we restore the session
298
    //however, do note, this is always done, KDE session management is not involved
299
300
    if( AmarokConfig::resumePlayback() )
301
    {
302
        const KUrl url = AmarokConfig::resumeTrack();
303
304
        // Only resume local files, because resuming remote protocols can have weird side effects.
305
        // See: http://bugs.kde.org/show_bug.cgi?id=172897
306
        if( url.isLocalFile() )
307
        {
308
            Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
309
            play( track, AmarokConfig::resumeTime() );
310
        }
311
    }
312
}
313
314
void
315
EngineController::endSession()
316
{
317
    //only update song stats, when we're not going to resume it
318
    if ( !AmarokConfig::resumePlayback() && m_currentTrack )
319
    {
320
        playbackEnded( trackPositionMs(), m_currentTrack->length(), Engine::EngineObserver::EndedQuit );
321
        trackChangedNotify( Meta::TrackPtr( 0 ) );
322
    }
323
}
324
325
//////////////////////////////////////////////////////////////////////////////////////////
326
// PUBLIC SLOTS
327
//////////////////////////////////////////////////////////////////////////////////////////
328
329
void
330
EngineController::play() //SLOT
331
{
332
    DEBUG_BLOCK
333
334
    // FIXME: what should we do in buffering state?
335
    if( state() == Phonon::PlayingState )
336
        return;
337
338
    resetFadeout();
339
340
    if( state() == Phonon::PausedState )
341
    {
342
        if( m_currentTrack && m_currentTrack->type() == "stream" ) // SHOUTcast does not support resuming, so we restart it.
343
        {
344
            debug() << "This is a stream that cannot be resumed after pausing. Restarting instead.";
345
            play( m_currentTrack );
346
            return;
347
        }
348
        else
349
        {
350
            m_media->play();
351
            return;
352
        }
353
    }
354
355
    The::playlistActions()->play();
356
}
357
358
void
359
EngineController::play( const Meta::TrackPtr& track, uint offset )
360
{
361
    DEBUG_BLOCK
362
363
    if( !track ) // Guard
364
        return;
365
366
    m_currentTrack = track;
367
    m_currentIsAudioCd = false;
368
    delete m_boundedPlayback;
369
    delete m_multiPlayback;
370
    delete m_multiSource;
371
    m_boundedPlayback = m_currentTrack->create<Capabilities::BoundedPlaybackCapability>();
372
    m_multiPlayback = m_currentTrack->create<Capabilities::MultiPlayableCapability>();
373
    m_multiSource  = m_currentTrack->create<Capabilities::MultiSourceCapability>();
374
375
376
    m_nextTrack.clear();
377
    m_nextUrl.clear();
378
    m_media->clearQueue();
379
380
    m_currentTrack->prepareToPlay();
381
382
    if( m_multiPlayback )
383
    {
384
        m_media->stop();
385
        connect( m_multiPlayback, SIGNAL( playableUrlFetched( const KUrl & ) ), this, SLOT( slotPlayableUrlFetched( const KUrl & ) ) );
386
        m_multiPlayback->fetchFirst();
387
    }
388
    else if ( m_multiSource )
389
    {
390
        m_media->stop();
391
        debug() << "Got a MultiSource Track with " <<  m_multiSource->sources().count() << " sources";
392
        connect( m_multiSource, SIGNAL( urlChanged( const KUrl & ) ), this, SLOT( slotPlayableUrlFetched( const KUrl & ) ) );
393
        playUrl( m_currentTrack->playableUrl(), 0 );
394
    }
395
    else if ( m_boundedPlayback )
396
    {
397
        debug() << "Starting bounded playback of url " << m_currentTrack->playableUrl() << " at position " << m_boundedPlayback->startPosition();
398
        playUrl( m_currentTrack->playableUrl(), m_boundedPlayback->startPosition() );
399
    }
400
    else
401
    {
402
        debug() << "Just a normal, boring track... :-P";
403
        playUrl( m_currentTrack->playableUrl(), offset );
404
    }
405
}
406
407
void
408
EngineController::replay() // slot
409
{
410
    DEBUG_BLOCK
411
412
    seek( 0 );
413
}
414
415
void
416
EngineController::playUrl( const KUrl &url, uint offset )
417
{
418
    DEBUG_BLOCK
419
420
    m_media->stop();
421
    resetFadeout();
422
423
    debug() << "URL: " << url.url();
424
    debug() << "offset: " << offset;
425
426
    if ( url.url().startsWith( "audiocd:/" ) )
427
    {
428
429
        m_currentIsAudioCd = true;
430
        //disconnect this signal for now or it will cause a loop that will cause a mutex lockup
431
        disconnect( m_controller, SIGNAL( titleChanged( int ) ), this, SLOT( slotTitleChanged( int ) ) );
432
433
        debug() << "play track from cd";
434
        QString trackNumberString = url.url();
435
        trackNumberString = trackNumberString.remove( "audiocd:/" );
436
437
        QStringList parts = trackNumberString.split( '/' );
438
439
        if ( parts.count() != 2 )
440
            return;
441
442
        QString discId = parts.at( 0 );
443
444
        //we really only want to play it if it is the disc that is currently present.
445
        //In the case of CDs for which we don't have any id, any "unknown" CDs will
446
        //be considered equal.
447
448
449
        //FIXME:
450
        //if ( MediaDeviceMonitor::instance()->currentCdId() != discId )
451
        //    return;
452
453
454
        int trackNumber = parts.at( 1 ).toInt();
455
456
        debug() << "3.2.1...";
457
458
        Phonon::MediaSource::Type type = m_media->currentSource().type();
459
        if( type != Phonon::MediaSource::Disc )
460
        {
461
            m_media->clear();
462
            m_media->setCurrentSource( Phonon::Cd );
463
        }
464
465
        debug() << "boom?";
466
        m_controller->setCurrentTitle( trackNumber );
467
        debug() << "no boom?";
468
469
        if( type == Phonon::MediaSource::Disc )
470
        {
471
            // The track has changed but the slot will not be called,
472
            // because it's still the same media source, which means
473
            // we need to do it explicitly.
474
            slotNewTrackPlaying( Phonon::Cd );
475
        }
476
477
        //reconnect it
478
        connect( m_controller, SIGNAL( titleChanged( int ) ), SLOT( slotTitleChanged( int ) ) );
479
480
    }
481
    else
482
    {
483
        if ( url.toLocalFile().isEmpty() )
484
        {
485
            m_media->setCurrentSource( url );
486
        }
487
        else
488
        {
489
            m_media->setCurrentSource( url.toLocalFile() );
490
        }
491
    }
492
493
    m_nextTrack.clear();
494
    m_nextUrl.clear();
495
    m_media->clearQueue();
496
497
    if( offset )
498
    {
499
        debug() << "seeking to " << offset;
500
        m_media->pause();
501
        m_media->seek( offset );
502
    }
503
    m_media->play();
504
505
    debug() << "track pos after play: " << trackPositionMs();
506
507
508
}
509
510
void
511
EngineController::pause() //SLOT
512
{
513
    m_media->pause();
514
}
515
516
void
517
EngineController::stop( bool forceInstant ) //SLOT
518
{
519
    DEBUG_BLOCK
520
521
    m_currentIsAudioCd = false;
522
    // need to get a new instance of multi if played again
523
    delete m_multiPlayback;
524
    delete m_multiSource;
525
526
    m_mutex.lock();
527
    m_nextTrack.clear();
528
    m_nextUrl.clear();
529
    m_media->clearQueue();
530
    m_mutex.unlock();
531
532
    //let Amarok know that the previous track is no longer playing
533
    if( m_currentTrack )
534
    {
535
        debug() << "m_currentTrack != 0";
536
        const qint64 pos = trackPositionMs();
537
        const qint64 length = m_currentTrack->length();
538
        m_currentTrack->finishedPlaying( double(pos)/double(length) );
539
        playbackEnded( pos, length, Engine::EngineObserver::EndedStopped );
540
        trackChangedNotify( Meta::TrackPtr( 0 ) );
541
    }
542
543
    // Stop instantly if fadeout is already running, or the media is not playing
544
    if( m_fadeoutTimer->isActive() || m_media->state() != Phonon::PlayingState )
545
    {
546
        forceInstant = true;
547
    }
548
549
    if( AmarokConfig::fadeout() && AmarokConfig::fadeoutLength() && !forceInstant )
550
    {
551
        // WARNING: this can cause a gap in playback in GStreamer
552
        if (! m_fader )
553
            createFadeoutEffect();
554
555
        m_fader->fadeOut( AmarokConfig::fadeoutLength() );
556
557
        m_fadeoutTimer->start( AmarokConfig::fadeoutLength() + 1000 ); //add 1s for good measure, otherwise seems to cut off early (buffering..)
558
559
        stateChangedNotify( Phonon::StoppedState, m_media->state() ); //immediately disable Stop action
560
    }
561
    else
562
    {
563
        m_media->stop();
564
        m_media->setCurrentSource( Phonon::MediaSource() );
565
    }
566
567
    m_currentTrack = 0;
568
}
569
570
bool
571
EngineController::isPaused() const
572
{
573
    return state() == Phonon::PausedState;
574
}
575
576
void
577
EngineController::playPause() //SLOT
578
{
579
    DEBUG_BLOCK
580
581
    //this is used by the TrayIcon, PlayPauseAction and DBus
582
    debug() << "PlayPause: EngineController state" << state();
583
584
    switch ( state() )
585
    {
586
        case Phonon::PausedState:
587
        case Phonon::StoppedState:
588
589
        case Phonon::LoadingState:
590
            play();
591
            break;
592
593
        default:
594
            pause();
595
            break;
596
    }
597
}
598
599
void
600
EngineController::seek( int ms ) //SLOT
601
{
602
    DEBUG_BLOCK
603
604
    if( m_media->isSeekable() )
605
    {
606
607
        debug() << "seek to: " << ms;
608
        int seekTo;
609
610
        if ( m_boundedPlayback )
611
        {
612
            seekTo = m_boundedPlayback->startPosition() + ms;
613
            if( seekTo < m_boundedPlayback->startPosition() )
614
                seekTo = m_boundedPlayback->startPosition();
615
            else if( seekTo > m_boundedPlayback->startPosition() + trackLength() )
616
                seekTo = m_boundedPlayback->startPosition() + trackLength();
617
        }
618
        else
619
            seekTo = ms;
620
621
        m_media->seek( static_cast<qint64>( seekTo ) );
622
        // FIXME: is this correct for bounded playback?
623
        trackPositionChangedNotify( seekTo, true ); /* User seek */
624
    }
625
    else
626
        debug() << "Stream is not seekable.";
627
}
628
629
630
void
631
EngineController::seekRelative( int ms ) //SLOT
632
{
633
    qint64 newPos = m_media->currentTime() + ms;
634
    seek( newPos <= 0 ? 0 : newPos );
635
}
636
637
void
638
EngineController::seekForward( int ms )
639
{
640
    seekRelative( ms );
641
}
642
643
void
644
EngineController::seekBackward( int ms )
645
{
646
    seekRelative( -ms );
647
}
648
649
int
650
EngineController::increaseVolume( int ticks ) //SLOT
651
{
652
    return setVolume( volume() + ticks );
653
}
654
655
int
656
EngineController::decreaseVolume( int ticks ) //SLOT
657
{
658
    return setVolume( volume() - ticks );
659
}
660
661
int
662
EngineController::setVolume( int percent ) //SLOT
663
{
664
    percent = qBound<qreal>( 0, percent, 100 );
665
    m_volume = percent;
666
667
    const qreal volume =  percent / 100.0;
668
    if ( !m_ignoreVolumeChangeAction && m_audio->volume() != volume )
669
    {
670
        m_ignoreVolumeChangeObserve = true;
671
        m_audio->setVolume( volume );
672
673
        AmarokConfig::setMasterVolume( percent );
674
        volumeChangedNotify( percent );
675
    }
676
    m_ignoreVolumeChangeAction = false;
677
678
    return percent;
679
}
680
681
int
682
EngineController::volume() const
683
{
684
    return m_volume;
685
}
686
687
bool
688
EngineController::isMuted() const
689
{
690
    return m_audio->isMuted();
691
}
692
693
void
694
EngineController::setMuted( bool mute ) //SLOT
695
{
696
    m_audio->setMuted( mute ); // toggle mute
697
698
    AmarokConfig::setMuteState( mute );
699
    muteStateChangedNotify( mute );
700
}
701
702
void
703
EngineController::toggleMute() //SLOT
704
{
705
    setMuted( !isMuted() );
706
}
707
708
Meta::TrackPtr
709
EngineController::currentTrack() const
710
{
711
    return m_currentTrack;
712
}
713
714
qint64
715
EngineController::trackLength() const
716
{
717
    const qint64 phononLength = m_media->totalTime(); //may return -1
718
719
    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
720
        return m_currentTrack->length();
721
    else
722
        return phononLength;
723
}
724
725
void
726
EngineController::setNextTrack( Meta::TrackPtr track )
727
{
728
    DEBUG_BLOCK
729
730
    debug() << "locking mutex";
731
    QMutexLocker locker( &m_mutex );
732
    debug() << "locked!";
733
734
    if( !track )
735
        return;
736
737
    track->prepareToPlay();
738
    if( track->playableUrl().isEmpty() )
739
        return;
740
741
    if( state() == Phonon::PlayingState ||
742
        state() == Phonon::BufferingState )
743
    {
744
        m_media->clearQueue();
745
        if( track->playableUrl().isLocalFile() )
746
            m_media->enqueue( track->playableUrl() );
747
        m_nextTrack = track;
748
        m_nextUrl = track->playableUrl();
749
    }
750
    else
751
    {
752
        play( track );
753
    }
754
}
755
756
Phonon::State
757
EngineController::state() const
758
{
759
    if ( m_fadeoutTimer->isActive() )
760
        return Phonon::StoppedState;
761
    else
762
        return phononMediaObject()->state();
763
}
764
765
bool
766
EngineController::isStream()
767
{
768
    DEBUG_BLOCK
769
770
    if( m_media )
771
        return m_media->currentSource().type() == Phonon::MediaSource::Stream;
772
    return false;
773
}
774
775
int
776
EngineController::trackPosition() const
777
{
778
//NOTE: there was a bunch of last.fm logic removed from here
779
//pretty sure it's irrelevant, if not, look back to mid-March 2008
780
    return static_cast<int>( m_media->currentTime() / 1000 );
781
}
782
783
int
784
EngineController::trackPositionMs() const
785
{
786
    return m_media->currentTime();
787
}
788
789
bool
790
EngineController::isEqSupported() const
791
{
792
    // If effect was created it means we have equalizer support
793
    return ( !m_equalizer.isNull() );
794
}
795
796
double
797
EngineController::eqMaxGain() const
798
{
799
   if( m_equalizer.isNull() )
800
       return 100;
801
   QList<Phonon::EffectParameter> mEqPar = m_equalizer->parameters();
802
   if( mEqPar.isEmpty() )
803
       return 100.0;
804
   double mScale;
805
   mScale = ( fabs(mEqPar.at(0).maximumValue().toDouble() ) +  fabs( mEqPar.at(0).minimumValue().toDouble() ) );
806
   mScale /= 2.0;
807
   return mScale;
808
}
809
810
QStringList
811
EngineController::eqBandsFreq() const
812
{
813
    // This will extract the bands frequency values from effect parameter name
814
    // as long as they follow the rules:
815
    // eq-preamp parameter will contain 'pre-amp' string
816
    // bands parameters are described using schema 'xxxHz'
817
    QStringList mBandsFreq;
818
    if( m_equalizer.isNull() )
819
       return mBandsFreq;
820
    QList<Phonon::EffectParameter> mEqPar = m_equalizer->parameters();
821
    if( mEqPar.isEmpty() )
822
       return mBandsFreq;
823
    QRegExp rx( "\\d+(?=Hz)" );
824
    foreach( const Phonon::EffectParameter &mParam, mEqPar )
825
    {
826
        if( mParam.name().contains( QString( "pre-amp" ) ) )
827
        {
828
            mBandsFreq << i18n( "Preamp" );
829
        }
830
        else if ( mParam.name().contains( rx ) )
831
        {
832
            if( rx.cap( 0 ).toInt() < 1000 )
833
            {
834
                mBandsFreq << QString( rx.cap( 0 )).append( "\nHz" );
835
            }
836
            else
837
            {
838
                mBandsFreq << QString::number( rx.cap( 0 ).toInt()/1000 ).append( "\nkHz" );
839
            }
840
        }
841
    }
842
    return mBandsFreq;
843
}
844
845
void
846
EngineController::eqUpdate() //SLOT
847
{
848
    // if equalizer not present simply return
849
    if( m_equalizer.isNull() )
850
        return;
851
    // check if equalizer should be disabled ??
852
    if( AmarokConfig::equalizerMode() <= 0 )
853
    {
854
        // Remove effect from path
855
        if( m_path.effects().indexOf( m_equalizer ) != -1 )
856
            m_path.removeEffect( m_equalizer );
857
    }
858
    else
859
    {
860
        // Set equalizer parameter according to the gains from settings
861
        QList<Phonon::EffectParameter> mEqPar = m_equalizer->parameters();
862
        QList<int> mEqParCfg = AmarokConfig::equalizerGains();
863
864
        QListIterator<int> mEqParNewIt( mEqParCfg );
865
        double scaledVal; // Scaled value to set from universal -100 - 100 range to plugin scale
866
        foreach( const Phonon::EffectParameter &mParam, mEqPar )
867
        {
868
            scaledVal = mEqParNewIt.hasNext() ? mEqParNewIt.next() : 0;
869
            scaledVal *= ( fabs(mParam.maximumValue().toDouble() ) +  fabs( mParam.minimumValue().toDouble() ) );
870
            scaledVal /= 200.0;
871
            m_equalizer->setParameterValue( mParam, scaledVal );
872
        }
873
        // Insert effect into path if needed
874
        if( m_path.effects().indexOf( m_equalizer ) == -1 )
875
        {
876
            if( !m_path.effects().isEmpty() )
877
            {
878
                m_path.insertEffect( m_equalizer, m_path.effects().first() );
879
            }
880
            else
881
            {
882
                m_path.insertEffect( m_equalizer );
883
            }
884
        }
885
    }
886
}
887
888
//////////////////////////////////////////////////////////////////////////////////////////
889
// PRIVATE SLOTS
890
//////////////////////////////////////////////////////////////////////////////////////////
891
892
void
893
EngineController::slotTick( qint64 position )
894
{
895
    if ( m_boundedPlayback )
896
    {
897
        qint64 newPosition = position;
898
        trackPositionChangedNotify( static_cast<long>( position - m_boundedPlayback->startPosition() ), false );
899
900
        // Calculate a better position.  Sometimes the position doesn't update
901
        // with a good resolution (for example, 1 sec for TrueAudio files in the
902
        // Xine-1.1.18 backend).  This tick function, in those cases, just gets
903
        // called multiple times with the same position.  We count how many
904
        // times this has been called prior, and adjust for it.
905
        if ( position == m_lastTickPosition )
906
            newPosition += ++m_lastTickCount * m_tickInterval;
907
        else
908
            m_lastTickCount = 0;
909
910
        m_lastTickPosition = position;
911
912
        //don't go beyond the stop point
913
        if ( newPosition >= m_boundedPlayback->endPosition() )
914
        {
915
            slotAboutToFinish();
916
        }
917
    }
918
    else
919
    {
920
        trackPositionChangedNotify( static_cast<long>( position ), false ); //it expects milliseconds
921
    }
922
}
923
924
void
925
EngineController::slotAboutToFinish()
926
{
927
    DEBUG_BLOCK
928
    debug() << "Track finished completely, updating statistics";
929
930
    if( m_currentTrack ) // not sure why this should not be the case, but sometimes happens. don't crash.
931
    {
932
        m_currentTrack->finishedPlaying( 1.0 ); // If we reach aboutToFinish, the track is done as far as we are concerned.
933
        trackFinishedNotify( m_currentTrack );
934
    }
935
    if( m_multiPlayback )
936
    {
937
        DEBUG_LINE_INFO
938
        m_mutex.lock();
939
        m_playWhenFetched = false;
940
        m_mutex.unlock();
941
        m_multiPlayback->fetchNext();
942
        debug() << "The queue has: " << m_media->queue().size() << " tracks in it";
943
    }
944
    else if( m_multiSource )
945
    {
946
        debug() << "source finished, lets get the next one";
947
        KUrl nextSource = m_multiSource->next();
948
949
        if ( !nextSource.isEmpty() )
950
        { //more sources
951
            m_mutex.lock();
952
            m_playWhenFetched = false;
953
            m_mutex.unlock();
954
            debug() << "playing next source: " << nextSource;
955
            slotPlayableUrlFetched( nextSource );
956
        }
957
        else if( m_media->queue().isEmpty() )
958
        { //go to next track
959
            The::playlistActions()->requestNextTrack();
960
            debug() << "no more sources, skip to next track";
961
        }
962
    }
963
    else if ( m_boundedPlayback )
964
    {
965
        debug() << "finished a track that consists of part of another track, go to next track even if this url is technically not done yet";
966
967
        //stop this track, now, as the source track might go on and on, and
968
        //there might not be any more tracks in the playlist...
969
        stop( true );
970
        The::playlistActions()->requestNextTrack();
971
        slotQueueEnded();
972
    }
973
    else if ( m_currentTrack && m_currentTrack->playableUrl().url().startsWith( "audiocd:/" ) )
974
    {
975
        debug() << "finished a CD track, don't care if queue is not empty, just get new track...";
976
977
        The::playlistActions()->requestNextTrack();
978
        slotQueueEnded();
979
    }
980
    else if( m_media->queue().isEmpty() )
981
        The::playlistActions()->requestNextTrack();
982
}
983
984
void
985
EngineController::slotQueueEnded()
986
{
987
    DEBUG_BLOCK
988
989
    if( m_currentTrack && !m_multiPlayback && !m_multiSource )
990
    {
991
        m_media->setCurrentSource( Phonon::MediaSource() );
992
        playbackEnded( trackPositionMs(), m_currentTrack->length(), Engine::EngineObserver::EndedStopped );
993
        m_currentTrack = 0;
994
        trackChangedNotify( m_currentTrack );
995
    }
996
997
    m_mutex.lock(); // in case setNextTrack is being handled right now.
998
999
    // Non-local urls are not enqueued so we must play them explicitly.
1000
    if( m_nextTrack )
1001
    {
1002
        DEBUG_LINE_INFO
1003
        play( m_nextTrack );
1004
    }
1005
    else if( !m_nextUrl.isEmpty() )
1006
    {
1007
        DEBUG_LINE_INFO
1008
        playUrl( m_nextUrl, 0 );
1009
    }
1010
    else
1011
        // possibly we are waiting for a fetch
1012
        m_playWhenFetched = true;
1013
1014
    m_mutex.unlock();
1015
}
1016
1017
static const qreal log10over20 = 0.1151292546497022842; // ln(10) / 20
1018
1019
void
1020
EngineController::slotNewTrackPlaying( const Phonon::MediaSource &source )
1021
{
1022
    DEBUG_BLOCK
1023
1024
    if( source.type() == Phonon::MediaSource::Empty )
1025
    {
1026
        debug() << "Empty MediaSource (engine stop)";
1027
        return;
1028
    }
1029
1030
    // the new track was taken from the queue, so clear these fields
1031
    if( m_nextTrack )
1032
    {
1033
        m_currentTrack = m_nextTrack;
1034
        m_nextTrack.clear();
1035
    }
1036
1037
    if( !m_nextUrl.isEmpty() )
1038
        m_nextUrl.clear();
1039
1040
    if ( m_currentTrack && AmarokConfig::replayGainMode() != AmarokConfig::EnumReplayGainMode::Off )
1041
    {
1042
        if( !m_preamp ) // replaygain was just turned on, and amarok was started with it off
1043
        {
1044
            m_preamp = new Phonon::VolumeFaderEffect( this );
1045
            m_path.insertEffect( m_preamp );
1046
        }
1047
1048
        Meta::Track::ReplayGainMode mode = ( AmarokConfig::replayGainMode() == AmarokConfig::EnumReplayGainMode::Track)
1049
                                         ? Meta::Track::TrackReplayGain
1050
                                         : Meta::Track::AlbumReplayGain;
1051
        // gain is usually negative (but may be positive)
1052
        qreal gain = m_currentTrack->replayGain( mode );
1053
        // peak is usually positive and smaller than gain (but may be negative)
1054
        qreal peak = m_currentTrack->replayPeakGain( mode );
1055
        if ( gain + peak > 0.0 )
1056
        {
1057
            debug() << "Gain of" << gain << "would clip at absolute peak of" << gain + peak;
1058
            gain -= gain + peak;
1059
        }
1060
        debug() << "Using gain of" << gain << "with relative peak of" << peak;
1061
        // we calculate the volume change ourselves, because m_preamp->setVolumeDecibel is
1062
        // a little confused about minus signs
1063
        m_preamp->setVolume( exp( gain * log10over20 ) );
1064
        m_preamp->fadeTo( exp( gain * log10over20 ), 0 ); // HACK: we use fadeTo because setVolume is b0rked in Phonon Xine before r1028879
1065
    }
1066
    else if( m_preamp )
1067
    {
1068
        m_preamp->setVolume( 1.0 );
1069
        m_preamp->fadeTo( 1.0, 0 ); // HACK: we use fadeTo because setVolume is b0rked in Phonon Xine before r1028879
1070
    }
1071
1072
    trackChangedNotify( m_currentTrack );
1073
    newTrackPlaying();
1074
}
1075
1076
void
1077
EngineController::slotStateChanged( Phonon::State newState, Phonon::State oldState ) //SLOT
1078
{
1079
    DEBUG_BLOCK
1080
1081
    // Sanity checks:
1082
    if( newState == oldState )
1083
        return;
1084
1085
    if( newState == Phonon::ErrorState )  // If media is borked, skip to next track
1086
    {
1087
        warning() << "Phonon failed to play this URL. Error: " << m_media->errorString();
1088
        if( m_multiPlayback )
1089
        {
1090
            DEBUG_LINE_INFO
1091
            m_mutex.lock();
1092
            m_playWhenFetched = true;
1093
            m_mutex.unlock();
1094
            m_multiPlayback->fetchNext();
1095
            debug() << "The queue has: " << m_media->queue().size() << " tracks in it";
1096
        }
1097
        else if( m_multiSource )
1098
        {
1099
            debug() << "source error, lets get the next one";
1100
            KUrl nextSource = m_multiSource->next();
1101
1102
            if ( !nextSource.isEmpty() )
1103
            { //more sources
1104
                m_mutex.lock();
1105
                m_playWhenFetched = false;
1106
                m_mutex.unlock();
1107
                debug() << "playing next source: " << nextSource;
1108
                slotPlayableUrlFetched( nextSource );
1109
            }
1110
            else if( m_media->queue().isEmpty() )
1111
                The::playlistActions()->requestNextTrack();
1112
        }
1113
1114
        else if( m_media->queue().isEmpty() )
1115
            The::playlistActions()->requestNextTrack();
1116
    }
1117
1118
    if ( m_fadeoutTimer->isActive() )
1119
    {
1120
        // We've stopped already as far as the rest of Amarok is concerned
1121
        if ( oldState == Phonon::PlayingState )
1122
            oldState = Phonon::StoppedState;
1123
1124
        if ( oldState == newState )
1125
            return;
1126
    }
1127
1128
    stateChangedNotify( newState, oldState );
1129
}
1130
1131
void
1132
EngineController::slotPlayableUrlFetched( const KUrl &url )
1133
{
1134
    DEBUG_BLOCK
1135
    debug() << "Fetched url: " << url;
1136
    if( url.isEmpty() )
1137
    {
1138
        DEBUG_LINE_INFO
1139
        The::playlistActions()->requestNextTrack();
1140
        return;
1141
    }
1142
1143
    if( !m_playWhenFetched )
1144
    {
1145
        DEBUG_LINE_INFO
1146
        m_mutex.lock();
1147
        m_media->clearQueue();
1148
        if( url.isLocalFile() )
1149
            m_media->enqueue( url );
1150
        m_nextTrack.clear();
1151
        m_nextUrl = url;
1152
        debug() << "The next url we're playing is: " << m_nextUrl;
1153
        // reset this flag each time
1154
        m_playWhenFetched = true;
1155
        m_mutex.unlock();
1156
    }
1157
    else
1158
    {
1159
        DEBUG_LINE_INFO
1160
        m_mutex.lock();
1161
        playUrl( url, 0 );
1162
        m_mutex.unlock();
1163
    }
1164
}
1165
1166
void
1167
EngineController::slotTrackLengthChanged( qint64 milliseconds )
1168
{
1169
    DEBUG_BLOCK
1170
1171
    trackLengthChangedNotify( ( m_multiPlayback || m_boundedPlayback ) ? trackLength() : milliseconds );
1172
}
1173
1174
void
1175
EngineController::slotMetaDataChanged()
1176
{
1177
    DEBUG_BLOCK
1178
1179
    QHash<qint64, QString> meta;
1180
1181
    meta.insert( Meta::valUrl, m_media->currentSource().url().toString() );
1182
1183
    QStringList artist = m_media->metaData( "ARTIST" );
1184
    debug() << "Artist     : " << artist;
1185
    if( !artist.isEmpty() )
1186
        meta.insert( Meta::valArtist, artist.first() );
1187
1188
    QStringList album = m_media->metaData( "ALBUM" );
1189
    debug() << "Album      : " << album;
1190
    if( !album.isEmpty() )
1191
        meta.insert( Meta::valAlbum, album.first() );
1192
1193
    QStringList title = m_media->metaData( "TITLE" );
1194
    debug() << "Title      : " << title;
1195
    if( !title.isEmpty() )
1196
        meta.insert( Meta::valTitle, title.first() );
1197
1198
    QStringList genre = m_media->metaData( "GENRE" );
1199
    debug() << "Genre      : " << genre;
1200
    if( !genre.isEmpty() )
1201
        meta.insert( Meta::valGenre, genre.first() );
1202
1203
    QStringList tracknum = m_media->metaData( "TRACKNUMBER" );
1204
    debug() << "Tracknumber: " << tracknum;
1205
    if( !tracknum.isEmpty() )
1206
        meta.insert( Meta::valTrackNr, tracknum.first() );
1207
1208
    QStringList length = m_media->metaData( "LENGTH" );
1209
    debug() << "Length     : " << length;
1210
    if( !length.isEmpty() )
1211
        meta.insert( Meta::valLength, length.first() );
1212
1213
    bool trackChanged = false;
1214
    if( m_lastTrack != m_currentTrack )
1215
    {
1216
        trackChanged = true;
1217
        m_lastTrack = m_currentTrack;
1218
    }
1219
    debug() << "Track changed: " << trackChanged;
1220
    newMetaDataNotify( meta, trackChanged );
1221
}
1222
1223
void
1224
EngineController::slotStopFadeout() //SLOT
1225
{
1226
    DEBUG_BLOCK
1227
1228
    m_media->stop();
1229
    m_media->setCurrentSource( Phonon::MediaSource() );
1230
    resetFadeout();
1231
}
1232
1233
void
1234
EngineController::resetFadeout()
1235
{
1236
    m_fadeoutTimer->stop();
1237
    if ( m_fader )
1238
    {
1239
        m_fader->setVolume( 1.0 );
1240
        m_fader->fadeTo( 1.0, 0 ); // HACK: we use fadeTo because setVolume is b0rked in Phonon Xine before r1028879
1241
    }
1242
}
1243
1244
void EngineController::slotTitleChanged( int titleNumber )
1245
{
1246
    DEBUG_BLOCK
1247
    Q_UNUSED( titleNumber );
1248
1249
    slotAboutToFinish();
1250
}
1251
1252
void EngineController::slotVolumeChanged( qreal newVolume )
1253
{
1254
    int percent = qBound<qreal>( 0, qRound(newVolume * 100), 100 );
1255
1256
    if ( !m_ignoreVolumeChangeObserve && m_volume != percent )
1257
    {
1258
        m_ignoreVolumeChangeAction = true;
1259
1260
        m_volume = percent;
1261
        AmarokConfig::setMasterVolume( percent );
1262
        volumeChangedNotify( percent );
1263
    }
1264
    else
1265
        m_volume = percent;
1266
1267
    m_ignoreVolumeChangeObserve = false;
1268
}
1269
1270
void EngineController::slotMutedChanged( bool mute )
1271
{
1272
    AmarokConfig::setMuteState( mute );
1273
    muteStateChangedNotify( mute );
1274
}
1275
1276
1277
bool EngineController::isPlayingAudioCd()
1278
{
1279
    return m_currentIsAudioCd;
1280
}
1281
1282
QString EngineController::prettyNowPlaying() const
1283
{
1284
    Meta::TrackPtr track = currentTrack();
1285
1286
    if( track )
1287
    {
1288
        QString title       = Qt::escape( track->name() );
1289
        QString prettyTitle = Qt::escape( track->prettyName() );
1290
        QString artist      = track->artist() ? Qt::escape( track->artist()->name() ) : QString();
1291
        QString album       = track->album() ? Qt::escape( track->album()->name() ) : QString();
1292
1293
        // ugly because of translation requirements
1294
        if ( !title.isEmpty() && !artist.isEmpty() && !album.isEmpty() )
1295
            title = i18nc( "track by artist on album", "<b>%1</b> by <b>%2</b> on <b>%3</b>", title, artist, album );
1296
1297
        else if ( !title.isEmpty() && !artist.isEmpty() )
1298
            title = i18nc( "track by artist", "<b>%1</b> by <b>%2</b>", title, artist );
1299
1300
        else if ( !album.isEmpty() )
1301
            // we try for pretty title as it may come out better
1302
            title = i18nc( "track on album", "<b>%1</b> on <b>%2</b>", prettyTitle, album );
1303
        else
1304
            title = "<b>" + prettyTitle + "</b>";
1305
1306
        if ( title.isEmpty() )
1307
            title = i18n( "Unknown track" );
1308
1309
        Capabilities::SourceInfoCapability *sic = track->create<Capabilities::SourceInfoCapability>();
1310
        if ( sic )
1311
        {
1312
            QString source = sic->sourceName();
1313
            if ( !source.isEmpty() )
1314
                title += ' ' + i18nc( "track from source", "from <b>%1</b>", source );
1315
1316
            delete sic;
1317
        }
1318
1319
        if ( track->length() > 0 ) {
1320
            QString length = Qt::escape( Meta::msToPrettyTime( track->length() ) );
1321
            title += " (" + length + ')';
1322
        }
1323
1324
        return title;
1325
    }
1326
    else
1327
        return i18n( "No track playing" );
1328
}
1329
1330
#include "EngineController.moc"