playermanager.cpp 19.5 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
}

PlayerManager::~PlayerManager()
{
78

79 80 81 82 83 84
}

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

85 86
bool PlayerManager::playing() const
{
87
    if(!m_setup)
88 89
        return false;

90 91
    Phonon::State state = m_media[m_curOutputPath]->state();
    return (state == Phonon::PlayingState || state == Phonon::BufferingState);
92 93 94 95
}

bool PlayerManager::paused() const
{
96
    if(!m_setup)
97 98
        return false;

99
    return m_media[m_curOutputPath]->state() == Phonon::PausedState;
100 101
}

102 103 104 105 106 107 108 109
bool PlayerManager::muted() const
{
    if(!m_setup)
        return false;

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

110 111
float PlayerManager::volume() const
{
112
    if(!m_setup)
113
        return 1.0;
114

115
    return m_output[m_curOutputPath]->volume();
116 117
}

118 119
int PlayerManager::status() const
{
120
    if(!m_setup)
121
        return StatusStopped;
122

123
    if(paused())
124
        return StatusPaused;
125

126
    if(playing())
127
        return StatusPlaying;
128

129 130 131
    return 0;
}

Scott Wheeler's avatar
Scott Wheeler committed
132
int PlayerManager::totalTime() const
133 134 135 136 137 138 139 140 141 142
{
    return totalTimeMSecs() / 1000;
}

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

int PlayerManager::totalTimeMSecs() const
143
{
144
    if(!m_setup)
145 146
        return 0;

147
    return m_media[m_curOutputPath]->totalTime();
148 149
}

150
int PlayerManager::currentTimeMSecs() const
151
{
152
    if(!m_setup)
153 154
        return 0;

155
    return m_media[m_curOutputPath]->currentTime();
156 157
}

158 159 160 161 162 163 164 165
bool PlayerManager::seekable() const
{
    if(!m_setup)
        return false;

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

166 167 168 169 170 171 172
QStringList PlayerManager::trackProperties()
{
    return FileHandle::properties();
}

QString PlayerManager::trackProperty(const QString &property) const
{
Michael Pyne's avatar
Michael Pyne committed
173
    if(!playing() && !paused())
174
        return QString();
175 176 177 178

    return m_file.property(property);
}

Michael Pyne's avatar
Michael Pyne committed
179 180 181 182 183
QPixmap PlayerManager::trackCover(const QString &size) const
{
    if(!playing() && !paused())
        return QPixmap();

184
    if(size.toLower() == "small")
Michael Pyne's avatar
Michael Pyne committed
185
        return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail);
186
    if(size.toLower() == "large")
Michael Pyne's avatar
Michael Pyne committed
187 188 189 190 191
        return m_file.coverInfo()->pixmap(CoverInfo::FullSize);

    return QPixmap();
}

192 193 194 195 196
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

197 198
QString PlayerManager::playingString() const
{
199
    if(!playing() || m_file.isNull())
200
        return QString();
201

202
    return m_file.tag()->playingString();
203 204
}

205
void PlayerManager::setPlaylistInterface(PlaylistInterface *interface)
206 207 208 209
{
    m_playlistInterface = interface;
}

210 211 212 213 214
void PlayerManager::setStatusLabel(StatusLabel *label)
{
    m_statusLabel = label;
}

215 216 217 218
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

219
void PlayerManager::play(const FileHandle &file)
220
{
221
    if(!m_setup)
222 223
        setup();

224
    if(!m_media[0] || !m_media[1] || !m_playlistInterface)
225 226
        return;

227 228 229 230
    stopCrossfade();

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

243
            if(!m_file.isNull())
244
            {
245 246
                mediaObject->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
                mediaObject->play();
247 248

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

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

        m_file = file;
260
    }
261

262 263
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
264 265
}

266 267 268 269 270 271 272 273 274
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
275 276 277 278 279
void PlayerManager::play()
{
    play(FileHandle::null());
}

280 281
void PlayerManager::pause()
{
282
    if(!m_setup)
283 284
        return;

285
    if(paused()) {
286 287 288 289
        play();
        return;
    }

290
    action("pause")->setEnabled(false);
291

292
    m_media[m_curOutputPath]->pause();
293 294

    emit signalPause();
295 296 297 298
}

void PlayerManager::stop()
{
299
    if(!m_setup || !m_playlistInterface)
300 301
        return;

302 303 304 305
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
306
    action("forwardAlbum")->setEnabled(false);
307

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

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

Laurent Montel's avatar
Laurent Montel committed
319
void PlayerManager::setVolume(float volume)
320
{
321
    if(!m_setup)
322
        setup();
323

324 325
    m_output[0]->setVolume(volume);
    m_output[1]->setVolume(volume);
326 327
}

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

333 334
    kDebug() << "Stopping crossfade to seek from" << m_media[m_curOutputPath]->currentTime()
             << "to" << seekTime;
335 336
    stopCrossfade();
    m_media[m_curOutputPath]->seek(seekTime);
337
    emit seeked(seekTime);
338 339 340 341
}

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

    stopCrossfade();
348 349
    mediaObject->seek(seekTo);
    emit seeked(seekTo);
350 351 352 353
}

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

    stopCrossfade();
