playermanager.cpp 18 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 9 10

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

    copyright            : (C) 2008 by Michael Pyne
    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 24
#include "playermanager.h"
#include <config.h>

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

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

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

40 41
#include <math.h>

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

using namespace ActionCollection;
52

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

55 56 57 58 59
////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

PlayerManager::PlayerManager() :
60
    QObject(),
61
    m_sliderAction(0),
62
    m_playlistInterface(0),
63
    m_statusLabel(0),
64
    m_noSeek(false),
65
    m_muted(false),
66 67 68
    m_setup(false),
    m_output(0),
    m_media(0)
69
{
Michael Pyne's avatar
Michael Pyne committed
70 71 72 73 74
// 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
75
    new PlayerAdaptor( this );
76
    QDBusConnection::sessionBus().registerObject("/Player", this);
Laurent Montel's avatar
Laurent Montel committed
77

78 79 80 81 82 83 84 85 86 87 88 89
}

PlayerManager::~PlayerManager()
{
}

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

PlayerManager *PlayerManager::instance() // static
{
90 91
    static PlayerManager manager;
    return &manager;
92 93
}

94 95
bool PlayerManager::playing() const
{
96
    if(!m_media)
97 98
        return false;

99
    return (m_media->state() == Phonon::PlayingState || m_media->state() == Phonon::BufferingState);
100 101 102 103
}

bool PlayerManager::paused() const
{
104
    if(!m_media)
105 106
        return false;

107
    return m_media->state() == Phonon::PausedState;
108 109 110 111
}

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

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

118 119
int PlayerManager::status() const
{
120
    if(!m_media)
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
    if(!m_media)
135 136
        return 0;

137
    return m_media->totalTime() / 1000;
138 139
}

Scott Wheeler's avatar
Scott Wheeler committed
140
int PlayerManager::currentTime() const
141
{
142
    if(!m_media)
143 144
        return 0;

145
    return m_media->currentTime() / 1000;
146 147
}

148
/*
149 150
int PlayerManager::position() const
{
151
    if(!m_media)
152 153
        return 0;

154
    long curr = m_media->currentTime();
155 156
    if(curr > 0)
        return static_cast<int>(static_cast<float>(curr * SliderAction::maxPosition) / m_media->totalTime() + 0.5f);
157
    return -1;
158
}
159
*/
160

161 162 163 164 165 166 167
QStringList PlayerManager::trackProperties()
{
    return FileHandle::properties();
}

QString PlayerManager::trackProperty(const QString &property) const
{
Michael Pyne's avatar
Michael Pyne committed
168
    if(!playing() && !paused())
169
        return QString();
170 171 172 173

    return m_file.property(property);
}

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

179
    if(size.toLower() == "small")
Michael Pyne's avatar
Michael Pyne committed
180
        return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail);
181
    if(size.toLower() == "large")
Michael Pyne's avatar
Michael Pyne committed
182 183 184 185 186
        return m_file.coverInfo()->pixmap(CoverInfo::FullSize);

    return QPixmap();
}

187 188 189 190 191
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

192 193 194
QString PlayerManager::playingString() const
{
    if(!playing())
195
        return QString();
196

197 198 199 200 201
    QString str = m_file.tag()->artist() + " - " + m_file.tag()->title();
    if(m_file.tag()->artist().isEmpty())
        str = m_file.tag()->title();

    return str;
202 203
}

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

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

214 215 216 217
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

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

    if(!m_media || !m_playlistInterface)
224 225
        return;

226
    if(file.isNull()) {
227 228 229 230
        if(paused())
            m_media->play();
        else if(playing()) {
            m_media->seek(0);
231
        }
232
        else {
233
            m_playlistInterface->playNext();
234
            m_file = m_playlistInterface->currentFile();
235

236
            if(!m_file.isNull())
237
            {
Matthias Kretz's avatar
Matthias Kretz committed
238
                m_media->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
239 240
                m_media->play();
            }
241
        }
242
    }
243
    else {
244
        m_file = file;
Matthias Kretz's avatar
Matthias Kretz committed
245

246 247 248
        if(m_media->state() == Phonon::PlayingState)
            crossfadeToFile(m_file);
        else {
Matthias Kretz's avatar
Matthias Kretz committed
249 250 251
            m_media->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
            m_media->play();
        }
252
    }
253

254 255
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
256 257
}

258 259 260 261 262 263 264 265 266
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
267 268 269 270 271
void PlayerManager::play()
{
    play(FileHandle::null());
}

