1 /*
2  *  Copyright (c) 2015 Jouni Pentikäinen <joupent@gmail.com>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "kis_animation_player.h"
20 
21 #include <QElapsedTimer>
22 #include <QTimer>
23 #include <QtMath>
24 
25 //#define PLAYER_DEBUG_FRAMERATE
26 
27 #include "kis_global.h"
28 #include "kis_algebra_2d.h"
29 
30 #include "kis_config.h"
31 #include "kis_config_notifier.h"
32 #include "kis_image.h"
33 #include "kis_canvas2.h"
34 #include "kis_animation_frame_cache.h"
35 #include "kis_signal_auto_connection.h"
36 #include "kis_image_animation_interface.h"
37 #include "kis_time_range.h"
38 #include "kis_signal_compressor.h"
39 #include <KisDocument.h>
40 #include <QFileInfo>
41 #include "KisSyncedAudioPlayback.h"
42 #include "kis_signal_compressor_with_param.h"
43 #include "kis_image_barrier_locker.h"
44 #include "kis_layer_utils.h"
45 #include "KisDecoratedNodeInterface.h"
46 
47 #include "kis_image_config.h"
48 #include <limits>
49 
50 #include "KisViewManager.h"
51 #include "kis_icon_utils.h"
52 
53 #include "KisPart.h"
54 #include "dialogs/KisAsyncAnimationCacheRenderDialog.h"
55 #include "KisRollingMeanAccumulatorWrapper.h"
56 
57 
58 struct KisAnimationPlayer::Private
59 {
60 public:
PrivateKisAnimationPlayer::Private61     Private(KisAnimationPlayer *_q)
62         : q(_q),
63           realFpsAccumulator(24),
64           droppedFpsAccumulator(24),
65           droppedFramesPortion(24),
66           dropFramesMode(true),
67           nextFrameExpectedTime(0),
68           expectedInterval(0),
69           currentFrame(0),
70           lastTimerInterval(0),
71           lastPaintedFrame(0),
72           playbackStatisticsCompressor(1000, KisSignalCompressor::FIRST_INACTIVE),
73           stopAudioOnScrubbingCompressor(100, KisSignalCompressor::POSTPONE),
74           audioOffsetTolerance(-1)
75           {}
76 
77     KisAnimationPlayer *q;
78 
79     bool useFastFrameUpload;
80     bool playing;
81 
82     QTimer *timer;
83 
84     /// The frame user started playback from
85     int uiFrame;
86     int firstFrame;
87     int lastFrame;
88     qreal playbackSpeed;
89 
90     KisCanvas2 *canvas;
91 
92     KisSignalAutoConnectionsStore cancelStrokeConnections;
93 
94     QElapsedTimer realFpsTimer;
95     KisRollingMeanAccumulatorWrapper realFpsAccumulator;
96     KisRollingMeanAccumulatorWrapper droppedFpsAccumulator;
97     KisRollingMeanAccumulatorWrapper droppedFramesPortion;
98 
99 
100     bool dropFramesMode;
101 
102     /// Measures time since playback (re)started
103     QElapsedTimer playbackTime;
104     int nextFrameExpectedTime;
105     int expectedInterval;
106     /// The frame the current playback (re)started on
107     int initialFrame;
108     /// The frame currently displayed
109     int currentFrame;
110     int lastTimerInterval;
111     int lastPaintedFrame;
112 
113     KisSignalCompressor playbackStatisticsCompressor;
114 
115     QScopedPointer<KisSyncedAudioPlayback> syncedAudio;
116     QScopedPointer<KisSignalCompressorWithParam<int> > audioSyncScrubbingCompressor;
117     KisSignalCompressor stopAudioOnScrubbingCompressor;
118 
119     int audioOffsetTolerance;
120     QVector<KisNodeWSP> disabledDecoratedNodes;
121 
122     void stopImpl(bool doUpdates);
123 
incFrameKisAnimationPlayer::Private124     int incFrame(int frame, int inc) {
125         frame += inc;
126         if (frame > lastFrame) {
127             const int framesFromFirst = frame - firstFrame;
128             const int rangeLength = lastFrame - firstFrame + 1;
129             frame = firstFrame + framesFromFirst % rangeLength;
130         }
131         return frame;
132     }
133 
framesToMSecKisAnimationPlayer::Private134     qint64 framesToMSec(qreal value, int fps) const {
135         return qRound(value / fps * 1000.0);
136     }
msecToFramesKisAnimationPlayer::Private137     qreal msecToFrames(qint64 value, int fps) const {
138         return qreal(value) * fps / 1000.0;
139     }
framesToWalltimeKisAnimationPlayer::Private140     int framesToWalltime(qreal frame, int fps) const {
141         return qRound(framesToMSec(frame, fps) / playbackSpeed);
142     }
walltimeToFramesKisAnimationPlayer::Private143     qreal walltimeToFrames(qint64 time, int fps) const {
144         return msecToFrames(time, fps) * playbackSpeed;
145     }
146 
playbackTimeInFramesKisAnimationPlayer::Private147     qreal playbackTimeInFrames(int fps) const {
148         const qint64 cycleLength = lastFrame - firstFrame + 1;
149         const qreal framesPlayed = walltimeToFrames(playbackTime.elapsed(), fps);
150         const qreal framesSinceFirst = std::fmod(initialFrame + framesPlayed - firstFrame, cycleLength);
151         return firstFrame + framesSinceFirst;
152     }
153 };
154 
KisAnimationPlayer(KisCanvas2 * canvas)155 KisAnimationPlayer::KisAnimationPlayer(KisCanvas2 *canvas)
156     : QObject(canvas)
157     , m_d(new Private(this))
158 {
159     m_d->useFastFrameUpload = false;
160     m_d->playing = false;
161     m_d->canvas = canvas;
162     m_d->playbackSpeed = 1.0;
163 
164     m_d->timer = new QTimer(this);
165     connect(m_d->timer, SIGNAL(timeout()), this, SLOT(slotUpdate()));
166     m_d->timer->setSingleShot(true);
167 
168     connect(KisConfigNotifier::instance(),
169             SIGNAL(dropFramesModeChanged()),
170             SLOT(slotUpdateDropFramesMode()));
171     slotUpdateDropFramesMode();
172 
173     connect(&m_d->playbackStatisticsCompressor, SIGNAL(timeout()),
174             this, SIGNAL(sigPlaybackStatisticsUpdated()));
175 
176     using namespace std::placeholders;
177     std::function<void (int)> callback(
178         std::bind(&KisAnimationPlayer::slotSyncScrubbingAudio, this, _1));
179 
180     const int defaultScrubbingUdpatesDelay = 40; /* 40 ms == 25 fps */
181 
182     m_d->audioSyncScrubbingCompressor.reset(
183         new KisSignalCompressorWithParam<int>(defaultScrubbingUdpatesDelay, callback, KisSignalCompressor::FIRST_ACTIVE));
184 
185     m_d->stopAudioOnScrubbingCompressor.setDelay(defaultScrubbingUdpatesDelay);
186     connect(&m_d->stopAudioOnScrubbingCompressor, SIGNAL(timeout()), SLOT(slotTryStopScrubbingAudio()));
187 
188     connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()), SLOT(slotUpdateAudioChunkLength()));
189     slotUpdateAudioChunkLength();
190 
191     connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioChannelChanged()), SLOT(slotAudioChannelChanged()));
192     connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioVolumeChanged()), SLOT(slotAudioVolumeChanged()));
193 
194     slotAudioChannelChanged();
195 }
196 
~KisAnimationPlayer()197 KisAnimationPlayer::~KisAnimationPlayer()
198 {}
199 
slotUpdateDropFramesMode()200 void KisAnimationPlayer::slotUpdateDropFramesMode()
201 {
202     KisConfig cfg(true);
203     m_d->dropFramesMode = cfg.animationDropFrames();
204 }
205 
slotSyncScrubbingAudio(int msecTime)206 void KisAnimationPlayer::slotSyncScrubbingAudio(int msecTime)
207 {
208     KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio);
209 
210     if (!m_d->syncedAudio->isPlaying()) {
211         m_d->syncedAudio->play(msecTime);
212     } else {
213         m_d->syncedAudio->syncWithVideo(msecTime);
214     }
215 
216     if (!isPlaying()) {
217         m_d->stopAudioOnScrubbingCompressor.start();
218     }
219 }
220 
slotTryStopScrubbingAudio()221 void KisAnimationPlayer::slotTryStopScrubbingAudio()
222 {
223     KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio);
224     if (m_d->syncedAudio && !isPlaying()) {
225         m_d->syncedAudio->stop();
226     }
227 }
228 
slotAudioChannelChanged()229 void KisAnimationPlayer::slotAudioChannelChanged()
230 {
231 
232     KisImageAnimationInterface *interface = m_d->canvas->image()->animationInterface();
233     QString fileName = interface->audioChannelFileName();
234     QFileInfo info(fileName);
235     if (info.exists() && !interface->isAudioMuted()) {
236         m_d->syncedAudio.reset(new KisSyncedAudioPlayback(info.absoluteFilePath()));
237         m_d->syncedAudio->setVolume(interface->audioVolume());
238         m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance);
239 
240         connect(m_d->syncedAudio.data(), SIGNAL(error(QString,QString)), SLOT(slotOnAudioError(QString,QString)));
241     } else {
242         m_d->syncedAudio.reset();
243     }
244 }
245 
slotAudioVolumeChanged()246 void KisAnimationPlayer::slotAudioVolumeChanged()
247 {
248     KisImageAnimationInterface *interface = m_d->canvas->image()->animationInterface();
249     if (m_d->syncedAudio) {
250         m_d->syncedAudio->setVolume(interface->audioVolume());
251     }
252 }
253 
slotOnAudioError(const QString & fileName,const QString & message)254 void KisAnimationPlayer::slotOnAudioError(const QString &fileName, const QString &message)
255 {
256     QString errorMessage(i18nc("floating on-canvas message", "Cannot open audio: \"%1\"\nError: %2", fileName, message));
257     m_d->canvas->viewManager()->showFloatingMessage(errorMessage, KisIconUtils::loadIcon("warning"));
258 }
259 
connectCancelSignals()260 void KisAnimationPlayer::connectCancelSignals()
261 {
262     m_d->cancelStrokeConnections.addConnection(
263         m_d->canvas->image().data(), SIGNAL(sigUndoDuringStrokeRequested()),
264         this, SLOT(slotCancelPlayback()));
265 
266     m_d->cancelStrokeConnections.addConnection(
267         m_d->canvas->image().data(), SIGNAL(sigStrokeCancellationRequested()),
268         this, SLOT(slotCancelPlayback()));
269 
270     m_d->cancelStrokeConnections.addConnection(
271         m_d->canvas->image().data(), SIGNAL(sigStrokeEndRequested()),
272         this, SLOT(slotCancelPlaybackSafe()));
273 
274     m_d->cancelStrokeConnections.addConnection(
275         m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()),
276         this, SLOT(slotUpdatePlaybackTimer()));
277 
278     m_d->cancelStrokeConnections.addConnection(
279         m_d->canvas->image()->animationInterface(), SIGNAL(sigFullClipRangeChanged()),
280         this, SLOT(slotUpdatePlaybackTimer()));
281 
282     m_d->cancelStrokeConnections.addConnection(
283         m_d->canvas->image()->animationInterface(), SIGNAL(sigPlaybackRangeChanged()),
284         this, SLOT(slotUpdatePlaybackTimer()));
285 }
286 
disconnectCancelSignals()287 void KisAnimationPlayer::disconnectCancelSignals()
288 {
289     m_d->cancelStrokeConnections.clear();
290 }
291 
slotUpdateAudioChunkLength()292 void KisAnimationPlayer::slotUpdateAudioChunkLength()
293 {
294     const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface();
295     const int animationFramePeriod = qMax(1, 1000 / animation->framerate());
296 
297     KisConfig cfg(true);
298     int scrubbingAudioUdpatesDelay = cfg.scrubbingAudioUpdatesDelay();
299 
300     if (scrubbingAudioUdpatesDelay < 0) {
301 
302         scrubbingAudioUdpatesDelay = qMax(1, animationFramePeriod);
303     }
304 
305     m_d->audioSyncScrubbingCompressor->setDelay(scrubbingAudioUdpatesDelay);
306     m_d->stopAudioOnScrubbingCompressor.setDelay(scrubbingAudioUdpatesDelay);
307 
308     m_d->audioOffsetTolerance = cfg.audioOffsetTolerance();
309     if (m_d->audioOffsetTolerance < 0) {
310         m_d->audioOffsetTolerance = animationFramePeriod;
311     }
312 
313     if (m_d->syncedAudio) {
314         m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance);
315     }
316 
317     if (m_d->playing) {
318         slotUpdatePlaybackTimer();
319     }
320 }
321 
slotUpdatePlaybackTimer()322 void KisAnimationPlayer::slotUpdatePlaybackTimer()
323 {
324      m_d->timer->stop();
325 
326     const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface();
327     const KisTimeRange &playBackRange = animation->playbackRange();
328     if (!playBackRange.isValid()) return;
329 
330     const int fps = animation->framerate();
331 
332     m_d->initialFrame = isPlaying() ? m_d->currentFrame : animation->currentUITime();
333     m_d->firstFrame = playBackRange.start();
334     m_d->lastFrame = playBackRange.end();
335     m_d->currentFrame = qBound(m_d->firstFrame, m_d->currentFrame, m_d->lastFrame);
336 
337 
338     m_d->expectedInterval = m_d->framesToWalltime(1, fps);
339     m_d->lastTimerInterval = m_d->expectedInterval;
340 
341     if (m_d->syncedAudio) {
342         m_d->syncedAudio->setSpeed(m_d->playbackSpeed);
343         const qint64 expectedAudioTime = m_d->framesToMSec(m_d->currentFrame, fps);
344         if (qAbs(m_d->syncedAudio->position() - expectedAudioTime) > m_d->framesToMSec(1.5, fps)) {
345             m_d->syncedAudio->syncWithVideo(expectedAudioTime);
346         }
347     }
348 
349     m_d->timer->start(m_d->expectedInterval);
350 
351     if (m_d->playbackTime.isValid()) {
352         m_d->playbackTime.restart();
353     } else {
354         m_d->playbackTime.start();
355     }
356 
357     m_d->nextFrameExpectedTime = m_d->playbackTime.elapsed() + m_d->expectedInterval;
358 }
359 
play()360 void KisAnimationPlayer::play()
361 {
362 
363     const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface();
364     {
365         const KisTimeRange &range = animation->playbackRange();
366         if (!range.isValid()) return;
367 
368         // when openGL is disabled, there is no animation cache
369         if (m_d->canvas->frameCache()) {
370             KisImageConfig cfg(true);
371 
372             const int dimensionLimit =
373                 cfg.useAnimationCacheFrameSizeLimit() ?
374                 cfg.animationCacheFrameSizeLimit() :
375                 std::numeric_limits<int>::max();
376 
377             const int maxImageDimension = KisAlgebra2D::maxDimension(m_d->canvas->image()->bounds());
378 
379             const QRect regionOfInterest =
380                 cfg.useAnimationCacheRegionOfInterest() && maxImageDimension > dimensionLimit ?
381                 m_d->canvas->regionOfInterest() :
382                 m_d->canvas->coordinatesConverter()->imageRectInImagePixels();
383 
384             const QRect minimalNeedRect =
385                 m_d->canvas->coordinatesConverter()->widgetRectInImagePixels().toAlignedRect() &
386                 m_d->canvas->coordinatesConverter()->imageRectInImagePixels();
387 
388             m_d->canvas->frameCache()->dropLowQualityFrames(range, regionOfInterest, minimalNeedRect);
389 
390             KisAsyncAnimationCacheRenderDialog dlg(m_d->canvas->frameCache(),
391                                                    range,
392                                                    200);
393 
394             dlg.setRegionOfInterest(regionOfInterest);
395 
396             KisAsyncAnimationCacheRenderDialog::Result result =
397                 dlg.regenerateRange(m_d->canvas->viewManager());
398 
399             if (result != KisAsyncAnimationCacheRenderDialog::RenderComplete) {
400                 return;
401             }
402 
403             m_d->canvas->setRenderingLimit(regionOfInterest);
404         } else {
405             KisImageBarrierLocker locker(m_d->canvas->image());
406             KisLayerUtils::recursiveApplyNodes(m_d->canvas->image()->root(),
407                 [this] (KisNodeSP node) {
408                     KisDecoratedNodeInterface *decoratedNode = dynamic_cast<KisDecoratedNodeInterface*>(node.data());
409                     if (decoratedNode && decoratedNode->decorationsVisible()) {
410                         decoratedNode->setDecorationsVisible(false, false);
411                         m_d->disabledDecoratedNodes.append(node);
412                     }
413                 });
414         }
415     }
416 
417     m_d->playing = true;
418 
419     m_d->uiFrame = animation->currentUITime();
420     m_d->currentFrame = m_d->uiFrame;
421     slotUpdatePlaybackTimer();
422     m_d->lastPaintedFrame = -1;
423 
424     connectCancelSignals();
425 
426     if (m_d->syncedAudio) {
427         KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface();
428         m_d->syncedAudio->play(m_d->framesToMSec(m_d->currentFrame, animationInterface->framerate()));
429     }
430 
431     emit sigPlaybackStarted();
432 }
433 
434 
stopImpl(bool doUpdates)435 void KisAnimationPlayer::Private::stopImpl(bool doUpdates)
436 {
437     if (syncedAudio) {
438         syncedAudio->stop();
439     }
440 
441     q->disconnectCancelSignals();
442 
443     timer->stop();
444     playing = false;
445     canvas->setRenderingLimit(QRect());
446 
447     if (!canvas->frameCache()) {
448         KisImageBarrierLocker locker(canvas->image());
449 
450         Q_FOREACH (KisNodeSP node, disabledDecoratedNodes) {
451             // we have just upgraded from a weak shared pointer
452             KIS_SAFE_ASSERT_RECOVER(node) { continue; }
453 
454             KisDecoratedNodeInterface *decoratedNode = dynamic_cast<KisDecoratedNodeInterface*>(node.data());
455             KIS_SAFE_ASSERT_RECOVER(decoratedNode) { continue; }
456 
457             decoratedNode->setDecorationsVisible(true, doUpdates);
458         }
459     }
460 
461     if (doUpdates) {
462         KisImageAnimationInterface *animation = canvas->image()->animationInterface();
463         if (animation->currentUITime() == uiFrame) {
464             canvas->refetchDataFromImage();
465         } else {
466             animation->switchCurrentTimeAsync(uiFrame);
467         }
468     }
469 
470     emit q->sigPlaybackStopped();
471 }
472 
stop()473 void KisAnimationPlayer::stop()
474 {
475     m_d->stopImpl(true);
476 }
477 
forcedStopOnExit()478 void KisAnimationPlayer::forcedStopOnExit()
479 {
480     m_d->stopImpl(false);
481 }
482 
isPlaying()483 bool KisAnimationPlayer::isPlaying()
484 {
485     return m_d->playing;
486 }
487 
currentTime()488 int KisAnimationPlayer::currentTime()
489 {
490     return m_d->lastPaintedFrame;
491 }
492 
displayFrame(int time)493 void KisAnimationPlayer::displayFrame(int time)
494 {
495     uploadFrame(time, true);
496 }
497 
slotUpdate()498 void KisAnimationPlayer::slotUpdate()
499 {
500     uploadFrame(-1, false);
501 }
502 
uploadFrame(int frame,bool forceSyncAudio)503 void KisAnimationPlayer::uploadFrame(int frame, bool forceSyncAudio)
504 {
505     KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface();
506     const int fps = animationInterface->framerate();
507     const bool syncToAudio = !forceSyncAudio && m_d->dropFramesMode && m_d->syncedAudio && m_d->syncedAudio->isPlaying();
508 
509     if (frame < 0) {
510         if (m_d->dropFramesMode) {
511             const qreal currentTimeInFrames = syncToAudio ?
512                 m_d->msecToFrames(m_d->syncedAudio->position(), fps) :
513                 m_d->playbackTimeInFrames(fps);
514             frame = qFloor(currentTimeInFrames);
515 
516             const int timeToNextFrame = m_d->framesToWalltime(frame + 1 - currentTimeInFrames, fps);
517             m_d->lastTimerInterval = qMax(0, timeToNextFrame);
518 
519             if (frame < m_d->currentFrame) {
520                 // Returned to beginning of animation. Restart audio playback.
521                 forceSyncAudio = true;
522             }
523         } else {
524             const qint64 currentTime = m_d->playbackTime.elapsed();
525             const qint64 framesDiff = currentTime - m_d->nextFrameExpectedTime;
526 
527             frame = m_d->incFrame(m_d->currentFrame, 1);
528             m_d->nextFrameExpectedTime = currentTime + m_d->expectedInterval;
529             m_d->lastTimerInterval = qMax(0.0, m_d->lastTimerInterval - 0.5 * framesDiff);
530         }
531 
532         m_d->currentFrame = frame;
533         m_d->timer->start(m_d->lastTimerInterval);
534         m_d->playbackStatisticsCompressor.start();
535     }
536 
537     if (m_d->syncedAudio && (!syncToAudio || forceSyncAudio)) {
538         const int msecTime = m_d->framesToMSec(frame, fps);
539         if (isPlaying()) {
540             slotSyncScrubbingAudio(msecTime);
541         } else {
542             m_d->audioSyncScrubbingCompressor->start(msecTime);
543         }
544     }
545 
546 
547     bool useFallbackUploadMethod = !m_d->canvas->frameCache();
548 
549     if (m_d->canvas->frameCache() &&
550         m_d->canvas->frameCache()->shouldUploadNewFrame(frame, m_d->lastPaintedFrame)) {
551 
552         if (m_d->canvas->frameCache()->uploadFrame(frame)) {
553             m_d->canvas->updateCanvas();
554 
555             m_d->useFastFrameUpload = true;
556         } else {
557             useFallbackUploadMethod = true;
558         }
559     }
560 
561     if (useFallbackUploadMethod &&
562         m_d->canvas->image()->animationInterface()->hasAnimation()) {
563 
564         m_d->useFastFrameUpload = false;
565 
566         if (m_d->canvas->image()->tryBarrierLock(true)) {
567             m_d->canvas->image()->unlock();
568 
569             // no OpenGL cache or the frame just not cached yet
570             animationInterface->switchCurrentTimeAsync(frame);
571         }
572     }
573 
574     if (!m_d->realFpsTimer.isValid()) {
575         m_d->realFpsTimer.start();
576     } else {
577         const int elapsed = m_d->realFpsTimer.restart();
578         m_d->realFpsAccumulator(elapsed);
579 
580         if (m_d->lastPaintedFrame >= 0) {
581             int numFrames = frame - m_d->lastPaintedFrame;
582             if (numFrames < 0) {
583                 numFrames += m_d->lastFrame - m_d->firstFrame + 1;
584             }
585 
586             m_d->droppedFramesPortion(qreal(int(numFrames != 1)));
587 
588             if (numFrames > 0) {
589                 m_d->droppedFpsAccumulator(qreal(elapsed) / numFrames);
590             }
591 
592 #ifdef PLAYER_DEBUG_FRAMERATE
593             qDebug() << "    RFPS:" << 1000.0 / m_d->realFpsAccumulator.rollingMean()
594                      << "DFPS:" << 1000.0 / m_d->droppedFpsAccumulator.rollingMean() << ppVar(numFrames);
595 #endif /* PLAYER_DEBUG_FRAMERATE */
596         }
597     }
598 
599     m_d->lastPaintedFrame = frame;
600     emit sigFrameChanged();
601 }
602 
effectiveFps() const603 qreal KisAnimationPlayer::effectiveFps() const
604 {
605     return 1000.0 / m_d->droppedFpsAccumulator.rollingMean();
606 }
607 
realFps() const608 qreal KisAnimationPlayer::realFps() const
609 {
610     return 1000.0 / m_d->realFpsAccumulator.rollingMean();
611 }
612 
framesDroppedPortion() const613 qreal KisAnimationPlayer::framesDroppedPortion() const
614 {
615     return m_d->droppedFramesPortion.rollingMean();
616 }
617 
slotCancelPlayback()618 void KisAnimationPlayer::slotCancelPlayback()
619 {
620     stop();
621 }
622 
slotCancelPlaybackSafe()623 void KisAnimationPlayer::slotCancelPlaybackSafe()
624 {
625     /**
626      * If there is no openGL support, then we have no (!) cache at
627      * all. Therefore we should regenerate frame on every time switch,
628      * which, yeah, can be very slow.  What is more important, when
629      * regenerating a frame animation interface will emit a
630      * sigStrokeEndRequested() signal and we should ignore it. That is
631      * not an ideal solution, because the user will be able to paint
632      * on random frames while playing, but it lets users with faulty
633      * GPUs see at least some preview of their animation.
634      */
635 
636     if (m_d->useFastFrameUpload) {
637         stop();
638     }
639 }
640 
playbackSpeed()641 qreal KisAnimationPlayer::playbackSpeed()
642 {
643     return m_d->playbackSpeed;
644 }
645 
slotUpdatePlaybackSpeed(double value)646 void KisAnimationPlayer::slotUpdatePlaybackSpeed(double value)
647 {
648     m_d->playbackSpeed = value;
649     if (m_d->playing) {
650         slotUpdatePlaybackTimer();
651     }
652 }
653