If the duration that is mentioned in the enclosure doesn't correspond to the real duration then update to the real duration in the database.
433 lines
14 KiB
C++
433 lines
14 KiB
C++
/**
|
|
* SPDX-FileCopyrightText: 2017 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
|
|
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
*/
|
|
|
|
#include "audiomanager.h"
|
|
|
|
#include <algorithm>
|
|
#include <QTimer>
|
|
#include <QAudio>
|
|
#include <QEventLoop>
|
|
|
|
#include "powermanagementinterface.h"
|
|
#include "datamanager.h"
|
|
#include "settingsmanager.h"
|
|
|
|
static const double MAX_RATE = 1.0;
|
|
static const double MIN_RATE = 2.5;
|
|
static const qint64 SKIP_STEP = 10000;
|
|
|
|
class AudioManagerPrivate
|
|
{
|
|
|
|
private:
|
|
|
|
PowerManagementInterface mPowerInterface;
|
|
|
|
QMediaPlayer m_player;
|
|
|
|
Entry* m_entry = nullptr;
|
|
bool m_readyToPlay = false;
|
|
bool m_isSeekable = false;
|
|
bool m_lockPositionSaving = false; // sort of lock mutex to prevent updating the player position while changing sources (which will emit lots of playerPositionChanged signals)
|
|
|
|
friend class AudioManager;
|
|
};
|
|
|
|
AudioManager::AudioManager(QObject *parent) : QObject(parent), d(std::make_unique<AudioManagerPrivate>())
|
|
{
|
|
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<QMediaPlayer::Error>::of(&QMediaPlayer::error), this, &AudioManager::errorChanged);
|
|
connect(&d->m_player, &QMediaPlayer::durationChanged, this, &AudioManager::durationChanged);
|
|
connect(&d->m_player, &QMediaPlayer::positionChanged, this, &AudioManager::positionChanged);
|
|
connect(&d->m_player, &QMediaPlayer::positionChanged, this, &AudioManager::savePlayPosition);
|
|
|
|
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
|
|
|
|
// 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<qreal>(d->m_player.volume() / 100.0);
|
|
auto userVolume = static_cast<qreal>(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) {
|
|
qDebug() << "AudioManager::error" << d->m_player.errorString();
|
|
}
|
|
|
|
return d->m_player.error();
|
|
}
|
|
|
|
qint64 AudioManager::duration() const
|
|
{
|
|
return d->m_player.duration();
|
|
}
|
|
|
|
qint64 AudioManager::position() const
|
|
{
|
|
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)
|
|
{
|
|
d->m_lockPositionSaving = true;
|
|
// First check if the previous track needs to be marked as read
|
|
// TODO: make grace time a setting in SettingsManager
|
|
if (d->m_entry) {
|
|
qDebug() << "Checking previous track";
|
|
qDebug() << "Left time" << (duration()-position());
|
|
qDebug() << "MediaStatus" << d->m_player.mediaStatus();
|
|
if (( (duration()-position()) < 15000)
|
|
|| (d->m_player.mediaStatus() == QMediaPlayer::EndOfMedia) ) {
|
|
qDebug() << "Mark as read:" << d->m_entry->title();
|
|
d->m_entry->setRead(true);
|
|
d->m_entry->enclosure()->setPlayPosition(0);
|
|
d->m_entry->setQueueStatus(false); // i.e. remove from queue TODO: make this a choice in settings
|
|
}
|
|
}
|
|
if (entry != nullptr) {
|
|
qDebug() << "Going to change source";
|
|
d->m_entry = entry;
|
|
Q_EMIT entryChanged(entry);
|
|
d->m_player.setMedia(QUrl(QStringLiteral("file://")+d->m_entry->enclosure()->path()));
|
|
// save the current playing track in the settingsfile for restoring on startup
|
|
DataManager::instance().setLastPlayingEntry(d->m_entry->id());
|
|
qDebug() << "Changed source to" << d->m_entry->title();
|
|
|
|
qint64 startingPosition = d->m_entry->enclosure()->playPosition();
|
|
// What follows is a dirty hack to get the player positioned at the
|
|
// correct spot. The audio only becomes seekable when the player is
|
|
// actually playing. So we start the playback and then set a timer to
|
|
// wait until the stream becomes seekable; then switch position and
|
|
// immediately pause the playback.
|
|
// Unfortunately, this will produce an audible glitch with the current
|
|
// QMediaPlayer backend.
|
|
d->m_player.play();
|
|
if (!d->m_player.isSeekable()) {
|
|
QEventLoop loop;
|
|
QTimer timer;
|
|
timer.setSingleShot(true);
|
|
timer.setInterval(2000);
|
|
loop.connect(&timer, SIGNAL (timeout()), &loop, SLOT (quit()) );
|
|
loop.connect(&d->m_player, SIGNAL (seekableChanged(bool)), &loop, SLOT (quit()));
|
|
qDebug() << "Starting waiting loop";
|
|
loop.exec();
|
|
}
|
|
if (d->m_player.mediaStatus() != QMediaPlayer::BufferedMedia) {
|
|
QEventLoop loop;
|
|
QTimer timer;
|
|
timer.setSingleShot(true);
|
|
timer.setInterval(2000);
|
|
loop.connect(&timer, SIGNAL (timeout()), &loop, SLOT (quit()) );
|
|
loop.connect(&d->m_player, SIGNAL (mediaStatusChanged(QMediaPlayer::MediaStatus)), &loop, SLOT (quit()));
|
|
qDebug() << "Starting waiting loop on media status" << d->m_player.mediaStatus();
|
|
loop.exec();
|
|
} qDebug() << "Changing position";
|
|
if (startingPosition > 1000) d->m_player.setPosition(startingPosition);
|
|
d->m_player.pause();
|
|
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);
|
|
qDebug() << "Duration" << d->m_player.duration()/1000 << d->m_entry->enclosure()->duration();
|
|
// Finally, check if duration mentioned in enclosure corresponds to real duration
|
|
if ((d->m_player.duration()/1000) != d->m_entry->enclosure()->duration()) {
|
|
d->m_entry->enclosure()->setDuration(d->m_player.duration()/1000);
|
|
qDebug() << "Correcting duration of" << d->m_entry->id() << "to" << d->m_player.duration()/1000;
|
|
}
|
|
} else {
|
|
DataManager::instance().setLastPlayingEntry(QStringLiteral("none"));
|
|
d->m_entry = nullptr;
|
|
Q_EMIT entryChanged(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);
|
|
}
|
|
// Unlock the position saving lock
|
|
d->m_lockPositionSaving = false;
|
|
}
|
|
|
|
void AudioManager::setMuted(bool muted)
|
|
{
|
|
d->m_player.setMuted(muted);
|
|
}
|
|
|
|
void AudioManager::setVolume(qreal volume)
|
|
{
|
|
qDebug() << "AudioManager::setVolume" << volume;
|
|
|
|
auto realVolume = static_cast<qreal>(QAudio::convertVolume(volume / 100.0, QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale));
|
|
d->m_player.setVolume(qRound(realVolume * 100));
|
|
}
|
|
|
|
/*
|
|
void AudioManager::setSource(const QUrl &source)
|
|
{
|
|
qDebug() << "AudioManager::setSource" << source;
|
|
|
|
d->m_player.setMedia({source});
|
|
}
|
|
*/
|
|
|
|
void AudioManager::setPosition(qint64 position)
|
|
{
|
|
qDebug() << "AudioManager::setPosition" << position;
|
|
|
|
d->m_player.setPosition(position);
|
|
}
|
|
|
|
void AudioManager::setPlaybackRate(const qreal rate)
|
|
{
|
|
qDebug() << "AudioManager::setPlaybackRate" << rate;
|
|
|
|
d->m_player.setPlaybackRate(rate);
|
|
}
|
|
|
|
void AudioManager::play()
|
|
{
|
|
qDebug() << "AudioManager::play";
|
|
|
|
d->m_player.play();
|
|
d->m_isSeekable = true;
|
|
Q_EMIT seekableChanged(d->m_isSeekable);
|
|
}
|
|
|
|
void AudioManager::pause()
|
|
{
|
|
qDebug() << "AudioManager::pause";
|
|
|
|
d->m_player.play();
|
|
d->m_isSeekable = true;
|
|
d->m_player.pause();
|
|
}
|
|
|
|
void AudioManager::playPause()
|
|
{
|
|
if (playbackState() == QMediaPlayer::State::PausedState)
|
|
play();
|
|
else if (playbackState() == QMediaPlayer::State::PlayingState)
|
|
pause();
|
|
}
|
|
|
|
void AudioManager::stop()
|
|
{
|
|
qDebug() << "AudioManager::stop";
|
|
|
|
d->m_player.stop();
|
|
d->m_isSeekable = false;
|
|
Q_EMIT seekableChanged(d->m_isSeekable);
|
|
}
|
|
|
|
void AudioManager::seek(qint64 position)
|
|
{
|
|
qDebug() << "AudioManager::seek" << position;
|
|
|
|
d->m_player.setPosition(position);
|
|
}
|
|
|
|
void AudioManager::skipForward()
|
|
{
|
|
qDebug() << "AudioManager::skipForward";
|
|
seek(std::min((position() + SKIP_STEP), duration()));
|
|
}
|
|
|
|
void AudioManager::skipBackward()
|
|
{
|
|
qDebug() << "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().getQueue().indexOf(d->m_entry->id());
|
|
if (index >= 0) {
|
|
// check if there is a next track
|
|
if (index < DataManager::instance().getQueue().count()-1) {
|
|
Entry* next_entry = DataManager::instance().getEntry(DataManager::instance().getQueue()[index+1]);
|
|
if (next_entry->enclosure()) {
|
|
qDebug() << "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()) {
|
|
QMediaPlayer::State previousTrackState = playbackState();
|
|
int index = DataManager::instance().getQueue().indexOf(d->m_entry->id());
|
|
qDebug() << "Skipping to" << DataManager::instance().getQueue()[index+1];
|
|
setEntry(DataManager::instance().getEntry(DataManager::instance().getQueue()[index+1]));
|
|
if (previousTrackState == QMediaPlayer::PlayingState) play();
|
|
} else {
|
|
qDebug() << "Next track cannot be played, changing entry to nullptr";
|
|
setEntry(nullptr);
|
|
}
|
|
}
|
|
|
|
void AudioManager::mediaStatusChanged()
|
|
{
|
|
qDebug() << "AudioManager::mediaStatusChanged" << d->m_player.mediaStatus();
|
|
|
|
// File has reached the end and has stopped
|
|
if (d->m_player.mediaStatus() == QMediaPlayer::EndOfMedia) {
|
|
next();
|
|
}
|
|
}
|
|
|
|
void AudioManager::playerStateChanged()
|
|
{
|
|
qDebug() << "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:
|
|
Q_EMIT playing();
|
|
d->mPowerInterface.setPreventSleep(true);
|
|
break;
|
|
case QMediaPlayer::State::PausedState:
|
|
Q_EMIT paused();
|
|
d->mPowerInterface.setPreventSleep(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void AudioManager::playerVolumeChanged()
|
|
{
|
|
qDebug() << "AudioManager::playerVolumeChanged" << d->m_player.volume();
|
|
|
|
QTimer::singleShot(0, [this]() {Q_EMIT volumeChanged();});
|
|
}
|
|
|
|
void AudioManager::playerMutedChanged()
|
|
{
|
|
qDebug() << "AudioManager::playerMutedChanged";
|
|
|
|
QTimer::singleShot(0, [this]() {Q_EMIT mutedChanged(muted());});
|
|
}
|
|
|
|
void AudioManager::savePlayPosition(qint64 position)
|
|
{
|
|
if (!d->m_lockPositionSaving) {
|
|
if (d->m_entry) {
|
|
if (d->m_entry->enclosure()) {
|
|
d->m_entry->enclosure()->setPlayPosition(position);
|
|
}
|
|
}
|
|
}
|
|
qDebug() << d->m_player.mediaStatus();
|
|
}
|