272 273
void PlayerManager::pause()
{
274
    if(!m_media)
275 276
        return;

277
    if(paused()) {
278 279 280 281
        play();
        return;
    }

282
    action("pause")->setEnabled(false);
283

284
    m_media->pause();
285 286

    emit signalPause();
287 288 289 290
}

void PlayerManager::stop()
{
291
    if(!m_media || !m_playlistInterface)
292 293
        return;

294 295 296 297
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
298
    action("forwardAlbum")->setEnabled(false);
299

300 301 302
    switch(m_media->state())
    {
    case Phonon::PlayingState:
303 304
        // Fall through

305
    case Phonon::BufferingState:
306 307 308 309 310 311 312 313 314 315
    {
        // Effect will be deleted once we actually stop playbackk.
        kDebug() << "Fading out playback.\n";
        Phonon::VolumeFaderEffect *fader = new Phonon::VolumeFaderEffect(m_media);

        fader->setFadeCurve(Phonon::VolumeFaderEffect::Fade9Decibel);
        fader->fadeOut(2000);

        m_audioPath.insertEffect(fader);
        QTimer::singleShot(2000, m_media, SLOT(stop()));
316
    }
317

318
    break;
319

320 321 322
    default:
        m_media->stop();
    }
323 324
}

Laurent Montel's avatar
Laurent Montel committed
325
void PlayerManager::setVolume(float volume)
326
{
327 328 329
    if(!m_output)
        setup();
    m_output->setVolume(volume);
330 331
}

Scott Wheeler's avatar
Scott Wheeler committed
332
void PlayerManager::seek(int seekTime)
333
{
334
    if(!m_media)
335
        return;
336

337
    m_media->seek(seekTime);
338 339
}

340
/*
341
void PlayerManager::seekPosition(int position)
342
{
343
    if(!m_media)
344 345
        return;

346
    if(!playing() || m_noSeek)
347
        return;
348

349
    slotUpdateTime(position);
350
    m_media->seek(static_cast<qint64>(static_cast<float>(m_media->totalTime() * position) / SliderAction::maxPosition + 0.5f));
351
}
352
*/
353 354 355

void PlayerManager::seekForward()
{
356 357 358
    const qint64 total = m_media->totalTime();
    const qint64 newtime = m_media->currentTime() + total / 100;
    m_media->seek(qMin(total, newtime));
359 360 361 362
}

void PlayerManager::seekBack()
{
363 364 365
    const qint64 total = m_media->totalTime();
    const qint64 newtime = m_media->currentTime() - total / 100;
    m_media->seek(qMax(qint64(0), newtime));
366 367
}

368 369
void PlayerManager::playPause()
{
David Faure's avatar
David Faure committed
370
    playing() ? action("pause")->trigger() : action("play")->trigger();
371 372
}

373 374
void PlayerManager::forward()
{
375 376
    m_playlistInterface->playNext();
    FileHandle file = m_playlistInterface->currentFile();
377

378 379 380 381 382 383 384 385
    if(!file.isNull())
        play(file);
    else
        stop();
}

void PlayerManager::back()
{
386 387
    m_playlistInterface->playPrevious();
    FileHandle file = m_playlistInterface->currentFile();
388

389 390 391 392 393 394
    if(!file.isNull())
        play(file);
    else
        stop();
}

395 396
void PlayerManager::volumeUp()
{
397
    if(!m_output)
398
        return;
399

400 401
    float volume = m_output->volume() + 0.04; // 4% up
    m_output->setVolume(volume);
402 403 404 405
}

void PlayerManager::volumeDown()
{
406
    if(!m_output)
407
        return;
408

409 410
    float volume = m_output->volume() - 0.04; // 4% up
    m_output->setVolume(volume);
411 412 413 414
}

void PlayerManager::mute()
{
415
    if(!m_output)
416 417
        return;

418
    m_output->setMuted(!m_output->isMuted());
419 420
}

421 422 423
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
424

425 426
void PlayerManager::slotNeedNextUrl()
{
Matthias Kretz's avatar
Matthias Kretz committed
427 428 429 430
    if(m_file.isNull())
    {
        return;
    }
431 432 433 434
    m_playlistInterface->playNext();
    FileHandle nextFile = m_playlistInterface->currentFile();
    if(!nextFile.isNull())
    {
Thiago Macieira's avatar
Thiago Macieira committed
435
        //kDebug() << m_file.absFilePath();
436
        m_file = nextFile;
Matthias Kretz's avatar
Matthias Kretz committed
437
        m_media->enqueue(KUrl::fromPath(m_file.absFilePath()));
438 439

        emit signalPlay();
440 441 442
    }
}

