/** * SPDX-FileCopyrightText: 2017 (c) Matthieu Gallien * SPDX-FileCopyrightText: 2021 Bart De Vries * * SPDX-License-Identifier: LGPL-3.0-or-later */ #include "audiomanager.h" #include #include #include #include #include #include #include "audiologging.h" #include "datamanager.h" #include "feed.h" #include "models/errorlogmodel.h" #include "powermanagementinterface.h" #include "settingsmanager.h" class AudioManagerPrivate { private: PowerManagementInterface mPowerInterface; QMediaPlayer m_player; Entry *m_entry = nullptr; bool m_readyToPlay = false; bool m_isSeekable = false; bool m_continuePlayback = false; // sort of lock mutex to prevent updating the player position while changing // sources (which will emit lots of playerPositionChanged signals) bool m_lockPositionSaving = false; // m_pendingSeek is used to indicate whether a seek action is still pending // * -1 corresponds to no seek action pending // * any positive value indicates that a seek to position=m_pendingSeek is // still pending qint64 m_pendingSeek = -1; QTimer *m_sleepTimer = nullptr; qint64 m_sleepTime = -1; qint64 m_remainingSleepTime = -1; friend class AudioManager; }; AudioManager::AudioManager(QObject *parent) : QObject(parent) , d(std::make_unique()) { connect(&d->m_player, &QMediaPlayer::mutedChanged, this, &AudioManager::playerMutedChanged); connect(&d->m_player, &QMediaPlayer::volumeChanged, this, &AudioManager::playerVolumeChanged); connect(&d->m_player, &QMediaPlayer::mediaChanged, this, &AudioManager::sourceChanged); connect(&d->m_player, &QMediaPlayer::mediaStatusChanged, this, &AudioManager::statusChanged); connect(&d->m_player, &QMediaPlayer::mediaStatusChanged, this, &AudioManager::mediaStatusChanged); connect(&d->m_player, &QMediaPlayer::stateChanged, this, &AudioManager::playbackStateChanged); connect(&d->m_player, &QMediaPlayer::stateChanged, this, &AudioManager::playerStateChanged); connect(&d->m_player, &QMediaPlayer::playbackRateChanged, this, &AudioManager::playbackRateChanged); connect(&d->m_player, QOverload::of(&QMediaPlayer::error), this, &AudioManager::errorChanged); connect(&d->m_player, &QMediaPlayer::durationChanged, this, &AudioManager::playerDurationChanged); connect(&d->m_player, &QMediaPlayer::positionChanged, this, &AudioManager::positionChanged); connect(this, &AudioManager::positionChanged, this, &AudioManager::savePlayPosition); connect(this, &AudioManager::playbackRateChanged, &DataManager::instance(), &DataManager::playbackRateChanged); connect(&DataManager::instance(), &DataManager::queueEntryMoved, this, &AudioManager::canGoNextChanged); connect(&DataManager::instance(), &DataManager::queueEntryAdded, this, &AudioManager::canGoNextChanged); connect(&DataManager::instance(), &DataManager::queueEntryRemoved, this, &AudioManager::canGoNextChanged); // we'll send custom seekableChanged signal to work around QMediaPlayer glitches connect(this, &AudioManager::logError, &ErrorLogModel::instance(), &ErrorLogModel::monitorErrorMessages); // Check if an entry was playing when the program was shut down and restore it if (DataManager::instance().lastPlayingEntry() != QStringLiteral("none")) { setEntry(DataManager::instance().getEntry(DataManager::instance().lastPlayingEntry())); } } AudioManager::~AudioManager() { d->mPowerInterface.setPreventSleep(false); } Entry *AudioManager::entry() const { return d->m_entry; } bool AudioManager::muted() const { return d->m_player.isMuted(); } qreal AudioManager::volume() const { auto realVolume = static_cast(d->m_player.volume() / 100.0); auto userVolume = static_cast(QAudio::convertVolume(realVolume, QAudio::LinearVolumeScale, QAudio::LogarithmicVolumeScale)); return userVolume * 100.0; } QUrl AudioManager::source() const { return d->m_player.media().request().url(); } QMediaPlayer::Error AudioManager::error() const { if (d->m_player.error() != QMediaPlayer::NoError) { qCDebug(kastsAudio) << "AudioManager::error" << d->m_player.errorString(); // Some error occurred: probably best to unset the lastPlayingEntry to // avoid a deadlock when starting up again. DataManager::instance().setLastPlayingEntry(QStringLiteral("none")); } return d->m_player.error(); } qint64 AudioManager::duration() const { // we fake the duration in case the track has not been properly loaded yet if (d->m_player.duration() > 0) { return d->m_player.duration(); } else if (d->m_entry && d->m_entry->enclosure()) { return d->m_entry->enclosure()->duration() * 1000; } else { return 0; } } qint64 AudioManager::position() const { // we fake the player position in case there is still a pending seek if (d->m_pendingSeek != -1) { return d->m_pendingSeek; } else { return d->m_player.position(); } } bool AudioManager::seekable() const { return d->m_isSeekable; } bool AudioManager::canPlay() const { return (d->m_readyToPlay); } bool AudioManager::canPause() const { return (d->m_readyToPlay); } bool AudioManager::canSkipForward() const { return (d->m_readyToPlay); } bool AudioManager::canSkipBackward() const { return (d->m_readyToPlay); } QMediaPlayer::State AudioManager::playbackState() const { return d->m_player.state(); } qreal AudioManager::playbackRate() const { return d->m_player.playbackRate(); } qreal AudioManager::minimumPlaybackRate() const { return MIN_RATE; } qreal AudioManager::maximumPlaybackRate() const { return MAX_RATE; } QMediaPlayer::MediaStatus AudioManager::status() const { return d->m_player.mediaStatus(); } void AudioManager::setEntry(Entry *entry) { // First unset current track, such that any signal that fires doesn't // operate on the wrong track Entry *oldEntry = d->m_entry; d->m_entry = nullptr; // reset any pending seek action, lock position saving and notify interval d->m_pendingSeek = -1; d->m_lockPositionSaving = true; d->m_player.setNotifyInterval(1000); // First check if the previous track needs to be marked as read // TODO: make grace time a setting in SettingsManager if (oldEntry) { qCDebug(kastsAudio) << "Checking previous track"; qCDebug(kastsAudio) << "Left time" << (duration() - position()); qCDebug(kastsAudio) << "MediaStatus" << d->m_player.mediaStatus(); if (((duration() > 0) && (position() > 0) && ((duration() - position()) < SKIP_TRACK_END)) || (d->m_player.mediaStatus() == QMediaPlayer::EndOfMedia)) { qCDebug(kastsAudio) << "Mark as read:" << oldEntry->title(); oldEntry->enclosure()->setPlayPosition(0); oldEntry->setRead(true); d->m_continuePlayback = SettingsManager::self()->continuePlayingNextEntry(); } } // do some checks on the new entry to see whether it's valid and not corrupted if (entry != nullptr && entry->hasEnclosure() && entry->enclosure() && entry->enclosure()->status() == Enclosure::Downloaded) { qCDebug(kastsAudio) << "Going to change source"; d->m_entry = entry; Q_EMIT entryChanged(entry); // the gst-pipeline is required to make sure that the pitch is not // changed when speeding up the audio stream // TODO: find a solution for Android (GStreamer not available on android by default) #if !defined Q_OS_ANDROID && !defined Q_OS_WIN qCDebug(kastsAudio) << "use custom pipeline"; d->m_player.setMedia(QUrl(QStringLiteral("gst-pipeline: playbin uri=file://") + d->m_entry->enclosure()->path() + QStringLiteral(" audio_sink=\"scaletempo ! audioconvert ! audioresample ! autoaudiosink\" video_sink=\"fakevideosink\""))); #else qCDebug(kastsAudio) << "regular audio backend"; d->m_player.setMedia(QUrl::fromLocalFile(d->m_entry->enclosure()->path())); #endif // save the current playing track in the settingsfile for restoring on startup DataManager::instance().setLastPlayingEntry(d->m_entry->id()); qCDebug(kastsAudio) << "Changed source to" << d->m_entry->title(); // call method which will try to make sure that the stream will skip to // the previously save position and make sure that the duration and // position are reported correctly prepareAudio(); } else { DataManager::instance().setLastPlayingEntry(QStringLiteral("none")); d->m_entry = nullptr; Q_EMIT entryChanged(nullptr); d->m_player.stop(); d->m_player.setMedia(nullptr); d->m_readyToPlay = false; Q_EMIT durationChanged(0); Q_EMIT positionChanged(0); Q_EMIT canPlayChanged(); Q_EMIT canPauseChanged(); Q_EMIT canSkipForwardChanged(); Q_EMIT canSkipBackwardChanged(); Q_EMIT canGoNextChanged(); d->m_isSeekable = false; Q_EMIT seekableChanged(false); } } void AudioManager::setMuted(bool muted) { d->m_player.setMuted(muted); } void AudioManager::setVolume(qreal volume) { qCDebug(kastsAudio) << "AudioManager::setVolume" << volume; auto realVolume = static_cast(QAudio::convertVolume(volume / 100.0, QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale)); d->m_player.setVolume(qRound(realVolume * 100)); } /* void AudioManager::setSource(const QUrl &source) { //qCDebug(kastsAudio) << "AudioManager::setSource" << source; d->m_player.setMedia({source}); } */ void AudioManager::setPosition(qint64 position) { qCDebug(kastsAudio) << "AudioManager::setPosition" << position; seek(position); } void AudioManager::setPlaybackRate(const qreal rate) { qCDebug(kastsAudio) << "AudioManager::setPlaybackRate" << rate; d->m_player.setPlaybackRate(rate); } void AudioManager::play() { qCDebug(kastsAudio) << "AudioManager::play"; // setting m_continuePlayback will make sure that, if the audio stream is // still being prepared, that the playback will start once it's ready d->m_continuePlayback = true; d->m_player.play(); d->m_isSeekable = true; Q_EMIT seekableChanged(d->m_isSeekable); d->mPowerInterface.setPreventSleep(true); } void AudioManager::pause() { qCDebug(kastsAudio) << "AudioManager::pause"; // setting m_continuePlayback will make sure that, if the audio stream is // still being prepared, that the playback will pause once it's ready d->m_continuePlayback = false; d->m_isSeekable = true; d->m_player.pause(); d->mPowerInterface.setPreventSleep(false); } void AudioManager::playPause() { if (playbackState() == QMediaPlayer::State::PausedState) play(); else if (playbackState() == QMediaPlayer::State::PlayingState) pause(); } void AudioManager::stop() { qCDebug(kastsAudio) << "AudioManager::stop"; d->m_player.stop(); d->m_continuePlayback = false; d->m_isSeekable = false; Q_EMIT seekableChanged(d->m_isSeekable); d->mPowerInterface.setPreventSleep(false); } void AudioManager::seek(qint64 position) { qCDebug(kastsAudio) << "AudioManager::seek" << position; // if there is still a pending seek, then we simply update that pending // value, and then manually send the positionChanged signal to have the UI // updated if (d->m_pendingSeek != -1) { d->m_pendingSeek = position; Q_EMIT positionChanged(position); } else { d->m_player.setPosition(position); } } void AudioManager::skipForward() { qCDebug(kastsAudio) << "AudioManager::skipForward"; seek(std::min((position() + SKIP_STEP), duration())); } void AudioManager::skipBackward() { qCDebug(kastsAudio) << "AudioManager::skipBackward"; seek(std::max((qint64)0, (position() - SKIP_STEP))); } bool AudioManager::canGoNext() const { // TODO: extend with streaming capability if (d->m_entry) { int index = DataManager::instance().queue().indexOf(d->m_entry->id()); if (index >= 0) { // check if there is a next track if (index < DataManager::instance().queue().count() - 1) { Entry *next_entry = DataManager::instance().getEntry(DataManager::instance().queue()[index + 1]); if (next_entry->enclosure()) { qCDebug(kastsAudio) << "Enclosure status" << next_entry->enclosure()->path() << next_entry->enclosure()->status(); if (next_entry->enclosure()->status() == Enclosure::Downloaded) { return true; } } } } } return false; } void AudioManager::next() { if (canGoNext()) { qCDebug(kastsAudio) << "Current playbackStatus before next() is:" << playbackState(); d->m_continuePlayback = playbackState() == QMediaPlayer::State::PlayingState; int index = DataManager::instance().queue().indexOf(d->m_entry->id()); qCDebug(kastsAudio) << "Skipping to" << DataManager::instance().queue()[index + 1]; setEntry(DataManager::instance().getEntry(DataManager::instance().queue()[index + 1])); } else { qCDebug(kastsAudio) << "Next track cannot be played, changing entry to nullptr"; setEntry(nullptr); } } void AudioManager::mediaStatusChanged() { qCDebug(kastsAudio) << "AudioManager::mediaStatusChanged" << d->m_player.mediaStatus(); // File has reached the end and has stopped if (d->m_player.mediaStatus() == QMediaPlayer::EndOfMedia) { next(); } // if there is a problem with the current track, make sure that it's not // loaded again when the application is restarted, skip to next track and // delete the enclosure if (d->m_player.mediaStatus() == QMediaPlayer::InvalidMedia) { // save pointer to this bad entry to allow // us to delete the enclosure after the track has been unloaded Entry *badEntry = d->m_entry; DataManager::instance().setLastPlayingEntry(QStringLiteral("none")); stop(); next(); if (badEntry && badEntry->enclosure()) { badEntry->enclosure()->deleteFile(); Q_EMIT logError(Error::Type::InvalidMedia, badEntry->feed()->url(), badEntry->id(), QMediaPlayer::InvalidMedia, i18n("Invalid Media"), QString()); } } } void AudioManager::playerStateChanged() { qCDebug(kastsAudio) << "AudioManager::playerStateChanged" << d->m_player.state(); switch (d->m_player.state()) { case QMediaPlayer::State::StoppedState: Q_EMIT stopped(); d->mPowerInterface.setPreventSleep(false); break; case QMediaPlayer::State::PlayingState: // setPreventSleep is set in play() to avoid it toggling too rapidly // see d->prepareAudioStream() for details Q_EMIT playing(); break; case QMediaPlayer::State::PausedState: // setPreventSleep is set in pause() to avoid it toggling too rapidly // see d->prepareAudioStream() for details Q_EMIT paused(); break; } } void AudioManager::playerDurationChanged(qint64 duration) { qCDebug(kastsAudio) << "AudioManager::playerDurationChanged" << duration; // Check if duration mentioned in enclosure corresponds to real duration if (duration > 0 && (duration / 1000) != d->m_entry->enclosure()->duration()) { qCDebug(kastsAudio) << "Correcting duration of" << d->m_entry->id() << "to" << duration / 1000 << "(was" << d->m_entry->enclosure()->duration() << ")"; d->m_entry->enclosure()->setDuration(duration / 1000); } qint64 correctedDuration = duration; QTimer::singleShot(0, this, [this, correctedDuration]() { Q_EMIT durationChanged(correctedDuration); }); } void AudioManager::playerVolumeChanged() { qCDebug(kastsAudio) << "AudioManager::playerVolumeChanged" << d->m_player.volume(); QTimer::singleShot(0, this, [this]() { Q_EMIT volumeChanged(); }); } void AudioManager::playerMutedChanged() { qCDebug(kastsAudio) << "AudioManager::playerMutedChanged" << muted(); QTimer::singleShot(0, this, [this]() { Q_EMIT mutedChanged(muted()); }); } void AudioManager::savePlayPosition() { qCDebug(kastsAudio) << "AudioManager::savePlayPosition"; // First check if there is still a pending seek checkForPendingSeek(); if (!d->m_lockPositionSaving) { if (d->m_entry) { if (d->m_entry->enclosure()) { d->m_entry->enclosure()->setPlayPosition(position()); } } } qCDebug(kastsAudio) << d->m_player.mediaStatus(); } void AudioManager::prepareAudio() { d->m_player.pause(); qint64 newDuration = duration(); qint64 startingPosition = d->m_entry->enclosure()->playPosition(); qCDebug(kastsAudio) << "Changing position to" << startingPosition / 1000 << "sec"; if (startingPosition <= newDuration) { d->m_pendingSeek = startingPosition; // Change notify interval temporarily. This will help with reducing the // startup audio glitch to a minimum. d->m_player.setNotifyInterval(50); // do not call d->m_player.setPosition() here since it might start // sending signals with a.o. incorrect duration and position } else { d->m_pendingSeek = -1; } // Emit positionChanged and durationChanged signals to make sure that // the GUI can see the faked values (if needed) qint64 newPosition = position(); Q_EMIT durationChanged(newDuration); Q_EMIT positionChanged(newPosition); d->m_readyToPlay = true; Q_EMIT canPlayChanged(); Q_EMIT canPauseChanged(); Q_EMIT canSkipForwardChanged(); Q_EMIT canSkipBackwardChanged(); Q_EMIT canGoNextChanged(); d->m_isSeekable = true; Q_EMIT seekableChanged(true); qCDebug(kastsAudio) << "Duration reported by d->m_player" << d->m_player.duration(); qCDebug(kastsAudio) << "Duration reported by enclosure (in ms)" << d->m_entry->enclosure()->duration() * 1000; qCDebug(kastsAudio) << "Duration reported by AudioManager" << newDuration; qCDebug(kastsAudio) << "Position reported by d->m_player" << d->m_player.position(); qCDebug(kastsAudio) << "Saved position stored in enclosure (in ms)" << startingPosition; qCDebug(kastsAudio) << "Position reported by AudioManager" << newPosition; if (d->m_continuePlayback) { // we call play() and not d->m_player.play() because we want to trigger // things like inhibit suspend play(); d->m_continuePlayback = false; } d->m_lockPositionSaving = false; } void AudioManager::checkForPendingSeek() { qint64 position = d->m_player.position(); qCDebug(kastsAudio) << "Seek pending?" << d->m_pendingSeek; qCDebug(kastsAudio) << "Current position" << position; // Check if we're supposed to skip to a new position if (d->m_pendingSeek != -1 && d->m_player.mediaStatus() == QMediaPlayer::BufferedMedia && d->m_player.duration() > 0) { if (abs(d->m_pendingSeek - position) > 2000) { qCDebug(kastsAudio) << "Position seek still pending to position" << d->m_pendingSeek; qCDebug(kastsAudio) << "Current reported position and duration" << d->m_player.position() << d->m_player.duration(); // be very careful because this setPosition call will trigger // a positionChanged signal, which will be nested, so we call it in // a QTimer::singleShot qint64 seekPosition = d->m_pendingSeek; QTimer::singleShot(0, this, [this, seekPosition]() { d->m_player.setPosition(seekPosition); }); } else { qCDebug(kastsAudio) << "Pending position seek has been executed; to position" << d->m_pendingSeek; d->m_pendingSeek = -1; d->m_player.setNotifyInterval(1000); } } } QString AudioManager::formattedDuration() const { return m_kformat.formatDuration(duration()); } QString AudioManager::formattedLeftDuration() const { qreal rate = 1.0; if (SettingsManager::self()->adjustTimeLeft()) { rate = playbackRate(); rate = (rate > 0.0) ? rate : 1.0; } qint64 diff = duration() - position(); return m_kformat.formatDuration(diff / rate); } QString AudioManager::formattedPosition() const { return m_kformat.formatDuration(position()); } qint64 AudioManager::sleepTime() const { if (d->m_sleepTimer) { return d->m_sleepTime; } else { return -1; } } qint64 AudioManager::remainingSleepTime() const { if (d->m_sleepTimer) { return d->m_remainingSleepTime; } else { return -1; } } void AudioManager::setSleepTimer(qint64 duration) { if (duration > 0) { if (d->m_sleepTimer) { stopSleepTimer(); } d->m_sleepTime = duration; d->m_remainingSleepTime = duration; d->m_sleepTimer = new QTimer(this); connect(d->m_sleepTimer, &QTimer::timeout, this, [this]() { (d->m_remainingSleepTime)--; if (d->m_remainingSleepTime > 0) { Q_EMIT remainingSleepTimeChanged(remainingSleepTime()); } else { pause(); stopSleepTimer(); } }); d->m_sleepTimer->start(1000); Q_EMIT sleepTimerChanged(duration); Q_EMIT remainingSleepTimeChanged(remainingSleepTime()); } else { stopSleepTimer(); } } void AudioManager::stopSleepTimer() { if (d->m_sleepTimer) { d->m_sleepTime = -1; d->m_remainingSleepTime = -1; delete d->m_sleepTimer; d->m_sleepTimer = nullptr; Q_EMIT sleepTimerChanged(-1); Q_EMIT remainingSleepTimeChanged(-1); } } QString AudioManager::formattedRemainingSleepTime() const { qint64 timeLeft = remainingSleepTime() * 1000; if (timeLeft < 0) { timeLeft = 0; } return m_kformat.formatDuration(timeLeft); }