360 361
    mediaObject->seek(seekTo);
    emit seeked(seekTo);
362 363
}

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

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

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

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

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

391 392
void PlayerManager::volumeUp()
{
393
    if(!m_setup)
394
        return;
395

396
    setVolume(volume() + 0.04); // 4% up
397 398 399 400
}

void PlayerManager::volumeDown()
{
401
    if(!m_output)
402
        return;
403

404
    setVolume(volume() - 0.04); // 4% down
405 406
}

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

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

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

    bool newState = !muted();
    setMuted(newState);
    return newState;
423 424
}

425 426 427
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
428

429 430
void PlayerManager::slotNeedNextUrl()
{
431
    if(m_file.isNull() || !m_crossfadeTracks)
Matthias Kretz's avatar
Matthias Kretz committed
432
        return;
433

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

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

443 444
void PlayerManager::slotFinished()
{
445 446 447 448
    // 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
449 450 451 452 453 454
    // 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;
455

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

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

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

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

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

    emit tick(msec);
484 485
}

486
void PlayerManager::slotStateChanged(Phonon::State newstate, Phonon::State oldstate)
Matthias Kretz's avatar
Matthias Kretz committed
487
{
488 489 490 491 492 493
    // 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
494
    if(newstate == Phonon::ErrorState) {
495 496 497
        QString errorMessage =
            i18nc(
              "%1 will be the /path/to/file, %2 will be some string from Phonon describing the error",
498 499
              "JuK is unable to play the audio file<nl/><filename>%1</filename><nl/>"
                "for the following reason:<nl/><message>%2</message>",
500
              m_file.absFilePath(),
501
              mediaObject->errorString()
502 503
            );

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

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

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

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

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

527 528 529 530 531 532
    // 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()));
533 534

        JuK::JuKInstance()->setWindowTitle(i18n("JuK"));
535

Michael Pyne's avatar
Michael Pyne committed
536
        emit signalStop();
537
    }
538 539 540 541 542 543 544 545
    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);

546 547 548 549 550 551 552
                
        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()));
        
553 554
        emit signalPlay();
    }
555 556
}

557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
void PlayerManager::slotSeekableChanged(bool isSeekable)
{
    // Use sender() since either media object may have sent the signal.
    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(!mediaObject)
        return;
    if(mediaObject != m_media[m_curOutputPath])
        return;

    emit seekableChanged(isSeekable);
}

void PlayerManager::slotMutedChanged(bool muted)
{
    // Use sender() since either output object may have sent the signal.
    Phonon::AudioOutput *output = qobject_cast<Phonon::AudioOutput *>(sender());
    if(!output)
        return;

    if(output != m_output[m_curOutputPath])
        return;

    emit mutedChanged(muted);
}

void PlayerManager::slotVolumeChanged(qreal volume)
{
    // Use sender() since either output object may have sent the signal.
    Phonon::AudioOutput *output = qobject_cast<Phonon::AudioOutput *>(sender());
    if(!output)
        return;

    if(output != m_output[m_curOutputPath])
        return;

    emit volumeChanged(volume);
}

595 596 597 598 599 600 601 602
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

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

603 604 605
    if(!action("pause") ||
       !action("stop") ||
       !action("back") ||
606
       !action("forwardAlbum") ||
607 608
       !action("forward") ||
       !action("trackPositionAction"))
609
    {
610
        kWarning() << "Could not find all of the required actions.";
611 612 613
        return;
    }

614 615 616 617
    if(m_setup)
        return;
    m_setup = true;

618 619 620 621 622 623
    // 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);
624 625
        connect(m_output[i], SIGNAL(mutedChanged(bool)), SLOT(slotMutedChanged(bool)));
        connect(m_output[i], SIGNAL(volumeChanged(qreal)), SLOT(slotVolumeChanged(qreal)));
626

627 628 629
        m_media[i] = new Phonon::MediaObject(this);
        m_audioPath[i] = Phonon::createPath(m_media[i], m_output[i]);
        m_media[i]->setTickInterval(200);
630
        m_media[i]->setPrefinishMark(2000);
631

632 633 634 635 636
        // 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
637
        connect(m_media[i], SIGNAL(stateChanged(Phonon::State,Phonon::State)), SLOT(slotStateChanged(Phonon::State,Phonon::State)));
638
        connect(m_media[i], SIGNAL(prefinishMarkReached(qint32)), SLOT(slotNeedNextUrl()));
639 640 641
        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()));
642
        connect(m_media[i], SIGNAL(seekableChanged(bool)), SLOT(slotSeekableChanged(bool)));
643
    }
644

645 646
    // initialize action states

647 648 649 650
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
651
    action("forwardAlbum")->setEnabled(false);
652

653
    QDBusConnection::sessionBus().registerObject("/Player", this);
654 655 656
}

void PlayerManager::slotUpdateGuiIfStopped()
657
{
658 659 660
    if(m_media[0]->state() == Phonon::StoppedState && m_media[1]->state() == Phonon::StoppedState)
        stop();
}
661

662 663 664
void PlayerManager::crossfadeToFile(const FileHandle &newFile)
{
    int nextOutputPath = 1 - m_curOutputPath;
665

666 667 668
    // Don't need this anymore
    disconnect(m_media[m_curOutputPath], SIGNAL(finished()), this, 0);
    connect(m_media[nextOutputPath], SIGNAL(finished()), SLOT(slotFinished()));
669

670
    m_fader[nextOutputPath]->setVolume(0.0f);
671

672
    emit signalItemChanged(newFile);
673 674
    m_media[nextOutputPath]->setCurrentSource(newFile.absFilePath());
    m_media[nextOutputPath]->play();
675

676 677
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[m_curOutputPath]->fadeTo(0.0f, 2000);
678

679
    m_fader[nextOutputPath]->fadeTo(1.0f, 2000);
680

681 682
    m_curOutputPath = nextOutputPath;
}
683

684 685 686 687
void PlayerManager::stopCrossfade()
{
    // According to the Phonon docs, setVolume immediately takes effect,
    // which is "good enough for government work" ;)
688

689 690 691
    // 1 - curOutputPath is the other output path...
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[1 - m_curOutputPath]->setVolume(0.0f);
692

693 694 695 696 697 698 699 700
    // 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();
701 702
}

Michael Pyne's avatar
Michael Pyne committed
703 704 705 706 707 708 709 710 711
QString PlayerManager::randomPlayMode() const
{
    if(action<KToggleAction>("randomPlay")->isChecked())
        return "Random";
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        return "AlbumRandom";
    return "NoRandom";
}

712 713 714 715 716
void PlayerManager::setCrossfadeEnabled(bool crossfadeEnabled)
{
    m_crossfadeTracks = crossfadeEnabled;
}

Michael Pyne's avatar
Michael Pyne committed
717 718
void PlayerManager::setRandomPlayMode(const QString &randomMode)
{
719
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
720
        action<KToggleAction>("randomPlay")->setChecked(true);
721
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
722
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
723
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
724 725 726
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

727
#include "playermanager.moc"
728

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