443 444
void PlayerManager::slotFinished()
{
445 446 447 448 449 450 451 452 453 454 455
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
    action("forwardAlbum")->setEnabled(false);

    m_playlistInterface->stop();

    m_file = FileHandle::null();

    emit signalStop();
456 457
}

458 459 460 461 462
void PlayerManager::slotLength(qint64 msec)
{
    m_statusLabel->setItemTotalTime(msec / 1000);
}

463
void PlayerManager::slotTick(qint64 msec)
464
{
465
    if(!m_media || !m_playlistInterface)
466
        return;
467

468
    m_noSeek = true;
469

470 471
    if(m_statusLabel) {
        m_statusLabel->setItemCurrentTime(msec / 1000);
472
    }
473

474
    m_noSeek = false;
475 476
}

Matthias Kretz's avatar
Matthias Kretz committed
477 478
void PlayerManager::slotStateChanged(Phonon::State newstate)
{
479 480
    if(newstate == Phonon::ErrorState) {
        switch(m_media->errorType()) {
481 482 483
            case Phonon::NoError:
                kDebug() << "received a state change to ErrorState but errorType is NoError!?";
                break;
484

Matthias Kretz's avatar
Matthias Kretz committed
485 486 487 488
            case Phonon::NormalError:
                forward();
                KMessageBox::information(0, m_media->errorString());
                break;
489

Matthias Kretz's avatar
Matthias Kretz committed
490 491 492 493 494 495 496
            case Phonon::FatalError:
                // stop playback
                stop();
                KMessageBox::sorry(0, m_media->errorString());
                break;
        }
    }
497 498 499 500 501 502 503 504 505 506 507 508
    else if(newstate == Phonon::StoppedState) {
        // Remove all effects from the current path.
        QList<Phonon::Effect *> effects = m_audioPath.effects();
        foreach(Phonon::Effect *effect, effects)
            delete effect;

        m_playlistInterface->stop();

        m_file = FileHandle::null();

        emit signalStop();
    }
509 510 511 512 513 514 515 516 517 518
    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);

        emit signalPlay();
    }
519 520 521 522 523 524 525 526 527 528
}

void PlayerManager::slotKillSender()
{
    // When called as a slot the sender() property has a QObject that called us.
    // If it's a MediaObject we should delete it to clean up from crossfading.
    // Note: This is not a good general practice (deleting our senders) and is dependant on what
    // MediaObject does.  So we use deleteLater() to delete at some later time.
    if(qobject_cast<Phonon::MediaObject *>(sender()))
        sender()->deleteLater();
Matthias Kretz's avatar
Matthias Kretz committed
529 530
}

531
/*
532 533 534 535 536
void PlayerManager::slotUpdateTime(int position)
{
    if(!m_statusLabel)
        return;

537
    float positionFraction = float(position) / SliderAction::maxPosition;
538
    float totalTime = float(m_media->totalTime()) / 1000.0f;
Scott Wheeler's avatar
Scott Wheeler committed
539
    int seekTime = int(positionFraction * totalTime + 0.5); // "+0.5" for rounding
540 541 542

    m_statusLabel->setItemCurrentTime(seekTime);
}
543
*/
544

545 546 547 548 549 550 551 552
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

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

553 554 555
    if(!action("pause") ||
       !action("stop") ||
       !action("back") ||
556
       !action("forwardAlbum") ||
557 558
       !action("forward") ||
       !action("trackPositionAction"))
559
    {
Thiago Macieira's avatar
Thiago Macieira committed
560
        kWarning(65432) << "Could not find all of the required actions.";
561 562 563
        return;
    }

564 565 566 567
    if(m_setup)
        return;
    m_setup = true;

568
    m_output = new Phonon::AudioOutput(Phonon::MusicCategory, this);
569

Matthias Kretz's avatar
Matthias Kretz committed
570
    m_media = new Phonon::MediaObject(this);
Matthias Kretz's avatar
Matthias Kretz committed
571
    connect(m_media, SIGNAL(stateChanged(Phonon::State, Phonon::State)), SLOT(slotStateChanged(Phonon::State)));
Matthias Kretz's avatar
Matthias Kretz committed
572
    connect(m_media, SIGNAL(aboutToFinish()), SLOT(slotNeedNextUrl()));
573
    m_audioPath = Phonon::createPath(m_media, m_output);
574
    m_media->setTickInterval(200);
575

576 577
    // initialize action states

578 579 580 581
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
582
    action("forwardAlbum")->setEnabled(false);
583

584 585
    // setup sliders

586
    m_sliderAction = action<SliderAction>("trackPositionAction");
587

