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
    // Make sure that the player() actually starts before doing anything.
255

256
    if(!playing()) {
257
        kWarning(65432) << "Unable to play " << file.absFilePath();
258 259 260
        stop();
        return;
    }
261

262 263 264
    action("pause")->setEnabled(true);
    action("stop")->setEnabled(true);
    action("forward")->setEnabled(true);
265 266
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        action("forwardAlbum")->setEnabled(true);
267
    action("back")->setEnabled(true);
268

269
    emit signalPlay();
270 271
}

272 273 274 275 276 277 278 279 280
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
281 282 283 284 285
void PlayerManager::play()
{
    play(FileHandle::null());
}

286 287
void PlayerManager::pause()
{
288
    if(!m_media)
289 290
        return;

291
    if(paused()) {
292 293 294 295
        play();
        return;
    }

296
    action("pause")->setEnabled(false);
297

298
    m_media->pause();
299 300

    emit signalPause();
301 302 303 304
}

void PlayerManager::stop()
{
305
    if(!m_media || !m_playlistInterface)
306 307
        return;

308 309 310 311
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
312
    action("forwardAlbum")->setEnabled(false);
313

314 315 316
    switch(m_media->state())
    {
    case Phonon::PlayingState:
317 318
        // Fall through

319
    case Phonon::BufferingState:
320 321 322 323 324 325 326 327 328 329
    {
        // 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()));
330
    }
331

332
    break;
333

334 335 336
    default:
        m_media->stop();
    }
337 338
}

Laurent Montel's avatar
Laurent Montel committed
339
void PlayerManager::setVolume(float volume)
340
{
341 342 343
    if(!m_output)
        setup();
    m_output->setVolume(volume);
344 345
}

Scott Wheeler's avatar
Scott Wheeler committed
346
void PlayerManager::seek(int seekTime)
347
{
348
    if(!m_media)
349
        return;
350

351
    m_media->seek(seekTime);
352 353
}

354
/*
355
void PlayerManager::seekPosition(int position)
356
{
357
    if(!m_media)
358 359
        return;

360
    if(!playing() || m_noSeek)
361
        return;
362

363
    slotUpdateTime(position);
364
    m_media->seek(static_cast<qint64>(static_cast<float>(m_media->totalTime() * position) / SliderAction::maxPosition + 0.5f));
365
}
366
*/
367 368 369

void PlayerManager::seekForward()
{
370 371 372
    const qint64 total = m_media->totalTime();
    const qint64 newtime = m_media->currentTime() + total / 100;
    m_media->seek(qMin(total, newtime));
373 374 375 376
}

void PlayerManager::seekBack()
{
377 378 379
    const qint64 total = m_media->totalTime();
    const qint64 newtime = m_media->currentTime() - total / 100;
    m_media->seek(qMax(qint64(0), newtime));
380 381
}

382 383
void PlayerManager::playPause()
{
David Faure's avatar
David Faure committed
384
    playing() ? action("pause")->trigger() : action("play")->trigger();
385 386
}

387 388
void PlayerManager::forward()
{
389 390
    m_playlistInterface->playNext();
    FileHandle file = m_playlistInterface->currentFile();
391

392 393 394 395 396 397 398 399
    if(!file.isNull())
        play(file);
    else
        stop();
}

void PlayerManager::back()
{
400 401
    m_playlistInterface->playPrevious();
    FileHandle file = m_playlistInterface->currentFile();
402

403 404 405 406 407 408
    if(!file.isNull())
        play(file);
    else
        stop();
}

409 410
void PlayerManager::volumeUp()
{
411
    if(!m_output)
412
        return;
413

414 415
    float volume = m_output->volume() + 0.04; // 4% up
    m_output->setVolume(volume);
416 417 418 419
}

void PlayerManager::volumeDown()
{
420
    if(!m_output)
421
        return;
422

423 424
    float volume = m_output->volume() - 0.04; // 4% up
    m_output->setVolume(volume);
425 426 427 428
}

void PlayerManager::mute()
{
429
    if(!m_output)
430 431
        return;

432
    m_output->setMuted(!m_output->isMuted());
433 434
}

435 436 437
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
438

