diff --git a/libs/ui/KisSyncedAudioPlayback.h b/libs/ui/KisSyncedAudioPlayback.h --- a/libs/ui/KisSyncedAudioPlayback.h +++ b/libs/ui/KisSyncedAudioPlayback.h @@ -15,6 +15,7 @@ void syncWithVideo(qint64 position); bool isPlaying() const; + qint64 position() const; void setVolume(qreal value); diff --git a/libs/ui/KisSyncedAudioPlayback.cpp b/libs/ui/KisSyncedAudioPlayback.cpp --- a/libs/ui/KisSyncedAudioPlayback.cpp +++ b/libs/ui/KisSyncedAudioPlayback.cpp @@ -99,6 +99,11 @@ return m_d->player.state() == QMediaPlayer::PlayingState; } +qint64 KisSyncedAudioPlayback::position() const +{ + return m_d->player.position(); +} + void KisSyncedAudioPlayback::setVolume(qreal value) { m_d->player.setVolume(qRound(100.0 * value)); diff --git a/libs/ui/canvas/kis_animation_player.h b/libs/ui/canvas/kis_animation_player.h --- a/libs/ui/canvas/kis_animation_player.h +++ b/libs/ui/canvas/kis_animation_player.h @@ -77,7 +77,7 @@ private: void connectCancelSignals(); void disconnectCancelSignals(); - void uploadFrame(int time); + void uploadFrame(int time, bool forceSyncAudio); private: struct Private; diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_animation_player.cpp --- a/libs/ui/canvas/kis_animation_player.cpp +++ b/libs/ui/canvas/kis_animation_player.cpp @@ -63,7 +63,7 @@ dropFramesMode(true), nextFrameExpectedTime(0), expectedInterval(0), - expectedFrame(0), + currentFrame(0), lastTimerInterval(0), lastPaintedFrame(0), playbackStatisticsCompressor(1000, KisSignalCompressor::FIRST_INACTIVE), @@ -78,7 +78,8 @@ QTimer *timer; - int initialFrame; + /// The frame user started playback from + int uiFrame; int firstFrame; int lastFrame; qreal playbackSpeed; @@ -95,10 +96,14 @@ bool dropFramesMode; + /// Measures time since playback (re)started QElapsedTimer playbackTime; int nextFrameExpectedTime; int expectedInterval; - int expectedFrame; + /// The frame the current playback (re)started on + int initialFrame; + /// The frame currently displayed + int currentFrame; int lastTimerInterval; int lastPaintedFrame; @@ -115,17 +120,32 @@ int incFrame(int frame, int inc) { frame += inc; if (frame > lastFrame) { - frame = firstFrame + frame - lastFrame - 1; + const int framesFromFirst = frame - firstFrame; + const int rangeLength = lastFrame - firstFrame + 1; + frame = firstFrame + framesFromFirst % rangeLength; } return frame; } - qint64 frameToMSec(int value, int fps) { - return qreal(value) / fps * 1000.0; + qint64 framesToMSec(qreal value, int fps) const { + return qRound(value / fps * 1000.0); } - int msecToFrame(qint64 value, int fps) { + qreal msecToFrames(qint64 value, int fps) const { return qreal(value) * fps / 1000.0; } + int framesToWalltime(qreal frame, int fps) const { + return qRound(framesToMSec(frame, fps) / playbackSpeed); + } + qreal walltimeToFrames(qint64 time, int fps) const { + return msecToFrames(time, fps) * playbackSpeed; + } + + qreal playbackTimeInFrames(int fps) const { + const qint64 cycleLength = lastFrame - firstFrame + 1; + const qreal framesPlayed = walltimeToFrames(playbackTime.elapsed(), fps); + const qreal framesSinceFirst = std::fmod(initialFrame + framesPlayed - firstFrame, cycleLength); + return firstFrame + framesSinceFirst; + } }; KisAnimationPlayer::KisAnimationPlayer(KisCanvas2 *canvas) @@ -289,6 +309,10 @@ if (m_d->syncedAudio) { m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance); } + + if (m_d->playing) { + slotUpdatePlaybackTimer(); + } } void KisAnimationPlayer::slotUpdatePlaybackTimer() @@ -301,17 +325,21 @@ const int fps = animation->framerate(); - m_d->initialFrame = animation->currentUITime(); + m_d->initialFrame = isPlaying() ? m_d->currentFrame : animation->currentUITime(); m_d->firstFrame = playBackRange.start(); m_d->lastFrame = playBackRange.end(); - m_d->expectedFrame = qBound(m_d->firstFrame, m_d->expectedFrame, m_d->lastFrame); + m_d->currentFrame = qBound(m_d->firstFrame, m_d->currentFrame, m_d->lastFrame); - m_d->expectedInterval = qreal(1000) / fps / m_d->playbackSpeed; + m_d->expectedInterval = m_d->framesToWalltime(1, fps); m_d->lastTimerInterval = m_d->expectedInterval; if (m_d->syncedAudio) { m_d->syncedAudio->setSpeed(m_d->playbackSpeed); + const qint64 expectedAudioTime = m_d->framesToMSec(m_d->currentFrame, fps); + if (qAbs(m_d->syncedAudio->position() - expectedAudioTime) > m_d->framesToMSec(1.5, fps)) { + m_d->syncedAudio->syncWithVideo(expectedAudioTime); + } } m_d->timer->start(m_d->expectedInterval); @@ -327,8 +355,9 @@ void KisAnimationPlayer::play() { + + const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface(); { - const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface(); const KisTimeRange &range = animation->playbackRange(); if (!range.isValid()) return; @@ -373,15 +402,16 @@ m_d->playing = true; + m_d->uiFrame = animation->currentUITime(); + m_d->currentFrame = m_d->uiFrame; slotUpdatePlaybackTimer(); - m_d->expectedFrame = m_d->firstFrame; m_d->lastPaintedFrame = -1; connectCancelSignals(); if (m_d->syncedAudio) { KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface(); - m_d->syncedAudio->play(m_d->frameToMSec(m_d->firstFrame, animationInterface->framerate())); + m_d->syncedAudio->play(m_d->framesToMSec(m_d->currentFrame, animationInterface->framerate())); } emit sigPlaybackStarted(); @@ -401,10 +431,10 @@ if (doUpdates) { KisImageAnimationInterface *animation = canvas->image()->animationInterface(); - if (animation->currentUITime() == initialFrame) { + if (animation->currentUITime() == uiFrame) { canvas->refetchDataFromImage(); } else { - animation->switchCurrentTimeAsync(initialFrame); + animation->switchCurrentTimeAsync(uiFrame); } } @@ -433,47 +463,50 @@ void KisAnimationPlayer::displayFrame(int time) { - uploadFrame(time); + uploadFrame(time, true); } void KisAnimationPlayer::slotUpdate() { - uploadFrame(-1); + uploadFrame(-1, false); } -void KisAnimationPlayer::uploadFrame(int frame) +void KisAnimationPlayer::uploadFrame(int frame, bool forceSyncAudio) { KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface(); + const int fps = animationInterface->framerate(); + const bool syncToAudio = !forceSyncAudio && m_d->dropFramesMode && m_d->syncedAudio && m_d->syncedAudio->isPlaying(); if (frame < 0) { - const int currentTime = m_d->playbackTime.elapsed(); - const int framesDiff = currentTime - m_d->nextFrameExpectedTime; - const qreal framesDiffNorm = qreal(framesDiff) / m_d->expectedInterval; + if (m_d->dropFramesMode) { + const qreal currentTimeInFrames = syncToAudio ? + m_d->msecToFrames(m_d->syncedAudio->position(), fps) : + m_d->playbackTimeInFrames(fps); + frame = qFloor(currentTimeInFrames); - // qDebug() << ppVar(framesDiff) - // << ppVar(m_d->expectedFrame) - // << ppVar(framesDiffNorm) - // << ppVar(m_d->lastTimerInterval); + const int timeToNextFrame = m_d->framesToWalltime(frame + 1 - currentTimeInFrames, fps); + m_d->lastTimerInterval = qMax(0, timeToNextFrame); - if (m_d->dropFramesMode) { - const int numDroppedFrames = qMax(0, qRound(framesDiffNorm)); - frame = m_d->incFrame(m_d->expectedFrame, numDroppedFrames); + if (frame < m_d->currentFrame) { + // Returned to beginning of animation. Restart audio playback. + forceSyncAudio = true; + } } else { - frame = m_d->expectedFrame; - } - - m_d->nextFrameExpectedTime = currentTime + m_d->expectedInterval; + const qint64 currentTime = m_d->playbackTime.elapsed(); + const qint64 framesDiff = currentTime - m_d->nextFrameExpectedTime; - m_d->lastTimerInterval = qMax(0.0, m_d->lastTimerInterval - 0.5 * framesDiff); - m_d->expectedFrame = m_d->incFrame(frame, 1); + frame = m_d->incFrame(m_d->currentFrame, 1); + m_d->nextFrameExpectedTime = currentTime + m_d->expectedInterval; + m_d->lastTimerInterval = qMax(0.0, m_d->lastTimerInterval - 0.5 * framesDiff); + } + m_d->currentFrame = frame; m_d->timer->start(m_d->lastTimerInterval); - m_d->playbackStatisticsCompressor.start(); } - if (m_d->syncedAudio) { - const int msecTime = m_d->frameToMSec(frame, animationInterface->framerate()); + if (m_d->syncedAudio && (!syncToAudio || forceSyncAudio)) { + const int msecTime = m_d->framesToMSec(frame, fps); if (isPlaying()) { slotSyncScrubbingAudio(msecTime); } else {