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