439 440
void PlayerManager::slotNeedNextUrl()
{
Matthias Kretz's avatar
Matthias Kretz committed
441 442 443 444
    if(m_file.isNull())
    {
        return;
    }
445 446 447 448
    m_playlistInterface->playNext();
    FileHandle nextFile = m_playlistInterface->currentFile();
    if(!nextFile.isNull())
    {
Thiago Macieira's avatar
Thiago Macieira committed
449
        //kDebug() << m_file.absFilePath();
450
        m_file = nextFile;
Matthias Kretz's avatar
Matthias Kretz committed
451
        m_media->enqueue(KUrl::fromPath(m_file.absFilePath()));
452 453

        emit signalPlay();
454 455 456
    }
}

457 458
void PlayerManager::slotFinished()
{
459 460 461 462 463 464 465 466 467 468 469
    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();
470 471
}

472 473 474 475 476
void PlayerManager::slotLength(qint64 msec)
{
    m_statusLabel->setItemTotalTime(msec / 1000);
}

477
void PlayerManager::slotTick(qint64 msec)
478
{
479
    if(!m_media || !m_playlistInterface)
480
        return;
481

482
    m_noSeek = true;
483

484 485
    if(m_statusLabel) {
        m_statusLabel->setItemCurrentTime(msec / 1000);
486
    }
487

488
    m_noSeek = false;
489 490
}

Matthias Kretz's avatar
Matthias Kretz committed
491 492
void PlayerManager::slotStateChanged(Phonon::State newstate)
{
493 494
    if(newstate == Phonon::ErrorState) {
        switch(m_media->errorType()) {
495 496 497
            case Phonon::NoError:
                kDebug() << "received a state change to ErrorState but errorType is NoError!?";
                break;
498

Matthias Kretz's avatar
Matthias Kretz committed
499 500 501 502
            case Phonon::NormalError:
                forward();
                KMessageBox::information(0, m_media->errorString());
                break;
503

Matthias Kretz's avatar
Matthias Kretz committed
504 505 506 507 508 509 510
            case Phonon::FatalError:
                // stop playback
                stop();
                KMessageBox::sorry(0, m_media->errorString());
                break;
        }
    }
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
    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();
    }
}

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
533 534
}

535
/*
536 537 538 539 540
void PlayerManager::slotUpdateTime(int position)
{
    if(!m_statusLabel)
        return;

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

    m_statusLabel->setItemCurrentTime(seekTime);
}
547
*/
548

549 550 551 552 553 554 555 556
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

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

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

568 569 570 571
    if(m_setup)
        return;
    m_setup = true;

572
    m_output = new Phonon::AudioOutput(Phonon::MusicCategory, this);
573

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

580 581
    // initialize action states

582 583 584 585
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
586
    action("forwardAlbum")->setEnabled(false);
587

588 589
    // setup sliders

590
    m_sliderAction = action<SliderAction>("trackPositionAction");
591

592
    /*
593 594 595 596
    connect(m_sliderAction, SIGNAL(signalPositionChanged(int)),
            this, SLOT(seekPosition(int)));
    connect(m_sliderAction->trackPositionSlider(), SIGNAL(valueChanged(int)),
            this, SLOT(slotUpdateTime(int)));
597
            */
598

599 600
    if(m_sliderAction->trackPositionSlider())
    {
Matthias Kretz's avatar
Matthias Kretz committed
601
        m_sliderAction->trackPositionSlider()->setMediaObject(m_media);
602 603 604 605
    }
    if(m_sliderAction->volumeSlider())
    {
        m_sliderAction->volumeSlider()->setAudioOutput(m_output);
606
    }
607

Matthias Kretz's avatar
Matthias Kretz committed
608
    connect(m_media, SIGNAL(totalTimeChanged(qint64)), SLOT(slotLength(qint64)));
609
    connect(m_media, SIGNAL(tick(qint64)), SLOT(slotTick(qint64)));
610
    connect(m_media, SIGNAL(finished()), SLOT(slotFinished()));
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 678 679 680 681
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
682 683 684 685 686 687 688 689 690 691 692
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)
{
693
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
694
        action<KToggleAction>("randomPlay")->setChecked(true);
695
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
696
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
697
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
698 699 700
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

701
#include "playermanager.moc"
702

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