playermanager.cpp 18.3 KB
Newer Older
1 2 3 4
/***************************************************************************
    begin                : Sat Feb 14 2004
    copyright            : (C) 2004 by Scott Wheeler
    email                : wheeler@kde.org
5 6 7 8

    copyright            : (C) 2007 by Matthias Kretz
    email                : kretz@kde.org

9
    copyright            : (C) 2008, 2009 by Michael Pyne
10
    email                : michael.pyne@kdemail.net
11 12 13 14 15 16 17 18 19 20 21
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

22 23
#include "playermanager.h"

24
#include <kdebug.h>
Matthias Kretz's avatar
Matthias Kretz committed
25
#include <kmessagebox.h>
26
#include <klocale.h>
27 28 29
#include <kactioncollection.h>
#include <kselectaction.h>
#include <ktoggleaction.h>
Andreas Pakulat's avatar
Andreas Pakulat committed
30
#include <kurl.h>
31

32 33 34
#include <Phonon/AudioOutput>
#include <Phonon/MediaObject>
#include <Phonon/VolumeFaderEffect>
35

Laurent Montel's avatar
Laurent Montel committed
36
#include <QPixmap>
37 38
#include <QTimer>

39 40
#include <math.h>

41
#include "playlistinterface.h"
42
#include "playeradaptor.h"
43
#include "slideraction.h"
44
#include "statuslabel.h"
45
#include "actioncollection.h"
46
#include "collectionlist.h"
Michael Pyne's avatar
Michael Pyne committed
47
#include "coverinfo.h"
48
#include "tag.h"
49
#include "scrobbler.h"
50
#include "juk.h"
51 52

using namespace ActionCollection;
53

Stephan Kulow's avatar
Stephan Kulow committed
54
enum PlayerManagerStatus { StatusStopped = -1, StatusPaused = 1, StatusPlaying = 2 };
55

56 57 58 59 60
////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

PlayerManager::PlayerManager() :
61
    QObject(),
62
    m_playlistInterface(0),
63
    m_statusLabel(0),
64
    m_setup(false),
65
    m_crossfadeTracks(true),
66
    m_curOutputPath(0)
67
{
Michael Pyne's avatar
Michael Pyne committed
68 69 70 71 72
// This class is the first thing constructed during program startup, and
// therefore has no access to the widgets needed by the setup() method.
// Since the setup() method will be called indirectly by the player() method
// later, just disable it here. -- mpyne
//    setup();
Laurent Montel's avatar
Laurent Montel committed
73
    new PlayerAdaptor( this );
74 75 76 77
    
    
    m_scrobbler = new Scrobbler(this);
    connect(this, SIGNAL(signalItemChanged(FileHandle)), m_scrobbler, SLOT(nowPlaying(FileHandle)));
78 79 80 81
}

PlayerManager::~PlayerManager()
{
82

83 84 85 86 87 88
}

////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

89 90
bool PlayerManager::playing() const
{
91
    if(!m_setup)
92 93
        return false;

94 95
    Phonon::State state = m_media[m_curOutputPath]->state();
    return (state == Phonon::PlayingState || state == Phonon::BufferingState);
96 97 98 99
}

bool PlayerManager::paused() const
{
100
    if(!m_setup)
101 102
        return false;

103
    return m_media[m_curOutputPath]->state() == Phonon::PausedState;
104 105
}

106 107 108 109 110 111 112 113
bool PlayerManager::muted() const
{
    if(!m_setup)
        return false;

    return m_output[m_curOutputPath]->isMuted();
}

114 115
float PlayerManager::volume() const
{
116
    if(!m_setup)
117
        return 1.0;
118

119
    return m_output[m_curOutputPath]->volume();
120 121
}

122 123
int PlayerManager::status() const
{
124
    if(!m_setup)
125
        return StatusStopped;
126

127
    if(paused())
128
        return StatusPaused;
129

130
    if(playing())
131
        return StatusPlaying;
132

133 134 135
    return 0;
}

Scott Wheeler's avatar
Scott Wheeler committed
136
int PlayerManager::totalTime() const
137 138 139 140 141 142 143 144 145 146
{
    return totalTimeMSecs() / 1000;
}

int PlayerManager::currentTime() const
{
    return currentTimeMSecs() / 1000;
}

int PlayerManager::totalTimeMSecs() const
147
{
148
    if(!m_setup)
149 150
        return 0;

151
    return m_media[m_curOutputPath]->totalTime();
152 153
}

154
int PlayerManager::currentTimeMSecs() const
155
{
156
    if(!m_setup)
157 158
        return 0;

159
    return m_media[m_curOutputPath]->currentTime();
160 161
}

162 163 164 165 166 167 168 169
bool PlayerManager::seekable() const
{
    if(!m_setup)
        return false;

    return m_media[m_curOutputPath]->isSeekable();
}

170 171 172 173 174 175 176
QStringList PlayerManager::trackProperties()
{
    return FileHandle::properties();
}

QString PlayerManager::trackProperty(const QString &property) const
{
Michael Pyne's avatar
Michael Pyne committed
177
    if(!playing() && !paused())
178
        return QString();
179 180 181 182

    return m_file.property(property);
}

Michael Pyne's avatar
Michael Pyne committed
183 184 185 186 187
QPixmap PlayerManager::trackCover(const QString &size) const
{
    if(!playing() && !paused())
        return QPixmap();

188
    if(size.toLower() == "small")
Michael Pyne's avatar
Michael Pyne committed
189
        return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail);
190
    if(size.toLower() == "large")
Michael Pyne's avatar
Michael Pyne committed
191 192 193 194 195
        return m_file.coverInfo()->pixmap(CoverInfo::FullSize);

    return QPixmap();
}

196 197 198 199 200
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

201 202
QString PlayerManager::playingString() const
{
203
    if(!playing() || m_file.isNull())
204
        return QString();
205

206
    return m_file.tag()->playingString();
207 208
}

209
void PlayerManager::setPlaylistInterface(PlaylistInterface *interface)
210 211 212 213
{
    m_playlistInterface = interface;
}

214 215 216 217 218
void PlayerManager::setStatusLabel(StatusLabel *label)
{
    m_statusLabel = label;
}

219 220 221 222
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

223
void PlayerManager::play(const FileHandle &file)
224
{
225
    if(!m_setup)
226 227
        setup();

228
    if(!m_media[0] || !m_media[1] || !m_playlistInterface)
229 230
        return;

231 232 233 234
    stopCrossfade();

    // The "currently playing" media object.
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
235
    
236
    if(file.isNull()) {
237
        if(paused())
238
            mediaObject->play();
239
        else if(playing()) {
240
            mediaObject->seek(0);
241
        }
242
        else {
243
            m_playlistInterface->playNext();
244
            m_file = m_playlistInterface->currentFile();
245

246
            if(!m_file.isNull())
247
            {
248 249
                mediaObject->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
                mediaObject->play();
250 251

                emit signalItemChanged(m_file);
252
            }
253
        }
254
    }
255
    else {
256
        mediaObject->setCurrentSource(KUrl::fromPath(file.absFilePath()));
257
        mediaObject->play();
258 259 260 261 262

        if(m_file != file)
            emit signalItemChanged(file);

        m_file = file;
263
    }
264

265 266
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
267 268
}

269 270 271 272 273 274 275 276 277
void PlayerManager::play(const QString &file)
{
    CollectionListItem *item = CollectionList::instance()->lookup(file);
    if(item) {
        Playlist::setPlaying(item);
        play(item->file());
    }
}

Scott Wheeler's avatar
Scott Wheeler committed
278 279 280 281 282
void PlayerManager::play()
{
    play(FileHandle::null());
}

283 284
void PlayerManager::pause()
{
285
    if(!m_setup)
286 287
        return;

288
    if(paused()) {
289 290 291 292
        play();
        return;
    }

293
    action("pause")->setEnabled(false);
294

295
    m_media[m_curOutputPath]->pause();
296 297

    emit signalPause();
298 299 300 301
}

void PlayerManager::stop()
{
302
    if(!m_setup || !m_playlistInterface)
303 304
        return;

305 306 307 308
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
309
    action("forwardAlbum")->setEnabled(false);
310

311 312 313 314
    // Fading out playback is for chumps.
    stopCrossfade();
    m_media[0]->stop();
    m_media[1]->stop();
315 316 317 318 319

    if(!m_file.isNull()) {
        m_file = FileHandle::null();
        emit signalItemChanged(m_file);
    }
320 321
}

Laurent Montel's avatar
Laurent Montel committed
322
void PlayerManager::setVolume(float volume)
323
{
324
    if(!m_setup)
325
        setup();
326

327 328
    m_output[0]->setVolume(volume);
    m_output[1]->setVolume(volume);
329 330
}

331
void PlayerManager::seek(int seekTime)
332
{
333
    if(!m_setup || m_media[m_curOutputPath]->currentTime() == seekTime)
334
        return;
335

336 337
    kDebug() << "Stopping crossfade to seek from" << m_media[m_curOutputPath]->currentTime()
             << "to" << seekTime;
338 339
    stopCrossfade();
    m_media[m_curOutputPath]->seek(seekTime);
340 341 342 343
}

void PlayerManager::seekForward()
{
344 345 346 347 348 349
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
    const qint64 total = mediaObject->totalTime();
    const qint64 newtime = mediaObject->currentTime() + total / 100;

    stopCrossfade();
    mediaObject->seek(qMin(total, newtime));
350 351 352 353
}

void PlayerManager::seekBack()
{
354 355 356 357 358 359
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
    const qint64 total = mediaObject->totalTime();
    const qint64 newtime = mediaObject->currentTime() - total / 100;

    stopCrossfade();
    mediaObject->seek(qMax(qint64(0), newtime));
360 361
}

362 363
void PlayerManager::playPause()
{
David Faure's avatar
David Faure committed
364
    playing() ? action("pause")->trigger() : action("play")->trigger();
365 366
}

367 368
void PlayerManager::forward()
{
369 370
    m_playlistInterface->playNext();
    FileHandle file = m_playlistInterface->currentFile();
371

372 373 374 375 376 377 378 379
    if(!file.isNull())
        play(file);
    else
        stop();
}

void PlayerManager::back()
{
380 381
    m_playlistInterface->playPrevious();
    FileHandle file = m_playlistInterface->currentFile();
382

383 384 385 386 387 388
    if(!file.isNull())
        play(file);
    else
        stop();
}

389 390
void PlayerManager::volumeUp()
{
391
    if(!m_setup)
392
        return;
393

394
    setVolume(volume() + 0.04); // 4% up
395 396 397 398
}

void PlayerManager::volumeDown()
{
399
    if(!m_output)
400
        return;
401

402
    setVolume(volume() - 0.04); // 4% down
403 404
}

405
void PlayerManager::setMuted(bool m)
406
{
407
    if(!m_setup)
408 409
        return;

410 411 412 413 414 415 416 417 418 419 420
    m_output[m_curOutputPath]->setMuted(m);
}

bool PlayerManager::mute()
{
    if(!m_setup)
        return false;

    bool newState = !muted();
    setMuted(newState);
    return newState;
421 422
}

423 424 425
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
426

427 428
void PlayerManager::slotNeedNextUrl()
{
429
    m_scrobbler->scrobble();
430
    if(m_file.isNull() || !m_crossfadeTracks)
Matthias Kretz's avatar
Matthias Kretz committed
431
        return;
432

433 434
    m_playlistInterface->playNext();
    FileHandle nextFile = m_playlistInterface->currentFile();
435 436

    if(!nextFile.isNull()) {
437
        m_file = nextFile;
438
        crossfadeToFile(m_file);
439 440 441
    }
}

442 443
void PlayerManager::slotFinished()
{
444 445 446 447
    // It is possible to end up in this function if a file simply fails to play or if the
    // user moves the slider all the way to the end, therefore see if we can keep playing
    // and if we can, do so.  Otherwise, stop.  Note that this slot should
    // only be called by the currently "main" output path (i.e. not from the
448 449 450 451 452 453
    // crossfading one).  However life isn't always so nice apparently, so do some
    // sanity-checking.

    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(mediaObject != m_media[m_curOutputPath])
        return;
454

455 456
    m_playlistInterface->playNext();
    m_file = m_playlistInterface->currentFile();
457

458 459 460 461
    if(m_file.isNull()) {
        stop();
    }
    else {
462
        emit signalItemChanged(m_file);
463 464 465
        m_media[m_curOutputPath]->setCurrentSource(m_file.absFilePath());
        m_media[m_curOutputPath]->play();
    }
466 467
}

468 469 470
void PlayerManager::slotLength(qint64 msec)
{
    m_statusLabel->setItemTotalTime(msec / 1000);
471
    emit totalTimeChanged(msec);
472 473
}

474
void PlayerManager::slotTick(qint64 msec)
475
{
476
    if(!m_setup || !m_playlistInterface)
477
        return;
478

479
    if(m_statusLabel)
480
        m_statusLabel->setItemCurrentTime(msec / 1000);
481 482

    emit tick(msec);
483 484
}

485
void PlayerManager::slotStateChanged(Phonon::State newstate, Phonon::State oldstate)
Matthias Kretz's avatar
Matthias Kretz committed
486
{
487 488 489 490 491 492
    // Use sender() since either media object may have sent the signal.
    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(!mediaObject)
        return;

    // Handle errors for either media object
493
    if(newstate == Phonon::ErrorState) {
494 495 496
        QString errorMessage =
            i18nc(
              "%1 will be the /path/to/file, %2 will be some string from Phonon describing the error",
497 498
              "JuK is unable to play the audio file<nl/><filename>%1</filename><nl/>"
                "for the following reason:<nl/><message>%2</message>",
499
              m_file.absFilePath(),
500
              mediaObject->errorString()
501 502
            );

503
        switch(mediaObject->errorType()) {
504 505 506
            case Phonon::NoError:
                kDebug() << "received a state change to ErrorState but errorType is NoError!?";
                break;
507

Matthias Kretz's avatar
Matthias Kretz committed
508 509
            case Phonon::NormalError:
                forward();
510
                KMessageBox::information(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
511
                break;
512

Matthias Kretz's avatar
Matthias Kretz committed
513 514
            case Phonon::FatalError:
                stop();
515
                KMessageBox::sorry(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
516 517 518
                break;
        }
    }
519

520 521 522 523 524
    // Now bail out if we're not dealing with the currently playing media
    // object.

    if(mediaObject != m_media[m_curOutputPath])
        return;
525

526 527 528 529 530 531
    // Handle state changes for the playing media object.
    if(newstate == Phonon::StoppedState && oldstate != Phonon::LoadingState) {
        // If this occurs it should be due to a transitory shift (i.e. playing a different
        // song when one is playing now), since it didn't occur in the error handler.  Just
        // in case we really did abruptly stop, handle that case in a couple of seconds.
        QTimer::singleShot(2000, this, SLOT(slotUpdateGuiIfStopped()));
532 533 534

        JuK::JuKInstance()->setWindowTitle(i18n("JuK"));
        
Michael Pyne's avatar
Michael Pyne committed
535
        emit signalStop();
536
    }
537 538 539 540 541 542 543 544
    else if(newstate == Phonon::PlayingState) {
        action("pause")->setEnabled(true);
        action("stop")->setEnabled(true);
        action("forward")->setEnabled(true);
        if(action<KToggleAction>("albumRandomPlay")->isChecked())
            action("forwardAlbum")->setEnabled(true);
        action("back")->setEnabled(true);

545 546 547 548 549 550 551
                
        JuK::JuKInstance()->setWindowTitle(i18nc(
            "%1 is the artist and %2 is the title of the currently playing track.", 
            "%1 - %2 :: JuK", 
            m_file.tag()->artist(), 
            m_file.tag()->title()));
        
552 553
        emit signalPlay();
    }
554 555
}

556 557 558 559 560 561 562 563
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

void PlayerManager::setup()
{
    // All of the actions required by this class should be listed here.

564 565 566
    if(!action("pause") ||
       !action("stop") ||
       !action("back") ||
567
       !action("forwardAlbum") ||
568 569
       !action("forward") ||
       !action("trackPositionAction"))
570
    {
571
        kWarning() << "Could not find all of the required actions.";
572 573 574
        return;
    }

575 576 577 578
    if(m_setup)
        return;
    m_setup = true;

579 580 581 582 583 584
    // We use two audio paths at all times to make cross fading easier (and to also easily
    // support not using cross fading with the same code).  The currently playing audio
    // path is controlled using m_curOutputPath.

    for(int i = 0; i < 2; ++i) {
        m_output[i] = new Phonon::AudioOutput(Phonon::MusicCategory, this);
585

586 587 588
        m_media[i] = new Phonon::MediaObject(this);
        m_audioPath[i] = Phonon::createPath(m_media[i], m_output[i]);
        m_media[i]->setTickInterval(200);
589
        m_media[i]->setPrefinishMark(2000);
590

591 592 593 594 595
        // Pre-cache a volume fader object
        m_fader[i] = new Phonon::VolumeFaderEffect(m_media[i]);
        m_audioPath[i].insertEffect(m_fader[i]);
        m_fader[i]->setVolume(1.0f);

Laurent Montel's avatar
Laurent Montel committed
596
        connect(m_media[i], SIGNAL(stateChanged(Phonon::State,Phonon::State)), SLOT(slotStateChanged(Phonon::State,Phonon::State)));
597
        connect(m_media[i], SIGNAL(prefinishMarkReached(qint32)), SLOT(slotNeedNextUrl()));
598 599 600 601
        connect(m_media[i], SIGNAL(totalTimeChanged(qint64)), SLOT(slotLength(qint64)));
        connect(m_media[i], SIGNAL(tick(qint64)), SLOT(slotTick(qint64)));
        connect(m_media[i], SIGNAL(finished()), SLOT(slotFinished()));
    }
602

603 604
    // initialize action states

605 606 607 608
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
609
    action("forwardAlbum")->setEnabled(false);
610

611
    QDBusConnection::sessionBus().registerObject("/Player", this);
612 613 614
}

void PlayerManager::slotUpdateGuiIfStopped()
615
{
616 617 618
    if(m_media[0]->state() == Phonon::StoppedState && m_media[1]->state() == Phonon::StoppedState)
        stop();
}
619

620 621 622
void PlayerManager::crossfadeToFile(const FileHandle &newFile)
{
    int nextOutputPath = 1 - m_curOutputPath;
623

624 625 626
    // Don't need this anymore
    disconnect(m_media[m_curOutputPath], SIGNAL(finished()), this, 0);
    connect(m_media[nextOutputPath], SIGNAL(finished()), SLOT(slotFinished()));
627

628
    m_fader[nextOutputPath]->setVolume(0.0f);
629

630
    emit signalItemChanged(newFile);
631 632
    m_media[nextOutputPath]->setCurrentSource(newFile.absFilePath());
    m_media[nextOutputPath]->play();
633

634 635
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[m_curOutputPath]->fadeTo(0.0f, 2000);
636

637
    m_fader[nextOutputPath]->fadeTo(1.0f, 2000);
638

639 640
    m_curOutputPath = nextOutputPath;
}
641

642 643 644 645
void PlayerManager::stopCrossfade()
{
    // According to the Phonon docs, setVolume immediately takes effect,
    // which is "good enough for government work" ;)
646

647 648 649
    // 1 - curOutputPath is the other output path...
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[1 - m_curOutputPath]->setVolume(0.0f);
650

651 652 653 654 655 656 657 658
    // We don't actually need to physically stop crossfading as the playback
    // code will call ->play() when necessary anyways.  If we hit stop()
    // here instead of pause() then we will trick our stateChanged handler
    // into thinking Phonon had a spurious stop and we'll switch tracks
    // unnecessarily.  (This isn't a problem after crossfade completes due to
    // the signals being disconnected).

    m_media[1 - m_curOutputPath]->pause();
659 660
}

Michael Pyne's avatar
Michael Pyne committed
661 662 663 664 665 666 667 668 669
QString PlayerManager::randomPlayMode() const
{
    if(action<KToggleAction>("randomPlay")->isChecked())
        return "Random";
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        return "AlbumRandom";
    return "NoRandom";
}

670 671 672 673 674
void PlayerManager::setCrossfadeEnabled(bool crossfadeEnabled)
{
    m_crossfadeTracks = crossfadeEnabled;
}

Michael Pyne's avatar
Michael Pyne committed
675 676
void PlayerManager::setRandomPlayMode(const QString &randomMode)
{
677
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
678
        action<KToggleAction>("randomPlay")->setChecked(true);
679
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
680
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
681
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
682 683 684
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

685
#include "playermanager.moc"
686

687
// vim: set et sw=4 tw=0 sta: