kasts/src/audiomanager.cpp

433 lines
14 KiB
C++
Raw Normal View History

2021-04-11 17:22:13 +02:00
/**
* 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>
2021-04-11 21:08:25 +02:00
#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
2021-04-11 21:08:25 +02:00
// 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
2021-04-13 22:11:12 +02:00
}
}
if (entry != nullptr) {
2021-04-13 22:11:12 +02:00
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
{
2021-04-13 22:11:12 +02:00
// 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) {
2021-04-13 22:11:12 +02:00
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();
}