playermanager.cpp 19.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/**
 * Copyright (C) 2004 Scott Wheeler <wheeler@kde.org>
 * Copyright (C) 2007 Matthias Kretz <kretz@kde.org>
 * Copyright (C) 2008, 2009 Michael Pyne <mpyne@kde.org>
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
18

19 20
#include "playermanager.h"

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

29 30 31
#include <Phonon/AudioOutput>
#include <Phonon/MediaObject>
#include <Phonon/VolumeFaderEffect>
32

Laurent Montel's avatar
Laurent Montel committed
33
#include <QPixmap>
34 35
#include <QTimer>

36 37
#include <math.h>

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

using namespace ActionCollection;
50

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

53 54 55 56 57
////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

PlayerManager::PlayerManager() :
58
    QObject(),
59
    m_playlistInterface(0),
60
    m_statusLabel(0),
61
    m_setup(false),
62
    m_crossfadeTracks(true),
63
    m_curOutputPath(0)
64
{
Michael Pyne's avatar
Michael Pyne committed
65 66 67 68 69
// 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
Port it  
Laurent Montel committed
70
    new PlayerAdaptor( this );
71 72 73 74
}

PlayerManager::~PlayerManager()
{
75

76 77 78 79 80 81
}

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

82 83
bool PlayerManager::playing() const
{
84
    if(!m_setup)
85 86
        return false;

87 88
    Phonon::State state = m_media[m_curOutputPath]->state();
    return (state == Phonon::PlayingState || state == Phonon::BufferingState);
89 90 91 92
}

bool PlayerManager::paused() const
{
93
    if(!m_setup)
94 95
        return false;

96
    return m_media[m_curOutputPath]->state() == Phonon::PausedState;
97 98
}

99 100 101 102 103 104 105 106
bool PlayerManager::muted() const
{
    if(!m_setup)
        return false;

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

107 108
float PlayerManager::volume() const
{
109
    if(!m_setup)
110
        return 1.0;
111

112
    return m_output[m_curOutputPath]->volume();
113 114
}

115 116
int PlayerManager::status() const
{
117
    if(!m_setup)
118
        return StatusStopped;
119

120
    if(paused())
121
        return StatusPaused;
122

123
    if(playing())
124
        return StatusPlaying;
125

126 127 128
    return 0;
}

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

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

int PlayerManager::totalTimeMSecs() const
140
{
141
    if(!m_setup)
142 143
        return 0;

144
    return m_media[m_curOutputPath]->totalTime();
145 146
}

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

152
    return m_media[m_curOutputPath]->currentTime();
153 154
}

155 156 157 158 159 160 161 162
bool PlayerManager::seekable() const
{
    if(!m_setup)
        return false;

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

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

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

    return m_file.property(property);
}

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

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

    return QPixmap();
}

189 190 191 192 193
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

194 195
QString PlayerManager::playingString() const
{
196
    if(!playing() || m_file.isNull())
197
        return QString();
198

199
    return m_file.tag()->playingString();
200 201
}

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

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

212 213 214 215
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

216
void PlayerManager::play(const FileHandle &file)
217
{
218
    if(!m_setup)
219 220
        setup();

221
    if(!m_media[0] || !m_media[1] || !m_playlistInterface)
222 223
        return;

224 225 226 227
    stopCrossfade();

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

240
            if(!m_file.isNull())
241
            {
242 243
                mediaObject->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
                mediaObject->play();
244 245

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

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

        m_file = file;
257
    }
258

259 260
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
261 262
}

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

277 278
void PlayerManager::pause()
{
279
    if(!m_setup)
280 281
        return;

282
    if(paused()) {
283 284 285 286
        play();
        return;
    }

287
    action("pause")->setEnabled(false);
288

289
    m_media[m_curOutputPath]->pause();
290 291

    emit signalPause();
292 293 294 295
}

void PlayerManager::stop()
{
296
    if(!m_setup || !m_playlistInterface)
297 298
        return;

299 300 301 302
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
303
    action("forwardAlbum")->setEnabled(false);
304

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

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

Laurent Montel's avatar
Laurent Montel committed
316
void PlayerManager::setVolume(float volume)
317
{
318
    if(!m_setup)
319
        setup();
320

321 322
    m_output[0]->setVolume(volume);
    m_output[1]->setVolume(volume);
323 324
}

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

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

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

    stopCrossfade();
345 346
    mediaObject->seek(seekTo);
    emit seeked(seekTo);
347 348 349 350
}

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

    stopCrossfade();
357 358
    mediaObject->seek(seekTo);
    emit seeked(seekTo);
359 360
}

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

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

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

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

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

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

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

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

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

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

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

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

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

422 423 424
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
425

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

431 432
    m_playlistInterface->playNext();
    FileHandle nextFile = m_playlistInterface->currentFile();
433 434

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

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

453 454
    m_playlistInterface->playNext();
    m_file = m_playlistInterface->currentFile();
455

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

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

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

477
    if(m_statusLabel)
478
        m_statusLabel->setItemCurrentTime(msec / 1000);
479 480

    emit tick(msec);
481 482
}

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

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

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

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

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

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

524 525 526 527 528 529
    // 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()));
530 531

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

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

543 544 545 546 547 548 549
                
        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()));
        
550 551
        emit signalPlay();
    }
552 553
}

554 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
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);
}

592 593 594 595 596 597 598 599
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

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

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

611 612 613 614
    if(m_setup)
        return;
    m_setup = true;

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

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

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

642 643
    // initialize action states

644 645 646 647
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
648
    action("forwardAlbum")->setEnabled(false);
649

650
    QDBusConnection::sessionBus().registerObject("/Player", this);
651 652 653
}

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

659 660 661
void PlayerManager::crossfadeToFile(const FileHandle &newFile)
{
    int nextOutputPath = 1 - m_curOutputPath;
662

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

667
    m_fader[nextOutputPath]->setVolume(0.0f);
668

669
    emit signalItemChanged(newFile);
670
    m_media[nextOutputPath]->setCurrentSource(QUrl::fromLocalFile(newFile.absFilePath()));
671
    m_media[nextOutputPath]->play();
672

673 674
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[m_curOutputPath]->fadeTo(0.0f, 2000);
675

676
    m_fader[nextOutputPath]->fadeTo(1.0f, 2000);
677

678 679
    m_curOutputPath = nextOutputPath;
}
680

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

686 687 688
    // 1 - curOutputPath is the other output path...
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[1 - m_curOutputPath]->setVolume(0.0f);
689

690 691 692 693 694 695 696 697
    // 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();
698 699
}

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

709 710 711 712 713
void PlayerManager::setCrossfadeEnabled(bool crossfadeEnabled)
{
    m_crossfadeTracks = crossfadeEnabled;
}

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

724
#include "playermanager.moc"
725

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