588
    /*
589 590 591 592
    connect(m_sliderAction, SIGNAL(signalPositionChanged(int)),
            this, SLOT(seekPosition(int)));
    connect(m_sliderAction->trackPositionSlider(), SIGNAL(valueChanged(int)),
            this, SLOT(slotUpdateTime(int)));
593
            */
594

595 596
    if(m_sliderAction->trackPositionSlider())
    {
Matthias Kretz's avatar
Matthias Kretz committed
597
        m_sliderAction->trackPositionSlider()->setMediaObject(m_media);
598 599 600 601
    }
    if(m_sliderAction->volumeSlider())
    {
        m_sliderAction->volumeSlider()->setAudioOutput(m_output);
602
    }
603

Matthias Kretz's avatar
Matthias Kretz committed
604
    connect(m_media, SIGNAL(totalTimeChanged(qint64)), SLOT(slotLength(qint64)));
605
    connect(m_media, SIGNAL(tick(qint64)), SLOT(slotTick(qint64)));
606
    connect(m_media, SIGNAL(finished()), SLOT(slotFinished()));
607 608
}

609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
void PlayerManager::crossfadeToFile(const FileHandle &newFile)
{
    // Crossfade by introducting a second MediaObject, inserting appropriate effects in the new
    // and old MediaObject, and letting the old MediaObject die a graceful death afterwards.

    Phonon::VolumeFaderEffect *fader1 = new Phonon::VolumeFaderEffect(m_media);
    Phonon::Path oldPath(m_audioPath);
    Phonon::MediaObject *mo = m_media;
    Phonon::AudioOutput *out = m_output;

    mo->disconnect(this);
    connect(mo, SIGNAL(finished()), SLOT(slotKillSender()));

    m_media = new Phonon::MediaObject(this);
    m_output = new Phonon::AudioOutput(Phonon::MusicCategory, this);

    // Have the old MediaObject own this to control its lifetime.
    Phonon::VolumeFaderEffect *fader2 = new Phonon::VolumeFaderEffect(mo);
    out->setParent(mo); // Likewise change parent

    m_audioPath = Phonon::createPath(m_media, m_output);
    m_output->setVolume(out->volume());
    m_output->setMuted(out->isMuted());
    m_media->setTickInterval(200);

    // Reconnect everything
    connect(m_media, SIGNAL(stateChanged(Phonon::State, Phonon::State)), SLOT(slotStateChanged(Phonon::State)));
    connect(m_media, SIGNAL(aboutToFinish()), SLOT(slotNeedNextUrl()));
    if(m_sliderAction->trackPositionSlider())
        m_sliderAction->trackPositionSlider()->setMediaObject(m_media);
    connect(m_media, SIGNAL(totalTimeChanged(qint64)), SLOT(slotLength(qint64)));
    connect(m_media, SIGNAL(tick(qint64)), SLOT(slotTick(qint64)));
    connect(m_media, SIGNAL(finished()), SLOT(slotFinished()));

    // If we're already fading in (VolumeFaderEffect present in out->effects()) then start from there.
    QList<Phonon::Effect *> effects(oldPath.effects());

    float initialVolume = out->volume();
    if(!effects.isEmpty()) {
        Phonon::VolumeFaderEffect *fader = qobject_cast<Phonon::VolumeFaderEffect *>(effects.front());
        if(fader)
            initialVolume = fader->volume();

        foreach(Phonon::Effect *effect, effects)
            delete effect; // Sayonara
    }

    fader1->setVolume(initialVolume);
    fader1->fadeTo(0.0f, 2000);

    fader2->setVolume(0.0f);
    fader2->fadeTo(m_output->volume(), 2000);

    QTimer::singleShot(2500, fader2, SLOT(deleteLater()));

    m_audioPath.insertEffect(fader2);
    oldPath.insertEffect(fader1);

    m_media->setCurrentSource(newFile.absFilePath());

    m_media->play();

    if(m_sliderAction->trackPositionSlider())
        m_sliderAction->trackPositionSlider()->setMediaObject(m_media);

    if(m_sliderAction->volumeSlider())
        m_sliderAction->volumeSlider()->setAudioOutput(m_output);
}

Michael Pyne's avatar
Michael Pyne committed
678 679 680 681 682 683 684 685 686 687 688
QString PlayerManager::randomPlayMode() const
{
    if(action<KToggleAction>("randomPlay")->isChecked())
        return "Random";
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        return "AlbumRandom";
    return "NoRandom";
}

void PlayerManager::setRandomPlayMode(const QString &randomMode)
{
689
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
690
        action<KToggleAction>("randomPlay")->setChecked(true);
691
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
692
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
693
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
694 695 696
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

697
#include "playermanager.moc"
698

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